大纲如下
微信公众号开发
- 服务器验证
- 处理消息
- 推送消息
- 生成菜单
小程序开发
- 获取个人信息
- 微信支付
总结
微信公众号开发
1.服务器校验
- timestamp(时间戳), nonce(随机字符串), signature(签名), token(令牌)
func AccessVerify(timestamp, nonce, signature, token string) (bool, error) {
info := []string{timestamp, nonce, token}
sort.Strings(info)
s := sha1.New()
if _, err := io.WriteString(s, strings.Join(info, "")); err != nil {
return false, errors.New("Sha1 Code Fail " + err.Error())
}
newSignature := fmt.Sprintf("%x", s.Sum(nil))
return newSignature == signature, nil
}
接收的URL必须是80端口或者443端口
2.处理消息
- 消息类型
- text (文本)
- image (图片)
- voice (语音)
- video (视频)
- shortvideo (小视频)
- location (地理位置)
- link (链接)
- event (事件)
- subscribe (关注事件)
- unsubscribe (取关事件)
- SCAN (扫描二维码事件)
- LOCATION (上报地理位置)
- CLICK (点击菜单拉取消息)
- VIEW (点击菜单跳转链接)
用户向公众号发送”你好”,这个数据先被微信服务器打包成xml格式的数据,然后才转发到我们的服务器上:
<xml>
<ToUserName><![CDATA[开发者Openid]]></ToUserName>
<FromUserName><![CDATA[用户Openid]]></FromUserName>
<CreateTime>1348831860</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[你好]]></Content>
<MsgId>1234567890123456</MsgId>
</xml>
我们先判断其消息类型是文本,然后可根据内容作相应的回复。 我们也要把我们要回复的消息封装成xml返回给微信服务器,微信服务器再发送给用户
<xml>
<ToUserName><![CDATA[用户Openid]]></ToUserName>
<FromUserName><![CDATA[开发者Openid]]></FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[你好,我是创服宝客服小宝👶,请问有什么可以为你服务]]></Content>
</xml>
封装xml时遇到了坑,避坑指南
3.推送消息 实际开发中,业务需要主动推送模板消息给指定用户。这时候需要发送模板消息。
access_token有效期是7200秒(2小时)
重复获取会让上次获取的access_token失效
获取access_token的服务器IP必须提前配置在公众平台的白名单中
首先要在微信公众平台上选取相应的模板
{{first.DATA}}
客户名称:{{keyword1.DATA}}
客服类型:{{keyword2.DATA}}
提醒内容:{{keyword3.DATA}}
通知时间:{{keyword4.DATA}}
{{remark.DATA}}
然后根据实际业务,把数据封装成json数据发送给微信服务器
{
"touser":"OPENID",
"template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY",
"url":"http://weixin.qq.com/download",
"miniprogram":{
"appid":"xiaochengxuappid12345",
"pagepath":"index?foo=bar"
},
"data":{
"first": {
"value":"尊敬的客户您好,您本月的创业报告已经生成,具体信息如下:",
"color":"#173177"
},
"keyword1":{
"value":"宁波创服宝科技有限公司",
"color":"#173177"
},
"keyword2": {
"value":"创业报告",
"color":"#173177"
},
"keyword3": {
"value":"请查收您的6月份创业报告",
"color":"#173177"
},
"remark":{
"value":"易创-让创业更容易",
"color":"#173177"
}
}
}
4.生成菜单 因为公众号是开发者模式,所以无法在公众平台配置菜单结构,只需向微信服务器发送相应的json数据:
{
"button":[
{
"type":"click",
"name":"联系我们",
"key":"contact_us"
},
{
"name":"关于我们",
"type":"view",
"url":"http://mp.weixin.qq.com/s?__biz=MzU2NTg0NDgzOA==&mid=100000014&idx=1&sn=1f8aa277a88de2bdf429564226aacdda&chksm=7cb4cf074bc3461112ed21128e57528a4acbe7e50dbaca42eefc3069c7d5fcd54d0fdfaf3f9c#rd"
},
{
"type":"miniprogram",
"name":"我的报告",
"appid":"xxxxxxxx",
"url":"http://mp.weixin.qq.com",
"pagepath":"pages/auth/auth"
}
]
}
小程序开发
1.获取个人信息
小程序登录时,需要用户授权获取用户隐私信息,如unionid(用户在公众号和小程序的唯一标志)
用户首先进入小程序(登录),会获取到code(登录凭证),我们用code向微信服务器换取session_key(会话密钥)
func LoginWXMiniProgram(code string) (WXRespUserInfo, error) {
var userInfo WXRespUserInfo
resp, err := http.Get(fmt.Sprintf(AuthCode2SessionUrl, MiniAppId, MiniAppSecret, code))
if err != nil {
return userInfo, errors.New(WXError.Error() + err.Error())
}
defer resp.Body.Close()
res, err := ioutil.ReadAll(resp.Body)
if err != nil {
return userInfo, errors.New(WXError.Error() + err.Error())
}
if err = json.Unmarshal(res, &userInfo); err != nil {
return userInfo, err
}
if userInfo.ErrCode == 0 {
return userInfo, nil
} else {
return userInfo, errors.New(WXError.Error() + userInfo.ErrMsg)
}
}
拿到session_key,用户在确定授权后,我们会拿到另外两个个数据:
encryptData(完整用户信息的加密数据), iv(加密算法的初始向量) + sessionKey(会话密钥)
我们通过解密算法来获取encryptData的用户信息 > 对称解密使用的算法为 AES-128-CBC,数据采用PKCS#7填充。
对称解密的目标密文为 Base64_Decode(encryptedData)。
对称解密秘钥 aeskey = Base64_Decode(session_key), aeskey 是16字节。
对称解密算法初始向量 为Base64_Decode(iv),其中iv由数据接口返回。
func DecryptWXOpenData(sessionKey, encryptData, iv string) (WXDecryptedUserInfo, error) {
var userInfo WXDecryptedUserInfo
aesCipherText, err := base64.StdEncoding.DecodeString(encryptData)
if err != nil {
return userInfo, errors.New(WXError.Error() + err.Error())
}
aesKey, err := base64.StdEncoding.DecodeString(sessionKey)
if err != nil {
return userInfo, errors.New(WXError.Error() + err.Error())
}
aesIV, err := base64.StdEncoding.DecodeString(iv)
if err != nil {
return userInfo, errors.New(WXError.Error() + err.Error())
}
data, err := AesDecrypt(aesCipherText, aesKey, aesIV)
if err != nil {
return userInfo, errors.New(WXError.Error() + err.Error())
}
aesPlantText := PKCS7UnPadding(data)
if err := json.Unmarshal(aesPlantText, &userInfo); err != nil {
return userInfo, errors.New("unmarshal data fail " + err.Error())
}
return userInfo, nil
}
func AesDecrypt(decodeData, decodeSessionKey, decodeIv []byte) ([]byte, error) {
block, err := aes.NewCipher(decodeSessionKey)
if err != nil {
return nil, err
}
blockMode := cipher.NewCBCDecrypter(block, decodeIv)
originData := make([]byte, len(decodeData))
blockMode.CryptBlocks(originData, decodeData)
return originData, nil
}
func PKCS7UnPadding(plantText []byte) []byte {
length := len(plantText)
if length > 0 {
unPadding := int(plantText[length-1])
return plantText[:(length - unPadding)]
}
return plantText
}
解密后获得的结构为:
{
"openId": "OPENID",
"nickName": "NICKNAME",
"gender": GENDER,
"city": "CITY",
"province": "PROVINCE",
"country": "COUNTRY",
"avatarUrl": "AVATARURL",
"unionId": "UNIONID",
"watermark": {
"appid":"APPID",
"timestamp":TIMESTAMP
}
}
2.微信支付
一、生成预支付订单
主要是生成signature(签名)的部分容易出错,重点强调下:
◆ 参数名ASCII码从小到大排序(字典序);
◆ 如果参数的值为空不参与签名;
◆ 参数名区分大小写;
◆ 验证调用返回或微信主动通知签名时,传送的sign参数不参与签名,将生成的签名与该sign值作校验。
◆ 微信接口可能增加字段,验证签名时必须支持增加的扩展字段
签名支持MD5加密和SHA256加密,这里使用的是MD5加密
func GetSign(mReq map[string]interface{}, key string) string {
var signStrings string
sortedKeys := make([]string, 0)
for k := range mReq {
sortedKeys = append(sortedKeys, k)
}
sort.Strings(sortedKeys)
for _, k := range sortedKeys {
value := fmt.Sprintf("%v", mReq[k])
if value != "" {
signStrings = fmt.Sprintf("%s%s=%s&", signStrings, k, value)
}
}
if key != "" {
signStrings = signStrings + "key=" + key
}
md5Ctx := md5.New()
md5Ctx.Write([]byte(signStrings))
cipherStr := md5Ctx.Sum(nil)
upperSign := strings.ToUpper(hex.EncodeToString(cipherStr))
return upperSign
}
订单总金额以分为单位,比如订单金额为0.57元,我们生成预付款订单的价格为57
注意:golang的float64类型,会把0.57变成0.56666…9,然后我们转化成int的时候变成了56,所以对float64要做相应的精度转化
二、组合数据再次签名
我们向微信服务器提交完表单后,若SUCCESS,会返回给我们prepare_id(预付订单ID)
我们把appId(小程序ID)、timeStamp(时间戳)、nonceStr(随机字符串)、package(”prepare_id=“xxx”“)进行组合签名, 获取paySignature(支付签名)
最后我们把timeStamp(时间戳)、nonceStr(随机字符串)、paySignature(支付签名)、signType(签名类型)、package(”prepare_id=“xxx”“)返回给前端小程序, 小程序页面就打开了支付面板
三、接收支付结果
用户在支付面板支付完之后,我们的服务器就等待微信服务器给我们发送支付结果,发送到哪个接口上呢?我们在 生成预支付订单的时候,设置了一个参数notify_url,微信支付的结果会发送到这个url上
同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知
- 我们在业务代码中一定要处理好微信同一个订单发送多次通知,不然可能造成商品的累加购买
在通知一直不成功的情况下,微信总共会发起10次通知,通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m
商户系统对于支付结果通知的内容一定要做签名验证,并校验返回的订单金额是否与商户侧的订单金额一致,防止数据泄漏导致出现“假通知”,造成资金损失
- 校验签名时,刚开始误认为返回的签名与我生成的支付签名是一样的,结果却是不一样的,是要重新进行签名,然后校验签名是否返回的签名相同。
生成支付签名时传入的参数: | 重新签名时传入的参数 |
---|---|
appid(小程序id) | appid(小程序id) |
body(信息主体) | —— |
mch_id(商户号) | mch_id(商户号) |
notify_url(回调URL) | —— |
trade_type(交易类型) | trade_type(交易类型) |
spbill_create_ip(终端IP) | —— |
total_fee(总金额) | —— |
out_trade_no(自定义订单ID) | out_trade_no(自定义订单ID) |
nonce_str(随机字符串) | nonce_str(随机字符串) |
openid(用户id) | openid(用户id) |
—— | fee_type(标价币种) |
—— | bank_type(付款银行) |
—— | cash_fee(现金支付金额) |
—— | is_subscribe(是否关注公众号) |
—— | return_code(返回代码) |
—— | result_code(业务结果) |
—— | transaction_id(微信订单ID) |
—— | time_end(交易结束时间) |
总结
公众号改为开发模式之后,接收的URL必须是80端口或者443端口
access_token有效期是7200秒(2小时), 重复获取会让上次获取的access_token失效,获取access_token的服务器IP必须提前配置在公众平台的白名单中
小程序登录获取的code只能使用一次,而且会在极短时间内失效
解密用户信息要严格遵守AES-128-CBC的解密方式,数据采用PKCS#7填充
golang的float64类型,会把0.57变成0.56666…9,然后我们转化成int的时候变成了56,所以对float64要做相应的精度转化
微信支付以分为单位
业务上处理好微信支付同一条记录返回多次
支付成功后返回的签名,我们要把与其一同返回的数据进行签名计算,拿到新的签名来与返回的签名作比较。而不能直接用支付的签名做校验