f
This commit is contained in:
@@ -39,6 +39,10 @@ service main {
|
|||||||
@doc "重新执行代理处理"
|
@doc "重新执行代理处理"
|
||||||
@handler AdminRetryAgentProcess
|
@handler AdminRetryAgentProcess
|
||||||
post /retry-agent-process/:id (AdminRetryAgentProcessReq) returns (AdminRetryAgentProcessResp)
|
post /retry-agent-process/:id (AdminRetryAgentProcessReq) returns (AdminRetryAgentProcessResp)
|
||||||
|
|
||||||
|
@doc "xpay补发货(查微信单并到账)"
|
||||||
|
@handler AdminXpayDeliver
|
||||||
|
post /xpay-deliver/:id (AdminXpayDeliverReq) returns (AdminXpayDeliverResp)
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -169,4 +173,15 @@ type (
|
|||||||
Message string `json:"message"` // 执行结果消息
|
Message string `json:"message"` // 执行结果消息
|
||||||
ProcessedAt string `json:"processed_at"` // 处理时间
|
ProcessedAt string `json:"processed_at"` // 处理时间
|
||||||
}
|
}
|
||||||
|
// xpay 补发货
|
||||||
|
AdminXpayDeliverReq {
|
||||||
|
Id string `path:"id"` // 订单ID
|
||||||
|
}
|
||||||
|
AdminXpayDeliverResp {
|
||||||
|
Credited bool `json:"credited"`
|
||||||
|
Notified bool `json:"notified"`
|
||||||
|
WechatDetail string `json:"wechat_detail"`
|
||||||
|
Errors []string `json:"errors"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,10 +27,6 @@ service main {
|
|||||||
@handler XpayPush
|
@handler XpayPush
|
||||||
get /pay/xpay/push
|
get /pay/xpay/push
|
||||||
post /pay/xpay/push
|
post /pay/xpay/push
|
||||||
|
|
||||||
// 运维:xpay 手动补发货
|
|
||||||
@handler XpayAdminDeliver
|
|
||||||
post /pay/xpay/admin/deliver (XpayAdminDeliverReq) returns (XpayAdminDeliverResp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@server (
|
@server (
|
||||||
@@ -79,15 +75,3 @@ 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"`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|||||||
@@ -77,13 +77,18 @@ WechatMini:
|
|||||||
AppSecret: "48a2c1e8ff1b7d4c0ff82fbefa64d2d0" # 小程序的AppSecret
|
AppSecret: "48a2c1e8ff1b7d4c0ff82fbefa64d2d0" # 小程序的AppSecret
|
||||||
WechatXpay:
|
WechatXpay:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
Env: 1
|
Env: 1 # 本地联调用沙箱;现网单请改 0 + 现网 AppKey
|
||||||
OfferId: "1450552691"
|
OfferId: "1450552691"
|
||||||
AppKey: "n1SSzeMiitPjiQwLlBn1s4Hn7SpkG3qD"
|
AppKey: "n1SSzeMiitPjiQwLlBn1s4Hn7SpkG3qD" # 沙箱 AppKey(仅 env=1)
|
||||||
ProdAppKey: "oYv2wooYzRmPDLdXkpBpqaml8cFaY0Bb"
|
MessagePushToken: "qncXpayPush2026" # 与 mp 后台消息推送 Token 保持一致
|
||||||
MessagePushToken: "qncXpayPush2026"
|
SessionKeyTTL: 2592000 # 30 天,与 JwtAuth.AccessExpire 对齐
|
||||||
AdminToken: "qncXpayAdmin2026"
|
|
||||||
SessionKeyTTL: 2592000
|
# Enabled: true
|
||||||
|
# Env: 0 # 0 现网 / 1 沙箱(须与 AppKey 配套)
|
||||||
|
# OfferId: "1450552691"
|
||||||
|
# AppKey: "oYv2wooYzRmPDLdXkpBpqaml8cFaY0Bb" # 现网 AppKey(env=0 必填现网密钥)
|
||||||
|
# MessagePushToken: "qncXpayPush2026" # 与 mp 后台消息推送 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:
|
||||||
|
|||||||
@@ -66,13 +66,12 @@ WechatMini:
|
|||||||
AppSecret: "48a2c1e8ff1b7d4c0ff82fbefa64d2d0" # 小程序的AppSecret
|
AppSecret: "48a2c1e8ff1b7d4c0ff82fbefa64d2d0" # 小程序的AppSecret
|
||||||
WechatXpay:
|
WechatXpay:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
Env: 1 # 0 现网 / 1 沙箱(当前:沙箱联调)
|
Env: 1 # 0 现网 / 1 沙箱(须与 AppKey 配套)
|
||||||
OfferId: "1450552691"
|
OfferId: "1450552691"
|
||||||
AppKey: "n1SSzeMiitPjiQwLlBn1s4Hn7SpkG3qD" # 沙箱 AppKey
|
AppKey: "n1SSzeMiitPjiQwLlBn1s4Hn7SpkG3qD" # 现网 AppKey(env=0 必填现网密钥)
|
||||||
ProdAppKey: "oYv2wooYzRmPDLdXkpBpqaml8cFaY0Bb" # 现网 AppKey(上线时 Env 改 0 并切换 AppKey)
|
|
||||||
MessagePushToken: "qncXpayPush2026" # 与 mp 后台消息推送 Token 保持一致
|
MessagePushToken: "qncXpayPush2026" # 与 mp 后台消息推送 Token 保持一致
|
||||||
AdminToken: "qncXpayAdmin2026" # 运维手动补发货接口 Bearer Token
|
|
||||||
SessionKeyTTL: 2592000 # 30 天,与 JwtAuth.AccessExpire 对齐
|
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:
|
||||||
|
|||||||
@@ -103,12 +103,10 @@ type WechatMiniConfig struct {
|
|||||||
// WechatXpayConfig 微信小程序虚拟支付(xpay)
|
// WechatXpayConfig 微信小程序虚拟支付(xpay)
|
||||||
type WechatXpayConfig struct {
|
type WechatXpayConfig struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
Env int // 0 现网 / 1 沙箱
|
Env int // 0 现网 / 1 沙箱
|
||||||
OfferId string
|
OfferId string
|
||||||
AppKey string // 与 Env 配套的 AppKey
|
AppKey string // 与 Env 配套的 AppKey(沙箱/现网各自在 yaml 中配置)
|
||||||
ProdAppKey string // 现网 AppKey(切换 env=0 时使用,可选)
|
|
||||||
MessagePushToken string
|
MessagePushToken string
|
||||||
AdminToken string
|
|
||||||
SessionKeyTTL int64
|
SessionKeyTTL int64
|
||||||
}
|
}
|
||||||
type QueryConfig struct {
|
type QueryConfig struct {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package pay
|
package admin_order
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"qnc-server/app/main/api/internal/logic/pay"
|
"qnc-server/app/main/api/internal/logic/admin_order"
|
||||||
"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/common/result"
|
"qnc-server/common/result"
|
||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
)
|
)
|
||||||
|
|
||||||
func XpayAdminDeliverHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
func AdminXpayDeliverHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var req types.XpayAdminDeliverReq
|
var req types.AdminXpayDeliverReq
|
||||||
if err := httpx.Parse(r, &req); err != nil {
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
result.ParamErrorResult(r, w, err)
|
result.ParamErrorResult(r, w, err)
|
||||||
return
|
return
|
||||||
@@ -23,8 +23,8 @@ func XpayAdminDeliverHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|||||||
result.ParamValidateErrorResult(r, w, err)
|
result.ParamValidateErrorResult(r, w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
l := pay.NewXpayAdminDeliverLogic(r.Context(), svcCtx)
|
l := admin_order.NewAdminXpayDeliverLogic(r.Context(), svcCtx)
|
||||||
resp, err := l.XpayAdminDeliver(&req, r)
|
resp, err := l.AdminXpayDeliver(&req)
|
||||||
result.HttpResult(r, w, resp, err)
|
result.HttpResult(r, w, resp, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,6 +368,12 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||||||
Path: "/retry-agent-process/:id",
|
Path: "/retry-agent-process/:id",
|
||||||
Handler: admin_order.AdminRetryAgentProcessHandler(serverCtx),
|
Handler: admin_order.AdminRetryAgentProcessHandler(serverCtx),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// xpay补发货(查微信单并到账)
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/xpay-deliver/:id",
|
||||||
|
Handler: admin_order.AdminXpayDeliverHandler(serverCtx),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// 更新订单
|
// 更新订单
|
||||||
Method: http.MethodPut,
|
Method: http.MethodPut,
|
||||||
@@ -947,11 +953,6 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||||||
Path: "/pay/xpay/push",
|
Path: "/pay/xpay/push",
|
||||||
Handler: pay.XpayPushHandler(serverCtx),
|
Handler: pay.XpayPushHandler(serverCtx),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Method: http.MethodPost,
|
|
||||||
Path: "/pay/xpay/admin/deliver",
|
|
||||||
Handler: pay.XpayAdminDeliverHandler(serverCtx),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
rest.WithPrefix("/api/v1"),
|
rest.WithPrefix("/api/v1"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
package admin_order
|
package admin_order
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"qnc-server/app/main/api/internal/svc"
|
paylogic "qnc-server/app/main/api/internal/logic/pay"
|
||||||
"qnc-server/app/main/api/internal/types"
|
"qnc-server/app/main/api/internal/svc"
|
||||||
"qnc-server/app/main/model"
|
"qnc-server/app/main/api/internal/types"
|
||||||
"qnc-server/common/xerr"
|
"qnc-server/app/main/model"
|
||||||
|
"qnc-server/common/xerr"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/google/uuid"
|
||||||
"github.com/zeromicro/go-zero/core/logx"
|
"github.com/pkg/errors"
|
||||||
"github.com/zeromicro/go-zero/core/stores/sqlx"
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
"github.com/google/uuid"
|
"github.com/zeromicro/go-zero/core/stores/sqlx"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -44,6 +45,10 @@ func (l *AdminRefundOrderLogic) AdminRefundOrder(req *types.AdminRefundOrderReq)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if model.IsXpayOrder(order) {
|
||||||
|
return l.handleXpayRefund(order, req)
|
||||||
|
}
|
||||||
|
|
||||||
// 根据支付平台处理退款
|
// 根据支付平台处理退款
|
||||||
switch order.PaymentPlatform {
|
switch order.PaymentPlatform {
|
||||||
case PaymentPlatformAlipay:
|
case PaymentPlatformAlipay:
|
||||||
@@ -107,6 +112,23 @@ func (l *AdminRefundOrderLogic) handleAlipayRefund(order *model.Order, req *type
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleXpayRefund 处理小程序虚拟支付退款
|
||||||
|
func (l *AdminRefundOrderLogic) handleXpayRefund(order *model.Order, req *types.AdminRefundOrderReq) (*types.AdminRefundOrderResp, error) {
|
||||||
|
if req.RefundAmount != order.Amount {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrMsg("虚拟支付订单仅支持全额退款"), "")
|
||||||
|
}
|
||||||
|
if err := paylogic.RefundXpayQueryOrder(l.ctx, l.svcCtx, order); err != nil {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "虚拟支付退款失败: %v", err)
|
||||||
|
}
|
||||||
|
refundNo := l.generateRefundNo(order.OrderNo)
|
||||||
|
_ = l.createRefundRecordOnly(order, req, refundNo, "", model.OrderRefundStatusSuccess)
|
||||||
|
return &types.AdminRefundOrderResp{
|
||||||
|
Status: model.OrderStatusRefunded,
|
||||||
|
RefundNo: refundNo,
|
||||||
|
Amount: req.RefundAmount,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// handleWechatRefund 处理微信退款
|
// handleWechatRefund 处理微信退款
|
||||||
func (l *AdminRefundOrderLogic) handleWechatRefund(order *model.Order, req *types.AdminRefundOrderReq) (*types.AdminRefundOrderResp, error) {
|
func (l *AdminRefundOrderLogic) handleWechatRefund(order *model.Order, req *types.AdminRefundOrderReq) (*types.AdminRefundOrderResp, error) {
|
||||||
// 调用微信退款接口
|
// 调用微信退款接口
|
||||||
@@ -133,18 +155,18 @@ func (l *AdminRefundOrderLogic) handleWechatRefund(order *model.Order, req *type
|
|||||||
func (l *AdminRefundOrderLogic) createRefundRecordAndUpdateOrder(order *model.Order, req *types.AdminRefundOrderReq, refundNo, platformRefundId, orderStatus, refundStatus string) error {
|
func (l *AdminRefundOrderLogic) createRefundRecordAndUpdateOrder(order *model.Order, req *types.AdminRefundOrderReq, refundNo, platformRefundId, orderStatus, refundStatus string) error {
|
||||||
return l.svcCtx.OrderModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
|
return l.svcCtx.OrderModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
|
||||||
// 创建退款记录
|
// 创建退款记录
|
||||||
refund := &model.OrderRefund{
|
refund := &model.OrderRefund{
|
||||||
Id: uuid.NewString(),
|
Id: uuid.NewString(),
|
||||||
RefundNo: refundNo,
|
RefundNo: refundNo,
|
||||||
PlatformRefundId: l.createNullString(platformRefundId),
|
PlatformRefundId: l.createNullString(platformRefundId),
|
||||||
OrderId: order.Id,
|
OrderId: order.Id,
|
||||||
UserId: order.UserId,
|
UserId: order.UserId,
|
||||||
ProductId: order.ProductId,
|
ProductId: order.ProductId,
|
||||||
RefundAmount: req.RefundAmount,
|
RefundAmount: req.RefundAmount,
|
||||||
RefundReason: l.createNullString(req.RefundReason),
|
RefundReason: l.createNullString(req.RefundReason),
|
||||||
Status: refundStatus, // 使用传入的状态,不再硬编码
|
Status: refundStatus, // 使用传入的状态,不再硬编码
|
||||||
RefundTime: sql.NullTime{Time: time.Now(), Valid: true},
|
RefundTime: sql.NullTime{Time: time.Now(), Valid: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := l.svcCtx.OrderRefundModel.Insert(ctx, session, refund); err != nil {
|
if _, err := l.svcCtx.OrderRefundModel.Insert(ctx, session, refund); err != nil {
|
||||||
return fmt.Errorf("创建退款记录失败: %v", err)
|
return fmt.Errorf("创建退款记录失败: %v", err)
|
||||||
@@ -169,18 +191,18 @@ func (l *AdminRefundOrderLogic) createRefundRecordAndUpdateOrder(order *model.Or
|
|||||||
|
|
||||||
// createRefundRecordOnly 仅创建退款记录,不更新订单状态(用于退款失败的情况)
|
// createRefundRecordOnly 仅创建退款记录,不更新订单状态(用于退款失败的情况)
|
||||||
func (l *AdminRefundOrderLogic) createRefundRecordOnly(order *model.Order, req *types.AdminRefundOrderReq, refundNo, platformRefundId, refundStatus string) error {
|
func (l *AdminRefundOrderLogic) createRefundRecordOnly(order *model.Order, req *types.AdminRefundOrderReq, refundNo, platformRefundId, refundStatus string) error {
|
||||||
refund := &model.OrderRefund{
|
refund := &model.OrderRefund{
|
||||||
Id: uuid.NewString(),
|
Id: uuid.NewString(),
|
||||||
RefundNo: refundNo,
|
RefundNo: refundNo,
|
||||||
PlatformRefundId: l.createNullString(platformRefundId),
|
PlatformRefundId: l.createNullString(platformRefundId),
|
||||||
OrderId: order.Id,
|
OrderId: order.Id,
|
||||||
UserId: order.UserId,
|
UserId: order.UserId,
|
||||||
ProductId: order.ProductId,
|
ProductId: order.ProductId,
|
||||||
RefundAmount: req.RefundAmount,
|
RefundAmount: req.RefundAmount,
|
||||||
RefundReason: l.createNullString(req.RefundReason),
|
RefundReason: l.createNullString(req.RefundReason),
|
||||||
Status: refundStatus,
|
Status: refundStatus,
|
||||||
RefundTime: sql.NullTime{Time: time.Now(), Valid: true},
|
RefundTime: sql.NullTime{Time: time.Now(), Valid: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := l.svcCtx.OrderRefundModel.Insert(l.ctx, nil, refund)
|
_, err := l.svcCtx.OrderRefundModel.Insert(l.ctx, nil, refund)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package admin_order
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"qnc-server/app/main/api/internal/logic/pay"
|
||||||
|
"qnc-server/app/main/api/internal/svc"
|
||||||
|
"qnc-server/app/main/api/internal/types"
|
||||||
|
"qnc-server/app/main/model"
|
||||||
|
"qnc-server/common/xerr"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AdminXpayDeliverLogic struct {
|
||||||
|
logx.Logger
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdminXpayDeliverLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminXpayDeliverLogic {
|
||||||
|
return &AdminXpayDeliverLogic{
|
||||||
|
Logger: logx.WithContext(ctx),
|
||||||
|
ctx: ctx,
|
||||||
|
svcCtx: svcCtx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *AdminXpayDeliverLogic) AdminXpayDeliver(req *types.AdminXpayDeliverReq) (resp *types.AdminXpayDeliverResp, err error) {
|
||||||
|
order, findErr := l.svcCtx.OrderModel.FindOne(l.ctx, req.Id)
|
||||||
|
if findErr != nil {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrMsg("订单不存在"), "%v", findErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !model.IsXpayOrder(order) {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrMsg("仅支持小程序虚拟支付订单"), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.svcCtx.XpayService == nil || !l.svcCtx.XpayService.Enabled() {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrMsg("虚拟支付未启用"), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, deliverErr := pay.DeliverXpayQueryOrder(l.ctx, l.svcCtx, order.OrderNo)
|
||||||
|
if deliverErr != nil {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "补发货失败: %v", deliverErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.AdminXpayDeliverResp{
|
||||||
|
Credited: result.Credited,
|
||||||
|
Notified: result.Notified,
|
||||||
|
WechatDetail: result.WechatDetail,
|
||||||
|
Errors: result.Errors,
|
||||||
|
Message: result.Message,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -42,8 +42,8 @@ func (l *PaymentCheckLogic) PaymentCheck(req *types.PaymentCheckReq) (resp *type
|
|||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单失败: %v", findErr)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单失败: %v", findErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// xpay 轮询:pending 时主动查微信单
|
// xpay 轮询:pending 时主动查微信单并到账(不通知微信发货)
|
||||||
if order.Status == "pending" && order.PaymentScene == "wxmini" &&
|
if order.Status == model.OrderStatusPending && model.IsXpayOrder(order) &&
|
||||||
l.svcCtx.XpayService != nil && l.svcCtx.XpayService.Enabled() {
|
l.svcCtx.XpayService != nil && l.svcCtx.XpayService.Enabled() {
|
||||||
if syncErr := l.syncXpayOrderStatus(order); syncErr != nil {
|
if syncErr := l.syncXpayOrderStatus(order); syncErr != nil {
|
||||||
l.Errorf("[xpay] 轮询查单失败 order_no=%s err=%v", req.OrderNo, syncErr)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
sessionKey, err := l.svcCtx.XpayService.GetSessionKey(l.ctx, userID)
|
status, err := l.svcCtx.XpayService.QueryOrder(l.ctx, openid, order.OrderNo, "")
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
status, err := l.svcCtx.XpayService.QueryOrder(l.ctx, openid, order.OrderNo, sessionKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if service.IsXpayPaidStatus(status.Status) {
|
if service.IsXpayPaidStatus(status.Status) {
|
||||||
credited, fulfillErr := fulfillQueryOrderPaid(l.ctx, l.svcCtx, order, "", status.PaidFee)
|
_, fulfillErr := fulfillQueryOrderPaid(l.ctx, l.svcCtx, order, status.WxOrderID, status.PaidFee)
|
||||||
if fulfillErr != nil {
|
return fulfillErr
|
||||||
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) {
|
if service.IsXpayClosedStatus(status.Status) {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "query":
|
case "query":
|
||||||
paymentTypeResp, err = l.QueryOrderPayment(req, session)
|
paymentTypeResp, err = l.QueryOrderPayment(req, session, useXpay)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)
|
userID, getUidErr := ctxdata.GetUidFromCtx(l.ctx)
|
||||||
if getUidErr != nil {
|
if getUidErr != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 获取用户信息失败, %+v", getUidErr)
|
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 {
|
if user.Inside == 1 {
|
||||||
amount = 0.01
|
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{
|
order := model.Order{
|
||||||
Id: uuid.NewString(),
|
Id: uuid.NewString(),
|
||||||
OrderNo: outTradeNo,
|
OrderNo: outTradeNo,
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
ProductId: product.Id,
|
ProductId: product.Id,
|
||||||
PaymentPlatform: req.PayMethod,
|
PaymentPlatform: paymentPlatform,
|
||||||
PaymentScene: resolvePaymentScene(l.ctx),
|
PaymentScene: resolvePaymentScene(l.ctx),
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
Status: "pending",
|
Status: "pending",
|
||||||
@@ -376,18 +407,9 @@ func (l *PaymentLogic) shouldUseXpay(req *types.PaymentReq) bool {
|
|||||||
func resolvePaymentScene(ctx context.Context) string {
|
func resolvePaymentScene(ctx context.Context) string {
|
||||||
platform, err := ctxdata.GetPlatformFromCtx(ctx)
|
platform, err := ctxdata.GetPlatformFromCtx(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "app"
|
return model.PaymentSceneApp
|
||||||
}
|
|
||||||
switch platform {
|
|
||||||
case model.PlatformWxMini:
|
|
||||||
return "wxmini"
|
|
||||||
case model.PlatformWxH5:
|
|
||||||
return "wxh5"
|
|
||||||
case model.PlatformH5:
|
|
||||||
return "h5"
|
|
||||||
default:
|
|
||||||
return "app"
|
|
||||||
}
|
}
|
||||||
|
return model.PaymentSceneFromPlatform(platform)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AgentVipOrderPayment 代理会员充值订单(已废弃,新系统使用升级功能替代)
|
// AgentVipOrderPayment 代理会员充值订单(已废弃,新系统使用升级功能替代)
|
||||||
|
|||||||
87
app/main/api/internal/logic/pay/xpay_deliver.go
Normal file
87
app/main/api/internal/logic/pay/xpay_deliver.go
Normal 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 "未执行到账"
|
||||||
|
}
|
||||||
48
app/main/api/internal/logic/pay/xpay_notify.go
Normal file
48
app/main/api/internal/logic/pay/xpay_notify.go
Normal 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)
|
||||||
|
}
|
||||||
83
app/main/api/internal/logic/pay/xpay_refund.go
Normal file
83
app/main/api/internal/logic/pay/xpay_refund.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -21,8 +21,14 @@ func fulfillQueryOrderPaid(ctx context.Context, svcCtx *svc.ServiceContext, orde
|
|||||||
|
|
||||||
orderFen := lzUtils.ToWechatAmount(order.Amount)
|
orderFen := lzUtils.ToWechatAmount(order.Amount)
|
||||||
if wechatPaidFen > 0 && wechatPaidFen != orderFen {
|
if wechatPaidFen > 0 && wechatPaidFen != orderFen {
|
||||||
logx.WithContext(ctx).Errorf("[xpay] 金额不一致 order_no=%s order_fen=%d wechat_fen=%d", order.OrderNo, orderFen, wechatPaidFen)
|
if model.IsXpayOrder(order) {
|
||||||
return false, nil
|
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"
|
order.Status = "paid"
|
||||||
@@ -30,8 +36,8 @@ func fulfillQueryOrderPaid(ctx context.Context, svcCtx *svc.ServiceContext, orde
|
|||||||
if platformOrderID != "" {
|
if platformOrderID != "" {
|
||||||
order.PlatformOrderId = sql.NullString{String: platformOrderID, Valid: true}
|
order.PlatformOrderId = sql.NullString{String: platformOrderID, Valid: true}
|
||||||
}
|
}
|
||||||
if order.PaymentScene == "" || order.PaymentScene == "app" {
|
if order.PaymentScene == "" || order.PaymentScene == model.PaymentSceneApp {
|
||||||
order.PaymentScene = "wxmini"
|
order.PaymentScene = model.PaymentSceneMiniProgram
|
||||||
}
|
}
|
||||||
|
|
||||||
if updateErr := svcCtx.OrderModel.UpdateWithVersion(ctx, nil, order); updateErr != nil {
|
if updateErr := svcCtx.OrderModel.UpdateWithVersion(ctx, nil, order); updateErr != nil {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"qnc-server/app/main/api/internal/svc"
|
"qnc-server/app/main/api/internal/svc"
|
||||||
@@ -33,8 +35,14 @@ func NewWxMiniAuthLogic(ctx context.Context, svcCtx *svc.ServiceContext) *WxMini
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *WxMiniAuthLogic) WxMiniAuth(req *types.WXMiniAuthReq) (resp *types.WXMiniAuthResp, err error) {
|
func (l *WxMiniAuthLogic) WxMiniAuth(req *types.WXMiniAuthReq) (resp *types.WXMiniAuthResp, err error) {
|
||||||
|
code := strings.TrimSpace(req.Code)
|
||||||
|
if code == "" || code == "the code is a mock one" || strings.Contains(code, " ") {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrMsg("微信登录凭证无效,请重新打开小程序"),
|
||||||
|
"无效的微信 code: %q", req.Code)
|
||||||
|
}
|
||||||
|
|
||||||
// 1. 获取session_key和openid
|
// 1. 获取session_key和openid
|
||||||
sessionKeyResp, err := l.GetSessionKey(req.Code)
|
sessionKeyResp, err := l.GetSessionKey(code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取session_key失败: %v", err)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取session_key失败: %v", err)
|
||||||
}
|
}
|
||||||
@@ -104,10 +112,10 @@ func (l *WxMiniAuthLogic) GetSessionKey(code string) (*SessionKeyResp, error) {
|
|||||||
appID = l.svcCtx.Config.WechatMini.AppID
|
appID = l.svcCtx.Config.WechatMini.AppID
|
||||||
appSecret = l.svcCtx.Config.WechatMini.AppSecret
|
appSecret = l.svcCtx.Config.WechatMini.AppSecret
|
||||||
|
|
||||||
url := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
|
apiURL := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
|
||||||
appID, appSecret, code)
|
url.QueryEscape(appID), url.QueryEscape(appSecret), url.QueryEscape(code))
|
||||||
|
|
||||||
resp, err := http.Get(url)
|
resp, err := http.Get(apiURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取session_key失败: %v", err)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取session_key失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"qnc-server/app/main/api/internal/logic/pay"
|
||||||
"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/app/main/model"
|
||||||
@@ -17,7 +18,6 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/hibiken/asynq"
|
"github.com/hibiken/asynq"
|
||||||
"github.com/zeromicro/go-zero/core/logx"
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
"github.com/zeromicro/go-zero/core/stores/sqlx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PaySuccessNotifyUserHandler struct {
|
type PaySuccessNotifyUserHandler struct {
|
||||||
@@ -195,6 +195,9 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq.
|
|||||||
logx.Errorf("发送代理处理任务失败,订单ID: %s, 错误: %v", order.Id, asyncErr)
|
logx.Errorf("发送代理处理任务失败,订单ID: %s, 错误: %v", order.Id, asyncErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// xpay:报告成功后再通知微信虚拟支付已发货
|
||||||
|
pay.NotifyXpayGoodsAfterReport(ctx, l.svcCtx, order)
|
||||||
|
|
||||||
_, delErr := l.svcCtx.Redis.DelCtx(ctx, redisKey)
|
_, delErr := l.svcCtx.Redis.DelCtx(ctx, redisKey)
|
||||||
if delErr != nil {
|
if delErr != nil {
|
||||||
logx.Errorf("删除Redis缓存失败,但任务已成功处理,订单ID: %s, 错误: %v", order.Id, delErr)
|
logx.Errorf("删除Redis缓存失败,但任务已成功处理,订单ID: %s, 错误: %v", order.Id, delErr)
|
||||||
@@ -222,40 +225,7 @@ func (l *PaySuccessNotifyUserHandler) handleError(ctx context.Context, err error
|
|||||||
return asynq.SkipRetry
|
return asynq.SkipRetry
|
||||||
}
|
}
|
||||||
|
|
||||||
// 退款
|
pay.TryRefundOnQueryFailure(ctx, l.svcCtx, order)
|
||||||
if order.PaymentPlatform == "wechat" {
|
|
||||||
refundErr := l.svcCtx.WechatPayService.WeChatRefund(ctx, order.OrderNo, order.Amount, order.Amount)
|
|
||||||
if refundErr != nil {
|
|
||||||
logx.Error(refundErr)
|
|
||||||
return asynq.SkipRetry
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
refund, refundErr := l.svcCtx.AlipayService.AliRefund(ctx, order.OrderNo, order.Amount)
|
|
||||||
if refundErr != nil {
|
|
||||||
logx.Error(refundErr)
|
|
||||||
return asynq.SkipRetry
|
|
||||||
}
|
|
||||||
if refund.IsSuccess() {
|
|
||||||
logx.Errorf("支付宝退款成功, orderID: %s", order.Id)
|
|
||||||
transErr := l.svcCtx.OrderModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error {
|
|
||||||
order.Status = "refunded"
|
|
||||||
if err := l.svcCtx.OrderModel.UpdateWithVersion(transCtx, session, order); err != nil {
|
|
||||||
return fmt.Errorf("更新订单状态失败: %v", err)
|
|
||||||
}
|
|
||||||
return l.svcCtx.AgentService.ReverseAgentSettlementOnOrderRefund(transCtx, session, order.Id)
|
|
||||||
})
|
|
||||||
if transErr != nil {
|
|
||||||
logx.Errorf("支付宝退款后更新订单或冲正代理分账失败, orderID: %s, err: %v", order.Id, transErr)
|
|
||||||
return fmt.Errorf("更新订单状态或冲正代理分账失败: %v", transErr)
|
|
||||||
}
|
|
||||||
return asynq.SkipRetry
|
|
||||||
} else {
|
|
||||||
logx.Errorf("支付宝退款失败:%v", refundErr)
|
|
||||||
return asynq.SkipRetry
|
|
||||||
}
|
|
||||||
// 直接成功
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return asynq.SkipRetry
|
return asynq.SkipRetry
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"qnc-server/app/main/api/internal/config"
|
"qnc-server/app/main/api/internal/config"
|
||||||
tianyuanapi "qnc-server/app/main/api/internal/service/tianyuanapi_sdk"
|
tianyuanapi "qnc-server/app/main/api/internal/service/tianyuanapi_sdk"
|
||||||
|
|
||||||
@@ -184,8 +185,9 @@ func (r *VerificationService) GetWechatH5OpenID(ctx context.Context, code string
|
|||||||
func (r *VerificationService) GetWechatMiniOpenID(ctx context.Context, code string) (string, error) {
|
func (r *VerificationService) GetWechatMiniOpenID(ctx context.Context, code string) (string, error) {
|
||||||
appID := r.c.WechatMini.AppID
|
appID := r.c.WechatMini.AppID
|
||||||
appSecret := r.c.WechatMini.AppSecret
|
appSecret := r.c.WechatMini.AppSecret
|
||||||
url := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", appID, appSecret, code)
|
apiURL := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
|
||||||
resp, err := http.Get(url)
|
url.QueryEscape(appID), url.QueryEscape(appSecret), url.QueryEscape(code))
|
||||||
|
resp, err := http.Get(apiURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"qnc-server/app/main/api/internal/config"
|
"qnc-server/app/main/api/internal/config"
|
||||||
"qnc-server/app/main/model"
|
"qnc-server/app/main/model"
|
||||||
"qnc-server/common/ctxdata"
|
"qnc-server/common/ctxdata"
|
||||||
@@ -446,9 +447,10 @@ func (w *WechatPayService) GenerateOutTradeNo() string {
|
|||||||
func (w *WechatPayService) getWechatMiniOpenID(ctx context.Context, code string) (string, error) {
|
func (w *WechatPayService) getWechatMiniOpenID(ctx context.Context, code string) (string, error) {
|
||||||
appID := w.config.WechatMini.AppID
|
appID := w.config.WechatMini.AppID
|
||||||
appSecret := w.config.WechatMini.AppSecret
|
appSecret := w.config.WechatMini.AppSecret
|
||||||
url := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", appID, appSecret, code)
|
apiURL := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
|
||||||
|
url.QueryEscape(appID), url.QueryEscape(appSecret), url.QueryEscape(code))
|
||||||
|
|
||||||
resp, err := http.Get(url)
|
resp, err := http.Get(apiURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("请求微信API失败: %v", err)
|
return "", fmt.Errorf("请求微信API失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
37
app/main/api/internal/service/xpayProductMapping.go
Normal file
37
app/main/api/internal/service/xpayProductMapping.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
// XpayProductIdMap 微信道具 productId(QCXG*)→ 本系统 product_en(toc_*)
|
||||||
|
// mp 后台道具 id 须与左侧 QCXG 编码一致;支付签名 productId 使用 QCXG,业务订单仍用 product_en。
|
||||||
|
var XpayProductIdMap = map[string]string{
|
||||||
|
"QCXG6B4E": "toc_VehicleClaimVerify",
|
||||||
|
"QCXGP00W": "toc_VehicleClaimDetail",
|
||||||
|
"QCXG3Z3L": "toc_VehicleMaintenanceDetail",
|
||||||
|
"QCXG3Y6B": "toc_VehicleMaintenanceSimple",
|
||||||
|
"QCXG4I1Z": "toc_VehicleTransferDetail",
|
||||||
|
"QCXG1H7Y": "toc_VehicleTransferSimple",
|
||||||
|
"QCXGY7F2": "toc_VehicleVinValuation",
|
||||||
|
"QCXG1U4U": "toc_VehicleMileageMixed",
|
||||||
|
"QCXG5U0Z": "toc_VehicleStaticInfo",
|
||||||
|
"QCXG4D2E": "toc_VehiclesUnderNameCount",
|
||||||
|
"QCXG9P1C": "toc_VehiclesUnderNamePlate",
|
||||||
|
"QCXGYTS2": "toc_PersonVehicleVerificationDetail",
|
||||||
|
"QCXGGB2Q": "toc_PersonVehicleVerification",
|
||||||
|
"QCXG7A2B": "toc_VehiclesUnderName",
|
||||||
|
}
|
||||||
|
|
||||||
|
var xpayProductEnToId map[string]string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
xpayProductEnToId = make(map[string]string, len(XpayProductIdMap))
|
||||||
|
for xpayID, productEn := range XpayProductIdMap {
|
||||||
|
xpayProductEnToId[productEn] = xpayID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveXpayProductId 将 product_en 转为 mp 道具 id;未配置时回退 product_en 本身。
|
||||||
|
func ResolveXpayProductId(productEn string) string {
|
||||||
|
if id, ok := xpayProductEnToId[productEn]; ok {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
return productEn
|
||||||
|
}
|
||||||
@@ -23,10 +23,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
xpaySessionKeyFmt = "qnc:wx:xpay:session:%s"
|
xpaySessionKeyFmt = "qnc:wx:xpay:session:%s"
|
||||||
xpayNotifiedKeyFmt = "qnc:xpay:notified:%s"
|
xpayNotifiedKeyFmt = "qnc:xpay:notified:%s"
|
||||||
xpayAccessTokenKey = "qnc:wx:xpay:access_token"
|
xpayAccessTokenKey = "qnc:wx:xpay:access_token"
|
||||||
xpayMode = "short_series_goods"
|
xpayMode = "short_series_goods"
|
||||||
xpayVirtualPaymentURI = "requestVirtualPayment"
|
xpayVirtualPaymentURI = "requestVirtualPayment"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -115,12 +115,13 @@ func (s *XpayService) BuildPayParams(ctx context.Context, userID, orderNo, produ
|
|||||||
}
|
}
|
||||||
|
|
||||||
goodsPrice := lzUtils.ToWechatAmount(amount)
|
goodsPrice := lzUtils.ToWechatAmount(amount)
|
||||||
|
xpayProductID := ResolveXpayProductId(productEn)
|
||||||
payload := signDataPayload{
|
payload := signDataPayload{
|
||||||
OfferId: s.config.WechatXpay.OfferId,
|
OfferId: s.config.WechatXpay.OfferId,
|
||||||
BuyQuantity: 1,
|
BuyQuantity: 1,
|
||||||
Env: s.Env(),
|
Env: s.Env(),
|
||||||
CurrencyType: "CNY",
|
CurrencyType: "CNY",
|
||||||
ProductId: productEn,
|
ProductId: xpayProductID,
|
||||||
GoodsPrice: goodsPrice,
|
GoodsPrice: goodsPrice,
|
||||||
OutTradeNo: orderNo,
|
OutTradeNo: orderNo,
|
||||||
Attach: fmt.Sprintf("query:%s", orderNo),
|
Attach: fmt.Sprintf("query:%s", orderNo),
|
||||||
@@ -134,8 +135,8 @@ func (s *XpayService) BuildPayParams(ctx context.Context, userID, orderNo, produ
|
|||||||
paySig := hmacSHA256Hex(appKey, xpayVirtualPaymentURI+"&"+signData)
|
paySig := hmacSHA256Hex(appKey, xpayVirtualPaymentURI+"&"+signData)
|
||||||
signature := hmacSHA256Hex(sessionKey, signData)
|
signature := hmacSHA256Hex(sessionKey, signData)
|
||||||
|
|
||||||
logx.WithContext(ctx).Infof("[xpay] create user=%s order_no=%s env=%d productId=%s goodsPrice=%d",
|
logx.WithContext(ctx).Infof("[xpay] create user=%s order_no=%s env=%d product_en=%s xpay_product_id=%s goodsPrice=%d",
|
||||||
userID, orderNo, s.Env(), productEn, goodsPrice)
|
userID, orderNo, s.Env(), productEn, xpayProductID, goodsPrice)
|
||||||
|
|
||||||
return &XpayPrepayData{
|
return &XpayPrepayData{
|
||||||
Provider: "xpay",
|
Provider: "xpay",
|
||||||
@@ -187,12 +188,15 @@ type xpayAPIResp struct {
|
|||||||
ErrCode int `json:"errcode"`
|
ErrCode int `json:"errcode"`
|
||||||
ErrMsg string `json:"errmsg"`
|
ErrMsg string `json:"errmsg"`
|
||||||
Data json.RawMessage `json:"data"`
|
Data json.RawMessage `json:"data"`
|
||||||
|
Order json.RawMessage `json:"order"` // query_order 官方返回 order 字段
|
||||||
}
|
}
|
||||||
|
|
||||||
type xpayOrderStatus struct {
|
type xpayOrderStatus struct {
|
||||||
Status int `json:"status"`
|
Status int `json:"status"`
|
||||||
PaidFee int64 `json:"paid_fee"`
|
PaidFee int64 `json:"paid_fee"`
|
||||||
OrderID string `json:"order_id"`
|
LeftFee int64 `json:"left_fee"`
|
||||||
|
OrderID string `json:"order_id"`
|
||||||
|
WxOrderID string `json:"wx_order_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *XpayService) callAPI(ctx context.Context, uri, bodyJSON, sessionKey string, needSignature bool) (*xpayAPIResp, error) {
|
func (s *XpayService) callAPI(ctx context.Context, uri, bodyJSON, sessionKey string, needSignature bool) (*xpayAPIResp, error) {
|
||||||
@@ -243,7 +247,8 @@ func (s *XpayService) QueryOrder(ctx context.Context, openid, orderNo, sessionKe
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
apiResp, err := s.callAPI(ctx, "/xpay/query_order", bodyJSON, sessionKey, true)
|
// 官方文档:query_order 仅需 pay_sig,不需用户态 signature
|
||||||
|
apiResp, err := s.callAPI(ctx, "/xpay/query_order", bodyJSON, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -251,13 +256,65 @@ func (s *XpayService) QueryOrder(ctx context.Context, openid, orderNo, sessionKe
|
|||||||
return nil, fmt.Errorf("query_order errcode=%d errmsg=%s", apiResp.ErrCode, apiResp.ErrMsg)
|
return nil, fmt.Errorf("query_order errcode=%d errmsg=%s", apiResp.ErrCode, apiResp.ErrMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
orderRaw := apiResp.Order
|
||||||
|
if len(orderRaw) == 0 {
|
||||||
|
orderRaw = apiResp.Data
|
||||||
|
}
|
||||||
var status xpayOrderStatus
|
var status xpayOrderStatus
|
||||||
if len(apiResp.Data) > 0 {
|
if len(orderRaw) > 0 {
|
||||||
_ = json.Unmarshal(apiResp.Data, &status)
|
_ = json.Unmarshal(orderRaw, &status)
|
||||||
}
|
}
|
||||||
return &status, nil
|
return &status, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RefundOrder 启动 xpay 退款任务(全额退)
|
||||||
|
func (s *XpayService) RefundOrder(ctx context.Context, openid, orderNo, refundOrderID string, refundFeeFen int64) error {
|
||||||
|
if refundFeeFen <= 0 {
|
||||||
|
return fmt.Errorf("退款金额无效")
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := s.QueryOrder(ctx, openid, orderNo, "")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("退款前查单失败: %w", err)
|
||||||
|
}
|
||||||
|
leftFee := status.LeftFee
|
||||||
|
if leftFee <= 0 {
|
||||||
|
leftFee = status.PaidFee
|
||||||
|
}
|
||||||
|
if leftFee <= 0 {
|
||||||
|
leftFee = refundFeeFen
|
||||||
|
}
|
||||||
|
if refundFeeFen > leftFee {
|
||||||
|
refundFeeFen = leftFee
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyObj := map[string]interface{}{
|
||||||
|
"openid": openid,
|
||||||
|
"env": s.Env(),
|
||||||
|
"order_id": orderNo,
|
||||||
|
"refund_order_id": refundOrderID,
|
||||||
|
"left_fee": leftFee,
|
||||||
|
"refund_fee": refundFeeFen,
|
||||||
|
"biz_meta": fmt.Sprintf("refund:%s", orderNo),
|
||||||
|
"refund_reason": 5,
|
||||||
|
"req_from": 3,
|
||||||
|
}
|
||||||
|
bodyJSON, err := compactJSON(bodyObj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
apiResp, err := s.callAPI(ctx, "/xpay/refund_order", bodyJSON, "", false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if apiResp.ErrCode != 0 && apiResp.ErrCode != 268490004 && apiResp.ErrCode != 268490014 {
|
||||||
|
return fmt.Errorf("refund_order errcode=%d errmsg=%s", apiResp.ErrCode, apiResp.ErrMsg)
|
||||||
|
}
|
||||||
|
logx.WithContext(ctx).Infof("[xpay] refund_order OK order_no=%s refund_order_id=%s fee=%d", orderNo, refundOrderID, refundFeeFen)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// NotifyProvideGoods 主动通知微信已发货
|
// NotifyProvideGoods 主动通知微信已发货
|
||||||
func (s *XpayService) NotifyProvideGoods(ctx context.Context, openid, orderNo, wxOrderID, sessionKey string) error {
|
func (s *XpayService) NotifyProvideGoods(ctx context.Context, openid, orderNo, wxOrderID, sessionKey string) error {
|
||||||
bodyObj := map[string]interface{}{
|
bodyObj := map[string]interface{}{
|
||||||
@@ -305,10 +362,10 @@ func (s *XpayService) VerifyPushSignature(signature, timestamp, nonce string) bo
|
|||||||
|
|
||||||
// XpayDeliverNotify 推送消息体(明文 JSON)
|
// XpayDeliverNotify 推送消息体(明文 JSON)
|
||||||
type XpayDeliverNotify struct {
|
type XpayDeliverNotify struct {
|
||||||
Event string `json:"Event"`
|
Event string `json:"Event"`
|
||||||
OpenId string `json:"OpenId"`
|
OpenId string `json:"OpenId"`
|
||||||
OutTradeNo string `json:"OutTradeNo"`
|
OutTradeNo string `json:"OutTradeNo"`
|
||||||
Env int `json:"Env"`
|
Env int `json:"Env"`
|
||||||
WeChatPayInfo struct {
|
WeChatPayInfo struct {
|
||||||
TransactionId string `json:"TransactionId"`
|
TransactionId string `json:"TransactionId"`
|
||||||
PaidTime int64 `json:"PaidTime"`
|
PaidTime int64 `json:"PaidTime"`
|
||||||
|
|||||||
@@ -816,6 +816,18 @@ type AdminRetryAgentProcessResp struct {
|
|||||||
ProcessedAt string `json:"processed_at"` // 处理时间
|
ProcessedAt string `json:"processed_at"` // 处理时间
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AdminXpayDeliverReq struct {
|
||||||
|
Id string `path:"id"` // 订单ID
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdminXpayDeliverResp struct {
|
||||||
|
Credited bool `json:"credited"`
|
||||||
|
Notified bool `json:"notified"`
|
||||||
|
WechatDetail string `json:"wechat_detail"`
|
||||||
|
Errors []string `json:"errors"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
type AdminRevenueStatistics struct {
|
type AdminRevenueStatistics struct {
|
||||||
TodayAmount float64 `json:"today_amount"` // 今日营收
|
TodayAmount float64 `json:"today_amount"` // 今日营收
|
||||||
MonthAmount float64 `json:"month_amount"` // 当月营收
|
MonthAmount float64 `json:"month_amount"` // 当月营收
|
||||||
@@ -1933,17 +1945,6 @@ 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"` // 查询订单数(订单数量,不是用户数)
|
||||||
|
|||||||
@@ -48,12 +48,12 @@ func main() {
|
|||||||
asynq := queue.NewCronJob(ctx, svcContext)
|
asynq := queue.NewCronJob(ctx, svcContext)
|
||||||
mux := asynq.Register()
|
mux := asynq.Register()
|
||||||
|
|
||||||
// 启动 asynq 消费者
|
// Run 会阻塞;成功日志必须在 Run 之前打印
|
||||||
|
fmt.Println("异步任务启动!!!")
|
||||||
if err := svcContext.AsynqServer.Run(mux); err != nil {
|
if err := svcContext.AsynqServer.Run(mux); err != nil {
|
||||||
logx.WithContext(ctx).Errorf("异步任务启动失败: %v", err)
|
logx.WithContext(ctx).Errorf("异步任务启动失败: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
fmt.Println("异步任务启动!!!")
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
server := rest.MustNewServer(c.RestConf)
|
server := rest.MustNewServer(c.RestConf)
|
||||||
|
|||||||
@@ -9,13 +9,38 @@ import (
|
|||||||
var ErrNotFound = sqlx.ErrNotFound
|
var ErrNotFound = sqlx.ErrNotFound
|
||||||
var ErrNoRowsUpdate = errors.New("update db no rows change")
|
var ErrNoRowsUpdate = errors.New("update db no rows change")
|
||||||
|
|
||||||
// 平台
|
// 平台(请求头 X-Platform / JWT platform)
|
||||||
var PlatformWxMini string = "wxmini"
|
var PlatformWxMini string = "wxmini"
|
||||||
var PlatformWxH5 string = "wxh5"
|
var PlatformWxH5 string = "wxh5"
|
||||||
var PlatformApp string = "app"
|
var PlatformApp string = "app"
|
||||||
var PlatformH5 string = "h5"
|
var PlatformH5 string = "h5"
|
||||||
var PlatformAdmin string = "admin"
|
var PlatformAdmin string = "admin"
|
||||||
|
|
||||||
|
// 订单支付场景 order.payment_scene(MySQL ENUM)
|
||||||
|
const (
|
||||||
|
PaymentSceneApp = "app"
|
||||||
|
PaymentSceneH5 = "h5"
|
||||||
|
PaymentSceneMiniProgram = "mini_program"
|
||||||
|
PaymentScenePublicAccount = "public_account"
|
||||||
|
PaymentSceneTest = "test"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PaymentSceneFromPlatform 将运行时平台标识映射为 order.payment_scene 枚举值
|
||||||
|
func PaymentSceneFromPlatform(platform string) string {
|
||||||
|
switch platform {
|
||||||
|
case PlatformWxMini:
|
||||||
|
return PaymentSceneMiniProgram
|
||||||
|
case PlatformWxH5:
|
||||||
|
return PaymentScenePublicAccount
|
||||||
|
case PlatformH5:
|
||||||
|
return PaymentSceneH5
|
||||||
|
case PlatformApp:
|
||||||
|
return PaymentSceneApp
|
||||||
|
default:
|
||||||
|
return PaymentSceneApp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 用户授权类型
|
// 用户授权类型
|
||||||
var UserAuthTypeMobile string = "mobile"
|
var UserAuthTypeMobile string = "mobile"
|
||||||
var UserAuthTypeWxMiniOpenID string = "wxmini_openid"
|
var UserAuthTypeWxMiniOpenID string = "wxmini_openid"
|
||||||
@@ -37,6 +62,28 @@ var AgentLeveNameNormal string = "normal"
|
|||||||
var AgentLeveNameVIP string = "VIP"
|
var AgentLeveNameVIP string = "VIP"
|
||||||
var AgentLeveNameSVIP string = "SVIP"
|
var AgentLeveNameSVIP string = "SVIP"
|
||||||
|
|
||||||
|
// 订单支付平台 order.payment_platform(varchar,非 ENUM)
|
||||||
|
const (
|
||||||
|
PaymentPlatformAlipay = "alipay"
|
||||||
|
PaymentPlatformWechat = "wechat" // 普通微信支付(JSAPI/H5/APP)
|
||||||
|
PaymentPlatformWechatXpay = "wechat_xpay" // 小程序虚拟支付
|
||||||
|
PaymentPlatformAppleIAP = "appleiap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsXpayOrder 是否为小程序虚拟支付订单(含历史 wechat+mini_program 查询单)
|
||||||
|
func IsXpayOrder(order *Order) bool {
|
||||||
|
if order == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if order.PaymentPlatform == PaymentPlatformWechatXpay {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 兼容改造前:查询单 Q_ 前缀 + 小程序场景 + pay_method=wechat
|
||||||
|
return order.PaymentPlatform == PaymentPlatformWechat &&
|
||||||
|
order.PaymentScene == PaymentSceneMiniProgram &&
|
||||||
|
len(order.OrderNo) > 2 && order.OrderNo[:2] == "Q_"
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
OrderStatusPending = "pending"
|
OrderStatusPending = "pending"
|
||||||
OrderStatusPaid = "paid"
|
OrderStatusPaid = "paid"
|
||||||
|
|||||||
16
dock/1.sql
Normal file
16
dock/1.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
create_time,
|
||||||
|
update_time,
|
||||||
|
delete_time,
|
||||||
|
del_state,
|
||||||
|
version,
|
||||||
|
product_name,
|
||||||
|
product_en,
|
||||||
|
description,
|
||||||
|
notes,
|
||||||
|
cost_price,
|
||||||
|
sell_price
|
||||||
|
FROM `product`
|
||||||
|
WHERE create_time > '2025-11-06 16:12:25'
|
||||||
|
AND del_state = 0;
|
||||||
0
dock/2.sql
Normal file
0
dock/2.sql
Normal file
26
dock/gen.js
Normal file
26
dock/gen.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const XLSX = require("xlsx");
|
||||||
|
const IMG = "https://www.quannengcha.com/banner.png";
|
||||||
|
const products = [
|
||||||
|
["QCXG6B4E", "出险记录核验", IMG, 38, "toc_VehicleClaimVerify"],
|
||||||
|
["QCXGP00W", "出险详版查询", IMG, 58, "toc_VehicleClaimDetail"],
|
||||||
|
["QCXG3Z3L", "维保详版查询", IMG, 58, "toc_VehicleMaintenanceDetail"],
|
||||||
|
["QCXG3Y6B", "维保简版查询", IMG, 38, "toc_VehicleMaintenanceSimple"],
|
||||||
|
["QCXG4I1Z", "过户详版查询", IMG, 19.8, "toc_VehicleTransferDetail"],
|
||||||
|
["QCXG1H7Y", "过户简版查询", IMG, 16.8, "toc_VehicleTransferSimple"],
|
||||||
|
["QCXGY7F2", "二手车估值", IMG, 16.8, "toc_VehicleVinValuation"],
|
||||||
|
["QCXG1U4U", "里程记录查询", IMG, 39.8, "toc_VehicleMileageMixed"],
|
||||||
|
["QCXG5U0Z", "静态信息查询", IMG, 6, "toc_VehicleStaticInfo"],
|
||||||
|
["QCXG4D2E", "名下车辆数量", IMG, 19.9, "toc_VehiclesUnderNameCount"],
|
||||||
|
["QCXG9P1C", "名下车辆车牌", IMG, 19.9, "toc_VehiclesUnderNamePlate"],
|
||||||
|
["QCXGYTS2", "人车核验详版", IMG, 19.8, "toc_PersonVehicleVerificationDetail"],
|
||||||
|
["QCXGGB2Q", "人车核验简版", IMG, 16.8, "toc_PersonVehicleVerification"],
|
||||||
|
["QCXG7A2B", "名下车辆", IMG, 19.9, "toc_VehiclesUnderName"],
|
||||||
|
];
|
||||||
|
const header = ["道具id", "道具名称", "道具图片", "道具价格", "备注"];
|
||||||
|
const ws = XLSX.utils.aoa_to_sheet([header, ...products]);
|
||||||
|
const wb = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
|
||||||
|
const out = "c:/Users/a1726/Desktop/qnc/示例-QCXG道具导入.xls";
|
||||||
|
XLSX.writeFile(wb, out, { bookType: "biff8" });
|
||||||
|
console.log("written", out);
|
||||||
|
console.log("OK", products.length);
|
||||||
Reference in New Issue
Block a user