Files
junhong_cmp_fiber/openspec/changes/archive/2026-09-18-close-august-iteration-gaps/verification.md
break d24e777a4c
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 15m19s
docs(验证证据): 以占位符替换本地 smoke JWT 字面串(ENG-SECRET-001)
- 验证说明中的本地 smoke JWT 占位值改为 <SMOKE_JWT_SECRET>,不改动证据语义与其它内容
2026-09-18 15:35:20 +08:00

494 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# close-august-iteration-gaps 验证说明
对应 tasks.md §13.6:记录验证证据(命令与原文输出),供归档核对。
本文件只记录**真实发生过**的命令与输出,来源为三个实施会话的汇报:
- `writer-refund-agent`(退款 / 代理提现域)
- `writer-polling-etc`(迁移执行者;优先轮询 / 通道阈值 / 员工账单 / 商户池 / H5 弹窗 / 手机号与验证码 / 导出列)
- `shared-artifacts-owner`(证据链与生成物)
会话未逐字保留原始输出的项一律标注「证据缺失」或标注证据强度不做补写、改写或美化。凡标「记录员复核」的是本次记录会话以只读方式dbhub MCP 只读查询、文件读取、git status重新核对的结果。
---
## 1. 环境与边界
- 目标库:`junhong_cmp_test`PostgreSQL 17.6`cxd.whcxd.cn:16159``sslmode=disable`)。
- 目标 Redis测试 Redis **DB 6**`cxd.whcxd.cn:16299`)。
- 依据 ENG-TEST-001验证只在维护者指定的测试环境执行**仅创建并清理本 Change 自己的 fixture未重置整库**;诊断查询走 dbhub MCP 只读ENG-DB-002未用 psql 等客户端直连(`scripts/migrate.sh` 内部对 legacy 表的检查属脚本既有行为)。
- 迁移执行者唯一:六对迁移的 up/down/up 全程由 `writer-polling-etc` 一人驱动,退款负责人未执行任何 `migrate` 命令。
- **受控路径声明**:下列验证属「受控路径 / 组件级」,不是真实外部端到端——
- 退款域:直接调用应用层与 `fiber` `app.Test`,企微侧使用 fake `ProviderPort`(不写企微渠道上下文);临时脚手架 `cmd/refundsmoke` 与账内 smoke 测试文件跑完已删除(`ls cmd/` 仅剩 api、audit-coverage、audit-retention-simulate、foundation-check、gendocs、migration-finalize、worker
- 商户池:独立临时 schema `mp_smoke_<ts>` 内 AutoMigrate 建表,未触碰 public另有对 public 商户池的**只读**投影核对。
- 手机号 / 验证码:临时 schema `paa_smoke_assoc` + 真实 Service/StoreRedis 侧真实读写 DB 6。
- 导出:以产品同一写出函数 `writeExportFile` 在临时目录落下 CSV 产物(单分片形态),未启动 API/Worker。
- 通道阈值 / 优先轮询:临时测试文件 + dbhub 只读,跑完删除。
---
## 2. 六对迁移 up/down/up
### 2.1 迁移清单(磁盘现状,记录员读取 `migrations/00023{2..7}_*.{up,down}.sql` 核对)
| 编号 | 内容 | 新列可空性 / 默认值 |
| --- | --- | --- |
| 000232 `add_h5_popup_type` | `tb_h5_popup_configuration``popup_type` + 取值 CHECK | NOT NULL DEFAULT `'announcement'` |
| 000233 `extend_refund_settlement_fields` | `tb_refund_request``source_payment_no``varchar(64)`/`original_channel_trade_no`**`varchar(100)`**,与来源列 `tb_payment.third_party_trade_no` 一致)/`offline_settlement_no``varchar(128)`/`offline_settled_at`/`offline_settled_by``tb_refund_request_attempt``remark` | 前三项 NOT NULL DEFAULT `''``offline_settled_at` 可空;`offline_settled_by` NOT NULL DEFAULT `0``remark` TEXT NOT NULL DEFAULT `''` |
| 000234 `extend_polling_priority_item` | 加 11 列;重建状态 CHECK 容纳 `closed`/`expired``next_run_at` 部分索引;回填资产列与 `priority_effective_from` | `asset_type` NOT NULL DEFAULT `''``asset_id` 可空;`device_no_snapshot` NOT NULL DEFAULT `''``agent_shop_id_snapshot` 可空;`attempt_limit` NOT NULL DEFAULT `3``started_at`/`finished_at`/`next_run_at`/`dequeued_at`/`priority_effective_from` 可空;`integration_log_id` NOT NULL DEFAULT `''` |
| 000235 `extend_carrier_threshold_hit` | 锁表加 7 列 + 两个判定时间索引 | `hit_traffic_mb`/`hit_threshold_value` 可空;`hit_threshold_unit` NOT NULL DEFAULT `''``judged_at` NOT NULL **DEFAULT `now()`**`trigger_source`/`resume_result` NOT NULL DEFAULT `''``unlocked_at` 可空 |
| 000236 `add_phone_association_invalidator_name` | 加 `invalidator_name_snapshot` | NOT NULL DEFAULT `''` |
| 000237 `extend_withdrawal_attempt_qualification` | 加 4 列 | `qualification_version_id` NOT NULL DEFAULT `0``qualification_checked_at` 可空;`qualification_passed` NOT NULL DEFAULT `0``qualification_failure_reason` NOT NULL DEFAULT `''` |
### 2.2 执行命令与关键输出原文
来源writer-polling-etc 汇报;原始输出未在最终汇报中逐字保留的,按会话记录列出关键行并标注「证据缺失」)
基线(`./scripts/migrate.sh version`
```
target=erp_pgsql@cxd.whcxd.cn:16159/junhong_cmp_test sslmode=disable
正在加载 .env 文件...
当前迁移版本:
231
✓ 迁移操作完成
```
逐对 `up 1`x6232→237首个往返的输出形态原文
```
--- up 1 (round 1) rc=0 ---
正在加载 .env 文件...
正在向上迁移 1 步...
迁移完成,开始检查 legacy 表...
正在检查 legacy 表 tb_user / tb_order...
⚠ 检测到以下 legacy 表仍然存在:
- tb_order
```
legacy 表检查告警是脚本既有行为,与本次迁移无关。)
往返顺序:`up` 逐对 232→237 → `down` 逐对 237→232回到 231`up` 逐对 232→237。结束后 `schema_migrations``version=237``dirty=false`writer-polling-etc 汇报)。
> 说明:**逐对 up/down 的完整原始输出(含每次版本号回显)未被三份汇报逐字保留,证据缺失**;可核对的是上述往返结果与最终 version/dirty。可由下列命令复现`set -a && . ./.env && set +a && ./scripts/migrate.sh version` → 逐对 `./scripts/migrate.sh up 1` ×6 → `./scripts/migrate.sh down 1` ×6 → `./scripts/migrate.sh up 1` ×6再用 dbhub 只读查询 `information_schema.columns` / `pg_constraint` / `pg_indexes` 核对(记录员复核命令见 2.4)。
### 2.3 回填与非空 fixture 证据
- 非空回填 fixture 由维护者授权后执行:顺序 `down 6` → 造 fixtureh5 弹窗行、polling 优先项活动行、通道阈值锁行)→ `up 6` → 核对回填 → 删除 fixture → 复核计数回原值。临时程序 `tmp_migration_fixture`(跑完已 `rm -rf`)。
- 会话记录的核对点:弹窗 `popup_type=announcement`;活动项 `priority_effective_from=迁移时刻`且非活动行不回填;锁行 `judged_at=创建时间`、命中数值与阈值 NULL`000234` 回填 329 行获得资产列。
- **证据缺失**:该 fixture 往返的逐条原始输出fixture 主键、回填前后计数)未在最终汇报中逐字保留。可由下列命令复现:`./scripts/migrate.sh down 6` → 一次性 Go fixture 程序 seedh5 弹窗行、polling 优先项活动行、通道阈值锁行)→ `./scripts/migrate.sh up 6` → dbhub 只读核对 `popup_type=announcement`、活动项 `priority_effective_from=迁移时刻`、锁行 `judged_at=created_at` 且命中数值/阈值为 NULL → cleanup 后复核计数回原值。会话实际使用的临时程序为 `tmp_migration_fixture`(跑完 `rm -rf`)。
### 2.4 记录员复核(本轮只读,`xd://mcp__postgres_execute_sql_main`
30 个新列全部存在,类型 / 可空 / 默认值与设计一致,例如:
```
tb_h5_popup_configuration.popup_type character varying nullable=NO default='announcement'
tb_carrier_traffic_threshold_lock.judged_at timestamp with tz nullable=NO default=now()
tb_carrier_traffic_threshold_lock.hit_traffic_mb numeric nullable=YES default=null
tb_polling_priority_item.attempt_limit integer nullable=NO default=3
tb_polling_priority_item.next_run_at timestamp with tz nullable=YES default=null
tb_refund_request.offline_settled_by bigint nullable=NO default=0
tb_refund_request_attempt.remark text nullable=NO default=''
tb_commission_withdrawal_request_attempt.qualification_passed smallint nullable=NO default=0
```
约束(`pg_get_constraintdef` 原文节选):
```
ck_polling_priority_item_status: CHECK (status IN ('pending','processing','completed','failed','closed','expired'))
ck_polling_priority_item_terminal_result: CHECK ((status='completed' AND result='success') OR (status='failed' AND result='failed') OR status IN ('pending','processing','closed','expired'))
ck_polling_priority_item_asset_type: CHECK (asset_type IN ('','iot_card','device'))
ck_carrier_traffic_threshold_lock_resume_result: CHECK (resume_result IN ('','resumed','skipped','manual'))
chk_h5_popup_configuration_popup_type: CHECK (popup_type IN ('promotion','announcement'))
chk_commission_withdrawal_attempt_qualification_reason: CHECK (qualification_passed = 0 OR qualification_failure_reason = '')
```
索引(`pg_indexes` 原文节选,确认活动项唯一键谓词未变):
```
uq_polling_priority_item_active: USING btree (card_id, task_type) WHERE deleted_at IS NULL AND status IN ('pending','processing')
idx_polling_priority_item_next_run: USING btree (next_run_at) WHERE deleted_at IS NULL AND status IN ('pending','processing') AND next_run_at IS NOT NULL
idx_carrier_traffic_threshold_lock_carrier_judged: USING btree (carrier_id, judged_at DESC, id DESC) WHERE deleted_at IS NULL
idx_carrier_traffic_threshold_lock_card_judged: USING btree (card_id, judged_at DESC, id DESC) WHERE deleted_at IS NULL
```
### 2.5 记录员发现的与汇报不一致处(如实标注)
- writer-polling-etc 汇报「清理后 `polling_total=329`」;记录员本轮只读查询 `tb_polling_priority_item``total=333``active=0`、无软删。其 4 条 smoke fixtureid=11091112`card_id=8022``created_at=2026-09-18 03:18:04+00`**确已删除**(按 card 8022 逐行核对,现存行最高 id=1091当前 id=11141117`card_id=8018``created_at=2026-09-18 03:30:07+00`)为清理之后由测试环境自身轮询产生的 4 条真实业务行(**测试环境自身产生的行,非本 Change fixture**)。因此 333 = 329 + 4属测试库持续产生业务行的基线漂移不是清理失败。
---
## 3. 各域 smoke 命令与关键输出
### 3.1 退款 / 代理提现来源会话writer-refund-agent
命令(原文):
```
set -a; . ./.env; set +a; JUNHONG_DATABASE_HOST="$DB_HOST" JUNHONG_DATABASE_PORT="$DB_PORT" JUNHONG_DATABASE_USER="$DB_USER" JUNHONG_DATABASE_PASSWORD="$DB_PASSWORD" JUNHONG_DATABASE_DBNAME="$DB_NAME" JUNHONG_DATABASE_SSLMODE="$DB_SSLMODE" JUNHONG_REDIS_ADDRESS=127.0.0.1 JUNHONG_REDIS_PORT=6379 JUNHONG_JWT_SECRET_KEY="<SMOKE_JWT_SECRET>" go run ./cmd/refundsmoke
```
关键输出原文(节选):
```
SMOKE TARGET | db=junhong_cmp_test host=cxd.whcxd.cn
CHECK PASS | 退款原因为空被拒且未建申请与尝试 | err=退款原因不能为空 code=1001 refunds=0 attempts=0
CHECK PASS | 退款创建成功 | refund_id=506 refund_no=RF20260918112351115237
CHECK PASS | 线下订单两流水号为空 | source_payment_no="" original_channel_trade_no=""
CHECK PASS | 申请人备注不写入主表审批备注 | 主表 remark=""
CHECK PASS | 申请人备注冻结进尝试 | attempt_id=163 remark="smoke-applicant-remark-1"
CHECK PASS | 备注只进新尝试且历史尝试不变 | attempt1="smoke-applicant-remark-1" attempt2="smoke-applicant-remark-2"
CHECK PASS | 线下订单原路能力不可用且有原因 | capability={Available:false Unavailable:该订单的退款方式矩阵不包含原路退款}
CHECK PASS | 企微材料含套餐用量(真流量口径) | used_mb=123 total_mb=456
CHECK PASS | 企微材料含资产类型与设备类型型号 | asset_type=device device_type=smoke_type device_model=smoke_model
CHECK PASS | 场景白名单标记新增字段必须映射且不扩大既有字段 | required_fields=7
CHECK PASS | 场景映射缺必须字段明确失败 | err=企业微信场景缺少必须映射的业务字段: 资产类型
CHECK PASS | 提现尝试冻结资格版本与校验结果 | version=49 passed=1 checked_at=2026-09-18 11:23:57 +0800
CHECK PASS | 资格被替换后历史尝试仍返回原校验结果 | historical_version=49 replacement_version=50
CHECK PASS | 店铺列表返回直接下级代理数量 | total=28 parent_shop_id=1629 count=2
SMOKE SUMMARY | pass=31 fail=0
```
脚手架 `cmd/refundsmoke/``internal/routes/refund_route_order_smoke_test.go``internal/infrastructure/wecom/required_mapping_smoke_test.go` 跑完已删除(`git status --short | grep smoke` 无命中;`go build ./internal/... ./pkg/...` 无输出成功)。
清理证据:业务表全部回零,例如 `tb_refund_request (refund_no LIKE 'SMOKE-AUG-GAP%')=0``tb_approval_instance (submitter 为 smoke-*)=0`;追加型事实 `tb_audit_event` 保留 24 行(按指示未删)。
### 3.2 员工账单来源会话writer-polling-etc本地 API @127.0.0.1:3010 对测试库)
命令:启动 `JUNHONG_SERVER_ADDRESS=:3010 go run ./cmd/api``POST /api/auth/login` 取 token 后 pytest/UDP 式调用列表接口。
四类筛选输出原文(`eval` 结果,节选;行内被会话显示截断处标 `…`
```
A bill_id=32 -> (200, 0, 'total=1', [{'id': 32, 'receivable_amount': 1000, 'received_amount': 0, 'unsettled_amount': 1000, 'reserved_amount': 0, 'remaining_amount': 1000, 'status': 0}])
B bill_id=27 -> (200, 0, 'total=1', [{'id': 27, 'receivable_amount': 10900, 'receiv… ← 截断
C debtor_keyword=lxp -> 1 行bill 27
D customer_keyword -> 命中(按店铺名子串)
```
预占场景断言(会话总结原文):`B: bill_id=27 … with reserved 10900 → unsettled 10900, remaining 0; difference = 10900 = reserved ✓`(两金额字段之差等于预占额 10900
- **核销通过时间正例来源会话writer-polling-etc 末次汇报原文,运行方式:本地 API @127.0.0.1:3010 + 测试库自建 fixture**——证明筛选依据是申请表 `decided_at` 而非分摊表 `released_at`
fixture自建并记录主键故意让分摊 `released_at` 与申请 `decided_at` 不同):
```
bill id=34source_no=SMOKE-AUG26-DECIDED-BILL、receivable=500、status=2
application id=13status=1、decided_at=2026-09-15T04:30:00Z
allocationstatus=1、released_at=2026-09-16T04:30:00Z
```
四类筛选与正/反例输出原文HTTP + 库内结果):
```
GET /api/admin/employee-collection-bills?decided_start_time=2026-09-15T04:29:00Z&decided_end_time=2026-09-15T04:31:00Z
→ http=200 code=0 total=1 ids=[34]
GET /api/admin/employee-collection-bills?decided_start_time=2026-09-16T04:29:00Z&decided_end_time=2026-09-16T04:31:00Z只含分摊 released_at
→ total=0 ids=[]
GET /api/admin/employee-collection-bills?bill_id=34
→ total=1 ids=[34]
GET /api/admin/employee-collection-bills?start_time=2026-09-14T00:00:00Z&end_time=2026-09-14T23:59:59Z
→ total=0
```
fixture 前后既有行计数:`bills 6→7、applications 2→3、allocations 2→3、attempts 2→2`;清理后回到 `6/2/2/2`。
> 证据强度A/B/C 列表筛选、两个金额口径与核销通过时间正例均为**真实 HTTP + 真实测试库**(核销通过时间正例含自建并清理的 fixture主键与逐字输出见上
### 3.3 商户池来源会话writer-polling-etc.MerchantPoolWorker
命令(临时内在包测试,跑完删除):
```
go test ./internal/application/merchantpayment -run 'TestSmoke' -v
→ TestSmokeRawOutputs PASS (5.64s)、TestSmokeRealPoolListReadOnly PASS (0.80s)、ok 6.970s
```
§3.6 四条原始输出(节选):
```
① 单成员池(成员 id=3 smoke-w1阈值 1已达 1 笔/500 分)创建支付:选中商户=id=3 name=smoke-w1err=<nil>,池世代 1→1
②-a 自然日(成员 1、2 均达标创建支付AppError{code=1175, message="当前周期暂无可用商户"}
②-b 自然月(世代 2成员 1、2 均达标创建支付AppError{code=1175, message="当前周期暂无可用商户"}1175 = CodeNoPaymentConfig
③ 每轮累计(世代 3成员 1、2 均达标)创建支付:选中商户=id=1 name=smoke-a1顺序首位err=<nil>,池世代 3→4
④ 时间池创建起点=2026-09-18T08:23:58+08:00更新请求起点=…01:23:58… → 保存后起点=2026-09-18T11:23:58+08:00
```
列表只读(前后数字原文):
```
routing_epoch=4tb_payment 行数=1tb_payment_merchant_routing_success 行数=7
routing_epoch=4tb_payment 行数=1tb_payment_merchant_routing_success 行数=7
ListPools 共 5 条 SQLcount(tb_payment_merchant_pool)SELECT * … ORDER BY id DESC LIMIT 20tb_payment_merchant_pool_member WHERE pool_id IN (3,2,1)tb_payment_merchant WHERE (id IN (…) AND status=1)tb_payment_merchant_routing_success WHERE (pool_id=3 AND routing_epoch=4) OR (…)
```
真实 public 池只读(同一测试库,仅 SELECT`tb_payment_merchant_pool 行数=2ListPools total=2`;查询前后 `routing_epoch=1`、`tb_payment=129`、`routing_success=0` 不变。临时 schema `mp_smoke%` 经 `information_schema.schemata` 确认为 0 行残留。
### 3.4 H5 运营弹窗来源会话writer-polling-etc
- 实现核对:`popup_type` 必填且校验取值域;候选排序以显式 `CASE` 类别表达式(风险换卡 > 推广 > 公告)为第一键,再按显式优先级、`updated_at`、`id`;缺省优先级按类型取值。
- 迁移回填 smoke§4.3):按维护者授权,先 `down 6` 造 H5 弹窗配置 fixture再 `up 6` 核对既有配置获得 `popup_type=announcement`fixture 已删除,当前 `tb_h5_popup_configuration` 为 0 行(记录员复核)。
- **证据缺失**:该回填 smoke 的逐字原始输出fixture 主键、回填前后行内容)未被最终汇报逐字保留。可由下列命令复现:`./scripts/migrate.sh down 6` → 通过 `POST /api/admin/h5-popup-configurations`(旧代码)创建一条运营弹窗配置 → `./scripts/migrate.sh up 6` → dbhub 只读查询该行 `popup_type` 应为 `announcement` → 删除该 fixture。
- §4.4(创建/更新缺 `popup_type` 以参数非法拒绝):**证据缺失**,未在汇报中找到原始输出。可由下列命令复现:`POST /api/admin/h5-popup-configurations`(不带 `popup_type`)应返回参数非法;实现侧取值域校验已写入且 `chk_h5_popup_configuration_popup_type` 约束在库中存在(记录员复核)。
### 3.5 优先轮询来源会话writer-polling-etc.PriorityPollingWorker
命令与结果(原文节选):
```
go build -o /tmp/omptest_api ./cmd/api; go build -o /tmp/omptest_worker ./cmd/worker → api_exit=0worker_exit=0
临时 smoke 程序对 junhong_cmp_test 实测 31 项后删除 → 失败项=0已清理 fixture 行数=6
临时测试 internal/task TestPriorityRoundSmoke → PASSattempts=2、started/finished/dequeued 已写、next_run_at=+30s、近 5 分钟生命周期审计 5 行
临时测试 cmd/worker TestPollingPriorityExpiryTaskSmoke → PASSmux 注册生效、真实执行一次到期任务 → expired+dequeued 且保留 attempts/reason、expire 审计 1 行
临时测试 internal/routes TestRegisterPollingPriorityRoutesOrder → PASS5 条路由命中处理器、/:id/unknown=404、POST /:id 与 DELETE /:id/close=405
只读 SQL 核验 tb_audit_event近 40 分钟close success=2/denied=5、expire success=2、retrigger success=2/denied=3、enqueue=10、claim=2、complete=1、fail=1、retry=1
```
31 项覆盖:入队新列、执行起止与日志标识、出队时间、人工关闭(代理/二次关闭拒绝、保留失败原因、键位回收)、超期出队保留事实、重触发(越权拒绝、尝试次数归零)、全部筛选与时间拒绝集、企业账号拒绝、投影只读性(前后状态快照一致)与空事实语义。
未验证推断(会话自述):真实 Gateway 上游调用路径未运行Asynq 定时任务在真实 Worker 进程内的调度触发未端到端执行(仅验证注册无错 + 处理器直接调用正确终结超期项)。
### 3.6 通道阈值命中来源会话writer-polling-etc.CarrierThresholdWorker
命令(临时测试,跑完删除):
```
go test ./internal/query/carrierthreshold -run TestAug26ThresholdHitSmoke -v → PASS
go test ./internal/routes -run TestAug26CarrierThresholdHitRouteSmoke -v → PASS
```
关键原文(节选):
```
①命中记录 {"hit_traffic_mb":1500.5,"hit_threshold_value":100,"hit_threshold_unit":"MB","trigger_source":"polling","judged_at":"2026-09-05T18:00:00+08:00","period_start":"2026-09-01T00:00:00+08:00","status":"locked","unlocked_at":null,"resume_result":""}
②改阈值 200/GB 后历史命中仍为 100/MB新周期命中为 200/GB+manual_sync
③跨期仅解锁 → resume_result=skipped、unlocked_at 非空、命中事实不变
④运营商软删 → resume_result=manual、anomaly_flag=1、命中事实不变
⑤代理账号 403、范围外卡 total=0⑥闭区间含两端 total=17 组非法时间输入全部 CodeInvalidParam
⑦page_size=500 被裁剪为 100
⑧历史行 {"hit_traffic_mb":null,"hit_threshold_value":null,"hit_threshold_unit":"","judged_at":"2026-08-20T11:00:00+08:00","created_at":"2026-08-20T11:00:00+08:00"}
路由:平台账号 GET 返回 status=200 body={"code":0,"data":{"items":[],"total":0,"page":1,"size":20}}/api/admin/carriers/carrier-traffic-threshold-hits 仍走参数路由返回 400 "无效的运营商 ID"(证明无路由冲突)
```
DB 痕迹(只读):`tb_carrier_traffic_threshold_lock 0 行`、fixture 0 条、`schema_migrations=237`(该 worker 未执行 migrate
### 3.7 手机号关联与验证码失败限制来源会话writer-polling-etc.PhoneSmsWorker
命令与结果(原文节选):
```
RedisJUNHONG_REDIS_DB=6 go run ./tmp_paa_smokeRedis = cxd.whcxd.cn:16299 DB 6→ 17 PASS / 0 FAIL
第5次错误验证码被拒且计数达到上限 code=1001 count=5
失败计数键带固定窗口过期时间 ttl=10m0s
锁定期内正确验证码被限流拒绝 code=1008 err=验证码校验失败次数过多,请稍后再试
校验失败不消费验证码 stored="246810"
窗口内校验成功后失败计数清零并重新计数 before=2 existsAfterSuccess=0 after=1
并发提交失败计数按实际次数累加 concurrent=6 普通失败=6 限流=0 计数=6
计数不可用时正确验证码正常通过(放行)
DB store/servicejunhong_cmp_test临时 schema paa_smoke_assoc
批量计数整批只发出一条 SQL statements=1
查询次数不随行数线性增长(整页一次批量聚合) 1行 statements=4 / 4行 statements=4
有效关联数量与「最多关联十项」上限判定同口径 counts=[3 3 3 3]
有效关系的解绑人三字段为空;已失效关系返回 name="smoke解绑人" id=9900 at=2026-09-18T11:07:50+08:00 method=backend_single
解绑后计数同步下降且返回本次解绑人名称快照 ValidAssociationCount:2 InvalidatorName:smoke解绑人A
真实表有效关系的名称快照列为空 validRowsWithSnapshot=0临时 schema 无残留 residualCount=0真实表未被 fixture 污染 realRows=0
```
未验证(会话自述):真实 admin HTTP 链路 + 真实表未端到端跑(投影/计数在结构同构的临时 schema 上验证,真实表仅 2 条只读断言);注册/绑定/换绑入口的 HTTP 返回码未实跑(错误码透传属代码/单测级);锁定到期恢复用 `PEXPIRE 200ms` 模拟,未等真实 10 分钟窗口。
### 3.8 导出列来源会话writer-polling-etc.ExportWorker
命令(一次性程序,跑完删除):
```
source .env.local
go test ./internal/task -run TestSmokeAug26ExportCSVProducts -v → PASS (2.83s)
→ 验证后rm 该文件gofmt -l internal/exporter/无输出、go build ./internal/exporter0、go vet ./internal/exporter无输出
```
佣金明细导出表头原文CSV 第一行20 列;前 15 列为规定列序):
```
店铺,业务员,用户组,资产类型,设备类型,设备型号,资产标识,订单号,下单时间,佣金金额,佣金来源,佣金状态,关联佣金明细,入账后金额,创建时间,记录来源,记录ID,来源退款单号,是否可提现,佣金入账时间
```
代表性数据行原文(含回溯负数、缺值为空):
```
AUG26COL店铺甲,AUG26COL_biz,AUG26COL用户组一,物联网卡,MiFi,CDB001,AUG26COLCARD001,AUG26COLORD001,2026-08-01 10:20:30,-10.00,成本价差,回溯,474,40.00,2026-08-03 09:00:00,回溯明细,14,AUG26COLREF001,不可提现,
AUG26COL店铺乙,,,设备,,,AUG26COLDEV002,AUG26COLORD002,2026-08-02 11:22:33,20.00,一次性佣金,已发放,,20.00,2026-08-04 08:00:00,原佣金,475,,,
```
报表导出序号列原文(激活情况,节选):
```
序号,店铺,采购数量,累计激活数,激活率,新增激活数,累计在网数,活跃用户数,累计用量(GB),单用户卡均(GB),含零预测卡均(GB),不含零预测卡均(GB)
1,AUG26COL店铺乙,1,1,1.00,-,1,0,0.00,0.00,0.00,-
2,AUG26COL店铺甲,1,1,1.00,-,1,1,1.00,1.00,1.55,1.55
,合计,2,2,1.00,-,2,1,1.00,0.50,0.78,1.55
```
列表行数与导出行数一致性(数字):佣金明细 `list_total=2 list_rows=2 export_total=2 export_rows=2`;激活情况/套餐续费 `导出行=3=分组行 2 + 合计行 1`。
清理fixture 各表计数 `account=0 shop=0 device=0 card=0 order=0 commission=0 clawback=0 group=0 report_head=0 report_activation=0 report_renewal=0`CSV 临时目录已删。
**未覆盖(会话明确不声称)**HTTP 端到端(`POST /api/admin/export-tasks` → asynq 派发/分片 → 对象存储上传/下载 URLXLSX 产物形态(本次只跑 CSV
### 3.9 时间筛选契约来源writer-polling-etc 会话 + 各域汇报)
员工账单旧参数拒绝raw 原文节选HTTP @:3010
```
legacy created_from http=400 code=1001 msg=账单时间筛选参数已统一为 start_time 与 end_time不再接受 created_from
legacy created_to http=400 code=1001 msg=账单时间筛选参数已统一为 start_time 与 end_time不再接受 created_to
date-only …
```
通道阈值命中7 组非法时间输入全部 `CodeInvalidParam`、闭区间含两端 `total=1`(见 3.6)。
优先轮询列表:时间范围走同一 `utils.ParseTimeRange`,拒绝集在 31 项 smoke 内通过(见 3.5)。
### 3.10 路由顺序来源PriorityPollingWorker / CarrierThresholdWorker 路由测试;记录员静态复核)
- 优先轮询:`POST ""`(入队)→ `GET ""`(列表)→ `POST "/:id/close"` → `POST "/:id/retrigger"` → `GET "/:id"`;路由测试实测 5 条命中、`/:id/unknown=404`、`POST /:id` 与 `DELETE /:id/close=405`。
- 退款:`GET "/order-options"` 注册于 `GET "/:id"` 之前(记录员读取 `internal/routes/refund.go` 复核:第 48 行 order-options 早于第 57 行 `/:id`)。
- 通道阈值命中:顶层组 `/carrier-traffic-threshold-hits` 注册于 `/carriers` 组之前(记录员读取 `internal/routes/carrier.go` 复核);路由测试实测 `/api/admin/carriers/carrier-traffic-threshold-hits` 仍走参数路由返回 400证明无冲突。
---
## 4. 门禁命令与结果
| 命令 | 结果 | 来源 |
| --- | --- | --- |
| `gofmt` | 各域改动文件 `gofmt -l` 输出为空shared-artifacts-owner 未改 Go 文件,不适用 | 各写入会话 + shared-artifacts-owner |
| `go build ./cmd/api ./cmd/worker` | `go_build_exit=0`(仅一条与仓库无关的模块缓存 stat cache 告警) | writer-polling-etc 会话原文 |
| `go run cmd/gendocs/main.go` 连续两次一致 | 第一次输出 `成功在以下位置生成 OpenAPI 文档: …/docs/admin-openapi.yaml``cmp` 无差异,`md5 = c2dd9172234500a60861452f344854c3` | shared-artifacts-owner |
| 生成物复核 | 记录员本轮 `md5 -q docs/admin-openapi.yaml` = `c2dd9172234500a60861452f344854c3`(与上一致) | 记录员复核 |
| `./scripts/context-health.sh` | **记录员本轮重跑:`- Validating...` / `Context 健康检查通过` / `context_health_exit=0`**(主 Spec 已由 shared-artifacts-owner 在不归档的前提下把本变更 12 份 delta 同步进 `openspec/specs/**`,主 Spec 需求名 169→180经记录员只读复核 `grep -c '^### Requirement:' openspec/specs/` = 180 | 记录员重跑会话shared-artifacts-owner 完成主 Spec 同步后) |
| `openspec validate --all` | `Totals: 36 passed, 0 failed (36 items)``exit=0`(记录员本轮重跑同样 36/0、`validate_all_exit=0` | shared-artifacts-owner + 记录员重跑 |
| `openspec validate --all --strict` | `Totals: 18 passed, 18 failed (36 items)``exit=1`;与未改动基线逐行 diff 为空失败均为既有告警级规则Purpose < 50 字符、Requirement > 500 字符) | shared-artifacts-owner |
| `openspec doctor --json` | `{"root":{…,"healthy":true,"status":[]},"store":null,"references":[],"status":[]}``exit=0` | shared-artifacts-owner |
归档预演shared-artifacts-owner在 `/tmp` 全量副本):`openspec archive close-august-iteration-gaps --yes --json` → `archivedAs=2026-09-18-close-august-iteration-gaps`、`specsUpdated=true`、`totals={added:11, modified:10, removed:0, renamed:0}`;修正副本内 delta 路径风格后 `./scripts/context-health.sh` 输出 `Context 健康检查通过`、`exit=0`。
主 Spec 同步不归档shared-artifacts-owner 已把本变更 12 份 delta 的需求同步进 `openspec/specs/**`(主 Spec 需求名 169→180记录员本轮在**真实仓库**重跑 `./scripts/context-health.sh` → `Context 健康检查通过`、`exit=0``openspec validate --all` → `Totals: 36 passed, 0 failed (36 items)`、`exit=0`。`docs/admin-openapi.yaml` 经脚本重生成后 md5 仍为 `c2dd9172234500a60861452f344854c3`(与重跑前一致)。
### 4.1 最终验收命令与原文(记录员本轮真实执行)
命令与输出为**本轮在真实仓库逐条实跑**所得,原文照录:
1. `gofmt -l`(对象为 `git status --porcelain -- '*.go'` 列出的全部现存 Go 文件,共 94 个)
```
gofmt -l <上述文件列表>
(无输出)
gofmt_exit=0
```
2. `go build ./cmd/api ./cmd/worker`
```
go: writing stat cache: open /Users/break/go/pkg/mod/cache/download/github.com/break/junhong_cmp_fiber/@v/v0.0.0-20260918014228-5e78809b9333.info161583859.tmp: permission denied
build_exit=0
```
(唯一输出为与仓库源码无关的本机模块缓存 stat cache 告警。)
3. `go run cmd/gendocs/main.go` 连续两次 + `cmp`
```
=== run1 ===
2026/09/18 15:25:30 成功在以下位置生成 OpenAPI 文档: /Users/break/csxjProject/junhong_cmp_fiber/docs/admin-openapi.yaml
run1_exit=0
=== run2 ===
2026/09/18 15:25:31 成功在以下位置生成 OpenAPI 文档: /Users/break/csxjProject/junhong_cmp_fiber/docs/admin-openapi.yaml
run2_exit=0
=== cmp run1 vs run2 ===
cmp_exit=0 (无差异)
md5生成前 c2dd9172234500a60861452f344854c3 / run1 c2dd9172234500a60861452f344854c3 / run2 c2dd9172234500a60861452f344854c3 / 当前工作树文件 c2dd9172234500a60861452f344854c3
```
4. `./scripts/context-health.sh`
```
- Validating...
Context 健康检查通过
context_health_exit=0
```
5. `openspec validate --all`
```
- Validating...
36 项逐条 ✓,含 change/close-august-iteration-gaps
Totals: 36 passed, 0 failed (36 items)
validate_all_exit=0
```
6. `openspec validate --all --strict`(如实记录)
```
Totals: 18 passed, 18 failed (36 items)
Details: openspec validate agent-funds-commission --type spec
validate_strict_exit=1
```
18 项失败均为既有告警级规则Purpose < 50 字符、Requirement > 500 字符),与本次改动无因果关系。**「与 HEAD 基线逐行一致」的本轮复核方式**:以 `git archive HEAD | tar -x -C /tmp/record_head_baseline`(只读仓库、只写 `/tmp`)在同一 openspec 版本下重跑基线,得
```
Totals: 17 passed, 18 failed (35 items)
head_strict_exit=1
```
(少的一项即本变更 `change/close-august-iteration-gaps`HEAD 尚无该目录);对两边输出中 `✓/✗ spec/*` 行集合逐行 `diff` → 无差异(各 34 行、`spec_only_diff_exit=0`),失败 spec 集合与逐行结论完全一致。
7. `openspec doctor --json`
```
{
"root": {
"path": "/Users/break/csxjProject/junhong_cmp_fiber",
"source": "nearest",
"healthy": true,
"status": []
},
"store": null,
"references": [],
"status": []
}
doctor_exit=0
```
---
## 5. 未覆盖与待维护者项
1. **企微模板控件映射tasks 14.3,待维护者)**:退款审批(套餐已用量与总量、资产类型、设备类型与型号、原支付渠道交易流水号)与代理注册审批(业务员)新增控件的映射需在生产按既有场景接口配置,并以 `GET /api/admin/wecom/scenes` 返回的启用记录与 `control_mapping` 作为验收证据。本变更不写入模板。
2. **HTTP 异步导出链路未覆盖**`POST /api/admin/export-tasks` → asynq 分片 → 对象存储下载/URL 未端到端执行(导出证据为 CSV 产物级,见 3.8XLSX 产物形态亦未覆盖。
3. **`openspec validate --all --strict` 既有基线失败**18 passed / 18 failed均为既有告警级规则与本次改动无因果关系与 HEAD 基线逐行 diff 为空;本轮复核方式见 4.1 第 6 条)。
4. **主 Spec 同步与归档前门禁(已解决)**:主 Spec 已在不归档的前提下同步本变更 12 份 delta需求名 169→180`./scripts/context-health.sh` 在真实仓库重跑输出「Context 健康检查通过」`exit=0`、`openspec validate --all` 36/0 `exit=0`(见第 4 节)。`openspec archive` 的最终执行仍属维护者动作。
5. **H5 §4.3/§4.4 的逐字原始输出未保留**(证据缺失,见 3.4,附可复现命令);员工账单 §2.8 核销通过时间正例的逐字输出已按实施会话原文补入 3.2(含 fixture 主键与四条 HTTP 结果)。
6. **跨域一次总冒烟未做**:各域 smoke 分域执行,未做一次覆盖全部端点的统一冒烟。
7. **未验证推断**(会话自述,非缺陷):优先轮询真实 Gateway 上游调用路径、Asynq 定时任务在真实 Worker 进程内的调度触发、注册/绑定/换绑入口 HTTP 返回码,均只到代码/注册无错/组件级验证。
---
## 6. 独立审查与修复
来源:独立审查会话(只读复核 + 受控验证)的结论,由终轮记录员照实登记;本节登记过程不改动代码、迁移、主 Spec 与证据 JSON。
### 6.1 审查结论
- 判定:**阻断 0 项、应修 1 项F1、建议 5 项、观察 8 项**。
- `go build ./cmd/api ./cmd/worker` → **exit=0**。
- 脚手架无残留:`git status --porcelain | grep -E '_test\.go|tmp_|zz_'` → 无命中exit=1`ls cmd/` → 仅 `api`、`audit-coverage`、`audit-retention-simulate`、`foundation-check`、`gendocs`、`migration-finalize`、`worker`,无 `cmd/refundsmoke`。
### 6.2 F1 处置:契约侧澄清(不改代码)
处置方式为**契约侧澄清**实现不动delta 与主 Spec 同一句改写为:
> 校验通过时才把「资格版本标识 + 校验时间 + 是否通过」冻结进当次审批尝试快照并在提现详情可查;校验未通过时按既有资料资格校验拒绝语义 MUST NOT 创建提现申请 / 审批实例 / 审批尝试,稳定原因记在该次拒绝结果与审计事实;两种情形均不改写历史尝试的校验结果;历史尝试按「无留痕」返回(版本 0、校验时间为空、是否通过 0、未通过原因为空串
Requirement 名与 Scenario 未变。落点:`openspec/changes/close-august-iteration-gaps/specs/agent-distribution-withdrawal/spec.md` 与 `openspec/specs/agent-distribution-withdrawal/spec.md` 的「提现冻结与企业微信终审」(记录员本轮只读复核两边同句一致)。
### 6.3 S1 处置:列宽放宽(代码 + 迁移)
- `migrations/000233_extend_refund_settlement_fields.up.sql``original_channel_trade_no` 由 `VARCHAR(64)` 放宽为 **`VARCHAR(100)`**,对齐来源列 `tb_payment.third_party_trade_no`(该列在 `migrations/000118_create_tb_payment.up.sql:11` 为 `VARCHAR(100)``internal/model/refund.go` 字段同步为 `type:varchar(100)` 并更新注释(第 53 行);**down 脚本不变**。
- 唯一迁移执行者随后执行 `down 5`237→232→ `up 5`232→237。执行者汇报的往返核对**执行者汇报,本轮记录员未复跑该往返**
- `down 5` 后:残留新列=0、`refunds=44` 不变、`version=232` / `dirty=false`
- `up 5` 后核对:`original_channel_trade_no=varchar(100)`、`source_payment_no=varchar(64)`、`offline_settlement_no=varchar(128)`、`offline_settled_at=timestamptz NULL`、`offline_settled_by=int8 NOT NULL default 0`、`attempt.remark=text NOT NULL default ''`、既有 44 行该列为空串可读、`version=237` / `dirty=false`。
- 记录员本轮只读复核(`information_schema.columns` + `schema_migrations`,测试库,与上述一致)原文:
```
tb_refund_request.original_channel_trade_no character varying max_len=100 nullable=NO default=''
tb_refund_request.source_payment_no character varying max_len=64 nullable=NO default=''
tb_refund_request.offline_settlement_no character varying max_len=128 nullable=NO default=''
tb_refund_request.offline_settled_at timestamp with time zone nullable=YES default=null
tb_refund_request.offline_settled_by bigint nullable=NO default=0
tb_refund_request_attempt.remark text nullable=NO default=''
schema_migrations: version=237 dirty=falsetb_refund_request: refunds=44该列为空串 44 行
```
### 6.4 S4 处置:缺失快照值的检查前移(代码)
`internal/infrastructure/wecom/approval_form.go`:把「新增必须字段缺快照值」的检查**前移到任何 `attachments.Upload` 之前**(在 `mappings` 循环之外,按 `RequiredSceneFields(businessType)` 扫描快照)。记录员静态复核:该检查循环位于 `approval_form.go:74-81`,早于循环内的 `buildControlValue`:94与其内部的 `attachments.Upload`:161
受控验证原文(附件端口为 **fake**
```
映射缺必须字段 → 明确失败upload_calls=0
快照缺 refund_package_used_mb → 明确失败upload_calls=0
字段齐备 + 额外映射既有可选字段但快照无值 → err=nil controls=8 upload_calls=1可选字段仍静默跳过
```
**口径限定**:该验证证明的是「校验发生在调用 `Upload` 之前」,**不等于**「不访问企业微信」——附件端口为 fake未触达真实企微。
### 6.5 O6 处置:过强注释收窄(注释)
`internal/application/carrierthreshold/evaluate.go` 的过强断言收窄为:
> 命中事实与停机锁是同一条 INSERT 的列,业务数据无法触发 hit_* CHECK数据库级故障与既有流量观测一致地整体回滚停机顺延到下一次观测重试。
(记录员静态复核:`evaluate.go:79-83` 注释已为该口径。)
### 6.6 S3 登记(不改代码,归入既有漂移)
`internal/store/postgres/phone_asset_association_store.go` 的 `CountValidByPhone` 本次仅改**实现体**,方法本体为既有代码,**故未删除**;其注释「用于十项上限判定」为既有文本,本次仅在原注释后追加「实现委托批量计数,保证同一计数口径」。据此归入**「既有漂移」****不得写成「本次新增死代码」**。
### 6.7 文档侧澄清Requirement 名与 Scenario 均未变)
1. **h5-popup-notification**`popup_type` 创建必填;更新按部分更新语义(未传保持原值,一经传入按同一取值域校验);类型变更且请求未显式传优先级时保持原优先级(类型缺省优先级只在创建路径生效)。
2. **phone-asset-association**:失败计数为「每次失败累加并刷新窗口时长,相邻失败间隔不超过窗口即持续累加」;锁定为一个窗口时长,且锁定期间提交不计数、不续期。
3. **export-time-filter**:计费周期起点 `period_start` 为**精确匹配键**,不属于第二个范围筛选字段,仍复用同一严格解析器。
4. **employee-collection-bill**:已核销金额大于应收金额(异常数据)时,未核销金额与剩余可核销金额均按 `0` 返回。
5. **order-refund-exchange**:仅「客户收款信息退款(线下到账)」方式可登记线下处理流水号(其余以状态非法拒绝);重提时来源支付事实与实收金额同批从同一原成功支付事实重新冻结。
### 6.8 未改动观察(如实登记)
- **O4**:店铺下级数量计数未显式叠加数据范围过滤;复核结论为**不可越界**,仅存在 Redis 缓存窗口。
- **O7**:验证码「验证码不存在或已过期」与「验证码错误」两类文案差异属**既有 As-Is**;如需统一须另立变更。
### 6.9 本轮未复核事项(证据边界)
- S1 的 `down 5` / `up 5` 往返过程与「残留新列=0」为**唯一执行者汇报**,终轮记录员未复跑该往返(迁移号现停留 237本轮复核的是往返后的列形态与版本状态见 6.3)。
- 6.4 的 `upload_calls` 三项结论来自审查会话的受控验证原文,终轮记录员本轮**未重跑**该脚手架(脚手架已按纪律清理)。