- submit.php 验签建单后不再 302,直接渲染自包含收银台 HTML, 支付宝二维码由服务端 skip2/go-qrcode 生成 PNG data URL 内嵌 - 新增公开轮询端点 /cashier-status(页面经 /api 前缀访问), 与 PaySvc.CashierStatus 共用 cashierStatusReply - Cashier.vue 收银台页同步改版:取景框二维码、倒计时进度条, 去掉跳上游收银台的外链 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
188 lines
6.0 KiB
Go
188 lines
6.0 KiB
Go
// pay_routes.go 易支付兼容协议端点:/submit.php(验签建单+直出收银台页)、/mapi.php(JSON下单)、/api.php(查单)。
|
||
package service
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"strings"
|
||
|
||
"github.com/cyi-cc/fun"
|
||
|
||
"github.com/cyi-cc/cyi-box/backend/internal/payx"
|
||
"github.com/cyi-cc/cyi-box/backend/internal/store"
|
||
)
|
||
|
||
// paySubmitHandler GET/POST /submit.php:验签建单后直接渲染收银台页(无跳转)。
|
||
func PaySubmitHandler(st *store.Store, up *payx.Client) fun.RouteHandler {
|
||
return func(c *fun.RouteCtx) error {
|
||
order, err := epayCreateOrder(c, st, up)
|
||
if err != nil {
|
||
return epayFail(c, err)
|
||
}
|
||
o, err := st.Q().GetPayOrderByTradeNo(context.Background(), order.TradeNo)
|
||
if err != nil {
|
||
return epayFail(c, err)
|
||
}
|
||
return renderCashier(c, o)
|
||
}
|
||
}
|
||
|
||
// payMapiHandler GET/POST /mapi.php:验签建单,JSON 返回 payurl/qrcode。
|
||
func PayMapiHandler(st *store.Store, up *payx.Client) fun.RouteHandler {
|
||
return func(c *fun.RouteCtx) error {
|
||
order, err := epayCreateOrder(c, st, up)
|
||
if err != nil {
|
||
return epayFail(c, err)
|
||
}
|
||
payurl := order.Qrcode
|
||
if payurl == "" {
|
||
payurl = order.Payurl
|
||
}
|
||
return epayJSON(c, map[string]any{
|
||
"code": 1, "msg": "success",
|
||
"trade_no": order.TradeNo,
|
||
"out_trade_no": order.OutTradeNo,
|
||
"type": order.Channel,
|
||
"name": order.Subject,
|
||
"money": fenToYuan(order.Money),
|
||
"payurl": payurl,
|
||
"qrcode": order.Qrcode,
|
||
"urlscheme": order.Qrcode,
|
||
})
|
||
}
|
||
}
|
||
|
||
// PayApiHandler GET/POST /api.php:act=order 查单(pid+key 验签)。
|
||
func PayApiHandler(st *store.Store) fun.RouteHandler {
|
||
return func(c *fun.RouteCtx) error {
|
||
act := strings.TrimSpace(c.Param("act"))
|
||
pidStr := strings.TrimSpace(c.Param("pid"))
|
||
key := strings.TrimSpace(c.Param("key"))
|
||
merPid, merKey, err := payMerchant(st)
|
||
if err != nil || pidStr != merPid || key != merKey {
|
||
return epayJSON(c, map[string]any{"code": -1, "msg": "商户校验失败"})
|
||
}
|
||
if act != "order" {
|
||
return epayJSON(c, map[string]any{"code": -1, "msg": "不支持的 act"})
|
||
}
|
||
tradeNo := strings.TrimSpace(c.Param("trade_no"))
|
||
outNo := strings.TrimSpace(c.Param("out_trade_no"))
|
||
var order struct {
|
||
TradeNo string
|
||
OutTradeNo string
|
||
Subject string
|
||
Money int64
|
||
Status int64
|
||
}
|
||
if tradeNo != "" {
|
||
r, err := st.Q().GetPayOrderByTradeNo(context.Background(), tradeNo)
|
||
if err != nil {
|
||
return epayJSON(c, map[string]any{"code": -1, "msg": "订单不存在"})
|
||
}
|
||
order.TradeNo, order.OutTradeNo, order.Subject = r.TradeNo, r.OutTradeNo, r.Subject
|
||
order.Money, order.Status = r.Money, r.Status
|
||
} else if outNo != "" {
|
||
r, err := st.Q().GetPayOrderByOutNo(context.Background(), outNo)
|
||
if err != nil {
|
||
return epayJSON(c, map[string]any{"code": -1, "msg": "订单不存在"})
|
||
}
|
||
order.TradeNo, order.OutTradeNo, order.Subject = r.TradeNo, r.OutTradeNo, r.Subject
|
||
order.Money, order.Status = r.Money, r.Status
|
||
} else {
|
||
return epayJSON(c, map[string]any{"code": -1, "msg": "请传入 out_trade_no 或 trade_no"})
|
||
}
|
||
status := int64(0)
|
||
if order.Status == payStatusPaid {
|
||
status = 1
|
||
}
|
||
return epayJSON(c, map[string]any{
|
||
"code": 1, "msg": "查询订单号成功",
|
||
"trade_no": order.TradeNo,
|
||
"out_trade_no": order.OutTradeNo,
|
||
"name": order.Subject,
|
||
"money": fenToYuan(order.Money),
|
||
"status": status,
|
||
})
|
||
}
|
||
}
|
||
|
||
// epayOrderResult 建单结果(submit/mapi 共用)。
|
||
type epayOrderResult struct {
|
||
TradeNo string
|
||
OutTradeNo string
|
||
Channel string
|
||
Subject string
|
||
Money int64
|
||
Payurl string
|
||
Qrcode string
|
||
}
|
||
|
||
// epayCreateOrder submit/mapi 共用下单逻辑:参数校验 → 验签 → 幂等建单。
|
||
func epayCreateOrder(c *fun.RouteCtx, st *store.Store, up *payx.Client) (epayOrderResult, error) {
|
||
var out epayOrderResult
|
||
pid := strings.TrimSpace(c.Param("pid"))
|
||
merPid, merKey, err := payMerchant(st)
|
||
if err != nil || merPid == "" {
|
||
return out, errParam("商户未配置")
|
||
}
|
||
if pid == "" || pid != merPid {
|
||
return out, errParam("PID 参数错误")
|
||
}
|
||
channel := strings.TrimSpace(c.Param("type"))
|
||
if channel == "" {
|
||
channel = "alipay"
|
||
}
|
||
if channel != "alipay" {
|
||
return out, errParam("当前仅支持支付宝支付(alipay)")
|
||
}
|
||
outTradeNo := strings.TrimSpace(c.Param("out_trade_no"))
|
||
if outTradeNo == "" || len(outTradeNo) > 64 {
|
||
return out, errParam("商户订单号 out_trade_no 需为 1-64 位")
|
||
}
|
||
subject := strings.TrimSpace(c.Param("name"))
|
||
if subject == "" || len(subject) > 100 {
|
||
return out, errParam("商品名称 name 需为 1-100 个字符")
|
||
}
|
||
money, err := yuanToFen(c.Param("money"))
|
||
if err != nil || money <= 0 || money > payMaxFen {
|
||
return out, errParam("订单金额非法(0-2000 元)")
|
||
}
|
||
notifyURL := strings.TrimSpace(c.Param("notify_url"))
|
||
returnURL := strings.TrimSpace(c.Param("return_url"))
|
||
for _, u := range []string{notifyURL, returnURL} {
|
||
if u != "" && !strings.HasPrefix(u, "http://") && !strings.HasPrefix(u, "https://") {
|
||
return out, errParam("notify_url / return_url 须为合法 URL")
|
||
}
|
||
}
|
||
if !epayVerify(c.Data, merKey, strings.TrimSpace(c.Param("sign"))) {
|
||
return out, errParam("签名验证失败")
|
||
}
|
||
order, err := createPayOrder(st, up, createPayOrderIn{
|
||
OutTradeNo: outTradeNo, Channel: channel, Subject: subject,
|
||
Money: money, NotifyURL: notifyURL, ReturnURL: returnURL,
|
||
ClientIP: c.RequestCtx.RemoteIP().String(),
|
||
})
|
||
if err != nil {
|
||
return out, err
|
||
}
|
||
out.TradeNo, out.OutTradeNo, out.Channel = order.TradeNo, order.OutTradeNo, order.Channel
|
||
out.Subject, out.Money, out.Payurl, out.Qrcode = order.Subject, order.Money, order.Payurl, order.Qrcode
|
||
return out, nil
|
||
}
|
||
|
||
func epayJSON(c *fun.RouteCtx, obj map[string]any) error {
|
||
raw, err := json.Marshal(obj)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
c.RequestCtx.Response.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||
c.RequestCtx.Write(raw)
|
||
return nil
|
||
}
|
||
|
||
func epayFail(c *fun.RouteCtx, err error) error {
|
||
return epayJSON(c, map[string]any{"code": -1, "msg": err.Error()})
|
||
}
|
||
|
||
func errParam(msg string) error { return fun.Error(4001, msg) }
|