mirror of
				https://github.com/cloudreve/cloudreve.git
				synced 2025-11-04 04:47:24 +08:00 
			
		
		
		
	Feat: vas for group / storage pack
This commit is contained in:
		@ -30,7 +30,7 @@ func migration() {
 | 
			
		||||
		DB = DB.Set("gorm:table_options", "ENGINE=InnoDB")
 | 
			
		||||
	}
 | 
			
		||||
	DB.AutoMigrate(&User{}, &Setting{}, &Group{}, &Policy{}, &Folder{}, &File{}, &StoragePack{}, &Share{},
 | 
			
		||||
		&Task{}, &Download{}, &Tag{}, &Webdav{})
 | 
			
		||||
		&Task{}, &Download{}, &Tag{}, &Webdav{}, &Order{})
 | 
			
		||||
 | 
			
		||||
	// 创建初始存储策略
 | 
			
		||||
	addDefaultPolicy()
 | 
			
		||||
@ -136,12 +136,10 @@ box-sizing: border-box; font-size: 14px; margin: 0;"><td style="font-family: 'He
 | 
			
		||||
solid #e9e9e9;"bgcolor="#fff"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 
 | 
			
		||||
14px; margin: 0;"><td class="alert alert-warning"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 16px; vertical-align: top; color: #fff; font-weight: 500; text-align: center; border-radius: 3px 3px 0 0; background-color: #2196F3; margin: 0; padding: 20px;"align="center"bgcolor="#FF9F00"valign="top">重设{siteTitle}密码</td></tr><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-wrap"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;"valign="top"><table width="100%"cellpadding="0"cellspacing="0"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-block"style="font-family: 'Helvetica 
 | 
			
		||||
Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;"valign="top">亲爱的<strong style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">{userName}</strong>:</td></tr><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-block"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;"valign="top">请点击下方按钮完成密码重设。如果非你本人操作,请忽略此邮件。</td></tr><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-block"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;"valign="top"><a href="{resetUrl}"class="btn-primary"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #2196F3; margin: 0; border-color: #2196F3; border-style: solid; border-width: 10px 20px;">重设密码</a></td></tr><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-block"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;"valign="top">感谢您选择{siteTitle}。</td></tr></table></td></tr></table><div class="footer"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;"><table width="100%"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="aligncenter content-block"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;"align="center"valign="top">此邮件由系统自动发送,请不要直接回复。</td></tr></table></div></div></td><td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;"valign="top"></td></tr></table></body></html>`, Type: "mail_template"},
 | 
			
		||||
		{Name: "allow_buy_pack", Value: `1`, Type: "pack"},
 | 
			
		||||
		{Name: "allow_buy_pack_by_pack", Value: `1`, Type: "pack"},
 | 
			
		||||
		{Name: "allow_buy_pack_by_slider", Value: `1`, Type: "pack"},
 | 
			
		||||
		{Name: "pack_data", Value: `[]`, Type: "pack"},
 | 
			
		||||
		{Name: "database_version", Value: `6`, Type: "version"},
 | 
			
		||||
		{Name: "payment_type", Value: `youzan`, Type: "payment"},
 | 
			
		||||
		{Name: "alipay_enabled", Value: `0`, Type: "payment"},
 | 
			
		||||
		{Name: "payjs_enabled", Value: `0`, Type: "payment"},
 | 
			
		||||
		{Name: "appid", Value: ``, Type: "payment"},
 | 
			
		||||
		{Name: "appkey", Value: ``, Type: "payment"},
 | 
			
		||||
		{Name: "shopid", Value: ``, Type: "payment"},
 | 
			
		||||
@ -149,13 +147,8 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti
 | 
			
		||||
		{Name: "allow_buy_group", Value: `1`, Type: "group_sell"},
 | 
			
		||||
		{Name: "group_sell_data", Value: `[]`, Type: "group_sell"},
 | 
			
		||||
		{Name: "gravatar_server", Value: `https://v2ex.assets.uxengine.net/gravatar/`, Type: "avatar"},
 | 
			
		||||
		{Name: "admin_color_body", Value: `fixed-nav sticky-footer bg-light`, Type: "admin"},
 | 
			
		||||
		{Name: "admin_color_nav", Value: `navbar navbar-expand-lg fixed-top navbar-light bg-light`, Type: "admin"},
 | 
			
		||||
		{Name: "js_code", Value: `<script type="text/javascript"></script>`, Type: "basic"},
 | 
			
		||||
		{Name: "defaultTheme", Value: `#3f51b5`, Type: "basic"},
 | 
			
		||||
		{Name: "themes", Value: `{"#3f51b5":{"palette":{"primary":{"light":"#7986cb","main":"#3f51b5","dark":"#303f9f","contrastText":"#fff"},"secondary":{"light":"#ff4081","main":"#f50057","dark":"#c51162","contrastText":"#fff"},"error":{"light":"#e57373","main":"#f44336","dark":"#d32f2f","contrastText":"#fff"},"explorer":{"filename":"#474849","icon":"#8f8f8f","bgSelected":"#D5DAF0","emptyIcon":"#e8e8e8"}}}}`, Type: "basic"},
 | 
			
		||||
		{Name: "refererCheck", Value: `true`, Type: "share"},
 | 
			
		||||
		{Name: "header", Value: `X-Sendfile`, Type: "download"},
 | 
			
		||||
		{Name: "aria2_token", Value: `your token`, Type: "aria2"},
 | 
			
		||||
		{Name: "aria2_token", Value: `your token`, Type: "aria2"},
 | 
			
		||||
		{Name: "aria2_temp_path", Value: ``, Type: "aria2"},
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										45
									
								
								models/order.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								models/order.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/HFO4/cloudreve/pkg/util"
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// PackOrderType 容量包订单
 | 
			
		||||
	PackOrderType = iota
 | 
			
		||||
	// GroupOrderType 用户组订单
 | 
			
		||||
	GroupOrderType
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// OrderUnpaid 未支付
 | 
			
		||||
	OrderUnpaid = iota
 | 
			
		||||
	// OrderPaid 已支付
 | 
			
		||||
	OrderPaid
 | 
			
		||||
	// OrderCanceled 已取消
 | 
			
		||||
	OrderCanceled
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Order 交易订单
 | 
			
		||||
type Order struct {
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	UserID    uint   // 创建者ID
 | 
			
		||||
	OrderNo   string // 商户自定义订单编号
 | 
			
		||||
	Type      int    // 订单类型
 | 
			
		||||
	Method    string // 支付类型
 | 
			
		||||
	ProductID int64  // 商品ID
 | 
			
		||||
	Num       int    // 商品数量
 | 
			
		||||
	Name      string // 订单标题
 | 
			
		||||
	Price     int    // 商品单价
 | 
			
		||||
	Status    int    // 订单状态
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create 创建订单记录
 | 
			
		||||
func (order *Order) Create() (uint, error) {
 | 
			
		||||
	if err := DB.Create(order).Error; err != nil {
 | 
			
		||||
		util.Log().Warning("无法插入离线下载记录, %s", err)
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	return order.ID, nil
 | 
			
		||||
}
 | 
			
		||||
@ -2,6 +2,7 @@ package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/HFO4/cloudreve/pkg/cache"
 | 
			
		||||
	"github.com/HFO4/cloudreve/pkg/util"
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"time"
 | 
			
		||||
@ -18,10 +19,18 @@ type StoragePack struct {
 | 
			
		||||
	Size        uint64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create 创建容量包
 | 
			
		||||
func (pack *StoragePack) Create() (uint, error) {
 | 
			
		||||
	if err := DB.Create(pack).Error; err != nil {
 | 
			
		||||
		util.Log().Warning("无法插入离线下载记录, %s", err)
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	return pack.ID, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetAvailablePackSize 返回给定用户当前可用的容量包总容量
 | 
			
		||||
func (user *User) GetAvailablePackSize() uint64 {
 | 
			
		||||
	var (
 | 
			
		||||
		packs       []StoragePack
 | 
			
		||||
		total       uint64
 | 
			
		||||
		firstExpire *time.Time
 | 
			
		||||
		timeNow     = time.Now()
 | 
			
		||||
@ -35,7 +44,7 @@ func (user *User) GetAvailablePackSize() uint64 {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 查找所有有效容量包
 | 
			
		||||
	DB.Where("expired_time > ? AND user_id = ?", timeNow, user.ID).Find(&packs)
 | 
			
		||||
	packs := user.GetAvailableStoragePacks()
 | 
			
		||||
 | 
			
		||||
	// 计算总容量, 并找到其中最早的过期时间
 | 
			
		||||
	for _, v := range packs {
 | 
			
		||||
@ -60,6 +69,15 @@ func (user *User) GetAvailablePackSize() uint64 {
 | 
			
		||||
	return total
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetAvailableStoragePacks 返回用户可用的容量包
 | 
			
		||||
func (user *User) GetAvailableStoragePacks() []StoragePack {
 | 
			
		||||
	var packs []StoragePack
 | 
			
		||||
	timeNow := time.Now()
 | 
			
		||||
	// 查找所有有效容量包
 | 
			
		||||
	DB.Where("expired_time > ? AND user_id = ?", timeNow, user.ID).Find(&packs)
 | 
			
		||||
	return packs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetExpiredStoragePack 获取已过期的容量包
 | 
			
		||||
func GetExpiredStoragePack() []StoragePack {
 | 
			
		||||
	var packs []StoragePack
 | 
			
		||||
 | 
			
		||||
@ -318,6 +318,7 @@ func GetTolerantExpiredUser() []User {
 | 
			
		||||
// GroupFallback 回退到初始用户组
 | 
			
		||||
func (user *User) GroupFallback() {
 | 
			
		||||
	if user.GroupExpires != nil && user.PreviousGroupID != 0 {
 | 
			
		||||
		user.Group.ID = user.PreviousGroupID
 | 
			
		||||
		DB.Model(&user).Updates(map[string]interface{}{
 | 
			
		||||
			"group_expires":     nil,
 | 
			
		||||
			"previous_group_id": 0,
 | 
			
		||||
@ -325,3 +326,13 @@ func (user *User) GroupFallback() {
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpgradeGroup 升级用户组
 | 
			
		||||
func (user *User) UpgradeGroup(id uint, expires *time.Time) error {
 | 
			
		||||
	user.Group.ID = id
 | 
			
		||||
	return DB.Model(&user).Updates(map[string]interface{}{
 | 
			
		||||
		"group_expires":     expires,
 | 
			
		||||
		"previous_group_id": user.GroupID,
 | 
			
		||||
		"group_id":          id,
 | 
			
		||||
	}).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										86
									
								
								pkg/payment/order.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								pkg/payment/order.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,86 @@
 | 
			
		||||
package payment
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	model "github.com/HFO4/cloudreve/models"
 | 
			
		||||
	"github.com/HFO4/cloudreve/pkg/serializer"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	// ErrUnknownPaymentMethod 未知支付方式
 | 
			
		||||
	ErrUnknownPaymentMethod = serializer.NewError(serializer.CodeNotFound, "未知支付方式", nil)
 | 
			
		||||
	// ErrUnsupportedPaymentMethod 未知支付方式
 | 
			
		||||
	ErrUnsupportedPaymentMethod = serializer.NewError(serializer.CodeNotFound, "此订单不支持此支付方式", nil)
 | 
			
		||||
	// ErrInsertOrder 无法插入订单记录
 | 
			
		||||
	ErrInsertOrder = serializer.NewError(serializer.CodeDBError, "无法插入订单记录", nil)
 | 
			
		||||
	// ErrScoreNotEnough 积分不足
 | 
			
		||||
	ErrScoreNotEnough = serializer.NewError(serializer.CodeNoPermissionErr, "积分不足", nil)
 | 
			
		||||
	// ErrCreateStoragePack 无法创建容量包
 | 
			
		||||
	ErrCreateStoragePack = serializer.NewError(serializer.CodeNoPermissionErr, "无法创建容量包", nil)
 | 
			
		||||
	// ErrGroupConflict 用户组冲突
 | 
			
		||||
	ErrGroupConflict = serializer.NewError(serializer.CodeNoPermissionErr, "当前用户组仍未过期,请前往个人设置手动解约后继续", nil)
 | 
			
		||||
	// ErrGroupInvalid 用户组冲突
 | 
			
		||||
	ErrGroupInvalid = serializer.NewError(serializer.CodeNoPermissionErr, "用户组不可用", nil)
 | 
			
		||||
	// ErrUpgradeGroup 用户组冲突
 | 
			
		||||
	ErrUpgradeGroup = serializer.NewError(serializer.CodeDBError, "无法升级用户组", 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"` // 是否需要支付
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewPaymentInstance 获取新的支付实例
 | 
			
		||||
func NewPaymentInstance(method string) (Pay, error) {
 | 
			
		||||
	if method == "score" {
 | 
			
		||||
		return &ScorePayment{}, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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.GroupOrderType
 | 
			
		||||
		productID = group.ID
 | 
			
		||||
		title = group.Name
 | 
			
		||||
		price = group.Price
 | 
			
		||||
	} else {
 | 
			
		||||
		orderType = model.PackOrderType
 | 
			
		||||
		productID = pack.ID
 | 
			
		||||
		title = pack.Name
 | 
			
		||||
		price = pack.Price
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 创建订单记录
 | 
			
		||||
	order := &model.Order{
 | 
			
		||||
		UserID:    user.ID,
 | 
			
		||||
		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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										64
									
								
								pkg/payment/purchase.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								pkg/payment/purchase.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,64 @@
 | 
			
		||||
package payment
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	model "github.com/HFO4/cloudreve/models"
 | 
			
		||||
	"github.com/HFO4/cloudreve/pkg/serializer"
 | 
			
		||||
	"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)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func checkGroupUpgrade(user *model.User, groupInfo *serializer.GroupProducts) error {
 | 
			
		||||
	// 检查用户是否已有未过期用户
 | 
			
		||||
	if user.PreviousGroupID != 0 {
 | 
			
		||||
		return ErrGroupConflict
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 用户组不能相同
 | 
			
		||||
	if user.GroupID == groupInfo.GroupID {
 | 
			
		||||
		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 err := user.UpgradeGroup(groupInfo.GroupID, &expires); err != nil {
 | 
			
		||||
		return ErrUpgradeGroup.WithError(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										44
									
								
								pkg/payment/score.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								pkg/payment/score.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,44 @@
 | 
			
		||||
package payment
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	model "github.com/HFO4/cloudreve/models"
 | 
			
		||||
	"github.com/HFO4/cloudreve/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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 创建订单记录
 | 
			
		||||
	if _, err := order.Create(); err != nil {
 | 
			
		||||
		return nil, ErrInsertOrder.WithError(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &OrderCreateRes{
 | 
			
		||||
		Payment: false,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										84
									
								
								pkg/serializer/vas.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								pkg/serializer/vas.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,84 @@
 | 
			
		||||
package serializer
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	model "github.com/HFO4/cloudreve/models"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type quota struct {
 | 
			
		||||
	Base  uint64         `json:"base"`
 | 
			
		||||
	Pack  uint64         `json:"pack"`
 | 
			
		||||
	Used  uint64         `json:"used"`
 | 
			
		||||
	Total uint64         `json:"total"`
 | 
			
		||||
	Packs []storagePacks `json:"packs"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type storagePacks struct {
 | 
			
		||||
	Name           string `json:"name"`
 | 
			
		||||
	Size           uint64 `json:"size"`
 | 
			
		||||
	ActivateDate   string `json:"activate_date"`
 | 
			
		||||
	Expiration     int    `json:"expiration"`
 | 
			
		||||
	ExpirationDate string `json:"expiration_date"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BuildUserQuotaResponse 序列化用户存储配额概况响应
 | 
			
		||||
func BuildUserQuotaResponse(user *model.User, packs []model.StoragePack) Response {
 | 
			
		||||
	packSize := user.GetAvailablePackSize()
 | 
			
		||||
	res := quota{
 | 
			
		||||
		Base:  user.Group.MaxStorage,
 | 
			
		||||
		Pack:  packSize,
 | 
			
		||||
		Used:  user.Storage,
 | 
			
		||||
		Total: packSize + user.Group.MaxStorage,
 | 
			
		||||
		Packs: make([]storagePacks, 0, len(packs)),
 | 
			
		||||
	}
 | 
			
		||||
	for _, pack := range packs {
 | 
			
		||||
		res.Packs = append(res.Packs, storagePacks{
 | 
			
		||||
			Name:           pack.Name,
 | 
			
		||||
			Size:           pack.Size,
 | 
			
		||||
			ActivateDate:   pack.ActiveTime.Format("2006-01-02 15:04:05"),
 | 
			
		||||
			Expiration:     int(pack.ExpiredTime.Sub(*pack.ActiveTime).Seconds()),
 | 
			
		||||
			ExpirationDate: pack.ExpiredTime.Format("2006-01-02 15:04:05"),
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return Response{
 | 
			
		||||
		Data: res,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PackProduct 容量包商品
 | 
			
		||||
type PackProduct struct {
 | 
			
		||||
	ID    int64  `json:"id"`
 | 
			
		||||
	Name  string `json:"name"`
 | 
			
		||||
	Size  uint64 `json:"size"`
 | 
			
		||||
	Time  int64  `json:"time"`
 | 
			
		||||
	Price int    `json:"price"`
 | 
			
		||||
	Score int    `json:"score"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GroupProducts 用户组商品
 | 
			
		||||
type GroupProducts struct {
 | 
			
		||||
	ID        int64    `json:"id"`
 | 
			
		||||
	Name      string   `json:"name"`
 | 
			
		||||
	GroupID   uint     `json:"group_id"`
 | 
			
		||||
	Time      int64    `json:"time"`
 | 
			
		||||
	Price     int      `json:"price"`
 | 
			
		||||
	Score     int      `json:"score"`
 | 
			
		||||
	Des       []string `json:"des"`
 | 
			
		||||
	Highlight bool     `json:"highlight"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BuildProductResponse 构建增值服务商品响应
 | 
			
		||||
func BuildProductResponse(groups []GroupProducts, packs []PackProduct, alipay, payjs bool) Response {
 | 
			
		||||
	// 隐藏响应中的用户组ID
 | 
			
		||||
	for i := 0; i < len(groups); i++ {
 | 
			
		||||
		groups[i].GroupID = 0
 | 
			
		||||
	}
 | 
			
		||||
	return Response{
 | 
			
		||||
		Data: map[string]interface{}{
 | 
			
		||||
			"packs":  packs,
 | 
			
		||||
			"groups": groups,
 | 
			
		||||
			"alipay": alipay,
 | 
			
		||||
			"payjs":  payjs,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								routers/controllers/vas.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								routers/controllers/vas.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,39 @@
 | 
			
		||||
package controllers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/HFO4/cloudreve/service/vas"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetQuota 获取容量配额信息
 | 
			
		||||
func GetQuota(c *gin.Context) {
 | 
			
		||||
	var service vas.GeneralVASService
 | 
			
		||||
	if err := c.ShouldBindUri(&service); err == nil {
 | 
			
		||||
		res := service.Quota(c, CurrentUser(c))
 | 
			
		||||
		c.JSON(200, res)
 | 
			
		||||
	} else {
 | 
			
		||||
		c.JSON(200, ErrorResponse(err))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetProduct 获取商品信息
 | 
			
		||||
func GetProduct(c *gin.Context) {
 | 
			
		||||
	var service vas.GeneralVASService
 | 
			
		||||
	if err := c.ShouldBindUri(&service); err == nil {
 | 
			
		||||
		res := service.Products(c, CurrentUser(c))
 | 
			
		||||
		c.JSON(200, res)
 | 
			
		||||
	} else {
 | 
			
		||||
		c.JSON(200, ErrorResponse(err))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewOrder 新建支付订单
 | 
			
		||||
func NewOrder(c *gin.Context) {
 | 
			
		||||
	var service vas.CreateOrderService
 | 
			
		||||
	if err := c.ShouldBindJSON(&service); err == nil {
 | 
			
		||||
		res := service.Create(c, CurrentUser(c))
 | 
			
		||||
		c.JSON(200, res)
 | 
			
		||||
	} else {
 | 
			
		||||
		c.JSON(200, ErrorResponse(err))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -354,6 +354,17 @@ func InitMasterRouter() *gin.Engine {
 | 
			
		||||
				tag.DELETE(":id", middleware.HashID(hashid.TagID), controllers.DeleteTag)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 增值服务相关
 | 
			
		||||
			vas := auth.Group("vas")
 | 
			
		||||
			{
 | 
			
		||||
				// 获取容量包及配额信息
 | 
			
		||||
				vas.GET("pack", controllers.GetQuota)
 | 
			
		||||
				// 获取商品信息,同时返回支付信息
 | 
			
		||||
				vas.GET("product", controllers.GetProduct)
 | 
			
		||||
				// 新建支付订单
 | 
			
		||||
				vas.POST("order", controllers.NewOrder)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										97
									
								
								service/vas/quota.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								service/vas/quota.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,97 @@
 | 
			
		||||
package vas
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	model "github.com/HFO4/cloudreve/models"
 | 
			
		||||
	"github.com/HFO4/cloudreve/pkg/payment"
 | 
			
		||||
	"github.com/HFO4/cloudreve/pkg/serializer"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GeneralVASService 通用增值服务
 | 
			
		||||
type GeneralVASService struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateOrderService 创建订单服务
 | 
			
		||||
type CreateOrderService struct {
 | 
			
		||||
	Action string `json:"action" binding:"required,eq=group|eq=pack"`
 | 
			
		||||
	Method string `json:"method" binding:"required,eq=alipay|eq=score|eq=payjs"`
 | 
			
		||||
	ID     int64  `json:"id" binding:"required"`
 | 
			
		||||
	Num    int    `json:"num" binding:"required,min=1,max=99"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create 创建新订单
 | 
			
		||||
func (service *CreateOrderService) Create(c *gin.Context, user *model.User) serializer.Response {
 | 
			
		||||
	// 取得当前商品信息
 | 
			
		||||
	packs, groups, err := decodeProductInfo()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return serializer.Err(serializer.CodeInternalSetting, "无法解析商品设置", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 查找要购买的商品
 | 
			
		||||
	var (
 | 
			
		||||
		pack  *serializer.PackProduct
 | 
			
		||||
		group *serializer.GroupProducts
 | 
			
		||||
	)
 | 
			
		||||
	if service.Action == "group" {
 | 
			
		||||
		for _, v := range groups {
 | 
			
		||||
			if v.ID == service.ID {
 | 
			
		||||
				group = &v
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		for _, v := range packs {
 | 
			
		||||
			if v.ID == service.ID {
 | 
			
		||||
				pack = &v
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if pack == nil && group == nil {
 | 
			
		||||
		return serializer.Err(serializer.CodeNotFound, "商品不存在", nil)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 创建订单
 | 
			
		||||
	res, err := payment.NewOrder(pack, group, service.Num, service.Method, user)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return serializer.Err(serializer.CodeNotSet, err.Error(), err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return serializer.Response{Data: res}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Products 获取商品信息
 | 
			
		||||
func (service *GeneralVASService) Products(c *gin.Context, user *model.User) serializer.Response {
 | 
			
		||||
	options := model.GetSettingByNames("alipay_enabled", "payjs_enabled")
 | 
			
		||||
	packs, groups, err := decodeProductInfo()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return serializer.Err(serializer.CodeInternalSetting, "无法解析商品设置", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return serializer.BuildProductResponse(groups, packs, options["alipay_enabled"] == "1", options["payjs_enabled"] == "1")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func decodeProductInfo() ([]serializer.PackProduct, []serializer.GroupProducts, error) {
 | 
			
		||||
	options := model.GetSettingByNames("pack_data", "group_sell_data", "alipay_enabled", "payjs_enabled")
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		packs  []serializer.PackProduct
 | 
			
		||||
		groups []serializer.GroupProducts
 | 
			
		||||
	)
 | 
			
		||||
	if err := json.Unmarshal([]byte(options["pack_data"]), &packs); err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if err := json.Unmarshal([]byte(options["group_sell_data"]), &groups); err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return packs, groups, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Quota 获取容量配额信息
 | 
			
		||||
func (service *GeneralVASService) Quota(c *gin.Context, user *model.User) serializer.Response {
 | 
			
		||||
	packs := user.GetAvailableStoragePacks()
 | 
			
		||||
	return serializer.BuildUserQuotaResponse(user, packs)
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user