Files
junhong_cmp_fiber/internal/service/wechat_config/service.go
break 98c145fe70
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 3m55s
实现支付商户池与微信授权配置
新增收款商户、商户池轮询、微信授权配置独立管理;三类新支付
(C端套餐购买、C端资产钱包充值、代理在线预存款充值)无条件
经商户池选择并冻结路由,无旧综合配置回退。merchant_id 为空
历史支付继续按 payment_config_id 双读。凭证版本化加载与
ID+版本缓存保证轮换一致性。删除商户池新支付创建开关及全部
引用。
2026-09-09 18:13:04 +08:00

730 lines
28 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"
"strconv"
"time"
"github.com/bytedance/sonic"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
"gorm.io/gorm"
merchantpayment "github.com/break/junhong_cmp_fiber/internal/application/merchantpayment"
systemconfigapp "github.com/break/junhong_cmp_fiber/internal/application/systemconfig"
"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/auditfailure"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/break/junhong_cmp_fiber/pkg/wechat"
)
// Redis 缓存键
const redisActiveConfigKey = "wechat:config:active"
// Service 微信参数配置业务服务
type Service struct {
store *postgres.WechatConfigStore
orderStore *postgres.OrderStore
rechargeOrderStore *postgres.RechargeOrderStore
agentRechargeStore *postgres.AgentRechargeStore
paymentStore *postgres.PaymentStore
audit systemconfigapp.AuditWriter
redis *redis.Client
logger *zap.Logger
}
// New 创建微信参数配置服务实例
func New(
store *postgres.WechatConfigStore,
orderStore *postgres.OrderStore,
rechargeOrderStore *postgres.RechargeOrderStore,
agentRechargeStore *postgres.AgentRechargeStore,
paymentStore *postgres.PaymentStore,
audit systemconfigapp.AuditWriter,
rdb *redis.Client,
logger *zap.Logger,
) *Service {
return &Service{
store: store,
orderStore: orderStore,
rechargeOrderStore: rechargeOrderStore,
agentRechargeStore: agentRechargeStore,
paymentStore: paymentStore,
audit: audit,
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 {
s.recordAuditFailure(ctx, systemconfigapp.ChangeAudit{
OperatorID: middleware.GetUserIDFromContext(ctx), OperationType: constants.AuditOperationPaymentConfigCreate,
Description: "拒绝创建非法支付连接配置", ConfigKey: "payment_config.new:" + req.Name,
DisplayName: req.Name, Identity: map[string]any{"name": req.Name, "provider_type": req.ProviderType, "credentials_configured": paymentRequestCredentialsConfigured(req)},
Result: constants.AuditResultDenied, ErrorCode: strconv.Itoa(errors.CodeInvalidParam), ErrorSummary: "支付连接配置字段校验失败",
})
return nil, err
}
if s.audit == nil {
return nil, errors.New(errors.CodeInvalidStatus, "支付配置审计接缝未配置")
}
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,
AliAppID: req.AliAppID,
AliPrivateKey: req.AliPrivateKey,
AliPublicKey: req.AliPublicKey,
AliNotifyURL: req.AliNotifyURL,
AliReturnURL: req.AliReturnURL,
AliProduction: req.AliProduction,
AliPayExpireMinutes: req.AliPayExpireMinutes,
}
// 支付宝过期分钟数默认值
if config.AliPayExpireMinutes == 0 {
config.AliPayExpireMinutes = model.DefaultAliPayExpireMinutes
}
config.Creator = middleware.GetUserIDFromContext(ctx)
err := s.store.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := s.store.WithTx(tx).Create(ctx, config); err != nil {
return err
}
return s.writeAudit(ctx, tx, constants.AuditOperationPaymentConfigCreate, "创建支付连接配置", nil, config)
})
if err != nil {
s.recordPaymentFailure(ctx, constants.AuditOperationPaymentConfigCreate, "创建支付连接配置失败", config, err)
return nil, errors.Wrap(errors.CodeInternalError, err, "创建微信支付配置失败")
}
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, "获取微信支付配置失败")
}
if s.audit == nil {
return nil, errors.New(errors.CodeInvalidStatus, "支付配置审计接缝未配置")
}
before := *config
// 合并字段:指针非 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)
// 支付宝支付
s.mergeStringField(&config.AliAppID, req.AliAppID)
s.mergeSensitiveField(&config.AliPrivateKey, req.AliPrivateKey)
s.mergeSensitiveField(&config.AliPublicKey, req.AliPublicKey)
s.mergeStringField(&config.AliNotifyURL, req.AliNotifyURL)
s.mergeStringField(&config.AliReturnURL, req.AliReturnURL)
if req.AliProduction != nil {
config.AliProduction = *req.AliProduction
}
if req.AliPayExpireMinutes != nil && *req.AliPayExpireMinutes > 0 {
config.AliPayExpireMinutes = *req.AliPayExpireMinutes
}
config.Updater = middleware.GetUserIDFromContext(ctx)
err = s.store.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := s.store.WithTx(tx).Update(ctx, config); err != nil {
return err
}
return s.writeAudit(ctx, tx, constants.AuditOperationPaymentConfigUpdate, "更新支付连接配置", &before, config)
})
if err != nil {
s.recordPaymentFailure(ctx, constants.AuditOperationPaymentConfigUpdate, "更新支付连接配置失败", config, err)
return nil, errors.Wrap(errors.CodeInternalError, err, "更新微信支付配置失败")
}
// 如果当前配置处于激活状态,清除缓存
if config.IsActive {
s.clearActiveConfigCache(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 s.audit == nil {
return errors.New(errors.CodeInvalidStatus, "支付配置审计接缝未配置")
}
// 不允许删除正在激活的配置
if config.IsActive {
s.recordPaymentDenied(ctx, constants.AuditOperationPaymentConfigDelete, "拒绝删除生效中的支付连接配置", config, errors.CodeWechatConfigActive)
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 {
s.recordPaymentDenied(ctx, constants.AuditOperationPaymentConfigDelete, "拒绝删除存在在途业务的支付连接配置", config, errors.CodeWechatConfigHasPendingOrders)
return errors.New(errors.CodeWechatConfigHasPendingOrders)
}
err = s.store.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := s.store.WithTx(tx).SoftDelete(ctx, id); err != nil {
return err
}
return s.writeAudit(ctx, tx, constants.AuditOperationPaymentConfigDelete, "删除支付连接配置", config, nil)
})
if err != nil {
s.recordPaymentFailure(ctx, constants.AuditOperationPaymentConfigDelete, "删除支付连接配置失败", config, err)
return errors.Wrap(errors.CodeInternalError, err, "删除微信支付配置失败")
}
s.clearActiveConfigCache(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, "获取微信支付配置失败")
}
if s.audit == nil {
return nil, errors.New(errors.CodeInvalidStatus, "支付配置审计接缝未配置")
}
before := *config
// 保留原激活配置快照,确保自动停用也进入该配置自身时间线。
oldActive, oldErr := s.store.GetActive(ctx)
// 事务内激活
db := s.store.DB()
if err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := s.store.ActivateInTx(ctx, tx, id); err != nil {
return err
}
after := before
after.IsActive = true
if oldErr == nil && oldActive != nil && oldActive.ID != id {
oldAfter := *oldActive
oldAfter.IsActive = false
if err := s.writeAudit(ctx, tx, constants.AuditOperationPaymentConfigDeactivate, "激活其他配置时停用原支付连接配置", oldActive, &oldAfter); err != nil {
return err
}
}
return s.writeAudit(ctx, tx, constants.AuditOperationPaymentConfigActivate, "激活支付连接配置", &before, &after)
}); err != nil {
s.recordPaymentFailure(ctx, constants.AuditOperationPaymentConfigActivate, "激活支付连接配置失败", config, err)
return nil, errors.Wrap(errors.CodeInternalError, err, "激活微信支付配置失败")
}
s.clearActiveConfigCache(ctx)
// 重新查询最新状态
config, _ = s.store.GetByID(ctx, id)
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 s.audit == nil {
return nil, errors.New(errors.CodeInvalidStatus, "支付配置审计接缝未配置")
}
before := *config
after := before
after.IsActive = false
err = s.store.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := s.store.WithTx(tx).Deactivate(ctx, id); err != nil {
return err
}
return s.writeAudit(ctx, tx, constants.AuditOperationPaymentConfigDeactivate, "停用支付连接配置", &before, &after)
})
if err != nil {
s.recordPaymentFailure(ctx, constants.AuditOperationPaymentConfigDeactivate, "停用支付连接配置失败", config, err)
return nil, errors.Wrap(errors.CodeInternalError, err, "停用微信支付配置失败")
}
s.clearActiveConfigCache(ctx)
// 重新查询最新状态
config, _ = s.store.GetByID(ctx, id)
return dto.FromWechatConfigModel(config), nil
}
func (s *Service) writeAudit(ctx context.Context, tx *gorm.DB, operation, description string, before, after *model.WechatConfig) error {
config := after
if config == nil {
config = before
}
resourceID := strconv.FormatUint(uint64(config.ID), 10)
requestID := ""
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
requestID = *value
}
return s.audit.WriteConfigChange(ctx, tx, systemconfigapp.ChangeAudit{
OperatorID: middleware.GetUserIDFromContext(ctx), OperationType: operation, Description: description,
ConfigKey: "payment_config." + resourceID, Module: "payment", ResourceID: &resourceID,
DisplayName: config.Name, Identity: paymentConfigIdentity(config),
BeforeData: paymentConfigAuditSnapshot(before), AfterData: paymentConfigAuditSnapshot(after),
RequestID: requestID, CorrelationID: requestID,
})
}
func (s *Service) recordPaymentDenied(ctx context.Context, operation, description string, config *model.WechatConfig, code int) {
s.recordAuditFailure(ctx, paymentFailureAudit(ctx, operation, description, config, constants.AuditResultDenied, code))
}
func (s *Service) recordPaymentFailure(ctx context.Context, operation, description string, config *model.WechatConfig, _ error) {
s.recordAuditFailure(ctx, paymentFailureAudit(ctx, operation, description, config, constants.AuditResultFailed, errors.CodeDatabaseError))
}
func (s *Service) recordAuditFailure(ctx context.Context, audit systemconfigapp.ChangeAudit) {
if s.audit == nil || s.store == nil || s.store.DB() == nil || audit.OperatorID == 0 || audit.ConfigKey == "" {
return
}
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
audit.RequestID = *value
audit.CorrelationID = *value
}
if err := s.store.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
return s.audit.WriteConfigChange(ctx, tx, audit)
}); err != nil {
auditfailure.RecordSecondaryWriteFailure(audit.OperationType, audit.ConfigKey, audit.RequestID, audit.CorrelationID, audit.ErrorCode, err)
}
}
func paymentFailureAudit(ctx context.Context, operation, description string, config *model.WechatConfig, result string, code int) systemconfigapp.ChangeAudit {
resourceID := strconv.FormatUint(uint64(config.ID), 10)
return systemconfigapp.ChangeAudit{
OperatorID: middleware.GetUserIDFromContext(ctx), OperationType: operation, Description: description,
ConfigKey: "payment_config." + resourceID, Module: "payment", ResourceID: &resourceID,
DisplayName: config.Name, Identity: paymentConfigIdentity(config), BeforeData: paymentConfigAuditSnapshot(config),
Result: result, ErrorCode: strconv.Itoa(code), ErrorSummary: description,
}
}
func paymentConfigIdentity(config *model.WechatConfig) map[string]any {
if config == nil {
return nil
}
return map[string]any{
"id": config.ID, "name": config.Name, "provider_type": config.ProviderType,
"is_active": config.IsActive, "credentials_configured": paymentConfigCredentialsConfigured(config),
}
}
func paymentConfigAuditSnapshot(config *model.WechatConfig) map[string]any {
if config == nil {
return nil
}
return map[string]any{
"id": config.ID, "name": config.Name, "description": config.Description,
"provider_type": config.ProviderType, "is_active": config.IsActive,
"oa_app_id": config.OaAppID, "oa_oauth_redirect_url": config.OaOAuthRedirectURL,
"miniapp_app_id": config.MiniappAppID, "wx_mch_id": config.WxMchID,
"wx_serial_no": config.WxSerialNo, "wx_notify_url": config.WxNotifyURL,
"fy_ins_cd": config.FyInsCd, "fy_mchnt_cd": config.FyMchntCd, "fy_term_id": config.FyTermID,
"fy_api_url": config.FyAPIURL, "fy_notify_url": config.FyNotifyURL,
"ali_app_id": config.AliAppID, "ali_notify_url": config.AliNotifyURL,
"ali_return_url": config.AliReturnURL, "ali_production": config.AliProduction,
"ali_pay_expire_minutes": config.AliPayExpireMinutes,
"credentials_configured": paymentConfigCredentialsConfigured(config),
"oauth_configured": config.OaAppSecret != "" || config.OaToken != "" || config.OaAesKey != "" || config.MiniappAppSecret != "",
"wechat_payment_configured": config.WxAPIV3Key != "" || config.WxAPIV2Key != "" || config.WxCertContent != "" || config.WxKeyContent != "",
"fuiou_configured": config.FyPrivateKey != "" || config.FyPublicKey != "",
"alipay_configured": config.AliPrivateKey != "" || config.AliPublicKey != "",
}
}
func paymentConfigCredentialsConfigured(config *model.WechatConfig) bool {
if config == nil {
return false
}
return config.OaAppSecret != "" || config.OaToken != "" || config.OaAesKey != "" || config.MiniappAppSecret != "" ||
config.WxAPIV3Key != "" || config.WxAPIV2Key != "" || config.WxCertContent != "" || config.WxKeyContent != "" ||
config.FyPrivateKey != "" || config.FyPublicKey != "" || config.AliPrivateKey != "" || config.AliPublicKey != ""
}
func paymentRequestCredentialsConfigured(request *dto.CreateWechatConfigRequest) bool {
return request != nil && (request.OaAppSecret != "" || request.OaToken != "" || request.OaAesKey != "" || request.MiniappAppSecret != "" ||
request.WxAPIV3Key != "" || request.WxAPIV2Key != "" || request.WxCertContent != "" || request.WxKeyContent != "" ||
request.FyPrivateKey != "" || request.FyPublicKey != "" || request.AliPrivateKey != "" || request.AliPublicKey != "")
}
// 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
}
// GetAuthorizationConfig 读取唯一启用且按凭证版本缓存的 C 端微信授权配置。
// 仅用于兼容现有微信 SDK 的配置结构,绝不回退到 tb_wechat_config。
func (s *Service) GetAuthorizationConfig(ctx context.Context) (*model.WechatConfig, error) {
if s == nil || s.store == nil {
return nil, errors.New(errors.CodeServiceUnavailable, "微信授权加载能力未配置")
}
authorization, err := merchantpayment.NewRuntimeLoader(s.store.DB(), s.redis).LoadAuthorization(ctx)
if err != nil {
return nil, err
}
return &model.WechatConfig{
IsActive: true,
OaAppID: authorization.OaAppID,
OaAppSecret: authorization.OaAppSecret,
OaToken: authorization.OaToken,
OaAesKey: authorization.OaAesKey,
OaOAuthRedirectURL: authorization.OaOAuthRedirectURL,
MiniappAppID: authorization.MiniappAppID,
MiniappAppSecret: authorization.MiniappAppSecret,
}, 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
}
}
// GetByIDUnscoped loads a historical comprehensive payment configuration, including soft-deleted rows.
func (s *Service) GetByIDUnscoped(ctx context.Context, id uint) (*model.WechatConfig, error) {
if s == nil || s.store == nil {
return nil, errors.New(errors.CodeServiceUnavailable, "旧支付配置加载能力未配置")
}
return s.store.GetByIDUnscoped(ctx, id)
}
// GetConfigForCallback 按支付单冻结的商户或旧支付配置加载回调凭证。
// merchant_id 为空仅表示数据留存期内的历史支付:本 Change 只允许按其
// payment_config_id 读取,独立 Change 完成留存清理后才可删除这条旧路径。
// 禁止按当前启用池、当前综合配置推断或回填历史支付的实际商户。
func (s *Service) GetConfigForCallback(ctx context.Context, orderNo string) (*model.WechatConfig, error) {
if s.paymentStore != nil {
payment, err := s.paymentStore.GetByPaymentNo(ctx, orderNo)
if err == nil && payment.MerchantID != nil {
loader := merchantpayment.NewRuntimeLoader(s.store.DB(), s.redis)
merchant, loadErr := loader.LoadMerchant(ctx, *payment.MerchantID)
if loadErr != nil {
return nil, loadErr
}
// 仅微信直连v3/v2商户需要全局授权配置中的 AppID富友商户不依赖该配置。
// 授权字段只注入内存中的渠道配置,绝不写入支付快照、日志或审计。
if merchant.ProviderType == model.ProviderTypeWechat || merchant.ProviderType == model.ProviderTypeWechatV2 {
authorization, authErr := loader.LoadAuthorization(ctx)
if authErr != nil {
return nil, authErr
}
return merchantpayment.MerchantConfig(merchant, authorization)
}
return merchantpayment.MerchantConfig(merchant, nil)
}
if err != nil && err != gorm.ErrRecordNotFound {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询支付单路由失败")
}
if err == nil && payment.PaymentConfigID == nil {
return nil, errors.New(errors.CodeNoPaymentConfig, "历史支付单缺少支付配置")
}
}
configID, err := s.resolvePaymentConfigID(ctx, orderNo)
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询历史支付配置失败")
}
if configID == nil {
return nil, errors.New(errors.CodeNoPaymentConfig, "历史支付单缺少支付配置")
}
cfg, loadErr := s.store.GetByIDUnscoped(ctx, *configID)
if loadErr != nil {
return nil, errors.Wrap(errors.CodeNoPaymentConfig, loadErr, "历史支付配置不可用")
}
return cfg, nil
}
// GetPaymentServiceForCallback 使用已选定的支付配置构造回调验签实例。
// 回调验签只能使用支付单冻结商户的当前凭证,不能复用进程启动时的旧支付实例。
func (s *Service) GetPaymentServiceForCallback(config *model.WechatConfig) (wechat.PaymentServiceInterface, error) {
if s == nil || s.redis == nil {
return nil, errors.New(errors.CodeServiceUnavailable, "微信支付回调加载能力未配置")
}
if config == nil || config.ProviderType != model.ProviderTypeWechat {
return nil, errors.New(errors.CodeWechatConfigUnavailable, "微信支付回调配置不可用")
}
app, err := wechat.NewPaymentAppFromConfig(config, config.OaAppID, wechat.NewRedisCache(s.redis), s.logger)
if err != nil {
return nil, errors.Wrap(errors.CodeWechatPayFailed, err, "构建微信支付回调验签实例失败")
}
return wechat.NewPaymentService(app, s.logger), nil
}
// resolvePaymentConfigID 按订单号前缀从对应表中取 payment_config_id
func (s *Service) resolvePaymentConfigID(ctx context.Context, orderNo string) (*uint, error) {
if s.paymentStore != nil {
payment, err := s.paymentStore.GetByPaymentNo(ctx, orderNo)
if err == nil {
if payment.PaymentConfigID != nil {
return payment.PaymentConfigID, nil
}
} else if err != gorm.ErrRecordNotFound {
return nil, err
}
}
switch {
case len(orderNo) >= 3 && orderNo[:3] == "ORD":
order, err := s.orderStore.GetByOrderNo(ctx, orderNo)
if err != nil {
return nil, err
}
return order.PaymentConfigID, nil
case len(orderNo) >= 4 && orderNo[:4] == constants.AssetRechargeOrderPrefix:
record, err := s.rechargeOrderStore.GetByRechargeOrderNo(ctx, orderNo)
if err != nil {
return nil, err
}
return record.PaymentConfigID, nil
case len(orderNo) >= 4 && orderNo[:4] == constants.AgentRechargeOrderPrefix:
record, err := s.agentRechargeStore.GetByRechargeNo(ctx, orderNo)
if err != nil {
return nil, err
}
return record.PaymentConfigID, nil
default:
return nil, fmt.Errorf("未知订单号前缀: %s", orderNo)
}
}