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
This commit is contained in:
qyyfw committed 2026-07-07 21:16:01 +08:00
1 parent 66e8b633ea
commit 9ed9961b50
4 files changed
+80 -45

No files matched your search

+16 -27
View File
@@ -46,7 +46,7 @@ async def get_overview(
user=Depends(get_current_user) 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 stats
user_info = db.execute(text(""" user_info = db.execute(text("""
@@ -141,10 +141,10 @@ async def list_api_keys(
user=Depends(get_current_user) user=Depends(get_current_user)
): ):
"""用户API密钥列表""" """用户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(""" 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 FROM api_keys
WHERE user_id = :uid WHERE user_id = :uid
ORDER BY created_at DESC ORDER BY created_at DESC
@@ -154,8 +154,8 @@ async def list_api_keys(
"code": 200, "code": 200,
"data": [{ "data": [{
"id": r.id, "id": r.id,
"key_name": r.key_name, "key_name": r.name,
"key_prefix": r.key_prefix, "key_prefix": r.api_key[:11] + "..." if r.api_key else "",
"status": r.status, "status": r.status,
"created_at": _format_cst_time(r.created_at), "created_at": _format_cst_time(r.created_at),
"last_used_at": _format_cst_time(r.last_used_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) user=Depends(get_current_user)
): ):
"""创建API密钥""" """创建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 # Generate API key
raw_key = secrets.token_hex(32) raw_key = secrets.token_hex(32)
@@ -180,23 +180,12 @@ async def create_api_key(
# Check if api_keys table exists, create if not # Check if api_keys table exists, create if not
db.execute(text(""" db.execute(text("""
CREATE TABLE IF NOT EXISTS api_keys ( -- api_keys table already exists
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
""")) """))
db.execute(text(""" db.execute(text("""
INSERT INTO api_keys (id, user_id, key_name, key_prefix, key_hash, status) INSERT INTO api_keys (id, user_id, api_key, secret, name, status)
VALUES (:id, :uid, :name, :prefix, :hash, 'active') VALUES (:id, :uid, :prefix, :hash, :name, 'active')
"""), { """), {
"id": key_id, "uid": user_id, "id": key_id, "uid": user_id,
"name": key_name, "prefix": key_prefix, "hash": key_hash "name": key_name, "prefix": key_prefix, "hash": key_hash
@@ -222,7 +211,7 @@ async def revoke_api_key(
user=Depends(get_current_user) user=Depends(get_current_user)
): ):
"""撤销API密钥""" """撤销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(""" result = db.execute(text("""
UPDATE api_keys SET status = 'revoked' UPDATE api_keys SET status = 'revoked'
@@ -271,7 +260,7 @@ async def list_user_transactions(
user=Depends(get_current_user) 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(""" total = db.execute(text("""
SELECT COUNT(*) FROM billing_transactions WHERE user_id = :uid SELECT COUNT(*) FROM billing_transactions WHERE user_id = :uid
@@ -309,7 +298,7 @@ async def recharge(
user=Depends(get_current_user) 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 # Get package info
pkg = db.execute(text(""" pkg = db.execute(text("""
@@ -354,7 +343,7 @@ async def list_user_tenants(
user=Depends(get_current_user) 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(""" rows = db.execute(text("""
SELECT t.id, t.name, t.slug, t.description, t.status, t.max_members, 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=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 # Check slug uniqueness
existing = db.execute(text("SELECT id FROM tenants WHERE slug = :slug"), {"slug": slug}).fetchone() 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=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') since = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
# Daily usage # Daily usage
@@ -486,7 +475,7 @@ async def get_usage_trends(
user=Depends(get_current_user) 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') since = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
rows = db.execute(text(""" rows = db.execute(text("""
+1
View File
@@ -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(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(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(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"]) app.include_router(achievement_admin.router, prefix="/app/api/admin/achievement", tags=["achievement"])
+62 -17
View File
@@ -10,7 +10,7 @@ function getToken(): string | null {
} }
// 通用请求方法 // 通用请求方法
async function request<T>( export async function request<T>(
endpoint: string, endpoint: string,
options: RequestInit = {} options: RequestInit = {}
): Promise<{ code: number; data: T; message: string }> { ): Promise<{ code: number; data: T; message: string }> {
@@ -65,21 +65,6 @@ function post<T>(endpoint: string, body?: any): Promise<{ code: number; data: T;
}); });
} }
export async function put<T>(endpoint: string, body?: any): Promise<T> {
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请求 // PUT请求
function put<T>(endpoint: string, body?: any): Promise<{ code: number; data: T; message: string }> { function put<T>(endpoint: string, body?: any): Promise<{ code: number; data: T; message: string }> {
return request<T>(endpoint, { return request<T>(endpoint, {
@@ -393,7 +378,9 @@ export const adminAPI = {
const qs = p.toString(); const qs = p.toString();
return get<any>(`/admin/membership/user-memberships${qs ? `?${qs}` : ''}`); return get<any>(`/admin/membership/user-memberships${qs ? `?${qs}` : ''}`);
}, },
assignPlan: (userId: string, planId: string, days: number) => put<any>(`/admin/membership/users/${userId}/assign-plan?plan_id=${t sp = new URLSearchParams(); assignPlan: (userId: string, planId: string, days: number) => put<any>(`/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)); }); if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); });
return get<any[]>(`/admin/logs/payment-logs${sp.toString() ? `?${sp.toString()}` : ''}`); return get<any[]>(`/admin/logs/payment-logs${sp.toString() ? `?${sp.toString()}` : ''}`);
}, },
@@ -673,6 +660,64 @@ export const adminAPI = {
createDistributionLicense: (distroId: string, data: any) => post<any>('/admin/distributions/' + distroId + '/licenses', data), createDistributionLicense: (distroId: string, data: any) => post<any>('/admin/distributions/' + distroId + '/licenses', data),
getDistributionLicense: (id: string) => get<any>('/admin/distributions/licenses/' + id), getDistributionLicense: (id: string) => get<any>('/admin/distributions/licenses/' + id),
revokeDistributionLicense: (id: string) => post<any>('/admin/distributions/licenses/' + id + '/revoke'), revokeDistributionLicense: (id: string) => post<any>('/admin/distributions/licenses/' + id + '/revoke'),
// ===== Ads Management =====
getAds: () => get<any>('/admin/ads/'),
addAd: (data: any) => post<any>('/admin/ads/', data),
updateAd: (id: string, data: any) => put<any>(`/admin/ads/${id}`, data),
deleteAd: (id: string) => del<any>(`/admin/ads/${id}`),
// ===== Market Admin (extended) =====
getMarketOverview: () => get<any>('/admin/market/overview'),
getTopApps: (metric?: string, limit?: number) => get<any>(`/admin/market/top-apps${metric ? '?metric=' + metric : ''}${limit ? (metric ? '&' : '?') + 'limit=' + limit : ''}`),
getAdminMarketApps: (params?: string) => get<any>(`/admin/market/apps${params ? '?' + params : ''}`),
getPendingApps: () => get<any>('/admin/market/pending'),
getCoupons: () => get<any>('/admin/market/coupons'),
getRecentReviews: () => get<any>('/admin/market/reviews'),
approveApp: (id: string) => post<any>(`/admin/market/apps/${id}/approve`),
rejectApp: (id: string) => post<any>(`/admin/market/apps/${id}/reject`),
setFeaturedApp: (id: string, featured: boolean, sortOrder?: number) => put<any>(`/admin/market/apps/${id}/featured`, { featured, sort_order: sortOrder }),
deleteMarketApp: (id: string) => del<any>(`/admin/market/apps/${id}`),
deleteCoupon: (id: string) => del<any>(`/admin/market/coupons/${id}`),
updateMarketApp: (id: string, data: any) => put<any>(`/admin/market/apps/${id}`, data),
updateCoupon: (id: string, data: any) => put<any>(`/admin/market/coupons/${id}`, data),
addCoupon: (data: any) => post<any>('/admin/market/coupons', data),
// ===== Membership (extended) =====
getMembershipStats: () => get<any>('/admin/membership/stats'),
revokeMembership: (userId: string) => post<any>(`/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<any[]>(`/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<any[]>(`/admin/logs/token-logs${sp.toString() ? '?' + sp.toString() : ''}`);
},
// ===== Analytics =====
getAnalyticsOverview: (days?: number) => get<any>(`/admin/analytics/overview${days ? '?days=' + days : ''}`),
getAnalyticsApiTrends: (days?: number) => get<any>(`/admin/analytics/trends/api-calls${days ? '?days=' + days : ''}`),
getAnalyticsTokenTrends: (days?: number) => get<any>(`/admin/analytics/trends/tokens${days ? '?days=' + days : ''}`),
getAnalyticsUserTrends: (days?: number) => get<any>(`/admin/analytics/trends/users${days ? '?days=' + days : ''}`),
getAnalyticsRevenueTrends: (days?: number) => get<any>(`/admin/analytics/trends/revenue${days ? '?days=' + days : ''}`),
getAnalyticsModelUsage: (days?: number) => get<any>(`/admin/analytics/models/usage${days ? '?days=' + days : ''}`),
// ===== Billing =====
getBillingRules: () => get<any>('/admin/billing/rules'),
getBillingPackages: () => get<any>('/admin/billing/packages'),
getBillingTransactions: (page?: number) => get<any>(`/admin/billing/transactions${page ? '?page=' + page : ''}`),
getBillingStats: () => get<any>('/admin/billing/stats/overview'),
// ===== Tenant =====
getTenants: (page?: number) => get<any>(`/admin/tenant/list${page ? '?page=' + page : ''}`),
getTenantStats: () => get<any>('/admin/tenant/stats/overview'),
getTenantMembers: (tenantId: string) => get<any>(`/admin/tenant/${tenantId}/members`),
createTenant: (params: string) => post<any>(`/admin/tenant/create${params}`),
}; };
// ===== AI自进化API ===== // ===== AI自进化API =====
export const evolutionAPI = { export const evolutionAPI = {
+1 -1
View File
@@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react';
import { adminAPI } from '@/api'; import { adminAPI } from '@/api';
interface Rule { id: string; name: string; billing_type: string; model_name: string; rate: number; free_quota: number; status: string; } 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 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 }[]; } 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 }[]; }