// cashier_page.go submit.php 直出的自包含收银台页(不 302、不依赖前端 SPA),
// 二维码由服务端生成 PNG data URL;状态经 /cashier-status 轮询。
package service
import (
"bytes"
"context"
"encoding/base64"
"html/template"
"strings"
"github.com/cyi-cc/fun"
"github.com/skip2/go-qrcode"
"github.com/cyi-cc/cyi-box/backend/internal/db"
"github.com/cyi-cc/cyi-box/backend/internal/store"
)
// cashierTmpl 收银台页面模板(新粗野主义风格,与前端 Cashier.vue 一致)。
var cashierTmpl = template.Must(template.New("cashier").Parse(`
收银台 · ¥{{.Money}}
{{or .Subject "商品订单"}}
¥{{.Money}}
{{if .QRDataURL}}

{{else}}
二维码生成失败,请刷新重试
{{end}}
支打开支付宝扫一扫完成付款
⏰ 支付剩余 --:--
`))
type cashierPageData struct {
TradeNo string
Subject string
Money string
QRDataURL string
ExpiredAt int64
Status int64
}
// renderCashier 渲染收银台 HTML(二维码内容 → PNG data URL)。
func renderCashier(c *fun.RouteCtx, o db.PayOrder) error {
d := cashierPageData{
TradeNo: o.TradeNo,
Subject: o.Subject,
Money: fenToYuan(o.Money),
ExpiredAt: o.ExpiredAt,
Status: o.Status,
}
if o.Qrcode != "" {
if png, err := qrcode.Encode(o.Qrcode, qrcode.Medium, 256); err == nil {
d.QRDataURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
}
}
var buf bytes.Buffer
if err := cashierTmpl.Execute(&buf, d); err != nil {
return err
}
c.RequestCtx.Response.Header.Set("Content-Type", "text/html; charset=utf-8")
c.RequestCtx.SetBody(buf.Bytes())
return nil
}
// PayCashierStatusHandler GET /cashier-status?trade_no=:收银台页轮询
// (公开接口,trade_no 不可枚举即凭证;前端经 /api 前缀代理访问)。
func PayCashierStatusHandler(st *store.Store) fun.RouteHandler {
return func(c *fun.RouteCtx) error {
status, returnURL, err := cashierStatusReply(st, strings.TrimSpace(c.Param("trade_no")))
if err != nil {
return epayJSON(c, map[string]any{"status": -1})
}
return epayJSON(c, map[string]any{"status": status, "return_url": returnURL})
}
}
// cashierStatusReply 收银台状态查询:订单态 + 已支付时的签名回跳地址(RPC 与页面轮询共用)。
func cashierStatusReply(st *store.Store, tradeNo string) (int64, string, error) {
o, err := st.Q().GetPayOrderByTradeNo(context.Background(), tradeNo)
if err != nil {
return 0, "", err
}
lazyCloseExpired(st, &o)
returnURL := ""
if o.Status == payStatusPaid && o.ReturnUrl != "" {
if pid, key, err := payMerchant(st); err == nil {
returnURL = buildSignedURL(o.ReturnUrl, notifyParams(o, pid), key)
}
}
return o.Status, returnURL, nil
}