From 56082bad4f08070b2624033f636603b501f370da Mon Sep 17 00:00:00 2001 From: Rogee Date: Thu, 15 Jan 2026 11:21:37 +0800 Subject: [PATCH] feat: add superadmin wallet view --- backend/app/http/super/v1/dto/super.go | 30 ++++++ backend/app/http/super/v1/routes.gen.go | 5 + backend/app/http/super/v1/users.go | 15 +++ backend/app/services/super.go | 88 +++++++++++++++++ backend/docs/docs.go | 98 +++++++++++++++++++ backend/docs/swagger.json | 98 +++++++++++++++++++ backend/docs/swagger.yaml | 67 +++++++++++++ docs/superadmin_progress.md | 10 +- .../superadmin/src/service/UserService.js | 4 + .../src/views/superadmin/UserDetail.vue | 85 ++++++++++++++++ 10 files changed, 495 insertions(+), 5 deletions(-) diff --git a/backend/app/http/super/v1/dto/super.go b/backend/app/http/super/v1/dto/super.go index 9eabe92..650df96 100644 --- a/backend/app/http/super/v1/dto/super.go +++ b/backend/app/http/super/v1/dto/super.go @@ -203,6 +203,36 @@ type UserItem struct { JoinedTenantCount int64 `json:"joined_tenant_count"` } +type SuperWalletResponse struct { + // Balance 账户可用余额(分)。 + Balance int64 `json:"balance"` + // BalanceFrozen 账户冻结余额(分)。 + BalanceFrozen int64 `json:"balance_frozen"` + // Transactions 最近交易记录。 + Transactions []SuperWalletTransaction `json:"transactions"` +} + +type SuperWalletTransaction struct { + // ID 订单ID。 + ID int64 `json:"id"` + // OrderType 订单类型。 + OrderType consts.OrderType `json:"order_type"` + // Title 交易标题。 + Title string `json:"title"` + // Amount 交易金额(分)。 + Amount int64 `json:"amount"` + // Type 交易流向(income/expense)。 + Type string `json:"type"` + // Date 交易时间(RFC3339)。 + Date string `json:"date"` + // TenantID 交易所属租户ID(充值为0)。 + TenantID int64 `json:"tenant_id"` + // TenantCode 租户编码。 + TenantCode string `json:"tenant_code"` + // TenantName 租户名称。 + TenantName string `json:"tenant_name"` +} + type UserStatistics struct { // Status 用户状态枚举。 Status consts.UserStatus `json:"status"` diff --git a/backend/app/http/super/v1/routes.gen.go b/backend/app/http/super/v1/routes.gen.go index 4792770..bb05f1a 100644 --- a/backend/app/http/super/v1/routes.gen.go +++ b/backend/app/http/super/v1/routes.gen.go @@ -185,6 +185,11 @@ func (r *Routes) Register(router fiber.Router) { PathParam[int64]("id"), Query[dto.SuperUserTenantListFilter]("filter"), )) + r.log.Debugf("Registering route: Get /super/v1/users/:id/wallet -> users.Wallet") + router.Get("/super/v1/users/:id/wallet"[len(r.Path()):], DataFunc1( + r.users.Wallet, + PathParam[int64]("id"), + )) r.log.Debugf("Registering route: Get /super/v1/users/statistics -> users.Statistics") router.Get("/super/v1/users/statistics"[len(r.Path()):], DataFunc0( r.users.Statistics, diff --git a/backend/app/http/super/v1/users.go b/backend/app/http/super/v1/users.go index 85c5602..4263497 100644 --- a/backend/app/http/super/v1/users.go +++ b/backend/app/http/super/v1/users.go @@ -43,6 +43,21 @@ func (c *users) Get(ctx fiber.Ctx, id int64) (*dto.UserItem, error) { return services.Super.GetUser(ctx, id) } +// Get user wallet +// +// @Router /super/v1/users/:id/wallet [get] +// @Summary Get user wallet +// @Description Get user wallet balance and transactions +// @Tags User +// @Accept json +// @Produce json +// @Param id path int64 true "User ID" +// @Success 200 {object} dto.SuperWalletResponse +// @Bind id path +func (c *users) Wallet(ctx fiber.Ctx, id int64) (*dto.SuperWalletResponse, error) { + return services.Super.GetUserWallet(ctx, id) +} + // List user tenants // // @Router /super/v1/users/:id/tenants [get] diff --git a/backend/app/services/super.go b/backend/app/services/super.go index 17fb374..3a50048 100644 --- a/backend/app/services/super.go +++ b/backend/app/services/super.go @@ -294,6 +294,94 @@ func (s *super) GetUser(ctx context.Context, id int64) (*super_dto.UserItem, err }, nil } +func (s *super) GetUserWallet(ctx context.Context, userID int64) (*super_dto.SuperWalletResponse, error) { + if userID == 0 { + return nil, errorx.ErrBadRequest.WithMsg("用户ID不能为空") + } + + // 查询用户余额。 + userTbl, userQuery := models.UserQuery.QueryContext(ctx) + u, err := userQuery.Where(userTbl.ID.Eq(userID)).First() + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errorx.ErrRecordNotFound + } + return nil, errorx.ErrDatabaseError.WithCause(err) + } + + // 仅返回最近交易记录,避免超管页面加载过重。 + orderTbl, orderQuery := models.OrderQuery.QueryContext(ctx) + orders, err := orderQuery. + Where(orderTbl.UserID.Eq(userID), orderTbl.Status.Eq(consts.OrderStatusPaid)). + Order(orderTbl.CreatedAt.Desc()). + Limit(20). + Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + + // 补齐订单对应的租户信息。 + tenantIDMap := make(map[int64]struct{}) + for _, o := range orders { + if o.TenantID > 0 { + tenantIDMap[o.TenantID] = struct{}{} + } + } + tenantIDs := make([]int64, 0, len(tenantIDMap)) + for id := range tenantIDMap { + tenantIDs = append(tenantIDs, id) + } + tenantMap := make(map[int64]*models.Tenant) + if len(tenantIDs) > 0 { + tenantTbl, tenantQuery := models.TenantQuery.QueryContext(ctx) + tenants, err := tenantQuery.Where(tenantTbl.ID.In(tenantIDs...)).Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + for _, tenant := range tenants { + tenantMap[tenant.ID] = tenant + } + } + + txs := make([]super_dto.SuperWalletTransaction, 0, len(orders)) + for _, o := range orders { + txType := "expense" + switch o.Type { + case consts.OrderTypeRecharge: + txType = "income" + case consts.OrderTypeWithdrawal: + txType = "expense" + case consts.OrderTypeContentPurchase: + txType = "expense" + } + + tenant := tenantMap[o.TenantID] + tenantCode := "" + tenantName := "" + if tenant != nil { + tenantCode = tenant.Code + tenantName = tenant.Name + } + txs = append(txs, super_dto.SuperWalletTransaction{ + ID: o.ID, + OrderType: o.Type, + Title: o.Type.Description(), + Amount: o.AmountPaid, + Type: txType, + Date: o.CreatedAt.Format(time.RFC3339), + TenantID: o.TenantID, + TenantCode: tenantCode, + TenantName: tenantName, + }) + } + + return &super_dto.SuperWalletResponse{ + Balance: u.Balance, + BalanceFrozen: u.BalanceFrozen, + Transactions: txs, + }, nil +} + func (s *super) UpdateUserStatus(ctx context.Context, id int64, form *super_dto.UserStatusUpdateForm) error { tbl, q := models.UserQuery.QueryContext(ctx) _, err := q.Where(tbl.ID.Eq(id)).Update(tbl.Status, consts.UserStatus(form.Status)) diff --git a/backend/docs/docs.go b/backend/docs/docs.go index ebd181a..2855383 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -1260,6 +1260,39 @@ const docTemplate = `{ } } }, + "/super/v1/users/{id}/wallet": { + "get": { + "description": "Get user wallet balance and transactions", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get user wallet", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SuperWalletResponse" + } + } + } + } + }, "/super/v1/withdrawals": { "get": { "description": "List withdrawal orders across tenants", @@ -5954,6 +5987,71 @@ const docTemplate = `{ } } }, + "dto.SuperWalletResponse": { + "type": "object", + "properties": { + "balance": { + "description": "Balance 账户可用余额(分)。", + "type": "integer" + }, + "balance_frozen": { + "description": "BalanceFrozen 账户冻结余额(分)。", + "type": "integer" + }, + "transactions": { + "description": "Transactions 最近交易记录。", + "type": "array", + "items": { + "$ref": "#/definitions/dto.SuperWalletTransaction" + } + } + } + }, + "dto.SuperWalletTransaction": { + "type": "object", + "properties": { + "amount": { + "description": "Amount 交易金额(分)。", + "type": "integer" + }, + "date": { + "description": "Date 交易时间(RFC3339)。", + "type": "string" + }, + "id": { + "description": "ID 订单ID。", + "type": "integer" + }, + "order_type": { + "description": "OrderType 订单类型。", + "allOf": [ + { + "$ref": "#/definitions/consts.OrderType" + } + ] + }, + "tenant_code": { + "description": "TenantCode 租户编码。", + "type": "string" + }, + "tenant_id": { + "description": "TenantID 交易所属租户ID(充值为0)。", + "type": "integer" + }, + "tenant_name": { + "description": "TenantName 租户名称。", + "type": "string" + }, + "title": { + "description": "Title 交易标题。", + "type": "string" + }, + "type": { + "description": "Type 交易流向(income/expense)。", + "type": "string" + } + } + }, "dto.SuperWithdrawalRejectForm": { "type": "object", "required": [ diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 53b3741..e5de6eb 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -1254,6 +1254,39 @@ } } }, + "/super/v1/users/{id}/wallet": { + "get": { + "description": "Get user wallet balance and transactions", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get user wallet", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SuperWalletResponse" + } + } + } + } + }, "/super/v1/withdrawals": { "get": { "description": "List withdrawal orders across tenants", @@ -5948,6 +5981,71 @@ } } }, + "dto.SuperWalletResponse": { + "type": "object", + "properties": { + "balance": { + "description": "Balance 账户可用余额(分)。", + "type": "integer" + }, + "balance_frozen": { + "description": "BalanceFrozen 账户冻结余额(分)。", + "type": "integer" + }, + "transactions": { + "description": "Transactions 最近交易记录。", + "type": "array", + "items": { + "$ref": "#/definitions/dto.SuperWalletTransaction" + } + } + } + }, + "dto.SuperWalletTransaction": { + "type": "object", + "properties": { + "amount": { + "description": "Amount 交易金额(分)。", + "type": "integer" + }, + "date": { + "description": "Date 交易时间(RFC3339)。", + "type": "string" + }, + "id": { + "description": "ID 订单ID。", + "type": "integer" + }, + "order_type": { + "description": "OrderType 订单类型。", + "allOf": [ + { + "$ref": "#/definitions/consts.OrderType" + } + ] + }, + "tenant_code": { + "description": "TenantCode 租户编码。", + "type": "string" + }, + "tenant_id": { + "description": "TenantID 交易所属租户ID(充值为0)。", + "type": "integer" + }, + "tenant_name": { + "description": "TenantName 租户名称。", + "type": "string" + }, + "title": { + "description": "Title 交易标题。", + "type": "string" + }, + "type": { + "description": "Type 交易流向(income/expense)。", + "type": "string" + } + } + }, "dto.SuperWithdrawalRejectForm": { "type": "object", "required": [ diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 37c3399..06c241b 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -1280,6 +1280,51 @@ definitions: description: VerifiedAt 实名认证时间(RFC3339)。 type: string type: object + dto.SuperWalletResponse: + properties: + balance: + description: Balance 账户可用余额(分)。 + type: integer + balance_frozen: + description: BalanceFrozen 账户冻结余额(分)。 + type: integer + transactions: + description: Transactions 最近交易记录。 + items: + $ref: '#/definitions/dto.SuperWalletTransaction' + type: array + type: object + dto.SuperWalletTransaction: + properties: + amount: + description: Amount 交易金额(分)。 + type: integer + date: + description: Date 交易时间(RFC3339)。 + type: string + id: + description: ID 订单ID。 + type: integer + order_type: + allOf: + - $ref: '#/definitions/consts.OrderType' + description: OrderType 订单类型。 + tenant_code: + description: TenantCode 租户编码。 + type: string + tenant_id: + description: TenantID 交易所属租户ID(充值为0)。 + type: integer + tenant_name: + description: TenantName 租户名称。 + type: string + title: + description: Title 交易标题。 + type: string + type: + description: Type 交易流向(income/expense)。 + type: string + type: object dto.SuperWithdrawalRejectForm: properties: reason: @@ -2815,6 +2860,28 @@ paths: summary: List user tenants tags: - User + /super/v1/users/{id}/wallet: + get: + consumes: + - application/json + description: Get user wallet balance and transactions + parameters: + - description: User ID + format: int64 + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.SuperWalletResponse' + summary: Get user wallet + tags: + - User /super/v1/users/statistics: get: consumes: diff --git a/docs/superadmin_progress.md b/docs/superadmin_progress.md index 5a49db2..aa1e110 100644 --- a/docs/superadmin_progress.md +++ b/docs/superadmin_progress.md @@ -33,12 +33,12 @@ ### 2.5 用户管理 `/superadmin/users` - 状态:**部分完成** - 已有:用户列表、角色/状态变更、统计。 -- 缺口:钱包明细、实名认证详情、通知/优惠券明细、充值记录等超管视图接口。 +- 缺口:实名认证详情、通知/优惠券明细、充值记录等超管视图接口。 ### 2.6 用户详情 `/superadmin/users/:userID` - 状态:**部分完成** -- 已有:用户资料、租户关系、订单查询。 -- 缺口:钱包流水、通知、优惠券、收藏/点赞等详情。 +- 已有:用户资料、租户关系、订单查询、钱包余额与流水。 +- 缺口:通知、优惠券、收藏/点赞等详情。 ### 2.7 内容治理 `/superadmin/contents` - 状态:**部分完成** @@ -82,8 +82,8 @@ ## 3) `/super/v1` 接口覆盖度概览 -- **已具备**:Auth、Tenants、Users、Contents、Orders、Withdrawals、Reports、Coupons(列表)、Creators(列表)。 -- **缺失/待补**:资产治理、通知中心、用户钱包/优惠券/通知明细、创作者成员审核、优惠券发放与冻结。 +- **已具备**:Auth、Tenants、Users(含钱包)、Contents、Orders、Withdrawals、Reports、Coupons(列表)、Creators(列表)。 +- **缺失/待补**:资产治理、通知中心、用户优惠券/通知明细、创作者成员审核、优惠券发放与冻结。 ## 4) 建议的下一步(按优先级) diff --git a/frontend/superadmin/src/service/UserService.js b/frontend/superadmin/src/service/UserService.js index b3a418b..3bd6b83 100644 --- a/frontend/superadmin/src/service/UserService.js +++ b/frontend/superadmin/src/service/UserService.js @@ -84,6 +84,10 @@ export const UserService = { if (!userID) throw new Error('userID is required'); return requestJson(`/super/v1/users/${userID}`); }, + async getUserWallet(userID) { + if (!userID) throw new Error('userID is required'); + return requestJson(`/super/v1/users/${userID}/wallet`); + }, async listUserTenants(userID, { page, limit, tenant_id, code, name, role, status, created_at_from, created_at_to } = {}) { if (!userID) throw new Error('userID is required'); diff --git a/frontend/superadmin/src/views/superadmin/UserDetail.vue b/frontend/superadmin/src/views/superadmin/UserDetail.vue index ea3afb5..1bd9735 100644 --- a/frontend/superadmin/src/views/superadmin/UserDetail.vue +++ b/frontend/superadmin/src/views/superadmin/UserDetail.vue @@ -15,6 +15,9 @@ const userID = computed(() => Number(route.params.userID)); const loading = ref(false); const user = ref(null); +const wallet = ref(null); +const walletLoading = ref(false); + const tabValue = ref('owned'); function formatDate(value) { @@ -31,6 +34,25 @@ function formatCny(amountInCents) { return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(amount); } +function formatWalletFlow(value) { + if (value === 'income') return '收入'; + if (value === 'expense') return '支出'; + return value || '-'; +} + +function formatOrderType(value) { + switch (value) { + case 'content_purchase': + return '内容购买'; + case 'recharge': + return '充值'; + case 'withdrawal': + return '提现'; + default: + return value || '-'; + } +} + function getStatusSeverity(status) { switch (status) { case 'active': @@ -65,6 +87,19 @@ async function loadUser() { } } +async function loadWallet() { + const id = userID.value; + if (!id || Number.isNaN(id)) return; + walletLoading.value = true; + try { + wallet.value = await UserService.getUserWallet(id); + } catch (error) { + toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载钱包信息', life: 4000 }); + } finally { + walletLoading.value = false; + } +} + const statusDialogVisible = ref(false); const statusLoading = ref(false); const statusOptionsLoading = ref(false); @@ -237,6 +272,7 @@ watch( ownedTenantsPage.value = 1; joinedTenantsPage.value = 1; loadUser(); + loadWallet(); loadOwnedTenants(); loadJoinedTenants(); }, @@ -306,6 +342,7 @@ onMounted(() => { 拥有的租户 加入的租户 + 钱包 @@ -453,6 +490,54 @@ onMounted(() => { + +
+
+
+
+
可用余额
+
{{ formatCny(wallet?.balance) }}
+
+
+
冻结余额
+
{{ formatCny(wallet?.balance_frozen) }}
+
+
+
+ + + + + + + + + + + + + + + + + + +
+