- 验证说明中的本地 smoke JWT 占位值改为 <SMOKE_JWT_SECRET>,不改动证据语义与其它内容
41 KiB
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命令。 - 受控路径声明:下列验证属「受控路径 / 组件级」,不是真实外部端到端——
- 退款域:直接调用应用层与
fiberapp.Test,企微侧使用 fakeProviderPort(不写企微渠道上下文);临时脚手架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/Store;Redis 侧真实读写 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(x6,232→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→ 造 fixture(h5 弹窗行、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 程序 seed(h5 弹窗行、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 fixture(id=1109–1112,card_id=8022,created_at=2026-09-18 03:18:04+00)确已删除(按 card 8022 逐行核对,现存行最高 id=1091);当前 id=1114–1117(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=34(source_no=SMOKE-AUG26-DECIDED-BILL、receivable=500、status=2) application id=13(status=1、decided_at=2026-09-15T04:30:00Z) allocation(status=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=0fixture 前后既有行计数:
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-w1,err=<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=4,tb_payment 行数=1,tb_payment_merchant_routing_success 行数=7
后:routing_epoch=4,tb_payment 行数=1,tb_payment_merchant_routing_success 行数=7
ListPools 共 5 条 SQL:count(tb_payment_merchant_pool);SELECT * … ORDER BY id DESC LIMIT 20;tb_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 行数=2,ListPools 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=0,worker_exit=0
临时 smoke 程序对 junhong_cmp_test 实测 31 项后删除 → 失败项=0;已清理 fixture 行数=6
临时测试 internal/task TestPriorityRoundSmoke → PASS:attempts=2、started/finished/dequeued 已写、next_run_at=+30s、近 5 分钟生命周期审计 5 行
临时测试 cmd/worker TestPollingPriorityExpiryTaskSmoke → PASS:mux 注册生效、真实执行一次到期任务 → expired+dequeued 且保留 attempts/reason、expire 审计 1 行
临时测试 internal/routes TestRegisterPollingPriorityRoutesOrder → PASS:5 条路由命中处理器、/: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=1;7 组非法时间输入全部 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)
命令与结果(原文节选):
Redis:JUNHONG_REDIS_DB=6 go run ./tmp_paa_smoke(Redis = 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/service(junhong_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/exporter(0)、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 派发/分片 → 对象存储上传/下载 URL);XLSX 产物形态(本次只跑 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 最终验收命令与原文(记录员本轮真实执行)
命令与输出为本轮在真实仓库逐条实跑所得,原文照录:
gofmt -l(对象为git status --porcelain -- '*.go'列出的全部现存 Go 文件,共 94 个)gofmt -l <上述文件列表> (无输出) gofmt_exit=0go build ./cmd/api ./cmd/worker(唯一输出为与仓库源码无关的本机模块缓存 stat cache 告警。)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=0go 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./scripts/context-health.sh- Validating... Context 健康检查通过 context_health_exit=0openspec validate --all- Validating... …(36 项逐条 ✓,含 change/close-august-iteration-gaps)… Totals: 36 passed, 0 failed (36 items) validate_all_exit=0openspec validate --all --strict(如实记录)18 项失败均为既有告警级规则(Purpose < 50 字符、Requirement > 500 字符),与本次改动无因果关系。「与 HEAD 基线逐行一致」的本轮复核方式:以Totals: 18 passed, 18 failed (36 items) Details: openspec validate agent-funds-commission --type spec validate_strict_exit=1git archive HEAD | tar -x -C /tmp/record_head_baseline(只读仓库、只写/tmp)在同一 openspec 版本下重跑基线,得(少的一项即本变更Totals: 17 passed, 18 failed (35 items) head_strict_exit=1change/close-august-iteration-gaps,HEAD 尚无该目录);对两边输出中✓/✗ spec/*行集合逐行diff→ 无差异(各 34 行、spec_only_diff_exit=0),失败 spec 集合与逐行结论完全一致。openspec doctor --json{ "root": { "path": "/Users/break/csxjProject/junhong_cmp_fiber", "source": "nearest", "healthy": true, "status": [] }, "store": null, "references": [], "status": [] } doctor_exit=0
5. 未覆盖与待维护者项
- 企微模板控件映射(tasks 14.3,待维护者):退款审批(套餐已用量与总量、资产类型、设备类型与型号、原支付渠道交易流水号)与代理注册审批(业务员)新增控件的映射需在生产按既有场景接口配置,并以
GET /api/admin/wecom/scenes返回的启用记录与control_mapping作为验收证据。本变更不写入模板。 - HTTP 异步导出链路未覆盖:
POST /api/admin/export-tasks→ asynq 分片 → 对象存储下载/URL 未端到端执行(导出证据为 CSV 产物级,见 3.8);XLSX 产物形态亦未覆盖。 openspec validate --all --strict既有基线失败:18 passed / 18 failed,均为既有告警级规则,与本次改动无因果关系(与 HEAD 基线逐行 diff 为空;本轮复核方式见 4.1 第 6 条)。- 主 Spec 同步与归档前门禁(已解决):主 Spec 已在不归档的前提下同步本变更 12 份 delta(需求名 169→180),
./scripts/context-health.sh在真实仓库重跑输出「Context 健康检查通过」exit=0、openspec validate --all36/0exit=0(见第 4 节)。openspec archive的最终执行仍属维护者动作。 - H5 §4.3/§4.4 的逐字原始输出未保留(证据缺失,见 3.4,附可复现命令);员工账单 §2.8 核销通过时间正例的逐字输出已按实施会话原文补入 3.2(含 fixture 主键与四条 HTTP 结果)。
- 跨域一次总冒烟未做:各域 smoke 分域执行,未做一次覆盖全部端点的统一冒烟。
- 未验证推断(会话自述,非缺陷):优先轮询真实 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=false;tb_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 均未变)
- h5-popup-notification:
popup_type创建必填;更新按部分更新语义(未传保持原值,一经传入按同一取值域校验);类型变更且请求未显式传优先级时保持原优先级(类型缺省优先级只在创建路径生效)。 - phone-asset-association:失败计数为「每次失败累加并刷新窗口时长,相邻失败间隔不超过窗口即持续累加」;锁定为一个窗口时长,且锁定期间提交不计数、不续期。
- export-time-filter:计费周期起点
period_start为精确匹配键,不属于第二个范围筛选字段,仍复用同一严格解析器。 - employee-collection-bill:已核销金额大于应收金额(异常数据)时,未核销金额与剩余可核销金额均按
0返回。 - 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三项结论来自审查会话的受控验证原文,终轮记录员本轮未重跑该脚手架(脚手架已按纪律清理)。