小程序图片审核要买第三方服务吗?官方接口其实免费——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("未配置内容安全密钥")
两个问题:
- Mock 恒返回"通过" —— 开发环境一切正常,生产一旦忘记配置,违禁图直接上线
- 未配置就抛异常 —— 会阻塞用户发布(体验问题),而图片审核又不像文本,没有本地词库可以兜底
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),政策可能调整,请以官方文档为准。