// Package wechat_config 提供微信参数配置管理的业务逻辑服务 // 包含配置的 CRUD、激活/停用、Redis 缓存等功能 package wechat_config import ( "context" "fmt" "time" "github.com/bytedance/sonic" "github.com/redis/go-redis/v9" "go.uber.org/zap" "gorm.io/gorm" "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/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" ) // Redis 缓存键 const redisActiveConfigKey = "wechat:config:active" // AuditServiceInterface 审计日志服务接口 type AuditServiceInterface interface { LogOperation(ctx context.Context, log *model.AccountOperationLog) } // Service 微信参数配置业务服务 type Service struct { store *postgres.WechatConfigStore orderStore *postgres.OrderStore auditService AuditServiceInterface redis *redis.Client logger *zap.Logger } // New 创建微信参数配置服务实例 func New(store *postgres.WechatConfigStore, orderStore *postgres.OrderStore, auditService AuditServiceInterface, rdb *redis.Client, logger *zap.Logger) *Service { return &Service{ store: store, orderStore: orderStore, auditService: auditService, redis: rdb, logger: logger, } } // Create 创建微信参数配置 // POST /api/admin/wechat-configs func (s *Service) Create(ctx context.Context, req *dto.CreateWechatConfigRequest) (*dto.WechatConfigResponse, error) { // 根据 provider_type 校验必填字段 if err := s.validateProviderFields(req); err != nil { return nil, err } var desc *string if req.Description != "" { desc = &req.Description } config := &model.WechatConfig{ Name: req.Name, Description: desc, ProviderType: req.ProviderType, IsActive: false, OaAppID: req.OaAppID, OaAppSecret: req.OaAppSecret, OaToken: req.OaToken, OaAesKey: req.OaAesKey, OaOAuthRedirectURL: req.OaOAuthRedirectURL, MiniappAppID: req.MiniappAppID, MiniappAppSecret: req.MiniappAppSecret, WxMchID: req.WxMchID, WxAPIV3Key: req.WxAPIV3Key, WxAPIV2Key: req.WxAPIV2Key, WxCertContent: req.WxCertContent, WxKeyContent: req.WxKeyContent, WxSerialNo: req.WxSerialNo, WxNotifyURL: req.WxNotifyURL, FyInsCd: req.FyInsCd, FyMchntCd: req.FyMchntCd, FyTermID: req.FyTermID, FyPrivateKey: req.FyPrivateKey, FyPublicKey: req.FyPublicKey, FyAPIURL: req.FyAPIURL, FyNotifyURL: req.FyNotifyURL, } config.Creator = middleware.GetUserIDFromContext(ctx) if err := s.store.Create(ctx, config); err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "创建微信支付配置失败") } // 审计日志 afterData := model.JSONB{ "id": config.ID, "name": config.Name, "provider_type": config.ProviderType, } go s.auditService.LogOperation(ctx, &model.AccountOperationLog{ OperatorID: middleware.GetUserIDFromContext(ctx), OperatorType: middleware.GetUserTypeFromContext(ctx), OperatorName: "", OperationType: "create", OperationDesc: fmt.Sprintf("创建微信支付配置:%s", config.Name), AfterData: afterData, RequestID: middleware.GetRequestIDFromContext(ctx), IPAddress: middleware.GetIPFromContext(ctx), UserAgent: middleware.GetUserAgentFromContext(ctx), }) return dto.FromWechatConfigModel(config), nil } // List 获取配置列表 // GET /api/admin/wechat-configs func (s *Service) List(ctx context.Context, req *dto.WechatConfigListRequest) ([]*dto.WechatConfigResponse, 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.ProviderType != nil { filters["provider_type"] = *req.ProviderType } filters["is_active"] = req.IsActive configs, total, err := s.store.List(ctx, opts, filters) if err != nil { return nil, 0, errors.Wrap(errors.CodeInternalError, err, "查询微信支付配置列表失败") } responses := make([]*dto.WechatConfigResponse, len(configs)) for i, c := range configs { responses[i] = dto.FromWechatConfigModel(c) } return responses, total, nil } // Get 获取配置详情 // GET /api/admin/wechat-configs/:id func (s *Service) Get(ctx context.Context, id uint) (*dto.WechatConfigResponse, error) { config, err := s.store.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeWechatConfigNotFound) } return nil, errors.Wrap(errors.CodeInternalError, err, "获取微信支付配置失败") } return dto.FromWechatConfigModel(config), nil } // Update 更新微信参数配置 // PUT /api/admin/wechat-configs/:id func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateWechatConfigRequest) (*dto.WechatConfigResponse, error) { config, err := s.store.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeWechatConfigNotFound) } return nil, errors.Wrap(errors.CodeInternalError, err, "获取微信支付配置失败") } // 合并字段:指针非 nil 时更新,敏感字段空字符串表示保持原值 if req.Name != nil { config.Name = *req.Name } if req.Description != nil { config.Description = req.Description } if req.ProviderType != nil { config.ProviderType = *req.ProviderType } // OAuth 公众号 s.mergeStringField(&config.OaAppID, req.OaAppID) s.mergeSensitiveField(&config.OaAppSecret, req.OaAppSecret) s.mergeSensitiveField(&config.OaToken, req.OaToken) s.mergeSensitiveField(&config.OaAesKey, req.OaAesKey) s.mergeStringField(&config.OaOAuthRedirectURL, req.OaOAuthRedirectURL) // OAuth 小程序 s.mergeStringField(&config.MiniappAppID, req.MiniappAppID) s.mergeSensitiveField(&config.MiniappAppSecret, req.MiniappAppSecret) // 微信直连支付 s.mergeStringField(&config.WxMchID, req.WxMchID) s.mergeSensitiveField(&config.WxAPIV3Key, req.WxAPIV3Key) s.mergeSensitiveField(&config.WxAPIV2Key, req.WxAPIV2Key) s.mergeSensitiveField(&config.WxCertContent, req.WxCertContent) s.mergeSensitiveField(&config.WxKeyContent, req.WxKeyContent) s.mergeStringField(&config.WxSerialNo, req.WxSerialNo) s.mergeStringField(&config.WxNotifyURL, req.WxNotifyURL) // 富友支付 s.mergeStringField(&config.FyInsCd, req.FyInsCd) s.mergeStringField(&config.FyMchntCd, req.FyMchntCd) s.mergeStringField(&config.FyTermID, req.FyTermID) s.mergeSensitiveField(&config.FyPrivateKey, req.FyPrivateKey) s.mergeSensitiveField(&config.FyPublicKey, req.FyPublicKey) s.mergeStringField(&config.FyAPIURL, req.FyAPIURL) s.mergeStringField(&config.FyNotifyURL, req.FyNotifyURL) config.Updater = middleware.GetUserIDFromContext(ctx) if err := s.store.Update(ctx, config); err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "更新微信支付配置失败") } // 如果当前配置处于激活状态,清除缓存 if config.IsActive { s.clearActiveConfigCache(ctx) } afterData := model.JSONB{ "id": config.ID, "name": config.Name, "provider_type": config.ProviderType, "is_active": config.IsActive, } go s.auditService.LogOperation(ctx, &model.AccountOperationLog{ OperatorID: middleware.GetUserIDFromContext(ctx), OperatorType: middleware.GetUserTypeFromContext(ctx), OperatorName: "", OperationType: "update", OperationDesc: fmt.Sprintf("更新微信支付配置:%s", config.Name), AfterData: afterData, RequestID: middleware.GetRequestIDFromContext(ctx), IPAddress: middleware.GetIPFromContext(ctx), UserAgent: middleware.GetUserAgentFromContext(ctx), }) return dto.FromWechatConfigModel(config), nil } // Delete 删除微信参数配置 // DELETE /api/admin/wechat-configs/:id func (s *Service) Delete(ctx context.Context, id uint) error { config, err := s.store.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeWechatConfigNotFound) } return errors.Wrap(errors.CodeInternalError, err, "获取微信支付配置失败") } // 不允许删除正在激活的配置 if config.IsActive { return errors.New(errors.CodeWechatConfigActive) } // 检查是否存在待支付订单 pendingOrders, err := s.store.CountPendingOrdersByConfigID(ctx, id) if err != nil { return errors.Wrap(errors.CodeInternalError, err, "检查在途订单失败") } pendingRecharges, err := s.store.CountPendingRechargesByConfigID(ctx, id) if err != nil { return errors.Wrap(errors.CodeInternalError, err, "检查在途充值失败") } if pendingOrders > 0 || pendingRecharges > 0 { return errors.New(errors.CodeWechatConfigHasPendingOrders) } if err := s.store.SoftDelete(ctx, id); err != nil { return errors.Wrap(errors.CodeInternalError, err, "删除微信支付配置失败") } s.clearActiveConfigCache(ctx) beforeData := model.JSONB{ "id": config.ID, "name": config.Name, "provider_type": config.ProviderType, } go s.auditService.LogOperation(ctx, &model.AccountOperationLog{ OperatorID: middleware.GetUserIDFromContext(ctx), OperatorType: middleware.GetUserTypeFromContext(ctx), OperatorName: "", OperationType: "delete", OperationDesc: fmt.Sprintf("删除微信支付配置:%s", config.Name), BeforeData: beforeData, RequestID: middleware.GetRequestIDFromContext(ctx), IPAddress: middleware.GetIPFromContext(ctx), UserAgent: middleware.GetUserAgentFromContext(ctx), }) return nil } // Activate 激活指定配置(同一时间只有一个激活配置) // POST /api/admin/wechat-configs/:id/activate func (s *Service) Activate(ctx context.Context, id uint) (*dto.WechatConfigResponse, error) { config, err := s.store.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeWechatConfigNotFound) } return nil, errors.Wrap(errors.CodeInternalError, err, "获取微信支付配置失败") } // 记录旧的激活配置名称 oldActiveName := "" oldActive, oldErr := s.store.GetActive(ctx) if oldErr == nil && oldActive != nil { oldActiveName = oldActive.Name } // 事务内激活 db := s.store.DB() if err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return s.store.ActivateInTx(ctx, tx, id) }); err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "激活微信支付配置失败") } s.clearActiveConfigCache(ctx) // 重新查询最新状态 config, _ = s.store.GetByID(ctx, id) desc := fmt.Sprintf("激活微信支付配置:%s", config.Name) if oldActiveName != "" && oldActiveName != config.Name { desc = fmt.Sprintf("激活微信支付配置:%s(原激活配置:%s)", config.Name, oldActiveName) } go s.auditService.LogOperation(ctx, &model.AccountOperationLog{ OperatorID: middleware.GetUserIDFromContext(ctx), OperatorType: middleware.GetUserTypeFromContext(ctx), OperatorName: "", OperationType: "activate", OperationDesc: desc, AfterData: model.JSONB{"id": config.ID, "name": config.Name, "is_active": true}, RequestID: middleware.GetRequestIDFromContext(ctx), IPAddress: middleware.GetIPFromContext(ctx), UserAgent: middleware.GetUserAgentFromContext(ctx), }) return dto.FromWechatConfigModel(config), nil } // Deactivate 停用指定配置 // POST /api/admin/wechat-configs/:id/deactivate func (s *Service) Deactivate(ctx context.Context, id uint) (*dto.WechatConfigResponse, error) { config, err := s.store.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeWechatConfigNotFound) } return nil, errors.Wrap(errors.CodeInternalError, err, "获取微信支付配置失败") } if err := s.store.Deactivate(ctx, id); err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "停用微信支付配置失败") } s.clearActiveConfigCache(ctx) // 重新查询最新状态 config, _ = s.store.GetByID(ctx, id) go s.auditService.LogOperation(ctx, &model.AccountOperationLog{ OperatorID: middleware.GetUserIDFromContext(ctx), OperatorType: middleware.GetUserTypeFromContext(ctx), OperatorName: "", OperationType: "deactivate", OperationDesc: fmt.Sprintf("停用微信支付配置:%s", config.Name), AfterData: model.JSONB{"id": config.ID, "name": config.Name, "is_active": false}, RequestID: middleware.GetRequestIDFromContext(ctx), IPAddress: middleware.GetIPFromContext(ctx), UserAgent: middleware.GetUserAgentFromContext(ctx), }) return dto.FromWechatConfigModel(config), nil } // GetActiveConfig 获取当前生效的支付配置(带 Redis 缓存) // 缓存策略:命中直接返回,未命中查 DB 后缓存 5 分钟,无记录缓存 "none" 1 分钟 func (s *Service) GetActiveConfig(ctx context.Context) (*model.WechatConfig, error) { // 尝试从 Redis 获取 val, err := s.redis.Get(ctx, redisActiveConfigKey).Result() if err == nil { if val == "none" { return nil, nil } var config model.WechatConfig if err := sonic.UnmarshalString(val, &config); err == nil { return &config, nil } } // Redis 未命中,查询数据库 config, err := s.store.GetActive(ctx) if err != nil { if err == gorm.ErrRecordNotFound { // 无激活配置,缓存空标记 1 分钟 s.redis.Set(ctx, redisActiveConfigKey, "none", 1*time.Minute) return nil, nil } return nil, errors.Wrap(errors.CodeInternalError, err, "查询激活配置失败") } // 缓存配置 5 分钟 if data, err := sonic.MarshalString(config); err == nil { s.redis.Set(ctx, redisActiveConfigKey, data, 5*time.Minute) } return config, nil } // GetActiveConfigForAPI 获取当前生效的支付配置(API 响应,已脱敏) func (s *Service) GetActiveConfigForAPI(ctx context.Context) (*dto.WechatConfigResponse, error) { config, err := s.GetActiveConfig(ctx) if err != nil { return nil, err } if config == nil { return nil, nil } return dto.FromWechatConfigModel(config), nil } // clearActiveConfigCache 清除激活配置的 Redis 缓存 func (s *Service) clearActiveConfigCache(ctx context.Context) { if err := s.redis.Del(ctx, redisActiveConfigKey).Err(); err != nil { s.logger.Warn("清除微信支付配置缓存失败", zap.Error(err)) } } // validateProviderFields 根据支付渠道类型校验必填字段 func (s *Service) validateProviderFields(req *dto.CreateWechatConfigRequest) error { switch req.ProviderType { case model.ProviderTypeWechat: if req.WxMchID == "" || req.WxAPIV3Key == "" || req.WxCertContent == "" || req.WxKeyContent == "" || req.WxSerialNo == "" || req.WxNotifyURL == "" { return errors.New(errors.CodeInvalidParam, "微信直连支付必填字段不完整:wx_mch_id, wx_api_v3_key, wx_cert_content, wx_key_content, wx_serial_no, wx_notify_url") } case model.ProviderTypeFuiou: if req.FyInsCd == "" || req.FyMchntCd == "" || req.FyTermID == "" || req.FyPrivateKey == "" || req.FyPublicKey == "" || req.FyAPIURL == "" || req.FyNotifyURL == "" { return errors.New(errors.CodeInvalidParam, "富友支付必填字段不完整:fy_ins_cd, fy_mchnt_cd, fy_term_id, fy_private_key, fy_public_key, fy_api_url, fy_notify_url") } } return nil } // mergeStringField 合并普通字符串字段:指针非 nil 时用新值覆盖 func (s *Service) mergeStringField(target *string, newVal *string) { if newVal != nil { *target = *newVal } } // mergeSensitiveField 合并敏感字段:指针非 nil 且非空字符串时覆盖,空字符串保持原值 func (s *Service) mergeSensitiveField(target *string, newVal *string) { if newVal != nil && *newVal != "" { *target = *newVal } }