All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m37s
- 轮询配置新增套餐状态、资产状态、重置日前窗口与窗口内流量间隔(迁移 248),间隔三态:正数按间隔、0 不检查、空继承 - 配置按 priority, id 排序合并;在库与已销售无套餐未匹配时告警不入队 - 新增优先级整体重排、匹配预览、立即重算到期时间(异步只提前)、分层统计与重置日前加密统计接口 - 已切换日快照计量的卡流量按观测时间归属,激活前用量只记已结算 - 订单购买与实名激活补齐资产级激活锁,激活前对已切换卡结算一次流量,失败只告警不阻塞,结算不触发通道阈值判定
417 lines
16 KiB
Go
417 lines
16 KiB
Go
package polling
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
|
|
auditinfra "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"
|
|
pollingcore "github.com/break/junhong_cmp_fiber/internal/polling"
|
|
"github.com/break/junhong_cmp_fiber/internal/store"
|
|
"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"
|
|
)
|
|
|
|
// ConfigService 轮询配置服务
|
|
type ConfigService struct {
|
|
configStore *postgres.PollingConfigStore
|
|
db *gorm.DB
|
|
auditWriter *auditinfra.Writer
|
|
redis *redis.Client
|
|
logger *zap.Logger
|
|
configMgr *pollingcore.PollingConfigManager
|
|
queueClient TaskEnqueuer
|
|
}
|
|
|
|
// SetAudit 注入轮询配置事务与统一审计 Writer。
|
|
func (s *ConfigService) SetAudit(db *gorm.DB, writer *auditinfra.Writer) {
|
|
s.db = db
|
|
s.auditWriter = writer
|
|
}
|
|
|
|
// NewConfigService 创建轮询配置服务实例
|
|
func NewConfigService(configStore *postgres.PollingConfigStore, redisClient *redis.Client, logger *zap.Logger) *ConfigService {
|
|
return &ConfigService{configStore: configStore, redis: redisClient, logger: logger}
|
|
}
|
|
|
|
func (s *ConfigService) notifyConfigChanged(ctx context.Context, event string) {
|
|
if s.redis == nil {
|
|
return
|
|
}
|
|
if err := s.redis.Publish(ctx, constants.RedisPollingConfigChangedChannel(), event).Err(); err != nil {
|
|
s.logger.Warn("通知轮询配置变更失败", zap.String("event", event), zap.Error(err))
|
|
}
|
|
}
|
|
|
|
// Create 创建轮询配置
|
|
func (s *ConfigService) Create(ctx context.Context, req *dto.CreatePollingConfigRequest) (*dto.PollingConfigResponse, error) {
|
|
currentUserID := middleware.GetUserIDFromContext(ctx)
|
|
if currentUserID == 0 {
|
|
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
|
}
|
|
|
|
// 验证配置名称唯一性
|
|
existing, _ := s.configStore.GetByName(ctx, req.ConfigName)
|
|
if existing != nil {
|
|
appErr := errors.New(errors.CodeInvalidParam, "配置名称已存在")
|
|
recordPollingFailure(ctx, s.db, s.auditWriter, auditinfra.PollingInput{
|
|
ActionCode: constants.AuditActionPollingConfigCreated, Summary: "拒绝创建重复轮询配置",
|
|
ResourceType: constants.AuditResourcePollingConfig, ResourceKey: req.ConfigName,
|
|
DisplayName: req.ConfigName, OperatorID: currentUserID, Result: constants.AuditResultDenied,
|
|
IdentitySnapshot: map[string]any{"config_name": req.ConfigName},
|
|
}, appErr)
|
|
return nil, appErr
|
|
}
|
|
|
|
config := &model.PollingConfig{
|
|
ConfigName: req.ConfigName,
|
|
CardCondition: req.CardCondition,
|
|
CardCategory: req.CardCategory,
|
|
CarrierID: req.CarrierID,
|
|
Priority: req.Priority,
|
|
RealnameCheckInterval: req.RealnameCheckInterval,
|
|
CarddataCheckInterval: req.CarddataCheckInterval,
|
|
PackageCheckInterval: req.PackageCheckInterval,
|
|
ProtectCheckInterval: req.ProtectCheckInterval,
|
|
CardStatusCheckInterval: req.CardStatusCheckInterval,
|
|
PackageState: req.PackageState,
|
|
AssetStatus: req.AssetStatus,
|
|
PreResetWindow: req.PreResetWindow,
|
|
PreResetCarddataInterval: req.PreResetCarddataInterval,
|
|
Status: 1,
|
|
Description: req.Description,
|
|
CreatedBy: ¤tUserID,
|
|
UpdatedBy: ¤tUserID,
|
|
}
|
|
if err := validatePollingConfig(config); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err := runPollingTransaction(ctx, s.db, s.auditWriter, func(tx *gorm.DB) error {
|
|
if err := s.configStore.WithTx(tx).Create(ctx, config); err != nil {
|
|
return err
|
|
}
|
|
return writePollingAudit(ctx, tx, s.auditWriter, auditinfra.PollingInput{
|
|
ActionCode: constants.AuditActionPollingConfigCreated, Summary: "创建轮询配置",
|
|
ResourceType: constants.AuditResourcePollingConfig, ResourceID: config.ID,
|
|
ResourceKey: pollingManualTriggerKey(config.ID), DisplayName: config.ConfigName,
|
|
OperatorID: currentUserID, IdentitySnapshot: pollingConfigIdentity(config), AfterData: pollingConfigState(config),
|
|
})
|
|
})
|
|
if err != nil {
|
|
appErr := errors.Wrap(errors.CodeInternalError, err, "创建轮询配置失败")
|
|
recordPollingFailure(ctx, s.db, s.auditWriter, auditinfra.PollingInput{
|
|
ActionCode: constants.AuditActionPollingConfigCreated, Summary: "创建轮询配置失败",
|
|
ResourceType: constants.AuditResourcePollingConfig,
|
|
ResourceKey: config.ConfigName, DisplayName: config.ConfigName, OperatorID: currentUserID,
|
|
IdentitySnapshot: pollingConfigIdentity(config), AfterData: pollingConfigState(config),
|
|
}, appErr)
|
|
return nil, appErr
|
|
}
|
|
|
|
s.notifyConfigChanged(ctx, "created")
|
|
resp := s.toResponse(config)
|
|
resp.Warnings = s.priorityWarnings(ctx, config)
|
|
return resp, nil
|
|
}
|
|
|
|
// Get 获取轮询配置详情
|
|
func (s *ConfigService) Get(ctx context.Context, id uint) (*dto.PollingConfigResponse, error) {
|
|
config, err := s.configStore.GetByID(ctx, id)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, errors.New(errors.CodePollingConfigNotFound, "轮询配置不存在")
|
|
}
|
|
return nil, errors.Wrap(errors.CodeInternalError, err, "获取轮询配置失败")
|
|
}
|
|
return s.toResponse(config), nil
|
|
}
|
|
|
|
// Update 更新轮询配置
|
|
func (s *ConfigService) Update(ctx context.Context, id uint, req *dto.UpdatePollingConfigRequest) (*dto.PollingConfigResponse, error) {
|
|
currentUserID := middleware.GetUserIDFromContext(ctx)
|
|
if currentUserID == 0 {
|
|
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
|
}
|
|
|
|
config, err := s.configStore.GetByID(ctx, id)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, errors.New(errors.CodePollingConfigNotFound, "轮询配置不存在")
|
|
}
|
|
return nil, errors.Wrap(errors.CodeInternalError, err, "获取轮询配置失败")
|
|
}
|
|
before := *config
|
|
|
|
// 更新字段
|
|
if req.ConfigName != nil {
|
|
// 检查名称唯一性
|
|
existing, _ := s.configStore.GetByName(ctx, *req.ConfigName)
|
|
if existing != nil && existing.ID != id {
|
|
appErr := errors.New(errors.CodeInvalidParam, "配置名称已存在")
|
|
recordPollingFailure(ctx, s.db, s.auditWriter, auditinfra.PollingInput{
|
|
ActionCode: constants.AuditActionPollingConfigUpdated, Summary: "拒绝更新为重复轮询配置名称",
|
|
ResourceType: constants.AuditResourcePollingConfig, ResourceID: config.ID,
|
|
ResourceKey: pollingManualTriggerKey(config.ID), DisplayName: config.ConfigName,
|
|
OperatorID: currentUserID, Result: constants.AuditResultDenied,
|
|
IdentitySnapshot: pollingConfigIdentity(config), BeforeData: pollingConfigState(config),
|
|
}, appErr)
|
|
return nil, appErr
|
|
}
|
|
config.ConfigName = *req.ConfigName
|
|
}
|
|
if req.CardCondition != nil {
|
|
config.CardCondition = *req.CardCondition
|
|
}
|
|
if req.CardCategory != nil {
|
|
config.CardCategory = *req.CardCategory
|
|
}
|
|
if req.CarrierID != nil {
|
|
config.CarrierID = req.CarrierID
|
|
}
|
|
if req.Priority != nil {
|
|
config.Priority = *req.Priority
|
|
}
|
|
if req.RealnameCheckInterval != nil {
|
|
config.RealnameCheckInterval = req.RealnameCheckInterval
|
|
}
|
|
if req.CarddataCheckInterval != nil {
|
|
config.CarddataCheckInterval = req.CarddataCheckInterval
|
|
}
|
|
if req.PackageCheckInterval != nil {
|
|
config.PackageCheckInterval = req.PackageCheckInterval
|
|
}
|
|
if req.ProtectCheckInterval != nil {
|
|
config.ProtectCheckInterval = req.ProtectCheckInterval
|
|
}
|
|
if req.CardStatusCheckInterval != nil {
|
|
config.CardStatusCheckInterval = req.CardStatusCheckInterval
|
|
}
|
|
if req.PackageState != nil {
|
|
config.PackageState = req.PackageState
|
|
}
|
|
if req.AssetStatus != nil {
|
|
config.AssetStatus = req.AssetStatus
|
|
}
|
|
if req.PreResetWindow != nil {
|
|
config.PreResetWindow = req.PreResetWindow
|
|
}
|
|
if req.PreResetCarddataInterval != nil {
|
|
config.PreResetCarddataInterval = req.PreResetCarddataInterval
|
|
}
|
|
if req.Description != nil {
|
|
config.Description = *req.Description
|
|
}
|
|
if err := applyPollingConfigClearFields(config, req.ClearFields); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validatePollingConfig(config); err != nil {
|
|
return nil, err
|
|
}
|
|
config.UpdatedBy = ¤tUserID
|
|
|
|
err = runPollingTransaction(ctx, s.db, s.auditWriter, func(tx *gorm.DB) error {
|
|
if err := s.configStore.WithTx(tx).Update(ctx, config); err != nil {
|
|
return err
|
|
}
|
|
return writePollingAudit(ctx, tx, s.auditWriter, auditinfra.PollingInput{
|
|
ActionCode: constants.AuditActionPollingConfigUpdated, Summary: "更新轮询配置",
|
|
ResourceType: constants.AuditResourcePollingConfig, ResourceID: config.ID,
|
|
ResourceKey: pollingManualTriggerKey(config.ID), DisplayName: config.ConfigName,
|
|
OperatorID: currentUserID, IdentitySnapshot: pollingConfigIdentity(config),
|
|
BeforeData: pollingConfigState(&before), AfterData: pollingConfigState(config),
|
|
})
|
|
})
|
|
if err != nil {
|
|
appErr := errors.Wrap(errors.CodeInternalError, err, "更新轮询配置失败")
|
|
recordPollingFailure(ctx, s.db, s.auditWriter, auditinfra.PollingInput{
|
|
ActionCode: constants.AuditActionPollingConfigUpdated, Summary: "更新轮询配置失败",
|
|
ResourceType: constants.AuditResourcePollingConfig, ResourceID: config.ID,
|
|
ResourceKey: pollingManualTriggerKey(config.ID), DisplayName: config.ConfigName,
|
|
OperatorID: currentUserID, IdentitySnapshot: pollingConfigIdentity(config),
|
|
BeforeData: pollingConfigState(&before), AfterData: pollingConfigState(config),
|
|
}, appErr)
|
|
return nil, appErr
|
|
}
|
|
|
|
s.notifyConfigChanged(ctx, "updated")
|
|
resp := s.toResponse(config)
|
|
resp.Warnings = s.priorityWarnings(ctx, config)
|
|
return resp, nil
|
|
}
|
|
|
|
// Delete 删除轮询配置
|
|
func (s *ConfigService) Delete(ctx context.Context, id uint) error {
|
|
currentUserID := middleware.GetUserIDFromContext(ctx)
|
|
if currentUserID == 0 {
|
|
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
|
}
|
|
config, err := s.configStore.GetByID(ctx, id)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return errors.New(errors.CodePollingConfigNotFound, "轮询配置不存在")
|
|
}
|
|
return errors.Wrap(errors.CodeInternalError, err, "获取轮询配置失败")
|
|
}
|
|
|
|
err = runPollingTransaction(ctx, s.db, s.auditWriter, func(tx *gorm.DB) error {
|
|
if err := s.configStore.WithTx(tx).Delete(ctx, id); err != nil {
|
|
return err
|
|
}
|
|
return writePollingAudit(ctx, tx, s.auditWriter, auditinfra.PollingInput{
|
|
ActionCode: constants.AuditActionPollingConfigDeleted, Summary: "删除轮询配置",
|
|
ResourceType: constants.AuditResourcePollingConfig, ResourceID: config.ID,
|
|
ResourceKey: pollingManualTriggerKey(config.ID), DisplayName: config.ConfigName,
|
|
OperatorID: currentUserID, IdentitySnapshot: pollingConfigIdentity(config), BeforeData: pollingConfigState(config),
|
|
})
|
|
})
|
|
if err != nil {
|
|
appErr := errors.Wrap(errors.CodeInternalError, err, "删除轮询配置失败")
|
|
recordPollingFailure(ctx, s.db, s.auditWriter, auditinfra.PollingInput{
|
|
ActionCode: constants.AuditActionPollingConfigDeleted, Summary: "删除轮询配置失败",
|
|
ResourceType: constants.AuditResourcePollingConfig, ResourceID: config.ID,
|
|
ResourceKey: pollingManualTriggerKey(config.ID), DisplayName: config.ConfigName,
|
|
OperatorID: currentUserID, IdentitySnapshot: pollingConfigIdentity(config), BeforeData: pollingConfigState(config),
|
|
}, appErr)
|
|
return appErr
|
|
}
|
|
|
|
s.notifyConfigChanged(ctx, "deleted")
|
|
return nil
|
|
}
|
|
|
|
// List 列表查询轮询配置
|
|
func (s *ConfigService) List(ctx context.Context, req *dto.PollingConfigListRequest) ([]*dto.PollingConfigResponse, int64, error) {
|
|
opts := &store.QueryOptions{
|
|
Page: req.Page,
|
|
PageSize: req.PageSize,
|
|
OrderBy: "priority ASC, id DESC",
|
|
}
|
|
if opts.Page == 0 {
|
|
opts.Page = 1
|
|
}
|
|
if opts.PageSize == 0 {
|
|
opts.PageSize = constants.DefaultPageSize
|
|
}
|
|
|
|
filters := make(map[string]interface{})
|
|
if req.Status != nil {
|
|
filters["status"] = *req.Status
|
|
}
|
|
if req.CardCondition != nil {
|
|
filters["card_condition"] = *req.CardCondition
|
|
}
|
|
if req.CardCategory != nil {
|
|
filters["card_category"] = *req.CardCategory
|
|
}
|
|
if req.CarrierID != nil {
|
|
filters["carrier_id"] = *req.CarrierID
|
|
}
|
|
if req.ConfigName != nil {
|
|
filters["config_name"] = *req.ConfigName
|
|
}
|
|
|
|
configs, total, err := s.configStore.List(ctx, opts, filters)
|
|
if err != nil {
|
|
return nil, 0, errors.Wrap(errors.CodeInternalError, err, "查询轮询配置列表失败")
|
|
}
|
|
|
|
responses := make([]*dto.PollingConfigResponse, len(configs))
|
|
for i, c := range configs {
|
|
responses[i] = s.toResponse(c)
|
|
}
|
|
|
|
return responses, total, nil
|
|
}
|
|
|
|
// UpdateStatus 更新配置状态(启用/禁用)
|
|
func (s *ConfigService) UpdateStatus(ctx context.Context, id uint, status int16) error {
|
|
currentUserID := middleware.GetUserIDFromContext(ctx)
|
|
if currentUserID == 0 {
|
|
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
|
}
|
|
|
|
config, err := s.configStore.GetByID(ctx, id)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return errors.New(errors.CodePollingConfigNotFound, "轮询配置不存在")
|
|
}
|
|
return errors.Wrap(errors.CodeInternalError, err, "获取轮询配置失败")
|
|
}
|
|
|
|
before := pollingConfigState(config)
|
|
err = runPollingTransaction(ctx, s.db, s.auditWriter, func(tx *gorm.DB) error {
|
|
if err := s.configStore.WithTx(tx).UpdateStatus(ctx, id, status, currentUserID); err != nil {
|
|
return err
|
|
}
|
|
config.Status = status
|
|
return writePollingAudit(ctx, tx, s.auditWriter, auditinfra.PollingInput{
|
|
ActionCode: constants.AuditActionPollingConfigStatusUpdated, Summary: "更新轮询配置状态",
|
|
ResourceType: constants.AuditResourcePollingConfig, ResourceID: config.ID,
|
|
ResourceKey: pollingManualTriggerKey(config.ID), DisplayName: config.ConfigName,
|
|
OperatorID: currentUserID, IdentitySnapshot: pollingConfigIdentity(config),
|
|
BeforeData: before, AfterData: pollingConfigState(config),
|
|
})
|
|
})
|
|
if err != nil {
|
|
appErr := errors.Wrap(errors.CodeInternalError, err, "更新轮询配置状态失败")
|
|
recordPollingFailure(ctx, s.db, s.auditWriter, auditinfra.PollingInput{
|
|
ActionCode: constants.AuditActionPollingConfigStatusUpdated, Summary: "更新轮询配置状态失败",
|
|
ResourceType: constants.AuditResourcePollingConfig, ResourceID: config.ID,
|
|
ResourceKey: pollingManualTriggerKey(config.ID), DisplayName: config.ConfigName,
|
|
OperatorID: currentUserID, IdentitySnapshot: pollingConfigIdentity(config), BeforeData: before,
|
|
}, appErr)
|
|
return appErr
|
|
}
|
|
|
|
s.notifyConfigChanged(ctx, "updated")
|
|
return nil
|
|
}
|
|
|
|
// ListEnabled 获取所有启用的配置
|
|
func (s *ConfigService) ListEnabled(ctx context.Context) ([]*dto.PollingConfigResponse, error) {
|
|
configs, err := s.configStore.ListEnabled(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(errors.CodeInternalError, err, "获取启用配置失败")
|
|
}
|
|
|
|
responses := make([]*dto.PollingConfigResponse, len(configs))
|
|
for i, c := range configs {
|
|
responses[i] = s.toResponse(c)
|
|
}
|
|
|
|
return responses, nil
|
|
}
|
|
|
|
// toResponse 转换为响应 DTO
|
|
func (s *ConfigService) toResponse(c *model.PollingConfig) *dto.PollingConfigResponse {
|
|
return &dto.PollingConfigResponse{
|
|
ID: c.ID,
|
|
ConfigName: c.ConfigName,
|
|
CardCondition: c.CardCondition,
|
|
CardCategory: c.CardCategory,
|
|
CarrierID: c.CarrierID,
|
|
Priority: c.Priority,
|
|
RealnameCheckInterval: c.RealnameCheckInterval,
|
|
CarddataCheckInterval: c.CarddataCheckInterval,
|
|
PackageCheckInterval: c.PackageCheckInterval,
|
|
ProtectCheckInterval: c.ProtectCheckInterval,
|
|
CardStatusCheckInterval: c.CardStatusCheckInterval,
|
|
PackageState: c.PackageState,
|
|
AssetStatus: c.AssetStatus,
|
|
PreResetWindow: c.PreResetWindow,
|
|
PreResetCarddataInterval: c.PreResetCarddataInterval,
|
|
Status: c.Status,
|
|
Description: c.Description,
|
|
CreatedAt: c.CreatedAt.Format(time.RFC3339),
|
|
UpdatedAt: c.UpdatedAt.Format(time.RFC3339),
|
|
}
|
|
}
|