Explorar el Código

Merge branch 'master' into develop

silenceper hace 6 años
padre
commit
de140f1037

+ 12 - 0
.github/FUNDING.yml

@@ -0,0 +1,12 @@
+# These are supported funding model platforms
+
+github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
+patreon: # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+custom: https://silenceper.com/img/wechat-pay.jpeg

+ 2 - 0
.github/ISSUE_TEMPLATE.md

@@ -0,0 +1,2 @@
+## 问题及现象
+<!-- 描述你的问题现象,报错**贴截图**粘贴或者贴具体信息,提供**必要的代码段**

+ 1 - 0
.travis.yml

@@ -1,6 +1,7 @@
 language: go
 
 go:
+  - 1.13.x
   - 1.12.x
   - 1.11.x
   - 1.10.x

+ 2 - 2
README.md

@@ -283,8 +283,8 @@ type Reply struct {
 ####  回复图片消息
 ```go
 //mediaID 可通过素材管理-上上传多媒体文件获得
-image :=message.NewVideo("mediaID")
-return &message.Reply{message.MsgTypeVideo, image}
+image :=message.NewImage("mediaID")
+return &message.Reply{message.MsgTypeImage, image}
 ```
 ####  回复视频消息
 ```go

+ 116 - 0
device/authorize.go

@@ -0,0 +1,116 @@
+package device
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/silenceper/wechat/util"
+)
+
+const (
+	// DeviceAdd 添加设备标识
+	DeviceAdd = iota
+	// DeviceUpgrade 更新设备标识
+	DeviceUpgrade
+)
+
+type reqDeviceAuthorize struct {
+	// 设备id的个数
+	DeviceNum string `json:"device_num"`
+	// 设备id的列表,json的array格式,其size必须等于device_num
+	DeviceList []ReqDevice `json:"device_list"`
+	// 请求操作的类型,限定取值为:0:设备授权(缺省值为0) 1:设备更新(更新已授权设备的各属性值)
+	OpType string `json:"op_type,omitempty"`
+	// 设备的产品编号(由微信硬件平台分配)。可在公众号设备功能管理页面查询。
+	//当 op_type 为‘0’,product_id 为‘1’时,不要填写 product_id 字段(会引起不必要错误);
+	//当 op_typy 为‘0’,product_id 不为‘1’时,必须填写 product_id 字段;
+	//当 op_type 为 1 时,不要填写 product_id 字段。
+	ProductID string `json:"product_id,omitempty"`
+}
+
+//ReqDevice 设备授权实体
+type ReqDevice struct {
+	// 设备的 device id
+	ID string `json:"id"`
+	// 设备的mac地址 格式采用16进制串的方式(长度为12字节),
+	// 不需要0X前缀,如: 1234567890AB
+	Mac string `json:"mac"`
+	//  支持以下四种连接协议:
+	//	android classic bluetooth – 1
+	//	ios classic bluetooth – 2
+	//	ble – 3
+	//	wifi -- 4
+	//	一个设备可以支持多种连接类型,用符号"|"做分割,客户端优先选择靠前的连接方式(优先级按|关系的排序依次降低),举例:
+	//	1:表示设备仅支持andiod classic bluetooth 1|2:表示设备支持andiod 和ios 两种classic bluetooth,但是客户端优先选择andriod classic bluetooth 协议,如果andriod classic bluetooth协议连接失败,再选择ios classic bluetooth协议进行连接
+	//	(注:安卓平台不同时支持BLE和classic类型)
+	ConnectProtocol string `json:"connect_protocol"`
+	//auth及通信的加密key,第三方需要将key烧制在设备上(128bit),格式采用16进制串的方式(长度为32字节),不需要0X前缀,如: 1234567890ABCDEF1234567890ABCDEF
+	AuthKey string `json:"auth_key"`
+	// 断开策略,目前支持: 1:退出公众号页面时即断开连接 2:退出公众号之后保持连接不断开
+	CloseStrategy string `json:"close_strategy"`
+	//连接策略,32位整型,按bit位置位,目前仅第1bit和第3bit位有效(bit置0为无效,1为有效;第2bit已被废弃),且bit位可以按或置位(如1|4=5),各bit置位含义说明如下:
+	//1:(第1bit置位)在公众号对话页面,不停的尝试连接设备
+	//4:(第3bit置位)处于非公众号页面(如主界面等),微信自动连接。当用户切换微信到前台时,可能尝试去连接设备,连上后一定时间会断开
+	ConnStrategy string `json:"conn_strategy"`
+	// auth version,设备和微信进行auth时,会根据该版本号来确认auth buf和auth key的格式(各version对应的auth buf及key的具体格式可以参看“客户端蓝牙外设协议”),该字段目前支持取值:
+	// 0:不加密的version
+	// 1:version 1
+	AuthVer string `json:"auth_ver"`
+	// 表示mac地址在厂商广播manufature data里含有mac地址的偏移,取值如下:
+	// -1:在尾部、
+	// -2:表示不包含mac地址 其他:非法偏移
+	ManuMacPos string `json:"manu_mac_pos"`
+	// 表示mac地址在厂商serial number里含有mac地址的偏移,取值如下:
+	// -1:表示在尾部
+	// -2:表示不包含mac地址 其他:非法偏移
+	SerMacPost string `json:"ser_mac_post"`
+	// 精简协议类型,取值如下:计步设备精简协议:1 (若该字段填1,connect_protocol 必须包括3。非精简协议设备切勿填写该字段)
+	BleSimpleProtocol string `json:"ble_simple_protocol,omitempty"`
+}
+
+//ResBaseInfo 授权回调实体
+type ResBaseInfo struct {
+	BaseInfo struct {
+		DeviceType string `json:"device_type"`
+		DeviceID   string `json:"device_id"`
+	} `json:"base_info"`
+}
+
+// 授权回调根信息
+type resDeviceAuthorize struct {
+	util.CommonError
+	Resp []ResBaseInfo `json:"resp"`
+}
+
+// DeviceAuthorize 设备授权
+func (d *Device) DeviceAuthorize(devices []ReqDevice, opType int, product string) (res []ResBaseInfo, err error) {
+	var accessToken string
+	accessToken, err = d.GetAccessToken()
+	if err != nil {
+		return
+	}
+
+	uri := fmt.Sprintf("%s?access_token=%s", uriAuthorize, accessToken)
+	req := reqDeviceAuthorize{
+		DeviceNum:  fmt.Sprintf("%d", len(devices)),
+		DeviceList: devices,
+		OpType:     fmt.Sprintf("%d", opType),
+		ProductID:  product,
+	}
+	var response []byte
+	response, err = util.PostJSON(uri, req)
+	if err != nil {
+		return nil, err
+	}
+	var result resDeviceAuthorize
+	err = json.Unmarshal(response, &result)
+	if err != nil {
+		return
+	}
+	if result.ErrCode != 0 {
+		err = fmt.Errorf("DeviceAuthorize Error , errcode=%d , errmsg=%s", result.ErrCode, result.ErrMsg)
+		return
+	}
+	res = result.Resp
+	return
+}

+ 106 - 0
device/bind.go

@@ -0,0 +1,106 @@
+package device
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/silenceper/wechat/util"
+)
+
+// ReqBind 设备绑定解绑共通实体
+type ReqBind struct {
+	Ticket   string `json:"ticket,omitempty"`
+	DeviceID string `json:"device_id"`
+	OpenID   string `json:"openid"`
+}
+type resBind struct {
+	BaseResp util.CommonError `json:"base_resp"`
+}
+
+// Bind 设备绑定
+func (d *Device) Bind(req ReqBind) (err error) {
+	var accessToken string
+	if accessToken, err = d.GetAccessToken(); err != nil {
+		return
+	}
+	uri := fmt.Sprintf("%s?access_token=%s", uriBind, accessToken)
+	var response []byte
+	if response, err = util.PostJSON(uri, req); err != nil {
+		return
+	}
+	var result resBind
+	if err = json.Unmarshal(response, &result); err != nil {
+		return
+	}
+	if result.BaseResp.ErrCode != 0 {
+		err = fmt.Errorf("DeviceBind Error , errcode=%d , errmsg=%s", result.BaseResp.ErrCode, result.BaseResp.ErrMsg)
+		return
+	}
+	return
+}
+
+// Unbind 设备解绑
+func (d *Device) Unbind(req ReqBind) (err error) {
+	var accessToken string
+	if accessToken, err = d.GetAccessToken(); err != nil {
+		return
+	}
+	uri := fmt.Sprintf("%s?access_token=%s", uriUnbind, accessToken)
+	var response []byte
+	if response, err = util.PostJSON(uri, req); err != nil {
+		return
+	}
+	var result resBind
+	if err = json.Unmarshal(response, &result); err != nil {
+		return
+	}
+	if result.BaseResp.ErrCode != 0 {
+		err = fmt.Errorf("DeviceBind Error , errcode=%d , errmsg=%s", result.BaseResp.ErrCode, result.BaseResp.ErrMsg)
+		return
+	}
+	return
+}
+
+// CompelBind 强制绑定用户和设备
+func (d *Device) CompelBind(req ReqBind) (err error) {
+	var accessToken string
+	if accessToken, err = d.GetAccessToken(); err != nil {
+		return
+	}
+	uri := fmt.Sprintf("%s?access_token=%s", uriCompelBind, accessToken)
+	var response []byte
+	if response, err = util.PostJSON(uri, req); err != nil {
+		return
+	}
+	var result resBind
+	if err = json.Unmarshal(response, &result); err != nil {
+		return
+	}
+	if result.BaseResp.ErrCode != 0 {
+		err = fmt.Errorf("DeviceBind Error , errcode=%d , errmsg=%s", result.BaseResp.ErrCode, result.BaseResp.ErrMsg)
+		return
+	}
+	return
+}
+
+// CompelUnbind 强制解绑用户和设备
+func (d *Device) CompelUnbind(req ReqBind) (err error) {
+	var accessToken string
+	if accessToken, err = d.GetAccessToken(); err != nil {
+		return
+	}
+	uri := fmt.Sprintf("%s?access_token=%s", uriCompelUnbind, accessToken)
+	var response []byte
+	if response, err = util.PostJSON(uri, req); err != nil {
+		return
+	}
+	var result resBind
+	if err = json.Unmarshal(response, &result); err != nil {
+		return
+	}
+	if result.BaseResp.ErrCode != 0 {
+		err = fmt.Errorf("DeviceBind Error , errcode=%d , errmsg=%s", result.BaseResp.ErrCode, result.BaseResp.ErrMsg)
+		return
+	}
+	return
+}

+ 60 - 0
device/device.go

@@ -0,0 +1,60 @@
+package device
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/silenceper/wechat/context"
+	"github.com/silenceper/wechat/util"
+)
+
+const (
+	uriAuthorize    = "https://api.weixin.qq.com/device/authorize_device"
+	uriQRCode       = "https://api.weixin.qq.com/device/create_qrcode"
+	uriVerifyQRCode = "https://api.weixin.qq.com/device/verify_qrcode"
+	uriBind         = "https://api.weixin.qq.com/device/bind"
+	uriUnbind       = "https://api.weixin.qq.com/device/unbind"
+	uriCompelBind   = "https://api.weixin.qq.com/device/compel_bind"
+	uriCompelUnbind = "https://api.weixin.qq.com/device/compel_unbind"
+	uriState        = "https://api.weixin.qq.com/device/get_stat"
+)
+
+//Device struct
+type Device struct {
+	*context.Context
+}
+
+//NewDevice 实例
+func NewDevice(context *context.Context) *Device {
+	device := new(Device)
+	device.Context = context
+	return device
+}
+
+// ResDeviceState 设备状态响应实体
+type ResDeviceState struct {
+	util.CommonError
+	Status     int    `json:"status"`
+	StatusInfo string `json:"status_info"`
+}
+
+// State 设备状态查询
+func (d *Device) State(device string) (res ResDeviceState, err error) {
+	var accessToken string
+	if accessToken, err = d.GetAccessToken(); err != nil {
+		return
+	}
+	uri := fmt.Sprintf("%s?access_token=%s&device_id=%s", uriState, accessToken, device)
+	var response []byte
+	if response, err = util.HTTPGet(uri); err != nil {
+		return
+	}
+	if err = json.Unmarshal(response, &res); err != nil {
+		return
+	}
+	if res.ErrCode != 0 {
+		err = fmt.Errorf("DeviceState Error , errcode=%d , errmsg=%s", res.ErrCode, res.ErrMsg)
+		return
+	}
+	return
+}

+ 9 - 0
device/message.go

@@ -0,0 +1,9 @@
+package device
+
+//MsgDevice 设备消息响应
+type MsgDevice struct {
+	DeviceType string
+	DeviceID   string
+	SessionID  string
+	OpenID     string
+}

+ 76 - 0
device/qrcode.go

@@ -0,0 +1,76 @@
+package device
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/silenceper/wechat/util"
+)
+
+//ResCreateQRCode 获取二维码的返回实体
+type ResCreateQRCode struct {
+	util.CommonError
+	DeviceNum int `json:"device_num"`
+	CodeList  []struct {
+		DeviceID string `json:"device_id"`
+		Ticket   string `json:"ticket"`
+	} `json:"code_list"`
+}
+
+// CreateQRCode 获取设备二维码
+func (d *Device) CreateQRCode(devices []string) (res ResCreateQRCode, err error) {
+	var accessToken string
+	if accessToken, err = d.GetAccessToken(); err != nil {
+		return
+	}
+	uri := fmt.Sprintf("%s?access_token=%s", uriQRCode, accessToken)
+	req := map[string]interface{}{
+		"device_num":     len(devices),
+		"device_id_list": devices,
+	}
+	var response []byte
+	if response, err = util.PostJSON(uri, req); err != nil {
+		return
+	}
+	if err = json.Unmarshal(response, &res); err != nil {
+		return
+	}
+	if res.ErrCode != 0 {
+		err = fmt.Errorf("DeviceCreateQRCode Error , errcode=%d , errmsg=%s", res.ErrCode, res.ErrMsg)
+		return
+	}
+	return
+}
+
+//ResVerifyQRCode 验证授权结果实体
+type ResVerifyQRCode struct {
+	util.CommonError
+	DeviceType string `json:"device_type"`
+	DeviceID   string `json:"device_id"`
+	Mac        string `json:"mac"`
+}
+
+// VerifyQRCode 验证设备二维码
+func (d *Device) VerifyQRCode(ticket string) (res ResVerifyQRCode, err error) {
+	var accessToken string
+	if accessToken, err = d.GetAccessToken(); err != nil {
+		return
+	}
+	uri := fmt.Sprintf("%s?access_token=%s", uriVerifyQRCode, accessToken)
+	req := map[string]interface{}{
+		"ticket": ticket,
+	}
+	fmt.Println(req)
+	var response []byte
+	if response, err = util.PostJSON(uri, req); err != nil {
+		return
+	}
+	if err = json.Unmarshal(response, &res); err != nil {
+		return
+	}
+	if res.ErrCode != 0 {
+		err = fmt.Errorf("DeviceCreateQRCode Error , errcode=%d , errmsg=%s", res.ErrCode, res.ErrMsg)
+		return
+	}
+	return
+}

+ 30 - 2
material/material.go

@@ -13,6 +13,7 @@ const (
 	addNewsURL     = "https://api.weixin.qq.com/cgi-bin/material/add_news"
 	addMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/add_material"
 	delMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/del_material"
+	getMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/get_material"
 )
 
 //Material 素材管理
@@ -36,6 +37,33 @@ type Article struct {
 	ShowCoverPic     int    `json:"show_cover_pic"`
 	Content          string `json:"content"`
 	ContentSourceURL string `json:"content_source_url"`
+	URL              string `json:"url"`
+	DownURL          string `json:"down_url"`
+}
+
+// GetNews 获取/下载永久素材
+func (material *Material) GetNews(id string) ([]*Article, error) {
+	accessToken, err := material.GetAccessToken()
+	if err != nil {
+		return nil, err
+	}
+	uri := fmt.Sprintf("%s?access_token=%s", getMaterialURL, accessToken)
+
+	var req struct {
+		MediaID string `json:"media_id"`
+	}
+	req.MediaID = id
+	responseBytes, err := util.PostJSON(uri, req)
+
+	var res struct {
+		NewsItem []*Article `json:"news_item"`
+	}
+	err = json.Unmarshal(responseBytes, &res)
+	if err != nil {
+		return nil, err
+	}
+
+	return res.NewsItem, nil
 }
 
 //reqArticles 永久性图文素材请求信息
@@ -138,11 +166,11 @@ func (material *Material) AddVideo(filename, title, introduction string) (mediaI
 	fields := []util.MultipartFormField{
 		{
 			IsFile:    true,
-			Fieldname: "video",
+			Fieldname: "media",
 			Filename:  filename,
 		},
 		{
-			IsFile:    true,
+			IsFile:    false,
 			Fieldname: "description",
 			Value:     fieldValue,
 		},

+ 160 - 0
message/customer_message.go

@@ -0,0 +1,160 @@
+package message
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/silenceper/wechat/context"
+	"github.com/silenceper/wechat/util"
+)
+
+const (
+	customerSendMessage = "https://api.weixin.qq.com/cgi-bin/message/custom/send"
+)
+
+//Manager 消息管理者,可以发送消息
+type Manager struct {
+	*context.Context
+}
+
+//NewMessageManager 实例化消息管理者
+func NewMessageManager(context *context.Context) *Manager {
+	return &Manager{
+		context,
+	}
+}
+
+//CustomerMessage  客服消息
+type CustomerMessage struct {
+	ToUser          string                `json:"touser"`                    //接受者OpenID
+	Msgtype         MsgType               `json:"msgtype"`                   //客服消息类型
+	Text            *MediaText            `json:"text,omitempty"`            //可选
+	Image           *MediaResource        `json:"image,omitempty"`           //可选
+	Voice           *MediaResource        `json:"voice,omitempty"`           //可选
+	Video           *MediaVideo           `json:"video,omitempty"`           //可选
+	Music           *MediaMusic           `json:"music,omitempty"`           //可选
+	News            *MediaNews            `json:"news,omitempty"`            //可选
+	Mpnews          *MediaResource        `json:"mpnews,omitempty"`          //可选
+	Wxcard          *MediaWxcard          `json:"wxcard,omitempty"`          //可选
+	Msgmenu         *MediaMsgmenu         `json:"msgmenu,omitempty"`         //可选
+	Miniprogrampage *MediaMiniprogrampage `json:"miniprogrampage,omitempty"` //可选
+}
+
+//NewCustomerTextMessage 文本消息结构体构造方法
+func NewCustomerTextMessage(toUser, text string) *CustomerMessage {
+	return &CustomerMessage{
+		ToUser:  toUser,
+		Msgtype: MsgTypeText,
+		Text: &MediaText{
+			text,
+		},
+	}
+}
+
+//NewCustomerImgMessage 图片消息的构造方法
+func NewCustomerImgMessage(toUser, mediaID string) *CustomerMessage {
+	return &CustomerMessage{
+		ToUser:  toUser,
+		Msgtype: MsgTypeImage,
+		Image: &MediaResource{
+			mediaID,
+		},
+	}
+}
+
+//NewCustomerVoiceMessage 语音消息的构造方法
+func NewCustomerVoiceMessage(toUser, mediaID string) *CustomerMessage {
+	return &CustomerMessage{
+		ToUser:  toUser,
+		Msgtype: MsgTypeVoice,
+		Voice: &MediaResource{
+			mediaID,
+		},
+	}
+}
+
+//MediaText 文本消息的文字
+type MediaText struct {
+	Content string `json:"content"`
+}
+
+//MediaResource  消息使用的永久素材id
+type MediaResource struct {
+	MediaID string `json:"media_id"`
+}
+
+//MediaVideo 视频消息包含的内容
+type MediaVideo struct {
+	MediaID      string `json:"media_id"`
+	ThumbMediaID string `json:"thumb_media_id"`
+	Title        string `json:"title"`
+	Description  string `json:"description"`
+}
+
+//MediaMusic 音乐消息包括的内容
+type MediaMusic struct {
+	Title        string `json:"title"`
+	Description  string `json:"description"`
+	Musicurl     string `json:"musicurl"`
+	Hqmusicurl   string `json:"hqmusicurl"`
+	ThumbMediaID string `json:"thumb_media_id"`
+}
+
+//MediaNews 图文消息的内容
+type MediaNews struct {
+	Articles []MediaArticles `json:"articles"`
+}
+
+//MediaArticles 图文消息的内容的文章列表中的单独一条
+type MediaArticles struct {
+	Title       string `json:"title"`
+	Description string `json:"description"`
+	URL         string `json:"url"`
+	Picurl      string `json:"picurl"`
+}
+
+//MediaMsgmenu 菜单消息的内容
+type MediaMsgmenu struct {
+	HeadContent string        `json:"head_content"`
+	List        []MsgmenuItem `json:"list"`
+	TailContent string        `json:"tail_content"`
+}
+
+//MsgmenuItem 菜单消息的菜单按钮
+type MsgmenuItem struct {
+	ID      string `json:"id"`
+	Content string `json:"content"`
+}
+
+//MediaWxcard 卡券的id
+type MediaWxcard struct {
+	CardID string `json:"card_id"`
+}
+
+//MediaMiniprogrampage 小程序消息
+type MediaMiniprogrampage struct {
+	Title        string `json:"title"`
+	Appid        string `json:"appid"`
+	Pagepath     string `json:"pagepath"`
+	ThumbMediaID string `json:"thumb_media_id"`
+}
+
+//Send 发送客服消息
+func (manager *Manager) Send(msg *CustomerMessage) error {
+	accessToken, err := manager.Context.GetAccessToken()
+	if err != nil {
+		return err
+	}
+	uri := fmt.Sprintf("%s?access_token=%s", customerSendMessage, accessToken)
+	response, err := util.PostJSON(uri, msg)
+	var result util.CommonError
+	err = json.Unmarshal(response, &result)
+	if err != nil {
+		return err
+	}
+	if result.ErrCode != 0 {
+		err = fmt.Errorf("customer msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
+		return err
+	}
+
+	return nil
+}

+ 41 - 5
message/message.go

@@ -1,6 +1,10 @@
 package message
 
-import "encoding/xml"
+import (
+	"encoding/xml"
+
+	"github.com/silenceper/wechat/device"
+)
 
 // MsgType 基本消息类型
 type MsgType string
@@ -63,6 +67,8 @@ const (
 	EventLocationSelect = "location_select"
 	//EventTemplateSendJobFinish 发送模板消息推送通知
 	EventTemplateSendJobFinish = "TEMPLATESENDJOBFINISH"
+	//EventWxaMediaCheck 异步校验图片/音频是否含有违法违规内容推送事件
+	EventWxaMediaCheck = "wxa_media_check"
 )
 
 const (
@@ -133,6 +139,26 @@ type MixMessage struct {
 	AuthorizationCode            string   `xml:"AuthorizationCode"`
 	AuthorizationCodeExpiredTime int64    `xml:"AuthorizationCodeExpiredTime"`
 	PreAuthCode                  string   `xml:"PreAuthCode"`
+
+	// 卡券相关
+	CardID              string `xml:"CardId"`
+	RefuseReason        string `xml:"RefuseReason"`
+	IsGiveByFriend      int32  `xml:"IsGiveByFriend"`
+	FriendUserName      string `xml:"FriendUserName"`
+	UserCardCode        string `xml:"UserCardCode"`
+	OldUserCardCode     string `xml:"OldUserCardCode"`
+	OuterStr            string `xml:"OuterStr"`
+	IsRestoreMemberCard int32  `xml:"IsRestoreMemberCard"`
+	UnionID             string `xml:"UnionId"`
+
+	// 内容审核相关
+	IsRisky       bool   `xml:"isrisky"`
+	ExtraInfoJSON string `xml:"extra_info_json"`
+	TraceID       string `xml:"trace_id"`
+	StatusCode    int    `xml:"status_code"`
+
+	//设备相关
+	device.MsgDevice
 }
 
 //EventPic 发图事件推送
@@ -156,22 +182,32 @@ type ResponseEncryptedXMLMsg struct {
 	Nonce        string   `xml:"Nonce"        json:"Nonce"`
 }
 
+// CDATA  使用该类型,在序列化为 xml 文本时文本会被解析器忽略
+type CDATA string
+
+// MarshalXML 实现自己的序列化方法
+func (c CDATA) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
+	return e.EncodeElement(struct {
+		string `xml:",cdata"`
+	}{string(c)}, start)
+}
+
 // CommonToken 消息中通用的结构
 type CommonToken struct {
 	XMLName      xml.Name `xml:"xml"`
-	ToUserName   string   `xml:"ToUserName"`
-	FromUserName string   `xml:"FromUserName"`
+	ToUserName   CDATA    `xml:"ToUserName"`
+	FromUserName CDATA    `xml:"FromUserName"`
 	CreateTime   int64    `xml:"CreateTime"`
 	MsgType      MsgType  `xml:"MsgType"`
 }
 
 //SetToUserName set ToUserName
-func (msg *CommonToken) SetToUserName(toUserName string) {
+func (msg *CommonToken) SetToUserName(toUserName CDATA) {
 	msg.ToUserName = toUserName
 }
 
 //SetFromUserName set FromUserName
-func (msg *CommonToken) SetFromUserName(fromUserName string) {
+func (msg *CommonToken) SetFromUserName(fromUserName CDATA) {
 	msg.FromUserName = fromUserName
 }
 

+ 2 - 2
message/text.go

@@ -3,12 +3,12 @@ package message
 //Text 文本消息
 type Text struct {
 	CommonToken
-	Content string `xml:"Content"`
+	Content CDATA `xml:"Content"`
 }
 
 //NewText 初始化文本消息
 func NewText(content string) *Text {
 	text := new(Text)
-	text.Content = content
+	text.Content = CDATA(content)
 	return text
 }

+ 39 - 2
miniprogram/decrypt.go

@@ -36,6 +36,17 @@ type UserInfo struct {
 	} `json:"watermark"`
 }
 
+// PhoneInfo 用户手机号
+type PhoneInfo struct {
+	PhoneNumber     string `json:"phoneNumber"`
+	PurePhoneNumber string `json:"purePhoneNumber"`
+	CountryCode     string `json:"countryCode"`
+	Watermark       struct {
+		Timestamp int64  `json:"timestamp"`
+		AppID     string `json:"appid"`
+	} `json:"watermark"`
+}
+
 // pkcs7Unpad returns slice of the original data without padding
 func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) {
 	if blockSize <= 0 {
@@ -57,8 +68,8 @@ func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) {
 	return data[:len(data)-n], nil
 }
 
-// Decrypt 解密数据
-func (wxa *MiniProgram) Decrypt(sessionKey, encryptedData, iv string) (*UserInfo, error) {
+// getCipherText returns slice of the cipher text
+func getCipherText(sessionKey, encryptedData, iv string) ([]byte, error) {
 	aesKey, err := base64.StdEncoding.DecodeString(sessionKey)
 	if err != nil {
 		return nil, err
@@ -81,6 +92,15 @@ func (wxa *MiniProgram) Decrypt(sessionKey, encryptedData, iv string) (*UserInfo
 	if err != nil {
 		return nil, err
 	}
+	return cipherText, nil
+}
+
+// Decrypt 解密数据
+func (wxa *MiniProgram) Decrypt(sessionKey, encryptedData, iv string) (*UserInfo, error) {
+	cipherText, err := getCipherText(sessionKey, encryptedData, iv)
+	if err != nil {
+		return nil, err
+	}
 	var userInfo UserInfo
 	err = json.Unmarshal(cipherText, &userInfo)
 	if err != nil {
@@ -91,3 +111,20 @@ func (wxa *MiniProgram) Decrypt(sessionKey, encryptedData, iv string) (*UserInfo
 	}
 	return &userInfo, nil
 }
+
+// DecryptPhone 解密数据(手机)
+func (wxa *MiniProgram) DecryptPhone(sessionKey, encryptedData, iv string) (*PhoneInfo, error) {
+	cipherText, err := getCipherText(sessionKey, encryptedData, iv)
+	if err != nil {
+		return nil, err
+	}
+	var phoneInfo PhoneInfo
+	err = json.Unmarshal(cipherText, &phoneInfo)
+	if err != nil {
+		return nil, err
+	}
+	if phoneInfo.Watermark.AppID != wxa.AppID {
+		return nil, ErrAppIDNotMatch
+	}
+	return &phoneInfo, nil
+}

+ 84 - 0
pay/notify_result.go

@@ -0,0 +1,84 @@
+package pay
+
+import (
+	"fmt"
+	"github.com/silenceper/wechat/util"
+	"sort"
+)
+
+// Base 公用参数
+type Base struct {
+	AppID    string `xml:"appid"`
+	MchID    string `xml:"mch_id"`
+	NonceStr string `xml:"nonce_str"`
+	Sign     string `xml:"sign"`
+}
+
+// NotifyResult 下单回调
+type NotifyResult struct {
+	Base
+	ReturnCode    string `xml:"return_code"`
+	ReturnMsg     string `xml:"return_msg"`
+	ResultCode    string `xml:"result_code"`
+	OpenID        string `xml:"openid"`
+	IsSubscribe   string `xml:"is_subscribe"`
+	TradeType     string `xml:"trade_type"`
+	BankType      string `xml:"bank_type"`
+	TotalFee      int    `xml:"total_fee"`
+	FeeType       string `xml:"fee_type"`
+	CashFee       int    `xml:"cash_fee"`
+	CashFeeType   string `xml:"cash_fee_type"`
+	TransactionID string `xml:"transaction_id"`
+	OutTradeNo    string `xml:"out_trade_no"`
+	Attach        string `xml:"attach"`
+	TimeEnd       string `xml:"time_end"`
+}
+
+// NotifyResp 消息通知返回
+type NotifyResp struct {
+	ReturnCode string `xml:"return_code"`
+	ReturnMsg  string `xml:"return_msg"`
+}
+
+// VerifySign 验签
+func (pcf *Pay) VerifySign(notifyRes NotifyResult) bool {
+	// 封装map 请求过来的 map
+	resMap := make(map[string]interface{})
+	resMap["appid"] = notifyRes.AppID
+	resMap["bank_type"] = notifyRes.BankType
+	resMap["cash_fee"] = notifyRes.CashFee
+	resMap["fee_type"] = notifyRes.FeeType
+	resMap["is_subscribe"] = notifyRes.IsSubscribe
+	resMap["mch_id"] = notifyRes.MchID
+	resMap["nonce_str"] = notifyRes.NonceStr
+	resMap["openid"] = notifyRes.OpenID
+	resMap["out_trade_no"] = notifyRes.OutTradeNo
+	resMap["result_code"] = notifyRes.ResultCode
+	resMap["return_code"] = notifyRes.ReturnCode
+	resMap["time_end"] = notifyRes.TimeEnd
+	resMap["total_fee"] = notifyRes.TotalFee
+	resMap["trade_type"] = notifyRes.TradeType
+	resMap["transaction_id"] = notifyRes.TransactionID
+	// 支付key
+	sortedKeys := make([]string, 0, len(resMap))
+	for k := range resMap {
+		sortedKeys = append(sortedKeys, k)
+	}
+	sort.Strings(sortedKeys)
+	// STEP2, 对key=value的键值对用&连接起来,略过空值
+	var signStrings string
+	for _, k := range sortedKeys {
+		value := fmt.Sprintf("%v", resMap[k])
+		if value != "" {
+			signStrings = signStrings + k + "=" + value + "&"
+		}
+	}
+	// STEP3, 在键值对的最后加上key=API_KEY
+	signStrings = signStrings + "key=" + pcf.PayKey
+	// STEP4, 进行MD5签名并且将所有字符转为大写.
+	sign := util.MD5Sum(signStrings)
+	if sign != notifyRes.Sign {
+		return false
+	}
+	return true
+}

+ 92 - 23
pay/pay.go

@@ -2,10 +2,17 @@ package pay
 
 import (
 	"bytes"
+	"crypto/hmac"
+	"crypto/md5"
+	"crypto/sha256"
+	"encoding/hex"
 	"encoding/xml"
 	"errors"
+	"hash"
 	"sort"
 	"strconv"
+	"strings"
+	"time"
 
 	"github.com/silenceper/wechat/context"
 	"github.com/silenceper/wechat/util"
@@ -27,15 +34,21 @@ type Params struct {
 	OutTradeNo string
 	OpenID     string
 	TradeType  string
+	SignType   string
+	Detail     string
+	Attach     string
+	GoodsTag   string
+	NotifyURL  string
 }
 
-// Config 是传出用于 jsdk 用的参数
+// Config 是传出用于 js sdk 用的参数
 type Config struct {
-	Timestamp int64
-	NonceStr  string
-	PrePayID  string
-	SignType  string
-	Sign      string
+	Timestamp string `json:"timestamp"`
+	NonceStr  string `json:"nonceStr"`
+	PrePayID  string `json:"prePayId"`
+	SignType  string `json:"signType"`
+	Package   string `json:"package"`
+	PaySign   string `json:"paySign"`
 }
 
 // PreOrder 是 unifie order 接口的返回
@@ -54,7 +67,7 @@ type PreOrder struct {
 	ErrCodeDes string `xml:"err_code_des,omitempty"`
 }
 
-//payRequest 接口请求参数
+// payRequest 接口请求参数
 type payRequest struct {
 	AppID          string `xml:"appid"`
 	MchID          string `xml:"mch_id"`
@@ -64,20 +77,20 @@ type payRequest struct {
 	SignType       string `xml:"sign_type,omitempty"`
 	Body           string `xml:"body"`
 	Detail         string `xml:"detail,omitempty"`
-	Attach         string `xml:"attach,omitempty"`      //附加数据
-	OutTradeNo     string `xml:"out_trade_no"`          //商户订单号
-	FeeType        string `xml:"fee_type,omitempty"`    //标价币种
-	TotalFee       string `xml:"total_fee"`             //标价金额
-	SpbillCreateIP string `xml:"spbill_create_ip"`      //终端IP
-	TimeStart      string `xml:"time_start,omitempty"`  //交易起始时间
-	TimeExpire     string `xml:"time_expire,omitempty"` //交易结束时间
-	GoodsTag       string `xml:"goods_tag,omitempty"`   //订单优惠标记
-	NotifyURL      string `xml:"notify_url"`            //通知地址
-	TradeType      string `xml:"trade_type"`            //交易类型
-	ProductID      string `xml:"product_id,omitempty"`  //商品ID
+	Attach         string `xml:"attach,omitempty"`      // 附加数据
+	OutTradeNo     string `xml:"out_trade_no"`          // 商户订单号
+	FeeType        string `xml:"fee_type,omitempty"`    // 标价币种
+	TotalFee       string `xml:"total_fee"`             // 标价金额
+	SpbillCreateIP string `xml:"spbill_create_ip"`      // 终端IP
+	TimeStart      string `xml:"time_start,omitempty"`  // 交易起始时间
+	TimeExpire     string `xml:"time_expire,omitempty"` // 交易结束时间
+	GoodsTag       string `xml:"goods_tag,omitempty"`   // 订单优惠标记
+	NotifyURL      string `xml:"notify_url"`            // 通知地址
+	TradeType      string `xml:"trade_type"`            // 交易类型
+	ProductID      string `xml:"product_id,omitempty"`  // 商品ID
 	LimitPay       string `xml:"limit_pay,omitempty"`   //
-	OpenID         string `xml:"openid,omitempty"`      //用户标识
-	SceneInfo      string `xml:"scene_info,omitempty"`  //场景信息
+	OpenID         string `xml:"openid,omitempty"`      // 用户标识
+	SceneInfo      string `xml:"scene_info,omitempty"`  // 场景信息
 }
 
 // NewPay return an instance of Pay package
@@ -86,20 +99,72 @@ func NewPay(ctx *context.Context) *Pay {
 	return &pay
 }
 
+// BridgeConfig get js bridge config
+func (pcf *Pay) BridgeConfig(p *Params) (cfg Config, err error) {
+	var (
+		buffer    strings.Builder
+		h         hash.Hash
+		timestamp = strconv.FormatInt(time.Now().Unix(), 10)
+	)
+	order, err := pcf.PrePayOrder(p)
+	if err != nil {
+		return
+	}
+	buffer.WriteString("appId=")
+	buffer.WriteString(order.AppID)
+	buffer.WriteString("&nonceStr=")
+	buffer.WriteString(order.NonceStr)
+	buffer.WriteString("&package=")
+	buffer.WriteString("prepay_id=" + order.PrePayID)
+	buffer.WriteString("&signType=")
+	buffer.WriteString(p.SignType)
+	buffer.WriteString("&timeStamp=")
+	buffer.WriteString(timestamp)
+	buffer.WriteString("&key=")
+	buffer.WriteString(pcf.PayKey)
+	if p.SignType == "MD5" {
+		h = md5.New()
+	} else {
+		h = hmac.New(sha256.New, []byte(pcf.PayKey))
+	}
+	h.Write([]byte(buffer.String()))
+	// 签名
+	cfg.PaySign = strings.ToUpper(hex.EncodeToString(h.Sum(nil)))
+	cfg.NonceStr = order.NonceStr
+	cfg.Timestamp = timestamp
+	cfg.PrePayID = order.PrePayID
+	cfg.SignType = p.SignType
+	cfg.Package = "prepay_id=" + order.PrePayID
+	return
+}
+
 // PrePayOrder return data for invoke wechat payment
 func (pcf *Pay) PrePayOrder(p *Params) (payOrder PreOrder, err error) {
 	nonceStr := util.RandomStr(32)
+	notifyURL := pcf.PayNotifyURL
+	// 签名类型
+	if p.SignType == "" {
+		p.SignType = "MD5"
+	}
+	// 通知地址
+	if p.NotifyURL != "" {
+		notifyURL = p.NotifyURL
+	}
 	param := make(map[string]interface{})
 	param["appid"] = pcf.AppID
 	param["body"] = p.Body
 	param["mch_id"] = pcf.PayMchID
 	param["nonce_str"] = nonceStr
-	param["notify_url"] = pcf.PayNotifyURL
 	param["out_trade_no"] = p.OutTradeNo
 	param["spbill_create_ip"] = p.CreateIP
 	param["total_fee"] = p.TotalFee
 	param["trade_type"] = p.TradeType
 	param["openid"] = p.OpenID
+	param["sign_type"] = p.SignType
+	param["detail"] = p.Detail
+	param["attach"] = p.Attach
+	param["goods_tag"] = p.GoodsTag
+	param["notify_url"] = notifyURL
 
 	bizKey := "&key=" + pcf.PayKey
 	str := orderParam(param, bizKey)
@@ -113,9 +178,13 @@ func (pcf *Pay) PrePayOrder(p *Params) (payOrder PreOrder, err error) {
 		OutTradeNo:     p.OutTradeNo,
 		TotalFee:       p.TotalFee,
 		SpbillCreateIP: p.CreateIP,
-		NotifyURL:      pcf.PayNotifyURL,
+		NotifyURL:      notifyURL,
 		TradeType:      p.TradeType,
 		OpenID:         p.OpenID,
+		SignType:       p.SignType,
+		Detail:         p.Detail,
+		Attach:         p.Attach,
+		GoodsTag:       p.GoodsTag,
 	}
 	rawRet, err := util.PostXML(payGateway, request)
 	if err != nil {
@@ -126,7 +195,7 @@ func (pcf *Pay) PrePayOrder(p *Params) (payOrder PreOrder, err error) {
 		return
 	}
 	if payOrder.ReturnCode == "SUCCESS" {
-		//pay success
+		// pay success
 		if payOrder.ResultCode == "SUCCESS" {
 			err = nil
 			return

+ 3 - 1
server/server.go

@@ -65,7 +65,9 @@ func (srv *Server) Serve() error {
 	}
 
 	//debug
-	//fmt.Println("request msg = ", string(srv.requestRawXMLMsg))
+	if srv.debug {
+		fmt.Println("request msg = ", string(srv.requestRawXMLMsg))
+	}
 
 	return srv.buildResponse(response)
 }

+ 6 - 0
wechat.go

@@ -1,6 +1,7 @@
 package wechat
 
 import (
+	"github.com/silenceper/wechat/device"
 	"net/http"
 	"sync"
 
@@ -111,3 +112,8 @@ func (wc *Wechat) GetQR() *qr.QR {
 func (wc *Wechat) GetMiniProgram() *miniprogram.MiniProgram {
 	return miniprogram.NewMiniProgram(wc.Context)
 }
+
+// GetDevice 获取智能设备的实例
+func (wc *Wechat) GetDevice() *device.Device {
+	return device.NewDevice(wc.Context)
+}