fix: improve transfer recognition and local data safety

This commit is contained in:
2026-07-26 21:36:46 +08:00
parent 7df25edd96
commit 882293343e
21 changed files with 505 additions and 128 deletions
@@ -1,3 +1,6 @@
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:miaoji_zhang/shared/api/api_client.dart';
import 'package:miaoji_zhang/shared/services/current_ledger_store.dart';
import 'package:miaoji_zhang/shared/services/shanghai_time.dart';
@@ -17,12 +20,23 @@ class GuestMergeService {
static final _dio = ApiClient.instance.dio;
static Future<GuestMergeResult> merge(Map<String, dynamic> snapshot) async {
final ledgerResponse = await _dio.post(
'/api/ledgers',
data: {'name': '游客数据', 'iconKey': 'wallet'},
);
final ledgerId = (ledgerResponse.data['id'] as num).toInt();
static Future<GuestMergeResult> merge(
Map<String, dynamic> snapshot, {
String ledgerName = '游客数据',
}) async {
final ledgersResponse = await _dio.get('/api/ledgers');
final existingLedger = (ledgersResponse.data as List<dynamic>)
.cast<Map<String, dynamic>>()
.where((item) => item['name'] == ledgerName)
.firstOrNull;
final ledgerId = existingLedger == null
? ((await _dio.post(
'/api/ledgers',
data: {'name': ledgerName, 'iconKey': 'wallet'},
)).data['id']
as num)
.toInt()
: (existingLedger['id'] as num).toInt();
final categoryMap = <int, int>{};
final available = <Map<String, dynamic>>[];
for (final type in ['expense', 'income']) {
@@ -37,6 +51,7 @@ class GuestMergeService {
for (final value in snapshot['categories'] as List<dynamic>? ?? const []) {
final category = value as Map<String, dynamic>;
if (category['isDeleted'] == true) continue;
final existing = available.where(
(item) =>
item['type'] == category['type'] &&
@@ -45,6 +60,8 @@ class GuestMergeService {
Map<String, dynamic> remote;
if (existing.isNotEmpty) {
remote = existing.first;
} else if (category['isCustom'] == false) {
continue;
} else {
final response = await _dio.post(
'/api/categories',
@@ -66,14 +83,15 @@ class GuestMergeService {
final oldId = (transaction['categoryId'] as num).toInt();
final custom = categoryMap[oldId];
if (custom != null) return custom;
final categoryType = _transactionCategoryType(transaction);
final exact = available.where(
(item) =>
item['type'] == transaction['type'] &&
item['type'] == categoryType &&
item['name'] == transaction['categoryName'],
);
if (exact.isNotEmpty) return (exact.first['id'] as num).toInt();
final fallback = available.firstWhere(
(item) => item['type'] == transaction['type'] && item['name'] == '其他',
(item) => item['type'] == categoryType && item['name'] == '其他',
);
return (fallback['id'] as num).toInt();
}
@@ -82,6 +100,7 @@ class GuestMergeService {
for (final value
in snapshot['transactions'] as List<dynamic>? ?? const []) {
final transaction = value as Map<String, dynamic>;
if (transaction['isDeleted'] == true) continue;
await _dio.post(
'/api/transactions',
data: {
@@ -91,9 +110,12 @@ class GuestMergeService {
'amount': transaction['amount'],
'note': transaction['note'],
'paymentMethod': transaction['paymentMethod'],
'transferDirection': transaction['transferDirection'],
'counterparty': transaction['counterparty'],
'occurredAt': transaction['occurredAt'],
'source': 'manual',
'sourceText': null,
'clientRequestId': _importRequestId(transaction),
},
);
transactionCount++;
@@ -136,14 +158,42 @@ class GuestMergeService {
) {
final transaction = (snapshot['transactions'] as List<dynamic>? ?? const [])
.cast<Map<String, dynamic>>()
.where((item) => item['categoryId'] == oldCategoryId)
.where(
(item) =>
item['categoryId'] == oldCategoryId && item['isDeleted'] != true,
)
.firstOrNull;
if (transaction == null) return null;
final match = available.where(
(item) =>
item['type'] == transaction['type'] &&
item['type'] == _transactionCategoryType(transaction) &&
item['name'] == transaction['categoryName'],
);
return match.isEmpty ? null : (match.first['id'] as num).toInt();
}
static String _transactionCategoryType(Map<String, dynamic> transaction) {
if (transaction['type'] != 'transfer') {
return transaction['type'] as String;
}
return transaction['transferDirection'] == 'in' ? 'income' : 'expense';
}
static String _importRequestId(Map<String, dynamic> transaction) {
final existing = transaction['clientRequestId']?.toString().trim();
if (existing != null && existing.isNotEmpty && existing.length <= 64) {
return existing;
}
final identity = jsonEncode([
transaction['id'],
transaction['ledgerId'],
transaction['categoryId'],
transaction['type'],
transaction['transferDirection'],
transaction['amount'],
transaction['occurredAt'],
transaction['note'],
]);
return 'local-import-${sha256.convert(utf8.encode(identity)).toString().substring(0, 48)}';
}
}