From e38a60ea44ed4ed2cf6b33f22affd9b5771de1a2 Mon Sep 17 00:00:00 2001 From: HFO4 <912394456@qq.com> Date: Sun, 16 Feb 2020 14:31:23 +0800 Subject: [PATCH] Feat: vas for group / storage pack --- models/migration.go | 13 ++--- models/order.go | 45 ++++++++++++++++++ models/storage_pack.go | 22 ++++++++- models/user.go | 11 +++++ pkg/payment/order.go | 86 +++++++++++++++++++++++++++++++++ pkg/payment/purchase.go | 64 +++++++++++++++++++++++++ pkg/payment/score.go | 44 +++++++++++++++++ pkg/serializer/vas.go | 84 +++++++++++++++++++++++++++++++++ routers/controllers/vas.go | 39 +++++++++++++++ routers/router.go | 11 +++++ service/vas/quota.go | 97 ++++++++++++++++++++++++++++++++++++++ 11 files changed, 504 insertions(+), 12 deletions(-) create mode 100644 models/order.go create mode 100644 pkg/payment/order.go create mode 100644 pkg/payment/purchase.go create mode 100644 pkg/payment/score.go create mode 100644 pkg/serializer/vas.go create mode 100644 routers/controllers/vas.go create mode 100644 service/vas/quota.go diff --git a/models/migration.go b/models/migration.go index 59f5844..4150182 100644 --- a/models/migration.go +++ b/models/migration.go @@ -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;">重设{siteTitle}密码
亲爱的{userName}
请点击下方按钮完成密码重设。如果非你本人操作,请忽略此邮件。
重设密码
感谢您选择{siteTitle}。
`, 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: ``, 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"}, diff --git a/models/order.go b/models/order.go new file mode 100644 index 0000000..48af776 --- /dev/null +++ b/models/order.go @@ -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 +} diff --git a/models/storage_pack.go b/models/storage_pack.go index fff09bd..b826395 100644 --- a/models/storage_pack.go +++ b/models/storage_pack.go @@ -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 diff --git a/models/user.go b/models/user.go index b21b819..b5ae094 100644 --- a/models/user.go +++ b/models/user.go @@ -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 +} diff --git a/pkg/payment/order.go b/pkg/payment/order.go new file mode 100644 index 0000000..20d9408 --- /dev/null +++ b/pkg/payment/order.go @@ -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) +} diff --git a/pkg/payment/purchase.go b/pkg/payment/purchase.go new file mode 100644 index 0000000..249d36c --- /dev/null +++ b/pkg/payment/purchase.go @@ -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 +} diff --git a/pkg/payment/score.go b/pkg/payment/score.go new file mode 100644 index 0000000..8e49a26 --- /dev/null +++ b/pkg/payment/score.go @@ -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 +} diff --git a/pkg/serializer/vas.go b/pkg/serializer/vas.go new file mode 100644 index 0000000..adc08f9 --- /dev/null +++ b/pkg/serializer/vas.go @@ -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, + }, + } +} diff --git a/routers/controllers/vas.go b/routers/controllers/vas.go new file mode 100644 index 0000000..ed1f0cb --- /dev/null +++ b/routers/controllers/vas.go @@ -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)) + } +} diff --git a/routers/router.go b/routers/router.go index 88ebef5..62b837a 100644 --- a/routers/router.go +++ b/routers/router.go @@ -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) + } + } } diff --git a/service/vas/quota.go b/service/vas/quota.go new file mode 100644 index 0000000..1e7c086 --- /dev/null +++ b/service/vas/quota.go @@ -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) +}