Add transfer tracking and secure admin access

This commit is contained in:
2026-07-26 11:57:57 +08:00
parent 0738953e6d
commit 7df25edd96
111 changed files with 6379 additions and 1934 deletions
@@ -194,6 +194,13 @@ class RecognitionImportService {
sourceText: candidate.sourceText,
occurredAt: ShanghaiTime.toCivil(occurredUtc),
clientRequestId: candidate.clientRequestId,
transferDirection: candidate.transferDirection,
counterparty: candidate.counterparty,
provider: candidate.provider,
providerTransactionId: candidate.providerTransactionId,
recognitionOccurrenceId: candidate.recognitionOccurrenceId,
evidenceFingerprint: candidate.evidenceFingerprint,
recognitionConfidence: candidate.identityConfidence,
);
await ScreenshotChannel.acknowledgeRecognitionCandidate(
candidate.id,
@@ -227,6 +234,13 @@ class RecognitionImportService {
source: candidate.source,
sourceText: candidate.sourceText,
occurredAt: ShanghaiTime.toCivil(occurredUtc),
transferDirection: candidate.transferDirection,
counterparty: candidate.counterparty,
provider: candidate.provider,
providerTransactionId: candidate.providerTransactionId,
recognitionOccurrenceId: candidate.recognitionOccurrenceId,
evidenceFingerprint: candidate.evidenceFingerprint,
recognitionConfidence: candidate.identityConfidence,
),
);
}
@@ -254,11 +268,23 @@ class RecognitionImportService {
RecognitionCandidate candidate,
Map<String, List<CategoryItem>> cache,
) async {
if (candidate.type != 'income' && candidate.type != 'expense') {
if (candidate.type != 'income' &&
candidate.type != 'expense' &&
candidate.type != 'transfer') {
throw StateError('识别结果缺少明确的收支类型');
}
final categories = cache[candidate.type] ??= await TxApi.categories(
candidate.type,
if (candidate.type == 'transfer' &&
candidate.transferDirection != 'in' &&
candidate.transferDirection != 'out') {
throw StateError('转账识别结果缺少方向');
}
final categoryType = candidate.type == 'transfer'
? candidate.transferDirection == 'in'
? 'income'
: 'expense'
: candidate.type;
final categories = cache[categoryType] ??= await TxApi.categories(
categoryType,
);
if (categories.isEmpty) throw StateError('当前账本没有可用分类');
return categories
@@ -315,16 +341,16 @@ class RecognitionImportService {
width: 42,
height: 42,
decoration: BoxDecoration(
color: candidate.type == 'income'
color: candidate.isIncome
? palette.primaryBackground
: palette.expenseBackground,
borderRadius: BorderRadius.circular(13),
),
child: Icon(
candidate.type == 'income'
candidate.isIncome
? Icons.south_west_rounded
: Icons.north_east_rounded,
color: candidate.type == 'income'
color: candidate.isIncome
? AppTheme.primary
: AppTheme.red,
),
@@ -343,7 +369,7 @@ class RecognitionImportService {
),
const SizedBox(height: 4),
Text(
'${candidate.appName} · ${candidate.type == 'income' ? '收入' : '支出'}',
'${candidate.appName} · ${candidate.typeLabel}',
style: TextStyle(
color: palette.text2,
fontSize: 12,
@@ -353,9 +379,9 @@ class RecognitionImportService {
),
),
Text(
'${candidate.type == 'income' ? '+' : '-'}¥${candidate.amount.toStringAsFixed(2)}',
'${candidate.isIncome ? '+' : '-'}¥${candidate.amount.toStringAsFixed(2)}',
style: TextStyle(
color: candidate.type == 'income'
color: candidate.isIncome
? AppTheme.primary
: AppTheme.red,
fontSize: 18,