package services import ( "encoding/json" "errors" "fmt" "platform/pkg/u" "platform/web/core" m "platform/web/models" q "platform/web/queries" "time" "github.com/shopspring/decimal" "gorm.io/gorm" ) var Resource = &resourceService{} type resourceService struct{} 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) if err != nil { return err } // 检查余额 _, amount, err := s.GetPrice(sku, data.Count(), &uid) if err != nil { return err } 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 := createResource(q, uid, now, data) if err != nil { return core.NewServErr("创建套餐失败", err) } // 生成账单 err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), sku.Name, amount, resource)) if err != nil { return core.NewServErr("生成账单失败", err) } return nil }) } func (s *resourceService) CreateResourceByTrade(uid int32, now time.Time, data *CreateResourceByTradeData, trade *m.Trade) error { // 检查交易 if trade == nil { return core.NewBizErr("交易数据不能为空") } if trade.Status != m.TradeStatusSuccess { return core.NewBizErr("交易状态不正确") } return q.Q.Transaction(func(q *q.Query) error { // 保存套餐 resource, err := createResource(q, uid, now, data.Req) if err != nil { return core.NewServErr("创建套餐失败", err) } // 生成账单 err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), data.GetSubject(), data.GetAmount(), resource, trade)) if err != nil { return core.NewServErr("生成账单失败", err) } return nil }) } func createResource(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, } 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, } 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, } 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) GetSku(data *CreateResourceData) (*m.ProductSku, error) { sku, err := q.ProductSku.Where(q.ProductSku.Code.Eq(data.Code())).Take() if err != nil { return nil, core.NewServErr("产品不可用", err) } return sku, nil } func (s *resourceService) GetPrice(sku *m.ProductSku, count int32, uid *int32) (decimal.Decimal, decimal.Decimal, error) { // 根据用户 id 查询特殊优惠 var uSku *m.ProductSkuUser if uid != nil { var err error uSku, err = q.ProductSkuUser.Where( q.ProductSkuUser.UserID.Eq(*uid), q.ProductSkuUser.ProductSkuID.Eq(sku.ID), ).Take() if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return decimal.Zero, decimal.Zero, core.NewServErr("客户特殊价查询失败", err) } } // 返回计算价格 price := sku.Price if uSku != nil && uSku.Price != nil { price = *uSku.Price } discount := sku.Discount if uSku != nil && uSku.Discount != nil { discount = *uSku.Discount } before := price.Mul(decimal.NewFromInt32(count)) after := before.Mul(decimal.NewFromFloat32(discount)) return before, after, nil } type CreateResourceData struct { Type m.ResourceType `json:"type" validate:"required"` Short *CreateShortResourceData `json:"short,omitempty"` Long *CreateLongResourceData `json:"long,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) Serialize() (string, error) { bytes, err := json.Marshal(c) return string(bytes), err } func (c *CreateResourceData) Deserialize(str string) error { return json.Unmarshal([]byte(str), c) } // 交易后创建套餐 type ResourceOnTradeComplete struct{} func (r ResourceOnTradeComplete) Check(t m.TradeType) (ProductInfo, bool) { if t == m.TradeTypePurchase { return &CreateResourceByTradeData{}, true } return nil, false } func (r ResourceOnTradeComplete) OnTradeComplete(info ProductInfo, trade *m.Trade) error { return Resource.CreateResourceByTrade(trade.UserID, time.Time(*trade.CompletedAt), info.(*CreateResourceByTradeData), trade) } type CreateResourceByTradeData struct { Subject string `json:"subject"` Amount decimal.Decimal `json:"amount"` Req *CreateResourceData `json:"data"` } func (e CreateResourceByTradeData) GetType() m.TradeType { return m.TradeTypePurchase } func (e CreateResourceByTradeData) GetSubject() string { return e.Subject } func (e CreateResourceByTradeData) GetAmount() decimal.Decimal { return e.Amount } func (e CreateResourceByTradeData) Serialize() (string, error) { bytes, err := json.Marshal(e) return string(bytes), err } func (e *CreateResourceByTradeData) Deserialize(str string) error { return json.Unmarshal([]byte(str), e) } func NewCreateResourceByTradeData(req *CreateResourceData) (*CreateResourceByTradeData, error) { sku, err := Resource.GetSku(req) if err != nil { return nil, err } _, amount, err := Resource.GetPrice(sku, req.Count(), nil) if err != nil { return nil, err } return &CreateResourceByTradeData{ Subject: sku.Name, Amount: amount, Req: req, }, nil } // 服务错误 type ResourceServiceErr string func (e ResourceServiceErr) Error() string { return string(e) } const ( ErrBalanceNotEnough = ResourceServiceErr("余额不足") )