W weiserv
← 返回博客

小程序图片审核要买第三方服务吗?官方接口其实免费——imgSecCheck 接入实录与失败安全设计

小程序图片审核要买第三方服务吗?官方接口其实免费

关键词:小程序 / 图片审核 / imgSecCheck / 内容安全 / UGC / Python

一、先纠正一个误解

如果你的小程序有用户上传图片(头像、商品图、评论配图、社区发帖),就必须接内容安全审核——这是平台的硬性要求。

但"必须接"不等于"必须买"。

方案 费用 额度 适用
微信官方 imgSecCheck 免费 2000 次/分、20 万次/天(单 appId) 满足合规最低要求,中小规模够用
腾讯云 IMS 约 25 元/万张(按量),套餐可低至 22 元/万张 首单免费 3000 张/15 天 需要 OCR 文字审核、AI 生成识别、自定义策略等增强能力

额度与参数依据微信官方文档(核实日期 2026-09)。政策可能调整,以官方文档为准。

结论:先用官方免费接口满足合规;真需要 OCR 文字识别、自定义策略这些增强能力时,再上付费服务。别一上来就花钱。


二、为什么必须接

小程序平台对用户生成内容(UGC)的安全审核是强制的。涉及这些场景都必须接:

  • 用户上传头像 / 照片
  • 社区发帖、评论配图
  • 电商商品图
  • 用户分享的图片

平台规范明确禁止「以本地关键词过滤、人工初审替代官方接口审核」——人工审核只能作为补充,不能替代

处罚梯度:警告限期整改 → 下架 → 严重违规永久封禁。

还有个前置条件容易被忽略:小程序必须是企业/组织主体并完成微信认证,个人主体无法调用内容安全接口,也开不了支付。


三、接入实战:完整代码

下面以 Python(Django)为例,走一遍完整流程。

3.1 获取并缓存 access_token

access_token 有效期 7200 秒,必须缓存,不能每次请求都去换。

def _wx_access_token() -> str:
    import urllib.parse
    import urllib.request
    from django.core.cache import cache

    appid = os.getenv("WECHAT_APPID")
    secret = os.getenv("WECHAT_SECRET")
    if not (appid and secret):
        return ""

    cached = cache.get("wx_access_token")
    if cached:
        return cached

    params = urllib.parse.urlencode({
        "grant_type": "client_credential",
        "appid": appid,
        "secret": secret,
    })
    try:
        with urllib.request.urlopen(TOKEN_URL + "?" + params, timeout=5) as resp:
            data = json.loads(resp.read().decode("utf-8"))
    except Exception as e:
        logger.error("获取 access_token 失败: %s", e)
        return ""

    token = data.get("access_token", "")
    if token:
        cache.set("wx_access_token", token, 7200 - 300)  # 提前 5 分钟刷新
    else:
        logger.error("access_token 返回异常: %s", data)
    return token

3.2 图片压缩:三个约束缺一不可

官方接口对图片有严格限制,不处理会直接报错:

约束
文件大小 ≤ 1MB
尺寸 ≤ 750×1334
格式 PNG / JPEG / JPG / GIF
def _compress_for_wx(data: bytes) -> bytes:
    import io
    from PIL import Image

    img = Image.open(io.BytesIO(data))

    # ① 透明通道转 RGB —— 最容易漏,PNG 直接存 JPEG 会报错
    if img.mode in ("RGBA", "P", "LA"):
        img = img.convert("RGB")

    # ② 等比缩放(thumbnail 而非 resize,避免变形)
    img.thumbnail((750, 1334), Image.LANCZOS)

    # ③ 质量递减,直到满足 1MB
    quality = 85
    while quality > 30:
        buf = io.BytesIO()
        img.save(buf, format="JPEG", quality=quality)
        if buf.tell() <= 1024 * 1024:
            return buf.getvalue()
        quality -= 15

    buf = io.BytesIO()
    img.save(buf, format="JPEG", quality=30)
    return buf.getvalue()

3.3 multipart 上传(不引入额外依赖)

接口要求 multipart/form-data 上传。用标准库 urllib 手写即可,不必为这一个接口引入 requests

def _post_multipart(url, field, filename, content, content_type="image/jpeg", timeout=10):
    import urllib.request
    import uuid

    boundary = uuid.uuid4().hex
    body = bytearray()
    body.extend(f"--{boundary}\r\n".encode())
    body.extend(
        f'Content-Disposition: form-data; name="{field}"; filename="{filename}"\r\n'.encode()
    )
    body.extend(f"Content-Type: {content_type}\r\n\r\n".encode())
    body.extend(content)
    body.extend(f"\r\n--{boundary}--\r\n".encode())

    req = urllib.request.Request(
        url, data=bytes(body),
        headers={"Content-Type": f"multipart/form-data; boundary={boundary}"},
        method="POST",
    )
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        return json.loads(resp.read().decode("utf-8"))

3.4 调用与结果处理

errcode = result.get("errcode", result.get("errCode", -1))
if errcode == 0:
    return "PASS"        # 内容正常
if errcode == 87014:
    return "REJECT"      # 命中违规,拦截
# 其余错误码(频率限制、参数错误等)不能当"通过"处理
raise RuntimeError(f"审核返回异常 errcode={errcode}")

四、★ 真正重要的部分:失败安全设计

代码谁都能写,但下面这个设计才是关键,也是很多教程不讲的地方

4.1 一个危险的默认实现

很多人的第一版会写成这样:

def check_image(url):
    if USE_MOCK:
        return "PASS"        # ← 开发环境直接放行
    # 生产环境接第三方
    raise NotImplementedError("未配置内容安全密钥")

两个问题

  1. Mock 恒返回"通过" —— 开发环境一切正常,生产一旦忘记配置,违禁图直接上线
  2. 未配置就抛异常 —— 会阻塞用户发布(体验问题),而图片审核又不像文本,没有本地词库可以兜底

4.2 正确的思路:降级方向必须是"更安全"

核心原则:安全类功能不可用时,降级方向应该是更安全(转人工、拦截),而不是更宽松(放行)。

改成这样:

def check_image(url: str) -> str:
    if USE_MOCK:
        logger.warning("图片审核 MOCK 模式,返回通过(仅开发环境)")
        return "PASS"

    # 1) 微信官方(免费,优先)
    if os.getenv("WECHAT_APPID") and os.getenv("WECHAT_SECRET"):
        try:
            return _wx_img_check(url)
        except Exception as e:
            logger.error("微信图片审核异常,转人工复核: %s", e)
            return "REVIEW"      # ← 转人工,不是放行

    # 2) 腾讯云 IMS(付费增强)
    if os.getenv("TENCENTCLOUD_SECRET_ID"):
        logger.error("已配置密钥但未实现 IMS 调用,转人工复核")
        return "REVIEW"

    # 3) 什么都没配 → 转人工,绝不静默放行
    logger.error("未配置任何图片审核通道,图片转人工复核,不放行")
    return "REVIEW"

三档返回值的语义

返回值 含义 业务处理
PASS 通过 直接展示
REVIEW 转人工 进入运营审核队列,不阻塞用户发布
REJECT 拦截 拒绝并提示用户

为什么是 REVIEW 而不是 REJECT

  • 返回 REJECT(拦截)→ 服务一挂用户就发不了图,误伤正常内容
  • 返回 PASS(放行)→ 违禁图上线,这是合规事故
  • 返回 REVIEW(转人工)→ 既不漏放,也不阻塞,是唯一稳妥的降级方向

五、几个容易踩的坑

表现 解法
图片没压缩 接口报参数错误 必须满足 ≤1MB + ≤750×1334
PNG 透明图 存 JPEG 报异常 convert("RGB")
access_token 没缓存 频繁请求被限流 缓存 7200 秒,提前 5 分钟刷新
个人主体小程序 接口直接不可用 必须是企业主体 + 微信认证
把"接口报错"当"通过" 违禁图漏放 异常必须转人工,不能默认放行
依赖第三方却无降级 服务故障即放行 本地前置校验 + 失败转人工

六、一页纸检查清单

接入图片审核前,对着过一遍:

# 检查项
1 小程序是企业/组织主体且已微信认证?
2 所有用户上传图片的场景都已覆盖?
3 图片压缩满足 ≤1MB / ≤750×1334?
4 PNG/RGBA 是否转 RGB?
5 access_token 是否缓存?
6 审核失败时是否转人工而非放行? ← 最关键
7 未配置任何通道时是否明确告警?
8 是否只依赖第三方、没有本地前置兜底?

七、写在最后

这篇文章其实想说两件事:

第一件是关于成本——合规不等于花钱。平台官方往往已经提供了满足最低要求的免费方案,先摸清楚再决定,别一上来就买服务。

第二件,也是更重要的一件,是关于设计

任何安全/审核类功能,降级方向必须是"更安全",而不是"更宽松"。

审核服务挂了,应该是"转人工",不是"放行"。配置忘了,应该是"明确告警 + 转人工",不是"静默通过"。

这个原则不只适用于图片审核——内容过滤、风控校验、权限检查,所有"拦不住就出事"的环节都适用

代码写错一眼能看出来,但"失败时默认放行"这种设计错误,往往在出事故那天才会被发现。


本文代码为本人项目中实际实现并验证通过的版本。接口额度与参数依据微信官方文档(核实日期 2026-09),政策可能调整,请以官方文档为准。

广告位占位 · post-inline