代理在线充值
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m6s

This commit is contained in:
2026-07-27 16:02:55 +08:00
parent a2166c8011
commit cbf909b878
42 changed files with 3121 additions and 195 deletions

View File

@@ -1,23 +1,42 @@
package admin
import (
"bytes"
"strconv"
"strings"
"github.com/bytedance/sonic"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
agentrechargeapp "github.com/break/junhong_cmp_fiber/internal/application/agentrecharge"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
agentrechargequery "github.com/break/junhong_cmp_fiber/internal/query/agentrecharge"
agentRechargeSvc "github.com/break/junhong_cmp_fiber/internal/service/agent_recharge"
"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/response"
)
// AgentRechargeHandler 代理预充值 Handler
type AgentRechargeHandler struct {
service *agentRechargeSvc.Service
online *agentrechargeapp.OnlineCreationService
status *agentrechargequery.PaymentStatusQuery
validator *validator.Validate
}
// SetOnlineCreationService 注入代理在线扫码充值用例。
func (h *AgentRechargeHandler) SetOnlineCreationService(service *agentrechargeapp.OnlineCreationService) {
h.online = service
}
// SetPaymentStatusQuery 注入代理充值本地支付状态 Query。
func (h *AgentRechargeHandler) SetPaymentStatusQuery(query *agentrechargequery.PaymentStatusQuery) {
h.status = query
}
// NewAgentRechargeHandler 创建代理预充值 Handler
func NewAgentRechargeHandler(service *agentRechargeSvc.Service, validator *validator.Validate) *AgentRechargeHandler {
return &AgentRechargeHandler{service: service, validator: validator}
@@ -27,13 +46,21 @@ func NewAgentRechargeHandler(service *agentRechargeSvc.Service, validator *valid
// POST /api/admin/agent-recharges
func (h *AgentRechargeHandler) Create(c *fiber.Ctx) error {
var req dto.CreateAgentRechargeRequest
if err := c.BodyParser(&req); err != nil {
decoder := sonic.ConfigStd.NewDecoder(bytes.NewReader(c.Body()))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if err := h.validator.Struct(&req); err != nil {
return errors.New(errors.CodeInvalidParam)
}
if req.PaymentMethod == constants.RechargeMethodWechat || req.PaymentMethod == constants.RechargeMethodAlipay {
return h.createOnline(c, req)
}
if strings.TrimSpace(req.RequestID) != "" {
return errors.New(errors.CodeInvalidParam, "线下充值不能传入在线请求标识")
}
result, err := h.service.Create(c.UserContext(), &req)
if err != nil {
return err
@@ -42,6 +69,45 @@ func (h *AgentRechargeHandler) Create(c *fiber.Ctx) error {
return response.Success(c, result)
}
func (h *AgentRechargeHandler) createOnline(c *fiber.Ctx, req dto.CreateAgentRechargeRequest) error {
if h.online == nil {
return errors.New(errors.CodeServiceUnavailable, "代理在线充值能力未配置")
}
if req.ShopID != nil || len(req.PaymentVoucherKey) > 0 || strings.TrimSpace(req.Remark) != "" {
return errors.New(errors.CodeInvalidParam, "在线充值不能指定店铺、支付凭证或运营备注")
}
result, err := h.online.Execute(c.UserContext(), agentrechargeapp.CreateOnlineCommand{
AccountID: middleware.GetUserIDFromContext(c.UserContext()), UserType: middleware.GetUserTypeFromContext(c.UserContext()),
CurrentShopID: middleware.GetShopIDFromContext(c.UserContext()), Amount: req.Amount,
PaymentMethod: req.PaymentMethod, RequestID: req.RequestID,
})
if err != nil {
return err
}
rechargeSource, rechargeSourceName := constants.GetAgentRechargeSource(result.Payment.PaymentMethod)
return response.Success(c, &dto.AgentRechargeOnlineResponse{
RechargeID: result.Recharge.ID, RechargeNo: result.Recharge.RechargeNo, PaymentNo: result.Payment.PaymentNo,
PaymentMethod: result.Payment.PaymentMethod, Amount: result.Payment.Amount, QRContent: result.Payment.QRContent,
RechargeSource: rechargeSource, RechargeSourceName: rechargeSourceName,
Status: result.Recharge.Status, StatusName: constants.GetRechargeStatusName(result.Recharge.Status),
})
}
// PaymentMethods 查询代理在线充值可用支付方式。
// GET /api/admin/agent-recharges/payment-methods
func (h *AgentRechargeHandler) PaymentMethods(c *fiber.Ctx) error {
if h.online == nil {
return errors.New(errors.CodeServiceUnavailable, "代理在线充值能力未配置")
}
result, err := h.online.AvailablePaymentMethods(c.UserContext(), middleware.GetUserTypeFromContext(c.UserContext()))
if err != nil {
return err
}
return response.Success(c, &dto.AgentRechargePaymentMethodsResponse{
Methods: result.Methods, MinAmount: result.MinAmount, MaxAmount: result.MaxAmount,
})
}
// List 查询代理充值订单列表
// GET /api/admin/agent-recharges
func (h *AgentRechargeHandler) List(c *fiber.Ctx) error {
@@ -49,6 +115,9 @@ func (h *AgentRechargeHandler) List(c *fiber.Ctx) error {
if err := c.QueryParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if err := h.validator.Struct(&req); err != nil {
return errors.New(errors.CodeInvalidParam)
}
list, total, err := h.service.List(c.UserContext(), &req)
if err != nil {
@@ -74,6 +143,23 @@ func (h *AgentRechargeHandler) Get(c *fiber.Ctx) error {
return response.Success(c, result)
}
// PaymentStatus 查询代理充值本地支付与到账状态。
// GET /api/admin/agent-recharges/:id/payment-status
func (h *AgentRechargeHandler) PaymentStatus(c *fiber.Ctx) error {
if h.status == nil {
return errors.New(errors.CodeServiceUnavailable, "代理充值支付状态查询未配置")
}
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil || id == 0 {
return errors.New(errors.CodeInvalidParam, "无效的充值记录ID")
}
result, err := h.status.Get(c.UserContext(), uint(id))
if err != nil {
return err
}
return response.Success(c, result)
}
// Reject 驳回代理充值订单
// POST /api/admin/agent-recharges/:id/reject
func (h *AgentRechargeHandler) Reject(c *fiber.Ctx) error {

View File

@@ -2,16 +2,18 @@ package callback
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/valyala/fasthttp/fasthttpadaptor"
"go.uber.org/zap"
agentrechargeApp "github.com/break/junhong_cmp_fiber/internal/application/agentrecharge"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
"github.com/break/junhong_cmp_fiber/internal/model"
orderService "github.com/break/junhong_cmp_fiber/internal/service/order"
rechargeService "github.com/break/junhong_cmp_fiber/internal/service/recharge"
@@ -21,6 +23,7 @@ import (
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/fuiou"
pkgmiddleware "github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/break/junhong_cmp_fiber/pkg/wechat"
"gorm.io/gorm"
)
@@ -44,6 +47,8 @@ type PaymentHandler struct {
wechatPayment wechat.PaymentServiceInterface
wechatConfigService WechatConfigServiceInterface
paymentStore *postgres.PaymentStore
agentPaymentConfirm *agentrechargeApp.ConfirmOnlinePaymentService
integration *integrationlog.Repository
logger *zap.Logger
}
@@ -55,6 +60,8 @@ func NewPaymentHandler(
wechatPayment wechat.PaymentServiceInterface,
wechatConfigService WechatConfigServiceInterface,
paymentStore *postgres.PaymentStore,
agentPaymentConfirm *agentrechargeApp.ConfirmOnlinePaymentService,
integration *integrationlog.Repository,
logger *zap.Logger,
) *PaymentHandler {
return &PaymentHandler{
@@ -65,15 +72,30 @@ func NewPaymentHandler(
wechatPayment: wechatPayment,
wechatConfigService: wechatConfigService,
paymentStore: paymentStore,
agentPaymentConfirm: agentPaymentConfirm,
integration: integration,
logger: logger,
}
}
type verifiedPaymentCallback struct {
PaymentNo string
PaymentMethod string
TransactionID string
Amount int64
ConfigID uint
MerchantIdentity string
PaidAt time.Time
Provider string
RawPayload []byte
ContentType string
}
// WechatPayCallback 微信支付回调(带签名验证)
// POST /api/callback/wechat-pay
func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
body := c.Body()
ctx := context.Background()
ctx := c.UserContext()
// 预解析订单号(不验签),用于按 payment_config_id 加载创建订单时所用的配置
orderNo, err := wechat.PeekOrderNo(body)
@@ -103,7 +125,13 @@ func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
}
// TotalFee 为字符串格式的分,解析失败则降级为 0后续 handlePaymentCallback 会记录日志)
totalFee, _ := strconv.ParseInt(result.TotalFee, 10, 64)
if err := h.dispatchWechatCallback(ctx, result.OutTradeNo, result.TransactionID, totalFee); err != nil {
paidAt, _ := time.ParseInLocation("20060102150405", result.SuccessTime, time.Local)
if err := h.dispatchWechatCallback(ctx, verifiedPaymentCallback{
PaymentNo: result.OutTradeNo, PaymentMethod: model.PaymentMethodWechat,
TransactionID: result.TransactionID, Amount: totalFee, ConfigID: cfg.ID,
MerchantIdentity: cfg.WxMchID, PaidAt: paidAt, Provider: constants.IntegrationProviderWechatPay,
RawPayload: body, ContentType: c.Get("Content-Type"),
}); err != nil {
return errors.Wrap(errors.CodeWechatCallbackInvalid, err, "处理微信支付回调失败")
}
@@ -117,7 +145,13 @@ func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
if result.TradeState != "SUCCESS" {
return nil
}
return h.dispatchWechatCallback(ctx, result.OutTradeNo, result.TransactionID, result.TotalAmount)
paidAt, _ := time.Parse(time.RFC3339, result.SuccessTime)
return h.dispatchWechatCallback(ctx, verifiedPaymentCallback{
PaymentNo: result.OutTradeNo, PaymentMethod: model.PaymentMethodWechat,
TransactionID: result.TransactionID, Amount: result.TotalAmount, ConfigID: cfg.ID,
MerchantIdentity: cfg.WxMchID, PaidAt: paidAt, Provider: constants.IntegrationProviderWechatPay,
RawPayload: body, ContentType: c.Get("Content-Type"),
})
})
if err != nil {
return errors.Wrap(errors.CodeWechatCallbackInvalid, err, "处理微信支付回调失败")
@@ -130,20 +164,22 @@ func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
return h.wechatV2SuccessResponse(c)
}
func (h *PaymentHandler) dispatchPaymentRecordCallback(ctx context.Context, paymentNo, paymentMethod, transactionID string, paidAmount int64) (bool, error) {
func (h *PaymentHandler) dispatchPaymentRecordCallback(ctx context.Context, callback verifiedPaymentCallback) (bool, error) {
if h.paymentStore != nil {
payment, err := h.paymentStore.GetByPaymentNo(ctx, paymentNo)
payment, err := h.paymentStore.GetByPaymentNo(ctx, callback.PaymentNo)
if err == nil {
switch payment.OrderType {
case model.PaymentOrderTypePackage:
return true, h.orderService.HandlePaymentRecordCallback(ctx, paymentNo, paymentMethod, transactionID, paidAmount)
return true, h.orderService.HandlePaymentRecordCallback(ctx, callback.PaymentNo, callback.PaymentMethod, callback.TransactionID, callback.Amount)
case model.PaymentOrderTypeRecharge:
if h.rechargeOrderService != nil {
return true, h.rechargeOrderService.HandlePaymentCallback(ctx, paymentNo, paymentMethod, transactionID)
return true, h.rechargeOrderService.HandlePaymentCallback(ctx, callback.PaymentNo, callback.PaymentMethod, callback.TransactionID)
}
return true, fmt.Errorf("充值订单服务未配置,无法处理支付单: %s", paymentNo)
return true, errors.New(errors.CodeInternalError, "充值订单服务未配置")
case model.PaymentOrderTypeAgentRecharge:
return true, h.confirmAgentRechargePayment(ctx, callback)
default:
return true, fmt.Errorf("未知支付记录类型: %s", payment.OrderType)
return true, errors.New(errors.CodeInvalidStatus, "未知支付记录类型")
}
}
if err != gorm.ErrRecordNotFound {
@@ -154,27 +190,83 @@ func (h *PaymentHandler) dispatchPaymentRecordCallback(ctx context.Context, paym
}
// dispatchWechatCallback 优先按支付单分发,旧单号继续按前缀兼容处理。
func (h *PaymentHandler) dispatchWechatCallback(ctx context.Context, outTradeNo, transactionID string, paidAmount int64) error {
handled, err := h.dispatchPaymentRecordCallback(ctx, outTradeNo, model.PaymentMethodWechat, transactionID, paidAmount)
func (h *PaymentHandler) dispatchWechatCallback(ctx context.Context, callback verifiedPaymentCallback) error {
handled, err := h.dispatchPaymentRecordCallback(ctx, callback)
if handled || err != nil {
return err
}
switch {
case strings.HasPrefix(outTradeNo, "ORD"):
return h.orderService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat, paidAmount)
case strings.HasPrefix(outTradeNo, constants.AssetRechargeOrderPrefix):
case strings.HasPrefix(callback.PaymentNo, "ORD"):
return h.orderService.HandlePaymentCallback(ctx, callback.PaymentNo, model.PaymentMethodWechat, callback.Amount)
case strings.HasPrefix(callback.PaymentNo, constants.AssetRechargeOrderPrefix):
if h.rechargeOrderService != nil {
return h.rechargeOrderService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentByWechat, transactionID)
return h.rechargeOrderService.HandlePaymentCallback(ctx, callback.PaymentNo, model.PaymentByWechat, callback.TransactionID)
}
return fmt.Errorf("充值订单服务未配置,无法处理订单: %s", outTradeNo)
case strings.HasPrefix(outTradeNo, constants.AgentRechargeOrderPrefix):
return errors.New(errors.CodeInternalError, "充值订单服务未配置")
case strings.HasPrefix(callback.PaymentNo, constants.AgentRechargeOrderPrefix):
if h.agentRechargeService != nil {
return h.agentRechargeService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat, transactionID, paidAmount)
return h.agentRechargeService.HandlePaymentCallback(ctx, callback.PaymentNo, model.PaymentMethodWechat, callback.TransactionID, callback.Amount)
}
return fmt.Errorf("代理充值服务未配置,无法处理订单: %s", outTradeNo)
return errors.New(errors.CodeInternalError, "代理充值服务未配置")
default:
return fmt.Errorf("未知订单号前缀: %s", outTradeNo)
return errors.New(errors.CodeInvalidStatus, "未知订单号前缀")
}
}
func (h *PaymentHandler) confirmAgentRechargePayment(ctx context.Context, callback verifiedPaymentCallback) error {
if h.agentPaymentConfirm == nil || h.integration == nil {
return errors.New(errors.CodeInternalError, "代理充值支付回调能力未配置")
}
resourceID, correlationID := callback.PaymentNo, callback.PaymentNo
idempotencyKey := callback.TransactionID
if idempotencyKey == "" {
idempotencyKey = callback.PaymentNo
}
log, _, err := h.integration.RecordInbound(ctx, integrationlog.InboundAttempt{
IdempotencyKey: idempotencyKey, Provider: callback.Provider,
Operation: constants.IntegrationOperationPaymentCallback, ExternalID: callback.TransactionID,
ResourceType: constants.IntegrationResourceTypeAgentRechargePayment, ResourceID: &resourceID,
RawPayload: callback.RawPayload, ContentType: callback.ContentType,
RequestID: pkgmiddleware.GetRequestIDFromContext(ctx), CorrelationID: &correlationID,
})
if err != nil {
return err
}
result, confirmErr := h.agentPaymentConfirm.Execute(ctx, agentrechargeApp.ConfirmOnlinePaymentCommand{
PaymentNo: callback.PaymentNo, PaymentMethod: callback.PaymentMethod, ConfigID: callback.ConfigID,
MerchantIdentity: callback.MerchantIdentity, ThirdPartyTradeNo: callback.TransactionID,
Amount: callback.Amount, PaidAt: callback.PaidAt, CorrelationID: correlationID,
})
if confirmErr != nil {
h.completePaymentCallbackLog(ctx, log, integrationlog.Completion{
Result: constants.IntegrationResultFailed, ProviderMessage: confirmErr.Error(),
ResponseSummary: map[string]any{"confirmed": false},
})
return confirmErr
}
if log.Result == constants.IntegrationResultPending {
_, err = h.integration.Complete(ctx, log.IntegrationID, integrationlog.Completion{
Result: constants.IntegrationResultSuccess, ProviderCode: "SUCCESS",
ResponseSummary: map[string]any{"confirmed": true, "already_confirmed": result.AlreadyConfirmed},
StateChanged: !result.AlreadyConfirmed,
})
if err != nil {
return err
}
}
return nil
}
func (h *PaymentHandler) completePaymentCallbackLog(ctx context.Context, log *model.IntegrationLog, completion integrationlog.Completion) {
if log == nil || log.Result != constants.IntegrationResultPending {
return
}
if _, err := h.integration.Complete(ctx, log.IntegrationID, completion); err != nil {
h.logger.Error("支付回调 Integration Log 终结失败",
zap.String("integration_id", log.IntegrationID),
zap.Error(err),
)
}
}
@@ -305,14 +397,16 @@ func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error {
return errors.New(errors.CodeWechatCallbackInvalid, "支付金额校验失败")
}
// 写入支付宝交易流水号
if err := h.paymentStore.UpdatePaymentInfo(ctx, payment.ID, notification.TradeNo, nil); err != nil {
h.logger.Error("支付宝回调:写入 third_party_trade_no 失败",
zap.String("out_trade_no", outTradeNo),
zap.String("trade_no", notification.TradeNo),
zap.Error(err),
)
// 不中断业务,继续分发(幂等业务层会处理状态)
// 新代理充值由统一确认事务原子写入交易号,旧业务保持原处理方式。
if payment.OrderType != model.PaymentOrderTypeAgentRecharge {
if err := h.paymentStore.UpdatePaymentInfo(ctx, payment.ID, notification.TradeNo, nil); err != nil {
h.logger.Error("支付宝回调:写入 third_party_trade_no 失败",
zap.String("out_trade_no", outTradeNo),
zap.String("trade_no", notification.TradeNo),
zap.Error(err),
)
// 不中断存量业务,继续由原幂等业务层处理状态。
}
}
// 按支付单 order_type 分发业务
@@ -353,6 +447,28 @@ func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error {
zap.String("order_type", payment.OrderType),
)
case model.PaymentOrderTypeAgentRecharge:
paidAt, parseErr := time.ParseInLocation("2006-01-02 15:04:05", notification.GmtPayment, time.Local)
if parseErr != nil {
h.logger.Error("支付宝回调:付款时间格式无效",
zap.String("out_trade_no", outTradeNo),
zap.Error(parseErr),
)
return errors.New(errors.CodeWechatCallbackInvalid, "支付宝付款时间格式错误")
}
if err := h.confirmAgentRechargePayment(ctx, verifiedPaymentCallback{
PaymentNo: outTradeNo, PaymentMethod: model.PaymentByAlipay,
TransactionID: notification.TradeNo, Amount: notifyAmountFen, ConfigID: cfg.ID,
MerchantIdentity: notification.AppId, PaidAt: paidAt, Provider: constants.IntegrationProviderAlipay,
RawPayload: c.Body(), ContentType: c.Get("Content-Type"),
}); err != nil {
h.logger.Error("支付宝回调:确认代理充值支付失败",
zap.String("out_trade_no", outTradeNo),
zap.Error(err),
)
return errors.Wrap(errors.CodeInternalError, err, "处理支付宝代理充值回调失败")
}
default:
h.logger.Error("支付宝回调:未知支付记录类型",
zap.String("out_trade_no", outTradeNo),
@@ -452,7 +568,11 @@ func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error {
orderNo := notify.MchntOrderNo
// OrderAmt 为字符串格式的分,解析失败则降级为 0
orderAmt, _ := strconv.ParseInt(notify.OrderAmt, 10, 64)
if handled, err := h.dispatchPaymentRecordCallback(ctx, orderNo, "fuiou", notify.TransactionId, orderAmt); err != nil {
if handled, err := h.dispatchPaymentRecordCallback(ctx, verifiedPaymentCallback{
PaymentNo: orderNo, PaymentMethod: "fuiou", TransactionID: notify.TransactionId, Amount: orderAmt,
ConfigID: cfg.ID, MerchantIdentity: cfg.FyMchntCd, Provider: model.ProviderTypeFuiou,
RawPayload: body, ContentType: c.Get("Content-Type"),
}); err != nil {
return c.Send(fuiou.BuildNotifyFailResponse(err.Error()))
} else if handled {
return c.Send(fuiou.BuildNotifySuccessResponse())