From 33b1232092d4935b8fc533fe4518581d940e0fd7 Mon Sep 17 00:00:00 2001 From: shunli Date: Thu, 28 May 2026 20:17:38 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=99=84=E4=BB=B6=E6=B2=BB?= =?UTF-8?q?=E7=90=86=E9=83=A8=E7=BD=B2=E6=A3=80=E6=9F=A5=E7=BC=BA=E5=8F=A3?= =?UTF-8?q?=EF=BC=8805-28-attachment-governance-deploy-smoke=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../check.jsonl | 5 ++ .../design.md | 65 ++++++++++++++++ .../implement.jsonl | 5 ++ .../prd.md | 74 +++++++++++++++++++ .../task.json | 32 ++++++++ .../manual/p2-fin-live-payment-batch.spec.ts | 33 ++++++++- server/src/routes/reimbursements.js | 10 +++ 7 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 .trellis/tasks/05-28-attachment-governance-deploy-smoke/check.jsonl create mode 100644 .trellis/tasks/05-28-attachment-governance-deploy-smoke/design.md create mode 100644 .trellis/tasks/05-28-attachment-governance-deploy-smoke/implement.jsonl create mode 100644 .trellis/tasks/05-28-attachment-governance-deploy-smoke/prd.md create mode 100644 .trellis/tasks/05-28-attachment-governance-deploy-smoke/task.json diff --git a/.trellis/tasks/05-28-attachment-governance-deploy-smoke/check.jsonl b/.trellis/tasks/05-28-attachment-governance-deploy-smoke/check.jsonl new file mode 100644 index 00000000..a922de4e --- /dev/null +++ b/.trellis/tasks/05-28-attachment-governance-deploy-smoke/check.jsonl @@ -0,0 +1,5 @@ +{"file": ".trellis/spec/hyb-oa/project-governance.md", "reason": "核对生产审批边界"} +{"file": ".trellis/spec/hyb-oa/deployment-trialrun.md", "reason": "核对部署与 smoke 门禁"} +{"file": ".trellis/spec/hyb-oa/backend-fastify-prisma.md", "reason": "核对迁移校验"} +{"file": ".trellis/spec/hyb-oa/security-privacy.md", "reason": "核对敏感信息边界"} +{"file": "docs/试运行与自动化验证手册.md", "reason": "核对公网验证手册"} diff --git a/.trellis/tasks/05-28-attachment-governance-deploy-smoke/design.md b/.trellis/tasks/05-28-attachment-governance-deploy-smoke/design.md new file mode 100644 index 00000000..f79e0207 --- /dev/null +++ b/.trellis/tasks/05-28-attachment-governance-deploy-smoke/design.md @@ -0,0 +1,65 @@ +# 附件治理部署与公网烟测设计 + +## 部署策略 + +采用现有单机试运行部署脚本,不新增部署机制: + +```bash +SYNC_ENV=0 bash deploy/trialrun/deploy.sh +``` + +理由: + +- `SYNC_ENV=0` 是项目规范要求的安全默认值,不覆盖远端 `.env`。 +- 脚本已经覆盖环境契约、rsync、远端 Docker Compose、Prisma migration、seed、`/ready` 和 runtime smoke。 +- 本轮业务附件 V1 不新增环境变量,不需要改远端配置。 + +## 迁移影响 + +本轮待部署迁移: + +- `server/prisma/migrations/20260528162000_business_attachment_governance_v1/migration.sql` + +迁移内容: + +- `purchase_attachments` 新增 `file_sha256`、治理状态、敏感级别、附件类型、复核/归档和下载策略字段。 +- `reimbursement_attachments` 新增治理状态、敏感级别、附件类型、复核/归档和下载策略字段。 +- `payment_batch_receipt_attachments` 新增治理状态、敏感级别、附件类型、复核/归档和下载策略字段。 +- 新增 hash、治理状态和敏感级别索引。 + +风险判断: + +- 新增字段均有默认值或可空,不改写历史附件文件。 +- 不删除列、不改主键、不重命名表,不涉及历史数据迁移。 +- 部署失败时优先回滚应用到上一个 commit;数据库新增列可暂留,旧代码会忽略新增字段。 + +## 执行顺序 + +1. 本地预检: + - `git status --short` + - `node deploy/trialrun/validate-env-contract.js` + - `cd server && npm run prisma:validate` + - `cd client && npm run typecheck && npm run build` + - Trellis governance 和 `git diff --check` +2. 用户明确批准部署。 +3. 执行: + - `SYNC_ENV=0 bash deploy/trialrun/deploy.sh` +4. 部署后检查: + - 记录部署 commit:`52e5284` 或当前 HEAD。 + - 确认公网 `/ready`。 + - 跑 live smoke readiness。 + - 进入 OA 付款批次回单附件,确认下载用途弹窗。 + - 检查审计日志中出现回单下载记录和用途。 + - 查看报销/采购附件区域,确认状态、类型、敏感级别标签。 + +## 回滚口径 + +- 应用回滚:部署上一个稳定 commit,并复跑 `/ready`、runtime smoke 和 live readiness。 +- 数据库:不主动删除新增列;只有确认无新代码依赖且另行批准时,才考虑反向 DDL。 +- 数据:不清理公网业务数据,不删除真实附件。 +- 配置:本轮不改远端 `.env`,无需配置回滚。 + +## 证据口径 + +- 可报告:commit hash、命令结论、`/ready` 状态、smoke 结果、脱敏错误摘要、审计动作是否存在。 +- 不报告:密码、token、Secret、完整账号、完整路径、真实附件内容、原始 payload。 diff --git a/.trellis/tasks/05-28-attachment-governance-deploy-smoke/implement.jsonl b/.trellis/tasks/05-28-attachment-governance-deploy-smoke/implement.jsonl new file mode 100644 index 00000000..0f0d8f24 --- /dev/null +++ b/.trellis/tasks/05-28-attachment-governance-deploy-smoke/implement.jsonl @@ -0,0 +1,5 @@ +{"file": ".trellis/spec/hyb-oa/project-governance.md", "reason": "部署和生产操作审批边界"} +{"file": ".trellis/spec/hyb-oa/deployment-trialrun.md", "reason": "公网部署、smoke 和回滚门禁"} +{"file": ".trellis/spec/hyb-oa/backend-fastify-prisma.md", "reason": "Prisma migration 部署和校验规则"} +{"file": ".trellis/spec/hyb-oa/security-privacy.md", "reason": "附件治理和敏感证据边界"} +{"file": "docs/试运行与自动化验证手册.md", "reason": "公网 live smoke 和试运行验证手册"} diff --git a/.trellis/tasks/05-28-attachment-governance-deploy-smoke/prd.md b/.trellis/tasks/05-28-attachment-governance-deploy-smoke/prd.md new file mode 100644 index 00000000..857b491f --- /dev/null +++ b/.trellis/tasks/05-28-attachment-governance-deploy-smoke/prd.md @@ -0,0 +1,74 @@ +# 附件治理部署与公网烟测 + +## Goal + +将已提交的“业务附件受控入库 V1”安全部署到公网 OA,并完成迁移、健康检查、公网 smoke 和关键业务验证,确认报销附件、采购附件、付款回单附件的治理字段、下载用途和审计留痕在公网可用。 + +## What I Already Know + +- 本轮待部署工作提交为 `52e5284`,任务已归档提交为 `fef5966`。 +- 业务附件 V1 包含 Prisma migration:`20260528162000_business_attachment_governance_v1`。 +- 部署入口为 `SYNC_ENV=0 bash deploy/trialrun/deploy.sh`,默认不覆盖远端 `.env`。 +- 部署脚本会在远端执行 `npm run prisma:migrate:deploy`、`seed:config`、`seed:trialrun`、`npx prisma migrate status`、`/ready`、external deps smoke 和 runtime smoke。 +- 部署后还需执行公网 live smoke 和面向附件治理的关键路径检查。 +- 生产部署、远端配置修改、数据库迁移和公网真实数据写入都需要明确批准。 + +## Scope + +- 部署前本地静态门禁:部署环境契约、Prisma validate、前端 typecheck/build、Trellis governance。 +- 部署前确认迁移影响和回滚口径:新增列/索引,不重置数据库、不改写历史迁移、不覆盖远端 `.env`。 +- 获得明确批准后执行公网部署。 +- 部署后检查: + - `/ready` + - `npx prisma migrate status` + - external deps smoke + - runtime smoke + - live smoke readiness + - 公网前端资源文案扫描 + - 付款回单下载用途弹窗和审计日志 + - 报销/采购附件治理标签展示 + +## Out Of Scope + +- 不迁移历史附件文件,不接 COS,不改存储驱动。 +- 不覆盖远端 `.env`,除非另行明确批准 `SYNC_ENV=1`。 +- 不执行真实付款、不导入银行流水、不写 ERP、不写企业微信。 +- 不使用真实敏感票据、合同正文、银行材料或完整账号做自动化证据。 +- 不做数据库 reset、迁移漂移修复或迁移历史重写。 + +## Requirements + +- R1:部署前必须确认本地工作区干净,当前 commit 可追溯。 +- R2:部署前必须跑本地静态门禁和 Trellis governance,失败则不部署。 +- R3:部署必须使用 `SYNC_ENV=0`,保持远端 `.env` 原状。 +- R4:部署脚本执行远端迁移后必须确认 migrate status 通过。 +- R5:公网 smoke 失败时,停止并保留错误摘要,不继续宣称可以测试。 +- R6:附件治理专项验收必须覆盖付款回单下载用途、审计日志、报销/采购附件治理标签。 +- R7:报告不得输出 Secret、密码、完整账号、完整本地路径、附件原文或原始 payload。 + +## Acceptance Criteria + +- [x] 部署前静态门禁通过。 +- [ ] 获得明确“批准部署”后执行公网部署。 +- [ ] 部署脚本完成,远端 `/ready` 通过。 +- [ ] 远端 Prisma migration status 通过。 +- [ ] 公网 live smoke 或 readiness 按规则输出 PASS/SKIP/FINDINGS,结论清晰。 +- [ ] 业务附件专项验证通过或记录阻断项。 +- [x] Trellis task validate、governance gate、`git diff --check` 通过。 + +## Test Plan + +- `node deploy/trialrun/validate-env-contract.js` +- `cd server && npm run prisma:validate` +- `cd client && npm run typecheck && npm run build` +- `python3 ./.trellis/scripts/task.py validate 05-28-attachment-governance-deploy-smoke` +- `node scripts/qa/check-trellis-governance.mjs` +- `git diff --check` +- 获批后:`SYNC_ENV=0 bash deploy/trialrun/deploy.sh` +- 部署后:`LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-live-smoke-readiness.sh` + +## Technical Notes + +- 参考规范:`.trellis/spec/hyb-oa/deployment-trialrun.md`、`.trellis/spec/hyb-oa/backend-fastify-prisma.md`、`.trellis/spec/hyb-oa/security-privacy.md`。 +- 参考手册:`docs/试运行与自动化验证手册.md`。 +- 本任务当前阶段只做部署前准备;生产部署必须等用户明确回复“批准部署”。 diff --git a/.trellis/tasks/05-28-attachment-governance-deploy-smoke/task.json b/.trellis/tasks/05-28-attachment-governance-deploy-smoke/task.json new file mode 100644 index 00000000..d80eb86e --- /dev/null +++ b/.trellis/tasks/05-28-attachment-governance-deploy-smoke/task.json @@ -0,0 +1,32 @@ +{ + "id": "attachment-governance-deploy-smoke", + "name": "attachment-governance-deploy-smoke", + "title": "附件治理部署与公网烟测", + "description": "部署业务附件治理 V1 到公网 OA,并完成迁移、健康检查、live smoke 和附件治理专项验证。", + "status": "in_progress", + "dev_type": null, + "scope": "deployment-smoke", + "package": "hyb-oa", + "priority": "P2", + "creator": "shunli", + "assignee": "shunli", + "createdAt": "2026-05-28", + "completedAt": null, + "branch": null, + "base_branch": "main", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [ + ".trellis/tasks/05-28-attachment-governance-deploy-smoke/prd.md", + ".trellis/tasks/05-28-attachment-governance-deploy-smoke/design.md", + ".trellis/tasks/archive/2026-05/05-28-business-attachment-governance-v1/prd.md", + "deploy/trialrun/deploy.sh", + "docs/试运行与自动化验证手册.md" + ], + "notes": "", + "meta": {} +} \ No newline at end of file diff --git a/client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts b/client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts index a7df8d7a..783cbb90 100644 --- a/client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts +++ b/client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts @@ -52,6 +52,10 @@ type ReceiptAttachment = { originalName: string; mimeType: string; fileSize: number; + governanceStatus: string; + sensitivityLevel: string; + attachmentType: string; + downloadPolicy: string; }; type FinanceLedgerResponse = { @@ -500,8 +504,18 @@ test.describe("公网付款批次受控验收", () => { finance.api, `/api/payment-batches/${batch.id}/receipt-attachments`, ); - expect(receiptList.items.some((item) => item.id === receipt.id)).toBe(true); - const receiptDownload = await finance.api.get(`/api/payment-batches/${batch.id}/receipt-attachments/${receipt.id}/file`); + const listedReceipt = receiptList.items.find((item) => item.id === receipt.id); + expect(listedReceipt).toBeTruthy(); + expect(listedReceipt?.governanceStatus).toBe("approved"); + expect(listedReceipt?.sensitivityLevel).toBe("confidential"); + expect(listedReceipt?.attachmentType).toBe("receipt"); + expect(listedReceipt?.downloadPolicy).toBe("reason_required"); + const receiptDownloadWithoutReason = await finance.api.get(`/api/payment-batches/${batch.id}/receipt-attachments/${receipt.id}/file`); + expect(receiptDownloadWithoutReason.status(), "bank receipt download without reason should be rejected").toBe(400); + const receiptDownloadReason = `${issueId} live payment receipt verification`; + const receiptDownload = await finance.api.get( + `/api/payment-batches/${batch.id}/receipt-attachments/${receipt.id}/file?reason=${encodeURIComponent(receiptDownloadReason)}`, + ); expect(receiptDownload.status(), "bank receipt download should succeed").toBe(200); batch = (await apiPostData<{ item: PaymentBatch }>(finance.api, `/api/payment-batches/${batch.id}/execute`, {})).item; @@ -533,6 +547,14 @@ test.describe("公网付款批次受控验收", () => { await expectAuditAction(admin.api, "PAYMENT_BATCH_RECEIPT_UPLOADED", (item) => item.targetId === batch.id && Array.isArray(item.extra?.attachmentIds), ); + await expectAuditAction(admin.api, "PAYMENT_BATCH_RECEIPT_DOWNLOADED", (item) => + item.targetId === batch.id && + item.extra?.attachmentId === receipt.id && + item.extra?.attachmentType === "receipt" && + item.extra?.sensitivityLevel === "confidential" && + item.extra?.governanceStatus === "approved" && + item.extra?.reason === receiptDownloadReason, + ); await expectAuditAction(admin.api, "PAYMENT_BATCH_EXECUTED", (item) => item.targetId === batch.id); await expectAuditAction(admin.api, "REIMBURSEMENT_PAYMENT_MARKED", (item) => item.targetId === reimbursement.id); await expectAuditAction(admin.api, "EXPORT_AP_OPERATIONS", (item) => item.actorId === finance.me.id); @@ -542,7 +564,14 @@ test.describe("公网付款批次受控验收", () => { batchStatus: batch.status, receiptAttachmentId: receipt.id, missingReceiptExecuteStatus, + receiptDownloadWithoutReasonStatus: receiptDownloadWithoutReason.status(), receiptDownloadStatus: receiptDownload.status(), + receiptGovernance: { + governanceStatus: listedReceipt?.governanceStatus, + sensitivityLevel: listedReceipt?.sensitivityLevel, + attachmentType: listedReceipt?.attachmentType, + downloadPolicy: listedReceipt?.downloadPolicy, + }, reimbursementStatus: paid.status, invoiceStatus: invoice.status, ledgerPaymentStatus: ledgerItem?.payment.status ?? "missing", diff --git a/server/src/routes/reimbursements.js b/server/src/routes/reimbursements.js index f8dc9547..697bb65c 100644 --- a/server/src/routes/reimbursements.js +++ b/server/src/routes/reimbursements.js @@ -2061,6 +2061,16 @@ async function reimbursementRoutes(fastify) { mime_type: true, file_size: true, file_sha256: true, + governance_status: true, + sensitivity_level: true, + attachment_type: true, + reviewed_by: true, + reviewed_at: true, + review_note: true, + archived_by: true, + archived_at: true, + archive_note: true, + download_policy: true, created_at: true, uploader: { select: {