/* Copyright 2025 linux.do Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package payment import ( "crypto/md5" "crypto/subtle" "errors" "fmt" "net/http" "sort" "strconv" "strings" "github.com/gin-gonic/gin" "github.com/linux-do/credit/internal/apps/oauth" "github.com/linux-do/credit/internal/common" "github.com/linux-do/credit/internal/db" "github.com/linux-do/credit/internal/model" "github.com/linux-do/credit/internal/service" "github.com/linux-do/credit/internal/util" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) // HandleParseOrderNoError 处理 ParseOrderNo 返回的错误,返回对应的 HTTP 响应 func HandleParseOrderNoError(c *gin.Context, err error) bool { if err == nil { return false } errMsg := err.Error() switch errMsg { case OrderNotFound: c.JSON(http.StatusNotFound, util.Err(errMsg)) case MerchantInfoNotFound, PayConfigNotFound: c.JSON(http.StatusInternalServerError, util.Err(errMsg)) case common.CannotPaySelf, common.TestModeCannotProcessOrder, OrderNoFormatError: c.JSON(http.StatusBadRequest, util.Err(errMsg)) case common.UnAuthorized: c.JSON(http.StatusUnauthorized, util.Err(errMsg)) default: c.JSON(http.StatusInternalServerError, util.Err(errMsg)) } return true } // OrderContext 订单上下文信息 type OrderContext struct { OrderID uint64 MerchantUser *model.User CurrentUser *model.User PayerPayConfig *model.UserPayConfig MerchantPayConfig *model.UserPayConfig MerchantAPIKey *model.MerchantAPIKey } // ParseOrderNo 解析订单号,获取订单上下文信息 func ParseOrderNo(c *gin.Context, orderNo string) (*OrderContext, error) { merchantIDStr, errGet := db.Redis.Get(c.Request.Context(), db.PrefixedKey(fmt.Sprintf(OrderMerchantIDCacheKeyFormat, orderNo))).Result() if errGet != nil { if errors.Is(errGet, redis.Nil) { return nil, errors.New(OrderNotFound) } return nil, errGet } merchantID, errParse := strconv.ParseUint(merchantIDStr, 10, 64) if errParse != nil { return nil, errors.New(OrderNoFormatError) } // 获取商户用户信息 var merchantUser model.User if err := db.DB(c.Request.Context()).Where("id = ? AND is_active = ?", merchantID, true).First(&merchantUser).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New(MerchantInfoNotFound) } return nil, err } currentUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey) orderNoStr, errDecrypt := util.Decrypt(merchantUser.SignKey, orderNo) if errDecrypt != nil { return nil, errors.New(OrderNoFormatError) } orderID, errParse := strconv.ParseUint(orderNoStr, 10, 64) if errParse != nil { return nil, errors.New(OrderNoFormatError) } var apiKey model.MerchantAPIKey if err := db.DB(c.Request.Context()). Where("client_id = (SELECT client_id FROM orders WHERE id = ?)", orderID). First(&apiKey).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New(OrderNotFound) } return nil, err } // 验证测试模式下的支付权限 if err := service.ValidateTestModePayment(currentUser.ID, merchantUser.ID, apiKey.TestMode); err != nil { return nil, err } ctx := &OrderContext{ OrderID: orderID, MerchantUser: &merchantUser, CurrentUser: currentUser, MerchantAPIKey: &apiKey, } // 获取付款用户的支付配置(用于限额检查) var payerPayConfig model.UserPayConfig if err := payerPayConfig.GetByPayScore(db.DB(c.Request.Context()), currentUser.PayScore); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New(PayConfigNotFound) } return nil, err } ctx.PayerPayConfig = &payerPayConfig // 获取商家的支付配置(用于手续费倍率) var merchantPayConfig model.UserPayConfig if err := merchantPayConfig.GetByPayScore(db.DB(c.Request.Context()), merchantUser.PayScore); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New(PayConfigNotFound) } return nil, err } ctx.MerchantPayConfig = &merchantPayConfig return ctx, nil } // GenerateSignature 生成签名 func GenerateSignature(params map[string]string, secret string, isMD5 bool) string { // 按key排序 keys := make([]string, 0, len(params)) for k := range params { if k == "sign" || k == "sign_type" { continue } // 空值不参与签名 if params[k] == "" { continue } keys = append(keys, k) } sort.Strings(keys) // 拼接签名字符串 var builder strings.Builder builder.Grow(256) for i, k := range keys { if i > 0 { builder.WriteByte('&') } builder.WriteString(k) builder.WriteByte('=') builder.WriteString(params[k]) } builder.WriteString(secret) if isMD5 { // MD5加密 hash := md5.Sum([]byte(builder.String())) return fmt.Sprintf("%x", hash) } return builder.String() } // VerifySignatureMD5 验证MD5签名 func VerifySignatureMD5(c *gin.Context, apiKey *model.MerchantAPIKey) (*CreateOrderRequest, error) { var req EPayRequest if err := c.ShouldBind(&req); err != nil { return nil, err } // 验证金额 if err := util.ValidateAmount(req.Amount); err != nil { return nil, err } if err := apiKey.GetByClientID(db.DB(c.Request.Context()), req.ClientID); err != nil { return nil, err } // 构建签名参数 params := map[string]string{ "pid": req.ClientID, "type": req.PayType, "out_trade_no": util.DerefString(req.MerchantOrderNo), "notify_url": req.NotifyURL, "return_url": req.ReturnURL, "name": req.OrderName, "device": req.Device, } params["money"] = req.Amount.Truncate(2).StringFixed(2) expectedSignFixed := GenerateSignature(params, apiKey.ClientSecret, true) params["money"] = req.Amount.Truncate(2).String() expectedSignTrimmed := GenerateSignature(params, apiKey.ClientSecret, true) matchFixed := subtle.ConstantTimeCompare([]byte(strings.ToLower(expectedSignFixed)), []byte(strings.ToLower(req.Sign))) == 1 matchTrimmed := subtle.ConstantTimeCompare([]byte(strings.ToLower(expectedSignTrimmed)), []byte(strings.ToLower(req.Sign))) == 1 if !matchFixed && !matchTrimmed { return nil, errors.New("签名验证失败") } return NewCreateOrderRequest(req.OrderName, req.MerchantOrderNo, req.Amount, req.PayType, req.NotifyURL, req.ReturnURL), nil } // VerifySignatureEd25519 验证 Ed25519 签名 func VerifySignatureEd25519(c *gin.Context, apiKey *model.MerchantAPIKey) (*CreateOrderRequest, error) { var req LDCPayRequest if err := c.ShouldBind(&req); err != nil { return nil, err } // 验证金额 if err := util.ValidateAmount(req.Amount); err != nil { return nil, err } if err := apiKey.GetByClientID(db.DB(c.Request.Context()), req.ClientID); err != nil { return nil, err } if len(apiKey.PublicKey) == 0 { return nil, errors.New("商户未配置公钥") } signatureBytes, err := util.Base64Decode(req.Sign) if err != nil { return nil, errors.New("签名格式错误") } // 构建签名参数 params := map[string]string{ "client_id": req.ClientID, "type": req.PayType, "out_trade_no": util.DerefString(req.MerchantOrderNo), "order_name": req.OrderName, "notify_url": req.NotifyURL, "return_url": req.ReturnURL, "money": req.Amount.Truncate(2).StringFixed(2), } signatureParam := GenerateSignature(params, apiKey.ClientSecret, false) validTrimmed := util.Ed25519Verify(apiKey.PublicKey, []byte(signatureParam), signatureBytes) if !validTrimmed { return nil, errors.New("签名验证失败") } return NewCreateOrderRequest(req.OrderName, req.MerchantOrderNo, req.Amount, req.PayType, req.NotifyURL, req.ReturnURL), nil }