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>
This commit is contained in:
2026-03-16 23:29:11 +08:00
parent aa41a5ed5e
commit c86afbfa8f
4 changed files with 860 additions and 0 deletions

View File

@@ -0,0 +1,473 @@
// 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
}
}