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 验证证据链。
13 KiB
13 KiB
1. 用户组与读侧推导
- 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 未重构。 - 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 已存在导入任务事实,禁止删表回滚)。 - 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*。 - 1.4 实现用户组 CRUD、启停、无成员二次确认删除与所属业务线维护,写审计;禁止修改编码,不引入层级与组管理员。证据:
internal/application/businessusergroup/service.go;实测创建成功、重复编码 400「业务用户组编码已存在」、停用成功、含成员删除 1050「用户组仍有成员,只能停用或先移走成员」。 - 1.5 实现平台用户批量设置/清空归属:启用平台用户与启用组校验、单事务替换原归属、停用组拒绝作为目标、同事务成员前后审计与业务回滚后的独立失败审计;新增共享「有效平台业务员」谓词供本 Change 复用,不重构既有内联判定。并发正确性以账号行锁(
tb_accountFOR 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.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。 - 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="导入任务入队失败"一致,启动补偿只扫待处理状态故不会重跑已失败任务。 - 3.3 实现 CSV 解析:剥离 UTF-8 BOM,UTF-8 校验失败时按既有 GBK 转换能力回退解码(解码实现置于中立工具包,不依赖支付集成包),仍失败按任务级失败报错;表头必须与固定列序完全一致,不符即任务级失败且不进入逐行阶段。证据:
pkg/utils/encoding.go(simplifiedchinese,任务层不 importpkg/fuiou);实测 BOM 与 GBK 文件均处理为 4 成功 2 失败,表头不符任务级失败且 items=0,不可解码文件任务级失败。列数异常的坏行必须走行级路径,故解析器设FieldsPerRecord = -1(否则标准库在首条记录定型字段数后直接返回ErrFieldCount,会把行级问题误升级为任务级失败);实测 3 列/5 列坏行 CSV:status=3 total=5 ok=3 fail=2,坏行记「行格式错误」(行号 3、4),其余行照常成功。 - 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 条与成功数一致。 - 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;生成文档中无模板端点。 - 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. 验证
- 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 清理记录)。 - 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 归档同步——新建主 specopenspec/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": []。