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

589 lines
16 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 {
2026-01-09 14:04:33 +08:00
config config.EasyPayConfig
client *http.Client
orderModel model.OrderModel // 订单模型,用于次数轮询模式查询用户订单数量
}
// getSelectedCID 获取当前应该使用的渠道ID
// ctx: 上下文用于次数轮询模式时访问Redis
// userID: 用户ID用于次数轮询模式时记录用户使用的渠道
func (e *EasyPayService) getSelectedCID(ctx context.Context, userID string) string {
// 如果没有配置 CID返回空字符串
if len(e.config.CIDs) == 0 {
return ""
}
// 如果只有一个渠道,直接返回
if len(e.config.CIDs) == 1 {
return e.config.CIDs[0]
}
// 根据轮询模式选择策略
rotateMode := e.config.RotateMode
if rotateMode == "" {
rotateMode = "day" // 默认天数轮询
}
switch rotateMode {
case "count":
// 次数轮询模式:按用户订单次数轮询
return e.selectCIDByCount(ctx, userID)
case "day":
// 天数轮询模式:按时间轮询
return e.selectCIDByRotation()
default:
// 默认使用天数轮询
logx.Infof("未知的轮询模式: %s使用默认天数轮询", rotateMode)
return e.selectCIDByRotation()
}
}
// selectCIDByRotation 按时间轮询策略选择CID
func (e *EasyPayService) selectCIDByRotation() string {
if len(e.config.CIDs) == 0 {
return ""
}
// 如果只有一个,直接返回
if len(e.config.CIDs) == 1 {
return e.config.CIDs[0]
}
// 获取轮询天数默认3天
rotateDays := e.config.RotateDays
if rotateDays <= 0 {
rotateDays = 3
}
// 计算从某个基准日期比如2020-01-01开始的天数
baseDate := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
now := time.Now()
daysSinceBase := int(now.Sub(baseDate).Hours() / 24)
// 按轮询天数计算当前应该使用的索引
rotationIndex := (daysSinceBase / rotateDays) % len(e.config.CIDs)
selectedCID := e.config.CIDs[rotationIndex]
logx.Infof("易支付渠道天数轮询选择: 总渠道数=%d, 轮询天数=%d, 当前索引=%d, 选择渠道=%s",
len(e.config.CIDs), rotateDays, rotationIndex, selectedCID)
return selectedCID
}
// selectCIDByCount 按次数轮询策略选择CID针对用户
func (e *EasyPayService) selectCIDByCount(ctx context.Context, userID string) string {
if len(e.config.CIDs) == 0 {
return ""
}
// 如果只有一个,直接返回
if len(e.config.CIDs) == 1 {
return e.config.CIDs[0]
}
// 如果没有用户ID回退到天数轮询
if userID == "" {
logx.Infof("次数轮询模式但用户ID为空回退到天数轮询")
return e.selectCIDByRotation()
}
// 查询该用户的易支付订单数量
orderCount := int64(0)
if e.orderModel != nil {
builder := e.orderModel.SelectBuilder().
Where("user_id = ?", userID).
Where("payment_platform = ?", "easypay_alipay")
count, err := e.orderModel.FindCount(ctx, builder, "id")
if err != nil {
logx.Errorf("查询用户易支付订单数量失败: %v使用索引0", err)
} else {
orderCount = count
}
}
// 根据订单数量计算应该使用的渠道索引订单数量从0开始所以第0个订单用索引0第1个订单用索引1以此类推
channelIndex := int(orderCount) % len(e.config.CIDs)
selectedCID := e.config.CIDs[channelIndex]
logx.Infof("易支付渠道次数轮询选择: 用户ID=%s, 总渠道数=%d, 用户订单数=%d, 选择索引=%d, 选择渠道=%s",
userID, len(e.config.CIDs), orderCount, channelIndex, selectedCID)
return selectedCID
2025-12-31 16:54:17 +08:00
}
// NewEasyPayService 创建易支付服务实例
2026-01-09 14:04:33 +08:00
func NewEasyPayService(c config.Config, orderModel model.OrderModel) *EasyPayService {
2025-12-31 16:54:17 +08:00
return &EasyPayService{
config: c.EasyPay,
client: &http.Client{
Timeout: 30 * time.Second,
},
2026-01-09 14:04:33 +08:00
orderModel: orderModel,
2025-12-31 16:54:17 +08:00
}
}
// 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订单页面跳转方式
2026-01-09 14:04:33 +08:00
func (e *EasyPayService) CreateEasyPayH5Order(ctx context.Context, amount float64, subject string, outTradeNo string, userID string) (string, error) {
2025-12-31 16:54:17 +08:00
// 格式化金额,保留两位小数
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",
}
2026-01-09 14:04:33 +08:00
// 获取并添加渠道ID
if cid := e.getSelectedCID(ctx, userID); cid != "" {
params["cid"] = cid
2025-12-31 16:54:17 +08:00
}
// 生成签名
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方式
2026-01-09 14:04:33 +08:00
func (e *EasyPayService) CreateEasyPayAppOrder(ctx context.Context, amount float64, subject string, outTradeNo string, clientIP string, userID string) (string, error) {
2025-12-31 16:54:17 +08:00
// 格式化金额,保留两位小数
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",
}
2026-01-09 14:04:33 +08:00
// 获取并添加渠道ID
if cid := e.getSelectedCID(ctx, userID); cid != "" {
params["cid"] = cid
2025-12-31 16:54:17 +08:00
}
// 生成签名
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("未获取到支付链接")
}
2026-01-09 14:04:33 +08:00
// CreateEasyPayOrderResult 易支付订单创建结果
type CreateEasyPayOrderResult struct {
PayURL string // 支付URL
CID string // 使用的渠道ID
}
2025-12-31 16:54:17 +08:00
// CreateEasyPayOrder 根据平台类型创建易支付订单
2026-01-09 14:04:33 +08:00
func (e *EasyPayService) CreateEasyPayOrder(ctx context.Context, amount float64, subject string, outTradeNo string, userID string) (*CreateEasyPayOrderResult, error) {
// 获取选中的渠道ID
selectedCID := e.getSelectedCID(ctx, userID)
2025-12-31 16:54:17 +08:00
// 根据 ctx 中的 platform 判断平台
platform, platformOk := ctx.Value("platform").(string)
if !platformOk {
2026-01-09 14:04:33 +08:00
return nil, fmt.Errorf("无效的支付平台")
2025-12-31 16:54:17 +08:00
}
2026-01-09 14:04:33 +08:00
var payURL string
var err error
2025-12-31 16:54:17 +08:00
switch platform {
case model.PlatformApp:
// APP平台使用API方式
clientIP := ""
if ip, ok := ctx.Value("client_ip").(string); ok {
clientIP = ip
}
2026-01-09 14:04:33 +08:00
payURL, err = e.CreateEasyPayAppOrder(ctx, amount, subject, outTradeNo, clientIP, userID)
2025-12-31 16:54:17 +08:00
case model.PlatformH5:
// H5平台使用页面跳转方式
2026-01-09 14:04:33 +08:00
payURL, err = e.CreateEasyPayH5Order(ctx, amount, subject, outTradeNo, userID)
2025-12-31 16:54:17 +08:00
default:
2026-01-09 14:04:33 +08:00
return nil, fmt.Errorf("不支持的支付平台: %s", platform)
2025-12-31 16:54:17 +08:00
}
2026-01-09 14:04:33 +08:00
if err != nil {
return nil, err
}
return &CreateEasyPayOrderResult{
PayURL: payURL,
CID: selectedCID,
}, nil
2025-12-31 16:54:17 +08:00
}
// 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"
}