Files
jnc-server/app/main/api/internal/service/easyPayService.go
2026-01-09 14:04:33 +08:00

589 lines
16 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
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
}
// NewEasyPayService 创建易支付服务实例
func NewEasyPayService(c config.Config, orderModel model.OrderModel) *EasyPayService {
return &EasyPayService{
config: c.EasyPay,
client: &http.Client{
Timeout: 30 * time.Second,
},
orderModel: orderModel,
}
}
// 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(ctx context.Context, amount float64, subject string, outTradeNo string, userID 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 cid := e.getSelectedCID(ctx, userID); cid != "" {
params["cid"] = 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, userID 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 cid := e.getSelectedCID(ctx, userID); cid != "" {
params["cid"] = 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("未获取到支付链接")
}
// CreateEasyPayOrderResult 易支付订单创建结果
type CreateEasyPayOrderResult struct {
PayURL string // 支付URL
CID string // 使用的渠道ID
}
// CreateEasyPayOrder 根据平台类型创建易支付订单
func (e *EasyPayService) CreateEasyPayOrder(ctx context.Context, amount float64, subject string, outTradeNo string, userID string) (*CreateEasyPayOrderResult, error) {
// 获取选中的渠道ID
selectedCID := e.getSelectedCID(ctx, userID)
// 根据 ctx 中的 platform 判断平台
platform, platformOk := ctx.Value("platform").(string)
if !platformOk {
return nil, fmt.Errorf("无效的支付平台")
}
var payURL string
var err error
switch platform {
case model.PlatformApp:
// APP平台使用API方式
clientIP := ""
if ip, ok := ctx.Value("client_ip").(string); ok {
clientIP = ip
}
payURL, err = e.CreateEasyPayAppOrder(ctx, amount, subject, outTradeNo, clientIP, userID)
case model.PlatformH5:
// H5平台使用页面跳转方式
payURL, err = e.CreateEasyPayH5Order(ctx, amount, subject, outTradeNo, userID)
default:
return nil, fmt.Errorf("不支持的支付平台: %s", platform)
}
if err != nil {
return nil, err
}
return &CreateEasyPayOrderResult{
PayURL: payURL,
CID: selectedCID,
}, nil
}
// 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"
}