package carrier import ( "context" "strconv" "time" "gorm.io/gorm" systemconfigapp "github.com/break/junhong_cmp_fiber/internal/application/systemconfig" carrierthreshold "github.com/break/junhong_cmp_fiber/internal/domain/carrierthreshold" "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" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/auditfailure" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" ) type Service struct { carrierStore *postgres.CarrierStore audit systemconfigapp.AuditWriter } func New(carrierStore *postgres.CarrierStore, audit systemconfigapp.AuditWriter) *Service { return &Service{carrierStore: carrierStore, audit: audit} } func (s *Service) Create(ctx context.Context, req *dto.CreateCarrierRequest) (*dto.CarrierResponse, error) { currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return nil, errors.New(errors.CodeUnauthorized, "未授权访问") } // 阈值字段只由超级管理员与平台账号配置:非平台账号提交即整体拒绝,不落任何配置。 if req.HasTrafficThresholdFields() && !canManageThreshold(ctx) { s.recordDenied(ctx, constants.AuditOperationCarrierCreate, "拒绝非平台账号配置运营商通道流量阈值", thresholdCreateProbe(req), errors.CodeForbidden) return nil, errors.New(errors.CodeForbidden, constants.PlatformManagementForbiddenMessage) } thresholdEnabled, thresholdValue, thresholdUnit := 0, req.TrafficThresholdValue, "" if req.TrafficThresholdEnabled != nil { thresholdEnabled = *req.TrafficThresholdEnabled } if req.TrafficThresholdUnit != nil { thresholdUnit = *req.TrafficThresholdUnit } if err := validateThresholdConfig(thresholdEnabled, thresholdValue, thresholdUnit); err != nil { s.recordDenied(ctx, constants.AuditOperationCarrierCreate, "拒绝保存非法运营商通道流量阈值配置", thresholdCreateProbe(req), errors.CodeInvalidParam) return nil, err } existing, _ := s.carrierStore.GetByCode(ctx, req.CarrierCode) if existing != nil { s.recordDenied(ctx, constants.AuditOperationCarrierCreate, "拒绝创建重复运营商配置", existing, errors.CodeCarrierCodeExists) return nil, errors.New(errors.CodeCarrierCodeExists, "运营商编码已存在") } if s.audit == nil { return nil, errors.New(errors.CodeInvalidStatus, "运营商配置审计接缝未配置") } carrier := &model.Carrier{ CarrierCode: req.CarrierCode, CarrierName: req.CarrierName, CarrierType: req.CarrierType, Description: req.Description, Status: constants.StatusEnabled, DataResetDay: 1, TrafficThresholdEnabled: thresholdEnabled, TrafficThresholdValue: thresholdValue, TrafficThresholdUnit: thresholdUnit, } if req.DataResetDay != nil { carrier.DataResetDay = *req.DataResetDay } if req.RealnameLinkType != nil { carrier.RealnameLinkType = *req.RealnameLinkType } if req.RealnameLinkTemplate != nil { carrier.RealnameLinkTemplate = *req.RealnameLinkTemplate } carrier.Creator = currentUserID err := s.carrierStore.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := s.carrierStore.WithTx(tx).Create(ctx, carrier); err != nil { return err } return s.writeAudit(ctx, tx, constants.AuditOperationCarrierCreate, "创建运营商配置", nil, carrier) }) if err != nil { s.recordFailure(ctx, constants.AuditOperationCarrierCreate, "创建运营商配置失败", carrier) return nil, errors.Wrap(errors.CodeInternalError, err, "创建运营商失败") } return s.toResponse(ctx, carrier), nil } func (s *Service) Get(ctx context.Context, id uint) (*dto.CarrierResponse, error) { carrier, err := s.carrierStore.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeCarrierNotFound, "运营商不存在") } return nil, errors.Wrap(errors.CodeInternalError, err, "获取运营商失败") } return s.toResponse(ctx, carrier), nil } func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateCarrierRequest) (*dto.CarrierResponse, error) { currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return nil, errors.New(errors.CodeUnauthorized, "未授权访问") } carrier, err := s.carrierStore.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeCarrierNotFound, "运营商不存在") } return nil, errors.Wrap(errors.CodeInternalError, err, "获取运营商失败") } before := *carrier // 阈值字段只由超级管理员与平台账号修改:非平台账号提交即整体拒绝。 if req.HasTrafficThresholdFields() && !canManageThreshold(ctx) { s.recordDenied(ctx, constants.AuditOperationCarrierUpdate, "拒绝非平台账号配置运营商通道流量阈值", &before, errors.CodeForbidden) return nil, errors.New(errors.CodeForbidden, constants.PlatformManagementForbiddenMessage) } // 阈值按「请求覆盖既有配置」后的结果校验,避免保存出启用但缺数值/单位的半配置。 thresholdEnabled, thresholdValue, thresholdUnit := carrier.TrafficThresholdEnabled, carrier.TrafficThresholdValue, carrier.TrafficThresholdUnit if req.TrafficThresholdEnabled != nil { thresholdEnabled = *req.TrafficThresholdEnabled } if req.TrafficThresholdValue != nil { thresholdValue = req.TrafficThresholdValue } if req.TrafficThresholdUnit != nil { thresholdUnit = *req.TrafficThresholdUnit } if err := validateThresholdConfig(thresholdEnabled, thresholdValue, thresholdUnit); err != nil { s.recordDenied(ctx, constants.AuditOperationCarrierUpdate, "拒绝保存非法运营商通道流量阈值配置", &before, errors.CodeInvalidParam) return nil, err } carrier.TrafficThresholdEnabled = thresholdEnabled carrier.TrafficThresholdValue = thresholdValue carrier.TrafficThresholdUnit = thresholdUnit if req.CarrierName != nil { carrier.CarrierName = *req.CarrierName } if req.Description != nil { carrier.Description = *req.Description } if req.DataResetDay != nil { carrier.DataResetDay = *req.DataResetDay } if req.RealnameLinkType != nil { carrier.RealnameLinkType = *req.RealnameLinkType } if req.RealnameLinkTemplate != nil { carrier.RealnameLinkTemplate = *req.RealnameLinkTemplate } if carrier.RealnameLinkType == "template" && carrier.RealnameLinkTemplate == "" { s.recordDenied(ctx, constants.AuditOperationCarrierUpdate, "拒绝保存非法运营商实名链接配置", &before, errors.CodeInvalidParam) return nil, errors.New(errors.CodeInvalidParam, "模板URL类型必须提供实名链接模板") } carrier.Updater = currentUserID err = s.carrierStore.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := s.carrierStore.WithTx(tx).Update(ctx, carrier); err != nil { return err } return s.writeAudit(ctx, tx, constants.AuditOperationCarrierUpdate, "更新运营商配置", &before, carrier) }) if err != nil { s.recordFailure(ctx, constants.AuditOperationCarrierUpdate, "更新运营商配置失败", carrier) return nil, errors.Wrap(errors.CodeInternalError, err, "更新运营商失败") } return s.toResponse(ctx, carrier), nil } func (s *Service) Delete(ctx context.Context, id uint) error { carrier, err := s.carrierStore.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeCarrierNotFound, "运营商不存在") } return errors.Wrap(errors.CodeInternalError, err, "获取运营商失败") } if s.audit == nil { return errors.New(errors.CodeInvalidStatus, "运营商配置审计接缝未配置") } err = s.carrierStore.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := s.carrierStore.WithTx(tx).Delete(ctx, id); err != nil { return err } return s.writeAudit(ctx, tx, constants.AuditOperationCarrierDelete, "删除运营商配置", carrier, nil) }) if err != nil { s.recordFailure(ctx, constants.AuditOperationCarrierDelete, "删除运营商配置失败", carrier) return errors.Wrap(errors.CodeInternalError, err, "删除运营商失败") } return nil } func (s *Service) List(ctx context.Context, req *dto.CarrierListRequest) ([]*dto.CarrierResponse, int64, error) { opts := &store.QueryOptions{ Page: req.Page, PageSize: req.PageSize, OrderBy: "id DESC", } if opts.Page == 0 { opts.Page = 1 } if opts.PageSize == 0 { opts.PageSize = constants.DefaultPageSize } filters := make(map[string]interface{}) if req.CarrierType != nil { filters["carrier_type"] = *req.CarrierType } if req.CarrierName != nil { filters["carrier_name"] = *req.CarrierName } if req.Status != nil { filters["status"] = *req.Status } carriers, total, err := s.carrierStore.List(ctx, opts, filters) if err != nil { return nil, 0, errors.Wrap(errors.CodeInternalError, err, "查询运营商列表失败") } responses := make([]*dto.CarrierResponse, len(carriers)) for i, c := range carriers { responses[i] = s.toResponse(ctx, c) } return responses, total, nil } func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error { currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return errors.New(errors.CodeUnauthorized, "未授权访问") } carrier, err := s.carrierStore.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeCarrierNotFound, "运营商不存在") } return errors.Wrap(errors.CodeInternalError, err, "获取运营商失败") } if s.audit == nil { return errors.New(errors.CodeInvalidStatus, "运营商配置审计接缝未配置") } before := *carrier carrier.Status = status carrier.Updater = currentUserID err = s.carrierStore.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := s.carrierStore.WithTx(tx).Update(ctx, carrier); err != nil { return err } return s.writeAudit(ctx, tx, constants.AuditOperationCarrierStatusUpdate, "更新运营商配置状态", &before, carrier) }) if err != nil { s.recordFailure(ctx, constants.AuditOperationCarrierStatusUpdate, "更新运营商配置状态失败", carrier) return errors.Wrap(errors.CodeInternalError, err, "更新运营商状态失败") } return nil } func (s *Service) writeAudit(ctx context.Context, tx *gorm.DB, operation, description string, before, after *model.Carrier) error { carrier := after if carrier == nil { carrier = before } resourceID := strconv.FormatUint(uint64(carrier.ID), 10) requestID := "" if value := middleware.GetRequestIDFromContext(ctx); value != nil { requestID = *value } return s.audit.WriteConfigChange(ctx, tx, systemconfigapp.ChangeAudit{ OperatorID: middleware.GetUserIDFromContext(ctx), OperationType: operation, Description: description, ConfigKey: "carrier." + carrier.CarrierCode, Module: "carrier", ResourceID: &resourceID, DisplayName: carrier.CarrierName, Identity: carrierIdentity(carrier), BeforeData: carrierAuditSnapshot(before), AfterData: carrierAuditSnapshot(after), RequestID: requestID, CorrelationID: requestID, }) } func (s *Service) recordDenied(ctx context.Context, operation, description string, carrier *model.Carrier, code int) { s.recordAuditResult(ctx, operation, description, carrier, constants.AuditResultDenied, code) } func (s *Service) recordFailure(ctx context.Context, operation, description string, carrier *model.Carrier) { s.recordAuditResult(ctx, operation, description, carrier, constants.AuditResultFailed, errors.CodeDatabaseError) } func (s *Service) recordAuditResult(ctx context.Context, operation, description string, carrier *model.Carrier, result string, code int) { if s.audit == nil || carrier == nil || s.carrierStore == nil || s.carrierStore.DB() == nil { return } resourceID := strconv.FormatUint(uint64(carrier.ID), 10) requestID := "" if value := middleware.GetRequestIDFromContext(ctx); value != nil { requestID = *value } audit := systemconfigapp.ChangeAudit{ OperatorID: middleware.GetUserIDFromContext(ctx), OperationType: operation, Description: description, ConfigKey: "carrier." + carrier.CarrierCode, Module: "carrier", ResourceID: &resourceID, DisplayName: carrier.CarrierName, Identity: carrierIdentity(carrier), BeforeData: carrierAuditSnapshot(carrier), Result: result, ErrorCode: strconv.Itoa(code), ErrorSummary: description, RequestID: requestID, CorrelationID: requestID, } if err := s.carrierStore.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { return s.audit.WriteConfigChange(ctx, tx, audit) }); err != nil { auditfailure.RecordSecondaryWriteFailure(operation, audit.ConfigKey, requestID, requestID, audit.ErrorCode, err) } } func carrierIdentity(carrier *model.Carrier) map[string]any { if carrier == nil { return nil } return map[string]any{ "id": carrier.ID, "carrier_code": carrier.CarrierCode, "carrier_name": carrier.CarrierName, "carrier_type": carrier.CarrierType, "status": carrier.Status, } } func carrierAuditSnapshot(carrier *model.Carrier) map[string]any { if carrier == nil { return nil } return map[string]any{ "id": carrier.ID, "carrier_code": carrier.CarrierCode, "carrier_name": carrier.CarrierName, "carrier_type": carrier.CarrierType, "description": carrier.Description, "status": carrier.Status, "realname_link_type": carrier.RealnameLinkType, "realname_link_template": carrier.RealnameLinkTemplate, "data_reset_day": carrier.DataResetDay, // 通道流量阈值的新增、修改、启用与停用都并入既有运营商更新审计的前后值快照,不另建审计动作。 "traffic_threshold_enabled": carrier.TrafficThresholdEnabled, "traffic_threshold_value": carrier.TrafficThresholdValue, "traffic_threshold_unit": carrier.TrafficThresholdUnit, } } // canManageThreshold 判断当前账号是否具备通道流量阈值字段的读写权限。 // 只有超级管理员与平台账号可见可写,与 requirePlatformManagement 的判定一致(引用同一用户类型枚举)。 func canManageThreshold(ctx context.Context) bool { userType := middleware.GetUserTypeFromContext(ctx) return userType == constants.UserTypeSuperAdmin || userType == constants.UserTypePlatform } // validateThresholdConfig 校验通道流量阈值配置:启停仅 0/1、数值必须为正、单位仅 MB/GB、配置齐备。 // 「启用但无数值/单位」与「只给数值不给单位」都会让达量判定失去唯一口径; // 非 0/1 的启停取值会被判定端按「未启用」静默处理,因此必须显式拒绝。 func validateThresholdConfig(enabled int, value *float64, unit string) error { if enabled != constants.StatusDisabled && enabled != constants.StatusEnabled { return errors.New(errors.CodeInvalidParam, "通道流量阈值是否启用仅支持 0 或 1") } if value != nil && *value <= 0 { return errors.New(errors.CodeInvalidParam, "通道流量阈值数值必须为正数") } if unit != "" && unit != carrierthreshold.UnitMB && unit != carrierthreshold.UnitGB { return errors.New(errors.CodeInvalidParam, "通道流量阈值单位仅支持 MB 或 GB") } configured := value != nil || unit != "" if configured && (value == nil || unit == "") { return errors.New(errors.CodeInvalidParam, "通道流量阈值数值与流量单位必须同时提供") } if enabled == constants.StatusEnabled && !configured { return errors.New(errors.CodeInvalidParam, "启用通道流量阈值必须同时提供正的阈值数值与流量单位") } return nil } // thresholdCreateProbe 构造仅供拒绝审计使用的运营商身份,不写库。 func thresholdCreateProbe(req *dto.CreateCarrierRequest) *model.Carrier { if req == nil { return nil } return &model.Carrier{ CarrierCode: req.CarrierCode, CarrierName: req.CarrierName, CarrierType: req.CarrierType, } } // toResponse 把运营商事实投影为响应;阈值字段只对超级管理员与平台账号返回。 func (s *Service) toResponse(ctx context.Context, c *model.Carrier) *dto.CarrierResponse { result := &dto.CarrierResponse{ ID: c.ID, CarrierCode: c.CarrierCode, CarrierName: c.CarrierName, CarrierType: c.CarrierType, Description: c.Description, DataResetDay: c.DataResetDay, RealnameLinkType: c.RealnameLinkType, RealnameLinkTemplate: c.RealnameLinkTemplate, Status: c.Status, CreatedAt: c.CreatedAt.Format(time.RFC3339), UpdatedAt: c.UpdatedAt.Format(time.RFC3339), } if !canManageThreshold(ctx) { return result } enabled := c.TrafficThresholdEnabled result.TrafficThresholdEnabled = &enabled result.TrafficThresholdValue = c.TrafficThresholdValue if c.TrafficThresholdUnit != "" { unit := c.TrafficThresholdUnit result.TrafficThresholdUnit = &unit } return result }