package services import ( "errors" "fmt" "platform/pkg/u" "platform/web/core" m "platform/web/models" q "platform/web/queries" "time" "github.com/shopspring/decimal" "gorm.io/gen/field" "gorm.io/gorm" ) var Resource = &resourceService{} type resourceService struct{} // CreateResourceByBalance 通过余额购买套餐 func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data *CreateResourceData) error { // 找到用户 user, err := q.User. Where(q.User.ID.Eq(uid)). Take() if err != nil { return err } // 获取 sku sku, err := s.GetSku(data.Code()) if err != nil { return err } // 检查余额 coupon, _, amount, actual, err := s.GetPrice(sku, data.Count(), &uid, data.CouponCode) if err != nil { return err } couponId := (*int32)(nil) if coupon != nil { couponId = &coupon.ID } newBalance := user.Balance.Sub(amount) if newBalance.IsNegative() { return ErrBalanceNotEnough } return q.Q.Transaction(func(q *q.Query) error { // 更新用户余额 _, err = q.User. Where( q.User.ID.Eq(uid), q.User.Balance.Eq(user.Balance), ). UpdateSimple(q.User.Balance.Value(newBalance)) if err != nil { return core.NewServErr("更新用户余额失败", err) } // 保存套餐 resource, err := s.Create(q, uid, now, data) if err != nil { return core.NewServErr("创建套餐失败", err) } // 生成账单 err = Bill.CreateForResourceByBalance(q, uid, resource.ID, couponId, sku.Name, amount, actual) if err != nil { return core.NewServErr("生成账单失败", err) } // 核销优惠券 if coupon != nil { err = Coupon.UseCoupon(q, coupon.ID) 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) GetSku(code string) (*m.ProductSku, error) { sku, err := q.ProductSku. Joins(q.ProductSku.Discount). Where(q.ProductSku.Code.Eq(code)). Take() if err != nil { return nil, core.NewServErr("产品不可用", err) } if sku.Discount == nil { return nil, core.NewServErr("价格查询失败", err) } return sku, nil } func (s *resourceService) GetPrice(sku *m.ProductSku, count int32, uid *int32, couponCode *string) (*m.Coupon, decimal.Decimal, decimal.Decimal, decimal.Decimal, error) { // 原价 price := sku.Price amount := price.Mul(decimal.NewFromInt32(count)) // 折扣价 discount := sku.Discount.Decimal() if uid != nil { // 用户特殊优惠 var err error uSku, err := q.ProductSkuUser. Joins(q.ProductSkuUser.Discount). Where( q.ProductSkuUser.UserID.Eq(*uid), q.ProductSkuUser.ProductSkuID.Eq(sku.ID)). Take() if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewServErr("客户特殊价查询失败", err) } if uSku.Discount == nil { return nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewServErr("价格获取失败") } uDiscount := uSku.Discount.Decimal() if uDiscount.Cmp(discount) > 0 { discount = uDiscount } } discounted := amount.Mul(discount) // 优惠价 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, decimal.Zero, decimal.Zero, decimal.Zero, err } couponApplied = discounted.Sub(coupon.Amount) } return coupon, amount, 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 (c *CreateResourceData) Count() int32 { switch { default: return 0 case c.Type == m.ResourceTypeShort && c.Short != nil: return c.Short.Quota case c.Type == m.ResourceTypeLong && c.Long != nil: return c.Long.Quota } } func (c *CreateResourceData) Code() string { switch { default: return "" case c.Type == m.ResourceTypeShort && c.Short != nil: return fmt.Sprintf( "mode=%s,live=%d,expire=%d", c.Short.Mode.Code(), c.Short.Live, u.Else(c.Short.Expire, 0), ) case c.Type == m.ResourceTypeLong && c.Long != nil: return fmt.Sprintf( "mode=%s,live=%d,expire=%d", c.Long.Mode.Code(), c.Long.Live, u.Else(c.Long.Expire, 0), ) } } func (c *CreateResourceData) TradeDetail() (*TradeDetail, error) { sku, err := Resource.GetSku(c.Code()) if err != nil { return nil, err } coupon, _, amount, actual, err := Resource.GetPrice(sku, c.Count(), nil, c.CouponCode) if err != nil { return nil, err } return &TradeDetail{ m.TradeTypePurchase, sku.Name, amount, actual, &coupon.ID, c, }, nil } // 服务错误 type ResourceServiceErr string func (e ResourceServiceErr) Error() string { return string(e) } const ( ErrBalanceNotEnough = ResourceServiceErr("余额不足") )