Gin框架对接彩虹易支付接口完整实现方案
一、基础配置
首先创建支付配置结构体,用于存储商户信息:
type PayConfig struct {
Pid int // 商户ID
Key string // 商户密钥
NotifyUrl string // 异步通知地址
ReturnUrl string // 跳转通知地址
ApiUrl string // 接口地址
}
二、支付请求实现
1. 页面跳转支付实现
func PageJumpPay(c *gin.Context) {
var req struct {
PayType string `json:"pay_type"` // alipay/wxpay/qqpay
Amount float64 `json:"amount" binding:"required"`
OrderNo string `json:"order_no" binding:"required"`
Goods string `json:"goods" binding:"required"`
}
if err := c.ShouldBind(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
params := map[string]interface{}{
"pid": config.PayCfg.Pid,
"type": req.PayType,
"out_trade_no": req.OrderNo,
"notify_url": config.PayCfg.NotifyUrl,
"return_url": config.PayCfg.ReturnUrl,
"name": req.Goods,
"money": fmt.Sprintf("%.2f", req.Amount),
}
sign := GenerateSign(params)
params["sign"] = sign
params["sign_type"] = "MD5"
// 构建form表单自动提交
html := `<html><body><form id="payForm" action="https://pay.javait.cn/submit.php" method="post">`
for k, v := range params {
html += fmt.Sprintf(`<input type="hidden" name="%s" value="%v" />`, k, v)
}
html += `</form><script>document.getElementById('payForm').submit();</script></body></html>`
c.Data(200, "text/html; charset=utf-8", []byte(html))
}
2. API接口支付实现
func ApiPay(c *gin.Context) {
var req struct {
PayType string `json:"pay_type" binding:"required"`
Amount float64 `json:"amount" binding:"required"`
OrderNo string `json:"order_no" binding:"required"`
Goods string `json:"goods" binding:"required"`
ClientIP string `json:"client_ip" binding:"required"`
Device string `json:"device"`
}
if err := c.ShouldBind(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
params := map[string]interface{}{
"pid": config.PayCfg.Pid,
"type": req.PayType,
"out_trade_no": req.OrderNo,
"notify_url": config.PayCfg.NotifyUrl,
"name": req.Goods,
"money": fmt.Sprintf("%.2f", req.Amount),
"clientip": req.ClientIP,
"device": req.Device,
}
sign := GenerateSign(params)
params["sign"] = sign
params["sign_type"] = "MD5"
client := resty.New()
resp, err := client.R().
SetHeader("Content-Type", "application/x-www-form-urlencoded").
SetFormData(convertParams(params)).
Post("https://pay.javait.cn/mapi.php")
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
var result map[string]interface{}
if err := json.Unmarshal(resp.Body(), &result); err != nil {
c.JSON(500, gin.H{"error": "parse response error"})
return
}
// 验证返回签名
if !VerifyResponseSign(result) {
c.JSON(500, gin.H{"error": "sign verify failed"})
return
}
c.JSON(200, result)
}
三、签名与验证实现
1. 签名生成函数
func GenerateSign(params map[string]interface{}) string {
// 过滤空值和签名相关字段
filtered := make(map[string]string)
for k, v := range params {
if k == "sign" || k == "sign_type" {
continue
}
if val, ok := v.(string); ok && val == "" {
continue
}
filtered[k] = fmt.Sprintf("%v", v)
}
// 按key排序
keys := make([]string, 0, len(filtered))
for k := range filtered {
keys = append(keys, k)
}
sort.Strings(keys)
// 拼接字符串
var signStr string
for i, k := range keys {
if i > 0 {
signStr += "&"
}
signStr += k + "=" + filtered[k]
}
signStr += config.PayCfg.Key
// MD5加密
h := md5.New()
h.Write([]byte(signStr))
return hex.EncodeToString(h.Sum(nil))
}
2. 响应签名验证
func VerifyResponseSign(data map[string]interface{}) bool {
sign, ok := data["sign"].(string)
if !ok {
return false
}
// 复制一份数据并移除sign字段
params := make(map[string]interface{})
for k, v := range data {
if k != "sign" {
params[k] = v
}
}
// 重新生成签名
calculatedSign := GenerateSign(params)
return calculatedSign == sign
}
四、支付结果通知处理
1. 异步通知处理
func NotifyHandler(c *gin.Context) {
params := make(map[string]interface{})
if err := c.ShouldBind(¶ms); err != nil {
c.String(400, "fail")
return
}
// 验证签名
if !VerifyResponseSign(params) {
c.String(400, "fail")
return
}
// 验证支付状态
status, ok := params["trade_status"].(string)
if !ok || status != "TRADE_SUCCESS" {
c.String(400, "fail")
return
}
// 处理业务逻辑
orderNo := params["out_trade_no"].(string)
amount := params["money"].(string)
// TODO: 更新订单状态
c.String(200, "success")
}
2. 跳转通知处理
func ReturnHandler(c *gin.Context) {
params := make(map[string]interface{})
if err := c.ShouldBind(¶ms); err != nil {
c.Redirect(302, "/error?msg=参数错误")
return
}
// 验证签名
if !VerifyResponseSign(params) {
c.Redirect(302, "/error?msg=签名验证失败")
return
}
// 验证支付状态
status, ok := params["trade_status"].(string)
if !ok || status != "TRADE_SUCCESS" {
c.Redirect(302, "/error?msg=支付未成功")
return
}
// 跳转到支付成功页面
orderNo := params["out_trade_no"].(string)
c.Redirect(302, "/pay/success?order_no="+orderNo)
}
五、辅助函数
// 转换参数为字符串map
func convertParams(params map[string]interface{}) map[string]string {
result := make(map[string]string)
for k, v := range params {
result[k] = fmt.Sprintf("%v", v)
}
return result
}
// 获取客户端IP
func GetClientIP(c *gin.Context) string {
ip := c.Request.Header.Get("X-Forwarded-For")
if ip == "" {
ip = c.Request.RemoteAddr
}
return strings.Split(ip, ":")
}
六、路由配置
func SetupPayRoutes(r *gin.Engine) {
payGroup := r.Group("/pay")
{
payGroup.POST("/page", PageJumpPay) // 页面跳转支付
payGroup.POST("/api", ApiPay) // API接口支付
payGroup.POST("/notify", NotifyHandler) // 异步通知
payGroup.GET("/return", ReturnHandler) // 跳转通知
}
}
七、使用示例
1. 发起页面跳转支付
// 前端调用示例
POST /pay/page
Content-Type: application/json
{
"pay_type": "alipay",
"amount": 100.00,
"order_no": "ORDER123456",
"goods": "VIP会员"
}
2. 发起API支付
// 前端调用示例
POST /pay/api
Content-Type: application/json
{
"pay_type": "wxpay",
"amount": 100.00,
"order_no": "ORDER123456",
"goods": "VIP会员",
"client_ip": "192.168.1.100",
"device": "mobile"
}
3. 处理支付结果
支付平台会异步通知到配置的notify_url
,并同步跳转到return_url
,这两个URL分别对应我们实现的/pay/notify
和/pay/return
接口。
八、注意事项
- 安全性:确保商户密钥(Key)妥善保管,不要硬编码在代码中
- 幂等性:支付结果通知处理要实现幂等性,防止重复处理
- 日志记录:记录所有支付请求和通知的原始数据,便于排查问题
- 金额验证:处理通知时要验证金额与订单金额是否一致
- 超时设置:HTTP请求设置合理的超时时间
- 异常处理:做好各种异常情况的处理,如网络超时、签名错误等
以上实现完整覆盖了支付接口对接的所有环节,包括发起支付、签名验证、结果通知处理等关键功能,可以直接集成到您的Gin项目中。