feat: add balance and ledger endpoints for tenant

- Implemented MyBalance and MyLedgerPage methods in the ledger service to retrieve current user balance and transaction history for a specified tenant.
- Added corresponding test cases for MyBalance and MyLedgerPage methods in the ledger test suite.
- Created DTOs for balance response and ledger items to structure the response data.
- Updated Swagger documentation to include new endpoints for retrieving tenant balance and ledgers.
- Added HTTP tests for the new endpoints to ensure proper functionality.
This commit is contained in:
2025-12-18 16:24:37 +08:00
parent 435e541dbe
commit 3249e405ac
13 changed files with 990 additions and 33 deletions

View File

@@ -2,6 +2,7 @@ package services
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
@@ -22,6 +23,44 @@ import (
"go.ipao.vip/gen/types"
)
type PurchaseOrderSnapshot struct {
ContentID int64 `json:"content_id"`
ContentTitle string `json:"content_title"`
ContentUserID int64 `json:"content_user_id"`
ContentVisibility consts.ContentVisibility `json:"content_visibility"`
PreviewSeconds int32 `json:"preview_seconds"`
PreviewDownloadable bool `json:"preview_downloadable"`
Currency consts.Currency `json:"currency"`
PriceAmount int64 `json:"price_amount"`
DiscountType consts.DiscountType `json:"discount_type"`
DiscountValue int64 `json:"discount_value"`
DiscountStartAt *time.Time `json:"discount_start_at,omitempty"`
DiscountEndAt *time.Time `json:"discount_end_at,omitempty"`
AmountOriginal int64 `json:"amount_original"`
AmountDiscount int64 `json:"amount_discount"`
AmountPaid int64 `json:"amount_paid"`
PurchaseAt time.Time `json:"purchase_at"`
PurchaseIdempotency string `json:"purchase_idempotency_key,omitempty"`
PurchasePricingNotes string `json:"purchase_pricing_notes,omitempty"`
}
type OrderItemSnapshot struct {
ContentID int64 `json:"content_id"`
ContentTitle string `json:"content_title"`
ContentUserID int64 `json:"content_user_id"`
AmountPaid int64 `json:"amount_paid"`
}
type TopupOrderSnapshot struct {
OperatorUserID int64 `json:"operator_user_id"`
TargetUserID int64 `json:"target_user_id"`
Amount int64 `json:"amount"`
Currency consts.Currency `json:"currency"`
Reason string `json:"reason,omitempty"`
IdempotencyKey string `json:"idempotency_key,omitempty"`
TopupAt time.Time `json:"topup_at"`
}
// PurchaseContentParams 定义“租户内使用余额购买内容”的入参。
type PurchaseContentParams struct {
// TenantID 租户 ID多租户隔离范围
@@ -56,6 +95,17 @@ type order struct {
ledger *ledger
}
func marshalSnapshot(v any) types.JSON {
b, err := json.Marshal(v)
if err != nil {
return types.JSON([]byte("{}"))
}
if len(b) == 0 {
return types.JSON([]byte("{}"))
}
return types.JSON(b)
}
// AdminTopupUser 租户管理员给租户成员充值(增加该租户下的可用余额)。
func (s *order) AdminTopupUser(
ctx context.Context,
@@ -111,6 +161,15 @@ func (s *order) AdminTopupUser(
}
// 先落订单paid再写入账本credit_topup确保“订单可追溯 + 账本可对账”。
snapshot := marshalSnapshot(&TopupOrderSnapshot{
OperatorUserID: operatorUserID,
TargetUserID: targetUserID,
Amount: amount,
Currency: consts.CurrencyCNY,
Reason: reason,
IdempotencyKey: idempotencyKey,
TopupAt: now,
})
orderModel := models.Order{
TenantID: tenantID,
UserID: targetUserID,
@@ -120,7 +179,7 @@ func (s *order) AdminTopupUser(
AmountOriginal: amount,
AmountDiscount: 0,
AmountPaid: amount,
Snapshot: types.JSON([]byte("{}")),
Snapshot: snapshot,
IdempotencyKey: idempotencyKey,
PaidAt: now,
CreatedAt: now,
@@ -559,6 +618,47 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
amountPaid := s.computeFinalPrice(priceAmount, &price, now)
out.AmountPaid = amountPaid
discountType := price.DiscountType
if discountType == "" {
discountType = consts.DiscountTypeNone
}
var discountStartAt *time.Time
if !price.DiscountStartAt.IsZero() {
t := price.DiscountStartAt
discountStartAt = &t
}
var discountEndAt *time.Time
if !price.DiscountEndAt.IsZero() {
t := price.DiscountEndAt
discountEndAt = &t
}
purchaseSnapshot := marshalSnapshot(&PurchaseOrderSnapshot{
ContentID: content.ID,
ContentTitle: content.Title,
ContentUserID: content.UserID,
ContentVisibility: content.Visibility,
PreviewSeconds: content.PreviewSeconds,
PreviewDownloadable: content.PreviewDownloadable,
Currency: consts.CurrencyCNY,
PriceAmount: priceAmount,
DiscountType: discountType,
DiscountValue: price.DiscountValue,
DiscountStartAt: discountStartAt,
DiscountEndAt: discountEndAt,
AmountOriginal: priceAmount,
AmountDiscount: priceAmount - amountPaid,
AmountPaid: amountPaid,
PurchaseAt: now,
PurchaseIdempotency: params.IdempotencyKey,
})
itemSnapshot := marshalSnapshot(&OrderItemSnapshot{
ContentID: content.ID,
ContentTitle: content.Title,
ContentUserID: content.UserID,
AmountPaid: amountPaid,
})
// 免费内容:无需冻结,保持单事务写订单+权益。
if amountPaid == 0 {
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
@@ -571,7 +671,7 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
AmountOriginal: priceAmount,
AmountDiscount: priceAmount - amountPaid,
AmountPaid: amountPaid,
Snapshot: types.JSON([]byte("{}")),
Snapshot: purchaseSnapshot,
IdempotencyKey: params.IdempotencyKey,
PaidAt: now,
CreatedAt: now,
@@ -587,7 +687,7 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
ContentID: params.ContentID,
ContentUserID: content.UserID,
AmountPaid: amountPaid,
Snapshot: types.JSON([]byte("{}")),
Snapshot: itemSnapshot,
CreatedAt: now,
UpdatedAt: now,
}
@@ -631,7 +731,7 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
AmountOriginal: priceAmount,
AmountDiscount: priceAmount - amountPaid,
AmountPaid: amountPaid,
Snapshot: types.JSON([]byte("{}")),
Snapshot: purchaseSnapshot,
IdempotencyKey: params.IdempotencyKey,
CreatedAt: now,
UpdatedAt: now,
@@ -646,7 +746,7 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
ContentID: params.ContentID,
ContentUserID: content.UserID,
AmountPaid: amountPaid,
Snapshot: types.JSON([]byte("{}")),
Snapshot: itemSnapshot,
CreatedAt: now,
UpdatedAt: now,
}
@@ -753,6 +853,45 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
amountPaid := s.computeFinalPrice(priceAmount, &price, now)
out.AmountPaid = amountPaid
discountType := price.DiscountType
if discountType == "" {
discountType = consts.DiscountTypeNone
}
var discountStartAt *time.Time
if !price.DiscountStartAt.IsZero() {
t := price.DiscountStartAt
discountStartAt = &t
}
var discountEndAt *time.Time
if !price.DiscountEndAt.IsZero() {
t := price.DiscountEndAt
discountEndAt = &t
}
purchaseSnapshot := marshalSnapshot(&PurchaseOrderSnapshot{
ContentID: content.ID,
ContentTitle: content.Title,
ContentUserID: content.UserID,
ContentVisibility: content.Visibility,
PreviewSeconds: content.PreviewSeconds,
PreviewDownloadable: content.PreviewDownloadable,
Currency: consts.CurrencyCNY,
PriceAmount: priceAmount,
DiscountType: discountType,
DiscountValue: price.DiscountValue,
DiscountStartAt: discountStartAt,
DiscountEndAt: discountEndAt,
AmountOriginal: priceAmount,
AmountDiscount: priceAmount - amountPaid,
AmountPaid: amountPaid,
PurchaseAt: now,
})
itemSnapshot := marshalSnapshot(&OrderItemSnapshot{
ContentID: content.ID,
ContentTitle: content.Title,
ContentUserID: content.UserID,
AmountPaid: amountPaid,
})
if amountPaid == 0 {
orderModel := &models.Order{
TenantID: params.TenantID,
@@ -763,7 +902,7 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
AmountOriginal: priceAmount,
AmountDiscount: priceAmount - amountPaid,
AmountPaid: amountPaid,
Snapshot: types.JSON([]byte("{}")),
Snapshot: purchaseSnapshot,
IdempotencyKey: "",
PaidAt: now,
CreatedAt: now,
@@ -779,7 +918,7 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
ContentID: params.ContentID,
ContentUserID: content.UserID,
AmountPaid: amountPaid,
Snapshot: types.JSON([]byte("{}")),
Snapshot: itemSnapshot,
CreatedAt: now,
UpdatedAt: now,
}
@@ -808,7 +947,7 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
AmountOriginal: priceAmount,
AmountDiscount: priceAmount - amountPaid,
AmountPaid: amountPaid,
Snapshot: types.JSON([]byte("{}")),
Snapshot: purchaseSnapshot,
IdempotencyKey: "",
CreatedAt: now,
UpdatedAt: now,
@@ -829,7 +968,7 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
ContentID: params.ContentID,
ContentUserID: content.UserID,
AmountPaid: amountPaid,
Snapshot: types.JSON([]byte("{}")),
Snapshot: itemSnapshot,
CreatedAt: now,
UpdatedAt: now,
}