Files
jnc-server/app/main/api/internal/service/easyPayService.go
2025-12-31 17:07:27 +08:00

460 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 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
}
}
// EasyPayQueryResponse 查询订单响应
type EasyPayQueryResponse struct {
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
}
}
// EasyPayRefundResponse 退款响应
type EasyPayRefundResponse struct {
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
}
}
// 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.GetCodeInt() != 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.GetCodeInt() != 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.GetCodeInt() != 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"
}