新增收款商户、商户池轮询、微信授权配置独立管理;三类新支付 (C端套餐购买、C端资产钱包充值、代理在线预存款充值)无条件 经商户池选择并冻结路由,无旧综合配置回退。merchant_id 为空 历史支付继续按 payment_config_id 双读。凭证版本化加载与 ID+版本缓存保证轮换一致性。删除商户池新支付创建开关及全部 引用。
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/application/merchantpayment"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
|
||||
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
||||
@@ -16,7 +17,7 @@ import (
|
||||
asset "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
||||
customerBinding "github.com/break/junhong_cmp_fiber/internal/service/customer_binding"
|
||||
rechargeSvc "github.com/break/junhong_cmp_fiber/internal/service/recharge"
|
||||
wechatConfigSvc "github.com/break/junhong_cmp_fiber/internal/service/wechat_config"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/alipay"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
@@ -41,7 +42,7 @@ type ClientWalletHandler struct {
|
||||
paymentStore *postgres.PaymentStore
|
||||
rechargeService *rechargeSvc.Service
|
||||
openIDStore *postgres.PersonalCustomerOpenIDStore
|
||||
wechatConfigService *wechatConfigSvc.Service
|
||||
merchantRuntime *merchantpayment.RuntimeLoader
|
||||
redis *redis.Client
|
||||
logger *zap.Logger
|
||||
db *gorm.DB
|
||||
@@ -73,7 +74,6 @@ func NewClientWalletHandler(
|
||||
paymentStore *postgres.PaymentStore,
|
||||
rechargeService *rechargeSvc.Service,
|
||||
openIDStore *postgres.PersonalCustomerOpenIDStore,
|
||||
wechatConfigService *wechatConfigSvc.Service,
|
||||
redisClient *redis.Client,
|
||||
logger *zap.Logger,
|
||||
db *gorm.DB,
|
||||
@@ -81,20 +81,20 @@ func NewClientWalletHandler(
|
||||
deviceStore *postgres.DeviceStore,
|
||||
) *ClientWalletHandler {
|
||||
return &ClientWalletHandler{
|
||||
assetService: assetService,
|
||||
customerBinding: binding,
|
||||
walletStore: walletStore,
|
||||
transactionStore: transactionStore,
|
||||
rechargeOrderStore: rechargeOrderStore,
|
||||
paymentStore: paymentStore,
|
||||
rechargeService: rechargeService,
|
||||
openIDStore: openIDStore,
|
||||
wechatConfigService: wechatConfigService,
|
||||
redis: redisClient,
|
||||
logger: logger,
|
||||
db: db,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
assetService: assetService,
|
||||
customerBinding: binding,
|
||||
walletStore: walletStore,
|
||||
transactionStore: transactionStore,
|
||||
rechargeOrderStore: rechargeOrderStore,
|
||||
paymentStore: paymentStore,
|
||||
rechargeService: rechargeService,
|
||||
openIDStore: openIDStore,
|
||||
merchantRuntime: merchantpayment.NewRuntimeLoader(db, redisClient),
|
||||
redis: redisClient,
|
||||
logger: logger,
|
||||
db: db,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,19 +321,11 @@ func (h *ClientWalletHandler) CreateRecharge(c *fiber.Ctx) error {
|
||||
// }
|
||||
// }
|
||||
|
||||
config, err := h.wechatConfigService.GetActiveConfig(resolved.SkipPermissionCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if config == nil {
|
||||
return errors.New(errors.CodeWechatConfigUnavailable)
|
||||
}
|
||||
|
||||
switch req.PaymentMethod {
|
||||
case constants.RechargeMethodAlipay:
|
||||
return h.createAlipayRecharge(c, resolved, config, wallet, req)
|
||||
return h.createAlipayRecharge(c, resolved, wallet, req)
|
||||
case constants.RechargeMethodWechat:
|
||||
return h.createWechatRecharge(c, resolved, config, wallet, req)
|
||||
return h.createWechatRecharge(c, resolved, wallet, req)
|
||||
default:
|
||||
return errors.New(errors.CodePaymentMethodUnavailable)
|
||||
}
|
||||
@@ -343,15 +335,13 @@ func (h *ClientWalletHandler) CreateRecharge(c *fiber.Ctx) error {
|
||||
func (h *ClientWalletHandler) createWechatRecharge(
|
||||
c *fiber.Ctx,
|
||||
resolved *resolvedWalletAssetContext,
|
||||
config *model.WechatConfig,
|
||||
wallet *model.AssetWallet,
|
||||
req dto.ClientCreateRechargeRequest,
|
||||
) error {
|
||||
appID, err := pickAppIDByType(config, req.AppType)
|
||||
authorization, appID, err := h.loadWechatAuthorization(resolved.SkipPermissionCtx, req.AppType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
openID, err := h.findOpenIDByCustomerAndAppID(resolved.SkipPermissionCtx, resolved.CustomerID, appID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -359,9 +349,47 @@ func (h *ClientWalletHandler) createWechatRecharge(
|
||||
|
||||
rechargeNo := generateClientRechargeNo()
|
||||
paymentNo := generateClientPaymentNo()
|
||||
rechargeOrder := &model.RechargeOrder{
|
||||
RechargeOrderNo: rechargeNo,
|
||||
UserID: resolved.CustomerID,
|
||||
AssetWalletID: wallet.ID,
|
||||
ResourceType: resolved.ResourceType,
|
||||
ResourceID: resolved.Asset.AssetID,
|
||||
Amount: req.Amount,
|
||||
Status: model.RechargeOrderStatusPending,
|
||||
ShopIDTag: wallet.ShopIDTag,
|
||||
EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||||
OperatorType: constants.OperatorTypePersonalCustomer,
|
||||
Generation: resolved.Generation,
|
||||
}
|
||||
payment := &model.Payment{
|
||||
PaymentNo: paymentNo,
|
||||
OrderID: rechargeOrder.ID,
|
||||
OrderType: model.PaymentOrderTypeRecharge,
|
||||
PaymentMethod: model.PaymentByWechat,
|
||||
Amount: req.Amount,
|
||||
Status: model.PaymentRecordStatusPending,
|
||||
}
|
||||
var config *model.WechatConfig
|
||||
if err := h.db.WithContext(resolved.SkipPermissionCtx).Transaction(func(tx *gorm.DB) error {
|
||||
route, selectedConfig, err := h.selectMerchantConfig(resolved.SkipPermissionCtx, tx, model.PaymentByWechat, authorization)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config = selectedConfig
|
||||
merchantpayment.FreezeRoute(payment, route)
|
||||
if err := h.rechargeOrderStore.CreateWithTx(resolved.SkipPermissionCtx, tx, rechargeOrder); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败")
|
||||
}
|
||||
payment.OrderID = rechargeOrder.ID
|
||||
if err := h.paymentStore.CreateWithTx(resolved.SkipPermissionCtx, tx, payment); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建支付记录失败")
|
||||
}
|
||||
return h.appendRechargePaymentCreatedAudit(resolved.SkipPermissionCtx, tx, payment, rechargeOrder)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 先初始化生效支付通道并创建预支付订单,确认支付通道可用
|
||||
// 避免先写入充值记录后支付初始化失败,导致产生孤儿记录
|
||||
attempt, startedAt, err := h.startRechargePaymentAttempt(resolved.SkipPermissionCtx, config, paymentNo, rechargeNo, req.Amount)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -380,49 +408,15 @@ func (h *ClientWalletHandler) createWechatRecharge(
|
||||
if completeErr := h.completeRechargePaymentAttempt(resolved.SkipPermissionCtx, attempt, startedAt, constants.IntegrationResultUnknown, "request_unknown", "充值支付预下单结果未知"); completeErr != nil {
|
||||
return completeErr
|
||||
}
|
||||
if updateErr := h.markRechargePaymentFailed(resolved.SkipPermissionCtx, payment); updateErr != nil {
|
||||
return updateErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := h.completeRechargePaymentAttempt(resolved.SkipPermissionCtx, attempt, startedAt, constants.IntegrationResultSuccess, "SUCCESS", ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 支付通道确认可用后,再创建充值订单和支付记录
|
||||
rechargeOrder := &model.RechargeOrder{
|
||||
RechargeOrderNo: rechargeNo,
|
||||
UserID: resolved.CustomerID,
|
||||
AssetWalletID: wallet.ID,
|
||||
ResourceType: resolved.ResourceType,
|
||||
ResourceID: resolved.Asset.AssetID,
|
||||
Amount: req.Amount,
|
||||
Status: model.RechargeOrderStatusPending,
|
||||
PaymentConfigID: &config.ID,
|
||||
ShopIDTag: wallet.ShopIDTag,
|
||||
EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||||
OperatorType: constants.OperatorTypePersonalCustomer,
|
||||
Generation: resolved.Generation,
|
||||
}
|
||||
payment := &model.Payment{
|
||||
PaymentNo: paymentNo,
|
||||
OrderID: rechargeOrder.ID,
|
||||
OrderType: model.PaymentOrderTypeRecharge,
|
||||
PaymentMethod: model.PaymentByWechat,
|
||||
Amount: req.Amount,
|
||||
Status: model.PaymentRecordStatusPending,
|
||||
PaymentConfigID: &config.ID,
|
||||
}
|
||||
if err := h.db.WithContext(resolved.SkipPermissionCtx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := h.rechargeOrderStore.CreateWithTx(resolved.SkipPermissionCtx, tx, rechargeOrder); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败")
|
||||
}
|
||||
payment.OrderID = rechargeOrder.ID
|
||||
if err := h.paymentStore.CreateWithTx(resolved.SkipPermissionCtx, tx, payment); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建支付记录失败")
|
||||
}
|
||||
return h.appendRechargePaymentCreatedAudit(resolved.SkipPermissionCtx, tx, payment, rechargeOrder)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, &dto.ClientRechargeResponse{
|
||||
Recharge: dto.ClientRechargeResult{
|
||||
RechargeID: rechargeOrder.ID,
|
||||
@@ -439,18 +433,11 @@ func (h *ClientWalletHandler) createWechatRecharge(
|
||||
func (h *ClientWalletHandler) createAlipayRecharge(
|
||||
c *fiber.Ctx,
|
||||
resolved *resolvedWalletAssetContext,
|
||||
config *model.WechatConfig,
|
||||
wallet *model.AssetWallet,
|
||||
req dto.ClientCreateRechargeRequest,
|
||||
) error {
|
||||
rechargeNo := generateClientRechargeNo()
|
||||
paymentNo := generateClientPaymentNo()
|
||||
expireMinutes := config.AliPayExpireMinutes
|
||||
if expireMinutes <= 0 {
|
||||
expireMinutes = 30
|
||||
}
|
||||
expireAt := time.Now().Add(time.Duration(expireMinutes) * time.Minute)
|
||||
|
||||
rechargeOrder := &model.RechargeOrder{
|
||||
RechargeOrderNo: rechargeNo,
|
||||
UserID: resolved.CustomerID,
|
||||
@@ -459,24 +446,33 @@ func (h *ClientWalletHandler) createAlipayRecharge(
|
||||
ResourceID: resolved.Asset.AssetID,
|
||||
Amount: req.Amount,
|
||||
Status: model.RechargeOrderStatusPending,
|
||||
PaymentConfigID: &config.ID,
|
||||
ShopIDTag: wallet.ShopIDTag,
|
||||
EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||||
OperatorType: constants.OperatorTypePersonalCustomer,
|
||||
Generation: resolved.Generation,
|
||||
}
|
||||
payment := &model.Payment{
|
||||
PaymentNo: paymentNo,
|
||||
OrderType: model.PaymentOrderTypeRecharge,
|
||||
PaymentMethod: model.PaymentByAlipay,
|
||||
Amount: req.Amount,
|
||||
Status: model.PaymentRecordStatusPending,
|
||||
PaymentConfigID: &config.ID,
|
||||
ExpireAt: &expireAt,
|
||||
PaymentNo: paymentNo,
|
||||
OrderType: model.PaymentOrderTypeRecharge,
|
||||
PaymentMethod: model.PaymentByAlipay,
|
||||
Amount: req.Amount,
|
||||
Status: model.PaymentRecordStatusPending,
|
||||
}
|
||||
|
||||
// WAP URL 是本地签名,不需要先调第三方,在事务内创建充值单和支付单
|
||||
var config *model.WechatConfig
|
||||
var expireAt time.Time
|
||||
if err := h.db.WithContext(resolved.SkipPermissionCtx).Transaction(func(tx *gorm.DB) error {
|
||||
route, selectedConfig, err := h.selectMerchantConfig(resolved.SkipPermissionCtx, tx, model.PaymentByAlipay, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expireMinutes := selectedConfig.AliPayExpireMinutes
|
||||
if expireMinutes <= 0 {
|
||||
expireMinutes = 30
|
||||
}
|
||||
expireAt = time.Now().Add(time.Duration(expireMinutes) * time.Minute)
|
||||
config = selectedConfig
|
||||
payment.ExpireAt = &expireAt
|
||||
merchantpayment.FreezeRoute(payment, route)
|
||||
if err := h.rechargeOrderStore.CreateWithTx(resolved.SkipPermissionCtx, tx, rechargeOrder); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败")
|
||||
}
|
||||
@@ -505,7 +501,6 @@ func (h *ClientWalletHandler) createAlipayRecharge(
|
||||
zap.String("recharge_no", rechargeNo),
|
||||
zap.Int64("amount", req.Amount),
|
||||
zap.Time("expire_at", expireAt),
|
||||
zap.Uint("config_id", config.ID),
|
||||
)
|
||||
|
||||
expireStr := expireAt.Format(time.RFC3339)
|
||||
@@ -738,7 +733,37 @@ func parseOptionalTime(value string) (*time.Time, error) {
|
||||
return nil, fmt.Errorf("invalid time format")
|
||||
}
|
||||
|
||||
func pickAppIDByType(config *model.WechatConfig, appType string) (string, error) {
|
||||
func (h *ClientWalletHandler) loadWechatAuthorization(ctx context.Context, appType string) (*model.WechatAuthorization, string, error) {
|
||||
if h.merchantRuntime == nil {
|
||||
return nil, "", errors.New(errors.CodeServiceUnavailable, "商户池路由能力未配置")
|
||||
}
|
||||
authorization, err := h.merchantRuntime.LoadAuthorization(ctx)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
appID, err := pickAppIDByType(authorization, appType)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return authorization, appID, nil
|
||||
}
|
||||
|
||||
func (h *ClientWalletHandler) selectMerchantConfig(ctx context.Context, tx *gorm.DB, paymentMethod string, authorization *model.WechatAuthorization) (*merchantpayment.RouteSelection, *model.WechatConfig, error) {
|
||||
if h.merchantRuntime == nil {
|
||||
return nil, nil, errors.New(errors.CodeServiceUnavailable, "商户池路由能力未配置")
|
||||
}
|
||||
route, err := h.merchantRuntime.SelectForNewPaymentWithTx(ctx, tx, paymentMethod, time.Now())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
config, err := merchantpayment.MerchantConfig(route.Merchant, authorization)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return route, config, nil
|
||||
}
|
||||
|
||||
func pickAppIDByType(config *model.WechatAuthorization, appType string) (string, error) {
|
||||
switch appType {
|
||||
case "official_account":
|
||||
if strings.TrimSpace(config.OaAppID) == "" {
|
||||
|
||||
@@ -54,7 +54,7 @@ func (h *ClientWechatHandler) GetJSSDKConfig(c *fiber.Ctx) error {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
wechatConfig, err := h.wechatConfigService.GetActiveConfig(c.UserContext())
|
||||
wechatConfig, err := h.wechatConfigService.GetAuthorizationConfig(c.UserContext())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -82,7 +82,7 @@ func (h *ClientWechatHandler) GetJSSDKConfig(c *fiber.Ctx) error {
|
||||
// GetAppID 获取当前生效的公众号 AppID
|
||||
// GET /api/c/v1/wechat/appid
|
||||
func (h *ClientWechatHandler) GetAppID(c *fiber.Ctx) error {
|
||||
wechatConfig, err := h.wechatConfigService.GetActiveConfig(c.UserContext())
|
||||
wechatConfig, err := h.wechatConfigService.GetAuthorizationConfig(c.UserContext())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user