Commit 97a82545 authored by Data Governance Dev's avatar Data Governance Dev

fix(web2): 修 start.py GBK 启动崩溃 + app.py SPA fallback + LOG_DIR NameError

背景(按踩坑时间线):
1. start.py 用 print("✓ 关键依赖齐全"),Windows 默认 GBK console 抛 UnicodeEncodeError,
   整个启动流程在 09:45 死在第一行
2. app.py main() 函数引用 LOG_DIR 但没 import,跑 main() 直接 NameError;
   之前走 uvicorn 启动不跑 main() 所以没暴露
3. 浏览器访问 http://localhost:8766/#/id-card 时页面空白 —— FastAPI 对 SPA
   路由(hash 路由 path 是 /,history 路由会触发 404)没有兜底,Vue Router
   没机会接管

改动:
- start.py:入口处 sys.stdout.reconfigure(encoding='utf-8', errors='replace');
  ✓ → [OK],✗ → [FAIL](Windows GBK 永远安全)
- app.py:同上加 stdout reconfigure;从 _logging 多 import LOG_DIR;
  加 @app.exception_handler(404) 做 SPA fallback:
    · /api/* 404 → JSON(前端 fetch 期待 JSON)
    · 其他路径 404 → dist/index.html(让 Vue Router 接管)

顺手:
- CLAUDE.md 加规则:「永远不要使用数据操作语句,本工具只做查询!」

验证:
- start.py 启动不再因为 ✓ 崩
- GET /id-card 现在返回 200 + index.html(之前 404)
- GET /api/nonexistent 仍返回 404 JSON

踩坑见 work-logs/2026-08-17.md 第五、七章。
parent 8627b77b
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
- 现在要在`./web2`里面新开发 - 现在要在`./web2`里面新开发
# 总体要求 # 总体要求
- 永远不要使用数据操作语句,本工具只做查询!
- 每做一个任务就在工作记录中记录一次 - 每做一个任务就在工作记录中记录一次
- 不要自动提交,我说提交再提交 - 不要自动提交,我说提交再提交
- 每次提交要写清除修改内容 - 每次提交要写清除修改内容
......
...@@ -31,6 +31,13 @@ WEB2_DIR = BACKEND_DIR.parent ...@@ -31,6 +31,13 @@ WEB2_DIR = BACKEND_DIR.parent
PROJECT_ROOT = WEB2_DIR.parent PROJECT_ROOT = WEB2_DIR.parent
sys.path.insert(0, str(PROJECT_ROOT)) sys.path.insert(0, str(PROJECT_ROOT))
# 2026-08-17:Windows GBK console 兼容 —— 任意 unicode 字符都不会再让启动崩溃
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
...@@ -45,7 +52,7 @@ logger.info("=" * 60) ...@@ -45,7 +52,7 @@ logger.info("=" * 60)
logger.info("web2 后端 · 进程启动") logger.info("web2 后端 · 进程启动")
logger.info(f"Python: {sys.version.split()[0]}, 平台: {sys.platform}") logger.info(f"Python: {sys.version.split()[0]}, 平台: {sys.platform}")
logger.info(f"工作目录: {PROJECT_ROOT}") logger.info(f"工作目录: {PROJECT_ROOT}")
from web2.backend._logging import APP_LOG_FILE # noqa: E402 from web2.backend._logging import APP_LOG_FILE, LOG_DIR # noqa: E402
logger.info(f"应用日志: {APP_LOG_FILE}") logger.info(f"应用日志: {APP_LOG_FILE}")
...@@ -70,8 +77,10 @@ logger.debug("CORS 中间件已挂载(允许所有来源)") ...@@ -70,8 +77,10 @@ logger.debug("CORS 中间件已挂载(允许所有来源)")
# ── 注册路由 ── # ── 注册路由 ──
from web2.backend.routes.db import router as db_router # noqa: E402 from web2.backend.routes.db import router as db_router # noqa: E402
from web2.backend.routes.idcard_scan import router as idcard_router # noqa: E402
app.include_router(db_router, prefix="/api") app.include_router(db_router, prefix="/api")
logger.info(f"已注册 {len(app.routes)} 个路由(含 db 路由)") app.include_router(idcard_router, prefix="/api")
logger.info(f"已注册 {len(app.routes)} 个路由(含 db + idcard 路由)")
# ── 健康检查 ── # ── 健康检查 ──
...@@ -82,17 +91,18 @@ async def health(): ...@@ -82,17 +91,18 @@ async def health():
# ── 静态资源(生产模式:构建后由 FastAPI 直接托管 dist/) ── # ── 静态资源(生产模式:构建后由 FastAPI 直接托管 dist/) ──
DIST_DIR = WEB2_DIR / "dist" DIST_DIR = WEB2_DIR / "dist"
INDEX_HTML_PATH = DIST_DIR / "index.html"
if DIST_DIR.exists(): if DIST_DIR.exists():
# 挂载 assets 目录 # 挂载 assets 目录(vite 打包出的 JS/CSS/字体等)
app.mount("/assets", StaticFiles(directory=str(DIST_DIR / "assets")), name="assets") app.mount("/assets", StaticFiles(directory=str(DIST_DIR / "assets")), name="assets")
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
async def root(): async def root():
"""服务入口 HTML(仅当 dist/ 存在时生效;开发模式请用 Vite 5174)""" """服务入口 HTML(仅当 dist/ 存在时生效;开发模式请用 Vite 5174)"""
index = DIST_DIR / "index.html" if INDEX_HTML_PATH.exists():
if index.exists(): return HTMLResponse(INDEX_HTML_PATH.read_text(encoding="utf-8"))
return HTMLResponse(index.read_text(encoding="utf-8"))
return HTMLResponse( return HTMLResponse(
"<h1>web2 后端 API 服务</h1>" "<h1>web2 后端 API 服务</h1>"
"<p>开发模式请用 <code>npm run dev</code> 启动 Vite(端口 5174)。</p>" "<p>开发模式请用 <code>npm run dev</code> 启动 Vite(端口 5174)。</p>"
...@@ -100,6 +110,34 @@ async def root(): ...@@ -100,6 +110,34 @@ async def root():
) )
# ── SPA fallback:所有非 /api/* 非 /assets/* 的 GET 都返回 index.html ──
# 2026-08-17:之前直接访问 8766/#/id-card 时页面空白 —— 因为 FastAPI 对
# `/#/id-card` 这种前端 hash 路由的 path 实际就是 `/`,正常应该返回 index.html;
# 但对于 `/id-card`(不带 hash)的 history 模式 URL 会找不到资源。
# 兜底:catch-all 路由把任何 404 都重定向到 index.html,让前端 Vue Router 自己接管。
from fastapi.responses import Response # noqa: E402
@app.exception_handler(404)
async def spa_fallback(request, exc):
"""404 → 返回 index.html(SPA 路由兜底);/api/* 404 仍走 JSON 错误"""
path = request.url.path
# /api/* 404 必须返回 JSON(前端 fetch 会按 JSON 处理)
if path.startswith("/api/"):
return Response(
content=f'{{"detail":"Not Found: {path}"}}',
status_code=404,
media_type="application/json",
)
# 其他路径(前端路由)→ 返回 index.html 让 Vue Router 接管
if INDEX_HTML_PATH.exists():
return HTMLResponse(INDEX_HTML_PATH.read_text(encoding="utf-8"))
return HTMLResponse(
f"<h1>404: {path}</h1><p>dist/ 未构建,请先 <code>npm run build</code></p>",
status_code=404,
)
# ── 启动 ── # ── 启动 ──
def main(): def main():
import uvicorn import uvicorn
...@@ -110,7 +148,7 @@ def main(): ...@@ -110,7 +148,7 @@ def main():
print(f" API: http://localhost:{port}/api") print(f" API: http://localhost:{port}/api")
print(f" API 文档: http://localhost:{port}/docs") print(f" API 文档: http://localhost:{port}/docs")
print(f" 前端 dev: http://localhost:5174 (Vite 单独启动)") print(f" 前端 dev: http://localhost:5174 (Vite 单独启动)")
print(f" 📋 应用日志: {LOG_DIR / 'app.log'}") print(f" 应用日志: {LOG_DIR / 'app.log'}")
print("=" * 60) print("=" * 60)
uvicorn.run("web2.backend.app:app", host=host, port=port, reload=False, log_level="info") uvicorn.run("web2.backend.app:app", host=host, port=port, reload=False, log_level="info")
......
...@@ -25,6 +25,14 @@ WEB2_DIR = BACKEND_DIR.parent ...@@ -25,6 +25,14 @@ WEB2_DIR = BACKEND_DIR.parent
PROJECT_ROOT = WEB2_DIR.parent PROJECT_ROOT = WEB2_DIR.parent
sys.path.insert(0, str(PROJECT_ROOT)) sys.path.insert(0, str(PROJECT_ROOT))
# 2026-08-17:Windows 默认 GBK console,遇到 ✓ / 🎯 / 中文 emoji 会抛 UnicodeEncodeError
# 把 stdout/stderr 强制换成 utf-8(出错字符用 ? 替代,避免再次出现 start.py 启动崩溃)
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
_START_LOG = WEB2_DIR / "logs" _START_LOG = WEB2_DIR / "logs"
_START_LOG.mkdir(parents=True, exist_ok=True) _START_LOG.mkdir(parents=True, exist_ok=True)
logging.basicConfig( logging.basicConfig(
...@@ -66,11 +74,11 @@ def main(): ...@@ -66,11 +74,11 @@ def main():
missing = check_dependencies() missing = check_dependencies()
if missing: if missing:
msg = f"缺少关键依赖: {', '.join(n for _, n in missing)}" msg = f"缺少关键依赖: {', '.join(n for _, n in missing)}"
print(f"\n✗ {msg}") print(f"\n[FAIL] {msg}")
print(f"\n请执行: pip install fastapi uvicorn pydantic pyyaml pymysql") print(f"\n请执行: pip install fastapi uvicorn pydantic pyyaml pymysql")
logger.error(msg) logger.error(msg)
sys.exit(1) sys.exit(1)
print("✓ 关键依赖齐全") print("[OK] 关键依赖齐全")
logger.info("关键依赖齐全") logger.info("关键依赖齐全")
# 2. 配置 # 2. 配置
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment