Просмотр исходного кода

add: [小程序] 增加 安全风控、内容安全1.0 & 2.0、code换取手机号 (#554)

* add: [小程序] 增加 安全风控、内容安全1.0 & 2.0、code换取手机号

* add: check suggest docs

* fix

* fix

Co-authored-by: luoyu <luoyu@medlinker.com>
save95 4 лет назад
Родитель
Сommit
56350c3655

+ 48 - 1
doc/api/miniprogram.md

@@ -1,3 +1,50 @@
 # 小程序
 
-TODO
+## 基础接口
+
+TODO
+
+## 内容安全
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/sec-check/security.mediaCheckAsync.html)
+
+|               名称                | 请求方式 | URL                              | 是否已实现 | 使用方法                               |
+| :-------------------------------: | -------- | :------------------------------- | ---------- | -------------------------------------- |
+| 异步校验图片/音频 <sub>v1.0</sub> | POST     | /wxa/media_check_async           | YES        | (security *Security) MediaCheckAsyncV1 |
+| 同步校验一张图片 <sub>v1.0</sub>  | POST     | /wxa/img_sec_check               | YES        | (security *Security) ImageCheckV1      |
+|         异步校验图片/音频         | POST     | /wxa/media_check_async?version=2 | YES        | (security *Security) MediaCheckAsync   |
+| 同步检查一段文本 <sub>v1.0</sub>  | 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 |
+

+ 13 - 0
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}
+}

+ 54 - 0
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
+}

+ 4 - 0
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 {

+ 18 - 0
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)

+ 60 - 0
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
+}

+ 256 - 0
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
+}