feat: add operator and business reference fields to tenant ledgers
- Added `operator_user_id`, `biz_ref_type`, and `biz_ref_id` fields to the TenantLedger model for enhanced auditing and traceability. - Updated the tenant ledgers query generation to include new fields. - Introduced new API endpoint for retrieving tenant ledger records with filtering options based on the new fields. - Enhanced Swagger documentation to reflect the new endpoint and its parameters. - Created DTOs for admin ledger filtering and item representation. - Implemented the admin ledger retrieval logic in the tenant service. - Added database migration scripts to introduce new fields and indexes for efficient querying.
This commit is contained in:
@@ -111,45 +111,137 @@ func (s *ledger) MyLedgerPage(ctx context.Context, tenantID, userID int64, filte
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AdminLedgerPage 分页查询租户内余额流水(租户后台审计用)。
|
||||
func (s *ledger) AdminLedgerPage(ctx context.Context, tenantID int64, filter *dto.AdminLedgerListFilter) (*requests.Pager, error) {
|
||||
if tenantID <= 0 {
|
||||
return nil, errorx.ErrInvalidParameter.WithMsg("tenant_id must be > 0")
|
||||
}
|
||||
if filter == nil {
|
||||
filter = &dto.AdminLedgerListFilter{}
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"tenant_id": tenantID,
|
||||
"operator_user_id": lo.FromPtr(filter.OperatorUserID),
|
||||
"user_id": lo.FromPtr(filter.UserID),
|
||||
"type": lo.FromPtr(filter.Type),
|
||||
"order_id": lo.FromPtr(filter.OrderID),
|
||||
"biz_ref_type": lo.FromPtr(filter.BizRefType),
|
||||
"biz_ref_id": lo.FromPtr(filter.BizRefID),
|
||||
"created_at_from": filter.CreatedAtFrom,
|
||||
"created_at_to": filter.CreatedAtTo,
|
||||
"pagination_page": filter.Page,
|
||||
"pagination_limit": filter.Limit,
|
||||
"pagination_offset": filter.Offset(),
|
||||
}).Info("services.ledger.admin.page")
|
||||
|
||||
filter.Pagination.Format()
|
||||
|
||||
tbl, query := models.TenantLedgerQuery.QueryContext(ctx)
|
||||
conds := []gen.Condition{tbl.TenantID.Eq(tenantID)}
|
||||
|
||||
if filter.OperatorUserID != nil && *filter.OperatorUserID > 0 {
|
||||
conds = append(conds, tbl.OperatorUserID.Eq(*filter.OperatorUserID))
|
||||
}
|
||||
if filter.UserID != nil && *filter.UserID > 0 {
|
||||
conds = append(conds, tbl.UserID.Eq(*filter.UserID))
|
||||
}
|
||||
if filter.Type != nil {
|
||||
conds = append(conds, tbl.Type.Eq(*filter.Type))
|
||||
}
|
||||
if filter.OrderID != nil && *filter.OrderID > 0 {
|
||||
conds = append(conds, tbl.OrderID.Eq(*filter.OrderID))
|
||||
}
|
||||
if filter.BizRefType != nil && *filter.BizRefType != "" {
|
||||
conds = append(conds, tbl.BizRefType.Eq(*filter.BizRefType))
|
||||
}
|
||||
if filter.BizRefID != nil && *filter.BizRefID > 0 {
|
||||
conds = append(conds, tbl.BizRefID.Eq(*filter.BizRefID))
|
||||
}
|
||||
if filter.CreatedAtFrom != nil {
|
||||
conds = append(conds, tbl.CreatedAt.Gte(*filter.CreatedAtFrom))
|
||||
}
|
||||
if filter.CreatedAtTo != nil {
|
||||
conds = append(conds, tbl.CreatedAt.Lte(*filter.CreatedAtTo))
|
||||
}
|
||||
|
||||
ledgers, total, err := query.Where(conds...).Order(tbl.ID.Desc()).FindByPage(int(filter.Offset()), int(filter.Limit))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := lo.Map(ledgers, func(m *models.TenantLedger, _ int) *dto.AdminLedgerItem {
|
||||
return &dto.AdminLedgerItem{
|
||||
Ledger: m,
|
||||
TypeDescription: m.Type.Description(),
|
||||
}
|
||||
})
|
||||
|
||||
return &requests.Pager{
|
||||
Pagination: filter.Pagination,
|
||||
Total: total,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Freeze 将可用余额转入冻结余额,并写入账本记录。
|
||||
func (s *ledger) Freeze(ctx context.Context, tenantID, userID, orderID, amount int64, idempotencyKey, remark string, now time.Time) (*LedgerApplyResult, error) {
|
||||
return s.apply(ctx, s.db, tenantID, userID, orderID, consts.TenantLedgerTypeFreeze, amount, -amount, amount, idempotencyKey, remark, now)
|
||||
// 冻结通常由用户自己发起(下单冻结);操作者默认等于余额账户归属 user_id。
|
||||
bizRefType := ""
|
||||
if orderID > 0 {
|
||||
bizRefType = "order"
|
||||
}
|
||||
return s.apply(ctx, s.db, tenantID, userID, userID, orderID, bizRefType, orderID, consts.TenantLedgerTypeFreeze, amount, -amount, amount, idempotencyKey, remark, now)
|
||||
}
|
||||
|
||||
// Unfreeze 将冻结余额转回可用余额,并写入账本记录。
|
||||
func (s *ledger) Unfreeze(ctx context.Context, tenantID, userID, orderID, amount int64, idempotencyKey, remark string, now time.Time) (*LedgerApplyResult, error) {
|
||||
return s.apply(ctx, s.db, tenantID, userID, orderID, consts.TenantLedgerTypeUnfreeze, amount, amount, -amount, idempotencyKey, remark, now)
|
||||
// 解冻通常由用户自己发起(失败回滚/退款回滚等可能由系统或管理员触发;此处默认等于 user_id)。
|
||||
bizRefType := ""
|
||||
if orderID > 0 {
|
||||
bizRefType = "order"
|
||||
}
|
||||
return s.apply(ctx, s.db, tenantID, userID, userID, orderID, bizRefType, orderID, consts.TenantLedgerTypeUnfreeze, amount, amount, -amount, idempotencyKey, remark, now)
|
||||
}
|
||||
|
||||
// FreezeTx 为 Freeze 的事务版本(由外层事务控制提交/回滚)。
|
||||
func (s *ledger) FreezeTx(ctx context.Context, tx *gorm.DB, tenantID, userID, orderID, amount int64, idempotencyKey, remark string, now time.Time) (*LedgerApplyResult, error) {
|
||||
return s.apply(ctx, tx, tenantID, userID, orderID, consts.TenantLedgerTypeFreeze, amount, -amount, amount, idempotencyKey, remark, now)
|
||||
func (s *ledger) FreezeTx(ctx context.Context, tx *gorm.DB, tenantID, operatorUserID, userID, orderID, amount int64, idempotencyKey, remark string, now time.Time) (*LedgerApplyResult, error) {
|
||||
bizRefType := ""
|
||||
if orderID > 0 {
|
||||
bizRefType = "order"
|
||||
}
|
||||
return s.apply(ctx, tx, tenantID, operatorUserID, userID, orderID, bizRefType, orderID, consts.TenantLedgerTypeFreeze, amount, -amount, amount, idempotencyKey, remark, now)
|
||||
}
|
||||
|
||||
// UnfreezeTx 为 Unfreeze 的事务版本(由外层事务控制提交/回滚)。
|
||||
func (s *ledger) UnfreezeTx(ctx context.Context, tx *gorm.DB, tenantID, userID, orderID, amount int64, idempotencyKey, remark string, now time.Time) (*LedgerApplyResult, error) {
|
||||
return s.apply(ctx, tx, tenantID, userID, orderID, consts.TenantLedgerTypeUnfreeze, amount, amount, -amount, idempotencyKey, remark, now)
|
||||
func (s *ledger) UnfreezeTx(ctx context.Context, tx *gorm.DB, tenantID, operatorUserID, userID, orderID, amount int64, idempotencyKey, remark string, now time.Time) (*LedgerApplyResult, error) {
|
||||
bizRefType := ""
|
||||
if orderID > 0 {
|
||||
bizRefType = "order"
|
||||
}
|
||||
return s.apply(ctx, tx, tenantID, operatorUserID, userID, orderID, bizRefType, orderID, consts.TenantLedgerTypeUnfreeze, amount, amount, -amount, idempotencyKey, remark, now)
|
||||
}
|
||||
|
||||
// DebitPurchaseTx 将冻结资金转为实际扣款(减少冻结余额),并写入账本记录。
|
||||
func (s *ledger) DebitPurchaseTx(ctx context.Context, tx *gorm.DB, tenantID, userID, orderID, amount int64, idempotencyKey, remark string, now time.Time) (*LedgerApplyResult, error) {
|
||||
return s.apply(ctx, tx, tenantID, userID, orderID, consts.TenantLedgerTypeDebitPurchase, amount, 0, -amount, idempotencyKey, remark, now)
|
||||
func (s *ledger) DebitPurchaseTx(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.TenantLedgerTypeDebitPurchase, amount, 0, -amount, idempotencyKey, remark, now)
|
||||
}
|
||||
|
||||
// CreditRefundTx 将退款金额退回到可用余额,并写入账本记录。
|
||||
func (s *ledger) CreditRefundTx(ctx context.Context, tx *gorm.DB, tenantID, userID, orderID, amount int64, idempotencyKey, remark string, now time.Time) (*LedgerApplyResult, error) {
|
||||
return s.apply(ctx, tx, tenantID, userID, orderID, consts.TenantLedgerTypeCreditRefund, amount, amount, 0, idempotencyKey, remark, now)
|
||||
func (s *ledger) CreditRefundTx(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.TenantLedgerTypeCreditRefund, amount, amount, 0, idempotencyKey, remark, now)
|
||||
}
|
||||
|
||||
// CreditTopupTx 将充值金额记入可用余额,并写入账本记录。
|
||||
func (s *ledger) CreditTopupTx(ctx context.Context, tx *gorm.DB, tenantID, userID, orderID, amount int64, idempotencyKey, remark string, now time.Time) (*LedgerApplyResult, error) {
|
||||
return s.apply(ctx, tx, tenantID, userID, orderID, consts.TenantLedgerTypeCreditTopup, amount, amount, 0, idempotencyKey, remark, now)
|
||||
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,
|
||||
tenantID, userID, orderID int64,
|
||||
tenantID, operatorUserID, userID, orderID int64,
|
||||
bizRefType string, bizRefID int64,
|
||||
ledgerType consts.TenantLedgerType,
|
||||
amount, deltaBalance, deltaFrozen int64,
|
||||
idempotencyKey, remark string,
|
||||
@@ -165,8 +257,11 @@ func (s *ledger) apply(
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"tenant_id": tenantID,
|
||||
"operator_user_id": operatorUserID,
|
||||
"user_id": userID,
|
||||
"order_id": orderID,
|
||||
"biz_ref_type": bizRefType,
|
||||
"biz_ref_id": bizRefID,
|
||||
"type": ledgerType,
|
||||
"amount": amount,
|
||||
"idempotency_key": idempotencyKey,
|
||||
@@ -196,6 +291,24 @@ func (s *ledger) apply(
|
||||
}
|
||||
}
|
||||
|
||||
// 结构化幂等快速路径:当调用方未传 idempotency_key,但提供了 biz_ref 时,按 (tenant,biz_ref,type) 去重。
|
||||
if idempotencyKey == "" && bizRefType != "" && bizRefID > 0 {
|
||||
var existing models.TenantLedger
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
out.Ledger = &existing
|
||||
out.TenantUser = ¤t
|
||||
return nil
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 使用行锁锁住 tenant_users,确保同一租户下同一用户余额更新的串行一致性。
|
||||
var tu models.TenantUser
|
||||
if err := tx.
|
||||
@@ -222,6 +335,20 @@ func (s *ledger) apply(
|
||||
}
|
||||
}
|
||||
|
||||
// 二次结构化幂等校验:与上面的幂等逻辑一致,避免并发下重复写入。
|
||||
if idempotencyKey == "" && bizRefType != "" && bizRefID > 0 {
|
||||
var existing models.TenantLedger
|
||||
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 {
|
||||
out.Ledger = &existing
|
||||
out.TenantUser = &tu
|
||||
return nil
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
balanceBefore := tu.Balance
|
||||
frozenBefore := tu.BalanceFrozen
|
||||
balanceAfter := balanceBefore + deltaBalance
|
||||
@@ -249,8 +376,11 @@ func (s *ledger) apply(
|
||||
// 写入账本:记录变更前后快照,便于对账与审计;幂等键用于去重。
|
||||
ledger := &models.TenantLedger{
|
||||
TenantID: tenantID,
|
||||
OperatorUserID: operatorUserID,
|
||||
UserID: userID,
|
||||
OrderID: orderID,
|
||||
BizRefType: bizRefType,
|
||||
BizRefID: bizRefID,
|
||||
Type: ledgerType,
|
||||
Amount: amount,
|
||||
BalanceBefore: balanceBefore,
|
||||
@@ -274,6 +404,17 @@ func (s *ledger) apply(
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// 结构化幂等回读:当未传 idempotency_key 时按 biz_ref 回读。
|
||||
if idempotencyKey == "" && bizRefType != "" && bizRefID > 0 {
|
||||
var existing models.TenantLedger
|
||||
if e2 := tx.
|
||||
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
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -287,24 +428,30 @@ func (s *ledger) apply(
|
||||
})
|
||||
if err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"tenant_id": tenantID,
|
||||
"user_id": userID,
|
||||
"order_id": orderID,
|
||||
"type": ledgerType,
|
||||
"idempotency_key": idempotencyKey,
|
||||
"tenant_id": tenantID,
|
||||
"operator_user_id": operatorUserID,
|
||||
"user_id": userID,
|
||||
"order_id": orderID,
|
||||
"biz_ref_type": bizRefType,
|
||||
"biz_ref_id": bizRefID,
|
||||
"type": ledgerType,
|
||||
"idempotency_key": idempotencyKey,
|
||||
}).WithError(err).Warn("services.ledger.apply.failed")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"tenant_id": tenantID,
|
||||
"user_id": userID,
|
||||
"order_id": orderID,
|
||||
"type": ledgerType,
|
||||
"ledger_id": out.Ledger.ID,
|
||||
"idempotency_key": idempotencyKey,
|
||||
"balance_after": out.TenantUser.Balance,
|
||||
"frozen_after": out.TenantUser.BalanceFrozen,
|
||||
"tenant_id": tenantID,
|
||||
"operator_user_id": operatorUserID,
|
||||
"user_id": userID,
|
||||
"order_id": orderID,
|
||||
"biz_ref_type": bizRefType,
|
||||
"biz_ref_id": bizRefID,
|
||||
"type": ledgerType,
|
||||
"ledger_id": out.Ledger.ID,
|
||||
"idempotency_key": idempotencyKey,
|
||||
"balance_after": out.TenantUser.Balance,
|
||||
"frozen_after": out.TenantUser.BalanceFrozen,
|
||||
}).Info("services.ledger.apply.ok")
|
||||
|
||||
return &out, nil
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"quyun/v2/database/models"
|
||||
"quyun/v2/pkg/consts"
|
||||
|
||||
"github.com/samber/lo"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
@@ -88,6 +89,9 @@ func (s *LedgerTestSuite) Test_Freeze() {
|
||||
So(res.Ledger.BalanceAfter, ShouldEqual, 700)
|
||||
So(res.Ledger.FrozenBefore, ShouldEqual, 0)
|
||||
So(res.Ledger.FrozenAfter, ShouldEqual, 300)
|
||||
So(res.Ledger.OperatorUserID, ShouldEqual, userID)
|
||||
So(res.Ledger.BizRefType, ShouldEqual, "")
|
||||
So(res.Ledger.BizRefID, ShouldEqual, int64(0))
|
||||
So(res.TenantUser.Balance, ShouldEqual, 700)
|
||||
So(res.TenantUser.BalanceFrozen, ShouldEqual, 300)
|
||||
})
|
||||
@@ -150,6 +154,9 @@ func (s *LedgerTestSuite) Test_Unfreeze() {
|
||||
So(err, ShouldBeNil)
|
||||
So(res, ShouldNotBeNil)
|
||||
So(res.Ledger.Type, ShouldEqual, consts.TenantLedgerTypeUnfreeze)
|
||||
So(res.Ledger.OperatorUserID, ShouldEqual, userID)
|
||||
So(res.Ledger.BizRefType, ShouldEqual, "")
|
||||
So(res.Ledger.BizRefID, ShouldEqual, int64(0))
|
||||
So(res.TenantUser.Balance, ShouldEqual, 1000)
|
||||
So(res.TenantUser.BalanceFrozen, ShouldEqual, 0)
|
||||
})
|
||||
@@ -183,12 +190,12 @@ func (s *LedgerTestSuite) Test_DebitPurchaseTx() {
|
||||
s.seedTenantUser(ctx, tenantID, userID, 1000, 0)
|
||||
|
||||
Convey("金额非法应返回参数错误", func() {
|
||||
_, err := Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, 123, 0, "k_debit_invalid_amount", "debit", now)
|
||||
_, err := Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, userID, 123, 0, "k_debit_invalid_amount", "debit", now)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("冻结余额不足应返回前置条件失败", func() {
|
||||
_, err := Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, 123, 300, "k_debit_no_frozen", "debit", now)
|
||||
_, err := Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, userID, 123, 300, "k_debit_no_frozen", "debit", now)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
@@ -196,10 +203,13 @@ func (s *LedgerTestSuite) Test_DebitPurchaseTx() {
|
||||
_, err := Ledger.Freeze(ctx, tenantID, userID, 0, 300, "k_freeze_for_debit", "freeze", now)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
res, err := Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, 123, 300, "k_debit_1", "debit", now)
|
||||
res, err := Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, userID, 123, 300, "k_debit_1", "debit", now)
|
||||
So(err, ShouldBeNil)
|
||||
So(res, ShouldNotBeNil)
|
||||
So(res.Ledger.Type, ShouldEqual, consts.TenantLedgerTypeDebitPurchase)
|
||||
So(res.Ledger.OperatorUserID, ShouldEqual, userID)
|
||||
So(res.Ledger.BizRefType, ShouldEqual, "order")
|
||||
So(res.Ledger.BizRefID, ShouldEqual, int64(123))
|
||||
So(res.TenantUser.Balance, ShouldEqual, 700)
|
||||
So(res.TenantUser.BalanceFrozen, ShouldEqual, 0)
|
||||
})
|
||||
@@ -208,10 +218,10 @@ func (s *LedgerTestSuite) Test_DebitPurchaseTx() {
|
||||
_, err := Ledger.Freeze(ctx, tenantID, userID, 0, 300, "k_freeze_for_debit_idem", "freeze", now)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, err = Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, 123, 300, "k_debit_idem", "debit", now)
|
||||
_, err = Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, userID, 123, 300, "k_debit_idem", "debit", now)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, err = Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, 123, 300, "k_debit_idem", "debit", now.Add(time.Second))
|
||||
_, err = Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, userID, 123, 300, "k_debit_idem", "debit", now.Add(time.Second))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
var tu2 models.TenantUser
|
||||
@@ -232,20 +242,23 @@ func (s *LedgerTestSuite) Test_CreditRefundTx() {
|
||||
s.seedTenantUser(ctx, tenantID, userID, 1000, 0)
|
||||
|
||||
Convey("金额非法应返回参数错误", func() {
|
||||
_, err := Ledger.CreditRefundTx(ctx, _db, tenantID, userID, 123, 0, "k_refund_invalid_amount", "refund", now)
|
||||
_, err := Ledger.CreditRefundTx(ctx, _db, tenantID, userID, userID, 123, 0, "k_refund_invalid_amount", "refund", now)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("成功退款应增加可用余额", func() {
|
||||
_, err := Ledger.Freeze(ctx, tenantID, userID, 0, 300, "k_freeze_for_refund", "freeze", now)
|
||||
So(err, ShouldBeNil)
|
||||
_, err = Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, 123, 300, "k_debit_for_refund", "debit", now)
|
||||
_, err = Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, userID, 123, 300, "k_debit_for_refund", "debit", now)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
res, err := Ledger.CreditRefundTx(ctx, _db, tenantID, userID, 123, 300, "k_refund_1", "refund", now)
|
||||
res, err := Ledger.CreditRefundTx(ctx, _db, tenantID, userID, userID, 123, 300, "k_refund_1", "refund", now)
|
||||
So(err, ShouldBeNil)
|
||||
So(res, ShouldNotBeNil)
|
||||
So(res.Ledger.Type, ShouldEqual, consts.TenantLedgerTypeCreditRefund)
|
||||
So(res.Ledger.OperatorUserID, ShouldEqual, userID)
|
||||
So(res.Ledger.BizRefType, ShouldEqual, "order")
|
||||
So(res.Ledger.BizRefID, ShouldEqual, int64(123))
|
||||
So(res.TenantUser.Balance, ShouldEqual, 1000)
|
||||
So(res.TenantUser.BalanceFrozen, ShouldEqual, 0)
|
||||
})
|
||||
@@ -253,12 +266,12 @@ func (s *LedgerTestSuite) Test_CreditRefundTx() {
|
||||
Convey("幂等键重复调用不应重复退款入账", func() {
|
||||
_, err := Ledger.Freeze(ctx, tenantID, userID, 0, 300, "k_freeze_for_refund_idem", "freeze", now)
|
||||
So(err, ShouldBeNil)
|
||||
_, err = Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, 123, 300, "k_debit_for_refund_idem", "debit", now)
|
||||
_, err = Ledger.DebitPurchaseTx(ctx, _db, tenantID, userID, userID, 123, 300, "k_debit_for_refund_idem", "debit", now)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, err = Ledger.CreditRefundTx(ctx, _db, tenantID, userID, 123, 300, "k_refund_idem", "refund", now)
|
||||
_, err = Ledger.CreditRefundTx(ctx, _db, tenantID, userID, userID, 123, 300, "k_refund_idem", "refund", now)
|
||||
So(err, ShouldBeNil)
|
||||
_, err = Ledger.CreditRefundTx(ctx, _db, tenantID, userID, 123, 300, "k_refund_idem", "refund", now.Add(time.Second))
|
||||
_, err = Ledger.CreditRefundTx(ctx, _db, tenantID, userID, userID, 123, 300, "k_refund_idem", "refund", now.Add(time.Second))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
var tu2 models.TenantUser
|
||||
@@ -278,23 +291,26 @@ func (s *LedgerTestSuite) Test_CreditTopupTx() {
|
||||
s.seedTenantUser(ctx, tenantID, userID, 1000, 0)
|
||||
|
||||
Convey("金额非法应返回参数错误", func() {
|
||||
_, err := Ledger.CreditTopupTx(ctx, _db, tenantID, userID, 456, 0, "k_topup_invalid_amount", "topup", now)
|
||||
_, err := Ledger.CreditTopupTx(ctx, _db, tenantID, 999, userID, 456, 0, "k_topup_invalid_amount", "topup", now)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("成功充值应增加可用余额并写入账本", func() {
|
||||
res, err := Ledger.CreditTopupTx(ctx, _db, tenantID, userID, 456, 200, "k_topup_1", "topup", now)
|
||||
res, err := Ledger.CreditTopupTx(ctx, _db, tenantID, 999, userID, 456, 200, "k_topup_1", "topup", now)
|
||||
So(err, ShouldBeNil)
|
||||
So(res, ShouldNotBeNil)
|
||||
So(res.Ledger.Type, ShouldEqual, consts.TenantLedgerTypeCreditTopup)
|
||||
So(res.Ledger.OperatorUserID, ShouldEqual, int64(999))
|
||||
So(res.Ledger.BizRefType, ShouldEqual, "order")
|
||||
So(res.Ledger.BizRefID, ShouldEqual, int64(456))
|
||||
So(res.TenantUser.Balance, ShouldEqual, 1200)
|
||||
So(res.TenantUser.BalanceFrozen, ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("幂等键重复调用不应重复充值入账", func() {
|
||||
_, err := Ledger.CreditTopupTx(ctx, _db, tenantID, userID, 456, 200, "k_topup_idem", "topup", now)
|
||||
_, err := Ledger.CreditTopupTx(ctx, _db, tenantID, 999, userID, 456, 200, "k_topup_idem", "topup", now)
|
||||
So(err, ShouldBeNil)
|
||||
_, err = Ledger.CreditTopupTx(ctx, _db, tenantID, userID, 456, 200, "k_topup_idem", "topup", now.Add(time.Second))
|
||||
_, err = Ledger.CreditTopupTx(ctx, _db, tenantID, 999, userID, 456, 200, "k_topup_idem", "topup", now.Add(time.Second))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
var tu2 models.TenantUser
|
||||
@@ -336,7 +352,7 @@ func (s *LedgerTestSuite) Test_MyLedgerPage() {
|
||||
|
||||
s.seedTenantUser(ctx, tenantID, userID, 1000, 0)
|
||||
|
||||
_, err := Ledger.CreditTopupTx(ctx, _db, tenantID, userID, 1, 200, "k_topup_for_page", "topup", now)
|
||||
_, err := Ledger.CreditTopupTx(ctx, _db, tenantID, userID, userID, 1, 200, "k_topup_for_page", "topup", now)
|
||||
So(err, ShouldBeNil)
|
||||
_, err = Ledger.Freeze(ctx, tenantID, userID, 2, 100, "k_freeze_for_page", "freeze", now.Add(time.Second))
|
||||
So(err, ShouldBeNil)
|
||||
@@ -356,3 +372,26 @@ func (s *LedgerTestSuite) Test_MyLedgerPage() {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *LedgerTestSuite) Test_AdminLedgerPage() {
|
||||
Convey("Ledger.AdminLedgerPage", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(1)
|
||||
userID := int64(2)
|
||||
now := time.Now().UTC()
|
||||
|
||||
s.seedTenantUser(ctx, tenantID, userID, 1000, 0)
|
||||
|
||||
// 模拟后台管理员为用户充值:operator_user_id 与 user_id 不同。
|
||||
_, err := Ledger.CreditTopupTx(ctx, _db, tenantID, 999, userID, 777, 200, "k_admin_topup_for_page", "topup", now)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("按 operator_user_id 过滤", func() {
|
||||
pager, err := Ledger.AdminLedgerPage(ctx, tenantID, &dto.AdminLedgerListFilter{
|
||||
OperatorUserID: lo.ToPtr(int64(999)),
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
So(pager.Total, ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -440,7 +440,7 @@ func (s *order) AdminTopupUser(
|
||||
if remark == "" {
|
||||
remark = fmt.Sprintf("topup by tenant_admin:%d", operatorUserID)
|
||||
}
|
||||
if _, err := s.ledger.CreditTopupTx(ctx, tx, tenantID, targetUserID, orderModel.ID, amount, ledgerKey, remark, now); err != nil {
|
||||
if _, err := s.ledger.CreditTopupTx(ctx, tx, tenantID, operatorUserID, targetUserID, orderModel.ID, amount, ledgerKey, remark, now); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -772,7 +772,7 @@ func (s *order) AdminRefundOrder(
|
||||
|
||||
// 先退余额(账本入账),后更新订单状态与权益,确保退款可对账且可追溯。
|
||||
if amount > 0 {
|
||||
if _, err := s.ledger.CreditRefundTx(ctx, tx, tenantID, orderModel.UserID, orderModel.ID, amount, refundKey, reason, now); err != nil {
|
||||
if _, err := s.ledger.CreditRefundTx(ctx, tx, tenantID, operatorUserID, orderModel.UserID, orderModel.ID, amount, refundKey, reason, now); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1075,7 +1075,7 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
|
||||
|
||||
// 3) 独立事务冻结余额:便于后续在订单事务失败时做补偿解冻。
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
_, err := s.ledger.FreezeTx(ctx, tx, params.TenantID, params.UserID, 0, amountPaid, freezeKey, "purchase freeze", now)
|
||||
_, err := s.ledger.FreezeTx(ctx, tx, params.TenantID, params.UserID, params.UserID, 0, amountPaid, freezeKey, "purchase freeze", now)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, pkgerrors.Wrap(err, "purchase freeze failed")
|
||||
@@ -1114,7 +1114,7 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
|
||||
if err := tx.Create(item).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := s.ledger.DebitPurchaseTx(ctx, tx, params.TenantID, params.UserID, orderModel.ID, amountPaid, debitKey, "purchase debit", now); err != nil {
|
||||
if _, err := s.ledger.DebitPurchaseTx(ctx, tx, params.TenantID, params.UserID, params.UserID, orderModel.ID, amountPaid, debitKey, "purchase debit", now); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&models.Order{}).
|
||||
@@ -1152,6 +1152,7 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
|
||||
ctx,
|
||||
tx,
|
||||
params.TenantID,
|
||||
params.UserID, // operator_user_id:购买者本人(下单链路中的补偿动作)
|
||||
params.UserID,
|
||||
0,
|
||||
amountPaid,
|
||||
@@ -1322,7 +1323,7 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if _, err := s.ledger.FreezeTx(ctx, tx, params.TenantID, params.UserID, 0, amountPaid, "", "purchase freeze", now); err != nil {
|
||||
if _, err := s.ledger.FreezeTx(ctx, tx, params.TenantID, params.UserID, params.UserID, 0, amountPaid, "", "purchase freeze", now); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1345,7 +1346,7 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := s.ledger.DebitPurchaseTx(ctx, tx, params.TenantID, params.UserID, orderModel.ID, amountPaid, "", "purchase debit", now); err != nil {
|
||||
if _, err := s.ledger.DebitPurchaseTx(ctx, tx, params.TenantID, params.UserID, params.UserID, orderModel.ID, amountPaid, "", "purchase debit", now); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -152,14 +152,14 @@ func (s *OrderTestSuite) Test_AdminTopupUser() {
|
||||
So(orderModel.Status, ShouldEqual, consts.OrderStatusPaid)
|
||||
So(orderModel.AmountPaid, ShouldEqual, 300)
|
||||
|
||||
snap := orderModel.Snapshot.Data()
|
||||
So(snap.Kind, ShouldEqual, string(consts.OrderTypeTopup))
|
||||
snap := orderModel.Snapshot.Data()
|
||||
So(snap.Kind, ShouldEqual, string(consts.OrderTypeTopup))
|
||||
|
||||
var snapData fields.OrdersTopupSnapshot
|
||||
So(json.Unmarshal(snap.Data, &snapData), ShouldBeNil)
|
||||
So(snapData.OperatorUserID, ShouldEqual, operatorUserID)
|
||||
So(snapData.TargetUserID, ShouldEqual, targetUserID)
|
||||
So(snapData.Amount, ShouldEqual, int64(300))
|
||||
var snapData fields.OrdersTopupSnapshot
|
||||
So(json.Unmarshal(snap.Data, &snapData), ShouldBeNil)
|
||||
So(snapData.OperatorUserID, ShouldEqual, operatorUserID)
|
||||
So(snapData.TargetUserID, ShouldEqual, targetUserID)
|
||||
So(snapData.Amount, ShouldEqual, int64(300))
|
||||
|
||||
var tu models.TenantUser
|
||||
So(_db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, targetUserID).First(&tu).Error, ShouldBeNil)
|
||||
|
||||
Reference in New Issue
Block a user