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

@@ -8,6 +8,7 @@ import (
merchantPaymentApp "github.com/break/junhong_cmp_fiber/internal/application/merchantpayment"
notificationApp "github.com/break/junhong_cmp_fiber/internal/application/notification"
packageExpiryApp "github.com/break/junhong_cmp_fiber/internal/application/packageexpiry"
packagesilenceApp "github.com/break/junhong_cmp_fiber/internal/application/packagesilence"
roleApp "github.com/break/junhong_cmp_fiber/internal/application/role"
shopApp "github.com/break/junhong_cmp_fiber/internal/application/shop"
systemConfigApp "github.com/break/junhong_cmp_fiber/internal/application/systemconfig"
@@ -42,6 +43,7 @@ import (
notificationQuery "github.com/break/junhong_cmp_fiber/internal/query/notification"
operationsreportquery "github.com/break/junhong_cmp_fiber/internal/query/operationsreport"
packageExpiryQuery "github.com/break/junhong_cmp_fiber/internal/query/packageexpiry"
packagesilenceQuery "github.com/break/junhong_cmp_fiber/internal/query/packagesilence"
packagetrafficalertquery "github.com/break/junhong_cmp_fiber/internal/query/packagetrafficalert"
priorityPollingQuery "github.com/break/junhong_cmp_fiber/internal/query/prioritypolling"
shopQuery "github.com/break/junhong_cmp_fiber/internal/query/shop"
@@ -126,6 +128,7 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
registerPaymentMethodConfigDefinitions(systemConfigRegistry, deps.Logger)
gatewaylimit.RegisterConfigDefinitions(systemConfigRegistry, deps.Logger)
carrierqueue.RegisterConfigDefinitions(systemConfigRegistry, deps.Logger)
packagesilenceApp.RegisterConfigDefinitions(systemConfigRegistry, deps.Logger)
systemConfigAlerts := systemConfigInfra.NewLogAlertSink(deps.Logger)
var systemConfigCache systemConfigInfra.Cache
if deps.Redis != nil {
@@ -313,11 +316,16 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
ShopSeriesGrant: admin.NewShopSeriesGrantHandler(svc.ShopSeriesGrant),
AdminOrder: admin.NewOrderHandler(svc.Order, validate),
AdminExchange: admin.NewExchangeHandler(svc.Exchange, exchangeQuery.NewListQuery(deps.DB), validate),
PaymentCallback: callback.NewPaymentHandler(
svc.Order, svc.Recharge, rechargeOrderService, svc.AgentRecharge,
deps.WechatPayment, svc.WechatConfig, paymentStore,
svc.AgentRechargePaymentConfirm, integrationlog.NewRepository(deps.DB), deps.Logger,
),
PaymentCallback: func() *callback.PaymentHandler {
handler := callback.NewPaymentHandler(
svc.Order, svc.Recharge, rechargeOrderService, svc.AgentRecharge,
deps.WechatPayment, svc.WechatConfig, paymentStore,
svc.AgentRechargePaymentConfirm, integrationlog.NewRepository(deps.DB), deps.Logger,
)
// 沉默期延期支付回调:按 order_type = silence_extension 分发
handler.SetSilencePaymentConfirmer(svc.PackageSilence.Extensions)
return handler
}(),
CTCCRealnameCallback: callback.NewCTCCRealnameHandler(
carriercallback.NewCTCCRealnameTranslator(), carriercallback.NewCTCCCardResolver(deps.DB),
integrationlog.NewRepository(deps.DB), svc.CardObservation, svc.CardObservationSeries, systemConfigReader, deps.Logger,
@@ -338,6 +346,9 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
PollingConfig: admin.NewPollingConfigHandler(svc.PollingConfig),
GatewayRateLimit: admin.NewGatewayRateLimitHandler(pollingSvcPkg.NewGatewayRateLimitService(deps.DB, auditInfra.NewWriter(auditInfra.NewRegistry(), nil), deps.GatewayLimit), deps.Redis, validate),
PollingConcurrency: admin.NewPollingConcurrencyHandler(svc.PollingConcurrency),
PackageSilence: admin.NewPackageSilenceHandler(svc.PackageSilence.Switches, svc.PackageSilence.Policies,
svc.PackageSilence.Extensions, packagesilenceQuery.NewQuery(deps.DB), validate),
ClientPackageSilence: app.NewClientPackageSilenceHandler(svc.PackageSilence.Extensions, packagesilenceQuery.NewQuery(deps.DB), validate),
PollingPause: admin.NewPollingPauseHandler(assetPollingSvc,
pollingSvcPkg.NewPollingStatusImportService(deps.DB, deps.QueueClient, deps.Logger), validate),
CarrierScheduler: admin.NewCarrierSchedulerHandler(pollingSvcPkg.NewCarrierSchedulerService(deps.DB, deps.Redis,

View File

@@ -0,0 +1,56 @@
package bootstrap
import (
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
"gorm.io/gorm"
approvalApp "github.com/break/junhong_cmp_fiber/internal/application/approval"
"github.com/break/junhong_cmp_fiber/internal/application/merchantpayment"
packagesilenceApp "github.com/break/junhong_cmp_fiber/internal/application/packagesilence"
refundchannelApp "github.com/break/junhong_cmp_fiber/internal/application/refundchannel"
auditInfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
packageSvc "github.com/break/junhong_cmp_fiber/internal/service/package"
)
// packageSilenceBundle 是沉默期相关用例的装配结果(API 与 Worker 共用)。
type packageSilenceBundle struct {
Switch *packagesilenceApp.Switch
Resolver *packagesilenceApp.Resolver
Setter *packagesilenceApp.Setter
Outbound *packagesilenceApp.OutboundRecorder
// 以下仅 API 与 Worker 中延期相关入口装配
Extensions *packagesilenceApp.ExtensionService
Policies *packagesilenceApp.PolicyService
Switches *packagesilenceApp.SwitchService
}
// newPackageSilenceBundle 装配沉默期总开关、判定、设置与出库时间用例,并把激活闸门注入激活服务。
func newPackageSilenceBundle(db *gorm.DB, redisClient *redis.Client, logger *zap.Logger, auditWriter *auditInfra.Writer, activation *packageSvc.ActivationService) *packageSilenceBundle {
sw := packagesilenceApp.NewDefaultSwitch(db, redisClient, logger)
resolver := packagesilenceApp.NewResolver(db, sw, auditWriter, logger)
if activation != nil {
resolver.SetActivator(activation)
activation.SetSilenceGate(resolver, sw)
}
return &packageSilenceBundle{
Switch: sw, Resolver: resolver,
Setter: packagesilenceApp.NewSetter(db, auditWriter),
Outbound: packagesilenceApp.NewOutboundRecorder(auditWriter),
Extensions: packagesilenceApp.NewExtensionService(db, resolver, auditWriter, logger),
Policies: packagesilenceApp.NewPolicyService(db, auditWriter),
Switches: packagesilenceApp.NewSwitchService(db, redisClient, sw, auditWriter),
}
}
// wireExtensions 注入延期用例的审批、权限、资产归属、商户池与支付渠道。
func (b *packageSilenceBundle) wireExtensions(approval approvalApp.Port, perms packagesilenceApp.PermissionChecker, ownership packagesilenceApp.OwnershipChecker,
runtime *merchantpayment.RuntimeLoader, ports packagesilenceApp.PaymentPorts, refunder refundchannelApp.Refunder) {
b.Setter.SetApprovalPort(approval)
b.Setter.SetPermissionChecker(perms)
b.Extensions.SetApprovalPort(approval)
b.Extensions.SetPermissionChecker(perms)
b.Extensions.SetOwnershipChecker(ownership)
b.Extensions.SetPayment(runtime, ports, refunder)
b.Policies.SetPermissionChecker(perms)
}

View File

@@ -2,6 +2,7 @@ package bootstrap
import (
"context"
packagesilenceApp "github.com/break/junhong_cmp_fiber/internal/application/packagesilence"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
"time"
@@ -97,6 +98,7 @@ type services struct {
AssetAudit *assetAuditSvc.Service
Role *roleSvc.Service
Permission *permissionSvc.Service
PackageSilence *packageSilenceBundle
PersonalCustomer *personalCustomerSvc.Service
ClientAuth *clientAuthSvc.Service
Shop *shopSvc.Service
@@ -267,6 +269,7 @@ func initServices(s *stores, deps *Dependencies) *services {
// 激活前流量结算:资产从无生效主套餐变为有生效主套餐前,对已切换日快照计量的卡查一次流量入账。
preActivationSettler := cardObservationInfra.NewPreActivationSettler(deps.DB, deps.GatewayClient, cardObservationService, postgres.NewCarrierStore(deps.DB), deps.Logger)
packageActivation.SetPreActivationSettler(preActivationSettler)
packageSilence := newPackageSilenceBundle(deps.DB, deps.Redis, deps.Logger, auditWriter, packageActivation)
stopResumeService := iotCardSvc.NewStopResumeService(
deps.Redis,
@@ -338,6 +341,8 @@ func initServices(s *stores, deps *Dependencies) *services {
orderService.SetPaymentIntegrationLog(integrationlog.NewRepository(deps.DB))
orderService.SetObservationSeriesEventWriter(observationSeriesEvents)
orderService.SetPriorityEventWriter(priorityEvents)
orderService.SetSilenceSetter(packageSilence.Setter)
orderService.SetOutboundRecorder(packageSilence.Outbound)
walletOutbox := outbox.NewRepository()
walletDebitEvents := walletinfra.NewDebitEventWriter(walletOutbox, auditWriter)
orderService.SetAgentWalletDebitService(walletapp.NewDebitService(walletDebitEvents, nil))
@@ -392,6 +397,7 @@ func initServices(s *stores, deps *Dependencies) *services {
refundService.SetNotificationOutbox(walletOutbox)
refundService.SetPaymentMerchantRuntime(merchantpayment.NewRuntimeLoader(deps.DB, deps.Redis))
refundService.SetLifecycleAudit(auditWriter)
refundService.SetSilenceSwitch(packageSilence.Switch)
// 渠道原路退款的登记与执行共用同一用例:API 侧只登记待执行事实与可靠事件,
// 真正的渠道调用由 Worker 消费该事件执行。
refundService.SetChannelRefundService(
@@ -405,10 +411,12 @@ func initServices(s *stores, deps *Dependencies) *services {
exchangeService := exchangeSvc.New(deps.DB, s.ExchangeOrder, s.IotCard, s.Device, s.AssetWallet, s.AssetWalletTransaction, s.PackageUsage, s.PackageUsageDailyRecord, s.ResourceTag, customerBinding, deps.Logger)
exchangeService.SetShippingCreatedNotifier(exchangeApp.NewShippingCreatedNotifier(exchangeInfra.NewShippingNotificationWriter(outbox.NewRepository())))
exchangeService.SetAccessAudit(auditWriter)
exchangeService.SetSilenceHook(packagesilenceApp.NewExchangeHook(packageSilence.Setter, packageSilence.Outbound))
exchangeService.SetPhoneAssetAssociationStore(s.PhoneAssetAssociation)
assetService := assetSvc.New(deps.DB, s.Device, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.DeviceSimBinding, s.Shop, deps.Redis, iotCard, deps.GatewayClient, s.AssetIdentifier, s.Order, s.OrderItem, s.ExchangeOrder)
assetService.SetPhoneAssetAssociationStore(s.PhoneAssetAssociation)
assetService.SetAccessAudit(auditWriter)
assetService.SetSilenceSwitch(packageSilence.Switch)
agentOpenAPI := agentOpenAPISvc.New(assetService, packageService, orderService, shopCommission, stopResumeService, device, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.AgentWallet, s.DeviceSimBinding, s.Device)
wecomApplicationRepository := wecomInfra.NewApplicationRepository(deps.DB)
wecomSceneRepository := wecomInfra.NewSceneRepository(deps.DB)
@@ -453,6 +461,15 @@ func initServices(s *stores, deps *Dependencies) *services {
roleService.SetAccessAudit(deps.DB, deps.Redis, auditWriter)
permissionService := permissionSvc.New(s.Permission, s.AccountRole, s.RolePermission, account, deps.Redis)
permissionService.SetAccessAudit(deps.DB, auditWriter)
packageSilence.wireExtensions(approvalCreationService, permissionService, customerBinding,
merchantpayment.NewRuntimeLoader(deps.DB, deps.Redis),
packagesilenceApp.PaymentPorts{
Wechat: paymentInfra.NewWechatWebAdapter(wechat.NewRedisCache(deps.Redis), paymentIntegration, deps.Logger),
Alipay: paymentInfra.NewAlipayWapAdapter(paymentIntegration, deps.Logger),
Fuiou: paymentInfra.NewFuiouScanAdapter(paymentIntegration, deps.Logger),
},
paymentInfra.NewRefundAdapter(wechat.NewRedisCache(deps.Redis), deps.Logger),
)
shopService := shopSvc.New(s.Shop, s.Account, s.ShopRole, s.Role)
shopService.SetAccessAudit(deps.DB, deps.Redis, auditWriter)
commissionWithdrawal := commissionWithdrawalSvc.New(deps.DB, s.Shop, s.Account, s.AgentWallet, s.AgentWalletTransaction, s.CommissionWithdrawalRequest)
@@ -513,6 +530,7 @@ func initServices(s *stores, deps *Dependencies) *services {
AssetAudit: assetAudit,
Role: roleService,
Permission: permissionService,
PackageSilence: packageSilence,
PersonalCustomer: personalCustomerSvc.NewService(deps.DB, s.PersonalCustomer, s.PersonalCustomerPhone, deps.Logger, auditWriter),
ClientAuth: clientAuthSvc.New(
deps.DB,

View File

@@ -67,6 +67,8 @@ type Handlers struct {
GatewayRateLimit *admin.GatewayRateLimitHandler
CarrierScheduler *admin.CarrierSchedulerHandler
PollingPause *admin.PollingPauseHandler
PackageSilence *admin.PackageSilenceHandler
ClientPackageSilence *app.ClientPackageSilenceHandler
PollingConcurrency *admin.PollingConcurrencyHandler
PollingMonitoring *admin.PollingMonitoringHandler
PollingAlert *admin.PollingAlertHandler

View File

@@ -12,6 +12,8 @@ import (
cardObservationApp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
carrierThresholdApp "github.com/break/junhong_cmp_fiber/internal/application/carrierthreshold"
employeecollectionApp "github.com/break/junhong_cmp_fiber/internal/application/employeecollection"
"github.com/break/junhong_cmp_fiber/internal/application/merchantpayment"
packagesilenceApp "github.com/break/junhong_cmp_fiber/internal/application/packagesilence"
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
assetAutoRenewalInfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/assetautorenewal"
auditInfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
@@ -19,6 +21,7 @@ import (
carrierThresholdInfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/carrierthreshold"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
paymentInfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/payment"
prioritypollingInfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/prioritypolling"
walletinfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/wallet"
assetAutoRenewalQuery "github.com/break/junhong_cmp_fiber/internal/query/assetautorenewal"
@@ -31,6 +34,7 @@ import (
pollingSvc "github.com/break/junhong_cmp_fiber/internal/service/polling"
purchaseValidationSvc "github.com/break/junhong_cmp_fiber/internal/service/purchase_validation"
"github.com/break/junhong_cmp_fiber/pkg/queue"
"github.com/break/junhong_cmp_fiber/pkg/wechat"
)
type workerServices struct {
@@ -92,6 +96,14 @@ func initWorkerServices(stores *queue.WorkerStores, deps *WorkerDependencies) *q
deps.Logger,
)
activationService.SetLifecycleAudit(auditWriter)
packageSilence := newPackageSilenceBundle(deps.DB, deps.Redis, deps.Logger, auditWriter, activationService)
// Worker 内延期用例只承担审批通过后的执行、查单恢复与异常原路退款,不创建新申请。
silencePaymentLog := integrationlog.NewRepository(deps.DB)
packageSilence.Extensions.SetPayment(merchantpayment.NewRuntimeLoader(deps.DB, deps.Redis), packagesilenceApp.PaymentPorts{
Wechat: paymentInfra.NewWechatWebAdapter(wechat.NewRedisCache(deps.Redis), silencePaymentLog, deps.Logger),
Alipay: paymentInfra.NewAlipayWapAdapter(silencePaymentLog, deps.Logger),
Fuiou: paymentInfra.NewFuiouScanAdapter(silencePaymentLog, deps.Logger),
}, paymentInfra.NewRefundAdapter(wechat.NewRedisCache(deps.Redis), deps.Logger))
usageService.SetLifecycleAudit(auditWriter)
resetService.SetLifecycleAudit(auditWriter)
@@ -186,6 +198,8 @@ func initWorkerServices(stores *queue.WorkerStores, deps *WorkerDependencies) *q
orderService.SetAgentWalletDebitService(walletapp.NewDebitService(walletDebitEvents, nil))
orderService.SetObservationSeriesEventWriter(observationSeriesEvents)
orderService.SetPriorityEventWriter(priorityEvents)
orderService.SetSilenceSetter(packageSilence.Setter)
orderService.SetOutboundRecorder(packageSilence.Outbound)
// 创建停复机服务并注入回调:流量耗尽自动停机、套餐激活/重置/支付后自动复机
stopResumeService := iotCardSvc.NewStopResumeService(
@@ -218,6 +232,7 @@ func initWorkerServices(stores *queue.WorkerStores, deps *WorkerDependencies) *q
ObservationEvents: observationSeriesEvents,
PriorityEvents: priorityEvents,
})
assetAutoRenewalService.SetSilenceRecorder(packagesilenceApp.PurchaseRecorder{Setter: packageSilence.Setter, Outbound: packageSilence.Outbound})
activationService.SetObservationSeriesEventWriter(observationSeriesEvents)
activationService.SetPriorityEventWriter(priorityEvents)
usageService.SetStopResumeCallback(stopResumeService)
@@ -253,5 +268,10 @@ func initWorkerServices(stores *queue.WorkerStores, deps *WorkerDependencies) *q
DeviceBatchAllocator: deviceBatchAllocator,
AssetPolling: assetPollingService,
IotCardPolling: iotCardAuditService,
SilenceSwitch: packageSilence.Switch,
SilenceResolver: packageSilence.Resolver,
SilenceSetter: packageSilence.Setter,
SilenceOutbound: packageSilence.Outbound,
SilenceExtensions: packageSilence.Extensions,
}
}