比较提交

...
2 次代码提交
作者 SHA1 备注 提交日期
wangsli b9185308a6 fix: 优先引导下载银行制单(09-08-reimbursement-payment-closeout)
ci-minimal / check-minimal (push) Canceled after 0s
ci-e2e / e2e (push) Canceled after 0s
2026-09-09 09:05:17 +08:00
wangsli e8cad812ad fix: 取消CEO默认AP台账权限(09-07-manager-ap-access-scope) 2026-09-09 09:05:17 +08:00
共修改 21 个文件,包含 1004 行新增和 59 行删除
@@ -2872,3 +2872,60 @@ isManagementPath(pathname, permissions);
<Eye aria-hidden="true" />
</button>
```
## Scenario: Payment Detail Guides First Order Before Receipt
### Scope / Trigger
The selected payment-batch detail after independent review, not the default
payment ledger landing page. Reuse the existing desktop and mobile action model.
### Signatures
`PaymentBatchesPage.paymentActionModel`, `canExportPaymentOrder`,
`paymentOrderExportCount`, `needsBankReceipt`, `canRegisterPaymentComplete`.
The backend remains authoritative for reviewed status, role and snapshot checks.
### Contracts
- A reviewed, exportable bank batch with no generated order and no receipt uses
`下载银行制单` as its single primary action. Do not ask for a bank receipt before
exposing the first order needed to make that payment.
- Keep `上传银行回单` in `更多` for users who already paid manually; generating an
order is not a new hard prerequisite for recording a legitimate payment.
- After first generation, use `上传银行回单` as primary and put re-download in
`更多`. With an existing receipt, keep `登记付款完成` primary even if no order
was generated. Unknown receipt state is not an empty receipt list.
- Re-download requires a reason and the duplicate-bank-submission warning.
Download success must not execute a payment, upload a receipt or mark paid.
- Legacy/non-bank/non-exportable batches retain the existing manual flow.
Do not add roles, bypass backend guards or change the payment state machine.
### Validation / Error Matrix
| State | Primary | Additional route |
| --- | --- | --- |
| Reviewed, exportable, first order, no receipt | Download order | Manual receipt upload in more |
| Order generated, no receipt | Upload receipt | Re-download in more |
| Receipt already present | Register payment complete | Existing allowed order actions |
| Download fails | Keep download step | Reason dialog remains; no payment mutation |
| Receipt query fails | No upload/complete primary inferred from absence | Reload/error feedback |
### Good / Bad Cases
Good: finance downloads the approved batch file, then receives a receipt prompt.
Bad: hiding first generation behind more while prompting for a not-yet-existing
receipt; or requiring an export before accepting an already-completed manual payment.
### Tests Required
Desktop/mobile first download, reason/cancel, received BIFF8 bytes, re-download
warning and no payment mutation, manual upload via the actual file chooser,
existing-receipt and non-bank behavior, and unknown-receipt refusal. Keep backend
authorization and independent workbook parsing tests separate from browser mocks.
### Wrong vs Correct
Wrong: a visible action or a generated file proves money was paid.
Correct: file generation, bank execution and OA payment registration are distinct
events. Synthetic browser/backend/container checks do not prove real bank acceptance.
+3 -3
查看文件
@@ -32,7 +32,7 @@ AP financial summary and reimbursement ledger reads, not general entity visibili
### 3. Contracts
Grant ledger reading to `ceo`, `finance`, `finance_mgr`, not `manager` or
Grant ledger reading to `finance`, `finance_mgr`, not `ceo`, `manager` or
`system_admin`. Preserve emergency `admin` wildcard semantics. Both reads require
the dedicated permission before data queries; query filters cannot confer access.
Frontend route and navigation projections use the same permission. Keep manager
@@ -44,13 +44,13 @@ management to preserve read access. Export keeps its existing independent
Feature enabled: missing ledger permission returns 403 / `PERM_001`, with no
financial query. Feature disabled: the existing 409 refusal may occur first.
CEO/finance/finance manager and manager+finance retain reading. Old payment or
Finance/finance manager and manager+finance or ceo+finance retain reading. CEO alone is refused. Old payment or
risk permissions alone no longer imply ledger reading.
### 5. Good / Base / Bad Cases
- Base: manager keeps payment review but cannot enter AP.
- Good: CEO reads AP without acquiring export or payment-write rights.
- Good: CEO retains approval and payment review but cannot read AP without a separate finance role.
- Bad: hiding a menu while leaving either backend read unrestricted.
### 6. Tests Required
@@ -1,10 +1,14 @@
# AP 全量读取最小收窄方案(专用权限修订已批准)
## 2026-09-08 最新获批修订
用户确认两个邮箱同名账号为本人,majianyong 保留 ceo;其他员工业务直接经理为 majianyong。CEO 审批身份不再携带全量台账读取。下方第 5 节起保留旧候选发布及预检历史,不作为修订后候选的发布授权;旧“CEO 可读”验收矩阵已被本节覆盖。当前不部署、不修改生产账号或经理关系。
## 1. 读取授权
获批修订:新增专用权限 `finance.ledger.view`,仅授予 ceo、finance、finance_mgr;两个 AP GET 及前端入口统一只检查此权限。manager 不授予;不存在 CEO 角色特判,旧付款管理、复核、风险和支付权限均不再独立授予 AP 读取。既有应急 admin 通配符语义不变。
获批修订:新增专用权限 `finance.ledger.view`,仅授予 finance、finance_mgr;两个 AP GET 及前端入口统一只检查此权限。manager、ceo 不授予;不存在 CEO 角色特判,旧付款管理、复核、风险和支付权限均不再独立授予 AP 读取。既有应急 admin 通配符语义不变。
前后端各有静态 ROLE_PERMISSIONS,需要同步三类角色和相关测试,不是只改服务端三行。无数据库权限迁移,不授予 CEO 写权限,不按人员账号特判,不采用 manager deny-list。AP 导出仍使用原有独立权限。
前后端各有静态 ROLE_PERMISSIONS,需要同步角色授予和相关测试,不是只改服务端三行。无数据库权限迁移,不授予 CEO 写权限,不按人员账号特判,不采用 manager deny-list。AP 导出仍使用原有独立权限。
## 2. 接入范围
@@ -20,9 +24,9 @@
| --- | --- | --- | --- |
| employee | 拒绝 | 按现有权限拒绝 | 拒绝 |
| manager,仅复核职责 | 拒绝 | 保留原业务条件 | 拒绝 |
| ceo | 保留全量读取 | 保留原业务条件 | 不新增 |
| ceo | 拒绝 | 保留原业务条件 | 不新增 |
| finance / finance_mgr | 专用权限允许 | 不改变角色和独立复核条件 | 按既有两项权限 |
| manager+已授权财务角色 | 允许 | 原条件不变 | 按财务授权 |
| manager/ceo+已授权财务角色 | 允许 | 原条件不变 | 按财务授权 |
| 仅系统管理员 | 不新增 | 不新增 | 不新增 |
| 仅持旧付款管理/复核/风险/支付权限 | 拒绝 | 按原权限 | 按既有两项权限 |
@@ -31,3 +35,261 @@ API 验证包括数据查询未发生、筛选不绕过;前端验证包括直
## 4. 安全与交付边界
不查询生产日志,不回填历史访问结论。不触碰既有门禁交审文件;若实施必须修改已交审测试,先保存对应增量基线,单独交本 task diff,不混报旧包。实现后独立只读复核,提交、推送和部署继续另行授权。
## 5. 单次 AP 发布方案草案(2026-09-08,未获生产执行批准)
### 5.1 状态、候选和实际变更范围
- 已提交推送:门禁包 `9916712`、AP 修复 `58dcee0`,已有两个 Trellis 提交亦已推送。用户“同意建议”仅用于继续准备方案,不倒写成单次发布例外批准。
- 拟用候选:`58dcee08f72013fe5b9448a3c2e82a759ba5b01f`,最终批准必须引用完整 SHA、目标和一次发布窗口。不得部署执行当时浮动的 main。
- 生产当前运行 SHA、镜像引用、迁移状态和可用回滚点尚未核实。远端 Git 的旧 main 不是生产基线。必须以生产基线到候选的完整差异确定实际发布范围,不能将 main 包含的 Trellis/QA 等改动称为“只发布 AP 一个提交”。
- AP 修复本身没有 schema/migration 或业务数据写入逻辑;但 `deploy.sh` 的 run-init 会执行 `prisma:migrate:deploy`、`seed:config`、`seed:trialrun`。无新增 migration 不等于整个部署零数据写入,seed 也不能未经核实视为无操作。
- 当前主工作树还有两个未跟踪 task;来源检查要求干净。后续应准备干净、来源可核验且满足既有 upstream 合同的隔离检出,不删除其他 task、不加忽略豁免、不携带未提交材料发布。本轮未创建发布检出。
### 5.2 例外批准合同(与验证结果分开)
本草案不提供普通发布豁免开关,也不启用 R1。若业务决定不等待完整门禁全绿,必须另行批准仅针对本候选、目标、窗口的一次受控发布,明确:
1. 已知本候选没有完整候选验收 PASS;本次接受哪些验证缺口及理由,而非宣布门禁通过。
2. 批准人、执行人、目标、完整候选 SHA、窗口、失败停止条件、回滚目标和回滚权限边界。
3. 是否接受既有部署 init/seed 的实际写入面;若超出批准范围或无法说明,停止,不临时跳过 init、修改脚本或强行部署。
4. 例外只涉及明确列出的候选验收缺口,不免除来源、环境身份、备份、迁移、数据库健康、公网边界及部署后验证等既有合同。
5. 发现额外候选差异、目标不符或回滚点不可用即停止,重新交审,不把一次例外延续到下一候选。
生产只读预检和生产发布是两个动作;可先单独授权只读预检。发布写入以及可能的 seed 写入,须在预检事实补齐后明确批准,不由本草案自动授权。
### 5.3 验证缺口与发布前要求
- 已有证据:五文件后端 123/123,定向合成浏览器 6/6,typecheck、隔离 build、语法和治理通过,用户独立复核通过。它们绑定已交审来源,不能改写成提交后 full-local PASS。
- 未取得:候选完整验收全绿、普通全绿耗时、全部浏览器根因清单及真实生产权限验收。旧 58 条为环境有效的失败观测;207 清单撤销决策资格。R1 继续暂停。
- 只读预检范围拟限定:确认目标身份、运行版本、当前备份/回滚窗口、已应用迁移及候选待执行迁移;不查询历史个人访问日志,不读取完整台账、银行信息或单据正文,不写库、不启动部署。
- 补齐实际发布差异后,按最终候选在干净隔离来源重验同一 AP 权限矩阵、付款复核与导出回归及来源检查;原验证不得冒充新候选结果。输出实际命令、环境、筛选、结果、耗时和 Git 状态,不追加完整门禁 PASS 字段。
- 既有发布入口和任何会触发外联/写入的 post-smoke 均须在最终方案列明,不能以“只读验收”概括整个部署流程。本轮不运行这些脚本。
### 5.4 发布后验收(待批准窗口内执行)
- `/ready`、前后端 build 标记匹配批准 SHA;原有健康、公网和备份合同检查不得缺失。
- 经理本人或获批的受控验证身份确认 AP 直连拒绝;后端两个 AP GET 无权请求拒绝且不返回数据。前端零请求和后端零财务查询是两层不同证据,不互相冒充。
- 财务、CEO 的授权身份确认 AP 汇总及台账可达;经理付款复核入口仍可达。不存在某种真实角色账号时不新增或冒用身份,应保留本地正例并明确实际验证缺项。
- 生产验收仅记录必要状态与脱敏结果,不保存完整台账,不进行真实审批、复核、付款、导出或修改角色来凑正例。实际业务写操作必须另外授权。
- 未完成必须项不得报告发布验收通过或“已可公网测试”。即使成功,也只声明 AP 入口收窄;经理仍可通过报销列表查看全部报销。
### 5.5 回滚与回滚验收(单独明确)
- 发布前先核实可恢复的旧应用镜像/构建及配置引用和数据库兼容性。一个容器名或“以后 revert”不是可用回滚点;目标 SHA 和操作步骤待只读预检补齐。
- AP 单独回退可通过另建 revert 提交实现,但 revert `58dcee0` 只撤回 AP,不会撤回其他随候选发布的改动,也不等同恢复未知生产基线;本轮不执行 revert。
- 健康失败、版本标记不符、财务/CEO 意外被拒绝或正常经理复核被阻断时停止发布推进,按获批回滚合同恢复。若 init 已发生写入,不能假设应用回退会撤销这些写入,更不能自动恢复数据库或覆盖期间业务数据。
- 回滚到旧权限会重新开放经理 AP 全量访问,这是一项明确的安全退让,须纳入回滚批准和后续补救,不称为“完全恢复正确权限”。
- 回滚后核对旧 SHA、应用健康、核心登录/财务读取/复核入口及备份合同;记录恢复范围、未回退数据和仍存在的 AP 权限开口。无可用回滚点则不批准应用切换。
### 5.6 当前需要的下一步授权
建议先批准本节限定的生产只读预检,以填实生产基线、init 写入面和回滚目标;之后提交候选与窗口明确的一次例外批准请求。本草案不请求立即部署,不请求启用 R1,不恢复其他修复支线。
## 6. 2026-09-08 获批只读预检结果(不是部署资格)
### 6.1 版本与实际差异
- 实测生产运行 `f223bfb72fb2baef51199702c2def84c41af7aef`,应用容器 running;当前配置镜像引用可解析且等于运行镜像,备份合同 BUILD_SHA 与运行版本一致。
- 拟发布 `58dcee08f72013fe5b9448a3c2e82a759ba5b01f` 相对生产共 7 个提交、103 文件。server/src 与 client/src 共 7 文件、+11/-4:前后端权限表、AP 守卫、服务端导航、前端路由/导航,以及 reimbursements 的 __testables 导出。
- deploy、Prisma schema/migrations、两个 seed 文件在此范围无差异;生产容器内两个 seed 文件的 SHA-256 也与候选一致。此处“7 文件”是应用源码变更口径,不是整个候选交付文件数,也不保证其他路径绝不被构建打包。
- 预检从固定候选的干净隔离检出执行,保留原工作树未跟踪 task;没有来源检查豁免。候选使用共享本地 Git 对象,仅作本地预检来源,不是额外独立备份。
### 6.2 迁移、备份与回滚证据分层
| 检查 | 本轮结果 | 不代表什么 |
| --- | --- | --- |
| 既有身份/部署合同聚合 | 13/13 PASS,bootstrap 只读模式退出 0,未进入 deploy.sh | 不是完整候选验收通过 |
| Prisma 状态 | 66 个 migration,schema up to date;候选/数据库对拍 pending=none | 未运行 migrate deploy,不保证以后状态不变 |
| 既有合同回滚窗口 | OPEN,terminatedRows=0 | 只是既有状态兼容性判据,不是整个恢复方案演练 |
| 备份服务 | 2026-09-08 02:30:01–02:30:51 CST,Result=success、ExecMainStatus=0;timer active | 不是备份文件摘要、异机副本或恢复实测 |
| 备份元数据 | 受控部署身份读取 last-success 被权限拒绝;已有非交互 sudo 能力检查也不可用 | 未读取/验证 manifest 和数据库/附件包摘要,不宣称备份缺失或损坏 |
| 旧应用资源 | 运行镜像 ID 可 inspect,当前配置引用指向该镜像 | 尚未固定独立回滚标签/制品;候选构建可能覆盖原标签,不能直接称“可用回滚点已准备” |
没有新增密钥、改变 SSH/sudo/文件权限、借容器挂载规避备份目录权限,也未执行备份、恢复、容器切换或任意部署写操作。备份验真和旧应用制品固定仍是发布前缺项。
### 6.3 seed 在已有生产数据上的行为
本轮未运行 seed。以下由候选源码与受限只读账号分支观测建立,不能将“源码不变”解释为“执行无写入”。
- seed-config:费用类型 upsert 更新默认属性;报销编号格式和文档分类更新;四类 workflow definition 先取消激活再写回 v1;假期日历更新并删除/重建日条目;考勤策略、预算规则、假期类型/额度更新。既有审批规则非空时保留、CCB 银行字典存在时保留、已有 feature flags 的创建冲突被保留。默认主体缺日历绑定时还可能补绑定。
- seed-trialrun:部门更新、工作区默认配置覆盖、指定 feature flags 强制开启并递增 version;这些是实写,不是迁移空操作。
- seed 管理账号的配置身份只匹配 1 条 active、未归档记录,目前角色为 admin+employee;未开启归档重激活,未配置新建账号密码。本次会进入 existing-active 分支:更新账号状态/时间,删除并重建 admin+employee 角色记录,密码不变;按此快照角色集合不新增。没有输出账号、手机号、邮箱或密码。
- 不把“该账号角色集合不变”推广为所有配置语义无变化。本轮未逐字段比对当前配置与 seed 默认值;即便值相同,更新时间、flag version、删除重建等写入仍会发生。
- 账号分支查询使用 default_transaction_read_only=on、REPEATABLE READ READ ONLY、10 秒 statement / 1 秒 lock timeout,确认 transaction_read_only=on,最后 ROLLBACK;没有读取业务单据或个人访问日志。
### 6.4 发布决定尚缺的条件
代码差异确实小且已复核,但现有部署运行 seed 的写入面不能折算进“11 行改动”后忽略。现在不形成发布批准:先通过已有获准的受控渠道补备份摘要/有效恢复点核验,明确保存旧应用制品的步骤;再明确接受既有 seed 的写入范围或另审部署方案。不得擅自跳过 seed、恢复数据库或创建常设门禁豁免。
原始证据在 `artifacts/qa-flow/manager-ap-access-scope/readonly-preflight-20260908/`:preflight.log、preflight/pre/、metadata-result-02.json、backup-service-status.txt。首次元数据读取失败单独保留,不覆盖或冒充通过。此结果没有改变 58 条清单、阶段 B、R1 或业务消息的状态。
### 6.5 2026-09-08 seed 配置只读差异补查
用户明确不批准发布例外,仅批准继续只读比对。观测时间为 2026-09-08 08:42:41 CST;运行版本仍为 f223bfb,容器内两个 seed 摘要与候选一致。通过现有 Compose 实际解析 init 环境,额外 force-on flags 为空,因此本次强制开启集合为默认 14 项,不将运行中 server 环境冒充下次 init 输入。
| 对比范围 | 当前状态与 seed 目标差异 | 重跑仍会发生的写入 |
| --- | --- | --- |
| 默认 14 个强制开启开关(含 project_linking_v1) | 14/14 存在;mode=force_on、rollout=global、部门集合为空,note 也一致;额外配置 0 项 | 每项 version +1,并执行更新;不称无写入或物理幂等 |
| seed-config 其他默认开关 | 无缺失待创建项;既有记录按创建冲突保留,不冒充全字段默认值相同 | 本次未发现缺失项插入需求 |
| 报销、请假、采购、差旅四类工作流 | 均只有 v1 且激活;name、definition_json、is_active、created_by 与目标一致;无其他激活版本 | 仍先取消激活再 upsert v1,不能把最终定义一致等同执行无副作用 |
| CN_2026_STD 假期日历 | 表头目标字段一致;39/39 日条目日期、类型、名称、note 一致,无多余日期,无需补默认主体绑定 | 仍删除重建 39 条日记录并更新时间;记录身份与审计元数据不能视为不变 |
比对脚本用 TypeScript AST 提取字面量默认值,不执行 seed 函数;生产查询使用 default_transaction_read_only=on、REPEATABLE READ READ ONLY、10 秒 statement / 1 秒 lock timeout,确认 transaction_read_only=on,ROLLBACK 完成后才输出脱敏结果。未读取业务单据、个人访问历史或原始工作流定义正文。
覆盖边界:这是上述三组的配置目标值比对,不是 seed 全部写入字段验真。费用类型、编号、文档分类、部门、工作区、考勤策略、预算及假期额度等仍只有源码写入面盘点,未逐字段比对生产值;不以三组无差异推断全部 seed 安全。值差异即使存在也不能单凭此证明人为调整动机。
证据目录:`artifacts/qa-flow/manager-ap-access-scope/seed-readonly-diff-20260908/`,成功结果为 comparison-02.json。首次 wrapper 使用 Compose v2 命令失败,尚未进入数据库查询;保留首次失败产物,改为兼容生产既有 docker-compose v1 后完成只读比对,未安装依赖或修改生产脚本。
发布状态不变:受控备份验真仍缺可用授权通道;独立回滚制品尚未固定,相关写操作未执行。本次三组对比不替代这两项,也不生成例外资格;未运行 seed、迁移、部署、提交或推送。
### 6.6 2026-09-08 剩余 seed 对象只读比对
沿用用户批准的 seed 只读比对范围,不申请或使用备份提权、不保存镜像。补查观测时间为 2026-09-08 09:22:19 CST;生产仍为 f223bfb,两个 seed 文件摘要与固定候选 58dcee0 一致。§6.5 的“其余对象未比对”是当时状态,以下结果补足该项,不倒填成同一事务快照。
| 对象 | 实际匹配 | 比较边界 |
| --- | --- | --- |
| 费用类型 | 44/44 | 39 个启用、5 个旧类型禁用;比较实际 update 的名称、描述、分类、排序、发票要求、默认/启用标志及 updated_by;既有 account_code_default 不被 update 覆写 |
| 报销编号规则 | 1/1 | prefix、date_format、separator、number_length、reset_period;已有 current_number 不被 update 重置 |
| 文档分类 | 11/11 | 实际 update 字段,包括默认工作流键、启用/归档状态和 updated_by |
| 考勤策略 | 1/1 | 最早 created_at 对应记录唯一,比较上下班/午休时间、迟到容差、启用状态及 updated_by |
| 默认预算控制规则 | 1/1 | 匹配 name/entity_type/未归档条件唯一;部门、主体、成本中心、费用类型限制与控制模式、优先级、启用、notes 等均一致 |
| 假期类型 | 3/3 | 名称、描述、附件要求/MIME、排序、启用及 updated_by |
| 假期默认额度策略 | 3/3 | annual_quota_days、enabled、description、updated_by;数字按数值比较;不是员工个人余额或假期流水查询 |
| 工作区设置 | 1/1 | profile、org_mode、primary_entry_mode、notification_strategy、device_priority、完整 module_visibility_json 均一致 |
| seed 目标部门 | 2/2 | 根部门及其管理子部门各唯一;parent_id 与 archived_at 均一致,不输出部门 ID |
新增 67 项目标记录均存在、匹配唯一,实际覆写字段均一致,无待创建记录。对 findFirst 无排序的分支没有假定任意某条记录代表全体:先核验匹配候选数量,本轮没有多候选歧义。
另外核实两条保留分支:CCB 字典记录存在,seed 不更新;approval_rules 有 3 条,seed 保留整组而不重建。不将“保持现有记录”写成“现有值等于默认值”。与前一轮 feature flags、工作流、日历及更早的管理账号分支证据合看,两个 seed main 调用的对象组均已有写入分支盘点和相应快照核对;账号状态/角色未在本次重查,引用 §6.3 的早先观测。
证据仍存现有 seed-readonly-diff-20260908 目录:defaults-remaining.json、compare-remaining.cjs、comparison-remaining-01.json、stderr-remaining-01.log。默认数组及工作区/编号/考勤 update 字面量由 AST 提取,不执行 seed 源码;其余字段投影逐项对照 update 处理体。数据库使用 READ ONLY 事务与既有超时限制,成功 ROLLBACK 后才输出结果,无业务单据或个人访问历史读取。
口径限制:上述是观测时刻“将被覆写的配置目标字段相同”,不是所有列不变、两轮同一快照或 seed 执行演练。时间戳、version、删除重建的记录身份/审计字段仍会变;中途失败、并发配置调整、运行时错误和恢复能力均未因此得到验证。不运行 seed 来验证最终状态,也不以此替代备份验真。
本地核验:比较脚本 node --check 通过;对象键顺序、额度数值、启用状态、审计字段、工作区 JSON 差异等正反合成检查通过,固定候选摘要与 67 项结果结构断言通过。首次本地合成检查因容器相对路径在仓库根不可用而失败,修正本地文件读取映射后通过;没有因此重跑生产查询。
发布例外仍未批准。备份摘要验真和独立回滚制品仍缺完成证据;摘要匹配不等于恢复实测,镜像回滚不撤销 seed 的数据库写入,完整候选验收仍未通过。没有生产写入、镜像保存、部署、提交或推送。
### 6.7 2026-09-08 获批固定旧运行镜像制品
用户对单独保存并校验当前 f223bfb 镜像明确回复“同意”。本节更新此前“镜像未固定”的状态,不扩大为备份读取或发布授权。
- 写入前确认运行 SHA=f223bfb72fb2baef51199702c2def84c41af7aef,镜像 ID=sha256:49585c3b9da993f3b0329ca972e8704ca422da508927c7daf1df77b70b0acad7,与前次预检一致。镜像 Size=1,079,067,702 字节;可用空间 10,094,391,296 字节,高于本次保守阈值 3,231,877,228 字节。
- 按固定镜像 ID 执行 docker image save;2026-09-08 09:48:50 CST 完成。image.tar 大小 1,117,595,648 字节,SHA-256 为 82283a7905a93dd60420f7f2b8f1fb3c483bb97fe2ef61cf4113be8a4c49da51。
- 存储定位:部署身份 home 锚点下 `.local/share/hyb-oa/rollback-artifacts/f223bfb-20260908T014752Z-0807083d/`;独立于 Docker 存储和部署源码目录。内含 image.tar、image.tar.sha256、receipt.json;目录 0700、文件 0600,没有改变既有目录权限、覆盖旧制品或新增镜像标签。
- 归档完整读取,manifest 仅含目标镜像,配置内容 SHA-256 等于固定镜像 ID;14/14 层内容摘要与配置 rootfs.diff_ids、现有镜像 RootFS.Layers 一致。另起只读进程使用 sha256sum --check 复读已保存文件通过,回执内容和文件大小也一致。
- 保存前后容器 ID、StartedAt、运行镜像及 BUILD_SHA 相同,未重启、切换、停止或清理服务;保存后可用空间 8,976,371,712 字节。未查询/写入数据库,未执行 seed、迁移、docker load 或部署。
- 制品是同机持久副本,不是异机或异盘备份;完整性校验不是镜像导入/恢复实测。应用回退不能撤销 seed 数据库写入,旧版本仍包含经理 AP 读取过授权;执行回滚也须按独立批准和验收合同。
脱敏证据目录:`artifacts/qa-flow/manager-ap-access-scope/rollback-image-20260908/`,含 precheck-01.json、save-result-01.json、verify-result-01.json 与对应脚本/错误日志。脚本语法和禁止操作检查通过;没有访问受限数据库备份、修改 SSH/sudo 权限或绕权限读取。
剩余前置:备份 manifest/包摘要仍未验真,获准渠道仍待指定;完整候选验收与恢复实测缺口仍需如实列入例外。此次保存授权不等于发布例外、实际部署、提交或推送授权。
### 6.8 管理员备份验真渠道交接(用户回传完整性通过)
用户确认可用具备读取权限的管理员账号登录,并批准执行端仅上传只读校验脚本,由用户自行运行后回传脱敏结果。此为获准的人工执行渠道,不授予执行端 sudo、备份文件读取或修改权限能力。
脚本位于系统临时目录下独立目录 hyb-oa-backup-check-7fptuwbq,文件 check-backup-admin.py;SHA-256=58a604fc1e0ebab824553fa0507683ed5d8c44872d36d56c41574d8959ea6a35。上传复读匹配,私有目录 0700、文件 0600;未执行。用户命令对所读脚本字节先验摘要再执行,不直接信任可变临时路径上的内容。
检查边界:从已有 systemd 合同定位 last-success 和对应备份批次,校验 manifest 摘要、runId/完成状态/候选前生产基线、数据库包及附件包文件名/字节数/摘要,复查状态和 manifest 未变化;仅输出结果、备份时间/年龄和版本,不输出原始合同、manifest、完整路径或业务内容。成功标记为 PASS_INTEGRITY_ONLY,失败为 NOT_VERIFIED 及阶段/错误类型;未执行恢复、未验证异机副本或自动生成发布资格。
2026-09-08 用户在管理员终端执行已核对摘要的脚本并回传 JSON:status=PASS_INTEGRITY_ONLY,manifest=sha256_match,database/storage 均为 size_and_sha256_match,build=f223bfb72fb2baef51199702c2def84c41af7aef,restoreTested=false。备份完成时间为 2026-09-07T18:30:51.081470Z,即北京时间 2026-09-08 02:30:51;校验时 ageHours=9.56,不把这一年龄写成后续发布时的实时年龄。
证据来源明确为用户手动执行回传,不冒充执行端直接读取原始 manifest 或亲自运行管理员校验。此前已上传制品、当前备份完整性结果与 seed 比对已可供例外审查,但完整候选验收、恢复演练和异机副本核验缺口仍在。若需要数据库恢复,该备份之后的新业务数据可能丢失,不能仅因摘要匹配就自动恢复或认为零数据损失。
流程更正:本机实际存在 hyinda-root 管理员 SSH 别名,与生产目标一致;此前只检查部署身份和其 sudo 能力,没有及时检查现有管理员连接配置。ssh -G 仅验证配置解析,未验证执行端实际 root 登录;应先发现已有通道并请求限定授权,而不是先将命令执行转交用户。用户本次质询不视为无限管理员授权,不追加生产操作。后续优先由执行端在明确获准的通道和操作范围内完成,避免不必要的手工往返。
### 6.9 发布例外补充:恢复历史与异机链时间线
用户提出恢复演练历史遗漏后,只读核对两个既有 task,没有发起远端管理员操作或实际恢复。
- 恢复历史:08-23-isolated-backup-restore-drill/research/restore-drill-evidence.md 记录 Attempt 1–5 均为 NO_GO,该任务尚无数据库与附件完整隔离恢复成功证据。restoreTested=false 不能只解释为本次没有顺手测试,必须带上这五次未完成的历史。
- 原因边界:Attempt 3 只确认数据库恢复命令非零,底层原因未知;其余身份/目标确认/安装器等问题有对应记录。不得把五次全部归因于已证实的演练器缺陷,也不得把五次 NO_GO 等同五份备份损坏或不可恢复。
- 异机链后续:08-28-production-arm-backup-transfer-incident/implement.md 记录 2026-08-28 已自然恢复一个新鲜 ARM 原子批次(约 12.71 小时龄),dead-man 为 PASS_OFFHOST_DEADMAN_FRESH,生产 daily/watchdog success/0。两连续自然周期和容量验收尚未收口,不能说事件已修复,也不能把 08-27 的链路失败直接当作 09-08 当前状态。
- 源端成功状态的含义:当前受管 scripts/run-automatic-backup.sh 在 rsync 和异机 finalize 返回成功之后才写 last-success。今日用户校验的成功状态因此不能简单解释为仅本地产出成功;但尚未在本轮独立读取 ARM 最新归档或核对远端脚本版本,不以本地回执替代异机现状验真。
- 容量已有更新:09-08 镜像保存前目标文件系统可用约 10.09 GB,保存后约 8.98 GB(约 8.36 GiB)。这是本轮保存时观测,不是此刻两端全部数据/备份路径的实时容量;必须计入新增镜像占用,不能继续引用约两周前的 10 GiB 作为当前余量。
例外必须显式保留:五次 NO_GO 且完整恢复能力未证实;摘要匹配不等于可恢复;异机归档现状和连续周期尚未本轮独立核验;数据库回退可能丢失备份时点后数据且镜像回退不能撤销 seed 写入;完整候选验收未通过。过去部署承担过风险不是本次可接受的证据或授权。
建议下一步限定为获批后的单次只读检查:生产数据库/附件/备份/镜像制品所在文件系统的容量,ARM 最新正式归档的时间、身份/完整性及相关 timer/service/dead-man 状态;只输出脱敏结果,不触发备份/传输/恢复,不清理、不改配置或权限。当前只读管理员通道授权待确认,不因用户提出调查建议自行连接或部署。
### 6.10 获批两端只读复核结果(2026-09-08 15:02–15:04 CST)
用户已对 §6.9 的限定只读操作明确回复同意,执行端使用既有生产管理员 SSH 通道完成源端采集,ARM 使用现有登录账号检查既有非交互 sudo 能力;未上传远端探针文件,代码经 stdin 执行。此为新的执行端直接观测,与 §6.8 用户回传证据分开记录。
| 检查 | 结果 | 证据边界 |
| --- | --- | --- |
| 生产容量 | 数据库、附件、本地备份及旧镜像制品同一文件系统;总计 63,291,080,704 字节,已用 54,349,017,088,可用 8,925,286,400(约 8.93 GB / 8.31 GiB),使用率 86%,可用 inode 1,400,907 | 不是马上磁盘满,也不是容量验收通过;未进行空间来源全盘调查或清理 |
| 本地备份 | 最新批次创建于 09-08 02:30:02 CST,约 12.53 小时龄,24 小时内;manifest 身份、精确三文件、数据库/附件字节数与 SHA-256 通过,版本/镜像匹配 f223bfb | 未恢复;只完整哈希最新批次,不声称所有历史批次均完整 |
| 本地正式批次占用 | 7 个目录,文件合计 141,513,496 字节;最新 DB 2,888,951 字节,附件包 17,337,974 字节 | 不把全部磁盘占用归因于正式备份;其他对象/临时产物未全盘盘点 |
| 生产 daily/watchdog | daily 今日 02:30:01–02:30:51 success/0;watchdog 今日 07:10:01–07:10:02 success/0;两 timer active/waiting | oneshot 执行后的 inactive/dead 不表示故障;不是两连续自然周期完整验收 |
| 源端异机成功回执 | 实际 runner SHA-256 与 f223bfb 受管源码一致;最新 last-success 与最新本地批次身份/manifest 摘要一致,源码只有异机传输及 finalize 成功后才写状态 | 支持“今日源端确认过异机成功”,不能替代本轮 ARM 文件独立验真 |
| ARM dead-man 调度 | 普通账号可读:今日 06:30:05 service success/0,timer active/waiting | 未读取私有 last-deadman summary,未校验远端运行脚本摘要,不冒充实际包摘要复核 |
| ARM 归档和容量 | 未完成:SSH 可连;现有 sudo -n 需要密码,未得到管理员执行能力 | 不表示归档缺失、损坏或磁盘不足;没有尝试绕权限或修改授权 |
生产当前容器 BUILD_SHA 和镜像 ID 仍匹配 f223bfb;执行端本轮重新验证了本地备份完整性,但没有查询业务库、个人日志或下载备份。ARM 第一次无脱敏结果返回,后续只读能力检查确认密码要求后停止受限部分;补读普通账号可见 systemd 属性不涉及私有归档访问。
结论:已有今日源端成功回执与 ARM dead-man 成功状态,不能把历史中断写成目前仍在持续;但 ARM 当前最新正式归档、文件摘要和目标路径容量仍未独立验证,P1 不结案。生产 86%/约 8.93 GB 余量保留容量风险,不能仅凭最新成对备份约 20.23 MB 推断部署构建峰值也可承受。
证据目录:`artifacts/qa-flow/manager-ap-access-scope/offhost-readonly-20260908/`;production-01.json、arm-access-diagnostic-01.json、arm-services-unprivileged-01.json。ARM 首次失败产物原样保留,不覆写成成功。比较探针通过 Python 语法、合成批次成功/损坏反例、文件系统分组检查;禁止操作未执行。
本次无备份/传输/恢复触发、无权限配置变更、无清理、无部署、无提交/推送。发布例外、恢复演练和剩余容量处置均未获批,不能从用户的只读授权推导。
#### 15:46–15:47 CST 追加:直接执行授权与实际凭据限制
用户要求由执行端直接操作,不再转交手工。使用既有 ARM 主机配置/密钥、显式 root 身份仅请求 id -u,结果为 SSH exit 255 且认证拒绝;不是主机连接失败或 host key 失败。此结果不能概括为 ARM 禁止所有 root 登录,只说明当前配置/密钥未成功认证。
随后以普通账号运行已批准只读探针,systemd 可读,dead-man 阈值为 93600 秒(26 小时,与恢复演练 24 小时门禁不同);读取归档路径容量时明确 PermissionError。当前身份既不能通过非交互 sudo,也不能直接读取该路径,故停止受限部分,不尝试其他凭据、不改权限、不借其他进程/容器绕权限。
普通账号可直接观测的根文件系统容量为总计 210,518,515,712 字节、可用 95,961,047,040 字节、使用率 53%。此为根文件系统事实;未确认受限归档目录的实际文件系统归属,不能把“约 95.96 GB 余量”写成归档路径已验证。证据 arm-unprivileged-02.json、arm-root-filesystem-01.json 保留在现有 offhost-readonly-20260908 目录。
不再要求用户反复手工执行。现有能力下未完成的 ARM 归档摘要及目标路径容量保持未验证;不自动形成发布资格。用户已授予本轮限定操作权限与现有凭据能否执行是两个独立事实。
### 6.11 ARM 归档与容量补证(用户管理员终端回传)
用户说明具备交互式 sudo 能力后,在 ARM 终端执行提供的只读检查命令并回传 JSON。本节补足 §6.10 当时未能读取的最新正式归档和目标路径容量,不改写执行端之前的权限拒绝,也不冒充执行端自行提权或直接读取了私有归档。
- status=PASS_INTEGRITY_ONLY;归档创建时间 2026-09-07T18:30:02+00:00,即北京时间 2026-09-08 02:30:02;校验时年龄 16.48 小时,在 24 小时门禁内。此为检查时事实,不是发布时永久有效的新鲜度保证。
- batchIdentitySha256=ecefcda74bacb640f842ca76ba9d6182ac03daf6700d8ce5400f21de52073b00;manifestSha256=902afe5a22e6624db99ea86a3873377b54561367509e0cdb8e74e6a73209a197。执行端已与生产 production-01.json 比较,两项逐字一致,说明源端和 ARM 所核对的是同一批次及同一 manifest。
- buildMatchesExpected=true、imageMatchesExpected=true;数据库包、附件包均为 size_and_sha256_match。结合源端独立哈希结果,两端对应包均匹配同一登记摘要;未下载/恢复真实备份数据。
- 归档路径 statvfs 返回可用 95,960,739,840 字节(约 95.96 GB / 89.37 GiB)、使用率 52.4%。本轮已取得归档路径本身的容量,不再仅依赖根文件系统推测;此前 53% 是另一时刻且向上取整的根文件系统口径,不据此推断磁盘趋势。
- restoreTested=false。完整性与归档存在已补证,不代表恢复能力、连续自然周期、故障根因修复或 P1 结案。不得将“5 次 NO_GO、无完整恢复成功记录”弱化成“本次未测”;也不将其解释为备份损坏。
当前发布例外应保留的缺口更新为:完整候选验收未通过;五次恢复演练未完成,数据库/附件完整恢复能力未获实证;旧镜像虽已独立保存及校验,未做导入/回滚演练且不能撤销 seed 写入;数据库恢复可能丢失备份时点后的业务数据;生产上次实测 86% 使用率/约 8.93 GB 余量,不是部署构建峰值容量保证。异机最新归档“未验真”不再作为当前缺口,但实际发布前仍须重新核对时效、运行身份与容量。
本轮只对齐用户回传与本地证据、更新既有 task 文档;没有新增远端操作、备份/传输/恢复/部署,也未提交或推送。此补证不是发布例外或部署授权。
### 6.12 一次性发布例外批准记录(2026-09-08)
用户对明确限定的审批问题回复“批准”:仅针对候选 58dcee0 的一次性发布例外,接受 §6.11 及审批问题列出的验证缺口;该次审批明确不执行部署,实际部署另行授权。本记录于北京时间 2026-09-08 19:05 后写入,不将记录时间冒充用户消息的精确发生时间。
- 固定候选:58dcee08f72013fe5b9448a3c2e82a759ba5b01f;已核实生产基线/应用回退目标:f223bfb72fb2baef51199702c2def84c41af7aef。例外不覆盖任意后续提交或与此基线不同的未审变更。
- 明确接受的缺口:完整候选验收未通过;恢复演练五次 NO_GO、尚无数据库与附件完整恢复成功证据;旧镜像未做实际导入/回滚演练;应用回退不能撤销 seed 的数据库写入,数据库恢复可能丢失备份时点后的业务数据;生产容量上次 86%/约 8.93 GB 余量不能当作发布构建峰值保证。
- 执行前条件保留:重新核对实际运行身份、固定候选、备份新鲜度/已校验证据对应关系、独立回滚制品和容量。条件不足、无法确认或范围/风险实质变化时停止,不能用本批准覆盖新缺口。
- 实际部署、数据库恢复、备份/镜像清理、配置/权限修改、提交/推送都不由本次例外自动授权。回滚验收仍按 §5 的独立合同,不把数据库恢复混入应用回退,也不擅自跳过部署 seed。
- 此为单次候选风险接受,不是测试通过回执、常设门禁豁免或 R1 启用。58 条红项、合同阶段 B、六个 view_all、恢复演练与 P1 连续周期/容量治理不随之结案。
当前状态更新为“发布例外已批准;实际部署未授权、未执行”。此前未批准的记录均为各阶段历史状态,不倒填为先前已经取得资格。
### 6.13 部署授权与验收凭据预检阻碍(2026-09-08)
用户随后明确“批准例外,可以部署”,并指定版本、经理拒绝/财务可读/CEO 可读/经理复核入口保留四项验收;验收通过后再删除旧镜像制品。实际部署授权已取得,不能继续写成未授权。用户引用的 ARM 未核验状态已由 §6.11 补证,不回退事实。
部署前按 §5.3 在固定 58dcee0 干净隔离检出重新运行既有 AP/权限/导航/导出/付款回归,五文件 123/123 PASS;本地合成浏览器 6/6 PASS、单 worker、零重试并开 trace。没有迁移 31 个认证 spec 或修改业务断言;复验后来源仍干净。
为了避免部署完成后才发现无法验收,使用业务手册指定的受管 live-smoke 凭据,仅执行登录、/me 角色核对及成功会话登出;凭据和 token 只在进程内使用,不回显或保存原始账号/响应正文。19:26 CST 结果如下:
| 受管验收角色 | 观测 | 结论 |
| --- | --- | --- |
| manager | 登录 200、/me 200、角色匹配且仅 manager/employee 范围,mustChangePassword=true;登出 200 | 不能在不改密的条件下用于正常业务页面验收 |
| finance | 登录 403 | 当前凭据未取得有效会话;具体拒绝原因未判定 |
| ceo | 登录 401 | 当前凭据未取得有效会话;具体拒绝原因未判定 |
每个凭据仅一次尝试,未反复重试、查密码、改密、修复角色、伪造 token 或冒用其他身份。它们是受管验收账号的结果,不能推广成真实业务人员账号不可用。登录/登出及失败请求会走既有认证写入(会话/审计/计数),不宣称这部分完全不写库;没有真实业务提交/审批/付款。
既有一次性例外接受完整候选验收和恢复演练缺口,但不能自动豁免此次明确要求的四项公网角色验收。新前置不具备,故停在部署写入前,不运行 deploy-existing/deploy.sh、构建、migrate、seed 或容器切换。19:28 CST 公网 /ready=200、版本 f223bfb72fb2,生产 AP 仍未收窄。
下一分叉仅是是否另行接受“四项公网角色验收延后,先发布且保留旧镜像”;未获明确接受前不自行作此扩展。删除镜像的前提是验收通过,目前不满足,故未删除/转移。也不把本地 6/6 或 123/123 写成四项公网 PASS。
证据目录为 `artifacts/qa-flow/manager-ap-access-scope/release-20260908/`。本轮没有提交、推送、部署或镜像清理;暂停原因是实际验收能力缺口,不是 ARM 凭据或用户未批准部署。
@@ -1,5 +1,156 @@
# 执行计划
## 2026-09-08 付款账户配置只读核对与候选纠偏
- 用户附件要求综合判断后续部署与业务恢复,并提出核对此前配置的建行付款账户。本轮只读核对配置,不将附件中的提交/推送建议、批量主体回填建议或业务付款建议转换为执行授权。
- 21:10 CST 生产 f223bfb:legal_entities 共 1 条且启用未归档;payment_accounts 共 1 条且启用未归档,关联该启用主体及启用未归档的 CCB 银行字典;加密账号字段非空。只返回聚合数和存在性,没有输出或读取户名/账号明文,没有取回密文,没有调用解密。事务只读且 ROLLBACK 完成。
- 结论仅为“建行付款账户已配置”,不建议重复新建;解密和 snapshot 生成未验证。snapshot 的实现会在进程内解密账号,所以“验证 snapshot 且完全不解密”不能同时成立。若获准校验,仅回传成功/失败,不输出明文、户名或完整 snapshot。
- 候选必须包含本轮 CEO 撤权修订,不能部署旧 58dcee0;修订尚未提交。旧备份/回滚制品和 seed 比对作为历史证据保留,但发布前仍需核对时效、存在性及生产漂移,不能写成“只有候选 SHA 改一下即可”,新候选批准须明确。
- 不认可直接回填旧快照的 7 条员工:全表含 smoke 标识记录,须先明确真实业务员工范围和目标主体,复核当前字段及原值,再另批写入。唯一启用主体只是技术上唯一,不自动等于全部记录业务归属已获确认。
- 本地 readiness 代码除主体和付款账户外仍检查收款人银行/账号、单据状态、部门、批次占用及复核人等;未调用真实业务提交或组批,不宣布三张单必定可重提/审批/付款,也不凭角色默认断言唯一真实复核人。业务人员可先修订单据,但状态流转由实际系统结果验收,不替用户操作。
- 证据:artifacts/qa-flow/manager-ap-access-scope/payer-config-readonly-20260908/result.json。探针语法通过;本轮无生产写入、提交、推送或部署。经理关系修正保持已执行并回读完成,用户接受执行证据不冒充用户独立生产验证。
## 2026-09-08 财务直接经理单字段修正(明确授权)
- 用户已确认 wangzhiling 为财务本人、yj 为员工本人,并明确授权仅将财务 direct_manager_id 从 NULL 设为 majianyong;不以是否已经发生提交故障作为修正前提。
- 执行合同:唯一生产运行身份核对;事务内锁定两条员工记录及其角色记录,核对唯一身份、正常任职状态、完整角色及原经理空值;条件 UPDATE 仅一个字段、必须影响一行;提交后另开只读连接回读。任何前置不符均停止,不覆盖非空值;提交结果不明时不自动重试。
- 不改角色、审批规则、单据、其他员工字段,不运行 seed、部署、付款或审批。经理与 CEO 是否在实际流程中合并审批尚未验证,不声称两次审批或自动去重。回退亦须另行授权,不能将“可改回空”冒充无影响。
- 执行结果:2026-09-08 20:47:57 CST,PASS_APPLIED_AND_READBACK。原值在加锁事务内确认为 NULL,条件 UPDATE 影响 1 行,仅 SET direct_manager_id;提交后销毁原连接,用新连接只读回读,财务经理已指向 majianyong,双方完整角色与执行前一致。运行版本仍 f223bfb,未部署。
- 执行过程原样保留:首次 stdin 模式 require.main 不等于 module,执行函数没有被调用,包装器因空输出解析失败;先只读确认原值仍空,再修正入口。入口自测首次因模拟 process.env 缺失失败,补齐模拟后通过,不冒充首轮通过。第二次在 UPDATE 前检测到未审阅触发器并回滚;只读审查发现唯一触发器仅禁止修改 employee_no,不产生字段写入,无 UPDATE rule。固定触发器/函数定义摘要,并在表级 ROW EXCLUSIVE 锁内复核后执行;未禁用任何触发器或约束。第三次成功提交并独立回读,没有重复成功写入。
- 证据目录 artifacts/qa-flow/manager-ap-access-scope/finance-manager-correction-20260908/:result-applied.json 为最终回执;result.json 为触发器前置拒绝回执;reconciliation.json、trigger-inspection.json、stderr.log 记录前次停止和只读复核;selftest.json 为身份/状态/原值校验,stdin-selftest.json 为修正后的入口及连接失败回执验证。最终执行脚本 SHA-256 为 9b3abf70a4b36313b800ef3caddbb53e704a9ce0e7ec2f0acad58cb4adf55c43。
- 本次数据关系修正已完成,不等于真实提交/审批闭环已验证。AP 修订保持用户已复核但未提交/部署;不因此启用门禁或恢复其他任务。
## 2026-09-08 AP 修订复核通过与全员工关系只读盘点
- 用户明确复核通过 CEO 读取修订;独立复核已由用户完成,不将其转换为提交、推送、部署或生产员工写入授权。
- 用户新增授权一次全员工列表查询,仅内部 ID、登录邮箱、角色、直接经理关系;不读取姓名、金额、票据、银行。输出用 E1–E8 替代 UUID,邮箱域名脱敏;完整邮箱与内部 ID 仅在远端进程内用于关联,不落本地回执。
- 20:36 CST 查询得到全表 8 条,非“8 条在职”;未查询任职状态,不能拿本次 5 条经理为空与旧 7 条在职/4 条为空直接比较。只读事务已成功 ROLLBACK,运行版本及镜像仍匹配 f223bfb。
- 脱敏清单:E1 smoke-finance,employee+finance,经理空;E2 majianyong,ceo+employee,经理空;E3 oa_smoke_02,employee+manager,经理 E2;E4 yj,employee,经理 E2;E5 smoke-manager,employee+manager,经理空;E6 oa_smoke_01,employee,经理 E5;E7 wangzhiling,employee+finance,经理空;E8 wangshunli,admin+employee,经理空。标签来自邮箱本地部分,不凭 smoke 字样断言所有此类记录都不是业务账号。
- 前次匹配失败的原因已具体化:系统记录为 wangzhiling 与 yj,而不是此前用于匹配的 wangzhuling 与 yangjun。两者是否分别对应用户所说的财务与员工仍需用户确认,不凭相似拼写/缩写认定真人。
- E7 财务角色记录的直接经理确实为空,是确定的字段缺口;若用户确认它是财务本人,则与“其他员工经理均为 majianyong”的业务口径冲突,须单独交审生产回填。没有执行提交,不能宣布已实测所有入口 409;E4 的经理配置已指向 majianyong,若其确为 yangjun,则此项关系一致,不代表全部审批链验证完成。
- 证据位于 artifacts/qa-flow/manager-ap-access-scope/employee-roster-readonly-20260908/result.json。探针语法检查通过;本轮不重跑产品测试、不改产品代码、不写数据库、不登录业务账号、不删回滚制品。部署仍暂停。
## 2026-09-08 身份确认后的 CEO 读取修订(待用户复核,不部署)
- 用户确认已匹配的邮箱同名账号为本人:wangshunli=admin+employee,majianyong=ceo+employee;确认其他员工业务直接经理为 majianyong,但不冒充数据库字段已一致。明确批准 AP 修订及另外两人的经理关系只读核对。
- 本轮运行时增量仅 server/src/constants/permissions.js、client/src/auth/permissions.ts 各删除 ceo 的 finance.ledger.view 一行。与 HEAD 全角色权限数组逐项比较,其他权限不变;审批路由、seed、工作流、账号、六个 view_all 均未修改。finance/finance_mgr 及 admin 通配保留,ceo+finance 仍按并集放行。
- 已更新角色、两个 AP GET、导航及浏览器矩阵:ceo 与 manager 拒绝;后端拒绝时零财务查询;前端直连 403、无入口、零 AP 请求。财务及两种混合角色正例渲染台账;CEO 四类审批与付款复核权限保留,导航单测确认付款入口保留。
- 复验:后端五文件 124/124 PASS;本地合成浏览器 7/7 PASS,单 worker、零重试、trace、2.7 分钟;typecheck、独立 artifacts 构建、全角色增量核对及 diff 检查通过。不是完整候选验收,不是生产角色验收;构建仍有既有 Browserslist 陈旧提示。独占 Vite 已停止,未修改归档截图。
- 20:23 CST 限定只读查询:majianyong 精确匹配一条;wangzhuling、yangjun 在工号、邮箱本地部分、企业微信绑定三个限定匹配方式下均为零条,因此二人的 direct_manager_id 未核实,不是查得为空,也不能断言不能提交。需要两人的系统工号才能继续定位,不需要密码、完整邮箱或业务内容。事务只读并成功 ROLLBACK,生产版本仍 f223bfb。
- PRD、当前设计矩阵、权限规约已同步;旧发布历史保留并明确被新矩阵覆盖,旧候选批准不自动转用于新改动。用户独立复核待办,不派代理,不以主会话自检冒充独立复核。
- 证据目录 artifacts/qa-flow/manager-ap-access-scope/ceo-scope-revision-20260908/:backend-final.log、browser.log、browser-results、typecheck.log、build.log、scope-check.json、manager-check.json、governance.log。review-code.diff 为代码/测试/规约增量,review-full.diff 包含既有未提交任务预检时间线,不能把后者全部归作本轮新增。
- 验证命令:server 下使用既有 PAYMENT_ORDER_PYTHON_BIN 执行 npm test -- tests/unit/routes/apOperations.permissions.test.js tests/unit/constants/permissions.test.js tests/unit/workspaceSettings.navigation.test.js tests/unit/routes/exports.apOperations.test.js tests/unit/routes/paymentBatches.reimbursement.test.js;client 下 npm run typecheck、vite build --outDir 指向上述独立目录;E2E_MODE=local E2E_BASE_URL=http://127.0.0.1:23119 playwright test tests/e2e/payment-reviewer-permissions.spec.ts --grep '只有付款复核职责|AP 专用读取权限' --workers=1 --retries=0 --trace=on --reporter=list,输出指向上述目录。
- 未提交、推送、部署或写生产;不删回滚制品,不回填员工经理。58 条、阶段 B、R1 保持暂停。交付保留:经理和 CEO 仍有原报销列表全量查看能力,本轮只收窄 AP。
## 2026-09-08 四账号与三单关系只读核对(最新边界)
- 用户批准四个指定账号的完整角色、当前与候选 AP 权限来源、通配符、直接经理及审批职责核对;分开核实当前 rejected 单的 submitter 与 owner。不读取金额、票据、银行、姓名或联系方式。
- 使用精确企业微信绑定匹配,缺失不猜身份;只读事务成功回滚后输出脱敏结果。当前三单不自动等同旧快照中的三单。
- 部署暂停,不修改账号、不移除角色、不删除回滚制品;结果交用户判断,不自动恢复部署。
- 20:06–20:07 CST 两次受控只读事务均回滚完成,生产版本和镜像仍匹配 f223bfb;运行权限表、认证插件、AP 路由摘要匹配旧版本。四标识精确企业微信绑定均无匹配;补充限定工号/邮箱本地部分候选查询,未扫描或输出人员名册。
- wangshunli 邮箱同名唯一候选为 admin+employee、持 *,当前及候选均可读 AP;majianyong 邮箱同名唯一候选为 ceo+employee、无 *,当前由 payment.batch.review、候选由 finance.ledger.view 放行。两候选均 active/在职/未归档、直接经理为空。邮箱同名不等于真人身份确认,不能写成四真人已核实。
- wangzhuling、yangjun 在以上匹配方式中均未找到;不推断账号不存在或角色为何。四账号完整映射尚未完成,需业务确认候选和提供另外两人的系统身份定位,不需要密码或银行信息。
- 当前恰有三条 rejected,三条 submitter_id=owner_id,均指向 majianyong 邮箱同名候选。未读取单号、金额、票据或银行,不能倒填历史三单对应关系。
- 当前启用的三条审批规则最高档确为 manager→ceo→finance,owner_self_fallback_role=finance;活动报销工作流引用 ceo。majianyong 同名候选具有四类审批及付款复核权限,明确分配给该候选的 workflow_tasks 有 completed 2 条,无其他状态;不把权限资格或任务数当作当前完整审批责任。其他活动工作流未提取到 role 类型引用不等于没有其他分配规则。
- 证据:artifacts/qa-flow/manager-ap-access-scope/account-role-readonly-20260908/ 的 result.json、result-candidates.json;后者账号标签仅表示邮箱同名候选。探针语法通过,未执行登录、部署、seed 或业务写入。初始绑定结果与补充候选结果分别保留。
## 2026-09-08 实际部署(已授权,公网角色验收前置失败,未进入部署写入)
- 用户明确批准例外并表示“可以部署”,目标仍为 58dcee08f72013fe5b9448a3c2e82a759ba5b01f;沿既有受管发布入口执行 migration/seed、应用切换、健康与版本校验及 AP 定向验收,不扩展产品改动。
- 用户消息沿用了旧的“ARM 归档未核验”状态;实际已由用户管理员回传并与源端对齐,不将已补证事实退回未知,也不因此另索同一批准。
- 执行顺序:固定干净候选定向复验 → 执行前运行身份/备份时效/旧镜像制品/容量检查 → 受管发布 → 版本与基础检查 → 经理 AP 拒绝、财务/CEO 可读、经理复核入口保留。使用既有受管 live-smoke 凭据,仅在进程内读取;不运行真实审批、复核、付款、导出或角色修改。
- 用户提出验收通过后删除旧镜像制品;该条件尚未满足,发布和验收过程中保留制品,不提前删除、不转移到 ARM、不做其他空间清理。容量与管理员凭据后续事项仍不扩大为本轮修复。
- 未授权数据库恢复、永久门禁豁免、提交/推送或其他任务归档。任何实际失败和验收缺项如实记录,不以部署命令退出 0 替代角色验收。
- 固定 58dcee0 干净隔离来源复验通过:后端五文件 123/123、六条合成浏览器 6/6,workers=1、retries=0、trace=on;浏览器耗时 1.6 分钟,独占 Vite 已停止,候选工作树仍干净。这不是完整候选验收,也不是公网角色验收。
- 19:26 CST 验收凭据预检发现新阻碍:受管 manager 登录 200、/me 200、角色匹配且无额外管理角色,但 mustChangePassword=true;finance 登录 403;ceo 登录 401。每个凭据仅尝试一次,成功的经理会话已登出(200),没有重置密码、修改角色、尝试替代凭据或操作业务。
- 登录/登出及失败尝试可能产生正常认证会话、审计、失败计数写入,不能表述为“生产完全零写入”。本轮未运行部署入口、migrate、seed、构建或容器切换;19:28 CST /ready 200、构建仍 f223bfb72fb2。
- 此结果仅说明当前受管验收凭据不能完成要求的四项公网验收,不证明真实业务人员账户故障;401/403 原因未进一步推断。验收范围实质变化不由已批准例外自动覆盖,暂停在部署前,待用户决定是否接受公网角色验收延后。镜像删除的“验收通过”条件未成立,保留独立制品。
- 证据位于 artifacts/qa-flow/manager-ap-access-scope/release-20260908/,含 backend-tests.log、browser-tests.log、browser-results、live-credentials-precheck.json、ready-before-paused-release.json;公网预检未保存 token、密码、完整身份或响应正文。
## 2026-09-08 一次性发布例外(用户已批准,实际部署待单独授权)
- 用户对“仅针对候选 58dcee0 的一次性发布例外,接受已列明缺口;这一步不执行部署”明确回复“批准”。固定候选为 58dcee08f72013fe5b9448a3c2e82a759ba5b01f,已核实生产基线为 f223bfb72fb2baef51199702c2def84c41af7aef。
- 接受范围:完整候选验收未通过;五次恢复演练均未完成、数据库/附件完整恢复及镜像实际回滚未经成功演练;镜像不撤销 seed 写入、数据库恢复可能丢失备份时点后数据;生产容量仍须执行前复核。
- 不授权本轮部署、提交、推送、恢复数据库、修改权限/门禁或清理。候选改变或风险/实际写入范围实质变化重新交审;实际部署前仍复核身份、备份时效、回滚制品及容量,不足或无法确认则停止。
- 此处仅登记例外决定,不把未通过测试标为通过,不启用 R1,不关闭恢复演练/P1 事件或扩大六个 view_all 范围。下一步只请求实际部署授权。
## 2026-09-08 ARM 管理员回传归档核验(此前受限项已补证)
- 用户自行在 ARM sudo 终端执行所提供的只读命令并回传 PASS_INTEGRITY_ONLY。最新归档创建于北京时间 09-08 02:30:02,校验时 ageHours=16.48、within24Hours=true;DB/附件大小及 SHA-256 均匹配,版本/镜像匹配预期。
- 本地将回传 batchIdentitySha256 和 manifestSha256 与生产 production-01.json 逐字比对,两项均相等;两端同一批次/manifest 的证据链已对齐。证据来源为用户回传,不冒充执行端直接取得 ARM 私有文件。
- 归档路径实际 statvfs 回传可用 95,960,739,840 字节、使用率 52.4%,补足此前仅有根文件系统代理量的缺口;不据此证明具体挂载身份,也不倒填旧时间点。
- 不再保留“本轮异机归档文件未验真”作为当前缺项;保留五次恢复 NO_GO、未做成功恢复/镜像导入演练、完整候选验收未通过、生产容量及备份时点后数据损失风险。P1 连续周期/容量验收未因此结案,发布仍未批准。
## 2026-09-08 ARM 管理员通道直接尝试(现有凭据受限,不再转交手工)
- 用户明确要求执行端直接操作、不再询问;据此对同一 ARM 目标用现有 SSH 配置/密钥尝试 root 身份,仅执行 id -u 验证。认证被拒,未登录成功;不尝试密码、其他身份或改授权。
- 普通账号执行现有只读探针,确认在 arm_capacity 阶段 PermissionError,归档受限不是仅凭 sudo 提示推断。dead-man 状态仍 success/0,阈值读到 93600 秒;未读取归档内容。
- 15:47 CST 补取普通账号可访问的 ARM 根文件系统:使用率 53%,可用 95,961,047,040 字节(约 95.96 GB / 89.37 GiB)。未证明该文件系统就是私有归档目录所在文件系统,不以此替代归档盘实测。
- 当前阻碍是凭据/文件权限而非业务授权;保留 ARM 最新归档及所属文件系统未独立验证,不继续索取用户手工操作,不把受限结果写成通过。无权限变更、生产写入或部署。
## 2026-09-08 两端容量与异机链只读复核(生产完成,ARM 权限受限)
- 用户已同意通过既有生产管理员和 ARM SSH 通道,由执行端完成一次限定只读检查:两端空间、最新正式异机归档时间/完整性及备份 timer/service/dead-man 状态。
- 使用现有权限;拒绝访问即停止该项,不修改权限。不触发备份/传输/恢复/告警,不清理、不上传远端探针文件、不部署。探针通过标准输入运行,只回传脱敏结果;真实备份不下载到开发机。
- 生产 15:02 CST:数据库/附件/备份/旧镜像制品均位于同一文件系统,使用率 86%,可用 8,925,286,400 字节;最新成对备份完整性通过、约 12.53 小时龄,正式本地批次目录 7 个,合计文件约 141.51 MB。运行 SHA/镜像未变,daily/watchdog 成功、timer active。
- 生产实际备份 runner 摘要与 f223bfb 受管源码匹配,last-success 对应最新本地批次且 manifest 摘要匹配;该源码在异机 finalize 成功后写成功状态。因此源端有今日传输/finalize 成功回执,不应继续称“两周前失败一直未恢复”。
- ARM SSH 可连,但 sudo -n 执行失败;只读 sudo 能力检查明确 passwordRequired=true。停止受限归档/容量读取,不更换身份、不索取密码、不改权限。普通账号读取到 dead-man 今日 06:30 success/0、timer active;未读取最新正式归档或 last-deadman 文件,未重算 ARM 包摘要。
- 结果不能冒充两端全通过、两连续自然周期验收或恢复成功。证据位于 artifacts/qa-flow/manager-ap-access-scope/offhost-readonly-20260908/;脚本语法、合成批次正反检查及文件系统分组检查通过。详见 design.md §6.10。
## 2026-09-08 发布例外恢复风险补充(本地核对完成,远端新调查待授权)
- 核对恢复演练 Attempt 1–5 全为 NO_GO;该 task 没有完整恢复成功证据。Attempt 3 底层恢复失败原因未知,不能把五次全部解释成已证实的工具缺陷。
- 后续 P1 task 记录 08-28 自然恢复一个新鲜 ARM 原子批次,但连续两个自然周期及容量验收未收口;不以 08-27 失败记录倒填今日现状。
- 发布例外已补上历史未成功恢复、异机现状未独立复核、数据回退损失和完整候选验收缺口,详见 design.md §6.9。镜像保存后可用约 8.98 GB,不能忽略本轮新增制品占用。
- 下一步拟一次只读核对两端容量及异机链,等待限定管理员通道授权;不实际恢复、不触发备份、不清理、不部署。
## 2026-09-08 管理员只读备份校验脚本交接(用户已回传完整性通过)
- 用户确认能使用具备备份读取权限的服务器管理员账号,并单独批准上传脚本、由用户在管理员终端执行。该授权不包含执行端提权或自行读取受限备份。
- 校验脚本已上传至系统临时目录下独立目录 hyb-oa-backup-check-7fptuwbq,目录 0700、文件 0600;文件名 check-backup-admin.py。上传后复读 SHA-256 与本地一致:58a604fc1e0ebab824553fa0507683ed5d8c44872d36d56c41574d8959ea6a35。
- 本地合成正例、数据库包损坏、状态文件缺字段反例通过;仅上传,未执行远端校验脚本、未读取备份、未改变既有权限。回执 admin-script-upload-01.json 存于既有 rollback-image-20260908 证据目录。
- 交给用户的命令先校验脚本内容摘要,再以 Python 隔离模式执行同一份已读字节;只回传脱敏校验结果。用户已回传 PASS_INTEGRITY_ONLY:manifest、数据库包、附件包摘要/大小匹配,备份版本 f223bfb,完成于北京时间 2026-09-08 02:30:51,校验时年龄 9.56 小时;restoreTested=false。
- 结果归因为用户在管理员终端执行,不冒充执行端直接核验。管理员 SSH 别名发现过晚造成不必要手工操作;后续先检查既有通道,再请求限定授权,不将连接配置当作已有无限授权。详见 design.md §6.8。
- 镜像独立保存、备份完整性和 seed 比对结果已具备,发布例外仍未批准。恢复实测、完整候选验收及恢复备份后丢失后续业务数据的风险仍须明确;未追加生产操作。
## 2026-09-08 独立旧镜像制品保存(已获批执行并完成校验)
- 用户对“保存 f223bfb 当前运行镜像为独立文件并校验;先检查空间、不重启、不切换、不删旧镜像、不部署”明确回复同意。
- 执行范围:核验容器/镜像身份及目标磁盘空间,在部署身份的持久用户目录中创建独立私有制品目录,docker save 固定镜像 ID;核对归档、配置 ID、层摘要,记录文件 SHA-256 和前后运行身份。空间不足或身份漂移停止,失败产物不冒充可用制品。
- 不读取受限数据库备份、不变更现有权限、不执行 docker load/镜像切换/清理;本授权不是备份验真、恢复演练或发布授权。
- 09:48:50 CST 保存完成:旧运行镜像 ID 与预检一致;独立 image.tar 为 1,117,595,648 字节,配置摘要及 14/14 层摘要匹配,另起只读校验进程复读 SHA-256 通过。容器身份、启动时间和运行版本未变。
- 制品位于部署身份持久用户目录、Docker 存储之外;私有目录 0700、文件 0600。它仍是同机副本,未做 docker load 或恢复演练,不覆盖主机/磁盘损坏风险。位置锚点、文件名与完整摘要见 design.md §6.7 及脱敏回执。
## 2026-09-08 剩余 seed 对象比对(已完成,不替代恢复前置)
- 09:22:19 CST 只读快照:费用类型 44、编号 1、分类 11、考勤 1、预算规则 1、假期类型 3、假期额度策略 3、工作区 1、部门 2,共 67 项,均唯一存在且实际覆写字段一致;CCB 已存在、3 条审批规则走保留分支。
- 这是配置目标字段一致,不是 seed 无写入。未查询员工假期余额;管理账号分支引用早先证据,本次未重查。前一轮 14 开关/四工作流/39 日历与本轮不是同一快照。细项与限制见 design.md §6.6。
- 只读事务 ROLLBACK 完成;源摘要匹配固定候选。比较脚本语法、比较原语正反检查和 67 项结果断言通过;首次本地自检路径映射错误已修正,无生产重试。
- 备份渠道仍待指定,独立镜像制品保存未获明确执行授权;不自动把补查完成转换为发布资格。R1、阶段 B、58 条及业务消息状态不变。
## 2026-09-08 seed 只读差异补查(已完成三组对比,仍不发布)
- 按用户授权比对默认 14 个强制开启开关、四类工作流和 CN_2026_STD 日历;实际 init 额外 flags 为 0。08:42:41 CST 快照结果:14/14 开关目标字段一致;四类仅 v1 激活且定义一致;日历表头及 39/39 日条目一致,无多余日期或待补主体绑定。
- 查询严格 READ ONLY 并完成 ROLLBACK;未执行 seed。即使目标值一致,version 递增、工作流取消激活再写回、日历删除重建仍会发生;其余 seed 写入对象未逐字段比对,不宣称全量无副作用。
- 证据在 artifacts/qa-flow/manager-ap-access-scope/seed-readonly-diff-20260908/;首次 Compose v2 不兼容失败与后续 v1 成功结果分开保留。详见 design.md §6.5。
- 备份验真仍受授权通道限制,独立回滚制品未固定;不绕权限、不擅自执行制品写操作、不批准或执行部署。58 条、阶段 B、R1 与业务消息状态不变。
## 2026-09-08 获批生产只读预检(已执行,未部署)
- 用户单独批准只读预检及 seed 行为核实。本轮从固定 58dcee0 干净隔离检出,以 PRODUCTION_READ_ONLY_PREFLIGHT=1 执行既有 bootstrap;exit 0,13/13 合同检查,迁移 pending=none,既有回滚窗口 OPEN,明确未进入 deploy.sh。
- 生产 BUILD_SHA=f223bfb;对候选 7 提交/103 文件,其中应用源码 7 文件 +11/-4,deploy/schema/migration/seed 无差异。seed 在生产容器内的摘要也与候选一致。
- 只读 seed 账号分支为唯一 active admin+employee,重跑角色集合不新增,但仍重写角色记录/时间。配置 seed 会更新或重置多类配置,不是零写入;细节见 design.md §6。
- 备份服务本日 02:30 成功、合同版本一致;备份目录元数据权限不足,未验证 manifest/备份文件摘要。旧运行镜像可 inspect,但未固定独立回滚制品,不把预检 PASS 扩大为备份恢复和发布资格 PASS。
- 未改生产权限或通道,未部署、迁移、seed、真实业务写入或查个人访问日志;只读 SQL 显式 READ ONLY 并 ROLLBACK。业务消息仍未发送。
- 本轮只补两份既有 task 文档与忽略目录证据,不提交、不推送,不修新问题。两个未跟踪其他 task 原样保留。
## 2026-09-08 发布方案准备(仅文档)
- 提交推送已完成:门禁 `9916712`、AP `58dcee0`,与已有两个 Trellis 提交一并到远端。此处如实补记实际结果,不生成第三笔提交或再次推送。
- 用户同意继续准备发布方案;已在 design.md §5 分开写单次例外批准、验证缺口、回滚验收。生产预检、执行发布、seed 写入及回滚均未授权或执行。
- 本轮只读仓库发现既有部署 run-init 包含迁移和 seed,不能承诺“无 schema 改动即零部署数据写入”;实际生产基线和回滚点待获批只读预检。
- 业务三个具体问题仍待真实人员沟通,没有发送工具,不代发、不记为已完成。非建行限制应按既有付款工具合同解释,不能未经业务决定扩展成所有付款路径禁止。
## 2026-09-08 独立复核与提交授权
- 用户已独立复核通过本 task 的 18 文件包,包括单权限守卫、原有获准角色无误伤、经理复核保留、前后端正反测试与 AP-only 边界;不是主会话自检替代独立复核。
@@ -55,3 +206,21 @@ git diff --check
- 复核代码、正反权限结果和范围;本轮不请求或执行生产变更。task 保持 in_progress。
- 明确保留:经理仍可从报销列表查看全部单据,本轮只收窄 AP 入口,未实现所有业务域“只看相关单据”。
## 2026-09-09 T0 独立交审副本
用户批准后续规划开始本地实施。AP 10 个既有差异文件已完整复制到专属 detached worktree,基线为 58dcee0;只在此副本纠正 task.json 的陈旧 CEO 授权描述并追加本节。付款按钮/制单测试的四文件差异未进入本工作树,原主工作树全部改动保留作对照,不宣称原混合工作树已清空。
隔离副本后端5文件124/124通过,保留付款复核/导出旁路回归。首轮123通过、1条BIFF8导出预期200实际409;本地默认Python缺xlwt/xlutils,补充已有固定PAYMENT_ORDER_PYTHON_BIN后同一集合124/124通过,代码与断言无改动。未重跑浏览器,既有独立复核及浏览器结果仍为此前证据,不倒写成本轮复跑。
| 代码 | 发布 | 业务验收 | 等谁 |
| --- | --- | --- | --- |
| CEO撤权修订已复核,独立副本已隔离复验;摘要已纠正 | 未提交/推送/部署;58dcee0不是最终修复候选 | 真实账号线上AP权限尚未做新版本验收;其他view_all仍保留 | 等明确提交授权,之后推送/部署按授权推进 |
与本轮隔离/校验证据的关联见原主工作区 `artifacts/qa-flow/project-roadmap-execution-20260909/`。本次是主会话自检,不冒充新增独立审核。提交建议标题:`fix: 取消CEO默认AP台账权限(09-07-manager-ap-access-scope)`;标题建议不代表已经提交。
## 2026-09-09 提交与推送授权
用户对“AP 与付款修复分两笔提交并推送,不部署、不执行真实恢复、不混入后续准备文档”明确回复“是”。本次只提交本 task 对应已隔离交审文件及当前授权记录;原恢复 task、错误豁免后续阶段和另外两个未跟踪 task 不进入这两笔。实际提交 SHA 与远端结果以 Git 回执为准,不提前宣称 push 成功;生产仍未部署,业务验收状态不随提交改变。
@@ -2,9 +2,9 @@
## Goal
仅收窄 AP 财务台账与付款汇总的全量读取入口:只有经理/付款复核职责的账号不得借付款复核权限访问全公司 AP 数据;保留 CEO 全量读取、既有财务读取及经理正常审批和付款复核能力。
仅收窄 AP 财务台账与付款汇总的全量读取入口:只有经理/付款复核职责的账号不得借付款复核权限访问全公司 AP 数据;保留既有财务读取及经理、CEO 正常审批和付款复核能力;CEO 角色本身不授予全量读取。
2026-09-07 用户已批准 AP-only 方案实施,附修订:使用 finance.ledger.view 专用权限,不使用 CEO 角色分支。仅授予 ceo、finance、finance_mgr;前后端同步。当前 in_progress;不查询生产、不部署。
2026-09-07 用户已批准 AP-only 方案实施,附修订:使用 finance.ledger.view 专用权限,不使用 CEO 角色分支。原授予 ceo、finance、finance_mgr;2026-09-08 身份确认后用户批准修订为仅 finance、finance_mgr,保留 admin 通配,ceo 审批与复核不变。另批准限定员工经理关系只读查询;不写生产、不部署。
## 已确认事实与边界
@@ -29,7 +29,7 @@
## Requirements
- R1:仅持经理/付款复核职责、没有其他 AP 全量授权的账号访问两个 AP GET 接口必须被后端拒绝,不返回汇总、台账或计数;不得仅靠隐藏前端菜单。
- R2:CEO、finance、finance_mgr 通过专用权限保留两个接口的全量读取;旧付款/风险权限码本身不再隐含读取。不得通过授予 CEO 付款管理等写权限实现读取,也不得删除经理的 `payment.batch.review`。
- R2:仅 finance、finance_mgr 通过专用权限保留两个接口的全量读取;仅 CEO 角色拒绝;旧付款/风险权限码本身不再隐含读取。不得通过授予 CEO 付款管理等写权限实现读取,也不得删除经理的 `payment.batch.review`。
- R3:经理原有正常审批、付款复核及独立复核约束不变。前端提供既有付款复核入口,不以恢复 AP 全量访问解决导航问题。
- R4:多角色按显式授权并集合成;manager 与已授权财务角色组合不得因包含 manager 被一票否决。系统管理员身份不自动等同财务权限。
- R5:查询参数、日期、关键词、分页不能绕过 AP 读取守卫。导出保持原权限,不将 CEO 全量读取等同导出或写入授权。
@@ -37,11 +37,11 @@
## Acceptance Criteria
- [ ] 普通员工和仅经理角色对两个 GET 均被拒绝;功能开启时为 403,关闭时可保留既有无数据 409。拒绝路径不调用财务数据查询。
- [ ] CEO、finance、finance_mgr 正例通过;manager+财务组合保留其财务授权;仅系统管理员不新增授权。
- [ ] 普通员工、仅经理及仅 CEO 角色对两个 GET 均被拒绝;功能开启时为 403,关闭时可保留既有无数据 409。拒绝路径不调用财务数据查询。
- [ ] finance、finance_mgr 正例通过;manager+财务及 ceo+财务组合保留其财务授权;仅系统管理员不新增授权。
- [ ] 日期、状态、关键词等查询组合不能改变拒绝结果;经理 AP 导出仍拒绝,既有导出角色不退化。
- [ ] 经理付款复核正例及原有独立复核负例通过;不扩大其付款执行、组批或其他写权限。
- [ ] 前端 AP 直连被阻止、导航不暴露不可用入口,经理付款复核入口仍可达;CEO 和财务 AP 入口保留。
- [ ] 前端 AP 直连被阻止、导航不暴露不可用入口,经理付款复核入口仍可达;仅 CEO 的 AP 入口拒绝,财务 AP 入口保留。
- [ ] 六个 view_all 未修改,交付明确“仅 AP 收窄”,不冒充所有业务单据范围收窄。
- [ ] 针对性验证与只读独立复核有记录;无部署、生产查询或写入,除非另获明确授权。
@@ -50,7 +50,7 @@
- Fastify inject:两个 AP GET、筛选组合、旧权限码反例、专用权限正例、feature-off、无权时零财务查询。
- 角色与导航单测:经理、CEO、财务、财务经理、系统管理员、多角色;保持经理付款入口。
- 既有 AP 导出及付款复核回归:保留独立授权、制单/复核分离及 CEO 无新增导出权。
- 本地合成浏览器:经理直连 403 和零 AP 请求、CEO/财务/财务经理/混合角色进入 AP 和台账、经理付款页保留;零重试、trace,排除合同用例及其他红项。
- 本地合成浏览器:经理和 CEO 直连 403 和零 AP 请求、财务/财务经理/混合角色进入 AP 和台账、经理付款页保留;零重试、trace,排除合同用例及其他红项。
- 前端 typecheck、隔离目录 build、语法、diff、治理检查。扩大付款回归需既有固定 BIFF8 Python 依赖,不改产品逻辑绕过环境缺项。
- 独立只读权限复核待用户;inline 主会话自检不冒充独立复核。
@@ -2,7 +2,7 @@
"id": "manager-ap-access-scope",
"name": "manager-ap-access-scope",
"title": "经理 AP 财务台账与付款汇总访问收紧",
"description": "使用 finance.ledger.view 收紧 AP 全量读取,保留 CEO 财务读取与经理复核;其他 view_all 和导出授权不变,不部署",
"description": "使用 finance.ledger.view 收紧 AP 全量读取,仅 finance/finance_mgr 授予,保留 admin 通配;CEO/经理审批与付款复核、其他 view_all 和导出授权不变。实现已复核,生产发布单独验收。",
"status": "in_progress",
"dev_type": null,
"scope": "AP-only 专用读取权限、对应前后端投影和权限验证",
@@ -0,0 +1,67 @@
# 业务闭环方案(规划,未实施)
## 1. 复用而非重建
现有入口是付款页面的待组批、付款批次、复核及“下载银行制单”。后端 POST /api/payment-batches/:id/payment-order 要求财务角色与 payment.order.export,批次 reviewed,完整且可用的同源收付款快照。生成成功会更新导出计数和审计;不能把这个 POST 当作生产只读预检。
代码入口:server/src/routes/paymentBatches.js、server/src/services/paymentOrderXls.js、server/src/scripts/fill-payment-order-template.py、client/src/pages/PaymentBatchesPage.tsx。历史范围见 archive/2026-08/08-02-corporate-bank-account-payment-order,本 task 不重开已完成实现。
## 2. 模板映射
| 列 | 模板字段 | 报销付款来源/行为 |
| --- | --- | --- |
| 1 | 序号 | 稳定排序的批次行号,文本 |
| 2–3 | 付款方账号/账户名称 | 复核批次冻结的公司付款账户 |
| 4 | 收款方行别代码 | CCB 本行固定文本 01,不生成他行 02 |
| 5–7 | 收款账号/名称/开户行名称 | 同一批次的收款快照 |
| 8 | 收款方联行号 | 沿现有本行制单合同留空 |
| 9 | 客户方流水号 | 批次号+行号,稳定、唯一并满足长度限制 |
| 10 | 金额 | 已批准批次项金额,数值且两位小数 |
| 11 | 用途 | 报销款,不复制工资/公积金用途 |
| 12 | 备注 | 报销单号 |
| 13–15 | 短信标志/手机/附加信息 | 0、不通知,后两列空;不读样例手机号 |
用户三个 input 文档的表头完全一致,空白模板与内置模板相同,账号列为文本。检查原件只提取结构与摘要,原件不改、不重新保存、不复制真实行到测试夹具。工资及公积金样例仅用于确定文件格式,不暗中扩为新业务。
## 3. 两条业务线
A:0014 当前已审批待付款 → 核定归属/主体 → 管理员直接维护真实 CCB 收款资料 → 财务重查 readiness → 财务组批 → 独立付款复核 → 财务下载 .xls → 授权人员网银导入核对并付款 → 回单与实际结果匹配 → 系统登记。
B:三张 rejected → 本人逐张修改 → 本人重提 → 实际审批人处理 → 通过后进入下一批付款。不等 A 或 AP 发布;未通过不能直接放入付款批次。
## 4. 停止条件与授权
- 身份、单号、原值不一致,或收付款资料非 CCB:停止对应操作,不按名字/唯一主体猜值。
- 生产 legal_entity_id 修正:单人单字段、前置锁定和原值检查、条件写入、提交后独立回读;须单独授权。失败不自动反复重试,回退另批。
- 银行真实资料由管理员/获授权维护人直接在系统录入;不让财务通过聊天转交卡号给执行端。
- 解密/生成真实制单涉及敏感数据;按明确范围批准,只输出脱敏验收结果。正式下载由财务操作且保留审计,不用管理员或伪身份抵扣。
- 已存在批次、复核不通过、资料变动或旧快照缺失:核实实际状态,不删批次或改数据库状态绕过。是否重建批次另行决定。
- 银行导入失败、部分成功或回单未核对:不标全批成功,不因重下载重复付款。
## 5. AP 发布独立处理
AP task 保留原实施写入面和已通过用户复核。提交/推送后冻结包含 CEO 撤权的完整新 SHA,重新核对发布所需时效、回滚制品和生产漂移;旧 58dcee0 不再是正确候选。没有新发布授权就不部署,不能因业务急付款而默认豁免门禁。
## 6. 验证层级
1. 首轮规划完成静态源码与原件结构检查;用户追加技术审查/修复授权后运行定向合成测试,并在当前生产容器以独立合成密钥和纯合成资料验证制单运行环境。后者不访问数据库、不用真实密钥、不读取真实银行卡、不调用正式导出接口、不落制单文件。
2. 执行阶段先用合成收付款数据核验模板、BIFF8、金额与文本类型、未复核拒绝、非建行拒绝、重下载与审计、付款角色分离;已有测试能复用则不另造。
3. 获准核实生产模板摘要、Python/固定依赖等运行前提时,不顺手触发真实导出;代码基线存在功能不等于生产运行时可用。
4. 真实财务路径、银行导入结果与最终回单属于业务验收,合成测试不能抵扣;分别记录未执行/通过/阻断,不提前全绿。
## 7. 八角度复核与修订
| 角度 | 审查结论 | 处理 |
| --- | --- | --- |
| 业务优先级 | 已审批 0014 和退回三单是不同状态,不互等 | 维持两条业务线,不把 AP/门禁作为重提前置 |
| 角色职责 | 财务可制单不等于可改员工银行卡;admin 通配也不替代制单路由的财务角色要求 | 管理员录资料,真实财务组批/导出,非制单人复核;不加权限 |
| 资料与隐私 | 付款方账户已有,但个人收款资料和所属主体缺失仍是真实阻断 | 按原值另批补 0014 归属人,不批量关联 smoke;敏感资料不进聊天 |
| 财务操作顺序 | 当前详情页未制单先把上传回单作为主动作,下载藏在更多,与原银行制单合同及本轮顺序冲突 | 最小修复动作模型:首次下载→上传回单→登记完成;人工上传保留在更多,不添加硬门禁 |
| 格式兼容 | 原件和内置模板一致;既有测试只验文件头、行数、总额等,缺少独立逐列读回 | 新增 xlrd 对生成文件的表头、列宽、工作表、前导零文本、金额与用途/备注读回断言 |
| 运行环境 | 本地普通 Python 可能无 BIFF8 依赖,不能据此推断生产缺失 | 使用既有固定 venv;生产容器合成生成/独立读回 PASS,不安装新依赖,不改生产 |
| 失败与重试 | 下载有审计写入;导出不等于付款,网络失败后的重下载也不能重复银行划款 | 补下载原因/取消零请求、文件成功下载、重下载警示、失败不推进状态、手工回单入口测试 |
| 发布与实际付款 | 当前正确 AP 修订与本轮界面补丁都未部署;银行文件“验证”和真正导入可能不分离 | 新候选另批;只有网银明确支持不提交的校验模式才做真实文件格式校验,否则不为测试上传文件,实际付款须按业务批准办理 |
本轮界面修复仅改变已选付款详情的动作优先级,继续使用既有按钮/更多/移动端固定操作区,不重画页面、不调整角色、审批、付款状态机、数据快照或后台接口。保留现金/历史批次人工付款、已有回单直接登记及回单查询失败时不冒充空状态的行为。
报销主体和收款资料应在组批前补齐,因为批次冻结的是当时的账户快照;生成文件不能静默取之后变更的个人账号替换已复核快照。已有批次若需取消/重建,重新交审,不直接更新旧快照。
@@ -0,0 +1,146 @@
# 执行顺序(本地技术审查/修复已批准,真实业务动作另批)
## 2026-09-08 获批技术执行
1. 检查 CLI/项目/npm 稳定版本及官方 update dry-run;保留现有定制,稳定通道无新版则不切 beta、不强刷文件。
2. 从业务先后、身份权限、资料安全、状态一致性、模板兼容、运行依赖、失败重试、部署边界八方面审查计划与实现。
3. 先运行付款 readiness、公司账户、复核/执行、BIFF8、审批退回相关定向单测;浏览器仅运行本地合成的付款路径,不运行已挂起的合同/真实账号全链用例。
4. 发现影响本业务的具体代码缺陷先用最小失败用例复现,再修复并回归;只缺生产资料的项目不冒充代码故障。仅存在覆盖缺口则补针对性验证,不宣布实际业务已完成。
5. 输出已修技术项、仍须真人操作的数据项、未验证生产项;只读复核由用户完成,不派代理,不自动提交或部署。
证据根目录:artifacts/qa-flow/reimbursement-payment-closeout/technical-review-20260908/。开始前完整 Git 状态、tracked 摘要、未提交 diff 和 index diff 已保存;既有 AP 修订/其他 task 不回滚、不混报。
## 技术阶段结果(本地完成,待用户复核,不等于业务完成)
### 2026-09-08 追加单字段生产授权
用户在“另行批准只补 0014 归属人的主体;银行资料由授权管理员直接录系统”之后回复“同意,批准”。本次仅执行 0014 归属人 legal_entity_id 从空值关联当前唯一启用未归档主体;不扩展为银行资料写入、解密、提交、推送、部署或真实付款授权。
只读预检通过:0014 唯一、approved 且未付、归属人是已确认 majianyong、角色 ceo+employee、主体字段为空、无付款批次项占用、目标主体唯一。员工 UPDATE 触发器/规则摘要与前次已审单字段修正一致。生产仍为 f223bfb。执行时再次锁定核对身份/状态/原值/目标摘要,条件更新恰好一行,再提交并以独立连接只读回读;失败不自动重试,提交状态不明时不宣称回滚。
证据目录:artifacts/qa-flow/reimbursement-payment-closeout/legal-entity-correction-20260908/。脚本仅读取身份与状态必要字段,不读取金额、票据、银行字段或真实密钥,输出脱敏核对结果。
执行结果:2026-09-08 北京时间 22:30,PASS_APPLIED_AND_READBACK;条件更新 1 行,原值为空,独立连接回读与获批主体一致,角色保持不变。仅更新 employees.legal_entity_id,不改单据、批次、银行字段或审批关系。个人银行资料当前是否补齐未重新查询,不宣称财务已可组批或付款。语法检查、task validate、治理及 diff 检查通过;主会话自审,不冒充独立人员复核。回退需要另批,且应以本次目标值为原值进行条件检查,不自动清空。
- Trellis:CLI/项目均为 0.6.16,npm 的 CLI/core stable latest 同为 0.6.16;beta 为 0.7.0-beta.3,不切预发布通道。按官方 upgrade --dry-run 和 update --dry-run 核验,无稳定版本升级;11 个项目定制文件保持原样,没有执行 force 覆盖、全局安装或改模板哈希。记录见 trellis-registry-tags.json、trellis-core-registry-tags.json 和两份 dry-run 日志。
- 八角度规划审查已完成并写入 design.md 第 7 节:业务先后、角色、资料隐私、操作顺序、格式、运行环境、失败重试、发布/银行授权。保留单据资料与技术问题的区分,不批量回填、不扩大角色或付款规则。
- 确认并修复一项界面顺序债:已复核且可制单、尚未生成且无回单的详情优先下载,生成后转上传回单;手工付款用户仍可在更多直接上传,已有回单仍可登记。使用既有样式/动作模型,运行时仅 client/src/pages/PaymentBatchesPage.tsx 一个文件新增变化;前后端权限、付款接口、审批及数据库均未修改。
- 先补断言再修:browser-red.log 明确复现首个下载动作被隐藏,1 条失败;该失败发生在登录成功后的动作断言,不是认证/网络前置。后续桌面/移动端测试实际收到了合成 .xls 文件,核对文件字节、原因/取消、重下载警示和不触发付款,且通过真实 filechooser 保留手工上传路径。
- 后端最终 9 文件 158/158 PASS(backend-verified.log),包括 readiness、付款账户、复核/执行/回单/导出、报销提交与退回通知、CEO/经理解析及新增的独立 BIFF8 逐列读回。新增 readback 初次因测试直接访问未配置的第 15 列宽触发 KeyError;改用 xlrd 的有效列宽 API 后通过,保留诊断日志,不当作生产缺陷。
- 浏览器同一最终源码的 16 条目标用例全部已覆盖通过,分两次完成:browser-final.log 为 13 PASS + 3 因本地 Vite 退出导致登录连接拒绝而未能执行;未改代码/断言,恢复同源服务后仅补跑这 3 条,browser-boundary-rerun.log 为 3/3 PASS。不是一次 16/16 全绿;自动 retries 始终为 0。之前 browser-payment.log 的 2 条失败是新增合成文件 helper 在 ESM 中误用 require,改为 createRequire 后通过,不混入业务缺陷清单。
- 本地环境前置:全新 context、原生在线采样 true、无 cookie、表单可见、监听 refresh=0,不安装 route mock;见 browser-environment.json。随后用例 API 均为合成 mock,不能冒充真实后端端到端或生产账号验收。Vite 中途退出原因未进一步归因;补跑把服务与测试绑定在同一受控 shell 生命周期,结束自动停止,未改全局诊断 harness。
- 当前生产容器 f223bfb 的纯合成制单探针 PASS(runtime-synthetic.json):内置模板摘要、BIFF8/3 sheet/15 列、表头、前导零账号文本、01 行别、金额均由独立 Python 读回确认。使用独立合成密钥与资料,不访问 DB、不读真实银行数据、不用真实密钥、不调用正式导出接口、不落制单文件;不代替真实账户解密或网银验收。
- npm run typecheck、独立 artifacts 目录 vite build、task validate、Trellis governance、process contract、git diff --check 均通过。文档注册检查通过但保留既有 1 项 warning(22 个未登记文档),构建保留既有 Browserslist 陈旧提示,不升级无关依赖。
- 范围核对 scope-check.json:相对本轮开始只改变 4 个已跟踪文件(付款页面、两份测试、前端规约),外加当前未跟踪 task 文档;既有 AP 包及其他 tracked 改动摘要不变,index 不变。原始 input 和归档截图没有写入,不提交/推送/部署,不更新生产资料。新动作顺序尚未上线,线上仍可使用既有更多中的制单入口。
- 桌面和移动端截图已主会话查看,确认主动作切换使用既有区域、人工路径保留;它们是合成页面,不是生产截图。本轮是主会话多角度自审及验证,不冒充独立人员复核;用户可审 technical-code.diff 和本 task。
### 复验命令
```bash
trellis upgrade --dry-run
trellis update --dry-run
cd server
PAYMENT_ORDER_PYTHON_BIN=<既有固定BIFF8虚拟环境解释器> npm test -- tests/unit/services/paymentOrderXls.test.js tests/unit/services/paymentBatchReadiness.test.js tests/unit/services/paymentProcessingProgress.test.js tests/unit/routes/paymentBatches.reimbursement.test.js tests/unit/routes/paymentAccounts.test.js tests/unit/routes/reimbursements.payment-batch-gate.test.js tests/unit/routes/reimbursement.submit-side-effects.test.js tests/unit/utils/workflow.test.js tests/unit/routes/workflow.return-notifications.test.js
cd ../client
npm run typecheck
./node_modules/.bin/vite build --outDir ../artifacts/qa-flow/reimbursement-payment-closeout/technical-review-20260908/build
PAYMENT_ORDER_PYTHON_BIN=<同一解释器> E2E_MODE=local E2E_BASE_URL=http://127.0.0.1:23121 ./node_modules/.bin/playwright test tests/e2e/system-action-confirmation.spec.ts --grep '付款|回单|制单|移动端固定操作区' --workers=1 --retries=0 --trace=on --reporter=list --output=<独立结果目录>
```
最后三条补跑使用同一 spec 与参数,仅 grep 改为“只有超级管理员职责|兼任付款复核职责|回单查询失败”,服务生命周期与测试同一 shell 管理。针对性结果不抵扣 58 条清单,也不充当 R1 所需完整候选全绿耗时。
| 优先级/工作包 | 主责 | 动作 | 结束判据 | 当前状态 |
| --- | --- | --- | --- | --- |
| P0 0014 付款资料解阻 | 执行端核对,用户批准,管理员录资料 | 主体已单独获批补齐并回读;管理员录真实收款银行/卡;获准检查公司付款账户可用性 | 实际财务页面不再因这些缺项禁用,其他 readiness 阻断如实列出 | 主体已补;银行资料与完整 readiness 未验,不宣称可付款 |
| P0 三张退回单重提 | majianyong,实际审批人 | 按退回理由逐张修改重提并实际审批,不代操作 | 每张有重提/审批结果或具体缺项,付款进度另算 | 待本人操作 |
| P1 付款复核与 .xls 制单 | wangzhiling,合格非制单复核人 | 财务组批,实际独立复核,填写原因下载模板制单;必要时先跑合成回归与获准运行时预检 | reviewed 后成功生成同模板 .xls;未复核拒绝;无敏感日志 | 既有功能,真实闭环未验 |
| P1 银行付款与回单 | 用户指定的银行操作人,财务 | 网银导入核对、银行批准执行、回单核对、系统登记 | 实际银行结果与系统付款状态/回单一致 | 尚未付款;操作人待实际指定 |
| 独立 AP 发布收口 | 执行端,用户批准 | 仅已审 AP 增量提交/推送、新候选、时效与漂移复核、单独授权部署 | 正确新版本及 AP 角色矩阵验收 | 复核通过,未提交/部署,不阻塞以上资料整理 |
## 本轮完成的准备
- 阅读 input 中三份制单 xls 的结构;空白模板与内置模板字节相同,均为 3 sheet/15 列合同,两份已填样例表头一致。未输出真实数据、未修改原件。
- 确认现有 reviewed 后制单功能、财务导出守卫和导出审计;相关功能文件相对生产基线 f223bfb 无 Git 差异。不把源码存在当作生产成功生成或银行导入通过。
- 新建一个业务闭环 planning task,复用旧付款准备包/已完成制单实现/AP task,不一次拆出多份待实施任务,不启动产品开发。
## 汇报纪律
后续只按“0014 是否可组批/已复核/已制单/银行已付/回单已核”“三张单各自是否重提/审批”汇报。维护业务进度与部署进度两个独立判断,不以文件数、测试数或已通知代替业务结果。
## 暂停项
全员主体回填、新员工自动关联、工资/公积金业务扩展、银行自动付款、六个 view_all、58 条红项、阶段 B、R1 均不进入本轮实施。
## 2026-09-09 综合审查后的后续任务规划(待批准,不扩展本 task 实施范围)
本节按用户要求综合主会话三视角审查与外部审查建议,复用当前记录保存后续独立任务清单;不批量新建 task、不自动提交/推送、部署、执行真实恢复或修改生产配置。后续新任务只在到达队首且范围获批后创建,现有任务只做经批准的方案修订。付款主线目前由用户要求等待。
业务最新证据:用户于 2026-09-09 提供财务页面截图,0014 已创建付款批次,待付款复核 0/1、银行制单未生成、报销仍待付款。证明组批已发生,不证明负责人已收到提醒、已复核、已制单或银行已付。既有“资料待补/尚未组批”是此前历史状态,不再当作当前阻断;本节未查询生产。
### 规划裁决
- 恢复验真改用受控人工最小路径,不继续叠加安装器/runner。不能把 Attempt 3 非零且底层原因未知归结为已证实工具故障,也不能认定备份损坏。
- “数据库还原并数表”只回答该批次数据库能否恢复,不代表附件、应用启动、正式切换或完整灾备能力。恢复目标隔离、合法管理员通道、批次身份/完整性、失败诊断和精确清理不得删除。
- 统一待办上调为跨模块业务交接任务,不再仅作为页面文案优化。既有依据为 09-05 错误解释 task implement §12、09-06 付款观察 task design §3;当前没有已批准的独立统一待办实现任务。
- 2026-10-05 距当前 2026-09-09 为 26 天。错误基线有 673 条指纹记录、695 次调用、73 个文件;不是 673 个已确认缺陷。清单分流现在启动规划,不等恢复/seed/待办做完;实施按独立提交单元排队。
- 不承诺“一个下午必成功”;第一次最小恢复以一个有界操作窗口为目标,遇失败收集脱敏原因并停止,不在现场加装置或自动反复尝试。
### 队列与验收合同
| 工作包 | Trellis 归属 | 范围/交付 | 完成判据与停止边界 |
| --- | --- | --- | --- |
| T0 两笔已审改动收口 | 复用 09-07-manager-ap-access-scope 与本 task | 按路径拆 AP 与付款按钮/制单测试两笔,分别引用 task;在 AP 包内纠正 task.json 的“保留 CEO 财务读取”陈旧摘要;分别记录代码/发布/业务验收/等谁 | 不漏既有文件、不混其他 task;提交、推送须明确批准,部署另批;必要复验针对各自增量,不批量归档。不以 Git 暂存路径隔离冒充 worktree 物理隔离 |
| T1 最小正式备份恢复验真 | 修订 08-23-isolated-backup-restore-drill,保留原历史 AC | ARM 已获准管理员通道,读取执行时最新正式备份;一次性无宿主端口 PG16,使用现有 restore-db.sh;先形成数据库结果,再单列配对附件结果 | 批次身份/摘要/新鲜度核验;目标非生产且无生产数据库连接;恢复命令全程退出 0、源 migration 对照及既定关键表可读;附件需另验解包/引用/适用摘要。DB PASS 不关闭原整体 task;任一失败保留阶段与安全原因,清理仅本轮目标,不删正式归档 |
| T2 日常发布与初始化分离 | 待新建独立 existing-deploy-seed-separation task;关联 09-05-existing-deploy-db-lifecycle-isolation | 保留 initialize 的初始化能力;existing 不再无条件运行覆盖既有配置的 seed;确有升级数据变更时走明确版本化步骤 | 合成库验证 initialize 可用、existing 不改管理员既有配置、重复发布语义稳定、缺配置有明确停止/补救路径;迁移、配置版本、旧版本兼容与回滚边界明确;既有 DB 生命周期隔离回归不退步。先验证再改变默认发布路径,真实部署另批 |
| T3 统一业务待办投影 MVP | 待新建 reimbursement-payment-todo-projection task;复用 09-06 观察记录 | 首批仅报销退回补改、财务待组批、付款待复核、复核后财务制单/付款登记四类;既有业务状态为事实源,复用待办/首页/通知能力,不建第二套审批引擎 | 接收人不用口头转述即可从工作台找到本人的下一步,数量与明细一致、深链有权可达;制单人不自审、不扩 AP 读取;处理后消除陈旧待办,通知失败不丢待办且有可观测重试,重复刷新/投递不重复任务。正式通知和真人流程另批;缺失交接样本保留未验,不用管理员代验抵扣 |
| T4a 员工业务就绪检查 | 待新建 employee-business-readiness task | 能登录/能提交/能付款分阶段检查;解释缺项、维护责任与入口,复用现有审批解析/readiness,不另造一套判定 | 覆盖负责人兜底、普通员工、缺经理、缺主体/银行卡、无权限维护、smoke 排除;不在入职时强收银行资料,不自动回填真实记录,不为补资料扩大财务权限 |
| T4b 职责与数据范围表 | 先做设计交付,关联 AP task;实现单独定范围 | 按已确认真实账号列审批/复核、全量读取、资料维护、付款执行四类能力;明确 view_all 未决项 | 用户逐项裁决;保留 CEO 审批路由、多角色权限并集及现有合法业务上下文。没有业务裁决不改六个 view_all,不摘角色,不以任务完成声称全部数据范围已收窄 |
| E 错误豁免到期分流 | 复用 09-05-error-explainability-payment-readiness,新增经批准的后续阶段 | 现在按文件/调用指纹列“本期迁移/有理由延期/已失效待核销”,指定责任人与各自验收/复审日期 | 保留首批主链路不得豁免、新旧调用结构校验、到期失效规则;延期逐模块审查并保留原到期日与原因,不批量改日期、不删规则凑绿;不能把目录删除或无命中直接当豁免已解决 |
T0 是新增代码工作的前置;T1 数据库路径获成功证据后推进 T2,配对附件仍作为独立恢复缺口保留,不能用 DB PASS 自动授权 T2 生产发布。T1 若有未解释的恢复非零,不以“时间已到”越过风险继续生产部署。T3 的已发生交接证据整理、T4b 职责设计和 E 分流可与 T1 规划并行,但主会话不派实现/检查代理,不在当前脏树叠加第三笔产品改动。T2 本地实施通过后,T3/T4 按业务价值排队,不要求所有基础任务整体归档才允许业务规划前进。
### T1 最小操作与保留保障
使用 ARM 现有正式归档与已有合法管理员操作路径,不把真实备份下载到开发机,不用生产容器作为恢复目标,不新增 sudoers 或修改目录权限。执行前确认资源余量、唯一批次、manifest 配对、大小/摘要、备份年龄和 PG/来源版本;创建本轮专用影子库、只挂载该批次只读数据或使用 stdin、无宿主端口,不挂生产数据卷,不启动 OA/worker、不执行迁移或 seed。
先独立确认数据库目标就绪并符合 restore-db.sh 的 shadow/名称确认门禁,执行既有恢复脚本,检查其退出码及源 migration/关键表可读性。表计数仅为一致性线索,不能代替完整恢复退出状态或凭空声明与生产逐表一致。若只完成数据库,结果明确写 DB_RESTORE_PASS、STORAGE_NOT_TESTED、APP_NOT_TESTED;附件完成后才补相应状态。原任务完整验收不被这项缩小后的实验替代。
操作保留简短脱敏回执:批次指纹、开始结束时间、数据库结果、附件结果、失败阶段/可安全提取的原因、清理结果。不强制新增 RTO 分段框架;不删除原正式演练要求,相关指标未测就保留未测。原始敏感错误仅留受控位置按既有隐私规则处理,清理前形成安全诊断,避免重演 Attempt 3 丢失原因。精确清理本轮一次性容器/卷/临时数据并回查,不清理旧镜像备份、正式归档或未知对象。
### T3 输入边界
目前只确认资料已补且财务成功组批,并观察到后续操作需通过聊天指引;尚未实际观察 majianyong 是否从现有待办/消息找到复核、复核后财务是否收到交接。三张退回单的“本人未处理”不能单独证明没有发通知。规划阶段分别核对业务状态投影、接收人解析、通知创建/投递和消息深链,不把四类不同故障预先合并成同一个已证实代码缺陷。已发生样本先定合同,后续样本用于补验,不伪造、不中断当前真人流程抓证据。
### E 初步模块分流与建议日期(待批准,不是已承诺全部迁移)
- 优先审查名单:auth/users/employees(登录与员工资料),workflow 路由及解析(审批人/退回),paymentAccounts/AP(付款资料和权限),todos/notifications/portal(交接可解释性),invoicePool(报销票据主链路)。按具体仍为 legacy 的调用选高频阻断,不重复迁移已覆盖的报销/付款主链路,也不假设这些模块的全部调用都必须本期完成。
- 次优先核对:采购/差旅、部门与主数据、预算/项目、合同;根据当前真人使用和变更范围决定迁移或有理由延期,不能仅因调用数大就统一延期。
- 其余模块:逐文件列剩余指纹、业务影响、现有兜底、责任人、拟新日期与理由后交审;低频或暂未使用需有依据,不把工资/绩效/资产等默认判定无人使用。当前仅形成模块初分,没有修改任何豁免。
- 建议 2026-09-11 前完成 73 文件全量处置清单;09-18 完成首批高频调用的迁移与定向验证;09-25 复审剩余容量和延期理由;09-30 前完成逐模块延期裁决;10-02 做一次剩余到期项检查。不得等到 10-05 才改日期。
- 当前门禁以 UTC 日期判断 expiresOn <= today 即失效,因此 2026-10-05 00:00 UTC(北京时间 08:00)即触发该日期的到期项,不按北京时间当天午夜前仍有效来安排。全部 673 条同日到期属于计划风险,不等于当天线上功能会自动停用。
### 明确不做
本轮不执行提交/推送/恢复/部署,不改变任何账号或真实银行资料,不自动发送通知或审批/付款;不增加通用 workflow 引擎、全站重设计、微服务、大文件专项重构、全员主体回填;58 条红项、合同阶段 B、R1 仍暂停。独立任务规划不改变当前付款 task 的业务完成判据,也不授权把其余改动顺带提交。
## 2026-09-09 综合规划批准后的首批本地执行
用户回复“开始实施”,综合规划的本地工作进入执行。审批、真实付款仍按用户要求等待;此次未获得提交/推送、ARM真实恢复或部署的单独授权,不将开始实施扩大解释为这些操作。
- T0:AP和付款建立两个独立detached交审副本,另一个ops-preparation副本仅维护原恢复task及错误解释task的准备文档。不是新开第三笔产品开发;原工作树保留,未重置或覆盖已有改动。
- 付款副本仅包含4个已审tracked文件和本task;未包含AP撤权差异。后端9文件158/158通过,使用已有固定BIFF8环境,无安装新依赖。浏览器未重新运行,不把此前13+3分轮证据改成新的同轮通过。
- E:673指纹/695调用/73文件逐项对上当前源码,原expiry和门禁不改;全量清单在独立准备副本的原错误解释task implement §13。16文件优先审查、25文件业务影响复核、32文件逐模块审查延期理由;未逐调用完成业务裁决,未批准任何延期,也未迁移新原因码。
- T1:独立准备副本内修订原恢复task的最小人工操作阶段,复用3份既有恢复/校验脚本,保留正式演练AC;没有创建新runner、修改sudoers、连接远端或读取真实备份。
- T2/T3/T4未开新的代码任务,既有58条、阶段B、R1不动。原因码分支迁移尚未开始,不能以“清单完成”宣布技术债清零。
| 代码 | 发布 | 业务验收 | 等谁 |
| --- | --- | --- | --- |
| 付款按钮/制单验证已审,独立副本已隔离复验 | 未提交/推送/部署,线上仍是既有更多入口 | 用户截图确认0014已建批;待复核0/1、制单未生成、未确认付款 | 业务按用户要求等待;技术先等两笔提交的明确授权 |
证据根为原主工作区 `artifacts/qa-flow/project-roadmap-execution-20260909/`。提交建议标题:`fix: 优先引导下载银行制单(09-08-reimbursement-payment-closeout)`;不自动提交或把其他准备文档混进这一笔。
## 2026-09-09 提交与推送授权
用户对“AP 与付款修复分两笔提交并推送,不部署、不执行真实恢复、不混入后续准备文档”明确回复“是”。本次只提交本 task 对应已隔离交审文件及当前授权记录;原恢复 task、错误豁免后续阶段和另外两个未跟踪 task 不进入这两笔。实际提交 SHA 与远端结果以 Git 回执为准,不提前宣称 push 成功;生产仍未部署,业务验收状态不随提交改变。
@@ -0,0 +1,62 @@
# 报销付款与银行制单业务闭环
## Goal
以业务结果为目标:已审批未付款的 0014 完成付款准备、独立付款复核、按用户模板生成建行制单、银行付款及回单登记;三张退回报销由本人修改重提,实际审批后再进入付款,不让两条业务线互相等待。
2026-09-08 首轮为规划;用户随后明确批准检查并按指南更新 Trellis、多角度审查修订计划,以及完成影响这些业务的技术债务。当前执行范围为本地技术审查、针对性合成验证和已定位阻断的最小修复;不改变既有人工制单/网银流程。未批准生产数据回填、真实账号解密、真实提交/审批/组批/导出/付款、部署或外部通知。此前财务直接经理单字段修正已完成,不重复操作。
## 已核实依据
- 截图显示 0014 待组批且被三项阻断:归属人缺公司主体、收款银行缺失、收款账号缺失。截图不是完整 readiness 通过证明,不凭页面人员名称替代写入前 owner_id 核实。
- 最新只读结果:一个启用未归档主体、一条匹配该主体的启用建行付款账户,密文字段非空;解密可用性未验证。公司付款账户不等于个人收款账户,不重复创建已有账户。
- 用户确认 E1/E3/E5/E6 为 smoke 账号,排除在业务回填范围外。只先准备 0014 归属人的单字段主体修正,不批量回填所有员工。
- input/制单模板.xls 与 server/src/assets/payment-order/ccb-payment-order-template.xls 字节完全一致,SHA-256 为 72e83ab6b7cd71263ca00196746de896e02a02390bab5c896dabd76421e40c0e。
- 两份已填写的工资、公积金制单样例均为 BIFF8,三个工作表、15 列,表头与模板完全一致。只提取结构,不复制样例的户名、账号、金额或联系人;样例用途不构成新增工资/公积金业务范围。
- 当前源码已经包含付款复核后下载银行制单功能。制单路由、生成器、Python 填表脚本、内置模板及付款页面与生产基线 f223bfb 的 Git 内容无差异;这证明基线包含能力,不证明运行时依赖、真实数据或企业网银导入已通过。
- AP 修订已由用户复核,未提交部署;58dcee0 不含撤销 CEO 台账授权的最新修订,不能用作该修复的最终候选。
## Scope
2026-09-08 追加授权与结果:用户批准仅补 0014 归属人的所属主体。已于北京时间 22:30 完成 majianyong 的 legal_entity_id 空值到唯一启用未归档主体的条件更新(1 行),提交后独立连接回读匹配。此项覆盖首轮“主体回填未授权”的历史状态;个人收款银行/账号仍待授权管理员在系统维护,本次未查验或写入银行资料,未证明完整 readiness 已通过,其他生产业务动作及部署仍未授权。
0014 付款前置资料、既有付款复核与 BIFF8 制单、人工银行付款及回单闭环;三张退回报销本人重提;AP 发布作为独立关联任务。当前获批交付为规划修订、业务相关技术审查及最小本地修复,真实业务动作另批。
## Requirements
- R1:业务优先;不要求 0014 等三张退回单,不要求三张退回单等 AP 发布。不让 R1 门禁、58 条红项或合同测试成为本 task 的扩修前置。
- R2:生产补数据前核对唯一业务记录、原值和目标。只准备 0014 归属人的 legal_entity_id 空值到获批主体的修正;非空或身份不符停止,执行另批并回读。唯一启用主体不代替业务归属批准。
- R3:真实收款开户行与账号由具备 employee.bank.manage 的授权人员直接录系统;现有真实账号由 wangshunli 管理员维护,普通 finance 不因制单需要新增维护权限。非建行停止,不发聊天、不填样例或占位账号。
- R4:付款账户解密检查与真实制单均在获准范围内执行;明文只在必要进程内存/实际交付文件出现,不输出日志、Trellis 或完整快照。解密可用性与真实银行账户正确性分别确认,不能互相替代。
- R5:财务用真实身份重新检查 0014 待组批状态,通过实际 readiness 后创建批次。已有批次占用、部门开关、收付款资料等阻断按实际返回逐项处理,不把某三项补齐称为所有检查通过。
- R6:报销审批和付款复核是两道不同业务动作。wangzhiling 制单,具备权限且不是制单人的实际人员复核;majianyong 为已有 ceo 业务复核人候选,须满足系统当前条件,不伪造账号或绕过独立复核。复核人同时为受益人的治理边界沿既有获批规则,不在本轮擅自加层或宣称不存在风险。
- R7:仅 reviewed 的合格报销银行付款批次由财务生成 BIFF8 .xls。沿用精确模板、15 列与三个工作表,账号和行别代码保持文本,行别为 01,金额两位小数,用途为报销款,备注关联单号,客户流水号可追溯且不重复;不改为 xlsx/CSV,不套用样例里的真实数据。
- R7a:详情页复核通过、可制单、未生成且无回单时,唯一主动作优先“下载银行制单”;生成后切换“上传银行回单”,重下载进入更多。未生成但已人工付款仍可从更多直接上传,已有回单优先登记完成;资料不完整、非银行或回单状态未知时不错误引导下载/付款。桌面和移动端使用同一动作模型,不改变列表页主动作或增加自动付款。
- R8:制单下载需要原因并写导出审计/次数,不是纯只读;重下载不等于新增银行付款。未复核、旧快照缺失、非建行、解密/依赖/模板失败不得生成部分文件,不用手填绕过本轮制单验收。
- R9:生成 .xls 不等于付款成功。由授权银行操作人导入网银、核对对象/笔数/金额、按银行流程批准执行;银行返回结果与回单核对后才在系统登记。失败/部分成功分别处理,不能整批提前标已付;单据状态、付款记录与回单一致才算闭环。
- R10:三张 rejected 各自核实退回原因、参与人和实际修改结果,由 majianyong 本人重提;不代维护、不复制绕过、不提前承诺所有审批必过。由实际分配的审批人处理,通过后进入后续批次,不重复纳入 0014 当前批次。
- R11:复用已有制单功能。用户已授权本地修复影响这些业务的技术债,先复现再做最小修复;若涉及新增业务语义、权限放宽、状态机重构或生产写入,重新交审,不先重构、升级业务依赖或新增付款模块。
## Acceptance Criteria
- [ ] 0014 的实际归属人、当前阻断和允许操作范围已核实;主体及真实收款资料按授权补齐,实际待组批可选,未触碰 smoke 账号。
- [ ] 付款账户及收款资料通过必要的受控校验;完整账号不进入聊天、日志、任务或普通接口。
- [ ] 实际财务创建批次、非制单人完成所需复核;未复核不能下载制单,真实复核结果有记录。
- [ ] 财务下载的 .xls 与用户空白模板结构一致,账号/01 不丢前导零,金额、条目与批准批次一致,失败不产出部分文件;真实文件不提交仓库。
- [ ] 网银实际导入和业务核对通过,执行结果与银行回单核验后才登记付款;没有银行结果时仅标“已制单/待付款”。
- [ ] 三张退回单逐张有实际重提结果或明确阻断,未完成的不虚报收口;审批完成与最终付款分别计进度。
- [ ] AP 修订按独立候选与发布授权处理;业务任务不冒充发布完成,不启用暂停事项。
## Out of Scope
不批量关联全员主体,不开发新员工自动关联,不扩工资/公积金/差旅付款,不新增银行直连或自动划款,不放宽 CCB/复核/资料守卫,不扩六个 view_all,不修 58 条、合同阶段 B 或启用 R1,不代发消息、不代替真人业务审批。
## Test Plan
- 本轮:原件结构/模板摘要比对、源码流程核对、task validate、Trellis governance、git diff --check;不生成真实制单,不执行业务操作。
- 执行阶段:复用 paymentOrderXls 与 paymentBatches.reimbursement 定向单测,以及真实付款页面对应的合成浏览器用例,检查 reviewed 守卫、导出权限、模板格式、账号文本和失败无部分输出;先发现具体缺口再补测试,不重跑无关完整红项。
- 生产与银行验收:在分别获准后核对数据原值/回读、实际财务身份操作、网银导入反馈及付款回单,未执行项不抵扣为通过。真实导出包含审计写入,不能标为只读。
## 执行边界
本次技术审查沿用既有“付款复核通过后,财务主动点击下载银行制单,再由授权银行操作人导入网银”的方式;不新增自动下载或自动付款。不重复索取本地技术审查/修复授权;各项生产写入、真实账号解密、导出、付款和发布仍分别获批。
@@ -0,0 +1,26 @@
{
"id": "reimbursement-payment-closeout",
"name": "reimbursement-payment-closeout",
"title": "报销付款与银行制单业务闭环",
"description": "围绕0014待付款、三张退回报销及建行xls制单推进业务闭环规划和限定技术债修复;复用已有付款能力,生产数据、真实业务操作和发布分别批准。",
"status": "in_progress",
"dev_type": null,
"scope": "business-payment-closeout",
"package": null,
"priority": "P1",
"creator": "shunli",
"assignee": "shunli",
"createdAt": "2026-09-08",
"completedAt": null,
"branch": "main",
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "用户追加批准Trellis更新检查、多角度计划复核及影响付款业务的本地技术债修复。稳定版已最新,不切beta、不覆盖项目定制。沿用人工下载xls/网银流程,修复首次制单动作优先级并补合成验证;生产数据/真实银行卡解密/实际业务操作/提交推送部署仍另批。",
"meta": {}
}
-1
查看文件
@@ -68,7 +68,6 @@ const ROLE_PERMISSIONS: Record<string, readonly string[]> = {
"payment.batch.review",
],
ceo: [
"finance.ledger.view",
...EMPLOYEE_BASE_PERMISSIONS,
...PROCUREMENT_SELF_SERVICE_PERMISSIONS,
...MASTERDATA_VIEW_PERMISSIONS,
+10 -6
查看文件
@@ -294,19 +294,22 @@ export function PaymentBatchesPage() {
!["executed", "cancelled"].includes(selectedBatch.status);
const paymentActionModel = useMemo(() => {
const canDownloadOrder =
selectedBatch?.status === "reviewed" &&
selectedBatch.canExportPaymentOrder &&
canExportPaymentOrder;
const primary = canReviewSelectedBatch
? "review"
: needsBankReceipt
? "uploadReceipt"
? canDownloadOrder && selectedBatch.paymentOrderExportCount === 0
? "downloadOrder"
: "uploadReceipt"
: canRegisterPaymentComplete
? "execute"
: null;
const more: PaymentActionKind[] = [];
if (
selectedBatch?.status === "reviewed" &&
selectedBatch.canExportPaymentOrder &&
canExportPaymentOrder
) {
if (primary === "downloadOrder" && canUploadReceipt) more.push("uploadReceipt");
if (canDownloadOrder && primary !== "downloadOrder") {
more.push(selectedBatch.paymentOrderExportCount > 0 ? "redownloadOrder" : "downloadOrder");
}
if (canCancelSelectedBatch) more.push("cancel");
@@ -316,6 +319,7 @@ export function PaymentBatchesPage() {
canExportPaymentOrder,
canRegisterPaymentComplete,
canReviewSelectedBatch,
canUploadReceipt,
needsBankReceipt,
selectedBatch,
]);
@@ -346,25 +346,28 @@ async function login(page: Page, actor: SyntheticActor) {
await expect(page).toHaveURL(/\/(?:portal|mobile)$/);
}
test("只有付款复核职责的经理不能直连应付账款且不请求台账", async ({ page }) => {
test.skip(process.env.E2E_MODE === "live", "只使用本地合成角色,不访问公网");
const pageErrors = capturePageErrors(page);
await mockSyntheticApis(page, REVIEWER_ACTOR);
await login(page, REVIEWER_ACTOR);
for (const role of ["manager", "ceo"]) {
test(`只有付款复核职责的 ${role} 不能直连应付账款且不请求台账`, async ({ page }) => {
test.skip(process.env.E2E_MODE === "live", "只使用本地合成角色,不访问公网");
const pageErrors = capturePageErrors(page);
await mockSyntheticApis(page, { ...REVIEWER_ACTOR, roleCodes: [role] });
await login(page, REVIEWER_ACTOR);
let apRequests = 0;
page.on("request", (request) => {
if (new URL(request.url()).pathname.startsWith("/api/ap-operations")) apRequests += 1;
let apRequests = 0;
page.on("request", (request) => {
if (new URL(request.url()).pathname.startsWith("/api/ap-operations")) apRequests += 1;
});
await page.goto("/ap-operations");
await expect(page).toHaveURL(/\/403$/);
await expect(page.getByRole("link", { name: "应付账款", exact: true })).toHaveCount(0);
expect(apRequests).toBe(0);
await expect(page.getByTestId("ap-export")).toHaveCount(0);
expectNoPageErrors(pageErrors);
});
await page.goto("/ap-operations");
await expect(page).toHaveURL(/\/403$/);
await expect(page.getByRole("link", { name: "应付账款", exact: true })).toHaveCount(0);
expect(apRequests).toBe(0);
await expect(page.getByTestId("ap-export")).toHaveCount(0);
expectNoPageErrors(pageErrors);
});
for (const roleCodes of [["ceo"], ["finance"], ["finance_mgr"], ["manager", "finance"]]) {
}
for (const roleCodes of [["finance"], ["finance_mgr"], ["manager", "finance"], ["ceo", "finance"]]) {
test(`AP 专用读取权限允许 ${roleCodes.join("+")} 进入台账且不提升导出权限`, async ({ page }) => {
test.skip(process.env.E2E_MODE === "live", "只使用本地合成角色,不访问公网");
const actor = { ...FINANCE_ACTOR, roleCodes };
@@ -375,8 +378,7 @@ for (const roleCodes of [["ceo"], ["finance"], ["finance_mgr"], ["manager", "fin
await expect(page.getByRole("heading", { level: 1, name: "应付账款", exact: true })).toBeVisible();
await page.getByRole("tab", { name: /^财务台账/ }).click();
await expect(page.getByTestId("finance-ledger-table")).toBeVisible();
if (roleCodes.includes("ceo")) await expect(page.getByTestId("ap-export")).toHaveCount(0);
else await expect(page.getByTestId("ap-export")).toBeVisible();
await expect(page.getByTestId("ap-export")).toBeVisible();
});
}
@@ -1,9 +1,12 @@
import type { Page, Route } from "@playwright/test";
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { test, expect } from "./helpers/fixtures";
import { login } from "./helpers/auth";
test.use({ serviceWorkers: "block" });
const loadServerModule = createRequire(import.meta.url);
function envelope<T>(data: T) {
return { requestId: "mock-system-action-confirm", data };
@@ -21,6 +24,26 @@ function navigationItem(key: string, href: string, label: string, icon: string,
return { key, href, label, icon, placement, permissionCodes: [], featureFlagKey: null };
}
function syntheticPaymentOrder() {
const { encryptStringOrThrow } = loadServerModule("../../../server/src/services/crypto");
const { generatePaymentOrderXls } = loadServerModule("../../../server/src/services/paymentOrderXls");
const config = { fieldEncryptionKey: Buffer.alloc(32, 7).toString("base64") };
return generatePaymentOrderXls(config, {
batch_no: "PAY-ACTION-001",
payment_account_snapshot_json: {
bankInstitutionCode: "CCB", accountName: "合成付款公司", bankName: "中国建设银行",
accountNumberEncrypted: encryptStringOrThrow(config, "000123456789012345678"),
},
items: [{
id: "synthetic-item", entity_code: "BX-ACTION-001", amount: "88.00",
snapshot_json: { payee: {
name: "合成收款员工", bankName: "中国建设银行", bankInstitutionCode: "CCB",
bankAccountEncrypted: encryptStringOrThrow(config, "000987654321098765432"),
} },
}],
}).buffer as Buffer;
}
const user = {
identifier: "[email protected]",
password: "ActionConfirm1234",
@@ -209,7 +232,9 @@ function paymentBatch(
voucherPrefix: "R7E",
totalAmount: "88.00",
highRisk,
paymentAccount: null,
paymentAccount: canExportPaymentOrder ? {
accountName: "合成付款公司", bankName: "中国建设银行", bankInstitutionCode: "CCB", accountNumberMasked: "0001********5678",
} : null,
paymentOrderExportCount,
canExportPaymentOrder,
paymentOrderExportBlockers: [],
@@ -281,6 +306,8 @@ async function mockSystemActionApis(
paymentChannel?: "bank" | "cash";
canExportPaymentOrder?: boolean;
paymentOrderExportCount?: number;
paymentOrderFile?: Buffer;
paymentOrderError?: boolean;
paymentEntityType?: "reimbursement" | "travel_order";
reimbursementStatus?: "draft" | "reviewing" | "paid";
travelReimbursement?: boolean;
@@ -290,12 +317,14 @@ async function mockSystemActionApis(
const currentMe = { ...me, ...options.meOverride };
const batchStatus = options.batchStatus ?? "reviewed";
let hasReceipt = options.hasReceipt ?? true;
let paymentOrderExportCount = options.paymentOrderExportCount ?? 0;
let currentReimbursementStatus = options.reimbursementStatus ?? "draft";
const calls = {
deleteReimbursement: 0,
deleteReimbursementAttachment: 0,
exportReimbursements: 0,
executePaymentBatch: 0,
paymentOrderReasons: [] as string[],
paymentBatchDetail: 0,
previewPaymentReceipt: 0,
uploadPaymentReceipt: 0,
@@ -489,7 +518,7 @@ async function mockSystemActionApis(
hasReceipt,
options.paymentChannel,
options.canExportPaymentOrder,
options.paymentOrderExportCount,
paymentOrderExportCount,
options.paymentEntityType,
);
await fulfillJson(route, {
@@ -515,13 +544,25 @@ async function mockSystemActionApis(
hasReceipt,
options.paymentChannel,
options.canExportPaymentOrder,
options.paymentOrderExportCount,
paymentOrderExportCount,
options.paymentEntityType,
),
});
return;
}
if (path === `/api/payment-batches/${paymentBatchId}/payment-order` && request.method() === "POST") {
calls.paymentOrderReasons.push(request.postDataJSON().reason);
if (options.paymentOrderError) {
await route.fulfill({ status: 503, contentType: "application/json", body: JSON.stringify({ error: { code: "SYSTEM_001", message: "银行制单生成失败" } }) });
return;
}
if (!options.paymentOrderFile) throw new Error("合成制单文件未配置");
paymentOrderExportCount += 1;
await route.fulfill({ status: 200, contentType: "application/vnd.ms-excel", headers: { "content-disposition": "attachment; filename=payment-order-PAY-ACTION-001.xls" }, body: options.paymentOrderFile });
return;
}
if (path === `/api/payment-batches/${paymentBatchId}/receipt-attachments` && request.method() === "GET") {
if (options.receiptQueryError) {
await route.fulfill({
@@ -739,14 +780,16 @@ test.describe("R7E 全站高风险操作二次确认", () => {
await page.goto(`/payment-batches?id=${paymentBatchId}`);
const actionPanel = page.getByTestId("payment-batch-action-panel");
await expect(actionPanel.getByRole("button", { name: "上传银行回单" })).toBeVisible();
await expect(actionPanel.getByTestId("payment-batch-action-downloadOrder")).toBeVisible();
await actionPanel.getByText("更多", { exact: true }).click();
await expect(actionPanel.getByRole("button", { name: "下载银行制单" })).toBeVisible();
await expect(actionPanel.getByRole("button", { name: "上传银行回单" })).toBeVisible();
await expect(page.getByTestId("payment-batch-mobile-actions")).toBeHidden();
await expect(page.getByTestId("payment-batch-receipt-panel").getByRole("button", { name: "上传银行回单" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "登记付款完成" })).toHaveCount(0);
await page.getByTestId("payment-batch-receipt-file-input").setInputFiles({
const desktopFileChooser = page.waitForEvent("filechooser");
await actionPanel.getByRole("button", { name: "上传银行回单" }).click();
await (await desktopFileChooser).setFiles({
name: "合成银行回单.pdf",
mimeType: "application/pdf",
buffer: Buffer.from("%PDF-1.4\nsynthetic receipt\n%%EOF\n", "utf8"),
@@ -766,12 +809,12 @@ test.describe("R7E 全站高风险操作二次确认", () => {
const mobileActions = page.getByTestId("payment-batch-mobile-actions");
await expect(mobileActions).toBeVisible();
await expect(page.getByTestId("payment-batch-action-panel")).toBeHidden();
await expect(mobileActions.getByRole("button", { name: "上传银行回单" })).toBeVisible();
await expect(mobileActions.getByTestId("payment-batch-action-downloadOrder")).toBeVisible();
await mobileActions.getByRole("button", { name: "更多" }).click();
await expect(page.getByRole("menuitem", { name: "下载银行制单" })).toBeVisible();
await page.keyboard.press("Escape");
await page.getByTestId("payment-batch-receipt-file-input").setInputFiles({
await expect(page.getByRole("menuitem", { name: "上传银行回单" })).toBeVisible();
const mobileFileChooser = page.waitForEvent("filechooser");
await page.getByRole("menuitem", { name: "上传银行回单" }).click();
await (await mobileFileChooser).setFiles({
name: "移动端合成银行回单.pdf",
mimeType: "application/pdf",
buffer: Buffer.from("%PDF-1.4\nmobile synthetic receipt\n%%EOF\n", "utf8"),
@@ -782,6 +825,72 @@ test.describe("R7E 全站高风险操作二次确认", () => {
await expect(mobileActions.getByRole("button", { name: "上传银行回单" })).toHaveCount(0);
});
for (const mobile of [false, true]) {
test(`财务${mobile ? "移动端" : "桌面端"}先制单再上传回单且重下载不登记付款`, async ({ page }, testInfo) => {
test.skip(process.env.E2E_MODE === "live", "只使用合成付款数据,不访问公网");
const paymentOrderFile = syntheticPaymentOrder();
const calls = await mockSystemActionApis(page, {
hasReceipt: false, canExportPaymentOrder: true, paymentOrderFile,
meOverride: { roleCodes: ["finance"], permissions: [...me.permissions, "payment.order.export"] },
});
if (mobile) await page.setViewportSize({ width: 390, height: 844 });
await login(page, user);
await page.goto(`/payment-batches?id=${paymentBatchId}`);
const actions = page.getByTestId(mobile ? "payment-batch-mobile-actions" : "payment-batch-action-panel");
await expect(actions.getByTestId("payment-batch-action-downloadOrder")).toBeVisible();
await page.screenshot({ path: testInfo.outputPath("before-payment-order.png"), fullPage: true });
await actions.getByTestId("payment-batch-action-downloadOrder").click();
const dialog = page.getByRole("dialog");
await expect(dialog.getByRole("button", { name: "确认下载" })).toBeDisabled();
await dialog.getByRole("button", { name: "返回", exact: true }).click();
expect(calls.paymentOrderReasons).toEqual([]);
await actions.getByTestId("payment-batch-action-downloadOrder").click();
await dialog.getByRole("textbox", { name: "制单下载原因" }).fill("合成网银制单验证");
const firstDownload = page.waitForEvent("download");
await dialog.getByRole("button", { name: "确认下载" }).click();
const downloaded = await firstDownload;
expect(downloaded.suggestedFilename()).toBe("payment-order-PAY-ACTION-001.xls");
const downloadedPath = await downloaded.path();
if (!downloadedPath) throw new Error("未收到合成制单文件");
expect(readFileSync(downloadedPath).equals(paymentOrderFile)).toBe(true);
expect(paymentOrderFile.subarray(0, 8).toString("hex")).toBe("d0cf11e0a1b11ae1");
await expect(actions.getByTestId("payment-batch-action-uploadReceipt")).toBeVisible();
await expect(actions.getByTestId("payment-batch-action-downloadOrder")).toHaveCount(0);
await expect(page.getByTestId("payment-batch-detail-panel")).toContainText("已生成 1 次");
await page.screenshot({ path: testInfo.outputPath("after-payment-order.png"), fullPage: true });
await actions.getByText("更多", { exact: true }).click();
if (mobile) await page.getByRole("menuitem", { name: "重新下载制单" }).click();
else await actions.getByTestId("payment-batch-action-redownloadOrder").click();
await expect(dialog).toContainText("请勿重复提交");
await dialog.getByRole("textbox", { name: "制单下载原因" }).fill("合成文件重新下载验证");
const secondDownload = page.waitForEvent("download");
await dialog.getByRole("button", { name: "确认下载" }).click();
await secondDownload;
await expect(page.getByTestId("payment-batch-detail-panel")).toContainText("已生成 2 次");
expect(calls.paymentOrderReasons).toEqual(["合成网银制单验证", "合成文件重新下载验证"]);
expect(calls.executePaymentBatch).toBe(0);
expect(calls.uploadPaymentReceipt).toBe(0);
});
}
test("制单失败保留下载步骤且不推进付款", async ({ page }) => {
test.skip(process.env.E2E_MODE === "live", "只使用合成付款数据,不访问公网");
const calls = await mockSystemActionApis(page, { hasReceipt: false, canExportPaymentOrder: true, paymentOrderError: true });
await login(page, user);
await page.goto(`/payment-batches?id=${paymentBatchId}`);
await page.getByTestId("payment-batch-action-panel").getByTestId("payment-batch-action-downloadOrder").click();
const dialog = page.getByRole("dialog");
await dialog.getByRole("textbox", { name: "制单下载原因" }).fill("合成制单失败验证");
await dialog.getByRole("button", { name: "确认下载" }).click();
await expect.poll(() => calls.paymentOrderReasons.length).toBe(1);
await expect(dialog.getByRole("button", { name: "确认下载" })).toBeEnabled();
await expect(dialog).toBeVisible();
await expect(page.getByTestId("payment-batch-detail-panel")).toContainText("尚未生成");
expect(calls.executePaymentBatch).toBe(0);
await dialog.getByRole("button", { name: "返回", exact: true }).click();
await expect(page.getByTestId("payment-batch-action-panel").getByTestId("payment-batch-action-downloadOrder")).toBeVisible();
});
test("付款详情按处理顺序展示并统一人民币金额格式", async ({ page }) => {
await mockSystemActionApis(page, { hasReceipt: false, highRisk: true });
await page.setViewportSize({ width: 1024, height: 768 });
-1
查看文件
@@ -162,7 +162,6 @@ const ROLE_PERMISSIONS = {
"payment.batch.review",
],
ceo: [
"finance.ledger.view",
...EMPLOYEE_BASE_PERMISSIONS,
...PROCUREMENT_SELF_SERVICE_PERMISSIONS,
...MASTERDATA_VIEW_PERMISSIONS,
@@ -6,13 +6,18 @@ const {
describe("Permission Constants", () => {
it("separates full AP reading from payment review and preserves multi-role grants", () => {
for (const role of ["ceo", "finance", "finance_mgr"]) {
for (const role of ["finance", "finance_mgr"]) {
expect(hasPermission(resolvePermissionsForRoles([role]), "finance.ledger.view")).toBe(true);
}
for (const role of ["manager", "employee", "system_admin", "hr", "administration"]) {
for (const role of ["ceo", "manager", "employee", "system_admin", "hr", "administration"]) {
expect(hasPermission(resolvePermissionsForRoles([role]), "finance.ledger.view")).toBe(false);
}
expect(hasPermission(resolvePermissionsForRoles(["manager", "finance"]), "finance.ledger.view")).toBe(true);
for (const role of ["manager", "ceo"]) {
expect(hasPermission(resolvePermissionsForRoles([role, "finance"]), "finance.ledger.view")).toBe(true);
for (const permission of ["expense.approve", "leave.approve", "attendance.approve", "procurement.approve", "payment.batch.review"]) {
expect(hasPermission(resolvePermissionsForRoles([role]), permission)).toBe(true);
}
}
expect(hasPermission(resolvePermissionsForRoles(["manager"]), "payment.batch.review")).toBe(true);
expect(hasPermission(resolvePermissionsForRoles(["ceo"]), "payment.batch.manage")).toBe(false);
});
@@ -44,7 +44,7 @@ async function buildApp(permissionCodes, enabled = true) {
describe("AP full-ledger permission boundary", () => {
it.each([
["employee", false], ["manager", false], ["system_admin", false],
["ceo", true], ["finance", true], ["finance_mgr", true], ["admin", true],
["ceo", false], ["finance", true], ["finance_mgr", true], ["admin", true],
])("checks both reads for %s", async (role, allowed) => {
const { app, queries } = await buildApp(resolvePermissionsForRoles([role]));
try {
@@ -74,7 +74,7 @@ describe("AP full-ledger permission boundary", () => {
},
);
it.each([["finance.ledger.view"], resolvePermissionsForRoles(["manager", "finance"])].map((permissions) => [permissions]))(
it.each([["finance.ledger.view"], resolvePermissionsForRoles(["manager", "finance"]), resolvePermissionsForRoles(["ceo", "finance"])].map((permissions) => [permissions]))(
"honors explicit or combined-role ledger authority %j", async (permissions) => {
const { app } = await buildApp(permissions);
try {
@@ -1,6 +1,7 @@
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { spawnSync } = require("node:child_process");
const { encryptStringOrThrow } = require("../../../src/services/crypto");
const {
@@ -51,6 +52,43 @@ describe("payment order BIFF8 generator", () => {
expect(first.totalAmount).toBe("3.20");
});
it("round-trips the bank template with exact text accounts, headers and decimal amounts", () => {
const batch = buildBatch();
const payerAccount = "000123456789012345678";
const payeeAccount = "000987654321098765432";
batch.payment_account_snapshot_json.accountNumberEncrypted = encrypt(payerAccount);
batch.items[1].snapshot_json.payee.bankAccountEncrypted = encrypt(payeeAccount);
const generated = generatePaymentOrderXls(config, batch);
const templatePath = path.resolve(__dirname, "../../../src/assets/payment-order/ccb-payment-order-template.xls");
const readback = spawnSync(process.env.PAYMENT_ORDER_PYTHON_BIN ?? "python3", ["-c", [
"import json,sys,xlrd",
"book=xlrd.open_workbook(file_contents=sys.stdin.buffer.read(),formatting_info=True)",
"template=xlrd.open_workbook(sys.argv[1],formatting_info=True)",
"sheet=book.sheet_by_index(0)",
"source=template.sheet_by_index(0)",
"rows=[row for row in range(1,sheet.nrows) if sheet.cell_value(row,0) != '']",
"print(json.dumps({'biff':book.biff_version,'sheets':book.sheet_names(),'columns':sheet.ncols,'headerMatches':sheet.row_values(0)==source.row_values(0),'widthsMatch':all(sheet.computed_column_width(col)==source.computed_column_width(col) for col in range(15)),'rows':[sheet.row_values(row) for row in rows],'textTypes':[[sheet.cell_type(row,col) for col in [1,3,4,8]] for row in rows],'amountFormats':[book.format_map[book.xf_list[sheet.cell_xf_index(row,9)].format_key].format_str for row in rows]}))",
].join("\n"), templatePath], { input: generated.buffer, maxBuffer: 1024 * 1024 });
expect(readback.stderr.toString("utf8")).toBe("");
expect(readback.status).toBe(0);
const parsed = JSON.parse(readback.stdout.toString("utf8"));
expect(parsed).toMatchObject({
biff: 80,
sheets: ["Sheet1", "Sheet2", "Sheet3"],
columns: 15,
headerMatches: true,
widthsMatch: true,
textTypes: [[1, 1, 1, 1], [1, 1, 1, 1]],
amountFormats: ["0.00", "0.00"],
});
expect(parsed.rows).toHaveLength(2);
expect(parsed.rows[0]).toEqual([
"1", payerAccount, "合成测试公司", "01", payeeAccount, "甲", "中国建设银行", "",
"PB-20260802-002-R001", 1.2, "报销款", "BX-20260802-0001", "0", "", "",
]);
expect(parsed.rows[1][9]).toBe(2);
});
it("fails the whole export and reports every blocking payee issue", () => {
const batch = buildBatch();
batch.items[1].snapshot_json.payee.bankInstitutionCode = "OTHER";
@@ -64,8 +64,8 @@ describe("workspace small-team navigation defaults", () => {
hasPermission: (candidate, permission) => hasPermission(candidate.permissionCodes, permission),
});
const keys = Object.values(navigation.sections).flat().map((item) => item.key);
expect(keys.includes("apOperations")).toBe(["ceo", "finance", "finance_mgr"].includes(role));
if (role === "manager") expect(keys).toContain("paymentBatches");
expect(keys.includes("apOperations")).toBe(["finance", "finance_mgr"].includes(role));
if (["manager", "ceo"].includes(role)) expect(keys).toContain("paymentBatches");
});
it("keeps only the small-team task path in default main navigation", () => {