feat: 移除“租户管理员为用户充值 / 每租户一套余额”能力:余额统一为全局用户余额
This commit is contained in:
@@ -18,23 +18,25 @@ import (
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// LedgerApplyResult 表示一次账本写入(含幂等命中)的结果,包含账本记录与租户用户余额快照。
|
||||
// LedgerApplyResult 表示一次账本写入(含幂等命中)的结果,包含账本记录与用户余额快照。
|
||||
type LedgerApplyResult struct {
|
||||
// Ledger 为本次创建的账本记录(若幂等命中则返回已有记录)。
|
||||
Ledger *models.TenantLedger
|
||||
// TenantUser 为写入后余额状态(若幂等命中则返回当前快照)。
|
||||
TenantUser *models.TenantUser
|
||||
// User 为写入后余额状态(若幂等命中则返回当前快照)。
|
||||
User *models.User
|
||||
}
|
||||
|
||||
// ledger 提供租户余额账本能力(冻结/解冻/扣减/退款/充值),支持幂等与行锁保证一致性。
|
||||
// ledger 提供租户账本能力(冻结/解冻/扣减/退款等),支持幂等与行锁保证一致性。
|
||||
// 注意:余额为 users 表的全局余额,用户可在已加入租户间共享消费。
|
||||
//
|
||||
// @provider
|
||||
type ledger struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// MyBalance 查询当前用户在指定租户下的余额信息(可用/冻结)。
|
||||
func (s *ledger) MyBalance(ctx context.Context, tenantID, userID int64) (*models.TenantUser, error) {
|
||||
// MyBalance 查询当前用户的全局余额信息(可用/冻结)。
|
||||
// 语义:必须先是该租户成员(否则返回 not found),但余额数据来源为 users。
|
||||
func (s *ledger) MyBalance(ctx context.Context, tenantID, userID int64) (*models.User, error) {
|
||||
if tenantID <= 0 || userID <= 0 {
|
||||
return nil, errorx.ErrInvalidParameter.WithMsg("tenant_id/user_id must be > 0")
|
||||
}
|
||||
@@ -44,14 +46,23 @@ func (s *ledger) MyBalance(ctx context.Context, tenantID, userID int64) (*models
|
||||
"user_id": userID,
|
||||
}).Info("services.ledger.me.balance")
|
||||
|
||||
tbl, query := models.TenantUserQuery.QueryContext(ctx)
|
||||
m, err := query.Where(tbl.TenantID.Eq(tenantID), tbl.UserID.Eq(userID)).First()
|
||||
if err != nil {
|
||||
// 必须先是租户成员。
|
||||
tblTU, queryTU := models.TenantUserQuery.QueryContext(ctx)
|
||||
if _, err := queryTU.Where(tblTU.TenantID.Eq(tenantID), tblTU.UserID.Eq(userID)).First(); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errorx.ErrRecordNotFound.WithMsg("tenant user not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tblU, queryU := models.UserQuery.QueryContext(ctx)
|
||||
m, err := queryU.Where(tblU.ID.Eq(userID), tblU.DeletedAt.IsNull()).First()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errorx.ErrRecordNotFound.WithMsg("user not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
@@ -232,11 +243,6 @@ func (s *ledger) CreditRefundTx(ctx context.Context, tx *gorm.DB, tenantID, oper
|
||||
return s.apply(ctx, tx, tenantID, operatorUserID, userID, orderID, "order", orderID, consts.TenantLedgerTypeCreditRefund, amount, amount, 0, idempotencyKey, remark, now)
|
||||
}
|
||||
|
||||
// CreditTopupTx 将充值金额记入可用余额,并写入账本记录。
|
||||
func (s *ledger) CreditTopupTx(ctx context.Context, tx *gorm.DB, tenantID, operatorUserID, userID, orderID, amount int64, idempotencyKey, remark string, now time.Time) (*LedgerApplyResult, error) {
|
||||
return s.apply(ctx, tx, tenantID, operatorUserID, userID, orderID, "order", orderID, consts.TenantLedgerTypeCreditTopup, amount, amount, 0, idempotencyKey, remark, now)
|
||||
}
|
||||
|
||||
func (s *ledger) apply(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
@@ -273,18 +279,32 @@ func (s *ledger) apply(
|
||||
var out LedgerApplyResult
|
||||
|
||||
err := tx.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// 必须先是租户成员(账本维度仍按 tenant_id 记录)。
|
||||
var tu models.TenantUser
|
||||
if err := tx.
|
||||
Where("tenant_id = ? AND user_id = ?", tenantID, userID).
|
||||
First(&tu).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errorx.ErrRecordNotFound.WithMsg("tenant user not found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 幂等快速路径:在进入行锁之前先查一次,减少锁竞争(命中则直接返回)。
|
||||
if idempotencyKey != "" {
|
||||
var existing models.TenantLedger
|
||||
if err := tx.
|
||||
Where("tenant_id = ? AND user_id = ? AND idempotency_key = ?", tenantID, userID, idempotencyKey).
|
||||
First(&existing).Error; err == nil {
|
||||
var current models.TenantUser
|
||||
if err := tx.Where("tenant_id = ? AND user_id = ?", tenantID, userID).First(¤t).Error; err != nil {
|
||||
var current models.User
|
||||
if err := tx.Where("id = ? AND deleted_at IS NULL", userID).First(¤t).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errorx.ErrRecordNotFound.WithMsg("user not found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
out.Ledger = &existing
|
||||
out.TenantUser = ¤t
|
||||
out.User = ¤t
|
||||
return nil
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
@@ -297,26 +317,29 @@ func (s *ledger) apply(
|
||||
if err := tx.
|
||||
Where("tenant_id = ? AND biz_ref_type = ? AND biz_ref_id = ? AND type = ?", tenantID, bizRefType, bizRefID, ledgerType).
|
||||
First(&existing).Error; err == nil {
|
||||
var current models.TenantUser
|
||||
if err := tx.Where("tenant_id = ? AND user_id = ?", tenantID, userID).First(¤t).Error; err != nil {
|
||||
var current models.User
|
||||
if err := tx.Where("id = ? AND deleted_at IS NULL", userID).First(¤t).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errorx.ErrRecordNotFound.WithMsg("user not found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
out.Ledger = &existing
|
||||
out.TenantUser = ¤t
|
||||
out.User = ¤t
|
||||
return nil
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 使用行锁锁住 tenant_users,确保同一租户下同一用户余额更新的串行一致性。
|
||||
var tu models.TenantUser
|
||||
// 使用行锁锁住 users,确保同一用户在“跨租户消费”场景下余额更新的串行一致性。
|
||||
var u models.User
|
||||
if err := tx.
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("tenant_id = ? AND user_id = ?", tenantID, userID).
|
||||
First(&tu).Error; err != nil {
|
||||
Where("id = ? AND deleted_at IS NULL", userID).
|
||||
First(&u).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errorx.ErrRecordNotFound.WithMsg("tenant user not found")
|
||||
return errorx.ErrRecordNotFound.WithMsg("user not found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -328,7 +351,7 @@ func (s *ledger) apply(
|
||||
Where("tenant_id = ? AND user_id = ? AND idempotency_key = ?", tenantID, userID, idempotencyKey).
|
||||
First(&existing).Error; err == nil {
|
||||
out.Ledger = &existing
|
||||
out.TenantUser = &tu
|
||||
out.User = &u
|
||||
return nil
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
@@ -342,15 +365,15 @@ func (s *ledger) apply(
|
||||
Where("tenant_id = ? AND biz_ref_type = ? AND biz_ref_id = ? AND type = ?", tenantID, bizRefType, bizRefID, ledgerType).
|
||||
First(&existing).Error; err == nil {
|
||||
out.Ledger = &existing
|
||||
out.TenantUser = &tu
|
||||
out.User = &u
|
||||
return nil
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
balanceBefore := tu.Balance
|
||||
frozenBefore := tu.BalanceFrozen
|
||||
balanceBefore := u.Balance
|
||||
frozenBefore := u.BalanceFrozen
|
||||
balanceAfter := balanceBefore + deltaBalance
|
||||
frozenAfter := frozenBefore + deltaFrozen
|
||||
|
||||
@@ -363,8 +386,8 @@ func (s *ledger) apply(
|
||||
}
|
||||
|
||||
// 先更新余额,再写账本:任何一步失败都回滚,保证“余额变更”和“账本记录”一致。
|
||||
if err := tx.Model(&models.TenantUser{}).
|
||||
Where("id = ?", tu.ID).
|
||||
if err := tx.Model(&models.User{}).
|
||||
Where("id = ?", u.ID).
|
||||
Updates(map[string]any{
|
||||
"balance": balanceAfter,
|
||||
"balance_frozen": frozenAfter,
|
||||
@@ -400,7 +423,7 @@ func (s *ledger) apply(
|
||||
Where("tenant_id = ? AND user_id = ? AND idempotency_key = ?", tenantID, userID, idempotencyKey).
|
||||
First(&existing).Error; e2 == nil {
|
||||
out.Ledger = &existing
|
||||
out.TenantUser = &tu
|
||||
out.User = &u
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -411,19 +434,19 @@ func (s *ledger) apply(
|
||||
Where("tenant_id = ? AND biz_ref_type = ? AND biz_ref_id = ? AND type = ?", tenantID, bizRefType, bizRefID, ledgerType).
|
||||
First(&existing).Error; e2 == nil {
|
||||
out.Ledger = &existing
|
||||
out.TenantUser = &tu
|
||||
out.User = &u
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
tu.Balance = balanceAfter
|
||||
tu.BalanceFrozen = frozenAfter
|
||||
tu.UpdatedAt = now
|
||||
u.Balance = balanceAfter
|
||||
u.BalanceFrozen = frozenAfter
|
||||
u.UpdatedAt = now
|
||||
|
||||
out.Ledger = ledger
|
||||
out.TenantUser = &tu
|
||||
out.User = &u
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -450,8 +473,8 @@ func (s *ledger) apply(
|
||||
"type": ledgerType,
|
||||
"ledger_id": out.Ledger.ID,
|
||||
"idempotency_key": idempotencyKey,
|
||||
"balance_after": out.TenantUser.Balance,
|
||||
"frozen_after": out.TenantUser.BalanceFrozen,
|
||||
"balance_after": out.User.Balance,
|
||||
"frozen_after": out.User.BalanceFrozen,
|
||||
}).Info("services.ledger.apply.ok")
|
||||
|
||||
return &out, nil
|
||||
|
||||
Reference in New Issue
Block a user