🎨 新增支付动态配置&优化支付回调&新增三方支付

This commit is contained in:
2025-12-09 21:47:22 +08:00
parent eaa5cdc100
commit 854c16e11c
26 changed files with 2963 additions and 72 deletions

480
utils/pay/client.go Normal file
View File

@@ -0,0 +1,480 @@
// Package pay 神奇支付SDK客户端
package pay
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
"git.echol.cn/loser/lckt/utils/pay/request"
"git.echol.cn/loser/lckt/utils/pay/resp"
)
// Config 客户端配置
type Config struct {
// PID 商户ID (必填)
PID int
// PrivateKey 商户私钥 (必填PEM格式)
PrivateKey string
// PlatformPubKey 平台公钥 (必填PEM格式)
PlatformPubKey string
// BaseURL API基础地址 (可选,默认 https://www.shenqiyzf.cn)
BaseURL string
// Timeout 请求超时时间 (可选默认30秒)
Timeout time.Duration
}
// Client 神奇支付客户端
type Client struct {
config Config
signer *Signer
httpClient *http.Client
}
// NewClient 创建客户端
func NewClient(config Config) (*Client, error) {
// 参数校验
if config.PID == 0 {
return nil, errors.New("商户ID(PID)不能为空")
}
if config.PrivateKey == "" {
return nil, errors.New("商户私钥不能为空")
}
if config.PlatformPubKey == "" {
return nil, errors.New("平台公钥不能为空")
}
// 默认值
if config.BaseURL == "" {
config.BaseURL = DefaultBaseURL
}
if config.Timeout == 0 {
config.Timeout = 30 * time.Second
}
// 创建签名器
signer, err := NewSigner(config.PrivateKey, config.PlatformPubKey)
if err != nil {
return nil, err
}
return &Client{
config: config,
signer: signer,
httpClient: &http.Client{
Timeout: config.Timeout,
},
}, nil
}
// doPost 发起POST请求
func (c *Client) doPost(path string, params map[string]string) (map[string]interface{}, error) {
// 添加公共参数
params["pid"] = strconv.Itoa(c.config.PID)
params["timestamp"] = strconv.FormatInt(time.Now().Unix(), 10)
// 签名
if err := c.signer.SignParams(params); err != nil {
return nil, fmt.Errorf("签名失败: %w", err)
}
// 构建表单
form := url.Values{}
for k, v := range params {
form.Set(k, v)
}
// 发起请求
apiURL := c.config.BaseURL + path
httpResp, err := c.httpClient.PostForm(apiURL, form)
if err != nil {
return nil, fmt.Errorf("请求失败: %w", err)
}
defer httpResp.Body.Close()
// 读取响应
body, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %w", err)
}
// 解析JSON
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("解析响应失败: %w, body: %s", err, string(body))
}
return result, nil
}
// parseAndVerify 解析响应并验签
func (c *Client) parseAndVerify(result map[string]interface{}, target interface{}) error {
// 转换为JSON再解析到目标结构
jsonBytes, err := json.Marshal(result)
if err != nil {
return fmt.Errorf("序列化响应失败: %w", err)
}
if err := json.Unmarshal(jsonBytes, target); err != nil {
return fmt.Errorf("解析响应失败: %w", err)
}
// 验签 (仅成功响应需要验签)
if code, ok := result["code"].(float64); ok && code == 0 {
if sign, ok := result["sign"].(string); ok && sign != "" {
if err := c.signer.VerifyParams(MapToStringMap(result)); err != nil {
return fmt.Errorf("验签失败: %w", err)
}
}
}
return nil
}
// ==================== 支付接口 ====================
// CreateOrder 统一下单
func (c *Client) CreateOrder(req *request.CreateOrder) (*resp.CreateOrder, error) {
params := map[string]string{
"method": req.Method,
"type": req.Type,
"out_trade_no": req.OutTradeNo,
"notify_url": req.NotifyURL,
"return_url": req.ReturnURL,
"name": req.Name,
"money": req.Money,
"clientip": req.ClientIP,
}
// 可选参数
if req.Device != "" {
params["device"] = req.Device
}
if req.Param != "" {
params["param"] = req.Param
}
if req.AuthCode != "" {
params["auth_code"] = req.AuthCode
}
if req.SubOpenid != "" {
params["sub_openid"] = req.SubOpenid
}
if req.SubAppid != "" {
params["sub_appid"] = req.SubAppid
}
if req.ChannelID > 0 {
params["channel_id"] = strconv.Itoa(req.ChannelID)
}
result, err := c.doPost(PathPayCreate, params)
if err != nil {
return nil, err
}
response := &resp.CreateOrder{}
if err := c.parseAndVerify(result, response); err != nil {
return nil, err
}
return response, nil
}
// BuildSubmitURL 构建页面跳转支付URL
func (c *Client) BuildSubmitURL(req *request.SubmitOrder) (string, error) {
params := map[string]string{
"pid": strconv.Itoa(c.config.PID),
"out_trade_no": req.OutTradeNo,
"notify_url": req.NotifyURL,
"return_url": req.ReturnURL,
"name": req.Name,
"money": req.Money,
"timestamp": strconv.FormatInt(time.Now().Unix(), 10),
}
// 可选参数
if req.Type != "" {
params["type"] = req.Type
}
if req.Param != "" {
params["param"] = req.Param
}
if req.ChannelID > 0 {
params["channel_id"] = strconv.Itoa(req.ChannelID)
}
// 签名
if err := c.signer.SignParams(params); err != nil {
return "", fmt.Errorf("签名失败: %w", err)
}
// 构建URL
u, _ := url.Parse(c.config.BaseURL + PathPaySubmit)
query := u.Query()
for k, v := range params {
query.Set(k, v)
}
u.RawQuery = query.Encode()
return u.String(), nil
}
// QueryOrder 订单查询
func (c *Client) QueryOrder(req *request.QueryOrder) (*resp.QueryOrder, error) {
params := make(map[string]string)
if req.TradeNo != "" {
params["trade_no"] = req.TradeNo
}
if req.OutTradeNo != "" {
params["out_trade_no"] = req.OutTradeNo
}
result, err := c.doPost(PathPayQuery, params)
if err != nil {
return nil, err
}
response := &resp.QueryOrder{}
if err := c.parseAndVerify(result, response); err != nil {
return nil, err
}
return response, nil
}
// Refund 订单退款
func (c *Client) Refund(req *request.Refund) (*resp.Refund, error) {
params := map[string]string{
"money": req.Money,
}
if req.TradeNo != "" {
params["trade_no"] = req.TradeNo
}
if req.OutTradeNo != "" {
params["out_trade_no"] = req.OutTradeNo
}
if req.OutRefundNo != "" {
params["out_refund_no"] = req.OutRefundNo
}
result, err := c.doPost(PathPayRefund, params)
if err != nil {
return nil, err
}
response := &resp.Refund{}
if err := c.parseAndVerify(result, response); err != nil {
return nil, err
}
return response, nil
}
// RefundQuery 退款查询
func (c *Client) RefundQuery(req *request.RefundQuery) (*resp.RefundQuery, error) {
params := make(map[string]string)
if req.RefundNo != "" {
params["refund_no"] = req.RefundNo
}
if req.OutRefundNo != "" {
params["out_refund_no"] = req.OutRefundNo
}
result, err := c.doPost(PathRefundQuery, params)
if err != nil {
return nil, err
}
response := &resp.RefundQuery{}
if err := c.parseAndVerify(result, response); err != nil {
return nil, err
}
return response, nil
}
// ==================== 通知处理 ====================
// VerifyNotify 验证支付通知 (从 url.Values 解析)
func (c *Client) VerifyNotify(values url.Values) (*resp.Notify, error) {
params := make(map[string]string)
for k := range values {
params[k] = values.Get(k)
}
return c.verifyNotifyFromMap(params)
}
// VerifyNotifyFromMap 验证支付通知 (从 map 解析)
func (c *Client) VerifyNotifyFromMap(params map[string]string) (*resp.Notify, error) {
return c.verifyNotifyFromMap(params)
}
// verifyNotifyFromMap 验证通知的内部实现
func (c *Client) verifyNotifyFromMap(params map[string]string) (*resp.Notify, error) {
// 验签
if err := c.signer.VerifyParams(params); err != nil {
return nil, fmt.Errorf("通知验签失败: %w", err)
}
// 验证交易状态
if params["trade_status"] != TradeStatusSuccess {
return nil, fmt.Errorf("交易状态异常: %s", params["trade_status"])
}
// 构建通知对象
notify := &resp.Notify{
TradeNo: params["trade_no"],
OutTradeNo: params["out_trade_no"],
ApiTradeNo: params["api_trade_no"],
Type: params["type"],
TradeStatus: params["trade_status"],
Addtime: params["addtime"],
Endtime: params["endtime"],
Name: params["name"],
Money: params["money"],
Param: params["param"],
Buyer: params["buyer"],
Timestamp: params["timestamp"],
Sign: params["sign"],
SignType: params["sign_type"],
}
if pidStr := params["pid"]; pidStr != "" {
if pid, err := strconv.Atoi(pidStr); err == nil {
notify.PID = pid
}
}
return notify, nil
}
// ==================== 商户接口 ====================
// GetMerchantInfo 查询商户信息
func (c *Client) GetMerchantInfo() (*resp.MerchantInfo, error) {
params := make(map[string]string)
result, err := c.doPost(PathMerchantInfo, params)
if err != nil {
return nil, err
}
response := &resp.MerchantInfo{}
if err := c.parseAndVerify(result, response); err != nil {
return nil, err
}
return response, nil
}
// GetOrderList 查询订单列表
func (c *Client) GetOrderList(req *request.OrderList) (*resp.OrderList, error) {
params := map[string]string{
"offset": strconv.Itoa(req.Offset),
"limit": strconv.Itoa(req.Limit),
}
if req.Status >= 0 {
params["status"] = strconv.Itoa(req.Status)
}
result, err := c.doPost(PathMerchantOrder, params)
if err != nil {
return nil, err
}
response := &resp.OrderList{}
if err := c.parseAndVerify(result, response); err != nil {
return nil, err
}
return response, nil
}
// ==================== 转账接口 ====================
// Transfer 发起转账
func (c *Client) Transfer(req *request.Transfer) (*resp.Transfer, error) {
params := map[string]string{
"type": req.Type,
"account": req.Account,
"money": req.Money,
}
if req.Name != "" {
params["name"] = req.Name
}
if req.Remark != "" {
params["remark"] = req.Remark
}
if req.OutBizNo != "" {
params["out_biz_no"] = req.OutBizNo
}
if req.BookID != "" {
params["bookid"] = req.BookID
}
result, err := c.doPost(PathTransfer, params)
if err != nil {
return nil, err
}
response := &resp.Transfer{}
if err := c.parseAndVerify(result, response); err != nil {
return nil, err
}
return response, nil
}
// TransferQuery 查询转账
func (c *Client) TransferQuery(req *request.TransferQuery) (*resp.TransferQuery, error) {
params := make(map[string]string)
if req.BizNo != "" {
params["biz_no"] = req.BizNo
}
if req.OutBizNo != "" {
params["out_biz_no"] = req.OutBizNo
}
result, err := c.doPost(PathTransferQuery, params)
if err != nil {
return nil, err
}
response := &resp.TransferQuery{}
if err := c.parseAndVerify(result, response); err != nil {
return nil, err
}
return response, nil
}
// GetBalance 查询可用余额
func (c *Client) GetBalance() (*resp.Balance, error) {
params := make(map[string]string)
result, err := c.doPost(PathBalance, params)
if err != nil {
return nil, err
}
response := &resp.Balance{}
if err := c.parseAndVerify(result, response); err != nil {
return nil, err
}
return response, nil
}

109
utils/pay/constants.go Normal file
View File

@@ -0,0 +1,109 @@
// Package pay 神奇支付SDK常量定义
package pay
// API 地址
const (
// DefaultBaseURL 默认API基础地址
DefaultBaseURL = "https://www.shenqiyzf.cn"
// API 路径
PathPayCreate = "/api/pay/create" // 统一下单
PathPaySubmit = "/api/pay/submit" // 页面跳转支付
PathPayQuery = "/api/pay/query" // 订单查询
PathPayRefund = "/api/pay/refund" // 订单退款
PathRefundQuery = "/api/pay/refundquery" // 退款查询
PathMerchantInfo = "/api/merchant/info" // 商户信息
PathMerchantOrder = "/api/merchant/orders" // 订单列表
PathTransfer = "/api/transfer/submit" // 转账发起
PathTransferQuery = "/api/transfer/query" // 转账查询
PathBalance = "/api/transfer/balance" // 余额查询
)
// 支付方式
const (
PayTypeAlipay = "alipay" // 支付宝
PayTypeWxpay = "wxpay" // 微信支付
PayTypeQQpay = "qqpay" // QQ钱包
)
// 接口类型 (Method)
const (
MethodWeb = "web" // 通用网页支付
MethodJump = "jump" // 跳转支付
MethodJsapi = "jsapi" // JSAPI支付(小程序/公众号)
MethodApp = "app" // APP支付
MethodScan = "scan" // 付款码支付
MethodApplet = "applet" // 小程序支付
)
// 设备类型
const (
DevicePC = "pc" // 电脑浏览器(默认)
DeviceMobile = "mobile" // 手机浏览器
DeviceQQ = "qq" // 手机QQ内浏览器
DeviceWechat = "wechat" // 微信内浏览器
DeviceAlipay = "alipay" // 支付宝客户端
)
// 发起支付类型 (PayType)
const (
PayTypeResultJump = "jump" // 返回支付跳转url
PayTypeResultHTML = "html" // 返回html代码
PayTypeResultQrcode = "qrcode" // 返回支付二维码
PayTypeResultURLScheme = "urlscheme" // 返回小程序跳转url scheme
PayTypeResultJsapi = "jsapi" // 返回JSAPI支付参数
PayTypeResultApp = "app" // 返回APP支付参数
PayTypeResultScan = "scan" // 付款码支付成功
PayTypeResultWxPlugin = "wxplugin" // 返回微信小程序插件参数
PayTypeResultWxApp = "wxapp" // 返回拉起微信小程序参数
)
// 订单支付状态
const (
OrderStatusUnpaid = 0 // 未支付
OrderStatusPaid = 1 // 已支付
OrderStatusRefunded = 2 // 已退款
OrderStatusFrozen = 3 // 已冻结
OrderStatusPreAuth = 4 // 预授权
)
// 退款状态
const (
RefundStatusFail = 0 // 退款失败
RefundStatusSuccess = 1 // 退款成功
)
// 转账状态
const (
TransferStatusProcessing = 0 // 正在处理
TransferStatusSuccess = 1 // 转账成功
TransferStatusFail = 2 // 转账失败
)
// 转账方式
const (
TransferTypeAlipay = "alipay" // 支付宝
TransferTypeWxpay = "wxpay" // 微信支付
TransferTypeQQpay = "qqpay" // QQ钱包
TransferTypeBank = "bank" // 银行卡
)
// 商户状态
const (
MerchantStatusBanned = 0 // 已封禁
MerchantStatusNormal = 1 // 正常
MerchantStatusPending = 2 // 待审核
)
// 结算方式
const (
SettleTypeAlipay = 1 // 支付宝
SettleTypeWechat = 2 // 微信
SettleTypeQQ = 3 // QQ钱包
SettleTypeBank = 4 // 银行卡
)
// 交易状态
const (
TradeStatusSuccess = "TRADE_SUCCESS" // 交易成功
)

313
utils/pay/notify.go Normal file
View File

@@ -0,0 +1,313 @@
// Package pay 神奇支付回调处理
package pay
import (
"errors"
"fmt"
"net/url"
"strconv"
"strings"
"time"
"git.echol.cn/loser/lckt/global"
"git.echol.cn/loser/lckt/model/app"
"git.echol.cn/loser/lckt/model/user"
"git.echol.cn/loser/lckt/model/vip"
"git.echol.cn/loser/lckt/utils/pay/resp"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// NotifyHandler 支付回调处理器
type NotifyHandler struct {
client *Client
}
// NewNotifyHandler 创建回调处理器
func NewNotifyHandler(client *Client) *NotifyHandler {
return &NotifyHandler{client: client}
}
// HandleNotify 处理支付回调通知
// 返回值: notify(通知内容), err(错误信息)
// 调用方应在 err == nil 时返回 "success" 给支付平台
func (h *NotifyHandler) HandleNotify(values url.Values) (*resp.Notify, error) {
// 1. 验签并解析通知
notify, err := h.client.VerifyNotify(values)
if err != nil {
global.GVA_LOG.Error("神奇支付回调验签失败", zap.Error(err))
return nil, fmt.Errorf("验签失败: %w", err)
}
// 2. 处理订单
if err := h.processOrder(notify); err != nil {
global.GVA_LOG.Error("神奇支付回调处理订单失败",
zap.String("out_trade_no", notify.OutTradeNo),
zap.Error(err))
return notify, err
}
global.GVA_LOG.Info("神奇支付回调处理成功",
zap.String("out_trade_no", notify.OutTradeNo),
zap.String("trade_no", notify.TradeNo),
zap.String("money", notify.Money))
return notify, nil
}
// HandleNotifyFromMap 从map处理支付回调通知
func (h *NotifyHandler) HandleNotifyFromMap(params map[string]string) (*resp.Notify, error) {
notify, err := h.client.VerifyNotifyFromMap(params)
if err != nil {
global.GVA_LOG.Error("神奇支付回调验签失败", zap.Error(err))
return nil, fmt.Errorf("验签失败: %w", err)
}
if err := h.processOrder(notify); err != nil {
global.GVA_LOG.Error("神奇支付回调处理订单失败",
zap.String("out_trade_no", notify.OutTradeNo),
zap.Error(err))
return notify, err
}
global.GVA_LOG.Info("神奇支付回调处理成功",
zap.String("out_trade_no", notify.OutTradeNo),
zap.String("trade_no", notify.TradeNo),
zap.String("money", notify.Money))
return notify, nil
}
// processOrder 处理订单业务逻辑(使用事务)
func (h *NotifyHandler) processOrder(notify *resp.Notify) error {
if notify.OutTradeNo == "" {
return errors.New("商户订单号为空")
}
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
// 1. 查询并锁定订单(防止并发)
var order app.Order
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("order_no = ?", notify.OutTradeNo).
First(&order).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("订单不存在: %s", notify.OutTradeNo)
}
return fmt.Errorf("查询订单失败: %w", err)
}
// 2. 幂等性检查:如果订单已支付,直接返回成功
if order.Status == 2 {
global.GVA_LOG.Info("订单已处理,跳过重复回调",
zap.String("order_no", notify.OutTradeNo))
return nil
}
// 3. 验证订单状态
if order.Status != 1 {
return fmt.Errorf("订单状态异常: %d", order.Status)
}
// 4. 更新订单状态为已支付
if err := tx.Model(&order).Updates(map[string]interface{}{
"status": 2,
"pay_type": h.getPayType(notify.Type),
}).Error; err != nil {
return fmt.Errorf("更新订单状态失败: %w", err)
}
// 5. 根据订单类型处理业务
switch order.OrderType {
case 1: // 课程订单
return h.handleCourseOrder(tx, &order)
case 2: // 全站VIP订单
return h.handleVipOrder(tx, &order)
case 3: // 讲师包月订单
return h.handleTeacherVipOrder(tx, &order)
default:
global.GVA_LOG.Warn("未知订单类型", zap.Int("order_type", order.OrderType))
return nil
}
})
}
// handleCourseOrder 处理课程订单
func (h *NotifyHandler) handleCourseOrder(tx *gorm.DB, order *app.Order) error {
// 计算讲师分成
return h.addTeacherCommission(tx, order.TeacherId, order.Price)
}
// handleVipOrder 处理VIP订单
func (h *NotifyHandler) handleVipOrder(tx *gorm.DB, order *app.Order) error {
// 1. 查询VIP信息
var vipInfo vip.Vip
if err := tx.Where("id = ?", order.VipId).First(&vipInfo).Error; err != nil {
return fmt.Errorf("查询VIP信息失败: %w", err)
}
// 2. 查询用户信息(锁定)
var userInfo user.User
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id = ?", order.UserId).
First(&userInfo).Error; err != nil {
return fmt.Errorf("查询用户信息失败: %w", err)
}
// 3. 计算会员过期时间
now := time.Now()
var expireTime time.Time
if userInfo.VipExpireTime != "" {
if parsed, err := time.Parse("2006-01-02", userInfo.VipExpireTime); err == nil && parsed.After(now) {
// 未过期,在原有基础上增加
expireTime = parsed.AddDate(0, 0, int(vipInfo.Expiration))
} else {
// 已过期或解析失败,从当前时间开始
expireTime = now.AddDate(0, 0, int(vipInfo.Expiration))
}
} else {
expireTime = now.AddDate(0, 0, int(vipInfo.Expiration))
}
// 4. 确定用户标签
userLabel := userInfo.UserLabel
if vipInfo.Level == 1 {
userLabel = 2 // VIP
} else if vipInfo.Level == 2 {
userLabel = 3 // SVIP
}
// 5. 更新用户信息
if err := tx.Model(&userInfo).Updates(map[string]interface{}{
"is_vip": 1,
"vip_expire_time": expireTime.Format("2006-01-02"),
"user_label": userLabel,
}).Error; err != nil {
return fmt.Errorf("更新用户VIP状态失败: %w", err)
}
return nil
}
// handleTeacherVipOrder 处理讲师包月订单
func (h *NotifyHandler) handleTeacherVipOrder(tx *gorm.DB, order *app.Order) error {
// 1. 解析讲师VIP ID列表
ids := strings.Split(order.TeacherVipId, ",")
now := time.Now()
for _, idStr := range ids {
idStr = strings.TrimSpace(idStr)
if idStr == "" {
continue
}
teacherVipId, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
global.GVA_LOG.Warn("解析讲师VIP ID失败", zap.String("id", idStr), zap.Error(err))
continue
}
// 2. 查询或创建用户讲师VIP记录
var teacherVip app.UserTeacherVip
err = tx.Where("teacher_id = ? AND user_id = ? AND teacher_vip_id = ?",
order.TeacherId, order.UserId, teacherVipId).
Order("id DESC").
First(&teacherVip).Error
if err == nil {
// 记录存在,计算新的过期时间
var newExpireAt time.Time
if expireTime, parseErr := time.Parse("2006-01-02 15:04:05", teacherVip.ExpireAt); parseErr == nil {
if teacherVip.IsExpire == 1 && expireTime.After(now) {
// 未过期,在原有基础上加一个月
newExpireAt = expireTime.AddDate(0, 1, 0)
} else {
// 已过期,从当前时间加一个月
newExpireAt = now.AddDate(0, 1, 0)
}
} else {
newExpireAt = now.AddDate(0, 1, 0)
}
// 更新记录
if err := tx.Model(&teacherVip).Updates(map[string]interface{}{
"expire_at": newExpireAt.Format("2006-01-02 15:04:05"),
"is_expire": 1,
}).Error; err != nil {
return fmt.Errorf("更新讲师VIP记录失败: %w", err)
}
} else if errors.Is(err, gorm.ErrRecordNotFound) {
// 记录不存在,创建新记录
newTeacherVip := app.UserTeacherVip{
TeacherId: uint(order.TeacherId),
UserId: uint(order.UserId),
TeacherVipId: uint(teacherVipId),
ExpireAt: now.AddDate(0, 1, 0).Format("2006-01-02 15:04:05"),
IsExpire: 1,
}
if err := tx.Create(&newTeacherVip).Error; err != nil {
return fmt.Errorf("创建讲师VIP记录失败: %w", err)
}
} else {
return fmt.Errorf("查询讲师VIP记录失败: %w", err)
}
}
// 3. 计算讲师分成
return h.addTeacherCommission(tx, order.TeacherId, order.Price)
}
// addTeacherCommission 增加讲师分成
func (h *NotifyHandler) addTeacherCommission(tx *gorm.DB, teacherId uint64, priceInCent int64) error {
if teacherId == 0 {
return nil
}
// 查询讲师信息(锁定)
var teacher user.User
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id = ?", teacherId).
First(&teacher).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
global.GVA_LOG.Warn("讲师不存在", zap.Uint64("teacher_id", teacherId))
return nil
}
return fmt.Errorf("查询讲师信息失败: %w", err)
}
// 计算分成金额 (价格单位是分,分成比例是百分比)
priceInYuan := float64(priceInCent) / 100.0
commission := priceInYuan * float64(teacher.ExpectRate) / 100.0
if commission <= 0 {
return nil
}
// 更新讲师余额
if err := tx.Model(&teacher).
Update("balance", gorm.Expr("balance + ?", commission)).Error; err != nil {
return fmt.Errorf("更新讲师余额失败: %w", err)
}
global.GVA_LOG.Info("讲师分成成功",
zap.Uint64("teacher_id", teacherId),
zap.Float64("commission", commission),
zap.Int("rate", teacher.ExpectRate))
return nil
}
// getPayType 根据支付方式获取支付类型
func (h *NotifyHandler) getPayType(payType string) int {
switch payType {
case PayTypeWxpay:
return 1 // 微信
case PayTypeAlipay:
return 2 // 支付宝
case PayTypeQQpay:
return 4 // QQ钱包
default:
return 1
}
}

View File

@@ -0,0 +1,159 @@
// Package request 神奇支付SDK请求结构体
package request
// CreateOrder 统一下单请求
type CreateOrder struct {
// Method 接口类型(必填)
// 可选值: web(通用网页支付) / jump(跳转支付) / jsapi(JSAPI支付) / app(APP支付) / scan(付款码支付) / applet(小程序支付)
Method string
// Device 设备类型(仅web需要)
// 可选值: pc(电脑浏览器) / mobile(手机浏览器) / qq(QQ浏览器) / wechat(微信浏览器) / alipay(支付宝客户端)
Device string
// Type 支付方式(必填)
// 可选值: alipay(支付宝) / wxpay(微信支付) / qqpay(QQ钱包)
Type string
// OutTradeNo 商户订单号(必填)
OutTradeNo string
// NotifyURL 异步通知地址(必填)
NotifyURL string
// ReturnURL 跳转通知地址(必填)
ReturnURL string
// Name 商品名称(必填)超过127字节会自动截取
Name string
// Money 商品金额(必填)单位最大2位小数
Money string
// ClientIP 用户IP地址(必填)
ClientIP string
// Param 业务扩展参数(可选),支付后原样返回
Param string
// AuthCode 被扫支付授权码(scan模式需要)
AuthCode string
// SubOpenid 用户Openid(jsapi需要)
SubOpenid string
// SubAppid 公众号/小程序AppId(jsapi需要)
SubAppid string
// ChannelID 自定义通道ID(可选)对应进件商户列表的ID
ChannelID int
}
// SubmitOrder 页面跳转支付请求(用于构建URL)
type SubmitOrder struct {
// Type 支付方式,不传会跳转到收银台
// 可选值: alipay(支付宝) / wxpay(微信支付) / qqpay(QQ钱包)
Type string
// OutTradeNo 商户订单号(必填)
OutTradeNo string
// NotifyURL 异步通知地址(必填)
NotifyURL string
// ReturnURL 跳转通知地址(必填)
ReturnURL string
// Name 商品名称(必填)
Name string
// Money 商品金额(必填),单位:元
Money string
// Param 业务扩展参数(可选)
Param string
// ChannelID 自定义通道ID(可选)
ChannelID int
}
// QueryOrder 订单查询请求
type QueryOrder struct {
// TradeNo 平台订单号(与商户订单号二选一)
TradeNo string
// OutTradeNo 商户订单号(与平台订单号二选一)
OutTradeNo string
}
// Refund 订单退款请求
type Refund struct {
// TradeNo 平台订单号(与商户订单号二选一)
TradeNo string
// OutTradeNo 商户订单号(与平台订单号二选一)
OutTradeNo string
// Money 退款金额(必填),单位:元
Money string
// OutRefundNo 商户退款单号(可选),可避免重复请求退款
OutRefundNo string
}
// RefundQuery 退款查询请求
type RefundQuery struct {
// RefundNo 平台退款单号(与商户退款单号二选一)
RefundNo string
// OutRefundNo 商户退款单号(与平台退款单号二选一)
OutRefundNo string
}
// OrderList 订单列表查询请求
type OrderList struct {
// Offset 查询偏移从0开始
Offset int
// Limit 每页条数最大50
Limit int
// Status 过滤订单状态(可选)
// -1: 不过滤, 0: 未支付, 1: 已支付
Status int
}
// Transfer 转账请求
type Transfer struct {
// Type 转账方式(必填)
// 可选值: alipay(支付宝) / wxpay(微信) / qqpay(QQ钱包) / bank(银行卡)
Type string
// Account 收款方账号(必填)
// 支付宝账号 / 微信OpenId / 银行卡号
Account string
// Name 收款方姓名(可选),传入则校验账号与姓名是否匹配
Name string
// Money 转账金额(必填),单位:元
Money string
// Remark 转账备注(可选)
Remark string
// OutBizNo 转账交易号(可选),可避免重复请求转账
OutBizNo string
// BookID 安全发账本ID(可选),仅支付宝安全发转账可传
BookID string
}
// TransferQuery 转账查询请求
type TransferQuery struct {
// BizNo 系统交易号(与商户交易号二选一)
BizNo string
// OutBizNo 商户交易号(与系统交易号二选一)
OutBizNo string
}

357
utils/pay/resp/response.go Normal file
View File

@@ -0,0 +1,357 @@
// Package resp 神奇支付SDK响应结构体
package resp
// Base 基础响应字段
type Base struct {
Code int `json:"code"` // 返回状态码0为成功
Msg string `json:"msg"` // 返回信息/错误信息
Timestamp string `json:"timestamp"` // 时间戳
Sign string `json:"sign"` // 签名
SignType string `json:"sign_type"` // 签名类型
}
// IsSuccess 判断是否成功
func (b *Base) IsSuccess() bool {
return b.Code == 0
}
// CreateOrder 统一下单响应
type CreateOrder struct {
Base
// TradeNo 平台订单号
TradeNo string `json:"trade_no"`
// PayType 发起支付类型
// jump: 跳转url / html: html代码 / qrcode: 二维码 / urlscheme: 小程序url scheme
// jsapi: JSAPI参数 / app: APP支付参数 / scan: 付款码成功 / wxplugin: 小程序插件 / wxapp: 拉起小程序
PayType string `json:"pay_type"`
// PayInfo 发起支付参数根据PayType不同返回不同内容
PayInfo string `json:"pay_info"`
}
// QueryOrder 订单查询响应
type QueryOrder struct {
Base
// TradeNo 平台订单号
TradeNo string `json:"trade_no"`
// OutTradeNo 商户订单号
OutTradeNo string `json:"out_trade_no"`
// ApiTradeNo 接口订单号(微信/支付宝返回的单号)
ApiTradeNo string `json:"api_trade_no"`
// Type 支付方式
Type string `json:"type"`
// Status 支付状态
// 0: 未支付 / 1: 已支付 / 2: 已退款 / 3: 已冻结 / 4: 预授权
Status int `json:"status"`
// PID 商户ID
PID int `json:"pid"`
// Addtime 订单创建时间
Addtime string `json:"addtime"`
// Endtime 订单完成时间(仅支付完成才返回)
Endtime string `json:"endtime"`
// Name 商品名称
Name string `json:"name"`
// Money 商品金额
Money string `json:"money"`
// RefundMoney 已退款金额(仅部分退款情况返回)
RefundMoney string `json:"refundmoney"`
// Param 业务扩展参数
Param string `json:"param"`
// Buyer 支付用户标识(一般为openid)
Buyer string `json:"buyer"`
// ClientIP 支付用户IP
ClientIP string `json:"clientip"`
}
// IsPaid 判断是否已支付
func (q *QueryOrder) IsPaid() bool {
return q.Status == 1
}
// IsRefunded 判断是否已退款
func (q *QueryOrder) IsRefunded() bool {
return q.Status == 2
}
// Refund 订单退款响应
type Refund struct {
Base
// RefundNo 平台退款单号
RefundNo string `json:"refund_no"`
// OutRefundNo 商户退款单号
OutRefundNo string `json:"out_refund_no"`
// TradeNo 平台订单号
TradeNo string `json:"trade_no"`
// Money 退款金额
Money string `json:"money"`
// ReduceMoney 扣减商户余额
ReduceMoney string `json:"reducemoney"`
}
// RefundQuery 退款查询响应
type RefundQuery struct {
Base
// RefundNo 平台退款单号
RefundNo string `json:"refund_no"`
// OutRefundNo 商户退款单号
OutRefundNo string `json:"out_refund_no"`
// TradeNo 平台订单号
TradeNo string `json:"trade_no"`
// OutTradeNo 商户订单号
OutTradeNo string `json:"out_trade_no"`
// Money 退款金额
Money string `json:"money"`
// ReduceMoney 扣减商户余额
ReduceMoney string `json:"reducemoney"`
// Status 退款状态: 0失败 / 1成功
Status int `json:"status"`
// Addtime 退款时间
Addtime string `json:"addtime"`
}
// IsRefundSuccess 判断退款是否成功
func (r *RefundQuery) IsRefundSuccess() bool {
return r.Status == 1
}
// Notify 支付结果通知
type Notify struct {
// PID 商户ID
PID int `json:"pid"`
// TradeNo 平台订单号
TradeNo string `json:"trade_no"`
// OutTradeNo 商户订单号
OutTradeNo string `json:"out_trade_no"`
// ApiTradeNo 接口订单号
ApiTradeNo string `json:"api_trade_no"`
// Type 支付方式
Type string `json:"type"`
// TradeStatus 交易状态,固定为 TRADE_SUCCESS
TradeStatus string `json:"trade_status"`
// Addtime 订单创建时间
Addtime string `json:"addtime"`
// Endtime 订单完成时间
Endtime string `json:"endtime"`
// Name 商品名称
Name string `json:"name"`
// Money 商品金额
Money string `json:"money"`
// Param 业务扩展参数
Param string `json:"param"`
// Buyer 支付用户标识
Buyer string `json:"buyer"`
// Timestamp 时间戳
Timestamp string `json:"timestamp"`
// Sign 签名
Sign string `json:"sign"`
// SignType 签名类型
SignType string `json:"sign_type"`
}
// MerchantInfo 商户信息响应
type MerchantInfo struct {
Base
// PID 商户ID
PID int `json:"pid"`
// Status 商户状态: 0封禁 / 1正常 / 2待审核
Status int `json:"status"`
// PayStatus 支付状态: 0关闭 / 1开启
PayStatus int `json:"pay_status"`
// SettleStatus 结算状态: 0关闭 / 1开启
SettleStatus int `json:"settle_status"`
// Money 商户余额
Money string `json:"money"`
// SettleType 结算方式: 1支付宝 / 2微信 / 3QQ钱包 / 4银行卡
SettleType int `json:"settle_type"`
// SettleAccount 结算账户
SettleAccount string `json:"settle_account"`
// SettleName 结算账户姓名
SettleName string `json:"settle_name"`
// OrderNum 订单总数量
OrderNum int `json:"order_num"`
// OrderNumToday 今日订单数量
OrderNumToday int `json:"order_num_today"`
// OrderNumLastday 昨日订单数量
OrderNumLastday int `json:"order_num_lastday"`
// OrderMoneyToday 今日订单收入
OrderMoneyToday string `json:"order_money_today"`
// OrderMoneyLastday 昨日订单收入
OrderMoneyLastday string `json:"order_money_lastday"`
}
// IsNormal 判断商户状态是否正常
func (m *MerchantInfo) IsNormal() bool {
return m.Status == 1
}
// IsPayEnabled 判断支付是否开启
func (m *MerchantInfo) IsPayEnabled() bool {
return m.PayStatus == 1
}
// OrderList 订单列表响应
type OrderList struct {
Base
// Data 订单列表
Data []OrderItem `json:"data"`
}
// OrderItem 订单列表项
type OrderItem struct {
TradeNo string `json:"trade_no"`
OutTradeNo string `json:"out_trade_no"`
ApiTradeNo string `json:"api_trade_no"`
Type string `json:"type"`
Status int `json:"status"`
PID int `json:"pid"`
Addtime string `json:"addtime"`
Endtime string `json:"endtime"`
Name string `json:"name"`
Money string `json:"money"`
Param string `json:"param"`
Buyer string `json:"buyer"`
ClientIP string `json:"clientip"`
}
// Transfer 转账响应
type Transfer struct {
Base
// Status 转账状态: 0正在处理 / 1转账成功
Status int `json:"status"`
// BizNo 系统交易号
BizNo string `json:"biz_no"`
// OutBizNo 商户交易号
OutBizNo string `json:"out_biz_no"`
// OrderID 接口转账单号(支付宝/微信返回)
OrderID string `json:"orderid"`
// PayDate 转账完成时间
PayDate string `json:"paydate"`
// CostMoney 转账花费金额
CostMoney string `json:"cost_money"`
}
// IsTransferSuccess 判断转账是否成功
func (t *Transfer) IsTransferSuccess() bool {
return t.Status == 1
}
// IsProcessing 判断是否正在处理
func (t *Transfer) IsProcessing() bool {
return t.Status == 0
}
// TransferQuery 转账查询响应
type TransferQuery struct {
Base
// Status 转账状态: 0正在处理 / 1转账成功 / 2转账失败
Status int `json:"status"`
// ErrMsg 转账失败原因(status=2时返回)
ErrMsg string `json:"errmsg"`
// BizNo 系统交易号
BizNo string `json:"biz_no"`
// OutBizNo 商户交易号
OutBizNo string `json:"out_biz_no"`
// OrderID 接口转账单号
OrderID string `json:"orderid"`
// PayDate 转账完成时间
PayDate string `json:"paydate"`
// Amount 转账金额
Amount string `json:"amount"`
// CostMoney 转账花费金额
CostMoney string `json:"cost_money"`
// Remark 转账备注
Remark string `json:"remark"`
}
// IsTransferSuccess 判断转账是否成功
func (t *TransferQuery) IsTransferSuccess() bool {
return t.Status == 1
}
// IsTransferFailed 判断转账是否失败
func (t *TransferQuery) IsTransferFailed() bool {
return t.Status == 2
}
// Balance 余额查询响应
type Balance struct {
Base
// AvailableMoney 商户可用余额
AvailableMoney string `json:"available_money"`
// TransferRate 转账手续费率(%)
TransferRate string `json:"transfer_rate"`
}

249
utils/pay/sign.go Normal file
View File

@@ -0,0 +1,249 @@
// Package pay 神奇支付SDK签名工具
package pay
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"sort"
"strconv"
"strings"
)
// Signer RSA签名器
type Signer struct {
privateKey *rsa.PrivateKey // 商户私钥
platformPubKey *rsa.PublicKey // 平台公钥
}
// NewSigner 创建签名器
func NewSigner(privateKeyPEM, platformPubKeyPEM string) (*Signer, error) {
privateKey, err := parsePrivateKey(privateKeyPEM)
if err != nil {
return nil, fmt.Errorf("解析商户私钥失败: %w", err)
}
platformPubKey, err := parsePublicKey(platformPubKeyPEM)
if err != nil {
return nil, fmt.Errorf("解析平台公钥失败: %w", err)
}
return &Signer{
privateKey: privateKey,
platformPubKey: platformPubKey,
}, nil
}
// parsePrivateKey 解析RSA私钥
// 支持: 纯Base64字符串、PKCS1格式、PKCS8格式
func parsePrivateKey(keyStr string) (*rsa.PrivateKey, error) {
keyStr = strings.TrimSpace(keyStr)
if keyStr == "" {
return nil, errors.New("私钥不能为空")
}
// 提取纯Base64内容
base64Content := extractBase64Content(keyStr)
if base64Content == "" {
return nil, errors.New("私钥内容为空")
}
// 验证是否是有效的Base64并解码
derBytes, err := base64.StdEncoding.DecodeString(base64Content)
if err != nil {
return nil, fmt.Errorf("私钥Base64解码失败: %w", err)
}
// 直接尝试解析DER格式不需要PEM包装
// 尝试 PKCS8 格式
if key, err := x509.ParsePKCS8PrivateKey(derBytes); err == nil {
if rsaKey, ok := key.(*rsa.PrivateKey); ok {
return rsaKey, nil
}
return nil, errors.New("解析成功但不是RSA私钥")
}
// 尝试 PKCS1 格式
if rsaKey, err := x509.ParsePKCS1PrivateKey(derBytes); err == nil {
return rsaKey, nil
}
// 检查是否误传了公钥
if _, err := x509.ParsePKIXPublicKey(derBytes); err == nil {
return nil, errors.New("您配置的是公钥而不是私钥!请到神奇支付商户后台获取正确的【商户私钥】")
}
return nil, fmt.Errorf("无法解析私钥格式,密钥长度=%d字节请确认私钥内容正确", len(derBytes))
}
// extractBase64Content 从各种格式中提取纯Base64内容
func extractBase64Content(keyStr string) string {
// 去除首尾空白
keyStr = strings.TrimSpace(keyStr)
// 如果包含PEM头尾提取中间内容
if strings.Contains(keyStr, "-----BEGIN") {
keyStr = strings.ReplaceAll(keyStr, "\r\n", "\n")
lines := strings.Split(keyStr, "\n")
var base64Lines []string
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "-----") {
continue
}
base64Lines = append(base64Lines, line)
}
keyStr = strings.Join(base64Lines, "")
}
// 移除所有空白字符
keyStr = strings.ReplaceAll(keyStr, "\n", "")
keyStr = strings.ReplaceAll(keyStr, "\r", "")
keyStr = strings.ReplaceAll(keyStr, " ", "")
keyStr = strings.ReplaceAll(keyStr, "\t", "")
return keyStr
}
// parsePublicKey 解析RSA公钥
// 支持: 纯Base64字符串、PKIX格式、PKCS1格式
func parsePublicKey(keyStr string) (*rsa.PublicKey, error) {
keyStr = strings.TrimSpace(keyStr)
if keyStr == "" {
return nil, errors.New("公钥不能为空")
}
// 提取纯Base64内容
base64Content := extractBase64Content(keyStr)
if base64Content == "" {
return nil, errors.New("公钥内容为空")
}
// 验证是否是有效的Base64并解码
derBytes, err := base64.StdEncoding.DecodeString(base64Content)
if err != nil {
return nil, fmt.Errorf("公钥Base64解码失败: %w", err)
}
// 尝试 PKIX 格式 (最常用)
if pub, err := x509.ParsePKIXPublicKey(derBytes); err == nil {
if rsaKey, ok := pub.(*rsa.PublicKey); ok {
return rsaKey, nil
}
return nil, errors.New("不是RSA公钥")
}
// 尝试 PKCS1 格式
if rsaKey, err := x509.ParsePKCS1PublicKey(derBytes); err == nil {
return rsaKey, nil
}
return nil, fmt.Errorf("无法解析公钥格式,密钥长度=%d字节", len(derBytes))
}
// Sign 使用商户私钥进行SHA256WithRSA签名
func (s *Signer) Sign(data string) (string, error) {
hash := sha256.Sum256([]byte(data))
signature, err := rsa.SignPKCS1v15(rand.Reader, s.privateKey, crypto.SHA256, hash[:])
if err != nil {
return "", fmt.Errorf("签名失败: %w", err)
}
return base64.StdEncoding.EncodeToString(signature), nil
}
// Verify 使用平台公钥验证签名
func (s *Signer) Verify(data, signature string) error {
sig, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
return fmt.Errorf("签名base64解码失败: %w", err)
}
hash := sha256.Sum256([]byte(data))
if err := rsa.VerifyPKCS1v15(s.platformPubKey, crypto.SHA256, hash[:], sig); err != nil {
return fmt.Errorf("验签失败: %w", err)
}
return nil
}
// BuildSignString 构建待签名字符串
// 规则:
// 1. 获取所有非空参数,排除 sign 和 sign_type
// 2. 按照 key 的 ASCII 码升序排序
// 3. 用 & 连接成 key=value 格式
func BuildSignString(params map[string]string) string {
// 过滤并收集有效的 key
var keys []string
for k, v := range params {
if k == "sign" || k == "sign_type" || v == "" {
continue
}
keys = append(keys, k)
}
// 按 ASCII 码升序排序
sort.Strings(keys)
// 拼接成 key=value 格式
var pairs []string
for _, k := range keys {
pairs = append(pairs, k+"="+params[k])
}
return strings.Join(pairs, "&")
}
// SignParams 对参数进行签名,会自动添加 sign 和 sign_type 字段
func (s *Signer) SignParams(params map[string]string) error {
signStr := BuildSignString(params)
sign, err := s.Sign(signStr)
if err != nil {
return err
}
params["sign"] = sign
params["sign_type"] = "RSA"
return nil
}
// VerifyParams 验证参数签名
func (s *Signer) VerifyParams(params map[string]string) error {
sign, ok := params["sign"]
if !ok || sign == "" {
return errors.New("缺少签名参数")
}
signStr := BuildSignString(params)
return s.Verify(signStr, sign)
}
// MapToStringMap 将 interface{} map 转换为 string map (用于验签)
func MapToStringMap(data map[string]interface{}) map[string]string {
result := make(map[string]string)
for k, v := range data {
if v == nil {
continue
}
switch val := v.(type) {
case string:
result[k] = val
case float64:
// JSON 数字默认解析为 float64
if val == float64(int64(val)) {
result[k] = strconv.FormatInt(int64(val), 10)
} else {
result[k] = strconv.FormatFloat(val, 'f', -1, 64)
}
case int:
result[k] = strconv.Itoa(val)
case int64:
result[k] = strconv.FormatInt(val, 10)
case bool:
result[k] = strconv.FormatBool(val)
default:
result[k] = fmt.Sprintf("%v", v)
}
}
return result
}