// Package packagetrafficalert 收口套餐真流量预警的规则维护事务脚本与每日扫描用例。 // // 规则维护是简单写:Handler → Application 事务脚本 → Persistence,事实与审计同事务。 // 每日扫描是复杂写:Application 编排 → Domain 判定 → Port/Infrastructure 原子写入预警事实、 // 可靠通知事件与审计;判定只使用套餐使用记录的真流量快照,不读取虚流量、展示量、卡级累计或通道累计。 package packagetrafficalert import ( "context" "strconv" "strings" "gorm.io/gorm" "github.com/break/junhong_cmp_fiber/internal/domain/packagetrafficalert" "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" ) // RuleService 套餐真流量预警规则维护事务脚本。 type RuleService struct { db *gorm.DB store *postgres.PackageTrafficAlertStore auditWriter *audit.Writer } // NewRuleService 创建套餐真流量预警规则事务脚本。 func NewRuleService(db *gorm.DB, store *postgres.PackageTrafficAlertStore, auditWriters ...*audit.Writer) *RuleService { service := &RuleService{db: db, store: store} if len(auditWriters) > 0 { service.auditWriter = auditWriters[0] } return service } // Create 为套餐商品创建唯一预警规则。 // 创建一律校验套餐存在且商品真流量额度大于零:商品 real_data_mb 只用于配置合法性, // 不作为扫描分母(分母取使用记录的真总量快照)。 func (s *RuleService) Create(ctx context.Context, request *dto.CreatePackageTrafficAlertRuleRequest) (*dto.PackageTrafficAlertRuleItem, error) { operatorID, err := requireOperator(ctx) if err != nil { return nil, err } if request == nil || request.PackageID == 0 { return nil, errors.New(errors.CodeInvalidParam, "套餐商品ID不能为空") } if !packagetrafficalert.IsValidThresholdPercent(request.ThresholdPercent) { return nil, errors.New(errors.CodeInvalidParam, "真流量预警阈值必须大于等于 1 且小于等于 100,允许两位小数") } enabled := constants.StatusEnabled if request.Enabled != nil && !*request.Enabled { enabled = constants.StatusDisabled } rule := &model.PackageTrafficAlertRule{ PackageID: request.PackageID, ThresholdPercent: packagetrafficalert.NormalizeThresholdPercent(request.ThresholdPercent), Enabled: enabled, Remark: request.Remark, BaseModel: model.BaseModel{Creator: operatorID, Updater: operatorID}, } var response *dto.PackageTrafficAlertRuleItem err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { pkg, loadErr := s.loadPackage(ctx, tx, request.PackageID) if loadErr != nil { return loadErr } if pkg.RealDataMB <= 0 { return errors.New(errors.CodeInvalidParam, "该套餐商品真流量额度不大于零,不能启用真流量预警规则") } store := s.store.WithTx(tx) exists, existsErr := store.ExistsRuleByPackageID(ctx, request.PackageID) if existsErr != nil { return errors.Wrap(errors.CodeDatabaseError, existsErr, "校验套餐预警规则失败") } if exists { return errors.New(errors.CodeInvalidParam, "套餐已存在真流量预警规则") } if createErr := store.CreateRule(ctx, rule); createErr != nil { if isDuplicateKey(createErr) { return errors.New(errors.CodeInvalidParam, "套餐已存在真流量预警规则") } return errors.Wrap(errors.CodeDatabaseError, createErr, "创建套餐真流量预警规则失败") } item := toRuleItem(rule, pkg.PackageName, pkg.RealDataMB) if auditErr := s.appendRuleAudit(ctx, tx, constants.AuditActionPackageTrafficAlertRuleCreated, "创建套餐真流量预警规则", rule, pkg.PackageName, nil, ruleAuditSnapshot(rule, pkg.PackageName)); auditErr != nil { return auditErr } response = item return nil }) if err != nil { return nil, err } return response, nil } // Update 修改阈值、启停与备注。 // 修改不回填既有预警,也不改写已冻结的预警快照;结果状态为启用时同样校验商品真流量额度大于零。 func (s *RuleService) Update(ctx context.Context, ruleID uint, request *dto.UpdatePackageTrafficAlertRuleRequest) (*dto.PackageTrafficAlertRuleItem, error) { operatorID, err := requireOperator(ctx) if err != nil { return nil, err } if request == nil || ruleID == 0 { return nil, errors.New(errors.CodeInvalidParam, "预警规则ID不能为空") } if request.ThresholdPercent != nil && !packagetrafficalert.IsValidThresholdPercent(*request.ThresholdPercent) { return nil, errors.New(errors.CodeInvalidParam, "真流量预警阈值必须大于等于 1 且小于等于 100,允许两位小数") } var response *dto.PackageTrafficAlertRuleItem err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { store := s.store.WithTx(tx) rule, lockErr := store.LockRuleByID(ctx, ruleID) if lockErr != nil { return ruleLookupError(lockErr) } before := ruleAuditSnapshot(rule, "") if request.ThresholdPercent != nil { rule.ThresholdPercent = packagetrafficalert.NormalizeThresholdPercent(*request.ThresholdPercent) } if request.Enabled != nil { rule.Enabled = constants.StatusEnabled if !*request.Enabled { rule.Enabled = constants.StatusDisabled } } if request.Remark != nil { rule.Remark = *request.Remark } pkg, pkgErr := s.loadPackage(ctx, tx, rule.PackageID) if pkgErr != nil { return pkgErr } if rule.Enabled == constants.StatusEnabled && pkg.RealDataMB <= 0 { return errors.New(errors.CodeInvalidParam, "该套餐商品真流量额度不大于零,不能启用真流量预警规则") } packageName := pkg.PackageName if updateErr := store.UpdateRule(ctx, rule, operatorID); updateErr != nil { return errors.Wrap(errors.CodeDatabaseError, updateErr, "更新套餐真流量预警规则失败") } after := ruleAuditSnapshot(rule, packageName) if before["enabled"] != after["enabled"] { action, summary := constants.AuditActionPackageTrafficAlertRuleEnabled, "启用套餐真流量预警规则" if rule.Enabled != constants.StatusEnabled { action, summary = constants.AuditActionPackageTrafficAlertRuleDisabled, "停用套餐真流量预警规则" } if auditErr := s.appendRuleAudit(ctx, tx, action, summary, rule, packageName, map[string]any{"enabled": before["enabled"]}, map[string]any{"enabled": after["enabled"]}); auditErr != nil { return auditErr } } if before["threshold_percent"] != after["threshold_percent"] || before["remark"] != after["remark"] { if auditErr := s.appendRuleAudit(ctx, tx, constants.AuditActionPackageTrafficAlertRuleUpdated, "更新套餐真流量预警规则", rule, packageName, before, after); auditErr != nil { return auditErr } } response = toRuleItem(rule, packageName, pkg.RealDataMB) return nil }) if err != nil { return nil, err } return response, nil } // loadPackage 查询套餐商品;不存在时按参数错误返回。 func (s *RuleService) loadPackage(ctx context.Context, tx *gorm.DB, packageID uint) (*model.Package, error) { var pkg model.Package if err := tx.WithContext(ctx).Where("id = ?", packageID).First(&pkg).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeInvalidParam, "套餐商品不存在") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐商品失败") } return &pkg, nil } // appendRuleAudit 在业务事务内追加预警规则事件。 func (s *RuleService) appendRuleAudit(ctx context.Context, tx *gorm.DB, action, summary string, rule *model.PackageTrafficAlertRule, packageName string, before, after map[string]any) error { if s.auditWriter == nil { return errors.New(errors.CodeInvalidStatus, "套餐真流量预警规则统一审计接缝未配置") } var resourceID *string if rule.ID != 0 { value := strconv.FormatUint(uint64(rule.ID), 10) resourceID = &value } displayName := packageName if displayName == "" { displayName = "套餐 " + strconv.FormatUint(uint64(rule.PackageID), 10) } // 使用 AppendAndGet:预警规则属于关键配置,「要求成功必达」的审计失败必须回滚事务(ENG-TX-001)。 if _, err := s.auditWriter.AppendAndGet(ctx, tx, audit.AppendInput{ ActionCode: action, Summary: summary, Result: constants.AuditResultSuccess, Actor: audit.ActorInput{Kind: constants.AuditActorAccount, ID: strconv.FormatUint(uint64(middleware.GetUserIDFromContext(ctx)), 10)}, Source: constants.AuditSourceAdminAPI, ScopeType: constants.AuditScopePlatform, Resources: []audit.ResourceInput{{ Type: constants.AuditResourcePackageTrafficAlertRule, ID: resourceID, Key: strconv.FormatUint(uint64(rule.PackageID), 10), DisplayName: displayName, Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRolePackageTrafficAlertRuleTarget, IdentitySnapshot: ruleAuditIdentity(rule, packageName), BeforeData: before, AfterData: after, }}, }); err != nil { return err } return nil } // requireOperator 要求调用方已通过后台鉴权,否则拒绝写入。 func requireOperator(ctx context.Context) (uint, error) { operatorID := middleware.GetUserIDFromContext(ctx) if operatorID == 0 { return 0, errors.New(errors.CodeUnauthorized) } return operatorID, nil } // ruleLookupError 把规则不存在映射为统一资源不可见错误。 func ruleLookupError(err error) error { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeForbidden, constants.PlatformManagementForbiddenMessage) } return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐真流量预警规则失败") } // isDuplicateKey 判断数据库错误是否为唯一键冲突。 func isDuplicateKey(err error) bool { if err == nil { return false } text := err.Error() return strings.Contains(text, "23505") || strings.Contains(text, "duplicate key") || strings.Contains(text, "SQLSTATE 23505") } // ruleAuditSnapshot 返回预警规则可审计的可变字段快照。 func ruleAuditSnapshot(rule *model.PackageTrafficAlertRule, packageName string) map[string]any { if rule == nil { return nil } snapshot := map[string]any{ "package_id": rule.PackageID, "threshold_percent": rule.ThresholdPercent, "enabled": rule.Enabled, "remark": rule.Remark, } if packageName != "" { snapshot["package_name"] = packageName } return snapshot } // ruleAuditIdentity 返回预警规则审计身份快照,字段落在注册表白名单内。 func ruleAuditIdentity(rule *model.PackageTrafficAlertRule, packageName string) map[string]any { if rule == nil { return nil } identity := map[string]any{ "id": rule.ID, "package_id": rule.PackageID, "threshold_percent": rule.ThresholdPercent, "enabled": rule.Enabled, "remark": rule.Remark, } if packageName != "" { identity["package_name"] = packageName } return identity } // toRuleItem 把规则投影为对外响应项。 func toRuleItem(rule *model.PackageTrafficAlertRule, packageName string, realDataMB int64) *dto.PackageTrafficAlertRuleItem { if rule == nil { return nil } return &dto.PackageTrafficAlertRuleItem{ ID: rule.ID, PackageID: rule.PackageID, PackageName: packageName, RealDataMB: realDataMB, ThresholdPercent: rule.ThresholdPercent, Enabled: rule.Enabled == constants.StatusEnabled, EnabledName: enabledName(rule.Enabled), Remark: rule.Remark, UpdatedAt: rule.UpdatedAt, } } // enabledName 返回启停状态的中文名称。 func enabledName(enabled int) string { if enabled == constants.StatusEnabled { return "启用" } return "停用" }