比较提交

...
2 次代码提交
作者 SHA1 备注 提交日期
wangsli b6ff44d504 回填报销文案热修元数据(08-30-b2-submit-error-ocr-fallback)
ci-minimal / check-minimal (push) Canceled after 0s
ci-e2e / e2e (push) Canceled after 0s
2026-08-31 23:23:12 +08:00
wangsli fddf3a3bc7 修复报销金额阻断文案门禁(08-30-b2-submit-error-ocr-fallback) 2026-08-31 23:22:41 +08:00
共修改 11 个文件,包含 33 行新增和 14 行删除
@@ -104,6 +104,7 @@ and verification contracts. Do not copy broad aesthetic guidance from
Rewriting Host to `127.0.0.1:3000` makes full-page reloads lose the session
with `AUTH_005`.
- Run `cd client && npm run typecheck && npm run build` for frontend changes.
- Any task that changes user-visible frontend copy must also run `node scripts/qa/check-ui-design-rules.mjs --strict` before commit and again before deployment. Treat the public bundle copy scan as a post-deploy witness, not the first place to discover forbidden user-facing terms. For reimbursement/invoice flows, regression assertions cover all four internal terms: `绑定整单`, `不绑定明细`, `发票池`, and `OCR`.
## Scenario: Playwright API Mocks Against A Production PWA Build
@@ -98,6 +98,7 @@ PC 和移动卡片在未归属时显示明细下拉框和“确认关联”。
- 公网使用合成草稿覆盖:未归属恢复、缺票、金额识别失败、超票面、off/未 OCR 人工核对和正常提交;不得改真人单据做 smoke。
- 部署后核 `/ready`、bundle marker、B2 仍为 `force_on + global`,并确认四张真人单据状态未被部署改变。
- 应用异常时回滚到本轮部署前生产候选;本轮无 schema/数据迁移,已存在票据关系无需补偿。
- 2026-08-31 首次发布在 init、应用切换、`/ready` 和 runtime smoke 通过后,被公网文案扫描拦截:4 处用户文案含内部术语 `OCR`。应用已按合同回滚至 `0f3ecfa`。根因不是公网扫描器,而是本地 `check-ui-design-rules.mjs --strict` 未进入本 task Test Plan;热修后把它提升为所有用户可见前端文案变更的提交前标准门禁,并以四词 E2E 断言防止同类回归。
## 风险控制
@@ -45,6 +45,7 @@
- [x] 运行 `cd server && npm run prisma:validate`、聚焦 Vitest,必要时全量相关路由回归。
- [x] 运行 `cd client && npm run typecheck && npm run build` 及聚焦 Playwright。
- [x] 运行 `node scripts/qa/check-process-contract.mjs --baseline --json`、`node scripts/qa/audit-docs.mjs --check-registry`、`node scripts/qa/check-trellis-governance.mjs` 和 `git diff --check`。
- [x] 部署失败热修:运行 `node scripts/qa/check-ui-design-rules.mjs --strict`,确保四个普通用户禁用术语(`绑定整单`、`不绑定明细`、`发票池`、`OCR`)在报销 PC/移动错误态均无命中;strict 扫描 148 个文件为 0 findings,self-test PASS,该门禁已在重新提交和部署前通过。
## 7. 只读复核、提交与部署门
@@ -79,7 +79,7 @@
- 后端聚焦测试覆盖未归属、无关联 PDF、金额识别失败、完整金额超额、off/未执行 OCR 人工核对、混合金额证据、免票补助、flag off 兼容和 0014 类正常通过。
- 原子关联接口覆盖成功、权限、状态、跨单、已有归属与并发零写入。
- 前端单元/组件及 PC/移动 E2E 覆盖安全中文映射、就近定位、原子恢复、人工核对提示、可访问告警和内部 ID 不泄露。
- 运行 Prisma validate、相关 Vitest、前端 typecheck/build、聚焦 Playwright、流程/文档/Trellis governance 与 `git diff --check`;部署后只用合成草稿做公网验收。
- 运行 Prisma validate、相关 Vitest、前端 typecheck/build、聚焦 Playwright、`node scripts/qa/check-ui-design-rules.mjs --strict`、流程/文档/Trellis governance 与 `git diff --check`;凡修改用户可见前端文案,strict UI 文案门禁必须在提交和部署前运行,不能只依赖公网 bundle 扫描兜底。部署后只用合成草稿做公网验收。
## Out of Scope
@@ -15,7 +15,7 @@
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": "8aed72d",
"commit": "fddf3a3",
"pr_url": null,
"subtasks": [],
"children": [],
+1
查看文件
@@ -45,6 +45,7 @@ const PAGE_TITLES: Array<{ path: string; title: string; group: string }> = [
{ path: "/mobile/purchase-requests", title: "移动采购", group: "移动工作台" },
{ path: "/mobile/reimbursements", title: "移动报销", group: "移动工作台" },
{ path: "/mobile", title: "移动首页", group: "移动工作台" },
{ path: "/system/settings", title: "管理", group: "管理" },
{ path: "/settings/profile", title: "个人设置", group: "账号设置" },
{ path: "/dashboard", title: "数据看板", group: "更多" },
];
@@ -1158,7 +1158,7 @@ export function MobileReimbursementDetailPage() {
/>
</label>
{shouldShowOfficeElectronicsHint(
expenseTypes.find((type) => type.id === itemForm.expenseTypeId)?.code,
expenseTypes.find((type) => type.id === itemForm.expenseTypeId),
itemForm.amount,
) ? (
<p className="col-span-2 text-xs text-amber-600">该金额通常应选择办公电子设备。</p>
@@ -1199,7 +1199,7 @@ export function MobileReimbursementDetailPage() {
<div className="shrink-0 text-base font-semibold text-zinc-900">{formatMoney(item.amount)}</div>
</div>
{item.description ? <p className="mt-2 text-sm leading-5 text-zinc-600">{item.description}</p> : null}
{shouldShowOfficeElectronicsHint(item.expenseType?.code, item.amount) ? (
{shouldShowOfficeElectronicsHint(item.expenseType, item.amount) ? (
<p className="mt-2 text-xs text-amber-600">该金额通常应选择办公电子设备。</p>
) : null}
{invoiceGuardItemMessages(invoiceGuardIssues, item.id).map((message) => (
@@ -1259,7 +1259,7 @@ export function MobileReimbursementDetailPage() {
{detail.invoiceAmountManualReviewRequired ? (
<p className="mt-3 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm leading-6 text-amber-800">
本单包含未执行 OCR 的发票,系统未自动比较相关明细金额;财务审批时需查看发票原件核对。
本单包含票面金额未自动读取的发票,系统未自动比较相关明细金额;财务审批时需查看发票原件核对。
</p>
) : null}
@@ -1418,7 +1418,7 @@ export function MobileReimbursementDetailPage() {
) : null}
{invoice.amountEvidenceStatus === "ocr_failed" ? (
<p className="mt-3 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm leading-5 text-red-700">
OCR 已处理但未取得票面金额,提交前需要重试识别或联系财务。
该发票的票面金额未能读取,暂时无法提交;请重新上传这张发票,或联系财务处理。
</p>
) : null}
{canMaintainReimbursement && !invoice.linkedItemId ? (
+1 -1
查看文件
@@ -138,7 +138,7 @@ export function PaymentAccountsPage() {
{canManage && companyState.kind === "multiple" ? (
<SectionPanel title="公司信息需要处理">
<div className="space-y-3 text-sm text-red-600">
<p>检测到多条启用的公司信息,系统不会猜测付款账户应关联哪一条。请先处理公司信息。</p>
<p>检测到多条启用的公司信息,系统不会猜测付款账户应关联哪一条。请先完善公司信息。</p>
{canManageCompanyInformation ? (
<Button asChild type="button" variant="secondary">
<Link to="/system/master-data">查看公司信息</Link>
+3 -3
查看文件
@@ -1346,7 +1346,7 @@ export function ReimbursementDetailPage() {
</TableCell>
<TableCell>
<div>{item.description ?? "-"}</div>
{shouldShowOfficeElectronicsHint(item.expenseType?.code, item.amount) ? (
{shouldShowOfficeElectronicsHint(item.expenseType, item.amount) ? (
<div className="mt-1 text-xs text-amber-600">
该金额通常应选择办公电子设备。
</div>
@@ -1480,7 +1480,7 @@ export function ReimbursementDetailPage() {
{detail.invoiceAmountManualReviewRequired ? (
<p className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm leading-6 text-amber-800">
本单包含未执行 OCR 的发票,系统未自动比较相关明细金额;财务审批时需查看发票原件核对。
本单包含票面金额未自动读取的发票,系统未自动比较相关明细金额;财务审批时需查看发票原件核对。
</p>
) : null}
@@ -1541,7 +1541,7 @@ export function ReimbursementDetailPage() {
) : null}
{invoice.amountEvidenceStatus === "ocr_failed" ? (
<p className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs leading-5 text-red-700">
OCR 已处理但未取得票面金额,提交前需要重试识别或联系财务。
该发票的票面金额未能读取,暂时无法提交;请重新上传这张发票,或联系财务处理。
</p>
) : null}
{canMaintainReimbursement && !invoice.linkedItemId ? (
@@ -8,9 +8,12 @@ import { Textarea } from "../../components/ui/textarea";
export const OFFICE_ELECTRONICS_HINT_THRESHOLD = 2000;
export function shouldShowOfficeElectronicsHint(expenseTypeCode: string | null | undefined, amount: string | number) {
export function shouldShowOfficeElectronicsHint(
expenseType: Pick<ExpenseType, "code"> | null | undefined,
amount: string | number,
) {
const numericAmount = Number(amount);
return expenseTypeCode === "office_supplies"
return expenseType?.code === "office_supplies"
&& Number.isFinite(numericAmount)
&& numericAmount >= OFFICE_ELECTRONICS_HINT_THRESHOLD;
}
@@ -70,7 +73,7 @@ export function ReimbursementItemEditor({
const selectedExpenseType = expenseTypes.find((item) => item.id === expenseTypeId) ?? null;
const numericAmount = Number(amount || 0);
const shouldWarnOfficeEquipment =
shouldShowOfficeElectronicsHint(selectedExpenseType?.code, numericAmount);
shouldShowOfficeElectronicsHint(selectedExpenseType, numericAmount);
const standardsQuery = useQuery({
enabled: Boolean(expenseTypeId),
@@ -955,7 +955,7 @@ test.describe("R111 国内报销发票与附件双端体验", () => {
const ocrFailedInvoice = {
...buildInvoice(),
id: "77777777-7777-4777-8777-777777777777",
invoiceNumber: "OCR-FAILED",
invoiceNumber: "AMOUNT-UNAVAILABLE",
originalName: "金额未识别.pdf",
totalAmount: null,
linkedItemId: "item-ocr-r111",
@@ -1010,6 +1010,12 @@ test.describe("R111 国内报销发票与附件双端体验", () => {
).toBeVisible();
await expect(page.getByText("这张发票还没有选择报销明细,请在下方完成关联。")).toBeVisible();
await expect(page.getByText("票面金额未自动识别,需由财务查看原件核对。")).toBeVisible();
await expect(page.getByText(
"该发票的票面金额未能读取,暂时无法提交;请重新上传这张发票,或联系财务处理。",
)).toBeVisible();
for (const forbiddenTerm of ["绑定整单", "不绑定明细", "发票池", "OCR"]) {
await expect(page.locator("body")).not.toContainText(forbiddenTerm);
}
await expect(page.getByText(unlinkedInvoiceId, { exact: false })).toHaveCount(0);
await expect(page.getByText("item-missing-r111", { exact: false })).toHaveCount(0);
await page.getByLabel("选择报销明细").selectOption("item-missing-r111");
@@ -1059,7 +1065,13 @@ test.describe("R111 国内报销发票与附件双端体验", () => {
await expect(page.getByText("尚未关联正式发票,请到“报销材料”添加发票并选择本明细。")).toBeVisible();
await expect(page.getByText("这张发票还没有选择报销明细,请在下方完成关联。")).toBeVisible();
await expect(page.getByText("票面金额未自动识别,需由财务查看原件核对。")).toBeVisible();
await expect(page.getByText(
"该发票的票面金额未能读取,暂时无法提交;请重新上传这张发票,或联系财务处理。",
)).toBeVisible();
await expect(page.getByLabel("选择报销明细")).toBeVisible();
for (const forbiddenTerm of ["绑定整单", "不绑定明细", "发票池", "OCR"]) {
await expect(page.locator("body")).not.toContainText(forbiddenTerm);
}
});
test("企业微信手机端保持移动布局,发票和附件能力与 PC 一致", async ({ browser }) => {