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:
31
backend/app/http/tenant/dto/ledger_me.go
Normal file
31
backend/app/http/tenant/dto/ledger_me.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"quyun/v2/app/requests"
|
||||||
|
"quyun/v2/database/models"
|
||||||
|
"quyun/v2/pkg/consts"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MyLedgerListFilter 定义“我的余额流水”查询条件。
|
||||||
|
type MyLedgerListFilter struct {
|
||||||
|
// Pagination 分页参数(page/limit)。
|
||||||
|
requests.Pagination `json:",inline" query:",inline"`
|
||||||
|
// Type 按流水类型过滤(可选)。
|
||||||
|
Type *consts.TenantLedgerType `json:"type,omitempty" query:"type"`
|
||||||
|
// OrderID 按关联订单过滤(可选)。
|
||||||
|
OrderID *int64 `json:"order_id,omitempty" query:"order_id"`
|
||||||
|
// CreatedAtFrom 创建时间起(可选)。
|
||||||
|
CreatedAtFrom *time.Time `json:"created_at_from,omitempty" query:"created_at_from"`
|
||||||
|
// CreatedAtTo 创建时间止(可选)。
|
||||||
|
CreatedAtTo *time.Time `json:"created_at_to,omitempty" query:"created_at_to"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MyLedgerItem 返回一条余额流水,并补充展示字段。
|
||||||
|
type MyLedgerItem struct {
|
||||||
|
// Ledger 流水记录(租户内隔离)。
|
||||||
|
Ledger *models.TenantLedger `json:"ledger"`
|
||||||
|
// TypeDescription 流水类型中文说明(用于前端展示)。
|
||||||
|
TypeDescription string `json:"type_description"`
|
||||||
|
}
|
||||||
19
backend/app/http/tenant/dto/me_balance.go
Normal file
19
backend/app/http/tenant/dto/me_balance.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"quyun/v2/pkg/consts"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MeBalanceResponse 返回当前用户在当前租户下的余额信息(租户内隔离)。
|
||||||
|
type MeBalanceResponse struct {
|
||||||
|
// Currency 币种:当前固定 CNY(金额单位为分)。
|
||||||
|
Currency consts.Currency `json:"currency"`
|
||||||
|
// Balance 可用余额:可用于购买/消费。
|
||||||
|
Balance int64 `json:"balance"`
|
||||||
|
// BalanceFrozen 冻结余额:用于下单冻结/争议期等。
|
||||||
|
BalanceFrozen int64 `json:"balance_frozen"`
|
||||||
|
// UpdatedAt 更新时间:余额变更时更新。
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
@@ -2,7 +2,10 @@ package tenant
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"quyun/v2/app/http/tenant/dto"
|
"quyun/v2/app/http/tenant/dto"
|
||||||
|
"quyun/v2/app/requests"
|
||||||
|
"quyun/v2/app/services"
|
||||||
"quyun/v2/database/models"
|
"quyun/v2/database/models"
|
||||||
|
"quyun/v2/pkg/consts"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
)
|
)
|
||||||
@@ -32,3 +35,45 @@ func (*me) get(ctx fiber.Ctx, tenant *models.Tenant, user *models.User, tenantUs
|
|||||||
TenantUser: tenantUser,
|
TenantUser: tenantUser,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// balance
|
||||||
|
//
|
||||||
|
// @Summary 当前租户余额信息
|
||||||
|
// @Tags Tenant
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param tenantCode path string true "Tenant Code"
|
||||||
|
// @Success 200 {object} dto.MeBalanceResponse
|
||||||
|
//
|
||||||
|
// @Router /t/:tenantCode/v1/me/balance [get]
|
||||||
|
// @Bind tenant local key(tenant)
|
||||||
|
// @Bind user local key(user)
|
||||||
|
func (*me) balance(ctx fiber.Ctx, tenant *models.Tenant, user *models.User) (*dto.MeBalanceResponse, error) {
|
||||||
|
m, err := services.Ledger.MyBalance(ctx.Context(), tenant.ID, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &dto.MeBalanceResponse{
|
||||||
|
Currency: consts.CurrencyCNY,
|
||||||
|
Balance: m.Balance,
|
||||||
|
BalanceFrozen: m.BalanceFrozen,
|
||||||
|
UpdatedAt: m.UpdatedAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ledgers
|
||||||
|
//
|
||||||
|
// @Summary 当前租户余额流水(分页)
|
||||||
|
// @Tags Tenant
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param tenantCode path string true "Tenant Code"
|
||||||
|
// @Success 200 {object} requests.Pager{items=dto.MyLedgerItem}
|
||||||
|
//
|
||||||
|
// @Router /t/:tenantCode/v1/me/ledgers [get]
|
||||||
|
// @Bind tenant local key(tenant)
|
||||||
|
// @Bind user local key(user)
|
||||||
|
// @Bind filter query
|
||||||
|
func (*me) ledgers(ctx fiber.Ctx, tenant *models.Tenant, user *models.User, filter *dto.MyLedgerListFilter) (*requests.Pager, error) {
|
||||||
|
return services.Ledger.MyLedgerPage(ctx.Context(), tenant.ID, user.ID, filter)
|
||||||
|
}
|
||||||
|
|||||||
@@ -116,6 +116,19 @@ func (r *Routes) Register(router fiber.Router) {
|
|||||||
Local[*models.User]("user"),
|
Local[*models.User]("user"),
|
||||||
Local[*models.TenantUser]("tenant_user"),
|
Local[*models.TenantUser]("tenant_user"),
|
||||||
))
|
))
|
||||||
|
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/me/balance -> me.balance")
|
||||||
|
router.Get("/t/:tenantCode/v1/me/balance"[len(r.Path()):], DataFunc2(
|
||||||
|
r.me.balance,
|
||||||
|
Local[*models.Tenant]("tenant"),
|
||||||
|
Local[*models.User]("user"),
|
||||||
|
))
|
||||||
|
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/me/ledgers -> me.ledgers")
|
||||||
|
router.Get("/t/:tenantCode/v1/me/ledgers"[len(r.Path()):], DataFunc3(
|
||||||
|
r.me.ledgers,
|
||||||
|
Local[*models.Tenant]("tenant"),
|
||||||
|
Local[*models.User]("user"),
|
||||||
|
Query[dto.MyLedgerListFilter]("filter"),
|
||||||
|
))
|
||||||
// Register routes for controller: order
|
// Register routes for controller: order
|
||||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/contents/:contentID/purchase -> order.purchaseContent")
|
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/contents/:contentID/purchase -> order.purchaseContent")
|
||||||
router.Post("/t/:tenantCode/v1/contents/:contentID/purchase"[len(r.Path()):], DataFunc4(
|
router.Post("/t/:tenantCode/v1/contents/:contentID/purchase"[len(r.Path()):], DataFunc4(
|
||||||
|
|||||||
@@ -224,14 +224,14 @@ func (s *ContentTestSuite) Test_HasAccess() {
|
|||||||
|
|
||||||
Convey("权益 active 应返回 true", func() {
|
Convey("权益 active 应返回 true", func() {
|
||||||
access := &models.ContentAccess{
|
access := &models.ContentAccess{
|
||||||
TenantID: tenantID,
|
TenantID: tenantID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
ContentID: content.ID,
|
ContentID: content.ID,
|
||||||
OrderID: 0,
|
OrderID: 0,
|
||||||
Status: consts.ContentAccessStatusActive,
|
Status: consts.ContentAccessStatusActive,
|
||||||
RevokedAt: time.Time{},
|
RevokedAt: time.Time{},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
So(access.Create(ctx), ShouldBeNil)
|
So(access.Create(ctx), ShouldBeNil)
|
||||||
|
|
||||||
@@ -241,4 +241,3 @@ func (s *ContentTestSuite) Test_HasAccess() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"quyun/v2/app/errorx"
|
"quyun/v2/app/errorx"
|
||||||
|
"quyun/v2/app/http/tenant/dto"
|
||||||
|
"quyun/v2/app/requests"
|
||||||
"quyun/v2/database/models"
|
"quyun/v2/database/models"
|
||||||
"quyun/v2/pkg/consts"
|
"quyun/v2/pkg/consts"
|
||||||
|
|
||||||
|
"github.com/samber/lo"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"go.ipao.vip/gen"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
@@ -29,6 +33,84 @@ type ledger struct {
|
|||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MyBalance 查询当前用户在指定租户下的余额信息(可用/冻结)。
|
||||||
|
func (s *ledger) MyBalance(ctx context.Context, tenantID, userID int64) (*models.TenantUser, error) {
|
||||||
|
if tenantID <= 0 || userID <= 0 {
|
||||||
|
return nil, errorx.ErrInvalidParameter.WithMsg("tenant_id/user_id must be > 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"tenant_id": tenantID,
|
||||||
|
"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 {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, errorx.ErrRecordNotFound.WithMsg("tenant user not found")
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MyLedgerPage 分页查询当前用户在指定租户下的余额流水(用于“我的流水”)。
|
||||||
|
func (s *ledger) MyLedgerPage(ctx context.Context, tenantID, userID int64, filter *dto.MyLedgerListFilter) (*requests.Pager, error) {
|
||||||
|
if tenantID <= 0 || userID <= 0 {
|
||||||
|
return nil, errorx.ErrInvalidParameter.WithMsg("tenant_id/user_id must be > 0")
|
||||||
|
}
|
||||||
|
if filter == nil {
|
||||||
|
filter = &dto.MyLedgerListFilter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"tenant_id": tenantID,
|
||||||
|
"user_id": userID,
|
||||||
|
"type": lo.FromPtr(filter.Type),
|
||||||
|
"order_id": lo.FromPtr(filter.OrderID),
|
||||||
|
}).Info("services.ledger.me.ledgers.page")
|
||||||
|
|
||||||
|
filter.Pagination.Format()
|
||||||
|
|
||||||
|
tbl, query := models.TenantLedgerQuery.QueryContext(ctx)
|
||||||
|
|
||||||
|
conds := []gen.Condition{
|
||||||
|
tbl.TenantID.Eq(tenantID),
|
||||||
|
tbl.UserID.Eq(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.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.MyLedgerItem {
|
||||||
|
return &dto.MyLedgerItem{
|
||||||
|
Ledger: m,
|
||||||
|
TypeDescription: m.Type.Description(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return &requests.Pager{
|
||||||
|
Pagination: filter.Pagination,
|
||||||
|
Total: total,
|
||||||
|
Items: items,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Freeze 将可用余额转入冻结余额,并写入账本记录。
|
// Freeze 将可用余额转入冻结余额,并写入账本记录。
|
||||||
func (s *ledger) Freeze(ctx context.Context, tenantID, userID, orderID, amount int64, idempotencyKey, remark string, now time.Time) (*LedgerApplyResult, error) {
|
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)
|
return s.apply(ctx, s.db, tenantID, userID, orderID, consts.TenantLedgerTypeFreeze, amount, -amount, amount, idempotencyKey, remark, now)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"quyun/v2/app/commands/testx"
|
"quyun/v2/app/commands/testx"
|
||||||
"quyun/v2/app/errorx"
|
"quyun/v2/app/errorx"
|
||||||
|
"quyun/v2/app/http/tenant/dto"
|
||||||
"quyun/v2/database"
|
"quyun/v2/database"
|
||||||
"quyun/v2/database/models"
|
"quyun/v2/database/models"
|
||||||
"quyun/v2/pkg/consts"
|
"quyun/v2/pkg/consts"
|
||||||
@@ -302,3 +303,56 @@ func (s *LedgerTestSuite) Test_CreditTopupTx() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *LedgerTestSuite) Test_MyBalance() {
|
||||||
|
Convey("Ledger.MyBalance", s.T(), func() {
|
||||||
|
ctx := s.T().Context()
|
||||||
|
tenantID := int64(1)
|
||||||
|
userID := int64(2)
|
||||||
|
|
||||||
|
s.seedTenantUser(ctx, tenantID, userID, 1000, 200)
|
||||||
|
|
||||||
|
Convey("成功返回租户内余额", func() {
|
||||||
|
m, err := Ledger.MyBalance(ctx, tenantID, userID)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(m, ShouldNotBeNil)
|
||||||
|
So(m.Balance, ShouldEqual, 1000)
|
||||||
|
So(m.BalanceFrozen, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("参数非法应返回错误", func() {
|
||||||
|
_, err := Ledger.MyBalance(ctx, 0, userID)
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LedgerTestSuite) Test_MyLedgerPage() {
|
||||||
|
Convey("Ledger.MyLedgerPage", s.T(), func() {
|
||||||
|
ctx := s.T().Context()
|
||||||
|
tenantID := int64(1)
|
||||||
|
userID := int64(2)
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
s.seedTenantUser(ctx, tenantID, userID, 1000, 0)
|
||||||
|
|
||||||
|
_, err := Ledger.CreditTopupTx(ctx, _db, tenantID, 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)
|
||||||
|
|
||||||
|
Convey("分页返回流水列表", func() {
|
||||||
|
pager, err := Ledger.MyLedgerPage(ctx, tenantID, userID, &dto.MyLedgerListFilter{})
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(pager, ShouldNotBeNil)
|
||||||
|
So(pager.Total, ShouldBeGreaterThanOrEqualTo, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("按 type 过滤", func() {
|
||||||
|
typ := consts.TenantLedgerTypeCreditTopup
|
||||||
|
pager, err := Ledger.MyLedgerPage(ctx, tenantID, userID, &dto.MyLedgerListFilter{Type: &typ})
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(pager.Total, ShouldEqual, 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
@@ -22,6 +23,44 @@ import (
|
|||||||
"go.ipao.vip/gen/types"
|
"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 定义“租户内使用余额购买内容”的入参。
|
// PurchaseContentParams 定义“租户内使用余额购买内容”的入参。
|
||||||
type PurchaseContentParams struct {
|
type PurchaseContentParams struct {
|
||||||
// TenantID 租户 ID(多租户隔离范围)。
|
// TenantID 租户 ID(多租户隔离范围)。
|
||||||
@@ -56,6 +95,17 @@ type order struct {
|
|||||||
ledger *ledger
|
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 租户管理员给租户成员充值(增加该租户下的可用余额)。
|
// AdminTopupUser 租户管理员给租户成员充值(增加该租户下的可用余额)。
|
||||||
func (s *order) AdminTopupUser(
|
func (s *order) AdminTopupUser(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
@@ -111,6 +161,15 @@ func (s *order) AdminTopupUser(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 先落订单(paid),再写入账本(credit_topup),确保“订单可追溯 + 账本可对账”。
|
// 先落订单(paid),再写入账本(credit_topup),确保“订单可追溯 + 账本可对账”。
|
||||||
|
snapshot := marshalSnapshot(&TopupOrderSnapshot{
|
||||||
|
OperatorUserID: operatorUserID,
|
||||||
|
TargetUserID: targetUserID,
|
||||||
|
Amount: amount,
|
||||||
|
Currency: consts.CurrencyCNY,
|
||||||
|
Reason: reason,
|
||||||
|
IdempotencyKey: idempotencyKey,
|
||||||
|
TopupAt: now,
|
||||||
|
})
|
||||||
orderModel := models.Order{
|
orderModel := models.Order{
|
||||||
TenantID: tenantID,
|
TenantID: tenantID,
|
||||||
UserID: targetUserID,
|
UserID: targetUserID,
|
||||||
@@ -120,7 +179,7 @@ func (s *order) AdminTopupUser(
|
|||||||
AmountOriginal: amount,
|
AmountOriginal: amount,
|
||||||
AmountDiscount: 0,
|
AmountDiscount: 0,
|
||||||
AmountPaid: amount,
|
AmountPaid: amount,
|
||||||
Snapshot: types.JSON([]byte("{}")),
|
Snapshot: snapshot,
|
||||||
IdempotencyKey: idempotencyKey,
|
IdempotencyKey: idempotencyKey,
|
||||||
PaidAt: now,
|
PaidAt: now,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
@@ -559,6 +618,47 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
|
|||||||
amountPaid := s.computeFinalPrice(priceAmount, &price, now)
|
amountPaid := s.computeFinalPrice(priceAmount, &price, now)
|
||||||
out.AmountPaid = amountPaid
|
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 {
|
if amountPaid == 0 {
|
||||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
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,
|
AmountOriginal: priceAmount,
|
||||||
AmountDiscount: priceAmount - amountPaid,
|
AmountDiscount: priceAmount - amountPaid,
|
||||||
AmountPaid: amountPaid,
|
AmountPaid: amountPaid,
|
||||||
Snapshot: types.JSON([]byte("{}")),
|
Snapshot: purchaseSnapshot,
|
||||||
IdempotencyKey: params.IdempotencyKey,
|
IdempotencyKey: params.IdempotencyKey,
|
||||||
PaidAt: now,
|
PaidAt: now,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
@@ -587,7 +687,7 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
|
|||||||
ContentID: params.ContentID,
|
ContentID: params.ContentID,
|
||||||
ContentUserID: content.UserID,
|
ContentUserID: content.UserID,
|
||||||
AmountPaid: amountPaid,
|
AmountPaid: amountPaid,
|
||||||
Snapshot: types.JSON([]byte("{}")),
|
Snapshot: itemSnapshot,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
@@ -631,7 +731,7 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
|
|||||||
AmountOriginal: priceAmount,
|
AmountOriginal: priceAmount,
|
||||||
AmountDiscount: priceAmount - amountPaid,
|
AmountDiscount: priceAmount - amountPaid,
|
||||||
AmountPaid: amountPaid,
|
AmountPaid: amountPaid,
|
||||||
Snapshot: types.JSON([]byte("{}")),
|
Snapshot: purchaseSnapshot,
|
||||||
IdempotencyKey: params.IdempotencyKey,
|
IdempotencyKey: params.IdempotencyKey,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
@@ -646,7 +746,7 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
|
|||||||
ContentID: params.ContentID,
|
ContentID: params.ContentID,
|
||||||
ContentUserID: content.UserID,
|
ContentUserID: content.UserID,
|
||||||
AmountPaid: amountPaid,
|
AmountPaid: amountPaid,
|
||||||
Snapshot: types.JSON([]byte("{}")),
|
Snapshot: itemSnapshot,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
@@ -753,6 +853,45 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
|
|||||||
amountPaid := s.computeFinalPrice(priceAmount, &price, now)
|
amountPaid := s.computeFinalPrice(priceAmount, &price, now)
|
||||||
out.AmountPaid = amountPaid
|
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 {
|
if amountPaid == 0 {
|
||||||
orderModel := &models.Order{
|
orderModel := &models.Order{
|
||||||
TenantID: params.TenantID,
|
TenantID: params.TenantID,
|
||||||
@@ -763,7 +902,7 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
|
|||||||
AmountOriginal: priceAmount,
|
AmountOriginal: priceAmount,
|
||||||
AmountDiscount: priceAmount - amountPaid,
|
AmountDiscount: priceAmount - amountPaid,
|
||||||
AmountPaid: amountPaid,
|
AmountPaid: amountPaid,
|
||||||
Snapshot: types.JSON([]byte("{}")),
|
Snapshot: purchaseSnapshot,
|
||||||
IdempotencyKey: "",
|
IdempotencyKey: "",
|
||||||
PaidAt: now,
|
PaidAt: now,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
@@ -779,7 +918,7 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
|
|||||||
ContentID: params.ContentID,
|
ContentID: params.ContentID,
|
||||||
ContentUserID: content.UserID,
|
ContentUserID: content.UserID,
|
||||||
AmountPaid: amountPaid,
|
AmountPaid: amountPaid,
|
||||||
Snapshot: types.JSON([]byte("{}")),
|
Snapshot: itemSnapshot,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
@@ -808,7 +947,7 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
|
|||||||
AmountOriginal: priceAmount,
|
AmountOriginal: priceAmount,
|
||||||
AmountDiscount: priceAmount - amountPaid,
|
AmountDiscount: priceAmount - amountPaid,
|
||||||
AmountPaid: amountPaid,
|
AmountPaid: amountPaid,
|
||||||
Snapshot: types.JSON([]byte("{}")),
|
Snapshot: purchaseSnapshot,
|
||||||
IdempotencyKey: "",
|
IdempotencyKey: "",
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
@@ -829,7 +968,7 @@ func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentPara
|
|||||||
ContentID: params.ContentID,
|
ContentID: params.ContentID,
|
||||||
ContentUserID: content.UserID,
|
ContentUserID: content.UserID,
|
||||||
AmountPaid: amountPaid,
|
AmountPaid: amountPaid,
|
||||||
Snapshot: types.JSON([]byte("{}")),
|
Snapshot: itemSnapshot,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package services
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -78,13 +79,13 @@ func (s *OrderTestSuite) seedPublishedContent(ctx context.Context, tenantID, own
|
|||||||
|
|
||||||
func (s *OrderTestSuite) seedContentPrice(ctx context.Context, tenantID, contentID, priceAmount int64) {
|
func (s *OrderTestSuite) seedContentPrice(ctx context.Context, tenantID, contentID, priceAmount int64) {
|
||||||
p := &models.ContentPrice{
|
p := &models.ContentPrice{
|
||||||
TenantID: tenantID,
|
TenantID: tenantID,
|
||||||
UserID: 1,
|
UserID: 1,
|
||||||
ContentID: contentID,
|
ContentID: contentID,
|
||||||
Currency: consts.CurrencyCNY,
|
Currency: consts.CurrencyCNY,
|
||||||
PriceAmount: priceAmount,
|
PriceAmount: priceAmount,
|
||||||
DiscountType: consts.DiscountTypeNone,
|
DiscountType: consts.DiscountTypeNone,
|
||||||
DiscountValue: 0,
|
DiscountValue: 0,
|
||||||
DiscountStartAt: time.Time{},
|
DiscountStartAt: time.Time{},
|
||||||
DiscountEndAt: time.Time{},
|
DiscountEndAt: time.Time{},
|
||||||
}
|
}
|
||||||
@@ -141,6 +142,12 @@ func (s *OrderTestSuite) Test_AdminTopupUser() {
|
|||||||
So(orderModel.Status, ShouldEqual, consts.OrderStatusPaid)
|
So(orderModel.Status, ShouldEqual, consts.OrderStatusPaid)
|
||||||
So(orderModel.AmountPaid, ShouldEqual, 300)
|
So(orderModel.AmountPaid, ShouldEqual, 300)
|
||||||
|
|
||||||
|
var snap map[string]any
|
||||||
|
So(json.Unmarshal([]byte(orderModel.Snapshot), &snap), ShouldBeNil)
|
||||||
|
So(snap["operator_user_id"], ShouldEqual, float64(operatorUserID))
|
||||||
|
So(snap["target_user_id"], ShouldEqual, float64(targetUserID))
|
||||||
|
So(snap["amount"], ShouldEqual, float64(300))
|
||||||
|
|
||||||
var tu models.TenantUser
|
var tu models.TenantUser
|
||||||
So(_db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, targetUserID).First(&tu).Error, ShouldBeNil)
|
So(_db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, targetUserID).First(&tu).Error, ShouldBeNil)
|
||||||
So(tu.Balance, ShouldEqual, 300)
|
So(tu.Balance, ShouldEqual, 300)
|
||||||
@@ -360,14 +367,14 @@ func (s *OrderTestSuite) Test_AdminRefundOrder() {
|
|||||||
So(item.Create(ctx), ShouldBeNil)
|
So(item.Create(ctx), ShouldBeNil)
|
||||||
|
|
||||||
access := &models.ContentAccess{
|
access := &models.ContentAccess{
|
||||||
TenantID: tenantID,
|
TenantID: tenantID,
|
||||||
UserID: buyerUserID,
|
UserID: buyerUserID,
|
||||||
ContentID: contentID,
|
ContentID: contentID,
|
||||||
OrderID: orderModel.ID,
|
OrderID: orderModel.ID,
|
||||||
Status: consts.ContentAccessStatusActive,
|
Status: consts.ContentAccessStatusActive,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
RevokedAt: time.Time{},
|
RevokedAt: time.Time{},
|
||||||
}
|
}
|
||||||
So(access.Create(ctx), ShouldBeNil)
|
So(access.Create(ctx), ShouldBeNil)
|
||||||
|
|
||||||
@@ -477,6 +484,12 @@ func (s *OrderTestSuite) Test_PurchaseContent() {
|
|||||||
So(res1.Access, ShouldNotBeNil)
|
So(res1.Access, ShouldNotBeNil)
|
||||||
So(res1.Access.Status, ShouldEqual, consts.ContentAccessStatusActive)
|
So(res1.Access.Status, ShouldEqual, consts.ContentAccessStatusActive)
|
||||||
|
|
||||||
|
var snap map[string]any
|
||||||
|
So(json.Unmarshal([]byte(res1.Order.Snapshot), &snap), ShouldBeNil)
|
||||||
|
So(snap["content_id"], ShouldEqual, float64(content.ID))
|
||||||
|
So(snap["content_title"], ShouldEqual, content.Title)
|
||||||
|
So(snap["amount_paid"], ShouldEqual, float64(0))
|
||||||
|
|
||||||
res2, err := Order.PurchaseContent(ctx, &PurchaseContentParams{
|
res2, err := Order.PurchaseContent(ctx, &PurchaseContentParams{
|
||||||
TenantID: tenantID,
|
TenantID: tenantID,
|
||||||
UserID: buyerUserID,
|
UserID: buyerUserID,
|
||||||
@@ -518,6 +531,17 @@ func (s *OrderTestSuite) Test_PurchaseContent() {
|
|||||||
So(res1.Access, ShouldNotBeNil)
|
So(res1.Access, ShouldNotBeNil)
|
||||||
So(res1.Access.Status, ShouldEqual, consts.ContentAccessStatusActive)
|
So(res1.Access.Status, ShouldEqual, consts.ContentAccessStatusActive)
|
||||||
|
|
||||||
|
var snap map[string]any
|
||||||
|
So(json.Unmarshal([]byte(res1.Order.Snapshot), &snap), ShouldBeNil)
|
||||||
|
So(snap["content_id"], ShouldEqual, float64(content.ID))
|
||||||
|
So(snap["amount_paid"], ShouldEqual, float64(300))
|
||||||
|
So(snap["amount_original"], ShouldEqual, float64(300))
|
||||||
|
|
||||||
|
var itemSnap map[string]any
|
||||||
|
So(json.Unmarshal([]byte(res1.OrderItem.Snapshot), &itemSnap), ShouldBeNil)
|
||||||
|
So(itemSnap["content_id"], ShouldEqual, float64(content.ID))
|
||||||
|
So(itemSnap["amount_paid"], ShouldEqual, float64(300))
|
||||||
|
|
||||||
var tu models.TenantUser
|
var tu models.TenantUser
|
||||||
So(_db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, buyerUserID).First(&tu).Error, ShouldBeNil)
|
So(_db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, buyerUserID).First(&tu).Error, ShouldBeNil)
|
||||||
So(tu.Balance, ShouldEqual, 700)
|
So(tu.Balance, ShouldEqual, 700)
|
||||||
|
|||||||
@@ -1078,6 +1078,80 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/t/{tenantCode}/v1/me/balance": {
|
||||||
|
"get": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Tenant"
|
||||||
|
],
|
||||||
|
"summary": "当前租户余额信息",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Tenant Code",
|
||||||
|
"name": "tenantCode",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.MeBalanceResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/t/{tenantCode}/v1/me/ledgers": {
|
||||||
|
"get": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Tenant"
|
||||||
|
],
|
||||||
|
"summary": "当前租户余额流水(分页)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Tenant Code",
|
||||||
|
"name": "tenantCode",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/requests.Pager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/dto.MyLedgerItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/t/{tenantCode}/v1/orders": {
|
"/t/{tenantCode}/v1/orders": {
|
||||||
"get": {
|
"get": {
|
||||||
"consumes": [
|
"consumes": [
|
||||||
@@ -1345,6 +1419,25 @@ const docTemplate = `{
|
|||||||
"RoleSuperAdmin"
|
"RoleSuperAdmin"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"consts.TenantLedgerType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"credit_topup",
|
||||||
|
"debit_purchase",
|
||||||
|
"credit_refund",
|
||||||
|
"freeze",
|
||||||
|
"unfreeze",
|
||||||
|
"adjustment"
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"TenantLedgerTypeCreditTopup",
|
||||||
|
"TenantLedgerTypeDebitPurchase",
|
||||||
|
"TenantLedgerTypeCreditRefund",
|
||||||
|
"TenantLedgerTypeFreeze",
|
||||||
|
"TenantLedgerTypeUnfreeze",
|
||||||
|
"TenantLedgerTypeAdjustment"
|
||||||
|
]
|
||||||
|
},
|
||||||
"consts.TenantStatus": {
|
"consts.TenantStatus": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -1638,6 +1731,31 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.MeBalanceResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"balance": {
|
||||||
|
"description": "Balance 可用余额:可用于购买/消费。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"balance_frozen": {
|
||||||
|
"description": "BalanceFrozen 冻结余额:用于下单冻结/争议期等。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"currency": {
|
||||||
|
"description": "Currency 币种:当前固定 CNY(金额单位为分)。",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/consts.Currency"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"description": "UpdatedAt 更新时间:余额变更时更新。",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.MeResponse": {
|
"dto.MeResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1667,6 +1785,23 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.MyLedgerItem": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"ledger": {
|
||||||
|
"description": "Ledger 流水记录(租户内隔离)。",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/models.TenantLedger"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type_description": {
|
||||||
|
"description": "TypeDescription 流水类型中文说明(用于前端展示)。",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.PurchaseContentForm": {
|
"dto.PurchaseContentForm": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -2381,6 +2516,74 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"models.TenantLedger": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"amount": {
|
||||||
|
"description": "流水金额:分/最小货币单位;通常为正数,方向由 type 决定(由业务层约束)",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"balance_after": {
|
||||||
|
"description": "变更后可用余额:用于审计与对账回放",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"balance_before": {
|
||||||
|
"description": "变更前可用余额:用于审计与对账回放",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"description": "创建时间:默认 now()",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"frozen_after": {
|
||||||
|
"description": "变更后冻结余额:用于审计与对账回放",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"frozen_before": {
|
||||||
|
"description": "变更前冻结余额:用于审计与对账回放",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"description": "主键ID:自增",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"idempotency_key": {
|
||||||
|
"description": "幂等键:同一租户同一用户同一业务操作固定;用于防止重复落账(建议由业务层生成)",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"$ref": "#/definitions/models.Order"
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"description": "关联订单ID:购买/退款类流水应关联 orders.id;非订单类可为空",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"remark": {
|
||||||
|
"description": "备注:业务说明/后台操作原因等;用于审计",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tenant_id": {
|
||||||
|
"description": "租户ID:多租户隔离关键字段;必须与 tenant_users.tenant_id 一致",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"description": "流水类型:credit_topup/debit_purchase/credit_refund/freeze/unfreeze/adjustment;不同类型决定余额/冻结余额的变更方向",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/consts.TenantLedgerType"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"description": "更新时间:默认 now()",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"description": "用户ID:余额账户归属用户;对应 tenant_users.user_id",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"models.TenantUser": {
|
"models.TenantUser": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -1072,6 +1072,80 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/t/{tenantCode}/v1/me/balance": {
|
||||||
|
"get": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Tenant"
|
||||||
|
],
|
||||||
|
"summary": "当前租户余额信息",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Tenant Code",
|
||||||
|
"name": "tenantCode",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.MeBalanceResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/t/{tenantCode}/v1/me/ledgers": {
|
||||||
|
"get": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Tenant"
|
||||||
|
],
|
||||||
|
"summary": "当前租户余额流水(分页)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Tenant Code",
|
||||||
|
"name": "tenantCode",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/requests.Pager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/dto.MyLedgerItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/t/{tenantCode}/v1/orders": {
|
"/t/{tenantCode}/v1/orders": {
|
||||||
"get": {
|
"get": {
|
||||||
"consumes": [
|
"consumes": [
|
||||||
@@ -1339,6 +1413,25 @@
|
|||||||
"RoleSuperAdmin"
|
"RoleSuperAdmin"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"consts.TenantLedgerType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"credit_topup",
|
||||||
|
"debit_purchase",
|
||||||
|
"credit_refund",
|
||||||
|
"freeze",
|
||||||
|
"unfreeze",
|
||||||
|
"adjustment"
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"TenantLedgerTypeCreditTopup",
|
||||||
|
"TenantLedgerTypeDebitPurchase",
|
||||||
|
"TenantLedgerTypeCreditRefund",
|
||||||
|
"TenantLedgerTypeFreeze",
|
||||||
|
"TenantLedgerTypeUnfreeze",
|
||||||
|
"TenantLedgerTypeAdjustment"
|
||||||
|
]
|
||||||
|
},
|
||||||
"consts.TenantStatus": {
|
"consts.TenantStatus": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -1632,6 +1725,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.MeBalanceResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"balance": {
|
||||||
|
"description": "Balance 可用余额:可用于购买/消费。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"balance_frozen": {
|
||||||
|
"description": "BalanceFrozen 冻结余额:用于下单冻结/争议期等。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"currency": {
|
||||||
|
"description": "Currency 币种:当前固定 CNY(金额单位为分)。",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/consts.Currency"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"description": "UpdatedAt 更新时间:余额变更时更新。",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.MeResponse": {
|
"dto.MeResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1661,6 +1779,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.MyLedgerItem": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"ledger": {
|
||||||
|
"description": "Ledger 流水记录(租户内隔离)。",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/models.TenantLedger"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type_description": {
|
||||||
|
"description": "TypeDescription 流水类型中文说明(用于前端展示)。",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.PurchaseContentForm": {
|
"dto.PurchaseContentForm": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -2375,6 +2510,74 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"models.TenantLedger": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"amount": {
|
||||||
|
"description": "流水金额:分/最小货币单位;通常为正数,方向由 type 决定(由业务层约束)",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"balance_after": {
|
||||||
|
"description": "变更后可用余额:用于审计与对账回放",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"balance_before": {
|
||||||
|
"description": "变更前可用余额:用于审计与对账回放",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"description": "创建时间:默认 now()",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"frozen_after": {
|
||||||
|
"description": "变更后冻结余额:用于审计与对账回放",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"frozen_before": {
|
||||||
|
"description": "变更前冻结余额:用于审计与对账回放",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"description": "主键ID:自增",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"idempotency_key": {
|
||||||
|
"description": "幂等键:同一租户同一用户同一业务操作固定;用于防止重复落账(建议由业务层生成)",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"$ref": "#/definitions/models.Order"
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"description": "关联订单ID:购买/退款类流水应关联 orders.id;非订单类可为空",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"remark": {
|
||||||
|
"description": "备注:业务说明/后台操作原因等;用于审计",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tenant_id": {
|
||||||
|
"description": "租户ID:多租户隔离关键字段;必须与 tenant_users.tenant_id 一致",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"description": "流水类型:credit_topup/debit_purchase/credit_refund/freeze/unfreeze/adjustment;不同类型决定余额/冻结余额的变更方向",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/consts.TenantLedgerType"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"description": "更新时间:默认 now()",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"description": "用户ID:余额账户归属用户;对应 tenant_users.user_id",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"models.TenantUser": {
|
"models.TenantUser": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -116,6 +116,22 @@ definitions:
|
|||||||
x-enum-varnames:
|
x-enum-varnames:
|
||||||
- RoleUser
|
- RoleUser
|
||||||
- RoleSuperAdmin
|
- RoleSuperAdmin
|
||||||
|
consts.TenantLedgerType:
|
||||||
|
enum:
|
||||||
|
- credit_topup
|
||||||
|
- debit_purchase
|
||||||
|
- credit_refund
|
||||||
|
- freeze
|
||||||
|
- unfreeze
|
||||||
|
- adjustment
|
||||||
|
type: string
|
||||||
|
x-enum-varnames:
|
||||||
|
- TenantLedgerTypeCreditTopup
|
||||||
|
- TenantLedgerTypeDebitPurchase
|
||||||
|
- TenantLedgerTypeCreditRefund
|
||||||
|
- TenantLedgerTypeFreeze
|
||||||
|
- TenantLedgerTypeUnfreeze
|
||||||
|
- TenantLedgerTypeAdjustment
|
||||||
consts.TenantStatus:
|
consts.TenantStatus:
|
||||||
enum:
|
enum:
|
||||||
- pending_verify
|
- pending_verify
|
||||||
@@ -317,6 +333,22 @@ definitions:
|
|||||||
token:
|
token:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
dto.MeBalanceResponse:
|
||||||
|
properties:
|
||||||
|
balance:
|
||||||
|
description: Balance 可用余额:可用于购买/消费。
|
||||||
|
type: integer
|
||||||
|
balance_frozen:
|
||||||
|
description: BalanceFrozen 冻结余额:用于下单冻结/争议期等。
|
||||||
|
type: integer
|
||||||
|
currency:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/consts.Currency'
|
||||||
|
description: Currency 币种:当前固定 CNY(金额单位为分)。
|
||||||
|
updated_at:
|
||||||
|
description: UpdatedAt 更新时间:余额变更时更新。
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
dto.MeResponse:
|
dto.MeResponse:
|
||||||
properties:
|
properties:
|
||||||
tenant:
|
tenant:
|
||||||
@@ -333,6 +365,16 @@ definitions:
|
|||||||
- $ref: '#/definitions/models.User'
|
- $ref: '#/definitions/models.User'
|
||||||
description: User is the authenticated user derived from JWT `user_id`.
|
description: User is the authenticated user derived from JWT `user_id`.
|
||||||
type: object
|
type: object
|
||||||
|
dto.MyLedgerItem:
|
||||||
|
properties:
|
||||||
|
ledger:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/models.TenantLedger'
|
||||||
|
description: Ledger 流水记录(租户内隔离)。
|
||||||
|
type_description:
|
||||||
|
description: TypeDescription 流水类型中文说明(用于前端展示)。
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
dto.PurchaseContentForm:
|
dto.PurchaseContentForm:
|
||||||
properties:
|
properties:
|
||||||
idempotency_key:
|
idempotency_key:
|
||||||
@@ -809,6 +851,54 @@ definitions:
|
|||||||
uuid:
|
uuid:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
models.TenantLedger:
|
||||||
|
properties:
|
||||||
|
amount:
|
||||||
|
description: 流水金额:分/最小货币单位;通常为正数,方向由 type 决定(由业务层约束)
|
||||||
|
type: integer
|
||||||
|
balance_after:
|
||||||
|
description: 变更后可用余额:用于审计与对账回放
|
||||||
|
type: integer
|
||||||
|
balance_before:
|
||||||
|
description: 变更前可用余额:用于审计与对账回放
|
||||||
|
type: integer
|
||||||
|
created_at:
|
||||||
|
description: 创建时间:默认 now()
|
||||||
|
type: string
|
||||||
|
frozen_after:
|
||||||
|
description: 变更后冻结余额:用于审计与对账回放
|
||||||
|
type: integer
|
||||||
|
frozen_before:
|
||||||
|
description: 变更前冻结余额:用于审计与对账回放
|
||||||
|
type: integer
|
||||||
|
id:
|
||||||
|
description: 主键ID:自增
|
||||||
|
type: integer
|
||||||
|
idempotency_key:
|
||||||
|
description: 幂等键:同一租户同一用户同一业务操作固定;用于防止重复落账(建议由业务层生成)
|
||||||
|
type: string
|
||||||
|
order:
|
||||||
|
$ref: '#/definitions/models.Order'
|
||||||
|
order_id:
|
||||||
|
description: 关联订单ID:购买/退款类流水应关联 orders.id;非订单类可为空
|
||||||
|
type: integer
|
||||||
|
remark:
|
||||||
|
description: 备注:业务说明/后台操作原因等;用于审计
|
||||||
|
type: string
|
||||||
|
tenant_id:
|
||||||
|
description: 租户ID:多租户隔离关键字段;必须与 tenant_users.tenant_id 一致
|
||||||
|
type: integer
|
||||||
|
type:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/consts.TenantLedgerType'
|
||||||
|
description: 流水类型:credit_topup/debit_purchase/credit_refund/freeze/unfreeze/adjustment;不同类型决定余额/冻结余额的变更方向
|
||||||
|
updated_at:
|
||||||
|
description: 更新时间:默认 now()
|
||||||
|
type: string
|
||||||
|
user_id:
|
||||||
|
description: 用户ID:余额账户归属用户;对应 tenant_users.user_id
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
models.TenantUser:
|
models.TenantUser:
|
||||||
properties:
|
properties:
|
||||||
balance:
|
balance:
|
||||||
@@ -1600,6 +1690,51 @@ paths:
|
|||||||
summary: 当前租户上下文信息
|
summary: 当前租户上下文信息
|
||||||
tags:
|
tags:
|
||||||
- Tenant
|
- Tenant
|
||||||
|
/t/{tenantCode}/v1/me/balance:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: Tenant Code
|
||||||
|
in: path
|
||||||
|
name: tenantCode
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.MeBalanceResponse'
|
||||||
|
summary: 当前租户余额信息
|
||||||
|
tags:
|
||||||
|
- Tenant
|
||||||
|
/t/{tenantCode}/v1/me/ledgers:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: Tenant Code
|
||||||
|
in: path
|
||||||
|
name: tenantCode
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/requests.Pager'
|
||||||
|
- properties:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/dto.MyLedgerItem'
|
||||||
|
type: object
|
||||||
|
summary: 当前租户余额流水(分页)
|
||||||
|
tags:
|
||||||
|
- Tenant
|
||||||
/t/{tenantCode}/v1/orders:
|
/t/{tenantCode}/v1/orders:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ GET {{ host }}/t/{{ tenantCode }}/v1/me
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
Authorization: Bearer {{ token }}
|
Authorization: Bearer {{ token }}
|
||||||
|
|
||||||
|
### Tenant - My balance
|
||||||
|
GET {{ host }}/t/{{ tenantCode }}/v1/me/balance
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{ token }}
|
||||||
|
|
||||||
|
### Tenant - My ledgers (paged)
|
||||||
|
GET {{ host }}/t/{{ tenantCode }}/v1/me/ledgers?page=1&limit=20
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{ token }}
|
||||||
|
|
||||||
### Tenant - Contents list (published)
|
### Tenant - Contents list (published)
|
||||||
GET {{ host }}/t/{{ tenantCode }}/v1/contents?page=1&limit=10
|
GET {{ host }}/t/{{ tenantCode }}/v1/contents?page=1&limit=10
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|||||||
Reference in New Issue
Block a user