Files
junhong_cmp_fiber/openspec/changes/archive/2026-09-14-add-shop-salesperson-groups/tasks.md
break c7f9e005af
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 1h43m42s
feat(业务用户组): AUG26-003 业务用户组与店铺负责人分组导入
- 迁移 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 验证证据链。
2026-09-14 16:51:44 +08:00

13 KiB
Raw Blame History

1. 用户组与读侧推导

  • 1.1 追踪平台用户、店铺负责人、数据范围、既有批量操作、CSV 解析与 BOM/GBK 处理、审计调用链,确认负责人字段与「有效平台业务员」的既有判定分布。证据:侦查结论落在本 Change design.md既有 6 处内联判定已定位(internal/application/shop/create.goupdate.gointernal/query/shop/business_owner.gointernal/infrastructure/shop/recipient_resolver.gointernal/infrastructure/wallet/debit_event.go),按 As-Is 未重构。
  • 1.2 新增成对迁移:业务用户组表(名称、稳定编码、所属业务线、排序、状态、备注)、平台用户—组关联表,以及编码唯一、账号唯一、组维度三处索引;down 带守卫,不回填历史组归属,不建外键。证据:migrations/000221_add_business_user_group_and_shop_owner_import.{up,down}.sql;隔离库 ./scripts/migrate.sh up221/u 332msdown221/d 252msup 全部成功;有数据时 down 被守卫拒绝(tb_shop_business_owner_import_task 已存在导入任务事实,禁止删表回滚)。
  • 1.3 新增模型与常量:用户组、成员关联、业务线单值与启停常量、导入任务状态复用既有导入状态机。证据:internal/model/business_user_group.gointernal/model/shop_business_owner_import_task.gopkg/constants/business_user_group.gopkg/constants/shop_business_owner_import.go;状态机复用 model.ImportTaskStatus*
  • 1.4 实现用户组 CRUD、启停、无成员二次确认删除与所属业务线维护写审计禁止修改编码不引入层级与组管理员。证据internal/application/businessusergroup/service.go;实测创建成功、重复编码 400「业务用户组编码已存在」、停用成功、含成员删除 1050「用户组仍有成员只能停用或先移走成员」。
  • 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 注册顺序并复测通过。
  • 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. 店铺负责人批量交接

  • 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 导入

  • 3.1 搭建导入场景骨架:新任务表与模型(任务号、文件键、状态、成功/失败计数、行明细、任务级错误、操作者快照、store、任务类型与队列常量、QueueForTaskType 分支与队列权重、运行时与 worker 的 store 装配、worker 处理器注册与启动补偿、发布门禁的未完成异步任务表清单。证据:internal/store/postgres/shop_business_owner_import_task_store.gopkg/constants/constants.goTaskTypeShopBusinessOwnerImport / QueueShopBusinessOwnerImport / QueueForTaskType 分支 / DefaultTaskQueueWeights 权重 4pkg/queue/handler.gointernal/bootstrap/{stores,worker_stores,services,handlers,types}.gocmd/worker/main.gorescuePendingShopBusinessOwnerImportTasks)、internal/infrastructure/releasegate/checker.go
  • 3.2 新增 shop_import 上传用途:前缀 shop-imports/、CSV 内容类型,同步上传请求的用途枚举与中文描述、对象存储用途映射、上传接口文档的用途表;建导入任务时校验 file_key 前缀与 .csv 扩展名。证据:pkg/constants/shop_business_owner_import.gopkg/storage/types.gopkg/storage/service.gointernal/model/dto/storage_dto.gointernal/routes/storage.go;实测 upload-url 返回 shop-imports/2026/09/14/<uuid>.csv,建任务校验拒绝非本目录/非 csv。入队失败必须落库MarkFailed 不带状态守卫(入队失败时任务仍为待处理)并回填 error_message;实测真实队列不可用条件下响应与库内 status=4 error_message="导入任务入队失败" 一致,启动补偿只扫待处理状态故不会重跑已失败任务。
  • 3.3 实现 CSV 解析:剥离 UTF-8 BOMUTF-8 校验失败时按既有 GBK 转换能力回退解码(解码实现置于中立工具包,不依赖支付集成包),仍失败按任务级失败报错;表头必须与固定列序完全一致,不符即任务级失败且不进入逐行阶段。证据:pkg/utils/encoding.gosimplifiedchinese,任务层不 import pkg/fuiou);实测 BOM 与 GBK 文件均处理为 4 成功 2 失败,表头不符任务级失败且 items=0不可解码文件任务级失败。列数异常的坏行必须走行级路径,故解析器设 FieldsPerRecord = -1(否则标准库在首条记录定型字段数后直接返回 ErrFieldCount,会把行级问题误升级为任务级失败);实测 3 列/5 列坏行 CSVstatus=3 total=5 ok=3 fail=2,坏行记「行格式错误」(行号 3、4其余行照常成功。
  • 3.4 实现逐行执行:每行独立事务,成功行提交、失败行不写店铺并保留原值;行号自数据首行起计并写入明细;按批更新进度计数;任务级失败与行级失败分开记录,任务级失败不产生行明细;不设行数硬上限,不新增体积常量。证据:internal/task/shop_business_owner_import.goprocessRow 独立事务、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=55 条失败覆盖全部 5 类行级原因,且审计子事件 1195 条与成功数一致。
  • 3.5 实现导入路由、Handler 与 DTO任务创建、列表、详情入口限超级管理员与平台用户在导入接口的 OpenAPI description 写明 CSV 模板要求;不新增模板下载端点、模板响应 DTO 或模板资源。证据:internal/routes/business_user_group.goshopOwnerImportDoc 含列序/互斥/备注/编码/唯一定位要求)、internal/handler/admin/shop_business_owner_import.go;实测创建/列表/详情成功,代理账号 8 个入口全部 403生成文档中无模板端点。
  • 3.6 注册审计动作码与资源:导入任务的创建与完成动作、独立任务资源定义,以及逐行实际变更的店铺负责人前后值与行备注审计;按 ENG-ROUTE-001 同步文档占位装配。证据:pkg/constants/audit.gointernal/infrastructure/audit/registry.gointernal/query/audit/timeline.gopkg/openapi/handlers.gocmd/api/docs.gocmd/gendocs/main.go;实测审计事件查询返回创建与完成事件;行备注写入事件 Metadata。

4. 验证

  • 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=4enterprise_id=2)经既有 TokenManager 铸造会话,逐个打 11 个新入口全部 HTTP 403 + 1005 无权限操作该资源或资源不存在。fixture 清理已执行(见 4.1 清理记录)。
  • 4.2 运行 gofmt -w(变更 Go 文件)、go build ./cmd/api ./cmd/workergo run cmd/gendocs/main.goopenspec validate add-shop-salesperson-groups --strictopenspec doctor --json./scripts/context-health.sh;自动化测试按项目决策为 N/A。 已完成:gofmt -w(变更/新增文件 gofmt -l 为空)、go build ./cmd/api ./cmd/workerexit 0/0go run cmd/gendocs/main.go(成功生成且连续两次输出一致)、openspec validate add-shop-salesperson-groups --strictChange is validopenspec validate --all36 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:25requirement-evidence.json 最后由 67893617 feat(退款): AUG26-006…12:11修改均早于本 Change 起始基点 957a235,本 Change 未改动这两个文件;business-user-group 当时只有 delta speccontext-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.jsonGET /api/admin/shops/{shop_id}/commission-records/{id} http 行、为 constants.TaskTypeRefundCommissionRecovery 行补 agent-funds-commission::套餐退款佣金回溯。②本 Change 归档同步——新建主 spec openspec/specs/business-user-group/spec.mdPurpose 取 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/shopsGET /api/admin/shops/{id}constants.TaskTypeShopBusinessOwnerImport 补 Requirement 覆盖(linked 双向相等)。 结果:./scripts/context-health.sh → 「Context 健康检查通过」exit 0openspec doctor --json "healthy": trueopenspec validate --all 与 gendocs 连续两次产物一致);openspec validate --allTotals: 37 passed, 0 failed (37 items)openspec doctor --json"healthy": true"status": []