新增收款商户、商户池轮询、微信授权配置独立管理;三类新支付 (C端套餐购买、C端资产钱包充值、代理在线预存款充值)无条件 经商户池选择并冻结路由,无旧综合配置回退。merchant_id 为空 历史支付继续按 payment_config_id 双读。凭证版本化加载与 ID+版本缓存保证轮换一致性。删除商户池新支付创建开关及全部 引用。
This commit is contained in:
218
internal/handler/admin/payment_merchant.go
Normal file
218
internal/handler/admin/payment_merchant.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
merchantpayment "github.com/break/junhong_cmp_fiber/internal/application/merchantpayment"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
)
|
||||
|
||||
// PaymentMerchantHandler 处理特权商户与商户池配置请求。
|
||||
type PaymentMerchantHandler struct {
|
||||
service *merchantpayment.ManagementService
|
||||
}
|
||||
|
||||
// NewPaymentMerchantHandler 创建商户池管理处理器。
|
||||
func NewPaymentMerchantHandler(service *merchantpayment.ManagementService) *PaymentMerchantHandler {
|
||||
return &PaymentMerchantHandler{service: service}
|
||||
}
|
||||
func pathID(c *fiber.Ctx) (uint, error) {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return 0, errors.New(errors.CodeInvalidParam, "无效的路径ID")
|
||||
}
|
||||
return uint(id), nil
|
||||
}
|
||||
|
||||
// CreateMerchant 创建支付商户。
|
||||
// POST /api/admin/payment-merchants
|
||||
func (h *PaymentMerchantHandler) CreateMerchant(c *fiber.Ctx) error {
|
||||
var r dto.PaymentMerchantRequest
|
||||
if err := c.BodyParser(&r); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
v, err := h.service.CreateMerchant(c.UserContext(), r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, v)
|
||||
}
|
||||
|
||||
// ListMerchants 分页查询支付商户。
|
||||
// GET /api/admin/payment-merchants
|
||||
func (h *PaymentMerchantHandler) ListMerchants(c *fiber.Ctx) error {
|
||||
var r dto.PaymentMerchantListRequest
|
||||
if err := c.QueryParser(&r); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
v, n, err := h.service.ListMerchants(c.UserContext(), r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.SuccessWithPagination(c, v, n, r.Page, r.PageSize)
|
||||
}
|
||||
|
||||
// GetMerchant 查询支付商户详情。
|
||||
// GET /api/admin/payment-merchants/:id
|
||||
func (h *PaymentMerchantHandler) GetMerchant(c *fiber.Ctx) error {
|
||||
id, err := pathID(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v, err := h.service.GetMerchant(c.UserContext(), id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, v)
|
||||
}
|
||||
|
||||
// UpdateMerchant 更新支付商户。
|
||||
// PUT /api/admin/payment-merchants/:id
|
||||
func (h *PaymentMerchantHandler) UpdateMerchant(c *fiber.Ctx) error {
|
||||
id, err := pathID(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var r dto.PaymentMerchantUpdateRequest
|
||||
if err := c.BodyParser(&r); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
v, err := h.service.UpdateMerchant(c.UserContext(), id, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, v)
|
||||
}
|
||||
|
||||
// DeleteMerchant 删除支付商户。
|
||||
// DELETE /api/admin/payment-merchants/:id
|
||||
func (h *PaymentMerchantHandler) DeleteMerchant(c *fiber.Ctx) error {
|
||||
id, err := pathID(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var r dto.PaymentMerchantDeleteRequest
|
||||
if err := c.BodyParser(&r); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
if err := h.service.DeleteMerchant(c.UserContext(), id, r.Confirm); err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, nil)
|
||||
}
|
||||
|
||||
// CreatePool 创建商户池。
|
||||
// POST /api/admin/payment-merchant-pools
|
||||
func (h *PaymentMerchantHandler) CreatePool(c *fiber.Ctx) error {
|
||||
var r dto.PaymentMerchantPoolRequest
|
||||
if err := c.BodyParser(&r); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
v, err := h.service.SavePool(c.UserContext(), 0, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, v)
|
||||
}
|
||||
|
||||
// UpdatePool 更新商户池。
|
||||
// PUT /api/admin/payment-merchant-pools/:id
|
||||
func (h *PaymentMerchantHandler) UpdatePool(c *fiber.Ctx) error {
|
||||
id, err := pathID(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var r dto.PaymentMerchantPoolRequest
|
||||
if err := c.BodyParser(&r); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
v, err := h.service.SavePool(c.UserContext(), id, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, v)
|
||||
}
|
||||
|
||||
// EnablePool 启用商户池。
|
||||
// POST /api/admin/payment-merchant-pools/:id/enable
|
||||
func (h *PaymentMerchantHandler) EnablePool(c *fiber.Ctx) error {
|
||||
id, err := pathID(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := h.service.SetPoolEnabled(c.UserContext(), id, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, result)
|
||||
}
|
||||
|
||||
// DisablePool 停用商户池。
|
||||
// POST /api/admin/payment-merchant-pools/:id/disable
|
||||
func (h *PaymentMerchantHandler) DisablePool(c *fiber.Ctx) error {
|
||||
id, err := pathID(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := h.service.SetPoolEnabled(c.UserContext(), id, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, result)
|
||||
}
|
||||
|
||||
// GetAuthorization 获取微信授权配置。
|
||||
// GET /api/admin/wechat-authorizations
|
||||
func (h *PaymentMerchantHandler) GetAuthorization(c *fiber.Ctx) error {
|
||||
v, err := h.service.GetAuthorization(c.UserContext())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, v)
|
||||
}
|
||||
|
||||
// ListPools 分页查询商户池。
|
||||
// GET /api/admin/payment-merchant-pools
|
||||
func (h *PaymentMerchantHandler) ListPools(c *fiber.Ctx) error {
|
||||
var r dto.PaymentMerchantPoolListRequest
|
||||
if err := c.QueryParser(&r); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
v, total, err := h.service.ListPools(c.UserContext(), r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.SuccessWithPagination(c, v, total, r.Page, r.PageSize)
|
||||
}
|
||||
|
||||
// GetPool 查询商户池详情。
|
||||
// GET /api/admin/payment-merchant-pools/:id
|
||||
func (h *PaymentMerchantHandler) GetPool(c *fiber.Ctx) error {
|
||||
id, err := pathID(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v, err := h.service.GetPool(c.UserContext(), id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, v)
|
||||
}
|
||||
|
||||
// SaveAuthorization 保存微信授权配置。
|
||||
// PUT /api/admin/wechat-authorizations/current
|
||||
func (h *PaymentMerchantHandler) SaveAuthorization(c *fiber.Ctx) error {
|
||||
var r dto.WechatAuthorizationRequest
|
||||
if err := c.BodyParser(&r); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
v, err := h.service.SaveAuthorization(c.UserContext(), r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, v)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ type AgentRechargeServiceInterface interface {
|
||||
type WechatConfigServiceInterface interface {
|
||||
GetActiveConfig(ctx context.Context) (*model.WechatConfig, error)
|
||||
GetConfigForCallback(ctx context.Context, orderNo string) (*model.WechatConfig, error)
|
||||
GetPaymentServiceForCallback(config *model.WechatConfig) (wechat.PaymentServiceInterface, error)
|
||||
}
|
||||
|
||||
type PaymentHandler struct {
|
||||
@@ -98,7 +99,7 @@ func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
|
||||
body := c.Body()
|
||||
ctx := c.UserContext()
|
||||
|
||||
// 预解析订单号(不验签),用于按 payment_config_id 加载创建订单时所用的配置
|
||||
// 预解析订单号(不验签),用于按支付单冻结商户或历史 payment_config_id 双读加载配置
|
||||
orderNo, err := wechat.PeekOrderNo(body)
|
||||
if err != nil {
|
||||
h.logger.Error("微信回调:预解析订单号失败", zap.Error(err))
|
||||
@@ -144,12 +145,13 @@ func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
case model.ProviderTypeWechat:
|
||||
if h.wechatPayment == nil {
|
||||
return errors.New(errors.CodeWechatCallbackInvalid, "微信 v3 支付未初始化")
|
||||
paymentSvc, err := h.wechatConfigService.GetPaymentServiceForCallback(cfg)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeWechatCallbackInvalid, err, "构建微信 v3 回调验签实例失败")
|
||||
}
|
||||
var httpReq http.Request
|
||||
fasthttpadaptor.ConvertRequest(c.Context(), &httpReq, true)
|
||||
_, err := h.wechatPayment.HandlePaymentNotify(&httpReq, func(result *wechat.PaymentNotifyResult) error {
|
||||
_, err = paymentSvc.HandlePaymentNotify(&httpReq, func(result *wechat.PaymentNotifyResult) error {
|
||||
if result.TradeState != "SUCCESS" {
|
||||
return h.recordIgnoredPaymentCallback(ctx, verifiedPaymentCallback{
|
||||
PaymentNo: result.OutTradeNo, TransactionID: result.TransactionID,
|
||||
@@ -421,7 +423,7 @@ func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error {
|
||||
return errors.New(errors.CodeInvalidParam, "订单号不能为空")
|
||||
}
|
||||
|
||||
// 按 payment_config_id 加载创建支付单时所用的配置(支持已停用配置)
|
||||
// 按支付单冻结商户或历史 payment_config_id 双读加载配置(支持已停用配置)
|
||||
cfg, err := h.wechatConfigService.GetConfigForCallback(ctx, outTradeNo)
|
||||
if err != nil || cfg == nil {
|
||||
h.logger.Error("支付宝回调:加载支付配置失败",
|
||||
@@ -494,13 +496,14 @@ func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error {
|
||||
return errors.New(errors.CodeWechatCallbackInvalid, "支付通道校验失败")
|
||||
}
|
||||
|
||||
// 校验 payment_config_id 与当前配置一致;旧数据为空时兼容并记录 warn
|
||||
if payment.PaymentConfigID == nil {
|
||||
// 商户支付单已由 GetConfigForCallback 按 merchant_id 绑定当前商户凭证;
|
||||
// payment_config_id 只属于历史双读路径,不能用于否定新商户回调。
|
||||
if payment.MerchantID == nil && payment.PaymentConfigID == nil {
|
||||
h.logger.Warn("支付宝回调:payment_config_id 为空,跳过配置 ID 校验(旧数据兼容)",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Uint("config_id", cfg.ID),
|
||||
)
|
||||
} else if *payment.PaymentConfigID != cfg.ID {
|
||||
} else if payment.MerchantID == nil && *payment.PaymentConfigID != cfg.ID {
|
||||
h.logger.Error("支付宝回调:payment_config_id 不匹配",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Uint("payment_config_id", *payment.PaymentConfigID),
|
||||
@@ -567,7 +570,7 @@ func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error {
|
||||
ctx := c.UserContext()
|
||||
c.Set("Content-Type", "text/plain; charset=utf-8")
|
||||
|
||||
// 预解析订单号(不验签),用于按 payment_config_id 加载创建订单时所用的配置
|
||||
// 预解析订单号(不验签),用于按支付单冻结商户或历史 payment_config_id 双读加载配置
|
||||
preNotify, err := fuiou.ParseNotify(body)
|
||||
if err != nil {
|
||||
h.logger.Error("富友回调:预解析失败",
|
||||
|
||||
Reference in New Issue
Block a user