新增设备类型选项接口
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled

This commit is contained in:
2026-09-22 12:35:26 +08:00
parent 22c3e7cc1a
commit 4330b8a40c
28 changed files with 671 additions and 8 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-20

View File

@@ -0,0 +1,82 @@
## Context
现有员工核销提交在同一事务内创建不可变审批尝试、通用审批实例、企业微信上下文和稳定的审批提交 Outbox申请保存最新尝试与最新实例用于展示。主 Spec 已要求提交失败、回调延迟或结果未知时保持在途并使用查询/恢复机制确认,但当前没有面向员工或管理员的恢复入口,也没有可供页面稳定判断的恢复投影。
通用审批提交事件键为 `approval:{instance_id}:submission`。恢复若绕过该事实直接创建新审批,会在“企微已受理、本地结果未知”时产生重复审批单;若释放账单预占再重建申请,则会改变业务材料与并发余额事实。因此恢复必须落在既有审批实例和可靠投递边界内。
## Goals / Non-Goals
**Goals:**
- 为员工核销建立统一、幂等的原审批恢复用例,覆盖明确失败、结果未知和已有企微单号待同步三类状态。
- 让原申请员工与有权管理员共享同一恢复规则,同时保持原申请人为企微发起人。
- 把可恢复性和安全失败摘要作为读侧投影返回,避免前端复制审批状态机。
- 保持审批尝试、申请材料、账单预占和审计事实可追溯。
**Non-Goals:**
- 不创建新的审批尝试或审批实例,不提供“强制重新提交”或本地人工终审。
- 不允许通过恢复入口修改核销材料、附件、分摊或收款方式,也不释放账单预占。
- 不改变企业微信提交、查询、回调和终态消费的既有渠道契约。
- 不为退款、代理充值或提现同时新增同类入口;通用恢复接缝可复用,但本 Change 只开放员工核销。
## Decisions
### 统一恢复用例只接受核销申请标识
新增 `POST /api/admin/employee-collection-applications/{id}/recover-approval`。Handler 只绑定申请 IDApplication 用例在事务内锁定申请和最新审批尝试,并校验申请仍为审批中、最新实例引用一致及调用方权限。请求不接受审批实例 ID、提交人或材料避免调用方替换恢复目标。
选择单一业务动作而不是分别暴露“重新提交”和“查询状态”:调用方无法可靠知道企业微信是否已受理,分裂入口会把结果未知判定泄漏给前端并放大重复提交风险。
### 恢复决策以原审批实例的渠道事实为准
用例读取通用审批实例、企业微信上下文及稳定提交事件,按固定优先级决策:
1. 已有企业微信审批单号:只请求查询同步原审批单。
2. 无审批单号且提交结果未知:恢复或触发既有结果确认流程,禁止直接创建提交。
3. 无审批单号且明确失败,并能确认未受理:把同一稳定事件恢复为可投递状态。
4. 事件正在等待、重试或处理:不写重复事件,返回正在恢复。
5. 申请、实例或企微审批已终态:返回状态冲突。
恢复实际外部动作继续由 Worker 执行HTTP 请求只原子恢复可靠事实或触发查询事实,不同步调用企微创建接口。这样 HTTP 超时不会造成调用方误认为失败后再次提交。
备选是在接口内同步查询或提交;放弃,因为请求超时和进程中断会重新引入结果未知窗口。
### 稳定事件原位恢复而非新增补偿事件
继续使用 `approval:{instance_id}:submission` 作为唯一提交业务键。对终态失败或耗尽重试的事件执行带当前状态谓词的条件更新,恢复其投递资格和必要的租约字段;对活动事件返回幂等结果。查询同步复用原审批实例的稳定查询/恢复事实,不生成另一个“人工恢复提交”事件键。
并发员工和管理员操作通过审批实例行锁与事件条件更新收敛,最多一个请求改变状态。恢复审计可每次记录触发动作,但业务提交事实保持唯一。
### 权限与目标实例使用确定规则
原申请员工可恢复本人申请;超级管理员和平台用户可代为恢复;代理、企业账号和其他账号拒绝。越权与不存在保持不可区分。管理员仅作为实际操作者写审计,审批实例中的提交人和企业微信发起身份不变。
恢复目标固定为申请最新尝试与最新实例,并校验业务类型为 `employee_collection_approval`、业务标识等于最新尝试标识、尝试关联实例一致。任一引用不一致都返回状态冲突,不按申请主表或其它候选实例猜测恢复目标。
### 可恢复性由服务端投影
核销申请列表和详情增加提交状态、可恢复标记、安全失败摘要及最近恢复时间。投影由申请状态、最新实例、企微上下文和提交事件批量计算;列表不得逐条查询放大。失败摘要使用稳定分类和脱敏信息,不返回附件、完整流水、企微原文或内部队列键。
现有企业微信审批上下文已保存 `last_recovery_at`,可直接作为最近渠道恢复时间;人工触发时间、操作者和动作结果由新增核销恢复审计表达,因此不新增业务列或数据库迁移,也不得使用通用 `updated_at` 冒充恢复时间。
### 恢复失败不改变业务申请与预占
身份、场景、模板映射或渠道暂不可用时,恢复返回明确可处置原因并保留原申请、审批实例和账单预占。配置修复后可再次恢复同一事实。数据库或事件状态更新失败时事务回滚,不形成“已记录恢复但事件未恢复”的分裂状态。
每次人工恢复记录申请、审批实例、操作者、触发前提交状态、选择的恢复动作与结果;审计禁止保存支付凭证、完整外部交易流水号和企微响应原文。
## Risks / Trade-offs
- [平台用户代办可能扩大既有核销写权限] → 恢复入口是独立受控动作,仅允许平台用户恢复原审批事实;不得据此放宽列表、详情、创建、重提或本地终审等其它核销权限。
- [Outbox 终态失败与结果未知目前使用相同状态] → 以企业微信外部单号、提交认领事实和集成结果联合判定;无法证明未受理时一律走查询确认,绝不直接重提。
- [列表投影联查增加查询成本] → 按页批量读取最新审批实例、企微上下文和事件状态,禁止逐条查询。
- [恢复事件后配置仍不可用导致反复失败] → 返回稳定失败分类并保留可再次恢复资格;恢复不增加审批单或账单预占。
- [人工恢复与 Worker 正常重试竞争] → 条件更新和租约状态确保只有一个执行获得处理权,其余返回当前状态。
## Migration Plan
1. 复用企业微信审批上下文既有 `last_recovery_at` 与审计事件时间表达恢复事实,不执行数据库迁移。
2. 发布支持恢复接缝、员工核销恢复用例、读侧投影和新路由的 API/Worker 二进制。
3. 在维护者指定测试环境验证明确失败、结果未知、已有企微单号、重复与并发恢复,核对企微审批单数量、尝试数量和账单预占均不增加。
4. 回滚时停止开放恢复入口并回退二进制;已恢复的原提交事件继续按既有可靠投递语义处理,不删除审批、申请或审计事实。

View File

@@ -0,0 +1,27 @@
## Why
员工核销申请因企业微信或提交链路异常未成功提交、提交失败或结果未知后,当前没有面向员工和管理员的安全恢复入口,导致申请持续占用账单可核销金额且无法继续审批。恢复必须复用原审批实例与稳定提交事实,避免企业微信已受理但本地未知时生成重复审批单。
## What Changes
- 为待审批的员工核销申请新增“恢复原审批”操作,允许原申请员工以及其数据范围内的超级管理员、平台用户触发。
- 恢复操作复用最新审批尝试、原审批实例和稳定提交事件,不修改申请材料、不释放账单预占、不创建新的审批尝试或第二张企业微信审批单。
- 对明确提交失败、提交结果未知和已取得企业微信审批单号但本地未确认的情形,分别恢复可靠投递、优先确认原提交结果或查询同步原审批状态。
- 对仍在正常重试、已进入企业微信审批中或已完成的申请执行幂等处理或明确拒绝,并记录不含敏感材料的恢复审计。
- 列表与详情返回恢复操作所需的安全提交状态、失败摘要与可恢复标记,使前端只展示服务端判定允许的操作。
## Capabilities
### New Capabilities
无。
### Modified Capabilities
- `employee-collection-bill`: 增加员工核销申请原审批的人工恢复、权限、幂等、状态展示与审计行为。
## Impact
- 新增后台员工核销申请恢复接口及 RouteSpec/OpenAPI 说明。
- 影响员工核销申请 Handler、Application 用例、只读投影、通用审批提交恢复接缝、Outbox 事件状态处理与审计。
- 复用现有 PostgreSQL、Outbox、Asynq 和企业微信审批适配器,不新增第三方依赖或数据库迁移;最近恢复时间使用企业微信审批上下文既有 `last_recovery_at`,人工触发人与动作结果写入审计事实。

View File

@@ -0,0 +1,45 @@
## ADDED Requirements
### Requirement: 核销申请原审批可受控恢复
系统 SHALL 提供 `POST /api/admin/employee-collection-applications/{id}/recover-approval`,允许原申请员工、超级管理员和平台用户恢复仍处于审批中的核销申请。恢复对象 MUST 固定为该申请的最新审批尝试与最新审批实例,并校验审批实例业务类型为 `employee_collection_approval`、业务标识等于最新尝试标识且尝试关联的审批实例一致;请求不得接受调用方传入审批实例或新材料。恢复 MUST 复用原审批实例及同一稳定提交事实,不得修改申请材料、账单分摊或审批快照,不得释放账单预占,不得新增审批尝试、审批实例或第二张企业微信审批单。管理员代为恢复时,企业微信发起人 MUST 保持原申请人,且系统 MUST 记录实际操作者。
恢复资格 MUST 由服务端根据申请状态、最新审批实例和提交事实判定。明确提交失败且可确认企业微信未受理时,系统 SHALL 恢复同一提交事实;提交结果未知时,系统 MUST 优先确认原提交结果,在无法证明未受理前不得再次提交创建审批;已取得企业微信审批单号但本地状态未确认时,系统 MUST 查询并同步原审批单。仍在正常重试或企业微信已处于审批中的请求 SHALL 幂等返回当前恢复状态,不得重复投递;申请或审批已经进入终态时 MUST 拒绝恢复。
核销申请列表与详情 SHALL 返回服务端判定的可恢复标记、审批提交状态及名称、安全失败摘要和最近恢复时间;列表投影 MUST 批量完成,查询次数不得随当页申请数量线性增长。恢复操作及结果 MUST 写入审计,但不得记录支付凭证、完整外部交易流水号、完整企业微信响应或内部事件键。代理、企业账号及其他无权账号的操作与申请不存在 MUST 不可区分。
#### Scenario: 明确提交失败后恢复原审批
- **GIVEN** 核销申请仍处于审批中,最新审批实例的提交明确失败,且系统可确认企业微信未受理
- **WHEN** 原申请员工或有权管理员请求恢复审批
- **THEN** 系统恢复同一稳定提交事实,保持原审批实例、申请材料、账单分摊和预占金额不变,且不创建第二张企业微信审批单
#### Scenario: 提交结果未知时禁止直接重提
- **GIVEN** 核销申请仍处于审批中,最新审批实例的提交结果未知
- **WHEN** 授权账号请求恢复审批
- **THEN** 系统进入原提交结果确认流程,在无法证明企业微信未受理前不再次调用创建审批,并返回正在确认的恢复状态
#### Scenario: 已取得企微审批单号时查询同步
- **GIVEN** 最新审批实例已有企业微信审批单号,但本地提交或审批状态尚未确认
- **WHEN** 授权账号请求恢复审批
- **THEN** 系统查询并同步该原审批单,不恢复创建提交且不生成新的审批实例
#### Scenario: 重复或并发恢复保持幂等
- **WHEN** 员工与管理员重复或并发恢复同一核销申请
- **THEN** 系统至多一次改变原提交事实的恢复状态,其余请求返回相同的正在恢复或已确认状态,审批实例数、审批尝试数和账单预占金额均不增加
#### Scenario: 正常重试中的申请再次恢复
- **GIVEN** 原审批提交事件仍处于等待重试或处理中
- **WHEN** 授权账号再次请求恢复
- **THEN** 系统不重复投递,返回正在恢复及当前提交状态
#### Scenario: 审批终态不可恢复
- **WHEN** 核销申请或其最新企业微信审批已经进入通过、驳回、撤销或关闭终态后请求恢复
- **THEN** 系统返回状态冲突,不改变申请、账单、审批实例或提交事实
#### Scenario: 申请人与管理员权限边界
- **WHEN** 原申请员工、超级管理员或平台用户请求恢复,或代理、企业账号及其他无权账号请求恢复
- **THEN** 系统允许前三类账号恢复并在管理员代操作时保持原申请人为企业微信发起人;对无权账号以与申请不存在不可区分的结果拒绝
#### Scenario: 配置修复后再次恢复
- **GIVEN** 原审批因申请人企业微信身份、审批场景或模板映射不可用而恢复失败,且申请仍满足恢复条件
- **WHEN** 维护者修复配置后授权账号再次请求恢复
- **THEN** 系统继续恢复同一原审批事实,不要求重建申请或释放账单预占

View File

@@ -0,0 +1,27 @@
## 1. 现有事实与恢复接缝
- [x] 1.1 走读通用审批实例、企业微信上下文、提交 Outbox、集成交互记录及核销申请最新尝试字段形成明确失败、结果未知、已有企微单号、正常重试与终态的判定矩阵并确认原申请员工、超级管理员和平台用户可恢复代理与企业账号拒绝。
- [x] 1.2 在通用审批 Application/Infrastructure 边界新增按原审批实例恢复提交或触发查询确认的接缝;稳定事件键继续使用 `approval:{instance_id}:submission`,活动事件幂等返回,终态失败事件以条件更新恢复投递资格。
- [x] 1.3 复用企业微信审批上下文既有 `last_recovery_at` 返回最近渠道恢复时间,并以核销恢复审计记录人工触发时间、操作者和动作结果;不新增数据库迁移,不使用通用 `updated_at` 冒充恢复时间。
## 2. 员工核销恢复用例
- [x] 2.1 在员工核销 Application 中实现恢复原审批用例:锁定申请与最新尝试,校验审批中状态、业务类型为 `employee_collection_approval`、业务标识等于最新尝试、尝试与最新实例引用一致,以及原申请员工或管理员权限;引用不一致、越权与终态申请均拒绝。
- [x] 2.2 按判定矩阵处理已有企微审批单号、结果未知、明确未受理失败和正常重试;恢复不得修改申请材料、审批快照、分摊或账单预占,不得新增审批尝试或审批实例。
- [x] 2.3 为每次人工恢复写入脱敏审计,记录申请、审批实例、实际操作者、触发前状态、恢复动作与结果;管理员代操作保持原申请人为企业微信发起人。
- [x] 2.4 保证员工与管理员重复或并发恢复时至多一次改变提交事实,其余请求返回一致的正在恢复、已确认提交或已在审批中结果。
## 3. 查询投影与 HTTP 入口
- [x] 3.1 扩展核销申请列表与详情 DTO返回服务端判定的可恢复标记、提交状态、安全失败摘要、恢复动作结果与最近恢复时间失败摘要不得包含附件、完整流水、企微原文或内部队列键。
- [x] 3.2 在核销申请 Query 中按页批量读取最新审批实例、企业微信上下文和提交事件,计算恢复投影,避免列表逐条查询。
- [x] 3.3 新增 `POST /api/admin/employee-collection-applications/{id}/recover-approval` Handler 与 RouteSpec只接受路径申请 ID同步可执行路由、`cmd/api/docs.go``cmd/gendocs/main.go` 及 OpenAPI 生成物要求。
- [x] 3.4 核对原申请员工、超级管理员、平台用户、代理与企业账号的路由门禁;前三类允许恢复,后两类拒绝,越权与不存在保持不可区分,且不扩大列表、详情或其他核销写操作权限。
## 4. 验证与规格同步
- [x] 4.1 在维护者指定测试环境验证明确失败恢复:同一稳定事件恢复并最终只产生一张企业微信审批单,审批尝试数、审批实例数和账单预占金额不增加。
- [x] 4.2 验证提交结果未知与已有企微单号两条路径只确认或查询原审批,不再次调用创建审批;正常重试中的重复恢复幂等返回。
- [x] 4.3 验证员工与管理员重复及并发操作、终态拒绝、本人和管理员权限边界、配置修复后再次恢复,以及恢复失败时申请材料和预占均不变化。
- [x] 4.4 核对列表与详情恢复投影、审计脱敏和批量查询行为,并运行 `gofmt -w``go build ./cmd/api ./cmd/worker``go run cmd/gendocs/main.go`
- [x] 4.5 同步证据矩阵和主 Spec 可达操作索引,运行 `openspec validate recover-employee-collection-approval --strict``openspec doctor --json``openspec validate --all``./scripts/context-health.sh`