// Package merchantpayment provides merchant pool payment routing use cases. package merchantpayment import ( "context" "reflect" "strconv" "strings" "time" "gorm.io/gorm" "gorm.io/gorm/clause" systemconfigapp "github.com/break/junhong_cmp_fiber/internal/application/systemconfig" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" "github.com/bytedance/sonic" ) // ManagementService 负责商户、商户池与授权配置写入。 type ManagementService struct { db *gorm.DB audit systemconfigapp.AuditWriter } // NewManagementService 创建商户配置用例。 func NewManagementService(db *gorm.DB, audit systemconfigapp.AuditWriter) *ManagementService { return &ManagementService{db: db, audit: audit} } func requireManager(ctx context.Context) error { kind := middleware.GetUserTypeFromContext(ctx) if kind != constants.UserTypeSuperAdmin && kind != constants.UserTypePlatform { return errors.New(errors.CodeForbidden, "无权限访问支付商户配置") } return nil } func normalizePage(page, size int) (int, int) { if page < 1 { page = 1 } if size < 1 { size = 20 } if size > 100 { size = 100 } return page, size } func validPaymentMethod(method string) bool { return method == "wechat" || method == "alipay" } func validateMerchantConfiguration(paymentMethod, providerType, merchantIdentity string, credentials model.JSONB) error { paymentMethod, providerType, merchantIdentity = strings.TrimSpace(paymentMethod), strings.TrimSpace(providerType), strings.TrimSpace(merchantIdentity) if !validPaymentMethod(paymentMethod) || merchantIdentity == "" || len(credentials) == 0 { return errors.New(errors.CodeInvalidParam, "支付商户配置不完整") } var config model.WechatConfig raw, err := sonic.Marshal(credentials) if err != nil || sonic.Unmarshal(raw, &config) != nil { return errors.New(errors.CodeInvalidParam, "支付商户凭证格式无效") } switch paymentMethod { case "wechat": switch providerType { case model.ProviderTypeWechat: if config.WxMchID != merchantIdentity || strings.TrimSpace(config.WxAPIV3Key) == "" || strings.TrimSpace(config.WxCertContent) == "" || strings.TrimSpace(config.WxKeyContent) == "" || strings.TrimSpace(config.WxSerialNo) == "" || strings.TrimSpace(config.WxNotifyURL) == "" { return errors.New(errors.CodeInvalidParam, "微信直连商户凭证不完整或身份不一致") } case model.ProviderTypeWechatV2: if config.WxMchID != merchantIdentity || strings.TrimSpace(config.WxAPIV2Key) == "" || strings.TrimSpace(config.WxNotifyURL) == "" { return errors.New(errors.CodeInvalidParam, "微信 v2 商户凭证不完整或身份不一致") } case model.ProviderTypeFuiou: if config.FyMchntCd != merchantIdentity || strings.TrimSpace(config.FyInsCd) == "" || strings.TrimSpace(config.FyTermID) == "" || strings.TrimSpace(config.FyPrivateKey) == "" || strings.TrimSpace(config.FyPublicKey) == "" || strings.TrimSpace(config.FyAPIURL) == "" || strings.TrimSpace(config.FyNotifyURL) == "" { return errors.New(errors.CodeInvalidParam, "富友商户凭证不完整或身份不一致") } default: return errors.New(errors.CodeInvalidParam, "微信支付服务商类型无效") } case "alipay": if providerType != "alipay" || config.AliAppID != merchantIdentity || strings.TrimSpace(config.AliPrivateKey) == "" || strings.TrimSpace(config.AliPublicKey) == "" || strings.TrimSpace(config.AliNotifyURL) == "" || strings.TrimSpace(config.AliReturnURL) == "" { return errors.New(errors.CodeInvalidParam, "支付宝商户凭证不完整或身份不一致") } } return nil } func validatePoolRequest(req dto.PaymentMerchantPoolRequest) error { if !validPaymentMethod(strings.TrimSpace(req.PaymentMethod)) { return errors.New(errors.CodeInvalidParam, "支付方式仅支持微信或支付宝") } switch req.Strategy { case model.PaymentMerchantStrategyAmount: if req.ThresholdAmount == nil || *req.ThresholdAmount <= 0 || req.ThresholdCount != nil || req.StatisticCycle == nil || !validStatisticCycle(*req.StatisticCycle) || req.TimePeriodValue != nil || req.TimePeriodUnit != nil || req.TimePeriodStartedAt != nil { return errors.New(errors.CodeInvalidParam, "金额轮询策略参数不完整") } case model.PaymentMerchantStrategyCount: if req.ThresholdCount == nil || *req.ThresholdCount <= 0 || req.ThresholdAmount != nil || req.StatisticCycle == nil || !validStatisticCycle(*req.StatisticCycle) || req.TimePeriodValue != nil || req.TimePeriodUnit != nil || req.TimePeriodStartedAt != nil { return errors.New(errors.CodeInvalidParam, "笔数轮询策略参数不完整") } case model.PaymentMerchantStrategyTime: if req.TimePeriodValue == nil || *req.TimePeriodValue < 1 || req.TimePeriodUnit == nil || !validTimeUnit(*req.TimePeriodUnit) || req.TimePeriodStartedAt == nil || req.ThresholdAmount != nil || req.ThresholdCount != nil || req.StatisticCycle != nil { return errors.New(errors.CodeInvalidParam, "时间轮询策略参数不完整") } default: return errors.New(errors.CodeInvalidParam, "不支持的商户池轮询策略") } return nil } func validStatisticCycle(value string) bool { return value == "round" || value == "day" || value == "month" } func validTimeUnit(value string) bool { return value == "minute" || value == "hour" || value == "day" } func (s *ManagementService) writeAudit(ctx context.Context, tx *gorm.DB, operation, description, key, name string, id uint, identity, before, after map[string]any) error { if s.audit == nil { return errors.New(errors.CodeInvalidStatus, "支付商户管理审计接缝未配置") } resourceID := strconv.FormatUint(uint64(id), 10) return s.audit.WriteConfigChange(ctx, tx, systemconfigapp.ChangeAudit{ OperatorID: middleware.GetUserIDFromContext(ctx), OperationType: operation, Description: description, ConfigKey: key, Module: "payment_merchant", ResourceID: &resourceID, DisplayName: name, Identity: identity, BeforeData: before, AfterData: after, Result: constants.AuditResultSuccess, }) } func merchantAuditIdentity(m *model.PaymentMerchant) map[string]any { return map[string]any{"id": m.ID, "name": m.Name, "payment_method": m.PaymentMethod, "provider_type": m.ProviderType, "merchant_identity": m.MerchantIdentity, "status": m.Status, "credential_version": m.CredentialVersion} } func poolAuditIdentity(p *model.PaymentMerchantPool) map[string]any { return map[string]any{"id": p.ID, "name": p.Name, "payment_method": p.PaymentMethod, "strategy": p.Strategy, "status": p.Status, "routing_epoch": p.RoutingEpoch} } func authorizationAuditIdentity(a *model.WechatAuthorization) map[string]any { return map[string]any{"id": a.ID, "status": a.Status, "credential_version": a.CredentialVersion, "oa_app_id": a.OaAppID, "miniapp_app_id": a.MiniappAppID} } // CreateMerchant 创建独立管理的支付商户。 func (s *ManagementService) CreateMerchant(ctx context.Context, req dto.PaymentMerchantRequest) (*dto.PaymentMerchantResponse, error) { if err := requireManager(ctx); err != nil { return nil, err } if s == nil || s.db == nil || strings.TrimSpace(req.Name) == "" || len(req.Credentials) == 0 { return nil, errors.New(errors.CodeInvalidParam, "商户参数或凭证不完整") } if !validPaymentMethod(strings.TrimSpace(req.PaymentMethod)) { return nil, errors.New(errors.CodeInvalidParam, "支付方式仅支持微信或支付宝") } m := &model.PaymentMerchant{Name: strings.TrimSpace(req.Name), PaymentMethod: strings.TrimSpace(req.PaymentMethod), ProviderType: strings.TrimSpace(req.ProviderType), MerchantIdentity: strings.TrimSpace(req.MerchantIdentity), Credentials: req.Credentials, CredentialVersion: 1, Remark: strings.TrimSpace(req.Remark), BaseModel: model.BaseModel{Creator: middleware.GetUserIDFromContext(ctx), Updater: middleware.GetUserIDFromContext(ctx)}} if req.Enabled { m.Status = model.PaymentMerchantStatusEnabled } if m.MerchantIdentity == "" || m.ProviderType == "" { return nil, errors.New(errors.CodeInvalidParam, "商户身份或服务商类型不能为空") } if err := validateMerchantConfiguration(m.PaymentMethod, m.ProviderType, m.MerchantIdentity, m.Credentials); err != nil { return nil, err } if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Create(m).Error; err != nil { return err } return s.writeAudit(ctx, tx, constants.AuditOperationPaymentConfigCreate, "创建支付商户", "payment_merchant:"+strconv.FormatUint(uint64(m.ID), 10), m.Name, m.ID, merchantAuditIdentity(m), nil, merchantAuditIdentity(m)) }); err != nil { if appErr, ok := err.(*errors.AppError); ok { return nil, appErr } return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建支付商户失败") } return merchantResponse(m), nil } // ListMerchants returns the privileged configuration projection. // ListPools returns one page of merchant pools and their ordered members without per-pool member queries. func (s *ManagementService) ListPools(ctx context.Context, req dto.PaymentMerchantPoolListRequest) ([]*dto.PaymentMerchantPoolResponse, int64, error) { if err := requireManager(ctx); err != nil { return nil, 0, err } page, size := normalizePage(req.Page, req.PageSize) query := s.db.WithContext(ctx).Model(&model.PaymentMerchantPool{}) var total int64 if err := query.Count(&total).Error; err != nil { return nil, 0, errors.Wrap(errors.CodeDatabaseError, err, "统计商户池失败") } var pools []model.PaymentMerchantPool if err := query.Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&pools).Error; err != nil { return nil, 0, errors.Wrap(errors.CodeDatabaseError, err, "查询商户池失败") } poolIDs := make([]uint, 0, len(pools)) for index := range pools { poolIDs = append(poolIDs, pools[index].ID) } membersByPool := make(map[uint][]uint, len(pools)) if len(poolIDs) > 0 { var members []model.PaymentMerchantPoolMember if err := s.db.WithContext(ctx).Where("pool_id IN ?", poolIDs).Order("pool_id ASC, sort_order ASC").Find(&members).Error; err != nil { return nil, 0, errors.Wrap(errors.CodeDatabaseError, err, "查询商户池成员失败") } for index := range members { member := &members[index] membersByPool[member.PoolID] = append(membersByPool[member.PoolID], member.MerchantID) } } result := make([]*dto.PaymentMerchantPoolResponse, 0, len(pools)) for index := range pools { pool := &pools[index] result = append(result, &dto.PaymentMerchantPoolResponse{ID: pool.ID, Name: pool.Name, PaymentMethod: pool.PaymentMethod, Enabled: pool.Status == model.PaymentMerchantStatusEnabled, Strategy: pool.Strategy, ThresholdAmount: pool.ThresholdAmount, ThresholdCount: pool.ThresholdCount, StatisticCycle: pool.StatisticCycle, TimePeriodValue: pool.TimePeriodValue, TimePeriodUnit: pool.TimePeriodUnit, TimePeriodStartedAt: pool.TimePeriodStartedAt, RoutingEpoch: pool.RoutingEpoch, MemberIDs: membersByPool[pool.ID], Remark: pool.Remark}) } return result, total, nil } // GetPool 查询一个商户池及其有序成员。 func (s *ManagementService) GetPool(ctx context.Context, id uint) (*dto.PaymentMerchantPoolResponse, error) { if err := requireManager(ctx); err != nil { return nil, err } var pool model.PaymentMerchantPool if err := s.db.WithContext(ctx).First(&pool, id).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "商户池不存在") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询商户池失败") } return poolResponse(ctx, s.db, &pool) } // ListMerchants 分页查询特权商户配置。 func (s *ManagementService) ListMerchants(ctx context.Context, req dto.PaymentMerchantListRequest) ([]*dto.PaymentMerchantResponse, int64, error) { if err := requireManager(ctx); err != nil { return nil, 0, err } page, size := normalizePage(req.Page, req.PageSize) query := s.db.WithContext(ctx).Model(&model.PaymentMerchant{}) if req.PaymentMethod != nil { query = query.Where("payment_method = ?", strings.TrimSpace(*req.PaymentMethod)) } if req.Enabled != nil { status := model.PaymentMerchantStatusDisabled if *req.Enabled { status = model.PaymentMerchantStatusEnabled } query = query.Where("status = ?", status) } var total int64 if err := query.Count(&total).Error; err != nil { return nil, 0, errors.Wrap(errors.CodeDatabaseError, err, "查询支付商户失败") } var rows []model.PaymentMerchant if err := query.Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&rows).Error; err != nil { return nil, 0, errors.Wrap(errors.CodeDatabaseError, err, "查询支付商户失败") } result := make([]*dto.PaymentMerchantResponse, 0, len(rows)) for index := range rows { result = append(result, merchantResponse(&rows[index])) } return result, total, nil } // GetMerchant 查询一个特权商户配置。 func (s *ManagementService) GetMerchant(ctx context.Context, id uint) (*dto.PaymentMerchantResponse, error) { if err := requireManager(ctx); err != nil { return nil, err } var m model.PaymentMerchant if err := s.db.WithContext(ctx).First(&m, id).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "支付商户不存在") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询支付商户失败") } return merchantResponse(&m), nil } // UpdateMerchant 更新商户凭证和可变配置。 func (s *ManagementService) UpdateMerchant(ctx context.Context, id uint, req dto.PaymentMerchantUpdateRequest) (*dto.PaymentMerchantResponse, error) { if err := requireManager(ctx); err != nil { return nil, err } if req.PaymentMethod != nil && !validPaymentMethod(strings.TrimSpace(*req.PaymentMethod)) { return nil, errors.New(errors.CodeInvalidParam, "支付方式仅支持微信或支付宝") } var m model.PaymentMerchant if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&m, id).Error; err != nil { return err } before := merchantAuditIdentity(&m) previous := m var refs int64 if err := tx.Model(&model.Payment{}).Where("merchant_id = ?", id).Count(&refs).Error; err != nil { return err } if refs > 0 && ((req.PaymentMethod != nil && *req.PaymentMethod != m.PaymentMethod) || (req.ProviderType != nil && *req.ProviderType != m.ProviderType) || (req.MerchantIdentity != nil && *req.MerchantIdentity != m.MerchantIdentity)) { return errors.New(errors.CodeConflict, "已被支付单引用,不能修改收款身份") } if req.Name != nil { m.Name = strings.TrimSpace(*req.Name) } if req.PaymentMethod != nil { m.PaymentMethod = strings.TrimSpace(*req.PaymentMethod) } if req.ProviderType != nil { m.ProviderType = strings.TrimSpace(*req.ProviderType) } if req.MerchantIdentity != nil { m.MerchantIdentity = strings.TrimSpace(*req.MerchantIdentity) } if req.Remark != nil { m.Remark = strings.TrimSpace(*req.Remark) } if req.Enabled != nil { m.Status = model.PaymentMerchantStatusDisabled if *req.Enabled { m.Status = model.PaymentMerchantStatusEnabled } } if req.Credentials != nil && !reflect.DeepEqual(m.Credentials, *req.Credentials) { m.Credentials = *req.Credentials } if previous.Name != m.Name || previous.PaymentMethod != m.PaymentMethod || previous.ProviderType != m.ProviderType || previous.MerchantIdentity != m.MerchantIdentity || previous.Status != m.Status || !reflect.DeepEqual(previous.Credentials, m.Credentials) { m.CredentialVersion++ } if err := validateMerchantConfiguration(m.PaymentMethod, m.ProviderType, m.MerchantIdentity, m.Credentials); err != nil { return err } if err := tx.Save(&m).Error; err != nil { return err } return s.writeAudit(ctx, tx, constants.AuditOperationPaymentConfigUpdate, "更新支付商户", "payment_merchant:"+strconv.FormatUint(uint64(m.ID), 10), m.Name, m.ID, merchantAuditIdentity(&m), before, merchantAuditIdentity(&m)) }); err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "支付商户不存在") } if appErr, ok := err.(*errors.AppError); ok { return nil, appErr } return nil, errors.Wrap(errors.CodeDatabaseError, err, "更新支付商户失败") } return merchantResponse(&m), nil } // DeleteMerchant 仅在未被引用且二次确认后删除商户。 func (s *ManagementService) DeleteMerchant(ctx context.Context, id uint, confirm bool) error { if err := requireManager(ctx); err != nil { return err } if !confirm { return errors.New(errors.CodeInvalidParam, "删除商户必须二次确认") } return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var refs, members int64 var merchant model.PaymentMerchant if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&merchant, id).Error; err != nil { return err } before := merchantAuditIdentity(&merchant) if err := tx.Model(&model.Payment{}).Where("merchant_id = ?", id).Count(&refs).Error; err != nil { return err } if refs > 0 { return errors.New(errors.CodeConflict, "已被支付单引用的商户不能删除") } if err := tx.Model(&model.PaymentMerchantPoolMember{}).Where("merchant_id = ?", id).Count(&members).Error; err != nil { return err } if members > 0 { return errors.New(errors.CodeConflict, "商户仍属于商户池") } r := tx.Delete(&model.PaymentMerchant{}, id) if r.Error != nil { return r.Error } if r.RowsAffected == 0 { return errors.New(errors.CodeNotFound, "支付商户不存在") } if err := s.writeAudit(ctx, tx, constants.AuditOperationPaymentConfigDelete, "删除支付商户", "payment_merchant:"+strconv.FormatUint(uint64(merchant.ID), 10), merchant.Name, merchant.ID, before, before, nil); err != nil { return err } return nil }) } // SavePool 创建或更新商户池,并原子替换有序成员。 func (s *ManagementService) SavePool(ctx context.Context, id uint, req dto.PaymentMerchantPoolRequest) (*dto.PaymentMerchantPoolResponse, error) { if err := requireManager(ctx); err != nil { return nil, err } if err := validatePoolRequest(req); err != nil { return nil, err } if len(req.MemberIDs) == 0 { return nil, errors.New(errors.CodeInvalidParam, "商户池至少需要一个商户") } var pool model.PaymentMerchantPool err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { creating := id == 0 var previousMemberIDs []uint if !creating { if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&pool, id).Error; err != nil { return err } var previousMembers []model.PaymentMerchantPoolMember if err := tx.Where("pool_id = ?", pool.ID).Order("sort_order ASC").Find(&previousMembers).Error; err != nil { return err } previousMemberIDs = make([]uint, 0, len(previousMembers)) for _, member := range previousMembers { previousMemberIDs = append(previousMemberIDs, member.MerchantID) } } else { pool.Creator = middleware.GetUserIDFromContext(ctx) pool.RoutingEpoch = 1 } before := poolAuditIdentity(&pool) if err := validatePoolMembers(ctx, tx, req.PaymentMethod, req.MemberIDs); err != nil { return err } if !creating && poolEpochChanged(&pool, &req, previousMemberIDs) { pool.RoutingEpoch++ } pool.Name, pool.PaymentMethod, pool.Strategy, pool.Remark = strings.TrimSpace(req.Name), strings.TrimSpace(req.PaymentMethod), strings.TrimSpace(req.Strategy), strings.TrimSpace(req.Remark) pool.ThresholdAmount, pool.ThresholdCount, pool.StatisticCycle, pool.TimePeriodValue, pool.TimePeriodUnit, pool.TimePeriodStartedAt = req.ThresholdAmount, req.ThresholdCount, req.StatisticCycle, req.TimePeriodValue, req.TimePeriodUnit, req.TimePeriodStartedAt if req.Enabled { var others int64 if err := tx.Model(&model.PaymentMerchantPool{}).Where("payment_method = ? AND status = ? AND id <> ?", pool.PaymentMethod, model.PaymentMerchantStatusEnabled, pool.ID).Count(&others).Error; err != nil { return err } if others > 0 { return errors.New(errors.CodeConflict, "该支付方式已有启用商户池") } } pool.Status = model.PaymentMerchantStatusDisabled if req.Enabled { pool.Status = model.PaymentMerchantStatusEnabled } pool.Updater = middleware.GetUserIDFromContext(ctx) if creating { if err := tx.Create(&pool).Error; err != nil { return err } } else if err := tx.Save(&pool).Error; err != nil { return err } if err := tx.Where("pool_id = ?", pool.ID).Delete(&model.PaymentMerchantPoolMember{}).Error; err != nil { return err } members := make([]model.PaymentMerchantPoolMember, 0, len(req.MemberIDs)) for i, merchantID := range req.MemberIDs { members = append(members, model.PaymentMerchantPoolMember{PoolID: pool.ID, MerchantID: merchantID, SortOrder: int64(i), BaseModel: model.BaseModel{Creator: middleware.GetUserIDFromContext(ctx), Updater: middleware.GetUserIDFromContext(ctx)}}) } if err := tx.Create(&members).Error; err != nil { return err } op := constants.AuditOperationPaymentConfigUpdate summary := "更新商户池" if creating { op = constants.AuditOperationPaymentConfigCreate summary = "创建商户池" } return s.writeAudit(ctx, tx, op, summary, "payment_merchant_pool:"+strconv.FormatUint(uint64(pool.ID), 10), pool.Name, pool.ID, poolAuditIdentity(&pool), before, poolAuditIdentity(&pool)) }) if err != nil { if appErr, ok := err.(*errors.AppError); ok { return nil, appErr } if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "商户池不存在") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "保存商户池失败") } return poolResponse(ctx, s.db, &pool) } // SetPoolEnabled enables or disables a pool after rechecking the active-pool and member invariants. func (s *ManagementService) SetPoolEnabled(ctx context.Context, id uint, enabled bool) (*dto.PaymentMerchantPoolResponse, error) { if err := requireManager(ctx); err != nil { return nil, err } var pool model.PaymentMerchantPool err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&pool, id).Error; err != nil { return err } before := poolAuditIdentity(&pool) if enabled { var others int64 if err := tx.Model(&model.PaymentMerchantPool{}).Where("payment_method = ? AND status = ? AND id <> ?", pool.PaymentMethod, model.PaymentMerchantStatusEnabled, pool.ID).Count(&others).Error; err != nil { return err } if others > 0 { return errors.New(errors.CodeConflict, "该支付方式已有启用商户池") } var members []model.PaymentMerchantPoolMember if err := tx.Where("pool_id = ?", pool.ID).Order("sort_order ASC").Find(&members).Error; err != nil { return err } ids := make([]uint, 0, len(members)) for _, member := range members { ids = append(ids, member.MerchantID) } if err := validatePoolMembers(ctx, tx, pool.PaymentMethod, ids); err != nil { return err } pool.Status = model.PaymentMerchantStatusEnabled } else { pool.Status = model.PaymentMerchantStatusDisabled } pool.Updater = middleware.GetUserIDFromContext(ctx) if err := tx.Save(&pool).Error; err != nil { return err } op := constants.AuditOperationPaymentConfigDeactivate summary := "停用商户池" if enabled { op = constants.AuditOperationPaymentConfigActivate summary = "启用商户池" } return s.writeAudit(ctx, tx, op, summary, "payment_merchant_pool:"+strconv.FormatUint(uint64(pool.ID), 10), pool.Name, pool.ID, poolAuditIdentity(&pool), before, poolAuditIdentity(&pool)) }) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "商户池不存在") } if appErr, ok := err.(*errors.AppError); ok { return nil, appErr } return nil, errors.Wrap(errors.CodeDatabaseError, err, "更新商户池状态失败") } return poolResponse(ctx, s.db, &pool) } func validatePoolMembers(ctx context.Context, tx *gorm.DB, method string, ids []uint) error { seen := map[uint]struct{}{} for _, id := range ids { if id == 0 { return errors.New(errors.CodeInvalidParam, "商户ID无效") } if _, ok := seen[id]; ok { return errors.New(errors.CodeInvalidParam, "商户池成员不能重复") } seen[id] = struct{}{} } var merchants []model.PaymentMerchant if err := tx.WithContext(ctx).Where("id IN ? AND payment_method = ? AND status = ?", ids, method, model.PaymentMerchantStatusEnabled).Find(&merchants).Error; err != nil { return err } if len(merchants) != len(ids) { return errors.New(errors.CodeConflict, "商户池成员必须存在、启用且支付方式一致") } for index := range merchants { merchant := &merchants[index] if err := validateMerchantConfiguration(merchant.PaymentMethod, merchant.ProviderType, merchant.MerchantIdentity, merchant.Credentials); err != nil { return err } } return nil } func poolEpochChanged(pool *model.PaymentMerchantPool, request *dto.PaymentMerchantPoolRequest, previousMemberIDs []uint) bool { if pool.PaymentMethod != strings.TrimSpace(request.PaymentMethod) || pool.Strategy != request.Strategy || !sameString(pool.StatisticCycle, request.StatisticCycle) || !sameInt64(pool.TimePeriodValue, request.TimePeriodValue) || !sameString(pool.TimePeriodUnit, request.TimePeriodUnit) || !sameTime(pool.TimePeriodStartedAt, request.TimePeriodStartedAt) { return true } if sameMemberOrder(previousMemberIDs, request.MemberIDs) { return false } return pool.StatisticCycle == nil || (*pool.StatisticCycle != "day" && *pool.StatisticCycle != "month") || !sameMemberSet(previousMemberIDs, request.MemberIDs) } func sameMemberOrder(left, right []uint) bool { if len(left) != len(right) { return false } for i := range left { if left[i] != right[i] { return false } } return true } func sameMemberSet(left, right []uint) bool { if len(left) != len(right) { return false } seen := make(map[uint]struct{}, len(left)) for _, id := range left { seen[id] = struct{}{} } for _, id := range right { if _, ok := seen[id]; !ok { return false } } return true } func sameString(left, right *string) bool { if left == nil || right == nil { return left == right } return *left == *right } func sameInt64(left, right *int64) bool { if left == nil || right == nil { return left == right } return *left == *right } func sameTime(a, b *time.Time) bool { if a == nil || b == nil { return a == b } return a.Equal(*b) } func merchantResponse(m *model.PaymentMerchant) *dto.PaymentMerchantResponse { return &dto.PaymentMerchantResponse{ID: m.ID, Name: m.Name, PaymentMethod: m.PaymentMethod, ProviderType: m.ProviderType, MerchantIdentity: m.MerchantIdentity, Credentials: m.Credentials, CredentialVersion: m.CredentialVersion, Enabled: m.Status == model.PaymentMerchantStatusEnabled, Remark: m.Remark, CreatedAt: m.CreatedAt, UpdatedAt: m.UpdatedAt} } func poolResponse(ctx context.Context, db *gorm.DB, p *model.PaymentMerchantPool) (*dto.PaymentMerchantPoolResponse, error) { var rows []model.PaymentMerchantPoolMember if err := db.WithContext(ctx).Where("pool_id = ?", p.ID).Order("sort_order ASC").Find(&rows).Error; err != nil { return nil, err } ids := make([]uint, 0, len(rows)) for _, row := range rows { ids = append(ids, row.MerchantID) } return &dto.PaymentMerchantPoolResponse{ID: p.ID, Name: p.Name, PaymentMethod: p.PaymentMethod, Enabled: p.Status == model.PaymentMerchantStatusEnabled, Strategy: p.Strategy, ThresholdAmount: p.ThresholdAmount, ThresholdCount: p.ThresholdCount, StatisticCycle: p.StatisticCycle, TimePeriodValue: p.TimePeriodValue, TimePeriodUnit: p.TimePeriodUnit, TimePeriodStartedAt: p.TimePeriodStartedAt, RoutingEpoch: p.RoutingEpoch, MemberIDs: ids, Remark: p.Remark}, nil } // GetAuthorization 查询特权全局授权配置。 func (s *ManagementService) GetAuthorization(ctx context.Context) (*dto.WechatAuthorizationResponse, error) { if err := requireManager(ctx); err != nil { return nil, err } var a model.WechatAuthorization if err := s.db.WithContext(ctx).First(&a).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, nil } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询微信授权配置失败") } return authorizationResponse(&a), nil } // SaveAuthorization 创建或更新唯一启用的授权配置。 func (s *ManagementService) SaveAuthorization(ctx context.Context, req dto.WechatAuthorizationRequest) (*dto.WechatAuthorizationResponse, error) { if err := requireManager(ctx); err != nil { return nil, err } if req.Enabled && (strings.TrimSpace(req.OaAppID) == "" || strings.TrimSpace(req.OaAppSecret) == "" || strings.TrimSpace(req.MiniappAppID) == "" || strings.TrimSpace(req.MiniappAppSecret) == "") { return nil, errors.New(errors.CodeInvalidParam, "启用微信授权配置时公众号和小程序凭证必须完整") } var authorization model.WechatAuthorization err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&authorization).Error if err != nil && err != gorm.ErrRecordNotFound { return err } previousStatus := authorization.Status creating := err == gorm.ErrRecordNotFound before := authorizationAuditIdentity(&authorization) if creating { authorization.Creator = middleware.GetUserIDFromContext(ctx) authorization.CredentialVersion = 1 } changed := authorization.OaAppID != req.OaAppID || authorization.OaAppSecret != req.OaAppSecret || authorization.OaToken != req.OaToken || authorization.OaAesKey != req.OaAesKey || authorization.OaOAuthRedirectURL != req.OaOAuthRedirectURL || authorization.MiniappAppID != req.MiniappAppID || authorization.MiniappAppSecret != req.MiniappAppSecret authorization.OaAppID, authorization.OaAppSecret, authorization.OaToken, authorization.OaAesKey, authorization.OaOAuthRedirectURL, authorization.MiniappAppID, authorization.MiniappAppSecret = req.OaAppID, req.OaAppSecret, req.OaToken, req.OaAesKey, req.OaOAuthRedirectURL, req.MiniappAppID, req.MiniappAppSecret authorization.Status = model.PaymentMerchantStatusDisabled if req.Enabled { authorization.Status = model.PaymentMerchantStatusEnabled } if !creating && (changed || previousStatus != authorization.Status) { authorization.CredentialVersion++ } authorization.Updater = middleware.GetUserIDFromContext(ctx) if creating { if err := tx.Create(&authorization).Error; err != nil { return err } } else { if err := tx.Save(&authorization).Error; err != nil { return err } } op := constants.AuditOperationPaymentConfigUpdate summary := "更新微信授权配置" if creating { op = constants.AuditOperationPaymentConfigCreate summary = "创建微信授权配置" } return s.writeAudit(ctx, tx, op, summary, "wechat_authorization:"+strconv.FormatUint(uint64(authorization.ID), 10), "微信授权配置", authorization.ID, authorizationAuditIdentity(&authorization), before, authorizationAuditIdentity(&authorization)) }) if err != nil { if appErr, ok := err.(*errors.AppError); ok { return nil, appErr } return nil, errors.Wrap(errors.CodeDatabaseError, err, "保存微信授权配置失败") } return authorizationResponse(&authorization), nil } func authorizationResponse(a *model.WechatAuthorization) *dto.WechatAuthorizationResponse { return &dto.WechatAuthorizationResponse{ID: a.ID, OaAppID: a.OaAppID, OaAppSecret: a.OaAppSecret, OaToken: a.OaToken, OaAesKey: a.OaAesKey, OaOAuthRedirectURL: a.OaOAuthRedirectURL, MiniappAppID: a.MiniappAppID, MiniappAppSecret: a.MiniappAppSecret, CredentialVersion: a.CredentialVersion, Enabled: a.Status == model.PaymentMerchantStatusEnabled, UpdatedAt: a.UpdatedAt} }