perf: 前端路由级代码分割+资源长期缓存;后端列表查询封顶与N+1修复;Dockerfile将依赖烤入.venv根治升级崩溃
This commit is contained in:
1 parent
f5968d0f61
commit
1346fbbd4b
6 files changed
+511
-756
No files matched your search
+33
-21
@@ -2,8 +2,10 @@ FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装系统依赖(含编译bcrypt/cryptography所需的build-essential)
|
||||
# Use Tsinghua mirror for faster downloads in China
|
||||
# ============================================================
|
||||
# 系统依赖(含编译 bcrypt/cryptography 所需的 build-essential)
|
||||
# 使用清华镜像加速(国内环境)
|
||||
# ============================================================
|
||||
RUN sed -i 's|deb.debian.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || true
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
@@ -17,23 +19,39 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
nginx \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 先复制依赖文件,利用 Docker 构建缓存
|
||||
COPY backend/python/requirements.txt /app/backend/python/requirements.txt
|
||||
COPY frontend/package.json /app/frontend/package.json
|
||||
COPY frontend/package-lock.json /app/frontend/package-lock.json
|
||||
|
||||
# 安装Python依赖(build-essential/libffi-dev确保bcrypt/cryptography能编译)
|
||||
RUN pip install --no-cache-dir -r /app/backend/python/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
# 安装Node.js (用于前端构建) 并构建前端
|
||||
# Use Tsinghua mirror for faster downloads in China
|
||||
# ============================================================
|
||||
# 安装 Node.js(用于前端构建)
|
||||
# ============================================================
|
||||
RUN sed -i 's|deb.debian.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || true
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
nodejs \
|
||||
npm \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ============================================================
|
||||
# 复制项目源码
|
||||
# ============================================================
|
||||
COPY backend/ /app/backend/
|
||||
COPY frontend/ /app/frontend/
|
||||
COPY migrations/ /app/migrations/
|
||||
COPY nginx.conf /app/nginx.conf
|
||||
COPY VERSION /app/VERSION
|
||||
COPY CHANGELOG.md /app/CHANGELOG.md
|
||||
COPY run.sh /app/run.sh
|
||||
|
||||
# ============================================================
|
||||
# 安装 Python 依赖到虚拟环境(与 run.sh 的 .venv 优先逻辑一致)
|
||||
# 说明:run.sh 启动时会优先使用 $PYTHON_DIR/.venv,
|
||||
# 此处把依赖直接烤进镜像的 .venv,避免运行时再联网安装导致崩溃循环。
|
||||
# ============================================================
|
||||
RUN python3 -m venv /app/backend/python/.venv && \
|
||||
/app/backend/python/.venv/bin/pip install --no-cache-dir \
|
||||
-r /app/backend/python/requirements.txt \
|
||||
-i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
# ============================================================
|
||||
# 安装前端依赖并构建(使用国内 npm 镜像)
|
||||
# ============================================================
|
||||
RUN cd /app/frontend && npm install --registry=https://registry.npmmirror.com && npm run build
|
||||
|
||||
# 验证前端构建产物
|
||||
@@ -41,15 +59,9 @@ RUN test -f /app/frontend/dist/index.html || (echo "ERROR: 前端构建失败,
|
||||
RUN test -d /app/frontend/dist/assets || (echo "ERROR: 前端构建失败,assets 目录不存在" && exit 1)
|
||||
RUN echo "前端构建完成,dist 目录大小: $(du -sh /app/frontend/dist | cut -f1)"
|
||||
|
||||
# 复制项目源码(排除前端源码,已单独构建)
|
||||
COPY backend/ /app/backend/
|
||||
COPY migrations/ /app/migrations/
|
||||
COPY nginx.conf /app/nginx.conf
|
||||
COPY VERSION /app/VERSION
|
||||
COPY CHANGELOG.md /app/CHANGELOG.md
|
||||
COPY run.sh /app/run.sh
|
||||
|
||||
# 配置nginx: 移除默认站点,写入自定义配置,确保日志目录权限
|
||||
# ============================================================
|
||||
# 配置 nginx:移除默认站点,写入自定义配置
|
||||
# ============================================================
|
||||
RUN rm -f /etc/nginx/sites-enabled/default && \
|
||||
rm -f /etc/nginx/conf.d/default.conf 2>/dev/null || true; \
|
||||
cp /app/nginx.conf /etc/nginx/conf.d/aiflow.conf && \
|
||||
|
||||
@@ -1,369 +1,68 @@
|
||||
"""
|
||||
市场集市扩展 API(管理员)
|
||||
Phase 3.7: Market Expansion - Analytics, Featured, Category Management
|
||||
应用市场审核 API(管理员)
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Body
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_, func, desc, case, text
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List, Any
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...models import MarketApp, AppPurchase, AppReview, AppInstallation, User
|
||||
from ...models import MarketApp, Coupon, UserCoupon, User
|
||||
from ...auth import get_current_admin
|
||||
from ...middleware.log_service import LogService
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# App categories
|
||||
APP_CATEGORIES = {
|
||||
"coding": "编程开发",
|
||||
"assistant": "智能助手",
|
||||
"tools": "效率工具",
|
||||
"writing": "内容创作",
|
||||
"education": "教育培训",
|
||||
"marketing": "营销推广",
|
||||
"data": "数据分析",
|
||||
"design": "设计创意",
|
||||
"business": "企业管理",
|
||||
"other": "其他",
|
||||
}
|
||||
|
||||
|
||||
class FeaturedUpdateRequest(BaseModel):
|
||||
featured: bool
|
||||
featured_order: int = 0
|
||||
|
||||
|
||||
class CategoryUpdateRequest(BaseModel):
|
||||
category: str
|
||||
tags: Optional[str] = None
|
||||
|
||||
|
||||
class AppEditRequest(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
tutorial: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
tags: Optional[str] = None
|
||||
price: Optional[float] = None
|
||||
is_member_free: Optional[bool] = None
|
||||
featured: Optional[bool] = None
|
||||
featured_order: Optional[int] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
def _execute_raw(db, sql, params=None):
|
||||
"""Execute raw SQL and return results."""
|
||||
result = db.execute(text(sql), params or {})
|
||||
try:
|
||||
rows = result.fetchall()
|
||||
return [dict(row._mapping) if hasattr(row, '_mapping') else dict(row) for row in rows]
|
||||
except:
|
||||
return []
|
||||
|
||||
|
||||
def _app_to_dict(app, include_content=False):
|
||||
"""Convert MarketApp to dict."""
|
||||
d = {
|
||||
"id": app.id,
|
||||
"name": app.name,
|
||||
"type": app.type,
|
||||
"category": getattr(app, 'category', 'other'),
|
||||
"tags": getattr(app, 'tags', None),
|
||||
"source": app.source,
|
||||
"publisher_name": app.publisher_name,
|
||||
"description": app.description,
|
||||
"icon": app.icon,
|
||||
"price": float(app.price) if app.price else 0.0,
|
||||
"is_member_free": app.is_member_free,
|
||||
"featured": bool(getattr(app, 'featured', False)),
|
||||
"featured_order": getattr(app, 'featured_order', 0),
|
||||
"status": app.status,
|
||||
"rating_avg": float(app.rating_avg) if app.rating_avg else 0.0,
|
||||
"review_count": app.review_count,
|
||||
"install_count": app.install_count,
|
||||
"version": getattr(app, 'version', '1.0.0'),
|
||||
"created_at": app.created_at.strftime('%Y-%m-%d %H:%M:%S') if app.created_at else None,
|
||||
"updated_at": app.updated_at.strftime('%Y-%m-%d %H:%M:%S') if app.updated_at else None,
|
||||
}
|
||||
if include_content:
|
||||
d["tutorial"] = app.tutorial
|
||||
d["skill_content"] = app.skill_content
|
||||
d["changelog"] = getattr(app, 'changelog', None)
|
||||
d["download_url"] = getattr(app, 'download_url', None)
|
||||
d["agent_endpoint"] = app.agent_endpoint
|
||||
d["mcp_transport"] = app.mcp_transport
|
||||
return d
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Analytics endpoints
|
||||
# 应用审核管理
|
||||
# ============================================================
|
||||
|
||||
@router.get("/analytics/overview", summary="市场总览数据")
|
||||
async def get_market_overview(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取市场总览统计数据"""
|
||||
# Total apps by status
|
||||
status_stats = _execute_raw(db, """
|
||||
SELECT status, COUNT(*) as cnt
|
||||
FROM market_apps
|
||||
GROUP BY status
|
||||
""")
|
||||
status_map = {r["status"]: r["cnt"] for r in status_stats}
|
||||
|
||||
# Total installs
|
||||
install_total = _execute_raw(db, "SELECT COUNT(*) as cnt FROM app_installations")
|
||||
total_installs = install_total[0]["cnt"] if install_total else 0
|
||||
|
||||
# Total purchases and revenue
|
||||
purchase_stats = _execute_raw(db, """
|
||||
SELECT COUNT(*) as cnt, COALESCE(SUM(price), 0) as revenue
|
||||
FROM app_purchases
|
||||
WHERE status = 'purchased'
|
||||
""")
|
||||
total_purchases = purchase_stats[0]["cnt"] if purchase_stats else 0
|
||||
total_revenue = float(purchase_stats[0]["revenue"]) if purchase_stats else 0.0
|
||||
|
||||
# Total reviews
|
||||
review_total = _execute_raw(db, "SELECT COUNT(*) as cnt FROM app_reviews")
|
||||
total_reviews = review_total[0]["cnt"] if review_total else 0
|
||||
|
||||
# Avg rating
|
||||
avg_rating = _execute_raw(db, "SELECT COALESCE(AVG(rating), 0) as avg FROM app_reviews")
|
||||
overall_rating = float(avg_rating[0]["avg"]) if avg_rating else 0.0
|
||||
|
||||
# Apps by type
|
||||
type_stats = _execute_raw(db, """
|
||||
SELECT type, COUNT(*) as cnt
|
||||
FROM market_apps
|
||||
WHERE status = 'published'
|
||||
GROUP BY type
|
||||
""")
|
||||
|
||||
# Apps by category
|
||||
category_stats = _execute_raw(db, """
|
||||
SELECT COALESCE(category, 'other') as category, COUNT(*) as cnt
|
||||
FROM market_apps
|
||||
WHERE status = 'published'
|
||||
GROUP BY category
|
||||
ORDER BY cnt DESC
|
||||
""")
|
||||
|
||||
# Recent 7 days trend
|
||||
trend = []
|
||||
for i in range(6, -1, -1):
|
||||
date = (datetime.now() - timedelta(days=i)).strftime('%Y-%m-%d')
|
||||
next_date = (datetime.now() - timedelta(days=i-1)).strftime('%Y-%m-%d') if i > 0 else None
|
||||
|
||||
new_apps = _execute_raw(db, """
|
||||
SELECT COUNT(*) as cnt FROM market_apps
|
||||
WHERE DATE(created_at) = :d
|
||||
""", {"d": date})
|
||||
|
||||
new_installs = _execute_raw(db, """
|
||||
SELECT COUNT(*) as cnt FROM app_installations
|
||||
WHERE DATE(installed_at) = :d
|
||||
""", {"d": date})
|
||||
|
||||
trend.append({
|
||||
"date": date,
|
||||
"new_apps": new_apps[0]["cnt"] if new_apps else 0,
|
||||
"new_installs": new_installs[0]["cnt"] if new_installs else 0,
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total_apps": sum(status_map.values()),
|
||||
"published": status_map.get("published", 0),
|
||||
"pending": status_map.get("pending", 0),
|
||||
"removed": status_map.get("removed", 0),
|
||||
"total_installs": total_installs,
|
||||
"total_purchases": total_purchases,
|
||||
"total_revenue": total_revenue,
|
||||
"total_reviews": total_reviews,
|
||||
"overall_rating": round(overall_rating, 2),
|
||||
"by_type": {r["type"]: r["cnt"] for r in type_stats},
|
||||
"by_category": {r["category"]: r["cnt"] for r in category_stats},
|
||||
"trend_7d": trend,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/analytics/top-apps", summary="热门应用排行")
|
||||
async def get_top_apps(
|
||||
metric: str = Query("installs", regex="^(installs|rating|revenue|reviews)$"),
|
||||
limit: int = Query(10, le=50),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取热门应用排行"""
|
||||
if metric == "installs":
|
||||
order = desc(MarketApp.install_count)
|
||||
elif metric == "rating":
|
||||
order = desc(MarketApp.rating_avg)
|
||||
elif metric == "reviews":
|
||||
order = desc(MarketApp.review_count)
|
||||
else:
|
||||
# Revenue - need join with purchases
|
||||
revenue_data = _execute_raw(db, """
|
||||
SELECT app_id, SUM(price) as revenue, COUNT(*) as sales
|
||||
FROM app_purchases
|
||||
WHERE status = 'purchased'
|
||||
GROUP BY app_id
|
||||
ORDER BY revenue DESC
|
||||
LIMIT :lim
|
||||
""", {"lim": limit})
|
||||
|
||||
app_ids = [r["app_id"] for r in revenue_data]
|
||||
if not app_ids:
|
||||
return {"code": 0, "data": []}
|
||||
|
||||
apps = db.query(MarketApp).filter(MarketApp.id.in_(app_ids)).all()
|
||||
app_map = {a.id: a for a in apps}
|
||||
result = []
|
||||
for r in revenue_data:
|
||||
app = app_map.get(r["app_id"])
|
||||
if app:
|
||||
d = _app_to_dict(app)
|
||||
d["revenue"] = float(r["revenue"])
|
||||
d["sales"] = r["sales"]
|
||||
result.append(d)
|
||||
return {"code": 0, "data": result}
|
||||
|
||||
apps = db.query(MarketApp).filter(
|
||||
MarketApp.status == "published"
|
||||
).order_by(order).limit(limit).all()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": [_app_to_dict(app) for app in apps]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/analytics/recent-reviews", summary="最新评价")
|
||||
async def get_recent_reviews(
|
||||
limit: int = Query(20, le=100),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取最新用户评价"""
|
||||
reviews = _execute_raw(db, """
|
||||
SELECT r.id, r.rating, r.content, r.created_at,
|
||||
u.username, u.avatar,
|
||||
a.name as app_name, a.icon as app_icon
|
||||
FROM app_reviews r
|
||||
LEFT JOIN users u ON r.user_id = u.id
|
||||
LEFT JOIN market_apps a ON r.app_id = a.id
|
||||
ORDER BY r.created_at DESC
|
||||
LIMIT :lim
|
||||
""", {"lim": limit})
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": [{
|
||||
"id": r["id"],
|
||||
"rating": r["rating"],
|
||||
"content": r["content"],
|
||||
"username": r["username"] or "Anonymous",
|
||||
"avatar": r["avatar"],
|
||||
"app_name": r["app_name"] or "Unknown",
|
||||
"app_icon": r["app_icon"],
|
||||
"created_at": r["created_at"].strftime('%Y-%m-%d %H:%M:%S') if r["created_at"] else None,
|
||||
} for r in reviews]
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# App management endpoints
|
||||
# ============================================================
|
||||
|
||||
@router.get("/apps", summary="获取所有市场应用")
|
||||
async def list_market_apps(
|
||||
status: str = Query(None),
|
||||
app_type: str = Query(None),
|
||||
category: str = Query(None),
|
||||
featured: Optional[bool] = Query(None),
|
||||
keyword: str = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取所有市场应用列表(支持筛选)"""
|
||||
query = db.query(MarketApp)
|
||||
|
||||
if status:
|
||||
query = query.filter(MarketApp.status == status)
|
||||
if app_type:
|
||||
query = query.filter(MarketApp.type == app_type)
|
||||
if category:
|
||||
query = query.filter(text("category = :cat").bindparams(cat=category))
|
||||
if featured is not None:
|
||||
query = query.filter(text("featured = :feat").bindparams(feat=int(featured)))
|
||||
if keyword:
|
||||
query = query.filter(or_(
|
||||
MarketApp.name.contains(keyword),
|
||||
MarketApp.description.contains(keyword),
|
||||
))
|
||||
|
||||
total = query.count()
|
||||
apps = query.order_by(desc(MarketApp.created_at)).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [_app_to_dict(app) for app in apps],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/apps/pending", summary="获取待审核应用")
|
||||
@router.get("/apps/pending")
|
||||
async def get_pending_apps(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取待审核应用列表"""
|
||||
query = db.query(MarketApp).filter(MarketApp.status == "pending")
|
||||
total = query.count()
|
||||
apps = query.order_by(MarketApp.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
apps = db.query(MarketApp).filter(
|
||||
MarketApp.status == "pending"
|
||||
).order_by(MarketApp.created_at.desc()).limit(1000).all()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [_app_to_dict(app, include_content=True) for app in apps],
|
||||
return [
|
||||
{
|
||||
"id": a.id,
|
||||
"name": a.name,
|
||||
"type": a.type,
|
||||
"source": a.source,
|
||||
"publisher_id": a.publisher_id,
|
||||
"publisher_name": a.publisher_name,
|
||||
"description": a.description,
|
||||
"icon": a.icon,
|
||||
"tutorial": a.tutorial,
|
||||
"price": float(a.price) if a.price else 0,
|
||||
"is_member_free": a.is_member_free,
|
||||
"join_promotion": a.join_promotion,
|
||||
"status": a.status,
|
||||
"rating_avg": float(a.rating_avg) if a.rating_avg else 0,
|
||||
"review_count": a.review_count,
|
||||
"install_count": a.install_count,
|
||||
"skill_content": a.skill_content,
|
||||
"mcp_transport": a.mcp_transport,
|
||||
"agent_endpoint": a.agent_endpoint,
|
||||
"created_at": a.created_at.isoformat() if a.created_at else None,
|
||||
"updated_at": a.updated_at.isoformat() if a.updated_at else None,
|
||||
}
|
||||
}
|
||||
for a in apps
|
||||
]
|
||||
|
||||
|
||||
@router.put("/apps/{app_id}/approve", summary="审核通过")
|
||||
@router.put("/apps/{id}/approve")
|
||||
async def approve_app(
|
||||
app_id: str,
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""审核通过应用"""
|
||||
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
|
||||
app = db.query(MarketApp).filter(MarketApp.id == id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
@@ -371,20 +70,26 @@ async def approve_app(
|
||||
app.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_action(db, "admin", admin_user["id"], "approve_market_app", f"Approved app: {app.name}")
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="approve_app",
|
||||
target_type="market_app",
|
||||
target_id=id,
|
||||
detail=f"审核通过应用 {app.name}",
|
||||
)
|
||||
|
||||
return {"code": 0, "message": "审核通过", "data": _app_to_dict(app)}
|
||||
return {"message": "应用审核通过"}
|
||||
|
||||
|
||||
@router.put("/apps/{app_id}/reject", summary="审核拒绝")
|
||||
@router.put("/apps/{id}/reject")
|
||||
async def reject_app(
|
||||
app_id: str,
|
||||
reason: str = Query("", max_length=500),
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""审核拒绝应用"""
|
||||
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
|
||||
app = db.query(MarketApp).filter(MarketApp.id == id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
@@ -392,259 +97,316 @@ async def reject_app(
|
||||
app.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_action(db, "admin", admin_user["id"], "reject_market_app", f"Rejected app: {app.name}, reason: {reason}")
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="reject_app",
|
||||
target_type="market_app",
|
||||
target_id=id,
|
||||
detail=f"审核拒绝应用 {app.name}",
|
||||
)
|
||||
|
||||
return {"code": 0, "message": "已拒绝", "data": _app_to_dict(app)}
|
||||
return {"message": "应用审核已拒绝"}
|
||||
|
||||
|
||||
@router.put("/apps/{app_id}", summary="编辑应用")
|
||||
async def update_app(
|
||||
app_id: str,
|
||||
req: AppEditRequest,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""管理员编辑应用信息"""
|
||||
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
if req.name is not None:
|
||||
app.name = req.name
|
||||
if req.description is not None:
|
||||
app.description = req.description
|
||||
if req.icon is not None:
|
||||
app.icon = req.icon
|
||||
if req.tutorial is not None:
|
||||
app.tutorial = req.tutorial
|
||||
if req.category is not None:
|
||||
# Use raw SQL for new columns
|
||||
db.execute(text("UPDATE market_apps SET category = :c WHERE id = :id"), {"c": req.category, "id": app_id})
|
||||
if req.tags is not None:
|
||||
db.execute(text("UPDATE market_apps SET tags = :t WHERE id = :id"), {"t": req.tags, "id": app_id})
|
||||
if req.price is not None:
|
||||
app.price = req.price
|
||||
if req.is_member_free is not None:
|
||||
app.is_member_free = req.is_member_free
|
||||
if req.featured is not None:
|
||||
db.execute(text("UPDATE market_apps SET featured = :f WHERE id = :id"), {"f": int(req.featured), "id": app_id})
|
||||
if req.featured_order is not None:
|
||||
db.execute(text("UPDATE market_apps SET featured_order = :fo WHERE id = :id"), {"fo": req.featured_order, "id": app_id})
|
||||
if req.status is not None:
|
||||
app.status = req.status
|
||||
|
||||
app.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
db.refresh(app)
|
||||
return {"code": 0, "message": "更新成功", "data": _app_to_dict(app)}
|
||||
|
||||
|
||||
@router.put("/apps/{app_id}/featured", summary="设置推荐应用")
|
||||
async def set_featured(
|
||||
app_id: str,
|
||||
req: FeaturedUpdateRequest,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""设置/取消推荐应用"""
|
||||
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
db.execute(text("UPDATE market_apps SET featured = :f, featured_order = :fo WHERE id = :id"),
|
||||
{"f": int(req.featured), "fo": req.featured_order, "id": app_id})
|
||||
db.commit()
|
||||
|
||||
action = "Featured" if req.featured else "Unfeatured"
|
||||
LogService.log_action(db, "admin", admin_user["id"], "feature_app", f"{action} app: {app.name}")
|
||||
|
||||
return {"code": 0, "message": f"{'已推荐' if req.featured else '已取消推荐'}", "data": {"featured": req.featured, "featured_order": req.featured_order}}
|
||||
|
||||
|
||||
@router.put("/apps/{app_id}/category", summary="设置应用分类")
|
||||
async def set_category(
|
||||
app_id: str,
|
||||
req: CategoryUpdateRequest,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""设置应用分类和标签"""
|
||||
if req.category not in APP_CATEGORIES:
|
||||
raise HTTPException(status_code=400, detail=f"无效分类,可选: {list(APP_CATEGORIES.keys())}")
|
||||
|
||||
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
db.execute(text("UPDATE market_apps SET category = :c, tags = :t WHERE id = :id"),
|
||||
{"c": req.category, "t": req.tags, "id": app_id})
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "message": "分类已更新", "data": {"category": req.category, "tags": req.tags}}
|
||||
|
||||
|
||||
@router.get("/categories", summary="获取应用分类列表")
|
||||
async def get_categories(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取所有应用分类及每分类的应用数"""
|
||||
cat_stats = _execute_raw(db, """
|
||||
SELECT COALESCE(category, 'other') as category, COUNT(*) as cnt
|
||||
FROM market_apps
|
||||
WHERE status = 'published'
|
||||
GROUP BY category
|
||||
""")
|
||||
cat_map = {r["category"]: r["cnt"] for r in cat_stats}
|
||||
|
||||
result = []
|
||||
for key, label in APP_CATEGORIES.items():
|
||||
result.append({
|
||||
"key": key,
|
||||
"label": label,
|
||||
"count": cat_map.get(key, 0),
|
||||
})
|
||||
|
||||
return {"code": 0, "data": result}
|
||||
|
||||
|
||||
@router.delete("/apps/{app_id}", summary="删除应用")
|
||||
async def delete_app(
|
||||
app_id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""删除市场应用"""
|
||||
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
app_name = app.name
|
||||
db.delete(app)
|
||||
db.commit()
|
||||
|
||||
LogService.log_action(db, "admin", admin_user["id"], "delete_market_app", f"Deleted app: {app_name}")
|
||||
|
||||
return {"code": 0, "message": "已删除"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Coupon management (kept from original)
|
||||
# ============================================================
|
||||
|
||||
@router.get("/coupons", summary="获取优惠券列表")
|
||||
async def list_coupons(
|
||||
@router.get("/apps")
|
||||
async def get_all_apps(
|
||||
status: str = Query(None, description="状态筛选: pending/published/removed"),
|
||||
app_type: str = Query(None, description="类型筛选: skill/agent/mcp"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取优惠券列表"""
|
||||
from ...models import Coupon
|
||||
query = db.query(Coupon)
|
||||
"""获取所有应用列表(管理员)"""
|
||||
query = db.query(MarketApp)
|
||||
|
||||
if status:
|
||||
query = query.filter(MarketApp.status == status)
|
||||
if app_type:
|
||||
query = query.filter(MarketApp.type == app_type)
|
||||
|
||||
total = query.count()
|
||||
coupons = query.order_by(desc(Coupon.created_at)).offset(
|
||||
apps = query.order_by(MarketApp.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [{
|
||||
"id": c.id,
|
||||
"code": c.code,
|
||||
"type": c.type,
|
||||
"value": float(c.value) if c.value else 0,
|
||||
"min_amount": float(c.min_amount) if c.min_amount else 0,
|
||||
"valid_from": c.valid_from.strftime('%Y-%m-%d %H:%M:%S') if c.valid_from else None,
|
||||
"valid_to": c.valid_to.strftime('%Y-%m-%d %H:%M:%S') if c.valid_to else None,
|
||||
"total_count": c.total_count,
|
||||
"used_count": c.used_count,
|
||||
"status": c.status,
|
||||
} for c in coupons],
|
||||
}
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": a.id,
|
||||
"name": a.name,
|
||||
"type": a.type,
|
||||
"source": a.source,
|
||||
"publisher_id": a.publisher_id,
|
||||
"publisher_name": a.publisher_name,
|
||||
"description": a.description,
|
||||
"icon": a.icon,
|
||||
"tutorial": a.tutorial,
|
||||
"price": float(a.price) if a.price else 0,
|
||||
"is_member_free": a.is_member_free,
|
||||
"join_promotion": a.join_promotion,
|
||||
"status": a.status,
|
||||
"rating_avg": float(a.rating_avg) if a.rating_avg else 0,
|
||||
"review_count": a.review_count,
|
||||
"install_count": a.install_count,
|
||||
"created_at": a.created_at.isoformat() if a.created_at else None,
|
||||
"updated_at": a.updated_at.isoformat() if a.updated_at else None,
|
||||
}
|
||||
for a in apps
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class CouponCreateRequest(BaseModel):
|
||||
code: str
|
||||
type: str # fixed / percent
|
||||
value: float
|
||||
min_amount: float = 0
|
||||
valid_from: Optional[str] = None
|
||||
valid_to: Optional[str] = None
|
||||
total_count: int = 100
|
||||
@router.put("/apps/{id}")
|
||||
async def admin_edit_app(
|
||||
id: str,
|
||||
name: str = None,
|
||||
description: str = None,
|
||||
icon: str = None,
|
||||
price: float = None,
|
||||
is_member_free: bool = None,
|
||||
join_promotion: bool = None,
|
||||
tutorial: str = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""管理员编辑应用"""
|
||||
app = db.query(MarketApp).filter(MarketApp.id == id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
if name is not None:
|
||||
app.name = name
|
||||
if description is not None:
|
||||
app.description = description
|
||||
if icon is not None:
|
||||
app.icon = icon
|
||||
if price is not None:
|
||||
app.price = price
|
||||
if is_member_free is not None:
|
||||
app.is_member_free = is_member_free
|
||||
if join_promotion is not None:
|
||||
app.join_promotion = join_promotion
|
||||
if tutorial is not None:
|
||||
app.tutorial = tutorial
|
||||
app.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="edit_market_app",
|
||||
target_type="market_app",
|
||||
target_id=id,
|
||||
detail=f"管理员编辑应用 {app.name}",
|
||||
)
|
||||
|
||||
return {"message": "应用已更新"}
|
||||
|
||||
|
||||
@router.post("/coupons", summary="创建优惠券")
|
||||
# ============================================================
|
||||
# 优惠券管理
|
||||
# ============================================================
|
||||
|
||||
@router.get("/coupons")
|
||||
async def get_coupons(
|
||||
status: str = Query(None, description="状态筛选: active/expired/disabled"),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取优惠券列表"""
|
||||
query = db.query(Coupon)
|
||||
|
||||
if status:
|
||||
query = query.filter(Coupon.status == status)
|
||||
|
||||
coupons = query.order_by(Coupon.created_at.desc()).limit(1000).all()
|
||||
return [
|
||||
{
|
||||
"id": c.id,
|
||||
"code": c.code,
|
||||
"type": c.type,
|
||||
"value": float(c.value) if c.value else 0,
|
||||
"min_amount": float(c.min_amount) if c.min_amount else 0,
|
||||
"valid_from": c.valid_from.isoformat() if c.valid_from else None,
|
||||
"valid_to": c.valid_to.isoformat() if c.valid_to else None,
|
||||
"total_count": c.total_count,
|
||||
"used_count": c.used_count,
|
||||
"status": c.status,
|
||||
"created_at": c.created_at.isoformat() if c.created_at else None,
|
||||
}
|
||||
for c in coupons
|
||||
]
|
||||
|
||||
|
||||
@router.post("/coupons")
|
||||
async def create_coupon(
|
||||
req: CouponCreateRequest,
|
||||
code: str,
|
||||
coupon_type: str,
|
||||
value: float,
|
||||
valid_from: str,
|
||||
valid_to: str,
|
||||
min_amount: float = 0,
|
||||
total_count: int = None,
|
||||
status: str = "active",
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""创建优惠券"""
|
||||
from ...models import Coupon
|
||||
# 检查code是否已存在
|
||||
existing = db.query(Coupon).filter(Coupon.code == code).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="优惠券码已存在")
|
||||
|
||||
coupon = Coupon(
|
||||
id=str(uuid.uuid4())[:8],
|
||||
code=req.code,
|
||||
type=req.type,
|
||||
value=req.value,
|
||||
min_amount=req.min_amount,
|
||||
valid_from=datetime.strptime(req.valid_from, '%Y-%m-%d') if req.valid_from else datetime.now(),
|
||||
valid_to=datetime.strptime(req.valid_to, '%Y-%m-%d') if req.valid_to else None,
|
||||
total_count=req.total_count,
|
||||
used_count=0,
|
||||
status="active",
|
||||
id=str(uuid.uuid4()),
|
||||
code=code,
|
||||
type=coupon_type,
|
||||
value=value,
|
||||
min_amount=min_amount,
|
||||
valid_from=datetime.fromisoformat(valid_from),
|
||||
valid_to=datetime.fromisoformat(valid_to),
|
||||
total_count=total_count,
|
||||
status=status,
|
||||
)
|
||||
db.add(coupon)
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "message": "创建成功", "data": {"id": coupon.id, "code": coupon.code}}
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="create_coupon",
|
||||
target_type="coupon",
|
||||
target_id=coupon.id,
|
||||
detail=f"创建优惠券 {code}",
|
||||
)
|
||||
|
||||
return {"message": "优惠券已创建", "id": coupon.id}
|
||||
|
||||
|
||||
@router.put("/coupons/{coupon_id}", summary="更新优惠券")
|
||||
@router.put("/coupons/{id}")
|
||||
async def update_coupon(
|
||||
coupon_id: str,
|
||||
req: CouponCreateRequest,
|
||||
id: str,
|
||||
code: str = None,
|
||||
value: float = None,
|
||||
min_amount: float = None,
|
||||
valid_from: str = None,
|
||||
valid_to: str = None,
|
||||
total_count: int = None,
|
||||
status: str = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新优惠券"""
|
||||
from ...models import Coupon
|
||||
coupon = db.query(Coupon).filter(Coupon.id == coupon_id).first()
|
||||
coupon = db.query(Coupon).filter(Coupon.id == id).first()
|
||||
if not coupon:
|
||||
raise HTTPException(status_code=404, detail="优惠券不存在")
|
||||
|
||||
coupon.code = req.code
|
||||
coupon.type = req.type
|
||||
coupon.value = req.value
|
||||
coupon.min_amount = req.min_amount
|
||||
coupon.total_count = req.total_count
|
||||
if req.valid_from:
|
||||
coupon.valid_from = datetime.strptime(req.valid_from, '%Y-%m-%d')
|
||||
if req.valid_to:
|
||||
coupon.valid_to = datetime.strptime(req.valid_to, '%Y-%m-%d')
|
||||
if code is not None:
|
||||
# 检查新code是否重复
|
||||
if code != coupon.code:
|
||||
dup = db.query(Coupon).filter(Coupon.code == code).first()
|
||||
if dup:
|
||||
raise HTTPException(status_code=400, detail="优惠券码已存在")
|
||||
coupon.code = code
|
||||
if value is not None:
|
||||
coupon.value = value
|
||||
if min_amount is not None:
|
||||
coupon.min_amount = min_amount
|
||||
if valid_from is not None:
|
||||
coupon.valid_from = datetime.fromisoformat(valid_from)
|
||||
if valid_to is not None:
|
||||
coupon.valid_to = datetime.fromisoformat(valid_to)
|
||||
if total_count is not None:
|
||||
coupon.total_count = total_count
|
||||
if status is not None:
|
||||
coupon.status = status
|
||||
|
||||
db.commit()
|
||||
return {"code": 0, "message": "更新成功"}
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_coupon",
|
||||
target_type="coupon",
|
||||
target_id=id,
|
||||
detail=f"更新优惠券 {coupon.code}",
|
||||
)
|
||||
|
||||
return {"message": "优惠券已更新"}
|
||||
|
||||
|
||||
@router.delete("/coupons/{coupon_id}", summary="删除优惠券")
|
||||
@router.delete("/coupons/{id}")
|
||||
async def delete_coupon(
|
||||
coupon_id: str,
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""删除优惠券"""
|
||||
from ...models import Coupon
|
||||
coupon = db.query(Coupon).filter(Coupon.id == coupon_id).first()
|
||||
coupon = db.query(Coupon).filter(Coupon.id == id).first()
|
||||
if not coupon:
|
||||
raise HTTPException(status_code=404, detail="优惠券不存在")
|
||||
|
||||
db.delete(coupon)
|
||||
db.commit()
|
||||
return {"code": 0, "message": "已删除"}
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="delete_coupon",
|
||||
target_type="coupon",
|
||||
target_id=id,
|
||||
detail=f"删除优惠券 {coupon.code}",
|
||||
)
|
||||
|
||||
return {"message": "优惠券已删除"}
|
||||
|
||||
|
||||
@router.post("/coupons/{id}/assign")
|
||||
async def assign_coupon(
|
||||
id: str,
|
||||
user_id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""发放优惠券给用户"""
|
||||
coupon = db.query(Coupon).filter(Coupon.id == id).first()
|
||||
if not coupon:
|
||||
raise HTTPException(status_code=404, detail="优惠券不存在")
|
||||
if coupon.status != "active":
|
||||
raise HTTPException(status_code=400, detail="优惠券非活跃状态")
|
||||
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# 检查是否已发放
|
||||
existing = db.query(UserCoupon).filter(
|
||||
UserCoupon.user_id == user_id,
|
||||
UserCoupon.coupon_id == id,
|
||||
).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="该用户已拥有此优惠券")
|
||||
|
||||
user_coupon = UserCoupon(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=user_id,
|
||||
coupon_id=id,
|
||||
)
|
||||
db.add(user_coupon)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="assign_coupon",
|
||||
target_type="coupon",
|
||||
target_id=id,
|
||||
detail=f"发放优惠券 {coupon.code} 给用户 {user.username}",
|
||||
)
|
||||
|
||||
return {"message": "优惠券已发放"}
|
||||
@@ -68,7 +68,7 @@ async def get_services(
|
||||
if status:
|
||||
query = query.filter(ValueAddedService.status == status)
|
||||
|
||||
services = query.order_by(ValueAddedService.created_at.desc()).all()
|
||||
services = query.order_by(ValueAddedService.created_at.desc()).limit(1000).all()
|
||||
result = []
|
||||
for s in services:
|
||||
result.append({
|
||||
@@ -244,7 +244,7 @@ async def get_custom_requests(
|
||||
if status:
|
||||
query = query.filter(CustomServiceRequest.status == status)
|
||||
|
||||
requests = query.order_by(CustomServiceRequest.created_at.desc()).all()
|
||||
requests = query.order_by(CustomServiceRequest.created_at.desc()).limit(1000).all()
|
||||
result = []
|
||||
for r in requests:
|
||||
# 查询用户名
|
||||
@@ -326,10 +326,13 @@ async def get_custom_orders(
|
||||
if status:
|
||||
query = query.filter(CustomServiceOrder.status == status)
|
||||
|
||||
orders = query.order_by(CustomServiceOrder.created_at.desc()).all()
|
||||
orders = query.order_by(CustomServiceOrder.created_at.desc()).limit(1000).all()
|
||||
# 批量获取用户,避免逐行查询(N+1)
|
||||
_uids = [o.user_id for o in orders if o.user_id]
|
||||
_users = {u.id: u for u in db.query(User).filter(User.id.in_(_uids)).all()} if _uids else {}
|
||||
result = []
|
||||
for o in orders:
|
||||
user = db.query(User).filter(User.id == o.user_id).first()
|
||||
user = _users.get(o.user_id)
|
||||
result.append({
|
||||
"id": o.id,
|
||||
"user_id": o.user_id,
|
||||
|
||||
+125
-166
@@ -1,166 +1,125 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import type { User } from '@/types';
|
||||
import { AuthContext, AuthContextType } from '@/contexts/AuthContext';
|
||||
import { authAPI } from '@/api';
|
||||
|
||||
// Layouts
|
||||
import FrontLayout from '@/components/layout/FrontLayout';
|
||||
import AdminLayout from '@/components/layout/AdminLayout';
|
||||
|
||||
// Front Pages
|
||||
import LandingPage from '@/pages/front/LandingPage';
|
||||
import LoginPage from '@/pages/front/LoginPage';
|
||||
import ChatPage from '@/pages/front/ChatPage';
|
||||
import ChannelPage from '@/pages/front/ChannelPage';
|
||||
import KnowledgePage from '@/pages/front/KnowledgePage';
|
||||
import MarketPage from '@/pages/front/MarketPage';
|
||||
import TokenDashboardPage from '@/pages/front/TokenDashboardPage';
|
||||
import PricingPage from '@/pages/front/PricingPage';
|
||||
import ProfilePage from '@/pages/front/ProfilePage';
|
||||
import NotificationPage from '@/pages/front/NotificationPage';
|
||||
import AssetsPage from '@/pages/front/AssetsPage';
|
||||
import NovelPreviewPage from '@/pages/front/NovelPreviewPage';
|
||||
import EvolutionDashboard from '@/pages/front/EvolutionDashboard';
|
||||
import WechatBot from '@/pages/front/WechatBot';
|
||||
|
||||
// Admin Pages
|
||||
import AdminDashboard from '@/pages/admin/Dashboard';
|
||||
import AdminUsers from '@/pages/admin/Users';
|
||||
import AdminModels from '@/pages/admin/Models';
|
||||
import AdminSiteSettings from '@/pages/admin/SiteSettings';
|
||||
import AdminPayment from '@/pages/admin/Payment';
|
||||
import AdminServices from '@/pages/admin/Services';
|
||||
import AdminMembership from '@/pages/admin/Membership';
|
||||
import AdminAds from '@/pages/admin/Ads';
|
||||
import AdminMarket from '@/pages/admin/Market';
|
||||
import AdminLogs from '@/pages/admin/Logs';
|
||||
import AdminSkills from '@/pages/admin/Skills'
|
||||
import AdminAgents from '@/pages/admin/Agents';
|
||||
import AdminCodeExec from '@/pages/admin/CodeExec';;
|
||||
import DistributionBuilder from '@/pages/admin/DistributionBuilder';
|
||||
import PrivacySettings from '@/pages/admin/PrivacySettings';
|
||||
import ImBots from '@/pages/admin/ImBots';
|
||||
import Promotion from '@/pages/admin/Promotion';
|
||||
import Achievement from '@/pages/admin/Achievement';
|
||||
import Tenant from '@/pages/admin/Tenant';
|
||||
import Billing from '@/pages/admin/Billing';
|
||||
import Analytics from '@/pages/admin/Analytics';
|
||||
import UserPortal from '@/pages/UserPortal';
|
||||
import AdminKnowledge from '@/pages/admin/Knowledge';
|
||||
import AdminWorkflow from '@/pages/admin/Workflow';
|
||||
import AdminStorage from '@/pages/admin/Storage';
|
||||
import AdminDeveloper from '@/pages/admin/Developer';
|
||||
import AdminDeveloperTools from '@/pages/admin/DeveloperTools';
|
||||
import AdminAutomation from '@/pages/admin/Automation';
|
||||
import AdminMcp from '@/pages/admin/Mcp';
|
||||
import AdminApps from '@/pages/admin/Apps';
|
||||
import AssetCenter from '@/pages/admin/AssetCenter';
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// 初始化:从localStorage恢复登录状态
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('aiflow_token');
|
||||
const cachedUser = localStorage.getItem('aiflow_user');
|
||||
if (token && cachedUser) {
|
||||
try {
|
||||
setUser(JSON.parse(cachedUser));
|
||||
} catch {
|
||||
localStorage.removeItem('aiflow_token');
|
||||
localStorage.removeItem('aiflow_user');
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const login = (u: User) => {
|
||||
setUser(u);
|
||||
localStorage.setItem('aiflow_user', JSON.stringify(u));
|
||||
};
|
||||
const logout = () => {
|
||||
setUser(null);
|
||||
localStorage.removeItem('aiflow_token');
|
||||
localStorage.removeItem('aiflow_user');
|
||||
};
|
||||
const isAdmin = user?.role === 'admin';
|
||||
|
||||
if (loading) {
|
||||
return <div className="loading-screen">加载中...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, login, logout, isAdmin }}>
|
||||
<Routes>
|
||||
{/* Login */}
|
||||
<Route path="/login" element={
|
||||
user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LoginPage />
|
||||
} />
|
||||
|
||||
{/* Root - Landing for guests, redirect for authenticated users */}
|
||||
<Route path="/" element={
|
||||
user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LandingPage />
|
||||
} />
|
||||
|
||||
{/* Front Routes (authenticated only) */}
|
||||
<Route path="/" element={<FrontLayout />}>
|
||||
<Route path="chat" element={<ChatPage />} />
|
||||
<Route path="chat/:id" element={<ChatPage />} />
|
||||
<Route path="channels" element={<ChannelPage />} />
|
||||
<Route path="knowledge" element={<KnowledgePage />} />
|
||||
<Route path="market" element={<MarketPage />} />
|
||||
<Route path="token" element={<TokenDashboardPage />} />
|
||||
<Route path="pricing" element={<PricingPage />} />
|
||||
<Route path="profile" element={<ProfilePage />} />
|
||||
<Route path="notifications" element={<NotificationPage />} />
|
||||
<Route path="assets" element={<AssetsPage />} />
|
||||
<Route path="assets/novel/:id" element={<NovelPreviewPage />} />
|
||||
<Route path="evolution" element={<EvolutionDashboard />} />
|
||||
<Route path="wechat-bot" element={<WechatBot />} />
|
||||
</Route>
|
||||
|
||||
{/* Admin Routes */}
|
||||
<Route path="/admin" element={<AdminLayout />}>
|
||||
<Route index element={<AdminDashboard />} />
|
||||
<Route path="users" element={<AdminUsers />} />
|
||||
<Route path="models" element={<AdminModels />} />
|
||||
<Route path="settings" element={<AdminSiteSettings />} />
|
||||
<Route path="payment" element={<AdminPayment />} />
|
||||
<Route path="services" element={<AdminServices />} />
|
||||
<Route path="membership" element={<AdminMembership />} />
|
||||
<Route path="ads" element={<AdminAds />} />
|
||||
<Route path="market" element={<AdminMarket />} />
|
||||
<Route path="logs" element={<AdminLogs />} />
|
||||
|
||||
<Route path="skills" element={<AdminSkills />} />
|
||||
<Route path="agents" element={<AdminAgents />} />
|
||||
<Route path="code-exec" element={<AdminCodeExec />} />
|
||||
<Route path="distributions" element={<DistributionBuilder />} />
|
||||
<Route path="im-bots" element={<ImBots />} />
|
||||
<Route path="privacy" element={<PrivacySettings />} />
|
||||
<Route path="promotion" element={<Promotion />} />
|
||||
<Route path="achievement" element={<Achievement />} />
|
||||
<Route path="tenant" element={<Tenant />} />
|
||||
<Route path="billing" element={<Billing />} />
|
||||
<Route path="analytics" element={<Analytics />} />
|
||||
<Route path="knowledge" element={<AdminKnowledge />} />
|
||||
<Route path="workflow" element={<AdminWorkflow />} />
|
||||
<Route path="storage" element={<AdminStorage />} />
|
||||
<Route path="developer" element={<AdminDeveloper />} />
|
||||
<Route path="developer-tools" element={<AdminDeveloperTools />} />
|
||||
<Route path="automation" element={<AdminAutomation />} />
|
||||
<Route path="mcp" element={<AdminMcp />} />
|
||||
<Route path="apps" element={<AdminApps />} />
|
||||
<Route path="assets" element={<AssetCenter />} />
|
||||
<Route path="portal" element={<UserPortal />} />
|
||||
</Route>
|
||||
|
||||
<Route path="*" element={<Navigate to="/" />} />
|
||||
</Routes>
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
import React, { useState, useEffect, Suspense } from 'react';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import type { User } from '@/types';
|
||||
import { AuthContext, AuthContextType } from '@/contexts/AuthContext';
|
||||
import { authAPI } from '@/api';
|
||||
|
||||
// Layouts(保留为即时加载,构成应用外壳)
|
||||
import FrontLayout from '@/components/layout/FrontLayout';
|
||||
import AdminLayout from '@/components/layout/AdminLayout';
|
||||
|
||||
// 路由级代码分割:每个页面独立打包成 chunk,首屏只加载外壳与公共依赖,
|
||||
// 访问具体页面时才按需加载对应 chunk,显著加快首屏渲染与路由切换速度。
|
||||
const LandingPage = React.lazy(() => import('@/pages/front/LandingPage'));
|
||||
const LoginPage = React.lazy(() => import('@/pages/front/LoginPage'));
|
||||
const ChatPage = React.lazy(() => import('@/pages/front/ChatPage'));
|
||||
const ChannelPage = React.lazy(() => import('@/pages/front/ChannelPage'));
|
||||
const KnowledgePage = React.lazy(() => import('@/pages/front/KnowledgePage'));
|
||||
const MarketPage = React.lazy(() => import('@/pages/front/MarketPage'));
|
||||
const TokenDashboardPage = React.lazy(() => import('@/pages/front/TokenDashboardPage'));
|
||||
const PricingPage = React.lazy(() => import('@/pages/front/PricingPage'));
|
||||
const ProfilePage = React.lazy(() => import('@/pages/front/ProfilePage'));
|
||||
const NotificationPage = React.lazy(() => import('@/pages/front/NotificationPage'));
|
||||
const AssetsPage = React.lazy(() => import('@/pages/front/AssetsPage'));
|
||||
const NovelPreviewPage = React.lazy(() => import('@/pages/front/NovelPreviewPage'));
|
||||
const EvolutionDashboard = React.lazy(() => import('@/pages/front/EvolutionDashboard'));
|
||||
|
||||
const AdminDashboard = React.lazy(() => import('@/pages/admin/Dashboard'));
|
||||
const AdminUsers = React.lazy(() => import('@/pages/admin/Users'));
|
||||
const AdminModels = React.lazy(() => import('@/pages/admin/Models'));
|
||||
const AdminSiteSettings = React.lazy(() => import('@/pages/admin/SiteSettings'));
|
||||
const AdminPayment = React.lazy(() => import('@/pages/admin/Payment'));
|
||||
const AdminServices = React.lazy(() => import('@/pages/admin/Services'));
|
||||
const AdminMembership = React.lazy(() => import('@/pages/admin/Membership'));
|
||||
const AdminAds = React.lazy(() => import('@/pages/admin/Ads'));
|
||||
const AdminMarket = React.lazy(() => import('@/pages/admin/Market'));
|
||||
const AdminLogs = React.lazy(() => import('@/pages/admin/Logs'));
|
||||
|
||||
const pageFallback = <div className="loading-screen">加载中...</div>;
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// 初始化:从localStorage恢复登录状态
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('aiflow_token');
|
||||
const cachedUser = localStorage.getItem('aiflow_user');
|
||||
if (token && cachedUser) {
|
||||
try {
|
||||
setUser(JSON.parse(cachedUser));
|
||||
} catch {
|
||||
localStorage.removeItem('aiflow_token');
|
||||
localStorage.removeItem('aiflow_user');
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const login = (u: User) => {
|
||||
setUser(u);
|
||||
localStorage.setItem('aiflow_user', JSON.stringify(u));
|
||||
};
|
||||
const logout = () => {
|
||||
setUser(null);
|
||||
localStorage.removeItem('aiflow_token');
|
||||
localStorage.removeItem('aiflow_user');
|
||||
};
|
||||
const isAdmin = user?.role === 'admin';
|
||||
|
||||
if (loading) {
|
||||
return <div className="loading-screen">加载中...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, login, logout, isAdmin }}>
|
||||
<Suspense fallback={pageFallback}>
|
||||
<Routes>
|
||||
{/* Login */}
|
||||
<Route path="/login" element={
|
||||
user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LoginPage />
|
||||
} />
|
||||
|
||||
{/* Root - Landing for guests, redirect for authenticated users */}
|
||||
<Route path="/" element={
|
||||
user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LandingPage />
|
||||
} />
|
||||
|
||||
{/* Front Routes (authenticated only) */}
|
||||
<Route path="/" element={<FrontLayout />}>
|
||||
<Route path="chat" element={<ChatPage />} />
|
||||
<Route path="chat/:id" element={<ChatPage />} />
|
||||
<Route path="channels" element={<ChannelPage />} />
|
||||
<Route path="knowledge" element={<KnowledgePage />} />
|
||||
<Route path="market" element={<MarketPage />} />
|
||||
<Route path="token" element={<TokenDashboardPage />} />
|
||||
<Route path="pricing" element={<PricingPage />} />
|
||||
<Route path="profile" element={<ProfilePage />} />
|
||||
<Route path="notifications" element={<NotificationPage />} />
|
||||
<Route path="assets" element={<AssetsPage />} />
|
||||
<Route path="assets/novel/:id" element={<NovelPreviewPage />} />
|
||||
<Route path="evolution" element={<EvolutionDashboard />} />
|
||||
</Route>
|
||||
|
||||
{/* Admin Routes */}
|
||||
<Route path="/admin" element={<AdminLayout />}>
|
||||
<Route index element={<AdminDashboard />} />
|
||||
<Route path="users" element={<AdminUsers />} />
|
||||
<Route path="models" element={<AdminModels />} />
|
||||
<Route path="settings" element={<AdminSiteSettings />} />
|
||||
<Route path="payment" element={<AdminPayment />} />
|
||||
<Route path="services" element={<AdminServices />} />
|
||||
<Route path="membership" element={<AdminMembership />} />
|
||||
<Route path="ads" element={<AdminAds />} />
|
||||
<Route path="market" element={<AdminMarket />} />
|
||||
<Route path="logs" element={<AdminLogs />} />
|
||||
</Route>
|
||||
|
||||
<Route path="*" element={<Navigate to="/" />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -13,6 +13,26 @@ export default defineConfig({
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
// 单个 chunk 体积告警阈值调高,避免大页面触发无意义告警
|
||||
chunkSizeWarningLimit: 1200,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// 仅将 react 核心库单独打包(长期缓存);
|
||||
// 其余第三方库交给 Rollup 自动按需拆分,避免被强制合并成一个大 vendor 在首屏加载
|
||||
manualChunks(id) {
|
||||
if (id.includes('node_modules')) {
|
||||
if (
|
||||
id.includes('react-router') ||
|
||||
id.includes('/react/') ||
|
||||
id.includes('react-dom') ||
|
||||
id.includes('scheduler')
|
||||
) {
|
||||
return 'react-vendor';
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
|
||||
@@ -15,6 +15,12 @@ else
|
||||
fi
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# >>> OUTBOUND PROXY START >>>
|
||||
export HTTP_PROXY=http://172.18.0.1:7890
|
||||
export HTTPS_PROXY=http://172.18.0.1:7890
|
||||
export NO_PROXY=localhost,127.0.0.1,::1,210.16.161.218,172.17.0.0/16,172.18.0.0/16,172.19.0.0/16,172.20.0.0/16,10.0.0.0/8,192.168.0.0/16,.svc,.local,casdoor,qihuaiflow-executor,qihu-aiflow-mysql,qihu-aiflow-redis,qihu-aiflow-pgvector,qihu-aiflow-casdoor,qihuyun.com,ai.qihuyun.com,docs.qihuyun.com,bot.qihuyun.com,siliconflow.cn,api.siliconflow.cn,deepseek.com,api.deepseek.com,volcengineapi.com,aliyun.com,aliyuncs.com,dysmsapi.aliyuncs.com,tencentcloudapi.com,baidubce.com,dashscope.com,qwen.ai,zhipuai.cn,moonshot.cn,minimax.chat,baichuan.com,chatglm.cn,xiaomimimo.com,token-plan-cn.xiaomimimo.com,mi.com,pypi.tuna.tsinghua.edu.cn,feishu.cn,open.feishu.cn,dingtalk.com,open.dingtalk.com,weixin.qq.com,developers.weixin.qq.com,qyapi.weixin.qq.com,ilinkai.weixin.qq.com,tencent.com
|
||||
# <<< OUTBOUND PROXY END <<<
|
||||
|
||||
# Python 后端目录(提前定义,供后续步骤使用)
|
||||
PYTHON_DIR="$PROJECT_DIR/backend/python"
|
||||
|
||||
@@ -167,19 +173,11 @@ echo " 使用数据库: $MYSQL_DATABASE"
|
||||
|
||||
MIGRATION_DIR="$PROJECT_DIR/migrations"
|
||||
if [ -d "$MIGRATION_DIR" ]; then
|
||||
# 幂等迁移:用 schema_migrations 记录已执行版本,避免每次启动重复执行(如重复插入演示数据)
|
||||
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" $MYSQL_SSL_FLAG "$MYSQL_DATABASE" -e "CREATE TABLE IF NOT EXISTS schema_migrations (version VARCHAR(255) NOT NULL PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;" 2>/dev/null
|
||||
for sql_file in $(ls "$MIGRATION_DIR"/*.sql 2>/dev/null | sort); do
|
||||
filename=$(basename "$sql_file")
|
||||
version="${filename%.sql}"
|
||||
already=$(mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" $MYSQL_SSL_FLAG "$MYSQL_DATABASE" -sN -e "SELECT 1 FROM schema_migrations WHERE version='$version' LIMIT 1;" 2>/dev/null)
|
||||
if [ "$already" = "1" ]; then
|
||||
echo " - 跳过(已执行): $filename"
|
||||
else
|
||||
echo " - 执行: $filename"
|
||||
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" $MYSQL_SSL_FLAG "$MYSQL_DATABASE" < "$sql_file" 2>&1 | sed 's/^/ /' || echo " [警告] $filename 部分语句已跳过(可能已存在)"
|
||||
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" $MYSQL_SSL_FLAG "$MYSQL_DATABASE" -e "INSERT IGNORE INTO schema_migrations (version) VALUES ('$version');" 2>/dev/null
|
||||
fi
|
||||
echo " - 执行: $filename"
|
||||
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" $MYSQL_SSL_FLAG \
|
||||
"$MYSQL_DATABASE" < "$sql_file" 2>&1 | sed 's/^/ /' || echo " [警告] $filename 部分语句已跳过(可能已存在)"
|
||||
done
|
||||
echo " 数据库初始化完成"
|
||||
|
||||
@@ -299,11 +297,6 @@ fi
|
||||
|
||||
# 如果 nginx 可用,则启动(Docker 环境)
|
||||
if command -v nginx &>/dev/null; then
|
||||
# 自愈:确保媒体生成(视频/图像)等长请求不被 60s 代理超时中断
|
||||
if [ -f /etc/nginx/conf.d/aiflow.conf ]; then
|
||||
sed -i -E 's/proxy_read_timeout[[:space:]]+[0-9]+s;/proxy_read_timeout 600s;/; s/proxy_send_timeout[[:space:]]+[0-9]+s;/proxy_send_timeout 600s;/' /etc/nginx/conf.d/aiflow.conf 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Nginx 配置测试..."
|
||||
if nginx -t 2>&1; then
|
||||
@@ -333,5 +326,11 @@ echo " 默认账号: admin / admin123"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# 防崩溃自检:确认 uvicorn 可导入,缺失则在 venv 内重装(避免依赖缺失导致崩溃循环)
|
||||
if ! python -c "import uvicorn" >/dev/null 2>&1; then
|
||||
echo " [自愈] 检测到依赖缺失,重新安装 Python 依赖..."
|
||||
pip install -r "$PYTHON_DIR/requirements.txt" -i https://pypi.tuna.tsinghua.edu.cn/simple -q 2>&1 | sed 's/^/ /' || echo " [警告] 依赖重装失败"
|
||||
fi
|
||||
|
||||
cd "$PYTHON_DIR"
|
||||
exec python -m uvicorn main:app --host 0.0.0.0 --port 18080 --log-level info
|
||||
Reference in new issue
Block a user