实施业务用户组成员管理
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m42s

This commit is contained in:
2026-09-21 21:20:38 +08:00
parent 4a16eb0b1e
commit 22c3e7cc1a
14 changed files with 836 additions and 106 deletions

View File

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

View File

@@ -0,0 +1,72 @@
## Context
当前成员关系表已保证一个平台账号至多一条未删除归属。`SetMembers` 实际只迁移请求中的账号,不会替换目标组完整集合;`ClearMembers` 则按账号全局清空归属,不校验这些账号属于哪个组。缺少成员和候选查询使前端无法展示真实归属,全局清空入口还允许错误页面移除其他组成员。
现有写用例已具备启用平台账号校验、账号行按 ID 升序加锁、唯一约束裁决和逐账号审计。成员查询需要保留禁用及软删除账号关系,而候选查询只面向启用未删除平台用户。
## Goals / Non-Goals
**Goals:**
- 为每个用户组提供可分页的成员和候选查询,明确账号当前归属与下一步动作。
- 把增加和移除都约束到目标组,避免整体覆盖和跨组误清空。
- 保持现有唯一归属、事务回滚、并发锁和审计不变量。
- 让成员变化继续实时驱动店铺所属组推导。
**Non-Goals:**
- 不引入完整成员集合保存、乐观版本号或组层级。
- 不改变角色、登录、权限、数据范围或店铺负责人本身。
- 不新增数据库迁移、导入导出或成员审批。
- 不保留旧成员写入口兼容别名。
## Decisions
### 查询拆分为当前成员与可选候选
新增组内成员 Query成员关系为主表并以 `Unscoped` 联查账号,使禁用和软删除账号仍可展示。关键字匹配用户名或手机号;账号状态筛选区分启用、禁用和已删除。返回关系创建时间及目标组快照。
新增候选 Query以启用未删除的平台账号为主表LEFT JOIN 当前未删除成员关系和用户组。支持全部、未分组、当前组和其他组筛选,并按当前关系计算 `add``already_member``move`。目标组停用时仍可查询,但统一返回不可添加标记。
选择两个端点而非在一个端点用模式参数混合:成员查询需要保留删除账号,候选查询必须排除它们,主表与授权语义不同。
### 增加成员沿用唯一归属替换语义并细分结果
把现有 `PUT /:id/members` 改为 `POST /:id/members`。写用例继续要求目标组启用、全部账号为可用平台用户,并按账号 ID 升序锁账号行。锁后读取原归属:无关系新增、其他组更新、当前组不写。
结果按锁后事实统计 `added_count``moved_count``unchanged_count`。只为新增或迁移账号写审计;当前组幂等项不写审计。请求外成员不参与任何删除或更新。
### 移除成员必须验证目标组归属
新增 `DELETE /:id/members`。目标组只要求存在未删除,停用组允许移除。事务内按账号 ID 升序锁账号行,再读取成员关系并要求每个账号当前都属于目标组;任何缺失或其他组关系导致整批状态冲突。
校验通过后执行 `WHERE business_user_group_id = ? AND account_id IN ? AND deleted_at IS NULL` 软删除,并要求影响行数等于请求账号数。校验账号本身时使用保留关系和 `Unscoped` 账号读取,因此禁用或已删除账号仍可清理。移除审计以账号为主资源、原组为引用资源。
备选继续调用全局 `ClearMembers`;放弃,因为缺少目标组谓词,无法阻止跨组误清空。
### 干净移除旧写入口
删除全局 `DELETE /business-user-groups/members` 路由、Handler 方法及专用 DTO现有 `PUT /:id/members` 改为 POST不保留 PUT 别名。前端同步迁移到组作用域端点。
这是有意的破坏性收敛:保留旧入口会留下两套不同安全边界,并使调用方继续把操作理解为整体保存或全局清空。
### 成员数在组查询中批量投影
用户组列表按当页组 ID 一次聚合成员关系数量,详情按目标组计数。计数口径为未删除成员关系,不因账号禁用或软删除排除,和“关系保留”契约一致。成员写入后无需更新组表或缓存。
店铺所属组仍通过当前负责人和成员关系实时推导;本变更不写店铺组 ID 或业务线。
## Risks / Trade-offs
- [破坏性路由切换影响前端] → 同一发布批次同步前端方法与路径OpenAPI 只保留新端点,旧路由明确不可达。
- [软删除账号缺少可读字段] → 使用账号表 Unscoped 读取历史字段并标记已删除,不恢复账号或关系。
- [并发迁移与移除同一账号] → 两条用例都按账号 ID 升序锁账号行,锁后重读归属,唯一索引作为最终裁决。
- [大组成员查询] → 强制分页和关键字长度限制;成员表已有组索引,不预建新索引。
- [候选手机号属于敏感信息] → 该能力仅超级管理员和平台用户可用,沿用现有平台业务员候选的手机号展示策略;实现时固定响应口径并避免审计复制完整手机号。
## Migration Plan
1. 不执行数据库迁移;发布成员/候选 Query、增量写用例和新路由。
2. 同步前端PUT 增加改为 POST全局 DELETE 改为携带组 ID 的 DELETE接入成员和候选分页。
3. 在维护者指定测试环境验证新增、迁移、幂等、停用组移除、删除账号清理、跨组拒绝和并发裁决。
4. 回滚需同步恢复旧前端调用和旧二进制;成员关系本身仍兼容旧模型,不需要数据回滚。

View File

@@ -0,0 +1,27 @@
## Why
业务用户组当前只能看到组本身,无法分页查看成员或在选择用户时识别其当前所属组;现有全局清空入口还可能误清除其他组成员,导致前端把增量归属误解为整体替换。需要把成员查看、候选选择和增量增删收敛到明确的组作用域。
## What Changes
- 新增组内成员分页列表,返回当前组的启用、禁用及已删除平台账号成员和关系状态。
- 新增成员候选分页列表,覆盖全部启用平台用户并返回当前所属组及 `add``already_member``move` 操作提示。
- 新增组作用域的增量增加接口:未分组用户加入目标组,其他组用户直接迁入,当前组成员幂等保持;不影响请求外成员。
- 新增组作用域的增量移除接口:只允许移除当前确实属于目标组的选中成员,支持清理禁用或已删除账号关系,不得清除其他组归属。
- **BREAKING**:以 `POST /api/admin/business-user-groups/{id}/members` 替换现有 `PUT` 成员设置入口,并移除全局 `DELETE /api/admin/business-user-groups/members`;调用方必须迁移到组作用域增量接口。
- 用户组列表与详情返回实时成员数量;成员变化继续实时影响店铺负责人所属组推导,不写店铺冗余字段。
## Capabilities
### New Capabilities
无。
### Modified Capabilities
- `business-user-group`: 增加成员与候选查询、组作用域增量增删、成员计数及旧成员接口清理行为。
## Impact
- 影响业务用户组 Query、DTO、Application、Store、Handler、路由、OpenAPI、审计和前端调用契约。
- 复用现有成员关系表、账号行锁、唯一约束和逐账号审计;不新增数据库迁移或第三方依赖。

View File

@@ -0,0 +1,69 @@
## ADDED Requirements
### Requirement: 业务用户组成员与候选用户可分页查询
系统 SHALL 提供 `GET /api/admin/business-user-groups/{id}/members`,使超级管理员和平台用户分页查看指定业务用户组当前保留的全部成员关系。成员列表 MUST 支持用户名或手机号关键字和账号启用状态筛选,并返回账号标识、用户名、手机号、启用状态、已删除标记、成员关系创建时间以及当前组标识、编码、名称和启停状态。平台账号禁用或软删除后,其成员关系 SHALL 保留并继续显示;软删除账号的查询不得恢复账号或改变店铺推导。
系统 SHALL 提供 `GET /api/admin/business-user-groups/{id}/member-candidates`,分页返回全部启用且未删除的平台用户及其当前业务用户组。候选列表 MUST 支持用户名或手机号关键字,以及全部、未分组、当前组、其他组四类归属筛选;每项 MUST 返回当前组标识、编码、名称、启停状态和稳定操作提示:未分组为 `add`、已在目标组为 `already_member`、属于其他组为 `move`。目标组停用时仍可查询成员与候选,但候选 MUST 标记不可添加。
业务用户组列表与详情 SHALL 返回当前保留成员关系的实时数量。查询与计数 MUST 批量完成,列表查询次数不得随当页用户组或成员数量线性增长。上述入口仅限超级管理员和平台用户,代理与企业账号 MUST 被拒绝。
#### Scenario: 查看启用、禁用和已删除成员
- **GIVEN** 某用户组包含启用账号、禁用账号和已软删除账号的保留成员关系
- **WHEN** 授权账号查询组内成员
- **THEN** 系统返回三类成员并分别标记账号状态和已删除状态,不修改成员关系
#### Scenario: 候选列表展示当前所属组
- **GIVEN** 候选平台用户中分别存在未分组、当前组和其他组成员
- **WHEN** 授权账号查询目标组成员候选
- **THEN** 系统分别返回 `add``already_member``move` 提示及当前所属组信息
#### Scenario: 停用组仍可维护退出
- **WHEN** 授权账号查询已停用用户组的成员和候选
- **THEN** 系统仍返回查询结果,候选标记不可添加,但已有成员可通过组作用域移除接口退出该组
#### Scenario: 用户组列表返回实时成员数
- **WHEN** 成员迁入或移出用户组后查询用户组列表或详情
- **THEN** 系统立即返回新的成员数量,无需更新用户组或店铺记录
### Requirement: 业务用户组成员按组作用域增量增加与移除
系统 SHALL 提供 `POST /api/admin/business-user-groups/{id}/members` 增量增加成员。目标组 MUST 存在、未删除且启用;请求账号去重后 MUST 非空,且全部账号必须为启用、未删除的平台用户,任一账号无效时整批不修改。未分组账号 SHALL 新增归属,其他组账号 SHALL 直接迁入目标组,已在目标组账号 SHALL 幂等保持;请求外的目标组成员 MUST 不受影响。响应 MUST 分别返回请求数、新增数、迁移数和未变化数。
系统 SHALL 提供 `DELETE /api/admin/business-user-groups/{id}/members` 增量移除成员。目标组 MUST 存在且未删除,启用或停用均可操作;所有请求账号必须在操作开始时属于目标组,任一账号无归属或属于其他组时整批拒绝。移除 MUST 仅软删除目标组内对应成员关系,不得清除其他组归属;禁用或已软删除平台账号的保留成员关系 MUST 可被移除。重复移除已不属于目标组的账号 MUST 返回状态冲突,不得静默成功。
增加和移除 MUST 在单一事务内按账号标识稳定顺序串行处理,并为每个实际变化账号记录原组、目标组、操作者和时间审计;已在目标组的幂等账号不得写虚假变更审计。成员变化后,账号负责店铺的所属业务用户组和业务线 MUST 按既有实时推导立即变化,不得写入店铺冗余组字段。
#### Scenario: 增量增加不覆盖其他成员
- **GIVEN** 目标组已有多名成员
- **WHEN** 管理员提交一组新的账号加入目标组
- **THEN** 系统只处理请求账号,原有未被请求的成员保持不变
#### Scenario: 其他组成员直接迁入
- **WHEN** 管理员把属于另一启用或停用组的平台用户增加到启用目标组
- **THEN** 系统将该账号唯一归属迁入目标组,原组成员数减少、目标组成员数增加,并记录前后组审计
#### Scenario: 当前组成员重复增加
- **WHEN** 请求包含已经属于目标组的账号
- **THEN** 系统保持其唯一成员关系并计入未变化数,不新增重复关系或虚假变更审计
#### Scenario: 增加请求含无效账号整批回滚
- **WHEN** 增加请求中任一账号不是启用且未删除的平台用户
- **THEN** 系统拒绝整批请求,其他有效账号的归属也不变化
#### Scenario: 只移除当前组成员
- **WHEN** 管理员从目标组移除一组当前成员
- **THEN** 系统只软删除这些账号在目标组的关系,目标组其他成员和其他组关系不受影响
#### Scenario: 移除请求含其他组成员整批拒绝
- **WHEN** 移除请求中任一账号不属于目标组或已无分组
- **THEN** 系统返回状态冲突且不移除任何请求账号
#### Scenario: 清理禁用或已删除账号关系
- **WHEN** 管理员从目标组移除已禁用或已软删除平台账号的保留成员关系
- **THEN** 系统允许移除并保留账号本身的禁用或删除状态不变
### Requirement: 业务用户组成员接口干净切换
系统 MUST 以 `POST /api/admin/business-user-groups/{id}/members` 取代原 `PUT` 成员设置入口,并 MUST 移除全局 `DELETE /api/admin/business-user-groups/members`。系统 MUST NOT 保留旧方法或路径别名;调用方必须使用组作用域的增加与移除接口,避免无目标组约束的清空操作。
#### Scenario: 旧成员维护入口不可达
- **WHEN** 调用方请求原 `PUT /api/admin/business-user-groups/{id}/members` 或全局 `DELETE /api/admin/business-user-groups/members`
- **THEN** 路由不提供旧成员维护能力,调用方必须迁移到新的 POST 或组作用域 DELETE 入口

View File

@@ -0,0 +1,35 @@
## 1. 成员与候选查询
- [x] 1.1 新增成员列表、候选列表、分页筛选与响应 DTO包含账号状态、删除标记、关系时间、当前组信息、操作提示和可添加标记。
- [x] 1.2 在业务用户组 Query 中实现当前成员分页:以成员关系为主表,`Unscoped` 批量读取启用、禁用和软删除平台账号,支持用户名/手机号与账号状态筛选。
- [x] 1.3 实现成员候选分页:仅启用未删除平台用户,批量关联当前成员关系和用户组,支持全部、未分组、当前组、其他组筛选并计算 `add``already_member``move`
- [x] 1.4 为用户组列表和详情批量投影实时成员数,计数包含禁用或已删除账号的未删除成员关系,避免逐组查询。
## 2. 增量增加成员
- [x] 2.1 将成员增加请求与结果 DTO 调整为增量语义,响应返回请求数、新增数、迁移数、未变化数和账号集合。
- [x] 2.2 实现 `POST /business-user-groups/{id}/members` 用例:要求目标组启用且全部账号为启用未删除平台用户,按账号 ID 升序锁定并读取锁后归属。
- [x] 2.3 未分组账号新增关系、其他组账号更新到目标组、当前组账号幂等不写;请求外成员不修改,任一无效账号整批回滚。
- [x] 2.4 只为实际新增和迁移账号记录逐账号审计,审计包含原组与目标组;幂等未变化账号不写虚假变更事件。
## 3. 增量移除成员
- [x] 3.1 新增组作用域移除请求与结果 DTO允许目标组启用或停用并支持禁用或软删除账号关系清理。
- [x] 3.2 实现 `DELETE /business-user-groups/{id}/members` 用例:按账号 ID 升序锁账号,要求全部请求账号锁后仍属于目标组,任一不匹配整批返回状态冲突。
- [x] 3.3 Store 新增带目标组和账号集合谓词的软删除操作,要求影响行数等于请求账号数,禁止清除其他组归属。
- [x] 3.4 为每个实际移除账号记录原组、空目标组与操作者审计,账号禁用或删除状态保持不变。
## 4. 路由干净切换与装配
- [x] 4.1 新增 `GET /business-user-groups/{id}/members``GET /business-user-groups/{id}/member-candidates` Handler 和 RouteSpec仅放行超级管理员与平台用户。
- [x] 4.2 将现有 `PUT /business-user-groups/{id}/members` 改为 POST并新增同路径 DELETE删除全局 `DELETE /business-user-groups/members`、旧 Handler 方法及专用 DTO不保留别名。
- [x] 4.3 同步 `internal/bootstrap``cmd/api/docs.go``cmd/gendocs/main.go` 与 OpenAPI 描述,明确停用组只可移除、其他组成员增加时直接迁入。
## 5. 验证与规格同步
- [ ] 5.1 在维护者指定测试环境验证成员分页、关键字、启停与软删除展示,候选归属筛选和三类操作提示。(阻塞:未提供维护者指定测试环境、账号和数据集)
- [ ] 5.2 验证未分组新增、跨启用/停用组迁移、当前组幂等、无效账号整批回滚及请求外成员不变。(阻塞:未提供维护者指定测试环境、账号和数据集)
- [ ] 5.3 验证启用/停用组移除、禁用/删除账号关系清理、其他组或无归属账号导致整批拒绝、重复移除冲突。(阻塞:未提供维护者指定测试环境、账号和数据集)
- [ ] 5.4 验证并发迁移与移除同一账号的锁后裁决、逐账号审计、成员数实时变化和店铺所属组实时推导。(阻塞:未提供维护者指定测试环境、账号和数据集)
- [x] 5.5 验证旧 PUT 与全局 DELETE 路由不可达,新 GET/POST/DELETE 路由和生成文档一致;运行 `gofmt -w``go build ./cmd/api ./cmd/worker``go run cmd/gendocs/main.go`
- [x] 5.6 同步主 Spec 可达操作索引和证据矩阵,运行 `openspec validate add-business-user-group-member-management --strict``openspec doctor --json``openspec validate --all``./scripts/context-health.sh`

View File

@@ -72,6 +72,48 @@
- **WHEN** 某行操作类型为换绑但业务员登录账号为空,或操作类型为清空但填写了业务员登录账号
- **THEN** 该行失败并保留店铺原负责人,其他行照常处理
### Requirement: 业务用户组成员与候选用户可分页查询
系统 SHALL 提供组内成员和成员候选分页查询。成员查询保留并展示启用、禁用及已软删除账号的未删除关系,支持用户名/手机号及账号状态筛选;候选查询仅返回启用且未删除的平台用户,支持全部、未分组、当前组和其他组筛选,并返回 `add``already_member``move` 操作提示。停用组仍可查询,候选统一标记不可添加;用户组列表和详情返回批量投影的实时保留成员数。
#### Scenario: 成员查询保留历史账号关系
- **WHEN** 授权账号查询包含启用、禁用和已删除账号关系的用户组
- **THEN** 系统分页返回三类成员并标记账号状态与删除状态,不修改关系
#### Scenario: 候选归属提示
- **WHEN** 授权账号查询目标组候选并分别存在未分组、当前组和其他组平台用户
- **THEN** 系统返回 `add``already_member``move` 提示及当前组信息
#### Scenario: 用户组成员数实时变化
- **WHEN** 成员迁入或移出后查询用户组列表或详情
- **THEN** 系统返回新的保留成员关系数量,无需更新组或店铺记录
#### Scenario: 增量增加与迁移
- **WHEN** 管理员向启用目标组提交启用未删除平台用户
- **THEN** 未分组账号新增、其他组账号迁入、当前组账号保持不变,任一无效账号整批回滚
#### Scenario: 组作用域增量移除
- **WHEN** 管理员从启用或停用目标组移除当前成员
- **THEN** 系统仅软删除目标组关系并记录逐账号审计,任一不匹配账号整批冲突
#### Scenario: 旧成员入口不可达
- **WHEN** 调用方请求旧 PUT 成员入口或全局 DELETE 成员入口
- **THEN** 系统不提供旧成员维护能力,调用方必须使用组作用域 POST/DELETE 入口
### Requirement: 业务用户组成员按组作用域增量增加与移除
系统 SHALL 以 `POST /api/admin/business-user-groups/{id}/members` 增量增加成员:目标组必须启用,账号必须是启用且未删除的平台用户;未分组账号新增、其他组账号迁入、当前组账号幂等,任一无效账号整批回滚。系统 SHALL 以 `DELETE /api/admin/business-user-groups/{id}/members` 增量移除成员:目标组启用或停用均可操作,所有账号必须锁后仍属于目标组;仅软删除目标组关系,禁用或已删除账号关系可清理,任一不匹配整批冲突。成员变化须逐账号记录原组、目标组、操作者和时间审计,并继续实时推导店铺所属组。
#### Scenario: 增量增加与迁移
- **WHEN** 管理员向启用目标组提交启用未删除平台用户
- **THEN** 未分组账号新增、其他组账号迁入、当前组账号保持不变,任一无效账号整批回滚
#### Scenario: 组作用域增量移除
- **WHEN** 管理员从启用或停用目标组移除当前成员
- **THEN** 系统仅软删除目标组关系并记录逐账号审计,任一不匹配账号整批冲突
### Requirement: 业务用户组成员接口干净切换
系统 MUST 不再提供旧成员维护入口和全局成员清空入口,不保留兼容别名;调用方必须使用组作用域的 POST/DELETE 接口。
#### Scenario: 旧成员入口不可达
- **WHEN** 调用方请求旧 PUT 成员入口或全局 DELETE 成员入口
- **THEN** 系统不提供旧成员维护能力,调用方必须使用组作用域 POST/DELETE 入口
## 可达操作索引
本节只用于入口导航,不是行为 Requirement业务义务以上述 Requirements 为准。
@@ -82,7 +124,7 @@
### 业务用户组成员归属
`PUT /api/admin/business-user-groups/{id}/members`(批量设置平台用户业务用户组归属`DELETE /api/admin/business-user-groups/members`批量清空平台用户业务用户组归属)
`GET /api/admin/business-user-groups/{id}/members`(分页查询目标组成员,包含禁用及已删除账号关系);`GET /api/admin/business-user-groups/{id}/member-candidates`(分页查询启用平台用户候选及当前归属);`POST /api/admin/business-user-groups/{id}/members`(增量增加成员,其他组成员直接迁入`DELETE /api/admin/business-user-groups/{id}/members`按目标组作用域增量移除成员,启用或停用组均可操作)。旧 PUT 成员入口与全局 DELETE 成员入口不再提供
### 店铺负责人批量交接