package recharge import ( "context" "errors" "strconv" "strings" "time" "gorm.io/gorm" "gorm.io/gorm/clause" biz "kra/internal/biz/recharge" "kra/internal/data/ledger" "kra/internal/utils/serial" ) type Provider interface{ DB() *gorm.DB } type repo struct{ data Provider } func NewRepo(data Provider) biz.Repo { return &repo{data: data} } func (r *repo) db(ctx context.Context) *gorm.DB { return r.data.DB().WithContext(ctx) } func (r *repo) Plans(ctx context.Context) ([]*biz.Plan, error) { var rows []ledger.RechargePlanPO if err := r.db(ctx).Where("enabled = ?", true).Order("sort ASC,id ASC").Find(&rows).Error; err != nil { return nil, err } out := make([]*biz.Plan, 0, len(rows)) for _, v := range rows { out = append(out, &biz.Plan{ID: v.ID, Sort: v.Sort, Diamonds: v.Diamonds, AmountCents: v.AmountCents, Enabled: v.Enabled}) } return out, nil } func (r *repo) SavePlans(ctx context.Context, plans []*biz.Plan) error { return r.db(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&ledger.RechargePlanPO{}).Error; err != nil { return err } for _, p := range plans { if err := tx.Create(&ledger.RechargePlanPO{Sort: p.Sort, Diamonds: p.Diamonds, AmountCents: p.AmountCents, Enabled: p.Enabled}).Error; err != nil { return err } } return nil }) } type row struct { ledger.RechargeOrderPO Username string `gorm:"column:username"` Nickname string `gorm:"column:nickname"` Provider string `gorm:"column:provider"` ProviderTradeNo string `gorm:"column:provider_trade_no"` PaymentStatus string `gorm:"column:payment_status"` FulfillmentStatus string `gorm:"column:fulfillment_status"` RefundStatus string `gorm:"column:refund_status"` } func query(db *gorm.DB, f biz.Filter) *gorm.DB { q := db.Model(&ledger.RechargeOrderPO{}). Joins("LEFT JOIN sys_users ON sys_users.id = live_recharge_orders.user_id"). Joins("LEFT JOIN pay_orders ON pay_orders.business_type = ? AND pay_orders.business_id = live_recharge_orders.order_no AND pay_orders.trade_no = live_recharge_orders.order_no", biz.BusinessType) if f.OrderNo != "" { q = q.Where("live_recharge_orders.order_no = ?", f.OrderNo) } if f.UserKeyword != "" { v := "%" + f.UserKeyword + "%" q = q.Where("sys_users.username LIKE ? OR sys_users.nick_name LIKE ?", v, v) } if f.PaymentStatus != "" { q = q.Where("pay_orders.payment_status = ?", f.PaymentStatus) } if f.Provider != "" { q = q.Where("pay_orders.provider = ?", f.Provider) } if len(f.CreatedAt) == 2 { q = q.Where("live_recharge_orders.created_at BETWEEN ? AND ?", f.CreatedAt[0], f.CreatedAt[1]) } return q } const orderSelect = "live_recharge_orders.*,sys_users.username,sys_users.nick_name AS nickname,pay_orders.provider,pay_orders.provider_trade_no,pay_orders.payment_status,pay_orders.fulfillment_status,pay_orders.refund_status" func (r *repo) List(ctx context.Context, f biz.Filter, o biz.ListOptions) ([]*biz.Order, int64, *biz.Summary, error) { q := query(r.db(ctx), f) var total int64 if err := q.Count(&total).Error; err != nil { return nil, 0, nil, err } var s struct{ PaidCount, AmountCents int64 } if err := query(r.db(ctx), f).Select("COALESCE(SUM(CASE WHEN live_recharge_orders.credited_at IS NOT NULL THEN 1 ELSE 0 END),0) AS paid_count,COALESCE(SUM(CASE WHEN live_recharge_orders.credited_at IS NOT NULL THEN live_recharge_orders.amount_cents ELSE 0 END),0) AS amount_cents").Scan(&s).Error; err != nil { return nil, 0, nil, err } var rows []row if err := q.Select(orderSelect).Order(orderBy(o.OrderBy)).Offset(o.Offset).Limit(o.Limit).Scan(&rows).Error; err != nil { return nil, 0, nil, err } out := make([]*biz.Order, 0, len(rows)) for i := range rows { out = append(out, toBiz(&rows[i])) } return out, total, &biz.Summary{Count: total, PaidCount: s.PaidCount, AmountCents: s.AmountCents}, nil } func (r *repo) Get(ctx context.Context, no string) (*biz.Order, error) { var v row if err := query(r.db(ctx), biz.Filter{OrderNo: no}).Select(orderSelect).Scan(&v).Error; err != nil { return nil, err } if v.ID == 0 { return nil, biz.ErrNotFound } return toBiz(&v), nil } func (r *repo) Create(ctx context.Context, userID, planID uint) (*biz.Order, error) { var plan ledger.RechargePlanPO if err := r.db(ctx).Where("id = ? AND enabled = ?", planID, true).First(&plan).Error; err != nil { return nil, biz.ErrInvalid } rate := int64(10) var set ledger.SettingPO if err := r.db(ctx).Where(&ledger.SettingPO{Key: "diamond_rate_cents"}).First(&set).Error; err == nil { if v, e := strconv.ParseInt(set.Value, 10, 64); e == nil { rate = v } } orderNo, err := serial.New("CZ") if err != nil { return nil, err } v := ledger.RechargeOrderPO{OrderNo: orderNo, UserID: userID, Diamonds: plan.Diamonds, AmountCents: plan.AmountCents, RateCents: rate} if err := r.db(ctx).Create(&v).Error; err != nil { return nil, err } return r.Get(ctx, v.OrderNo) } func (r *repo) MarkPaid(ctx context.Context, no string) (*biz.Order, error) { var credited ledger.RechargeOrderPO err := r.db(ctx).Transaction(func(tx *gorm.DB) error { var v ledger.RechargeOrderPO if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("order_no = ?", no).First(&v).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return biz.ErrNotFound } return err } if v.CreditedAt != nil { credited = v return nil } now := time.Now() v.CreditedAt = &now if err := tx.Save(&v).Error; err != nil { return err } wallet := ledger.WalletPO{UserID: v.UserID} if err := tx.Where("user_id = ?", v.UserID).FirstOrCreate(&wallet).Error; err != nil { return err } if err := tx.Model(&wallet).Updates(map[string]any{"diamond_balance": gorm.Expr("diamond_balance + ?", v.Diamonds), "total_recharge_cents": gorm.Expr("total_recharge_cents + ?", v.AmountCents)}).Error; err != nil { return err } credited = v return nil }) if err != nil { return nil, err } return toBiz(&row{RechargeOrderPO: credited}), nil } func toBiz(v *row) *biz.Order { return &biz.Order{ ID: v.ID, OrderNo: v.OrderNo, UserID: v.UserID, Username: v.Username, Nickname: v.Nickname, Diamonds: v.Diamonds, AmountCents: v.AmountCents, Provider: v.Provider, ProviderTradeNo: v.ProviderTradeNo, PaymentStatus: v.PaymentStatus, FulfillmentStatus: v.FulfillmentStatus, RefundStatus: v.RefundStatus, CreatedAt: v.CreatedAt, CreditedAt: v.CreditedAt, } } func orderBy(v string) string { m := map[string]string{"diamonds_asc": "live_recharge_orders.diamonds ASC", "diamonds_desc": "live_recharge_orders.diamonds DESC", "amount_asc": "live_recharge_orders.amount_cents ASC", "amount_desc": "live_recharge_orders.amount_cents DESC", "created_at_asc": "live_recharge_orders.created_at ASC", "created_at_desc": "live_recharge_orders.created_at DESC", "id_asc": "live_recharge_orders.id ASC", "id_desc": "live_recharge_orders.id DESC"} if x := m[strings.ToLower(v)]; x != "" { return x } return "live_recharge_orders.created_at DESC" }