- 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>
474 lines
16 KiB
Go
474 lines
16 KiB
Go
// 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
|
||
}
|
||
}
|