All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 1m32s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
20 KiB
20 KiB
规则细节以
docs/沉默期技术方案/实名即生效套餐沉默期技术方案_v3.md(下称"技术方案")为准,任务中的 §n 指技术方案章节。测试环境是唯一验证面(PostgreSQLjunhong_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 并补写、调整审批中 → 不过期。
- 核对结果(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,第四种
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",执行后记录 20005waiting、截止 2026-10-18 23:59:59、流水backfill,重复执行"待回填 0"。
- 核对结果(2026-10-27,部署
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 处理)。 - 已知影响:资产若只有一笔沉默期已过期的待生效主套餐,钱包自动续费资格仍按"存在待生效主套餐"拦截(与现状一致,不在本变更范围)。
- 需改并已改:激活闸门
- 排查结论(2026-10-27):
- 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。
- 备注(2026-10-27):按业务决定取消"新资产写换货完成时间与
- 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)→ 套餐记录 20175waiting、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。
- 验证结果(2026-10-27,部署
- 11.2 总开关打开前检查:逐项制造不满足条件,确认拒绝及返回内容;全部满足后打开成功并写审计
- 进度(2026-10-27):未满足时
PUT /switch返回 1275 与failed_items;临时停用默认政策后 4 项全部拦下(已恢复);测试库 16 个实名即生效套餐与 708 / 710 / 712 已配置默认沉默期 30 天(各写审计package.silence_default_changed),回填后前 3 项通过;剩"沉默期企微审批场景"待业务建模板后配置,再验证打开成功与审计。
- 进度(2026-10-27):未满足时
- 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。