彩虹易支付Go语言Gin框架对接指南:高效支付系统实现方案
引言
在当今数字化时代,支付接口对接是各类应用开发中不可或缺的环节。对于Go语言开发者而言,如何高效地对接第三方支付接口是一个常见的挑战。本文将详细介绍如何使用Go语言的Gin框架对接彩虹易支付接口,提供完整的订单管理、状态同步和超时处理解决方案。
彩虹易支付简介
彩虹易支付是一款开源的支付系统,支持多种支付渠道(如支付宝、微信支付、QQ支付等),提供简单易用的API接口,适合个人和中小企业快速集成支付功能。其核心优势包括:
- 支持多种支付方式
- 提供异步通知和主动查询两种订单状态同步机制
- 完善的签名验证机制保障交易安全
简洁明了的API文档便于开发
系统架构设计
我们的支付系统采用分层架构设计,主要包含以下模块:
- 模型层:定义支付配置和订单数据结构
- 控制器层:处理支付请求、回调通知和订单监控
- 服务层:封装第三方支付接口调用逻辑
- 工具层:提供签名生成、订单号生成等基础工具
核心依赖包
以下是实现该支付系统所需的主要Go依赖包:
import (
"github.com/gin-gonic/gin" // Web框架
"github.com/go-gorm/gorm" // ORM库
"github.com/robfig/cron/v3" // 定时任务库
"github.com/google/uuid" // UUID生成
"crypto/md5" // 签名加密
"encoding/hex" // 进制转换
"net/http" // HTTP客户端
"encoding/json" // JSON处理
"strconv" // 字符串转换
"time" // 时间处理
"math/rand" // 随机数生成
"strings" // 字符串操作
"sort" // 排序
"math" // 数学运算
)
数据库模型设计
我们需要定义两个核心模型:支付配置和支付订单。
// 支付配置模型
type PayConfig struct {
ID uint `gorm:"primaryKey;comment:ID" json:"id"`
Pid uint `gorm:"comment:商户ID" json:"pid"`
PayKey string `gorm:"size:128;comment:商户密钥" json:"payKey"`
PayUrl string `gorm:"size:64;接口地址" json:"payUrl"`
NotifyUrl string `gorm:"comment:异步通知地址" json:"notifyUrl"`
ReturnUrl string `gorm:"comment:跳转通知地址" json:"returnUrl"`
CreateTime utils.HTime `gorm:"comment:创建时间" json:"createTime"`
UpdateTime utils.HTime `gorm:"comment:更新时间;autoUpdateTime" json:"updateTime"`
}
// 支付订单模型
type PayOrder struct {
ID uint `gorm:"primaryKey;comment:ID" json:"id"`
TradeNo string `gorm:"size:64;comment:订单号" json:"tradeNo"`
Type string `gorm:"size:16;comment:支付方式" json:"type"`
Name string `gorm:"size:127;comment:商品名称" json:"name"`
Money float64 `gorm:"type:decimal(10,2);comment:商品金额" json:"money"`
Username string `gorm:"comment:用户账号" json:"username"`
Status int `gorm:"default:1;comment:支付状态(1->未支付,2->已支付,3->已取消)" json:"status"`
CreateTime utils.HTime `gorm:"comment:创建时间" json:"createTime"`
PayTime utils.HTime `gorm:"comment:支付时间" json:"payTime"`
}
支付流程实现
整个支付流程包括订单创建、支付请求、异步通知和订单状态监控四个主要环节。
1. 创建支付订单
// 创建支付订单
func CreatePayOrder(db *gorm.DB, payConfig *model.PayConfig) gin.HandlerFunc {
return func(c *gin.Context) {
// 1. 获取请求参数
var req struct {
Type string `form:"type" binding:"required,oneof=alipay wxpay qqpay"`
Name string `form:"name" binding:"required"`
Money float64 `form:"money" binding:"gt=0"`
}
if err := c.ShouldBind(&req); err != nil {
result.Failed(c, int(result.ApiCode.QueShaoCanShu), "参数错误:"+err.Error())
return
}
// 2. 从JWT获取用户名
username, exists := c.Get("username")
if !exists {
result.Failed(c, int(result.ApiCode.Failed), "未登录或登录已过期")
return
}
// 3. 验证用户是否存在
var user model.User
if err := db.Where("username = ?", username).First(&user).Error; err != nil {
result.Failed(c, int(result.ApiCode.UserNotExist), "用户不存在")
return
}
// 4. 开启数据库事务
err := db.Transaction(func(tx *gorm.DB) error {
// 生成临时本地订单号
tempLocalTradeNo := generatePAYTradeNo()
// 准备第三方支付参数
paymentParams := map[string]string{
"pid": strconv.Itoa(int(payConfig.Pid)),
"type": req.Type,
"out_trade_no": tempLocalTradeNo,
"notify_url": payConfig.NotifyUrl,
"return_url": payConfig.ReturnUrl,
"name": req.Name,
"money": fmt.Sprintf("%.2f", req.Money),
"clientip": c.ClientIP(),
"device": "pc",
"param": "",
}
// 生成签名
sign := generatePaymentSign(paymentParams, payConfig.PayKey)
paymentParams["sign"] = sign
paymentParams["sign_type"] = "MD5"
// 调用第三方支付接口
payResult, innerErr := sendPaymentRequest(paymentParams, payConfig.PayUrl+"/mapi.php")
if innerErr != nil {
return fmt.Errorf("调用第三方接口失败: %w", innerErr)
}
// 检查第三方接口返回状态
code, ok := payResult["code"].(float64)
if !ok || code != 1 {
msg, _ := payResult["msg"].(string)
return fmt.Errorf("第三方创建订单失败: code=%v, msg=%s", code, msg)
}
// 提取第三方生成的订单号
thirdTradeNo, ok := payResult["trade_no"].(string)
if !ok || thirdTradeNo == "" {
return fmt.Errorf("第三方未返回有效订单号,响应: %v", payResult)
}
// 创建本地订单
order = model.PayOrder{
TradeNo: thirdTradeNo,
Type: req.Type,
Name: req.Name,
Money: req.Money,
Username: user.Username,
CreateTime: utils.HTime{Time: time.Now()},
Status: constant.OrderNotPay,
}
// 保存本地订单到数据库
if err := tx.Create(&order).Error; err != nil {
return fmt.Errorf("本地订单保存失败: %w", err)
}
return nil
})
// 处理事务结果
if err != nil {
result.Failed(c, int(result.ApiCode.Failed), "创建支付订单失败: "+err.Error())
return
}
// 返回成功响应
result.Success(c, gin.H{
"order": order,
"payResult": payResult,
"msg": "订单创建成功,请完成支付",
})
}
}
2. 处理异步通知
// 处理支付结果通知
func NotifyPayOrder(db *gorm.DB, payConfig *model.PayConfig) gin.HandlerFunc {
return func(c *gin.Context) {
// 获取所有GET参数
params := make(map[string]string)
for key, values := range c.Request.URL.Query() {
if len(values) > 0 {
params[key] = values[0]
}
}
// 获取第三方订单号
thirdTradeNo := params["trade_no"]
if thirdTradeNo == "" {
c.String(http.StatusBadRequest, "missing trade_no")
return
}
// 验证签名
sign := params["sign"]
if sign == "" {
c.String(http.StatusBadRequest, "missing sign")
return
}
// 验证签名是否正确
if !verifyPaymentSign(params, payConfig.PayKey, sign) {
c.String(http.StatusBadRequest, "invalid sign")
return
}
// 验证支付状态
tradeStatus := params["trade_status"]
if tradeStatus != "TRADE_SUCCESS" {
c.String(http.StatusOK, "success")
return
}
// 开启数据库事务
err := db.Transaction(func(tx *gorm.DB) error {
// 用第三方订单号查询本地订单
var order model.PayOrder
if err := tx.Where("trade_no = ?", thirdTradeNo).First(&order).Error; err != nil {
return fmt.Errorf("查询本地订单失败: %w", err)
}
// 检查订单状态,防止重复处理
if order.Status == constant.OrderPaySuccess {
return nil
}
// 验证金额
orderMoney := order.Money
notifyMoney, err := strconv.ParseFloat(params["money"], 64)
if err != nil {
return fmt.Errorf("解析通知金额失败: %v", err)
}
// 使用容差值比较两个浮点数
const epsilon = 0.0001
if math.Abs(orderMoney-notifyMoney) > epsilon {
return fmt.Errorf("金额不匹配: 订单金额=%.2f, 通知金额=%.2f", orderMoney, notifyMoney)
}
// 更新订单状态为已支付
order.Status = constant.OrderPaySuccess
order.PayTime = utils.HTime{Time: time.Now()}
// 更新订单信息
return tx.Save(&order).Error
})
if err != nil {
log.Printf("处理支付通知失败: %v", err)
c.String(http.StatusInternalServerError, "处理通知失败")
return
}
// 返回success表示接收成功
c.String(http.StatusOK, "success")
}
}
3. 订单状态主动查询
// 查询订单状态
func QueryPayOrder(payConfig *model.PayConfig, tradeNo string) (map[string]interface{}, error) {
// 准备查询参数
queryParams := url.Values{}
queryParams.Add("act", "order")
queryParams.Add("pid", strconv.Itoa(int(payConfig.Pid)))
queryParams.Add("key", payConfig.PayKey)
queryParams.Add("trade_no", tradeNo) // 使用第三方订单号查询
// 构建完整URL
queryUrl := fmt.Sprintf("%s/api.php?%s",
payConfig.PayUrl,
queryParams.Encode())
// 发送GET请求
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest("GET", queryUrl, nil)
if err != nil {
return nil, fmt.Errorf("创建查询请求失败: %w", err)
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("发送查询请求失败: %w", err)
}
defer resp.Body.Close()
// 解析JSON响应
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("解析查询结果失败: %w", err)
}
// 验证响应签名
if sign, ok := result["sign"].(string); ok {
verifyParams := make(map[string]string)
for k, v := range result {
if k != "sign" {
if strVal, ok := v.(string); ok {
verifyParams[k] = strVal
}
}
}
verifySign := generatePaymentSign(verifyParams, payConfig.PayKey)
if verifySign != sign {
return nil, fmt.Errorf("响应签名验证失败")
}
}
// 检查API返回状态码
code, ok := result["code"].(float64)
if !ok {
return nil, fmt.Errorf("响应中缺少code字段: %v", result)
}
if code != 1 {
msg, _ := result["msg"].(string)
if msg == "" {
msg = "未知错误"
}
return nil, fmt.Errorf("API调用失败: code=%v, msg=%s", code, msg)
}
// 确保包含支付状态字段
if _, ok := result["status"]; !ok {
return nil, fmt.Errorf("响应中缺少status字段: %v", result)
}
return result, nil
}
4. 订单状态监控任务
// 启动支付订单监控任务
func StartPayOrderMonitor(payConfig *model.PayConfig) {
c := cron.New(cron.WithSeconds())
// 每5秒执行一次
_, err := c.AddFunc("@every 5s", func() {
monitorUnpaidOrders(payConfig)
})
if err != nil {
global.Log.Errorf("启动支付监控任务失败: %v", err)
return
}
c.Start()
global.Log.Infof("支付订单监控任务已启动(每5秒执行一次,检查所有未支付订单)")
}
// 监控未支付订单
func monitorUnpaidOrders(payConfig *model.PayConfig) {
global.Log.Infof("开始检查未支付订单...")
var unpaidOrders []model.PayOrder
// 查询所有未支付订单
if err := core.Db.Where("status = ?", constant.OrderNotPay).Find(&unpaidOrders).Error; err != nil {
global.Log.Errorf("查询未支付订单失败: %v", err)
return
}
global.Log.Infof("发现 %d 个未支付订单,开始同步状态...", len(unpaidOrders))
for _, order := range unpaidOrders {
global.Log.Debugf("开始同步订单状态: 订单号=%s, 创建时间=%v", order.TradeNo, order.CreateTime)
// 计算订单创建时间是否超过15分钟
fifteenMinutesAgo := time.Now().Add(-15 * time.Minute)
if order.CreateTime.Time.Before(fifteenMinutesAgo) {
global.Log.Infof("订单超时未支付(>15分钟),标记为取消: %s", order.TradeNo)
cancelOrder(order.TradeNo)
continue
}
// 跳过刚创建的订单(30秒内)
if time.Since(order.CreateTime.Time) < 30*time.Second {
global.Log.Debugf("订单创建时间过近(<30秒),跳过本次查询: %s", order.TradeNo)
continue
}
// 查询第三方支付状态
payResult, err := QueryPayOrder(payConfig, order.TradeNo)
if err != nil {
if strings.Contains(err.Error(), "订单号不存在") {
global.Log.Warnf("第三方订单不存在,标记为取消: %s", order.TradeNo)
cancelOrder(order.TradeNo)
} else {
global.Log.Errorf("查询订单状态失败(将重试): 订单号=%s, 错误=%v", order.TradeNo, err)
}
continue
}
// 解析第三方支付状态
statusVal, statusExists := payResult["status"]
if !statusExists {
global.Log.Warnf("响应中缺少status字段: 订单号=%s", order.TradeNo)
continue
}
// 尝试多种类型转换
var status int
switch v := statusVal.(type) {
case float64:
status = int(v)
case string:
var err error
status, err = strconv.Atoi(v)
if err != nil {
global.Log.Warnf("status字段格式错误: 订单号=%s, 值=%v", order.TradeNo, v)
continue
}
case int:
status = v
case int64:
status = int(v)
default:
global.Log.Warnf("status字段类型不支持: 订单号=%s, 类型=%T", order.TradeNo, v)
continue
}
// 根据状态更新本地订单
if status == 1 {
global.Log.Infof("第三方确认已支付: 订单号=%s", order.TradeNo)
updateOrderToPaid(order.TradeNo, payResult)
} else {
global.Log.Debugf("第三方确认未支付: 订单号=%s, 状态=%d", order.TradeNo, status)
}
}
global.Log.Infof("未支付订单状态同步完成")
}
签名验证机制
签名验证是支付系统安全的关键环节,我们需要实现严格的签名生成和验证算法。
// 生成支付签名
func generatePaymentSign(params map[string]string, secretKey string) string {
// 1. 过滤空值和排除sign、sign_type
var keys []string
for k, v := range params {
if k != "sign" && k != "sign_type" && v != "" {
keys = append(keys, k)
}
}
// 2. 按参数名ASCII排序
sort.Strings(keys)
// 3. 拼接参数为key=value&key=value格式
var paramStr string
for i, k := range keys {
if i > 0 {
paramStr += "&"
}
paramStr += fmt.Sprintf("%s=%s", k, params[k])
}
// 4. 拼接密钥并MD5加密
paramStr += secretKey
h := md5.New()
h.Write([]byte(paramStr))
return hex.EncodeToString(h.Sum(nil))
}
// 验证支付通知签名
func verifyPaymentSign(params map[string]string, secretKey string, receivedSign string) bool {
// 复制参数,排除sign字段
paramsToSign := make(map[string]string)
for k, v := range params {
if k != "sign" {
paramsToSign[k] = v
}
}
// 生成签名
generatedSign := generatePaymentSign(paramsToSign, secretKey)
// 比较签名
return generatedSign == receivedSign
}
系统部署与配置
- 配置数据库连接:确保GORM正确连接到MySQL数据库
- 初始化支付配置:在数据库中插入彩虹易支付的商户信息
- 启动服务:运行Gin应用,监听HTTP请求
- 配置定时任务:启动订单监控任务,确保订单状态及时同步
优化与扩展建议
- 限流与熔断:添加对第三方接口的限流和熔断机制,防止频繁请求导致被封
- 批量处理:对于大量未支付订单,采用分批处理策略,避免内存溢出
- 告警机制:添加异常告警,当出现大量签名验证失败或订单状态异常时及时通知管理员
- 数据统计:添加支付数据统计功能,方便业务分析
总结
通过本文的实现方案,我们成功完成了彩虹易支付与Go语言Gin框架的对接。该方案充分利用了Gin框架的高性能和灵活性,结合彩虹易支付的强大功能,为开发者提供了一个完整、高效的支付系统解决方案。系统涵盖了订单创建、支付、异步通知和主动查询等核心环节,并通过定时任务确保订单状态的最终一致性。
这个方案不仅适用于彩虹易支付,也可以作为其他支付接口对接的参考框架,开发者可以根据具体需求进行相应的调整和扩展。