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:
473
internal/service/wechat_config/service.go
Normal file
473
internal/service/wechat_config/service.go
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user