新增代理注册申请管理
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m41s

This commit is contained in:
2026-09-21 16:51:37 +08:00
parent b063617153
commit 4a16eb0b1e
22 changed files with 927 additions and 179 deletions

View File

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

View File

@@ -0,0 +1,88 @@
## Context
代理扫码注册在审批前已保存完整注册记录,并关联一个业务类型为 `agent_distribution_approval` 的通用审批实例。终态消费者在通过时原子创建店铺、代理账号和钱包,在驳回时只更新注册记录。现有对外入口只有无需认证的创建接口,后台没有查询或恢复入口。
注册记录的 `parent_shop_id` 是提交时分销码所属直接上级店铺事实,可直接支撑代理直属范围;审批前不存在下级店铺,因此不能复用店铺列表或基于已建店层级查询。通用审批与企业微信上下文已经保存提交状态、外部审批单号、失败摘要和恢复时间,可用于只读投影与原实例恢复。
## Goals / Non-Goals
**Goals:**
- 为平台与直接上级代理提供注册申请列表和详情,区分业务状态、审批状态和提交状态。
- 以注册时冻结的 `parent_shop_id` 固化代理直属可见范围。
- 提供提交失败或结果未知时的原审批恢复,不改变注册终审和建店事务。
- 在允许业务处理所需完整联系资料的同时,排除认证凭证和内部集成细节。
**Non-Goals:**
- 不把待审批注册记录投影为虚拟店铺,不加入现有店铺列表。
- 不提供本地人工通过、驳回、修改注册资料或删除申请。
- 不允许代理查看更深层下级申请,不使用 `SubordinateShopIDs` 扩大范围。
- 不新增数据库迁移、导出、批量审批或第三方依赖。
## Decisions
### 使用独立注册 Query不复用店铺或通用审批列表
`distributionwithdrawal` 查询边界增加注册列表与详情。主查询以 `tb_agent_distribution_registration` 为业务事实,审批实例和企业微信上下文只用于状态投影。
备选把申请混入店铺列表;放弃,因为审批前不存在店铺,虚拟店铺会污染 ID、状态和权限语义。备选只查询通用审批列表放弃因为通用快照不是完整注册查询模型也不适合业务筛选。
### 代理范围精确匹配直接上级店铺
超级管理员和平台用户不加店铺过滤。代理账号必须有有效当前店铺,并施加 `parent_shop_id = 当前店铺 ID`;范围为空时列表返回空,详情和恢复返回统一不可见错误。企业账号和其他类型拒绝。
不使用当前店铺的下级集合:业务已确认代理只看使用自己分销码提交的直接申请,更深层申请属于其下级代理的运营范围。注册记录保持提交时的直接上级快照,即使之后层级变化也不重算。
### 查询分别投影三层状态
响应分别返回:
- 注册业务状态:待审批、已通过、已驳回;
- 通用审批状态:提交中、审批中及各终态;
- 企业微信提交状态:待提交、发送中、已提交、失败、结果未知。
可恢复标记由服务端基于三层事实和上级店铺启用状态计算,前端不得自行推断。列表按页收集上级店铺和审批实例 ID批量读取店铺名称、审批实例及企微上下文避免 N+1。
筛选时间复用严格 RFC3339 闭区间解析规则;申请时间使用 `start_time/end_time`,决定时间使用 `decided_start_time/decided_end_time`。手机号精确匹配,店铺名称、编号、用户名和联系人使用受长度限制的子串匹配。
### 完整联系资料仅作为授权业务投影
根据业务决定平台账号和有权直接上级代理均返回完整手机号、完整省市区和详细地址便于处理注册申请。DTO 明确禁止包含密码哈希验证码没有持久化在注册记录也不得通过其它联查返回。失败摘要只使用安全文本不返回企微响应原文、Outbox 事件键、租约或内部重试字段。
审计只记录注册 ID、店铺编号、上级店铺、状态和脱敏手机号不复制完整手机号或地址。列表/详情访问沿用统一访问审计,不为每次读取新增业务动作。
### 恢复用例编排通用审批可靠事实
新增 `POST /api/admin/agent-distribution-registrations/{id}/recover-approval`。Application 用例在事务内锁定注册记录,校验状态仍待审批、实例非空、实例业务类型和业务 ID 一致,并复核权限及上级店铺仍启用。
恢复顺序:
1. 已有企微 `sp_no`:请求同步原审批详情。
2. 提交结果未知:触发原实例确认流程,禁止直接重提。
3. 明确失败且确认未受理:恢复 `approval:{instance_id}:submission` 稳定事件。
4. 事件等待重试、处理中或审批进行中:幂等返回当前状态。
5. 注册或审批终态:状态冲突。
HTTP 不同步调用企微创建审批接口,外部查询和提交继续由 Worker 执行。恢复不修改注册资料或密码哈希,不创建任何业务实体,也不新增审批实例。
### 停用上级只读不恢复
列表与详情读取历史注册记录时不要求上级店铺启用,保证运营可追溯。恢复必须要求上级店铺仍存在且启用,因为审批通过终态同样要求启用上级;在无落地条件时恢复只会制造无法完成的审批。
恢复动作写审计,记录操作者及其店铺、实例、前置状态、所选动作和安全结果;不复制个人敏感原文。
## Risks / Trade-offs
- [代理可见完整手机号与地址扩大个人信息暴露] → 严格限制为直接上级店铺,企业账号拒绝,响应与审计白名单排除认证凭证和不必要复制。
- [平台用户当前无实际店铺范围] → 按已确认规则平台全量,不引入未定义的平台范围推导。
- [上级店铺后续迁移导致历史归属与当前层级不同] → 以注册时 `parent_shop_id` 为稳定运营事实,不重算。
- [恢复与正常 Worker 重试竞争] → 实例锁和稳定事件条件更新保证单次状态变化,活动事件只返回当前状态。
- [关键词筛选拖慢列表] → 限制输入长度并仅在注册业务列上查询;数据规模扩大后再评估专用索引,不预建检索系统。
## Migration Plan
1. 不执行数据库迁移;发布注册 Query、恢复用例、后台路由及审批状态投影。
2. 在维护者指定测试环境创建直属、非直属、待审批、已通过、已驳回及提交异常申请,验证数据范围和筛选。
3. 验证明确失败、结果未知、已有 `sp_no`、并发恢复和上级停用拒绝,不产生第二个实例或任何半套业务实体。
4. 回滚时移除后台入口并回退二进制;注册记录、审批实例和审计事实保持不变。

View File

@@ -0,0 +1,27 @@
## Why
代理扫码注册记录、审批实例和终态结果已经落库,但后台没有列表或详情入口,平台和上级代理无法感知谁提交了申请、当前是否进入企业微信审批以及最终通过或驳回。提交失败或结果未知时也缺少按原审批实例恢复的业务入口,造成待审批记录长期不可处置。
## What Changes
- 新增代理注册申请后台分页列表和详情,展示注册资料、直接上级店铺、业务状态、审批状态、提交异常与终态结果。
- 超级管理员和平台用户可查看全部申请;代理只可查看 `parent_shop_id` 等于其当前店铺的直接下级申请,不包含更深层级;企业账号拒绝。
- 平台和有权代理均可查看完整手机号与完整注册地址;密码哈希、短信验证码、企业微信原始响应和内部事件字段不得返回。
- 支持按注册状态、申请与决定时间、上级店铺、手机号及注册资料关键字筛选,并区分业务待审批、企微审批中、提交失败和结果未知。
- 为提交失败、结果未知或已有企业微信审批单号但本地未确认的待审批注册申请新增原审批恢复入口,复用原审批实例与稳定提交事实,不创建新注册记录或第二张审批单。
- 上级店铺停用后仍允许查看历史申请,但不得恢复待审批申请;审批仍只能由企业微信终审。
## Capabilities
### New Capabilities
无。
### Modified Capabilities
- `agent-distribution-withdrawal`: 增加代理注册申请后台查询、直接上级数据范围、个人资料展示与原审批恢复行为。
## Impact
- 新增代理注册后台 Query、DTO、Handler、路由和 OpenAPI影响通用审批恢复接缝、企业微信提交状态投影及审计。
- 复用现有代理注册记录、通用审批实例、企业微信上下文、Outbox、Asynq 和店铺事实;不新增数据库迁移或第三方依赖。

View File

@@ -0,0 +1,65 @@
## ADDED Requirements
### Requirement: 代理注册申请后台可查询
系统 SHALL 提供 `GET /api/admin/agent-distribution-registrations``GET /api/admin/agent-distribution-registrations/{id}`,使超级管理员和平台用户查看全部代理扫码注册申请,使代理账号只查看 `parent_shop_id` 等于其当前所属店铺的直接下级申请。代理范围 MUST 仅按注册时冻结的直接上级店铺判定,不得包含更深层下级、其他店铺申请,也不得因后续店铺层级变化重新推导;无有效所属店铺的代理列表 MUST 为空,详情 MUST 与不存在不可区分。企业账号 MUST 被拒绝。
列表 SHALL 支持按注册业务状态、申请时间闭区间、决定时间闭区间、手机号精确值及店铺名称、店铺编号、代理用户名、联系人关键字筛选,并支持筛选审批提交失败或结果未知。上级店铺筛选仅对超级管理员和平台用户开放。结果默认按申请时间和申请标识倒序分页。
列表与详情 SHALL 返回注册申请标识、业务状态及名称、申请店铺和账号资料、联系人、完整手机号、完整省市区及详细地址、直接上级店铺标识与名称、分销码快照、申请与决定时间、驳回原因、审批实例与审批状态、审批提交状态、安全失败摘要、可恢复标记和最近恢复时间。系统 MUST NOT 返回密码哈希、短信验证码、企业微信原始响应、内部事件键或租约字段。查询 MUST 批量投影上级店铺和审批状态,列表查询次数不得随当页申请数量线性增长。
#### Scenario: 平台查看全部注册申请
- **WHEN** 超级管理员或平台用户查询代理注册申请列表
- **THEN** 系统返回全部符合筛选条件的申请及其业务、审批和提交状态
#### Scenario: 代理查看直接下级申请
- **GIVEN** 当前代理店铺既有直接使用其分销码提交的申请,也有更深层级或其他店铺的申请
- **WHEN** 代理查询列表或详情
- **THEN** 系统只返回 `parent_shop_id` 等于当前店铺的申请,不返回更深层级或其他店铺申请
#### Scenario: 无店铺代理查询
- **WHEN** 未关联有效店铺的代理查询注册申请
- **THEN** 列表返回空结果,详情与申请不存在不可区分
#### Scenario: 查看完整联系资料但不返回凭证
- **WHEN** 有权平台账号或代理查看注册申请列表或详情
- **THEN** 响应包含完整手机号和完整注册地址,但不包含密码哈希、短信验证码、企微原始响应或内部事件字段
#### Scenario: 区分业务待审批与提交异常
- **GIVEN** 两条注册记录均为业务待审批,其中一条已进入企业微信审批,另一条提交失败或结果未知
- **WHEN** 授权账号查询列表
- **THEN** 系统分别返回其审批提交状态和可恢复标记,不把两条记录展示为相同的审批状态
### Requirement: 代理注册原审批可受控恢复
系统 SHALL 提供 `POST /api/admin/agent-distribution-registrations/{id}/recover-approval`,允许超级管理员、平台用户以及申请直接上级店铺的代理恢复仍处于待审批的注册申请。恢复 MUST 校验注册记录已关联唯一通用审批实例,且实例业务类型为 `agent_distribution_approval`、业务标识等于注册记录标识。无权操作与申请不存在 MUST 不可区分;上级店铺已停用时历史申请仍可查询,但待审批申请 MUST NOT 恢复。
恢复 MUST 复用原审批实例和同一稳定提交事实,不得修改注册资料或密码哈希,不得新建注册记录、审批实例或第二张企业微信审批单,不得创建店铺、账号、钱包或上下级关系,也不得在本地通过或驳回申请。明确提交失败且可确认企业微信未受理时,系统 SHALL 恢复同一提交事实;提交结果未知时 MUST 优先确认原提交结果;已有企业微信审批单号时 MUST 查询同步原审批单;正常重试或已进入企业微信审批中 SHALL 幂等返回当前状态;注册记录或审批已经终态时 MUST 拒绝恢复。
恢复操作 MUST 写入脱敏审计,记录注册申请、审批实例、操作者、操作者店铺、恢复前提交状态、选择的恢复动作与结果,但不得复制完整手机号、详细地址、密码哈希、企业微信原始响应或内部队列键。
#### Scenario: 明确提交失败后恢复原审批
- **GIVEN** 注册记录仍待审批,原审批提交明确失败且可确认企业微信未受理,上级店铺仍启用
- **WHEN** 授权账号请求恢复
- **THEN** 系统恢复同一稳定提交事实,不新增注册记录或审批实例,不创建店铺、账号或钱包
#### Scenario: 提交结果未知时禁止重提
- **GIVEN** 原审批实例提交结果未知
- **WHEN** 授权账号请求恢复
- **THEN** 系统进入原提交结果确认流程,在无法证明未受理前不再次创建审批
#### Scenario: 已有企微审批单号时查询同步
- **GIVEN** 原审批实例已有企业微信审批单号但本地状态未确认
- **WHEN** 授权账号请求恢复
- **THEN** 系统查询同步原审批单,不恢复创建提交且不生成新审批实例
#### Scenario: 重复或并发恢复保持幂等
- **WHEN** 平台账号和直接上级代理重复或并发恢复同一注册申请
- **THEN** 系统至多一次改变原提交事实,其余请求返回相同当前状态,注册记录数和审批实例数不增加
#### Scenario: 停用上级店铺不可恢复
- **GIVEN** 注册申请的直接上级店铺已停用
- **WHEN** 授权账号查看或恢复该待审批申请
- **THEN** 系统允许查看历史申请但拒绝恢复,不创建任何下级实体
#### Scenario: 终态申请不可恢复
- **WHEN** 已通过或已驳回注册申请请求恢复
- **THEN** 系统返回状态冲突,不改变注册记录、已创建实体或审批事实

View File

@@ -0,0 +1,35 @@
## 1. 注册申请查询模型
- [x] 1.1 新增代理注册列表、详情、分页与筛选 DTO字段覆盖注册业务状态、完整联系资料、直接上级店铺、审批状态、提交状态、失败摘要、可恢复标记和最近恢复时间明确排除密码哈希及内部集成字段。
- [x] 1.2 在 `distributionwithdrawal` Query 中实现注册列表:支持状态、申请与决定时间闭区间、手机号精确值、资料关键字、上级店铺和提交异常筛选,默认按 `created_at DESC, id DESC` 分页。
- [x] 1.3 实现注册详情并与列表复用同一数据范围和投影口径;按页批量读取上级店铺、审批实例和企微上下文,避免 N+1。
- [x] 1.4 明确三层状态投影:注册业务状态、通用审批状态和企微提交状态;由服务端计算安全失败摘要及可恢复标记。
## 2. 权限、范围与隐私
- [x] 2.1 超级管理员和平台用户查询全部申请;代理强制 `parent_shop_id = 当前店铺 ID`,不使用 `SubordinateShopIDs`,无有效店铺时列表为空、详情不可见;企业账号拒绝。
- [x] 2.2 上级店铺筛选只对超级管理员和平台用户开放;代理请求不得通过筛选参数扩大直属范围,越权与不存在保持不可区分。
- [x] 2.3 平台和有权代理返回完整手机号与完整注册地址;响应、日志和业务审计不得包含密码哈希、验证码、企微原文、内部事件键或租约字段,审计中的手机号必须脱敏。
## 3. 原审批恢复用例
- [x] 3.1 在通用审批 Application/Infrastructure 边界提供原实例查询同步、结果未知确认和稳定提交事件恢复接缝,复用 `approval:{instance_id}:submission`,活动事件幂等返回。
- [x] 3.2 实现代理注册恢复用例:锁定注册记录,校验仍待审批、审批实例非空、业务类型与业务 ID 一致、调用方范围正确且上级店铺仍启用。
- [x] 3.3 按固定优先级处理已有 `sp_no`、结果未知、明确未受理失败、正常重试和审批中;恢复不修改注册资料或密码哈希,不创建注册记录、审批实例、店铺、账号、钱包或层级。
- [x] 3.4 保证平台与代理重复或并发恢复至多一次改变原提交事实;终态申请、停用上级店铺和实例关联异常返回稳定冲突。
- [x] 3.5 为人工恢复写脱敏审计,记录注册申请、审批实例、操作者及店铺、前置提交状态、恢复动作与结果。
## 4. HTTP 入口与文档
- [x] 4.1 新增后台注册申请 Handler接入列表、详情和恢复用例边界只做参数绑定与统一响应权限和状态规则留在 Query/Application。
- [x] 4.2 注册 `GET /api/admin/agent-distribution-registrations``GET /api/admin/agent-distribution-registrations/{id}``POST /api/admin/agent-distribution-registrations/{id}/recover-approval` RouteSpec。
- [x] 4.3 同步 `internal/bootstrap` 装配、`cmd/api/docs.go``cmd/gendocs/main.go` 与 OpenAPI 描述,标明接口包含授权业务处理所需的完整手机号和地址。验证:`pkg/openapi/handlers.go``cmd/api/docs.go``cmd/gendocs/main.go` 已装配 `AgentDistributionRegistrationHandler`;重新生成的 `docs/admin-openapi.yaml` 包含列表、详情和恢复接口及完整资料说明。
## 5. 验证与规格同步
- [ ] 5.1 在维护者指定测试环境验证平台全量、代理直属、非直属与更深层不可见、无店铺代理空列表及企业账号拒绝。
- [ ] 5.2 验证状态、时间、手机号、关键字、上级店铺和提交异常筛选,核对完整联系资料可见且密码哈希、验证码与内部集成字段不泄露。
- [ ] 5.3 验证明失败、结果未知、已有 `sp_no`、正常重试、重复和并发恢复;核对注册记录数、审批实例数及店铺、账号、钱包数量不因恢复增加。
- [ ] 5.4 验证上级店铺停用后历史仍可查询但不能恢复,终态申请不可恢复,列表状态投影批量读取无 N+1。
- [x] 5.5 运行 `gofmt -w``go build ./cmd/api ./cmd/worker``go run cmd/gendocs/main.go`,同步主 Spec 可达操作索引和证据矩阵。验证gofmt/build/gendocs 成功;主 Spec 与 `entry-capability-requirement-matrix.json``requirement-evidence.json` 已加入三条后台入口。
- [x] 5.6 运行 `openspec validate add-agent-registration-management --strict``openspec doctor --json``openspec validate --all``./scripts/context-health.sh`。验证:目标 Change strict、doctor、context-health 通过;全量校验在主 Spec 场景修复后重跑。

View File

@@ -79,6 +79,46 @@
- **WHEN** 同一注册审批终态被重复投递
- **THEN** 系统至多创建一次店铺、账号、钱包与层级关系
### Requirement: 代理注册申请后台可查询
系统 SHALL 提供 `GET /api/admin/agent-distribution-registrations``GET /api/admin/agent-distribution-registrations/{id}`,使超级管理员和平台用户查看全部代理扫码注册申请,使代理账号只查看 `parent_shop_id` 等于其当前所属店铺的直接下级申请;企业账号 MUST 被拒绝。列表 SHALL 支持业务状态、申请与决定时间闭区间、手机号精确值、注册资料关键字、上级店铺和审批提交异常筛选,并按 `created_at DESC, id DESC` 分页。
列表与详情 SHALL 返回注册业务、审批和企微提交三层状态、完整手机号与完整注册地址、直接上级店铺、审批实例、安全失败摘要、可恢复标记及最近恢复时间;系统 MUST NOT 返回密码哈希、验证码、企微原文、内部事件键或租约字段,并 MUST 批量投影关联店铺和审批状态,避免按申请数量线性增加查询。
#### Scenario: 平台与代理查询范围
- **WHEN** 平台账号或直接上级代理查询代理注册申请
- **THEN** 平台账号返回全部符合筛选条件的申请,代理仅返回 `parent_shop_id` 等于当前店铺的申请,企业账号被拒绝
#### Scenario: 联系资料投影排除凭证
- **WHEN** 有权账号查看注册申请列表或详情
- **THEN** 响应包含完整手机号和注册地址,但不包含密码哈希、验证码、企微原文、内部事件键或租约字段
#### Scenario: 提交异常状态可区分
- **WHEN** 两条待审批申请分别处于审批中和提交失败或结果未知
- **THEN** 列表分别返回企微提交状态及服务端计算的可恢复标记
### Requirement: 代理注册原审批可受控恢复
系统 SHALL 提供 `POST /api/admin/agent-distribution-registrations/{id}/recover-approval`,允许有权平台账号及申请直接上级代理恢复仍待审批的注册申请。恢复 MUST 校验原审批实例及业务关联、上级店铺启用状态和调用方范围,复用原实例与稳定提交事实;不得新建注册记录、审批实例、企业微信审批单、店铺、账号或钱包。结果未知时 MUST 优先确认原提交,已有审批单号时 MUST 同步原审批,明确未受理失败时才可恢复提交事件,终态申请或停用上级 MUST 返回稳定冲突;恢复 MUST 写入脱敏审计。
#### Scenario: 明确失败恢复原提交事实
- **WHEN** 待审批申请的原提交明确失败且确认未被企业微信受理,且上级店铺仍启用
- **THEN** 系统恢复同一提交事实,不新建注册记录、审批实例或业务实体
#### Scenario: 结果未知优先确认
- **WHEN** 待审批申请的原提交结果未知
- **THEN** 系统进入原实例确认流程,不直接创建新的企业微信审批
#### Scenario: 终态或停用上级拒绝恢复
- **WHEN** 注册申请已终态或其直接上级店铺已停用时请求恢复
- **THEN** 系统返回稳定冲突且不改变注册、审批或实体事实
### Requirement: 提现资料资格
代理首次提现前 SHALL 提交企业微信资料资格申请;合同和法人身份证正反面必填,企业代理填写统一社会信用代码、个人代理填写法人身份证号。合同资格主体 MUST 填写统一社会信用代码或身份证号;营业执照、门头照可选,发票仅企业可选且其抬头/统一社会信用代码 MUST 与合同主体一致。审批通过且资料未过期才有效;合同和身份证通过后长期有效,直到代理替换资料、超级管理员作废或代理停用。
@@ -254,6 +294,10 @@
`POST /api/c/v1/agent-distribution-registrations`(代理扫码注册,公开接口)。
### 代理注册申请后台管理
`GET /api/admin/agent-distribution-registrations`(查询代理注册申请列表);`GET /api/admin/agent-distribution-registrations/{id}`(查询代理注册申请详情);`POST /api/admin/agent-distribution-registrations/{id}/recover-approval`(恢复原审批提交)。
### 提现资料资格
`POST /api/admin/shops/{shop_id}/withdrawal-qualifications`(提交提现资料资格);`GET /api/admin/shops/{shop_id}/withdrawal-qualifications`(查询提现资料资格版本);`POST /api/admin/withdrawal-qualifications/{id}/void`(作废提现资料资格)。