跳转到主要内容

一、整体架构

方案 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 参考链接