diff --git a/README.md b/README.md index 5e8cb8e..5d389c4 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,8 @@ cd .. 访问地址: ```text -http://localhost:9081 +新版前端:http://localhost:9081 +旧版前端:http://localhost:9082 ``` 开发前端时可运行: diff --git a/app/main.py b/app/main.py index ad4397d..5de7a6d 100644 --- a/app/main.py +++ b/app/main.py @@ -1,10 +1,11 @@ from pathlib import Path +import socket +from typing import Literal import uvicorn from fastapi import FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, JSONResponse -from fastapi.staticfiles import StaticFiles from app import state from app.api.routers import auth, cache, config, database, health, history, script, service, tasks, upload @@ -13,12 +14,15 @@ from app.config import BASE_DIR APP_VERSION = "2.0.2" +APP_HOST = "0.0.0.0" +NEW_FRONTEND_PORT = 9081 +OLD_FRONTEND_PORT = 9082 FRONTEND_DIST_DIR = BASE_DIR / "frontend" / "dist" -FRONTEND_ASSETS_DIR = FRONTEND_DIST_DIR / "assets" FRONTEND_OLD_DIR = BASE_DIR / "frontend_old" +FrontendMode = Literal["new", "old", "split"] -def create_app() -> FastAPI: +def create_app(frontend_mode: FrontendMode = "new") -> FastAPI: app = FastAPI( title="CapacityReport", description="容量报表数据处理系统", @@ -38,7 +42,7 @@ def create_app() -> FastAPI: app.middleware("http")(jwt_middleware) register_routes(app) - register_frontend(app) + register_frontend(app, frontend_mode) return app @@ -73,61 +77,130 @@ def register_routes(app: FastAPI) -> None: app.include_router(router) -def register_frontend(app: FastAPI) -> None: +def register_frontend(app: FastAPI, frontend_mode: FrontendMode) -> None: @app.get("/old", include_in_schema=False) - async def serve_old_frontend(): - index_file = FRONTEND_OLD_DIR / "index.html" - if index_file.exists(): - return FileResponse(str(index_file)) - return JSONResponse(status_code=503, content={"detail": "旧版前端入口文件不存在"}) + async def serve_old_frontend(request: Request): + if not _should_serve_old_frontend(request, frontend_mode): + raise HTTPException(status_code=404, detail=f"旧版前端请访问 {OLD_FRONTEND_PORT} 端口") + return _serve_old_index() - if FRONTEND_OLD_DIR.exists(): - app.mount("/old", StaticFiles(directory=str(FRONTEND_OLD_DIR), html=True), name="frontend_old") - - if FRONTEND_ASSETS_DIR.exists(): - app.mount("/assets", StaticFiles(directory=str(FRONTEND_ASSETS_DIR)), name="assets") + @app.get("/old/{path:path}", include_in_schema=False) + async def serve_old_frontend_file(request: Request, path: str): + if not _should_serve_old_frontend(request, frontend_mode): + raise HTTPException(status_code=404, detail=f"旧版前端请访问 {OLD_FRONTEND_PORT} 端口") + return _serve_old_file(path) @app.get("/", include_in_schema=False) @app.get("/{path:path}", include_in_schema=False) - async def serve_spa(path: str = ""): + async def serve_frontend(request: Request, path: str = ""): if path.startswith("api/"): raise HTTPException(status_code=404, detail="接口不存在") - if not FRONTEND_DIST_DIR.exists(): - return JSONResponse( - status_code=503, - content={ - "detail": "前端构建产物不存在,请先执行 cd frontend && npm install && npm run build" - }, - ) + if _should_serve_old_frontend(request, frontend_mode): + return _serve_old_frontend_path(path) - requested_file = _safe_frontend_file(path) - if requested_file and requested_file.exists() and requested_file.is_file(): - return FileResponse(str(requested_file)) - - index_file = FRONTEND_DIST_DIR / "index.html" - if index_file.exists(): - return FileResponse(str(index_file)) - - return JSONResponse(status_code=503, content={"detail": "前端入口文件不存在"}) + return _serve_new_frontend_path(path) -def _safe_frontend_file(path: str) -> Path | None: +def _should_serve_old_frontend(request: Request, frontend_mode: FrontendMode) -> bool: + if frontend_mode == "old": + return True + if frontend_mode == "new": + return False + return _request_port(request) == OLD_FRONTEND_PORT + + +def _request_port(request: Request) -> int | None: + server = request.scope.get("server") + if isinstance(server, tuple) and len(server) >= 2: + return server[1] + return request.url.port + + +def _serve_new_frontend_path(path: str): + if not FRONTEND_DIST_DIR.exists(): + return JSONResponse( + status_code=503, + content={"detail": "前端构建产物不存在,请先执行 cd frontend && npm install && npm run build"}, + ) + + requested_file = _safe_file(FRONTEND_DIST_DIR, path) + if requested_file and requested_file.exists() and requested_file.is_file(): + return FileResponse(str(requested_file)) + + index_file = FRONTEND_DIST_DIR / "index.html" + if index_file.exists(): + return FileResponse(str(index_file)) + + return JSONResponse(status_code=503, content={"detail": "前端入口文件不存在"}) + + +def _serve_old_frontend_path(path: str): + if not path: + return _serve_old_index() + + requested_file = _safe_file(FRONTEND_OLD_DIR, path) + if requested_file and requested_file.exists() and requested_file.is_file(): + return FileResponse(str(requested_file)) + + return _serve_old_index() + + +def _serve_old_file(path: str): + requested_file = _safe_file(FRONTEND_OLD_DIR, path) + if requested_file and requested_file.exists() and requested_file.is_file(): + return FileResponse(str(requested_file)) + raise HTTPException(status_code=404, detail="旧版前端文件不存在") + + +def _serve_old_index(): + if not FRONTEND_OLD_DIR.exists(): + return JSONResponse(status_code=503, content={"detail": "旧版前端目录不存在"}) + + index_file = FRONTEND_OLD_DIR / "index.html" + if index_file.exists(): + return FileResponse(str(index_file)) + + return JSONResponse(status_code=503, content={"detail": "旧版前端入口文件不存在"}) + + +def _safe_file(root: Path, path: str) -> Path | None: if not path: return None - requested = (FRONTEND_DIST_DIR / path).resolve() + root = root.resolve() + requested = (root / path).resolve() try: - requested.relative_to(FRONTEND_DIST_DIR.resolve()) + requested.relative_to(root) except ValueError: return None return requested -app = create_app() +def _create_listen_socket(port: int) -> socket.socket: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind((APP_HOST, port)) + sock.listen(2048) + sock.set_inheritable(True) + return sock + + +def run_split_frontend_servers() -> None: + sockets = [_create_listen_socket(NEW_FRONTEND_PORT), _create_listen_socket(OLD_FRONTEND_PORT)] + config = uvicorn.Config(split_app, host=APP_HOST, port=NEW_FRONTEND_PORT, reload=False) + server = uvicorn.Server(config) + server.run(sockets=sockets) + + +app = create_app("new") +old_app = create_app("old") +split_app = create_app("split") if __name__ == "__main__": print(f"CapacityReport v{APP_VERSION}") print(f"配置更新时间: {state.config.update}") - uvicorn.run("app.main:app", host="0.0.0.0", port=9081, reload=False) + print(f"新版前端: http://localhost:{NEW_FRONTEND_PORT}") + print(f"旧版前端: http://localhost:{OLD_FRONTEND_PORT}") + run_split_frontend_servers() diff --git a/build/Dockerfile b/build/Dockerfile index 26612a8..bd98447 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -35,7 +35,7 @@ COPY --from=frontend-build /frontend/dist /app/frontend/dist RUN chmod +x /app/start.sh 2>/dev/null || true -EXPOSE 9081 +EXPOSE 9081 9082 HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ CMD curl -f http://localhost:9081/health || exit 1 diff --git a/build/README.md b/build/README.md index cba84e0..0322f89 100644 --- a/build/README.md +++ b/build/README.md @@ -55,4 +55,4 @@ sh update.sh - 默认镜像版本 - 版本要求范围 - 数据库账号密码 -- 端口映射 +- 端口映射:新版前端 `19081 -> 9081`,旧版前端 `19082 -> 9082` diff --git a/build/build.py b/build/build.py index d68cc70..fad5181 100644 --- a/build/build.py +++ b/build/build.py @@ -36,8 +36,12 @@ MYSQL_HOST = "capacity-mysql" MYSQL_PORT = 3306 # 应用端口映射 -APP_PORT_HOST = 19081 -APP_PORT_CONTAINER = 9081 +APP_NEW_PORT_HOST = 19081 +APP_NEW_PORT_CONTAINER = 9081 +APP_OLD_PORT_HOST = 19082 +APP_OLD_PORT_CONTAINER = 9082 +APP_PORT_HOST = APP_NEW_PORT_HOST +APP_PORT_CONTAINER = APP_NEW_PORT_CONTAINER MYSQL_PORT_HOST = 13306 MYSQL_PORT_CONTAINER = 3306 @@ -294,7 +298,8 @@ check_port() {{ fi {{}} -check_port {APP_PORT_HOST} "App" +check_port {APP_NEW_PORT_HOST} "New frontend" +check_port {APP_OLD_PORT_HOST} "Old frontend" check_port {MYSQL_PORT_HOST} "MySQL" echo "" @@ -441,7 +446,8 @@ echo "==========================================" echo "Deployment completed!" echo "==========================================" echo "" -echo "Access: http://localhost:{APP_PORT_HOST}" +echo "New frontend: http://localhost:{APP_NEW_PORT_HOST}" +echo "Old frontend: http://localhost:{APP_OLD_PORT_HOST}" echo "" echo "Commands:" echo " Logs: $DOCKER_COMPOSE_CMD logs -f" @@ -637,7 +643,8 @@ echo "==========================================" echo "Update completed!" echo "==========================================" echo "" -echo "Application: http://localhost:{APP_PORT_HOST}" +echo "New frontend: http://localhost:{APP_NEW_PORT_HOST}" +echo "Old frontend: http://localhost:{APP_OLD_PORT_HOST}" echo "" echo "Database status:" if docker ps --format "{{.Names}}" | grep -q "capacity-mysql"; then diff --git a/build/docker-compose.yml b/build/docker-compose.yml index 3421433..7b5fb53 100644 --- a/build/docker-compose.yml +++ b/build/docker-compose.yml @@ -16,6 +16,7 @@ services: restart: unless-stopped ports: - "19081:9081" + - "19082:9082" volumes: # 持久化数据 - ./cache:/app/cache diff --git a/docs/project_context.md b/docs/project_context.md index 415f7c7..adc0a06 100644 --- a/docs/project_context.md +++ b/docs/project_context.md @@ -1,5 +1,12 @@ # 项目上下文记录 +## 2026-05-15:拆分新旧前端访问端口 + +- `python -m app.main` 现在由同一个 FastAPI 进程同时监听 `9081` 和 `9082`,共享后端运行状态、任务锁和 API。 +- `9081` 固定服务新版 Vue 3 前端,`9082` 固定服务 `frontend_old/` 旧版 HTML/CSS/JS 前端;旧版页面仍通过 `/old/...` 加载本地 Monaco 等静态资源。 +- `app.main:app` 保留为新版单端口 ASGI 实例,`app.main:old_app` 保留为旧版单端口 ASGI 实例,`app.main:split_app` 用于按请求端口切换前端。 +- `run.bat`、`supervisord.conf`、Dockerfile、Docker Compose 和离线构建脚本已同步新旧端口:本地为 `9081/9082`,容器宿主机映射为 `19081/19082`。 + ## 2026-05-15:增加旧版前端对照入口和新版路由 - 从旧提交 `54773f547f6fcb853d73785f05ff5ac39ab2e5f5` 恢复原生 HTML/CSS/JS 前端到 `frontend_old/`,包含旧版 `index.html`、`login.html`、样式、脚本和本地 Monaco 资源。 diff --git a/run.bat b/run.bat index dc93646..7969486 100644 --- a/run.bat +++ b/run.bat @@ -40,7 +40,8 @@ if not exist "frontend\dist\index.html" ( :loop echo. echo [%date% %time%] Starting service... -echo [INFO] Service URL: http://localhost:9081 +echo [INFO] New frontend: http://localhost:9081 +echo [INFO] Old frontend: http://localhost:9082 echo [INFO] Close this window to stop the service. echo. diff --git a/supervisord.conf b/supervisord.conf index 0921bab..40f9fea 100644 --- a/supervisord.conf +++ b/supervisord.conf @@ -16,7 +16,7 @@ supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface serverurl=unix:///var/run/supervisor.sock [program:fastapi] -command=uvicorn app.main:app --host 0.0.0.0 --port 9081 --workers 1 +command=python -m app.main directory=/app autostart=true autorestart=true