Skip to content

Commit 3fe8498

Browse files
committed
fix(ai-billing): handle transfer type and persist ai tags
- add transfer/from_account/to_account/tags parsing to BillInfo - avoid defaulting unknown AI type to income in AI chat flow - support transfer account mapping and toAccountId persistence - merge AI-recognized tags with billing-source tags when saving - update extraction prompt contract for transfer + tags fields - verified with flutter analyze on touched core files
1 parent 4d54195 commit 3fe8498

6 files changed

Lines changed: 257 additions & 72 deletions

File tree

lib/ai/tasks/bill_extraction_task.dart

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ class BillInfo {
3939
/// 账户名称
4040
final String? account;
4141

42+
/// 转账来源账户名称(可选)
43+
final String? fromAccount;
44+
45+
/// 转账目标账户名称(可选)
46+
final String? toAccount;
47+
48+
/// 标签列表(可选)
49+
final List<String>? tags;
50+
4251
/// 账本ID
4352
final int? ledgerId;
4453

@@ -52,6 +61,9 @@ class BillInfo {
5261
this.category,
5362
this.type,
5463
this.account,
64+
this.fromAccount,
65+
this.toAccount,
66+
this.tags,
5567
this.ledgerId,
5668
this.confidence = 0.0,
5769
});
@@ -68,6 +80,9 @@ class BillInfo {
6880
category: json['category'],
6981
type: json['type'] != null ? _parseBillType(json['type']) : null,
7082
account: json['account'],
83+
fromAccount: json['from_account'] ?? json['fromAccount'],
84+
toAccount: json['to_account'] ?? json['toAccount'],
85+
tags: _parseTags(json['tags'] ?? json['tag']),
7186
ledgerId: json['ledgerId'],
7287
confidence: json['confidence']?.toDouble() ?? 0.8,
7388
);
@@ -81,6 +96,9 @@ class BillInfo {
8196
'category': category,
8297
'type': type?.toString().split('.').last,
8398
'account': account,
99+
'from_account': fromAccount,
100+
'to_account': toAccount,
101+
'tags': tags,
84102
'ledgerId': ledgerId,
85103
'confidence': confidence,
86104
};
@@ -90,12 +108,34 @@ class BillInfo {
90108
final str = value.toString().toLowerCase();
91109
if (str.contains('income') || str == '收入') return BillType.income;
92110
if (str.contains('expense') || str == '支出') return BillType.expense;
111+
if (str.contains('transfer') || str == '转账' || str == '轉帳') {
112+
return BillType.transfer;
113+
}
93114
return null;
94115
}
95116

117+
static List<String>? _parseTags(dynamic value) {
118+
if (value == null) return null;
119+
120+
final tags = <String>[];
121+
122+
if (value is String) {
123+
tags.addAll(value
124+
.split(RegExp(r'[,\n,、;;|]+'))
125+
.map((s) => s.trim())
126+
.where((s) => s.isNotEmpty));
127+
} else if (value is List) {
128+
tags.addAll(value
129+
.map((item) => item.toString().trim())
130+
.where((s) => s.isNotEmpty));
131+
}
132+
133+
return tags.isEmpty ? null : tags;
134+
}
135+
96136
@override
97137
String toString() {
98-
return 'BillInfo(amount: $amount, time: $time, note: $note, category: $category, type: $type, account: $account)';
138+
return 'BillInfo(amount: $amount, time: $time, note: $note, category: $category, type: $type, account: $account, fromAccount: $fromAccount, toAccount: $toAccount, tags: $tags)';
99139
}
100140
}
101141

@@ -106,4 +146,7 @@ enum BillType {
106146

107147
/// 支出
108148
expense,
149+
150+
/// 转账
151+
transfer,
109152
}

lib/pages/ai/ai_chat_page.dart

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,8 @@ class _AIChatPageState extends ConsumerState<AIChatPage>
409409
color: isUser
410410
? ref.watch(primaryColorProvider).withOpacity(0.1)
411411
: BeeTokens.surface(context),
412-
borderRadius: BorderRadius.circular(12.0.scaled(context, ref)),
412+
borderRadius:
413+
BorderRadius.circular(12.0.scaled(context, ref)),
413414
border: Border.all(
414415
color: isUser
415416
? ref.watch(primaryColorProvider).withOpacity(0.3)
@@ -911,8 +912,11 @@ class _AIChatPageState extends ConsumerState<AIChatPage>
911912
time: transaction.happenedAt,
912913
note: transaction.note,
913914
category: categoryName,
914-
type:
915-
transaction.type == 'expense' ? BillType.expense : BillType.income,
915+
type: transaction.type == 'expense'
916+
? BillType.expense
917+
: (transaction.type == 'transfer'
918+
? BillType.transfer
919+
: BillType.income),
916920
account: accountName,
917921
ledgerId: transaction.ledgerId,
918922
confidence: 1.0,
@@ -995,10 +999,8 @@ class _AIChatPageState extends ConsumerState<AIChatPage>
995999
ref.read(statsRefreshProvider.notifier).state++;
9961000

9971001
// 触发云同步(旧账本和新账本都需要同步)
998-
await PostProcessor.sync(ref,
999-
ledgerId: transaction.ledgerId);
1000-
await PostProcessor.sync(ref,
1001-
ledgerId: selectedLedgerId);
1002+
await PostProcessor.sync(ref, ledgerId: transaction.ledgerId);
1003+
await PostProcessor.sync(ref, ledgerId: selectedLedgerId);
10021004

10031005
logger.info('AIChat',
10041006
'修改账本成功: ${transaction.ledgerId} -> $selectedLedgerId,已刷新统计信息和触发云同步');

lib/services/ai/ai_chat_service.dart

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,18 @@ class AIChatService {
127127
category: billInfo.category,
128128
type: billInfo.type,
129129
account: billInfo.account,
130+
fromAccount: billInfo.fromAccount,
131+
toAccount: billInfo.toAccount,
132+
tags: billInfo.tags,
130133
ledgerId: ledgerId,
131134
confidence: billInfo.confidence,
132135
);
133136

134137
logger.info('AIChat', '附加账本ID到BillInfo: ledgerId=$ledgerId');
135138

136139
// 保存到数据库并获取实际的分类和账户名称
137-
final (transactionId, actualCategory, actualAccount) = await _saveBill(billInfo, l10n: l10n);
140+
final (transactionId, actualCategory, actualAccount) =
141+
await _saveBill(billInfo, l10n: l10n);
138142

139143
// 使用实际的分类和账户名称更新 billInfo
140144
final updatedBillInfo = BillInfo(
@@ -144,6 +148,9 @@ class AIChatService {
144148
category: actualCategory ?? billInfo.category,
145149
type: billInfo.type,
146150
account: actualAccount ?? billInfo.account,
151+
fromAccount: billInfo.fromAccount,
152+
toAccount: billInfo.toAccount,
153+
tags: billInfo.tags,
147154
ledgerId: ledgerId,
148155
confidence: billInfo.confidence,
149156
);
@@ -165,14 +172,16 @@ class AIChatService {
165172
}
166173

167174
/// 处理自由对话 - 使用 AIProviderFactory.chat()
168-
Future<AIResponse> _handleFreeChat(String input, {String? languageCode}) async {
175+
Future<AIResponse> _handleFreeChat(String input,
176+
{String? languageCode}) async {
169177
logger.info('AIChat', '开始自由对话 (语言: ${languageCode ?? "默认"})');
170178

171179
try {
172180
// 根据语言构建系统提示
173181
String systemPrompt;
174182
if (languageCode == 'en') {
175-
systemPrompt = 'You are BeeCount\'s AI assistant, mainly helping users with bookkeeping. '
183+
systemPrompt =
184+
'You are BeeCount\'s AI assistant, mainly helping users with bookkeeping. '
176185
'If users ask about statistics, queries and other functions, please inform them that they are not supported yet and guide them to use the bookkeeping function. '
177186
'Please respond in English.';
178187
} else {
@@ -206,8 +215,10 @@ class AIChatService {
206215

207216
/// 保存账单 - 复用 BillCreationService 的逻辑
208217
/// 返回 (transactionId, actualCategoryName, actualAccountName)
209-
Future<(int, String?, String?)> _saveBill(BillInfo bill, {AppLocalizations? l10n}) async {
210-
logger.info('AIChat', '开始保存账单: amount=${bill.amount}, category=${bill.category}, ledgerId=${bill.ledgerId}');
218+
Future<(int, String?, String?)> _saveBill(BillInfo bill,
219+
{AppLocalizations? l10n}) async {
220+
logger.info('AIChat',
221+
'开始保存账单: amount=${bill.amount}, category=${bill.category}, ledgerId=${bill.ledgerId}');
211222

212223
// 使用 BillInfo 中的 ledgerId,如果为空则使用第一个账本
213224
int ledgerId;
@@ -220,8 +231,13 @@ class AIChatService {
220231
logger.warning('AIChat', '未指定账本ID,使用第一个账本: $ledgerId');
221232
}
222233

223-
// 确定交易类型
224-
final transactionType = bill.type == BillType.expense ? 'expense' : 'income';
234+
// 确定交易类型(修复:type 为空时不再默认收入)
235+
final transactionType = _resolveTransactionType(bill);
236+
237+
// 转账场景优先使用 from_account 作为转出账户
238+
final aiAccountName = transactionType == 'transfer'
239+
? (bill.fromAccount ?? bill.account)
240+
: bill.account;
225241

226242
// 将 BillInfo 转换为 OcrResult(复用 BillCreationService 的逻辑)
227243
final ocrResult = OcrResult(
@@ -231,7 +247,7 @@ class AIChatService {
231247
rawText: bill.note ?? '',
232248
allNumbers: bill.amount != null ? [bill.amount!.abs().toString()] : [],
233249
aiCategoryName: bill.category,
234-
aiAccountName: bill.account,
250+
aiAccountName: aiAccountName,
235251
aiType: transactionType,
236252
);
237253

@@ -246,6 +262,9 @@ class AIChatService {
246262
ledgerId: ledgerId,
247263
note: bill.note,
248264
billingTypes: [TagSeedService.billingTypeAi],
265+
fromAccountName: bill.fromAccount,
266+
toAccountName: bill.toAccount,
267+
customTagNames: bill.tags,
249268
l10n: l10n,
250269
autoAddTags: autoAddTags,
251270
);
@@ -271,7 +290,8 @@ class AIChatService {
271290
}
272291
}
273292

274-
logger.info('AIChat', '记账成功: id=$id, category=$actualCategoryName, account=$actualAccountName');
293+
logger.info('AIChat',
294+
'记账成功: id=$id, category=$actualCategoryName, account=$actualAccountName');
275295
return (id, actualCategoryName, actualAccountName);
276296
} else {
277297
throw Exception('创建交易失败');
@@ -290,6 +310,20 @@ class AIChatService {
290310
}
291311
}
292312

313+
String _resolveTransactionType(BillInfo bill) {
314+
if (bill.type == BillType.transfer) return 'transfer';
315+
if (bill.type == BillType.expense) return 'expense';
316+
if (bill.type == BillType.income) return 'income';
317+
318+
final category = bill.category?.trim();
319+
if (category == '转账' ||
320+
category == '轉帳' ||
321+
category?.toLowerCase() == 'transfer') {
322+
return 'transfer';
323+
}
324+
325+
return 'expense';
326+
}
293327
}
294328

295329
/// AI 响应模型

lib/services/ai/bill_extraction_service.dart

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,14 +196,18 @@ class BillExtractionService {
196196
- 商品名称(长标题需简化,如"2025春季新款黑色斜纹格纹半身裙"→"黑色半身裙")
197197
- 用户描述(如"给女儿买")
198198
- 没有则留空
199-
4. category: 从分类列表选择
200-
5. type: income或expense
201-
6. account: 支付账户(可选)
199+
4. category: 从分类列表选择(转账可填"转账")
200+
5. type: income、expense 或 transfer
201+
6. account: 支付账户(收入/支出可用)
202+
7. from_account: 转出账户(仅转账可用)
203+
8. to_account: 转入账户(仅转账可用)
204+
9. tag/tags: 标签(可选,单个字符串或字符串数组)
202205
203206
示例:
204207
输入"昨天中午吃饭50" → {"amount":-50,"time":"2025-11-24T12:00:00","category":"餐饮","type":"expense"}
205208
输入"早上在星巴克买咖啡30" → {"amount":-30,"time":"{{CURRENT_DATE}}T09:00:00","note":"星巴克","category":"咖啡","type":"expense"}
206209
输入"商品:2025春季新款黑色半身裙 金额:¥299" → {"amount":-299,"note":"黑色半身裙","category":"服装","type":"expense"}
210+
输入"从建行转800到零钱包" → {"amount":800,"category":"转账","type":"transfer","from_account":"建行","to_account":"零钱包","tag":"自己"}
207211
208212
注意:只返回JSON,尽量推断时间不要返回null,note必须≤15字(长标题要精简)''';
209213

0 commit comments

Comments
 (0)