fix
This commit is contained in:
388
app/main/api/internal/service/easyPayService.go
Normal file
388
app/main/api/internal/service/easyPayService.go
Normal file
@@ -0,0 +1,388 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"jnc-server/app/main/api/internal/config"
|
||||
"jnc-server/app/main/model"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
// EasyPayService 易支付服务
|
||||
type EasyPayService struct {
|
||||
config config.EasyPayConfig
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewEasyPayService 创建易支付服务实例
|
||||
func NewEasyPayService(c config.Config) *EasyPayService {
|
||||
return &EasyPayService{
|
||||
config: c.EasyPay,
|
||||
client: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// EasyPayOrderResponse API接口支付响应
|
||||
type EasyPayOrderResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
TradeNo string `json:"trade_no,omitempty"`
|
||||
OId string `json:"O_id,omitempty"`
|
||||
PayUrl string `json:"payurl,omitempty"`
|
||||
Qrcode string `json:"qrcode,omitempty"`
|
||||
Img string `json:"img,omitempty"`
|
||||
}
|
||||
|
||||
// EasyPayQueryResponse 查询订单响应
|
||||
type EasyPayQueryResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
TradeNo string `json:"trade_no,omitempty"`
|
||||
OutTradeNo string `json:"out_trade_no,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Pid string `json:"pid,omitempty"`
|
||||
Addtime string `json:"addtime,omitempty"`
|
||||
Endtime string `json:"endtime,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Money string `json:"money,omitempty"`
|
||||
Status int `json:"status,omitempty"`
|
||||
Param string `json:"param,omitempty"`
|
||||
Buyer string `json:"buyer,omitempty"`
|
||||
}
|
||||
|
||||
// EasyPayRefundResponse 退款响应
|
||||
type EasyPayRefundResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
// EasyPayNotification 支付回调通知
|
||||
type EasyPayNotification struct {
|
||||
Pid string
|
||||
Name string
|
||||
Money string
|
||||
OutTradeNo string
|
||||
TradeNo string
|
||||
Param string
|
||||
TradeStatus string
|
||||
Type string
|
||||
Sign string
|
||||
SignType string
|
||||
}
|
||||
|
||||
// generateSign 生成MD5签名
|
||||
func (e *EasyPayService) generateSign(params map[string]string) string {
|
||||
// 排除 sign、sign_type 和空值
|
||||
filteredParams := make(map[string]string)
|
||||
for k, v := range params {
|
||||
if k != "sign" && k != "sign_type" && v != "" {
|
||||
filteredParams[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// 按参数名ASCII码从小到大排序
|
||||
keys := make([]string, 0, len(filteredParams))
|
||||
for k := range filteredParams {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
// 拼接成URL键值对格式
|
||||
var parts []string
|
||||
for _, k := range keys {
|
||||
parts = append(parts, fmt.Sprintf("%s=%s", k, filteredParams[k]))
|
||||
}
|
||||
queryString := strings.Join(parts, "&")
|
||||
|
||||
// 拼接商户密钥并MD5加密
|
||||
signString := queryString + e.config.PKEY
|
||||
hash := md5.Sum([]byte(signString))
|
||||
return fmt.Sprintf("%x", hash) // 转为小写
|
||||
}
|
||||
|
||||
// verifySign 验证签名
|
||||
func (e *EasyPayService) verifySign(params map[string]string, sign string) bool {
|
||||
calculatedSign := e.generateSign(params)
|
||||
return strings.EqualFold(calculatedSign, sign)
|
||||
}
|
||||
|
||||
// CreateEasyPayH5Order 创建易支付H5订单(页面跳转方式)
|
||||
func (e *EasyPayService) CreateEasyPayH5Order(amount float64, subject string, outTradeNo string) (string, error) {
|
||||
// 格式化金额,保留两位小数
|
||||
moneyStr := fmt.Sprintf("%.2f", amount)
|
||||
|
||||
params := map[string]string{
|
||||
"name": subject,
|
||||
"money": moneyStr,
|
||||
"type": "alipay",
|
||||
"out_trade_no": outTradeNo,
|
||||
"notify_url": e.config.NotifyUrl,
|
||||
"pid": e.config.PID,
|
||||
"return_url": e.config.ReturnUrl,
|
||||
"sign_type": "MD5",
|
||||
}
|
||||
// 如果配置了渠道ID,则添加
|
||||
if e.config.CID != "" {
|
||||
params["cid"] = e.config.CID
|
||||
}
|
||||
|
||||
// 生成签名
|
||||
sign := e.generateSign(params)
|
||||
params["sign"] = sign
|
||||
|
||||
// 构建支付URL
|
||||
baseURL := strings.TrimSuffix(e.config.ApiURL, "/")
|
||||
payURL := fmt.Sprintf("%s/submit.php", baseURL)
|
||||
|
||||
// 构建查询字符串
|
||||
values := url.Values{}
|
||||
for k, v := range params {
|
||||
values.Set(k, v)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s?%s", payURL, values.Encode()), nil
|
||||
}
|
||||
|
||||
// CreateEasyPayAppOrder 创建易支付APP订单(API方式)
|
||||
func (e *EasyPayService) CreateEasyPayAppOrder(ctx context.Context, amount float64, subject string, outTradeNo string, clientIP string) (string, error) {
|
||||
// 格式化金额,保留两位小数
|
||||
moneyStr := fmt.Sprintf("%.2f", amount)
|
||||
|
||||
params := map[string]string{
|
||||
"pid": e.config.PID,
|
||||
"type": "alipay",
|
||||
"out_trade_no": outTradeNo,
|
||||
"notify_url": e.config.NotifyUrl,
|
||||
"name": subject,
|
||||
"money": moneyStr,
|
||||
"clientip": clientIP,
|
||||
"device": "pc",
|
||||
"sign_type": "MD5",
|
||||
}
|
||||
// 如果配置了渠道ID,则添加
|
||||
if e.config.CID != "" {
|
||||
params["cid"] = e.config.CID
|
||||
}
|
||||
|
||||
// 生成签名
|
||||
sign := e.generateSign(params)
|
||||
params["sign"] = sign
|
||||
|
||||
// 构建请求URL
|
||||
baseURL := strings.TrimSuffix(e.config.ApiURL, "/")
|
||||
apiURL := fmt.Sprintf("%s/mapi.php", baseURL)
|
||||
|
||||
// 构建form-data请求
|
||||
values := url.Values{}
|
||||
for k, v := range params {
|
||||
values.Set(k, v)
|
||||
}
|
||||
|
||||
// 发送POST请求
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", apiURL, strings.NewReader(values.Encode()))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建请求失败: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := e.client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
var orderResp EasyPayOrderResponse
|
||||
if err := json.Unmarshal(body, &orderResp); err != nil {
|
||||
return "", fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, string(body))
|
||||
}
|
||||
|
||||
if orderResp.Code != 1 {
|
||||
return "", fmt.Errorf("创建订单失败: %s", orderResp.Msg)
|
||||
}
|
||||
|
||||
// 优先返回支付URL,如果没有则返回二维码
|
||||
if orderResp.PayUrl != "" {
|
||||
return orderResp.PayUrl, nil
|
||||
}
|
||||
if orderResp.Qrcode != "" {
|
||||
return orderResp.Qrcode, nil
|
||||
}
|
||||
if orderResp.Img != "" {
|
||||
return orderResp.Img, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("未获取到支付链接")
|
||||
}
|
||||
|
||||
// CreateEasyPayOrder 根据平台类型创建易支付订单
|
||||
func (e *EasyPayService) CreateEasyPayOrder(ctx context.Context, amount float64, subject string, outTradeNo string) (string, error) {
|
||||
// 根据 ctx 中的 platform 判断平台
|
||||
platform, platformOk := ctx.Value("platform").(string)
|
||||
if !platformOk {
|
||||
return "", fmt.Errorf("无效的支付平台")
|
||||
}
|
||||
|
||||
switch platform {
|
||||
case model.PlatformApp:
|
||||
// APP平台使用API方式
|
||||
clientIP := ""
|
||||
if ip, ok := ctx.Value("client_ip").(string); ok {
|
||||
clientIP = ip
|
||||
}
|
||||
return e.CreateEasyPayAppOrder(ctx, amount, subject, outTradeNo, clientIP)
|
||||
case model.PlatformH5:
|
||||
// H5平台使用页面跳转方式
|
||||
return e.CreateEasyPayH5Order(amount, subject, outTradeNo)
|
||||
default:
|
||||
return "", fmt.Errorf("不支持的支付平台: %s", platform)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleEasyPayNotification 处理易支付回调通知
|
||||
func (e *EasyPayService) HandleEasyPayNotification(r *http.Request) (*EasyPayNotification, error) {
|
||||
// 解析GET参数
|
||||
params := make(map[string]string)
|
||||
for k, v := range r.URL.Query() {
|
||||
if len(v) > 0 {
|
||||
params[k] = v[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 获取签名
|
||||
sign, ok := params["sign"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("缺少签名参数")
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
if !e.verifySign(params, sign) {
|
||||
return nil, fmt.Errorf("签名验证失败")
|
||||
}
|
||||
|
||||
// 构建通知对象
|
||||
notification := &EasyPayNotification{
|
||||
Pid: params["pid"],
|
||||
Name: params["name"],
|
||||
Money: params["money"],
|
||||
OutTradeNo: params["out_trade_no"],
|
||||
TradeNo: params["trade_no"],
|
||||
Param: params["param"],
|
||||
TradeStatus: params["trade_status"],
|
||||
Type: params["type"],
|
||||
Sign: sign,
|
||||
SignType: params["sign_type"],
|
||||
}
|
||||
|
||||
return notification, nil
|
||||
}
|
||||
|
||||
// QueryOrderStatus 查询订单状态
|
||||
func (e *EasyPayService) QueryOrderStatus(ctx context.Context, outTradeNo string) (*EasyPayQueryResponse, error) {
|
||||
// 构建查询URL
|
||||
baseURL := strings.TrimSuffix(e.config.ApiURL, "/")
|
||||
queryURL := fmt.Sprintf("%s/api.php?act=order&pid=%s&key=%s&out_trade_no=%s",
|
||||
baseURL, e.config.PID, e.config.PKEY, url.QueryEscape(outTradeNo))
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", queryURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建请求失败: %v", err)
|
||||
}
|
||||
|
||||
resp, err := e.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
var queryResp EasyPayQueryResponse
|
||||
if err := json.Unmarshal(body, &queryResp); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, string(body))
|
||||
}
|
||||
|
||||
if queryResp.Code != 1 {
|
||||
return nil, fmt.Errorf("查询订单失败: %s", queryResp.Msg)
|
||||
}
|
||||
|
||||
return &queryResp, nil
|
||||
}
|
||||
|
||||
// Refund 申请退款
|
||||
func (e *EasyPayService) Refund(ctx context.Context, outTradeNo string, refundAmount float64) error {
|
||||
// 格式化金额,保留两位小数
|
||||
moneyStr := fmt.Sprintf("%.2f", refundAmount)
|
||||
|
||||
params := map[string]string{
|
||||
"pid": e.config.PID,
|
||||
"key": e.config.PKEY,
|
||||
"out_trade_no": outTradeNo,
|
||||
"money": moneyStr,
|
||||
}
|
||||
|
||||
// 构建请求URL
|
||||
baseURL := strings.TrimSuffix(e.config.ApiURL, "/")
|
||||
refundURL := fmt.Sprintf("%s/api.php?act=refund", baseURL)
|
||||
|
||||
// 构建form-data请求
|
||||
values := url.Values{}
|
||||
for k, v := range params {
|
||||
values.Set(k, v)
|
||||
}
|
||||
|
||||
// 发送POST请求
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", refundURL, strings.NewReader(values.Encode()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建请求失败: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := e.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
var refundResp EasyPayRefundResponse
|
||||
if err := json.Unmarshal(body, &refundResp); err != nil {
|
||||
return fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, string(body))
|
||||
}
|
||||
|
||||
if refundResp.Code != 1 {
|
||||
return fmt.Errorf("退款失败: %s", refundResp.Msg)
|
||||
}
|
||||
|
||||
logx.Infof("易支付退款成功,订单号: %s, 退款金额: %s", outTradeNo, moneyStr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsPaymentSuccess 判断支付是否成功
|
||||
func (e *EasyPayService) IsPaymentSuccess(notification *EasyPayNotification) bool {
|
||||
return notification.TradeStatus == "TRADE_SUCCESS"
|
||||
}
|
||||
Reference in New Issue
Block a user