diff --git a/client/src/pages/InvoicePoolPage.tsx b/client/src/pages/InvoicePoolPage.tsx
index e72c1319..9cb5456d 100644
--- a/client/src/pages/InvoicePoolPage.tsx
+++ b/client/src/pages/InvoicePoolPage.tsx
@@ -177,7 +177,7 @@ export function InvoicePoolPage() {
if (previousStatus === "pending_ocr" && currentStatus && currentStatus !== "pending_ocr") {
void queryClient.invalidateQueries({ queryKey: ["invoice-pool"] });
if (currentStatus === "available") {
- toast.success("票据识别已完成,可继续绑定或验真");
+ toast.success("票据识别已完成,可继续添加到业务单据或财务复核");
} else if (currentStatus === "ocr_failed") {
toast.error(`票据识别未完成:${formatInvoiceOcrErrorMessage(detailQuery.data?.ocrLastError)}`);
}
@@ -385,9 +385,9 @@ export function InvoicePoolPage() {
queryClient.invalidateQueries({ queryKey: ["invoice-pool", "verification"] }),
]);
setError("");
- toast.success("验真已触发");
+ toast.success("发票核验已提交");
},
- onError: (err) => handleError(err, "触发验真失败,请稍后重试"),
+ onError: (err) => handleError(err, "提交发票核验失败,请稍后重试"),
});
const retryVerificationMutation = useMutation({
@@ -398,9 +398,9 @@ export function InvoicePoolPage() {
queryClient.invalidateQueries({ queryKey: ["invoice-pool", "verification"] }),
]);
setError("");
- toast.success("验真重试已触发");
+ toast.success("发票核验已重新提交");
},
- onError: (err) => handleError(err, "重试验真失败,请稍后重试"),
+ onError: (err) => handleError(err, "重新提交发票核验失败,请稍后重试"),
});
const overrideDuplicateMutation = useMutation({
diff --git a/client/src/pages/MobileReimbursementDetailPage.tsx b/client/src/pages/MobileReimbursementDetailPage.tsx
index 24aeeb13..687ec0b6 100644
--- a/client/src/pages/MobileReimbursementDetailPage.tsx
+++ b/client/src/pages/MobileReimbursementDetailPage.tsx
@@ -449,7 +449,7 @@ export function MobileReimbursementDetailPage() {
const handleTrackedInvoicesUnavailable = useCallback((items: Array<{ id: string; originalName: string | null; message: string; availabilityMessage?: string | null }>) => {
const message = items
- .map((item) => `${item.originalName ?? item.id.slice(0, 8)}:${item.availabilityMessage ?? item.message}`)
+ .map((item) => `${item.originalName ?? "未命名发票"}:${item.availabilityMessage ?? item.message}`)
.join(";");
setInvoiceActionError(message);
setInvoiceActionMessage("");
@@ -940,7 +940,7 @@ export function MobileReimbursementDetailPage() {
= {
+ block: "超额阻断",
+ warn: "超额提醒",
+ manual_review: "人工复核",
+ };
+ return controlMode ? labels[controlMode] ?? "按制度处理" : "按制度处理";
}
export function ReimbursementDetailPage() {
@@ -533,7 +542,7 @@ export function ReimbursementDetailPage() {
const handleTrackedInvoicesUnavailable = useCallback((items: Array<{ id: string; originalName: string | null; message: string; availabilityMessage?: string | null; ocrLastError?: string | null }>) => {
const message = items
- .map((item) => `${item.originalName ?? item.id.slice(0, 8)}: ${item.availabilityMessage ?? item.message ?? formatInvoiceOcrErrorMessage(item.ocrLastError)}`)
+ .map((item) => `${item.originalName ?? "未命名发票"}: ${item.availabilityMessage ?? item.message ?? formatInvoiceOcrErrorMessage(item.ocrLastError)}`)
.join(";");
setInvoiceActionError(message);
toast.error("部分发票需要处理");
@@ -1356,10 +1365,10 @@ export function ReimbursementDetailPage() {
本次提交后预计占用:{budgetHint.nextUsedAmount} / {budgetHint.allocatedAmount}
({budgetHint.nextUsagePercent.toFixed(2)}%)
- 控制模式:{budgetHint.controlMode}
+ 控制方式:{budgetControlModeLabel(budgetHint.controlMode)}
{budgetHint.exceedsBudget && budgetHint.controlMode === "block" ? (
- 当前金额将超出预算上限,提交时会被阻断(block 模式)。
+ 当前金额将超出预算上限,提交时会被阻断。
) : budgetHint.exceedsThreshold ? (
@@ -1376,8 +1385,8 @@ export function ReimbursementDetailPage() {
{canManageIntegration ? (
) : null}
@@ -186,7 +230,7 @@ export function InvoiceDetailPanel({
disabled={retryVerificationMutation.isPending}
onClick={() => retryVerificationMutation.mutate(detail.id)}
>
- {retryVerificationMutation.isPending ? "重试中..." : "重试验真"}
+ {retryVerificationMutation.isPending ? "重试中..." : "重新核验"}
) : null}
@@ -266,29 +310,29 @@ export function InvoiceDetailPanel({
{canVerifyInvoice ? (
-
验真记录
+
辅助核验记录
{(verificationQuery.data?.items ?? []).length ? (
{(verificationQuery.data?.items ?? []).map((record, index) => (
记录 {index + 1}
-
- {record.status}
+
+ {verificationStatusLabel(record.status)}
- 服务商: {record.provider} |
+ 来源: {verificationProviderLabel(record.provider)} |
重试次数: {record.attempts} |
- 验真时间: {record.requestedAt || "-"}
+ 核验时间: {record.requestedAt || "-"}
- {record.resultMessage ?
{record.resultMessage}
: null}
- {record.errorMessage ?
{record.errorMessage}
: null}
+ {record.resultMessage ?
{verificationMessage(record.resultMessage)}
: null}
+ {record.errorMessage ?
{verificationMessage(record.errorMessage)}
: null}
))}
) : (
-
暂无验真记录
+
暂无辅助核验记录
)}
) : null}
diff --git a/client/tests/e2e/invoice-ocr-flow.spec.ts b/client/tests/e2e/invoice-ocr-flow.spec.ts
index 9a4ce8f2..dbaa8dda 100644
--- a/client/tests/e2e/invoice-ocr-flow.spec.ts
+++ b/client/tests/e2e/invoice-ocr-flow.spec.ts
@@ -27,7 +27,7 @@ test("E2E 轻交互 OCR:报销详情上传后自动添加并提交", async ({
await login(page, EMPLOYEE);
const reimbursementId = await createBlankReimbursement(page);
- await page.goto(`/reimbursements/${reimbursementId}`);
+ await expect(page).toHaveURL(new RegExp(`/reimbursements/${reimbursementId}$`));
await reimbursementDetail.assertLoaded();
await page.getByTestId(TEST_IDS.reimbursement.invoiceAdd).click();
diff --git a/issues/2026-05-09_20-57-19-mvp-mainline-dev-kickoff.csv b/issues/2026-05-09_20-57-19-mvp-mainline-dev-kickoff.csv
index 7aeab55c..3083e0ef 100644
--- a/issues/2026-05-09_20-57-19-mvp-mainline-dev-kickoff.csv
+++ b/issues/2026-05-09_20-57-19-mvp-mainline-dev-kickoff.csv
@@ -1,7 +1,7 @@
ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes
A1,MVP主线代码差距盘点,"对照当前 PRD 和 7 份模块规约,盘点 workflow、reimbursements、invoicePool、wecomAuth、leave、purchaseRequests、contractsRegistry、projects 等现有实现、页面、回归脚本和缺口;本 Issue 不改业务代码。","1) 形成可复用实现、待修缺口和不重复造轮子的清单;2) 明确 A2-A8 的实际改动边界;3) 确认首轮默认不新增公共 API、不新增数据库表;4) 若发现必须新增接口、字段或迁移,拆独立后续 Issue。","流程级验证:按 PRD 的审批、报销、企业微信入口、请假、采购、合同、项目七条主线逐项映射到现有 server/client/tests 文件;命令:rg 检索关键路由、页面和测试;命令:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-09_20-57-19-mvp-mainline-dev-kickoff.csv。","ripgrep|manual|python3",DONE,DONE,DONE,"server/src/routes/*|client/src/pages/*|client/src/api/*|client/tests/e2e/*|docs/*.md","[]","A1 已完成只读差距盘点,不改业务代码;盘点文档:docs/MVP主线代码差距盘点.md;如后续发现需迁移或新增 API,必须另拆 Issue 并写回滚策略。"
A2,统一审批中心入口与状态收口,"收口 PC /workflow/tasks、移动 /mobile/approvals、业务详情页审批历史和通知 deeplink 的一致性,确保正式审批动作回到 OA 完成。","1) 待办、已办、业务详情页状态一致;2) 审批动作通过、驳回、退回、撤回、转审仍写入 OA 工作流历史;3) 企业微信消息只 deeplink 回 OA,不出现企业微信原生审批入口;4) 并发或已处理任务有明确提示。","流程级验证:提交一条报销或请假审批 -> PC 待办处理 -> 移动待办同步消失或进入已办 -> 业务详情审批历史一致;命令:cd server && ./scripts/regression-workflow-actions.sh;命令:cd client && npm run test:e2e -- workflow-quick-approve.spec.ts mobile-approvals-offline.spec.ts。","bash|npm|playwright|manual",DONE,DONE,DONE,"server/src/routes/workflow.js|server/src/utils/workflow.js|client/src/pages/WorkflowTasksPage.tsx|client/src/pages/MobileApprovalsPage.tsx|client/src/pages/WorkflowTaskDetailPage.tsx","[""A1""]","A2 开发、审查和回归已完成;A2.1 修正详情页 pending 文案;本地 DB 漂移已通过 hyb_oa_regression 隔离库解阻;workflow 后端回归、PC 快捷审批 E2E、移动审批 E2E、client typecheck/build 均通过。"
-A3,报销发票OCR主链路收口,"收口上传、OCR 状态、查重、绑定明细、提交审批、付款标记和导出中的体验或状态缺口,优先复用现有 reimbursement 与 invoicePool 实现。","1) 员工能完成报销主流程:上传发票/附件 -> OCR/查重 -> 绑定明细 -> 提交审批;2) 财务能处理重复、付款标记和导出;3) 普通用户页面不出现内部状态码、发票池、绑定整单等不应见文案;4) OCR 失败、重复、附件缺失有业务提示。","流程级验证:创建报销草稿 -> 上传票据 -> OCR 进入终态 -> 绑定明细 -> 提交审批 -> 审批通过 -> 财务付款/导出;命令:cd server && ./scripts/regression-dedup.sh && ./scripts/regression-ocr.sh;命令:cd client && npm run test:e2e -- r102-reimbursement-main-flow.spec.ts invoice-ocr-flow.spec.ts。","bash|npm|playwright|manual",TODO,TODO,TODO,"server/src/routes/reimbursements.js|server/src/routes/invoicePool.js|server/src/routes/invoicePool/*|client/src/pages/ReimbursementDetailPage.tsx|client/src/pages/InvoicePoolPage.tsx|client/src/pages/reimbursement-detail/*|client/src/pages/invoice-pool/*","[""A1"",""A2""]","涉及报销和发票状态;默认不改 OCR provider 和数据库结构,若需 schema/API 变更另拆 Issue。"
+A3,报销发票OCR主链路收口,"收口上传、OCR 状态、查重、绑定明细、提交审批、付款标记和导出中的体验或状态缺口,优先复用现有 reimbursement 与 invoicePool 实现。","1) 员工能完成报销主流程:上传发票/附件 -> OCR/查重 -> 绑定明细 -> 提交审批;2) 财务能处理重复、付款标记和导出;3) 普通用户页面不出现内部状态码、发票池、绑定整单等不应见文案;4) OCR 失败、重复、附件缺失有业务提示。","流程级验证:创建报销草稿 -> 上传票据 -> OCR 进入终态 -> 绑定明细 -> 提交审批 -> 审批通过 -> 财务付款/导出;命令:cd server && ./scripts/regression-dedup.sh && ./scripts/regression-ocr.sh;命令:cd client && npm run test:e2e -- r102-reimbursement-main-flow.spec.ts invoice-ocr-flow.spec.ts。","bash|npm|playwright|manual",DONE,DONE,DONE,"server/src/routes/reimbursements.js|server/src/routes/invoicePool.js|server/src/routes/invoicePool/*|client/src/pages/ReimbursementDetailPage.tsx|client/src/pages/InvoicePoolPage.tsx|client/src/pages/reimbursement-detail/*|client/src/pages/invoice-pool/*","[""A1"",""A2""]","A3 已完成:前端收口报销/票据/OCR文案、隐藏内部码值和 UUID 兜底、财务导出不再面向报销页显示 ERP 术语;未新增 API、未新增迁移、未改变 OCR provider 或状态机。验证通过:prisma validate、node --check reimbursements/invoicePool、regression-dedup、regression-ocr、client typecheck/build、E2E 报销主流程+invoice-ocr-flow,视觉截图 output/playwright/a3-reimbursement-ocr。"
A4,企业微信入口绑定通知回OA,"收口企业微信 config、登录/绑定、移动落点、通知 deeplink 和脱敏消息,企业微信只作为入口、身份、通知和打卡原始采集边界。","1) 企业微信手机入口默认进入 /mobile;2) 已绑定用户直达,未绑定可回退账号密码;3) 停用员工不得从企业微信进入业务系统;4) 通知内容脱敏且 deeplink 回 OA;5) 通知失败不阻断主业务。","流程级验证:模拟企业微信 UA 访问入口 -> 已绑定进入移动工作台 -> 未绑定回退账号密码 -> 通知 deeplink 打开 OA 审批页;命令:cd client && npm run test:e2e -- wecom-entry.spec.ts wecom-first-bind.spec.ts r124-mobile-entry-matrix.spec.ts;后端补 node --check src/routes/wecomAuth.js。","npm|playwright|node|manual",DONE,DONE,DONE,"server/src/routes/wecomAuth.js|server/src/services/notifications/*|client/src/lib/wecomEntry.ts|client/src/lib/runtimeContext.ts|client/src/lib/notificationLinks.ts","[""A1"",""A2""]","A4 已完成:企业微信入口测试默认 /mobile;通知 deeplink 限定为 OA 相对路径并补回归;未接企业微信原生审批、飞书或打卡同步;真实企业微信凭据/真机 live smoke 留到 A8,缺配置时 SKIP。"
A5,请假补卡外出加班审批与考勤口径,"优先收口现有请假审批、团队状态、日历展示和考勤解释边界;首轮不做企业微信打卡 API 同步,不重造定位、人脸、Wi-Fi 或排班。","1) 请假审批状态正确;2) 已批准请假影响团队状态或日历展示;3) 补卡、外出、加班若现有实现不足,先记录缺口或做最小入口收口;4) 文案不引导员工去企业微信原生审批。","流程级验证:员工提交请假 -> 主管/HR 在 OA 审批 -> 团队状态或日历体现结果 -> 驳回/撤回后状态正确;命令:cd client && npm run test:e2e -- leave-flow.spec.ts leave-team-calendar.spec.ts;命令:cd server && node --check src/routes/leave.js。","npm|playwright|node|manual",TODO,TODO,TODO,"server/src/routes/leave.js|server/src/services/leave.js|client/src/pages/LeaveRequestPage.tsx|client/src/pages/MobileLeavePage.tsx|client/src/pages/TeamPresencePage.tsx","[""A1"",""A2""]","首轮不接企业微信打卡同步;若补卡/外出/加班缺运行时能力,A1 后先拆最小 Issue,不扩大到完整考勤系统。"
A6,P1采购合同项目最小可用收口,"只做现有 P1 能力的最小可用检查和缺口修复,不扩展预算、支付、ERP、AI、复杂招采或重型项目管理。","1) 采购/费用可申请并进入 OA 审批;2) 合同台账可登记、查询、记录编号、负责人和归档位置;3) 项目能关联关键事项并展示摘要;4) P2 能力不作为普通员工主路径。","流程级验证:创建采购申请 -> OA 审批 -> 项目页查看关联;登记合同台账 -> 查询合同摘要;项目页查看任务/里程碑/风险/业务关联;命令:cd server && ./scripts/regression-procurement.sh;命令:cd client && npm run test:e2e -- procurement-flow.spec.ts project-workbench.spec.ts;合同台账执行人工或补 E2E。","bash|npm|playwright|manual",TODO,TODO,TODO,"server/src/routes/purchaseRequests.js|server/src/routes/contractsRegistry.js|server/src/routes/projects.js|client/src/pages/PurchaseRequestsPage.tsx|client/src/pages/ContractsRegistryPage.tsx|client/src/pages/ProjectsPage.tsx|client/src/pages/ProjectDetailPage.tsx","[""A1"",""A2""]","P1 只收口最小可用;预算/ERP/AI/完整差旅/重型项目管理不进入首轮开发。"