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,186 @@
package dto
import (
"fmt"
"github.com/break/junhong_cmp_fiber/internal/model"
)
// CreateWechatConfigRequest 创建微信参数配置请求
type CreateWechatConfigRequest struct {
Name string `json:"name" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"配置名称"`
Description string `json:"description" validate:"omitempty,max=500" maxLength:"500" description:"配置描述"`
ProviderType string `json:"provider_type" validate:"required,oneof=wechat fuiou" required:"true" description:"支付渠道类型 (wechat:微信直连, fuiou:富友)"`
OaAppID string `json:"oa_app_id" validate:"omitempty,max=100" maxLength:"100" description:"公众号AppID"`
OaAppSecret string `json:"oa_app_secret" validate:"omitempty,max=200" maxLength:"200" description:"公众号AppSecret"`
OaToken string `json:"oa_token" validate:"omitempty,max=200" maxLength:"200" description:"公众号Token"`
OaAesKey string `json:"oa_aes_key" validate:"omitempty,max=200" maxLength:"200" description:"公众号AES加密Key"`
OaOAuthRedirectURL string `json:"oa_oauth_redirect_url" validate:"omitempty,max=500" maxLength:"500" description:"OAuth回调地址"`
MiniappAppID string `json:"miniapp_app_id" validate:"omitempty,max=100" maxLength:"100" description:"小程序AppID"`
MiniappAppSecret string `json:"miniapp_app_secret" validate:"omitempty,max=200" maxLength:"200" description:"小程序AppSecret"`
WxMchID string `json:"wx_mch_id" validate:"omitempty,max=100" maxLength:"100" description:"微信商户号"`
WxAPIV3Key string `json:"wx_api_v3_key" validate:"omitempty,max=200" maxLength:"200" description:"微信APIv3密钥"`
WxAPIV2Key string `json:"wx_api_v2_key" validate:"omitempty,max=200" maxLength:"200" description:"微信APIv2密钥"`
WxCertContent string `json:"wx_cert_content" validate:"omitempty" description:"微信支付证书内容(PEM格式)"`
WxKeyContent string `json:"wx_key_content" validate:"omitempty" description:"微信支付密钥内容(PEM格式)"`
WxSerialNo string `json:"wx_serial_no" validate:"omitempty,max=200" maxLength:"200" description:"微信证书序列号"`
WxNotifyURL string `json:"wx_notify_url" validate:"omitempty,max=500" maxLength:"500" description:"微信支付回调地址"`
FyInsCd string `json:"fy_ins_cd" validate:"omitempty,max=50" maxLength:"50" description:"富友机构号"`
FyMchntCd string `json:"fy_mchnt_cd" validate:"omitempty,max=50" maxLength:"50" description:"富友商户号"`
FyTermID string `json:"fy_term_id" validate:"omitempty,max=50" maxLength:"50" description:"富友终端号"`
FyPrivateKey string `json:"fy_private_key" validate:"omitempty" description:"富友私钥(PEM格式)"`
FyPublicKey string `json:"fy_public_key" validate:"omitempty" description:"富友公钥(PEM格式)"`
FyAPIURL string `json:"fy_api_url" validate:"omitempty,max=500" maxLength:"500" description:"富友API地址"`
FyNotifyURL string `json:"fy_notify_url" validate:"omitempty,max=500" maxLength:"500" description:"富友支付回调地址"`
}
// UpdateWechatConfigRequest 更新微信参数配置请求
type UpdateWechatConfigRequest struct {
Name *string `json:"name" validate:"omitempty,min=1,max=100" minLength:"1" maxLength:"100" description:"配置名称"`
Description *string `json:"description" validate:"omitempty,max=500" maxLength:"500" description:"配置描述"`
ProviderType *string `json:"provider_type" validate:"omitempty,oneof=wechat fuiou" description:"支付渠道类型 (wechat:微信直连, fuiou:富友)"`
OaAppID *string `json:"oa_app_id" validate:"omitempty,max=100" maxLength:"100" description:"公众号AppID"`
OaAppSecret *string `json:"oa_app_secret" validate:"omitempty,max=200" maxLength:"200" description:"公众号AppSecret"`
OaToken *string `json:"oa_token" validate:"omitempty,max=200" maxLength:"200" description:"公众号Token"`
OaAesKey *string `json:"oa_aes_key" validate:"omitempty,max=200" maxLength:"200" description:"公众号AES加密Key"`
OaOAuthRedirectURL *string `json:"oa_oauth_redirect_url" validate:"omitempty,max=500" maxLength:"500" description:"OAuth回调地址"`
MiniappAppID *string `json:"miniapp_app_id" validate:"omitempty,max=100" maxLength:"100" description:"小程序AppID"`
MiniappAppSecret *string `json:"miniapp_app_secret" validate:"omitempty,max=200" maxLength:"200" description:"小程序AppSecret"`
WxMchID *string `json:"wx_mch_id" validate:"omitempty,max=100" maxLength:"100" description:"微信商户号"`
WxAPIV3Key *string `json:"wx_api_v3_key" validate:"omitempty,max=200" maxLength:"200" description:"微信APIv3密钥"`
WxAPIV2Key *string `json:"wx_api_v2_key" validate:"omitempty,max=200" maxLength:"200" description:"微信APIv2密钥"`
WxCertContent *string `json:"wx_cert_content" validate:"omitempty" description:"微信支付证书内容(PEM格式)"`
WxKeyContent *string `json:"wx_key_content" validate:"omitempty" description:"微信支付密钥内容(PEM格式)"`
WxSerialNo *string `json:"wx_serial_no" validate:"omitempty,max=200" maxLength:"200" description:"微信证书序列号"`
WxNotifyURL *string `json:"wx_notify_url" validate:"omitempty,max=500" maxLength:"500" description:"微信支付回调地址"`
FyInsCd *string `json:"fy_ins_cd" validate:"omitempty,max=50" maxLength:"50" description:"富友机构号"`
FyMchntCd *string `json:"fy_mchnt_cd" validate:"omitempty,max=50" maxLength:"50" description:"富友商户号"`
FyTermID *string `json:"fy_term_id" validate:"omitempty,max=50" maxLength:"50" description:"富友终端号"`
FyPrivateKey *string `json:"fy_private_key" validate:"omitempty" description:"富友私钥(PEM格式)"`
FyPublicKey *string `json:"fy_public_key" validate:"omitempty" description:"富友公钥(PEM格式)"`
FyAPIURL *string `json:"fy_api_url" validate:"omitempty,max=500" maxLength:"500" description:"富友API地址"`
FyNotifyURL *string `json:"fy_notify_url" validate:"omitempty,max=500" maxLength:"500" description:"富友支付回调地址"`
}
// WechatConfigListRequest 微信参数配置列表查询请求
type WechatConfigListRequest struct {
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
ProviderType *string `json:"provider_type" query:"provider_type" validate:"omitempty,oneof=wechat fuiou" description:"支付渠道类型 (wechat:微信直连, fuiou:富友)"`
IsActive *bool `json:"is_active" query:"is_active" description:"是否激活 (true:已激活, false:未激活)"`
}
// WechatConfigResponse 微信参数配置响应
type WechatConfigResponse struct {
ID uint `json:"id" description:"配置ID"`
Name string `json:"name" description:"配置名称"`
Description string `json:"description" description:"配置描述"`
ProviderType string `json:"provider_type" description:"支付渠道类型 (wechat:微信直连, fuiou:富友)"`
IsActive bool `json:"is_active" description:"是否激活"`
OaAppID string `json:"oa_app_id" description:"公众号AppID"`
OaAppSecret string `json:"oa_app_secret" description:"公众号AppSecret(已脱敏)"`
OaToken string `json:"oa_token" description:"公众号Token(已脱敏)"`
OaAesKey string `json:"oa_aes_key" description:"公众号AES加密Key(已脱敏)"`
OaOAuthRedirectURL string `json:"oa_oauth_redirect_url" description:"OAuth回调地址"`
MiniappAppID string `json:"miniapp_app_id" description:"小程序AppID"`
MiniappAppSecret string `json:"miniapp_app_secret" description:"小程序AppSecret(已脱敏)"`
WxMchID string `json:"wx_mch_id" description:"微信商户号"`
WxAPIV3Key string `json:"wx_api_v3_key" description:"微信APIv3密钥(已脱敏)"`
WxAPIV2Key string `json:"wx_api_v2_key" description:"微信APIv2密钥(已脱敏)"`
WxCertContent string `json:"wx_cert_content" description:"微信支付证书内容(配置状态)"`
WxKeyContent string `json:"wx_key_content" description:"微信支付密钥内容(配置状态)"`
WxSerialNo string `json:"wx_serial_no" description:"微信证书序列号"`
WxNotifyURL string `json:"wx_notify_url" description:"微信支付回调地址"`
FyInsCd string `json:"fy_ins_cd" description:"富友机构号"`
FyMchntCd string `json:"fy_mchnt_cd" description:"富友商户号"`
FyTermID string `json:"fy_term_id" description:"富友终端号"`
FyPrivateKey string `json:"fy_private_key" description:"富友私钥(配置状态)"`
FyPublicKey string `json:"fy_public_key" description:"富友公钥(配置状态)"`
FyAPIURL string `json:"fy_api_url" description:"富友API地址"`
FyNotifyURL string `json:"fy_notify_url" description:"富友支付回调地址"`
CreatedAt string `json:"created_at" description:"创建时间"`
UpdatedAt string `json:"updated_at" description:"更新时间"`
}
// WechatConfigListResponse 微信参数配置列表响应
type WechatConfigListResponse struct {
List []*WechatConfigResponse `json:"list" description:"配置列表"`
Total int64 `json:"total" description:"总数"`
Page int `json:"page" description:"当前页"`
PageSize int `json:"page_size" description:"每页数量"`
}
// MaskShortSecret 对短密钥进行脱敏处理
// 长度小于 8 返回 "***"否则保留前4位和后4位
func MaskShortSecret(val string) string {
if len(val) < 8 {
return "***"
}
return fmt.Sprintf("%s***%s", val[:4], val[len(val)-4:])
}
// MaskLongSecret 对长密钥/证书进行脱敏处理
// 空值返回 "[未配置]",否则返回 "[已配置]"
func MaskLongSecret(val string) string {
if val == "" {
return "[未配置]"
}
return "[已配置]"
}
// FromWechatConfigModel 从模型转换为响应 DTO敏感字段自动脱敏
func FromWechatConfigModel(m *model.WechatConfig) *WechatConfigResponse {
resp := &WechatConfigResponse{
ID: m.ID,
Name: m.Name,
ProviderType: m.ProviderType,
IsActive: m.IsActive,
OaAppID: m.OaAppID,
OaAppSecret: MaskShortSecret(m.OaAppSecret),
OaToken: MaskShortSecret(m.OaToken),
OaAesKey: MaskShortSecret(m.OaAesKey),
OaOAuthRedirectURL: m.OaOAuthRedirectURL,
MiniappAppID: m.MiniappAppID,
MiniappAppSecret: MaskShortSecret(m.MiniappAppSecret),
WxMchID: m.WxMchID,
WxAPIV3Key: MaskShortSecret(m.WxAPIV3Key),
WxAPIV2Key: MaskShortSecret(m.WxAPIV2Key),
WxCertContent: MaskLongSecret(m.WxCertContent),
WxKeyContent: MaskLongSecret(m.WxKeyContent),
WxSerialNo: m.WxSerialNo,
WxNotifyURL: m.WxNotifyURL,
FyInsCd: m.FyInsCd,
FyMchntCd: m.FyMchntCd,
FyTermID: m.FyTermID,
FyPrivateKey: MaskLongSecret(m.FyPrivateKey),
FyPublicKey: MaskLongSecret(m.FyPublicKey),
FyAPIURL: m.FyAPIURL,
FyNotifyURL: m.FyNotifyURL,
CreatedAt: m.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: m.UpdatedAt.Format("2006-01-02 15:04:05"),
}
if m.Description != nil {
resp.Description = *m.Description
}
return resp
}

View File

@@ -0,0 +1,56 @@
package model
import "gorm.io/gorm"
// 支付渠道类型常量
const (
ProviderTypeWechat = "wechat" // 微信直连
ProviderTypeFuiou = "fuiou" // 富友
)
// WechatConfig 微信参数配置模型
// 管理微信公众号 OAuth、小程序、微信直连支付、富友支付等多套配置
// 同一时间只有一条记录处于 is_active=true全局唯一生效配置
type WechatConfig struct {
gorm.Model
BaseModel `gorm:"embedded"`
Name string `gorm:"column:name;type:varchar(100);not null;comment:配置名称" json:"name"`
Description *string `gorm:"column:description;type:text;comment:配置描述" json:"description,omitempty"`
ProviderType string `gorm:"column:provider_type;type:varchar(20);not null;comment:支付渠道类型(wechat-微信直连,fuiou-富友)" json:"provider_type"`
IsActive bool `gorm:"column:is_active;type:boolean;not null;default:false;comment:是否激活(全局唯一)" json:"is_active"`
// OAuth 公众号
OaAppID string `gorm:"column:oa_app_id;type:varchar(100);not null;default:'';comment:公众号AppID" json:"oa_app_id"`
OaAppSecret string `gorm:"column:oa_app_secret;type:varchar(200);not null;default:'';comment:公众号AppSecret" json:"oa_app_secret"`
OaToken string `gorm:"column:oa_token;type:varchar(200);default:'';comment:公众号Token" json:"oa_token"`
OaAesKey string `gorm:"column:oa_aes_key;type:varchar(200);default:'';comment:公众号AES加密Key" json:"oa_aes_key"`
OaOAuthRedirectURL string `gorm:"column:oa_oauth_redirect_url;type:varchar(500);default:'';comment:OAuth回调地址" json:"oa_oauth_redirect_url"`
// OAuth 小程序
MiniappAppID string `gorm:"column:miniapp_app_id;type:varchar(100);default:'';comment:小程序AppID" json:"miniapp_app_id"`
MiniappAppSecret string `gorm:"column:miniapp_app_secret;type:varchar(200);default:'';comment:小程序AppSecret" json:"miniapp_app_secret"`
// 支付-微信直连
WxMchID string `gorm:"column:wx_mch_id;type:varchar(100);default:'';comment:微信商户号" json:"wx_mch_id"`
WxAPIV3Key string `gorm:"column:wx_api_v3_key;type:varchar(200);default:'';comment:微信APIv3密钥" json:"wx_api_v3_key"`
WxAPIV2Key string `gorm:"column:wx_api_v2_key;type:varchar(200);default:'';comment:微信APIv2密钥" json:"wx_api_v2_key"`
WxCertContent string `gorm:"column:wx_cert_content;type:text;default:'';comment:微信支付证书内容" json:"wx_cert_content"`
WxKeyContent string `gorm:"column:wx_key_content;type:text;default:'';comment:微信支付密钥内容" json:"wx_key_content"`
WxSerialNo string `gorm:"column:wx_serial_no;type:varchar(200);default:'';comment:微信证书序列号" json:"wx_serial_no"`
WxNotifyURL string `gorm:"column:wx_notify_url;type:varchar(500);default:'';comment:微信支付回调地址" json:"wx_notify_url"`
// 支付-富友
FyInsCd string `gorm:"column:fy_ins_cd;type:varchar(50);default:'';comment:富友机构号" json:"fy_ins_cd"`
FyMchntCd string `gorm:"column:fy_mchnt_cd;type:varchar(50);default:'';comment:富友商户号" json:"fy_mchnt_cd"`
FyTermID string `gorm:"column:fy_term_id;type:varchar(50);default:'';comment:富友终端号" json:"fy_term_id"`
FyPrivateKey string `gorm:"column:fy_private_key;type:text;default:'';comment:富友私钥" json:"fy_private_key"`
FyPublicKey string `gorm:"column:fy_public_key;type:text;default:'';comment:富友公钥" json:"fy_public_key"`
FyAPIURL string `gorm:"column:fy_api_url;type:varchar(500);default:'';comment:富友API地址" json:"fy_api_url"`
FyNotifyURL string `gorm:"column:fy_notify_url;type:varchar(500);default:'';comment:富友支付回调地址" json:"fy_notify_url"`
}
// TableName 指定表名
func (WechatConfig) TableName() string {
return "tb_wechat_config"
}

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
}
}

View File

@@ -0,0 +1,145 @@
package postgres
import (
"context"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/store"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
// WechatConfigStore 微信参数配置数据访问层
type WechatConfigStore struct {
db *gorm.DB
rdb *redis.Client
}
// NewWechatConfigStore 创建微信参数配置 Store 实例
func NewWechatConfigStore(db *gorm.DB, rdb *redis.Client) *WechatConfigStore {
return &WechatConfigStore{db: db, rdb: rdb}
}
// Create 创建微信参数配置
func (s *WechatConfigStore) Create(ctx context.Context, config *model.WechatConfig) error {
return s.db.WithContext(ctx).Create(config).Error
}
// GetByID 根据 ID 获取配置(遵循软删除)
func (s *WechatConfigStore) GetByID(ctx context.Context, id uint) (*model.WechatConfig, error) {
var config model.WechatConfig
if err := s.db.WithContext(ctx).First(&config, id).Error; err != nil {
return nil, err
}
return &config, nil
}
// GetByIDUnscoped 根据 ID 获取配置(包含已软删除的记录,用于回调处理)
func (s *WechatConfigStore) GetByIDUnscoped(ctx context.Context, id uint) (*model.WechatConfig, error) {
var config model.WechatConfig
if err := s.db.WithContext(ctx).Unscoped().First(&config, id).Error; err != nil {
return nil, err
}
return &config, nil
}
// List 查询配置列表,支持按 provider_type 和 is_active 过滤
func (s *WechatConfigStore) List(ctx context.Context, opts *store.QueryOptions, filters map[string]interface{}) ([]*model.WechatConfig, int64, error) {
var configs []*model.WechatConfig
var total int64
query := s.db.WithContext(ctx).Model(&model.WechatConfig{})
if providerType, ok := filters["provider_type"].(string); ok && providerType != "" {
query = query.Where("provider_type = ?", providerType)
}
if isActive, ok := filters["is_active"].(*bool); ok && isActive != nil {
query = query.Where("is_active = ?", *isActive)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
if opts == nil {
opts = store.DefaultQueryOptions()
}
offset := (opts.Page - 1) * opts.PageSize
query = query.Offset(offset).Limit(opts.PageSize)
if opts.OrderBy != "" {
query = query.Order(opts.OrderBy)
}
if err := query.Find(&configs).Error; err != nil {
return nil, 0, err
}
return configs, total, nil
}
// Update 更新微信参数配置
func (s *WechatConfigStore) Update(ctx context.Context, config *model.WechatConfig) error {
return s.db.WithContext(ctx).Save(config).Error
}
// SoftDelete 软删除微信参数配置
func (s *WechatConfigStore) SoftDelete(ctx context.Context, id uint) error {
return s.db.WithContext(ctx).Delete(&model.WechatConfig{}, id).Error
}
// GetActive 获取当前激活的配置
// 返回 gorm.ErrRecordNotFound 如果没有激活的配置
func (s *WechatConfigStore) GetActive(ctx context.Context) (*model.WechatConfig, error) {
var config model.WechatConfig
if err := s.db.WithContext(ctx).Where("is_active = ? AND deleted_at IS NULL", true).First(&config).Error; err != nil {
return nil, err
}
return &config, nil
}
// ActivateInTx 在事务内激活指定配置(先停用所有,再激活指定记录)
func (s *WechatConfigStore) ActivateInTx(ctx context.Context, tx *gorm.DB, id uint) error {
// 先停用所有激活的配置
if err := tx.WithContext(ctx).Model(&model.WechatConfig{}).
Where("is_active = ?", true).
Update("is_active", false).Error; err != nil {
return err
}
// 再激活指定配置
return tx.WithContext(ctx).Model(&model.WechatConfig{}).
Where("id = ?", id).
Update("is_active", true).Error
}
// DB 返回底层数据库连接,用于事务操作
func (s *WechatConfigStore) DB() *gorm.DB {
return s.db
}
// Deactivate 停用指定配置
func (s *WechatConfigStore) Deactivate(ctx context.Context, id uint) error {
return s.db.WithContext(ctx).Model(&model.WechatConfig{}).
Where("id = ?", id).
Update("is_active", false).Error
}
// CountPendingOrdersByConfigID 统计指定配置的待支付订单数
func (s *WechatConfigStore) CountPendingOrdersByConfigID(ctx context.Context, configID uint) (int64, error) {
var count int64
err := s.db.WithContext(ctx).
Table("tb_order").
Where("payment_config_id = ? AND payment_status = ? AND deleted_at IS NULL", configID, 1).
Count(&count).Error
return count, err
}
// CountPendingRechargesByConfigID 统计指定配置的待支付充值记录数
func (s *WechatConfigStore) CountPendingRechargesByConfigID(ctx context.Context, configID uint) (int64, error) {
var count int64
err := s.db.WithContext(ctx).
Table("tb_asset_recharge_record").
Where("payment_config_id = ? AND status = ? AND deleted_at IS NULL", configID, 1).
Count(&count).Error
return count, err
}