diff --git a/.gitignore b/.gitignore index 5cd2360..6e1011a 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ data/* /app/main/api/_* __debug_bin.exe **/.../ +*.exe # 文档目录 diff --git a/app/main/api/desc/front/pay.api b/app/main/api/desc/front/pay.api index bf1353f..e43cdce 100644 --- a/app/main/api/desc/front/pay.api +++ b/app/main/api/desc/front/pay.api @@ -45,6 +45,10 @@ service main { @handler PaymentCheck post /pay/check (PaymentCheckReq) returns (PaymentCheckResp) + + // 小程序虚拟支付:上报微信/Apple 客户端提示与步骤(写入服务端日志并可选查微信单) + @handler XpayClientEvent + post /pay/xpay/client-event (XpayClientEventReq) returns (XpayClientEventResp) } type ( @@ -69,6 +73,23 @@ type ( WxOrderStatus int `json:"wx_order_status,optional"` // 微信侧订单 status WxOrderDetail string `json:"wx_order_detail,optional"` // query_order 原始 order 摘要(排查用) } + XpayClientEventReq { + OrderNo string `json:"order_no,optional"` + Stage string `json:"stage" validate:"required"` // prepay_ok, invoke, success, fail, check + EventType string `json:"event_type" validate:"required,oneof=info tip fail success"` + Message string `json:"message,optional"` // 微信/Apple 展示给用户的话术(非 HTTP 报错) + ErrMsg string `json:"err_msg,optional"` + ErrCode int `json:"err_code,optional"` + Errno int `json:"errno,optional"` + SignData string `json:"sign_data,optional"` + Device string `json:"device,optional"` + Extra string `json:"extra,optional"` + } + XpayClientEventResp { + WxOrderStatus int `json:"wx_order_status,optional"` + WxOrderDetail string `json:"wx_order_detail,optional"` + WxSyncError string `json:"wx_sync_error,optional"` + } ) type ( diff --git a/app/main/api/internal/handler/pay/xpayclienteventhandler.go b/app/main/api/internal/handler/pay/xpayclienteventhandler.go new file mode 100644 index 0000000..46da1f9 --- /dev/null +++ b/app/main/api/internal/handler/pay/xpayclienteventhandler.go @@ -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 XpayClientEventHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.XpayClientEventReq + 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.NewXpayClientEventLogic(r.Context(), svcCtx) + resp, err := l.XpayClientEvent(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/routes.go b/app/main/api/internal/handler/routes.go index 3a408cc..a67dc75 100644 --- a/app/main/api/internal/handler/routes.go +++ b/app/main/api/internal/handler/routes.go @@ -976,6 +976,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/pay/payment", Handler: pay.PaymentHandler(serverCtx), }, + { + Method: http.MethodPost, + Path: "/pay/xpay/client-event", + Handler: pay.XpayClientEventHandler(serverCtx), + }, }..., ), rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), diff --git a/app/main/api/internal/logic/pay/xpayclienteventlogic.go b/app/main/api/internal/logic/pay/xpayclienteventlogic.go new file mode 100644 index 0000000..84a2931 --- /dev/null +++ b/app/main/api/internal/logic/pay/xpayclienteventlogic.go @@ -0,0 +1,65 @@ +package pay + +import ( + "context" + + "qnc-server/app/main/api/internal/svc" + "qnc-server/app/main/api/internal/types" + "qnc-server/common/ctxdata" + + "github.com/zeromicro/go-zero/core/logx" +) + +type XpayClientEventLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewXpayClientEventLogic(ctx context.Context, svcCtx *svc.ServiceContext) *XpayClientEventLogic { + return &XpayClientEventLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// XpayClientEvent 记录小程序虚拟支付各步骤及微信/Apple 客户端提示(非 HTTP 异常) +func (l *XpayClientEventLogic) XpayClientEvent(req *types.XpayClientEventReq) (*types.XpayClientEventResp, error) { + userID, _ := ctxdata.GetUidFromCtx(l.ctx) + + l.Infof("[xpay:client-event] user=%s order_no=%s stage=%s event=%s message=%q err_msg=%q err_code=%d errno=%d sign_data=%s device=%s extra=%s", + userID, req.OrderNo, req.Stage, req.EventType, req.Message, req.ErrMsg, req.ErrCode, req.Errno, + req.SignData, req.Device, req.Extra) + + resp := &types.XpayClientEventResp{} + + if req.OrderNo == "" || l.svcCtx.XpayService == nil || !l.svcCtx.XpayService.Enabled() { + return resp, nil + } + + // 客户端 fail/tip 时主动 query_order,把微信服务端侧状态一并记入日志 + if req.EventType == "fail" || req.EventType == "tip" { + openid, err := l.svcCtx.XpayService.GetWxMiniOpenID(l.ctx, l.svcCtx.UserAuthModel, userID) + if err != nil { + resp.WxSyncError = "获取 openid 失败: " + err.Error() + l.Errorf("[xpay:client-event] query_order skip order_no=%s reason=%v", req.OrderNo, err) + return resp, nil + } + + status, qErr := l.svcCtx.XpayService.QueryOrder(l.ctx, openid, req.OrderNo, "") + if qErr != nil { + resp.WxSyncError = qErr.Error() + l.Errorf("[xpay:client-event] query_order FAIL order_no=%s client_msg=%q wx_err=%v", + req.OrderNo, req.Message, qErr) + return resp, nil + } + + resp.WxOrderStatus = status.Status + resp.WxOrderDetail = status.RawOrder + l.Infof("[xpay:client-event] query_order SNAPSHOT order_no=%s client_msg=%q wx_status=%d wx_order_id=%s paid_fee=%d err_msg=%s raw=%s", + req.OrderNo, req.Message, status.Status, status.WxOrderID, status.PaidFee, status.ErrMsg, status.RawOrder) + } + + return resp, nil +} diff --git a/app/main/api/internal/logic/pay/xpaypushlogic.go b/app/main/api/internal/logic/pay/xpaypushlogic.go index 6df15ac..ae6adcc 100644 --- a/app/main/api/internal/logic/pay/xpaypushlogic.go +++ b/app/main/api/internal/logic/pay/xpaypushlogic.go @@ -7,6 +7,7 @@ import ( "qnc-server/app/main/api/internal/service" "qnc-server/app/main/api/internal/svc" + "github.com/zeromicro/go-zero/core/logx" ) @@ -61,6 +62,9 @@ func (l *XpayPushLogic) HandlePOST(w http.ResponseWriter, r *http.Request) { } ctx := r.Context() + logx.WithContext(ctx).Infof("[xpay:push] recv event=%s order_no=%s openid=%s env=%d body=%s", + notify.Event, orderNo, notify.OpenId, notify.Env, string(body)) + already, _ := l.svcCtx.XpayService.AlreadyNotified(ctx, orderNo) if already { l.writePushResp(w, 0, "already notified") diff --git a/app/main/api/internal/types/types.go b/app/main/api/internal/types/types.go index 421bfc1..99d4076 100644 --- a/app/main/api/internal/types/types.go +++ b/app/main/api/internal/types/types.go @@ -1935,6 +1935,25 @@ type PaymentCheckResp struct { WxOrderDetail string `json:"wx_order_detail,optional"` // query_order 原始 order 摘要(排查用) } +type XpayClientEventReq struct { + OrderNo string `json:"order_no,optional"` + Stage string `json:"stage" validate:"required"` // prepay_ok, invoke, success, fail, check + EventType string `json:"event_type" validate:"required,oneof=info tip fail success"` + Message string `json:"message,optional"` // 微信/Apple 展示给用户的话术 + ErrMsg string `json:"err_msg,optional"` + ErrCode int `json:"err_code,optional"` + Errno int `json:"errno,optional"` + SignData string `json:"sign_data,optional"` + Device string `json:"device,optional"` + Extra string `json:"extra,optional"` +} + +type XpayClientEventResp struct { + WxOrderStatus int `json:"wx_order_status,optional"` + WxOrderDetail string `json:"wx_order_detail,optional"` + WxSyncError string `json:"wx_sync_error,optional"` +} + type PaymentReq struct { Id string `json:"id"` PayMethod string `json:"pay_method"` // 支付方式: wechat, alipay, appleiap, test(仅开发环境), test_empty(仅开发环境-空报告模式)