From b78852f01b3fc10e29bd8092eff81aa8ed86196d Mon Sep 17 00:00:00 2001 From: Simon Date: Sun, 6 Sep 2026 19:00:49 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=89=8D=E7=AB=AF=E5=90=8C=E6=BA=90?= =?UTF-8?q?=E4=BB=A3=E7=90=86=20/api=20=E2=86=92=20=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=EF=BC=88=E4=BB=BB=E6=84=8F=20IP=20=E8=AE=BF=E9=97=AE=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=20NetworkError=EF=BC=89+=20SQLite=20=E5=86=99?= =?UTF-8?q?=E9=94=81=E7=AD=89=E5=BE=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 根因①:页面硬编码 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: * --- .../persistence/sqlalchemy/session.py | 19 ++++++++++++++++++- backend/app/main.py | 6 ++++-- docs/USAGE.md | 9 ++++++--- frontend/web/.env.local.example | 11 +++++++++-- frontend/web/lib/api.ts | 11 +++++++++-- frontend/web/next.config.ts | 7 +++++++ 6 files changed, 53 insertions(+), 10 deletions(-) diff --git a/backend/app/infrastructure/persistence/sqlalchemy/session.py b/backend/app/infrastructure/persistence/sqlalchemy/session.py index 138c4f2..9e084d9 100644 --- a/backend/app/infrastructure/persistence/sqlalchemy/session.py +++ b/backend/app/infrastructure/persistence/sqlalchemy/session.py @@ -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, diff --git a/backend/app/main.py b/backend/app/main.py index 82affca..6f95932 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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=["*"], diff --git a/docs/USAGE.md b/docs/USAGE.md index 29c8ee1..4e9ebad 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -1,7 +1,7 @@ # qlib-platform 使用说明 > 适用代码版本:M1–M5 全部完成,含 QlibEngine v1(Qlib 数据管线)与 Parquet 导出 -> (HEAD ≈ `bbb5c1e`);若文档与代码不一致,以代码与 +> (HEAD ≈ `fc12ba8`);若文档与代码不一致,以代码与 > [ARCHITECTURE.md](./ARCHITECTURE.md) / [ROADMAP.md](./ROADMAP.md) 为准。 > > 本文覆盖:安装配置、数据同步、启动前后端、研究 API、研究引擎与 Qlib 接入、 @@ -291,8 +291,11 @@ pnpm run build 中向 `ResearchService` 注入 `QlibEngine()` 启用 QlibDataset 数据管线(见 §6.1)。 - **Parquet 导出文件在哪、怎么读**:`data/parquet/stock_daily/.parquet`; `pd.read_parquet(...)` 直接读取(pyarrow 已随依赖安装)。 -- **前端连不上后端**:确认后端运行在 8000、`frontend/web/.env.local` 的 - `NEXT_PUBLIC_API_BASE` 正确、CORS 白名单含 `localhost:3000 / 127.0.0.1:3000`。 +- **前端连不上后端 / NetworkError**:前端默认经 Next **同源代理**访问 `/api/*` + (next.config.ts rewrites → `127.0.0.1:8000`,可用 `BACKEND_API_URL` 覆盖), + 任意 IP 访问 `:3000` 都不需要 CORS 或硬编码后端地址;后端 CORS 开发期为 `*`。 + 若 API 返回 500 且日志出现 `database is locked`,多半是正在跑全市场数据同步 + (长写事务),同步结束后自动恢复(引擎已加 busy_timeout 等待)。 - **数据库被改动想重置**:删除 `data/quant.db` 后 `uv run alembic upgrade head` 重建 表结构(行情需重新同步)。 - **想切换 MySQL**:`.env` 设 `DATABASE_URL=mysql+pymysql://user:pass@host/db`, diff --git a/frontend/web/.env.local.example b/frontend/web/.env.local.example index 0786c06..5f2809d 100644 --- a/frontend/web/.env.local.example +++ b/frontend/web/.env.local.example @@ -1,2 +1,9 @@ -# 后端 API 基础地址(开发期) -NEXT_PUBLIC_API_BASE=http://127.0.0.1:8000/api +# 前端环境示例(复制为 .env.local 使用) + +# 默认同源代理:前端 /api/* → 后端(next.config.ts rewrites,默认 http://127.0.0.1:8000)。 +# 以下两种情况需要配置: +# 1) 后端跑在别的机器/端口:给 next dev/start 进程设 BACKEND_API_URL +# (注意这是 Next 服务端代理变量,不属于 NEXT_PUBLIC_*) +# BACKEND_API_URL=http://192.168.1.160:8000 +# 2) 不经过 Next 代理、让浏览器直连后端:设置 NEXT_PUBLIC_API_BASE(含 /api,浏览器可访问的地址) +# NEXT_PUBLIC_API_BASE=http://192.168.1.160:8000/api diff --git a/frontend/web/lib/api.ts b/frontend/web/lib/api.ts index 626b425..5b96575 100644 --- a/frontend/web/lib/api.ts +++ b/frontend/web/lib/api.ts @@ -1,5 +1,12 @@ -/** 后端 API 客户端(开发期指向本地 FastAPI,地址见 .env.local)。 */ -const BASE = process.env.NEXT_PUBLIC_API_BASE ?? "http://127.0.0.1:8000/api"; +/** + * 后端 API 客户端。 + * + * 默认走同源代理:页面请求相对路径 /api/*,由 next.config.ts 的 rewrites 转发到后端 + * (默认 http://127.0.0.1:8000,可用环境变量 BACKEND_API_URL 覆盖)。 + * 需要直连(不走代理)时可用 NEXT_PUBLIC_API_BASE 显式指定后端地址(含 /api)。 + */ +const explicitBase = process.env.NEXT_PUBLIC_API_BASE?.replace(/\/$/, ""); +const BASE = explicitBase ?? "/api"; export async function apiGet(path: string): Promise { const resp = await fetch(`${BASE}${path}`); diff --git a/frontend/web/next.config.ts b/frontend/web/next.config.ts index 2f6491c..4de33fc 100644 --- a/frontend/web/next.config.ts +++ b/frontend/web/next.config.ts @@ -2,6 +2,13 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { reactStrictMode: true, + // 同源代理:页面请求 /api/* → 后端 FastAPI(默认 127.0.0.1:8000) + // 使任何 IP/域名访问前端都无需 CORS 与硬编码后端地址。 + // 后端地址可用环境变量 BACKEND_API_URL 覆盖(如 0.0.0.0 部署到别的机器)。 + async rewrites() { + const backend = process.env.BACKEND_API_URL ?? "http://127.0.0.1:8000"; + return [{ source: "/api/:path*", destination: `${backend}/api/:path*` }]; + }, }; export default nextConfig;