feat(业务用户组): AUG26-003 业务用户组与店铺负责人分组导入
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 1h43m42s
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 1h43m42s
- 迁移 000221:新增 tb_business_user_group、tb_business_user_group_member、tb_shop_business_owner_import_task,成员一账号一行由部分唯一索引保证,店铺所属组按当前负责人实时推导,不回填历史分组。 - 用户组 CRUD、成员改组/清空归属、店铺批量交接(原子失败不部分写入)。 - 店铺负责人 CSV 导入任务:逐行独立事务、逐行明细、任务级与行级失败分离。 - 读侧推导与筛选:未分组、业务线、停用组可筛出并带停用标记。 - 补齐操作审计动作与资源、openapi 清单、发布门禁巡检表清单。 - 归档 add-shop-salesperson-groups 变更并同步 openspec/specs/business-user-group,补齐 AUG26-003 验证证据链。
This commit is contained in:
@@ -1,42 +0,0 @@
|
||||
## Context
|
||||
|
||||
现有店铺已存在负责人候选和数据范围能力;用户组是新的业务分类,不能复用 RBAC 角色或代理店铺层级。推导关系必须保持实时,避免负责人改组后大量回写店铺造成不一致。
|
||||
|
||||
## Decisions
|
||||
|
||||
- 新增业务用户组表和平台用户—组关联(用户唯一)表;组编码唯一且不可改,停用不删除既有成员关联。
|
||||
- 店铺不保存组 ID。列表/详情以店铺负责人关联平台用户,再左连接用户组得到组及停用状态;按组筛选同样使用该关系。
|
||||
- 勾选批量交接采用一次事务:先按操作者数据范围锁定/校验全量店铺及目标用户,再统一更新和写审计。任何校验失败不写入。
|
||||
- Excel 使用既有异步导入模式逐行事务;每行在数据范围内查询,统一拒绝文案不区分无权与不存在,并持久化任务明细。
|
||||
- 用户组成员批量设置直接替换关联;不引入组管理员、层级、额外权限或数据范围计算。
|
||||
|
||||
## 管理动作契约
|
||||
|
||||
### 用户组及成员
|
||||
|
||||
- `POST /business-user-groups`:超级管理员、平台用户提交 `code`(1~64 字符,未删除组内唯一)、`name`(1~100 字符)、`sort`(非负整数)、`enabled`、`remark`(最多 500 字符)。成功返回组 ID 与字段;重复编码返回“业务用户组编码已存在”。
|
||||
- `PUT /business-user-groups/:id`:允许更新名称、排序、启停、备注;`code` 永不允许修改。不存在/已删除返回既有资源不存在。
|
||||
- `DELETE /business-user-groups/:id`:请求须带二次确认;存在成员时返回“用户组仍有成员,只能停用或先移走成员”,不物理删除。
|
||||
- `PUT /business-user-groups/:id/members`:请求 `account_ids` 非空数组;所有账号必须是启用平台用户且目标组启用。事务内替换每个账号旧组关系,任一账号无效则全量回滚。`DELETE /business-user-groups/members` 使用同一校验清空指定账号归属。成功操作写成员前后审计。
|
||||
|
||||
### 店铺负责人批量交接
|
||||
|
||||
- `PUT /shops/business-owner/batch`:请求 `shop_ids`(非空、去重)及 `business_owner_account_id`(有效平台业务员)或显式 `null`(清空)。先按操作者数据范围锁定并校验所有店铺,再统一更新 `tb_shop.business_owner_account_id` 并逐店写审计;任何目标无权、不存在、已删除或负责人无效时,返回统一失败且整批无写入。
|
||||
- Excel 导入使用既有异步导入任务;每行提供店铺标识及负责人账号标识或清空标识。每行独立授权、存在性、负责人有效性校验和事务更新;结果保存行号、成功/失败、失败原因、变更前后负责人及汇总。无权和不存在对调用方使用同一错误文案。
|
||||
|
||||
### 读侧投影
|
||||
|
||||
- 扩展既有店铺列表、详情、筛选与导出:返回 `business_owner_account_id`、负责人名称、`business_user_group_id`、组编码、组名称、组启用状态;组字段从当前负责人—成员关系实时左连接。
|
||||
- 组筛选只匹配当前负责人所属组;负责人为空或无成员关系时归入“未分组”。历史店铺不回填;负责人改组/停用后下一次读立即反映变化。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- 实时 join 增加列表复杂度 → 为负责人和成员关联建立查询索引,不以冗余字段换一致性风险。
|
||||
- 负责人/分组并发更新 → 店铺交接和用户改组均使用事务与受影响行检查;读取接受当前已提交快照。
|
||||
- 导入部分成功 → 明确为逐行语义,任务明细是唯一结果来源。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 新增成对迁移创建用户组、成员关联和导入/查询索引,不回填历史组归属。
|
||||
2. 部署读侧空组兼容,再启用维护、批量和导入入口。
|
||||
3. 隔离库验证成员唯一、停用保留、实时推导、批量原子失败、导入逐行结果及迁移 up/down/up。
|
||||
@@ -1,25 +0,0 @@
|
||||
## Why
|
||||
|
||||
店铺负责人只能逐店维护,平台用户也没有稳定的业务分类;店铺按组统计、筛选和批量交接缺少统一、可追溯口径。
|
||||
|
||||
本 Change 落实 AUG26-003:业务用户组只描述平台用户的业务分类,不改变角色权限、数据范围或代理店铺分组;店铺所属组始终由当前负责人实时推导。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增业务用户组(名称、不可变唯一编码、排序、启停、备注)及平台用户单组归属。
|
||||
- 新增店铺负责人批量设置/清空和 Excel 导入;勾选操作全量校验且原子,导入逐行独立执行并返回明细。
|
||||
- 店铺列表、详情和筛选显示实时推导的负责人业务用户组;停用组保留成员和展示,不影响登录、权限、数据范围或负责人。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `business-user-group`: 用户组生命周期、成员归属、店铺负责人交接及推导查询。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- 无。现有身份权限和数据范围拒绝行为保持不变。
|
||||
|
||||
## Impact
|
||||
|
||||
影响平台用户、店铺列表/详情、导入任务、数据范围校验、审计、DTO/OpenAPI 和新增 Schema。
|
||||
@@ -1,38 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
以不改变既有角色和数据范围的方式标记平台用户业务分类,并将店铺负责人和业务用户组的批量维护、推导展示与审计定义为一致的可观察行为。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 业务用户组生命周期与成员归属
|
||||
系统 SHALL 允许超级管理员和平台用户维护业务用户组的名称、创建时必填且在未删除组内唯一的稳定编码、排序、启用状态和备注;编码创建后 MUST NOT 修改。用户组不得设置上级、层级或组管理员。仅无成员用户组可由超级管理员或平台用户二次确认删除;有成员时只能停用或先移走成员。
|
||||
|
||||
每个启用平台用户最多属于一个启用业务用户组。超级管理员和平台用户可选择多个平台用户,批量设置至一个启用组或批量清空归属;设置直接替换原归属,停用组不得作为目标。业务用户组 MUST NOT 改变后台角色、登录、权限、数据范围或店铺具体负责人归属。停用后不得新增成员,已有成员关系保留并显示已停用,管理员仍可将成员改组或清空。
|
||||
|
||||
#### Scenario: 批量替换平台用户分组
|
||||
- **WHEN** 管理员选择多个启用平台用户并指定一个启用业务用户组
|
||||
- **THEN** 系统将每个目标用户的原分组直接替换为目标组,不改变其角色、数据范围和登录状态
|
||||
|
||||
#### Scenario: 停用含成员用户组
|
||||
- **WHEN** 管理员停用仍含平台用户成员的业务用户组
|
||||
- **THEN** 系统保留成员关系并标记组已停用,拒绝新增成员但允许后续改组或清空成员
|
||||
|
||||
### Requirement: 店铺负责人和所属组实时推导
|
||||
店铺 SHALL 以当前绑定的平台业务员作为负责人。店铺所属业务用户组 MUST 实时由该负责人的当前用户组推导,不得把组 ID 冗余写入店铺;负责人变更、负责人改组或组停用后,店铺列表、详情和筛选的结果立即按新关系变化。店铺无负责人、负责人无分组时所属组为空;负责人所属组停用时仍返回该组并明确其已停用。
|
||||
|
||||
#### Scenario: 负责人改组改变店铺展示
|
||||
- **WHEN** 某平台业务员的业务用户组被替换或清空
|
||||
- **THEN** 该业务员当前负责的所有店铺在列表、详情和按组筛选中即时呈现新的组或空组,无需更新店铺记录
|
||||
|
||||
### Requirement: 店铺负责人批量交接与导入
|
||||
超级管理员和平台用户 SHALL 仅在其店铺数据权限内勾选多家店铺,批量设置为一个有效平台业务员或批量清空负责人。勾选批量操作 MUST 在提交前校验全部目标店铺均存在、未删除且可管理,并校验目标业务员有效;任一项失败时整批不修改并返回统一失败结果。成功时必须为每家店铺记录负责人前后值、操作者、时间和入口审计。
|
||||
|
||||
Excel 导入 MUST 按每行店铺标识独立校验和执行:有效行成功更新,无权限、店铺不存在/已删除或目标业务员无效行失败;任务返回成功数、失败数和逐行失败原因。导入不得因一行失败回滚其他已成功行,并必须记录每行实际变更审计。
|
||||
|
||||
#### Scenario: 勾选批量包含越权店铺
|
||||
- **WHEN** 管理员提交的店铺集合中任一店铺不在其数据范围、已删除或不存在
|
||||
- **THEN** 系统不修改集合中任何店铺负责人,并返回统一失败结果且不泄露越权店铺存在性
|
||||
|
||||
#### Scenario: 导入包含有效和无效行
|
||||
- **WHEN** 店铺负责人 Excel 导入同时包含可管理店铺和无权或无效店铺
|
||||
- **THEN** 系统更新每个有效行、保留失败行原值,并返回逐行结果及成功/失败汇总
|
||||
@@ -1,17 +0,0 @@
|
||||
## 1. 用户组与查询
|
||||
|
||||
- [ ] 1.1 追踪平台用户、店铺负责人、数据范围、现有批量导入和审计调用链,确认负责人字段及“有效平台业务员”的既有判定。
|
||||
- [ ] 1.2 新增成对迁移、模型和常量:业务用户组、平台用户唯一组关联、编码唯一/启停约束及负责人—成员推导查询索引。
|
||||
- [ ] 1.3 实现用户组 CRUD、启停、无成员二次确认删除、平台用户批量设置/清空及审计;禁止修改编码、向停用组新增成员和改变既有 RBAC。
|
||||
- [ ] 1.4 扩展店铺列表、详情与筛选 Query/DTO/OpenAPI,实时返回负责人组、编码和停用标识,不将组写入店铺。
|
||||
|
||||
## 2. 店铺负责人批量维护
|
||||
|
||||
- [ ] 2.1 实现勾选店铺批量设置/清空:先以操作者数据范围校验所有店铺和目标业务员,在单事务中更新全部店铺并记录逐店审计;任一项失败整批不改。
|
||||
- [ ] 2.2 接入既有异步 Excel 导入任务,逐行校验店铺标识、数据范围和目标业务员,保存成功/失败明细、汇总和实际审计;越权使用统一失败文案。
|
||||
- [ ] 2.3 注册后台路由和权限,更新 `cmd/api/docs.go` 与 `cmd/gendocs/main.go`;Handler 使用全局错误处理和 `pkg/response`。
|
||||
|
||||
## 3. 验证
|
||||
|
||||
- [ ] 3.1 在隔离数据库验证迁移 up/down/up、编码/成员唯一、组停用、负责人改组实时展示、批量原子失败、导入混合结果和数据范围拒绝。
|
||||
- [ ] 3.2 运行 `gofmt -w`(变更 Go 文件)、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-shop-salesperson-groups --strict`、`openspec doctor --json` 和 `./scripts/context-health.sh`;自动化测试按项目决策为 N/A。
|
||||
@@ -0,0 +1,125 @@
|
||||
## Context
|
||||
|
||||
- 店铺已存在负责人字段 `tb_shop.business_owner_account_id`(`migrations/000169_add_shop_business_owner.up.sql`:单列普通索引、无外键、不回填历史)。店铺列表/详情/候选读取收口在 `internal/query/shop/business_owner.go`,**不使用 JOIN**:负责人摘要由 `loadBusinessOwners` 二次批量查询后在 Go 中装配,`List` 在同一查询上先 `Count` 再 `Find`。
|
||||
- 数据范围由 `pkg/middleware/data_scope.go` 的 `ApplyShopIDFilter` 承担;`SubordinateShopIDs` 只对代理账号预计算,超管与平台用户恒为不受限 —— 因此「行级无权」在超管/平台入口下不可达。
|
||||
- 单店更新已显式拒绝代理设置店铺业务员(`internal/application/shop/update.go`)。
|
||||
- 仓库**不存在统一导入任务框架**:既有四个导入场景(IoT 卡、设备、订单套餐失效、资产套餐批量订购)各自独立成表、成 store、成 task type、成队列;集中的只有 `QueueForTaskType` 与 worker 注册表两处。设备导入表的启动补偿按表扫描并以设备任务类型重新入队,因此不能复用其表。
|
||||
- 已有可复用先例:`encoding/csv` 解析、UTF-8 BOM 剥离(`internal/task/device_batch_allocation.go`、`internal/task/asset_package_batch_order.go`)、异步导入任务状态机、jsonb 行明细、按批进度更新、启动补偿、逐行事务(`internal/task/device_import.go`)。
|
||||
- 仓库唯一 GBK 转换能力位于支付集成包 `pkg/fuiou`(导出函数 `GBKToUTF8`);`pkg/utils` 是既有的中立工具包。
|
||||
- 仓库**不存在店铺导出场景**:`internal/exporter` 的注册表与场景白名单、导出 DTO 场景枚举、店铺路由段均无 shop。
|
||||
- 用户组是新的业务分类,不能复用 RBAC 角色或代理店铺层级;推导必须实时,避免负责人改组后回写店铺造成不一致。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- 用户组只描述平台用户业务分类,不进入鉴权、数据范围或登录链路。
|
||||
- 店铺所属组与业务线始终由当前负责人实时推导,店铺不落组字段。
|
||||
- 勾选批量全成或全不成;CSV 导入逐行独立、成功行提交、失败行保留原值。
|
||||
- 推导、批量与导入复用既有审计、数据范围与统一错误响应口径。
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- 店铺导出场景;导入模板下载端点与服务端模板资源。
|
||||
- 用户组层级、上级组、组管理员。
|
||||
- 改动既有 RBAC、登录、数据范围与店铺具体负责人归属。
|
||||
- 重构既有 5 处内联的「有效平台业务员」判定。
|
||||
- 复用 `openspec/specs/shop-bulk-import` 维护者手工执行的离线 SQL 生成器。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 数据模型
|
||||
|
||||
- **用户组表**:`code`(1~64 字符,创建时必填,未删除组内唯一,创建后不可修改)、`name`(1~100 字符)、`business_line`(可选单值字符串,取值 `standard` 标品 / `smart` 智能产品 / `other` 其他,允为空)、`sort`(非负整数)、`status`(0 禁用 / 1 启用,遵循 ENG-STATE-001)、`remark`(最多 500 字符)。不设上级、层级与组管理员。
|
||||
- **成员表**:一账号一行,`account_id` 唯一。
|
||||
- **三处索引**:用户组 `UNIQUE(code) WHERE deleted_at IS NULL`;成员 `UNIQUE(account_id) WHERE deleted_at IS NULL`(结构性保证「一个账号至多一个组」);成员 `(business_user_group_id) WHERE deleted_at IS NULL`。**不给 `business_line` 建索引**:它是 3 值枚举且用户组表体量小,存在性子查询按 `account_id` 唯一索引定位成员后回表,额外索引无收益。
|
||||
- 账号改组走 **UPDATE 组 ID**,不做软删加新增,避免唯一索引与垃圾行。账号被软删时成员关系保留并继续参与推导(店铺仍展示已软删负责人,口径一致),账号删除不级联清成员。
|
||||
- 全部关联只保存 ID,不新增 GORM 关联标签或数据库外键(ENG-MODEL-001)。
|
||||
|
||||
### 读侧推导
|
||||
|
||||
- 店铺**不落**组字段。列表/详情继续沿用「Go 批量装配」:在既有 `loadBusinessOwners` 之上追加一次按 `account_id IN (...)` 的成员批量查询与一次用户组批量查询,无 N+1。
|
||||
- 返回字段:`business_user_group_id`、组编码、组名称、组启用状态、组业务线。
|
||||
- **筛选必须用 `EXISTS` 子查询,不得改用 JOIN**:`List` 在同一查询上先 `Count` 再 `Find`,JOIN 会使计数行膨胀并引入列歧义。备选方案 JOIN + `DISTINCT` 计数被否决(计数语义脆弱、易误改)。
|
||||
- 未分组口径:`business_owner_account_id IS NULL OR NOT EXISTS(该负责人的未删除成员关系)`。**停用组的负责人不计入未分组**,停用组仍需可被筛出并携带已停用标记。
|
||||
- 不改动 `Count`/`Find` 的既有分页与排序契约;`idx_shop_business_owner_account_id` 继续支撑关联与批量更新。
|
||||
|
||||
### 用户组成员维护
|
||||
|
||||
- 批量设置/清空成员的请求为账号 ID 数组;每个账号必须是启用平台用户,目标组必须是启用组。单事务内替换每个账号的原组关系,任一账号无效则全量回滚。停用组不得作为目标,且不得新增成员;停用后成员可改组或清空。
|
||||
- 采用「角色权限批量」先例:全量预读 + 预校验 + 单事务 + 同事务审计 + 业务回滚后独立短事务失败审计。备选「逐条尽力而为」被否决(与 PRD「设置会直接替换原所属组」的确定性语义不符)。
|
||||
|
||||
### 勾选批量交接
|
||||
|
||||
- 请求携带店铺 ID 集合与目标业务员 ID。清空表达沿用既有「可空字段 + 是否出现标志」模式(与单店更新的请求契约一致),不引入操作类型字符串。
|
||||
- 单事务:`ApplyShopIDFilter` + `id IN (...) FOR UPDATE` 锁定 → 命中数不等于请求数即失败 → 校验目标业务员为启用平台用户 → 统一更新 → 逐店写审计 → 检查受影响行数。任一项失败整批不写入。
|
||||
- 失败文案统一为「无权限操作该资源或资源不存在」,不区分无权、不存在与已删除。
|
||||
|
||||
### CSV 导入
|
||||
|
||||
- **格式固定为 CSV**,使用标准库 `encoding/csv` 流式解析;不引入 Excel 解析或生成能力,也不改动既有 `pkg/utils/excel.go`。
|
||||
- **模板由前端提供**。后端不提供模板下载端点、模板响应 DTO 或静态模板资源;固定列序、操作类型取值与编码要求只写入导入接口的 OpenAPI description。
|
||||
- 独立成表、独立 store、独立 task type 与队列,不复用设备导入任务表(其启动补偿按表绑定设备任务类型)。
|
||||
- 每行独立事务:成功行提交,失败行不写 `tb_shop` 并保留原值;任何一行失败不回滚其他已成功行。
|
||||
- 按批更新进度计数(进度写失败不回滚已提交行);任务收尾一次事务写汇总与行明细。
|
||||
|
||||
### 编码与解析
|
||||
|
||||
- 先剥离 UTF-8 BOM(沿用 `bytes.TrimPrefix(data, []byte{0xEF, 0xBB, 0xBF})` 既有做法)。
|
||||
- 剥离后若字节不是合法 UTF-8,则按既有 GBK 转换能力尝试解码;解码仍失败按**任务级失败**处理并给出明确原因。
|
||||
- 解码实现落在中立工具包(`pkg/utils`),使用仓库已依赖的 `golang.org/x/text/encoding/simplifiedchinese`。备选「直接调用 `pkg/fuiou.GBKToUTF8`」被否决:会让任务层依赖支付集成包,层级不成立;备选「改造 `pkg/fuiou` 抽出共享工具」被否决:属于需求未触碰模块的重构。
|
||||
- 表头必须与固定列序完全一致;不一致即**任务级失败**并给出明确原因,不进入逐行阶段。
|
||||
- 行号从**数据首行**起计(表头不计入),写入行明细。
|
||||
- **不设行数硬上限**,不新增体积常量:体积沿用既有上传与下载链路的校验,不复制设备/资产场景的 1000 行与 10MB 场景常量。
|
||||
- 任务级失败(文件不可下载、格式或表头不符、编码无法解码、无数据行)与行级失败(业务校验不通过)分开记录,任务级失败不产生行明细。
|
||||
|
||||
### 权限与审计
|
||||
|
||||
- 用户组维护、成员维护、勾选批量与 CSV 导入入口一律限超级管理员与平台用户(与既有导入入口及「代理不得设置店铺业务员」一致)。代理与企业返回 403。
|
||||
- 由于超管/平台数据范围恒为不受限,「行级无权」在现入口下不可达;「无权限与不存在使用同一文案、不泄露存在性」作为勾选批量的强制原则保留,导入侧失败原因枚举按实际可达收敛。
|
||||
- 新增一处共享的「有效平台业务员」谓词供导入与批量目标校验复用;**不重构**既有 5 处内联判定(`internal/query/shop/business_owner.go`、`internal/application/shop/create.go`、`internal/application/shop/update.go`、`internal/infrastructure/shop/recipient_resolver.go`、`internal/infrastructure/wallet/debit_event.go`)。
|
||||
- 审计:勾选批量用批次根事件(平台作用域,承载批次统计)+ 逐店子事件(店铺作用域);导入新增任务级动作码(创建 / 完成)与独立任务资源,逐行实际变更写店铺资源前后值审计(负责人前后值、操作者、时间、行备注)。审计写入与业务事实同事务,业务回滚后的失败/拒绝审计用独立短事务(ENG-TX-001)。
|
||||
|
||||
## 管理动作契约
|
||||
|
||||
### 用户组及成员
|
||||
|
||||
- `POST /business-user-groups`:提交 `code`、`name`、`business_line`(可选)、`sort`、`enabled`、`remark`。重复编码返回「业务用户组编码已存在」。
|
||||
- `GET /business-user-groups`、`GET /business-user-groups/:id`:返回组字段含业务线,供维护与筛选下拉使用。
|
||||
- `PUT /business-user-groups/:id`:允许更新名称、业务线、排序、启停、备注;`code` 永不允许修改。不存在或已删除返回既有资源不存在。
|
||||
- `DELETE /business-user-groups/:id`:须带二次确认;存在成员时返回「用户组仍有成员,只能停用或先移走成员」,不物理删除。
|
||||
- `PUT /business-user-groups/:id/members`:请求 `account_ids` 为非空数组;所有账号必须是启用平台用户且目标组启用。事务内替换每个账号原组关系,任一账号无效则全量回滚。`DELETE /business-user-groups/members` 使用同一校验清空指定账号归属。成功操作写成员前后审计。
|
||||
|
||||
### 店铺负责人批量交接
|
||||
|
||||
- `PUT /shops/business-owner/batch`:请求 `shop_ids`(非空、去重)及目标业务员 ID,或显式空值表示清空。先锁定并校验全部目标店铺,再统一更新 `tb_shop.business_owner_account_id` 并逐店写审计;任一目标无权、不存在、已删除或负责人无效时返回统一失败且整批无写入。
|
||||
|
||||
### 店铺负责人 CSV 导入
|
||||
|
||||
- `POST /shops/business-owner-imports`:请求携带上传后的 `file_key`。校验 `file_key` 位于 `shop-imports/` 前缀下且扩展名为 `.csv`(沿用既有导入服务的 file_key 前缀校验先例),创建待处理任务并入队。
|
||||
- `GET /shops/business-owner-imports`、`GET /shops/business-owner-imports/:id`:任务列表与详情,详情返回成功数、失败数、逐行失败原因与任务级错误原因。
|
||||
- **OpenAPI description 必须写明模板要求**:文件格式 CSV;固定列序「店铺编码、操作类型、业务员登录账号、备注」;操作类型取值「换绑」「清空」;换绑必须填业务员登录账号,清空不得填;备注可选并写入该行审计;编码 UTF-8 且可带 BOM,非 UTF-8 时按既有 GBK 转换能力尝试解码,仍失败则明确报错;店铺以店铺编码唯一定位,业务员以登录账号唯一定位。
|
||||
|
||||
### 读侧投影
|
||||
|
||||
- 扩展既有店铺列表、详情与筛选:返回 `business_owner_account_id`、负责人名称、业务用户组 ID、组编码、组名称、组启用状态与组业务线;均从当前负责人—成员关系实时推导。
|
||||
- 筛选支持按用户组、按用户组业务线与按未分组。组筛选只匹配当前负责人所属组;负责人为空或无成员关系时归入未分组;停用组可被筛出且带停用标记。历史店铺不回填,负责人改组或组停用后下一次读立即反映变化。**不含导出**。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- 实时推导增加读侧查询次数 → 以 `account_id` 唯一索引做批量装配,不引入冗余字段换一致性风险;明确不改成 JOIN 以免破坏既有 `Count` 语义。
|
||||
- 「有效平台业务员」判定将有第 6 处实现 → 新增共享谓词并只在新链路使用;既有 5 处保留为 As-Is,避免顺手重构。
|
||||
- 逐行事务遇大文件时任务时长与数据库往返增加 → 明确不设行数上限、按批更新进度;不引入批量合并事务(会破坏「一行失败不影响其他行」的语义)。
|
||||
- GBK 解码是启发式(GBK 解码器对多数字节对不报错)→ 不静默改写内容:仅在 UTF-8 校验失败时尝试解码,解码报错即任务级失败并给出原因。
|
||||
- 编排可产生重复投递 → worker 依状态机幂等(待处理首跑,处理中被视为中断重跑,终态直接跳过),沿用既有导入场景语义。
|
||||
- 批量与导入并发 → 均以行锁 + 单事务保证原子性;读取接受当前已提交快照。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 新增成对迁移创建用户组表、成员表与三处索引,不回填历史组归属与历史分组快照;`down` 带守卫,存在数据时拒绝静默回滚。
|
||||
2. 先部署读侧空组兼容(新字段为空、新筛选不命中),再启用用户组维护、成员维护、勾选批量与 CSV 导入入口。
|
||||
3. 在 ENG-TEST-001 指定的唯一测试面验证:迁移 up/down/up、编码唯一、账号唯一、停用组保留成员、负责人改组实时推导、未分组与业务线筛选、批量原子失败、导入混合结果与任务级/行级失败分离、无行数上限。
|
||||
|
||||
## Open Questions
|
||||
|
||||
无。GBK 解码落点、批量审计作用域、上传用途命名与入口角色范围已在本设计内定稿。
|
||||
@@ -0,0 +1,28 @@
|
||||
## Why
|
||||
|
||||
店铺负责人只能逐店维护,平台用户也没有稳定的业务分类;店铺按组统计、筛选和批量交接缺少统一、可追溯口径。
|
||||
|
||||
本 Change 落实 AUG26-003:业务用户组只描述平台用户的业务分类,不改变角色权限、数据范围或代理店铺分组;店铺所属组始终由当前负责人实时推导。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增业务用户组(名称、创建后不可修改的稳定编码、所属业务线、排序、启停、备注)及平台用户单组归属。
|
||||
- 新增店铺负责人勾选批量设置/清空和 CSV 导入:勾选操作全量预校验且整批原子;导入逐行独立执行并返回逐行明细。
|
||||
- 店铺列表、详情与筛选显示实时推导的负责人业务用户组及其业务线,并支持按用户组、按用户组业务线和未分组筛选;停用组保留成员与展示,不影响登录、权限、数据范围或负责人。
|
||||
- 新增 `shop_import` 上传用途。导入模板由前端提供,后端只在导入接口文档中描述固定列序与编码要求,不提供模板下载端点,不引入 Excel 解析或生成能力。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `business-user-group`: 用户组生命周期、成员归属、店铺负责人交接与导入及推导查询。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- 无。现有身份权限和数据范围拒绝行为保持不变。
|
||||
|
||||
## Impact
|
||||
|
||||
影响平台用户、店铺列表/详情与筛选、上传用途、导入任务、数据范围校验、审计、DTO/OpenAPI 和新增 Schema。
|
||||
|
||||
非目标:店铺导出场景(`internal/exporter` 无店铺场景,列为后续独立需求)、用户组层级与组管理员、`openspec/specs/shop-bulk-import` 维护者手工执行的离线 SQL 生成器。
|
||||
@@ -0,0 +1,71 @@
|
||||
## Purpose
|
||||
|
||||
以不改变既有角色和数据范围的方式标记平台用户业务分类,并将店铺负责人和业务用户组的批量维护、推导展示与审计定义为一致的可观察行为。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 业务用户组生命周期与成员归属
|
||||
系统 SHALL 允许超级管理员和平台用户维护业务用户组的名称、创建时必填且在未删除组内唯一的稳定编码、可选所属业务线、排序、启用状态和备注;编码创建后 MUST NOT 修改。所属业务线取值范围 MUST 为标品、智能产品、其他三项单值,且 MAY 为空。用户组不得设置上级、层级或组管理员。仅无成员用户组可由超级管理员或平台用户二次确认删除;有成员时只能停用或先移走成员。
|
||||
|
||||
每个启用平台用户最多属于一个启用业务用户组。超级管理员和平台用户可选择多个平台用户,批量设置至一个启用组或批量清空归属;设置直接替换原归属,停用组不得作为目标。业务用户组 MUST NOT 改变后台角色、登录、权限、数据范围或店铺具体负责人归属。停用后不得新增成员,已有成员关系保留并显示已停用,管理员仍可将成员改组或清空。平台账号被删除后其成员关系 MUST 保留并继续参与店铺推导。
|
||||
|
||||
#### Scenario: 批量替换平台用户分组
|
||||
- **WHEN** 管理员选择多个启用平台用户并指定一个启用业务用户组
|
||||
- **THEN** 系统将每个目标用户的原分组直接替换为目标组,不改变其角色、数据范围和登录状态,且每个用户至多保留一个分组
|
||||
|
||||
#### Scenario: 停用含成员用户组
|
||||
- **WHEN** 管理员停用仍含平台用户成员的业务用户组
|
||||
- **THEN** 系统保留成员关系并标记组已停用,拒绝新增成员和把该组作为批量目标,但允许后续改组或清空成员
|
||||
|
||||
#### Scenario: 维护用户组所属业务线
|
||||
- **WHEN** 管理员创建或更新业务用户组的所属业务线
|
||||
- **THEN** 系统按标品、智能产品、其他三项单值保存该字段,并允许清空为空值
|
||||
|
||||
### Requirement: 店铺负责人和所属组实时推导
|
||||
店铺 SHALL 以当前绑定的平台业务员作为负责人。店铺所属业务用户组及其业务线 MUST 实时由该负责人的当前用户组推导,不得把组 ID 或业务线冗余写入店铺;负责人变更、负责人改组或组停用后,店铺列表、详情和筛选的结果立即按新关系变化。店铺无负责人、负责人无分组时所属组为空;负责人所属组停用时仍返回该组并明确其已停用。
|
||||
|
||||
店铺列表与详情 SHALL 返回负责人账号标识、负责人名称、业务用户组标识、组编码、组名称、组启用状态与组业务线。店铺列表 SHALL 支持按业务用户组、按用户组业务线与按未分组筛选;未分组 MUST 包含无负责人与负责人无成员关系两种情形,停用组的负责人 MUST NOT 归入未分组,且停用组 MUST 可被筛出并携带已停用标记。
|
||||
|
||||
#### Scenario: 负责人改组改变店铺展示
|
||||
- **WHEN** 某平台业务员的业务用户组被替换或清空
|
||||
- **THEN** 该业务员当前负责的所有店铺在列表、详情和按组筛选中即时呈现新的组或空组,无需更新店铺记录
|
||||
|
||||
#### Scenario: 按未分组与业务线筛选店铺
|
||||
- **WHEN** 管理员按未分组或按某一用户组业务线筛选店铺列表
|
||||
- **THEN** 未分组结果包含无负责人与负责人无成员关系的店铺、且不含负责人属于停用组的店铺;业务线结果只包含负责人当前所属组业务线匹配的店铺
|
||||
|
||||
#### Scenario: 停用组仍可筛出
|
||||
- **WHEN** 某业务用户组被停用且其成员仍为部分店铺的负责人
|
||||
- **THEN** 系统仍按该组筛选出这些店铺,并在结果中标记该组已停用
|
||||
|
||||
### Requirement: 店铺负责人批量交接
|
||||
超级管理员和平台用户 SHALL 勾选多家店铺,批量设置为一个有效平台业务员或批量清空负责人。批量操作 MUST 在提交前校验全部目标店铺均存在、未删除且可管理,并校验目标业务员为启用平台用户;任一项失败时整批不修改并返回统一失败结果,且 MUST NOT 形成无权、不存在与已删除之间的可枚举差异。成功时必须为每家店铺记录负责人前后值、操作者、时间和入口审计,并同时保留批次汇总结果。
|
||||
|
||||
#### Scenario: 勾选批量包含越权或不存在店铺
|
||||
- **WHEN** 管理员提交的店铺集合中任一店铺不在其可管理范围、已删除或不存在
|
||||
- **THEN** 系统不修改集合中任何店铺负责人,并返回统一失败结果且不泄露该店铺存在性
|
||||
|
||||
#### Scenario: 勾选批量清空负责人
|
||||
- **WHEN** 管理员对多家店铺提交清空负责人的批量请求且全部校验通过
|
||||
- **THEN** 系统在同一事务内清空全部目标店铺负责人,为每家店铺记录负责人前后值审计,店铺所属组随之推导为空
|
||||
|
||||
### Requirement: 店铺负责人 CSV 导入
|
||||
超级管理员和平台用户 SHALL 通过 CSV 导入批量设置或清空店铺负责人;导入 MUST 异步执行并返回成功数、失败数与逐行明细。导入入口仅限超级管理员与平台用户,故导入失败原因不含数据范围无权。
|
||||
|
||||
导入文件 MUST 为 CSV,编码 MUST 为 UTF-8 且 MAY 带 BOM;非 UTF-8 时系统 SHALL 按既有 GBK 转换能力尝试解码,仍失败时按任务级失败并给出明确原因。文件首行表头 MUST 与固定列序「店铺编码、操作类型、业务员登录账号、备注」完全一致,不一致时为任务级失败。操作类型 MUST 为「换绑」或「清空」;换绑 MUST 填写业务员登录账号,清空 MUST NOT 填写业务员登录账号。备注可选,填写时 MUST 写入该行审计。店铺 MUST 以店铺编码唯一定位,业务员 MUST 以登录账号唯一定位。
|
||||
|
||||
每行 MUST 独立校验与执行:有效行成功更新,失败行 MUST 保留原值且不影响其他已成功行;结果 MUST 保存行号(自数据首行起计,表头不计入)、成功或失败状态与失败原因。任务级失败与行级失败 MUST 分开记录,任务级失败不产生行明细。导入 MUST NOT 设置行数硬上限。所有实际变更 MUST 记录店铺负责人前后值与行备注审计。
|
||||
|
||||
行级失败原因 MUST 限定为:店铺编码不存在或已删除、操作类型非法、换绑未填写业务员登录账号、清空却填写了业务员登录账号、业务员登录账号不存在或非启用平台用户、行格式错误。任务级失败原因 MUST 覆盖:文件格式或表头不符、编码无法解码、文件无数据行。
|
||||
|
||||
#### Scenario: 导入包含有效和无效行
|
||||
- **WHEN** 店铺负责人 CSV 导入同时包含可成功换绑或清空的行与校验失败的行
|
||||
- **THEN** 系统更新每个有效行、保留失败行原值,并返回逐行行号、失败原因及成功与失败汇总
|
||||
|
||||
#### Scenario: 表头或编码不符整批失败
|
||||
- **WHEN** 上传文件的表头与固定列序不一致,或文件既非合法 UTF-8 又无法按既有 GBK 转换能力解码
|
||||
- **THEN** 系统不处理任何行、不产生行明细,并返回明确的任务级失败原因
|
||||
|
||||
#### Scenario: 换绑与清空字段互斥
|
||||
- **WHEN** 某行操作类型为换绑但业务员登录账号为空,或操作类型为清空但填写了业务员登录账号
|
||||
- **THEN** 该行失败并保留店铺原负责人,其他行照常处理
|
||||
@@ -0,0 +1,30 @@
|
||||
## 1. 用户组与读侧推导
|
||||
|
||||
- [x] 1.1 追踪平台用户、店铺负责人、数据范围、既有批量操作、CSV 解析与 BOM/GBK 处理、审计调用链,确认负责人字段与「有效平台业务员」的既有判定分布。证据:侦查结论落在本 Change design.md;既有 6 处内联判定已定位(`internal/application/shop/create.go`、`update.go`、`internal/query/shop/business_owner.go`、`internal/infrastructure/shop/recipient_resolver.go`、`internal/infrastructure/wallet/debit_event.go`),按 As-Is 未重构。
|
||||
- [x] 1.2 新增成对迁移:业务用户组表(名称、稳定编码、所属业务线、排序、状态、备注)、平台用户—组关联表,以及编码唯一、账号唯一、组维度三处索引;`down` 带守卫,不回填历史组归属,不建外键。证据:`migrations/000221_add_business_user_group_and_shop_owner_import.{up,down}.sql`;隔离库 `./scripts/migrate.sh up`(221/u 332ms)→ `down`(221/d 252ms)→ `up` 全部成功;有数据时 `down` 被守卫拒绝(`tb_shop_business_owner_import_task 已存在导入任务事实,禁止删表回滚`)。
|
||||
- [x] 1.3 新增模型与常量:用户组、成员关联、业务线单值与启停常量、导入任务状态复用既有导入状态机。证据:`internal/model/business_user_group.go`、`internal/model/shop_business_owner_import_task.go`、`pkg/constants/business_user_group.go`、`pkg/constants/shop_business_owner_import.go`;状态机复用 `model.ImportTaskStatus*`。
|
||||
- [x] 1.4 实现用户组 CRUD、启停、无成员二次确认删除与所属业务线维护,写审计;禁止修改编码,不引入层级与组管理员。证据:`internal/application/businessusergroup/service.go`;实测创建成功、重复编码 400「业务用户组编码已存在」、停用成功、含成员删除 1050「用户组仍有成员,只能停用或先移走成员」。
|
||||
- [x] 1.5 实现平台用户批量设置/清空归属:启用平台用户与启用组校验、单事务替换原归属、停用组拒绝作为目标、同事务成员前后审计与业务回滚后的独立失败审计;新增共享「有效平台业务员」谓词供本 Change 复用,不重构既有内联判定。并发正确性以**账号行锁**(`tb_account` `FOR UPDATE` + `ORDER BY id ASC`)实现逐账号串行化,同时消除「清空时锁不到成员行」的幻读与「多账号相反顺序」的死锁;唯一索引冲突仍映射为稳定业务错误(幂等冲突兜底)。证据:`pkg/constants.IsAvailablePlatformBusinessOwner`;实测批量设置成功(group_id=1,账号 142/149)、含无效账号整批未修改(1001)、停用组拒绝(1050「目标用户组已停用,不能作为成员归属目标」)、清空归属成功(`DELETE /business-user-groups/members` → group_id=0 且账号回到未分组)。清空端点初次注册被 `DELETE /:id` 吞掉(返回「无效的路径ID」),已按「静态路径先于动态参数」修正 `internal/routes/business_user_group.go` 注册顺序并复测通过。
|
||||
- [x] 1.6 扩展店铺列表/详情 DTO 与 Query:实时返回负责人账号、负责人名称、组标识、组编码、组名称、组启用状态与组业务线;新增按用户组、按用户组业务线、按未分组三个筛选,使用存在性子查询而非 JOIN,不将组或业务线写入店铺。证据:`internal/query/shop/business_owner.go`(新增 `loadBusinessUserGroups` 批量装配 + 三个 EXISTS 条件,Count/Find 未改 JOIN);实测 `business_user_group_id=1` 命中 13 家且逐店返回组编码/名称/启用标记,`ungrouped=true` 命中 11 家且停用组负责人未计入未分组。
|
||||
|
||||
## 2. 店铺负责人批量交接
|
||||
|
||||
- [x] 2.1 实现勾选店铺批量设置/清空:全量预校验(存在性、未删除、可管理、目标业务员有效)+ 行锁 + 单事务更新 + 逐店前后值审计 + 批次根事件(平台作用域)与批次汇总;任一项失败整批不改,失败文案不区分无权与不存在。证据:`internal/application/shop/batch_business_owner.go` + `internal/infrastructure/audit/business_user_group.go`;实测成功批次返回 batch_key/shop_count、清空成功、含不存在店铺批次返回 1005 统一文案且 549 负责人保持 142(无部分写入)、缺失 `business_owner_account_id` 被拒绝。
|
||||
|
||||
## 3. 店铺负责人 CSV 导入
|
||||
|
||||
- [x] 3.1 搭建导入场景骨架:新任务表与模型(任务号、文件键、状态、成功/失败计数、行明细、任务级错误、操作者快照)、store、任务类型与队列常量、`QueueForTaskType` 分支与队列权重、运行时与 worker 的 store 装配、worker 处理器注册与启动补偿、发布门禁的未完成异步任务表清单。证据:`internal/store/postgres/shop_business_owner_import_task_store.go`、`pkg/constants/constants.go`(`TaskTypeShopBusinessOwnerImport` / `QueueShopBusinessOwnerImport` / `QueueForTaskType` 分支 / `DefaultTaskQueueWeights` 权重 4)、`pkg/queue/handler.go`、`internal/bootstrap/{stores,worker_stores,services,handlers,types}.go`、`cmd/worker/main.go`(`rescuePendingShopBusinessOwnerImportTasks`)、`internal/infrastructure/releasegate/checker.go`。
|
||||
- [x] 3.2 新增 `shop_import` 上传用途:前缀 `shop-imports/`、CSV 内容类型,同步上传请求的用途枚举与中文描述、对象存储用途映射、上传接口文档的用途表;建导入任务时校验 `file_key` 前缀与 `.csv` 扩展名。证据:`pkg/constants/shop_business_owner_import.go`、`pkg/storage/types.go`、`pkg/storage/service.go`、`internal/model/dto/storage_dto.go`、`internal/routes/storage.go`;实测 upload-url 返回 `shop-imports/2026/09/14/<uuid>.csv`,建任务校验拒绝非本目录/非 csv。入队失败必须落库:`MarkFailed` 不带状态守卫(入队失败时任务仍为待处理)并回填 `error_message`;实测真实队列不可用条件下响应与库内 `status=4 error_message="导入任务入队失败"` 一致,启动补偿只扫待处理状态故不会重跑已失败任务。
|
||||
- [x] 3.3 实现 CSV 解析:剥离 UTF-8 BOM,UTF-8 校验失败时按既有 GBK 转换能力回退解码(解码实现置于中立工具包,不依赖支付集成包),仍失败按任务级失败报错;表头必须与固定列序完全一致,不符即任务级失败且不进入逐行阶段。证据:`pkg/utils/encoding.go`(`simplifiedchinese`,任务层不 import `pkg/fuiou`);实测 BOM 与 GBK 文件均处理为 4 成功 2 失败,表头不符任务级失败且 items=0,不可解码文件任务级失败。列数异常的坏行必须走**行级**路径,故解析器设 `FieldsPerRecord = -1`(否则标准库在首条记录定型字段数后直接返回 `ErrFieldCount`,会把行级问题误升级为任务级失败);实测 3 列/5 列坏行 CSV:`status=3 total=5 ok=3 fail=2`,坏行记「行格式错误」(行号 3、4),其余行照常成功。
|
||||
- [x] 3.4 实现逐行执行:每行独立事务,成功行提交、失败行不写店铺并保留原值;行号自数据首行起计并写入明细;按批更新进度计数;任务级失败与行级失败分开记录,任务级失败不产生行明细;不设行数硬上限,不新增体积常量。证据:`internal/task/shop_business_owner_import.go`(`processRow` 独立事务、`constants.ShopBusinessOwnerImportProgressBatchSize=100`);实测 UTF-8 混合行 4/2 且失败行保留原值;无行数上限用 1200 行任务验证。按批进度计数必须同时写入已知行总数(否则处理中中间态违反 `chk_shop_business_owner_import_task_counts` 而静默丢失进度):实测任务处理中 `status=2 total=1200 success=99 fail=1` 正常落库。大文件成功路径:1200 行(30 家自建店铺循环)实测 `total=1200 ok=1195 fail=5`,5 条失败覆盖全部 5 类行级原因,且审计子事件 1195 条与成功数一致。
|
||||
- [x] 3.5 实现导入路由、Handler 与 DTO:任务创建、列表、详情;入口限超级管理员与平台用户;在导入接口的 OpenAPI description 写明 CSV 模板要求;不新增模板下载端点、模板响应 DTO 或模板资源。证据:`internal/routes/business_user_group.go`(`shopOwnerImportDoc` 含列序/互斥/备注/编码/唯一定位要求)、`internal/handler/admin/shop_business_owner_import.go`;实测创建/列表/详情成功,代理账号 8 个入口全部 403;生成文档中无模板端点。
|
||||
- [x] 3.6 注册审计动作码与资源:导入任务的创建与完成动作、独立任务资源定义,以及逐行实际变更的店铺负责人前后值与行备注审计;按 ENG-ROUTE-001 同步文档占位装配。证据:`pkg/constants/audit.go`、`internal/infrastructure/audit/registry.go`、`internal/query/audit/timeline.go`、`pkg/openapi/handlers.go`、`cmd/api/docs.go`、`cmd/gendocs/main.go`;实测审计事件查询返回创建与完成事件;行备注写入事件 Metadata。
|
||||
|
||||
## 4. 验证
|
||||
|
||||
- [x] 4.1 在 ENG-TEST-001 指定的唯一测试面验证:迁移 up/down/up、编码唯一与账号唯一、停用组保留成员且拒绝作为目标、负责人改组实时推导、未分组与业务线筛选、停用组可筛出、批量原子失败、导入混合结果、表头与编码任务级失败、无行数上限;fixture 仅增删本 Change 自己的记录。证据:迁移 up(221/u)/down/down 被守卫拒绝/up(221);编码唯一 400;停用组 1050 拒绝作为目标且保留成员;未分组与业务线筛选、停用组可筛出且带停用标记;批量交接原子失败返回 1005 且无部分写入。**负责人改组实时推导逐店比对**:对业务员 142 名下全部 11 家店铺(id 1、5、535、536、540、541、542、543、549、1464、1467)逐一比对接组前/改组后/清空后三次列表与详情返回的组字段——改组后 11/11 全部由 `AUG26-GA(standard)` 变为 `AUG26-GB(smart)`,清空后 11/11 组字段全空;库端证明 `tb_shop` 无任何组列(`information_schema` 查 `%group%`/`%business_line%` 返回 0 列)且 11 家店铺行 `business_owner_account_id` 前后全等、`updated_at` 未被组变更改写。**企业账号 403**:用既有启用企业账号(id 150、`user_type=4`、`enterprise_id=2`)经既有 TokenManager 铸造会话,逐个打 11 个新入口全部 HTTP 403 + `1005 无权限操作该资源或资源不存在`。fixture 清理已执行(见 4.1 清理记录)。
|
||||
- [x] 4.2 运行 `gofmt -w`(变更 Go 文件)、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-shop-salesperson-groups --strict`、`openspec doctor --json` 和 `./scripts/context-health.sh`;自动化测试按项目决策为 N/A。
|
||||
已完成:`gofmt -w`(变更/新增文件 `gofmt -l` 为空)、`go build ./cmd/api ./cmd/worker`(exit 0/0)、`go run cmd/gendocs/main.go`(成功生成且连续两次输出一致)、`openspec validate add-shop-salesperson-groups --strict`(Change is valid)、`openspec validate --all`(36 passed / 0 failed)。
|
||||
历史失败(已修复):`./scripts/context-health.sh` 曾输出「Requirement 证据链与 Specs 不一致」,差异集合为 `agent-funds-commission::回溯明细关联查询与导出`、`agent-funds-commission::套餐退款佣金回溯` 两条主 spec Requirement 缺少 `requirement-evidence.json` 证据行(证据侧无多余项)。归因为既有漂移:承载方 `openspec/specs/agent-funds-commission/spec.md` 最后由 `18796b16 归档`(14:25)、`requirement-evidence.json` 最后由 `67893617 feat(退款): AUG26-006…`(12:11)修改,均早于本 Change 起始基点 `957a235`,本 Change 未改动这两个文件;`business-user-group` 当时只有 delta spec(context-health 只扫 `openspec/specs/**`),故归档前还需为该 capability 补证据行与入口矩阵行。
|
||||
本轮补齐与复验(2026-09-14 归档轮,真实执行原文):①既有漂移修复——在 `docs/verification/context-reset/requirement-evidence.json` 追加上述 2 条证据行(真实只读命令:`grep -nE 'uk_commission_clawback_refund_original|chk_commission_clawback_amount|chk_commission_clawback_withdrawable|chk_commission_clawback_status' migrations/000220_add_commission_clawback_record.up.sql`,命中 23/25/26/27 行;`grep -n 'ledger_clawback' internal/store/postgres/commission_record_store.go internal/exporter/commission_record_scene.go`,命中两处 UNION ALL 合并查询),并在 `entry-capability-requirement-matrix.json` 补 `GET /api/admin/shops/{shop_id}/commission-records/{id}` http 行、为 `constants.TaskTypeRefundCommissionRecovery` 行补 `agent-funds-commission::套餐退款佣金回溯`。②本 Change 归档同步——新建主 spec `openspec/specs/business-user-group/spec.md`(Purpose 取 delta 原文,4 条 ADDED Requirement 与 11 个 Scenario 全量落地,末尾按既有惯例补「## 可达操作索引」列 11 个新端点),`requirement-evidence.json` 追加 4 条 `business-user-group` 证据行(真实只读 `grep -nE` 命中 service/store/query/constants 原文),矩阵补 11 条 http 行并为 `GET /api/admin/shops`、`GET /api/admin/shops/{id}`、`constants.TaskTypeShopBusinessOwnerImport` 补 Requirement 覆盖(`linked` 双向相等)。
|
||||
结果:`./scripts/context-health.sh` → 「Context 健康检查通过」(exit 0,含 `openspec doctor --json` `"healthy": true`、`openspec validate --all` 与 gendocs 连续两次产物一致);`openspec validate --all` → `Totals: 37 passed, 0 failed (37 items)`;`openspec doctor --json` → `"healthy": true`、`"status": []`。
|
||||
Reference in New Issue
Block a user