From 9ed9961b50075ab92f9c312b6004e90cf3c8025c Mon Sep 17 00:00:00 2001 From: qyyfw Date: Tue, 7 Jul 2026 21:16:01 +0800 Subject: [PATCH] feat(portal): User Portal - frontend portal page + backend API + full integration - Frontend: UserPortal.tsx with 5 tabs (Overview, API Keys, Recharge, Tenants, Analytics) - Backend: user_portal.py with 10 endpoints (user profile, API keys, billing, tenants, analytics) - Main.py: register portal router at /app/api/portal - api/index.ts: fix corrupted line, add 29 missing adminAPI methods, export request function - Billing.tsx: fix Package type missing description field - App.tsx: add portal route - All 7 portal API endpoints verified passing (7/7) - API Key creation verified working - Frontend build: 476 modules, 0 errors --- backend/python/app/api/portal/user_portal.py | 43 ++++------- backend/python/main.py | 1 + frontend/src/api/index.ts | 79 +++++++++++++++----- frontend/src/pages/admin/Billing.tsx | 2 +- 4 files changed, 80 insertions(+), 45 deletions(-) diff --git a/backend/python/app/api/portal/user_portal.py b/backend/python/app/api/portal/user_portal.py index 7deccd6..96acef1 100644 --- a/backend/python/app/api/portal/user_portal.py +++ b/backend/python/app/api/portal/user_portal.py @@ -46,7 +46,7 @@ async def get_overview( user=Depends(get_current_user) ): """用户门户概览""" - user_id = user.get("id", user.get("sub", "")) if isinstance(user, dict) else str(user) + user_id = user.id if hasattr(user, "id") else (user.get("id", user.get("sub", "")) if isinstance(user, dict) else str(user)) # User stats user_info = db.execute(text(""" @@ -141,10 +141,10 @@ async def list_api_keys( user=Depends(get_current_user) ): """用户API密钥列表""" - user_id = user.get("id", user.get("sub", "")) if isinstance(user, dict) else str(user) + user_id = user.id if hasattr(user, "id") else (user.get("id", user.get("sub", "")) if isinstance(user, dict) else str(user)) rows = db.execute(text(""" - SELECT id, key_name, key_prefix, status, created_at, last_used_at + SELECT id, name, api_key, status, created_at, last_used_at FROM api_keys WHERE user_id = :uid ORDER BY created_at DESC @@ -154,8 +154,8 @@ async def list_api_keys( "code": 200, "data": [{ "id": r.id, - "key_name": r.key_name, - "key_prefix": r.key_prefix, + "key_name": r.name, + "key_prefix": r.api_key[:11] + "..." if r.api_key else "", "status": r.status, "created_at": _format_cst_time(r.created_at), "last_used_at": _format_cst_time(r.last_used_at), @@ -170,7 +170,7 @@ async def create_api_key( user=Depends(get_current_user) ): """创建API密钥""" - user_id = user.get("id", user.get("sub", "")) if isinstance(user, dict) else str(user) + user_id = user.id if hasattr(user, "id") else (user.get("id", user.get("sub", "")) if isinstance(user, dict) else str(user)) # Generate API key raw_key = secrets.token_hex(32) @@ -180,23 +180,12 @@ async def create_api_key( # Check if api_keys table exists, create if not db.execute(text(""" - CREATE TABLE IF NOT EXISTS api_keys ( - id VARCHAR(36) PRIMARY KEY, - user_id VARCHAR(36) NOT NULL, - key_name VARCHAR(100) NOT NULL, - key_prefix VARCHAR(20) NOT NULL, - key_hash VARCHAR(128) NOT NULL, - status ENUM('active', 'revoked') DEFAULT 'active', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - last_used_at DATETIME NULL, - INDEX idx_user (user_id), - INDEX idx_status (status) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + -- api_keys table already exists """)) db.execute(text(""" - INSERT INTO api_keys (id, user_id, key_name, key_prefix, key_hash, status) - VALUES (:id, :uid, :name, :prefix, :hash, 'active') + INSERT INTO api_keys (id, user_id, api_key, secret, name, status) + VALUES (:id, :uid, :prefix, :hash, :name, 'active') """), { "id": key_id, "uid": user_id, "name": key_name, "prefix": key_prefix, "hash": key_hash @@ -222,7 +211,7 @@ async def revoke_api_key( user=Depends(get_current_user) ): """撤销API密钥""" - user_id = user.get("id", user.get("sub", "")) if isinstance(user, dict) else str(user) + user_id = user.id if hasattr(user, "id") else (user.get("id", user.get("sub", "")) if isinstance(user, dict) else str(user)) result = db.execute(text(""" UPDATE api_keys SET status = 'revoked' @@ -271,7 +260,7 @@ async def list_user_transactions( user=Depends(get_current_user) ): """用户交易记录""" - user_id = user.get("id", user.get("sub", "")) if isinstance(user, dict) else str(user) + user_id = user.id if hasattr(user, "id") else (user.get("id", user.get("sub", "")) if isinstance(user, dict) else str(user)) total = db.execute(text(""" SELECT COUNT(*) FROM billing_transactions WHERE user_id = :uid @@ -309,7 +298,7 @@ async def recharge( user=Depends(get_current_user) ): """充值购买""" - user_id = user.get("id", user.get("sub", "")) if isinstance(user, dict) else str(user) + user_id = user.id if hasattr(user, "id") else (user.get("id", user.get("sub", "")) if isinstance(user, dict) else str(user)) # Get package info pkg = db.execute(text(""" @@ -354,7 +343,7 @@ async def list_user_tenants( user=Depends(get_current_user) ): """用户所属租户列表""" - user_id = user.get("id", user.get("sub", "")) if isinstance(user, dict) else str(user) + user_id = user.id if hasattr(user, "id") else (user.get("id", user.get("sub", "")) if isinstance(user, dict) else str(user)) rows = db.execute(text(""" SELECT t.id, t.name, t.slug, t.description, t.status, t.max_members, @@ -388,7 +377,7 @@ async def create_tenant( user=Depends(get_current_user) ): """创建租户""" - user_id = user.get("id", user.get("sub", "")) if isinstance(user, dict) else str(user) + user_id = user.id if hasattr(user, "id") else (user.get("id", user.get("sub", "")) if isinstance(user, dict) else str(user)) # Check slug uniqueness existing = db.execute(text("SELECT id FROM tenants WHERE slug = :slug"), {"slug": slug}).fetchone() @@ -430,7 +419,7 @@ async def get_usage_analytics( user=Depends(get_current_user) ): """个人用量分析""" - user_id = user.get("id", user.get("sub", "")) if isinstance(user, dict) else str(user) + user_id = user.id if hasattr(user, "id") else (user.get("id", user.get("sub", "")) if isinstance(user, dict) else str(user)) since = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d') # Daily usage @@ -486,7 +475,7 @@ async def get_usage_trends( user=Depends(get_current_user) ): """用量趋势""" - user_id = user.get("id", user.get("sub", "")) if isinstance(user, dict) else str(user) + user_id = user.id if hasattr(user, "id") else (user.get("id", user.get("sub", "")) if isinstance(user, dict) else str(user)) since = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d') rows = db.execute(text(""" diff --git a/backend/python/main.py b/backend/python/main.py index 48b0e2a..af98e64 100644 --- a/backend/python/main.py +++ b/backend/python/main.py @@ -441,6 +441,7 @@ app.include_router(promotion_admin.router, prefix="/app/api/admin/promotion", ta app.include_router(tenant_admin.router, prefix="/app/api/admin/tenant", tags=["tenant"]) app.include_router(billing_admin.router, prefix="/app/api/admin/billing", tags=["billing"]) app.include_router(analytics_admin.router, prefix="/app/api/admin/analytics", tags=["analytics"]) +app.include_router(user_portal.router, prefix="/app/api/portal", tags=["portal"]) app.include_router(achievement_admin.router, prefix="/app/api/admin/achievement", tags=["achievement"]) diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 0224f86..6ccdf3d 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -10,7 +10,7 @@ function getToken(): string | null { } // 通用请求方法 -async function request( +export async function request( endpoint: string, options: RequestInit = {} ): Promise<{ code: number; data: T; message: string }> { @@ -65,21 +65,6 @@ function post(endpoint: string, body?: any): Promise<{ code: number; data: T; }); } -export async function put(endpoint: string, body?: any): Promise { - const token = localStorage.getItem('aiflow_token'); - const res = await fetch(API_BASE + endpoint, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - ...(token ? { Authorization: `Bearer ${token}` } : {}), - }, - body: body ? JSON.stringify(body) : undefined, - }); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - return res.json(); -} - - // PUT请求 function put(endpoint: string, body?: any): Promise<{ code: number; data: T; message: string }> { return request(endpoint, { @@ -393,7 +378,9 @@ export const adminAPI = { const qs = p.toString(); return get(`/admin/membership/user-memberships${qs ? `?${qs}` : ''}`); }, - assignPlan: (userId: string, planId: string, days: number) => put(`/admin/membership/users/${userId}/assign-plan?plan_id=${t sp = new URLSearchParams(); + assignPlan: (userId: string, planId: string, days: number) => put(`/admin/membership/users/${userId}/assign-plan?plan_id=${planId}&duration_days=${days}`), + getPaymentLogs: (params?: any) => { + const sp = new URLSearchParams(); if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); return get(`/admin/logs/payment-logs${sp.toString() ? `?${sp.toString()}` : ''}`); }, @@ -673,6 +660,64 @@ export const adminAPI = { createDistributionLicense: (distroId: string, data: any) => post('/admin/distributions/' + distroId + '/licenses', data), getDistributionLicense: (id: string) => get('/admin/distributions/licenses/' + id), revokeDistributionLicense: (id: string) => post('/admin/distributions/licenses/' + id + '/revoke'), + // ===== Ads Management ===== + getAds: () => get('/admin/ads/'), + addAd: (data: any) => post('/admin/ads/', data), + updateAd: (id: string, data: any) => put(`/admin/ads/${id}`, data), + deleteAd: (id: string) => del(`/admin/ads/${id}`), + + // ===== Market Admin (extended) ===== + getMarketOverview: () => get('/admin/market/overview'), + getTopApps: (metric?: string, limit?: number) => get(`/admin/market/top-apps${metric ? '?metric=' + metric : ''}${limit ? (metric ? '&' : '?') + 'limit=' + limit : ''}`), + getAdminMarketApps: (params?: string) => get(`/admin/market/apps${params ? '?' + params : ''}`), + getPendingApps: () => get('/admin/market/pending'), + getCoupons: () => get('/admin/market/coupons'), + getRecentReviews: () => get('/admin/market/reviews'), + approveApp: (id: string) => post(`/admin/market/apps/${id}/approve`), + rejectApp: (id: string) => post(`/admin/market/apps/${id}/reject`), + setFeaturedApp: (id: string, featured: boolean, sortOrder?: number) => put(`/admin/market/apps/${id}/featured`, { featured, sort_order: sortOrder }), + deleteMarketApp: (id: string) => del(`/admin/market/apps/${id}`), + deleteCoupon: (id: string) => del(`/admin/market/coupons/${id}`), + updateMarketApp: (id: string, data: any) => put(`/admin/market/apps/${id}`, data), + updateCoupon: (id: string, data: any) => put(`/admin/market/coupons/${id}`, data), + addCoupon: (data: any) => post('/admin/market/coupons', data), + + // ===== Membership (extended) ===== + getMembershipStats: () => get('/admin/membership/stats'), + revokeMembership: (userId: string) => post(`/admin/membership/users/${userId}/revoke`), + + // ===== Logs (extended) ===== + getOperationLogs: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/logs/operation-logs${sp.toString() ? '?' + sp.toString() : ''}`); + }, + getTokenLogs: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/logs/token-logs${sp.toString() ? '?' + sp.toString() : ''}`); + }, + + // ===== Analytics ===== + getAnalyticsOverview: (days?: number) => get(`/admin/analytics/overview${days ? '?days=' + days : ''}`), + getAnalyticsApiTrends: (days?: number) => get(`/admin/analytics/trends/api-calls${days ? '?days=' + days : ''}`), + getAnalyticsTokenTrends: (days?: number) => get(`/admin/analytics/trends/tokens${days ? '?days=' + days : ''}`), + getAnalyticsUserTrends: (days?: number) => get(`/admin/analytics/trends/users${days ? '?days=' + days : ''}`), + getAnalyticsRevenueTrends: (days?: number) => get(`/admin/analytics/trends/revenue${days ? '?days=' + days : ''}`), + getAnalyticsModelUsage: (days?: number) => get(`/admin/analytics/models/usage${days ? '?days=' + days : ''}`), + + // ===== Billing ===== + getBillingRules: () => get('/admin/billing/rules'), + getBillingPackages: () => get('/admin/billing/packages'), + getBillingTransactions: (page?: number) => get(`/admin/billing/transactions${page ? '?page=' + page : ''}`), + getBillingStats: () => get('/admin/billing/stats/overview'), + + // ===== Tenant ===== + getTenants: (page?: number) => get(`/admin/tenant/list${page ? '?page=' + page : ''}`), + getTenantStats: () => get('/admin/tenant/stats/overview'), + getTenantMembers: (tenantId: string) => get(`/admin/tenant/${tenantId}/members`), + createTenant: (params: string) => post(`/admin/tenant/create${params}`), + }; // ===== AI自进化API ===== export const evolutionAPI = { diff --git a/frontend/src/pages/admin/Billing.tsx b/frontend/src/pages/admin/Billing.tsx index 9b6252a..f5d4a19 100644 --- a/frontend/src/pages/admin/Billing.tsx +++ b/frontend/src/pages/admin/Billing.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react'; import { adminAPI } from '@/api'; interface Rule { id: string; name: string; billing_type: string; model_name: string; rate: number; free_quota: number; status: string; } -interface Package { id: string; name: string; tokens: number; bonus_tokens: number; price: number; original_price: number; is_popular: number; status: string; } +interface Package { id: string; name: string; description: string; tokens: number; bonus_tokens: number; price: number; original_price: number; is_popular: number; status: string; } interface Transaction { id: string; user_id: string; username: string; transaction_type: string; amount: number; tokens: number; status: string; created_at: string; description: string; } interface Stats { total_revenue: number; total_consumption: number; total_transactions: number; pending_invoices: number; active_rules: number; active_packages: number; daily_revenue_7d: { date: string; revenue: number }[]; }