fix: 前端同源代理 /api → 后端(任意 IP 访问不再 NetworkError)+ SQLite 写锁等待

- 根因①:页面硬编码 http://127.0.0.1:8000/api,从 192.168.1.160:3000 访问时浏览器请求自身/跨域失败
- 根因②(500):全市场数据同步(长写事务)持有 SQLite 锁导致读接口 database is locked
- 前端:next.config.ts rewrites /api/* → http://127.0.0.1:8000/api(BACKEND_API_URL 可覆盖);
  lib/api.ts 默认相对 /api 同源(NEXT_PUBLIC_API_BASE 保留为直连覆盖项);.env.local.example 说明两种方式
- 后端:CORS 开发期 allow_origins=["*"](无凭据,上线前收紧);SQLite engine busy_timeout=30s
- USAGE FAQ 同步;验证:192.168.1.160:3000/api/health 与 /api/factors 经代理 200,CORS 头 access-control-allow-origin: *
This commit is contained in:
Simon
2026-09-06 19:00:49 +08:00
parent fc12ba89ad
commit b78852f01b
6 changed files with 53 additions and 10 deletions
@@ -8,19 +8,36 @@ from __future__ import annotations
from collections.abc import Generator
from sqlalchemy import create_engine
from sqlalchemy import create_engine, event
from sqlalchemy.orm import Session, sessionmaker
from app.core.config import get_settings
_settings = get_settings()
# SQLite 写锁等待加固:全市场同步等长写事务进行时,读接口等待而非立刻
# "database is locked";另在连接上设置 busy_timeout。WAL 迁移留作后续专项
# (需在无活跃写事务时统一切换,避免混合 journal 模式)。
_connect_args: dict = {}
if _settings.database_url.startswith("sqlite"):
_connect_args["timeout"] = 30
engine = create_engine(
_settings.database_url,
echo=_settings.sqlalchemy_echo,
future=True,
connect_args=_connect_args,
)
@event.listens_for(engine, "connect")
def _set_sqlite_pragmas(dbapi_conn, _record) -> None: # pragma: no cover —— 仅 SQLite 路径
if _settings.database_url.startswith("sqlite"):
cur = dbapi_conn.cursor()
cur.execute("PRAGMA busy_timeout = 30000")
cur.close()
SessionLocal = sessionmaker(
bind=engine,
autoflush=False,
+4 -2
View File
@@ -19,10 +19,12 @@ app = FastAPI(
description="A股个人量化研究平台 API(研究引擎 / Tushare 数据源)",
)
# 开发期允许本地前端跨域(Phase 3 前端 dev server;上线收紧为白名单)
# 开发期 CORS:允许任意来源(个人本地平台,无 Cookie 凭据,allow_credentials=False)。
# 前端默认走 Next 同源代理(见 frontend/web/next.config.ts rewrites),直连 API 也放行;
# 上线部署前请收紧为明确白名单。
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"],
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],