This commit is contained in:
2024-02-25 08:30:34 +08:00
commit 4947f39e74
273 changed files with 45396 additions and 0 deletions

43
pkg/payment/alipay.go Normal file
View File

@ -0,0 +1,43 @@
package payment
import (
"fmt"
"net/url"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
alipay "github.com/smartwalle/alipay/v3"
)
// Alipay 支付宝当面付支付处理
type Alipay struct {
Client *alipay.Client
}
// Create 创建订单
func (pay *Alipay) Create(order *model.Order, pack *serializer.PackProduct, group *serializer.GroupProducts, user *model.User) (*OrderCreateRes, error) {
gateway, _ := url.Parse("/api/v3/callback/alipay")
var p = alipay.TradePreCreate{
Trade: alipay.Trade{
NotifyURL: model.GetSiteURL().ResolveReference(gateway).String(),
Subject: order.Name,
OutTradeNo: order.OrderNo,
TotalAmount: fmt.Sprintf("%.2f", float64(order.Price*order.Num)/100),
},
}
if _, err := order.Create(); err != nil {
return nil, ErrInsertOrder.WithError(err)
}
res, err := pay.Client.TradePreCreate(p)
if err != nil {
return nil, ErrIssueOrder.WithError(err)
}
return &OrderCreateRes{
Payment: true,
QRCode: res.QRCode,
ID: order.OrderNo,
}, nil
}

93
pkg/payment/custom.go Normal file
View File

@ -0,0 +1,93 @@
package payment
import (
"encoding/json"
"errors"
"fmt"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/request"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/gofrs/uuid"
"github.com/qiniu/go-sdk/v7/sms/bytes"
"net/http"
"net/url"
)
// Custom payment client
type Custom struct {
client request.Client
endpoint string
authClient auth.Auth
}
const (
paymentTTL = 3600 * 24 // 24h
CallbackSessionPrefix = "custom_payment_callback_"
)
func newCustomClient(endpoint, secret string) *Custom {
authClient := auth.HMACAuth{
SecretKey: []byte(secret),
}
return &Custom{
endpoint: endpoint,
authClient: auth.General,
client: request.NewClient(
request.WithCredential(authClient, paymentTTL),
request.WithMasterMeta(),
),
}
}
// Request body from Cloudreve to create a new payment
type NewCustomOrderRequest struct {
Name string `json:"name"` // Order name
OrderNo string `json:"order_no"` // Order number
NotifyURL string `json:"notify_url"` // Payment callback url
Amount int64 `json:"amount"` // Order total amount
}
// Create a new payment
func (pay *Custom) Create(order *model.Order, pack *serializer.PackProduct, group *serializer.GroupProducts, user *model.User) (*OrderCreateRes, error) {
callbackID := uuid.Must(uuid.NewV4())
gateway, _ := url.Parse(fmt.Sprintf("/api/v3/callback/custom/%s/%s", order.OrderNo, callbackID))
callback, err := auth.SignURI(pay.authClient, model.GetSiteURL().ResolveReference(gateway).String(), paymentTTL)
if err != nil {
return nil, fmt.Errorf("failed to sign callback url: %w", err)
}
cache.Set(CallbackSessionPrefix+callbackID.String(), order.OrderNo, paymentTTL)
body := &NewCustomOrderRequest{
Name: order.Name,
OrderNo: order.OrderNo,
NotifyURL: callback.String(),
Amount: int64(order.Price * order.Num),
}
bodyJson, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to encode body: %w", err)
}
res, err := pay.client.Request("POST", pay.endpoint, bytes.NewReader(bodyJson)).
CheckHTTPResponse(http.StatusOK).DecodeResponse()
if err != nil {
return nil, fmt.Errorf("failed to request payment gateway: %w", err)
}
if res.Code != 0 {
return nil, errors.New(res.Error)
}
if _, err := order.Create(); err != nil {
return nil, ErrInsertOrder.WithError(err)
}
return &OrderCreateRes{
Payment: true,
QRCode: res.Data.(string),
ID: order.OrderNo,
}, nil
}

171
pkg/payment/order.go Normal file
View File

@ -0,0 +1,171 @@
package payment
import (
"fmt"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/iGoogle-ink/gopay/wechat/v3"
"github.com/qingwg/payjs"
"github.com/smartwalle/alipay/v3"
"math/rand"
"net/url"
"time"
)
var (
// ErrUnknownPaymentMethod 未知支付方式
ErrUnknownPaymentMethod = serializer.NewError(serializer.CodeInternalSetting, "Unknown payment method", nil)
// ErrUnsupportedPaymentMethod 未知支付方式
ErrUnsupportedPaymentMethod = serializer.NewError(serializer.CodeInternalSetting, "This order cannot be paid with this method", nil)
// ErrInsertOrder 无法插入订单记录
ErrInsertOrder = serializer.NewError(serializer.CodeDBError, "Failed to insert order record", nil)
// ErrScoreNotEnough 积分不足
ErrScoreNotEnough = serializer.NewError(serializer.CodeInsufficientCredit, "", nil)
// ErrCreateStoragePack 无法创建容量包
ErrCreateStoragePack = serializer.NewError(serializer.CodeDBError, "Failed to create storage pack record", nil)
// ErrGroupConflict 用户组冲突
ErrGroupConflict = serializer.NewError(serializer.CodeGroupConflict, "", nil)
// ErrGroupInvalid 用户组冲突
ErrGroupInvalid = serializer.NewError(serializer.CodeGroupInvalid, "", nil)
// ErrAdminFulfillGroup 管理员无法购买用户组
ErrAdminFulfillGroup = serializer.NewError(serializer.CodeFulfillAdminGroup, "", nil)
// ErrUpgradeGroup 用户组冲突
ErrUpgradeGroup = serializer.NewError(serializer.CodeDBError, "Failed to update user's group", nil)
// ErrUInitPayment 无法初始化支付实例
ErrUInitPayment = serializer.NewError(serializer.CodeInternalSetting, "Failed to initialize payment client", nil)
// ErrIssueOrder 订单接口请求失败
ErrIssueOrder = serializer.NewError(serializer.CodeInternalSetting, "Failed to create order", nil)
// ErrOrderNotFound 订单不存在
ErrOrderNotFound = serializer.NewError(serializer.CodeNotFound, "", nil)
)
// Pay 支付处理接口
type Pay interface {
Create(order *model.Order, pack *serializer.PackProduct, group *serializer.GroupProducts, user *model.User) (*OrderCreateRes, error)
}
// OrderCreateRes 订单创建结果
type OrderCreateRes struct {
Payment bool `json:"payment"` // 是否需要支付
ID string `json:"id,omitempty"` // 订单号
QRCode string `json:"qr_code,omitempty"` // 支付二维码指向的地址
}
// NewPaymentInstance 获取新的支付实例
func NewPaymentInstance(method string) (Pay, error) {
switch method {
case "score":
return &ScorePayment{}, nil
case "alipay":
options := model.GetSettingByNames("alipay_enabled", "appid", "appkey", "shopid")
if options["alipay_enabled"] != "1" {
return nil, ErrUnknownPaymentMethod
}
// 初始化支付宝客户端
var client, err = alipay.New(options["appid"], options["appkey"], true)
if err != nil {
return nil, ErrUInitPayment.WithError(err)
}
// 加载支付宝公钥
err = client.LoadAliPayPublicKey(options["shopid"])
if err != nil {
return nil, ErrUInitPayment.WithError(err)
}
return &Alipay{Client: client}, nil
case "payjs":
options := model.GetSettingByNames("payjs_enabled", "payjs_secret", "payjs_id")
if options["payjs_enabled"] != "1" {
return nil, ErrUnknownPaymentMethod
}
callback, _ := url.Parse("/api/v3/callback/payjs")
payjsConfig := &payjs.Config{
Key: options["payjs_secret"],
MchID: options["payjs_id"],
NotifyUrl: model.GetSiteURL().ResolveReference(callback).String(),
}
return &PayJSClient{Client: payjs.New(payjsConfig)}, nil
case "wechat":
options := model.GetSettingByNames("wechat_enabled", "wechat_appid", "wechat_mchid", "wechat_serial_no", "wechat_api_key", "wechat_pk_content")
if options["wechat_enabled"] != "1" {
return nil, ErrUnknownPaymentMethod
}
client, err := wechat.NewClientV3(options["wechat_appid"], options["wechat_mchid"], options["wechat_serial_no"], options["wechat_api_key"], options["wechat_pk_content"])
if err != nil {
return nil, ErrUInitPayment.WithError(err)
}
return &Wechat{Client: client, ApiV3Key: options["wechat_api_key"]}, nil
case "custom":
options := model.GetSettingByNames("custom_payment_enabled", "custom_payment_endpoint", "custom_payment_secret")
if !model.IsTrueVal(options["custom_payment_enabled"]) {
return nil, ErrUnknownPaymentMethod
}
return newCustomClient(options["custom_payment_endpoint"], options["custom_payment_secret"]), nil
default:
return nil, ErrUnknownPaymentMethod
}
}
// NewOrder 创建新订单
func NewOrder(pack *serializer.PackProduct, group *serializer.GroupProducts, num int, method string, user *model.User) (*OrderCreateRes, error) {
// 获取支付实例
pay, err := NewPaymentInstance(method)
if err != nil {
return nil, err
}
var (
orderType int
productID int64
title string
price int
)
if pack != nil {
orderType = model.PackOrderType
productID = pack.ID
title = pack.Name
price = pack.Price
} else if group != nil {
if err := checkGroupUpgrade(user, group); err != nil {
return nil, err
}
orderType = model.GroupOrderType
productID = group.ID
title = group.Name
price = group.Price
} else {
orderType = model.ScoreOrderType
productID = 0
title = fmt.Sprintf("%d 积分", num)
price = model.GetIntSetting("score_price", 1)
}
// 创建订单记录
order := &model.Order{
UserID: user.ID,
OrderNo: orderID(),
Type: orderType,
Method: method,
ProductID: productID,
Num: num,
Name: fmt.Sprintf("%s - %s", model.GetSettingByName("siteName"), title),
Price: price,
Status: model.OrderUnpaid,
}
return pay.Create(order, pack, group, user)
}
func orderID() string {
return fmt.Sprintf("%s%d",
time.Now().Format("20060102150405"),
100000+rand.Intn(900000),
)
}

31
pkg/payment/payjs.go Normal file
View File

@ -0,0 +1,31 @@
package payment
import (
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/qingwg/payjs"
)
// PayJSClient PayJS支付处理
type PayJSClient struct {
Client *payjs.PayJS
}
// Create 创建订单
func (pay *PayJSClient) Create(order *model.Order, pack *serializer.PackProduct, group *serializer.GroupProducts, user *model.User) (*OrderCreateRes, error) {
if _, err := order.Create(); err != nil {
return nil, ErrInsertOrder.WithError(err)
}
PayNative := pay.Client.GetNative()
res, err := PayNative.Create(int64(order.Price*order.Num), order.Name, order.OrderNo, "", "")
if err != nil {
return nil, ErrIssueOrder.WithError(err)
}
return &OrderCreateRes{
Payment: true,
QRCode: res.CodeUrl,
ID: order.OrderNo,
}, nil
}

137
pkg/payment/purchase.go Normal file
View File

@ -0,0 +1,137 @@
package payment
import (
"encoding/json"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"strconv"
"time"
)
// GivePack 创建容量包
func GivePack(user *model.User, packInfo *serializer.PackProduct, num int) error {
timeNow := time.Now()
expires := timeNow.Add(time.Duration(packInfo.Time*int64(num)) * time.Second)
pack := model.StoragePack{
Name: packInfo.Name,
UserID: user.ID,
ActiveTime: &timeNow,
ExpiredTime: &expires,
Size: packInfo.Size,
}
if _, err := pack.Create(); err != nil {
return ErrCreateStoragePack.WithError(err)
}
cache.Deletes([]string{strconv.FormatUint(uint64(user.ID), 10)}, "pack_size_")
return nil
}
func checkGroupUpgrade(user *model.User, groupInfo *serializer.GroupProducts) error {
if user.Group.ID == 1 {
return ErrAdminFulfillGroup
}
// 检查用户是否已有未过期用户
if user.PreviousGroupID != 0 && user.GroupID != groupInfo.GroupID {
return ErrGroupConflict
}
// 用户组不能相同
if user.GroupID == groupInfo.GroupID && user.PreviousGroupID == 0 {
return ErrGroupInvalid
}
return nil
}
// GiveGroup 升级用户组
func GiveGroup(user *model.User, groupInfo *serializer.GroupProducts, num int) error {
if err := checkGroupUpgrade(user, groupInfo); err != nil {
return err
}
timeNow := time.Now()
expires := timeNow.Add(time.Duration(groupInfo.Time*int64(num)) * time.Second)
if user.PreviousGroupID != 0 {
expires = user.GroupExpires.Add(time.Duration(groupInfo.Time*int64(num)) * time.Second)
}
if err := user.UpgradeGroup(groupInfo.GroupID, &expires); err != nil {
return ErrUpgradeGroup.WithError(err)
}
return nil
}
// GiveScore 积分充值
func GiveScore(user *model.User, num int) error {
user.AddScore(num)
return nil
}
// GiveProduct “发货”
func GiveProduct(user *model.User, pack *serializer.PackProduct, group *serializer.GroupProducts, num int) error {
if pack != nil {
return GivePack(user, pack, num)
} else if group != nil {
return GiveGroup(user, group, num)
} else {
return GiveScore(user, num)
}
}
// OrderPaid 订单已支付处理
func OrderPaid(orderNo string) error {
order, err := model.GetOrderByNo(orderNo)
if err != nil || order.Status == model.OrderPaid {
return ErrOrderNotFound.WithError(err)
}
// 更新订单状态为 已支付
order.UpdateStatus(model.OrderPaid)
user, err := model.GetActiveUserByID(order.UserID)
if err != nil {
return serializer.NewError(serializer.CodeUserNotFound, "", err)
}
// 查询商品
options := model.GetSettingByNames("pack_data", "group_sell_data")
var (
packs []serializer.PackProduct
groups []serializer.GroupProducts
)
if err := json.Unmarshal([]byte(options["pack_data"]), &packs); err != nil {
return err
}
if err := json.Unmarshal([]byte(options["group_sell_data"]), &groups); err != nil {
return err
}
// 查找要购买的商品
var (
pack *serializer.PackProduct
group *serializer.GroupProducts
)
if order.Type == model.GroupOrderType {
for _, v := range groups {
if v.ID == order.ProductID {
group = &v
break
}
}
} else if order.Type == model.PackOrderType {
for _, v := range packs {
if v.ID == order.ProductID {
pack = &v
break
}
}
}
// "发货"
return GiveProduct(&user, pack, group, order.Num)
}

45
pkg/payment/score.go Normal file
View File

@ -0,0 +1,45 @@
package payment
import (
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
)
// ScorePayment 积分支付处理
type ScorePayment struct {
}
// Create 创建新订单
func (pay *ScorePayment) Create(order *model.Order, pack *serializer.PackProduct, group *serializer.GroupProducts, user *model.User) (*OrderCreateRes, error) {
if pack != nil {
order.Price = pack.Score
} else {
order.Price = group.Score
}
// 检查此订单是否可用积分支付
if order.Price == 0 {
return nil, ErrUnsupportedPaymentMethod
}
// 扣除用户积分
if !user.PayScore(order.Price * order.Num) {
return nil, ErrScoreNotEnough
}
// 商品“发货”
if err := GiveProduct(user, pack, group, order.Num); err != nil {
user.AddScore(order.Price * order.Num)
return nil, err
}
// 创建订单记录
order.Status = model.OrderPaid
if _, err := order.Create(); err != nil {
return nil, ErrInsertOrder.WithError(err)
}
return &OrderCreateRes{
Payment: false,
}, nil
}

88
pkg/payment/wechat.go Normal file
View File

@ -0,0 +1,88 @@
package payment
import (
"errors"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/iGoogle-ink/gopay"
"github.com/iGoogle-ink/gopay/wechat/v3"
"net/url"
"time"
)
// Wechat 微信扫码支付接口
type Wechat struct {
Client *wechat.ClientV3
ApiV3Key string
}
// Create 创建订单
func (pay *Wechat) Create(order *model.Order, pack *serializer.PackProduct, group *serializer.GroupProducts, user *model.User) (*OrderCreateRes, error) {
gateway, _ := url.Parse("/api/v3/callback/wechat")
bm := make(gopay.BodyMap)
bm.
Set("description", order.Name).
Set("out_trade_no", order.OrderNo).
Set("notify_url", model.GetSiteURL().ResolveReference(gateway).String()).
SetBodyMap("amount", func(bm gopay.BodyMap) {
bm.Set("total", int64(order.Price*order.Num)).
Set("currency", "CNY")
})
wxRsp, err := pay.Client.V3TransactionNative(bm)
if err != nil {
return nil, ErrIssueOrder.WithError(err)
}
if wxRsp.Code == wechat.Success {
if _, err := order.Create(); err != nil {
return nil, ErrInsertOrder.WithError(err)
}
return &OrderCreateRes{
Payment: true,
QRCode: wxRsp.Response.CodeUrl,
ID: order.OrderNo,
}, nil
}
return nil, ErrIssueOrder.WithError(errors.New(wxRsp.Error))
}
// GetPlatformCert 获取微信平台证书
func (pay *Wechat) GetPlatformCert() string {
if cert, ok := cache.Get("wechat_platform_cert"); ok {
return cert.(string)
}
res, err := pay.Client.GetPlatformCerts()
if err == nil {
// 使用反馈证书中启用时间较晚的
var (
currentLatest *time.Time
currentCert string
)
for _, cert := range res.Certs {
effectiveTime, err := time.Parse("2006-01-02T15:04:05-0700", cert.EffectiveTime)
if err != nil {
if currentLatest == nil {
currentLatest = &effectiveTime
currentCert = cert.PublicKey
continue
}
if currentLatest.Before(effectiveTime) {
currentLatest = &effectiveTime
currentCert = cert.PublicKey
}
}
}
cache.Set("wechat_platform_cert", currentCert, 3600*10)
return currentCert
}
util.Log().Debug("Failed to get Wechat Pay platform certificate: %s", err)
return ""
}