🎨 新增支付动态配置&优化支付回调&新增三方支付
This commit is contained in:
480
utils/pay/client.go
Normal file
480
utils/pay/client.go
Normal 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
109
utils/pay/constants.go
Normal 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
313
utils/pay/notify.go
Normal 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
|
||||
}
|
||||
}
|
||||
159
utils/pay/request/request.go
Normal file
159
utils/pay/request/request.go
Normal 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
357
utils/pay/resp/response.go
Normal 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
249
utils/pay/sign.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user