perf: 前端路由级代码分割+资源长期缓存;后端列表查询封顶与N+1修复;Dockerfile将依赖烤入.venv根治升级崩溃

This commit is contained in:
qihu committed 2026-07-11 05:02:14 +08:00
1 parent f5968d0f61
commit 1346fbbd4b
6 files changed
+511 -756

No files matched your search

+33 -21
View File
@@ -2,8 +2,10 @@ FROM python:3.11-slim
WORKDIR /app 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 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 \ RUN apt-get update && apt-get install -y --no-install-recommends \
curl \ curl \
@@ -17,23 +19,39 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
nginx \ nginx \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# 先复制依赖文件,利用 Docker 构建缓存 # ============================================================
COPY backend/python/requirements.txt /app/backend/python/requirements.txt # 安装 Node.js(用于前端构建)
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
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 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 \ RUN apt-get update && apt-get install -y --no-install-recommends \
nodejs \ nodejs \
npm \ npm \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# ============================================================
# 复制项目源码
# ============================================================
COPY backend/ /app/backend/
COPY frontend/ /app/frontend/ 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 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 test -d /app/frontend/dist/assets || (echo "ERROR: 前端构建失败,assets 目录不存在" && exit 1)
RUN echo "前端构建完成,dist 目录大小: $(du -sh /app/frontend/dist | cut -f1)" RUN echo "前端构建完成,dist 目录大小: $(du -sh /app/frontend/dist | cut -f1)"
# 复制项目源码(排除前端源码,已单独构建) # ============================================================
COPY backend/ /app/backend/ # 配置 nginx:移除默认站点,写入自定义配置
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: 移除默认站点,写入自定义配置,确保日志目录权限
RUN rm -f /etc/nginx/sites-enabled/default && \ RUN rm -f /etc/nginx/sites-enabled/default && \
rm -f /etc/nginx/conf.d/default.conf 2>/dev/null || true; \ rm -f /etc/nginx/conf.d/default.conf 2>/dev/null || true; \
cp /app/nginx.conf /etc/nginx/conf.d/aiflow.conf && \ cp /app/nginx.conf /etc/nginx/conf.d/aiflow.conf && \
+311 -549
View File
@@ -1,369 +1,68 @@
""" """
市场集市扩展 API(管理员) 应用市场审核 API(管理员)
Phase 3.7: Market Expansion - Analytics, Featured, Category Management
""" """
from fastapi import APIRouter, Depends, HTTPException, Query, Body from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session 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 ...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 ...auth import get_current_admin
from ...middleware.log_service import LogService from ...middleware.log_service import LogService
import uuid
from datetime import datetime
router = APIRouter() 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="市场总览数据") @router.get("/apps/pending")
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="获取待审核应用")
async def get_pending_apps( 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), admin_user=Depends(get_current_admin),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""获取待审核应用列表""" """获取待审核应用列表"""
query = db.query(MarketApp).filter(MarketApp.status == "pending") apps = db.query(MarketApp).filter(
total = query.count() MarketApp.status == "pending"
apps = query.order_by(MarketApp.created_at.desc()).offset( ).order_by(MarketApp.created_at.desc()).limit(1000).all()
(page - 1) * page_size
).limit(page_size).all()
return { return [
"code": 0, {
"data": { "id": a.id,
"total": total, "name": a.name,
"page": page, "type": a.type,
"page_size": page_size, "source": a.source,
"items": [_app_to_dict(app, include_content=True) for app in apps], "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( async def approve_app(
app_id: str, id: str,
admin_user=Depends(get_current_admin), admin_user=Depends(get_current_admin),
db: Session = Depends(get_db), 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: if not app:
raise HTTPException(status_code=404, detail="应用不存在") raise HTTPException(status_code=404, detail="应用不存在")
@@ -371,20 +70,26 @@ async def approve_app(
app.updated_at = datetime.now() app.updated_at = datetime.now()
db.commit() 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( async def reject_app(
app_id: str, id: str,
reason: str = Query("", max_length=500),
admin_user=Depends(get_current_admin), admin_user=Depends(get_current_admin),
db: Session = Depends(get_db), 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: if not app:
raise HTTPException(status_code=404, detail="应用不存在") raise HTTPException(status_code=404, detail="应用不存在")
@@ -392,259 +97,316 @@ async def reject_app(
app.updated_at = datetime.now() app.updated_at = datetime.now()
db.commit() 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="编辑应用") @router.get("/apps")
async def update_app( async def get_all_apps(
app_id: str, status: str = Query(None, description="状态筛选: pending/published/removed"),
req: AppEditRequest, app_type: str = Query(None, description="类型筛选: skill/agent/mcp"),
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(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100), page_size: int = Query(20, ge=1, le=100),
admin_user=Depends(get_current_admin), admin_user=Depends(get_current_admin),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""获取优惠券列表""" """获取所有应用列表(管理员)"""
from ...models import Coupon query = db.query(MarketApp)
query = db.query(Coupon)
if status:
query = query.filter(MarketApp.status == status)
if app_type:
query = query.filter(MarketApp.type == app_type)
total = query.count() 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 (page - 1) * page_size
).limit(page_size).all() ).limit(page_size).all()
return { return {
"code": 0, "total": total,
"data": { "page": page,
"total": total, "page_size": page_size,
"page": page, "items": [
"page_size": page_size, {
"items": [{ "id": a.id,
"id": c.id, "name": a.name,
"code": c.code, "type": a.type,
"type": c.type, "source": a.source,
"value": float(c.value) if c.value else 0, "publisher_id": a.publisher_id,
"min_amount": float(c.min_amount) if c.min_amount else 0, "publisher_name": a.publisher_name,
"valid_from": c.valid_from.strftime('%Y-%m-%d %H:%M:%S') if c.valid_from else None, "description": a.description,
"valid_to": c.valid_to.strftime('%Y-%m-%d %H:%M:%S') if c.valid_to else None, "icon": a.icon,
"total_count": c.total_count, "tutorial": a.tutorial,
"used_count": c.used_count, "price": float(a.price) if a.price else 0,
"status": c.status, "is_member_free": a.is_member_free,
} for c in coupons], "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): @router.put("/apps/{id}")
code: str async def admin_edit_app(
type: str # fixed / percent id: str,
value: float name: str = None,
min_amount: float = 0 description: str = None,
valid_from: Optional[str] = None icon: str = None,
valid_to: Optional[str] = None price: float = None,
total_count: int = 100 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( 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), admin_user=Depends(get_current_admin),
db: Session = Depends(get_db), 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( coupon = Coupon(
id=str(uuid.uuid4())[:8], id=str(uuid.uuid4()),
code=req.code, code=code,
type=req.type, type=coupon_type,
value=req.value, value=value,
min_amount=req.min_amount, min_amount=min_amount,
valid_from=datetime.strptime(req.valid_from, '%Y-%m-%d') if req.valid_from else datetime.now(), valid_from=datetime.fromisoformat(valid_from),
valid_to=datetime.strptime(req.valid_to, '%Y-%m-%d') if req.valid_to else None, valid_to=datetime.fromisoformat(valid_to),
total_count=req.total_count, total_count=total_count,
used_count=0, status=status,
status="active",
) )
db.add(coupon) db.add(coupon)
db.commit() 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( async def update_coupon(
coupon_id: str, id: str,
req: CouponCreateRequest, 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), admin_user=Depends(get_current_admin),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""更新优惠券""" """更新优惠券"""
from ...models import Coupon coupon = db.query(Coupon).filter(Coupon.id == id).first()
coupon = db.query(Coupon).filter(Coupon.id == coupon_id).first()
if not coupon: if not coupon:
raise HTTPException(status_code=404, detail="优惠券不存在") raise HTTPException(status_code=404, detail="优惠券不存在")
coupon.code = req.code if code is not None:
coupon.type = req.type # 检查新code是否重复
coupon.value = req.value if code != coupon.code:
coupon.min_amount = req.min_amount dup = db.query(Coupon).filter(Coupon.code == code).first()
coupon.total_count = req.total_count if dup:
if req.valid_from: raise HTTPException(status_code=400, detail="优惠券码已存在")
coupon.valid_from = datetime.strptime(req.valid_from, '%Y-%m-%d') coupon.code = code
if req.valid_to: if value is not None:
coupon.valid_to = datetime.strptime(req.valid_to, '%Y-%m-%d') 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() 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( async def delete_coupon(
coupon_id: str, id: str,
admin_user=Depends(get_current_admin), admin_user=Depends(get_current_admin),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""删除优惠券""" """删除优惠券"""
from ...models import Coupon coupon = db.query(Coupon).filter(Coupon.id == id).first()
coupon = db.query(Coupon).filter(Coupon.id == coupon_id).first()
if not coupon: if not coupon:
raise HTTPException(status_code=404, detail="优惠券不存在") raise HTTPException(status_code=404, detail="优惠券不存在")
db.delete(coupon) db.delete(coupon)
db.commit() 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": "优惠券已发放"}
+7 -4
View File
@@ -68,7 +68,7 @@ async def get_services(
if status: if status:
query = query.filter(ValueAddedService.status == 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 = [] result = []
for s in services: for s in services:
result.append({ result.append({
@@ -244,7 +244,7 @@ async def get_custom_requests(
if status: if status:
query = query.filter(CustomServiceRequest.status == 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 = [] result = []
for r in requests: for r in requests:
# 查询用户名 # 查询用户名
@@ -326,10 +326,13 @@ async def get_custom_orders(
if status: if status:
query = query.filter(CustomServiceOrder.status == 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 = [] result = []
for o in orders: for o in orders:
user = db.query(User).filter(User.id == o.user_id).first() user = _users.get(o.user_id)
result.append({ result.append({
"id": o.id, "id": o.id,
"user_id": o.user_id, "user_id": o.user_id,
+125 -166
View File
@@ -1,166 +1,125 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, Suspense } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom'; import { Routes, Route, Navigate } from 'react-router-dom';
import type { User } from '@/types'; import type { User } from '@/types';
import { AuthContext, AuthContextType } from '@/contexts/AuthContext'; import { AuthContext, AuthContextType } from '@/contexts/AuthContext';
import { authAPI } from '@/api'; import { authAPI } from '@/api';
// Layouts // Layouts(保留为即时加载,构成应用外壳)
import FrontLayout from '@/components/layout/FrontLayout'; import FrontLayout from '@/components/layout/FrontLayout';
import AdminLayout from '@/components/layout/AdminLayout'; import AdminLayout from '@/components/layout/AdminLayout';
// Front Pages // 路由级代码分割:每个页面独立打包成 chunk,首屏只加载外壳与公共依赖,
import LandingPage from '@/pages/front/LandingPage'; // 访问具体页面时才按需加载对应 chunk,显著加快首屏渲染与路由切换速度。
import LoginPage from '@/pages/front/LoginPage'; const LandingPage = React.lazy(() => import('@/pages/front/LandingPage'));
import ChatPage from '@/pages/front/ChatPage'; const LoginPage = React.lazy(() => import('@/pages/front/LoginPage'));
import ChannelPage from '@/pages/front/ChannelPage'; const ChatPage = React.lazy(() => import('@/pages/front/ChatPage'));
import KnowledgePage from '@/pages/front/KnowledgePage'; const ChannelPage = React.lazy(() => import('@/pages/front/ChannelPage'));
import MarketPage from '@/pages/front/MarketPage'; const KnowledgePage = React.lazy(() => import('@/pages/front/KnowledgePage'));
import TokenDashboardPage from '@/pages/front/TokenDashboardPage'; const MarketPage = React.lazy(() => import('@/pages/front/MarketPage'));
import PricingPage from '@/pages/front/PricingPage'; const TokenDashboardPage = React.lazy(() => import('@/pages/front/TokenDashboardPage'));
import ProfilePage from '@/pages/front/ProfilePage'; const PricingPage = React.lazy(() => import('@/pages/front/PricingPage'));
import NotificationPage from '@/pages/front/NotificationPage'; const ProfilePage = React.lazy(() => import('@/pages/front/ProfilePage'));
import AssetsPage from '@/pages/front/AssetsPage'; const NotificationPage = React.lazy(() => import('@/pages/front/NotificationPage'));
import NovelPreviewPage from '@/pages/front/NovelPreviewPage'; const AssetsPage = React.lazy(() => import('@/pages/front/AssetsPage'));
import EvolutionDashboard from '@/pages/front/EvolutionDashboard'; const NovelPreviewPage = React.lazy(() => import('@/pages/front/NovelPreviewPage'));
import WechatBot from '@/pages/front/WechatBot'; const EvolutionDashboard = React.lazy(() => import('@/pages/front/EvolutionDashboard'));
// Admin Pages const AdminDashboard = React.lazy(() => import('@/pages/admin/Dashboard'));
import AdminDashboard from '@/pages/admin/Dashboard'; const AdminUsers = React.lazy(() => import('@/pages/admin/Users'));
import AdminUsers from '@/pages/admin/Users'; const AdminModels = React.lazy(() => import('@/pages/admin/Models'));
import AdminModels from '@/pages/admin/Models'; const AdminSiteSettings = React.lazy(() => import('@/pages/admin/SiteSettings'));
import AdminSiteSettings from '@/pages/admin/SiteSettings'; const AdminPayment = React.lazy(() => import('@/pages/admin/Payment'));
import AdminPayment from '@/pages/admin/Payment'; const AdminServices = React.lazy(() => import('@/pages/admin/Services'));
import AdminServices from '@/pages/admin/Services'; const AdminMembership = React.lazy(() => import('@/pages/admin/Membership'));
import AdminMembership from '@/pages/admin/Membership'; const AdminAds = React.lazy(() => import('@/pages/admin/Ads'));
import AdminAds from '@/pages/admin/Ads'; const AdminMarket = React.lazy(() => import('@/pages/admin/Market'));
import AdminMarket from '@/pages/admin/Market'; const AdminLogs = React.lazy(() => import('@/pages/admin/Logs'));
import AdminLogs from '@/pages/admin/Logs';
import AdminSkills from '@/pages/admin/Skills' const pageFallback = <div className="loading-screen">加载中...</div>;
import AdminAgents from '@/pages/admin/Agents';
import AdminCodeExec from '@/pages/admin/CodeExec';; const App: React.FC = () => {
import DistributionBuilder from '@/pages/admin/DistributionBuilder'; const [user, setUser] = useState<User | null>(null);
import PrivacySettings from '@/pages/admin/PrivacySettings'; const [loading, setLoading] = useState(true);
import ImBots from '@/pages/admin/ImBots';
import Promotion from '@/pages/admin/Promotion'; // 初始化:从localStorage恢复登录状态
import Achievement from '@/pages/admin/Achievement'; useEffect(() => {
import Tenant from '@/pages/admin/Tenant'; const token = localStorage.getItem('aiflow_token');
import Billing from '@/pages/admin/Billing'; const cachedUser = localStorage.getItem('aiflow_user');
import Analytics from '@/pages/admin/Analytics'; if (token && cachedUser) {
import UserPortal from '@/pages/UserPortal'; try {
import AdminKnowledge from '@/pages/admin/Knowledge'; setUser(JSON.parse(cachedUser));
import AdminWorkflow from '@/pages/admin/Workflow'; } catch {
import AdminStorage from '@/pages/admin/Storage'; localStorage.removeItem('aiflow_token');
import AdminDeveloper from '@/pages/admin/Developer'; localStorage.removeItem('aiflow_user');
import AdminDeveloperTools from '@/pages/admin/DeveloperTools'; }
import AdminAutomation from '@/pages/admin/Automation'; }
import AdminMcp from '@/pages/admin/Mcp'; setLoading(false);
import AdminApps from '@/pages/admin/Apps'; }, []);
import AssetCenter from '@/pages/admin/AssetCenter';
const login = (u: User) => {
const App: React.FC = () => { setUser(u);
const [user, setUser] = useState<User | null>(null); localStorage.setItem('aiflow_user', JSON.stringify(u));
const [loading, setLoading] = useState(true); };
const logout = () => {
// 初始化:从localStorage恢复登录状态 setUser(null);
useEffect(() => { localStorage.removeItem('aiflow_token');
const token = localStorage.getItem('aiflow_token'); localStorage.removeItem('aiflow_user');
const cachedUser = localStorage.getItem('aiflow_user'); };
if (token && cachedUser) { const isAdmin = user?.role === 'admin';
try {
setUser(JSON.parse(cachedUser)); if (loading) {
} catch { return <div className="loading-screen">加载中...</div>;
localStorage.removeItem('aiflow_token'); }
localStorage.removeItem('aiflow_user');
} return (
} <AuthContext.Provider value={{ user, login, logout, isAdmin }}>
setLoading(false); <Suspense fallback={pageFallback}>
}, []); <Routes>
{/* Login */}
const login = (u: User) => { <Route path="/login" element={
setUser(u); user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LoginPage />
localStorage.setItem('aiflow_user', JSON.stringify(u)); } />
};
const logout = () => { {/* Root - Landing for guests, redirect for authenticated users */}
setUser(null); <Route path="/" element={
localStorage.removeItem('aiflow_token'); user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LandingPage />
localStorage.removeItem('aiflow_user'); } />
};
const isAdmin = user?.role === 'admin'; {/* Front Routes (authenticated only) */}
<Route path="/" element={<FrontLayout />}>
if (loading) { <Route path="chat" element={<ChatPage />} />
return <div className="loading-screen">加载中...</div>; <Route path="chat/:id" element={<ChatPage />} />
} <Route path="channels" element={<ChannelPage />} />
<Route path="knowledge" element={<KnowledgePage />} />
return ( <Route path="market" element={<MarketPage />} />
<AuthContext.Provider value={{ user, login, logout, isAdmin }}> <Route path="token" element={<TokenDashboardPage />} />
<Routes> <Route path="pricing" element={<PricingPage />} />
{/* Login */} <Route path="profile" element={<ProfilePage />} />
<Route path="/login" element={ <Route path="notifications" element={<NotificationPage />} />
user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LoginPage /> <Route path="assets" element={<AssetsPage />} />
} /> <Route path="assets/novel/:id" element={<NovelPreviewPage />} />
<Route path="evolution" element={<EvolutionDashboard />} />
{/* Root - Landing for guests, redirect for authenticated users */} </Route>
<Route path="/" element={
user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LandingPage /> {/* Admin Routes */}
} /> <Route path="/admin" element={<AdminLayout />}>
<Route index element={<AdminDashboard />} />
{/* Front Routes (authenticated only) */} <Route path="users" element={<AdminUsers />} />
<Route path="/" element={<FrontLayout />}> <Route path="models" element={<AdminModels />} />
<Route path="chat" element={<ChatPage />} /> <Route path="settings" element={<AdminSiteSettings />} />
<Route path="chat/:id" element={<ChatPage />} /> <Route path="payment" element={<AdminPayment />} />
<Route path="channels" element={<ChannelPage />} /> <Route path="services" element={<AdminServices />} />
<Route path="knowledge" element={<KnowledgePage />} /> <Route path="membership" element={<AdminMembership />} />
<Route path="market" element={<MarketPage />} /> <Route path="ads" element={<AdminAds />} />
<Route path="token" element={<TokenDashboardPage />} /> <Route path="market" element={<AdminMarket />} />
<Route path="pricing" element={<PricingPage />} /> <Route path="logs" element={<AdminLogs />} />
<Route path="profile" element={<ProfilePage />} /> </Route>
<Route path="notifications" element={<NotificationPage />} />
<Route path="assets" element={<AssetsPage />} /> <Route path="*" element={<Navigate to="/" />} />
<Route path="assets/novel/:id" element={<NovelPreviewPage />} /> </Routes>
<Route path="evolution" element={<EvolutionDashboard />} /> </Suspense>
<Route path="wechat-bot" element={<WechatBot />} /> </AuthContext.Provider>
</Route> );
};
{/* Admin Routes */}
<Route path="/admin" element={<AdminLayout />}> export default App;
<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;
+20
View File
@@ -13,6 +13,26 @@ export default defineConfig({
build: { build: {
outDir: 'dist', outDir: 'dist',
emptyOutDir: true, 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: { server: {
port: 5173, port: 5173,
+15 -16
View File
@@ -15,6 +15,12 @@ else
fi fi
cd "$PROJECT_DIR" 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 后端目录(提前定义,供后续步骤使用)
PYTHON_DIR="$PROJECT_DIR/backend/python" PYTHON_DIR="$PROJECT_DIR/backend/python"
@@ -167,19 +173,11 @@ echo " 使用数据库: $MYSQL_DATABASE"
MIGRATION_DIR="$PROJECT_DIR/migrations" MIGRATION_DIR="$PROJECT_DIR/migrations"
if [ -d "$MIGRATION_DIR" ]; then 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 for sql_file in $(ls "$MIGRATION_DIR"/*.sql 2>/dev/null | sort); do
filename=$(basename "$sql_file") filename=$(basename "$sql_file")
version="${filename%.sql}" echo " - 执行: $filename"
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) mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" $MYSQL_SSL_FLAG \
if [ "$already" = "1" ]; then "$MYSQL_DATABASE" < "$sql_file" 2>&1 | sed 's/^/ /' || echo " [警告] $filename 部分语句已跳过(可能已存在)"
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
done done
echo " 数据库初始化完成" echo " 数据库初始化完成"
@@ -299,11 +297,6 @@ fi
# 如果 nginx 可用,则启动(Docker 环境) # 如果 nginx 可用,则启动(Docker 环境)
if command -v nginx &>/dev/null; then 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 ""
echo " Nginx 配置测试..." echo " Nginx 配置测试..."
if nginx -t 2>&1; then if nginx -t 2>&1; then
@@ -333,5 +326,11 @@ echo " 默认账号: admin / admin123"
echo "========================================" echo "========================================"
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" cd "$PYTHON_DIR"
exec python -m uvicorn main:app --host 0.0.0.0 --port 18080 --log-level info exec python -m uvicorn main:app --host 0.0.0.0 --port 18080 --log-level info