跳轉到主要內容

一、整體架構

方案 A:直連模式(默認)

方案 B:反向代理模式(推薦)

核心原則:前端不直接調用微信 API,所有微信接口調用都通過 Supabase Edge Functions 代理,避免在客戶端暴露 AppSecret。用戶可以選擇直連模式或反向代理模式解決 IP 白名單問題。

二、前置準備

2.1 微信開發者平台後台配置

微信開發者平台 完成以下操作:
  1. 獲取開發者憑證
    • 訪問 微信開發者平台
    • 選擇對應的公眾號
    • 在「基礎信息」中查看或重置
    • 記錄 AppID(開發者 ID)
    • 記錄或重置 AppSecret(開發密鑰)
  2. 配置 IP 白名單
    • 在「基礎信息 → 開發密鑰 → API IP白名單」中添加 Edge Function 的出口 IP
    • ⚠️ 重要:Supabase Edge Functions 使用動態 IP,首次調用會返回 errcode: 40164,從錯誤信息中提取 IP 並添加到白名單
    • 提取方式:解析 errmsg 中的 invalid ip x.x.x.x
  3. 確認公眾號類型
    • 已認證的服務號擁有全部接口權限
    • 訂閱號部分接口受限(如自定義菜單、數據統計)

2.2 Supabase 項目配置

  1. 環境變數 / Edge Function Secrets 需要配置以下 Secrets(通過 Supabase Dashboard 或 CLI):
    Secret 名称說明示例
    SUPERUN_WECHAT_APP_ID公眾號 AppIDwx1234567890abcdef
    SUPERUN_WECHAT_APP_SECRET公眾號 AppSecreta1b2c3d4e5f6...
    SUPERUN_WECHAT_API_PROXY_URL反向代理地址(可選)https://wechat-proxy.example.com
    SUPABASE_URLSupabase 項目 URL(內置)https://xxxxx.supabase.co
    SUPABASE_ANON_KEYSupabase 匿名密鑰(內置)eyJhbG...
    SUPABASE_SERVICE_ROLE_KEYSupabase 服務角色密鑰(內置)eyJhbG...
    其中 SUPABASE_URLSUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEY 是 Supabase 內置的,無需手動配置。 SUPERUN_WECHAT_API_PROXY_URL 為可選項,不配置則直連微信 API(需手動維護 IP 白名單),配置後所有微信請求走代理服務器。
  2. Edge Function 配置supabase/config.toml
    project_id = "<your-project-id>"
    [functions.wechat-token]
    verify_jwt = false
    [functions.wechat-data]
    verify_jwt = false
    [functions.wechat-articles]
    verify_jwt = false
    [functions.wechat-menu]
    verify_jwt = false
    
    verify_jwt = false 允許 Edge Function 之間互相調用。認證邏輯在函數內部通過解析 JWT 手動實現。

三、數據庫表結構

3.1 wechat_tokens — 令牌緩存表

CREATE TABLE wechat_tokens (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  access_token TEXT NOT NULL,
  expires_at TIMESTAMPTZ NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
用途:緩存微信 access_token,避免每次請求都調用微信令牌接口(2 小時有效期,每日調用次數有限)。

3.2 articles — 圖文內容表

CREATE TABLE articles (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL,
  title TEXT NOT NULL DEFAULT '',
  content TEXT NOT NULL DEFAULT '',
  author TEXT NOT NULL DEFAULT '',
  digest TEXT NOT NULL DEFAULT '',
  thumb_url TEXT NOT NULL DEFAULT '',
  status TEXT NOT NULL DEFAULT 'draft',       -- draft / published / deleted
  wechat_media_id TEXT NOT NULL DEFAULT '',    -- 微信侧的 article_id
  wechat_article_url TEXT NOT NULL DEFAULT '', -- 發布後的文章鏈接
  published_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

3.3 menu_configs — 菜單配置表

CREATE TABLE menu_configs (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL,
  menu_data JSONB NOT NULL DEFAULT '{}',    -- 微信菜單 JSON 結構
  version INTEGER NOT NULL DEFAULT 1,
  status TEXT NOT NULL DEFAULT 'draft',     -- draft / published / deleted
  published_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

3.4 auto_reply_rules — 自動回復規則表

CREATE TABLE auto_reply_rules (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL,
  rule_type TEXT NOT NULL DEFAULT 'keyword',    -- keyword / subscribe / default
  keyword TEXT NOT NULL DEFAULT '',
  match_type TEXT NOT NULL DEFAULT 'exact',      -- exact / contain
  reply_content TEXT NOT NULL DEFAULT '',
  reply_type TEXT NOT NULL DEFAULT 'text',        -- text / image / voice
  is_enabled BOOLEAN NOT NULL DEFAULT true,
  priority INTEGER NOT NULL DEFAULT 0,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

四、Edge Functions 詳解

4.1 wechat-token — 令牌管理服務

職責:獲取並緩存微信 access_token,所有其他 Edge Function 通過調用此服務獲取令牌。 請求方式GETPOST 核心邏輯
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";

// 支持反向代理:配置 SUPERUN_WECHAT_API_PROXY_URL 后走代理,否則直連
const WECHAT_API_BASE = Deno.env.get("SUPERUN_WECHAT_API_PROXY_URL") || "https://api.weixin.qq.com";
const WECHAT_TOKEN_URL = `${WECHAT_API_BASE}/cgi-bin/token`;
const TOKEN_BUFFER_MINUTES = 5; // 提前 5 分鐘刷新

serve(async (req) => {
  const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
  const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
  const supabase = createClient(supabaseUrl, supabaseServiceKey);

  // 1. 檢查數據库中是否有未過期的緩存令牌
  const bufferTime = new Date(
    Date.now() + TOKEN_BUFFER_MINUTES * 60 * 1000
  ).toISOString();
  const { data: cachedToken } = await supabase
    .from("wechat_tokens")
    .select("access_token, expires_at")
    .gt("expires_at", bufferTime)
    .order("created_at", { ascending: false })
    .limit(1)
    .maybeSingle();

  if (cachedToken) {
    return new Response(
      JSON.stringify({ access_token: cachedToken.access_token }),
      { status: 200 }
    );
  }

  // 2. 緩存失效,向微信 API 請求新令牌
  const appId = Deno.env.get("SUPERUN_WECHAT_APP_ID");
  const appSecret = Deno.env.get("SUPERUN_WECHAT_APP_SECRET");
  const tokenUrl =
    `${WECHAT_TOKEN_URL}?grant_type=client_credential&appid=${appId}&secret=${appSecret}`;
  const wechatResponse = await fetch(tokenUrl);
  const wechatData = await wechatResponse.json();

  // 3. 處理 IP 白名單錯誤(errcode 40164)
  if (wechatData.errcode) {
    let blockedIp: string | null = null;
    if (wechatData.errcode === 40164 && wechatData.errmsg) {
      const ipMatch = wechatData.errmsg.match(/invalid ip (\d+\.\d+\.\d+\.\d+)/);
      if (ipMatch) blockedIp = ipMatch[1];
    }
    return new Response(
      JSON.stringify({
        error: `WeChat API error: ${wechatData.errmsg}`,
        errcode: wechatData.errcode,
        blocked_ip: blockedIp,
      }),
      { status: 502 }
    );
  }

  // 4. 緩存新令牌到數據库
  const expiresAt = new Date(
    Date.now() + wechatData.expires_in * 1000
  ).toISOString();
  await supabase.from("wechat_tokens").insert({
    access_token: wechatData.access_token,
    expires_at: expiresAt,
  });

  // 5. 清理已過期的舊令牌
  await supabase
    .from("wechat_tokens")
    .delete()
    .lt("expires_at", new Date().toISOString());

  return new Response(
    JSON.stringify({ access_token: wechatData.access_token }),
    { status: 200 }
  );
});
返回格式
// 成功
{ "access_token": "ACCESS_TOKEN_VALUE" }

// 失敗(IP 白名單問題)
{
  "error": "WeChat API error: invalid ip 1.2.3.4...",
  "errcode": 40164,
  "blocked_ip": "1.2.3.4"
}

4.2 wechat-data — 數據統計服務

職責:代理微信數據統計接口(datacube)和粉絲 / 菜單查詢。 請求方式POST TokenResult 模式(所有需要 access_token 的函數統一使用):
interface TokenResult {
  token?: string;
  error?: string;
  blocked_ip?: string | null;
}

async function getAccessToken(): Promise<TokenResult> {
  const response = await fetch(
    `${SUPABASE_URL}/functions/v1/wechat-token`,
    {
      headers: {
        Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
      },
    }
  );
  const data = await response.json();
  if (!data.access_token) {
    return {
      error: data.error || "Failed to get access token",
      blocked_ip: data.blocked_ip || null,
    };
  }
  return { token: data.access_token };
}
關鍵設計:當令牌獲取失敗(如 IP 被封)時,不拋異常,而是返回 { blocked_ip } 給前端,讓前端展示友好提示。
支持的 action
action微信 API說明日期限制
get-menuGET /cgi-bin/menu/get獲取當前菜單
get-follower-countGET /cgi-bin/user/get獲取粉絲總數
user-summaryPOST /datacube/getusersummary用戶增減數據最多 7 天
user-cumulatePOST /datacube/getusercumulate累計用戶數據最多 7 天
article-summaryPOST /datacube/getarticlesummary圖文群發日數據最多 1 天
article-totalPOST /datacube/getarticletotal圖文群發總數據最多 1 天
upstream-msgPOST /datacube/getupstreammsg消息概況最多 7 天
interface-summaryPOST /datacube/getinterfacesummary接口分析最多 30 天
調用示例(前端 → Edge Function):
import { supabase } from "@/integrations/supabase/client";

// 獲取粉絲数
const { data } = await supabase.functions.invoke("wechat-data", {
  body: { action: "get-follower-count" },
});
console.log(data.data.total); // 粉絲總數

// 獲取用戶增減數據(默認最近 7 天)
const { data: userData } = await supabase.functions.invoke("wechat-data", {
  body: {
    action: "user-summary",
    begin_date: "2026-02-26", // 可選,格式 YYYY-MM-DD
    end_date: "2026-03-04",   // 可選
  },
});
console.log(userData.data); // [{ ref_date, user_source, ... }]

4.3 wechat-articles — 圖文管理服務

職責:圖文 CRUD、發布到微信、從微信同步已發布文章。 支持的 action
action說明需要認證
save-draft保存 / 更新草稿
publish發布到微信(草稿 → 群發)
delete軟刪除文章
sync-published從微信同步已發布文章
發布流程(兩步操作):
// 所有函數開頭都有這行,支持代理切換
const WECHAT_API_BASE = Deno.env.get("SUPERUN_WECHAT_API_PROXY_URL") || "https://api.weixin.qq.com";
// 1. 先添加到微信草稿箱
const draftResponse = await fetch(
  `${WECHAT_API_BASE}/cgi-bin/draft/add?access_token=${accessToken}`,
  {
    method: "POST",
    body: JSON.stringify({
      articles: [{
        title: "文章標題",
        author: "作者",
        digest: "摘要",
        content: "<p>正文 HTML</p>",
        content_source_url: "",
        thumb_media_id: "",       // 封面素材 ID(可為空)
        need_open_comment: 0,
        only_fans_can_comment: 0,
      }],
    }),
  }
);
const { media_id } = await draftResponse.json();

// 2. 提交群發
const publishResponse = await fetch(
  `${WECHAT_API_BASE}/cgi-bin/freepublish/submit?access_token=${accessToken}`,
  {
    method: "POST",
    body: JSON.stringify({ media_id }),
  }
);
const { publish_id } = await publishResponse.json();
同步已發布文章(核心邏輯):
// ⚠️ 關鍵注意點:freepublish/batchget 返回的是 article_id,不是 media_id!
const WECHAT_API_BASE = Deno.env.get("SUPERUN_WECHAT_API_PROXY_URL") || "https://api.weixin.qq.com";
const batchResponse = await fetch(
  `${WECHAT_API_BASE}/cgi-bin/freepublish/batchget?access_token=${accessToken}`,
  {
    method: "POST",
    body: JSON.stringify({ offset: 0, count: 20, no_content: 0 }),
  }
);
const batchResult = await batchResponse.json();

// 返回結構示例:
// {
//   "total_count": 5,
//   "item_count": 5,
//   "item": [
//     {
//       "article_id": "Ai6E...",    ← 正確字段名
//       "content": {
//         "news_item": [
//           { "title": "...", "author": "...", "url": "...", ... }
//         ]
//       },
//       "update_time": 1709712000
//     }
//   ]
// }

// 去重邏輯:用 article_id 匹配數據库中的 wechat_media_id 字段
const articleIds = allItems.map((item) => item.article_id);
const { data: existing } = await supabaseAdmin
  .from("articles")
  .select("wechat_media_id")
  .in("wechat_media_id", articleIds);

// 插入時,多圖文用 article_id + 下標區分
const wechat_media_id = newsIndex === 0
  ? item.article_id
  : `${item.article_id}_${newsIndex}`;
前端調用
// 從微信同步
const { data } = await supabase.functions.invoke("wechat-articles", {
  body: { action: "sync-published" },
});
// data: { success: true, new_count: 5, total_count: 5 }

// 保存草稿
const { data } = await supabase.functions.invoke("wechat-articles", {
  body: {
    action: "save-draft",
    title: "標題",
    content: "<p>正文</p>",
    author: "作者",
    digest: "摘要",
  },
});

// 發布到微信
const { data } = await supabase.functions.invoke("wechat-articles", {
  body: { action: "publish", article_id: "uuid-of-article" },
});

4.4 wechat-menu — 自定義菜單服務

職責:將本地菜單配置同步到微信、清空微信菜單。 支持的 action
action說明参数
publish發布菜單到微信menu_config_id (必填)
delete清空微信菜單menu_config_id (可選)
核心邏輯
const WECHAT_API_BASE = Deno.env.get("SUPERUN_WECHAT_API_PROXY_URL") || "https://api.weixin.qq.com";
// 發布菜單
const response = await fetch(
  `${WECHAT_API_BASE}/cgi-bin/menu/create?access_token=${accessToken}`,
  {
    method: "POST",
    body: JSON.stringify(menuData), // { button: [...] }
  }
);

// 清空菜單
const response = await fetch(
  `${WECHAT_API_BASE}/cgi-bin/menu/delete?access_token=${accessToken}`,
  { method: "GET" }
);
智能處理:當 publish 時發現菜單為空(button.length === 0),自動調用 delete 接口清空微信菜單,而非創建一個空菜單。 菜單數據結構(微信標準格式):
{
  "button": [
    {
      "type": "view",
      "name": "官網",
      "url": "https://example.com"
    },
    {
      "name": "服務",
      "sub_button": [
        {
          "type": "view",
          "name": "在線客服",
          "url": "https://example.com/service"
        },
        {
          "type": "click",
          "name": "最新活動",
          "key": "LATEST_ACTIVITY"
        }
      ]
    }
  ]
}

五、前端集成模式

5.1 統一的 Hook 封裝

所有微信 API 調用都封裝為 React Query hooks:
// hooks/useArticles.ts
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { supabase } from "@/integrations/supabase/client";

interface SyncResult {
  success?: boolean;
  new_count?: number;
  total_count?: number;
  error?: string;
  blocked_ip?: string | null;
}

export function useSyncWechatArticles() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: async (): Promise<SyncResult> => {
      const { data, error } = await supabase.functions.invoke(
        "wechat-articles",
        { body: { action: "sync-published" } }
      );
      if (error) throw error;
      if (data?.blocked_ip) {
        return { blocked_ip: data.blocked_ip, new_count: 0 };
      }
      if (data?.error) throw new Error(data.error);
      return data as SyncResult;
    },
    onSuccess: (result) => {
      if (!result.blocked_ip) {
        queryClient.invalidateQueries({ queryKey: ["articles"] });
      }
    },
  });
}

5.2 IP 白名單錯誤處理(通用模式)

所有與微信交互的頁面都實現了統一的 IP 白名單錯誤處理:
const [lastBlockedIp, setLastBlockedIp] = useState<string | null>(null);

// 處理響應
const handleResult = (result: { blocked_ip?: string | null }) => {
  if (result.blocked_ip) {
    setLastBlockedIp(result.blocked_ip);
    toast.error(
      `微信接口連接失敗,請將 IP ${result.blocked_ip} 添加到公眾號 IP 白名單`,
      { duration: 15000 }
    );
    return;
  }
  // ... 正常處理
};

// UI 提示條
{lastBlockedIp && (
  <div className="bg-orange-50 border border-orange-200 rounded-lg p-3">
    <p>
      IP <code>{lastBlockedIp}</code> 未在微信公眾號白名單中
    </p>
    <button onClick={() => navigator.clipboard.writeText(lastBlockedIp)}>
      複製 IP
    </button>
  </div>
)}

5.3 手動同步(連通性檢查)

// hooks/useWechatSync.ts
export function useWechatManualSync() {
  const queryClient = useQueryClient();
  const [isSyncing, setIsSyncing] = useState(false);
  const [lastBlockedIp, setLastBlockedIp] = useState<string | null>(null);

  const syncWechatData = useCallback(async () => {
    setIsSyncing(true);
    setLastBlockedIp(null);

    // 用輕量的 get-follower-count 作為連通性檢查
    const { data, error } = await supabase.functions.invoke("wechat-data", {
      body: { action: "get-follower-count" },
    });

    setIsSyncing(false);

    if (data?.blocked_ip) {
      setLastBlockedIp(data.blocked_ip);
      return { success: false, blockedIp: data.blocked_ip };
    }
    if (error || data?.error) {
      return { success: false };
    }

    // 成功後刷新相關數據
    queryClient.invalidateQueries({ queryKey: ["wechat-overview"] });
    return { success: true };
  }, [queryClient]);

  return { syncWechatData, isSyncing, lastBlockedIp };
}

六、關鍵注意事項

6.1 IP 白名單問題 — 兩種解決方案

Supabase Edge Functions 使用動態出口 IP,無法預先配置白名單。用戶可根據自身情況選擇以下任一方案:

方案 A:IP 白名單(手動維護)

適合場景:快速驗證、臨時使用、沒有服務器資源。 步驟
  1. 首次調用微信 API 會返回 errcode: 40164
  2. errmsg 中提取被拒絕的 IP:invalid ip x.x.x.x
  3. 將該 IP 添加到公眾號白名單
  4. 應用前端會展示橙色提示條,支持一鍵複製 IP
缺點
  • Edge Function IP 可能會變動,需要重新添加
  • 需要手動操作微信後台
配置:無需額外配置,默認就是這個模式。

方案 B:反向代理(推薦,一勞永逸)

適合場景:生產環境、長期使用、不想每次 IP 變動都手動加白名單。 原理:在一台有固定公網 IP 的服務器上部署 Nginx 反向代理,所有微信 API 請求都經過這台服務器轉發。微信只看到代理服務器的固定 IP,加一次白名單就永久生效。 步驟 1:部署代理服務器 準備一台有固定公網 IP 的服務器(雲服務器即可,最低配置就行),安裝 Nginx,配置如下:
server {
    listen 443 ssl;
    server_name wechat-proxy.your-domain.com;
    ssl_certificate     /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    location / {
        proxy_pass https://api.weixin.qq.com;
        proxy_set_header Host api.weixin.qq.com;
        proxy_ssl_server_name on;
    }
}
也可以用 HTTP(端口 80),但建議使用 HTTPS 保證傳輸安全。
步驟 2:配置微信 IP 白名單 將代理服務器的固定公網 IP 添加到微信開發者平台「基礎信息 → 開發密鑰 → API IP白名單」。 步驟 3:配置 Edge Function Secret 在 Supabase 後台添加 Edge Function Secret:
Secret 名称示例
SUPERUN_WECHAT_API_PROXY_URL代理服務器地址https://wechat-proxy.your-domain.com
注意:地址末尾不要加 /
步驟 4:驗證 配置完成後重新部署 Edge Functions,調用任意微信接口驗證連通性。 代碼層面的實現 所有 Edge Function 均通過以下一行代碼自動切換直連 / 代理模式:
const WECHAT_API_BASE = Deno.env.get("SUPERUN_WECHAT_API_PROXY_URL") || "https://api.weixin.qq.com";
  • 未配置 SUPERUN_WECHAT_API_PROXY_URL → 直連 api.weixin.qq.com(方案 A)
  • 已配置 → 所有請求走代理地址(方案 B)
以下 4 個 Edge Function 均已支持該配置:
  • wechat-token(1 處)
  • wechat-data(8 處)
  • wechat-articles(3 處)
  • wechat-menu(3 處)
切換方式
  • 從方案 A 切到方案 B:添加 SUPERUN_WECHAT_API_PROXY_URL Secret 並重新部署
  • 從方案 B 切回方案 A:刪除該 Secret 並重新部署
  • 切換無需修改任何代碼

兩種方案對比

方案 A:IP 白名單方案 B:反向代理
配置難度無需額外服務器需要一台雲服務器 + 域名
維護成本IP 變動時需手動更新配置一次後無需維護
穩定性依賴 Edge Function IP 不變始終穩定
適合場景開發測試、臨時使用生產環境、長期運行
切換方式默認添加/刪除 Secret 即可

6.2 Access Token 管理

  • 微信 access_token 有效期 2 小時,每日調用次數有限
  • 通過 wechat_tokens 表緩存,提前 5 分鐘刷新
  • 所有 Edge Function 都通過調用 wechat-token 函數獲取令牌,避免重複請求
  • 定期清理過期令牌記錄

6.3 微信 API 常見 errcode

errcode含義處理方式
40001access_token 無效清除緩存重新獲取
40164IP 不在白名單提取 IP 提示用戶添加
42001access_token 過期自動刷新
46003菜單不存在正常情況,返回空菜單
61501日期範圍超限檢查日期區間是否超過 API 限制
45047超出發布頻率限制提示用戶稍後重試

6.4 數據統計日期限制

  • 微信數據有 1 天延遲end_date 最早只能是昨天
  • getusersummary / getusercumulate:最大跨度 7 天(含首尾)
  • getarticlesummary / getarticletotal:最大跨度 1 天
  • getupstreammsg:最大跨度 7 天
  • getinterfacesummary:最大跨度 30 天
  • 日期格式:YYYY-MM-DD

6.5 freepublish/batchget 字段注意

  • 該接口返回的每條記錄中,標識字段名為 article_id,不是 media_id
  • media_id 是草稿箱接口(draft/add)返回的字段
  • 混淆這兩個字段會導致同步時所有值為 undefined,數據無法寫入

6.6 Edge Function 間調用

  • wechat-datawechat-articleswechat-menu 都通過 HTTP 調用 wechat-token
  • 調用時使用 SUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEY 做 Bearer Token
  • config.toml 中設置 verify_jwt = false 以允許跨函數調用

6.7 錯誤返回策略

所有涉及微信 API 的 Edge Function 統一遵循:
  • 令牌獲取失敗 + IP 被封:返回 HTTP 200 + { blocked_ip: "x.x.x.x" }
    • 返回 200 是為了讓前端 supabase.functions.invoke() 能正確讀取 data(非 200 會被放入 error
  • 微信 API 業務錯誤:返回 HTTP 200 + { error: "...", errcode: xxx }
  • 系統錯誤:返回 HTTP 500 + { error: "..." }

七、部署清單

第一次部署前檢查

通用步驟(兩種方案都需要):
  • 微信開發者平台已獲取 AppIDAppSecret
  • Supabase Edge Function Secrets 已配置 SUPERUN_WECHAT_APP_IDSUPERUN_WECHAT_APP_SECRET
  • 數據库已創建 wechat_tokensarticlesmenu_configsauto_reply_rules
  • supabase/config.toml 中所有微信相關函數已設置 verify_jwt = false
方案 A 額外步驟(IP 白名單):
  • 部署後首次調用,從錯誤響應中獲取 Edge Function 出口 IP
  • 將出口 IP 添加到微信開發者平台 IP 白名單
  • 再次調用驗證連通性
方案 B 額外步驟(反向代理):
  • 部署代理服務器(Nginx 配置見 6.1 節)
  • 將代理服務器的固定 IP 添加到微信 IP 白名單
  • 配置 Edge Function Secret SUPERUN_WECHAT_API_PROXY_URL
  • 重新部署並驗證連通性

Edge Function 文件結構

supabase/functions/
├── wechat-token/
│   └── index.ts       # 令牌管理(獲取、緩存、清理)
├── wechat-data/
│   └── index.ts       # 數據統計代理(粉絲、菜單、datacube)
├── wechat-articles/
│   └── index.ts       # 圖文管理(CRUD、發布、同步)
├── wechat-menu/
│   └── index.ts       # 菜單管理(發布、清空)
└── ai-content-assist/
    └── index.ts       # AI 內容輔助(非微信相關)

八、微信 API 參考鏈接