## 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/.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": []`。