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
@@ -83,19 +83,22 @@ public class ParseController(AppDbContext db, ILlmClient llm, AgentService agent
"expense"));
}
var bill = drafts[0];
var category = await db.Categories.FirstAsync(
c => c.Id == bill.CategoryId && c.Type == bill.Type,
ct);
var bill = drafts[0];
var category = await db.Categories.FirstAsync(
c => c.Id == bill.CategoryId &&
c.Type == bill.Type.CategoryType(bill.TransferDirection),
ct);
return Ok(new OcrParseResponse(
true,
category.Id,
category.Name,
category.IconKey,
bill.Amount,
bill.PaymentMethod,
bill.Note,
bill.Type == TransactionType.Income ? "income" : "expense"));
bill.PaymentMethod,
bill.Note,
bill.Type.ToWire(),
bill.TransferDirection.ToWire(),
bill.Counterparty));
}
/// <summary>上传截屏或小票,提取其中全部独立交易。</summary>
@@ -171,18 +174,24 @@ public class ParseController(AppDbContext db, ILlmClient llm, AgentService agent
foreach (var result in results.Take(20))
{
var normalizedType = result.Type.Trim().ToLowerInvariant();
if (normalizedType is not ("income" or "expense"))
var transferDirection = result.TransferDirection is "in" or "out"
? result.TransferDirection
: null;
if (normalizedType is not ("income" or "expense" or "transfer") ||
normalizedType == "transfer" && transferDirection is null)
{
items.Add(new ImageParseItemResponse(
false, 0, "待确认", "tag", "unknown",
result.Amount, result.PaymentMethod, result.Note,
result.OccurredAt));
result.OccurredAt,
transferDirection,
result.Counterparty));
continue;
}
var type = normalizedType == "income"
? TransactionType.Income
: TransactionType.Expense;
var category = FindCategory(categories, type, result.CategoryName);
var categoryType = normalizedType == "income" || transferDirection == "in"
? TransactionType.Income
: TransactionType.Expense;
var category = FindCategory(categories, categoryType, result.CategoryName);
items.Add(new ImageParseItemResponse(
true,
category.Id,
@@ -192,7 +201,9 @@ public class ParseController(AppDbContext db, ILlmClient llm, AgentService agent
result.Amount,
result.PaymentMethod,
result.Note,
result.OccurredAt));
result.OccurredAt,
transferDirection,
result.Counterparty));
}
var first = items[0];
@@ -206,7 +217,9 @@ public class ParseController(AppDbContext db, ILlmClient llm, AgentService agent
first.Note,
first.Type,
items,
first.OccurredAt));
first.OccurredAt,
first.TransferDirection,
first.Counterparty));
}
[HttpPost("recognition-batch")]
@@ -307,7 +320,12 @@ public class ParseController(AppDbContext db, ILlmClient llm, AgentService agent
candidate.RecognitionKind,
candidate.CategoryHint,
candidate.Confidence,
candidate.EvidenceIds ?? [])).ToList();
candidate.EvidenceIds ?? [],
candidate.TransferDirection,
candidate.Counterparty,
candidate.ProviderTransactionId,
candidate.RecognitionOccurrenceId,
candidate.IdentityConfidence)).ToList();
IReadOnlyList<RecognitionBatchModelAction>? modelActions;
try
@@ -366,11 +384,29 @@ public class ParseController(AppDbContext db, ILlmClient llm, AgentService agent
action = "keep";
reason = "撤销证据不足,已保留本地结果";
}
var type = model?.Type is "income" or "expense" ? model.Type : candidate.Type;
var type = model?.Type is "income" or "expense" or "transfer"
? model.Type
: candidate.Type;
var transferDirection = type == "transfer"
? model?.TransferDirection is "in" or "out"
? model.TransferDirection
: candidate.TransferDirection is "in" or "out"
? candidate.TransferDirection
: null
: null;
if (type == "transfer" && transferDirection is null)
{
action = "keep";
type = candidate.Type;
transferDirection = candidate.TransferDirection;
reason = "转账方向不明确,已保留本地结果";
}
var amount = model?.Amount is > 0 ? model.Amount.Value : candidate.Amount;
var category = FindCategory(
categories,
type == "income" ? TransactionType.Income : TransactionType.Expense,
type == "income" || transferDirection == "in"
? TransactionType.Income
: TransactionType.Expense,
model?.CategoryName ?? candidate.CategoryHint ?? "其他");
result.Add(new RecognitionBatchActionResponse(
action,
@@ -385,7 +421,11 @@ public class ParseController(AppDbContext db, ILlmClient llm, AgentService agent
string.IsNullOrWhiteSpace(model?.Note) ? candidate.Merchant : model.Note,
model?.OccurredAt ?? candidate.OccurredAt,
confidence,
reason));
reason,
transferDirection,
string.IsNullOrWhiteSpace(model?.Counterparty)
? candidate.Counterparty
: model.Counterparty));
}
var usedEvidence = new HashSet<string>();
@@ -395,13 +435,16 @@ public class ParseController(AppDbContext db, ILlmClient llm, AgentService agent
!evidenceById.TryGetValue(model.EvidenceId, out var evidence) ||
!string.IsNullOrWhiteSpace(evidence.CandidateId) ||
!usedEvidence.Add(model.EvidenceId) ||
model.Type is not ("income" or "expense") ||
model.Type is not ("income" or "expense" or "transfer") ||
model.Type == "transfer" && model.TransferDirection is not ("in" or "out") ||
model.Amount is not > 0)
{
continue;
}
var type = model.Type == "income" ? TransactionType.Income : TransactionType.Expense;
var category = FindCategory(categories, type, model.CategoryName ?? "其他");
var categoryType = model.Type == "income" || model.TransferDirection == "in"
? TransactionType.Income
: TransactionType.Expense;
var category = FindCategory(categories, categoryType, model.CategoryName ?? "其他");
result.Add(new RecognitionBatchActionResponse(
"create",
model.ActionId,
@@ -415,7 +458,9 @@ public class ParseController(AppDbContext db, ILlmClient llm, AgentService agent
model.Note,
model.OccurredAt ?? evidence.CapturedAt,
model.Confidence,
model.Reason));
model.Reason,
model.TransferDirection,
model.Counterparty));
}
return result.Take(20).ToList();
}