修复附件治理部署检查缺口(05-28-attachment-governance-deploy-smoke)

这个提交包含在:
2026-05-28 20:17:38 +08:00
父节点 fef59661fc
当前提交 33b1232092
共修改 7 个文件,包含 222 行新增和 2 行删除
@@ -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": "核对公网验证手册"}
@@ -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。
@@ -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 和试运行验证手册"}
@@ -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`。
- 本任务当前阶段只做部署前准备;生产部署必须等用户明确回复“批准部署”。
@@ -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": {}
}
@@ -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",
+10
查看文件
@@ -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: {