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=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("""
|
||||||
|
|||||||
@@ -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
@@ -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 = {
|
||||||
|
|||||||
@@ -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 }[]; }
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user