package main
import (
"encoding/json"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
"yourproject/wantupay"
)
var pay *wantupay.Client
// 套餐表:金额永远查服务端的表,前端只传套餐名
var plans = map[string][2]string{
"small": {"0.01", "积分充值·体验"},
"big": {"100.00", "积分充值100元"},
}
// 支付渠道:填顽兔开户时给你开的那条。"alipay" 支付宝;"chinaums" 银联商务聚合码,
// 一张码微信/支付宝/云闪付都能扫。两条都开的可由前端传上来;传了没开通的渠道会报 42201
const channel = "alipay"
func main() {
var err error
pay, err = wantupay.NewClient(wantupay.Config{
BaseURL: os.Getenv("WANTU_PAY_BASE_URL"),
MerchantID: os.Getenv("WANTU_MERCHANT_ID"),
APISecret: os.Getenv("WANTU_API_SECRET"),
})
if err != nil {
log.Fatal(err)
}
http.HandleFunc("/recharge", recharge)
http.HandleFunc("/wantu/webhook", webhook)
http.HandleFunc("/orders/", queryOrder)
log.Fatal(http.ListenAndServe(":3000", nil))
}
func recharge(w http.ResponseWriter, r *http.Request) {
var body struct {
Plan string `json:"plan"`
UID int `json:"uid"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "参数错误", http.StatusBadRequest)
return
}
plan, ok := plans[body.Plan]
if !ok {
http.Error(w, "未知套餐", http.StatusBadRequest)
return
}
outTradeNo := "R" + strconv.FormatInt(time.Now().UnixNano(), 10) // 服务端生成,保证唯一;建议换成库表序列
order, err := pay.CreateOrder(r.Context(), wantupay.CreateOrderParams{
OutTradeNo: outTradeNo,
Amount: plan[0],
Subject: plan[1],
Channel: channel,
PayMode: "qrcode", // 充值页内嵌二维码;整页跳转用 "redirect"
Attach: "uid=" + strconv.Itoa(body.UID),
})
if err != nil {
log.Println("下单失败:", err)
http.Error(w, "下单失败", http.StatusBadGateway)
return
}
// 此时应把 outTradeNo、金额、用户、状态=待支付 落到你的订单表
writeJSON(w, map[string]string{
"out_trade_no": outTradeNo,
"channel": order.Channel,
"pay_url": order.PayURL,
})
}
func webhook(w http.ResponseWriter, r *http.Request) {
raw, err := io.ReadAll(r.Body) // 必须是请求体原文,验签要在任何 JSON 解析之前
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
event, err := pay.VerifyWebhook(r.Header, raw)
if err != nil {
w.WriteHeader(http.StatusBadRequest) // 验签失败:不处理业务,网关会重试
return
}
if event.EventType == wantupay.EventOrderPaid {
// 入账三件套:查到本地订单 → 金额一致 → 事务内幂等入账
// UPDATE orders SET status='paid' WHERE out_trade_no=$1 AND status='created'
// 更新行数为 1 才执行加积分;为 0 说明是重复回调,直接返回 200
log.Printf("收到支付成功: %s 金额 %s", event.Data.OutTradeNo, event.Data.Amount)
}
w.WriteHeader(http.StatusOK) // 不认识的事件类型也回 200,不要报错
}
func queryOrder(w http.ResponseWriter, r *http.Request) {
outTradeNo := strings.TrimPrefix(r.URL.Path, "/orders/")
order, err := pay.QueryOrder(r.Context(), wantupay.OrderRef{OutTradeNo: outTradeNo})
if err != nil {
http.Error(w, "查询失败", http.StatusBadGateway)
return
}
writeJSON(w, map[string]string{"status": order.Status})
}
func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(v)
}