Files
jnc-server/app/main/api/internal/service/easyPayService.go

460 lines
12 KiB
Go
Raw Normal View History

2025-12-31 16:54:17 +08:00
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 {
2025-12-31 17:07:27 +08:00
Code interface{} `json:"code"` // 可能是 int 或 string
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"`
}
// GetCodeInt 获取 code 的 int 值
func (r *EasyPayOrderResponse) GetCodeInt() int {
switch v := r.Code.(type) {
case int:
return v
case float64:
return int(v)
case string:
if v == "1" || v == "error" {
if v == "1" {
return 1
}
return 0
}
return 0
default:
return 0
}
2025-12-31 16:54:17 +08:00
}
// EasyPayQueryResponse 查询订单响应
type EasyPayQueryResponse struct {
2025-12-31 17:07:27 +08:00
Code interface{} `json:"code"` // 可能是 int 或 string
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 interface{} `json:"status,omitempty"` // 可能是 int 或 string
Param string `json:"param,omitempty"`
Buyer string `json:"buyer,omitempty"`
}
// GetCodeInt 获取 code 的 int 值
func (r *EasyPayQueryResponse) GetCodeInt() int {
switch v := r.Code.(type) {
case int:
return v
case float64:
return int(v)
case string:
if v == "1" {
return 1
}
return 0
default:
return 0
}
}
// GetStatusInt 获取 status 的 int 值
func (r *EasyPayQueryResponse) GetStatusInt() int {
switch v := r.Status.(type) {
case int:
return v
case float64:
return int(v)
case string:
if v == "1" {
return 1
}
return 0
default:
return 0
}
2025-12-31 16:54:17 +08:00
}
// EasyPayRefundResponse 退款响应
type EasyPayRefundResponse struct {
2025-12-31 17:07:27 +08:00
Code interface{} `json:"code"` // 可能是 int 或 string
Msg string `json:"msg"`
}
// GetCodeInt 获取 code 的 int 值
func (r *EasyPayRefundResponse) GetCodeInt() int {
switch v := r.Code.(type) {
case int:
return v
case float64:
return int(v)
case string:
if v == "1" {
return 1
}
return 0
default:
return 0
}
2025-12-31 16:54:17 +08:00
}
// 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))
}
2025-12-31 17:07:27 +08:00
if orderResp.GetCodeInt() != 1 {
2025-12-31 16:54:17 +08:00
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))
}
2025-12-31 17:07:27 +08:00
if queryResp.GetCodeInt() != 1 {
2025-12-31 16:54:17 +08:00
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))
}
2025-12-31 17:07:27 +08:00
if refundResp.GetCodeInt() != 1 {
2025-12-31 16:54:17 +08:00
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"
}