package service import ( "context" "crypto/md5" "encoding/json" "fmt" "io" "sim-server/app/main/api/internal/config" "sim-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" }