package services import ( "context" "database/sql" "encoding/json" "fmt" "platform/pkg/u" bill2 "platform/web/domains/bill" resource2 "platform/web/domains/resource" trade2 "platform/web/domains/trade" g "platform/web/globals" "platform/web/globals/orm" m "platform/web/models" q "platform/web/queries" "time" "github.com/shopspring/decimal" ) var Resource = &resourceService{} type resourceService struct{} func (s *resourceService) CreateResource(uid int32, now time.Time, ser *CreateResourceData) error { data, err := ser.ToData() if err != nil { return err } var name = data.GetName() var amount = data.GetPrice() // 保存交易信息 err = q.Q.Transaction(func(q *q.Query) error { // 检查用户 user, err := q.User. Where(q.User.ID.Eq(uid)). Take() if err != nil { return err } // 检查余额 if user.Balance.Cmp(amount) < 0 { return ErrBalanceNotEnough } // 保存套餐 resource, err := createResource(q, uid, now, data) if err != nil { return err } // 生成账单 bill := m.Bill{ UserID: uid, ResourceID: &resource.ID, BillNo: ID.GenReadable("bil"), Info: u.P("购买套餐 - " + name), Type: int32(bill2.TypeConsume), Amount: amount, } err = q.Bill. Omit(q.Bill.TradeID, q.Bill.RefundID). Create(&bill) if err != nil { return err } // 更新用户余额 _, err = q.User. Where(q.User.ID.Eq(uid)). UpdateSimple(q.User.Balance.Value(user.Balance.Sub(amount))) if err != nil { return err } return nil }, &sql.TxOptions{Isolation: sql.LevelRepeatableRead}) if err != nil { return err } return nil } func (s *resourceService) PrepareResource(uid int32, now time.Time, ser *PrepareResourceData) (*TradeCreateResult, error) { data, err := ser.ToData() if err != nil { return nil, err } name := data.GetName() amount := data.GetPrice() method := ser.PaymentMethod platform := ser.PaymentPlatform // 保存到数据库 var result *TradeCreateResult err = q.Q.Transaction(func(q *q.Query) error { var err error // 生成交易订单 result, err = Trade.CreateTrade(q, uid, now, &TradeCreateData{ Subject: "购买套餐 - " + name, Amount: amount, ExpireAt: time.Now().Add(30 * time.Minute), Type: trade2.TypeRecharge, Method: method, Platform: platform, }) if err != nil { return err } // 保存请求缓存 resourceSerializer := new(PrepareResourceData) if err := resourceSerializer.ByData(data); err != nil { return err } err = g.Redis.Set(context.Background(), result.TradeNo, &PrepareResourceCache{ Uid: uid, TradeId: result.Trade.ID, BillId: result.Bill.ID, PrepareResourceData: resourceSerializer, }, 30*time.Minute).Err() if err != nil { return err } return nil }) if err != nil { return nil, err } return result, nil } func (s *resourceService) CompleteResource(tradeNo string, now time.Time, opResult ...*TradeSuccessResult) error { // 获取请求缓存 reqStr, err := g.Redis.Get(context.Background(), tradeNo).Result() if err != nil { return err } cache := new(PrepareResourceCache) if err := json.Unmarshal([]byte(reqStr), cache); err != nil { return err } // 检查交易结果 var rs *TradeSuccessResult if len(opResult) > 0 && opResult[0] != nil { rs = opResult[0] } else { var err error rs, err = Trade.VerifyTrade(&TradeVerifyData{ TradeNo: tradeNo, Method: cache.PaymentMethod, }) if err != nil { return err } } data, err := cache.ToData() if err != nil { return err } // 保存交易信息 err = q.Q.Transaction(func(q *q.Query) error { // 完成交易 _, err = Trade.OnTradeCreated(q, &OnTradeCreateData{ TradeNo: tradeNo, TradeSuccessResult: *rs, }) if err != nil { return fmt.Errorf("完成交易失败: %w", err) } // 保存套餐 resource, err := createResource(q, cache.Uid, now, data) if err != nil { return fmt.Errorf("创建套餐失败: %w", err) } // 更新账单 _, err = q.Bill.Debug(). Select(q.Bill.ResourceID). Updates(&m.Bill{ ID: cache.BillId, ResourceID: &resource.ID, }) if err != nil { return fmt.Errorf("更新账单失败: %w", err) } // 删除缓存 err = g.Redis.Del(context.Background(), tradeNo).Err() if err != nil { return fmt.Errorf("删除缓存失败: %w", err) } return nil }) if err != nil { return err } return nil } func (s *resourceService) CancelResource(tradeNo string, now time.Time, opRevoked ...bool) error { // 删除请求缓存 cacheStr, err := g.Redis.GetDel(context.Background(), tradeNo).Result() if err != nil { return err } cache := new(PrepareResourceCache) if err := json.Unmarshal([]byte(cacheStr), cache); err != nil { return err } // 取消交易 if len(opRevoked) <= 0 { err = Trade.CancelTrade(tradeNo, cache.PaymentMethod) if err != nil { return err } } // 更新订单状态 err = Trade.OnTradeCanceled(q.Q, tradeNo, now) if err != nil { return err } return nil } func createResource(q *q.Query, uid int32, now time.Time, data CreateTypeResourceDataInter) (*m.Resource, error) { // 套餐基本信息 var resource = m.Resource{ UserID: uid, ResourceNo: u.P(ID.GenReadable("res")), Active: true, } switch data := data.(type) { // 短效套餐 case *CreateShortResourceData: var duration = time.Duration(data.Expire) * 24 * time.Hour resource.Type = int32(resource2.TypeShort) resource.Short = &m.ResourceShort{ Type: data.Mode, Live: data.Live, Quota: &data.Quota, Expire: u.P(orm.LocalDateTime(now.Add(duration))), DailyLimit: data.DailyLimit, } // 长效套餐 case *CreateLongResourceData: var duration = time.Duration(data.Expire) * 24 * time.Hour resource.Type = int32(resource2.TypeLong) resource.Long = &m.ResourceLong{ Type: data.Mode, Live: data.Live, Quota: &data.Quota, Expire: u.P(orm.LocalDateTime(now.Add(duration))), DailyLimit: data.DailyLimit, } default: return nil, fmt.Errorf("不支持的套餐类型") } err := q.Resource.Create(&resource) if err != nil { return nil, err } return &resource, nil } type CreateTypeResourceDataInter interface { GetName() string GetPrice() decimal.Decimal } type CreateShortResourceData struct { Live int32 `json:"live" validate:"required,min=180"` Mode int32 `json:"mode" validate:"required"` Expire int32 `json:"expire"` DailyLimit int32 `json:"daily_limit" validate:"min=2000"` Quota int32 `json:"quota" validate:"min=10000"` name string price *decimal.Decimal } func (data *CreateShortResourceData) GetName() string { if data.name == "" { var mode string switch data.Mode { case 1: mode = "包时" case 2: mode = "包量" } data.name = fmt.Sprintf("短效动态%s %v 分钟", mode, data.Live/60) } return data.name } func (data *CreateShortResourceData) GetPrice() decimal.Decimal { if data.price == nil { var factor int32 switch data.Mode { case 1: factor = data.DailyLimit * data.Expire case 2: 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)) data.price = &dec } return *data.price } type CreateLongResourceData struct { Live int32 `json:"live" validate:"required,oneof=1 4 8 12 24"` Mode int32 `json:"mode" validate:"required,oneof=1 2"` Expire int32 `json:"expire"` DailyLimit int32 `json:"daily_limit" validate:"min=100"` Quota int32 `json:"quota" validate:"min=500"` name string price *decimal.Decimal } func (data *CreateLongResourceData) GetName() string { if data.name == "" { var mode string switch data.Mode { case 1: mode = "包时" case 2: mode = "包量" } data.name = fmt.Sprintf("长效动态%s %d 小时", mode, data.Live) } return data.name } func (data *CreateLongResourceData) GetPrice() decimal.Decimal { if data.price == nil { var factor int32 = 0 switch resource2.Mode(data.Mode) { case resource2.ModeTime: factor = data.Expire * data.DailyLimit case resource2.ModeCount: factor = data.Quota } var base int32 switch data.Live { 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)) data.price = &dec } return *data.price } type CreateResourceData struct { Type resource2.Type `json:"type" validate:"required"` Short *CreateShortResourceData `json:"short,omitempty"` Long *CreateLongResourceData `json:"long,omitempty"` } func (s *CreateResourceData) ToData() (CreateTypeResourceDataInter, error) { switch s.Type { case resource2.TypeShort: return s.Short, nil case resource2.TypeLong: return s.Long, nil } return nil, fmt.Errorf("不支持的套餐类型") } func (s *CreateResourceData) ByData(data CreateTypeResourceDataInter) error { switch data := data.(type) { case *CreateShortResourceData: s.Type = resource2.TypeShort s.Short = data case *CreateLongResourceData: s.Type = resource2.TypeLong s.Long = data default: return fmt.Errorf("不支持的套餐类型") } return nil } type PrepareResourceData struct { CreateResourceData PaymentMethod trade2.Method `json:"payment_method" validate:"required"` PaymentPlatform trade2.Platform `json:"payment_platform" validate:"required"` } type PrepareResourceCache struct { Uid int32 `json:"uid"` TradeId int32 `json:"trade_id"` BillId int32 `json:"bill_id"` *PrepareResourceData } func (c PrepareResourceCache) MarshalBinary() (data []byte, err error) { data, err = json.Marshal(c) if err != nil { return nil, err } return data, nil } func (c PrepareResourceCache) UnmarshalBinary(data []byte) error { if err := json.Unmarshal(data, &c); err != nil { return err } return nil } type ResourceServiceErr string func (e ResourceServiceErr) Error() string { return string(e) } const ( ErrBalanceNotEnough = ResourceServiceErr("余额不足") )