- 根因①:页面硬编码 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: *
34 lines
1.1 KiB
TypeScript
34 lines
1.1 KiB
TypeScript
/**
|
|
* 后端 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}`);
|
|
if (!resp.ok) throw new Error(`GET ${path} → ${resp.status}: ${await resp.text()}`);
|
|
return (await resp.json()) as T;
|
|
}
|
|
|
|
export async function apiPost<T>(path: string, body: unknown): Promise<T> {
|
|
const resp = await fetch(`${BASE}${path}`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (!resp.ok) {
|
|
const text = await resp.text();
|
|
throw new Error(`POST ${path} → ${resp.status}: ${text.slice(0, 300)}`);
|
|
}
|
|
return (await resp.json()) as T;
|
|
}
|
|
|
|
export interface ApiError {
|
|
status: number;
|
|
message: string;
|
|
}
|