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" }