Files
junhong_cmp_fiber/openspec/changes/add-package-silence-period/tasks.md
邹进 c33cc6f6c3
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 1m32s
docs: 沉默期 11.1、3.5 测试环境验证结果与前端修改汇总
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:07:46 +09:00

20 KiB
Raw Blame History

规则细节以 docs/沉默期技术方案/实名即生效套餐沉默期技术方案_v3.md(下称"技术方案")为准,任务中的 §n 指技术方案章节。测试环境是唯一验证面(PostgreSQL junhong_cmp_test、Redis DB 6、Iteration 分支部署、cmp-test 日志);迁移用 ~/go/bin/migrate 与 .env 的 DB_*;fixture 只创建 / 删除本变更自己的记录;只读诊断用 dbhub。

1. 数据层(§3、§13.1、§17.2)

  • 1.1 迁移①:tb_package_usage 新增 10 个沉默期字段(silence_status、silence_start_at、silence_period_days、silence_deadline_at、silence_extended_days、silence_paid_extended_days、silence_extension_count、silence_free_extension_used、silence_expired_at、silence_adjust_pending),计数类 NOT NULL DEFAULT 0,布尔 NOT NULL DEFAULT false,带列注释;成对 down
  • 1.2 迁移②:部分索引 idx_package_usage_silence_scan (silence_deadline_at, id) WHERE deleted_at IS NULL AND status = 0 AND silence_status = 'waiting'
  • 1.3 迁移③:tb_package 新增 silence_period_days(可空)、silence_extension_enabled(NOT NULL DEFAULT true)
  • 1.4 迁移④:新建 tb_package_silence_extension_policy、tb_package_silence_record、tb_package_silence_extension_order、tb_package_silence_extension_batch、tb_package_silence_extension,含 §7.1 / §7.2 的唯一约束与部分唯一索引(同一套餐最多一笔未结束订单、线下收款方式 + 交易单号唯一、UNIQUE(batch_id, package_usage_id)、同一套餐最多一条未结束明细)
  • 1.5 迁移⑤⑥:插入默认延期政策(全部套餐、长期、500 分/月、免费 1 次、累计 6 个月、单次不限、无免审批场景);插入系统配置 package.silence.enabled(false)、package.silence.default_days(30)、package.silence.extension_unit_price(500)
  • 1.6 迁移⑦:扩展 tb_wecom_approval_scene.chk_wecom_approval_scene_business,在 000216 现有白名单基础上加入 package_silence_approval;down 恢复原白名单
  • 1.7 迁移⑧⑨:tb_iot_card、tb_device 新增 first_purchase_at timestamptz NULL 与索引;新建 tb_asset_first_purchase_record(UNIQUE(asset_type, asset_id, generation))
  • 1.8 测试库执行 up → down → up,确认约束、索引、默认数据正确
  • 1.9 模型:PackageUsage、Package、IotCard、Device 增加字段;新增 6 个表的模型
  • 1.10 Store:延期政策、沉默期流水、延期订单、批次、明细、首次购买套餐记录的 Store(条件更新方法返回影响行数)
  • 1.11 常量与错误码:silence_status 取值、延期路径与类型、免审批场景枚举、order_type = silence_extension、审批业务类型 package_silence_approval、审计动作(§11.2)、权限点(§11.1)、错误码(§12.5)

2. 领域规则(§4.2、§5.2、§8.2、§3.6)

  • 2.1 internal/domain/package/silence.go:SilenceDeadline(date, days)(当日 23:59:59,Asia/Shanghai)
  • 2.2 沉默期判定:输入当前状态、截止时间、实名时间、是否有调整审批、当前时间,输出 wait / end / expire / noop
  • 2.3 延期报价:输入截止日、今天、输入天数、政策快照、已付费延期天数、延期类型,输出 overdue、min/max 天数与月数、计费月数与天数、金额、新截止日、能否办理与原因
  • 2.4 政策匹配(指定套餐 > 系列 > 全部,启用且在生效期)与同级重叠校验;免审批判断
  • 2.5 用 §8.2 示例表四种情况、输入 1/30/31/60 天、跨月跨年日期在测试环境调用路径中核对一次结果(不写自动化测试)
    • 核对结果(2026-10-27,临时程序调用领域函数,已删除):截止 10-01 时 10-20 / 12-15 / 次年 03-20 / 次年 04-05 分别为 overdue 19/75/170/186、min 1/3/6/7、max 6/6/6/6,选最少新截止 10-31 / 12-30 / 03-30,第四种 exceeds_limit;输入 1/30/31/60 天计费 30/30/60/60;09-24 10:00 + 30 → 10-24 23:59:59;跨年 12-20 + 30 → 01-19,跨月 01-31 + 30 → 03-02;判定:截止当天 23:30 实名 → end、截止后实名 → expire、已实名无时间 → end 并补写、调整审批中 → 不过期。

3. 套餐配置(§3.2、§3.3)

  • 3.1 套餐新增 / 编辑 DTO 与校验:实名即生效套餐必填 silence_period_days(1~90)、silence_extension_enabled;普通套餐禁止写入;修改默认值写审计 package.silence_default_changed
  • 3.2 生效条件变更约束:套餐服务与店铺分配(单个、批量、系列授权)共用校验函数,阻断 §3.3 的三种变更,from_purchase → from_activation 要求同时提供默认沉默期
  • 3.3 "未配置沉默期的实名即生效套餐"列表接口(筛选、导出)
  • 3.4 分配覆盖口径(design D11):共用条件"正式套餐本身 from_activation 或存在未删除的分配覆盖为 from_activation",用于未配置清单(列表、导出)与打开前检查;购买即生效正式套餐允许选填默认沉默期(不强制),加油包仍禁止;分配覆盖 from_purchase → from_activation 在套餐已配置默认沉默期时放行(单个、批量、系列授权共用校验)
  • 3.5 测试环境核对:套餐 708 / 710 / 712 出现在未配置清单与打开前检查中;配置默认沉默期后回填套餐记录 20005;未配置时分配覆盖为实名即生效被拒绝,配置后放行
    • 核对结果(2026-10-27,部署 af57de0d):打开前检查与 silence_unconfigured=true 列表均为 708 / 710 / 712(3 个);新建购买即生效套餐 825 在系列授权 215 覆盖为实名即生效返回 1262,配置默认沉默期 20 天后放行(分配 289);加油包 709 填沉默期返回 1001;708 / 710 / 712 配置 30 天后回填工具清单"可回填 1、将过期 1",执行后记录 20005 waiting、截止 2026-10-18 23:59:59、流水 backfill,重复执行"待回填 0"。

4. 沉默期设置、判定与激活拦截(§4、§5)

  • 4.1 应用层 internal/application/packagesilence/resolve.go:ResolveUsageSilence(加锁、读实名事实、条件更新、写流水与审计);实名时间按 §5.1(卡 first_realname_at;设备取绑定卡 max(实名, 绑定) 的最小值;已实名但时间为空的视为期内并补写)
  • 4.2 订单 activatePackage 与自动购包 activatePackages:创建套餐记录时按 §4.1 设置沉默期并写流水(总开关关闭也写)
    • 备注:除订单 activatePackage 与自动购包 activatePackages 外,钱包自动续费(internal/application/assetautorenewal/renew.go)也创建套餐记录,同样接入(无人参与,只用默认值);三处都在创建记录的同一事务内调用 packagesilence.Setter.ApplyOnUsageCreatedInTx。
  • 4.3 后台购买:DTO 增加 silence_period_days、silence_reason;权限 package_silence:override;超默认值按政策决定直接生效或提交调整审批(silence_adjust_pending = true);无人参与路径只用默认值
    • 备注:审批实例 (business_type, business_id) 唯一,三种申请共用 package_silence_approval,因此购买时调整也建一个批次(path = adjust,迁移 000262 已含该取值与 adjust_* 字段),审批统一关联批次 ID。调整值经上下文传入 activatePackage(后台下单为钱包 / 线下即时支付,同一请求内发放套餐)。
  • 4.4 canActivatePendingUsage 在实名判断前调用 ResolveUsageSilence(仅总开关打开时拦截)
    • 备注:ActivateByRealname 不经过 canActivatePendingUsage,同样在主套餐激活前加了闸门;挂在过期主套餐下的待实名加油包随主套餐不激活(D21)。
  • 4.5 getNextPendingMainPackage 跳过 silence_status = expired;findAndActivateOrphanPackages 候选查询排除 expired(总开关打开时)
  • 4.6 全局排查 PackageUsageStatusPending 的 42 处查询,逐一确认过期套餐的处理(激活、排队、展示、统计、停复机评估、换货、退款),在任务备注中列出结论
    • 排查结论(2026-10-27):
      • 需改并已改:激活闸门 canActivatePendingUsage、实名激活 ActivateByRealname、排队队首 getNextPendingMainPackage、主套餐到期后提交下一个激活任务 polling.activateNextPackage、孤儿扫描 findAndActivateOrphanPackages、加油包挂靠 FindAttachableMainPackageForAddon(过期主套餐不能再挂加油包)、退款申请创建(4.7)。
      • 不改(过期仍按待生效处理是正确的):换货迁移 migration.go:105(随记录迁移)、退款 / 资产失效 InvalidatePackagesForRefund、InvalidateAllPackagesByAsset(失效照常)、加油包级联失效、实名轮询范围 polling_realname_scope.go(过期后仍需采集实名时间,延期后才能判定期内已实名并立即激活)、订单 / 自动购包 / 续费创建时的状态赋值、套餐到期提醒与续费查询 query/packageexpiry、query/assetautorenewal、运营报表 domain/operationsreport(统计口径不变)、HasPendingPackageUsage(优先轮询收敛条件)、agent_open_api 与各展示处的状态名(展示在 4.8 处理)。
      • 已知影响:资产若只有一笔沉默期已过期的待生效主套餐,钱包自动续费资格仍按"存在待生效主套餐"拦截(与现状一致,不在本变更范围)。
  • 4.7 退款申请创建拦截 silence_status = expired(总开关打开时),返回 silence_expired_not_refundable
  • 4.8 资产套餐列表 / 详情(后台与 H5)增加沉默期字段与状态名;waiting 但已过截止的展示为"沉默期已过期";总开关关闭时不返回沉默期展示字段与延期入口标记
  • 4.9 套餐沉默期流水查询接口

5. 扫描任务与总开关(§3.7、§5.4)

  • 5.1 定时任务 package:silence:scan:每 10 分钟、单批 200、最多 20 批、Unique 9 分钟、MaxRetry 3;只在总开关打开时处理;注册到 worker 与调度
  • 5.2 总开关读取(系统配置缓存)与各入口的统一判断函数
  • 5.3 总开关写接口(仅超级管理员):打开前 4 项检查,不满足返回 silence_switch_precheck_failed 与未满足项数量;切换写审计 package.silence_switch_changed
  • 5.4 关闭时延期资格、下单、线下登记、特殊延期申请返回 silence_feature_disabled;已发生的支付回调、查单、审批回调不受开关影响

6. 延期政策(§3.6)

  • 6.1 政策应用层:列表、详情、新增、编辑、启停、查询某套餐当前匹配政策;保存时重叠校验;写审计
  • 6.2 后台 Handler 与路由,权限 package_silence:policy

7. 延期办理(§7、§8)

  • 7.1 延期资格接口(后台与 H5):按 §12.2 返回全部字段;前置条件与原因码
  • 7.2 在线下单(H5 自助):参照 agentrecharge/online_creation.go,创建延期订单、批次、明细与 tb_payment,商户池路由并 FreezeRoute,冻结政策快照与金额
  • 7.3 后台二维码:运营创建订单(上传申请依据),调用渠道适配器生成支付内容写入 qr_content 返回;已有待支付订单时先查单,未付关闭旧单、已付不新建
  • 7.4 支付回调:internal/handler/callback/payment.go 按 order_type = silence_extension 分发到延期用例;金额核对;pending_payment → paid → completed 条件更新幂等
  • 7.5 支付恢复:未支付订单查单与超时关闭(参照 recover_online_payment.go),处理"订单不存在"
  • 7.6 线下登记:参照 offline_creation.go,收款方式字典、交易单号唯一、实收金额等于计算金额、凭证;按政策快照决定提交审批或直接执行;权限 package_silence:offline
  • 7.7 特殊延期:单个申请与批量导入(Excel 明细、逐行校验);免费次数校验与并发控制;优惠价登记收款信息;按政策快照决定审批或直接执行;批量权限 package_silence:extend_batch
  • 7.8 延期执行用例(§8.6):同事务锁载体与套餐、重校验、更新字段、写明细 / 批次 / 流水 / 审计;提交后调用 ResolveUsageSilence;以明细状态条件更新保证幂等
  • 7.9 执行异常:明细 exception;在线订单按冻结商户原路退款(复用退款渠道适配)并写订单退款字段;后台异常明细重试 / 关闭接口,权限 package_silence:exception
  • 7.10 确认延期订单不触发佣金计算、不计入代理累计充值与业绩

8. 企微审批(§11.3)

  • 8.1 接入 internal/application/approval 创建与可靠提交:业务类型 package_silence_approval,申请类型控件与三种申请的表单映射(§11.3 表);发起人未绑定企微成员拒绝提交
  • 8.2 回调分发:按审批实例关联的调整申请 / 延期批次 / 延期订单处理通过、驳回、撤回;重复事件只处理一次
  • 8.3 调整审批通过后重算截止日并调用 ResolveUsageSilence;驳回保持默认值并立即判定

9. 换货与首次购买套餐时间(§9、§17)

  • 9.1 换货迁移写沉默期流水(换货单号、前后资产、截止时间);确认 expired 记录随迁移且不重算
  • 9.2 首次购买套餐时间写入函数:订单 activatePackage 与自动购包入口调用;条件 first_purchase_at IS NULL AND generation = 订单世代;绑定在设备上的卡跳过;影响行数为 1 时插入首次购买套餐记录
  • 9.3 换货:旧资产世代 +1 的同一事务清空 first_purchase_at 并结束旧记录;换货后的新资产不因换货写入,当前世代真正购买套餐时才记录
    • 备注(2026-10-27):按业务决定取消"新资产写换货完成时间与 exchange 记录",并把"出库时间"更名为"首次购买套餐时间"(字段 first_purchase_at、表 tb_asset_first_purchase_record、接口 /first-purchase-records),迁移 000266 在测试库已 up → down → up。
  • 9.4 卡 / 设备列表返回并可按首次购买套餐时间筛选;详情与资产解析返回;导出增加"首次购买套餐时间"列;首次购买套餐记录查询接口

10. 展示、导出、文档与回填工具(§12、§6、§17.4)

  • 10.1 延期订单列表、详情、导出(字段与线上订单一致);H5 我的延期订单;即将过期 / 已过期套餐列表与导出;延期批次与明细查询
  • 10.2 财务交易流水(internal/query/audit/finance.go)纳入延期订单支付类型
  • 10.3 新增 Handler 同步路由、cmd/api/docs.go、cmd/gendocs/main.go,重新生成 OpenAPI 文档
  • 10.4 沉默期回填工具(cmd/):--dry-run 输出影响清单(§6 第 7 条各项);执行模式分批、只处理空值、可重复执行、按实名时间判定 ended、写流水
  • 10.5 首次购买套餐时间回填工具:按当前世代最早已支付 / 已退款套餐订单回填,排除绑定在设备上的卡,插入 backfill 记录,输出可回填数与无订单数
  • 10.6 前端接口变更说明(新增接口、字段、错误码、总开关对页面的影响)
  • 10.7 gofmt、go build ./...、go vet、openspec validate add-package-silence-period

11. 测试环境验证(§14、§17.6)

  • 11.1 部署后确认总开关关闭时行为不变:新购买写入沉默期字段,过期后实名仍激活,延期接口返回 silence_feature_disabled
    • 验证结果(2026-10-27,部署 69ec98aa):总开关 enabled=false;代理 539 未实名卡 8019 后台钱包购买验证套餐 823(订单 892)→ 套餐记录 20175 waiting、30 天、截止 2026-11-26 23:59:59,流水 set / order_paid,首次购买套餐时间与记录写入;资格、试算、二维码下单、线下登记、特殊延期均返回 1274,未产生延期订单 / 批次 / 明细;把 20175 截止改为 2026-10-20 后将卡标为已实名,孤儿扫描 10 秒内激活(status=1),silence_status 保持 waiting、不写判定流水。
    • 发现并修复(69ec98aa):后台平台自营资产与赠送套餐线下订单 buyer_type=personal,activateMainPackage 误判为 C 端购买,未实名也直接生效、不进沉默期;增加 order.Source == client 条件后,代理卡 8030 线下购买赠送套餐 824(订单 893)→ 记录 20176 待实名、waiting、30 天。
    • 待 11.10 清理:套餐 822 / 823 / 824 / 825、分配 288 / 289、订单 891(卡 11895)/ 892(卡 8019,代理 539 钱包扣 1 分)/ 893(卡 8030)及其套餐记录、沉默期流水、首次购买套餐时间与记录;卡 8019 恢复 real_name_status=0、first_realname_at=NULL、enable_polling=true。
  • 11.2 总开关打开前检查:逐项制造不满足条件,确认拒绝及返回内容;全部满足后打开成功并写审计
    • 进度(2026-10-27):未满足时 PUT /switch 返回 1275 与 failed_items;临时停用默认政策后 4 项全部拦下(已恢复);测试库 16 个实名即生效套餐与 708 / 710 / 712 已配置默认沉默期 30 天(各写审计 package.silence_default_changed),回填后前 3 项通过;剩"沉默期企微审批场景"待业务建模板后配置,再验证打开成功与审计。
  • 11.3 沉默期设置、购买时调整(直接生效 / 审批通过 / 驳回 / 免审批)、实名与过期判定(§14 第 1~12 条)
  • 11.4 延期资格与计费(§14 第 13~15 条,含示例表四种情况)
  • 11.5 四种延期办理(§14 第 16~21 条):H5 微信或支付宝真实小额支付、后台二维码扫码、线下登记审批、特殊与免费延期、批量部分失败、跨天支付异常退款
  • 11.6 延期政策与免审批开关(§14 第 22~23 条)
  • 11.7 换货与历史回填(§14 第 24~25 条):回填 dry-run 清单与执行、重复执行结果不变
  • 11.8 首次购买套餐时间(§17.6 全部场景)
  • 11.9 应急关闭:打开后关闭,确认规则暂停、数据保留、进行中支付回调照常完成
  • 11.10 清理本变更创建的测试数据,记录验证结果到本文件

实施备注(2026-10-27)

  • 迁移编号 000259~000266(000266 为首次购买套餐时间更名;测试库 junhong_cmp_test 已执行 up → down 7 → up,默认政策、三项系统配置、扫描索引、部分唯一索引、审批白名单核对正确)。
  • 审批关联:三种申请共用 package_silence_approval,审批实例 business_id 统一为批次 ID;购买时调整也建批次(path = adjust),线下付费的批次与订单一一对应。
  • H5 自助与后台二维码都复用代理在线充值的渠道适配器 CreatePaymentURL,返回 qr_content(支付链接 / 二维码内容),未新增 JSAPI 支付方式。
  • 在线订单执行异常(跨天、金额不一致、订单关闭后到账等)自动按冻结商户原路退款,恢复任务 package:silence:order_recover 每 5 分钟重试;线下与特殊延期的异常只能人工重试或关闭。
  • 支付截止时间不跨天(不晚于当天 23:59:59),降低跨天支付进入异常的概率。
  • 总开关在通用系统配置接口中只读,只能通过 PUT /api/admin/package-silence/switch 切换;进程内缓存 5 秒。
  • 回填工具:cmd/silence-backfill(-target silence|first_purchase,默认只出清单,-execute 执行);2026-10-27 在测试库 dry-run:沉默期候选 1 条(套餐未配置默认沉默期、不回填),首次购买套餐时间可回填卡 9 / 设备 6、无订单卡 7、绑定在设备上的卡 15。
  • 前端说明:docs/2026-10-27-实名即生效套餐沉默期-前端接口变更说明.md。