Files
platform/web/services/resource.go

293 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package services
import (
"fmt"
"platform/pkg/u"
"platform/web/core"
m "platform/web/models"
q "platform/web/queries"
"time"
"github.com/shopspring/decimal"
"gorm.io/gen/field"
)
var Resource = &resourceService{}
type resourceService struct{}
// CreateResourceByBalance 通过余额购买套餐
func (s *resourceService) CreateResourceByBalance(user *m.User, data *CreateResourceData) error {
now := time.Now()
// 获取 sku
detail, err := data.TradeDetail(user)
if err != nil {
return core.NewServErr("获取产品支付信息失败", err)
}
return q.Q.Transaction(func(q *q.Query) error {
// 更新用户余额
if err := User.UpdateBalance(q, user, detail.Actual.Neg(), "余额购买产品", nil); err != nil {
return core.NewServErr("更新用户余额失败", err)
}
// 保存套餐
resource, err := s.Create(q, user.ID, now, data)
if err != nil {
return core.NewServErr("创建套餐失败", err)
}
// 生成账单
err = Bill.CreateForResource(q, user.ID, resource.ID, nil, detail)
if err != nil {
return core.NewServErr("生成账单失败", err)
}
// 核销优惠券
if detail.CouponId != nil {
err = Coupon.UseCoupon(q, *detail.CouponId)
if err != nil {
return core.NewServErr("核销优惠券失败", err)
}
}
return nil
})
}
func (s *resourceService) Create(q *q.Query, uid int32, now time.Time, data *CreateResourceData) (*m.Resource, error) {
// 套餐基本信息
var resource = m.Resource{
UserID: uid,
ResourceNo: u.P(ID.GenReadable("res")),
Active: true,
Type: data.Type,
Code: data.Type.Code(),
}
switch data.Type {
// 短效套餐
case m.ResourceTypeShort:
var short = data.Short
if short == nil {
return nil, core.NewBizErr("短效套餐数据不能为空")
}
resource.Short = &m.ResourceShort{
Live: short.Live,
Type: short.Mode,
Quota: short.Quota,
Code: data.Code(),
}
if short.Mode == m.ResourceModeTime {
if short.Expire == nil {
return nil, core.NewBizErr("包时套餐过期时间不能为空")
}
var duration = time.Duration(*short.Expire) * 24 * time.Hour
resource.Short.ExpireAt = u.P(now.Add(duration))
}
// 长效套餐
case m.ResourceTypeLong:
var long = data.Long
if long == nil {
return nil, core.NewBizErr("长效套餐数据不能为空")
}
resource.Long = &m.ResourceLong{
Live: long.Live,
Type: long.Mode,
Quota: long.Quota,
Code: data.Code(),
}
if long.Mode == m.ResourceModeTime {
if long.Expire == nil {
return nil, core.NewBizErr("包时套餐过期时间不能为空")
}
var duration = time.Duration(*long.Expire) * 24 * time.Hour
resource.Long.ExpireAt = u.P(now.Add(duration))
}
default:
return nil, core.NewBizErr("不支持的套餐类型")
}
err := q.Resource.Create(&resource)
if err != nil {
return nil, core.NewServErr("创建套餐失败", err)
}
return &resource, nil
}
func (s *resourceService) Update(data *UpdateResourceData) error {
if data.Active == nil {
return core.NewBizErr("更新套餐失败active 不能为空")
}
do := make([]field.AssignExpr, 0)
if data.Active != nil {
do = append(do, q.Resource.Active.Value(*data.Active))
}
_, err := q.Resource.
Where(q.Resource.ID.Eq(data.Id)).
UpdateSimple(do...)
if err != nil {
return core.NewServErr("更新套餐失败", err)
}
return nil
}
type UpdateResourceData struct {
core.IdReq
Active *bool `json:"active"`
}
func (s *resourceService) CalcPrice(skuCode string, count int32, user *m.User, couponCode *string) (*m.ProductSku, *m.ProductDiscount, *m.Coupon, decimal.Decimal, decimal.Decimal, error) {
sku, err := q.ProductSku.
Joins(q.ProductSku.Discount).
Where(q.ProductSku.Code.Eq(skuCode)).
Take()
if err != nil {
return nil, nil, nil, decimal.Zero, decimal.Zero, core.NewServErr("产品不可用", err)
}
// 原价
price := sku.Price
amount := price.Mul(decimal.NewFromInt32(count))
// 折扣价
discount := sku.Discount
if discount == nil {
return nil, nil, nil, decimal.Zero, decimal.Zero, core.NewServErr("价格查询失败", err)
}
discountRate := discount.Rate()
if user != nil && user.DiscountID != nil { // 用户特殊优惠
uDiscount, err := q.ProductDiscount.Where(q.ProductDiscount.ID.Eq(*user.DiscountID)).Take()
if err != nil {
return nil, nil, nil, decimal.Zero, decimal.Zero, core.NewServErr("客户特殊价查询失败", err)
}
uDiscountRate := uDiscount.Rate()
if uDiscountRate.Cmp(discountRate) > 0 {
discountRate = uDiscountRate
discount = uDiscount
}
}
discounted := amount.Mul(discountRate)
// 优惠价
uid := (*int32)(nil)
if user != nil {
uid = &user.ID
}
coupon := (*m.Coupon)(nil)
couponApplied := discounted.Copy()
if couponCode != nil {
var err error
coupon, err = Coupon.GetCouponAvailableByCode(*couponCode, discounted, uid)
if err != nil {
return nil, nil, nil, decimal.Zero, decimal.Zero, err
}
couponApplied = discounted.Sub(coupon.Amount)
}
return sku, discount, coupon, discounted, couponApplied, nil
}
type CreateResourceData struct {
Type m.ResourceType `json:"type" validate:"required"`
Short *CreateShortResourceData `json:"short,omitempty"`
Long *CreateLongResourceData `json:"long,omitempty"`
CouponCode *string `json:"coupon,omitempty"`
}
type CreateShortResourceData struct {
Live int32 `json:"live" validate:"required"`
Mode m.ResourceMode `json:"mode" validate:"required"`
Quota int32 `json:"quota" validate:"required"`
Expire *int32 `json:"expire"`
name string
price *decimal.Decimal
}
type CreateLongResourceData struct {
Live int32 `json:"live" validate:"required"`
Mode m.ResourceMode `json:"mode" validate:"required"`
Quota int32 `json:"quota" validate:"required"`
Expire *int32 `json:"expire"`
name string
price *decimal.Decimal
}
func (data *CreateResourceData) Count() int32 {
switch {
default:
return 0
case data.Type == m.ResourceTypeShort && data.Short != nil:
return data.Short.Quota
case data.Type == m.ResourceTypeLong && data.Long != nil:
return data.Long.Quota
}
}
func (data *CreateResourceData) Code() string {
switch {
default:
return ""
case data.Type == m.ResourceTypeShort && data.Short != nil:
return fmt.Sprintf(
"mode=%s,live=%d,expire=%d",
data.Short.Mode.Code(), data.Short.Live, u.Else(data.Short.Expire, 0),
)
case data.Type == m.ResourceTypeLong && data.Long != nil:
return fmt.Sprintf(
"mode=%s,live=%d,expire=%d",
data.Long.Mode.Code(), data.Long.Live, u.Else(data.Long.Expire, 0),
)
}
}
func (data *CreateResourceData) TradeDetail(user *m.User) (*TradeDetail, error) {
sku, discount, coupon, amount, actual, err := Resource.CalcPrice(data.Code(), data.Count(), user, data.CouponCode)
if err != nil {
return nil, err
}
var discountId *int32 = nil
if discount != nil {
discountId = &discount.ID
}
var couponId *int32 = nil
if coupon != nil {
couponId = &coupon.ID
}
return &TradeDetail{
data,
m.TradeTypePurchase,
sku.Name,
amount, actual,
discountId, discount,
couponId, coupon,
}, nil
}
// 服务错误
type ResourceServiceErr string
func (e ResourceServiceErr) Error() string {
return string(e)
}
const (
ErrBalanceNotEnough = ResourceServiceErr("余额不足")
)