This commit is contained in:
Mrx
2026-06-06 11:52:06 +08:00
parent a79c464329
commit a85436950e
16 changed files with 864 additions and 25 deletions

View File

@@ -3,10 +3,13 @@ package pay
import (
"context"
"strings"
"qnc-server/app/main/api/internal/service"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
@@ -26,26 +29,72 @@ func NewPaymentCheckLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Paym
}
func (l *PaymentCheckLogic) PaymentCheck(req *types.PaymentCheckReq) (resp *types.PaymentCheckResp, err error) {
// 根据订单号前缀判断订单类型
if strings.HasPrefix(req.OrderNo, "U_") {
// 升级订单
order, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级订单失败: %v", err)
order, findErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
if findErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级订单失败: %v", findErr)
}
return &types.PaymentCheckResp{
Type: "agent_upgrade",
Status: order.Status,
}, nil
return &types.PaymentCheckResp{Type: "agent_upgrade", Status: order.Status}, nil
}
// 查询订单(包括代理订单)
order, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单失败: %v", err)
order, findErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
if findErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单失败: %v", findErr)
}
return &types.PaymentCheckResp{
Type: "query",
Status: order.Status,
}, nil
// xpay 轮询pending 时主动查微信单
if order.Status == "pending" && order.PaymentScene == "wxmini" &&
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)
} else {
order, _ = l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
}
}
return &types.PaymentCheckResp{Type: "query", Status: order.Status}, nil
}
func (l *PaymentCheckLogic) syncXpayOrderStatus(order *model.Order) error {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return err
}
if order.UserId != userID {
return errors.New("无权查询此订单")
}
openid, err := l.svcCtx.XpayService.GetWxMiniOpenID(l.ctx, l.svcCtx.UserAuthModel, userID)
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)
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
}
if service.IsXpayClosedStatus(status.Status) {
order.Status = "closed"
return l.svcCtx.OrderModel.UpdateWithVersion(l.ctx, nil, order)
}
return nil
}

View File

@@ -33,6 +33,7 @@ type PaymentTypeResp struct {
outTradeNo string
description string
orderID string // 订单ID用于开发环境测试支付模式
productEn string // 产品英文名xpay 道具 ID
}
// enableDevTestPayment 测试支付功能开关,设置为 true 以启用测试支付功能
@@ -50,6 +51,7 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
var paymentTypeResp *PaymentTypeResp
var prepayData interface{}
var orderID string
useXpay := l.shouldUseXpay(req)
// 检查是否为开发环境的测试支付模式
env := os.Getenv("ENV")
@@ -95,9 +97,11 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
return nil
}
// 正常支付流程
// 正常支付流程(微信小程序 xpay 在事务外构造支付参数)
var createOrderErr error
if req.PayMethod == "wechat" {
if useXpay {
// 订单已在 QueryOrderPayment 中创建,此处跳过 JSAPI 预下单
} else if req.PayMethod == "wechat" {
prepayData, createOrderErr = l.svcCtx.WechatPayService.CreateWechatOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo, req.Code)
} else if req.PayMethod == "alipay" {
prepayData, createOrderErr = l.svcCtx.AlipayService.CreateAlipayOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo)
@@ -113,6 +117,22 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
return nil, err
}
// 微信小程序虚拟支付:构造 signData / paySig / signature
if useXpay && paymentTypeResp != nil {
userID, uidErr := ctxdata.GetUidFromCtx(l.ctx)
if uidErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败")
}
xpayParams, buildErr := l.svcCtx.XpayService.BuildPayParams(l.ctx, userID, paymentTypeResp.outTradeNo, paymentTypeResp.productEn, paymentTypeResp.amount)
if buildErr != nil {
if strings.Contains(buildErr.Error(), "过期") || strings.Contains(buildErr.Error(), "会话") {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, buildErr.Error()), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "构造虚拟支付参数失败: %v", buildErr)
}
prepayData = xpayParams
}
// 开发环境测试支付模式:事务提交后处理订单状态更新和后续流程
if isDevTestPayment && paymentTypeResp != nil && paymentTypeResp.orderID != "" {
// 使用 goroutine 异步处理,确保事务已完全提交
@@ -257,7 +277,7 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses
UserId: userID,
ProductId: product.Id,
PaymentPlatform: req.PayMethod,
PaymentScene: "app",
PaymentScene: resolvePaymentScene(l.ctx),
Amount: amount,
Status: "pending",
}
@@ -330,7 +350,44 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 保存代理订单失败: %+v", agentOrderInsert)
}
}
return &PaymentTypeResp{amount: amount, outTradeNo: outTradeNo, description: product.ProductName, orderID: orderID}, nil
return &PaymentTypeResp{
amount: amount,
outTradeNo: outTradeNo,
description: product.ProductName,
orderID: orderID,
productEn: product.ProductEn,
}, nil
}
func (l *PaymentLogic) shouldUseXpay(req *types.PaymentReq) bool {
if req.PayMethod != "wechat" || req.PayType != "query" {
return false
}
if l.svcCtx.XpayService == nil || !l.svcCtx.XpayService.Enabled() {
return false
}
platform, err := ctxdata.GetPlatformFromCtx(l.ctx)
if err != nil || platform != model.PlatformWxMini {
return false
}
return true
}
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"
}
}
// AgentVipOrderPayment 代理会员充值订单(已废弃,新系统使用升级功能替代)

View File

@@ -0,0 +1,99 @@
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

@@ -0,0 +1,48 @@
package pay
import (
"context"
"database/sql"
"time"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/model"
"qnc-server/pkg/lzkit/lzUtils"
"github.com/zeromicro/go-zero/core/logx"
)
// fulfillQueryOrderPaid 幂等将查询订单标记为已支付并触发报告生成
// wechatPaidFen微信侧实付金额>0 时须与订单 amount 一致
func fulfillQueryOrderPaid(ctx context.Context, svcCtx *svc.ServiceContext, order *model.Order, platformOrderID string, wechatPaidFen int64) (credited bool, err error) {
if order.Status == "paid" {
return false, nil
}
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
}
order.Status = "paid"
order.PayTime = sql.NullTime{Time: time.Now(), Valid: true}
if platformOrderID != "" {
order.PlatformOrderId = sql.NullString{String: platformOrderID, Valid: true}
}
if order.PaymentScene == "" || order.PaymentScene == "app" {
order.PaymentScene = "wxmini"
}
if updateErr := svcCtx.OrderModel.UpdateWithVersion(ctx, nil, order); updateErr != nil {
return false, updateErr
}
if asyncErr := svcCtx.AsynqService.SendQueryTask(order.Id); asyncErr != nil {
logx.WithContext(ctx).Errorf("[xpay] SendQueryTask 失败 order_id=%s err=%v", order.Id, asyncErr)
return true, asyncErr
}
logx.WithContext(ctx).Infof("[xpay] 订单到账 order_no=%s order_id=%s", order.OrderNo, order.Id)
return true, nil
}

View File

@@ -0,0 +1,97 @@
package pay
import (
"encoding/json"
"io"
"net/http"
"qnc-server/app/main/api/internal/service"
"qnc-server/app/main/api/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
)
type XpayPushLogic struct {
logx.Logger
svcCtx *svc.ServiceContext
}
func NewXpayPushLogic(svcCtx *svc.ServiceContext) *XpayPushLogic {
return &XpayPushLogic{svcCtx: svcCtx}
}
// HandleGET mp 后台配置消息推送时的验签
func (l *XpayPushLogic) HandleGET(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
signature := q.Get("signature")
timestamp := q.Get("timestamp")
nonce := q.Get("nonce")
echostr := q.Get("echostr")
if l.svcCtx.XpayService.VerifyPushSignature(signature, timestamp, nonce) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(echostr))
return
}
w.WriteHeader(http.StatusForbidden)
}
// HandlePOST 接收 xpay_goods_deliver_notify
func (l *XpayPushLogic) HandlePOST(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
l.writePushResp(w, 1, "read body failed")
return
}
var notify service.XpayDeliverNotify
if err := json.Unmarshal(body, &notify); err != nil {
l.writePushResp(w, 1, "parse failed")
return
}
if notify.Event != "" && notify.Event != "xpay_goods_deliver_notify" {
l.writePushResp(w, 0, "ignored")
return
}
orderNo := notify.OutTradeNo
if orderNo == "" {
l.writePushResp(w, 1, "missing OutTradeNo")
return
}
ctx := r.Context()
already, _ := l.svcCtx.XpayService.AlreadyNotified(ctx, orderNo)
if already {
l.writePushResp(w, 0, "already notified")
return
}
order, findErr := l.svcCtx.OrderModel.FindOneByOrderNo(ctx, orderNo)
if findErr != nil {
logx.WithContext(ctx).Errorf("[xpay push] 订单不存在 order_no=%s err=%v", orderNo, findErr)
l.writePushResp(w, 1, "order not found")
return
}
wxOrderID := notify.WeChatPayInfo.TransactionId
credited, fulfillErr := fulfillQueryOrderPaid(ctx, l.svcCtx, order, wxOrderID, notify.GoodsInfo.ActualPrice)
if fulfillErr != nil {
logx.WithContext(ctx).Errorf("[xpay push] 到账失败 order_no=%s err=%v", orderNo, fulfillErr)
l.writePushResp(w, 1, fulfillErr.Error())
return
}
_ = l.svcCtx.XpayService.MarkNotified(ctx, orderNo)
logx.WithContext(ctx).Infof("[xpay push] event=xpay_goods_deliver_notify order_no=%s credited=%v", orderNo, credited)
l.writePushResp(w, 0, "success")
}
func (l *XpayPushLogic) writePushResp(w http.ResponseWriter, errCode int, errMsg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"ErrCode": errCode,
"ErrMsg": errMsg,
})
}