Files
kra-new/internal/data/recharge/recharge.go
T
2026-09-23 17:42:55 +08:00

192 lines
7.0 KiB
Go

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"
}