Files
BinancePayTool/server/cashier.go
T
qianyubtcandClaude Fable 5 f87ec65541 BinancePayTool v0.1.0:币安支付个人收款网关(Go 核心 + 四语言 SDK)
- Go 服务端:唯一金额/备注码/回填订单编号三路匹配、托管收银页、HMAC 回调七级重试、SQLite 单二进制
- SDK:Go / Java / PHP / Python,共享签名测试向量(docs/test_vectors.json)
- 跨语言 e2e(真实二进制 × Python SDK × mock 币安)、GitHub Actions CI、Dockerfile
- 协议文档 docs/protocol.md、调研与实测结论 docs/research.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-02 14:09:32 +08:00

119 lines
2.9 KiB
Go

package main
import (
_ "embed"
"encoding/json"
"errors"
"html/template"
"net/http"
"net/url"
"regexp"
"strings"
)
//go:embed templates/cashier.html
var cashierHTML string
var cashierTpl = template.Must(template.New("cashier").Parse(cashierHTML))
var reBinanceOrderID = regexp.MustCompile(`^[0-9]{6,20}$`)
func (a *App) handleCashier(w http.ResponseWriter, r *http.Request) {
o, err := a.st.OrderByToken(r.PathValue("token"))
if errors.Is(err, ErrNotFound) {
http.NotFound(w, r)
return
}
if err != nil {
http.Error(w, "internal error", 500)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
cashierTpl.Execute(w, map[string]any{
"PayAmount": fmtAmount(o.PayAmount),
"Currency": o.Currency,
"UID": a.cfg.BinanceUID,
"Email": a.cfg.ReceiveEmail,
"HasQR": a.cfg.QRImage != "",
"NoteCode": o.NoteCode,
"ExpiresAt": o.ExpiresAt,
"Token": o.Token,
})
}
func (a *App) redirectURL(o *Order) string {
if o.ReturnURL == "" || o.Status != "paid" {
return ""
}
sep := "?"
if strings.Contains(o.ReturnURL, "?") {
sep = "&"
}
return o.ReturnURL + sep + "order_id=" + url.QueryEscape(o.ID) +
"&merchant_order_id=" + url.QueryEscape(o.MerchantOrderID) + "&status=paid"
}
func (a *App) handleStatus(w http.ResponseWriter, r *http.Request) {
o, err := a.st.OrderByToken(r.PathValue("token"))
if errors.Is(err, ErrNotFound) {
http.NotFound(w, r)
return
}
if err != nil {
http.Error(w, "internal error", 500)
return
}
writeJSON(w, 200, map[string]any{
"status": o.Status,
"paid_at": o.PaidAt,
"redirect": a.redirectURL(o),
})
}
func (a *App) handleClaim(w http.ResponseWriter, r *http.Request) {
tok := r.PathValue("token")
o, err := a.st.OrderByToken(tok)
if errors.Is(err, ErrNotFound) {
http.NotFound(w, r)
return
}
if err != nil {
fail(w, 500, "ERR_INTERNAL", "内部错误")
return
}
if !a.allow("claim:"+tok, 5, 60) || !a.allow("claimip:"+clientIP(r), 20, 60) {
fail(w, 429, "ERR_RATE_LIMIT", "操作过于频繁")
return
}
var req struct {
BinanceOrderID string `json:"binance_order_id"`
}
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&req); err != nil {
fail(w, 400, "ERR_PARAM", "JSON 解析失败")
return
}
req.BinanceOrderID = strings.TrimSpace(req.BinanceOrderID)
if !reBinanceOrderID.MatchString(req.BinanceOrderID) {
fail(w, 400, "ERR_PARAM", "订单编号格式错误")
return
}
res, err := a.matcher.Claim(o, req.BinanceOrderID)
if err != nil {
fail(w, 502, "ERR_INTERNAL", "查询币安流水失败,请稍后再试")
return
}
ok(w, map[string]any{"code": res.Code, "status": res.Status})
}
func (a *App) handleQR(w http.ResponseWriter, r *http.Request) {
if a.cfg.QRImage == "" {
http.NotFound(w, r)
return
}
if _, err := a.st.OrderByToken(r.PathValue("token")); err != nil {
http.NotFound(w, r)
return
}
http.ServeFile(w, r, a.cfg.QRImage)
}