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" ) 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 } // 检查余额 amount, err := data.GetAmount() 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) } // 生成账单 subject, err := data.GetSubject() if err != nil { return err } err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), subject, amount, resource)) if err != nil { return core.NewServErr("生成账单失败", err) } return nil }) } func (s *resourceService) CreateResourceByTrade(uid int32, now time.Time, data *CreateResourceData, 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) if err != nil { return core.NewServErr("创建套餐失败", err) } // 生成账单 subject, err := data.GetSubject() if err != nil { return err } amount, err := data.GetAmount() if err != nil { return err } err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), subject, amount, 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 } 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,min=180"` 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) GetType() m.TradeType { return m.TradeTypePurchase } func (c *CreateResourceData) GetSubject() (string, error) { switch { default: return "", errors.New("无效的套餐类型") case c.Type == m.ResourceTypeShort && c.Short != nil: return c.Short.GetSubject() case c.Type == m.ResourceTypeLong && c.Long != nil: return c.Long.GetSubject() } } func (c *CreateResourceData) GetAmount() (decimal.Decimal, error) { switch { default: return decimal.Zero, errors.New("无效的套餐类型") case c.Type == m.ResourceTypeShort && c.Short != nil: return c.Short.GetAmount() case c.Type == m.ResourceTypeLong && c.Long != nil: return c.Long.GetAmount() } } 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) } func (data *CreateShortResourceData) GetSubject() (string, error) { if data.name == "" { var mode string switch data.Mode { default: return "", errors.New("无效的套餐模式") case m.ResourceModeTime: mode = "包时" case m.ResourceModeQuota: mode = "包量" } data.name = fmt.Sprintf("短效动态%s %v 分钟", mode, data.Live/60) } return data.name, nil } func (data *CreateShortResourceData) GetAmount() (decimal.Decimal, error) { if data.price == nil { var factor int32 switch data.Mode { default: return decimal.Zero, errors.New("无效的套餐模式") case m.ResourceModeTime: if data.Expire == nil { return decimal.Zero, errors.New("包时套餐过期时间不能为空") } factor = data.Quota * *data.Expire case m.ResourceModeQuota: factor = data.Quota } var base = data.Live if base == 180 { base = 150 } var dec = decimal.Decimal{}. Add(decimal.NewFromInt32(base * factor)). Div(decimal.NewFromInt(30000)) if dec.IsZero() { return decimal.Zero, errors.New("计算金额错误") } data.price = &dec } return *data.price, nil } func (data *CreateLongResourceData) GetSubject() (string, error) { if data.name == "" { var mode string switch data.Mode { default: return "", errors.New("无效的套餐模式") case m.ResourceModeTime: mode = "包时" case m.ResourceModeQuota: mode = "包量" } data.name = fmt.Sprintf("长效动态%s %d 小时", mode, data.Live) } return data.name, nil } func (data *CreateLongResourceData) GetAmount() (decimal.Decimal, error) { if data.price == nil { var factor int32 = 0 switch data.Mode { default: return decimal.Zero, errors.New("无效的套餐模式") case m.ResourceModeTime: if data.Expire == nil { return decimal.Zero, errors.New("包时套餐过期时间不能为空") } factor = *data.Expire * data.Quota case m.ResourceModeQuota: factor = data.Quota } var base int32 switch data.Live { default: return decimal.Zero, errors.New("无效的套餐时长") case 1: base = 30 case 4: base = 80 case 8: base = 120 case 12: base = 180 case 24: base = 350 } // data.price = big.NewRat(int64(base*factor), 100) var dec = decimal.Decimal{}. Add(decimal.NewFromInt32(base * factor)). Div(decimal.NewFromInt(100)) if dec.IsZero() { return decimal.Zero, errors.New("计算金额错误") } data.price = &dec } return *data.price, nil } // 交易后创建套餐 type ResourceOnTradeComplete struct{} func (r ResourceOnTradeComplete) Check(t m.TradeType) (ProductInfo, bool) { if t == m.TradeTypePurchase { return &CreateResourceData{}, true } return nil, false } func (r ResourceOnTradeComplete) OnTradeComplete(info ProductInfo, trade *m.Trade) error { return Resource.CreateResourceByTrade(trade.UserID, time.Time(*trade.CompletedAt), info.(*CreateResourceData), trade) } // 服务错误 type ResourceServiceErr string func (e ResourceServiceErr) Error() string { return string(e) } const ( ErrBalanceNotEnough = ResourceServiceErr("余额不足") )