☰
小程序UGC内容安全检测接入实战:文本鉴黄、异步图片审核与误杀兜底
2026/10/7 20:35:41 网站建设 项目流程

背景

只要小程序里开放了用户生成内容(评价、晒单、头像昵称、社区发帖、客服留言),就绕不开平台合规要求。以微信生态为例,运营规范明确要求平台对UGC内容履行安全审核义务,实践中通常采用官方提供的内容安全接口(security.msgSecCheck文本检测、security.mediaCheckAsync媒体异步检测)完成初筛,再叠加人工复审。本文复盘我们在一个带图文评价功能的商城项目中接入这套能力的全过程,包括接口选型、签名与token管理、异步回调设计,以及上线后踩到的4个坑。

一、接口能力梳理与选型

内容安全相关接口有几个关键差异,选错会直接导致架构返工:

  • msgSecCheck(v2):文本检测,同步返回。入参为openid、scene、version=2、content,返回里包含result.suggest(pass/review/risky)和detail.label标签枚举。单条文本长度有限制(按官方文档当前为2500个汉字以内),超长需分段。
  • imgSecCheck:旧版图片同步检测,对图片大小有较严格限制(1MB内),大文件要自己压缩,高峰期同步等待体验差,新项目不建议作为主方案。
  • mediaCheckAsync:媒体异步检测,支持图片和视频,通过消息推送回调结果,适合晒单图、头像这类不需要即时拦的场景。

我们的最终分工:评价文字走msgSecCheck同步拦截;晒单图片走mediaCheckAsync,内容先落库为"审核中"状态,回调驱动状态流转;用户头像因为要即时生效,走前端压缩后同步检测的降级路径。

二、access_token 的正确管理姿势

所有接口都依赖access_token,有效期7200秒,且全系统共享一个配额——多个服务各自刷新会互相顶掉旧token,表现为随机的40001错误。踩过这个坑后,统一收敛到一个token服务:

typeTokenServicestruct{mu sync.Mutex tokenstringexpiresAt time.Time appIDstringappSecretstring}func(s*TokenService)Get(ctx context.Context)(string,error){s.mu.Lock()defers.mu.Unlock()// 提前300秒过期,避免临界点失效ifs.token!=""&&time.Now().Before(s.expiresAt.Add(-300*time.Second)){returns.token,nil}url:=fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",s.appID,s.appSecret,)req,_:=http.NewRequestWithContext(ctx,"GET",url,nil)resp,err:=http.DefaultClient.Do(req)iferr!=nil{return"",err}deferresp.Body.Close()varretstruct{AccessTokenstring`json:"access_token"`ExpiresInint`json:"expires_in"`ErrCodeint`json:"errcode"`ErrMsgstring`json:"errmsg"`}iferr:=json.NewDecoder(resp.Body).Decode(&ret);err!=nil{return"",err}ifret.ErrCode!=0{return"",fmt.Errorf("token api error: %d %s",ret.ErrCode,ret.ErrMsg)}s.token=ret.AccessToken s.expiresAt=time.Now().Add(time.Duration(ret.ExpiresIn)*time.Second)returns.token,nil}

多实例部署时,单机锁不够用,需要把token放到Redis并加分布式锁,保证全局只有一个实例执行刷新,其余实例读缓存。

三、文本同步检测:分段与标签处理

长评价超过接口长度上限时不能硬截断,否则后半段完全失控。我们的处理是按句号/换行等语义边界切片,每片独立检测,任一叶片命中 risky 即整体拒绝,全部 pass 才放行,出现 review 则转人工:

funccheckText(ctx context.Context,ts*TokenService,openid,contentstring)(string,error){token,err:=ts.Get(ctx)iferr!=nil{return"",err}chunks:=splitByRune(content,2000)// 留余量,按语义边界切worst:="pass"for_,chunk:=rangechunks{body,_:=json.Marshal(map[string]interface{}{"version":2,"scene":2,// 2=评论场景,按官方枚举传"openid":openid,"content":chunk,})url:="https://api.weixin.qq.com/wxa/msg_sec_check?access_token="+token resp,err:=http.Post(url,"application/json",bytes.NewReader(body))iferr!=nil{return"",err}varretstruct{ErrCodeint`json:"errcode"`Resultstruct{Suggeststring`json:"suggest"`}`json:"result"`}json.NewDecoder(resp.Body).Decode(&ret)resp.Body.Close()ifret.ErrCode!=0{// 见坑3:40001要强制刷新token重试一次return"",fmt.Errorf("sec check failed: %d",ret.ErrCode)}worst=mergeSuggest(worst,ret.Result.Suggest)}returnworst,nil}// risky优先级最高,review次之funcmergeSuggest(a,bstring)string{rank:=map[string]int{"pass":0,"review":1,"risky":2}ifrank[b]>rank[a]{returnb}returna}

四、图片异步检测:状态机 + 回调幂等

mediaCheckAsync的核心是回调不可靠假设——推送可能延迟、可能重复、可能因服务器抖动而丢失。图片记录设计成显式状态机:

pending(提交成功) → reviewing(检测中) → blocked(命中) ↘ pass(放行) 任何状态 → callback_timeout(超过SLA未收到回调,走主动复查)

提交时拿到官方返回的trace_id,落库时与业务图片ID绑定。回调消息体里用trace_id反查记录,处理逻辑必须幂等:

funcHandleMediaCallback(ctx context.Context,msg*MediaCheckCallback)error{// 用trace_id做唯一约束,重复回调直接返回成功record,err:=repo.GetByTraceID(ctx,msg.TraceID)iferr!=nil{returnerr}ifrecord.IsFinal(){// 已是终态,幂等直接ACKreturnnil}suggest:=msg.Result.Suggestswitchsuggest{case"risky":returnrepo.Transit(ctx,record.ID,"reviewing","blocked")case"pass":returnrepo.Transit(ctx,record.ID,"reviewing","pass")default:returnrepo.Transit(ctx,record.ID,"reviewing","manual_review")}}

状态流转用UPDATE ... WHERE status='reviewing'的乐观条件兜底并发,防止两条重复回调把状态改乱。另外必须建一个定时兜底任务:对超过30分钟仍处于reviewing的记录,主动调用结果查询或重新提交。上线第一周,这个兜底任务救回了约3%因回调丢失卡在中间态的图片。

五、踩坑记录

坑1:测试内容固定,上线首日漏过变体

联调时一直用官方文档给的固定测试文本,全部命中、流程正常,误以为召回没问题。上线后发现各种谐音、拆字、表情包夹字绕过初筛。补救措施:对review级别记录全部留档,定期回流补充自建敏感词库做二次匹配,同时关注官方标签枚举更新,把新标签纳入处置策略。

坑2:图片压缩过度导致判定失真

为满足旧版同步接口的体积限制,前端把晒单图压到300KB以下,画面细节糊到连人工都看不清,检测侧同样出现误判。改用异步接口后恢复正常画质上传,只做尺寸(最长边1280)和格式(统一转JPEG,quality 0.82)的轻量处理,误杀率明显下降。

坑3:40001错误直接失败,没有重试

access_token临界过期或被其他服务顶掉时,接口返回40001。最初实现直接把错误抛给用户,评价发送失败。正确做法是把40001/42001这类token类错误识别出来,强制作废缓存token并刷新一次,重试原请求;只有重试仍失败才降级(比如转"审核中"稍后处理),而不是让业务直接报错。

坑4:误杀没有申诉通道,正常用户被永久拦截

一次促销词在文本模型上被标成review转人工,客服没及时处理,用户的好评卡了两天,投诉到平台。之后做了两件事:一是评价类内容即便判定review也先保存为"仅自己可见",给出"内容审核中,预计X小时内展示"的明确预期,而不是让用户以为内容丢了;二是在客服后台提供一键复核入口,人工确认安全后即时放行。审核系统不可能零误杀,兜底体验比追求极致召回更重要。

六、上线后的监控指标

建议至少盯三个指标:接口层错误率(区分token类、频控类、内容类)、各suggest级别占比(risky比例突然飙升往往是被刷垃圾内容的信号)、回调延迟与丢失率(配合兜底任务的触发量观察)。配合内容量增长,还需要评估接口频控,高频场景给检测调用加本地限流,避免突刺流量触发官方频率限制后整段业务降级。

小结

内容安全接入的技术难度不在调接口,而在三个工程细节:token统一管理避免互相顶号、异步回调按状态机和幂等设计并接受"回调会丢"、为误杀和失败预留用户可感知的兜底路径。把这三点做扎实,审核能力才不会成为业务的不稳定因素。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询