diff --git a/PROGRESS-frontend-optimization-20260828.md b/PROGRESS-frontend-optimization-20260828.md index c1cabf33..1ea0cb02 100644 --- a/PROGRESS-frontend-optimization-20260828.md +++ b/PROGRESS-frontend-optimization-20260828.md @@ -142,7 +142,7 @@ 基线 **2196** → 本轮 **2205 pass / 0 fail / 0 skipped**(+9:chart-library-session 5、home-streaming-render-split 1、site-style-isolation 2、account-dialog-overlay 1)。 -浏览器(`next start` PORT 3010,无后端):`/login` 可见压缩 logo 与 Inter 变量 class、Link 头 `woff2` preload;`/membership` 站点样式完整;`/admin/users` 仍 403。未登录 `/` 因本机 Supabase 未配置走恢复页,不是本轮回归。后台已登录四页目视仍见 `BLOCKED.md`。 +浏览器(`next start` PORT 3010,未注入 `AUTH_PROVIDER=self-hosted` 与本地 PostgreSQL URL):`/login` 可见压缩 logo 与 Inter 变量 class、Link 头 `woff2` preload;`/membership` 站点样式完整;`/admin/users` 仍 403。未登录 `/` 因本机没有业务库连接而走恢复页,不是本轮回归。运行时是 Better Auth + 自维护 PostgreSQL,不是 Supabase。后台已登录四页目视仍见 `BLOCKED.md`。 ## 验收命令(本机) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 2c13e7ef..b72634cc 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -6589,3 +6589,19 @@ - 复发自:无 - 修复版本:待发布 +## BUG-431 | 自托管 PostgreSQL 缺库时仍对用户写“Supabase 尚未配置” + +- 状态:resolved +- 首次发现:2026-08-29 +- 最近更新:2026-08-29 +- 影响面:`GET /api/account`、会员余额、登录失败提示、会话/报告/星盘库等 503 +- 用户现象:运行时已是 Better Auth + 本地 PostgreSQL,缺业务库 URL 或未设 `AUTH_PROVIDER=self-hosted` 时,会员页“当前余额”和首页仍显示“Supabase 尚未配置”。 +- 触发条件:未注入 `APP_DATABASE_URL` / `SERVICE_DATABASE_URL`,或 `AUTH_PROVIDER` 未设为 `self-hosted` 因而落到旧适配器路径。 +- 根因:BUG-010 已禁止浏览器直连 Supabase,但 API 与 `friendlyError` 仍把配置失败翻译成 Supabase 文案和 `SUPABASE_NOT_CONFIGURED`。产品已不用 Supabase。 +- 修复:对外统一为“数据库尚未配置” / `DATABASE_NOT_CONFIGURED`,以及“请先配置数据库环境变量。”。内部类名仍叫 `SupabaseConfigurationError`,避免本轮改适配器标识。 +- 验证:`frontend/tests/self-hosted-error-copy.test.ts` 扫描 `frontend/src` 禁止上述旧文案,并锁定错误码与内部消息。 +- 防复发:用户可见中文不得再出现“Supabase 尚未配置”;缺库 503 必须说数据库,不得说已退役的托管服务。 +- 相关记录:BUG-010 +- 复发自:BUG-010 +- 修复版本:待发布 + diff --git a/frontend/src/app/api/account/route.ts b/frontend/src/app/api/account/route.ts index 48915dd9..92886e9d 100644 --- a/frontend/src/app/api/account/route.ts +++ b/frontend/src/app/api/account/route.ts @@ -161,7 +161,7 @@ export async function GET() { }); } catch (error) { if (isSupabaseConfigurationError(error)) { - return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 }); + return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 }); } return NextResponse.json({ error: "账户服务暂时不可用" }, { status: 500 }); } @@ -346,7 +346,7 @@ export async function PATCH(request: Request) { }); } catch (error) { if (isSupabaseConfigurationError(error)) { - return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 }); + return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 }); } return NextResponse.json({ error: "账户服务暂时不可用" }, { status: 500 }); } diff --git a/frontend/src/app/api/birth-time-guide/route.ts b/frontend/src/app/api/birth-time-guide/route.ts index d0ba805d..de46cba0 100644 --- a/frontend/src/app/api/birth-time-guide/route.ts +++ b/frontend/src/app/api/birth-time-guide/route.ts @@ -42,7 +42,7 @@ export async function POST(request: Request) { } catch (error) { if (isSupabaseConfigurationError(error)) { return NextResponse.json( - { error: "服务尚未配置", message: "请先配置 Supabase 环境变量。" }, + { error: "服务尚未配置", message: "请先配置数据库环境变量。" }, { status: 503 }, ); } diff --git a/frontend/src/app/api/birth-time-journey/route.ts b/frontend/src/app/api/birth-time-journey/route.ts index 21fd4b78..1f1ba2c6 100644 --- a/frontend/src/app/api/birth-time-journey/route.ts +++ b/frontend/src/app/api/birth-time-journey/route.ts @@ -63,7 +63,7 @@ export async function POST(request: Request) { } catch (error) { if (isSupabaseConfigurationError(error)) { return NextResponse.json( - { error: "服务尚未配置", message: "请先配置 Supabase 环境变量。" }, + { error: "服务尚未配置", message: "请先配置数据库环境变量。" }, { status: 503 }, ); } diff --git a/frontend/src/app/api/chart-profiles/[id]/route.ts b/frontend/src/app/api/chart-profiles/[id]/route.ts index 2acce5c1..82f050b8 100644 --- a/frontend/src/app/api/chart-profiles/[id]/route.ts +++ b/frontend/src/app/api/chart-profiles/[id]/route.ts @@ -31,7 +31,7 @@ export async function DELETE(_request: Request, context: RouteContext) { return NextResponse.json({ ok: true }); } catch (error) { if (isSupabaseConfigurationError(error)) { - return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 }); + return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 }); } return NextResponse.json({ error: errorMessage(error, "星盘删除失败") }, { status: 500 }); } diff --git a/frontend/src/app/api/chart-profiles/route.ts b/frontend/src/app/api/chart-profiles/route.ts index a9f37060..f190866e 100644 --- a/frontend/src/app/api/chart-profiles/route.ts +++ b/frontend/src/app/api/chart-profiles/route.ts @@ -28,7 +28,7 @@ export async function GET() { return NextResponse.json({ profiles: data ?? [] }); } catch (error) { if (isSupabaseConfigurationError(error)) { - return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 }); + return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 }); } return NextResponse.json({ error: errorMessage(error, "星盘库暂时不可用") }, { status: 500 }); } @@ -81,7 +81,7 @@ export async function POST(request: Request) { return NextResponse.json({ profile: data }); } catch (error) { if (isSupabaseConfigurationError(error)) { - return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 }); + return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 }); } return NextResponse.json({ error: errorMessage(error, "星盘保存失败") }, { status: 500 }); } diff --git a/frontend/src/app/api/consult/cancel/route.ts b/frontend/src/app/api/consult/cancel/route.ts index b64138ae..32ca9669 100644 --- a/frontend/src/app/api/consult/cancel/route.ts +++ b/frontend/src/app/api/consult/cancel/route.ts @@ -18,7 +18,7 @@ export async function POST(request: Request) { accounting = createAdminSupabaseClient(); } catch { return NextResponse.json( - { error: "服务尚未配置", message: "请先配置 Supabase 环境变量。" }, + { error: "服务尚未配置", message: "请先配置数据库环境变量。" }, { status: 503 }, ); } diff --git a/frontend/src/app/api/consult/route.ts b/frontend/src/app/api/consult/route.ts index 7b59819e..8ecc358f 100644 --- a/frontend/src/app/api/consult/route.ts +++ b/frontend/src/app/api/consult/route.ts @@ -234,7 +234,7 @@ export async function POST(request: Request) { accounting = createAdminSupabaseClient(); } catch { return NextResponse.json( - { error: "服务尚未配置", message: "请先配置 Supabase 环境变量。" }, + { error: "服务尚未配置", message: "请先配置数据库环境变量。" }, { status: 503 }, ); } diff --git a/frontend/src/app/api/models/route.ts b/frontend/src/app/api/models/route.ts index 7aeb65af..d7a83817 100644 --- a/frontend/src/app/api/models/route.ts +++ b/frontend/src/app/api/models/route.ts @@ -11,7 +11,7 @@ export async function GET() { } catch (error) { if (error instanceof Error) { return NextResponse.json( - { error: "服务尚未配置", message: "请先配置 Supabase 环境变量。" }, + { error: "服务尚未配置", message: "请先配置数据库环境变量。" }, { status: 503 }, ); } diff --git a/frontend/src/app/api/payment/orders/route.ts b/frontend/src/app/api/payment/orders/route.ts index 204b7843..7a002003 100644 --- a/frontend/src/app/api/payment/orders/route.ts +++ b/frontend/src/app/api/payment/orders/route.ts @@ -42,7 +42,7 @@ export async function GET() { } catch (error) { if (isSupabaseConfigurationError(error)) { return NextResponse.json( - { error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, + { error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 }, ); } diff --git a/frontend/src/app/api/redeem/route.ts b/frontend/src/app/api/redeem/route.ts index 1be0d1eb..2fe41915 100644 --- a/frontend/src/app/api/redeem/route.ts +++ b/frontend/src/app/api/redeem/route.ts @@ -75,7 +75,7 @@ export async function POST(request: Request) { } catch (error) { if (isSupabaseConfigurationError(error)) { return NextResponse.json( - { error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, + { error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 }, ); } diff --git a/frontend/src/app/api/reports/[reportId]/route.ts b/frontend/src/app/api/reports/[reportId]/route.ts index 08af4663..60b3f560 100644 --- a/frontend/src/app/api/reports/[reportId]/route.ts +++ b/frontend/src/app/api/reports/[reportId]/route.ts @@ -80,7 +80,7 @@ export async function GET(request: Request, context: RouteContext) { } catch (error) { if (isSupabaseConfigurationError(error)) { return NextResponse.json( - { error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, + { error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 }, ); } @@ -122,7 +122,7 @@ export async function DELETE(request: Request, context: RouteContext) { } catch (error) { if (isSupabaseConfigurationError(error)) { return NextResponse.json( - { error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, + { error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 }, ); } diff --git a/frontend/src/app/api/reports/route.ts b/frontend/src/app/api/reports/route.ts index e7ea8447..b6a4db04 100644 --- a/frontend/src/app/api/reports/route.ts +++ b/frontend/src/app/api/reports/route.ts @@ -93,7 +93,7 @@ export async function GET() { } catch (error) { if (isSupabaseConfigurationError(error)) { return NextResponse.json( - { error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, + { error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 }, ); } @@ -211,7 +211,7 @@ export async function POST(request: Request) { } catch (error) { if (isSupabaseConfigurationError(error)) { return NextResponse.json( - { error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, + { error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 }, ); } diff --git a/frontend/src/app/api/sessions/[id]/route.ts b/frontend/src/app/api/sessions/[id]/route.ts index 771085cb..813a158a 100644 --- a/frontend/src/app/api/sessions/[id]/route.ts +++ b/frontend/src/app/api/sessions/[id]/route.ts @@ -51,7 +51,7 @@ export async function PATCH(request: Request, context: RouteContext) { return NextResponse.json({ error: error.message }, { status: 413 }); } if (isSupabaseConfigurationError(error)) { - return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 }); + return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 }); } return NextResponse.json({ error: "聊天记录暂时无法同步" }, { status: 500 }); } @@ -74,7 +74,7 @@ export async function DELETE(_request: Request, context: RouteContext) { return NextResponse.json({ ok: true }); } catch (error) { if (isSupabaseConfigurationError(error)) { - return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 }); + return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 }); } return NextResponse.json({ error: error instanceof Error ? error.message : "删除聊天记录失败" }, { status: 500 }); } diff --git a/frontend/src/app/api/sessions/route.ts b/frontend/src/app/api/sessions/route.ts index 00cc62f7..ce6fe523 100644 --- a/frontend/src/app/api/sessions/route.ts +++ b/frontend/src/app/api/sessions/route.ts @@ -52,7 +52,7 @@ export async function POST(request: Request) { return NextResponse.json({ error: error.message }, { status: 413 }); } if (isSupabaseConfigurationError(error)) { - return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 }); + return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 }); } return NextResponse.json({ error: "聊天记录暂时无法同步" }, { status: 500 }); } diff --git a/frontend/src/app/api/synastry-reports/route.ts b/frontend/src/app/api/synastry-reports/route.ts index 3ebcc869..bf13c7cd 100644 --- a/frontend/src/app/api/synastry-reports/route.ts +++ b/frontend/src/app/api/synastry-reports/route.ts @@ -36,7 +36,7 @@ export async function GET() { return NextResponse.json({ reports: data ?? [] }); } catch (error) { if (isSupabaseConfigurationError(error)) { - return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 }); + return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 }); } return NextResponse.json({ error: errorMessage(error, "合盘历史暂时不可用") }, { status: 500 }); } @@ -78,7 +78,7 @@ export async function POST(request: Request) { return NextResponse.json({ report: data }); } catch (error) { if (isSupabaseConfigurationError(error)) { - return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 }); + return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 }); } return NextResponse.json({ error: errorMessage(error, "合盘历史保存失败") }, { status: 500 }); } diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 1647bc2d..83807c6f 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -944,8 +944,11 @@ function isProfileComplete(profile: Profile) { } function friendlyError(message: string) { - return message.includes("Supabase") && (message.includes("配置") || message.includes("environment") || message.includes("URL")) - ? "Supabase 尚未配置" + return ( + message.includes("数据库配置缺失") + || (message.includes("Supabase") && (message.includes("配置") || message.includes("environment") || message.includes("URL"))) + ) + ? "数据库尚未配置" : message; } diff --git a/frontend/src/components/email-otp-login.tsx b/frontend/src/components/email-otp-login.tsx index 184c9a63..7d9c1e7f 100644 --- a/frontend/src/components/email-otp-login.tsx +++ b/frontend/src/components/email-otp-login.tsx @@ -19,7 +19,7 @@ function authMessage(caught: unknown) { message.includes("environment") || message.includes("URL") ) - return "Supabase 尚未配置"; + return "数据库尚未配置"; if (lower.includes("expired") || lower.includes("invalid")) return "验证码错误或已过期,请重新获取"; if (lower.includes("rate limit")) return "发送过于频繁,请稍后再试"; diff --git a/frontend/src/lib/onboarding-post.ts b/frontend/src/lib/onboarding-post.ts index 4877afcc..5b9cf77d 100644 --- a/frontend/src/lib/onboarding-post.ts +++ b/frontend/src/lib/onboarding-post.ts @@ -148,7 +148,7 @@ export function createOnboardingPost(dependencies: OnboardingPostDependencies): session = await dependencies.openSession(); } catch { // no-excuse-ok: catch -- route boundary converts missing configuration. return Response.json( - { error: "服务尚未配置", message: "请先配置 Supabase 环境变量。" }, + { error: "服务尚未配置", message: "请先配置数据库环境变量。" }, { status: 503 }, ); } diff --git a/frontend/src/lib/supabase/config.ts b/frontend/src/lib/supabase/config.ts index 36a023ea..a237b461 100644 --- a/frontend/src/lib/supabase/config.ts +++ b/frontend/src/lib/supabase/config.ts @@ -1,8 +1,8 @@ export class SupabaseConfigurationError extends Error { - readonly code = "SUPABASE_NOT_CONFIGURED"; + readonly code = "DATABASE_NOT_CONFIGURED"; constructor(missing: string[]) { - super(`Supabase 配置缺失:${missing.join(", ")}`); + super(`数据库配置缺失:${missing.join(", ")}`); this.name = "SupabaseConfigurationError"; } } diff --git a/frontend/tests/self-hosted-error-copy.test.ts b/frontend/tests/self-hosted-error-copy.test.ts new file mode 100644 index 00000000..de4c425d --- /dev/null +++ b/frontend/tests/self-hosted-error-copy.test.ts @@ -0,0 +1,43 @@ +import assert from "node:assert/strict"; +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; +import { SupabaseConfigurationError } from "../src/lib/supabase/config.ts"; + +const srcRoot = fileURLToPath(new URL("../src", import.meta.url)); + +function walk(directory: string): string[] { + const entries = readdirSync(directory); + const files: string[] = []; + for (const name of entries) { + const path = join(directory, name); + if (statSync(path).isDirectory()) { + files.push(...walk(path)); + continue; + } + if (path.endsWith(".ts") || path.endsWith(".tsx")) files.push(path); + } + return files; +} + +test("user-visible copy never names Supabase after the PostgreSQL cutover", () => { + const stale = [ + "Supabase 尚未配置", + "请先配置 Supabase 环境变量。", + "Supabase 配置缺失", + "SUPABASE_NOT_CONFIGURED", + ]; + const hits: string[] = []; + for (const file of walk(srcRoot)) { + const source = readFileSync(file, "utf8"); + for (const needle of stale) { + if (source.includes(needle)) hits.push(`${file.slice(srcRoot.length + 1)}: ${needle}`); + } + } + assert.deepEqual(hits, []); + const missing = new SupabaseConfigurationError(["APP_DATABASE_URL"]); + assert.equal(missing.code, "DATABASE_NOT_CONFIGURED"); + assert.match(missing.message, /数据库配置缺失:APP_DATABASE_URL/); + assert.doesNotMatch(missing.message, /Supabase/); +});