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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:32:58 +09:00

117 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package packagesilence 实现实名即生效套餐沉默期的设置、判定、扫描、总开关与延期用例(add-package-silence-period)。
package packagesilence
import (
"context"
"strconv"
"sync"
"time"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/systemconfig"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// switchCacheTTL 是总开关在进程内的缓存时间;应急关闭最多延迟这么久在各进程生效。
const switchCacheTTL = 5 * time.Second
// RegisterConfigDefinitions 注册沉默期相关系统配置。
// 总开关注册为只读:通用系统配置接口不能修改,只能走带打开前检查的专用接口。
func RegisterConfigDefinitions(registry *systemconfig.Registry, logger *zap.Logger) {
if registry == nil {
return
}
minDays, maxDays := int64(constants.SilencePackageDaysMin), int64(constants.SilencePackageDaysMax)
minPrice, maxPrice := int64(0), int64(1000000)
definitions := []systemconfig.Definition{
{Key: constants.SystemConfigSilenceEnabled, Module: constants.SystemConfigModulePackage, ValueType: constants.SystemConfigTypeBool,
DefaultValue: "false", Readonly: true,
Description: "沉默期总开关:关闭时只记录沉默期,不拦截激活、不扫描、不展示、延期申请拒绝;只能通过沉默期总开关接口切换"},
{Key: constants.SystemConfigSilenceDefaultDays, Module: constants.SystemConfigModulePackage, ValueType: constants.SystemConfigTypeInt,
DefaultValue: strconv.Itoa(constants.SilenceDefaultPeriodDays), Description: "新建实名即生效套餐时预填的默认沉默期(天)",
Min: &minDays, Max: &maxDays},
{Key: constants.SystemConfigSilenceExtensionUnitPrice, Module: constants.SystemConfigModulePackage, ValueType: constants.SystemConfigTypeInt,
DefaultValue: strconv.Itoa(constants.SilenceDefaultUnitPrice), Description: "新建延期政策时预填的单价(分/月),实际计价以政策为准",
Min: &minPrice, Max: &maxPrice},
}
for _, definition := range definitions {
if _, exists := registry.Get(definition.Key); exists {
continue
}
if err := registry.Register(definition); err != nil && logger != nil {
logger.Error("注册沉默期系统配置失败,将使用代码默认值", zap.String("config_key", definition.Key), zap.Error(err))
}
}
}
// ConfigReader 读取系统配置的最小接缝。
type ConfigReader interface {
Get(ctx context.Context, key string) (string, error)
}
// Switch 读取沉默期总开关 package.silence.enabled。
// 读取失败时按关闭处理(行为与现状一致),不因配置故障误拦截激活或退款。
type Switch struct {
reader ConfigReader
logger *zap.Logger
mu sync.Mutex
cached bool
expiresAt time.Time
}
// NewSwitch 创建总开关读取器。
func NewSwitch(reader ConfigReader, logger *zap.Logger) *Switch {
if logger == nil {
logger = zap.NewNop()
}
return &Switch{reader: reader, logger: logger}
}
// NewDefaultSwitch 用独立注册表与读取器创建总开关(API 与 Worker 共用同一份数据库与缓存数据)。
func NewDefaultSwitch(db *gorm.DB, redisClient *redis.Client, logger *zap.Logger) *Switch {
registry := systemconfig.NewRegistry()
RegisterConfigDefinitions(registry, logger)
var cache systemconfig.Cache
if redisClient != nil {
cache = systemconfig.NewRedisCache(redisClient)
}
reader := systemconfig.NewReader(db, registry, cache, systemconfig.NewLogAlertSink(logger))
return NewSwitch(reader, logger)
}
// Enabled 返回总开关是否打开。
func (s *Switch) Enabled(ctx context.Context) bool {
if s == nil || s.reader == nil {
return false
}
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
if now.Before(s.expiresAt) {
return s.cached
}
value, err := s.reader.Get(ctx, constants.SystemConfigSilenceEnabled)
if err != nil {
s.logger.Warn("读取沉默期总开关失败,按关闭处理", zap.Error(err))
s.cached = false
} else {
s.cached = value == "true"
}
s.expiresAt = now.Add(switchCacheTTL)
return s.cached
}
// Invalidate 清除进程内缓存(本进程切换开关后立即生效)。
func (s *Switch) Invalidate() {
if s == nil {
return
}
s.mu.Lock()
s.expiresAt = time.Time{}
s.mu.Unlock()
}