mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-01 14:46:36 +08:00
279 lines
8.0 KiB
Go
279 lines
8.0 KiB
Go
/*
|
|
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
|
|
}
|