Files
junhong_cmp_fiber/internal/service/wechat_config/service.go
huang c86afbfa8f feat: 新增微信参数配置模块(Model、DTO、Store、Service)
- wechat_config.go: WechatConfig GORM 模型,含 ProviderTypeWechat/Fuiou 常量
- wechat_config_dto.go: Create/Update/List 请求 DTO,响应 DTO 含脱敏逻辑
- wechat_config_store.go: CRUD、GetActive、ActivateInTx(事务内唯一激活)、软删除保护查询
- service.go: 业务逻辑,按渠道校验必填字段、Redis 缓存管理(wechat:config:active)、删除保护、审计日志

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-16 23:29:11 +08:00

474 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}
}