fix
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 21s
Docker Build & Deploy / Deploy to Production (push) Successful in 5s

This commit is contained in:
2025-12-27 11:50:12 +08:00
parent 9dbdc04a82
commit e68aef6ba1
8 changed files with 122 additions and 76 deletions

View File

@@ -19,9 +19,9 @@ public interface ITransactionRecordRepository : IBaseRepository<TransactionRecor
/// <param name="month">筛选月份</param>
/// <returns>交易记录列表、最后发生时间和最后ID</returns>
Task<(List<TransactionRecord> list, DateTime? lastOccurredAt, long lastId)> GetPagedListAsync(
DateTime? lastOccurredAt,
long? lastId,
int pageSize = 20,
DateTime? lastOccurredAt,
long? lastId,
int pageSize = 20,
string? searchKeyword = null,
string? classify = null,
TransactionType? type = null,
@@ -130,7 +130,7 @@ public interface ITransactionRecordRepository : IBaseRepository<TransactionRecor
/// <param name="keyword">关键词</param>
/// <returns>匹配的交易记录列表</returns>
Task<List<TransactionRecord>> QueryByWhereAsync(string sql);
/// <summary>
/// 执行完整的SQL查询
/// </summary>
@@ -163,9 +163,9 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
}
public async Task<(List<TransactionRecord> list, DateTime? lastOccurredAt, long lastId)> GetPagedListAsync(
DateTime? lastOccurredAt,
long? lastId,
int pageSize = 20,
DateTime? lastOccurredAt,
long? lastId,
int pageSize = 20,
string? searchKeyword = null,
string? classify = null,
TransactionType? type = null,
@@ -175,25 +175,24 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
var query = FreeSql.Select<TransactionRecord>();
// 如果提供了搜索关键词,则添加搜索条件
if (!string.IsNullOrWhiteSpace(searchKeyword))
{
query = query.Where(t => t.Reason.Contains(searchKeyword) ||
t.Classify.Contains(searchKeyword) ||
t.Card.Contains(searchKeyword) ||
t.ImportFrom.Contains(searchKeyword));
}
query = query.WhereIf(!string.IsNullOrWhiteSpace(searchKeyword),
t => t.Reason.Contains(searchKeyword!) ||
t.Classify.Contains(searchKeyword!) ||
t.Card.Contains(searchKeyword!) ||
t.ImportFrom.Contains(searchKeyword!));
// 按分类筛选
if (!string.IsNullOrWhiteSpace(classify))
{
if (classify == "未分类")
{
classify = string.Empty;
}
query = query.Where(t => t.Classify == classify);
}
// 按交易类型筛选
if (type.HasValue)
{
query = query.Where(t => t.Type == type.Value);
}
query = query.WhereIf(type.HasValue, t => t.Type == type!.Value);
// 按年月筛选
if (year.HasValue && month.HasValue)
@@ -370,7 +369,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
{
var dt = await FreeSql.Ado.ExecuteDataTableAsync(completeSql);
var result = new List<dynamic>();
foreach (System.Data.DataRow row in dt.Rows)
{
var expando = new System.Dynamic.ExpandoObject() as IDictionary<string, object>;
@@ -380,7 +379,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
}
result.Add(expando);
}
return result;
}
public async Task<MonthlyStatistics> GetMonthlyStatisticsAsync(int year, int month)
@@ -401,7 +400,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
foreach (var record in records)
{
var amount = Math.Abs(record.Amount);
if (record.Type == TransactionType.Expense)
{
statistics.TotalExpense += amount;

View File

@@ -341,7 +341,7 @@ public class ImportService(
}
// 使用正则表达式提取退款金额
var regex = new System.Text.RegularExpressions.Regex(@"¥(-?\d+(\.\d+)?)");
var regex = new Regex(@"¥(-?\d+(\.\d+)?)");
var match = regex.Match(status);
if (match.Success && decimal.TryParse(match.Groups[1].Value, out var refundAmount))
{

View File

@@ -3,19 +3,19 @@
<div class="app-root">
<RouterView />
<van-tabbar v-model="active" v-show="showTabbar">
<van-tabbar-item icon="notes" to="/calendar">
<van-tabbar-item name="ccalendar" icon="notes" to="/calendar">
日历
</van-tabbar-item>
<van-tabbar-item icon="chart-trending-o" to="/statistics">
<van-tabbar-item name="statistics" icon="chart-trending-o" to="/statistics">
统计
</van-tabbar-item>
<van-tabbar-item icon="balance-list" to="/" @click="handleTabClick('/')">
<van-tabbar-item name="balance" icon="balance-list" to="/" @click="handleTabClick('/')">
账单
</van-tabbar-item>
<van-tabbar-item icon="records" to="/email" @click="handleTabClick('/email')">
<van-tabbar-item name="email" icon="records" to="/email" @click="handleTabClick('/email')">
邮件
</van-tabbar-item>
<van-tabbar-item icon="setting" to="/setting">
<van-tabbar-item name="setting" icon="setting" to="/setting">
设置
</van-tabbar-item>
</van-tabbar>
@@ -25,7 +25,7 @@
<script setup>
import { RouterView, useRoute } from 'vue-router'
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
import '@/styles/common.css'
const updateVh = () => {
@@ -61,7 +61,7 @@ const showTabbar = computed(() => {
route.path === '/statistics'
})
const active = ref(0)
const active = ref('')
const theme = ref('light')
// 检测系统深色模式
@@ -76,8 +76,32 @@ onMounted(() => {
updateTheme()
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
mediaQuery.addEventListener('change', updateTheme)
setActive(route.path)
})
// 监听路由变化调整
watch(() => route.path, (newPath) => {
setActive(newPath)
})
const setActive = (path) => {
active.value = (() => {
switch (path) {
case '/calendar':
return 'ccalendar'
case '/statistics':
return 'statistics'
case '/email':
return 'email'
case '/setting':
return 'setting'
default:
return 'balance'
}
})()
console.log(active.value, path)
}
onUnmounted(() => {
if (mediaQuery) {
mediaQuery.removeEventListener('change', updateTheme)

View File

@@ -152,3 +152,12 @@
word-break: break-all;
white-space: normal;
}
/* 账单列表滚动容器 */
.bills-scroll-container {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
}

View File

@@ -25,6 +25,7 @@
<p v-if="dateTransactions.length"> {{ dateTransactions.length }} 笔交易</p>
</div>
<div class="bills-scroll-container">
<TransactionList
:transactions="dateTransactions"
:loading="listLoading"
@@ -33,6 +34,7 @@
@click="viewDetail"
/>
</div>
</div>
</van-popup>
<!-- 交易详情组件 -->

View File

@@ -141,13 +141,15 @@
<div class="list-header">
<h3 style="margin: 16px;">关联账单列表</h3>
</div>
<TransactionList
:transactions="transactionList"
:loading="false"
:finished="true"
:show-delete="false"
@click="handleTransactionClick"
/>
<div class="bills-scroll-container">
<TransactionList
:transactions="transactionList"
:loading="false"
:finished="true"
:show-delete="false"
@click="handleTransactionClick"
/>
</div>
</div>
</van-popup>
@@ -434,7 +436,13 @@ onMounted(() => {
.transaction-list-popup {
height: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.list-header{
padding: 16px;
border-bottom: 1px solid #ebedf0;
}
.email-content {

View File

@@ -562,7 +562,7 @@ const goToAnalysis = () => {
// 打开分类账单列表
const goToCategoryBills = (classify, type) => {
selectedClassify.value = classify || ''
selectedClassify.value = classify || '未分类'
selectedType.value = type
selectedCategoryTitle.value = `${classify || '未分类'} - ${type === 0 ? '支出' : '收入'}`
@@ -1076,14 +1076,6 @@ onActivated(() => {
color: var(--van-text-color-2);
}
/* 账单列表滚动容器 */
.bills-scroll-container {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
}
/* 修复深色模式下van组件的背景色 */
.category-bills :deep(.van-list) {
background: transparent;

View File

@@ -15,7 +15,7 @@ public class TransactionRecordController(
/// 获取交易记录列表(分页)
/// </summary>
[HttpGet]
public async Task<PagedResponse<Entity.TransactionRecord>> GetListAsync(
public async Task<PagedResponse<TransactionRecord>> GetListAsync(
[FromQuery] DateTime? lastOccurredAt = null,
[FromQuery] long? lastId = null,
[FromQuery] string? searchKeyword = null,
@@ -39,7 +39,7 @@ public class TransactionRecordController(
month);
var total = await transactionRepository.GetTotalCountAsync();
return new PagedResponse<Entity.TransactionRecord>
return new PagedResponse<TransactionRecord>
{
Success = true,
Data = list.ToArray(),
@@ -51,7 +51,7 @@ public class TransactionRecordController(
catch (Exception ex)
{
logger.LogError(ex, "获取交易记录列表失败,时间: {LastTime}, ID: {LastId}", lastOccurredAt, lastId);
return PagedResponse<Entity.TransactionRecord>.Fail($"获取交易记录列表失败: {ex.Message}");
return PagedResponse<TransactionRecord>.Fail($"获取交易记录列表失败: {ex.Message}");
}
}
@@ -59,17 +59,17 @@ public class TransactionRecordController(
/// 根据ID获取交易记录详情
/// </summary>
[HttpGet("{id}")]
public async Task<BaseResponse<Entity.TransactionRecord>> GetByIdAsync(long id)
public async Task<BaseResponse<TransactionRecord>> GetByIdAsync(long id)
{
try
{
var transaction = await transactionRepository.GetByIdAsync(id);
if (transaction == null)
{
return BaseResponse<Entity.TransactionRecord>.Fail("交易记录不存在");
return BaseResponse<TransactionRecord>.Fail("交易记录不存在");
}
return new BaseResponse<Entity.TransactionRecord>
return new BaseResponse<TransactionRecord>
{
Success = true,
Data = transaction
@@ -78,7 +78,7 @@ public class TransactionRecordController(
catch (Exception ex)
{
logger.LogError(ex, "获取交易记录详情失败交易ID: {TransactionId}", id);
return BaseResponse<Entity.TransactionRecord>.Fail($"获取交易记录详情失败: {ex.Message}");
return BaseResponse<TransactionRecord>.Fail($"获取交易记录详情失败: {ex.Message}");
}
}
@@ -86,12 +86,12 @@ public class TransactionRecordController(
/// 根据邮件ID获取交易记录列表
/// </summary>
[HttpGet("{emailId}")]
public async Task<BaseResponse<List<Entity.TransactionRecord>>> GetByEmailIdAsync(long emailId)
public async Task<BaseResponse<List<TransactionRecord>>> GetByEmailIdAsync(long emailId)
{
try
{
var transactions = await transactionRepository.GetByEmailIdAsync(emailId);
return new BaseResponse<List<Entity.TransactionRecord>>
return new BaseResponse<List<TransactionRecord>>
{
Success = true,
Data = transactions
@@ -100,7 +100,7 @@ public class TransactionRecordController(
catch (Exception ex)
{
logger.LogError(ex, "获取邮件交易记录失败邮件ID: {EmailId}", emailId);
return BaseResponse<List<Entity.TransactionRecord>>.Fail($"获取邮件交易记录失败: {ex.Message}");
return BaseResponse<List<TransactionRecord>>.Fail($"获取邮件交易记录失败: {ex.Message}");
}
}
@@ -118,7 +118,7 @@ public class TransactionRecordController(
return BaseResponse.Fail("交易时间格式不正确");
}
var transaction = new Entity.TransactionRecord
var transaction = new TransactionRecord
{
OccurredAt = occurredAt,
Reason = dto.Reason ?? string.Empty,
@@ -416,7 +416,7 @@ public class TransactionRecordController(
}
// 第三步将查询结果序列化为JSON直接传递给AI生成分析报告
var dataJson = System.Text.Json.JsonSerializer.Serialize(queryResults, new System.Text.Json.JsonSerializerOptions
var dataJson = System.Text.Json.JsonSerializer.Serialize(queryResults, new JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
@@ -482,13 +482,13 @@ public class TransactionRecordController(
/// 获取指定日期的交易记录
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<Entity.TransactionRecord>>> GetByDateAsync([FromQuery] string date)
public async Task<BaseResponse<List<TransactionRecord>>> GetByDateAsync([FromQuery] string date)
{
try
{
if (!DateTime.TryParse(date, out var targetDate))
{
return BaseResponse<List<Entity.TransactionRecord>>.Fail("日期格式不正确");
return BaseResponse<List<TransactionRecord>>.Fail("日期格式不正确");
}
// 获取当天的开始和结束时间
@@ -497,7 +497,7 @@ public class TransactionRecordController(
var records = await transactionRepository.GetByDateRangeAsync(startDate, endDate);
return new BaseResponse<List<Entity.TransactionRecord>>
return new BaseResponse<List<TransactionRecord>>
{
Success = true,
Data = records
@@ -506,7 +506,7 @@ public class TransactionRecordController(
catch (Exception ex)
{
logger.LogError(ex, "获取指定日期的交易记录失败,日期: {Date}", date);
return BaseResponse<List<Entity.TransactionRecord>>.Fail($"获取指定日期的交易记录失败: {ex.Message}");
return BaseResponse<List<TransactionRecord>>.Fail($"获取指定日期的交易记录失败: {ex.Message}");
}
}
@@ -536,12 +536,12 @@ public class TransactionRecordController(
/// 获取未分类的账单列表
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<Entity.TransactionRecord>>> GetUnclassifiedAsync([FromQuery] int pageSize = 10)
public async Task<BaseResponse<List<TransactionRecord>>> GetUnclassifiedAsync([FromQuery] int pageSize = 10)
{
try
{
var records = await transactionRepository.GetUnclassifiedAsync(pageSize);
return new BaseResponse<List<Entity.TransactionRecord>>
return new BaseResponse<List<TransactionRecord>>
{
Success = true,
Data = records
@@ -550,7 +550,7 @@ public class TransactionRecordController(
catch (Exception ex)
{
logger.LogError(ex, "获取未分类账单列表失败");
return BaseResponse<List<Entity.TransactionRecord>>.Fail($"获取未分类账单列表失败: {ex.Message}");
return BaseResponse<List<TransactionRecord>>.Fail($"获取未分类账单列表失败: {ex.Message}");
}
}
@@ -574,7 +574,7 @@ public class TransactionRecordController(
}
// 获取指定ID的账单
var records = new List<Entity.TransactionRecord>();
var records = new List<TransactionRecord>();
foreach (var id in request.TransactionIds)
{
var record = await transactionRepository.GetByIdAsync(id);
@@ -702,14 +702,14 @@ public class TransactionRecordController(
/// 获取按交易摘要分组的统计信息(支持分页)
/// </summary>
[HttpGet]
public async Task<PagedResponse<Repository.ReasonGroupDto>> GetReasonGroupsAsync(
public async Task<PagedResponse<ReasonGroupDto>> GetReasonGroupsAsync(
[FromQuery] int pageIndex = 1,
[FromQuery] int pageSize = 20)
{
try
{
var (list, total) = await transactionRepository.GetReasonGroupsAsync(pageIndex, pageSize);
return new PagedResponse<Repository.ReasonGroupDto>
return new PagedResponse<ReasonGroupDto>
{
Success = true,
Data = list.ToArray(),
@@ -719,7 +719,7 @@ public class TransactionRecordController(
catch (Exception ex)
{
logger.LogError(ex, "获取交易摘要分组失败");
return PagedResponse<Repository.ReasonGroupDto>.Fail($"获取交易摘要分组失败: {ex.Message}");
return PagedResponse<ReasonGroupDto>.Fail($"获取交易摘要分组失败: {ex.Message}");
}
}
@@ -780,21 +780,33 @@ public class TransactionRecordController(
{{categoryInfo}}
你需要分析用户的需求,提取以下信息:
1. 查询SQL, 根据用户的描述生成SQL Where 子句,用于查询交易记录。例如:Reason LIKE '%关键词%'
Table Schema:
1. 查询SQL, 根据用户的描述生成完整SQL句,用于查询交易记录。例如:SELECT * FROM TransactionRecord WHERE Reason LIKE '%关键词%' OR Classify LIKE '%关键词2%' LIMIT 500
[重要Table Schema:]
```
TransactionRecord (
Id LONG,
Reason STRING,
Reason STRING NOT NULL,
Amount DECIMAL,
RefundAmount DECIMAL,
Balance DECIMAL,
OccurredAt DATETIME,
EmailMessageId LONG,
Type INT,
Classify STRING,
ImportNo STRING,
ImportFrom STRING
Classify STRING NOT NULL,
ImportNo STRING NOT NULL,
ImportFrom STRING NOT NULL
)
```
[重要]
如果用户没有限制则最多查询500条记录如果用户指定了时间范围请在SQL中加入时间过滤条件。
[重要SQL限制]
必须是SELECT * FROM TransactionRecord 开头的SQL语句。
当前日期:{{DateTime.Now:yyyy年M月d日}}
[重要SQLite日期函数]
- 提取年份strftime('%Y', OccurredAt)
- 提取月份strftime('%m', OccurredAt)
- 提取日期strftime('%Y-%m-%d', OccurredAt)
- 不要使用 YEAR()、MONTH()、DAY() 函数SQLite不支持
2. 目标交易类型0:支出, 1:收入, 2:不计入收支)
3. 目标分类名称(必须从上面的分类列表中选择)
@@ -835,7 +847,7 @@ public class TransactionRecordController(
}
// 根据关键词查询交易记录
var allRecords = await transactionRepository.QueryByWhereAsync(analysisInfo.Sql);
var allRecords = await transactionRepository.ExecuteRawSqlAsync(analysisInfo.Sql);
logger.LogInformation("NLP分析查询到 {Count} 条记录SQL: {Sql}", allRecords.Count, analysisInfo.Sql);
// 为每条记录预设分类