解决微信H5支付提示”商家存在未配置的参数,请联系商家解决”的问题

很多人对接微信H5支付的时候有时候会提示一个“商家存在未配置的参数,请联系商家解决”的问题,明明按照文档上面的对接已经对接起来了,而且mweb_url参数也回来了,但是调起微信却报这个错误
这个问题一般是因为域名与微信商户平台配置的域名不一致导致的,解决的办法也很简单
1、登陆微信商户平台,查看自己配置的域名有没有问题(要备案且用https访问),不过一般能添加上去都是没有问题的
2、检查网站提交支付的域名与微信商户平台的域名是否一致,如果不一致,一个是可以把域名添加到微信商户平台上面,第二个是网站域名换成和微信商户平台的域名一致
3、上面的两个很多人都知道排查,也很容易排查出来,第三个特别要注意的是头部参数Referer(具体做什么的自行百度),这个的域名如果不一致也会导致出现这个问题,而且这个是隐形的,容易忽略,这个做聚合支付是最容易出现的,明明mweb_url已经回来了,就是调起出问题,很多人喜欢用redirect去直接调起,这样很容易导致下面提交上来的地址直接传给微信了,如果这个时候两个域名不一致就会出现这个问题了,解决办法很容易

echo "<script language='javascript' type='text/javascript'>window.location.href='$mweb_url'</script>";

模拟点击提交,这个时候Referer就统一了

微信支付是有些坑,但是只要耐心去解决,还是很容易的

微信 H5 支付流程以及一些坑

原文:https://blog.niceue.com/front-end-development/wechat-h5-payment-process-as-well-as-some-pits.html

最近做的 SPA 网站集成了微信支付,使用的是微信 H5 调起支付API接口。做完后对微信 H5 支付的流程有了进一步的了解,在前后端调试接口的过程中也遇到了一些问题,在这里记录下来。

支付流程

  1. 在订单页 ajax 请求后端发起下单,后端挂起请求
  2. 后端根据订单号结合微信支付相关配置参数向微信服务器发起统一下单
  3. 下单成功,微信通知前面传递的 notify_url,返回 prepay_id (预支付交易会话标识)
  4. 后端返回前端 JSAPI 调用的参数
  5. 前端使用 JSSDK 的 wx.chooseWXPay 发起支付
wx.chooseWXPay({
  timestamp: '',   // 支付签名时间戳
  nonceStr: '',    // 支付签名随机串
  package: '',     // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=***)
  signType: 'MD5', // 签名方式
  paySign: '',     // 支付签名
})

这里看起来是 5 步,但其实还少了一步。调用“统一下单”接口的时候需要微信用户的 openid。

openid 是什么?

官方解释

加密后的微信号,每个用户对每个公众号的 openid 是唯一的。对于不同公众号,同一用户的 openid 不同

这个 openid 只能在微信环境通过重定向拿到。但是在下单的时候用的是 ajax 请求,还用重定向用户体验就比较差。所以需要在进入微信的时候就通过重定向拿到 openid 缓存起来,后面就可以直接使用了。所以入口页面就要做点小动作。

获取 openid

官方有OpenID的获取指引。对于 H5 应用,获取方式分 3 步走:

1. 后端重定向到获取 code 接口

后端可以定义一个 /wechat/redirect 接口,例如:

http://yoursite.com/wechat/redirect?url=home_url 

用这个地址重定向获取 code 地址,其中的 url 参数为最终的重定向地址,在拿到 openid 后跳转到该 url

2. 获取 code:

接口地址

https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirec

接口参数:

  • appid: 公众账号ID
  • redirect_uri: 接收 code 的回调地址(请UrlEncode)
  • response_type: 固定值 code
  • scope: 应用授权作用域,填 snsapi_base 或者 snsapi_login(可获取用户信息,如头像、昵称等)
  • state: 用于保持请求和回调的状态,防止 csrf 攻击,可设置为简单的随机数加 session 进行校验

提示:snsapi_login 会跳转到授权页让用户授权
微信接着会重定向三次,第三次重定向返回到 redirect_uri 地址,并且带上了 code 参数

3. 通过 code 获取 openid 和 access_token

接口地址

https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

获取到 openid 后注意在 session 中缓存起来

一些需要注意的坑

1. 获取 code 的接口地址

OpenID的获取指引文档,请注意黑色标题“微信公众平台”和“微信开放平台”。两个地方获取 code 的接口地址不一样,但参数是一样的。最开始后端看错了文档使用的是“网站应用微信登录”文档里面提供的获取 code 的接口,这样子怎么都是调不通的。

// 微信公众平台是
https://open.weixin.qq.com/connect/oauth2/authorize
// 微信开放平台是
https://open.weixin.qq.com/connect/qrconnect

2. 后端签名用 timeStamp,而前端调用支付接口使用全小写 timestamp

微信官方网页端调起支付API这个接口文档参数却误导观众,写的是驼峰的 timeStamp
后端为了方便返回给前端的也是 timeStamp,所以在前端需要转换
解决办法:

if (!data.timestamp) data.timestamp = data.timeStamp

3. iOS 和 Android 版微信对“支付授权目录”的检测不同

http://yousite.com/mobile/#!/checkout

以上路径,iOS 微信识别正确:http://yousite.com/mobile/
而 Android 微信识别出的目录是:http://yousite.com/mobile/#!/checkout
这应该是 Android 版微信的 Bug
解决办法是在 # 前添加一个 ?http://yousite.com/mobile/?#!/checkout

【微信小程序】小程序webview业务域名配置、提示不支持打开非业务域名等问题

webview标签是一个内置组件,用途:承载网页的容器。会自动铺满整个小程序页面。

谨记:个人类型的小程序暂不支持使用。并且打开的链接必须是https开头的,否则还是会报错。

当你写好代码之后准备测试跳转的时候BUG出来了,它会提示你无法打开如下图

 此时你只需要进入微信公众平台

然后找到一个业务域名的配置区域就可以了,配置好域名之后你就可以,打开你要跳转的H5页面链接了。对了跳转的时候有可能会有参数和你需要新添加的参数,所以需要加密也和解密一下URL里面的参数。

加密方法:

_url = encodeURIComponent(_url);//加密,解决带参数问题

解密方法:

_url = decodeURIComponent(_url)//解密,解决url带参数问题

然后把_url传入web-view标签的src属性就可以跳转了!

聊天机器人的定义和现状

聊天机器人,是一种通过自然语言模拟人类进行对话的程序。通常运行在特定的软件平台上,如PC平台或者移动终端设备平台。

近年来,很多科技公司都开始推出基于聊天机器人的AI产品和技术,如微软在2016年Build开发者大会发布了BOT平台,在此之前还推出过基于情感计算的聊天机器人小冰;Facebook在2016年发布了Facebook Messenger bot开发者平台;百度也推出了用于交互式搜索的聊天机器人小度。聊天机器人系统可以看作是机器人产业与“互联网+”的结合,符合国家的科研及产业化发展方向。

 从应用场景的角度来看,聊天机器人可以分为在线客服、娱乐、教育、个人助理和智能问答五个种类。

 在线客服聊天机器人系统的主要功能是同用户进行基本沟通并自动回复用户有关产品或服务的问题,以实现降低企业客服运营成本、提升用户体验的目的。其应用场景通常为网站首页和手机终端。代表性的商用系统有小I机器人、京东的JIMI客服机器人等。用户可以通过与JIMI聊天了解商品的具体信息以及反馈购物中存在的问题等。值得称赞的是,JIMI具备一定的拒识能力,即能够知道自己不能回答用户的哪些问题以及何时应该转向人工客服。

娱乐场景下聊天机器人系统的主要功能是同用户进行开放主题的对话,从而实现对用户的精神陪伴、情感慰藉和心理疏导等作用。其应用场景通常为社交媒体、儿童玩具等。代表性的系统如微软“小冰”、微信“小微”、“小黄鸡”、“爱情玩偶”等。其中微软“小冰”和微信“小微”除了能够与用户进行开放主题的聊天之外,还能提供特定主题的服务,如天气预报和生活常识等。

应用于教育场景下的聊天机器人系统根据教育的内容不同包括构建交互式的语言使用环境,帮助用户学习某种语言;在学习某项专业技能中,指导用户逐步深入地学习并掌握该技能;在用户的特定年龄阶段,帮助用户进行某种知识的辅助学习等。其应用场景通常为具备人机交互功能的学习、培训类软件以及智能玩具等。这里以科大讯飞公司的开心熊宝(具备移动终端应用软件和实体型玩具两种形态)智能玩具为例,“熊宝”可以通过语音对话的形式辅助儿童学习唐诗、宋词以及回答简单的常识性问题等。

个人助理类应用主要通过语音或文字与聊天机器人系统进行交互,实现个人事务的查询及代办功能,如天气查询、空气质量查询、定位、短信收发、日程提醒、智能搜索等,从而更便捷地辅助用户的日常事务处理。其应用场景通常为便携式移动终端设备。代表性的商业系统有AppleSiri、GoogleNow、微软Cortana、出门问问等。

智能问答类的聊天机器人主要功能包括回答用户以自然语言形式提出的事实型问题和需要计算和逻辑推理型的问题,以达到直接满足用户的信息需求及辅助用户进行决策的目的。其应用场景通常作为问答服务整合到聊天机器人系统中。典型的智能问答系统除了IBMWatson之外,还有WolframAlpha和Magi,后两者都是基于结构化知识库的问答系统,且分别仅支持英文和中文的问答。

微信聊天机器人的技术架构

本文实现的的聊天机器人是基于微信公众号开发平台,微信用户订阅机器人公众号,然后就可以愉快的聊天了。

整个系统的数据流图如下:

接下来就是对业务架构进行技术实现选型。

需要我们自己实现的是上图中的Chatbot Server和Chatbot Engine。

Chatbot Server其实就是一个普通的微信开发者应用服务器,所有基于微信平台的应用开发都需要有自己的服务器。

Chatbot Engine是聊天机器人的具体实现,提供SDK或者Rest API接口给Chatbot Server调用。

对于Chatbot Server,github上有一些开源的微信应用开发框架,利用它们可以轻松实现一个微信应用服务器,本文采用(https://github.com/messense/wechat-bot)项目,它不仅包含了web server,还完整的实现了微信chatbot功能。

对于Chatbot Engine,github上也有一些开源的chatbot框架,基于这些框架,可以开发自己的聊天引擎,本文采用(https://github.com/gunthercox/ChatterBot)项目,它提供了一个chatbot框架,可以扩展自己的引擎适配器。

技术实现

Chatbot Server

本例采用https://github.com/messense/wechat-bot项目。

其web server采用tornado,并采用wechatpy(https://github.com/jxtech/wechatpy)作为微信开发SDK。

项目的bot引擎可扩展,目前支持 simsimi,talkbot,v2ex等引擎,这些都是商业引擎的。

我们自己编写插件,支持chatterbot引擎,只需要实现respond接口就可以了。

#coding=utf-8

from chatterbot import ChatBot

from chatterbot.trainers import ChatterBotCorpusTrainer

deepbot =  ChatBot(

    ‘yuntongxun’,

    logic_adapters=[

        “chatterbot.adapters.logic.ClosestMatchAdapter”,

#        “chatterbot.adapters.logic.DeepLogicAdapter”

    ]

)

deepbot.set_trainer(ChatterBotCorpusTrainer)

deepbot.train(“chatterbot.corpus.english”)

deepbot.train(“chatterbot.corpus.chinese”)

__name__ = ‘deepbot’

def test(data, msg=None, bot=None):

    return True

def respond(data, msg=None, bot=None):

    return str(deepbot.get_response(data))

if __name__ == ‘__main__’:

    print(respond(“你好”))

Chatbot Engine

Chatbot Server 调用Chatbot Engine有2种方式,一种是Chatbot Engine以SDK的方式提供给Chatbot Server本地调用,一种是将Chatbot Engine封装成web服务,提供Web API给Chatbot Server调用。

对于SDK服务,可以直接用pip安装SDK,pip install chatterbot。

对于web服务,chatterbot项目也有相关的开源的web服务封装实现,其中一个是基于django框架(https://github.com/gunthercox/django_chatterbot),还一个是基于flask框架(https://github.com/chamkank/flask-chatterbot)。

本例上文展示的代码采用了SDK本地调用的方式。

总之,利用开源的chatterbot项目,我们一行代码都不用写就可以实现系统中的Chatbot Engine。

微信公众号后台配置

本例采用了微信订阅号,登录后台开发者中心,可以配置服务端回调URL,用户消息和开发者需要的事件推送,将会被转发到该URL中。如下图所示配置:

图中的URL地址:http://longtailer.cn,就是Chatbot Server的域名地址,所以事先需要准备一个公网的服务器和域名。

运行效果展示

总结

本文是《实现微信聊天机器人》系列的初级篇,介绍了微信机器人的技术架构和实现框架,对于Chatbot Engine的实现原理没有详细说明,Chatbot Engine是聊天机器人的核心,它涉及到自然语言处理、机器学习的相关原理和算法。

小程序开发之弹出框

小程序开发过程中,很多地方为了便利我们多采用小程序自带弹出框来实现交互效果。这也够大多数开发使用,下面我给大家详细介绍下小程序弹出框官方api传送门:https://developers.weixin.qq.com/miniprogram/dev/api/api-react.html?search-key=wx.show

  • wx.showToast()
    • title:显示的提示信息,在没有图标的情况下,文本内容可显示两行
    • icon:  显示的图标
      • success:成功图标
      • loading:加载图标
      • none:没有图标
    • image:自定义显示的图标,优先级高于icon
    • duration:延迟的时间,弹出框弹出后的显示时间
    • mask:true/false是否显示遮罩层
    • success:接口调用成功的回调函数
    • fail:接口调用失败的回调函数
    • complete:不管成功还是失败都会执行的函数

注:一般在点击事件中调用,可结合使用wx.hideTotast来使用,


  • wx.showLoading()
    • title:加载的提示信息eg:玩命加载中…
    • mask:是否现思遮罩层
    • success:接口调用成功的回调函数
    • fail:接口调用失败的回调函数
    • complete:无论成功还是失败都会执行的函数

注:一般需要结合使用wx.hideLoading();来使用可分别在onLoad和onReady中使用,数据渲染完成后关闭


  • wx.showModal()
    • title:提示信息的标题
    • content:提示信息的内容
    • showCancel:true/false是否显示取消按钮
    • cancelText:取消按钮的文本内容,不得超过四个字符
    • cancelColor:取消按钮的文本颜色,默认#000000
    • confirmText:确认按钮的文本内容,不得超过四个字符
    • confirmColor:却惹按钮的文本颜色,默认#000000
    • success:接口成功的回调
    • fail:接口失败的回调
  • complete:无论成功或失败都会嗲用

注:一般带年纪确认或取消,我们需要在success中进行事件的判断处理

1234567891011wx.showModal({title: '提示',content: '这是一个模态弹窗',success: function(res) {if (res.confirm) {console.log('用户点击确定')else if (res.cancel) {console.log('用户点击取消')}}})

  • wx.showActionSheet()
    • itemList: 底部弹出的信息元素为数组。
    • success:成功的回调
    • fail:失败的回调
123456789wx.showActionSheet({itemList: ['A''B''C'],success: function(res) {console.log(res.tapIndex)},fail: function(res) {console.log(res.errMsg)}})

注:bug是当数组中的元素较多时,弹出框的高度会很高,且不能上下滑动,因此,大家可以看我总结的小程序开发之组件的简单使用,这篇中我有总结弹出框组件的封装。


这里的话需要用到小程序自带的picker组件,picker一共分为三类

  • 普通选择器:mode = selector
    • range:下拉列表的数据,为一个obj或array。只有当mode=selector或者 mode= multiSelector时有效。
    • range-key:当range为对象时,range-key用来指定显示对象的哪一个属性上的属性值
    • value:下拉项的下标,可通过改变该属性来实现确定后数据的对应渲染。
    • bindchange:value改变时触发
    • bindcancel:取消或者点击遮罩层的时候触发
    • disabled:true/false是否禁用
12345<picker bindchange="bindPickerChange" value="{{index}}" range="{{array}}"><view class="picker">当前选择:{{array[index]}}</view></picker>
12345678910111213141516Page({/***array:普通选择器下拉的数据*index:默认显示的时下标为0的数据*/data: {array: ['美国''中国''巴西''日本'],index: 0,},bindPickerChange: function (e) {console.log('picker下拉项发生变化后,下标为:', e.detail.value)this.setData({index: e.detail.value})},})
  • 多列选择器:mode = multiSelector
    • range:下拉列表的数据,通过二位三位数组来表示对应列的数据。只有当mode=selector或者 mode= multiSelector时有效。
    • range-key: 当 range是一个 二维Object Array 时,通过 range-key 来指定 Object 中 key 的值作为选择器显示内容
    • value:是一个数组,依次对应每一列的下标。从0开始
    • bindchange:点击确定后触发的事件,返回当前的所有列的行元素所在的下标
    • bindcolumnchange:value改变时触发,返回的值为第几列第几行下标发生变化。
    • bindcancel:取消选择或点击遮罩层时触发
12345678<view class="section"><view class="section__title">多列选择器</view><picker mode="multiSelector" bindchange="bindMultiPickerChange" bindcolumnchange="bindMultiPickerColumnChange" value="{{multiIndex}}" range="{{multiArray}}"><view class="picker">当前选择:{{multiArray[0][multiIndex[0]]}},{{multiArray[1][multiIndex[1]]}},{{multiArray[2][multiIndex[2]]}}</view></picker></view>
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879Page({/***multiArray:多列选择的二位数组*multiIndex:默认显示的每列的下标*/data: {multiArray: [['无脊柱动物''脊柱动物'], ['扁性动物''线形动物''环节动物''软体动物''节肢动物'], ['猪肉绦虫''吸血虫']],multiIndex: [0, 0, 0],}, bindMultiPickerChange: function (e) {this.setData({multiIndex: e.detail.value})},bindMultiPickerColumnChange: function (e) {console.log(e);var data = {multiArray: this.data.multiArray,multiIndex: this.data.multiIndex};data.multiIndex[e.detail.column] = e.detail.value;switch (e.detail.column) {case 0:switch (data.multiIndex[0]) {case 0:data.multiArray[1] = ['扁性动物''线形动物''环节动物''软体动物''节肢动物'];data.multiArray[2] = ['猪肉绦虫''吸血虫'];break;case 1:data.multiArray[1] = ['鱼''两栖动物''爬行动物'];data.multiArray[2] = ['鲫鱼''带鱼'];break;}data.multiIndex[1] = 0;data.multiIndex[2] = 0;break;case 1:switch (data.multiIndex[0]) {case 0:switch (data.multiIndex[1]) {case 0:data.multiArray[2] = ['猪肉绦虫''吸血虫'];break;case 1:data.multiArray[2] = ['蛔虫'];break;case 2:data.multiArray[2] = ['蚂蚁''蚂蟥'];break;case 3:data.multiArray[2] = ['河蚌''蜗牛''蛞蝓'];break;case 4:data.multiArray[2] = ['昆虫''甲壳动物''蛛形动物''多足动物'];break;}break;case 1:switch (data.multiIndex[1]) {case 0:data.multiArray[2] = ['鲫鱼''带鱼'];break;case 1:data.multiArray[2] = ['青蛙''娃娃鱼'];break;case 2:data.multiArray[2] = ['蜥蜴''龟''壁虎'];break;}break;}data.multiIndex[2] = 0;// console.log(data.multiIndex);break;}this.setData(data);},})
  • 时间选择器:mode = time
  • value:表示选中的时间,时间格式为 hh:mm
  • start:表示时间的开始点,格式为hh:mm
  • end:表示时间的街书店,格式为hh:mm
  • bindchange:点击确定的时候触发,返回value值,返回的是选中的时间,
  • bindcancel:点击取消时或点击遮罩层时触发
  • disabled:true/false是否禁用
12345678<view class="section"><view class="section__title">时间选择器</view><picker mode="time" value="{{time}}" start="09:01" end="21:01" bindchange="bindTimeChange" bindcancel="cancel"><view class="picker">当前选择: {{time}}</view></picker></view>
123456789101112131415Page({/***time:选择的时间*/data: {time: '12:01',},bindTimeChange: function (e) {// console.log('picker发送选择改变,携带值为', e.detail.value)console.error(e);this.setData({time: e.detail.value})},})
  • 日期选择器:mode = data
    • value: 表示选中的日期,格式为”YYYY-MM-DD”
    • start: 表示有效日期范围的开始,字符串格式为”YYYY-MM-DD”
    • end: 表示有效日期范围的结束,字符串格式为”YYYY-MM-DD”
    • fields:参数(year,month,day)表示日期的粒度,为year时,下拉下标只显示年份选择,为month时会显示year,month,为day时会显示year,month,day
    • bindchange:点击确定的时候触发,返回的值为选择的时间
    • bindcancel:点击取消时或点击遮罩层时触发
    • disabled:true/false是否禁用
12345678<view class="section"><view class="section__title">日期选择器</view><picker mode="date" value="{{date}}" start="2015-09-01" end="2017-09-01" bindchange="bindDateChange"><view class="picker">当前选择: {{date}}</view></picker></view>
1234567891011121314Page({/***data:选择的日期*/data: {     date: '2016-09-01',},bindDateChange: function (e) {console.log('picker发送选择改变,携带值为', e.detail.value)this.setData({date: e.detail.value})},})
  • 省市区选择器:mode = region
    • value:  表示选中的省市区,默认选中每一列的第一个值
    • custom-item: 可为每一列的顶部添加一个自定义的项
    • start: 表示有效日期范围的开始,字符串格式为”YYYY-MM-DD”
    • end: 表示有效日期范围的结束,字符串格式为”YYYY-MM-DD”
    • fields:参数(year,month,day)表示日期的粒度,为year时,下拉下标只显示年份选择,为month时会显示year,month,为day时会显示year,month,day
    • bindchange:点击确定的时候触发, value 改变时触发 change 事件,event.detail = {value: value, code: code, postcode: postcode},其中字段code是统计用区划代码,postcode是邮政编码
    • bindcancel:点击取消时或点击遮罩层时触发
    • disabled:true/false是否禁用
12345678<view class="section"><view class="section__title">省市区选择器</view><picker mode="region" bindchange="bindRegionChange" value="{{region}}" custom-item="{{customItem}}"><view class="picker">当前选择:{{region[0]}},{{region[1]}},{{region[2]}}</view></picker></view>
123456789101112131415Page({/***time:选择的时间*/data: {     region: ['广东省''广州市''海珠区'],customItem: "全部"},bindRegionChange: function (e) {console.log('picker发送选择改变,携带值为', e.detail.value)this.setData({region: e.detail.value})}})

request:fail response data convert to UTF-8 fail(bom问题

这个问题,偶尔会有人遇到,但是因为提问者,没有将自己的环境等信息说明清楚,导致回答者十分难以回答;

以下为案例:

案例一:有@愚匠   @tony 提供的案例

“request:fail response data convert to UTF8 fail”已经解决了
是因为浪云SAE的实名认证问题,在没有实名认证情况下,返回的信息含有一个有特殊编码内容的中文字符串,ios系统无法识别,通过实名认证之后,这个字符串消失了,ios就好使了

根据愚匠的案例,基本可以得出,这个问题,是因为不可见的特殊编码内容导致的;

可以参考以下安卓中的案例进行排查是否有此问题:

后台返回数据中有bom非法字符, 前端可以用.trim()方法去一下, 治标的话得让后台把所有的文件编码格式改为utf-8
官方解释:近日有发现类似问题的都是因为返回的数据是 UTF-8 with BOM(即数据的开头是一个不可见字符 unicode 65279),Android 平台没有自动过滤,导致 JSON.parse 失败。目前需要开发者自行兼容,下个版本 Android 会过滤此字符。

<?php 前边有一个 <feff>:http://www.wxapp-union.com/portal.php?mod=view&aid=959

http://www.wxapp-union.com/forum.php?mod=viewthread&tid=1366:
你请求得到的res.data是否有值,如果没有值就检查一下ssl的问题。
如果有值但没有赋值成功,最可能的原因是获取的是一个字符串而不是一个数组或对象。
你需要做一个格式化, if(typeof res.data === ‘string’)var data = JSON.parse(res.data.trim());再用data赋值。
这个的原因是php输出的不会忽略BOM的文件头,特别使用windows自带写字板修改后就会有个\ufeff的字符在文件开始处,这个是不可见但会实际包含的。最后返回的就是字符串而非json数据,你直接对data赋值字符串是无法达到你想要的效果的,所以需要去掉,并重新格式化变成一个数组或对象。

新增案例:提供者@wsy0800@
求高手帮助 request:fail response data convert to UTF8 fail

新增案例:提供者:刘超
怎么让他不转,我响应的数据就是utf-8的

找到原因了
我对接的微擎 微擎会判断是否微信  如果是则跳转  那么返回的东西就是空  也就是转不了utf8的原因

微信小程序实现验证码倒计时效果

效果图

wxml

<input class='input-pwd' placeholder="新密码" placeholder-style='color: #000' password focus bindconfirm='getPwd'/>
<input class='input-tel' type='number' placeholder="手机号" placeholder-style='color: #000' maxlength='11 confirm-type='done' />
<input class='input-verify' type='number' placeholder-style='color: #000' placeholder='手机验证码'></input>
<button class='verify-btn' disabled='{{disabled}}' bindtap="getVerificationCode">{{time}}</button>

<button class='confirm-btn' bindtap='confirm_btn'>确认修改</button>

wxss

复制代码
/* pages/forgetpwd/forgetpwd.wxss */
input{
  padding-left: 20rpx;
  border-bottom: 1rpx solid #ccc;
  height: 80rpx;
  line-height: 80rpx;
  width: 95%;
  margin: 0 auto;
  font-size: 28rpx;
}
.input-verify{
  width: 67%;
  margin-left: 10rpx;
  float: left;
}
.verify-btn{
  width: 26%;
  height: 65rpx;
  float: right;
  line-height: 65rpx;
  background: #fff;    
  color: #5FD79D;
  margin: 20rpx 10rpx;
  font-size: 28rpx;
}
.confirm-btn{
  width: 80%;
  height: 90rpx;
  margin: 150rpx auto;
  background: #5FD79D;    
  color: #fff;
}
复制代码

js

复制代码
// pages/forgetpwd/forgetpwd.js
var interval = null //倒计时函数
Page({

  /**
   * 页面的初始数据
   */
  data: {
    time: '获取验证码', //倒计时 
    currentTime: 60
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {

  },

  getPwd:function(e){
    console.log(e.detail.value)
  },

  /**
   * 确认修改
   */
  confirm_btn:function(){
    wx.redirectTo({
      url: '/pages/login/login',
    })
  },
  
  getCode: function (options){
    var that = this;
    var currentTime = that.data.currentTime
    interval = setInterval(function () {
      currentTime--;
      that.setData({
        time: currentTime+'秒'
      })
      if (currentTime <= 0) {
        clearInterval(interval)
        that.setData({
          time: '重新发送',
          currentTime:60,
          disabled: false   
        })
      }
    }, 1000)  
  },
  getVerificationCode(){
    this.getCode();
    var that = this
    that.setData({
      disabled:true
    })
  },
})
复制代码

微信小程序picker实现的省市区三级联动

微信小程序的省市区三级联动需要使用到的是Picker多列选择器,参考文档:https://www.w3cschool.cn/weixinapp/d9mw1q95.html

案例中用到的省市区的json文件在文后发出出来。

废话不多说,来看看具体地实现吧。

视图view
请选择

JS代码
var arrays = common.getAreaInfo();//在头部引入 省市区地址js,这里封装成了方法了。


data:
data: {

citysIndex: [0, 0, 0], //给一个初始值索引,因为有三列,所以3个0

},

onLoad:
onLoad: function(options) {
var that = this;
if (wx.getStorageSync(‘global_cityData’)) {
var cityArray = wx.getStorageSync(‘global_cityData’);
} else {
//定义三列空数组
var cityArray = [
[],
[],
[],
];
for (let i = 0, len = arrays.length; i < len; i++) {
switch (arrays[i][‘level’]) {
case 1:
//第一列
cityArray[0].push(arrays[i][“name”]);
break;
case 2:
//第二列(默认由第一列第一个关联)
if (arrays[i][‘sheng’] == arrays[0][‘sheng’]) {
cityArray[1].push(arrays[i][“name”]);
}
break;
case 3:
//第三列(默认第一列第一个、第二列第一个关联)
if (arrays[i][‘sheng’] == arrays[0][‘sheng’] && arrays[i][‘di’] == arrays[1][‘di’]) {
cityArray[2].push(arrays[i][“name”]);
}
break;
}
}
wx.setStorageSync(‘global_cityData’, cityArray);
}

that.setData({
  cityArray: cityArray
});

},

下面就是两个事件了

func_changeCitysChange: function(e) {
var that = this;
var cityArray = that.data.cityArray;

var address='';
if (that.data.ssq == undefined){
  //下面方法中没有设置ssq,应该给它默认值 ,此时citysIndex相当于[0,0,0]
  var citysIndex = that.data.citysIndex;
  for (let i in citysIndex) {
    address += cityArray[i][citysIndex[i]]
  }
}else{
  address = that.data.ssq;
}

wx.showModal({
  title: '',
  content: address+'',
})

},
func_changeCitysChangeColumn: function(e) {
var that = this;
var cityArray = that.data.cityArray;

var list1 = []; //存放第二列数据,即市的列
var list2 = []; //存放第三列数据,即区的列

var citysIndex = [];
//主要是注意地址文件中的字段关系,省、市、区关联的字段有 sheng、di、level
switch (e.detail.column) {
  case 0:
    //滑动左列
    for (let i = 0, len = arrays.length; i < len; i++) {          
      if (arrays[i]['name'] == cityArray[0][e.detail.value]) {
        var sheng = arrays[i]['sheng'];
      }
      if (arrays[i]['sheng'] == sheng && arrays[i]['level'] == 2) {
        list1.push(arrays[i]['name']);
      }
      if (arrays[i]['sheng'] == sheng && arrays[i]['level'] == 3 && arrays[i]['di'] == arrays[1]['di']) {
        list2.push(arrays[i]['name']);
      }
    }       


    citysIndex = [e.detail.value, 0, 0];
    var ssq = cityArray[0][e.detail.value] + list1[0] + list2[0]+'';          

    that.setData({
      global_sheng: sheng
    });  
    break;
  case 1:
    //滑动中列
    var  di;
    var sheng = that.data.global_sheng;
    list1 = cityArray[1];
    for (let i = 0, len = arrays.length; i < len; i++) {     
      if (arrays[i]['name'] == cityArray[1][e.detail.value]) {
        di = arrays[i]['di'];
      }         
    } 
    for (let i = 0, len = arrays.length; i < len; i++) {
      if (arrays[i]['sheng'] == sheng && arrays[i]['level'] == 3 && arrays[i]['di'] == di) {
        list2.push(arrays[i]['name']);
      }
    }
    citysIndex = [that.data.citysIndex[0], e.detail.value, 0];

    var ssq = cityArray[0][that.data.citysIndex[0]] + list1[e.detail.value] + list2[0] + '';

    break;
  case 2:
    //滑动右列
    list1 = cityArray[1];
    list2 = cityArray[2];
    citysIndex = [that.data.citysIndex[0], that.data.citysIndex[1], e.detail.value];

    var ssq = cityArray[0][that.data.citysIndex[0]] + list1[that.data.citysIndex[1]] + list2[e.detail.value] + '';
    break;
}

that.setData({
  "cityArray[1]": list1,//重新赋值中列数组,即联动了市
  "cityArray[2]": list2,//重新赋值右列数组,即联动了区
  citysIndex: citysIndex,//更新索引
  ssq: ssq,//获取选中的省市区
});

},
用到的省市区js文件,点下面链接。
省市区js文件,点击查看,提取码: xy6v

微信小程序-省市区联动选择器

wx_selectArea

地址联动选择器采用微信小程序的 picker-view 组件

模板引入

提供 template 模板引入

  1. 引入wxmlwxss
// example.wxml
<import src="../../template/index.wxml"/>
<template is="areaPicker" data="{{...areaPicker}}" />

// example.wxss
@import '../../template/index.wxss';
  1. 组件初始化
// example.js

import initAreaPicker, { getSelectedAreaData } from '../../template/index';
Page({
    onShow() {
      initAreaPicker({
        // hideDistrict: true, // 是否隐藏区县选择栏,默认显示
      });
    },
    getSelecedData() {
        console.table(getSelectedAreaData()); // 提供`getSelectedAreaData`方法,返回当前选择的省市区信息组成的数组
    }
});

截图

省市区选择器
省市区选择器

旧版

小程序不支持 picker-view 组件时,用 scroll-view 模拟的联动选择器(不再维护)

旧版省市区选择器
GitHub地址(含新旧两个版本): https://github.com/treadpit/w…

微信小程序+人脸识别详解

识别过程借助于百度AI,服务器依旧是 SSM 框架

服务端代码
  • Base64Util
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.parking.util;

public class Base64Util {
    private static final char last2byte = (char)Integer.parseInt("00000011", 2);
    private static final char last4byte = (char)Integer.parseInt("00001111", 2);
    private static final char last6byte = (char)Integer.parseInt("00111111", 2);
    private static final char lead6byte = (char)Integer.parseInt("11111100", 2);
    private static final char lead4byte = (char)Integer.parseInt("11110000", 2);
    private static final char lead2byte = (char)Integer.parseInt("11000000", 2);
    private static final char[] encodeTable = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'};

    public Base64Util() {
    }

    public static String encode(byte[] from) {
        StringBuilder to = new StringBuilder((int)((double)from.length * 1.34D) + 3);
        int num = 0;
        char currentByte = 0;

        int i;
        for(i = 0; i < from.length; ++i) {
            for(num %= 8; num < 8; num += 6) {
                switch(num) {
                case 0:
                    currentByte = (char)(from[i] & lead6byte);
                    currentByte = (char)(currentByte >>> 2);
                case 1:
                case 3:
                case 5:
                default:
                    break;
                case 2:
                    currentByte = (char)(from[i] & last6byte);
                    break;
                case 4:
                    currentByte = (char)(from[i] & last4byte);
                    currentByte = (char)(currentByte << 2);
                    if(i + 1 < from.length) {
                        currentByte = (char)(currentByte | (from[i + 1] & lead2byte) >>> 6);
                    }
                    break;
                case 6:
                    currentByte = (char)(from[i] & last2byte);
                    currentByte = (char)(currentByte << 4);
                    if(i + 1 < from.length) {
                        currentByte = (char)(currentByte | (from[i + 1] & lead4byte) >>> 4);
                    }
                }

                to.append(encodeTable[currentByte]);
            }
        }

        if(to.length() % 4 != 0) {
            for(i = 4 - to.length() % 4; i > 0; --i) {
                to.append("=");
            }
        }

        return to.toString();
    }
}

此工具类用于将图片文件转换为 Base64 字符串的形式

  • FileUtil
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.parking.util;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class FileUtil {
    public FileUtil() {
    }

    public static byte[] readFileByBytes(String filePath) throws IOException {
        File file = new File(filePath);
        if(!file.exists()) {
            throw new FileNotFoundException(filePath);
        } else {
            ByteArrayOutputStream bos = new ByteArrayOutputStream((int)file.length());
            BufferedInputStream in = null;

            try {
                in = new BufferedInputStream(new FileInputStream(file));
                short bufSize = 1024;
                byte[] buffer = new byte[bufSize];

                int len1;
                while(-1 != (len1 = in.read(buffer, 0, bufSize))) {
                    bos.write(buffer, 0, len1);
                }

                byte[] var7 = bos.toByteArray();
                byte[] var9 = var7;
                return var9;
            } finally {
                try {
                    if(in != null) {
                        in.close();
                    }
                } catch (IOException var14) {
                    var14.printStackTrace();
                }

                bos.close();
            }
        }
    }
}

此工具类用于处理图片文件

- HttpUtil 
package com.parking.util;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Map;


public class HttpUtil {

    public static String post(String requestUrl, String accessToken, String params)
            throws Exception {
        String contentType = "application/x-www-form-urlencoded";
        return HttpUtil.post(requestUrl, accessToken, contentType, params);
    }

    public static String post(String requestUrl, String accessToken, String contentType, String params)
            throws Exception {
        String encoding = "UTF-8";
        if (requestUrl.contains("nlp")) {
            encoding = "GBK";
        }
        return HttpUtil.post(requestUrl, accessToken, contentType, params, encoding);
    }

    public static String post(String requestUrl, String accessToken, String contentType, String params, String encoding)
            throws Exception {
        String url = requestUrl + "?access_token=" + accessToken;
        return HttpUtil.postGeneralUrl(url, contentType, params, encoding);
    }

    public static String postGeneralUrl(String generalUrl, String contentType, String params, String encoding)
            throws Exception {
        URL url = new URL(generalUrl);

        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("POST");

        connection.setRequestProperty("Content-Type", contentType);
        connection.setRequestProperty("Connection", "Keep-Alive");
        connection.setUseCaches(false);
        connection.setDoOutput(true);
        connection.setDoInput(true);


        DataOutputStream out = new DataOutputStream(connection.getOutputStream());
        out.write(params.getBytes(encoding));
        out.flush();
        out.close();

        connection.connect();

        Map<String, List<String>> headers = connection.getHeaderFields();
 
        for (String key : headers.keySet()) {
            System.err.println(key + "--->" + headers.get(key));
        }
        
        BufferedReader in = null;
        in = new BufferedReader(
                new InputStreamReader(connection.getInputStream(), encoding));
        String result = "";
        String getLine;
        while ((getLine = in.readLine()) != null) {
            result += getLine;
        }
        in.close();
        System.err.println("result:" + result);
        return result;
    }
}
  • 人脸识别登录
    @ResponseBody
    @RequestMapping(value = "/FaceLogins", method = RequestMethod.POST)
    public Object loginFace(@RequestParam("files") CommonsMultipartFile file,
            HttpServletRequest request, HttpServletResponse response)
            throws IOException {

        String resMsg = "";
        String path = null;
        

        try {

            long startTime = System.currentTimeMillis();

            System.out.println("fileName:" + file.getOriginalFilename());
            path = request.getSession().getServletContext()
                    .getRealPath("upload");
            System.out.println("path:" + path);

            String fileTyle = ".png";// 全部以png格式进行保存
            String newFileName = "tempLogin" + fileTyle;
            System.out.println(newFileName);
            System.out.println(fileTyle);
            File newFile = new File(path, newFileName);

            file.transferTo(newFile);
            long endTime = System.currentTimeMillis();
            System.out.println("运行时间:" + String.valueOf(endTime - startTime)
                    + "ms");
            resMsg = "1";


        } catch (FileNotFoundException e) {

            e.printStackTrace();
            resMsg = "0";
        }

        System.out.println(resMsg);
        
        
        if(resMsg.equals("1")){
            // 进行人脸识别
            String username = (String)faceVerify(path+"\\tempLogin.png");
            
            System.out.println("username"+username);
            
            
            return username;
        }
        
        
        return 0;       
        

    }


    public Object faceVerify(String pathFile) {
        // 请求url
        String url = "https://aip.baidubce.com/rest/2.0/face/v3/search";
        try {

            byte[] bytes = FileUtil
                    .readFileByBytes(pathFile);
            String image = Base64Util.encode(bytes);

            Map<String, Object> map = new HashMap<String, Object>();
            map.put("image", image);
            map.put("image_type", "BASE64");
            map.put("liveness_control", "NORMAL");
            map.put("group_id_list", "park_sys");
            map.put("quality_control", "NONE");

            String param = GsonUtils.toJson(map);

            // 注意这里仅为了简化编码每一次请求都去获取access_token,线上环境access_token有过期时间,
            // 客户端可自行缓存,过期后重新获取。
            String accessToken = "5555555555";//请自行获取令牌

            String result = HttpUtil.post(url, accessToken, "application/json",
                    param);
            System.out.println(result);

            // 处理返回JSON
            JSONObject json;
            json = JSONObject.fromObject(result);
            
            //获取识别状态
            String code = json.getString("error_code");
            String msg  = json.getString("error_msg");
            
                        
            
            //人脸识别成功
            if (code.equals("0")&&msg.equals("SUCCESS")){
                JSONArray JArray = json.getJSONObject("result").getJSONArray("user_list");
                json = JSONObject.fromObject(JArray.get(0).toString());
                System.out.println(json.getString("user_id"));
                return json.getString("user_id");
            }else{
                return "Error";
            }
            
            
            
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

实现思路是从微信客户端获取上传的文件图片,并调用百度人脸识别接口与人脸库图片进行匹配识别,获取返回的用户信息,调用Service方法进行判断是否成功登录。

微信小程序

  faceLogin:function(){

    var flagTemp = '';

    var that = this;
    wx.chooseImage({
      count: 1, // 默认9
      sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
      sourceType: ['camera'],
  

      success: function (res) {
        wx.showLoading({
          title: '识别中',
        })
        var tempFilePaths = res.tempFilePaths
        wx.uploadFile({
          url: Api1,//使用人脸识别接口
          //method: 'GET',
          filePath: tempFilePaths[0],
          header: {
            'content-type': 'application/json' // 默认值
          },
          name: 'files',          
          success: function (res) {
            that.flagTemp = res.data;
            console.log("flag" + that.flagTemp);
          if (res.data != 0) {
            var uUsername = that.flagTemp;
            console.log(uUsername)

            user = {
              uUsername: uUsername,              
            }

            wx.request({          
              url: Api2,
              method: 'GET',
              data: user,
              header: {
                "Content-Type": "application/x-www-form-urlencoded"  // 默认值
              },
              success: function (res) {
                wx.hideLoading();
                app.globalData.userInfo = res.data;
                console.log(res.data);
                if (res.data != 0) {
                  wx.switchTab({
                    url: '../index/index'
                  })
                } else {
                  wx.showModal({
                    title: '识别失败',
                    content: '请重新识别',
                    showCancel: false, //不显示取消按钮
                    confirmText: '确定'
                  })
                }
              }
            })
          }
          
          }
        })
      }
    })

  },

只允许用户调用摄像头进行拍照,并调用文件上传API将图片上传进客户端获取用户名后在使用request方法对SpringMVC的Action进行用户查询,实现登录功能。
注意:如果不使用wx.request进行数据申请,是取不到服务端返回的JSON数据的