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>
161 lines
7.3 KiB
Go
161 lines
7.3 KiB
Go
package packagesilence
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
|
|
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
|
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
|
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
|
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
|
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
|
)
|
|
|
|
// SwitchService 切换沉默期总开关(技术方案 §3.7):仅超级管理员;打开前做 4 项检查;每次切换写审计。
|
|
type SwitchService struct {
|
|
db *gorm.DB
|
|
redis *redis.Client
|
|
sw *Switch
|
|
audit *audit.Writer
|
|
now func() time.Time
|
|
}
|
|
|
|
// NewSwitchService 创建总开关切换用例。
|
|
func NewSwitchService(db *gorm.DB, redisClient *redis.Client, sw *Switch, writer *audit.Writer) *SwitchService {
|
|
return &SwitchService{db: db, redis: redisClient, sw: sw, audit: writer, now: time.Now}
|
|
}
|
|
|
|
// Status 返回总开关当前值与打开前检查结果(页面展示用)。
|
|
func (s *SwitchService) Status(ctx context.Context) (*dto.SilenceSwitchResponse, error) {
|
|
items, err := s.Precheck(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &dto.SilenceSwitchResponse{Enabled: s.sw.Enabled(ctx), Precheck: items, PrecheckPassed: precheckPassed(items)}, nil
|
|
}
|
|
|
|
// Precheck 执行打开前检查,返回每一项的结果(未满足项带数量)。
|
|
func (s *SwitchService) Precheck(ctx context.Context) ([]dto.SilenceSwitchPrecheckItem, error) {
|
|
db := s.db.WithContext(ctx)
|
|
var unconfigured int64
|
|
if err := db.Model(&model.Package{}).Where(postgres.SilenceUnconfiguredPackageCondition("tb_package")).Count(&unconfigured).Error; err != nil {
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "统计未配置默认沉默期的套餐失败")
|
|
}
|
|
var unbackfilled int64
|
|
if err := db.Model(&model.PackageUsage{}).
|
|
Where("status = ? AND pending_realname_activation = ? AND master_usage_id IS NULL AND expiry_base_snapshot = ? AND silence_status IS NULL",
|
|
constants.PackageUsageStatusPending, true, constants.PackageExpiryBaseFromActivation).
|
|
Count(&unbackfilled).Error; err != nil {
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "统计未回填沉默期的套餐记录失败")
|
|
}
|
|
now := s.now()
|
|
var defaultPolicies int64
|
|
if err := db.Model(&model.PackageSilenceExtensionPolicy{}).
|
|
Where("scope_type = ? AND status = ? AND effective_from <= ? AND (effective_to IS NULL OR effective_to > ?)",
|
|
constants.SilencePolicyScopeAll, constants.SilencePolicyStatusEnabled, now, now).
|
|
Count(&defaultPolicies).Error; err != nil {
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询全部套餐延期政策失败")
|
|
}
|
|
var scenes int64
|
|
if err := db.Model(&model.WeComApprovalScene{}).
|
|
Where("business_type = ? AND status = ? AND template_id <> ''", constants.ApprovalBusinessTypePackageSilence, constants.StatusEnabled).
|
|
Count(&scenes).Error; err != nil {
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询沉默期审批场景失败")
|
|
}
|
|
return []dto.SilenceSwitchPrecheckItem{
|
|
{Code: constants.SilenceSwitchPrecheckUnconfiguredPackages, Name: "未配置默认沉默期的套餐", Passed: unconfigured == 0, Count: unconfigured},
|
|
{Code: constants.SilenceSwitchPrecheckUnbackfilledUsages, Name: "未回填沉默期的待实名套餐记录", Passed: unbackfilled == 0, Count: unbackfilled},
|
|
{Code: constants.SilenceSwitchPrecheckDefaultPolicyMissing, Name: "启用中的全部套餐延期政策", Passed: defaultPolicies > 0, Count: defaultPolicies},
|
|
{Code: constants.SilenceSwitchPrecheckApprovalSceneMissing, Name: "沉默期企微审批场景已配置并启用", Passed: scenes > 0, Count: scenes},
|
|
}, nil
|
|
}
|
|
|
|
func precheckPassed(items []dto.SilenceSwitchPrecheckItem) bool {
|
|
for _, item := range items {
|
|
if !item.Passed {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Toggle 打开或关闭总开关。打开前检查未通过时拒绝,返回 silence_switch_precheck_failed 与未满足项。
|
|
// 关闭(应急)不做检查:规则暂停、数据保留,已发生的支付回调与审批回调照常处理。
|
|
func (s *SwitchService) Toggle(ctx context.Context, enable bool) (*dto.SilenceSwitchResponse, error) {
|
|
if middleware.GetUserTypeFromContext(ctx) != constants.UserTypeSuperAdmin {
|
|
return nil, errors.New(errors.CodeForbidden, "只有超级管理员可以切换沉默期总开关")
|
|
}
|
|
operatorID := middleware.GetUserIDFromContext(ctx)
|
|
items, err := s.Precheck(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if enable && !precheckPassed(items) {
|
|
failed := make([]dto.SilenceSwitchPrecheckItem, 0, len(items))
|
|
for _, item := range items {
|
|
if !item.Passed {
|
|
failed = append(failed, item)
|
|
}
|
|
}
|
|
return nil, errors.NewWithData(errors.CodeSilenceSwitchPrecheckFailed, map[string]any{"failed_items": failed},
|
|
"沉默期总开关打开前检查未通过")
|
|
}
|
|
value := "false"
|
|
if enable {
|
|
value = "true"
|
|
}
|
|
now := s.now()
|
|
var before string
|
|
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
if err := tx.Exec("SELECT pg_advisory_xact_lock(hashtext(?))", constants.SystemConfigSilenceEnabled).Error; err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "锁定沉默期总开关失败")
|
|
}
|
|
var existing model.SystemConfig
|
|
findErr := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("config_key = ?", constants.SystemConfigSilenceEnabled).First(&existing).Error
|
|
switch {
|
|
case findErr == gorm.ErrRecordNotFound:
|
|
before = "false"
|
|
existing = model.SystemConfig{
|
|
ConfigKey: constants.SystemConfigSilenceEnabled, ConfigValue: value, ValueType: constants.SystemConfigTypeBool,
|
|
Module: constants.SystemConfigModulePackage, Description: "沉默期总开关", IsReadonly: true,
|
|
Creator: operatorID, Updater: operatorID, CreatedAt: now, UpdatedAt: now,
|
|
}
|
|
if err := tx.Create(&existing).Error; err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "写入沉默期总开关失败")
|
|
}
|
|
case findErr != nil:
|
|
return errors.Wrap(errors.CodeDatabaseError, findErr, "查询沉默期总开关失败")
|
|
default:
|
|
before = existing.ConfigValue
|
|
if err := tx.Model(&model.SystemConfig{}).Where("id = ?", existing.ID).
|
|
Updates(map[string]any{"config_value": value, "updater": operatorID, "updated_at": now}).Error; err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "更新沉默期总开关失败")
|
|
}
|
|
}
|
|
if s.audit == nil {
|
|
return nil
|
|
}
|
|
id := constants.SystemConfigSilenceEnabled
|
|
return s.audit.AppendSilenceResource(ctx, tx, constants.AuditActionPackageSilenceSwitchChanged, "切换沉默期总开关",
|
|
audit.ResourceInput{
|
|
Type: constants.AuditResourceSystemConfig, ID: &id, Key: id, DisplayName: "沉默期总开关",
|
|
Role: constants.AuditResourceRoleConfig, IdentitySnapshot: map[string]any{"config_key": id, "module": constants.SystemConfigModulePackage},
|
|
BeforeData: map[string]any{"value": before}, AfterData: map[string]any{"value": value},
|
|
}, map[string]any{"precheck": items, "operator_id": operatorID})
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if s.redis != nil {
|
|
_ = s.redis.Del(context.WithoutCancel(ctx), constants.RedisSystemConfigKey(constants.SystemConfigSilenceEnabled)).Err()
|
|
}
|
|
s.sw.Invalidate()
|
|
return &dto.SilenceSwitchResponse{Enabled: enable, Precheck: items, PrecheckPassed: precheckPassed(items)}, nil
|
|
}
|