From f20b852e98885a45e486e8a0970052f687cf853d Mon Sep 17 00:00:00 2001 From: hzm <934585316@qq.com> Date: Sat, 7 Mar 2026 11:09:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=BA=E5=87=8F=E5=B0=91=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E7=AB=AF=E5=87=BA=E5=8F=A3=E6=B5=81=E9=87=8F=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=BE=B9=E7=BC=98=E5=87=BD=E6=95=B0=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E6=95=B0=E6=8D=AE=E5=BA=93=E8=A1=A8=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 32 ++++++++ app/page.jsx | 31 +++++--- doc/edgeFunctions/check-data.ts | 125 ++++++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 10 deletions(-) create mode 100644 doc/edgeFunctions/check-data.ts diff --git a/README.md b/README.md index a588b43..aae5ff1 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,38 @@ 5. 目前项目用到的 sql 语句,查看项目 supabase.sql 文件。 +### Supabase Edge Functions 配置说明 + +本项目使用 Supabase Edge Functions 来检查用户配置状态。需要部署以下边缘函数: + +#### 1. 边缘函数文件位置 +边缘函数代码位于 `doc/edgeFunctions/*` + +#### 2. 部署步骤 + +1. **新建边缘函数代码**: +位置在 Supabase控制台 → Edge Functions + +2. **复制边缘函数代码**: + 将 `doc/edgeFunctions/*` 复制到 `supabase/functions/*`。复制成功后即可点击部署 + +#### 3. 边缘函数说明 + +**check-data** - 检查用户配置状态 +- **功能**:检查用户在 `user_configs` 表中的配置状态 +- **认证**:需要用户 JWT token +- **返回状态**: + - `not_found`:用户配置记录不存在 + - `empty`:用户配置记录存在但数据为空 + - `found`:用户配置记录存在且有有效数据 + +#### 4. 环境变量要求 + +边缘函数需要以下 Supabase 环境变量(自动注入,无需手动配置): +- `SUPABASE_URL`:Supabase 项目 URL +- `SUPABASE_ANON_KEY`:Supabase 匿名公钥 +- `SUPABASE_SERVICE_ROLE_KEY`:Supabase Service Role Key(用于绕过 RLS 查询) + 更多 Supabase 相关内容查阅官方文档。 ### 构建与部署 diff --git a/app/page.jsx b/app/page.jsx index 4718276..946991f 100644 --- a/app/page.jsx +++ b/app/page.jsx @@ -3179,13 +3179,13 @@ export default function HomePage() { const fetchCloudConfig = async (userId, checkConflict = false) => { if (!userId) return; try { - const { data, error } = await supabase - .from('user_configs') - .select('id, data, updated_at') - .eq('user_id', userId) - .maybeSingle(); - if (error) throw error; - if (!data?.id) { + const { data: checkResult, error: checkError } = await supabase.functions.invoke(`check-data?userId=${userId}`, { + method: 'GET', + }); + + if (checkError) throw checkError; + + if (checkResult.status === 'not_found') { const { error: insertError } = await supabase .from('user_configs') .insert({ user_id: userId }); @@ -3193,19 +3193,30 @@ export default function HomePage() { setCloudConfigModal({ open: true, userId, type: 'empty' }); return; } + + if (checkResult.status === 'empty') { + setCloudConfigModal({ open: true, userId, type: 'empty' }); + return; + } + + const { data, error } = await supabase + .from('user_configs') + .select('id, data, updated_at') + .eq('user_id', userId) + .single(); + + if (error) throw error; + if (data?.data && isPlainObject(data.data) && Object.keys(data.data).length > 0) { const localPayload = collectLocalPayload(); const localComparable = getComparablePayload(localPayload); const cloudComparable = getComparablePayload(data.data); if (localComparable !== cloudComparable) { - // 如果数据不一致 if (checkConflict) { - // 只有明确要求检查冲突时才提示(例如刚登录时) setCloudConfigModal({ open: true, userId, type: 'conflict', cloudData: data.data }); return; } - // 否则直接覆盖本地(例如已登录状态下的刷新) await applyCloudConfig(data.data, data.updated_at); return; } diff --git a/doc/edgeFunctions/check-data.ts b/doc/edgeFunctions/check-data.ts new file mode 100644 index 0000000..8afed8a --- /dev/null +++ b/doc/edgeFunctions/check-data.ts @@ -0,0 +1,125 @@ +/** + * 检测用户配置是否存在 + */ +// supabase/functions/check-data/index.ts + +import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; +import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", +}; + +serve(async (req: Request) => { + if (req.method === "OPTIONS") { + return new Response("ok", { headers: corsHeaders }); + } + + if (req.method !== "GET") { + return new Response( + JSON.stringify({ error: "Method not allowed" }), + { status: 405, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + try { + const authHeader = req.headers.get("Authorization"); + if (!authHeader) { + return new Response( + JSON.stringify({ error: "Missing authorization header" }), + { status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + const supabaseUrl = Deno.env.get("SUPABASE_URL") ?? ""; + const supabaseAnonKey = Deno.env.get("SUPABASE_ANON_KEY") ?? ""; + const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? ""; + + const supabaseClient = createClient(supabaseUrl, supabaseAnonKey, { + global: { + headers: { Authorization: authHeader }, + }, + }); + + const { data: { user }, error: userError } = await supabaseClient.auth.getUser(); + + if (userError || !user) { + return new Response( + JSON.stringify({ error: "Invalid JWT", exists: false, status: "error" }), + { status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + const url = new URL(req.url); + const userId = url.searchParams.get("userId"); + + if (!userId) { + return new Response( + JSON.stringify({ error: "Missing 'userId' parameter" }), + { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + if (user.id !== userId) { + return new Response( + JSON.stringify({ error: "Unauthorized: User ID mismatch" }), + { status: 403, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey); + + const { data, error } = await supabaseAdmin + .from("user_configs") + .select("data") + .eq("user_id", userId) + .single(); + + if (error && error.code === "PGRST116") { + return new Response( + JSON.stringify({ + exists: false, + status: "not_found", + }), + { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + if (error) { + throw error; + } + + const jsonData = data?.data; + + const hasValidData = + jsonData !== null && + typeof jsonData === "object" && + Object.keys(jsonData).length > 0; + + if (hasValidData) { + return new Response( + JSON.stringify({ + exists: true, + status: "found", + }), + { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } else { + return new Response( + JSON.stringify({ + exists: false, + status: "empty", + }), + { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + } catch (err) { + console.error("Error:", err); + return new Response( + JSON.stringify({ error: err.message, exists: false, status: "error" }), + { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } +}); \ No newline at end of file