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:
@@ -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
@@ -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=["*"],
|
||||
|
||||
+6
-3
@@ -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/<year>.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`,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<T>(path: string): Promise<T> {
|
||||
const resp = await fetch(`${BASE}${path}`);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user