This commit is contained in:
Mrx
2026-06-06 17:03:08 +08:00
parent a85436950e
commit 35e9191981
28 changed files with 666 additions and 286 deletions

View File

@@ -42,8 +42,8 @@ func (l *PaymentCheckLogic) PaymentCheck(req *types.PaymentCheckReq) (resp *type
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单失败: %v", findErr)
}
// xpay 轮询pending 时主动查微信单
if order.Status == "pending" && order.PaymentScene == "wxmini" &&
// xpay 轮询pending 时主动查微信单并到账(不通知微信发货)
if order.Status == model.OrderStatusPending && model.IsXpayOrder(order) &&
l.svcCtx.XpayService != nil && l.svcCtx.XpayService.Enabled() {
if syncErr := l.syncXpayOrderStatus(order); syncErr != nil {
l.Errorf("[xpay] 轮询查单失败 order_no=%s err=%v", req.OrderNo, syncErr)
@@ -68,27 +68,14 @@ func (l *PaymentCheckLogic) syncXpayOrderStatus(order *model.Order) error {
if err != nil {
return err
}
sessionKey, err := l.svcCtx.XpayService.GetSessionKey(l.ctx, userID)
if err != nil {
return err
}
status, err := l.svcCtx.XpayService.QueryOrder(l.ctx, openid, order.OrderNo, sessionKey)
status, err := l.svcCtx.XpayService.QueryOrder(l.ctx, openid, order.OrderNo, "")
if err != nil {
return err
}
if service.IsXpayPaidStatus(status.Status) {
credited, fulfillErr := fulfillQueryOrderPaid(l.ctx, l.svcCtx, order, "", status.PaidFee)
if fulfillErr != nil {
return fulfillErr
}
if credited {
wxOrderID := ""
_ = l.svcCtx.XpayService.NotifyProvideGoods(l.ctx, openid, order.OrderNo, wxOrderID, sessionKey)
_ = l.svcCtx.XpayService.MarkNotified(l.ctx, order.OrderNo)
}
return nil
_, fulfillErr := fulfillQueryOrderPaid(l.ctx, l.svcCtx, order, status.WxOrderID, status.PaidFee)
return fulfillErr
}
if service.IsXpayClosedStatus(status.Status) {

View File

@@ -67,7 +67,7 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
}
case "query":
paymentTypeResp, err = l.QueryOrderPayment(req, session)
paymentTypeResp, err = l.QueryOrderPayment(req, session, useXpay)
if err != nil {
return err
}
@@ -225,7 +225,7 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
}
}
func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Session) (resp *PaymentTypeResp, err error) {
func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Session, useXpay bool) (resp *PaymentTypeResp, err error) {
userID, getUidErr := ctxdata.GetUidFromCtx(l.ctx)
if getUidErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 获取用户信息失败, %+v", getUidErr)
@@ -271,12 +271,43 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses
if user.Inside == 1 {
amount = 0.01
}
// 同一 queryId 重复发起支付:复用已有订单,避免 order_no 唯一键冲突导致二次支付失败
if existingOrder, findOrderErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, outTradeNo); findOrderErr == nil {
switch existingOrder.Status {
case model.OrderStatusPaid:
return nil, errors.Wrapf(xerr.NewErrMsg("订单已支付,请前往报告查看"), "订单号: %s", outTradeNo)
case model.OrderStatusPending:
if existingOrder.Status != model.OrderStatusPending {
existingOrder.Status = model.OrderStatusPending
if updateErr := l.svcCtx.OrderModel.UpdateWithVersion(l.ctx, session, existingOrder); updateErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "重置订单状态失败: %+v", updateErr)
}
}
return &PaymentTypeResp{
amount: existingOrder.Amount,
outTradeNo: outTradeNo,
description: product.ProductName,
orderID: existingOrder.Id,
productEn: product.ProductEn,
}, nil
default:
return nil, errors.Wrapf(xerr.NewErrMsg("订单状态异常,请重新发起查询"), "status=%s", existingOrder.Status)
}
} else if !errors.Is(findOrderErr, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单失败: %+v", findOrderErr)
}
paymentPlatform := req.PayMethod
if useXpay {
paymentPlatform = model.PaymentPlatformWechatXpay
}
order := model.Order{
Id: uuid.NewString(),
OrderNo: outTradeNo,
UserId: userID,
ProductId: product.Id,
PaymentPlatform: req.PayMethod,
PaymentPlatform: paymentPlatform,
PaymentScene: resolvePaymentScene(l.ctx),
Amount: amount,
Status: "pending",
@@ -376,18 +407,9 @@ func (l *PaymentLogic) shouldUseXpay(req *types.PaymentReq) bool {
func resolvePaymentScene(ctx context.Context) string {
platform, err := ctxdata.GetPlatformFromCtx(ctx)
if err != nil {
return "app"
}
switch platform {
case model.PlatformWxMini:
return "wxmini"
case model.PlatformWxH5:
return "wxh5"
case model.PlatformH5:
return "h5"
default:
return "app"
return model.PaymentSceneApp
}
return model.PaymentSceneFromPlatform(platform)
}
// AgentVipOrderPayment 代理会员充值订单(已废弃,新系统使用升级功能替代)

View File

@@ -0,0 +1,87 @@
package pay
import (
"context"
"strings"
"qnc-server/app/main/api/internal/service"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/model"
)
// XpayDeliverResult 同步 xpay 支付状态(查微信单 → 本地到账 → 异步查报告)
type XpayDeliverResult struct {
Credited bool `json:"credited"`
Notified bool `json:"notified"`
WechatDetail string `json:"wechat_detail"`
Errors []string `json:"errors"`
Message string `json:"message"`
}
// DeliverXpayQueryOrder 按商户订单号同步微信虚拟支付到账(不通知微信发货,发货在报告成功后)
func DeliverXpayQueryOrder(ctx context.Context, svcCtx *svc.ServiceContext, orderNo string) (*XpayDeliverResult, error) {
order, err := svcCtx.OrderModel.FindOneByOrderNo(ctx, orderNo)
if err != nil {
return nil, err
}
resp := &XpayDeliverResult{WechatDetail: "ok"}
if order.Status == model.OrderStatusPaid {
resp.Message = "订单已支付,无需同步"
return resp, nil
}
if !model.IsXpayOrder(order) {
resp.Errors = append(resp.Errors, "非小程序虚拟支付订单")
resp.Message = "非小程序虚拟支付订单"
return resp, nil
}
if svcCtx.XpayService == nil || !svcCtx.XpayService.Enabled() {
resp.Errors = append(resp.Errors, "虚拟支付未启用")
resp.Message = "虚拟支付未启用"
return resp, nil
}
openid, err := svcCtx.XpayService.GetWxMiniOpenID(ctx, svcCtx.UserAuthModel, order.UserId)
if err != nil {
resp.Errors = append(resp.Errors, err.Error())
resp.Message = "获取用户 openid 失败"
return resp, nil
}
status, qErr := svcCtx.XpayService.QueryOrder(ctx, openid, order.OrderNo, "")
if qErr != nil {
resp.Errors = append(resp.Errors, qErr.Error())
resp.WechatDetail = qErr.Error()
resp.Message = "查询微信订单失败"
return resp, nil
}
if !service.IsXpayPaidStatus(status.Status) {
resp.Errors = append(resp.Errors, "微信侧订单未支付")
resp.Message = "微信侧订单未支付"
return resp, nil
}
platformOrderID := status.WxOrderID
credited, fulfillErr := fulfillQueryOrderPaid(ctx, svcCtx, order, platformOrderID, status.PaidFee)
resp.Credited = credited
if fulfillErr != nil {
resp.Errors = append(resp.Errors, fulfillErr.Error())
}
resp.Message = buildXpayDeliverMessage(resp)
return resp, nil
}
func buildXpayDeliverMessage(r *XpayDeliverResult) string {
if len(r.Errors) > 0 {
return strings.Join(r.Errors, "")
}
if r.Credited {
return "同步成功,订单已到账并开始查询报告"
}
return "未执行到账"
}

View File

@@ -0,0 +1,48 @@
package pay
import (
"context"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/model"
"github.com/zeromicro/go-zero/core/logx"
)
// NotifyXpayGoodsAfterReport 报告生成成功后通知微信虚拟支付已发货(合规/结算)
func NotifyXpayGoodsAfterReport(ctx context.Context, svcCtx *svc.ServiceContext, order *model.Order) {
if order == nil || !model.IsXpayOrder(order) {
return
}
if svcCtx.XpayService == nil || !svcCtx.XpayService.Enabled() {
return
}
already, err := svcCtx.XpayService.AlreadyNotified(ctx, order.OrderNo)
if err != nil {
logx.WithContext(ctx).Errorf("[xpay] 检查发货通知状态失败 order_no=%s err=%v", order.OrderNo, err)
return
}
if already {
return
}
openid, err := svcCtx.XpayService.GetWxMiniOpenID(ctx, svcCtx.UserAuthModel, order.UserId)
if err != nil {
logx.WithContext(ctx).Errorf("[xpay] 报告成功后通知发货:获取 openid 失败 order_no=%s err=%v", order.OrderNo, err)
return
}
sessionKey, _ := svcCtx.XpayService.GetSessionKey(ctx, order.UserId)
wxOrderID := ""
if order.PlatformOrderId.Valid {
wxOrderID = order.PlatformOrderId.String
}
if notifyErr := svcCtx.XpayService.NotifyProvideGoods(ctx, openid, order.OrderNo, wxOrderID, sessionKey); notifyErr != nil {
logx.WithContext(ctx).Errorf("[xpay] 报告成功后 notify_provide_goods 失败 order_no=%s err=%v", order.OrderNo, notifyErr)
return
}
_ = svcCtx.XpayService.MarkNotified(ctx, order.OrderNo)
logx.WithContext(ctx).Infof("[xpay] 报告成功后已通知微信发货 order_no=%s", order.OrderNo)
}

View File

@@ -0,0 +1,83 @@
package pay
import (
"context"
"fmt"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/model"
"qnc-server/pkg/lzkit/lzUtils"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
// RefundXpayQueryOrder 对 xpay 查询订单发起全额退款并更新本地订单状态
func RefundXpayQueryOrder(ctx context.Context, svcCtx *svc.ServiceContext, order *model.Order) error {
if svcCtx.XpayService == nil || !svcCtx.XpayService.Enabled() {
return fmt.Errorf("虚拟支付未启用")
}
openid, err := svcCtx.XpayService.GetWxMiniOpenID(ctx, svcCtx.UserAuthModel, order.UserId)
if err != nil {
return fmt.Errorf("获取 openid 失败: %w", err)
}
refundFeeFen := lzUtils.ToWechatAmount(order.Amount)
refundOrderID := buildXpayRefundOrderID(order.OrderNo)
if refundErr := svcCtx.XpayService.RefundOrder(ctx, openid, order.OrderNo, refundOrderID, refundFeeFen); refundErr != nil {
return refundErr
}
return svcCtx.OrderModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error {
order.Status = model.OrderStatusRefunded
if updateErr := svcCtx.OrderModel.UpdateWithVersion(transCtx, session, order); updateErr != nil {
return updateErr
}
return svcCtx.AgentService.ReverseAgentSettlementOnOrderRefund(transCtx, session, order.Id)
})
}
func buildXpayRefundOrderID(orderNo string) string {
const prefix = "RF_"
id := prefix + orderNo
if len(id) > 32 {
id = id[:32]
}
if len(id) < 8 {
id = id + "00000000"
id = id[:8]
}
return id
}
// TryRefundOnQueryFailure 查询失败时按支付渠道退款
func TryRefundOnQueryFailure(ctx context.Context, svcCtx *svc.ServiceContext, order *model.Order) {
var refundErr error
switch {
case model.IsXpayOrder(order):
refundErr = RefundXpayQueryOrder(ctx, svcCtx, order)
case order.PaymentPlatform == model.PaymentPlatformWechat:
refundErr = svcCtx.WechatPayService.WeChatRefund(ctx, order.OrderNo, order.Amount, order.Amount)
default:
refund, err := svcCtx.AlipayService.AliRefund(ctx, order.OrderNo, order.Amount)
if err != nil {
refundErr = err
} else if !refund.IsSuccess() {
refundErr = fmt.Errorf("支付宝退款失败: %s", refund.Msg)
} else {
transErr := svcCtx.OrderModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error {
order.Status = model.OrderStatusRefunded
if err := svcCtx.OrderModel.UpdateWithVersion(transCtx, session, order); err != nil {
return err
}
return svcCtx.AgentService.ReverseAgentSettlementOnOrderRefund(transCtx, session, order.Id)
})
if transErr != nil {
refundErr = transErr
}
}
}
if refundErr != nil {
logx.WithContext(ctx).Errorf("[refund] 查询失败自动退款未成功 order_no=%s platform=%s err=%v", order.OrderNo, order.PaymentPlatform, refundErr)
}
}

View File

@@ -1,99 +0,0 @@
package pay
import (
"context"
"net/http"
"strings"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type XpayAdminDeliverLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewXpayAdminDeliverLogic(ctx context.Context, svcCtx *svc.ServiceContext) *XpayAdminDeliverLogic {
return &XpayAdminDeliverLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
type XpayAdminDeliverResp struct {
Credited bool `json:"credited"`
Notified bool `json:"notified"`
WechatDetail string `json:"wechat_detail"`
Errors []string `json:"errors"`
}
func (l *XpayAdminDeliverLogic) XpayAdminDeliver(req *types.XpayAdminDeliverReq, r *http.Request) (*XpayAdminDeliverResp, error) {
auth := r.Header.Get("Authorization")
token := strings.TrimPrefix(auth, "Bearer ")
if token == "" || token != l.svcCtx.Config.WechatXpay.AdminToken {
return nil, errors.Wrapf(xerr.NewErrMsg("未授权"), "")
}
order, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrMsg("订单不存在"), "")
}
resp := &XpayAdminDeliverResp{WechatDetail: "ok"}
if order.Status == "paid" {
resp.Credited = false
return resp, nil
}
openid, err := l.svcCtx.XpayService.GetWxMiniOpenID(l.ctx, l.svcCtx.UserAuthModel, order.UserId)
if err != nil {
resp.Errors = append(resp.Errors, err.Error())
return resp, nil
}
sessionKey, err := l.svcCtx.XpayService.GetSessionKey(l.ctx, order.UserId)
if err != nil {
resp.Errors = append(resp.Errors, "session_key 不可用: "+err.Error())
return resp, nil
}
status, qErr := l.svcCtx.XpayService.QueryOrder(l.ctx, openid, order.OrderNo, sessionKey)
if qErr != nil {
resp.Errors = append(resp.Errors, qErr.Error())
resp.WechatDetail = qErr.Error()
return resp, nil
}
if !serviceIsPaid(status.Status) {
resp.Errors = append(resp.Errors, "微信侧订单未支付")
return resp, nil
}
credited, fulfillErr := fulfillQueryOrderPaid(l.ctx, l.svcCtx, order, "", status.PaidFee)
resp.Credited = credited
if fulfillErr != nil {
resp.Errors = append(resp.Errors, fulfillErr.Error())
}
if notifyErr := l.svcCtx.XpayService.NotifyProvideGoods(l.ctx, openid, order.OrderNo, "", sessionKey); notifyErr != nil {
resp.Notified = false
resp.WechatDetail = notifyErr.Error()
resp.Errors = append(resp.Errors, notifyErr.Error())
} else {
resp.Notified = true
_ = l.svcCtx.XpayService.MarkNotified(l.ctx, order.OrderNo)
}
return resp, nil
}
func serviceIsPaid(status int) bool {
return status == 2 || status == 3 || status == 4
}

View File

@@ -21,8 +21,14 @@ func fulfillQueryOrderPaid(ctx context.Context, svcCtx *svc.ServiceContext, orde
orderFen := lzUtils.ToWechatAmount(order.Amount)
if wechatPaidFen > 0 && wechatPaidFen != orderFen {
logx.WithContext(ctx).Errorf("[xpay] 金额不一致 order_no=%s order_fen=%d wechat_fen=%d", order.OrderNo, orderFen, wechatPaidFen)
return false, nil
if model.IsXpayOrder(order) {
order.Amount = lzUtils.RoundMoney(float64(wechatPaidFen) / 100)
orderFen = wechatPaidFen
logx.WithContext(ctx).Infof("[xpay] 同步微信实付金额 order_no=%s amount=%.2f fen=%d", order.OrderNo, order.Amount, wechatPaidFen)
} else {
logx.WithContext(ctx).Errorf("[xpay] 金额不一致 order_no=%s order_fen=%d wechat_fen=%d", order.OrderNo, orderFen, wechatPaidFen)
return false, nil
}
}
order.Status = "paid"
@@ -30,8 +36,8 @@ func fulfillQueryOrderPaid(ctx context.Context, svcCtx *svc.ServiceContext, orde
if platformOrderID != "" {
order.PlatformOrderId = sql.NullString{String: platformOrderID, Valid: true}
}
if order.PaymentScene == "" || order.PaymentScene == "app" {
order.PaymentScene = "wxmini"
if order.PaymentScene == "" || order.PaymentScene == model.PaymentSceneApp {
order.PaymentScene = model.PaymentSceneMiniProgram
}
if updateErr := svcCtx.OrderModel.UpdateWithVersion(ctx, nil, order); updateErr != nil {