diff --git a/app/main/api/desc/front/pay.api b/app/main/api/desc/front/pay.api index 52f3dc1..382bfc0 100644 --- a/app/main/api/desc/front/pay.api +++ b/app/main/api/desc/front/pay.api @@ -22,6 +22,10 @@ service main { // 微信退款回调 @handler WechatPayRefundCallback post /pay/wechat/refund_callback + + // 易支付回调 + @handler EasyPayCallback + get /pay/easypay/callback } @server ( diff --git a/app/main/api/etc/main.dev.yaml b/app/main/api/etc/main.dev.yaml index b0b5151..9517d34 100644 --- a/app/main/api/etc/main.dev.yaml +++ b/app/main/api/etc/main.dev.yaml @@ -82,3 +82,11 @@ Authorization: Promotion: PromotionDomain: "http://localhost:8888" # 推广域名(用于生成短链) OfficialDomain: "http://localhost:5678" # 正式站点域名(短链重定向的目标域名) +EasyPay: + Enabled: true + ApiURL: "https://zpayz.cn/" + PID: "2025123009590455" + PKEY: "f61pwaOj93lYpesM82ZnPAVwFojuSL7F" + CID: "12200" + NotifyUrl: "https://6m4685017o.goho.co/api/v1/pay/easypay/callback" + ReturnUrl: "http://localhost:5678/payment/result" diff --git a/app/main/api/etc/main.yaml b/app/main/api/etc/main.yaml index 379f627..df7f105 100644 --- a/app/main/api/etc/main.yaml +++ b/app/main/api/etc/main.yaml @@ -82,3 +82,11 @@ Authorization: Promotion: PromotionDomain: "https://p.dsjcq168.cn" # 推广域名(用于生成短链) OfficialDomain: "https://www.dsjcq168.cn" # 正式站点域名(短链重定向的目标域名) +EasyPay: + Enabled: true + ApiURL: "https://zpayz.cn/" + PID: "2025123009590455" + PKEY: "f61pwaOj93lYpesM82ZnPAVwFojuSL7F" + CID: "12200" + NotifyUrl: "https://www.dsjcq168.cn/api/v1/pay/easypay/callback" + ReturnUrl: "https://www.dsjcq168.cn/payment/result" diff --git a/app/main/api/internal/config/config.go b/app/main/api/internal/config/config.go index 8f0a53d..77bd5af 100644 --- a/app/main/api/internal/config/config.go +++ b/app/main/api/internal/config/config.go @@ -15,6 +15,7 @@ type Config struct { Alipay AlipayConfig Wxpay WxpayConfig Applepay ApplepayConfig + EasyPay EasyPayConfig Tianyuanapi TianyuanapiConfig SystemConfig SystemConfig WechatH5 WechatH5Config @@ -33,20 +34,20 @@ type JwtAuth struct { RefreshAfter int64 } type VerifyCode struct { - AccessKeyID string - AccessKeySecret string - EndpointURL string - SignName string - TemplateCode string - ValidTime int - ComplaintTemplate string // 投诉通知短信模板 + AccessKeyID string + AccessKeySecret string + EndpointURL string + SignName string + TemplateCode string + ValidTime int + ComplaintTemplate string // 投诉通知短信模板 } type Encrypt struct { SecretKey string } type AlipayConfig struct { - Enabled bool // 是否启用支付宝支付 + Enabled bool // 是否启用支付宝支付 AppID string PrivateKey string AlipayPublicKey string @@ -58,7 +59,7 @@ type AlipayConfig struct { ReturnURL string } type WxpayConfig struct { - Enabled bool // 是否启用微信支付 + Enabled bool // 是否启用微信支付 AppID string MchID string MchCertificateSerialNumber string @@ -71,7 +72,7 @@ type WxpayConfig struct { RefundNotifyUrl string } type ApplepayConfig struct { - Enabled bool // 是否启用Apple支付 + Enabled bool // 是否启用Apple支付 ProductionVerifyURL string SandboxVerifyURL string // 沙盒环境的验证 URL Sandbox bool @@ -84,7 +85,7 @@ type SystemConfig struct { ThreeVerify bool } type WechatH5Config struct { - Enabled bool // 是否启用微信公众号登录 + Enabled bool // 是否启用微信公众号登录 AppID string AppSecret string } @@ -121,3 +122,14 @@ type PromotionConfig struct { PromotionDomain string // 推广域名(用于生成短链) OfficialDomain string // 正式站点域名(短链重定向的目标域名) } + +// EasyPayConfig 易支付配置 +type EasyPayConfig struct { + Enabled bool // 是否启用易支付 + ApiURL string // 接口地址 + PID string // 商户ID + PKEY string // 商户密钥 + CID string // 支付渠道ID + NotifyUrl string // 异步通知地址 + ReturnUrl string // 页面跳转地址 +} diff --git a/app/main/api/internal/handler/pay/easypaycallbackhandler.go b/app/main/api/internal/handler/pay/easypaycallbackhandler.go new file mode 100644 index 0000000..c7781a0 --- /dev/null +++ b/app/main/api/internal/handler/pay/easypaycallbackhandler.go @@ -0,0 +1,17 @@ +package pay + +import ( + "net/http" + + "jnc-server/app/main/api/internal/logic/pay" + "jnc-server/app/main/api/internal/svc" + "jnc-server/common/result" +) + +func EasyPayCallbackHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := pay.NewEasyPayCallbackLogic(r.Context(), svcCtx) + err := l.EasyPayCallback(w, r) + result.HttpResult(r, w, nil, err) + } +} diff --git a/app/main/api/internal/handler/routes.go b/app/main/api/internal/handler/routes.go index a3c8510..8f6e62d 100644 --- a/app/main/api/internal/handler/routes.go +++ b/app/main/api/internal/handler/routes.go @@ -771,6 +771,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/pay/alipay/callback", Handler: pay.AlipayCallbackHandler(serverCtx), }, + { + Method: http.MethodGet, + Path: "/pay/easypay/callback", + Handler: pay.EasyPayCallbackHandler(serverCtx), + }, { Method: http.MethodPost, Path: "/pay/wechat/callback", diff --git a/app/main/api/internal/logic/admin_order/adminrefundorderlogic.go b/app/main/api/internal/logic/admin_order/adminrefundorderlogic.go index 0819ecd..6f0a30f 100644 --- a/app/main/api/internal/logic/admin_order/adminrefundorderlogic.go +++ b/app/main/api/internal/logic/admin_order/adminrefundorderlogic.go @@ -18,10 +18,11 @@ import ( ) const ( - PaymentPlatformAlipay = "alipay" - PaymentPlatformWechat = "wechat" - OrderStatusPaid = "paid" - RefundNoPrefix = "refund-" + PaymentPlatformAlipay = "alipay" + PaymentPlatformWechat = "wechat" + PaymentPlatformEasyPay = "easypay_alipay" + OrderStatusPaid = "paid" + RefundNoPrefix = "refund-" ) type AdminRefundOrderLogic struct { @@ -50,6 +51,8 @@ func (l *AdminRefundOrderLogic) AdminRefundOrder(req *types.AdminRefundOrderReq) return l.handleAlipayRefund(order, req) case PaymentPlatformWechat: return l.handleWechatRefund(order, req) + case PaymentPlatformEasyPay: + return l.handleEasyPayRefund(order, req) default: return nil, errors.Wrapf(xerr.NewErrMsg("不支持的支付平台"), "AdminRefundOrder, 不支持的支付平台: %s", order.PaymentPlatform) } @@ -130,6 +133,40 @@ func (l *AdminRefundOrderLogic) handleWechatRefund(order *model.Order, req *type }, nil } +// handleEasyPayRefund 处理易支付退款 +func (l *AdminRefundOrderLogic) handleEasyPayRefund(order *model.Order, req *types.AdminRefundOrderReq) (*types.AdminRefundOrderResp, error) { + // 检查易支付服务是否启用 + if l.svcCtx.EasyPayService == nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "AdminRefundOrder, 易支付服务未启用") + } + + // 调用易支付退款接口 + err := l.svcCtx.EasyPayService.Refund(l.ctx, order.OrderNo, req.RefundAmount) + if err != nil { + // 易支付退款失败,创建失败记录但不更新订单状态 + refundNo := l.generateRefundNo(order.OrderNo) + createErr := l.createRefundRecordOnly(order, req, refundNo, "", model.OrderRefundStatusFailed) + if createErr != nil { + logx.Errorf("创建易支付退款失败记录时出错: %v", createErr) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "AdminRefundOrder, 易支付退款失败 err: %v", err) + } + + // 易支付退款成功,创建成功记录 + refundNo := l.generateRefundNo(order.OrderNo) + // 易支付退款是同步的,直接标记为成功 + err = l.createRefundRecordAndUpdateOrder(order, req, refundNo, "", model.OrderStatusRefunded, model.OrderRefundStatusSuccess) + if err != nil { + return nil, err + } + + return &types.AdminRefundOrderResp{ + Status: model.OrderStatusRefunded, + RefundNo: refundNo, + Amount: req.RefundAmount, + }, nil +} + // createRefundRecordAndUpdateOrder 创建退款记录并更新订单状态 func (l *AdminRefundOrderLogic) createRefundRecordAndUpdateOrder(order *model.Order, req *types.AdminRefundOrderReq, refundNo, platformRefundId, orderStatus, refundStatus string) error { return l.svcCtx.OrderModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { diff --git a/app/main/api/internal/logic/pay/easypaycallbacklogic.go b/app/main/api/internal/logic/pay/easypaycallbacklogic.go new file mode 100644 index 0000000..953625c --- /dev/null +++ b/app/main/api/internal/logic/pay/easypaycallbacklogic.go @@ -0,0 +1,122 @@ +package pay + +import ( + "context" + "jnc-server/app/main/api/internal/service" + "jnc-server/pkg/lzkit/lzUtils" + "net/http" + "strings" + "time" + + "jnc-server/app/main/api/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type EasyPayCallbackLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewEasyPayCallbackLogic(ctx context.Context, svcCtx *svc.ServiceContext) *EasyPayCallbackLogic { + return &EasyPayCallbackLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *EasyPayCallbackLogic) EasyPayCallback(w http.ResponseWriter, r *http.Request) error { + // 检查易支付服务是否启用 + if l.svcCtx.EasyPayService == nil { + logx.Errorf("易支付服务未启用") + w.WriteHeader(http.StatusInternalServerError) + return nil + } + + notification, err := l.svcCtx.EasyPayService.HandleEasyPayNotification(r) + if err != nil { + logx.Errorf("易支付回调处理失败: %v", err) + w.WriteHeader(http.StatusBadRequest) + return nil + } + + // 根据订单号前缀判断订单类型 + orderNo := notification.OutTradeNo + if strings.HasPrefix(orderNo, "Q_") { + // 查询订单处理 + return l.handleQueryOrderPayment(w, notification) + } else if strings.HasPrefix(orderNo, "A_") { + // 旧系统会员充值订单(已废弃,新系统使用升级功能) + // 直接返回成功,避免旧订单影响 + w.WriteHeader(http.StatusOK) + w.Write([]byte("success")) + return nil + } else if strings.HasPrefix(orderNo, "U_") { + // 系统简化:移除升级功能,直接返回成功 + w.WriteHeader(http.StatusOK) + w.Write([]byte("success")) + return nil + } else { + // 兼容旧订单,假设没有前缀的是查询订单 + return l.handleQueryOrderPayment(w, notification) + } +} + +// 处理查询订单支付 +func (l *EasyPayCallbackLogic) handleQueryOrderPayment(w http.ResponseWriter, notification *service.EasyPayNotification) error { + order, findOrderErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, notification.OutTradeNo) + if findOrderErr != nil { + logx.Errorf("易支付回调,查找订单失败: %+v", findOrderErr) + w.WriteHeader(http.StatusOK) + w.Write([]byte("success")) + return nil + } + + if order.Status != "pending" { + w.WriteHeader(http.StatusOK) + w.Write([]byte("success")) + return nil + } + + // 验证支付状态 + if !l.svcCtx.EasyPayService.IsPaymentSuccess(notification) { + logx.Infof("易支付回调,订单未支付成功,订单号: %s, 状态: %s", notification.OutTradeNo, notification.TradeStatus) + w.WriteHeader(http.StatusOK) + w.Write([]byte("success")) + return nil + } + + // 验证金额(转换为字符串比较) + amountStr := lzUtils.ToAlipayAmount(order.Amount) + if amountStr != notification.Money { + logx.Errorf("易支付回调,金额不一致,订单号: %s, 订单金额: %s, 回调金额: %s", notification.OutTradeNo, amountStr, notification.Money) + w.WriteHeader(http.StatusOK) + w.Write([]byte("success")) + return nil + } + + // 更新订单状态 + order.Status = "paid" + order.PayTime = lzUtils.TimeToNullTime(time.Now()) + order.PlatformOrderId = lzUtils.StringToNullString(notification.TradeNo) + + if updateErr := l.svcCtx.OrderModel.UpdateWithVersion(l.ctx, nil, order); updateErr != nil { + logx.Errorf("易支付回调,修改订单信息失败: %+v", updateErr) + w.WriteHeader(http.StatusOK) + w.Write([]byte("success")) + return nil + } + + // 发送异步任务处理后续流程 + if asyncErr := l.svcCtx.AsynqService.SendQueryTask(order.Id); asyncErr != nil { + logx.Errorf("异步任务调度失败: %v", asyncErr) + // 不返回错误,因为订单已经更新成功 + } + + // 返回success,易支付要求返回纯字符串success + w.WriteHeader(http.StatusOK) + w.Write([]byte("success")) + return nil +} diff --git a/app/main/api/internal/logic/pay/paymentchecklogic.go b/app/main/api/internal/logic/pay/paymentchecklogic.go index d4ed102..3b0fe71 100644 --- a/app/main/api/internal/logic/pay/paymentchecklogic.go +++ b/app/main/api/internal/logic/pay/paymentchecklogic.go @@ -2,10 +2,12 @@ package pay import ( "context" - "strings" "jnc-server/app/main/api/internal/svc" "jnc-server/app/main/api/internal/types" "jnc-server/common/xerr" + "jnc-server/pkg/lzkit/lzUtils" + "strings" + "time" "github.com/pkg/errors" "github.com/zeromicro/go-zero/core/logx" @@ -38,12 +40,61 @@ func (l *PaymentCheckLogic) PaymentCheck(req *types.PaymentCheckReq) (resp *type Status: order.Status, }, nil } - + // 查询订单(包括代理订单) order, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单失败: %v", err) } + + // 如果订单状态是 pending 且支付平台是易支付,主动查询易支付订单状态 + if order.Status == "pending" && order.PaymentPlatform == "easypay_alipay" { + // 检查易支付服务是否启用 + if l.svcCtx.EasyPayService != nil { + // 主动查询易支付订单状态 + queryResp, queryErr := l.svcCtx.EasyPayService.QueryOrderStatus(l.ctx, req.OrderNo) + if queryErr != nil { + logx.Errorf("主动查询易支付订单状态失败,订单号: %s, 错误: %v", req.OrderNo, queryErr) + // 查询失败不影响返回,继续返回当前订单状态 + } else { + // 如果易支付返回订单已支付(status == 1),更新本地订单状态 + if queryResp.Status == 1 { + logx.Infof("主动查询发现易支付订单已支付,订单号: %s,开始更新订单状态", req.OrderNo) + + // 重新查询订单(获取最新版本号) + order, err = l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) + if err != nil { + logx.Errorf("更新订单状态前重新查询订单失败: %v", err) + } else if order.Status == "pending" { + // 更新订单状态 + order.Status = "paid" + order.PayTime = lzUtils.TimeToNullTime(time.Now()) + if queryResp.TradeNo != "" { + order.PlatformOrderId = lzUtils.StringToNullString(queryResp.TradeNo) + } + + if updateErr := l.svcCtx.OrderModel.UpdateWithVersion(l.ctx, nil, order); updateErr != nil { + logx.Errorf("主动查询后更新订单状态失败: %v", updateErr) + } else { + logx.Infof("主动查询后成功更新订单状态为已支付,订单号: %s", req.OrderNo) + + // 发送异步任务处理后续流程 + if asyncErr := l.svcCtx.AsynqService.SendQueryTask(order.Id); asyncErr != nil { + logx.Errorf("主动查询后发送异步任务失败: %v", asyncErr) + } + + // 返回更新后的状态 + return &types.PaymentCheckResp{ + Type: "query", + Status: "paid", + }, nil + } + } + } + } + } + } + return &types.PaymentCheckResp{ Type: "query", Status: order.Status, diff --git a/app/main/api/internal/logic/pay/paymentlogic.go b/app/main/api/internal/logic/pay/paymentlogic.go index 5685f7a..67a72d2 100644 --- a/app/main/api/internal/logic/pay/paymentlogic.go +++ b/app/main/api/internal/logic/pay/paymentlogic.go @@ -5,14 +5,14 @@ import ( "database/sql" "encoding/json" "fmt" - "os" - "strings" - "time" "jnc-server/app/main/api/internal/svc" "jnc-server/app/main/api/internal/types" "jnc-server/app/main/model" "jnc-server/common/ctxdata" "jnc-server/common/xerr" + "os" + "strings" + "time" "github.com/google/uuid" "github.com/pkg/errors" @@ -92,6 +92,11 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp, prepayData, createOrderErr = l.svcCtx.WechatPayService.CreateWechatOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo) } else if req.PayMethod == "alipay" { prepayData, createOrderErr = l.svcCtx.AlipayService.CreateAlipayOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo) + } else if req.PayMethod == "easypay_alipay" { + if l.svcCtx.EasyPayService == nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "易支付服务未启用") + } + prepayData, createOrderErr = l.svcCtx.EasyPayService.CreateEasyPayOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo) } else if req.PayMethod == "appleiap" { prepayData = l.svcCtx.ApplePayService.GetIappayAppID(paymentTypeResp.outTradeNo) } diff --git a/app/main/api/internal/service/easyPayService.go b/app/main/api/internal/service/easyPayService.go new file mode 100644 index 0000000..60c4446 --- /dev/null +++ b/app/main/api/internal/service/easyPayService.go @@ -0,0 +1,388 @@ +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 int `json:"code"` + 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"` +} + +// EasyPayQueryResponse 查询订单响应 +type EasyPayQueryResponse struct { + Code int `json:"code"` + 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 int `json:"status,omitempty"` + Param string `json:"param,omitempty"` + Buyer string `json:"buyer,omitempty"` +} + +// EasyPayRefundResponse 退款响应 +type EasyPayRefundResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` +} + +// 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.Code != 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.Code != 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.Code != 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" +} diff --git a/app/main/api/internal/svc/servicecontext.go b/app/main/api/internal/svc/servicecontext.go index e440b96..e5826ae 100644 --- a/app/main/api/internal/svc/servicecontext.go +++ b/app/main/api/internal/svc/servicecontext.go @@ -74,6 +74,7 @@ type ServiceContext struct { AlipayService *service.AliPayService WechatPayService *service.WechatPayService ApplePayService *service.ApplePayService + EasyPayService *service.EasyPayService ApiRequestService *service.ApiRequestService AsynqServer *asynq.Server AsynqService *service.AsynqService @@ -181,6 +182,15 @@ func NewServiceContext(c config.Config) *ServiceContext { } else { logx.Info("Apple支付服务已禁用") } + + // 根据配置决定是否初始化易支付服务 + var easyPayService *service.EasyPayService + if c.EasyPay.Enabled { + easyPayService = service.NewEasyPayService(c) + logx.Info("易支付服务已启用") + } else { + logx.Info("易支付服务已禁用") + } apiRequestService := service.NewApiRequestService(c, featureModel, productFeatureModel, tianyuanapi) verificationService := service.NewVerificationService(c, tianyuanapi, apiRequestService) asynqService := service.NewAsynqService(c) @@ -261,6 +271,7 @@ func NewServiceContext(c config.Config) *ServiceContext { AlipayService: alipayService, WechatPayService: wechatPayService, ApplePayService: applePayService, + EasyPayService: easyPayService, ApiRequestService: apiRequestService, AsynqServer: asynqServer, AsynqService: asynqService,