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:
1 parent
66e8b633ea
commit
9ed9961b50
4 files changed
+80
-45
No files matched your search
@@ -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("""
|
||||
|
||||
@@ -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"])
|
||||
|
||||
|
||||
|
||||
+62
-17
@@ -10,7 +10,7 @@ function getToken(): string | null {
|
||||
}
|
||||
|
||||
// 通用请求方法
|
||||
async function request<T>(
|
||||
export async function request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): 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请求
|
||||
function put<T>(endpoint: string, body?: any): Promise<{ code: number; data: T; message: string }> {
|
||||
return request<T>(endpoint, {
|
||||
@@ -393,7 +378,9 @@ export const adminAPI = {
|
||||
const qs = p.toString();
|
||||
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)); });
|
||||
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),
|
||||
getDistributionLicense: (id: string) => get<any>('/admin/distributions/licenses/' + id),
|
||||
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 =====
|
||||
export const evolutionAPI = {
|
||||
|
||||
@@ -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 }[]; }
|
||||
|
||||
|
||||
Reference in new issue
Block a user