修复报销票据门禁提示与 OCR 降级(08-30-b2-submit-error-ocr-fallback)

这个提交包含在:
2026-08-31 20:20:20 +08:00
父节点 8b19538584
当前提交 f57e2236f2
共修改 21 个文件,包含 1829 行新增和 49 行删除
+61
查看文件
@@ -10,6 +10,35 @@ function readStringList(value: unknown): string[] {
.filter((item) => item.length > 0);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return value != null && typeof value === "object" && !Array.isArray(value);
}
function countGuardAmountUnavailableItems(value: unknown): number {
return Array.isArray(value)
? value.filter((item) => (
isRecord(item)
&& typeof item.itemId === "string"
&& item.itemId.length > 0
&& readStringList(item.invoiceIds).length > 0
)).length
: 0;
}
function countGuardAmountExceededItems(value: unknown): number {
return Array.isArray(value)
? value.filter((item) => (
isRecord(item)
&& typeof item.itemId === "string"
&& item.itemId.length > 0
&& typeof item.declaredAmount === "string"
&& item.declaredAmount.length > 0
&& typeof item.invoiceTotal === "string"
&& item.invoiceTotal.length > 0
)).length
: 0;
}
const FEATURE_FLAG_DISABLED_MESSAGES: Record<string, string> = {
portal_home_v1_disabled: "工作台首页当前未启用,请联系管理员检查环境开关与功能开关配置。",
announcements_v1_disabled: "公告模块当前未启用,请联系管理员检查环境开关与功能开关配置。",
@@ -131,6 +160,38 @@ export function mapApiErrorMessage(
return "所有报销明细金额都必须大于 0,请修改后再提交。";
}
if (details?.items === "invoice_item_guard_failed") {
const messages: string[] = [];
const rawUnlinkedCount = details.unlinkedInvoiceCount;
const unlinkedCount = typeof rawUnlinkedCount === "number"
&& Number.isInteger(rawUnlinkedCount)
&& rawUnlinkedCount >= 0
? rawUnlinkedCount
: 0;
const missingCount = readStringList(details.missingInvoiceItemIds).length;
const unavailableCount = countGuardAmountUnavailableItems(details.amountUnavailableItems);
const exceededCount = countGuardAmountExceededItems(details.amountExceededItems);
if (unlinkedCount > 0) messages.push(`${unlinkedCount} 张发票未选择报销明细`);
if (missingCount > 0) messages.push(`${missingCount} 条需票明细尚未关联发票`);
if (unavailableCount > 0) messages.push(`${unavailableCount} 条明细的发票金额未识别`);
if (exceededCount > 0) messages.push(`${exceededCount} 条明细的申报金额高于发票合计`);
return messages.length
? `票据检查未通过:${messages.join(";")}。请按下方提示处理。`
: "票据检查未通过,请核对每条明细与发票的关联和金额后重试。";
}
if (details?.invoice === "invalid_unlinked_item_state") {
return "这张发票的关联状态已变化,请刷新后重试。";
}
if (details?.reimbursement === "invalid_status_for_invoice_item_link") {
return "当前报销单状态已变化,不能再关联发票,请刷新页面。";
}
if (details?.linkedItemId === "item_not_in_reimbursement") {
return "所选明细已不存在或不属于本单,请刷新后重新选择。";
}
const passwordErrors = readStringList(details?.password);
if (passwordErrors.length) {
return `新密码不符合要求:${passwordErrors.join(";")}。`;
+19
查看文件
@@ -194,6 +194,7 @@ export type ReimbursementDetail = {
mimeType: string | null;
fileSize: number | null;
linkedItemId: string | null;
amountEvidenceStatus: "verified" | "manual_review" | "ocr_failed";
}>;
attachments: Array<{
id: string;
@@ -225,6 +226,7 @@ export type ReimbursementDetail = {
approvalHistory: ReimbursementApprovalHistoryItem[];
riskFlags: RiskFlag[];
invoiceGuardEnabled?: boolean;
invoiceAmountManualReviewRequired?: boolean;
items: ReimbursementItem[];
};
@@ -391,6 +393,8 @@ export type ReimbursementSubmitResponse = {
precheckDecision: "pass" | "warn" | "manual_review";
standardWarnings: ExpenseStandardCheck[];
openedRiskFlagCount: number;
invoiceAmountManualReviewRequired: boolean;
manualReviewItemCount: number;
};
function toQueryString(query: Record<string, string | number | undefined>) {
@@ -472,6 +476,21 @@ export async function submitReimbursement(reimbursementId: string) {
});
}
export async function linkReimbursementInvoiceItem(
reimbursementId: string,
invoiceId: string,
linkedItemId: string,
) {
return apiRequestData<{
reimbursementId: string;
invoiceId: string;
linkedItemId: string;
}>(`/api/reimbursements/${reimbursementId}/invoices/${invoiceId}/item-link`, {
method: "PATCH",
body: JSON.stringify({ linkedItemId }),
});
}
export async function withdrawReimbursement(reimbursementId: string, comment?: string) {
return apiRequestData<Record<string, unknown>>(`/api/reimbursements/${reimbursementId}/withdraw`, {
method: "POST",