chore(task): 归档问题盘点、制单与部署体检任务
这个提交包含在:
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,27 @@
|
||||
# 当前问题与功能组合盘点设计
|
||||
|
||||
## 证据层级
|
||||
|
||||
1. 用户现场确认和当前生产只读证据用于判断真实完成状态。
|
||||
2. 当前代码的导航、路由、功能开关和权限用于判断“存在什么能力、谁能看到”。
|
||||
3. `docs/` 业务事实源用于判断模块边界和既有产品口径。
|
||||
4. Trellis task 和历史提交用于追溯实施状态,但未勾选项不能单独证明生产未完成。
|
||||
|
||||
## 分类方法
|
||||
|
||||
- **保留并优先验证**:资金、审批、身份、审计、备份或已发生真实使用的核心能力。
|
||||
- **保留但简化**:有业务价值,但小团队不需要复杂配置或专业系统深度的能力。
|
||||
- **默认隐藏**:低频、可替代或尚无真实使用证据,不应占用日常导航。
|
||||
- **冻结开发**:保留现状,不追加功能,等待真实需求和数据证明。
|
||||
- **退役前调研**:方向已不符合当前路线,但删除前必须确认生产数据、审计引用、迁移和回滚边界。
|
||||
|
||||
## 安全与变更边界
|
||||
|
||||
- 盘点仅写当前任务目录,不修改业务事实源、功能开关、导航、权限、schema 或生产数据。
|
||||
- 不执行需要生产凭据的查询;既有现场事实仅以脱敏结论引用。
|
||||
- 任何实际隐藏、停用或退役都必须另建 Trellis task,先做生产只读数据和依赖审计,再获得明确批准。
|
||||
|
||||
## 输出
|
||||
|
||||
- `research/portfolio-review.md`:完整问题清单、模块矩阵、优先级和下一任务建议。
|
||||
- 聊天交接摘要:可直接复制到新对话,不依赖当前聊天历史。
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,56 @@
|
||||
# 当前问题与功能组合盘点
|
||||
|
||||
## Goal
|
||||
|
||||
为下一个开发对话提供一份可直接使用的交接摘要:准确说明当前已完成能力、未完成问题、生产人工流程进度,并对现有业务模块给出“保留 / 简化 / 隐藏 / 冻结 / 退役调研”建议,避免不到 10 人的内部 OA 继续扩张低价值功能。
|
||||
|
||||
## Background
|
||||
|
||||
- PC 企业微信扫码登录已完成实现、生产部署、真实扫码、审计和回滚验收。
|
||||
- 真实差旅报销已由员工提交,后续主管、财务、付款和复核进度不得靠 Trellis 勾选状态猜测。
|
||||
- 用户已明确反馈 PC 登录卡片过窄,视觉上像手机页。
|
||||
- 用户关注现有“团队、周报等”功能是否真正需要,希望对全部模块做一次价值盘点。
|
||||
- 本任务以只读代码、配置、文档和已有生产证据为主,不因盘点结论直接删除数据或修改生产开关。
|
||||
|
||||
## Requirements
|
||||
|
||||
- R1:列出当前问题,区分真实业务阻断、生产待人工验收、已知技术债、产品体验问题和不紧急待办。
|
||||
- R2:盘点用户可见主导航、备用导航、管理入口、移动页和功能开关,不只按文档名称猜测。
|
||||
- R3:每个模块至少按实际使用人、使用频率、失败成本、无替代性、数据/审计价值和维护成本做判断。
|
||||
- R4:给出“保留并优先验证 / 保留但简化 / 默认隐藏 / 冻结开发 / 退役前调研”分类及理由。
|
||||
- R5:明确团队、周报、考勤/在岗、公告、待办、项目、采购、合同、资产、薪资、绩效、库存盘点、企微归档等已有能力的去留建议,并检查是否还有漏项。
|
||||
- R6:输出一份可直接粘贴到新对话的交接摘要,包含已完成、当前问题、推荐顺序、不可越过的生产边界和下一对话建议首个任务。
|
||||
|
||||
## Scope
|
||||
|
||||
- 只读检查客户端主导航、备用导航、管理入口、移动入口和直达路由。
|
||||
- 只读检查服务端导航注册表、工作区显隐、功能开关、权限门禁和已有业务事实源。
|
||||
- 将结论写入当前 Trellis task 的研究材料,并在聊天中提供独立交接摘要。
|
||||
- 不启动任务实施阶段;本任务没有业务代码、生产配置或数据写入。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] 问题清单能区分“已解决但待记录”与“真正未完成”,不重复打开已闭环任务。
|
||||
- [x] 功能清单与当前导航、功能开关、后端路由和业务文档一致,每项有证据而非仅凭名称。
|
||||
- [x] 团队、周报及其他低频模块有明确的保留/隐藏/冻结/退役建议和取舍。
|
||||
- [x] 不将未授权的生产查询、开关修改、数据删除、依赖升级或功能退役当作盘点必要步骤。
|
||||
- [x] 交接摘要可脱离当前聊天独立理解,并给出下一对话可直接执行的推荐首任务。
|
||||
|
||||
## Planning Result
|
||||
|
||||
- 详细证据和建议见 `research/portfolio-review.md`。
|
||||
- 总体策略:没有真实使用证据的非核心模块默认隐藏并冻结开发;保留代码和数据,未经单独调研不直接退役。
|
||||
- 本轮不进入实施,不修改生产功能开关或导航。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 本轮不删除功能、数据表、历史文档或业务数据。
|
||||
- 本轮不修改生产功能开关、导航、角色、审批规则或组织数据。
|
||||
- 本轮不直接修复 PC 登录页桌面布局或服务端依赖告警;只记录、排序并给出后续任务建议。
|
||||
|
||||
## Test Plan
|
||||
|
||||
- 对照客户端导航注册、React 路由、服务端导航注册、功能开关和权限要求,检查模块清单是否漏项。
|
||||
- 对照管理员手册、业务规约、当前 Trellis tasks 和最近归档提交,区分已完成事项与未完成事项。
|
||||
- 运行 `task.py validate`、Trellis governance 和 `git diff --check`。
|
||||
- 检查研究材料不含密码、Secret、完整账号、真实票据、银行资料、完整本地路径或原始 payload。
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
# 当前问题与功能组合盘点
|
||||
|
||||
## 1. 判断口径
|
||||
|
||||
本次盘点只依据当前代码入口、功能开关、权限边界、业务事实源和用户现场反馈,不以历史任务未勾选项单独推断生产状态。
|
||||
|
||||
对不到 10 人的内部 OA,功能保留优先级依次看:是否承载真实资金或责任链、是否需要审计留痕、是否没有更低成本替代、是否已有真实使用、失败是否会影响业务。存在代码但没有真实使用价值的模块,不立即删除数据或路由,先隐藏入口并冻结开发。
|
||||
|
||||
## 2. 已完成且不要重复开启
|
||||
|
||||
- 应用健康监控已建立独立站外探测和企业微信告警,不再是“应用零监控”。
|
||||
- 自动异机备份曾因附件宿主挂载漂移失败,严格守卫成功阻断错误备份;unit 已刷新,补跑成功并收到恢复通知。
|
||||
- 企业微信成员可预绑定或按唯一企业邮箱补绑定,不再要求首次输入 OA 密码后手工绑定。
|
||||
- 企业微信工作台免登和普通 PC 浏览器企业微信扫码登录均已部署并经真实用户验证。
|
||||
- 腾讯通用票据 OCR 已切到真实接口,住宿电子普通发票和铁路电子客票均完成真实样票验收;不再使用 mock。
|
||||
- 差旅报销指南、火车票/住宿支付截图、伙食补助 100 元/天、市内交通补助 80 元/天及包干口径已经落文档和费用类型。
|
||||
- PC 报销补充附件 1–5 个、中文文件名附件打开失败已经修复和部署;本次真实报销因员工改用手机提交,PC 现场复验顺延到下次。
|
||||
- CEO、系统管理员和财务职责已纠正:公司负责人不是系统管理员的业务上级;负责人本人报销由非本人财务兜底,不允许自审。
|
||||
|
||||
## 3. 当前真正存在的问题
|
||||
|
||||
### P0:先完成真实业务闭环
|
||||
|
||||
1. 首笔真实差旅报销已经由员工用手机提交,但主管审批、财务复核、付款批次、银行回单和最终复核的现场进度尚未确认。下一步不是继续开发报销新能力,而是让真实处理人按职责走完并逐步反馈。
|
||||
2. PC 多附件与中文文件名修复虽已部署,但尚无真实员工在 PC 上复验;不阻塞当前手机单据,列入下一笔报销的验收清单。
|
||||
|
||||
### P1:近期应单独建任务处理
|
||||
|
||||
1. PC 登录页卡片宽度约 460px,在桌面上仍像手机页面。扫码能力已正常,问题是桌面登录布局,不是认证链路。
|
||||
2. OCR 已能识别金额,但系统尚未把“明细金额大于全部已绑定有效发票金额”形成可持久化、财务可见的警告;当前仍靠员工说明和财务人工核对。
|
||||
3. 差旅补助仍由员工手写“天数 × 单价”,系统没有结构化天数字段,也不能按日标准计算。现有费用标准只按单条金额比较,直接录 100/80 元会误报多日补助,不能用作临时替代。
|
||||
4. 腾讯 OCR 免费额度耗尽或云账户异常缺少主动运维提示。后付费已关闭,因此主要风险是 OCR 静默不可用,而不是失控扣费。优先确认腾讯控制台可用的额度/费用告警;确有 API 可安全读取时再考虑 OA timer,避免为一个低频依赖自建复杂监控。
|
||||
5. 全员企微免登虽已有可用机制,但仍应逐人记录一次真实登录;账号密码回退、忘记密码“新验证码使旧验证码失效”的提示,以及邮箱验证码登录后首次改密仍要求旧密码的问题尚未整体收口。
|
||||
|
||||
### P2:技术债和治理项
|
||||
|
||||
1. 服务端生产依赖审计存在 16 个存量高危、0 个严重;客户端没有高危/严重。需单独做兼容性和回归充分的依赖治理,不能在业务任务里顺手升级。
|
||||
2. 部署后自动备份/WARCH3 等 systemd unit 仍依赖部署收尾时显式刷新或核对。检查清单已有门禁,但尚不是完全自动的部署步骤,仍有再次漂移的操作风险。
|
||||
3. 新员工账号启用还不是完整产品流程:通讯录资料、OA 账户、唯一 binding、主管、角色、首次免登、密码回退和审计应形成一套可重复门禁。
|
||||
4. 文档和 QA 治理规模偏重,历史上仍有未登记文档 warning。只在不削弱资金、权限、备份和认证门禁的前提下逐步裁剪。
|
||||
|
||||
## 4. 功能组合建议
|
||||
|
||||
| 能力 | 当前产品位置/边界 | 建议 | 理由 |
|
||||
| --- | --- | --- | --- |
|
||||
| 工作台、通知 | 主入口 | 保留并简化 | 只聚合本人待办、报销进度和少量异常,不做大而全门户。 |
|
||||
| 待我处理/统一审批 | 主导航,按审批角色显示 | 保留并优先验证 | 资金和责任链核心;与普通“待办”不是同一类能力。 |
|
||||
| 报销、发票、OCR | 主导航/财务入口 | 保留并优先验证 | 已有真实业务,是当前第一主线。 |
|
||||
| 付款批次、应付账款、银行回单 | 仅财务/复核角色 | 保留并优先验证 | 构成财务闭环和 maker-checker,不向普通员工开放。 |
|
||||
| 审计日志、备份、健康监控 | 管理/运维 | 保留 | 高失败成本、不可替代;继续低频后台化。 |
|
||||
| 账号、角色、组织、直属主管 | 管理入口 | 保留并简化 | 是所有审批和免登的基础;避免重复的“人员/账号/部门/组织”日常入口。 |
|
||||
| 主数据、审批规则、费用标准、报销模板 | 管理入口 | 保留 | 直接支撑报销和审批;只由管理员按需维护。 |
|
||||
| 采购 | 主导航,按权限显示 | 保留但按需使用 | 有金额、附件和审批留痕价值;真实采购发生时使用,不继续做复杂招采。 |
|
||||
| 合同台账 | 主导航,受项目权限限制 | 保留但简化 | 保存关键字段、到期提醒和附件关联;不建设电子签章或完整合同系统。 |
|
||||
| 项目 | 主导航,feature flag 控制 | 保留但简化 | 只做项目主档、关键状态和报销/采购/合同关联,不替代专业项目管理工具。 |
|
||||
| 资产台账 | 主导航,按资产权限显示 | 保留但低频 | 对电脑、账号、电子资料和离职交接有审计价值;不做重型资产系统。 |
|
||||
| 资产盘点 | “更多”/管理员 | 默认隐藏,按季度启用 | 有资产台账后才有价值,不应占日常导航。 |
|
||||
| 企业微信审批历史、统一考勤摘要 | “更多”,只读留存 | 保留但后台化 | 企业微信主办,OA 做历史、审计和兜底;不重造企微打卡。 |
|
||||
| 请假记录 | 当前主导航 | 建议降到“更多”或个人页 | 日常申请在企业微信完成,OA 只需历史和关联,不应和报销并列为 OA 主办入口。 |
|
||||
| 团队状态 | 当前主导航候选,presence flag | 默认隐藏、冻结开发 | 10 人以内可由企微状态/群沟通替代;只有负责人明确每天使用时再打开。 |
|
||||
| 周报 | 当前“更多”,weekly flag | 默认隐藏、冻结开发 | 若没有固定周会和阅读责任人,容易变成重复填报;项目关键进展可直接记在项目或周会纪要。 |
|
||||
| 普通待办板 | 有路由和 flag,但默认隐藏 | 继续隐藏,退役前调研 | 与审批待办、企微消息和项目事项重复;先确认无真实数据,再考虑退役。 |
|
||||
| 公告 | 当前“更多”,announcement flag | 默认隐藏,必要时临时启用 | 少人数用企微群即可;只有制度发布、签收留痕或长期可检索通知才值得保留。 |
|
||||
| 数据看板 | “更多”候选,默认隐藏 | 冻结开发 | 当前数据量太小,统计容易产生伪价值;积累 2–3 个月真实数据后再评估。 |
|
||||
| 完整差旅申请/统付/对账 | “更多”候选 | 冻结开发 | 当前差旅可由事前口头/简化审批加报销完成,不继续扩成旅行平台。 |
|
||||
| 预算管理/预算硬控 | “更多”候选 | 冻结开发 | 公司当前预算池和成本中心尚未形成稳定使用;报销/采购审批先满足控制需要。 |
|
||||
| 薪资 | 有受限直达路由,无正式导航 | 默认隐藏,退役前调研 | 高敏、低频且通常由财务/代账处理;没有明确内部发薪流程和真实数据就不应启用。 |
|
||||
| 绩效 | 有受限直达路由,无正式导航 | 默认隐藏,退役前调研 | 10 人以下更适合负责人直接反馈;无固定绩效周期时维护成本大于价值。 |
|
||||
| 人员生命周期 | 管理页内能力 | 保留最小版 | 保留入职、在职、离职、主管和企微映射;冻结复杂 HR 套件。 |
|
||||
| 流程定义编辑器 | 管理入口但 flag 默认关闭 | 冻结 | 固定流程更适合当前团队,避免把审批引擎变成管理员负担。 |
|
||||
| ERP、AI 助手、切换控制台、OA 原生打卡 | 代码/历史入口,默认关闭 | 退役前调研 | ERP 已明确不接;AI 无核心场景;切换控制台属于迁移期工具;OA 打卡与企微重复。确认无生产数据和审计依赖后再分批退役。 |
|
||||
| 全局搜索、代理委托、涉密引用 | 辅助/直达能力 | 保留代码、默认隐藏 | 数据量或实际场景出现后再开启,不进入近期开发主线。 |
|
||||
|
||||
## 5. 推荐执行顺序
|
||||
|
||||
1. 让首笔真实报销走完主管、财务、付款和复核,遇到阻断立即修真实 bug。
|
||||
2. 单独修 PC 登录页桌面布局,不改已经通过的企微扫码认证链路。
|
||||
3. 做全员免登和账号回退验收,形成新员工启用清单。
|
||||
4. 为 OCR 金额差异增加可持久化、员工和财务都可见的 warn;不先做 block。
|
||||
5. 增加结构化补助天数和按日标准计算。
|
||||
6. 处理腾讯 OCR 额度/故障告警和服务端高危依赖。
|
||||
7. 另建“导航瘦身”任务,把默认隐藏策略真正落到生产;先隐藏,不删除代码或数据。
|
||||
|
||||
## 6. 下一对话建议首任务
|
||||
|
||||
如果真实报销链没有出现阻断,首个开发任务建议为“PC 登录页桌面布局优化”。它边界清楚、用户已明确反馈、与已验证认证链解耦。若真实报销链任一步出现阻断,则应暂停布局任务,优先修复该真实业务阻断。
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"id": "current-issues-feature-portfolio-review",
|
||||
"name": "current-issues-feature-portfolio-review",
|
||||
"title": "当前问题与功能组合盘点",
|
||||
"description": "只读盘点当前问题、导航、功能开关与模块价值,形成小团队功能去留建议和新对话交接摘要。",
|
||||
"status": "completed",
|
||||
"dev_type": "docs",
|
||||
"scope": "只读审查客户端导航与路由、服务端功能开关与权限、现有业务事实源和当前 Trellis 状态;仅写当前任务规划与研究材料。",
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "shunli",
|
||||
"assignee": "shunli",
|
||||
"createdAt": "2026-07-30",
|
||||
"completedAt": "2026-08-03",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [
|
||||
"client/src/features/navigation/schema.ts",
|
||||
"client/src/components/navigation/NavLinks.tsx",
|
||||
"client/src/router/index.tsx",
|
||||
"server/src/services/workspaceSettings.js",
|
||||
"server/src/services/featureFlags/index.js",
|
||||
"docs/管理员手册.md"
|
||||
],
|
||||
"notes": "只读问题与功能组合盘点已完成并形成后续执行顺序;本任务未授权也未实施业务代码、生产配置或数据写入,当前整理以新的真实试用与任务收口决策为准。",
|
||||
"meta": {}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
# 生产密钥恢复与环境文件外置:设计
|
||||
|
||||
## 1. 设计目标
|
||||
|
||||
本任务解决两个相互关联但必须分阶段交付的问题:
|
||||
|
||||
1. 主机或环境文件丢失时,人知道每个配置键如何恢复、代价是什么、由谁完成。
|
||||
2. 生产环境文件不再位于源码同步树内,任何 `rsync --delete` 都无法物理触及它。
|
||||
|
||||
Phase 1 只建立恢复合同;Phase 2 才改变生产运行路径。数据库/附件备份继续保持现有边界,不吸收明文 Secret。
|
||||
|
||||
## 2. Phase 1:机器可校验的恢复分类
|
||||
|
||||
### 2.1 分类清单
|
||||
|
||||
新增一个位于 `deploy/trialrun/` 的 JSON 合同文件,按类别保存 `.env.example` 键名数组:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"categories": {
|
||||
"template_rebuild": [],
|
||||
"external_reissue": [],
|
||||
"direct_rotate": [],
|
||||
"downtime_reset": [],
|
||||
"preserve_original": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 文件只允许大写环境键名和固定类别,不含值、默认值副本、长度、哈希、账号或外部平台地址。
|
||||
- `validate-env-contract.js` 解析 `.env.example` 和分类文件,断言全集相等、无重复、无未知类别、无多余键。
|
||||
- 普通非敏感配置也必须进入 `template_rebuild`,避免清单只覆盖看起来像 Secret 的少数键。
|
||||
- `FIELD_ENCRYPTION_KEY` 固定归入 `preserve_original`;`POSTGRES_PASSWORD`/`DATABASE_URL` 的关系在恢复卡中说明,数据库角色口令本身归入 `downtime_reset`。
|
||||
|
||||
### 2.2 恢复卡
|
||||
|
||||
更新现有 `deploy/trialrun/runbook.md` 紧急恢复卡,不新建第二份 runbook:
|
||||
|
||||
- 每类给出丢失影响、责任角色、恢复来源、停止条件和恢复后验证。
|
||||
- 数据库流程只描述受控“重置”:停止应用写入与 DB 容器、备份认证配置、用无公网暴露的隔离启动临时调整认证、修改角色口令、恢复原认证配置、重启并执行 migration status/ready/runtime smoke。任何一步失败都保持入口关闭。
|
||||
- `preserve_original` 明确原值丢失会导致既有密文永久不可读;不得用新随机值冒充恢复成功。
|
||||
- 共享密码库由系统管理员和公司负责人双人可取。仓库只记录角色与季度取用演练结果,不记录产品地址、账号或恢复码。
|
||||
|
||||
### 2.3 Phase 1 验证
|
||||
|
||||
- 单元测试覆盖完整分类、重复键、未知键、漏键、非法类别和分类文件出现疑似值。
|
||||
- 环境合同继续检查 config/example/Compose;分类变化纳入同一个失败出口。
|
||||
- 敏感扫描证明新增文件只有键名与类别。
|
||||
- Phase 1 不通过 SSH、数据库、Compose 或 systemd 访问生产。
|
||||
|
||||
## 3. Phase 2:生产环境文件外置
|
||||
|
||||
### 3.1 运行合同
|
||||
|
||||
- `REMOTE_ENV_FILE` 成为唯一运行事实源,必须是同步树外的安全绝对路径、普通文件、root 所有、`0600`。
|
||||
- Compose 的四个 `env_file` 从字面量 `./.env` 改为必须显式提供的 `${REMOTE_ENV_FILE}`;同一变量也继续传给 Compose `--env-file` 负责插值。
|
||||
- `deploy.sh`、`deploy-existing.sh`、远端 preflight、自动备份/WARCH3 安装器、备份 runner、巡检和收口脚本统一接收该变量,不保留 `${REMOTE_DIR}/deploy/trialrun/.env` 的生产默认值。
|
||||
- 本地开发可显式使用仓库外的测试 env;validator 的否定合同针对生产 Compose/部署默认与路径嵌套,不因为开发者工作区存在 gitignored 测试文件而误报。
|
||||
|
||||
### 3.2 负向防护
|
||||
|
||||
- `deploy.sh` 在 provenance、mkdir、rsync 之前拒绝 `REMOTE_ENV_FILE == REMOTE_DIR` 或任一方向嵌套。
|
||||
- `preflight-remote.sh` 验证 env owner/mode/非 symlink,并确认路径不在 `REMOTE_DIR` 下。
|
||||
- `validate-env-contract.js` 拒绝 Compose 再出现 `env_file: ./.env`、部署脚本重新默认到同步树或 runbook 把生产事实源写回仓库目录。
|
||||
- `rsync` 的 `.env` exclude 可以作为纵深防御保留,但不再承担安全正确性。
|
||||
|
||||
### 3.3 受控迁移状态机
|
||||
|
||||
1. **Prepare**:只读冻结活动 Compose、env 路径、4 个 service、PGDATA/storage 和构建身份;检查共享密钥封存完成状态,不读取值。
|
||||
2. **Copy**:在主机上以 root 原子复制当前 env 到外置临时文件,核对键集合、owner/mode/非 symlink 后原子改名;不在日志输出 diff 或值。
|
||||
3. **Switch**:重装 4 个 units,使用新 env 完成 Compose config、新 init 配置预检并切换应用。
|
||||
4. **Verify**:migration status、`/ready`、external/runtime smoke、普通登录、企微登录/入口、automatic backup daily/watchdog 和 timer 身份均通过。
|
||||
5. **Retire**:在同一批准窗口内按精确旧路径删除旧副本;随后验证旧路径不存在、容器与 units 只引用新路径。
|
||||
|
||||
任何 Verify 失败都在旧副本尚存时切回旧合同并重新安装 units;禁止在验证完成前删除旧副本。Retire 后若需回滚,只能使用外置事实源,不能把 Secret 搬回同步树。
|
||||
|
||||
## 4. 密钥版本化的延期边界
|
||||
|
||||
当前 `v1:<iv>:<tag>:<ciphertext>` 没有 key id,单一 `FIELD_ENCRYPTION_KEY` 无法支持安全轮换。本任务只保证原值可恢复,不修改加密服务、数据库密文或敏感字段 API。
|
||||
|
||||
后续独立安全任务至少需要:新格式版本与 key id、keyring 配置合同、当前写入键、旧键只读、惰性与批量重加密、覆盖银行账号/身份证号/通知渠道密文的枚举与计数、轮换回滚和旧键删除门禁。
|
||||
|
||||
## 5. 风险与回滚
|
||||
|
||||
- 最大风险是两份 env 共存期间扩大 Secret 暴露面,因此窗口必须短、两份均 root-only,完成后有否定删除验证。
|
||||
- Compose 插值与 service `env_file` 是两个用途,必须同时验证,不能只改命令行 `--env-file`。
|
||||
- 数据库口令重置步骤属于事故恢复,不在常规部署或自动测试中执行;文档不得弱化停机和隔离要求。
|
||||
- 具体密码管理器和成员授权是仓库外 EXTERNAL_GATED 动作,缺少双人取用确认时 Phase 2 不得开始。
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
# 生产密钥恢复与环境文件外置:实施计划
|
||||
|
||||
## 0. 边界
|
||||
|
||||
- [x] 本任务按 Phase 1、Phase 2 分开批准、提交和验收;Phase 1 批准不授权生产访问或迁移。
|
||||
- [x] 不读取、打印、哈希或提交生产 Secret;只处理键名、类别、布尔状态和计数。
|
||||
- [x] `FIELD_ENCRYPTION_KEY` 版本化/keyring/重加密登记为后续任务,不在本轮顺手实现。
|
||||
|
||||
## 1. Phase 1:恢复卡与分类合同
|
||||
|
||||
- [x] 从 `.env.example` 自动提取当前键集合,逐项归入 5 个固定类别;对 `FIELD_ENCRYPTION_KEY`、数据库口令、JWT/pepper、外部平台凭据做人工复核。
|
||||
- [x] 新增无值的 JSON 分类合同,并扩展 `validate-env-contract.js`:全集相等、恰好一次、类别固定、键名格式正确、内容不含疑似赋值/Secret 正文。
|
||||
- [x] 更新 `deploy/trialrun/runbook.md` 顶部恢复卡,写清 5 类影响、责任人与恢复后验证;数据库为隔离停机重置,不写成明文找回。
|
||||
- [x] 更新 `.trellis/spec/hyb-oa/deployment-trialrun.md`,固化环境恢复分类、双人托管、`preserve_original` 最高风险和 keyring 延期边界。
|
||||
- [x] 增加聚焦单元测试:正常 164-key 合同、漏键、重复键、多余键、未知类别、非法键名/疑似值、`FIELD_ENCRYPTION_KEY` 与 `POSTGRES_PASSWORD` 错误分类。
|
||||
- [x] 运行环境合同、聚焦测试、server 全量测试、JS 语法、task/governance/process/docs/sensitive/diff 门禁。
|
||||
- [x] Phase 1 单独提交;证据明确“未访问或修改生产”,不把文档完成冒充密钥已封存。
|
||||
|
||||
## 5. Phase 1 验证证据(2026-08-01)
|
||||
|
||||
- 环境合同:PASS,`.env.example` 164 个键,恢复分类 164 个键,恰好覆盖一次。
|
||||
- Prisma 与语法:`prisma validate`、validator/test `node --check` 均 PASS。
|
||||
- 服务端全量:173 个测试文件、1144 个测试 PASS;其中恢复分类聚焦用例 8/8 PASS,既有部署安全用例 7/7 PASS。
|
||||
- 治理门禁:task validate、Trellis governance、process contract、sensitive-content scan、`git diff --check` 均 PASS;docs registry audit 为 0 error、1 个既有汇总 warning(22 个未登记 docs 文件)。
|
||||
- 人工复核:五类语义、双人托管、PostgreSQL 隔离停机重置与 `FIELD_ENCRYPTION_KEY` 原值保留边界一致;清单仅含键名和类别。
|
||||
- 操作边界:本阶段未连接、读取或修改生产;未创建密码库条目,未迁移 env,未修改 Compose/systemd,未重启服务,也未删除任何生产文件。
|
||||
|
||||
## 2. Phase 2:代码与脚本准备(仍不自动迁移)
|
||||
|
||||
- [ ] Compose 四个 service 使用显式 `${REMOTE_ENV_FILE}`;部署、备份、WARCH3、巡检和 QA 脚本移除同步树内生产默认。
|
||||
- [ ] `deploy.sh`、preflight 和环境合同拒绝 env 与同步树重叠,校验 root owner、`0600`、普通非 symlink 文件;保留 rsync exclude 仅作纵深防御。
|
||||
- [ ] 更新 runbook、checklist、管理员手册和 OPS7 中所有生产路径示例;本地示例明确使用非生产测试 env。
|
||||
- [ ] 扩展合成回归,覆盖旧路径拒绝、新路径通过、Compose v1/v2 config、4 个 unit 参数一致、rsync 前失败和旧副本否定检查。
|
||||
- [ ] 运行 Docker Compose config、部署合同、备份合同、全量测试与 Trellis 门禁;单独提交但不连接生产。
|
||||
|
||||
## 3. Phase 2:生产迁移(另行明确批准)
|
||||
|
||||
- [ ] 只读冻结活动 project、构建、当前 env、PGDATA/storage、4 个 service/timer;确认系统管理员与公司负责人均可取用 `preserve_original` 封存项,只记录完成状态。
|
||||
- [ ] root 原子复制到同步树外目标,校验键集合、owner/mode/非 symlink;不输出值或文件内容。
|
||||
- [ ] 重装 4 个 units,运行 Compose config、新 init 配置预检并切换 server;未获批准不启用 worker/minio profile。
|
||||
- [ ] 验证 migration status、`/ready`、external/runtime smoke、普通登录、企微入口、automatic backup daily/watchdog、4 个 timer 和构建身份。
|
||||
- [ ] 验收全部通过后按精确旧路径删除旧副本;再次证明旧路径不存在,活动容器和 units 只引用新路径。
|
||||
- [ ] 任一验证失败时,在旧副本删除前回滚 unit/Compose 引用;记录失败尝试,不美化为一次成功。
|
||||
|
||||
## 4. 后续独立任务
|
||||
|
||||
- [ ] 建立 `FIELD_ENCRYPTION_KEY` 版本化与安全轮换任务:带 key id 的新密文格式、keyring、惰性/批量重加密、全字段覆盖、审计与旧键删除门禁。
|
||||
- [ ] 约 18 个孤儿 E2E 继续作为独立测试治理任务,不与密钥恢复混做。
|
||||
@@ -0,0 +1,77 @@
|
||||
# 生产密钥恢复与环境文件外置
|
||||
|
||||
## Goal
|
||||
|
||||
在不把生产 Secret 写入仓库、普通备份、日志或聊天的前提下,为 OA 建立可执行的环境配置恢复合同,并把生产环境文件迁出源码同步树,避免 rsync 误删、单机损坏或单管理员缺席导致系统无法恢复。
|
||||
|
||||
## Background
|
||||
|
||||
- `5dac336` 首次发布误把 Compose 配置目录当作仓库根目录,`rsync --delete` 删除了远端 `.env`;本次仅因旧 server 容器仍在运行,才能按允许键恢复配置。
|
||||
- 当前数据库和附件已有异机备份,环境文件及密钥没有独立恢复合同。主机整体丢失时,仅恢复数据库与附件不足以启动 OA。
|
||||
- `07de198` 已把仓库根、远端 env、PGDATA 和 storage 的检查前移到 rsync 之前,封住最可能复发路径,但没有解决整机丢失、密钥托管或环境文件仍位于同步树内的问题。
|
||||
- 当前 `.env.example` 合同含 164 个键;生产事故恢复曾观察到 163 个可恢复键。第一阶段只按键名和用途分类,不读取、输出或提交生产值;第二阶段再只读解释生产键集合差异。
|
||||
- 仓库中不只有 4 个 systemd service 引用 `REMOTE_ENV_FILE`:Compose、部署/备份入口、自动备份安装器、日常巡检脚本、若干 QA 收口脚本及 runbook/checklist 都存在当前路径假设,迁移必须统一收敛。
|
||||
- `FIELD_ENCRYPTION_KEY` 已用于银行账号、身份证号和通知渠道 Secret 等数据库密文。原值丢失后不能从数据库反推;直接换新值会使既有密文不可读,除非持有旧值完成受控重加密。
|
||||
|
||||
## Scope
|
||||
|
||||
- 当前获批并执行的范围仅为 Phase 1:脱敏恢复分类、现有恢复卡、环境合同校验、聚焦测试与稳定规约。
|
||||
- Phase 2 的代码合同、生产 env 外置及旧副本删除仍保留在本任务中规划,但必须分别取得后续明确批准后才可实施。
|
||||
- 本轮不访问生产,不读取或托管 Secret,不修改 Compose/systemd,不重启服务,也不执行任何生产删除。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Phase 1:恢复合同与分类(不碰生产)
|
||||
|
||||
- 在现有 `deploy/trialrun/runbook.md` 的紧急恢复卡中增加环境配置恢复,不另建重复操作手册。
|
||||
- 用机器可校验的脱敏清单覆盖 `.env.example` 中每一个键;清单只包含键名和恢复类别,不含值、长度、哈希或原始 payload;各类别的影响和责任边界统一写入现有恢复卡。
|
||||
- 至少支持用户已确认的四类:
|
||||
1. `template_rebuild`:端口、路径、开关、TTL 等,从 `.env.example` 与 runbook 重建。
|
||||
2. `external_reissue`:企微、Webhook、腾讯云、SMTP、OIDC、S3/AI/ERP 等外部凭据,从对应平台重新签发或获取。
|
||||
3. `direct_rotate`:JWT、验证码 pepper、试运行管理员口令等可直接轮换项;必须写明会话失效、验证码失效或账号重置等代价。
|
||||
4. `downtime_reset`:数据库口令等不能反推明文、但可在保留 PGDATA 时通过隔离停机操作重置的项目。恢复卡必须明确这是“重置”而非“找回”,先备份认证配置、停止应用写入、隔离数据库、临时调整认证、修改角色口令、恢复认证配置并做完整 smoke;不得让数据库临时暴露公网。
|
||||
- 第 5 类 `preserve_original` 为最高风险:`FIELD_ENCRYPTION_KEY` 等必须保存原值的密钥,只能从受控共享密码库/加密封存副本恢复;没有旧值时不得宣称轮换即可恢复,必须如实说明既有银行账号、身份证号和通知渠道密文将不可读并需重新收集/配置。
|
||||
- 密钥封存不得只由单一管理员持有。共享密码库或加密封存副本至少由系统管理员和公司负责人各自可取;仓库只记录责任角色、复核周期和恢复验证,不记录产品库地址、账号、恢复码或密钥正文。
|
||||
- 第一阶段限制为文档、脱敏分类清单、静态校验和测试,目标半天内完成;不连接生产、不创建密码库条目、不迁移 env、不重启服务。
|
||||
|
||||
### Phase 2:环境文件外置(另行生产批准)
|
||||
|
||||
- 生产 `REMOTE_ENV_FILE` 迁移到源码同步树外的 root-only 绝对路径,目标权限为 root 所有、`0600`、普通非 symlink 文件。
|
||||
- Compose 的 `server`、`worker`、`init`、`minio_init`,部署入口、备份/巡检脚本和 4 个运维 service 必须统一使用显式 `REMOTE_ENV_FILE`,不再依赖 `./.env`。
|
||||
- `deploy.sh` 对 existing 生产部署拒绝 `REMOTE_ENV_FILE` 与 `REMOTE_DIR` 相同或互相嵌套;`validate-env-contract.js` 负向断言生产 Compose/部署合同不得把 env 放回同步树。不能仅依赖 rsync exclude。
|
||||
- 迁移使用受控短暂停机/切换窗口:从当前事实源原子复制到外置路径,验证 owner/mode/非 symlink 与键集合,更新全部 units,完成 Compose config、新 init 配置预检、应用切换、`/ready`、外部依赖、runtime smoke、登录/企微和备份 timer 验证。
|
||||
- 迁移期允许存在新旧两份密钥副本作为明确回退点;验收后必须按精确旧路径删除旧副本,并再次验证旧文件不存在、运行容器与 systemd 均引用新路径。删除属于生产破坏性动作,必须在第二阶段批准窗口内显式执行和记录。
|
||||
- 共享密码库具体产品、成员授权和生产 Secret 写入由用户/公司负责人在仓库外完成;Codex 只验证职责与完成状态,不读取 Secret。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] `.env.example` 当前全部键恰好归入一个获批恢复类别;新增、删除或重复分类会使合同测试失败。
|
||||
- [x] 恢复卡能分别回答模板重建、外部重申请、直接轮换、数据库停机重置和原值保留;每类写明业务影响、操作边界和恢复后验证,`preserve_original` 标为最高风险。
|
||||
- [x] 数据库口令恢复明确使用隔离停机“重置”流程,不声称可从 `pg_authid` 反推明文,不留下 trust 认证或公网暴露。
|
||||
- [x] 密钥托管职责至少覆盖系统管理员与公司负责人两人;仓库、日志和普通数据库/附件备份中不出现密钥正文。
|
||||
- [x] Phase 1 的文档、分类清单、validator 和单元测试通过,且没有生产访问或写入。
|
||||
- [ ] Phase 2 代码合同使生产 env 物理位于同步树外,并负向拒绝同步树内 env;所有 Compose/部署/备份/巡检引用统一。
|
||||
- [ ] 获批生产迁移后,新路径 owner/mode、Compose config、init 配置预检、`/ready`、外部依赖、runtime smoke、登录/企微及 4 个 timer 均通过。
|
||||
- [ ] 旧副本删除后有否定验证,生产运行合同仍指向新路径;可回退点、删除时间和操作人有脱敏审计记录。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 不把明文 `.env` 加入现有数据库/附件备份或 Git。
|
||||
- 不在本任务内采购或替用户选择密码管理器产品。
|
||||
- 不让 Codex、CI、聊天或任务证据读取/保存生产 Secret。
|
||||
- 不借机轮换所有密钥、重加密全部敏感字段或重构配置中心。
|
||||
- 不在本任务实现密钥版本、keyring、惰性重加密或全量重加密;另立安全设计任务后才能改变密文格式。
|
||||
- 不在 Phase 1 迁移生产路径、更新 systemd、重启容器或删除旧 env。
|
||||
|
||||
## Known Limitation / Deferred
|
||||
|
||||
- 当前 AES-256-GCM 密文格式为 `v1:<iv>:<tag>:<ciphertext>`,只有格式版本,没有 key id;服务端配置也只有单一 `FIELD_ENCRYPTION_KEY`,因此无法判断密文由哪把钥匙生成或安全并存新旧钥匙。
|
||||
- `preserve_original` 是当前恢复现实,不是永久设计。若将来怀疑密钥泄露,现有系统无法在保持既有密文可读的同时轮换。
|
||||
- 后续独立任务应设计带 key id 的新密文版本、受控 keyring、写入只用当前钥匙、旧密文读取后的惰性重加密,以及覆盖全部密文字段的可审计批量迁移/回滚;在该设计完成前不得删除旧钥匙。
|
||||
|
||||
## Test Plan
|
||||
|
||||
- 环境与语法:`node deploy/trialrun/validate-env-contract.js`、相关 `node --check`、`cd server && npm run prisma:validate`。
|
||||
- 回归测试:聚焦运行恢复分类与部署安全单元测试,并运行 `cd server && npm test` 全量测试。
|
||||
- Trellis/文档门禁:task validate、Trellis governance、process contract、docs registry audit、sensitive-content scan 与 `git diff --check`。
|
||||
- 人工复核:逐项检查 164 个键的恢复语义、恢复卡的停机/隔离边界,以及新增材料不含生产值、哈希或真实托管位置。
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "production-secret-recovery-env-externalization",
|
||||
"name": "production-secret-recovery-env-externalization",
|
||||
"title": "生产密钥恢复与环境文件外置",
|
||||
"description": "分阶段建立生产环境配置恢复合同,并在另行批准后将生产 env 迁出源码同步树。",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": "Phase 1: 脱敏恢复分类与恢复卡;Phase 2: 生产 env 外置代码合同及另行批准的迁移",
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "shunli",
|
||||
"assignee": "shunli",
|
||||
"createdAt": "2026-08-01",
|
||||
"completedAt": "2026-08-03",
|
||||
"branch": "main",
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "获批的 Phase 1 恢复分类、恢复卡、环境合同校验与规约已交付。Phase 2 生产环境文件外置、旧副本删除和密钥轮换从未获得实施批准,本次明确作为未来独立任务暂缓,不把未执行的生产迁移计入本任务完成声明。",
|
||||
"meta": {}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
# 报销通知与处理进度简化:设计
|
||||
|
||||
## 1. 设计目标
|
||||
|
||||
用最少改动恢复真实待办通知,并让员工看到职责清晰的报销全流程。工作流审批、付款批次和付款执行仍是三个独立状态源;页面只做汇总,不把它们合成一个状态机。
|
||||
|
||||
## 2. 通知只读诊断
|
||||
|
||||
按以下顺序诊断目标财务任务,任何一步都只记录布尔状态、计数、固定结果码和脱敏结论:
|
||||
|
||||
1. 从目标报销的最新工作流实例定位主管已完成任务与当前财务任务,不保存内部 ID 到仓库证据。
|
||||
2. 以当前财务任务为 `target_id` 查询 `workflow.task_assigned` 站内通知,并确认收件人是否为预期财务人员。
|
||||
3. 若通知不存在,回到主管审批后的下一任务创建/收件人解析路径;若存在,继续查询 delivery 状态与固定 `result_code`。
|
||||
4. 只核对收件员工 active、`notification_push_enabled`、企微绑定是否存在,及启用渠道能否匹配 workflow 域和财务角色;不读取 userid、渠道 Secret、消息正文或响应原文。
|
||||
5. 只读结论不足以授权配置修改、补发、重启或生产写入,后续动作单独申请。
|
||||
|
||||
## 3. 审批动作标签
|
||||
|
||||
- `client/src/lib/approvalDisplay.ts` 的 `workflowActionLabel()` 为唯一共享映射。
|
||||
- PC `WorkflowTaskDetailPage.tsx` 和移动 `MobileApprovalsPage.tsx` 删除局部副本并导入共享函数。
|
||||
- 保持未知动作的原值兜底用于诊断,但所有当前受支持动作必须有中文映射;测试覆盖 `start` 与 `submit`。
|
||||
|
||||
## 4. 员工进度表达
|
||||
|
||||
- 后端处理进度阶段键保持兼容,只把普通详情的标签调整为“付款复核”“付款完成”。
|
||||
- `付款完成` 当前节点/待处理说明明确“由财务登记,非审批环节”;已完成时继续显示付款人、时间及回单是否留存。
|
||||
- “查看所在付款批次”只对已有批次读取权限的人显示,普通员工不接触内部批次对象名。
|
||||
- 五阶段继续使用一条竖向进度线,因为它表达全流程进展;文案区分审批与付款,不另加 Tab、折叠或第二张流程图。
|
||||
|
||||
## 5. 付款阶段通知
|
||||
|
||||
本节实施以第 2 节诊断结论为前置门:若原链路是投递配置问题,付款阶段只新增复用既有 dispatcher 的发送点;若原链路是通知创建或 side-effect flush 的代码问题,先修复并用既有主管→财务通知证明公共路径,再让两个付款发送点复用同一修复。禁止在根因未定时并行新增后再混合排障。
|
||||
|
||||
### 5.1 批次创建后
|
||||
|
||||
- 新建批次事务内创建“待付款复核”站内通知,事务提交后走现有 IM dispatcher。
|
||||
- 收件人由 active 员工的真实角色权限计算:必须有 `payment.batch.review` 且角色包含 `manager` 或 `ceo`,并排除批次创建人;不得写死姓名或向所有管理角色泛发。
|
||||
- 通知目标是 `payment_batch`,deeplink 进入现有付款页面的目标批次。
|
||||
|
||||
### 5.2 批次复核后
|
||||
|
||||
- 复核事务内创建“待执行付款”通知,收件人优先为仍 active 且具备 `payment.batch.manage` 的批次创建人;创建人不可用时,才解析其他具备管理权限的 active 财务人员,并排除当前复核人。
|
||||
- 不把付款动作通知给普通申请人;申请人在批次执行成功后继续收到既有 `reimbursement.paid`。
|
||||
|
||||
### 5.3 合同与幂等
|
||||
|
||||
- 新增明确的付款待办通知类型,不复用“报销已付款”或“工作流待审批”伪装语义。当前付款批次只开放报销实体,因此通知映射到既有 `reimbursement` domain,复用现有渠道配置;未来若重新开放其他实体付款,再按实体拆分 domain,不在本轮新建需要生产改配的 `payment` domain。
|
||||
- 通知创建与业务状态变更在同一事务内;投递失败不回滚已完成业务动作,但保留 delivery 失败证据。
|
||||
- 重复创建/复核请求由既有状态锁拒绝,测试证明不会产生重复通知。
|
||||
|
||||
## 6. 兼容与回滚
|
||||
|
||||
- 不修改数据库 schema、历史审批快照、既有通知记录或付款批次状态。
|
||||
- 文案改动可直接回滚;共享映射回滚不会影响服务端事实。
|
||||
- 付款通知若出现收件范围错误,关闭对应通知类型/代码路径即可,不撤销已经创建或复核的付款批次。
|
||||
- 本轮不合并财务复核与组批;累计 5–10 张真实单后的用户访谈是重新立项条件,不是当前发布门禁。
|
||||
|
||||
## 7. 付款入口与正式常开防复发
|
||||
|
||||
### 7.1 已确认根因
|
||||
|
||||
- 当前生产库 `payment_batch_v1` 为版本 1,创建时间等于更新时间,`updated_by` 为空,且没有任何 `FEATURE_FLAG_UPDATED` 审计。
|
||||
- 这证明当前库是在初始化时由 seed 首次创建默认 `force_off`,不是 seed 覆盖已有值,也不是 QA runner 或控制台在当前库执行了关闭。
|
||||
- 当前运行环境同时为 `PAYMENT_BATCH_ENABLED=false`,因此数据库即使改成 `force_on` 也会被 `env_hard_off` 拦截。
|
||||
- 当前唯一 active 财务人员具备 `payment.batch.manage`,但付款入口和已批准候选均因功能门控被隐藏;权限不是根因。
|
||||
|
||||
### 7.2 稳定基线
|
||||
|
||||
- 试运行正式基线把 `PAYMENT_BATCH_ENABLED` 设为 true,并把 seed 中 `payment_batch_v1` 的首次创建默认值设为全局 `force_on`;seed 继续只创建缺失记录,不更新已有记录。
|
||||
- 部署/runtime smoke 明确断言付款批次有效开启,防止换库、换 `.env` 或恢复后再次静默关闭。
|
||||
- QA 短窗口保存进入前完整快照并在 finally 恢复该快照;只有明确的正式启用流程在成功时保留 `force_on`,失败才恢复调用前状态。
|
||||
|
||||
### 7.3 入口规则
|
||||
|
||||
- PC 继续使用现有“付款”导航。
|
||||
- 移动财务工作台在付款能力开启时固定显示财务卡片,不再要求先存在批次或异常;卡片进入受同一付款权限保护的 `/mobile/payment-batches`,复用现有响应式付款页,避免企微小屏把 PC 路径重定向回首页。
|
||||
- 入口只对真实 finance/finance_mgr 且具备付款权限的人员显示;不向普通员工或仅审批人开放创建能力。
|
||||
- 本轮不自动打开创建面板、不自动选择报销、不代替真实财务动作。
|
||||
|
||||
## 8. 付款批次创建与复核闭环
|
||||
|
||||
### 8.1 已确认的状态事实
|
||||
|
||||
- `POST /payment-batches` 已在同一事务内生成 `batch_no`、创建批次与批次项、写创建审计,并创建 `payment_batch.review_requested`;事务提交后立即进入企微投递。
|
||||
- 当前没有 submit API。`draft` 不是“尚未提交”,而是“已经提交付款复核、尚未被非制单人复核”。继续向用户显示“草稿”会制造一个不存在的提交动作。
|
||||
- 不新增状态和接口:数据库继续使用 `draft -> reviewed -> executed/failed`,用户语义改为 `待付款复核 -> 待财务付款 -> 已付款/登记失败`。
|
||||
|
||||
### 8.2 KISS 创建合同
|
||||
|
||||
- 创建页只保留候选搜索、报销选择、已选数量和一个主按钮“创建并提交复核”。
|
||||
- 服务端 `name` 改为可选;未传时按 `APP_TIMEZONE` 生成 `YYYY-MM-DD 付款批次`。现有调用方显式传入名称时继续兼容。
|
||||
- 唯一批次编号继续由服务端生成,不把编号输入暴露给用户;本轮不引入数据库序列表或编号迁移。
|
||||
- 未传付款渠道时默认 `bank`,从而保持银行回单门禁;付款日期不在创建日预填,登记付款完成时未覆盖则按公司时区当天写入。凭证前缀保持可选且默认空。
|
||||
- 既有报销批次若历史渠道为空,API 展示和执行门禁按 `bank` 解释但不回写数据库;因此当前真实批次也会先要求回单,不需要生产数据修正。
|
||||
- 日期、渠道、凭证号的异常覆盖继续留在批次项,不让低频字段占据首次创建主路径。
|
||||
|
||||
### 8.3 当前责任与动作
|
||||
|
||||
- 创建成功反馈:`已提交付款复核`;详情说明:`等待负责人复核,制单人不可复核自己的批次`。
|
||||
- `draft`:复核责任人显示“负责人”;符合 maker-checker 的复核账号显示主动作“复核通过”,制单人不显示伪装成可用动作的禁用复核按钮。
|
||||
- `reviewed`:财务责任人显示“财务”;页面说明先在线下银行完成付款并上传回单,主动作改为“登记付款完成”。
|
||||
- `reviewed + bank + 0 份有效回单`:唯一主动作是“上传银行回单”,点击直接打开既有文件选择器;上传成功刷新附件计数后,主动作切换成“登记付款完成”。非银行渠道不强制回单,直接显示登记动作。
|
||||
- `executed`:显示“已付款”,不再提供付款动作;`failed` 显示“付款登记失败”并允许财务按既有规则重试。
|
||||
- PC 与移动复用同一响应式页面、状态函数和责任说明,不建立第二套移动状态机。
|
||||
- 付款批次状态中文映射放入客户端共享模块,付款列表、详情和其他客户端调用方统一导入;服务端报销处理进度继续输出业务阶段,但用跨层测试断言其“待付款复核”语义与客户端 `draft` 映射一致。
|
||||
|
||||
### 8.4 单一复核责任人
|
||||
|
||||
- 解析 active、具备 `payment.batch.review` 且不是制单人的候选人。
|
||||
- 先按稳定顺序选择 `ceo` 一人;不存在时再选择 `manager` 一人。一个员工同时有多个角色仍只产生一个收件人。
|
||||
- 创建事务在写批次之前完成候选解析;没有合格人员时返回可解释的业务错误,保证不会出现“批次已创建但无人能复核”。
|
||||
- 无合格人员错误面向财务显示“当前没有可复核人(复核人必须是非制单人且在职),请联系管理员”,并保留固定错误详情供自动化断言。
|
||||
- 页面显示职责而不预猜姓名;通知只发给最终选中的真实责任人。历史通知不补发、不删除。
|
||||
|
||||
### 8.5 兼容与回滚
|
||||
|
||||
- 不改 Prisma schema、历史批次或报销事实;既有 `draft` 批次部署后直接按“待付款复核”展示。
|
||||
- 若新创建合同需回滚,客户端可恢复旧表单,服务端仍接受显式名称和可选默认字段;状态机与历史数据不变。
|
||||
- 任何真实批次复核、回单上传、付款登记仍由业务人员操作,不纳入部署自动化。
|
||||
|
||||
## 9. 付款详情与日序号收口
|
||||
|
||||
### 9.1 页面信息层级
|
||||
|
||||
- 保留 `/payment-batches?id=<batch-id>` 的现有显式选择详情,不为低频付款批次增加独立路由。
|
||||
- 详情连续顺序固定为:批次概要、当前处理/下一步、报销明细、银行回单。状态和职责先于长明细,避免财务再次寻找“现在该做什么”。
|
||||
- 顶部操作组是不变的主动作坐标:`draft` 的合格非制单人可“复核通过”;`reviewed + bank + 无有效回单` 为“上传银行回单”;回单具备后为“登记付款完成”。回单章节只承载证据与次要说明,不复制同等级主按钮。
|
||||
- 当前责任只显示职责,不预猜未来姓名。状态信息缺失时降级为可理解的“待处理”,不让下游对象缺失打空详情。
|
||||
|
||||
### 9.2 金额单一展示合同
|
||||
|
||||
- `client/src/lib/displayFormat.ts` 的 `formatMoney()` 是付款页唯一人民币格式化入口。
|
||||
- 列表、移动卡片、详情总额、批次项、复核/付款确认文案和高风险阈值均传原始数值给共享函数;币种字段仍可作为业务事实保留,但 CNY 不再以裸值加代码拼接。
|
||||
|
||||
### 9.3 编号和自动标题
|
||||
|
||||
- 新格式为 `PB-YYYYMMDD-NNN`,日期使用 `fastify.config.appTimezone`,序号为当日三位正整数。既有长编号不迁移,导出、AP 与外部交接继续把编号视为不透明字符串。
|
||||
- 自动标题只能在取得编号后生成:`YYYY-MM-DD 付款批次 NNN`。请求显式传 `name` 时保持原值,不追加序号。
|
||||
- 当日序号大于 999 时返回稳定校验错误并回滚,不改成四位或随机后缀,避免格式在边界时静默漂移。
|
||||
|
||||
### 9.4 咨询锁与事务边界
|
||||
|
||||
- 使用 PostgreSQL 双 int4 事务级咨询锁:第一参数为项目登记的固定命名空间“付款批次日序号”,第二参数为公司时区 `YYYYMMDD`。
|
||||
- 在同一个 Prisma 交互事务中按固定顺序执行:取得咨询锁 → 查询当日严格新格式 `^PB-YYYYMMDD-[0-9]{3}$` 的最大序号 → 生成编号/自动标题 → 创建批次及既有项、审计、通知。
|
||||
- 不使用 `hashtext`,不把旧长格式纳入最大值,不在 P2002 后原事务内重试;PostgreSQL 唯一冲突会使事务进入 aborted 状态,任何兜底只能重试整个事务。本设计通过咨询锁消除正常同日并发冲突。
|
||||
- 无 schema 迁移;回滚时可恢复旧生成器,已经生成的新短编号仍满足唯一字符串合同,无需改写。
|
||||
|
||||
## 10. 付款复核对象与阶段编辑边界
|
||||
|
||||
### 10.1 状态门禁
|
||||
|
||||
- `draft` 是已提交待复核,不允许批次项 PATCH、回单上传或回单作废;财务仅可查看和取消,合格非制单人可复核。
|
||||
- `reviewed/failed` 允许财务登记凭证号、实际付款日期以及上传/作废回单;付款渠道已是复核事实,不再接受批次项覆盖。
|
||||
- `executed/cancelled` 的付款资料和回单全部只读;下载继续受既有权限、用途和附件安全门禁保护。
|
||||
- 客户端按状态渐进展示当前阶段控件,服务端以同一矩阵拒绝绕过请求。删除 `draft` 的可编辑路径后,不新增一条“待复核修改审计”来延续旧草稿语义。
|
||||
|
||||
### 10.2 渠道与复核事实
|
||||
|
||||
- 创建面板保留“选择报销”主任务,同时增加一个批次级付款渠道下拉框,默认 `bank`,请求继续使用既有 `defaultPaymentChannel` 字段。
|
||||
- 渠道和批次金额、收款信息一起构成复核输入;创建后锁定。确需改变渠道时取消未执行批次并重新创建,不在复核后静默改写。
|
||||
|
||||
### 10.3 收款信息 DTO
|
||||
|
||||
- 报销付款对象取 `ReimbursementForm.owner`,返回姓名、开户银行和经既有加密服务解密后立即脱敏的账户摘要;响应、快照、日志和测试均不得包含完整账号或密文。
|
||||
- 新批次把上述低敏摘要写入既有 `snapshot_json`,不新增 schema;既有批次缺快照字段时按 `entity_id` 查询当前报销归属人并只读补齐响应,不回写历史批次。
|
||||
- 复核详情继续显示单号、摘要、金额和状态,并补充付款渠道与收款摘要;页面四段结构和二次确认弹窗保持不变。
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
# 报销通知与处理进度简化:实施计划
|
||||
|
||||
## 0. 边界与前置
|
||||
|
||||
- [x] 获得最终规划批准后再启动任务;生产只读诊断随本计划获批,任何生产写入、补发、配置修改或重启仍需另行批准。
|
||||
- [x] 不修改审批链、付款 maker-checker、数据库 schema、历史任务或真实付款数据。
|
||||
- [x] 生产诊断不输出通知正文、完整账号、企微 userid、内部业务 ID、Secret 或原始响应。
|
||||
|
||||
## 1. 生产只读通知诊断
|
||||
|
||||
- [x] 定位目标报销最新工作流的当前财务任务,先查 `workflow.task_assigned` 站内通知是否存在。
|
||||
- [x] 有通知则查 delivery 状态/固定结果码、推送开关、绑定存在性和渠道匹配;无通知则核对下一任务与收件人解析事实。
|
||||
- [x] 形成只含布尔值、计数和错误分类的结论;需要修生产配置、补发或重启时停止并申请批准。
|
||||
|
||||
诊断结论(脱敏):当前财务任务及对应站内通知均已生成,收件人 active、开启推送且存在企微绑定;投递尝试为 0,生产无启用且匹配的企微通知渠道。根因位于投递配置层,不是审批推进或通知创建代码。本任务没有修改生产配置、补发、重启或部署。
|
||||
|
||||
## 2. 共享动作标签
|
||||
|
||||
- [x] PC 与移动审批页移除局部 `workflowActionLabel`,统一导入共享实现。
|
||||
- [x] 增加 `start`/`submit` 中文映射回归,检查打印及其他调用方没有第三份重复映射。
|
||||
|
||||
## 3. 处理进度文案
|
||||
|
||||
- [x] 将员工详情的后两阶段改为“付款复核”“付款完成”。
|
||||
- [x] 付款完成待处理说明明确“由财务登记,非审批环节”,保留权限门控的批次入口和回单完成状态。
|
||||
- [x] 更新 PC/移动组件测试,断言五阶段顺序与普通员工不暴露内部批次入口。
|
||||
|
||||
## 4. 付款责任人通知
|
||||
|
||||
- [x] 以前述只读诊断结论为实施门:先证明公共通知创建/投递路径的根因与修复边界,再确定本节只新增发送点还是必须复用同一代码修复。
|
||||
- [x] 增加付款复核待办与付款执行待办的通知类型、domain 和脱敏文案。
|
||||
- [x] 批次创建事务内解析有复核权限的 active manager/ceo,排除创建人并创建通知;提交后复用现有 dispatcher。
|
||||
- [x] 批次复核事务内通知 active 批次创建人/实际付款责任人,排除复核人;付款执行成功沿用现有申请人通知。
|
||||
- [x] 覆盖无合格收件人、创建人停用、重复请求、投递失败不回滚业务和普通用户不可读批次等边界。
|
||||
|
||||
## 5. 验证与审查
|
||||
|
||||
- [x] 运行 workflow/reimbursement/payment-batch 聚焦 Vitest、付款并发与 maker-checker 回归。
|
||||
- [x] 运行客户端 typecheck/build、相关 PC/移动 Playwright 和动作标签单测。
|
||||
- [x] 运行 Prisma validate、task/governance/process/sensitive/diff 门禁。
|
||||
- [x] 因涉及 workflow/付款职责和通知收件范围,实施后执行只读 reviewer 检查,不允许 reviewer 改代码。
|
||||
- [x] 已获批部署并完成公网检查;企微实际到达仍必须由新业务事件和真机确认,不能用历史 WMSG1 或自动化替代。
|
||||
|
||||
只读审查结论:首次检查发现站内通知中心缺少 `payment_batch` 目标链接,已补 PC/移动统一直达付款页并增加回归;复查未发现阻断项。通知记录与付款动作同事务,IM 投递发生在提交后且失败不回滚;复核人解析排除制单人,付款责任人解析排除复核人;重复创建/复核由既有锁和状态校验拒绝,不重复通知。生产仍无启用且匹配的企微渠道,属于已知部署后配置门,不得表述为企微通知已恢复。
|
||||
|
||||
生产发布记录(2026-08-01,脱敏):
|
||||
|
||||
- 已将已发布提交 `aa6b9bc` 按既有安装固化入口部署;环境合同、迁移状态、`/ready`、外部依赖 smoke、运行时 smoke、公网构建标识和前端资源扫描通过。
|
||||
- 历史 WMSG1 已证明当前企微应用具备应用消息发送能力;本轮没有另发渠道测试消息。
|
||||
- 生产仅启用 1 条 `wecom_app_message` 渠道,限定 workflow/reimbursement 域和 7 类审批、报销、付款通知;非相关业务类型不匹配。
|
||||
- `EMAIL_ENABLED=false` 已在新容器生效;没有创建邮件通知渠道。
|
||||
- 渠道创建写入 1 条审计;没有扫描、补发或重放任何历史通知。
|
||||
- 当前仅证明“新事件可匹配企微渠道”;仍待王芝玲财务复核/创建付款批次等真实新动作产生 delivery,并由收件人真机确认消息到达与 Deeplink 回流。
|
||||
|
||||
## 6. 运行观察
|
||||
|
||||
- [x] 运行约定为每周集中建立一次付款批次,不建设排程功能。
|
||||
- [ ] 王芝玲累计处理 5–10 张真实报销后触发一次访谈,决定财务复核与加入批次是否值得另立合并任务(未来触发条件,当前尚未发生)。
|
||||
|
||||
## 7. 付款入口与常开恢复
|
||||
|
||||
- [x] 先查生产 R54 变更历史、当前 flag 版本、创建/更新时间、updated_by 和环境硬开关,不先恢复。
|
||||
- [x] 确认当前库没有 `FEATURE_FLAG_UPDATED` 审计;根因是当前库首次初始化与 `.env` 均采用关闭默认值,不是 QA runner 或人工关闭。
|
||||
- [x] 将正式试运行环境默认硬开关和 seed 首次创建默认值改为付款常开,seed 仍不得覆盖已有记录。
|
||||
- [x] 修复 QA 窗口 runner:结束时恢复调用前快照,不无条件写死 `force_off`;正式启用 runner 成功保持 `force_on`、失败恢复原值。
|
||||
- [x] 移动财务工作台在付款能力启用但尚无批次时仍显示“付款”入口;PC 入口保持不变;新增移动付款路由避免小屏自动回首页。
|
||||
- [x] 补测试覆盖新环境初始化、runner 原值 on/off、移动端零批次入口、普通员工不可见和已批准候选可读。
|
||||
- [x] 通过只读审查后提交、部署;受控设置生产环境硬开关 true 和数据库全局 `force_on`,写审计并验证候选计数,不创建真实批次。
|
||||
|
||||
本地验证(2026-08-01):seed/部署合同/Portal 聚焦单测 19 passed;flag 快照 helper 的原值 `force_off department_allowlist` 与 `force_on global` 两类恢复测试 2 passed;移动信息架构 6 passed;客户端 typecheck/build、Prisma validate、脚本语法、Trellis/task/process/sensitive/diff 门禁通过。
|
||||
|
||||
只读审查(2026-08-01):未发现阻断项。PC 与移动付款路由使用同一权限集合,普通员工不会因新增移动路由获得付款能力;移动财务卡片同时受真实财务角色/付款权限和后端 `financeSummary` 门控;env、DB flag 与 runtime smoke 三层缺一即失败;短窗口在任何临时写入前保存完整快照,成功/失败分支均不会把调用前 `force_on` 或 allowlist 静默改成固定关闭。审查未修改代码。
|
||||
|
||||
生产恢复与发布(2026-08-01,脱敏):
|
||||
|
||||
- 静态 env 中唯一 `PAYMENT_BATCH_ENABLED=false` 已受控改为 true;修改时运行中旧容器未重启,未形成半开启能力。
|
||||
- 既有受控 live 管理员凭据已失效,Feature Flag API 在任何 flag 写入前返回 401;没有重置账号或读取密码。随后改用服务器内 Prisma,在单个 Serializable 事务中锁定唯一 `payment_batch_v1`,断言版本 1/`force_off` 后更新为版本 2/`force_on global`,并新增恰好 1 条带 before/after 的 `FEATURE_FLAG_UPDATED` 审计。
|
||||
- 提交 `37b4ef2` 已推送并通过既有安装固化入口部署;环境合同、远端 preflight、migration、`/ready`、external deps、runtime smoke、公网构建标识和前端资源扫描通过。
|
||||
- runtime smoke 明确证明 env true 与 `payment_batch_v1=flag_force_on_global`;生产只读核验确认唯一 active 财务人员仍有付款管理权限、系统导航含 `paymentBatches`、已批准未付款计数为 1、可建批次候选计数为 1。
|
||||
- 没有替财务创建付款批次,没有替负责人复核,也没有上传回单或执行付款。下一条真实验证由王芝玲创建批次触发,届时核验马建勇收到付款复核企微通知。
|
||||
|
||||
## 8. 首张真实付款批次闭环修正(已实施,待部署批准)
|
||||
|
||||
- [x] 只读核对创建路由、状态转换、通知发送点、前端按钮显隐和聚焦 E2E;确认不存在 submit API,`draft` 创建时已经发送复核通知。
|
||||
- [x] 生产只读核验最新真实批次:已自动编号、状态 `draft`、未复核/未付款、含 1 个报销项;复核通知已生成且有 1 次企微成功投递。核验未读取名称、金额、人员标识、消息正文或原始 payload。
|
||||
- [x] 服务端允许省略名称,自动生成批次标题和银行渠道默认值;付款完成登记缺少日期时按公司时区当天落账。
|
||||
- [x] 既有报销批次渠道为空时按银行转账进行 API 展示和回单门禁,不执行生产数据回填。
|
||||
- [x] 复核责任人解析改为单一 `ceo` 优先、`manager` 兜底;没有合格非制单复核人时在任何业务写入前拒绝创建。
|
||||
- [x] 创建页收敛为选择报销 + “创建并提交复核”,移除名称、默认日期、渠道和凭证前缀的高频路径输入;异常覆盖仍留在详情。
|
||||
- [x] 统一状态和动作:`draft=待付款复核`、`reviewed=待财务付款`、`executed=已付款`、`failed=付款登记失败`;“复核”改为“复核通过”,“执行”改为“登记付款完成”。
|
||||
- [x] 提取付款批次共享状态映射并替换客户端局部映射;增加跨层语义回归,锁定报销进度与付款页对 `draft` 的一致表达。
|
||||
- [x] 详情按状态显示当前责任职责、下一步和 maker-checker 说明;银行渠道无有效回单时唯一主动作是“上传银行回单”,上传后切换为“登记付款完成”;不新增 Tab、流程设计器、第二轮复核或 submit API。
|
||||
- [x] 更新 server route 单测与付款主链 Playwright,覆盖自动字段、无复核人 fail-closed、单一收件人、状态/动作中文和实际付款日缺省值。
|
||||
- [x] 运行 server focused Vitest、客户端 typecheck/build、聚焦 Playwright、Prisma validate、Trellis/governance/process/sensitive/diff 门禁。
|
||||
- [x] 因修改付款责任解析和状态表达,实施后执行只读 reviewer 审查;通过后再申请提交、部署和公网/真机验证批准。
|
||||
|
||||
本地验证(2026-08-01):付款/报销详情/通知/导出/导航聚焦 Vitest 85 passed;隔离 PostgreSQL 付款并发门禁覆盖双执行、执行/取消、重复建批次和直接付款竞争并全部通过;客户端 typecheck/build 通过;付款权限 5 条、高风险动作 8 条 Playwright 共 13 passed。reviewer 首轮未发现数据安全或状态机阻断项,指出“兼任复核职责的制单人仍显示自审按钮”和“回单查询失败误报为空”两项交互问题;已修为前端同步排除制单人、失败态只显示错误与重新加载,复查未发现新 blocker。既有真实批次未被读取正文、修改、复核、上传回单或登记付款。
|
||||
|
||||
生产发布(2026-08-02,脱敏):提交 `7c39f8b` 已推送并通过既有安装固化入口部署;同步前远端源码、普通环境文件、PGDATA 和附件目录合同通过,未同步或覆盖 `.env`。63 项 migration 为 up to date,init/server 依赖层命中缓存;`/ready` 构建短标识、external deps smoke、runtime smoke、付款能力 env/flag 常开、公网前端资源扫描和付款懒加载块四项关键文案均通过。当前 shell 未配置 live-smoke 专用凭据,因此没有宣称登录后业务链自动验收;部署未替任何人复核批次、上传回单或登记付款。
|
||||
|
||||
## 9. 付款详情与批次短编号(已实施并部署)
|
||||
|
||||
- [x] 将付款详情重排为“批次概要 → 当前处理/下一步 → 报销明细 → 银行回单”,保留现有内联详情和顶部唯一主动作,不新增独立路由。
|
||||
- [x] 付款页列表、移动卡片、详情总额、批次项、确认弹窗和高风险阈值全部改用共享 `formatMoney()`。
|
||||
- [x] 使用公司时区生成 `PB-YYYYMMDD-NNN`,在同一 Prisma 交互事务中先取得双 int4 `pg_advisory_xact_lock`,再查询严格新格式最大日序号并创建批次。
|
||||
- [x] 自动标题改为 `YYYY-MM-DD 付款批次 NNN`;显式名称保持原值;既有编号不迁移;日序号超过 999 时 fail-closed。
|
||||
- [x] 更新 route 单测,覆盖时区、自动标题、显式名称、同日递增、跨日归零、同日旧长编号隔离和边界错误;更新真实 PostgreSQL 并发门禁,证明同日并发编号唯一递增。
|
||||
- [x] 更新付款主链 Playwright,断言详情信息顺序、顶部主动作和全页金额格式;运行前端 typecheck/build。
|
||||
- [x] 运行 Prisma validate、node syntax、task/governance/process/sensitive/diff 门禁;因涉及付款创建并发和原始 SQL,执行只读 reviewer,不允许 reviewer 改代码。
|
||||
- [x] 把双参数咨询锁命名空间、锁顺序、错误矩阵和测试要求沉淀到 backend spec;通过后中文提交,不在未获新批准时部署。
|
||||
|
||||
本地验证(2026-08-02):付款 route 单测 39 passed;隔离 PostgreSQL 付款并发门禁覆盖双执行、执行/取消、重复建批次、同日两来源日序号、直接付款竞争和缺失制单人场景并全部通过;付款权限与高风险动作 Playwright 14 passed;客户端 typecheck/build、Prisma validate、相关脚本语法、Trellis task/governance/process/sensitive/diff 门禁通过。文档注册审计保持 0 error,既有 22 个未登记 docs 文件 warning 与本轮无关。
|
||||
|
||||
实现中发现 Prisma 7.8 driver adapter 无法反序列化 `SELECT pg_advisory_xact_lock(...)` 的 PostgreSQL `void` 列并返回 `P2010`;锁调用改用 `$executeRaw` 后真实 PostgreSQL 并发门禁通过。该陷阱、双 int4 命名空间登记、固定锁顺序和禁止在已 aborted 事务内重试 `P2002` 已写入 backend spec。
|
||||
|
||||
只读审查(2026-08-02):未发现权限、maker-checker、付款状态机或历史编号兼容阻断项。新编号分配在既有报销锁定、状态/占用和复核人校验之后、所有批次/批次项/审计/通知写入之前;咨询锁、严格新格式查询和创建保持在同一交互事务。旧长编号不参与日序号,超过 999 时零业务写入;付款页未新增路由或可操作权限,回单缺失时顶部仍只有一个主动作。审查未修改生产数据,也未执行部署。
|
||||
|
||||
生产发布与部署合同恢复(2026-08-02,脱敏):
|
||||
|
||||
- 提交 `5d274af` 已推送并通过 `deploy-existing.sh` 的既有合同入口部署;六键合同读取、环境合同、远端 preflight、63 项 migration、`/ready` 构建标识、external deps、runtime smoke、付款能力常开和公网资源扫描通过。当前 shell 未配置 live-smoke 专用凭据,因此没有宣称登录后业务链自动验收。
|
||||
- 部署前从运行中容器定位权威环境候选并逐键进行 SHA-256 比对:生产环境文件中的 163 个实际赋值键全部 PASS,0 个不可观测键。PASS 键名集合等于提交 `5d274af` 的 `deploy/trialrun/.env.example` 中全部非注释赋值键;唯一未进入生产集合的是注释状态的可选 `SHADOW_DATABASE_URL`。关键不可替代或身份相关键 `FIELD_ENCRYPTION_KEY`、`DATABASE_URL`、`POSTGRES_USER`、`POSTGRES_DB`、`POSTGRES_PASSWORD`、`JWT_SECRET` 均在 PASS 集合内。记录不含任何值、散列值、宿主路径或主机标识。
|
||||
- 权威环境文件仅复制到规范部署根,旧文件原地保留;新副本字节 SHA-256 一致、部署用户属主且权限为 `0600`。部署全程保持 `SYNC_ENV=0`,没有从本地同步或覆盖生产环境文件,也没有删除四个旧源码候选目录。
|
||||
- 部署后以新完整 SHA 重装 automatic-backup 与 WARCH3 合同;4 个 service 的工作目录、环境文件、构建 SHA 与规范根一致,4 个 timer 均为 enabled/active。
|
||||
- 已手工触发一次完整真实自动备份周期:源端成功状态新鲜且构建标识一致,`resolve_active_runtime` 的运行构建、附件挂载和 PGDATA 挂载三项断言通过;异机归档完成,随后 watchdog 证明异机恢复点新鲜。没有输出归档路径、文件名、远端标识或任何密钥。
|
||||
- 旧源码候选目录与旧环境文件继续保留,清理不属于本轮授权范围。任务仍保留运行观察项,不因本次部署归档。
|
||||
|
||||
## 10. 付款复核对象与阶段编辑收口(本轮)
|
||||
|
||||
- [x] 服务端将批次项编辑限制为 `reviewed/failed` 的凭证号与实际付款日期,拒绝任何创建后渠道覆盖;`draft/executed/cancelled` 均不可编辑。
|
||||
- [x] 服务端将回单上传/作废限制为 `reviewed/failed`,保留所有状态下的授权查看/下载;拒绝请求不写附件、存储对象或审计。
|
||||
- [x] 创建快照加入收款人姓名、开户银行和脱敏账户摘要;既有批次通过只读 fallback 补齐 DTO,不迁移或回写生产数据。
|
||||
- [x] 创建面板增加付款渠道选择并传既有 `defaultPaymentChannel`;默认银行转账,创建成功后重置。
|
||||
- [x] 详情待复核阶段隐藏付款登记输入和回单维护动作;复核后显示凭证号/日期与回单动作;已付款/已取消全部只读,四段结构和确认弹窗不变。
|
||||
- [x] 服务端测试覆盖状态矩阵、渠道锁定、零副作用和脱敏 DTO;前端测试覆盖创建 payload、待复核零编辑、复核后登记及已付款回单只读。
|
||||
- [x] 运行付款 focused Vitest、相关 Playwright、客户端 typecheck/build、Prisma validate、task/governance/process/sensitive/diff 门禁,并执行付款状态机只读审查。
|
||||
|
||||
本地验证(2026-08-02):付款路由与并发保护单测 58 passed;付款权限/高风险动作 Playwright 15 passed;客户端 typecheck/build、Prisma validate、Node syntax、隔离 PostgreSQL 付款并发门禁、task/governance/process/docs/sensitive/diff 门禁通过。首次 Playwright 运行因本地静态预览未启动而在登录页统一 `ERR_CONNECTION_REFUSED`,启动本地预览后原样重跑全绿,不属于业务断言失败。文档审计保持 0 error,既有 22 个未登记 docs 文件 warning 与本轮无关。
|
||||
|
||||
只读审查首轮发现:原始 `snapshotJson` 仍可透传异常/历史快照,列表执行不必要的收款资料解密,且缺少 `failed` 正向和回单存储补偿测试。已移除付款响应的原始快照字段,详情只返回格式校验后的固定收款字段,列表不再加载或解密详情级收款资料;补充恶意快照、`failed`、拒绝零文件残留和上传期间状态变化清理回归。未修改生产数据,未执行部署或真实付款动作。
|
||||
|
||||
只读复查随后发现付款页仍把精简列表项当详情使用,会让真实收款依据显示为空;已改为按所选批次 ID 加载详情,并将 E2E 锁定为“列表收款字段为空、详情返回脱敏字段且页面确实发起详情请求”。最终复查无 Blocker/High/Medium/Low,未修改文件。
|
||||
|
||||
生产发布与资料门禁(2026-08-02,脱敏):
|
||||
|
||||
- 提交 `185d64d` 已推送并通过既有安装固化入口部署;聚合体检 13 项、环境与数据合同、63 项 migration、`/ready` 构建标识、external deps、runtime smoke、付款能力常开、公网构建标识、前端资源扫描和部署后备份合同刷新全部通过。部署保持 `SYNC_ENV=0`,没有同步或覆盖生产环境文件。
|
||||
- 首次生产只读核验仅输出固定检查项结论:目标员工记录 PASS,开户银行 FAIL,加密银行账号非空且可解密格式 FAIL;未输出账号、密文、路径、主机或原始 payload。管理员随后通过受支持的敏感资料 API 在本地浏览器补录,账号没有经过聊天、仓库或 SQL。
|
||||
- 补录后按同一只读口径复核:目标员工记录、开户银行、加密银行账号非空且可解密格式三项全部 PASS。进一步只读确认既有批次仍为待付款复核且付款渠道为银行转账,与实际付款方式一致。
|
||||
- 本次保留既有批次,不再为补写收款摘要取消重建:历史批次通过 `loadLegacyPayees` 实时回查展示当前脱敏收款信息,且资料从复核到付款完成期间保持冻结。接受该批次未在 `snapshot_json` 固化收款摘要所带来的一次性审计证据弱化;后续新批次继续在创建时固化摘要。真实业务人员依次完成非制单复核、线下转账、回单上传和付款完成登记。
|
||||
@@ -0,0 +1,106 @@
|
||||
# 报销通知与处理进度简化
|
||||
|
||||
## Goal
|
||||
|
||||
让报销单在 5–10 人公司的真实流程中做到两件事:新的责任人能及时收到待办通知;申请人能看懂“审批”和“付款处理”的区别,不再把四个动作误认为四轮审批。
|
||||
|
||||
## Background
|
||||
|
||||
- 马建勇已在生产完成杨俊报销的主管审批,但下一节点王芝玲没有收到预期审批通知;王芝玲此前已通过企业微信登录,不能把问题归因于“从未登录”。
|
||||
- 代码在报销提交和工作流流转时会创建 `workflow.task_assigned`,主管通过后也会生成下一任务并排队通知,因此应先区分“站内通知未生成”和“已生成但企微未投递”。
|
||||
- 当前员工详情展示“付款批次复核”“付款与回单”,容易被理解为两轮额外审批;实际前者是非制单人付款复核,后者是财务线下付款后的回单与状态登记,不是审批。
|
||||
- 当前职责基线:马建勇负责主管审批和非制单付款复核;王芝玲负责财务复核、创建付款批次、线下付款、上传回单和登记付款完成。一个付款批次可以覆盖多张报销单。
|
||||
- `client/src/lib/approvalDisplay.ts` 已把 `start` 映射为“提交”,但 PC `WorkflowTaskDetailPage.tsx` 与移动 `MobileApprovalsPage.tsx` 各自复制了缺少 `start` 的局部映射,导致显示 `start · 杨俊`。
|
||||
- 首张真实付款批次暴露了创建闭环的表达缺陷:服务端在创建事务内已经自动编号、锁定报销、生成付款复核通知并开始投递,但数据库状态和页面仍显示“草稿”,页面也不存在提交接口或提交按钮,导致制单人无法判断是否已经送审、由谁处理、下一步是什么。
|
||||
- 生产只读核验确认该真实批次已经自动编号,当前未复核/未付款;付款复核通知已经生成并有企微成功投递。当前解析同时创建了 `ceo` 和 `manager` 两类站内收件记录,不符合“一个明确责任人、不要泛发”的小团队运行目标。
|
||||
|
||||
## Scope
|
||||
|
||||
- 先做一次获批的生产只读诊断:只核对目标工作流任务、王芝玲对应站内通知是否存在、投递记录状态/固定错误分类、通知开关、企微绑定状态和渠道匹配状态;不读取 Secret、消息正文或原始企微 payload。
|
||||
- 删除 PC/移动审批页重复的动作标签映射,统一使用共享 `workflowActionLabel()`;`start` 和 `submit` 均显示“提交”。
|
||||
- 员工报销进度保留五个业务阶段,但改为:申请提交、主管审批、财务复核、付款复核、付款完成;付款完成明确为“财务登记,非审批环节”。
|
||||
- 保留付款 maker-checker:付款批次创建人与复核人不得相同,当前只需一次非制单复核,不增加更多复核层级。
|
||||
- 通知以“新的责任人需要行动”为准:提交后通知主管、主管通过后通知财务、创建付款批次后通知非制单复核人、付款复核后通知财务/批次创建人继续付款、付款完成后通知申请人。
|
||||
- 每周集中建立一次付款批次作为当前运行约定,不在本任务建设定时排程、自动组批或强制付款日。
|
||||
- 修复财务首次使用时的入口死锁:付款能力正式常开时,即使尚无付款批次,财务人员也必须能在 PC 和移动工作台看到“付款”入口并进入创建页。
|
||||
- 恢复生产 `payment_batch_v1` 正式常开基线,并防止新环境初始化或 QA 窗口收尾把正式常开状态静默改回关闭。
|
||||
- 收口付款批次创建到复核的首个真实闭环:创建页只要求选择待付款报销,编号、标题和常用付款默认值由服务端生成;创建动作明确等价于提交付款复核,不再让用户寻找不存在的二次提交按钮。
|
||||
- 付款页用业务状态而非内部状态名表达下一责任:`draft` 显示“待付款复核”,`reviewed` 显示“待财务付款”,`executed` 显示“已付款”;详情说明当前责任职责和唯一下一步。
|
||||
- 付款复核通知收件人按 `ceo` 优先、`manager` 兜底选择一个确定的非制单人;没有合格复核人时创建前失败并给出可解释提示,不生成无人负责的批次。
|
||||
|
||||
## Requirements
|
||||
|
||||
- 通知诊断必须先判断站内通知是否生成,再进入企微投递层,不能直接假设缺少通知代码或重复创建通知。
|
||||
- 若本次缺失由生产配置、人员推送开关、企微绑定或渠道配置造成,先给出脱敏根因和最小修复建议;任何生产写入、配置修改、补发消息或重启均需再次批准。
|
||||
- 新增付款阶段通知时必须复用现有通知、投递和审计服务;通知只给真实需要行动的人,不向所有 finance/manager 角色泛发重复消息。
|
||||
- 普通员工页面不突出“付款批次”内部对象名;有付款批次权限的人员仍可通过现有权限门控入口查看批次。
|
||||
- 共享动作标签是唯一事实源;PC、移动和打印页不得继续维护互相漂移的局部映射。
|
||||
- 不删除主管审批、财务复核或付款复核,不放宽自审,不把付款完成伪装成审批通过。
|
||||
- 付款开关恢复前先查询 R54 变更历史;若当前库无变更审计,必须区分“seed 首次创建默认值”和“seed 覆盖已有值”,不得把不存在的 runner/人工操作写成根因。
|
||||
- QA 短窗口结束时必须恢复调用前快照;正式启用流程成功后保持常开,失败才回滚,不得无条件写死 `force_off`。
|
||||
- 正式环境和新初始化的试运行环境必须同时满足环境硬开关与数据库开关常开;任何一层关闭都应由部署/运行门禁发现。
|
||||
- 付款批次内部仍可保留 `draft` 作为兼容状态,但该状态已代表“创建并提交、等待独立复核”,前端、通知和测试不得再把它描述成尚未提交的可编辑草稿。
|
||||
- 新建批次不要求财务手工编批次名称、付款日期、渠道或凭证前缀;服务端继续自动生成唯一批次编号,按公司时区生成标题,默认付款渠道为银行转账,实际付款日期在登记付款完成时缺省取登记当天。异常项仍可在批次详情中覆盖。
|
||||
- 付款动作必须使用“登记付款完成”等事实性文案,不得用“执行付款”暗示 OA 会连接银行或自动转账。
|
||||
- 付款复核仍是一轮 maker-checker;责任收件人只选一个,优先具备复核权限的 active `ceo`,没有时再按稳定顺序选择 active `manager`,始终排除制单人。
|
||||
- 付款页同一时刻只显示一个主动作:银行渠道复核通过但尚无有效回单时显示“上传银行回单”;上传后才显示“登记付款完成”,不能让用户先撞到 `bank_receipt_required` 才知道前置条件。
|
||||
- 付款批次状态标签必须使用共享展示映射;列表、详情、移动响应式视图和相关测试不得各自复制 `draft/reviewed/executed/failed/cancelled` 中文文案。
|
||||
- 付款详情保持现有列表内显式选择后的详情形态,不新增独立详情路由;信息顺序固定为“批次概要 → 当前处理/下一步 → 报销明细 → 银行回单”。顶部操作组始终承载当前唯一主动作,回单章节后移不得把“上传银行回单”推到明细列表之后。
|
||||
- 付款页的列表、详情总额、批次项、确认弹窗和高风险阈值统一使用共享 `formatMoney()`;不得继续裸输出金额或在页面内新增货币格式化副本。
|
||||
- 新建付款批次编号使用公司时区日期和三位日序号 `PB-YYYYMMDD-NNN`;既有编号不迁移。未显式传名称时自动标题为 `YYYY-MM-DD 付款批次 NNN`,显式名称原样保留。
|
||||
- 日序号必须在创建批次的同一交互事务中,先取得双参数 PostgreSQL 事务级咨询锁,再查询当日严格新格式最大序号并创建;咨询锁使用固定 int4 命名空间和 int4 `YYYYMMDD`,不得依赖唯一冲突后的原事务内重试。
|
||||
- `draft` 已表示“创建并提交、待付款复核”,批次项不得继续沿用未提交草稿的编辑权限;付款凭证号、实际付款日期和银行回单只能在 `reviewed/failed` 财务处理阶段维护,`executed/cancelled` 全部只读。
|
||||
- 付款渠道在创建面板确定并作为复核事实提交,默认银行转账;创建后不得通过批次项覆盖改变,避免复核对象在复核前后漂移。
|
||||
- 付款复核明细必须显示收款人姓名、开户银行和脱敏银行账户摘要;不得向付款批次 API 暴露完整账号或加密原值。新批次固化脱敏快照,既有批次以当前报销归属人只读降级展示。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] 脱敏证据能明确回答本次财务通知属于“未生成”还是“已生成未送达”,并定位到单一失败层或明确下一步,不包含通知正文、账号、Secret 或原始 payload。
|
||||
- [ ] 马建勇通过主管审批后,王芝玲能够收到对应的新待办站内通知;企微投递恢复需另有真实投递证据后才能标为通过。
|
||||
- [ ] PC 与移动审批处理历史均显示“提交 · 杨俊”,不再出现原始 `start`;动作标签映射只保留一个共享实现。
|
||||
- [ ] 普通报销详情依次显示“申请提交、主管审批、财务复核、付款复核、付款完成”,并明确付款完成不是审批。
|
||||
- [ ] 创建付款批次后通知实际可复核人员;复核通过后通知实际付款责任人;付款完成继续通知申请人,且同一事件不会重复创建/投递。
|
||||
- [ ] 付款复核仍强制非制单人且仅一轮;既有付款批次、审批快照和历史记录不迁移、不改写。
|
||||
- [ ] PC/移动相关组件测试、通知路由/服务测试、付款 maker-checker 回归、前端 typecheck/build 与 Trellis 门禁通过。
|
||||
- [ ] 财务人员在尚无付款批次但存在已批准待付款报销时,PC 与移动端均可看到“付款”入口,并能看到该报销作为新建批次候选。
|
||||
- [ ] 正式初始化基线为付款批次常开;QA 窗口测试覆盖原值为 on/off 两种情况并恢复原值,不再硬编码关闭。
|
||||
- [ ] 生产恢复后只验证入口和候选可见,不代替财务创建批次、负责人复核或执行付款。
|
||||
- [ ] 新建批次页面只需选择报销;提交后自动得到唯一编号、按公司时区生成的标题、银行转账默认渠道,并直接进入“待付款复核”。
|
||||
- [ ] 创建按钮显示“创建并提交复核”,成功反馈明确“已提交,等待负责人复核”;页面不存在需要用户寻找的第二个提交动作。
|
||||
- [ ] 制单人看到“待付款复核”和“负责人复核(制单人不可自审)”;实际复核人看到可用的“复核通过”;复核后财务看到“待财务付款”和“登记付款完成”。
|
||||
- [ ] 同一批次只通知一个确定的非制单复核责任人,`ceo` 优先、`manager` 兜底;无合格复核人时拒绝创建且不写批次、批次项、审计或通知。
|
||||
- [ ] 未填写实际付款日期时,登记付款完成以公司时区当天落账;银行转账仍要求先上传银行回单,OA 不执行真实转账。
|
||||
- [ ] 银行渠道复核通过且没有有效回单时,唯一主动作是“上传银行回单”;上传成功后唯一主动作切换为“登记付款完成”,不会先触发可预见的 409。
|
||||
- [ ] 无合格复核人的错误明确说明“复核人必须是非制单人且在职,请联系管理员”,不把配置问题伪装成财务人员操作错误。
|
||||
- [ ] 既有真实批次不改 ID、不改金额、不改报销绑定、不改复核/付款事实;部署后仅获得新的业务文案和下一步说明,不替任何人推进状态。
|
||||
- [ ] 既有报销批次渠道为空时按银行转账展示并执行回单门禁,但不回写历史记录;当前真实批次同样先上传回单再登记付款完成。
|
||||
- [ ] 付款详情按“批次概要、当前处理/下一步、报销明细、银行回单”连续展示;各状态唯一主动作位于顶部,银行回单章节内不出现第二个同等级主按钮。
|
||||
- [ ] 付款页所有人民币金额统一显示为 `¥1,234.56` 形式,列表、详情、批次项、确认弹窗和高风险阈值无裸金额残留。
|
||||
- [ ] 新批次编号为公司时区 `PB-YYYYMMDD-NNN`,同日并发创建得到唯一递增序号,跨日从 `001` 开始;超过 `999` 时 fail-closed,不改变编号格式。
|
||||
- [ ] 自动标题在编号生成后使用相同日序号;调用方显式提供名称时不追加序号;旧编号保持原值且导出/AP/外部传递继续按不透明字符串兼容。
|
||||
- [ ] 待付款复核期间批次项和回单均不可编辑;后端直接 PATCH/上传/作废请求同样拒绝,且不产生静默未审计变更。
|
||||
- [ ] 创建批次时可选择付款渠道且默认银行转账;创建后渠道只读,复核人看到的渠道与创建事实一致。
|
||||
- [ ] 每条付款明细显示收款人姓名、开户银行和脱敏账户摘要,不返回完整账号、密文或其他身份信息;当前既有批次无需迁移即可展示。
|
||||
- [ ] `reviewed/failed` 允许登记凭证号、实际付款日期及维护回单;`executed/cancelled` 的回单与付款资料只读。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 不在本任务删除付款批次、付款复核或银行回单要求。
|
||||
- 不接银行接口,不让 OA 自动执行真实付款。
|
||||
- 不建设每周付款日排程、自动组批、批次自动复核或定时通知。
|
||||
- 不在未获批准时修改生产通知渠道、员工推送开关、企微绑定、环境变量或服务状态。
|
||||
- 不顺带重构全部审批页面或统一所有业务状态标签。
|
||||
- 不新增“提交中”数据库状态或单独 submit API,不增加第二轮付款审批,不建设可配置的付款复核人选择器。
|
||||
- 不把批次创建与财务复核合并,不由系统自动复核、自动上传回单或自动登记付款完成。
|
||||
|
||||
## Deferred Decision With Trigger
|
||||
|
||||
- 本轮不合并“财务复核通过”和“加入/创建本周付款批次”。决定性依据是王芝玲尚未实际执行过财务复核,无法判断她会随到随审还是与周付款批次一起处理;提前合并可能迫使她延迟复核或频繁建批次。
|
||||
- 触发条件:王芝玲累计处理 5–10 张真实报销单后,由负责人直接询问“财务复核与加入付款批次通常一次完成,还是分开完成?”若一次完成,再立小任务评估合并;若随到随审、定期组批,则永久保持两个动作分离。
|
||||
|
||||
## Test Plan
|
||||
|
||||
- 聚焦测试动作标签、下一审批任务通知、付款批次创建/复核/执行通知和幂等性。
|
||||
- 运行相关 server Vitest、付款并发/maker-checker 回归、客户端 typecheck/build 与相关 Playwright。
|
||||
- 运行 task validate、Trellis governance、process contract、sensitive-content scan 与 `git diff --check`。
|
||||
- 生产只读诊断和任何后续真机通知验证分开记录;没有实际企微到达证据时不得宣称通知已恢复。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "reimbursement-notification-progress-clarity",
|
||||
"name": "reimbursement-notification-progress-clarity",
|
||||
"title": "报销通知与处理进度简化",
|
||||
"description": "诊断报销审批通知投递,统一审批动作中文映射,并按小团队实际职责简化付款阶段展示。",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": "报销审批通知只读诊断、审批动作标签去重、付款阶段文案与必要通知;不削弱付款 maker-checker",
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "shunli",
|
||||
"assignee": "shunli",
|
||||
"createdAt": "2026-08-01",
|
||||
"completedAt": "2026-08-03",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "通知、付款入口、批次状态文案、付款资料门禁与详情进度实现已完成并进入生产,后续详情页统一任务又收敛了共享阶段语义与操作区。唯一尚未发生的真实业务观察是杨俊批次登记付款完成后的通知与 executed 状态,已转由 07-27-yang-jun-reimbursement-readiness 承接;本任务不再保留重复等待。",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,181 @@
|
||||
# 企业银行账户与付款制单设计
|
||||
|
||||
## 1. 边界与交付顺序
|
||||
|
||||
本任务在 OA 内建立付款准备能力,不连接银行 API、不提交付款、不代替网银复核。交付按同一任务内的三个层次顺序完成:
|
||||
|
||||
1. 银行机构与公司付款账户主数据。
|
||||
2. 员工收款信息默认脱敏、规范化银行标识和付款批次冻结快照。
|
||||
3. 已复核批次的 BIFF8 制单导出、使用状态与审计。
|
||||
|
||||
三层共享同一迁移和权限设计,拆成独立任务会使中间版本产生无法制单或无法建批次的短暂合同,因此本轮保留为一个复杂 Trellis task,按提交批次分层验证。
|
||||
|
||||
## 2. 数据模型
|
||||
|
||||
### 2.1 BankInstitution
|
||||
|
||||
新增银行机构主数据:
|
||||
|
||||
- `id` UUID
|
||||
- `code` 唯一、不可变的规范化代码;首版至少存在 `CCB`
|
||||
- `name` 展示名称
|
||||
- `enabled`、`archived_at`
|
||||
- `created_at/by`、`updated_at/by`
|
||||
|
||||
银行机构由 `payment.account.manage` 管理。员工收款资料与公司付款账户都引用同一主数据,不把银行名称字符串当身份键。已被引用的代码不能修改或物理删除;停用只阻止新绑定。
|
||||
|
||||
### 2.2 PaymentAccount
|
||||
|
||||
新增独立付款账户表并关联 `LegalEntity`:
|
||||
|
||||
- `id`、`legal_entity_id`、`bank_institution_id`
|
||||
- `account_name`
|
||||
- `account_number_encrypted`
|
||||
- `enabled`、`archived_at`
|
||||
- `created_at/by`、`updated_at/by`
|
||||
|
||||
PostgreSQL 使用部分唯一索引保证同一法人主体最多一条 `enabled=true AND archived_at IS NULL` 的账户;服务端先做自然中文校验,数据库索引负责并发兜底。
|
||||
|
||||
已启用账户不原地覆盖账号。变更操作在同一事务内归档旧记录并创建新记录,保留历史身份;停用只改变旧记录状态。完整账号沿用现有字段加密服务,普通 DTO只计算脱敏尾号。
|
||||
|
||||
### 2.3 Employee 与 PaymentBatch
|
||||
|
||||
- `Employee` 增加可空 `bank_institution_id`。存量数据不通过自由文本自动猜测;由 HR/admin 在 UI 中确认。
|
||||
- `PaymentBatch` 增加可空 `legal_entity_id`、`payment_account_id`、内部付款账户快照、制单次数、最近制单时间和操作者。
|
||||
- 报销付款项的 `snapshot_json.payee` 增加内部密文和银行机构代码;既有姓名、银行显示名和脱敏尾号继续保留。
|
||||
|
||||
付款账户快照仅存内部使用字段:账户记录 ID、法人主体 ID、账户名称、银行机构代码/名称、原始密文和脱敏尾号。API 映射层只返回名称、银行名称和脱敏尾号。
|
||||
|
||||
迁移全部新增为可空,禁止回填历史完整账号。历史批次因此保持可查看、可人工付款,但制单明确失败关闭。
|
||||
|
||||
## 3. 权限与威胁模型
|
||||
|
||||
新增权限:
|
||||
|
||||
| 权限 | 角色 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `payment.account.manage` | `finance_mgr`、`admin/*` | 银行机构与公司付款账户新增、替换、停用 |
|
||||
| `payment.account.read` | `finance`、`finance_mgr`、`admin/*` | 公司账户脱敏列表/详情 |
|
||||
| `payment.account.read_full` | `finance`、`finance_mgr`、`admin/*` | 填理由查看公司完整账号 |
|
||||
| `payment.order.export` | 明确具有 `finance` 或 `finance_mgr` 业务角色者 | 已复核批次制单;单有 `admin/*` 不作为财务业务角色代办 |
|
||||
|
||||
`employee.bank.manage` 不授予财务或 system_admin。员工完整收款账号查看继续使用独立读取权限,但现有 GET 默认改为脱敏;完整值通过带原因的 POST 动作获取并先写审计。
|
||||
|
||||
公司付款账户维护防的是误配置、错误来源账户和缺少及时知情,不新增双人审批。真实受益人欺诈边界由“HR/admin 维护员工收款资料”和“finance/finance_mgr 制单”分离控制。
|
||||
|
||||
## 4. 主数据 API 与页面
|
||||
|
||||
### 4.1 公司付款账户
|
||||
|
||||
新增专用路由而不复用 Feature Flag:
|
||||
|
||||
- 脱敏列出银行机构与付款账户。
|
||||
- 创建首个账户、替换当前账户、停用当前账户。
|
||||
- POST 完整账号查看动作,body 必须有 `reason`。
|
||||
|
||||
管理入口放在 PC `管理 -> 基础资料/财务配置 -> 企业银行账户`。默认卡片只显示法人主体、账户名称、银行名称、脱敏尾号和状态;完整账号查看、替换、停用均使用明确动作与后果确认。移动端不提供配置入口。
|
||||
|
||||
账户新增、替换或停用在事务内写脱敏审计,并为所有 active admin 与 CEO 创建 OA 通知;通知不带完整账号、密文或本地路径,也不要求 CEO 具备账户主数据页面权限。
|
||||
|
||||
### 4.2 员工收款信息
|
||||
|
||||
人员档案增加收款信息区:
|
||||
|
||||
- HR/admin 可选银行机构并录入账号。
|
||||
- finance/finance_mgr 只能看银行名称和脱敏尾号。
|
||||
- 员工本人在个人资料看到自己的银行名称和脱敏尾号。
|
||||
- 授权角色查看完整账号时填写原因;服务端先审计后返回,前端不会把完整值缓存到长期状态或普通查询缓存。
|
||||
|
||||
## 5. 批次创建与冻结快照
|
||||
|
||||
对 `entityType=reimbursement` 且银行渠道的新批次:
|
||||
|
||||
1. 在同一事务锁定所选报销。
|
||||
2. 解析每张报销所有人的 `legal_entity_id`,要求非空且全批次一致。
|
||||
3. 查询该法人主体唯一启用的建行付款账户。
|
||||
4. 要求每位收款人有 `CCB` 银行机构与可用账号密文。
|
||||
5. 直接复制员工现有密文,不解密后重新加密;只为脱敏尾号做一次受控解密。
|
||||
6. 冻结付款账户和每一项收款信息后创建批次。
|
||||
|
||||
缺法人主体、混合法人主体、缺付款账户、非建行、缺账号或解密失败时,创建失败并一次性返回自然中文处理建议。其他渠道及非报销实体沿用原流程,但不具备银行制单能力。
|
||||
|
||||
API 映射函数必须显式挑选安全字段。测试扫描所有普通批次/API/AP 响应,断言不存在 `v1:` 密文、完整账号或原始 `snapshot_json`。
|
||||
|
||||
## 6. 制单服务与接口
|
||||
|
||||
### 6.1 资格校验
|
||||
|
||||
POST 制单接口接收 `{ reason }`,仅允许明确 finance/finance_mgr 业务角色、`payment.order.export` 权限和 `reviewed` 状态。它必须拒绝:
|
||||
|
||||
- 历史/不完整快照
|
||||
- 非报销或非银行渠道
|
||||
- 公司或任一员工不是 `CCB`
|
||||
- 缺完整密文、账号解密失败
|
||||
- 缺金额、业务编号或付款账户
|
||||
- 其他四种批次状态
|
||||
|
||||
校验聚合所有业务阻塞项后统一返回;权限失败仍立即拒绝,不泄露批次内容。
|
||||
|
||||
### 6.2 确定性行与字段
|
||||
|
||||
项目按 `entity_code ASC, id ASC` 显式排序。唯一 ID 仅作同业务编号异常重复时的兜底,不进入银行文件。
|
||||
|
||||
- 序号:从 1 开始的字符串。
|
||||
- 付款方账号/名称:付款账户快照。
|
||||
- 行别代码:固定字符串 `01`。
|
||||
- 收款方账号/名称、开户行:收款快照。
|
||||
- 联行号:空字符串。
|
||||
- 客户流水号:单一 `buildCustomerSerial(batchNo, rowIndex)` 函数生成 `<batchNo>-R<NNN>`,写入前校验长度、字符集和批内唯一;`R` 显式区分批次序号与行序号。
|
||||
- 金额:初版数值单元格 + 两位小数格式。
|
||||
- 用途:`报销款`。
|
||||
- 备注:报销单编号。
|
||||
- 短信标志:字符串 `0`;手机号与附加信息留空。
|
||||
|
||||
禁止部分成功:任意一项失败时不生成文件、不更新制单状态、不写成功导出审计。
|
||||
|
||||
### 6.3 BIFF8 模板填充与下载
|
||||
|
||||
- 仓库保存一份经脱敏和元数据检查的固定空白 BIFF8 模板资产;它保留三个工作表、15 列表头和三行预格式化空数据行,不含真实业务数据。运行时校验固定 SHA-256,不解析上传文件,也不接受请求控制模板路径。
|
||||
- Node 继续负责解密、资格校验、聚合 blockers、排序、流水号、金额和审计;固定 Python 填充器只接收已经校验的行 DTO,不承担权限或状态机判断。
|
||||
- Node 通过子进程 stdin 发送 JSON,Python 从 stdout 返回 BIFF8 二进制。完整账号不得进入 argv、env、日志或 stderr;子进程使用固定解释器路径、超时和 `maxBuffer`,并校验退出码、输出大小与 OLE 魔数。
|
||||
- Python 使用固定版本 `xlrd==2.0.2`、`xlwt==1.3.0`、`xlutils==2.0.0`,以 `XLWTWriter + process(XLRDReader(...))` 复制原工作簿,再按模板首个数据行样式写值。生产镜像与 CI 从同一份 `--require-hashes` requirements 安装到专用 venv。
|
||||
- `xlutils.filter` 不自动保留 BIFF8 富文本 run。填充器从 `formatting_info=True` 的 `rich_text_runlist_map` 读取每个表头分段,把模板中的完整字体属性转换为 `xlwt.Font`,再显式调用 `write_rich_text`;不得只硬编码红色或重造一种近似字体。
|
||||
- 模板已有三行数据样式;超过三笔时复制第一个数据行的样式并设置相同行高。金额列复制原样式后只把 `num_format_str` 改为 `0.00`,七个标识字段以字符串写入。
|
||||
- 生成器不再依赖 SheetJS;`xlsx` 依赖从服务端移除。先完成审计和制单计数事务,再设置 `application/vnd.ms-excel`、安全附件名、`nosniff`、`private, no-store` 等响应头并发送。
|
||||
- 不写对象存储、临时制单文件或服务器持久目录。网络中断后的已记录导出按保守口径视为已生成,用户可填写新原因重导。
|
||||
|
||||
## 7. 页面动作与进度
|
||||
|
||||
付款批次 DTO 返回脱敏付款账户摘要、`paymentOrderExportCount`、最近导出时间/人和 `canExportPaymentOrder`/阻塞原因。
|
||||
|
||||
- 新快照 + reviewed + 未导出:顶部主动作 `下载银行制单`;回单区仍可直接上传,人工路径不被阻断。
|
||||
- 已导出且无回单:主动作 `上传银行回单`,`重新下载制单` 放入 `更多`。
|
||||
- 重导确认固定展示:`重新下载不会作废之前的文件。如果上一份已在网银导入,请勿重复提交。`
|
||||
- 已有回单:主动作 `登记付款完成`。
|
||||
- 旧批次或资料不完整:不显示可执行制单动作,展示具体原因;原人工付款动作保持。
|
||||
- 详情显示 `未生成制单` 或 `已生成 N 次`;处理进度按是否导出显示已批准的两种文案。
|
||||
|
||||
整体详情页结构统一仍由后续独立任务负责,本任务只在当前操作组和处理进度数据中接入制单状态,不顺带重构页面骨架。
|
||||
|
||||
## 8. 审计、通知与隐私
|
||||
|
||||
审计动作至少覆盖:付款账户创建/替换/停用、公司完整账号查看、员工完整账号查看、银行制单首次导出/重导和失败阻断摘要。审计 extra 只存业务 ID、法人主体、批次号、笔数、合计金额、原因、导出次数和结果代码;不存完整账号、密文、工作簿 buffer 或原始快照。
|
||||
|
||||
普通日志只记录固定错误码、批次 ID/业务号和数量。依赖或解密错误返回自然中文业务错误,禁止裸 `Error` 使页面白屏,也禁止把底层加密异常文本返回客户端。
|
||||
|
||||
## 9. 验证与依赖门禁
|
||||
|
||||
- Node 单元/路由测试覆盖权限矩阵、单一启用账户并发约束、通知、审计、批次快照、历史拒绝、CCB-only、确定性排序、重导和隐私负向断言。
|
||||
- 使用合成数据生成 `.xls`,由 CI 的固定 Python 环境独立读回三个工作表、15 列、七个字符串字段、金额、中文和重复导出一致性,并比较列宽、行高、字体、加粗、底色、边框、对齐及富文本 run 等模板样式签名。
|
||||
- 覆盖超过三笔的扩展行、模板 hash 漂移、解释器/依赖缺失、子进程失败、输出越界和 stderr 隐私负向断言。
|
||||
- 用合成非建行项证明整批失败且没有文件、制单计数或成功审计。
|
||||
- Excel/WPS 人工打开验证中文、长字符串和金额;企业网银只有存在明确“仅校验”步骤时才使用虚构建行数据做最终确认。
|
||||
- 固定 Python requirements 必须带完整哈希并在 CI/镜像使用 `--require-hashes`;任何包或模板哈希不符时暂停,不允许联网浮动安装或自动降级到重建工作簿。
|
||||
|
||||
## 10. 迁移、上线与回滚
|
||||
|
||||
- 迁移只新增表、可空关系、可空快照和制单统计字段,不修改或回填存量账号/批次。
|
||||
- 部署后先由 admin 建立 CCB 主数据与公司付款账户,再由 HR/admin 为员工确认 CCB 机构;这两类生产写入必须另行批准,完整账号不经过聊天或仓库。
|
||||
- 配置完成前暂停创建新的银行渠道付款批次,避免产生无法制单的新历史快照;已有杨俊批次继续人工付款。
|
||||
- 公网 `/ready`、角色/隐私只读检查和一条合成/受控流程通过后才通知财务使用制单。
|
||||
- 代码回滚保留新增表和可空字段,不删除已产生的审计、通知或制单统计;旧应用忽略新字段并继续人工付款。
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,134 @@
|
||||
# 企业银行账户与付款制单实施计划
|
||||
|
||||
## 1. 实施前门禁
|
||||
|
||||
- [x] 用户审查并批准 PRD、设计与本实施计划后,才运行 `task.py start`。
|
||||
- [x] 运行 `trellis-before-dev`,读取 backend/frontend/hyb-oa 相关 spec 与研究材料。
|
||||
- [x] 核对工作树,不提交本地空白银行模板、生成制单文件、真实账号或个人 workspace。
|
||||
- [x] 记录当前 `FIELD_ENCRYPTION_KEY` 载荷兼容口径,但不读取或输出任何密钥/真实账号。
|
||||
|
||||
## 2. 依赖与迁移
|
||||
|
||||
- [x] 以精确官方 tarball安装 SheetJS `0.20.3`,锁定 package-lock 完整性;确认许可证与依赖门禁,不使用 npm registry 的 `[email protected]`。
|
||||
- [x] 新增 `BankInstitution`、`PaymentAccount`,以及 Employee/PaymentBatch 可空关系、内部快照和制单统计字段。
|
||||
- [x] 迁移增加同一法人主体单一 active 付款账户的 PostgreSQL 部分唯一索引;不回填存量批次或员工银行机构。
|
||||
- [x] 更新 Prisma Client 并运行 `npm run prisma:validate`;核对 migration SQL 和回滚兼容说明。
|
||||
|
||||
## 3. 权限、账户与员工收款资料
|
||||
|
||||
- [x] 新增 `payment.account.manage/read/read_full` 与 `payment.order.export`,按设计分配角色;清理 `finance_mgr` 重复的 `payment.batch.manage` 条目。
|
||||
- [x] 实现银行机构和付款账户脱敏 CRUD/替换/停用 API,事务内审计并通知 active admin/CEO。
|
||||
- [x] 实现公司完整账号带原因查看:先写审计,再返回;负向路径不返回明文。
|
||||
- [x] 扩展员工敏感资料接口:默认脱敏、HR/admin 可写银行机构与账号、finance/finance_mgr 只读;完整查看独立带原因审计。
|
||||
- [x] 员工本人 API 返回银行名称与脱敏尾号,不返回完整账号。
|
||||
- [x] 增加管理端企业银行账户页面和人员档案收款信息区;移动端不增加复杂配置入口。
|
||||
|
||||
## 4. 批次冻结快照
|
||||
|
||||
- [x] 对报销银行批次解析并锁定单一法人主体、唯一 active CCB 付款账户和完整员工 CCB 收款资料。
|
||||
- [x] 直接复制现有账号密文进入内部快照;只为脱敏显示做受控解密,不进行解密后再加密。
|
||||
- [x] 扩展付款 DTO,只映射付款账户/收款账户脱敏字段及制单统计,禁止原始 snapshot 或 `v1:` 密文外泄。
|
||||
- [x] 旧批次继续显示兼容信息、允许人工付款,但永久不具备制单资格。
|
||||
|
||||
## 5. BIFF8 制单
|
||||
|
||||
- [x] 建立独立制单服务:固定 15 列和三个工作表、稳定排序、单一流水号函数、七列字符串类型、金额两位小数、中文和 CCB-only 校验。
|
||||
- [x] 实现 POST 制单下载接口:finance/finance_mgr 业务角色 + 独立权限 + reviewed 状态 + 必填原因。
|
||||
- [x] 在生成任何文件前聚合阻塞项;任何非 CCB、历史快照、缺资料或异常均整批失败,不生成部分文件。
|
||||
- [x] 首次导出/重导在事务内写制单统计与脱敏审计,之后以内存 buffer 和 no-store 安全响应头下载,不持久化文件。
|
||||
- [x] 重导确认固定展示重复付款风险原文,不暗示旧文件已作废。
|
||||
|
||||
## 6. 付款详情交互
|
||||
|
||||
- [x] DTO 提供制单次数、最近导出摘要、可制单状态与聚合阻塞原因,页面用自然中文逐项说明。
|
||||
- [ ] 未导出新批次主动作显示“下载银行制单”;已导出无回单时主动作显示“上传银行回单”;当前重导为顶部次动作,待详情页结构统一任务再收进正式“更多”槽位。
|
||||
- [x] 人工付款可直接上传回单,不把制单设为上传或登记付款的门禁。
|
||||
- [x] 详情显示是否已生成制单;处理进度按未导出/已导出显示两种批准文案。
|
||||
- [x] 不在本任务重构完整详情页骨架;与未来详情结构统一任务保持组件边界清楚。
|
||||
|
||||
## 7. 自动化与独立读回
|
||||
|
||||
- [x] 后端测试覆盖权限矩阵、active 唯一约束、账户替换/停用、通知、审计、默认脱敏和完整查看原因。
|
||||
- [x] 付款批次测试覆盖单一法人主体、CCB-only、密文复制、员工资料后改不漂移、旧批次拒绝和普通响应无密文。
|
||||
- [x] 制单测试覆盖 15 列、三个工作表、确定性排序、流水号校验、七列字符串、金额、中文、重导一致性、整批失败和无持久化。
|
||||
- [x] 增加可复现的 `xlrd==2.0.2` 独立读回环境和 CI 步骤;合成文件仅存临时目录并自动清理。
|
||||
- [ ] 前端测试覆盖角色可见性、员工本人脱敏、管理表单、导出原因、重导警告、主动作切换和旧批次人工兜底。
|
||||
|
||||
## 8. 全量验证与只读复核
|
||||
|
||||
- [x] `cd server && npm run prisma:validate`。
|
||||
- [x] 对全部触及的服务端 JS 运行 `node --check`,运行付款批次/用户/导出/通知聚焦 Vitest。
|
||||
- [x] `cd client && npm run typecheck && npm run build`;未新增或运行本任务前端 E2E。
|
||||
- [x] 运行 BIFF8 独立读回脚本;Excel/WPS 人工打开仍待执行。
|
||||
- [x] 运行依赖审计、Trellis governance、process contract、敏感扫描、`git diff --check`;依赖审计保留仓库既有 14 项告警,新增 SheetJS 未命中告警。
|
||||
- [x] 对权限、状态机、迁移、密文数据流和 API 响应执行一次独立只读 review;发现的聚合阻塞项合同漂移已由主流程修复并复验。
|
||||
|
||||
## 9. 文档、提交与上线边界
|
||||
|
||||
- [x] 仅把稳定安全/付款合同更新到现有 spec、产品事实源和管理员手册,不新建无必要业务文档。
|
||||
- [x] 按逻辑分批中文提交,提交信息引用 `corporate-bank-account-payment-order`。
|
||||
- [x] 不在代码任务中写入任何生产银行资料;生产配置、部署和员工资料确认分别取得明确批准。
|
||||
- [ ] 部署后先由 admin 配置 CCB 与公司付款账户,再由 HR/admin 确认员工 CCB;配置完成前暂停新建银行批次。
|
||||
- [x] 旧杨俊批次继续人工付款并上传回单,不补写快照、不尝试制单。
|
||||
- [ ] 公网门禁通过后才通知王芝玲测试制单;没有独立网银校验步骤时不做生产网银导入试验。
|
||||
|
||||
## 10. 本地验证记录
|
||||
|
||||
- 服务端全量:176 个测试文件、1186 项测试通过。
|
||||
- 本任务聚焦:付款批次与 BIFF8 服务 57 项测试通过;公司账户、员工敏感资料和个人资料测试包含在全量结果中。
|
||||
- 前端:TypeScript 检查和生产构建通过。
|
||||
- 数据与文件:Prisma schema 校验通过;`xlrd==2.0.2` 独立读回确认三个工作表、15 列、长账号字符串、`01` 字符串、数值金额与中文。
|
||||
- 治理:Trellis task 校验、governance、process contract、敏感内容扫描和 `git diff --check` 通过;文档注册审计仅保留仓库既有 22 个未登记文件警告。
|
||||
- 依赖:`[email protected]` 来自官方精确 tarball且锁定;生产依赖审计仍有 14 项仓库既有告警,未执行破坏性 `npm audit fix --force`。
|
||||
- 尚未完成:Excel/WPS 人工打开、前端 E2E/截图、生产数据库迁移、真实资料配置、部署、公网检查和企业网银校验。
|
||||
|
||||
## 11. 原模板保真修订(2026-08-03)
|
||||
|
||||
- [x] Excel 人工打开否决 SheetJS 重建输出:字段合同正确,但原模板列宽、行高、字体、底色、边框和对齐丢失。
|
||||
- [x] 用户批准改为“固定脱敏原模板 + Python BIFF8 保真填充”,不再接受重建工作簿方案。
|
||||
- [x] 提交经敏感检查的空白模板资产并固定 SHA-256;不提交 `input/` 原件或任何生成制单文件。
|
||||
- [x] 用固定 Python requirements 实现 stdin JSON / stdout BIFF8 填充器,stderr 仅固定错误码。
|
||||
- [x] Node 改为子进程桥接,保留现有资格、冻结快照、blockers、排序、流水号、审计与导出统计合同。
|
||||
- [x] 生产镜像和 CI 使用同一带哈希 requirements 和绝对 venv 解释器;移除服务端 SheetJS 生成依赖。
|
||||
- [x] 独立读回增加模板视觉签名、超过三行扩展、模板/运行时失败和隐私负向测试。
|
||||
- [x] 重新运行聚焦/全量后端、前端、治理和只读复核;用合成数据交付 Excel 人工复核,不部署。
|
||||
|
||||
### 修订验证记录
|
||||
|
||||
- 受控模板仅有 15 个表头和三行预格式化空数据行;模板 SHA-256 与运行时常量一致,字符串扫描未发现账号、URL、凭据或业务数据。
|
||||
- 固定 Python 环境的独立读回已确认三个工作表、15 列、账号/`01` 文本、数值金额 `0.00`,并逐格比较列宽、行高、字体/加粗、底色、边框、对齐;第四笔扩展行同样通过。
|
||||
- 聚焦测试在资格门禁修订后为 70 项通过;新增覆盖 admin 非财务拒绝、四种非 reviewed 状态、历史 item 非银行 override、模板 hash、依赖缺失、子进程超时/输出越界和 stderr 隐私。
|
||||
- 后端全量最终为 176 个文件、1199 项全部通过;此前 Dockerfile 缓存层文本合同失败已通过恢复独立 `RUN cd server && npm ci` 修复并纳入最终全量。
|
||||
- 前端 typecheck/build、Prisma validate、流程合同、Trellis governance、敏感扫描和 diff 检查通过;文档审计仅保留仓库既有 22 个未登记文件警告。
|
||||
- 本地生产镜像已真实构建通过;Alpine Python 3.14.5 成功从带哈希 requirements 安装固定依赖,镜像内生成与独立 BIFF8 读回合同通过。部署、公网与网银验证仍待单独批准。
|
||||
- 当前实现已生成并独立读回一份 gitignored 合成 Excel 验收文件;用户已用 Excel 确认当前实现文件外观可接受。
|
||||
- 付款/隐私只读复核在资格门禁修订后确认无 Blocker、High 或 Medium;唯一 Low(失败路径缺显式“无成功审计”断言)已补测试并复验。
|
||||
|
||||
## 12. Excel 富文本与流水号可读性修订(2026-08-03)
|
||||
|
||||
- [x] Excel 人工验收发现原模板红色必填星号在生成文件中变黑;`xlrd` 诊断确认原模板存在 rich-text run,而生成文件的 run map 为空。
|
||||
- [x] 用户批准显式恢复模板富文本,并把客户流水号从 `批次号-001` 改为 `批次号-R001`;首份文件尚未导入网银,不产生历史格式兼容成本。
|
||||
- [x] 从模板逐段复制全部字体属性并调用 `write_rich_text`,不硬编码近似红色字体。
|
||||
- [x] 独立读回逐段断言必填星号红色、其余文字黑色,并覆盖模板其他富文本表头。
|
||||
- [x] 更新流水号单元测试与独立读回,重新生成 Excel 交付人工验收。
|
||||
- [x] 完成聚焦/全量验证和生产镜像真实构建;构建通过前不批准部署。
|
||||
|
||||
### 富文本修订验证记录
|
||||
|
||||
- 模板读取保持 `formatting_info=True`;填充器显式恢复所有模板富文本 run,并逐段复制字体名、字号、粗体、颜色、字符集及其他字体属性。
|
||||
- 独立读回逐段比较模板与生成文件,确认必填星号使用模板红色、其余表头文字使用模板黑色;第四笔扩展行样式仍与模板首个数据行一致。
|
||||
- 客户流水号已统一为 `批次号-R三位行号`,合成验收文件确认首行 `R001`、第四行 `R004`;固定验证夹具日期与 PRD 示例统一为 2026-08-03。
|
||||
- 一次人工验收文件校验失败被定位为诊断阶段名滞后:空短信附加信息实际为 BIFF8 `BLANK`,真正失败来自后续固定流水号夹具日期不一致;校验器已为字段值断言设置独立阶段名,避免再次误报为空单元格问题。
|
||||
- 聚焦测试 70 项、服务端全量 1199 项、宿主机独立 BIFF8 合同和已构建生产镜像内合同均通过;未部署、未写生产银行资料。
|
||||
- 用户已使用 Excel 打开最终合成验收文件,确认红色必填星号和 `R001` 流水号显示符合预期,人工外观验收通过。
|
||||
|
||||
## 13. 生产部署记录(2026-08-03)
|
||||
|
||||
- [x] 发布提交已进入 upstream,标准来源校验确认 published,不使用 `UNPUBLISHED-OVERRIDE`。
|
||||
- [x] 部署前 13 项只读聚合体检全部通过;普通部署身份、root forced-command、数据挂载、运行 build 与备份合同均无漂移。
|
||||
- [x] 生产远端真实完成 Alpine Python、专用 venv 和带哈希 BIFF8 依赖构建;系统 Python 层约增加 73 MiB。
|
||||
- [x] 生产迁移 `20260802143000_corporate_bank_account_payment_order` 成功应用,migration status 为 up to date;seed 只新增一条 `CCB` 银行机构,不写公司账户或员工账号。
|
||||
- [x] `/ready`、external deps smoke、runtime smoke、公网页面资源扫描与前后端 build 标识核对通过;生产容器内 BIFF8 原模板合同再次通过。
|
||||
- [x] `deploy-existing.sh` 自动刷新四个备份/WARCH3 build drop-in 成功;部署后再次运行 13 项体检仍为 `PASS=13 FAIL=0`。
|
||||
- [ ] 带账号 live smoke 与真实 OCR 因本机缺少测试凭据/样票分别保持 `SKIP`;不得据此宣称真实账号业务流或真实 OCR 已验收。
|
||||
- [ ] 生产公司付款账户与员工规范化银行资料尚未配置;完成前不通知财务测试新制单功能。
|
||||
@@ -0,0 +1,143 @@
|
||||
# 企业银行账户与付款制单
|
||||
|
||||
## Goal
|
||||
|
||||
为财务付款批次提供受控的公司付款账户、员工收款银行标识和企业网银 BIFF8 `.xls` 制单能力,使付款复核看到的收款账户与实际导出的账户同源,同时避免完整银行账号在普通页面、日志、审计或服务器文件中泄露。
|
||||
|
||||
## Background
|
||||
|
||||
- 用户提供的空白银行制单模板为传统 BIFF8 `.xls`,包含三个工作表;第一个工作表有 15 列表头,另两个工作表为空。
|
||||
- 银行导入合同明确要求 `.xls`,不再把 `.xlsx` 或 CSV 作为首版候选。
|
||||
- 模板必填字段为序号、付款方账号、付款方名称、收款方行别代码、收款方账号、收款方名称、金额和用途。
|
||||
- 当前系统只有员工开户银行自由文本和加密账号;没有规范化银行标识、公司付款账户模型或银行制单导出。
|
||||
- 当前付款批次快照只冻结收款人姓名、开户银行和脱敏尾号。历史兼容路径可用于展示,但不得用于真实划款制单。
|
||||
- MVP 采用每个法人主体仅一条启用的公司付款账户;数据模型仍应允许将来存在历史账户或多账户,不把账号直接堆到 `LegalEntity`。
|
||||
- 当前公司付款账户与员工工资卡均为中国建设银行;首版生产场景限定为建行本行转账。
|
||||
|
||||
## Scope
|
||||
|
||||
- 建立法人主体维度的受控企业银行账户、规范化银行机构和员工收款资料维护能力。
|
||||
- 在新建银行付款批次中冻结公司付款账户与员工收款账户,普通页面与 API 仅展示脱敏摘要。
|
||||
- 为符合条件的已复核建行批次生成确定性的 BIFF8 `.xls` 制单文件,并记录带原因的逐次审计。
|
||||
- 保留历史批次和异常场景的人工付款、上传回单与登记完成路径;不连接银行、不自动提交或放款。
|
||||
- 生产银行资料写入、部署、公网验证和企业网银导入不属于本次代码实施授权范围。
|
||||
|
||||
## Requirements
|
||||
|
||||
### R1 公司付款账户
|
||||
|
||||
- 新建隶属于法人主体的付款账户资料,至少包含账户名称、加密账号、规范化银行标识、银行显示名称、启用/归档状态及审计字段。
|
||||
- MVP 对同一法人主体只允许一条启用记录;更换账户不得覆盖历史记录。
|
||||
- 普通页面默认只展示开户行和脱敏尾号;完整账号不得随列表或普通详情响应返回。
|
||||
- 公司付款账户不得放入 `.env`、Feature Flag、普通 JSON 偏好或明文配置值。
|
||||
- 公司付款账户维护的主要风险是误操作、制单失效、错误公司账户出款和缺少及时知情,不将其描述为改变收款人的欺诈路径,也不为 MVP 增加审批流或双人确认。
|
||||
- 公司付款账户新增、变更、停用仅授权 `finance_mgr` 与 `admin`;当前没有 active `finance_mgr` 时由 `admin` 维护。
|
||||
- `finance` 只能读取公司付款账户的开户行与脱敏尾号并在授权状态下制单,不得新增、修改、停用或归档付款账户。
|
||||
- `finance`、`finance_mgr` 与 `admin` 仅可通过独立“查看完整账号”动作填写原因后查看公司付款账户完整账号;每次查看必须先写脱敏审计,普通查询永不返回完整账号。
|
||||
- `hr`、`manager`、`ceo` 与 `system_admin` 不获得公司付款账户主数据读取或维护权限;付款批次合法复核人仅可在该批次中看到冻结的付款账户脱敏尾号。`admin` 的全权限作为受控管理入口保留。
|
||||
- 新增、变更、停用付款账户除写审计外,还必须即时通知所有 active `admin` 与 `ceo`;通知只含法人主体、动作、操作者、时间和脱敏尾号,不含完整账号或密文。
|
||||
|
||||
### R2 员工收款银行标识
|
||||
|
||||
- 员工收款资料增加从受控选项选择的规范化银行标识,不依赖“建设银行/建行/某某分行”等自由文本比较,也不直接保存“本行/他行”这种相对关系。
|
||||
- 在人员档案提供“收款信息”入口:HR 与 `admin` 可维护规范化开户银行和账号;`finance`、`finance_mgr` 只读,不能编辑。
|
||||
- 员工本人可查看自己的开户银行和脱敏尾号,但不直接查看完整账号、不自助修改;HR/admin/finance/finance_mgr 默认同样只看脱敏尾号。
|
||||
- HR、`admin`、`finance` 与 `finance_mgr` 查看员工完整账号时必须通过独立动作填写原因并先写查看审计;现有默认返回完整账号的敏感资料读取路径必须收紧为默认脱敏。
|
||||
- 首版仅支持规范化标识为 `CCB` 的公司付款账户和员工收款账户;双方均为建行时生成字符串 `01` 并保留前导零。
|
||||
- 任一批次项的收款银行不是建行时,拒绝整份制单并列出对应员工及处理建议;不得生成 `02`、不得跳过该项、不得生成部分文件。
|
||||
- `employee.bank.manage` 继续只授予 HR 与 `admin`,不得为了财务制单便利把该权限授予 `finance`、`finance_mgr` 或 `system_admin`。
|
||||
- 真实资金欺诈的关键路径是“修改收款账号 -> 制单导出 -> 向错误受益人付款”;员工收款资料维护与财务制单必须保持角色分离。
|
||||
|
||||
### R3 可追溯收款快照
|
||||
|
||||
- 创建付款批次时直接复制员工已有的银行账号密文进入内部快照,不执行“解密后再加密”。
|
||||
- 复核页面的脱敏尾号与制单导出的完整账号必须来自同一份批次快照。
|
||||
- 员工资料在批次创建后发生变化,不得静默改变已复核批次的制单内容。
|
||||
- 缺少加密账号快照的历史批次一律禁止制单导出,不允许实时回查员工资料兜底,并返回明确错误。
|
||||
- 普通付款 DTO、AP 页面、日志和审计不得返回密文、完整账号或原始快照。
|
||||
- 付款批次快照同时冻结所选公司付款账户的身份与脱敏尾号;付款复核页面应显示该脱敏尾号,使复核人能回答“从哪个账户付、付给谁、付多少”。
|
||||
|
||||
### R4 银行制单导出
|
||||
|
||||
- 从符合状态与权限要求的付款批次生成与受控空白原模板视觉和结构一致的 BIFF8 `.xls`;不得重建一个仅字段相同但丢失列宽、行高、字体、底色、边框或对齐的工作簿。
|
||||
- 银行制单是推荐的效率工具而非付款完成硬门禁;旧快照、格式异常或人工付款场景仍可通过线下转账、上传回单和登记完成闭环,银行回单继续是银行渠道付款完成的证据门禁。
|
||||
- 新批次已复核、资料完整且尚未制单时,顶部主动作显示“下载银行制单”;首次制单后主动作切换为“上传银行回单”,重新下载进入“更多”。已经人工付款的用户可直接在回单区域上传,不被制单状态阻断。
|
||||
- 批次详情显示是否已生成制单;处理进度在未制单时显示“等待财务线下付款并上传回单”,制单后显示“已生成银行制单,等待付款完成并上传回单”。
|
||||
- 仅 `reviewed`(待财务付款)批次允许制单;`draft`、`failed`、`cancelled` 与 `executed` 均禁止制单。
|
||||
- `reviewed` 状态允许因文件丢失或本地操作失败重复导出,每次仍须填写原因并独立审计;重新下载前必须明确提示“重新下载不会作废之前的文件。如果上一份已在网银导入,请勿重复提交。”重复导出的业务字段必须来自同一冻结快照,不允许临时覆盖账号、金额或用途。
|
||||
- `executed` 后不得重新生成可导入网银的指令文件;付款归档依赖银行回单与付款台账,避免把归档需要和可重复付款指令混为一体。
|
||||
- 原样保留三个工作表、15 列顺序与表头,以及原模板的列宽、表头/数据行行高、字体、加粗、底色、边框、对齐、换行和单元格内富文本分段;必填表头的首个 `*` 保持原模板红色,其余文字保持原模板黑色和字体属性。不得把格式伪装成 `.xls` 扩展名。
|
||||
- 仓库可提交一份经脱敏检查、仅含空白表头和预格式化空行的受控模板资产;运行时只读取该固定资产,不接受用户上传模板或请求指定路径。模板哈希漂移时制单失败关闭。
|
||||
- 模板预置数据行不足时,新行必须复制首个数据行的样式和行高;金额列仅覆盖为数值类型与 `0.00` 格式,不改变其他视觉属性。
|
||||
- 用途固定为“报销款”;客户方流水号格式为 `<付款批次编号>-R<三位行序号>`,例如 `PB-20260803-001-R001`;备注写对应报销单编号;开户行名称取冻结快照;付款凭证号不进入制单。
|
||||
- 批次项必须使用显式、稳定且带唯一兜底键的排序,禁止依赖数据库自然顺序;同一批次重复导出时每项的行序号和客户流水号必须不变。
|
||||
- 客户方流水号由单一生成函数负责,并在写文件前校验长度、字符集和批次内唯一性;不允许静默截断或分散内联拼接。首轮验证若证明银行只接受纯数字,应只调整该生成函数与对应合同。
|
||||
- 付款方账号、收款方账号、联行号、客户流水号、手机号、行别代码和短信通知标志七列必须以字符串单元格写入,禁止数值类型推断;短信通知标志首版写字符串 `0`。
|
||||
- 金额首版按数值写入并设置两位小数格式,必须无科学计数法和精度损失;若银行安全导入验证明确拒绝,再改为两位小数字符串。
|
||||
- 收款人名称、用途与备注中文不得乱码。
|
||||
- 收款方联行号首版固定留空;首次安全验证使用字符串 `01`、建行虚构账号和空联行号,覆盖真实生产主场景。跨行 `02` 与联行号留待支持非建行员工时另立增量任务。
|
||||
- 导出文件通过响应直接下载,不持久化到服务器存储,不记录原始制单内容;只有脱敏空白模板资产可以进入 Git。
|
||||
- 完整账号只可通过子进程标准输入传给固定填充器,不得进入命令行参数、环境变量、日志或错误输出;生成的二进制只从标准输出返回,并设置明确的大小上限。
|
||||
- 导出使用独立权限、必填原因和逐次审计;审计只记录操作人、批次、笔数、合计金额及原因,不记录完整账号。
|
||||
- 旧快照批次、非银行渠道、缺少付款账户、缺少员工银行标识或账号、公司或员工银行不是建行的批次必须在导出前一次性列出不可制单原因。
|
||||
|
||||
### R5 本地与人工验证
|
||||
|
||||
- 使用合成数据生成 `.xls`,再用独立解析验证工作表、表头、字段类型、中文、金额和模板视觉签名;至少覆盖列宽、行高、字体、加粗、底色、边框、对齐和表头富文本分段,逐段断言必填星号红色、其余文字黑色。
|
||||
- 独立解析器及版本必须进入可复现的测试环境,不能依赖某台开发机偶然安装的 `xlrd`。
|
||||
- 至少使用 Excel 或 WPS 人工打开一次,确认中文和账号显示。
|
||||
- 只有企业网银存在明确的“仅校验、不提交”环节时,才允许用完全虚构数据做最终格式确认;否则不进行生产网银试验。
|
||||
|
||||
### R6 依赖安全
|
||||
|
||||
- BIFF8 生成依赖必须在设计阶段确定受支持版本、来源、锁定和完整性校验方式;CI 与生产镜像必须使用同一份带哈希的固定 Python requirements。
|
||||
- 不得静默接受已知高危依赖告警;若只能使用存在已知解析漏洞的版本,必须证明运行路径只生成、不解析不可信输入,并单独获得风险例外批准。
|
||||
|
||||
## Test Plan
|
||||
|
||||
- 验证 Prisma schema 与迁移,检查单一 active 付款账户约束和存量数据兼容边界。
|
||||
- 运行公司付款账户、员工敏感资料、付款批次、个人资料和 BIFF8 生成服务的聚焦测试及服务端全量测试。
|
||||
- 使用合成数据生成 BIFF8 文件,并由固定版本 `xlrd` 独立读回工作表、表头、字段类型、金额与中文。
|
||||
- 运行前端 TypeScript 检查和生产构建,核对权限可见性、默认脱敏、带原因 reveal、制单与人工付款动作。
|
||||
- 执行权限、状态机、迁移、密文流和普通 API DTO 的只读复核,并运行 Trellis、流程、文档、敏感内容与 diff 门禁。
|
||||
- Excel/WPS 人工打开、生产网银仅校验、公网部署与真实银行资料配置分别作为需额外授权的上线验收,不纳入本地自动化通过声明。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] 管理入口可为法人主体建立、启用、停用和归档公司付款账户,默认仅显示开户行与脱敏尾号。
|
||||
- [ ] 只有 `finance_mgr` 与 `admin` 能维护公司付款账户;`finance` 只能看脱敏信息和执行授权制单,HR/经理/CEO/system_admin 无主数据访问权。
|
||||
- [ ] `finance`、`finance_mgr` 与 `admin` 查看公司完整账号时必须填写原因并留下成功查看审计,普通列表和详情响应不含完整账号。
|
||||
- [ ] 公司付款账户新增、变更或停用后,active admin 与 CEO 收到不含完整账号的即时通知,并存在脱敏审计。
|
||||
- [ ] 同一法人主体无法同时存在两条启用的付款账户,历史账户不会被覆盖。
|
||||
- [ ] 员工收款资料通过受控选项维护规范化银行标识;公司和员工均为 `CCB` 时生成字符串 `01`,任一非建行项导致整批制单失败且不遗漏任何付款项。
|
||||
- [ ] 员工本人可核对自己的开户银行和脱敏尾号;HR/admin 可维护,finance/finance_mgr 只读,所有授权完整账号查看均要求原因并留审计。
|
||||
- [ ] `finance`、`finance_mgr` 与 `system_admin` 均不能修改员工收款账号或银行标识,现有 HR/财务职责分离没有被放宽。
|
||||
- [ ] 新付款批次冻结加密完整账号;普通 API 与页面只返回脱敏信息。
|
||||
- [ ] 修改员工收款资料后,既有批次的复核尾号和制单内容保持不变。
|
||||
- [ ] 付款复核页面同时显示冻结的公司付款账户脱敏尾号、收款人、收款账户尾号和金额。
|
||||
- [ ] 历史批次制单请求被明确拒绝,且不会实时读取当前员工完整账号。
|
||||
- [ ] 未制单批次、已制单批次和人工付款路径均可按既定主动作/次动作继续,制单状态不是上传回单或登记付款完成的硬门禁。
|
||||
- [ ] 批次详情和处理进度能区分“未生成制单”与“已生成制单待回单”,便于观察实际使用覆盖率。
|
||||
- [ ] 授权财务人员填写原因后可下载符合模板的 BIFF8 `.xls`,下载行为留下脱敏审计。
|
||||
- [x] Excel 打开生成文件时,原模板列宽、行高、字体、加粗、底色、边框、对齐和换行保持不变;超过三笔时新增行仍复制相同数据行样式。
|
||||
- [x] 所有必填表头逐段保留原模板富文本:首个星号红色、其余文字黑色;客户流水号使用 `批次号-R三位行号`,不存在两个无标签 `001` 连续造成的误读。
|
||||
- [ ] 重新下载前展示重复付款风险原文;每次重导独立记录原因,但不会宣称旧文件已失效。
|
||||
- [ ] 只有 `reviewed` 批次可制单;同一 `reviewed` 批次可带新原因重导,其他四种状态均被明确拒绝。
|
||||
- [ ] 批次进入 `executed` 后无法重新生成银行制单,银行回单与付款台账仍可用于归档核验。
|
||||
- [ ] 七个标识/开关字段读回均为字符串,长账号无精度丢失,`01` 不丢前导零,短信标志精确为字符串 `0`。
|
||||
- [ ] 批次项按显式确定性顺序生成;同一冻结批次重复导出时,每张报销的行号、客户流水号和业务内容逐项一致。
|
||||
- [ ] 客户流水号的长度、字符集与唯一性校验失败时制单被明确阻断,不发生截断、替换或自动降级。
|
||||
- [ ] 金额读回值精确到两位小数且无科学计数法;数值/字符串类型最终以安全银行验证结果固化。
|
||||
- [ ] 使用字符串 `01`、建行合成账号和空联行号完成本地验证;合成非建行项证明整批失败且不会生成部分文件。
|
||||
- [ ] 缺失资料或状态不合格时,页面一次性说明全部阻塞项及处理方式。
|
||||
- [ ] 自动化、本地人工打开和可选网银校验均使用合成数据,不触发真实付款。
|
||||
- [ ] 模板哈希漂移、Python 或固定依赖缺失、填充器失败时返回固定安全错误,stderr、日志和测试证据均不含完整账号或原始 payload,人工付款路径不受影响。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 工资代发、供应商付款、跨境付款和多币种制单。
|
||||
- 银行 API 直连、自动提交付款、自动放款或自动复核。
|
||||
- 首版支持多个同时启用的公司付款账户。
|
||||
- 非建行、跨行 `02`、联行号维护和混合银行批次制单。
|
||||
- 为历史付款批次补写或回填完整账号快照。
|
||||
- 将真实账号、含业务数据的模板或生成的制单文件提交到仓库;经检查的脱敏空白模板资产除外。
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
# 企业银行账户与 BIFF8 制单研究
|
||||
|
||||
## 模板证据
|
||||
|
||||
- 用户提供的空白模板是真实 BIFF8 `.xls`(OLE Compound Document),代码页 936,无宏、无外部链接。
|
||||
- 工作簿固定包含 `Sheet1`、`Sheet2`、`Sheet3`;只有 `Sheet1` 有 15 列表头,另两个工作表为空。
|
||||
- 必填列为序号、付款方账号/名称、收款方行别代码、收款方账号/名称、金额、用途。
|
||||
- 数据行多数使用文本格式;账号、行别代码、联行号、流水号、手机号和短信标志必须在写入值时显式使用字符串类型,不能依赖显示格式补救。
|
||||
- 2026-08-03 Excel 人工打开证明 SheetJS 重建工作簿虽满足字段合同,但无法保留原模板列宽、行高、宋体/加粗、底色、边框和对齐,因此该方案被否决。
|
||||
- 原始 `input/` 文件仍只作为本地分析输入;实施允许从中制作一份经脱敏、元数据和样式签名检查的受控空白模板资产进入 Git 与运行时镜像。含业务数据的模板和生成文件仍禁止提交。
|
||||
|
||||
## 当前数据与权限
|
||||
|
||||
- `Employee` 只有 `bank_name` 与 `bank_account_encrypted`,没有规范化银行机构关系;现有密文字段使用 `FIELD_ENCRYPTION_KEY` 的 `v1:` 载荷。
|
||||
- `buildPayeeSnapshot()` 当前会解密员工账号后只保留脱敏尾号;付款 API 通过 `readSnapshotPayee()` 映射脱敏 DTO,历史项可用 `loadLegacyPayees()` 实时回查展示。
|
||||
- 历史实时回查只适合兼容展示,不能用于制单;旧批次没有冻结完整密文时必须拒绝制单。
|
||||
- `LegalEntity` 已是员工、成本中心、供应商等主数据锚点,独立 `PaymentAccount` 关系比直接向 `LegalEntity` 堆账号字段更适合保留历史和未来多账户。
|
||||
- 当前角色:`finance` 有 `payment.batch.manage` 与 `employee.bank.read`;`finance_mgr` 另有 `system.settings`、`system.audit.view`;HR 有 `employee.bank.manage`;`system_admin` 没有银行读取权限;`admin` 为通配权限。
|
||||
- `finance_mgr` 权限数组中 `payment.batch.manage` 重复两次,行为无害,可在本任务触及权限表时机械清理。
|
||||
|
||||
## 现有可复用模式
|
||||
|
||||
- 敏感查看/下载已有“填写原因 -> 服务端校验 -> 先写审计 -> 再返回内容”的模式,参考用户敏感资料查看、附件用途对话框和高敏附件下载合同。
|
||||
- 通知可复用 `createNotificationsWithRecords()` 与按角色解析 active 员工的查询方式;付款账户通知只发送脱敏摘要。
|
||||
- 财务导出已有内存生成、审计与 Blob 下载模式,但现有 `ExcelJS` 只能生成 `.xlsx`,不能满足 BIFF8 合同。
|
||||
- 管理配置应进入 PC `管理` 路由下的明确子页面;复杂配置与制单不进入移动端。
|
||||
|
||||
## BIFF8 依赖结论
|
||||
|
||||
- SheetJS CE 即使读取模板并启用样式选项后再写 BIFF8,仍无法保真复制字体、底色、边框、对齐和行高,因此从运行时生成路径移除。
|
||||
- 隔离验证确认 `xlrd==2.0.2`、`xlwt==1.3.0`、`xlutils==2.0.0` 使用 `XLWTWriter + process(XLRDReader(...))` 可保留原模板视觉合同;金额列可在复制样式后单独覆盖 `0.00`。
|
||||
- 固定 Python 填充器只读取仓库内受控模板,只从 stdin 接受 Node 已校验的合成行 DTO,并把二进制写 stdout;不接受上传模板或外部路径。
|
||||
- Python 包必须以带哈希 requirements 固定,并由 CI 与生产镜像同源安装。解释器、依赖、模板或哈希异常均 fail closed,不回退到无样式生成。
|
||||
|
||||
## 独立读回验证
|
||||
|
||||
- 当前开发机可用 `xlrd 2.0.2`,但本轮需新增最小、带哈希的固定 requirements,使生成和读回环境在 CI 与镜像可复现。
|
||||
- 生成由固定 Python 模板填充器完成;合同脚本再以独立打开步骤读回并比较原模板样式签名,而不是依赖生成器返回的内部对象。
|
||||
- 验证文件必须写入临时目录并在结束后清理;失败输出只报告字段名和断言,不打印合成账号之外的任何内容。
|
||||
|
||||
## 关键设计约束
|
||||
|
||||
- 当前生产主场景是建设银行公司账户向建设银行员工工资卡付款;首版只生成字符串 `01`,跨行 `02` 明确不支持。
|
||||
- 任何非 `CCB` 员工项必须使整批制单失败,不能跳行或生成部分文件。
|
||||
- 批次项必须显式按稳定业务编号并以唯一 ID 兜底排序,确保重复导出的行号和流水号一致。
|
||||
- 制单是推荐效率工具,不是付款完成门禁;旧批次和人工付款路径继续以银行回单为完成证据。
|
||||
- 首次导出后的处理进度需要可见,但完整文件不在服务端留存;批次只记录导出次数、最近导出时间/人和审计证据。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "corporate-bank-account-payment-order",
|
||||
"name": "corporate-bank-account-payment-order",
|
||||
"title": "企业银行账户与付款制单",
|
||||
"description": "建立法人主体企业银行账户、员工受控收款信息、冻结付款快照与建设银行 BIFF8 制单能力。",
|
||||
"status": "completed",
|
||||
"dev_type": "feature",
|
||||
"scope": "公司付款账户、员工收款资料、付款批次冻结快照与建设银行 BIFF8 制单",
|
||||
"package": "hyb-oa",
|
||||
"priority": "P2",
|
||||
"creator": "shunli",
|
||||
"assignee": "shunli",
|
||||
"createdAt": "2026-08-02",
|
||||
"completedAt": "2026-08-03",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": "5261d2849bf3098b433f7356d22a4df2fff94cee",
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "公司付款账户、员工规范银行标识、冻结快照与建行 BIFF8 制单已部署;Excel 人工验收、生产迁移、公网检查、公司付款账户和杨俊银行机构配置均完成。下一张自然形成的新批次再做离线制单验证;只有网银存在明确的仅校验不提交环节才进行可选导入,该运营触发不继续占用开发任务。",
|
||||
"meta": {}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
# 部署前聚合体检与备份合同自动刷新:设计
|
||||
|
||||
## 1. 边界与组件
|
||||
|
||||
本任务只改已有部署入口,不改变首次初始化状态机:
|
||||
|
||||
1. 本地只读聚合体检入口读取受控 unit 六键,并分别通过普通部署 SSH 与专用 forced-command SSH 验证远端事实。
|
||||
2. `deploy-existing.sh` 在体检全绿后继续调用既有 `deploy.sh`;`deploy.sh` 的 fail-fast remote preflight 保留。
|
||||
3. 应用、公网检查全部通过后,`deploy-existing.sh` 调用 root forced-command 的 `refresh`。
|
||||
4. root-owned 自包含程序只维护四个固定 service 的 build drop-in,并验证四个既有 timer。
|
||||
|
||||
不新增后台服务、数据库对象、业务 API 或业务页面。
|
||||
|
||||
## 2. 聚合体检执行模型
|
||||
|
||||
### 2.1 入口与输入
|
||||
|
||||
- 普通部署目标继续使用 `DEPLOY_SERVER`。
|
||||
- 独立维护目标使用新的显式 `DEPLOY_REFRESH_SERVER`;格式使用现有安全 SSH target 字符集,不从主机名、用户名或别名规则猜测。
|
||||
- 六键仍以 automatic-backup daily unit 的 allowlisted `Environment=` 为事实源,不回显值。
|
||||
- 共享只读解析函数可由体检与 `deploy-existing.sh` 复用,避免两份六键 allowlist 漂移;不得把解析结果写入仓库或临时报告。
|
||||
|
||||
### 2.2 不短路聚合器
|
||||
|
||||
主脚本保留 `set -uo pipefail`,明确不启用 `set -e`。每项使用以下等价模型:
|
||||
|
||||
```bash
|
||||
run_check() {
|
||||
local check_name="$1"
|
||||
shift
|
||||
if ("$@") >/dev/null 2>&1; then
|
||||
record_result "$check_name" PASS
|
||||
else
|
||||
record_result "$check_name" FAIL
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
检查函数在子 shell 中运行,不能修改主流程状态。聚合器记录所有结果后统一输出固定顺序的 `检查项 PASS/FAIL` 和计数,最后按总失败数返回 0/非 0。依赖项无法取得时,依赖检查返回 FAIL;无依赖检查仍继续执行。
|
||||
|
||||
### 2.3 检查分组
|
||||
|
||||
- 本地/合同:普通 SSH transport、六键齐全、root SSH transport、forced-command probe。
|
||||
- 普通部署身份:仓库根标识、目录可写、env 普通文件且可读、PGDATA/附件存在。
|
||||
- 运行时:唯一 server/db、活动 build、附件 mount、PGDATA mount。
|
||||
- 备份合同:daily unit build 等于活动 build。
|
||||
- root 收尾能力:forced-command 程序可执行并返回固定 probe 标识。
|
||||
|
||||
所有检查仅把 stdout/stderr 捕获到内存并丢弃;用户可见输出由聚合器根据固定结果生成,不转发底层错误文本。
|
||||
|
||||
## 3. forced-command 安全模型
|
||||
|
||||
### 3.1 密钥边界
|
||||
|
||||
- 使用独立公钥。安装器解析 key blob/fingerprint,若 root `authorized_keys` 已存在同 key blob 的非 managed、非 forced-command 行则拒绝安装。
|
||||
- managed 行使用固定标记,可原子替换自身,保留其他 root 管理 key;不得覆盖整个 `authorized_keys`。
|
||||
- 授权选项固定为 `command="<root-owned-target>",no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-pty,no-user-rc`。
|
||||
- 专用本地 SSH alias 只使用该 key。普通 root 应急管理 key 不属于部署自动化合同。
|
||||
|
||||
### 3.2 自包含程序
|
||||
|
||||
- 安装器把审核后的源文件复制到固定 `/usr/local/sbin` 目标,验证 root:root、`0755`、regular file、非 symlink。
|
||||
- 程序启动后设置固定 `PATH`、`umask 077`,清理调用方可控的路径/loader 类环境变量,仅读取 root-owned systemd unit、Docker inspect 结果和 `SSH_ORIGINAL_COMMAND`。
|
||||
- 精确命令矩阵:`probe` 只验证运行前提;`refresh` 执行 build drop-in 更新;空值、空白变体、额外参数和其他命令全部返回固定 unsupported 分类。
|
||||
- 程序不得 source 或 exec 仓库内容,不接受 argv、stdin 配置、路径、unit、SHA 或 shell 片段。
|
||||
|
||||
## 4. build 刷新合同
|
||||
|
||||
### 4.1 事实读取
|
||||
|
||||
1. 从 root-owned automatic-backup daily unit 读取固定 allowlist:Compose project 与当前工作目录/env 合同只用于定位唯一活动 server。
|
||||
2. 通过 Docker labels/inspect 定位唯一 running server,读取容器 `BUILD_SHA`。
|
||||
3. build 必须为完整小写十六进制提交标识;前端标识固定取前 12 位,不接受调用方输入。
|
||||
|
||||
### 4.2 最小写入
|
||||
|
||||
- 固定目标仅为 automatic-backup daily/watchdog/drill-reminder 与 WARCH3 四个 service。
|
||||
- 对每个 service 原子安装 root-owned `20-active-build.conf` drop-in,只包含 `BUILD_SHA` 和 `VITE_BUILD_SHA` 覆盖;不改主 unit、WorkingDirectory、env 路径、ExecStart 或 timer 配置。
|
||||
- 所有 drop-in 写入完成后执行一次 `systemctl daemon-reload`。
|
||||
- 读取 systemd 最终合并后的 Environment,逐个断言两个 SHA;再逐个断言四个 timer `is-enabled=enabled` 且 `is-active=active`。
|
||||
- 使用固定 root-owned lock 防止两个 refresh 并发。中途失败不得报告成功;已写 drop-in 不自动回滚到旧 SHA,因为旧 SHA 已与活动应用不一致,人工修复应继续向活动 build 收敛。
|
||||
|
||||
## 5. 调用顺序与失败语义
|
||||
|
||||
```text
|
||||
聚合体检全绿
|
||||
-> deploy.sh(同步、构建、迁移、/ready、smoke、公网检查)
|
||||
-> forced-command refresh
|
||||
-> unit SHA + timer 验证
|
||||
-> 部署完整完成
|
||||
```
|
||||
|
||||
- 体检失败:零远端写入,部署退出非零。
|
||||
- `deploy.sh` 失败:保持既有发布错误语义,不调用 refresh。
|
||||
- refresh transport 失败:固定分类 `root_identity_unavailable`。
|
||||
- remote command 126/127 或 probe 标识缺失:固定分类 `refresh_program_unavailable`。
|
||||
- helper 返回/复核 SHA 不一致:`unit_build_not_refreshed`。
|
||||
- SHA 已一致但任一 timer 非 enabled/active:`timer_not_active`。
|
||||
- 后刷新失败时应用已部署且 smoke 通过,包装脚本输出固定 `DEPLOYED_BUT_CONTRACT_REFRESH_FAILED` 并退出非零;不伪装回滚,也不打印底层路径/响应。
|
||||
|
||||
## 6. 安装与发布边界
|
||||
|
||||
- 代码合入不自动修改生产 root 文件或 `authorized_keys`。
|
||||
- 首次安装分两步:root 运行安装器;普通身份与 forced-command 身份分别执行 probe。两步均需生产写入批准和脱敏证据。
|
||||
- 新版 `deploy-existing.sh` 对已有部署直接要求 `DEPLOY_REFRESH_SERVER`,缺失即在写前体检失败,不保留可长期绕过的 optional 模式。生产切换顺序固定为:先用旧运行版本不受影响的方式安装 forced-command 并完成 probe,再执行新版 wrapper;远端入口尚未安装时不得开始同步。
|
||||
- 回滚代码时可恢复旧 `deploy-existing.sh`;root drop-in 仍指向当前活动 build,不影响服务启动。删除 forced-command 或 drop-in 属于单独 root 运维动作。
|
||||
|
||||
## 7. 测试设计
|
||||
|
||||
- SSH fixture 必须区分 transport 255、远端 126/127、helper 固定业务退出码和成功标识。
|
||||
- 聚合测试故意让第一个、第三个检查失败,断言最后一个检查仍执行且输出顺序固定。
|
||||
- 脱敏测试向底层 stderr 注入伪造路径、主机、secret、容器 ID,断言用户输出不包含它们。
|
||||
- helper fixture 使用隔离的 fake systemctl/docker/install,不触碰真实 `/etc`;生产程序固定 PATH,测试通过源码级显式 test harness 注入依赖,安装目标不携带测试覆盖开关。
|
||||
- 静态合同断言 installed helper 不引用 `REMOTE_DIR` 下脚本,不使用 `eval`/`bash -c`/动态 unit 名,不读取调用方 SHA。
|
||||
|
||||
## 8. 取舍与延期
|
||||
|
||||
- 选择 systemd drop-in 而非重新调用 installer:只写每次部署必变的 build 字段,避免执行部署用户可写源码,也避免复制两套完整 unit 生成逻辑。
|
||||
- 选择 forced-command 专用 key 而非 sudoers:授权面只包含 probe/refresh;代价是需要一次独立 key 和 root authorized_keys 安装。
|
||||
- 不加每周 timer:当前漂移已完成一次性全量对账,先用真实部署观察是否仍有持续漂移来源。
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
# 部署前聚合体检与备份合同自动刷新:实施计划
|
||||
|
||||
## 0. 边界与回滚点
|
||||
|
||||
- [x] 用户批准本规划后才运行 `task.py start`;批准规划不等于批准生产安装。
|
||||
- [x] 实施阶段只修改仓库代码、测试、spec/runbook;不连接生产、不改 root `authorized_keys`、不安装 forced-command、不部署。
|
||||
- [x] 保留 `deploy.sh` 首次初始化/应急兼容;`deploy-existing.sh` 的自动强制启用与生产 forced-command 安装安排在另行批准的同一窗口。
|
||||
- [x] 不删除旧源码目录、旧环境文件、旧 unit 或备份制品。
|
||||
|
||||
## 1. 共享合同读取与聚合体检
|
||||
|
||||
- [x] 提取六键 systemd Environment allowlist 的共享只读解析逻辑,供 `deploy-existing.sh` 和新体检入口使用;任何输出不包含值。
|
||||
- [x] 新增聚合器与固定检查表,主流程不用 `set -e`,每项子 shell 隔离、显式收集状态,最后统一返回。
|
||||
- [x] 实现普通部署身份的仓库根、可写性、env 可读性、PGDATA/附件和运行容器 mount/build 核对。
|
||||
- [x] 实现独立 `DEPLOY_REFRESH_SERVER` 的 transport/probe 核对,并把身份不可用与程序不可用分开分类。
|
||||
- [x] 让 `deploy-existing.sh` 在任何远端写入前要求体检全绿;保留既有 remote preflight。
|
||||
|
||||
## 2. root-owned 自包含刷新程序
|
||||
|
||||
- [x] 新增无仓库运行时依赖的 forced-command target:固定 PATH/umask、清理危险环境、精确解析 `SSH_ORIGINAL_COMMAND`。
|
||||
- [x] `probe` 验证 root 身份、固定工具、root-owned unit 与固定目标前提,只输出固定标识。
|
||||
- [x] `refresh` 取得固定锁,从 root-owned unit 与唯一 running server 解析活动 build,拒绝调用方路径/SHA/unit 输入。
|
||||
- [x] 以原子 root-owned systemd drop-in 覆盖四个 service 的两项 build 环境,daemon-reload 后验证最终 SHA。
|
||||
- [x] 验证四个 timer enabled/active;用稳定退出码区分 unit 未刷新和 timer 未运行。
|
||||
- [x] 不调用 repository installer,不使用 `eval`、动态 shell、动态 unit 名或部署用户可写可执行文件。
|
||||
|
||||
## 3. forced-command 安装器
|
||||
|
||||
- [x] 将 target 安装到固定 `/usr/local/sbin`,验证 root ownership、mode 和非 symlink。
|
||||
- [x] 读取显式专用公钥,拒绝同 key blob 的不受限 root 授权;只原子维护自身 managed authorized_keys 行并保留其他 key。
|
||||
- [x] forced-command 选项禁用 agent/port/X11/PTY/user-rc,安装完成后执行本地静态合同检查;不把 installer 本身加入免密或 forced-command。
|
||||
- [x] 增加卸载/回滚说明,但不在无独立批准时自动删除授权行或 drop-in。
|
||||
|
||||
## 4. 部署后接线与状态表达
|
||||
|
||||
- [x] `deploy-existing.sh` 只在 `deploy.sh` 完整成功后调用 `refresh`。
|
||||
- [x] 成功时才打印完整部署完成;失败时固定输出 `DEPLOYED_BUT_CONTRACT_REFRESH_FAILED`、四类之一并退出非零,不转发敏感 stderr。
|
||||
- [x] 不增加 optional/跳过刷新开关;新版已有部署入口缺少 `DEPLOY_REFRESH_SERVER` 即在写前阻塞。生产窗口先安装并 probe forced-command,再运行新版 wrapper。
|
||||
- [x] 更新 runbook/checklist,明确两个 SSH 身份、forced-command 专用 key、部署前聚合报告、后刷新非零语义和生产安装单独批准。
|
||||
|
||||
## 5. 测试与门禁
|
||||
|
||||
- [x] 新增聚合器 fixtures:多个同时失败、首项失败仍执行末项、依赖失败、固定输出顺序和最终退出码。
|
||||
- [x] 新增脱敏 fixtures:底层输出含伪造路径/主机/env/容器 ID 时,外层报告不泄露。
|
||||
- [x] 新增 forced-command fixtures:空命令、未知命令、附加参数、probe、refresh、锁、原子 drop-in、四 service SHA 与四 timer。
|
||||
- [x] 覆盖四类后刷新失败:root transport、程序 126/127、unit SHA、timer 状态;断言 wrapper 非零且不打印完整成功。
|
||||
- [x] 扩展 deployment safety contract,锁定体检在 mkdir/rsync 前,refresh 在 `/ready`、smoke 和公网检查后,且 helper 不执行仓库路径。
|
||||
- [x] 运行:
|
||||
- `bash -n`(所有新增/修改 shell)
|
||||
- `node deploy/trialrun/validate-env-contract.js`
|
||||
- `cd server && npm test -- tests/unit/scripts/deployment-safety-contract.test.js`
|
||||
- 新增聚合/forced-command fixture runner
|
||||
- `bash scripts/qa/run-automatic-backup-contract.sh`
|
||||
- `python3 ./.trellis/scripts/task.py validate 08-02-deployment-preflight-backup-contract-refresh`
|
||||
- Trellis governance、process contract、docs registry、sensitive scan、`git diff --check`
|
||||
|
||||
## 6. 审查、提交与生产门
|
||||
|
||||
- [x] 实施后执行只读安全/部署 reviewer,重点核查 forced-command key 是否真正受限、root target 是否执行部署用户可写内容、聚合器是否短路、状态分类是否准确;reviewer 不改代码。
|
||||
- [x] 用 `trellis-update-spec` 更新 deployment-trialrun 的稳定合同,避免只把知识留在 task。
|
||||
- [x] 中文提交并引用 `08-02-deployment-preflight-backup-contract-refresh`;不自动提交、不自动部署。
|
||||
- [x] 获得独立生产批准后,安装专用 forced-command/key,完成真实 `probe`、安装完整性复核和 13 项只读聚合体检;未部署应用、未调用 `refresh`、未清理旧目录。
|
||||
- [x] 下一次真实发布使用新版 wrapper 验证自动 `refresh` 与完整成功语义;2026-08-03 发布完整通过,应用与公网检查成功后 forced-command `refresh` 返回成功,部署后 13 项体检仍为 `PASS=13 FAIL=0`。
|
||||
|
||||
本地验证(2026-08-02):聚合/forced-command fixture 通过,覆盖 13 项汇总、多个并发失败不短路、脱敏输出、四类收尾映射、精确命令白名单、四个 build drop-in、四个 timer、managed key 更新及不受限重复 key 拒绝;deployment safety Vitest 9 passed;环境合同通过;automatic-backup 合同 73 checks passed;全部 shell/Node 语法通过。只读复查发现 forced-command target 缺少运行时自身 ownership/mode/path 校验,已补 `validate_self()`;最终复核又将 systemd build 校验从子串匹配收紧为精确键与最后覆盖值,并用 `OLD_BUILD_SHA` 回归证明不会静默误判。相关聚合、环境合同和 Vitest 均复跑通过;复查未发现新的 root 提权、动态执行或部署顺序 blocker。该本地验证阶段未连接、安装或部署生产。
|
||||
|
||||
生产安装证据(2026-08-02,脱敏):现有 root 管理身份与普通部署身份均唯一定位;专用 key/alias 已建立,root-owned target、managed authorization、临时 staging 清理及已提交源码摘要全部 PASS;forced-command `probe` PASS;生产只读聚合体检 `PASS=13 FAIL=0`。未运行 `refresh`,未部署应用,未删除历史源码目录或环境文件。
|
||||
@@ -0,0 +1,93 @@
|
||||
# 部署前聚合体检与备份合同自动刷新
|
||||
|
||||
## Goal
|
||||
|
||||
让已有部署在任何远端写入前一次性看清合同、权限、数据挂载和备份构建身份的全部漂移,并在应用切换成功后自动刷新备份/WARCH3 service 的构建标识。部署不再依赖操作者逐个撞 fail-fast 护栏,也不再依赖人工记住“部署后重装 timer”。
|
||||
|
||||
## Background
|
||||
|
||||
- 2026-08-02 的受控部署连续暴露三个历史漂移断面:冻结的 `REMOTE_DIR` 不是仓库根、root 维护身份与部署身份未按实际通路区分、冻结的环境文件路径不可由部署用户读取。现有护栏串行 fail-fast,因此每修复一处才看到下一处。
|
||||
- 同一轮恢复已完成一次全量对账:六键合同、163 个生产环境键、运行容器、PGDATA/附件挂载、4 个 service、4 个 timer、真实备份和异机恢复点均已对齐并跑绿。本任务把该对账方法变成可重复机制。
|
||||
- `deploy.sh` 只把新 `BUILD_SHA` 注入应用容器,不重装 automatic-backup/WARCH3 units;备份 runner 又要求 unit 中冻结的 build 与活动容器一致。人工漏做刷新时,应用可正常运行而自动备份静默 fail-closed。
|
||||
- 仓库已有 forced-command 先例:异机备份接收端以 root-owned 可执行文件配合 `SSH_ORIGINAL_COMMAND` 白名单和 SSH 转发/PTY 禁用选项限制授权面。
|
||||
|
||||
## Scope
|
||||
|
||||
- 为已有部署增加一次性汇总的只读体检,并在任何远端写入前阻断全部已发现的合同、权限、挂载和 build 漂移。
|
||||
- 建立独立 forced-command SSH 身份与 root-owned 自包含刷新程序,只更新四个固定 service 的 build drop-in并验证四个既有 timer。
|
||||
- 把聚合体检接到 `deploy-existing.sh` 前置,把自动刷新接到完整部署与公网检查之后,并补齐测试、runbook、checklist 和部署 spec。
|
||||
- 保留 `deploy.sh` 首次初始化/应急路径和既有 installers;生产安装、authorized_keys 修改和首次启用另行批准。
|
||||
|
||||
## Requirements
|
||||
|
||||
### 1. 只读聚合体检
|
||||
|
||||
- 新增已有部署专用体检入口,由 `deploy-existing.sh` 在任何 `mkdir`、rsync、Compose、systemd 或其他远端写入前调用。
|
||||
- 体检至少覆盖:六键合同完整;`REMOTE_DIR` 是完整仓库根且部署用户可写;`REMOTE_ENV_FILE` 为普通文件且部署用户可读;PGDATA 含 `PG_VERSION`;附件目录存在;运行中唯一 server/db 容器与合同中的 build、PGDATA、附件挂载一致;automatic-backup unit 的 `BUILD_SHA` 与活动 server 一致;普通部署 SSH 身份可用;独立 root forced-command 身份可用且收尾程序存在、可执行。
|
||||
- 主流程不得使用 `set -e` 实现检查遍历。每个检查必须在独立子 shell 中执行并显式收集退出码;前一项失败不能阻止无依赖的后续项执行。依赖事实缺失时,相关项仍以固定检查名报告 FAIL,不得静默跳过。
|
||||
- 输出只包含固定检查项名、`PASS/FAIL`、汇总计数和固定错误分类;不输出 SSH 别名、主机、路径、env 值或散列、容器 ID、密钥、unit 原文及原始命令响应。
|
||||
- 任一必需检查失败,体检最终退出非零,`deploy-existing.sh` 不得继续同步或部署。现有 fail-fast `preflight-remote.sh` 保留为第二道防线。
|
||||
|
||||
### 2. 独立 root forced-command 通路
|
||||
|
||||
- 使用独立维护 SSH 身份,不以 `sudo -n true` 代表 root 能力,不新增 NOPASSWD sudoers。
|
||||
- 专用公钥必须只以 forced-command 形式授权;同一 key blob 若还存在不受限 root 授权,安装器必须 fail-closed。不得复用当前不受限 root 管理密钥来制造“看似受限”的入口。
|
||||
- forced-command 指向 `/usr/local/sbin` 下 root-owned、非符号链接、`0755` 的自包含程序;程序不得 source、exec 或调用 `REMOTE_DIR` 及其他部署用户可写目录中的脚本、二进制或配置。
|
||||
- SSH 入口禁用 agent forwarding、端口转发、X11、PTY 和 user rc;只接受 `SSH_ORIGINAL_COMMAND` 的精确白名单子命令,首版仅允许无附加参数的 `probe` 与 `refresh`。
|
||||
|
||||
### 3. 部署后自动刷新
|
||||
|
||||
- `deploy-existing.sh` 只有在应用部署、`/ready`、runtime/external smoke 与公网检查成功后,才通过独立 root forced-command 身份调用 `refresh`。
|
||||
- root 程序从 root-owned systemd 合同和唯一活动 server 容器读取 Compose 身份及完整 `BUILD_SHA`,严格校验后仅更新四个固定 service 的 build 环境覆盖:automatic-backup daily、watchdog、drill-reminder 与 WARCH3。不得接受调用方传入路径、unit 名、SHA 或任意命令。
|
||||
- 刷新必须原子写入 root-owned systemd drop-in,执行 `daemon-reload`,再验证四个 service 的最终 `BUILD_SHA/VITE_BUILD_SHA` 与活动容器一致,以及四个既有 timer 均为 enabled/active。不得调用仓库内 installer,也不得顺带重写其他 unit 字段。
|
||||
- 刷新失败时 `deploy-existing.sh` 必须退出非零并明确标记“应用已部署但合同未收口”,不得打印普通成功结论,也不得自动回滚已通过 smoke 的应用。
|
||||
- 对外固定区分四类失败:root SSH 身份不可用;收尾程序不存在或不可执行;程序完成但 unit SHA 未更新;unit 已更新但 timer 未 enabled/active。
|
||||
|
||||
### 4. 安装、兼容与生产边界
|
||||
|
||||
- 提供一次性 root 安装器,将经过审核的自包含程序复制为 root-owned 文件并安装/更新 managed forced-command 授权行;安装器本身仍需显式 root 执行,不进入免密授权。
|
||||
- 保留现有 automatic-backup/WARCH3 installers 作为首次安装、路径/完整合同变化和人工修复入口;自动收尾只覆盖每次发布必变的 build 标识。
|
||||
- 现有 `deploy.sh` 首次初始化/受控应急入口不强制依赖 forced-command;自动体检与刷新只接入 `deploy-existing.sh`。
|
||||
- 生产安装 forced-command、修改 root `authorized_keys` 或首次启用自动收尾均属于生产写入,实施完成后必须另行批准;本任务规划批准和代码批准不授权远端安装。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] 合成场景同时制造至少两个前置失败时,体检报告包含全部预期 FAIL 项,且最后一个独立检查确实执行;不能因 `set -e`、pipefail 或函数非零提前退出。
|
||||
- [ ] 体检全绿时只输出固定项名和 PASS/汇总;敏感扫描与测试证明没有路径、主机、env 值/散列、容器 ID 或原始响应。
|
||||
- [ ] 六键缺失、仓库根错误、env 不可读、源码根不可写、PGDATA/附件缺失、实际挂载漂移、unit build 漂移均能在远端写入前一次汇总发现。
|
||||
- [ ] 普通部署身份和 root forced-command 身份分别验证;root transport 不可用与远端程序不存在/不可执行得到不同固定分类。
|
||||
- [ ] forced-command 只接受精确 `probe`、`refresh`;空命令、带额外参数、其他子命令均拒绝。安装器拒绝同一公钥同时存在不受限 root 授权。
|
||||
- [ ] 安装后的 root 程序为 root-owned、`0755`、非符号链接,且静态/动态测试证明不执行仓库或其他部署用户可写路径中的程序。
|
||||
- [ ] 成功 `refresh` 从活动容器取得 build,原子更新四个固定 service 的 drop-in,四个最终 SHA 一致且四个 timer enabled/active;调用方无法指定 SHA、路径或 unit。
|
||||
- [ ] 四类收尾失败分别有回归:SSH root 身份不可用、程序不存在/不可执行、unit SHA 未更新、timer 未运行。
|
||||
- [ ] 任一收尾失败发生在应用已切换后,`deploy-existing.sh` 退出非零并输出固定“已部署但未收口”状态;成功时才输出部署完整完成。
|
||||
- [ ] `deploy.sh` 直接初始化/应急路径保持兼容;automatic-backup/WARCH3 既有 installer、真实备份与 watchdog 行为不被改写。
|
||||
- [ ] shell syntax、部署安全单测、环境合同、聚合体检 fixtures、forced-command fixtures、Trellis/governance/process/sensitive/diff 门禁全部通过。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 不建设 CI/CD、蓝绿发布、自动回滚或新的通用远程执行框架。
|
||||
- 不把体检挂成每周 timer;先观察它在后续真实部署中是否持续发现漂移,再单独评估第 5 个 unit。
|
||||
- 不在本任务删除四个旧源码目录、旧环境文件或其他历史回退材料;体检跑绿也只形成后续清理的申请前提。
|
||||
- 不通过 `SYNC_ENV=1` 修复环境文件,不自动挑选、复制或轮换生产 Secret。
|
||||
- 不让 root 收尾程序修改 Compose、数据库、附件、业务数据、备份制品或执行真实恢复。
|
||||
|
||||
## Deferred Decision With Trigger
|
||||
|
||||
- 每周只读体检 timer 暂不建设。触发条件:本脚本上线后在后续真实部署中再次发现非一次性漂移,或出现人工改服务器导致合同在部署间隔内失配;届时单独立项评估定时运行、告警和第 5 个 unit 的维护成本。
|
||||
- 旧目录/旧环境文件清理单独审批。至少要求聚合体检在规范根上跑绿、一次后续真实部署自动收尾跑绿,且当前异机备份仍新鲜。
|
||||
- root-owned helper 自身摘要比对暂不加入首版协议。触发条件:生产安装后的 helper 与仓库审核版本出现过内容漂移,或后续需要把 root-owned 独立制品纳入统一完整性巡检;届时单独设计可信摘要来源、升级与回滚合同,不能让 helper 自报摘要后由自身证明可信。
|
||||
|
||||
## Test Plan
|
||||
|
||||
- `bash -n` 检查新增/修改的所有部署 shell。
|
||||
- 扩展 `server/tests/unit/scripts/deployment-safety-contract.test.js`,锁定 preflight 在任何远端写入前、刷新在应用与公网检查后、失败必须向上传播。
|
||||
- 新增纯 fixture 合同测试,使用假的 SSH/systemctl/docker 响应覆盖聚合不短路、脱敏输出、身份/程序四类失败、固定 unit/drop-in 和 timer 验证。
|
||||
- 运行 `node deploy/trialrun/validate-env-contract.js`、相关 automatic-backup 合同测试及既有部署安全回归。
|
||||
- 运行 Trellis task validate、governance、process contract、docs registry、sensitive content 与 `git diff --check`。
|
||||
|
||||
## Notes
|
||||
|
||||
- Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
|
||||
- Lightweight tasks can remain PRD-only.
|
||||
- For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start`.
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "deployment-preflight-backup-contract-refresh",
|
||||
"name": "deployment-preflight-backup-contract-refresh",
|
||||
"title": "部署前聚合体检与备份合同自动刷新",
|
||||
"description": "新增只读聚合部署体检,并通过独立 forced-command root 身份自动刷新部署后的备份构建合同。",
|
||||
"status": "completed",
|
||||
"dev_type": "infra",
|
||||
"scope": "deploy/trialrun 与部署安全合同",
|
||||
"package": "hyb-oa",
|
||||
"priority": "P2",
|
||||
"creator": "shunli",
|
||||
"assignee": "shunli",
|
||||
"createdAt": "2026-08-02",
|
||||
"completedAt": "2026-08-03",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "聚合只读体检、root forced-command 收尾、四个 unit 构建合同自动刷新及回归已部署;后续真实部署连续通过 13/13 体检与合同刷新,并在一次错误 SSH 别名尝试中于任何写操作前成功阻断,任务目标已由生产实战验证。",
|
||||
"meta": {}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户