微信开发

大纲如下

  • 微信公众号开发

    • 服务器验证
    • 处理消息
    • 推送消息
    • 生成菜单
  • 小程序开发

    • 获取个人信息
    • 微信支付
  • 总结


微信公众号开发

1.服务器校验

image

  • 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.推送消息 实际开发中,业务需要主动推送模板消息给指定用户。这时候需要发送模板消息。 image

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.微信支付

image

一、生成预支付订单

主要是生成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要做相应的精度转化

微信支付以分为单位

业务上处理好微信支付同一条记录返回多次

支付成功后返回的签名,我们要把与其一同返回的数据进行签名计算,拿到新的签名来与返回的签名作比较。而不能直接用支付的签名做校验