feat: 实名即生效套餐沉默期、延期办理与资产出库时间
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 8m39s

- 迁移 000259~000265:套餐记录沉默期字段与扫描索引、套餐默认沉默期、延期政策/流水/订单/批次/明细 5 张表、默认政策与系统配置(总开关默认关闭)、企微审批白名单、卡与设备出库时间及出库记录
- 沉默期设置(订单发放、自动购包、钱包续费)、后台购买时调整(超默认值按政策免审或企微审批)、统一判定与激活闸门、排队与孤儿扫描跳过过期套餐、过期套餐禁止退款、每 10 分钟扫描
- 总开关专用接口与打开前 4 项检查;延期政策;延期资格与计费;H5 自助与后台二维码在线支付(商户池路由)、支付回调与查单恢复、线下登记、特殊/批量延期、异常重试与原路退款;沉默期审批场景与终态处理
- 换货沉默期流水与出库时间;卡/设备列表、详情、导出与出库记录接口;延期订单与即将过期套餐列表及导出;财务流水纳入延期支付
- 沉默期与出库时间回填工具 cmd/silence-backfill;前端接口变更说明

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
邹进
2026-10-27 16:32:58 +09:00
parent d2b0d2f534
commit 909f4d0ec2
112 changed files with 10473 additions and 82 deletions

View File

@@ -14,6 +14,7 @@ import (
"gorm.io/gorm/clause"
cardObservationApp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
packagesilenceapp "github.com/break/junhong_cmp_fiber/internal/application/packagesilence"
priorityapp "github.com/break/junhong_cmp_fiber/internal/application/prioritypolling"
packagedomain "github.com/break/junhong_cmp_fiber/internal/domain/package"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
@@ -54,6 +55,18 @@ type AutoPurchaseHandler struct {
observationSeriesEvents cardObservationApp.SeriesEventWriter
priorityEvents priorityapp.PriorityEventWriter
auditWriter *audit.Writer
silenceSetter *packagesilenceapp.Setter
outboundRecorder *packagesilenceapp.OutboundRecorder
}
// SetSilenceSetter 注入沉默期设置用例;自动购包是无人参与路径,只用套餐默认沉默期。
func (h *AutoPurchaseHandler) SetSilenceSetter(setter *packagesilenceapp.Setter) {
h.silenceSetter = setter
}
// SetOutboundRecorder 注入资产出库时间写入用例。
func (h *AutoPurchaseHandler) SetOutboundRecorder(recorder *packagesilenceapp.OutboundRecorder) {
h.outboundRecorder = recorder
}
// NewAutoPurchaseHandler 创建充值后自动购包处理器
@@ -589,6 +602,7 @@ func (h *AutoPurchaseHandler) activatePackages(
return err
}
createdUsage := false
for _, pkg := range packages {
var existingUsage model.PackageUsage
err := tx.Where("order_id = ? AND package_id = ?", order.ID, pkg.ID).First(&existingUsage).Error
@@ -598,6 +612,7 @@ func (h *AutoPurchaseHandler) activatePackages(
if err != gorm.ErrRecordNotFound {
return err
}
createdUsage = true
if pkg.PackageType == constants.PackageTypeFormal {
if err = h.activateMainPackage(ctx, tx, order, pkg, carrierType, carrierID, now); err != nil {
@@ -611,7 +626,10 @@ func (h *AutoPurchaseHandler) activatePackages(
}
}
}
if createdUsage && h.outboundRecorder != nil {
// 出库时间:资产当前世代第 1 次购买套餐的支付时间
return h.outboundRecorder.RecordForOrderInTx(ctx, tx, order)
}
return nil
}
@@ -722,6 +740,16 @@ func (h *AutoPurchaseHandler) activateMainPackage(
}).Error; err != nil {
return err
}
// 沉默期:无人参与路径只用套餐默认值(上下文不携带调整值)
if h.silenceSetter != nil {
paidAt := now
if order.PaidAt != nil {
paidAt = *order.PaidAt
}
if err = h.silenceSetter.ApplyOnUsageCreatedInTx(ctx, tx, usage, pkg, paidAt, constants.SilenceTriggerAutoPurchase); err != nil {
return err
}
}
// 仅在生效状态分支追加优先轮询请求:待生效(含等待实名激活)不属于触发场景。
if status == constants.PackageUsageStatusActive {

View File

@@ -0,0 +1,75 @@
package task
import (
"context"
"github.com/hibiken/asynq"
"go.uber.org/zap"
packagesilenceapp "github.com/break/junhong_cmp_fiber/internal/application/packagesilence"
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// PackageSilenceScanHandler 处理沉默期扫描任务 package:silence:scan(每 10 分钟)。
// 总开关关闭时直接返回;判定使用条件更新,重复执行幂等。
type PackageSilenceScanHandler struct {
resolver *packagesilenceapp.Resolver
logger *zap.Logger
}
// NewPackageSilenceScanHandler 创建沉默期扫描任务处理器。
func NewPackageSilenceScanHandler(resolver *packagesilenceapp.Resolver, logger *zap.Logger) *PackageSilenceScanHandler {
return &PackageSilenceScanHandler{resolver: resolver, logger: logger}
}
// Handle 执行一轮沉默期扫描。
func (h *PackageSilenceScanHandler) Handle(ctx context.Context, _ *asynq.Task) error {
if h.resolver == nil {
return nil
}
ctx = auditcontext.With(ctx, auditcontext.Context{
ActorKind: constants.AuditActorScheduledJob, ActorID: constants.TaskTypePackageSilenceScan,
ActorName: "沉默期扫描任务", Source: constants.AuditSourceScheduler,
})
result, err := h.resolver.Scan(ctx)
if err != nil {
h.logger.Error("沉默期扫描失败", zap.Error(err))
return err
}
if result.Scanned > 0 {
h.logger.Info("沉默期扫描完成", zap.Int("scanned", result.Scanned), zap.Int("expired", result.Expired), zap.Int("ended", result.Ended))
}
return nil
}
// PackageSilenceOrderRecoverHandler 处理延期订单查单恢复任务(每 5 分钟):查单、超时关闭、异常订单原路退款。
type PackageSilenceOrderRecoverHandler struct {
extensions *packagesilenceapp.ExtensionService
logger *zap.Logger
}
// NewPackageSilenceOrderRecoverHandler 创建延期订单恢复任务处理器。
func NewPackageSilenceOrderRecoverHandler(extensions *packagesilenceapp.ExtensionService, logger *zap.Logger) *PackageSilenceOrderRecoverHandler {
return &PackageSilenceOrderRecoverHandler{extensions: extensions, logger: logger}
}
// Handle 执行一轮延期订单恢复。
func (h *PackageSilenceOrderRecoverHandler) Handle(ctx context.Context, _ *asynq.Task) error {
if h.extensions == nil {
return nil
}
ctx = auditcontext.With(ctx, auditcontext.Context{
ActorKind: constants.AuditActorScheduledJob, ActorID: constants.TaskTypePackageSilenceOrderRecover,
ActorName: "沉默期延期订单恢复任务", Source: constants.AuditSourceScheduler,
})
result, err := h.extensions.Recover(ctx)
if err != nil {
h.logger.Error("沉默期延期订单恢复失败", zap.Error(err))
return err
}
if result.Confirmed+result.Closed+result.Refunded > 0 {
h.logger.Info("沉默期延期订单恢复完成", zap.Int("confirmed", result.Confirmed), zap.Int("closed", result.Closed), zap.Int("refunded", result.Refunded))
}
return nil
}