f
This commit is contained in:
@@ -22,6 +22,15 @@ service main {
|
|||||||
// 微信退款回调
|
// 微信退款回调
|
||||||
@handler WechatPayRefundCallback
|
@handler WechatPayRefundCallback
|
||||||
post /pay/wechat/refund_callback
|
post /pay/wechat/refund_callback
|
||||||
|
|
||||||
|
// 微信小程序虚拟支付发货推送(GET 验签 + POST 事件)
|
||||||
|
@handler XpayPush
|
||||||
|
get /pay/xpay/push
|
||||||
|
post /pay/xpay/push
|
||||||
|
|
||||||
|
// 运维:xpay 手动补发货
|
||||||
|
@handler XpayAdminDeliver
|
||||||
|
post /pay/xpay/admin/deliver (XpayAdminDeliverReq) returns (XpayAdminDeliverResp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@server (
|
@server (
|
||||||
@@ -70,3 +79,15 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
XpayAdminDeliverReq {
|
||||||
|
OrderNo string `json:"order_no" validate:"required"`
|
||||||
|
}
|
||||||
|
XpayAdminDeliverResp {
|
||||||
|
Credited bool `json:"credited"`
|
||||||
|
Notified bool `json:"notified"`
|
||||||
|
WechatDetail string `json:"wechat_detail"`
|
||||||
|
Errors []string `json:"errors"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,15 @@ WechatH5:
|
|||||||
WechatMini:
|
WechatMini:
|
||||||
AppID: "wx5bacc94add2da981" # 小程序的AppID
|
AppID: "wx5bacc94add2da981" # 小程序的AppID
|
||||||
AppSecret: "48a2c1e8ff1b7d4c0ff82fbefa64d2d0" # 小程序的AppSecret
|
AppSecret: "48a2c1e8ff1b7d4c0ff82fbefa64d2d0" # 小程序的AppSecret
|
||||||
|
WechatXpay:
|
||||||
|
Enabled: true
|
||||||
|
Env: 1
|
||||||
|
OfferId: "1450552691"
|
||||||
|
AppKey: "n1SSzeMiitPjiQwLlBn1s4Hn7SpkG3qD"
|
||||||
|
ProdAppKey: "oYv2wooYzRmPDLdXkpBpqaml8cFaY0Bb"
|
||||||
|
MessagePushToken: "qncXpayPush2026"
|
||||||
|
AdminToken: "qncXpayAdmin2026"
|
||||||
|
SessionKeyTTL: 2592000
|
||||||
Query:
|
Query:
|
||||||
ShareLinkExpire: 604800 # 7天 = 7 * 24 * 60 * 60 = 604800秒
|
ShareLinkExpire: 604800 # 7天 = 7 * 24 * 60 * 60 = 604800秒
|
||||||
AdminConfig:
|
AdminConfig:
|
||||||
|
|||||||
@@ -64,6 +64,15 @@ WechatH5:
|
|||||||
WechatMini:
|
WechatMini:
|
||||||
AppID: "wx5bacc94add2da981" # 小程序的AppID
|
AppID: "wx5bacc94add2da981" # 小程序的AppID
|
||||||
AppSecret: "48a2c1e8ff1b7d4c0ff82fbefa64d2d0" # 小程序的AppSecret
|
AppSecret: "48a2c1e8ff1b7d4c0ff82fbefa64d2d0" # 小程序的AppSecret
|
||||||
|
WechatXpay:
|
||||||
|
Enabled: true
|
||||||
|
Env: 1 # 0 现网 / 1 沙箱(当前:沙箱联调)
|
||||||
|
OfferId: "1450552691"
|
||||||
|
AppKey: "n1SSzeMiitPjiQwLlBn1s4Hn7SpkG3qD" # 沙箱 AppKey
|
||||||
|
ProdAppKey: "oYv2wooYzRmPDLdXkpBpqaml8cFaY0Bb" # 现网 AppKey(上线时 Env 改 0 并切换 AppKey)
|
||||||
|
MessagePushToken: "qncXpayPush2026" # 与 mp 后台消息推送 Token 保持一致
|
||||||
|
AdminToken: "qncXpayAdmin2026" # 运维手动补发货接口 Bearer Token
|
||||||
|
SessionKeyTTL: 2592000 # 30 天,与 JwtAuth.AccessExpire 对齐
|
||||||
Query:
|
Query:
|
||||||
ShareLinkExpire: 604800 # 7天 = 7 * 24 * 60 * 60 = 604800秒
|
ShareLinkExpire: 604800 # 7天 = 7 * 24 * 60 * 60 = 604800秒
|
||||||
AdminConfig:
|
AdminConfig:
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type Config struct {
|
|||||||
WechatH5 WechatH5Config
|
WechatH5 WechatH5Config
|
||||||
Authorization AuthorizationConfig // 授权书配置
|
Authorization AuthorizationConfig // 授权书配置
|
||||||
WechatMini WechatMiniConfig
|
WechatMini WechatMiniConfig
|
||||||
|
WechatXpay WechatXpayConfig
|
||||||
Query QueryConfig
|
Query QueryConfig
|
||||||
AdminConfig AdminConfig
|
AdminConfig AdminConfig
|
||||||
TaxConfig TaxConfig
|
TaxConfig TaxConfig
|
||||||
@@ -98,6 +99,18 @@ type WechatMiniConfig struct {
|
|||||||
AppID string
|
AppID string
|
||||||
AppSecret string
|
AppSecret string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WechatXpayConfig 微信小程序虚拟支付(xpay)
|
||||||
|
type WechatXpayConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
Env int // 0 现网 / 1 沙箱
|
||||||
|
OfferId string
|
||||||
|
AppKey string // 与 Env 配套的 AppKey
|
||||||
|
ProdAppKey string // 现网 AppKey(切换 env=0 时使用,可选)
|
||||||
|
MessagePushToken string
|
||||||
|
AdminToken string
|
||||||
|
SessionKeyTTL int64
|
||||||
|
}
|
||||||
type QueryConfig struct {
|
type QueryConfig struct {
|
||||||
ShareLinkExpire int64
|
ShareLinkExpire int64
|
||||||
}
|
}
|
||||||
|
|||||||
30
app/main/api/internal/handler/pay/xpayadmindeliverhandler.go
Normal file
30
app/main/api/internal/handler/pay/xpayadmindeliverhandler.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package pay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"qnc-server/app/main/api/internal/logic/pay"
|
||||||
|
"qnc-server/app/main/api/internal/svc"
|
||||||
|
"qnc-server/app/main/api/internal/types"
|
||||||
|
"qnc-server/common/result"
|
||||||
|
"qnc-server/pkg/lzkit/validator"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func XpayAdminDeliverHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.XpayAdminDeliverReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
result.ParamErrorResult(r, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validator.Validate(req); err != nil {
|
||||||
|
result.ParamValidateErrorResult(r, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l := pay.NewXpayAdminDeliverLogic(r.Context(), svcCtx)
|
||||||
|
resp, err := l.XpayAdminDeliver(&req, r)
|
||||||
|
result.HttpResult(r, w, resp, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/main/api/internal/handler/pay/xpaypushhandler.go
Normal file
22
app/main/api/internal/handler/pay/xpaypushhandler.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package pay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"qnc-server/app/main/api/internal/logic/pay"
|
||||||
|
"qnc-server/app/main/api/internal/svc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func XpayPushHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
l := pay.NewXpayPushLogic(svcCtx)
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
l.HandleGET(w, r)
|
||||||
|
case http.MethodPost:
|
||||||
|
l.HandlePOST(w, r)
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -937,6 +937,21 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||||||
Path: "/pay/wechat/refund_callback",
|
Path: "/pay/wechat/refund_callback",
|
||||||
Handler: pay.WechatPayRefundCallbackHandler(serverCtx),
|
Handler: pay.WechatPayRefundCallbackHandler(serverCtx),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/pay/xpay/push",
|
||||||
|
Handler: pay.XpayPushHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/pay/xpay/push",
|
||||||
|
Handler: pay.XpayPushHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/pay/xpay/admin/deliver",
|
||||||
|
Handler: pay.XpayAdminDeliverHandler(serverCtx),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
rest.WithPrefix("/api/v1"),
|
rest.WithPrefix("/api/v1"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ package pay
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"qnc-server/app/main/api/internal/service"
|
||||||
"qnc-server/app/main/api/internal/svc"
|
"qnc-server/app/main/api/internal/svc"
|
||||||
"qnc-server/app/main/api/internal/types"
|
"qnc-server/app/main/api/internal/types"
|
||||||
|
"qnc-server/app/main/model"
|
||||||
|
"qnc-server/common/ctxdata"
|
||||||
"qnc-server/common/xerr"
|
"qnc-server/common/xerr"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/zeromicro/go-zero/core/logx"
|
"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) {
|
func (l *PaymentCheckLogic) PaymentCheck(req *types.PaymentCheckReq) (resp *types.PaymentCheckResp, err error) {
|
||||||
// 根据订单号前缀判断订单类型
|
|
||||||
if strings.HasPrefix(req.OrderNo, "U_") {
|
if strings.HasPrefix(req.OrderNo, "U_") {
|
||||||
// 升级订单
|
order, findErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
|
||||||
order, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
|
if findErr != nil {
|
||||||
if err != nil {
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级订单失败: %v", findErr)
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级订单失败: %v", err)
|
|
||||||
}
|
}
|
||||||
return &types.PaymentCheckResp{
|
return &types.PaymentCheckResp{Type: "agent_upgrade", Status: order.Status}, nil
|
||||||
Type: "agent_upgrade",
|
|
||||||
Status: order.Status,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询订单(包括代理订单)
|
order, findErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
|
||||||
order, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
|
if findErr != nil {
|
||||||
if err != nil {
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单失败: %v", findErr)
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单失败: %v", err)
|
|
||||||
}
|
}
|
||||||
return &types.PaymentCheckResp{
|
|
||||||
Type: "query",
|
// xpay 轮询:pending 时主动查微信单
|
||||||
Status: order.Status,
|
if order.Status == "pending" && order.PaymentScene == "wxmini" &&
|
||||||
}, nil
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ type PaymentTypeResp struct {
|
|||||||
outTradeNo string
|
outTradeNo string
|
||||||
description string
|
description string
|
||||||
orderID string // 订单ID,用于开发环境测试支付模式
|
orderID string // 订单ID,用于开发环境测试支付模式
|
||||||
|
productEn string // 产品英文名,xpay 道具 ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// enableDevTestPayment 测试支付功能开关,设置为 true 以启用测试支付功能
|
// enableDevTestPayment 测试支付功能开关,设置为 true 以启用测试支付功能
|
||||||
@@ -50,6 +51,7 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
|
|||||||
var paymentTypeResp *PaymentTypeResp
|
var paymentTypeResp *PaymentTypeResp
|
||||||
var prepayData interface{}
|
var prepayData interface{}
|
||||||
var orderID string
|
var orderID string
|
||||||
|
useXpay := l.shouldUseXpay(req)
|
||||||
|
|
||||||
// 检查是否为开发环境的测试支付模式
|
// 检查是否为开发环境的测试支付模式
|
||||||
env := os.Getenv("ENV")
|
env := os.Getenv("ENV")
|
||||||
@@ -95,9 +97,11 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 正常支付流程
|
// 正常支付流程(微信小程序 xpay 在事务外构造支付参数)
|
||||||
var createOrderErr error
|
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)
|
prepayData, createOrderErr = l.svcCtx.WechatPayService.CreateWechatOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo, req.Code)
|
||||||
} else if req.PayMethod == "alipay" {
|
} else if req.PayMethod == "alipay" {
|
||||||
prepayData, createOrderErr = l.svcCtx.AlipayService.CreateAlipayOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo)
|
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
|
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 != "" {
|
if isDevTestPayment && paymentTypeResp != nil && paymentTypeResp.orderID != "" {
|
||||||
// 使用 goroutine 异步处理,确保事务已完全提交
|
// 使用 goroutine 异步处理,确保事务已完全提交
|
||||||
@@ -257,7 +277,7 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses
|
|||||||
UserId: userID,
|
UserId: userID,
|
||||||
ProductId: product.Id,
|
ProductId: product.Id,
|
||||||
PaymentPlatform: req.PayMethod,
|
PaymentPlatform: req.PayMethod,
|
||||||
PaymentScene: "app",
|
PaymentScene: resolvePaymentScene(l.ctx),
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
Status: "pending",
|
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 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 代理会员充值订单(已废弃,新系统使用升级功能替代)
|
// AgentVipOrderPayment 代理会员充值订单(已废弃,新系统使用升级功能替代)
|
||||||
|
|||||||
99
app/main/api/internal/logic/pay/xpayadmindeliverlogic.go
Normal file
99
app/main/api/internal/logic/pay/xpayadmindeliverlogic.go
Normal 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
|
||||||
|
}
|
||||||
48
app/main/api/internal/logic/pay/xpayorderfulfilllogic.go
Normal file
48
app/main/api/internal/logic/pay/xpayorderfulfilllogic.go
Normal 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
|
||||||
|
}
|
||||||
97
app/main/api/internal/logic/pay/xpaypushlogic.go
Normal file
97
app/main/api/internal/logic/pay/xpaypushlogic.go
Normal 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, ¬ify); 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -65,13 +65,20 @@ func (l *WxMiniAuthLogic) WxMiniAuth(req *types.WXMiniAuthReq) (resp *types.WXMi
|
|||||||
userID = user.Id
|
userID = user.Id
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 生成JWT Token(动态计算userType)
|
// 4. 缓存 session_key(xpay 虚拟支付签名需要)
|
||||||
|
if l.svcCtx.XpayService != nil && l.svcCtx.XpayService.Enabled() {
|
||||||
|
if saveErr := l.svcCtx.XpayService.SaveSessionKey(l.ctx, userID, sessionKeyResp.SessionKey); saveErr != nil {
|
||||||
|
l.Errorf("缓存 xpay session_key 失败 userID=%s err=%v", userID, saveErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 生成JWT Token(动态计算userType)
|
||||||
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
|
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成JWT Token失败: %v", err)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成JWT Token失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 返回登录结果
|
// 6. 返回登录结果
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
return &types.WXMiniAuthResp{
|
return &types.WXMiniAuthResp{
|
||||||
AccessToken: token,
|
AccessToken: token,
|
||||||
|
|||||||
349
app/main/api/internal/service/xpayService.go
Normal file
349
app/main/api/internal/service/xpayService.go
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"qnc-server/app/main/api/internal/config"
|
||||||
|
"qnc-server/app/main/model"
|
||||||
|
"qnc-server/pkg/lzkit/lzUtils"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
xpaySessionKeyFmt = "qnc:wx:xpay:session:%s"
|
||||||
|
xpayNotifiedKeyFmt = "qnc:xpay:notified:%s"
|
||||||
|
xpayAccessTokenKey = "qnc:wx:xpay:access_token"
|
||||||
|
xpayMode = "short_series_goods"
|
||||||
|
xpayVirtualPaymentURI = "requestVirtualPayment"
|
||||||
|
)
|
||||||
|
|
||||||
|
// XpayPrepayData 前端 wx.requestVirtualPayment 参数
|
||||||
|
type XpayPrepayData struct {
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
SignData string `json:"signData"`
|
||||||
|
PaySig string `json:"paySig"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type XpayService struct {
|
||||||
|
config config.Config
|
||||||
|
redis *redis.Redis
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewXpayService(c config.Config, r *redis.Redis) *XpayService {
|
||||||
|
return &XpayService{config: c, redis: r}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *XpayService) Enabled() bool {
|
||||||
|
return s.config.WechatXpay.Enabled && s.config.WechatXpay.OfferId != "" && s.config.WechatXpay.AppKey != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *XpayService) Env() int {
|
||||||
|
return s.config.WechatXpay.Env
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *XpayService) SessionKeyRedisKey(userID string) string {
|
||||||
|
return fmt.Sprintf(xpaySessionKeyFmt, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *XpayService) SaveSessionKey(ctx context.Context, userID, sessionKey string) error {
|
||||||
|
ttl := int(s.config.WechatXpay.SessionKeyTTL)
|
||||||
|
if ttl <= 0 {
|
||||||
|
ttl = int(s.config.JwtAuth.AccessExpire)
|
||||||
|
}
|
||||||
|
return s.redis.SetexCtx(ctx, s.SessionKeyRedisKey(userID), sessionKey, ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *XpayService) GetSessionKey(ctx context.Context, userID string) (string, error) {
|
||||||
|
val, err := s.redis.GetCtx(ctx, s.SessionKeyRedisKey(userID))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if val == "" {
|
||||||
|
return "", redis.Nil
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hmacSHA256Hex(key, data string) string {
|
||||||
|
mac := hmac.New(sha256.New, []byte(key))
|
||||||
|
mac.Write([]byte(data))
|
||||||
|
return hex.EncodeToString(mac.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func compactJSON(v interface{}) (string, error) {
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type signDataPayload struct {
|
||||||
|
OfferId string `json:"offerId"`
|
||||||
|
BuyQuantity int `json:"buyQuantity"`
|
||||||
|
Env int `json:"env"`
|
||||||
|
CurrencyType string `json:"currencyType"`
|
||||||
|
ProductId string `json:"productId"`
|
||||||
|
GoodsPrice int64 `json:"goodsPrice"`
|
||||||
|
OutTradeNo string `json:"outTradeNo"`
|
||||||
|
Attach string `json:"attach"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildPayParams 构造虚拟支付双签参数
|
||||||
|
func (s *XpayService) BuildPayParams(ctx context.Context, userID, orderNo, productEn string, amount float64) (*XpayPrepayData, error) {
|
||||||
|
sessionKey, err := s.GetSessionKey(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
if err == redis.Nil {
|
||||||
|
return nil, fmt.Errorf("微信会话已过期,请重新打开小程序")
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("获取微信会话失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
goodsPrice := lzUtils.ToWechatAmount(amount)
|
||||||
|
payload := signDataPayload{
|
||||||
|
OfferId: s.config.WechatXpay.OfferId,
|
||||||
|
BuyQuantity: 1,
|
||||||
|
Env: s.Env(),
|
||||||
|
CurrencyType: "CNY",
|
||||||
|
ProductId: productEn,
|
||||||
|
GoodsPrice: goodsPrice,
|
||||||
|
OutTradeNo: orderNo,
|
||||||
|
Attach: fmt.Sprintf("query:%s", orderNo),
|
||||||
|
}
|
||||||
|
signData, err := compactJSON(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
appKey := s.config.WechatXpay.AppKey
|
||||||
|
paySig := hmacSHA256Hex(appKey, xpayVirtualPaymentURI+"&"+signData)
|
||||||
|
signature := hmacSHA256Hex(sessionKey, signData)
|
||||||
|
|
||||||
|
logx.WithContext(ctx).Infof("[xpay] create user=%s order_no=%s env=%d productId=%s goodsPrice=%d",
|
||||||
|
userID, orderNo, s.Env(), productEn, goodsPrice)
|
||||||
|
|
||||||
|
return &XpayPrepayData{
|
||||||
|
Provider: "xpay",
|
||||||
|
Mode: xpayMode,
|
||||||
|
SignData: signData,
|
||||||
|
PaySig: paySig,
|
||||||
|
Signature: signature,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *XpayService) getAccessToken(ctx context.Context) (string, error) {
|
||||||
|
cached, err := s.redis.GetCtx(ctx, xpayAccessTokenKey)
|
||||||
|
if err == nil && cached != "" {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
appID := s.config.WechatMini.AppID
|
||||||
|
secret := s.config.WechatMini.AppSecret
|
||||||
|
url := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", appID, secret)
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
var tokenResp struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
ErrCode int `json:"errcode"`
|
||||||
|
ErrMsg string `json:"errmsg"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if tokenResp.ErrCode != 0 || tokenResp.AccessToken == "" {
|
||||||
|
return "", fmt.Errorf("获取 access_token 失败: errcode=%d errmsg=%s", tokenResp.ErrCode, tokenResp.ErrMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
ttl := tokenResp.ExpiresIn - 300
|
||||||
|
if ttl < 60 {
|
||||||
|
ttl = 60
|
||||||
|
}
|
||||||
|
_ = s.redis.SetexCtx(ctx, xpayAccessTokenKey, tokenResp.AccessToken, ttl)
|
||||||
|
return tokenResp.AccessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type xpayAPIResp struct {
|
||||||
|
ErrCode int `json:"errcode"`
|
||||||
|
ErrMsg string `json:"errmsg"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type xpayOrderStatus struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
PaidFee int64 `json:"paid_fee"`
|
||||||
|
OrderID string `json:"order_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *XpayService) callAPI(ctx context.Context, uri, bodyJSON, sessionKey string, needSignature bool) (*xpayAPIResp, error) {
|
||||||
|
accessToken, err := s.getAccessToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
appKey := s.config.WechatXpay.AppKey
|
||||||
|
paySig := hmacSHA256Hex(appKey, uri+"&"+bodyJSON)
|
||||||
|
|
||||||
|
reqURL := fmt.Sprintf("https://api.weixin.qq.com%s?access_token=%s&pay_sig=%s", uri, accessToken, paySig)
|
||||||
|
if needSignature && sessionKey != "" {
|
||||||
|
sig := hmacSHA256Hex(sessionKey, bodyJSON)
|
||||||
|
reqURL += "&signature=" + sig
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, bytes.NewReader([]byte(bodyJSON)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
httpResp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer httpResp.Body.Close()
|
||||||
|
respBody, _ := io.ReadAll(httpResp.Body)
|
||||||
|
|
||||||
|
var apiResp xpayAPIResp
|
||||||
|
if err := json.Unmarshal(respBody, &apiResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析 xpay 响应失败: %w, body=%s", err, string(respBody))
|
||||||
|
}
|
||||||
|
logx.WithContext(ctx).Infof("[xpay] call uri=%s body=%s", uri, string(respBody))
|
||||||
|
return &apiResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryOrder 调微信 query_order
|
||||||
|
func (s *XpayService) QueryOrder(ctx context.Context, openid, orderNo, sessionKey string) (*xpayOrderStatus, error) {
|
||||||
|
bodyObj := map[string]interface{}{
|
||||||
|
"openid": openid,
|
||||||
|
"env": s.Env(),
|
||||||
|
"order_id": orderNo,
|
||||||
|
}
|
||||||
|
bodyJSON, err := compactJSON(bodyObj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
apiResp, err := s.callAPI(ctx, "/xpay/query_order", bodyJSON, sessionKey, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if apiResp.ErrCode != 0 {
|
||||||
|
return nil, fmt.Errorf("query_order errcode=%d errmsg=%s", apiResp.ErrCode, apiResp.ErrMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
var status xpayOrderStatus
|
||||||
|
if len(apiResp.Data) > 0 {
|
||||||
|
_ = json.Unmarshal(apiResp.Data, &status)
|
||||||
|
}
|
||||||
|
return &status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyProvideGoods 主动通知微信已发货
|
||||||
|
func (s *XpayService) NotifyProvideGoods(ctx context.Context, openid, orderNo, wxOrderID, sessionKey string) error {
|
||||||
|
bodyObj := map[string]interface{}{
|
||||||
|
"openid": openid,
|
||||||
|
"env": s.Env(),
|
||||||
|
"order_id": orderNo,
|
||||||
|
}
|
||||||
|
if wxOrderID != "" {
|
||||||
|
bodyObj["wx_order_id"] = wxOrderID
|
||||||
|
}
|
||||||
|
bodyJSON, err := compactJSON(bodyObj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
apiResp, err := s.callAPI(ctx, "/xpay/notify_provide_goods", bodyJSON, sessionKey, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if apiResp.ErrCode != 0 && apiResp.ErrCode != 268490004 {
|
||||||
|
return fmt.Errorf("notify_provide_goods errcode=%d errmsg=%s", apiResp.ErrCode, apiResp.ErrMsg)
|
||||||
|
}
|
||||||
|
logx.WithContext(ctx).Infof("[xpay] notify_provide_goods OK order_no=%s", orderNo)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPaidStatus 微信订单状态 2/3/4 视为支付成功
|
||||||
|
func IsXpayPaidStatus(status int) bool {
|
||||||
|
return status == 2 || status == 3 || status == 4
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsXpayClosedStatus 5~10 视为关闭
|
||||||
|
func IsXpayClosedStatus(status int) bool {
|
||||||
|
return status >= 5 && status <= 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyPushSignature GET 验签(消息推送配置)
|
||||||
|
func (s *XpayService) VerifyPushSignature(signature, timestamp, nonce string) bool {
|
||||||
|
token := s.config.WechatXpay.MessagePushToken
|
||||||
|
arr := []string{token, timestamp, nonce}
|
||||||
|
sort.Strings(arr)
|
||||||
|
hash := sha1.Sum([]byte(strings.Join(arr, "")))
|
||||||
|
return signature == hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// XpayDeliverNotify 推送消息体(明文 JSON)
|
||||||
|
type XpayDeliverNotify struct {
|
||||||
|
Event string `json:"Event"`
|
||||||
|
OpenId string `json:"OpenId"`
|
||||||
|
OutTradeNo string `json:"OutTradeNo"`
|
||||||
|
Env int `json:"Env"`
|
||||||
|
WeChatPayInfo struct {
|
||||||
|
TransactionId string `json:"TransactionId"`
|
||||||
|
PaidTime int64 `json:"PaidTime"`
|
||||||
|
} `json:"WeChatPayInfo"`
|
||||||
|
GoodsInfo struct {
|
||||||
|
ProductId string `json:"ProductId"`
|
||||||
|
Quantity int `json:"Quantity"`
|
||||||
|
OrigPrice int64 `json:"OrigPrice"`
|
||||||
|
ActualPrice int64 `json:"ActualPrice"`
|
||||||
|
Attach string `json:"Attach"`
|
||||||
|
} `json:"GoodsInfo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *XpayService) AlreadyNotified(ctx context.Context, orderNo string) (bool, error) {
|
||||||
|
key := fmt.Sprintf(xpayNotifiedKeyFmt, orderNo)
|
||||||
|
val, err := s.redis.GetCtx(ctx, key)
|
||||||
|
if err == redis.Nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return val != "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *XpayService) MarkNotified(ctx context.Context, orderNo string) error {
|
||||||
|
key := fmt.Sprintf(xpayNotifiedKeyFmt, orderNo)
|
||||||
|
return s.redis.SetexCtx(ctx, key, "1", 7*24*3600)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWxMiniOpenID 查询用户小程序 openid
|
||||||
|
func (s *XpayService) GetWxMiniOpenID(ctx context.Context, userAuthModel model.UserAuthModel, userID string) (string, error) {
|
||||||
|
auth, err := userAuthModel.FindOneByUserIdAuthType(ctx, userID, model.UserAuthTypeWxMiniOpenID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return auth.AuthKey, nil
|
||||||
|
}
|
||||||
@@ -90,6 +90,7 @@ type ServiceContext struct {
|
|||||||
// 服务
|
// 服务
|
||||||
AlipayService *service.AliPayService
|
AlipayService *service.AliPayService
|
||||||
WechatPayService *service.WechatPayService
|
WechatPayService *service.WechatPayService
|
||||||
|
XpayService *service.XpayService
|
||||||
ApplePayService *service.ApplePayService
|
ApplePayService *service.ApplePayService
|
||||||
ApiRequestService *service.ApiRequestService
|
ApiRequestService *service.ApiRequestService
|
||||||
WhitelistService *service.WhitelistService
|
WhitelistService *service.WhitelistService
|
||||||
@@ -190,6 +191,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
|||||||
// ============================== 业务服务初始化 ==============================
|
// ============================== 业务服务初始化 ==============================
|
||||||
alipayService := service.NewAliPayService(c)
|
alipayService := service.NewAliPayService(c)
|
||||||
wechatPayService := service.NewWechatPayService(c, userAuthModel, service.InitTypeWxPayPubKey)
|
wechatPayService := service.NewWechatPayService(c, userAuthModel, service.InitTypeWxPayPubKey)
|
||||||
|
xpayService := service.NewXpayService(c, redisClient)
|
||||||
applePayService := service.NewApplePayService(c)
|
applePayService := service.NewApplePayService(c)
|
||||||
tianyuanapiCallLogService := service.NewTianyuanapiCallLogService(tianyuanapiCallLogModel, featureModel)
|
tianyuanapiCallLogService := service.NewTianyuanapiCallLogService(tianyuanapiCallLogModel, featureModel)
|
||||||
whitelistService := service.NewWhitelistService(c, userFeatureWhitelistModel, whitelistOrderModel, whitelistOrderItemModel, queryModel, featureModel)
|
whitelistService := service.NewWhitelistService(c, userFeatureWhitelistModel, whitelistOrderModel, whitelistOrderItemModel, queryModel, featureModel)
|
||||||
@@ -307,6 +309,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
|||||||
// 服务
|
// 服务
|
||||||
AlipayService: alipayService,
|
AlipayService: alipayService,
|
||||||
WechatPayService: wechatPayService,
|
WechatPayService: wechatPayService,
|
||||||
|
XpayService: xpayService,
|
||||||
ApplePayService: applePayService,
|
ApplePayService: applePayService,
|
||||||
ApiRequestService: apiRequestService,
|
ApiRequestService: apiRequestService,
|
||||||
WhitelistService: whitelistService,
|
WhitelistService: whitelistService,
|
||||||
|
|||||||
@@ -1933,6 +1933,17 @@ type PaymentResp struct {
|
|||||||
OrderNo string `json:"order_no"`
|
OrderNo string `json:"order_no"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type XpayAdminDeliverReq struct {
|
||||||
|
OrderNo string `json:"order_no" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type XpayAdminDeliverResp struct {
|
||||||
|
Credited bool `json:"credited"`
|
||||||
|
Notified bool `json:"notified"`
|
||||||
|
WechatDetail string `json:"wechat_detail"`
|
||||||
|
Errors []string `json:"errors"`
|
||||||
|
}
|
||||||
|
|
||||||
type PeriodConversionData struct {
|
type PeriodConversionData struct {
|
||||||
PeriodLabel string `json:"period_label"` // 时间段标签(如:今日、昨日、前日)
|
PeriodLabel string `json:"period_label"` // 时间段标签(如:今日、昨日、前日)
|
||||||
QueryUsers int64 `json:"query_users"` // 查询订单数(订单数量,不是用户数)
|
QueryUsers int64 `json:"query_users"` // 查询订单数(订单数量,不是用户数)
|
||||||
|
|||||||
Reference in New Issue
Block a user