From 56350c3655c9dc22e045388ac43adf67256d79b1 Mon Sep 17 00:00:00 2001 From: save95 <3973008+save95@users.noreply.github.com> Date: Fri, 22 Apr 2022 10:09:27 +0800 Subject: [PATCH] =?UTF-8?q?add:=20[=E5=B0=8F=E7=A8=8B=E5=BA=8F]=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20=E5=AE=89=E5=85=A8=E9=A3=8E=E6=8E=A7?= =?UTF-8?q?=E3=80=81=E5=86=85=E5=AE=B9=E5=AE=89=E5=85=A81.0=20&=202.0?= =?UTF-8?q?=E3=80=81code=E6=8D=A2=E5=8F=96=E6=89=8B=E6=9C=BA=E5=8F=B7=20(#?= =?UTF-8?q?554)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add: [小程序] 增加 安全风控、内容安全1.0 & 2.0、code换取手机号 * add: check suggest docs * fix * fix Co-authored-by: luoyu --- doc/api/miniprogram.md | 49 ++++- miniprogram/business/business.go | 13 ++ miniprogram/business/phone_number.go | 54 ++++++ miniprogram/content/content.go | 4 + miniprogram/miniprogram.go | 18 ++ miniprogram/riskcontrol/riskcontrol.go | 60 ++++++ miniprogram/security/security.go | 256 +++++++++++++++++++++++++ 7 files changed, 453 insertions(+), 1 deletion(-) create mode 100644 miniprogram/business/business.go create mode 100644 miniprogram/business/phone_number.go create mode 100644 miniprogram/riskcontrol/riskcontrol.go create mode 100644 miniprogram/security/security.go diff --git a/doc/api/miniprogram.md b/doc/api/miniprogram.md index 5f3c1f3..75d2a24 100644 --- a/doc/api/miniprogram.md +++ b/doc/api/miniprogram.md @@ -1,3 +1,50 @@ # 小程序 -TODO \ No newline at end of file +## 基础接口 + +TODO + +## 内容安全 + +[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/sec-check/security.mediaCheckAsync.html) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| :-------------------------------: | -------- | :------------------------------- | ---------- | -------------------------------------- | +| 异步校验图片/音频 v1.0 | POST | /wxa/media_check_async | YES | (security *Security) MediaCheckAsyncV1 | +| 同步校验一张图片 v1.0 | POST | /wxa/img_sec_check | YES | (security *Security) ImageCheckV1 | +| 异步校验图片/音频 | POST | /wxa/media_check_async?version=2 | YES | (security *Security) MediaCheckAsync | +| 同步检查一段文本 v1.0 | POST | /wxa/msg_sec_check | YES | (security *Security) MsgCheckV1 | +| 同步检查一段文本 | POST | /wxa/msg_sec_check?version=2 | YES | (security *Security) MsgCheck | + + +## OCR + +[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/ocr/ocr.bankcard.html) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| :------------: | -------- | :--------------------- | ---------- | -------- | +| 银行卡识别 | POST | /cv/ocr/bankcard | | | +| 营业执照识别 | POST | /cv/ocr/bizlicense | | | +| 驾驶证识别 | POST | /cv/ocr/drivinglicense | | | +| 身份证识别 | POST | /cv/ocr/idcard | | | +| 通用印刷体识别 | POST | /cv/ocr/comm | | | +| 行驶证识别 | POST | /cv/ocr/driving | | | + + +## 手机号 + +[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/phonenumber/phonenumber.getPhoneNumber.html) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| :----------------: | -------- | :------------------------------- | ---------- | ----------------------------------- | +| code换取用户手机号 | POST | /wxa/business/getuserphonenumber | YES | (business *Business) GetPhoneNumber | + + +## 安全风控 + +[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/safety-control-capability/riskControl.getUserRiskRank.html) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | +| :----------------: | -------- | :------------------- | ---------- | ------------------------------------------ | +| 获取用户的安全等级 | POST | /wxa/getuserriskrank | YES | (riskControl *RiskControl) GetUserRiskRank | + diff --git a/miniprogram/business/business.go b/miniprogram/business/business.go new file mode 100644 index 0000000..1b0d826 --- /dev/null +++ b/miniprogram/business/business.go @@ -0,0 +1,13 @@ +package business + +import "github.com/silenceper/wechat/v2/miniprogram/context" + +// Business 业务 +type Business struct { + *context.Context +} + +// NewBusiness init +func NewBusiness(ctx *context.Context) *Business { + return &Business{ctx} +} diff --git a/miniprogram/business/phone_number.go b/miniprogram/business/phone_number.go new file mode 100644 index 0000000..bf99057 --- /dev/null +++ b/miniprogram/business/phone_number.go @@ -0,0 +1,54 @@ +package business + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + getPhoneNumberURL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s" +) + +// GetPhoneNumberRequest 获取手机号请求 +type GetPhoneNumberRequest struct { + Code string `json:"code"` // 手机号获取凭证 +} + +// PhoneInfo 手机号信息 +type PhoneInfo struct { + PhoneNumber string `json:"phoneNumber"` // 用户绑定的手机号(国外手机号会有区号) + PurePhoneNumber string `json:"purePhoneNumber"` // 没有区号的手机号 + CountryCode string `json:"countryCode"` // 区号 + Watermark struct { + AppID string `json:"appid"` // 小程序appid + Timestamp int64 `json:"timestamp"` // 用户获取手机号操作的时间戳 + } `json:"watermark"` +} + +// GetPhoneNumber code换取用户手机号。 每个code只能使用一次,code的有效期为5min +func (business *Business) GetPhoneNumber(in *GetPhoneNumberRequest) (info PhoneInfo, err error) { + accessToken, err := business.GetAccessToken() + if err != nil { + return + } + + uri := fmt.Sprintf(getPhoneNumberURL, accessToken) + response, err := util.PostJSON(uri, in) + if err != nil { + return + } + + // 使用通用方法返回错误 + var resp struct { + util.CommonError + PhoneInfo PhoneInfo `json:"phone_info"` + } + err = util.DecodeWithError(response, &resp, "business.GetPhoneNumber") + if nil != err { + return + } + + info = resp.PhoneInfo + return +} diff --git a/miniprogram/content/content.go b/miniprogram/content/content.go index 91f6921..d53f2db 100644 --- a/miniprogram/content/content.go +++ b/miniprogram/content/content.go @@ -24,6 +24,8 @@ func NewContent(ctx *context.Context) *Content { // CheckText 检测文字 // @text 需要检测的文字 +// Deprecated +// 采用 security.MsgCheckV1 替代,返回值更加丰富 func (content *Content) CheckText(text string) error { accessToken, err := content.GetAccessToken() if err != nil { @@ -44,6 +46,8 @@ func (content *Content) CheckText(text string) error { // CheckImage 检测图片 // 所传参数为要检测的图片文件的绝对路径,图片格式支持PNG、JPEG、JPG、GIF, 像素不超过 750 x 1334,同时文件大小以不超过 300K 为宜,否则可能报错 // @media 图片文件的绝对路径 +// Deprecated +// 采用 security.ImageCheckV1 替代,返回值更加丰富 func (content *Content) CheckImage(media string) error { accessToken, err := content.GetAccessToken() if err != nil { diff --git a/miniprogram/miniprogram.go b/miniprogram/miniprogram.go index 6741574..d76d7c2 100644 --- a/miniprogram/miniprogram.go +++ b/miniprogram/miniprogram.go @@ -4,6 +4,7 @@ import ( "github.com/silenceper/wechat/v2/credential" "github.com/silenceper/wechat/v2/miniprogram/analysis" "github.com/silenceper/wechat/v2/miniprogram/auth" + "github.com/silenceper/wechat/v2/miniprogram/business" "github.com/silenceper/wechat/v2/miniprogram/config" "github.com/silenceper/wechat/v2/miniprogram/content" "github.com/silenceper/wechat/v2/miniprogram/context" @@ -11,6 +12,8 @@ import ( "github.com/silenceper/wechat/v2/miniprogram/message" "github.com/silenceper/wechat/v2/miniprogram/privacy" "github.com/silenceper/wechat/v2/miniprogram/qrcode" + "github.com/silenceper/wechat/v2/miniprogram/riskcontrol" + "github.com/silenceper/wechat/v2/miniprogram/security" "github.com/silenceper/wechat/v2/miniprogram/shortlink" "github.com/silenceper/wechat/v2/miniprogram/subscribe" "github.com/silenceper/wechat/v2/miniprogram/tcb" @@ -59,6 +62,11 @@ func (miniProgram *MiniProgram) GetAnalysis() *analysis.Analysis { return analysis.NewAnalysis(miniProgram.ctx) } +// GetBusiness 业务接口 +func (miniProgram *MiniProgram) GetBusiness() *business.Business { + return business.NewBusiness(miniProgram.ctx) +} + // GetPrivacy 小程序隐私协议相关API func (miniProgram *MiniProgram) GetPrivacy() *privacy.Privacy { return privacy.NewPrivacy(miniProgram.ctx) @@ -99,6 +107,16 @@ func (miniProgram *MiniProgram) GetURLLink() *urllink.URLLink { return urllink.NewURLLink(miniProgram.ctx) } +// GetRiskControl 安全风控接口 +func (miniProgram *MiniProgram) GetRiskControl() *riskcontrol.RiskControl { + return riskcontrol.NewRiskControl(miniProgram.ctx) +} + +// GetSecurity 内容安全接口 +func (miniProgram *MiniProgram) GetSecurity() *security.Security { + return security.NewSecurity(miniProgram.ctx) +} + // GetShortLink 小程序短链接口 func (miniProgram *MiniProgram) GetShortLink() *shortlink.ShortLink { return shortlink.NewShortLink(miniProgram.ctx) diff --git a/miniprogram/riskcontrol/riskcontrol.go b/miniprogram/riskcontrol/riskcontrol.go new file mode 100644 index 0000000..2618772 --- /dev/null +++ b/miniprogram/riskcontrol/riskcontrol.go @@ -0,0 +1,60 @@ +package riskcontrol + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/miniprogram/context" + "github.com/silenceper/wechat/v2/util" +) + +const ( + getUserRiskRankURL = "https://api.weixin.qq.com/wxa/getuserriskrank?access_token=%s" +) + +// RiskControl 安全风控 +type RiskControl struct { + *context.Context +} + +// NewRiskControl init +func NewRiskControl(ctx *context.Context) *RiskControl { + return &RiskControl{ctx} +} + +// UserRiskRankRequest 获取用户安全等级请求 +type UserRiskRankRequest struct { + AppID string `json:"appid"` // 小程序 app id + OpenID string `json:"openid"` // 用户的 openid + Scene uint8 `json:"scene"` // 场景值,0:注册,1:营销作弊 + ClientIP string `json:"client_ip"` // 用户访问源ip + + Mobile string `json:"mobile_no"` // 用户手机号 + Email string `json:"email_address"` // 用户邮箱地址 + ExtendedInfo string `json:"extended_info"` // 额外补充信息 + IsTest bool `json:"is_test"` // false:正式调用,true:测试调用 +} + +// UserRiskRank 用户安全等级 +type UserRiskRank struct { + util.CommonError + UnionID int64 `json:"union_id"` // 唯一请求标识 + RiskRank uint8 `json:"risk_rank"` // 用户风险等级 +} + +// GetUserRiskRank 根据提交的用户信息数据获取用户的安全等级 risk_rank,无需用户授权。 +func (riskControl *RiskControl) GetUserRiskRank(in *UserRiskRankRequest) (res UserRiskRank, err error) { + accessToken, err := riskControl.GetAccessToken() + if err != nil { + return + } + + uri := fmt.Sprintf(getUserRiskRankURL, accessToken) + response, err := util.PostJSON(uri, in) + if err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &res, "GetUserRiskRank") + return +} diff --git a/miniprogram/security/security.go b/miniprogram/security/security.go new file mode 100644 index 0000000..7e4d5dc --- /dev/null +++ b/miniprogram/security/security.go @@ -0,0 +1,256 @@ +package security + +import ( + "fmt" + "strconv" + + "github.com/silenceper/wechat/v2/miniprogram/context" + "github.com/silenceper/wechat/v2/util" +) + +const ( + mediaCheckAsyncURL = "https://api.weixin.qq.com/wxa/media_check_async?access_token=%s" + imageCheckURL = "https://api.weixin.qq.com/wxa/img_sec_check?access_token=%s" + msgCheckURL = "https://api.weixin.qq.com/wxa/msg_sec_check?access_token=%s" +) + +// Security 内容安全 +type Security struct { + *context.Context +} + +// NewSecurity init +func NewSecurity(ctx *context.Context) *Security { + return &Security{ctx} +} + +// MediaCheckAsyncV1Request 图片/音频异步校验请求参数 +type MediaCheckAsyncV1Request struct { + MediaURL string `json:"media_url"` // 要检测的图片或音频的url,支持图片格式包括jpg, jepg, png, bmp, gif(取首帧),支持的音频格式包括mp3, aac, ac3, wma, flac, vorbis, opus, wav + MediaType uint8 `json:"media_type"` // 1:音频;2:图片 +} + +// MediaCheckAsyncV1 异步校验图片/音频是否含有违法违规内容 +// Deprecated +// 在2021年9月1日停止更新,请尽快更新至 2.0 接口。建议使用 MediaCheckAsync +func (security *Security) MediaCheckAsyncV1(in *MediaCheckAsyncV1Request) (traceID string, err error) { + accessToken, err := security.GetAccessToken() + if err != nil { + return + } + + uri := fmt.Sprintf(mediaCheckAsyncURL, accessToken) + response, err := util.PostJSON(uri, in) + if err != nil { + return + } + + // 使用通用方法返回错误 + var res struct { + util.CommonError + TraceID string `json:"trace_id"` + } + err = util.DecodeWithError(response, &res, "MediaCheckAsyncV1") + if err != nil { + return + } + + traceID = res.TraceID + return +} + +// MediaCheckAsyncRequest 图片/音频异步校验请求参数 +type MediaCheckAsyncRequest struct { + MediaURL string `json:"media_url"` // 要检测的图片或音频的url,支持图片格式包括jpg, jepg, png, bmp, gif(取首帧),支持的音频格式包括mp3, aac, ac3, wma, flac, vorbis, opus, wav + MediaType uint8 `json:"media_type"` // 1:音频;2:图片 + OpenID string `json:"openid"` // 用户的openid(用户需在近两小时访问过小程序) + Scene uint8 `json:"scene"` // 场景枚举值(1 资料;2 评论;3 论坛;4 社交日志) +} + +// MediaCheckAsync 异步校验图片/音频是否含有违法违规内容 +func (security *Security) MediaCheckAsync(in *MediaCheckAsyncRequest) (traceID string, err error) { + accessToken, err := security.GetAccessToken() + if err != nil { + return + } + + var req struct { + MediaCheckAsyncRequest + Version uint `json:"version"` // 接口版本号,2.0版本为固定值2 + } + req.MediaCheckAsyncRequest = *in + req.Version = 2 + + uri := fmt.Sprintf(mediaCheckAsyncURL, accessToken) + response, err := util.PostJSON(uri, req) + if err != nil { + return + } + + // 使用通用方法返回错误 + var res struct { + util.CommonError + TraceID string `json:"trace_id"` + } + err = util.DecodeWithError(response, &res, "MediaCheckAsync") + if err != nil { + return + } + + traceID = res.TraceID + return +} + +// ImageCheckV1 校验一张图片是否含有违法违规内容(同步) +// https://developers.weixin.qq.com/miniprogram/dev/framework/security.imgSecCheck.html +// Deprecated +// 在2021年9月1日停止更新。建议使用 MediaCheckAsync +func (security *Security) ImageCheckV1(filename string) (err error) { + accessToken, err := security.GetAccessToken() + if err != nil { + return + } + + uri := fmt.Sprintf(imageCheckURL, accessToken) + response, err := util.PostFile("media", filename, uri) + if err != nil { + return + } + + // 使用通用方法返回错误 + return util.DecodeWithCommonError(response, "ImageCheckV1") +} + +// CheckSuggest 检查建议 +type CheckSuggest string + +const ( + // CheckSuggestRisky 违规风险建议 + CheckSuggestRisky CheckSuggest = "risky" + // CheckSuggestPass 安全 + CheckSuggestPass CheckSuggest = "pass" + // CheckSuggestReview 需要审查 + CheckSuggestReview CheckSuggest = "review" +) + +// MsgScene 文本场景 +type MsgScene uint8 + +const ( + // MsgSceneMaterial 资料文件检查场景 + MsgSceneMaterial MsgScene = iota + 1 + // MsgSceneComment 评论 + MsgSceneComment + // MsgSceneForum 论坛 + MsgSceneForum + // MsgSceneSocialLog 社交日志 + MsgSceneSocialLog +) + +// CheckLabel 检查命中标签 +type CheckLabel int + +func (cl CheckLabel) String() string { + switch cl { + case 100: + return "正常" + case 10001: + return "广告" + case 20001: + return "时政" + case 20002: + return "色情" + case 20003: + return "辱骂" + case 20006: + return "违法犯罪" + case 20008: + return "欺诈" + case 20012: + return "低俗" + case 20013: + return "版权" + case 21000: + return "其他" + default: + return strconv.Itoa(int(cl)) + } +} + +// MsgCheckRequest 文本检查请求 +type MsgCheckRequest struct { + OpenID string `json:"openid"` // 用户的openid(用户需在近两小时访问过小程序) + Scene MsgScene `json:"scene"` // 场景枚举值(1 资料;2 评论;3 论坛;4 社交日志) + Content string `json:"content"` // 需检测的文本内容,文本字数的上限为 2500 字,需使用 UTF-8 编码 + Nickname string `json:"nickname"` // (非必填)用户昵称,需使用UTF-8编码 + Title string `json:"title"` // (非必填)文本标题,需使用UTF-8编码 + Signature string `json:"signature"` // (非必填)个性签名,该参数仅在资料类场景有效(scene=1),需使用UTF-8编码 +} + +// MsgCheckResponse 文本检查响应 +type MsgCheckResponse struct { + util.CommonError + TraceID string `json:"trace_id"` // 唯一请求标识 + Result struct { + Suggest CheckSuggest `json:"suggest"` // 建议 + Label CheckLabel `json:"label"` // 命中标签 + } `json:"result"` // 综合结果 + Detail []struct { + ErrCode int64 `json:"errcode"` // 错误码,仅当该值为0时,该项结果有效 + Strategy string `json:"strategy"` // 策略类型 + Suggest string `json:"suggest"` // 建议 + Label CheckLabel `json:"label"` // 命中标签 + Prob uint `json:"prob"` // 置信度。0-100,越高代表越有可能属于当前返回的标签(label) + Keyword string `json:"keyword"` // 命中的自定义关键词 + } `json:"detail"` // 详细检测结果 +} + +// MsgCheckV1 检查一段文本是否含有违法违规内容 +// Deprecated +// 在2021年9月1日停止更新,请尽快更新至 2.0 接口。建议使用 MsgCheck +func (security *Security) MsgCheckV1(content string) (res MsgCheckResponse, err error) { + accessToken, err := security.GetAccessToken() + if err != nil { + return + } + + var req struct { + Content string `json:"content"` + } + req.Content = content + + uri := fmt.Sprintf(msgCheckURL, accessToken) + response, err := util.PostJSON(uri, req) + if err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &res, "security.MsgCheckV1") + return +} + +// MsgCheck 检查一段文本是否含有违法违规内容 +func (security *Security) MsgCheck(in *MsgCheckRequest) (res MsgCheckResponse, err error) { + accessToken, err := security.GetAccessToken() + if err != nil { + return + } + + var req struct { + MsgCheckRequest + Version uint `json:"version"` + } + req.MsgCheckRequest = *in + req.Version = 2 + + uri := fmt.Sprintf(msgCheckURL, accessToken) + response, err := util.PostJSON(uri, req) + if err != nil { + return + } + + // 使用通用方法返回错误 + err = util.DecodeWithError(response, &res, "security.MsgCheck") + return +}