feat: 拆分新旧前端访问端口

This commit is contained in:
2026-05-15 13:54:17 +08:00
parent 279e5d7364
commit f8ed9199b0
9 changed files with 137 additions and 47 deletions
+2 -1
View File
@@ -71,7 +71,8 @@ cd ..
访问地址: 访问地址:
```text ```text
http://localhost:9081 新版前端:http://localhost:9081
旧版前端:http://localhost:9082
``` ```
开发前端时可运行: 开发前端时可运行:
+98 -25
View File
@@ -1,10 +1,11 @@
from pathlib import Path from pathlib import Path
import socket
from typing import Literal
import uvicorn import uvicorn
from fastapi import FastAPI, HTTPException, Request from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from app import state from app import state
from app.api.routers import auth, cache, config, database, health, history, script, service, tasks, upload 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_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_DIST_DIR = BASE_DIR / "frontend" / "dist"
FRONTEND_ASSETS_DIR = FRONTEND_DIST_DIR / "assets"
FRONTEND_OLD_DIR = BASE_DIR / "frontend_old" 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( app = FastAPI(
title="CapacityReport", title="CapacityReport",
description="容量报表数据处理系统", description="容量报表数据处理系统",
@@ -38,7 +42,7 @@ def create_app() -> FastAPI:
app.middleware("http")(jwt_middleware) app.middleware("http")(jwt_middleware)
register_routes(app) register_routes(app)
register_frontend(app) register_frontend(app, frontend_mode)
return app return app
@@ -73,35 +77,54 @@ def register_routes(app: FastAPI) -> None:
app.include_router(router) 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) @app.get("/old", include_in_schema=False)
async def serve_old_frontend(): async def serve_old_frontend(request: Request):
index_file = FRONTEND_OLD_DIR / "index.html" if not _should_serve_old_frontend(request, frontend_mode):
if index_file.exists(): raise HTTPException(status_code=404, detail=f"旧版前端请访问 {OLD_FRONTEND_PORT} 端口")
return FileResponse(str(index_file)) return _serve_old_index()
return JSONResponse(status_code=503, content={"detail": "旧版前端入口文件不存在"})
if FRONTEND_OLD_DIR.exists(): @app.get("/old/{path:path}", include_in_schema=False)
app.mount("/old", StaticFiles(directory=str(FRONTEND_OLD_DIR), html=True), name="frontend_old") async def serve_old_frontend_file(request: Request, path: str):
if not _should_serve_old_frontend(request, frontend_mode):
if FRONTEND_ASSETS_DIR.exists(): raise HTTPException(status_code=404, detail=f"旧版前端请访问 {OLD_FRONTEND_PORT} 端口")
app.mount("/assets", StaticFiles(directory=str(FRONTEND_ASSETS_DIR)), name="assets") return _serve_old_file(path)
@app.get("/", include_in_schema=False) @app.get("/", include_in_schema=False)
@app.get("/{path:path}", 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/"): if path.startswith("api/"):
raise HTTPException(status_code=404, detail="接口不存在") raise HTTPException(status_code=404, detail="接口不存在")
if _should_serve_old_frontend(request, frontend_mode):
return _serve_old_frontend_path(path)
return _serve_new_frontend_path(path)
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(): if not FRONTEND_DIST_DIR.exists():
return JSONResponse( return JSONResponse(
status_code=503, status_code=503,
content={ content={"detail": "前端构建产物不存在,请先执行 cd frontend && npm install && npm run build"},
"detail": "前端构建产物不存在,请先执行 cd frontend && npm install && npm run build"
},
) )
requested_file = _safe_frontend_file(path) requested_file = _safe_file(FRONTEND_DIST_DIR, path)
if requested_file and requested_file.exists() and requested_file.is_file(): if requested_file and requested_file.exists() and requested_file.is_file():
return FileResponse(str(requested_file)) return FileResponse(str(requested_file))
@@ -112,22 +135,72 @@ def register_frontend(app: FastAPI) -> None:
return JSONResponse(status_code=503, content={"detail": "前端入口文件不存在"}) return JSONResponse(status_code=503, content={"detail": "前端入口文件不存在"})
def _safe_frontend_file(path: str) -> Path | None: 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: if not path:
return None return None
requested = (FRONTEND_DIST_DIR / path).resolve() root = root.resolve()
requested = (root / path).resolve()
try: try:
requested.relative_to(FRONTEND_DIST_DIR.resolve()) requested.relative_to(root)
except ValueError: except ValueError:
return None return None
return requested 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__": if __name__ == "__main__":
print(f"CapacityReport v{APP_VERSION}") print(f"CapacityReport v{APP_VERSION}")
print(f"配置更新时间: {state.config.update}") 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()
+1 -1
View File
@@ -35,7 +35,7 @@ COPY --from=frontend-build /frontend/dist /app/frontend/dist
RUN chmod +x /app/start.sh 2>/dev/null || true 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 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
CMD curl -f http://localhost:9081/health || exit 1 CMD curl -f http://localhost:9081/health || exit 1
+1 -1
View File
@@ -55,4 +55,4 @@ sh update.sh
- 默认镜像版本 - 默认镜像版本
- 版本要求范围 - 版本要求范围
- 数据库账号密码 - 数据库账号密码
- 端口映射 - 端口映射:新版前端 `19081 -> 9081`,旧版前端 `19082 -> 9082`
+12 -5
View File
@@ -36,8 +36,12 @@ MYSQL_HOST = "capacity-mysql"
MYSQL_PORT = 3306 MYSQL_PORT = 3306
# 应用端口映射 # 应用端口映射
APP_PORT_HOST = 19081 APP_NEW_PORT_HOST = 19081
APP_PORT_CONTAINER = 9081 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_HOST = 13306
MYSQL_PORT_CONTAINER = 3306 MYSQL_PORT_CONTAINER = 3306
@@ -294,7 +298,8 @@ check_port() {{
fi 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" check_port {MYSQL_PORT_HOST} "MySQL"
echo "" echo ""
@@ -441,7 +446,8 @@ echo "=========================================="
echo "Deployment completed!" echo "Deployment completed!"
echo "==========================================" echo "=========================================="
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 ""
echo "Commands:" echo "Commands:"
echo " Logs: $DOCKER_COMPOSE_CMD logs -f" echo " Logs: $DOCKER_COMPOSE_CMD logs -f"
@@ -637,7 +643,8 @@ echo "=========================================="
echo "Update completed!" echo "Update completed!"
echo "==========================================" echo "=========================================="
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 ""
echo "Database status:" echo "Database status:"
if docker ps --format "{{.Names}}" | grep -q "capacity-mysql"; then if docker ps --format "{{.Names}}" | grep -q "capacity-mysql"; then
+1
View File
@@ -16,6 +16,7 @@ services:
restart: unless-stopped restart: unless-stopped
ports: ports:
- "19081:9081" - "19081:9081"
- "19082:9082"
volumes: volumes:
# 持久化数据 # 持久化数据
- ./cache:/app/cache - ./cache:/app/cache
+7
View File
@@ -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:增加旧版前端对照入口和新版路由 ## 2026-05-15:增加旧版前端对照入口和新版路由
- 从旧提交 `54773f547f6fcb853d73785f05ff5ac39ab2e5f5` 恢复原生 HTML/CSS/JS 前端到 `frontend_old/`,包含旧版 `index.html`、`login.html`、样式、脚本和本地 Monaco 资源。 - 从旧提交 `54773f547f6fcb853d73785f05ff5ac39ab2e5f5` 恢复原生 HTML/CSS/JS 前端到 `frontend_old/`,包含旧版 `index.html`、`login.html`、样式、脚本和本地 Monaco 资源。
+2 -1
View File
@@ -40,7 +40,8 @@ if not exist "frontend\dist\index.html" (
:loop :loop
echo. echo.
echo [%date% %time%] Starting service... 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 [INFO] Close this window to stop the service.
echo. echo.
+1 -1
View File
@@ -16,7 +16,7 @@ supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
serverurl=unix:///var/run/supervisor.sock serverurl=unix:///var/run/supervisor.sock
[program:fastapi] [program:fastapi]
command=uvicorn app.main:app --host 0.0.0.0 --port 9081 --workers 1 command=python -m app.main
directory=/app directory=/app
autostart=true autostart=true
autorestart=true autorestart=true