2017-02-25 13:38:03 qq_35700004 阅读数 1921

首先,页面内容


参数官方API说的很清楚了,我就不重复阐述。

参数填正确后打开这个页面会显示一个二维码


接下来用微信扫码,确定授权后这个页面会自动跳转到redirect_uri?code=xxx&state=xxxx

然后你就获得了关键的东西之一code!

接下来需要你用appId(开放平台申请的网站应用id)、appSecret(密匙)、code这三个参数去获取AccessToken

我的代码贴上

/**
* 获取授权凭证

* @param appId 公众账号的唯一标识
* @param appSecret 公众账号的密钥
* @param code
* @return Token
*/
public static getAccessToken(String appId, String appSecret, String code) {
WeixinOauth2Token wat = null;
// 拼接请求地址
String requestUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
requestUrl = requestUrl.replace("APPID", appId);
requestUrl = requestUrl.replace("SECRET", appSecret);
requestUrl = requestUrl.replace("CODE", code);
// 获取授权凭证
JSONObject jsonObject = HttpLink.httpsRequest(requestUrl, "GET", null,"https");
if (null != jsonObject) {
try {
wat = new WeixinOauth2Token();
wat.setAccessToken(jsonObject.getString("access_token"));
wat.setExpiresIn(jsonObject.getInt("expires_in"));
wat.setRefreshToken(jsonObject.getString("refresh_token"));
wat.setOpenId(jsonObject.getString("openid"));
wat.setScope(jsonObject.getString("scope"));
} catch (Exception e) {
wat = null;
int errorCode = jsonObject.getInt("errcode");
String errorMsg = jsonObject.getString("errmsg");
log.error("获取凭证失败 errcode:{} errmsg:{}", errorCode, errorMsg);
}
}
return wat;
}

贴上工具类httplink

import java.io.BufferedReader;     
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import net.sf.json.JSONException;
import net.sf.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * 通用工具类
 */
public class HttpLink {
private static Logger log = LoggerFactory.getLogger(HttpLink.class);


// 凭证获取(GET)
public final static String token_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";


/**
* 发送http、https请求

* @param requestUrl 请求地址
* @param requestMethod 请求方式(GET、POST)
* @param outputStr 提交的数据

* @param linkType http、https
* @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值

*/
public static JSONObject httpsRequest(String requestUrl, String requestMethod, String outputStr,String linkType) {
JSONObject jsonObject = null;
try {
if("https".equals(linkType)){
// 创建SSLContext对象,并使用指定的信任管理器初始化
TrustManager[] tm = { new MyX509TrustManager() };
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.init(null, tm, new java.security.SecureRandom());
// 从上述SSLContext对象中得到SSLSocketFactory对象
SSLSocketFactory ssf = sslContext.getSocketFactory();


URL url = new URL(requestUrl);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(ssf);

conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
// 设置请求方式(GET/POST)
conn.setRequestMethod(requestMethod);
jsonObject = closeLink(null,conn,outputStr);
}
else{
URL url = new URL(requestUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
if(requestMethod.endsWith("GET")){
conn.setDoInput(true);
}
else{
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
}
conn.setRequestMethod(requestMethod);
jsonObject = closeLink(conn,null,outputStr);
}
} catch (ConnectException ce) {
log.error("连接超时:{}", ce);
} catch (Exception e) {
log.error("https请求异常:{}", e);
}
return jsonObject;
}
/**
* 返回结果 释放资源
* @param conn
* @param conns
* @return
*/
public static JSONObject closeLink(HttpURLConnection conn,HttpsURLConnection conns,String ots){
try {
// 当ots不为null时向输出流写数据
if (null != ots) {
OutputStream outputStream=null;
if(conn!=null){
outputStream=conn.getOutputStream();
}
else{
outputStream=conns.getOutputStream();
}
// 设置编码格式
outputStream.write(ots.getBytes("UTF-8"));
outputStream.close();
}
JSONObject jsonObject = null;
// 从输入流读取返回内容
InputStream inputStream = conn==null?conns.getInputStream():conn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuffer buffer = new StringBuffer();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}


// 释放资源
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
inputStream = null;
if(conn!=null){
conn.disconnect();
}
else{
conns.disconnect();
}
jsonObject = JSONObject.fromObject(buffer.toString());
return jsonObject;
} catch (Exception e) {
return null;
}
}
}

还需要一个信任管理器,这里置为空就好,jre目录\lib\security里面自带信任好了的ssl证书不用管。



import java.security.cert.CertificateException; 
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;


/**
 * 信任管理器
 */
public class MyX509TrustManager implements X509TrustManager {


// 检查客户端证书
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}


// 检查服务器端证书
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}


// 返回受信任的X509证书数组
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}

WeixinAgentToken实体

/**
 * accessToken实体
 */
public class WeixinAgentToken  {
// 网页授权接口调用凭证
private String accessToken;
// 凭证有效时长
private int expiresIn;
// 用于刷新凭证
private String refreshToken;
// 用户标识
private String openId;
// 用户授权作用域
private String scope;


public String getAccessToken() {
return accessToken;
}


public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}


public int getExpiresIn() {
return expiresIn;
}


public void setExpiresIn(int expiresIn) {
this.expiresIn = expiresIn;
}


public String getRefreshToken() {
return refreshToken;
}


public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}


public String getOpenId() {
return openId;
}


public void setOpenId(String openId) {
this.openId = openId;
}


public String getScope() {
return scope;
}


public void setScope(String scope) {
this.scope = scope;
}


}

接下来调用getAccessToken这个方法就可以了,这时候你可以从返回的实体拿到AccessToken以及openid,下一步开始。

调用下面这个方法

/**
* 开发平台获取用户信息

* @param accessToken 接口访问凭证
* @param openId 用户标识
* @return WeixinUserInfo
*/
public static WeixinUserInfo getAgentWxUserInfo(String accessToken, String openId) {
WeixinUserInfo weixinUserInfo = null;
// 拼接请求地址
String requestUrl = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID";
requestUrl = requestUrl.replace("ACCESS_TOKEN", accessToken).replace("OPENID", openId);
// 获取用户信息
JSONObject jsonObject = HttpLink.httpsRequest(requestUrl, "GET", null,"https");
if (null != jsonObject) {
try {
weixinUserInfo = new WeixinUserInfo();
// 用户的标识
weixinUserInfo.setOpenId(jsonObject.getString("openid"));
// 昵称
weixinUserInfo.setNickname(jsonObject.getString("nickname"));
// 用户的性别(1是男性,2是女性,0是未知)
weixinUserInfo.setSex(jsonObject.getInt("sex"));
// 用户所在国家
weixinUserInfo.setCountry(jsonObject.getString("country"));
// 用户所在省份
weixinUserInfo.setProvince(jsonObject.getString("province"));
// 用户所在城市
weixinUserInfo.setCity(jsonObject.getString("city"));
// 用户的语言,简体中文为zh_CN
weixinUserInfo.setLanguage(jsonObject.getString("language"));
// 用户头像
weixinUserInfo.setHeadImgUrl(jsonObject.getString("headimgurl"));
//开发平台统一unionid
weixinUserInfo.setUnionid(jsonObject.getString("unionid"));
} catch (Exception e) {
log.error("开放平台获取用户信息失败errmsg:{}", e.getMessage());
}
}
return weixinUserInfo;
}

贴上实体



/**
 * 微信用户的基本信息
 */
public class WeixinUserInfo {
// 用户的标识
private String openId;
// 关注状态(1是关注,0是未关注),未关注时获取不到其余信息
private int subscribe;
// 用户关注时间,为时间戳。如果用户曾多次关注,则取最后关注时间
private String subscribeTime;
// 昵称
private String nickname;
// 用户的性别(1是男性,2是女性,0是未知)
private int sex;
// 用户所在国家
private String country;
// 用户所在省份
private String province;
// 用户所在城市
private String city;
// 用户的语言,简体中文为zh_CN
private String language;
// 用户头像
private String headImgUrl;
//unionid
private String unionid;

public String getOpenId() {
return openId;
}


public void setOpenId(String openId) {
this.openId = openId;
}


public int getSubscribe() {
return subscribe;
}


public void setSubscribe(int subscribe) {
this.subscribe = subscribe;
}


public String getSubscribeTime() {
return subscribeTime;
}


public void setSubscribeTime(String subscribeTime) {
this.subscribeTime = subscribeTime;
}


public String getNickname() {
return nickname;
}


public void setNickname(String nickname) {
this.nickname = nickname;
}


public int getSex() {
return sex;
}


public void setSex(int sex) {
this.sex = sex;
}


public String getCountry() {
return country;
}


public void setCountry(String country) {
this.country = country;
}


public String getProvince() {
return province;
}


public void setProvince(String province) {
this.province = province;
}


public String getCity() {
return city;
}


public void setCity(String city) {
this.city = city;
}


public String getLanguage() {
return language;
}


public void setLanguage(String language) {
this.language = language;
}


public String getHeadImgUrl() {
return headImgUrl;
}


public void setHeadImgUrl(String headImgUrl) {
this.headImgUrl = headImgUrl;
}


public String getUnionid() {
return unionid;
}


public void setUnionid(String unionid) {
this.unionid = unionid;
}
}

好了,用户信息获取完毕。接下来该做啥做啥。

2014-09-29 22:25:54 zdhjiony 阅读数 148

最近开发完成了一个微信推广渠道管理—带参数二维码管理平台,即:微信二维码来源统计分析、微信带参数二维码统计分析、微信渠道二维码统计分析、自动生成微信二维码统计分析、微信海报二维码统计分析管理平台(地推派传单提供有效渠道分析)。解决了微信公众号地推派单推广渠道跟踪的问题,可以实时进行监控派单推广渠道的扫码微信的关注,取消,新增,净增等相关数据,实时进行数据分析,为提供推广决策有了强有力的帮助和分析,同时对派单人员进行有效的考核,比如每个派单人员现场扫码的关注用户数情况,进行有效的跟踪,为奖惩提供有效的数据。我觉得这个功能倒很实用,大家试想一下下面这几种情形,你辛辛苦苦做了一个送餐的服务号,现在你准备去做推广了,现在找了3个地推的人员去扫楼,发传单,这还不够,你又找了本地的百度营销人员,也是付费推广。可是你心里肯定犯嘀咕,这3个地推人员谁干的好,谁干的差?又或者是谁又偷懒了,谁又直接把传单扔垃圾桶了?这个百度的推广效果又怎么样?现在这个带参数的二维码就是来帮你解决这个问题的。用它可以帮你发现好的推广方法和效率高的推广人员,然后你可以将营销资源资源转移到这些好的推广渠道,好的推广方法,好的推广人员身上。下面进行系统管理平台的部分截图。    

 一、列举所有渠道所分配的带参数二维码:


 
 

二、下图为每个渠道列出每天的微信数据关注,取消数据情况。


 
 

三、下图是统计各个渠道的每天的数据及汇总。


   

最近系统也改版升级了,提供部分的功能截图看看:

1、设置二维码场景类型,比如把二维码印在传单里,印在名片里,印在海报里等等,我们就可以把二维码进行场景类型归类,方便统计不同场景下的推广情况。



 

 

2、可以查看每个二维码的数据情况,也可以点击查看微信数据查看每天的数据。


 

3、统计每天的用户新增情况。

 

主要的功能是这些,因为我做了推广微信二维码的统计,这个在很多微信公众平台里都很急需,而且目前好像很缺失这方面的详细统计;还有最近有不少客户问到,原来已在使用的微信平台,如何对接咱们这个二维码统计呢,为了方便融合第三方微信平台,我特意做了个通用接口,只需调用该接口,即可使用二维码统计。详细接口请加微信联系我: 扫描下面二维码或输入 jionywx 加我微信,微信验证是:javaeye



 
 

2019-05-09 10:57:08 weixin_38361347 阅读数 340

微信扫描二维码后 【未关注】 和 【关注】 之后的扫码事件推送

可能会遇到这么个需求,通过微信生成的二维码,在用户扫码后(这个期间,若用户第一次扫码,没关注公众号,此时会跳转到关注界面,点击关注之后,才会推送消息,然后就可以用到下面关注取消推送事件机制了。)

微信生成二维码 传送门

以下是官方文档说明:

接收事件推送

在微信用户和公众号产生交互的过程中,用户的某些操作会使得微信服务器通过事件推送的形式通知到开发者在开发者中心处设置的服务器地址,从而开发者可以获取到该信息。其中,某些事件推送在发生后,是允许开发者回复用户的,某些则不允许,详细内容如下:

目录

1 关注/取消关注事件

2 扫描带参数二维码事件

关注/取消关注事件

用户在关注与取消关注公众号时,微信会把这个事件推送到开发者填写的URL。方便开发者给用户下发欢迎消息或者做帐号的解绑。为保护用户数据隐私,开发者收到用户取消关注事件时需要删除该用户的所有信息。

微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次。

关于重试的消息排重,推荐使用FromUserName + CreateTime 排重。

假如服务器无法保证在五秒内处理并回复,可以直接回复空串,微信服务器不会对此作任何处理,并且不会发起重试。

推送XML数据包示例:

<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[FromUser]]></FromUserName>
  <CreateTime>123456789</CreateTime>
  <MsgType><![CDATA[event]]></MsgType>
  <Event><![CDATA[subscribe]]></Event>
</xml>

参数说明:
参数 描述
ToUserName 开发者微信号
FromUserName 发送方帐号(一个OpenID)
CreateTime 消息创建时间 (整型)
MsgType 消息类型,event
Event 事件类型,subscribe(订阅)、unsubscribe(取消订阅)

扫描带参数二维码事件

用户扫描带场景值二维码时,可能推送以下两种事件:

如果用户还未关注公众号,则用户可以关注公众号,关注后微信会将带场景值关注事件推送给开发者。
如果用户已经关注公众号,则微信会将带场景值扫描事件推送给开发者。
  1. 用户未关注时,进行关注后的事件推送

推送XML数据包示例:

<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[FromUser]]></FromUserName>
  <CreateTime>123456789</CreateTime>
  <MsgType><![CDATA[event]]></MsgType>
  <Event><![CDATA[subscribe]]></Event>
  <EventKey><![CDATA[qrscene_123123]]></EventKey>
  <Ticket><![CDATA[TICKET]]></Ticket>
</xml>

参数说明:
参数 描述
ToUserName 开发者微信号
FromUserName 发送方帐号(一个OpenID)
CreateTime 消息创建时间 (整型)
MsgType 消息类型,event
Event 事件类型,subscribe
EventKey 事件KEY值,qrscene_为前缀,后面为二维码的参数值
Ticket 二维码的ticket,可用来换取二维码图片

  1. 用户已关注时的事件推送

推送XML数据包示例:

<![CDATA[toUser]]> <![CDATA[FromUser]]> 123456789 <![CDATA[event]]> <![CDATA[SCAN]]> <![CDATA[SCENE_VALUE]]> <![CDATA[TICKET]]>

参数说明:
参数 描述
ToUserName 开发者微信号
FromUserName 发送方帐号(一个OpenID)
CreateTime 消息创建时间 (整型)
MsgType 消息类型,event
Event 事件类型,SCAN
EventKey 事件KEY值,是一个32位无符号整数,即创建二维码时的二维码scene_id
Ticket 二维码的ticket,可用来换取二维码图片

注意:未关注和关注推送的信息类型不一致

类型 MsgType 消息类型 Event 事件类型 EventKey事件KEY值 Ticket 二维码的ticket
未关注 event subscribe qrscene_(以此开头后面是key值) TICKET
关注 event SCAN SCENE_VALUE(值即为key值) TICKET

获取二维码中的值,根据MsgType (消息类型)一致 可以根据 Event (事件类型)来区分,然后根据key值判断获取!

不废话了直接上代码:
 		
 		// xmlData 是微信返回给你服务器的xml数据包,转为map后的数据格式。


		String event = xmlData.get("Event");
        String wxId = xmlData.get("FromUserName");

			//url事件
        if (event.equals("VIEW")) {
            return baseMsg = RequestParamTool.url(xmlData);
            //点击事件
        }else if (event.equals("CLICK")) {
            return  baseMsg = RequestParamTool.handleClick(xmlData);
			//Event 事件类型 判断关注和未关注
        } else if (event.equals("SCAN") || event.equals("subscribe")) {
            String eventKey = xmlData.get("EventKey");
            //
            if (eventKey.contains("qrscene_")) {
                eventKey = eventKey.replace("qrscene_", "");
            }
            
            log.info("eventKey   === {} " , eventKey); //eventKey
2017-09-09 15:07:55 csdn_Info 阅读数 6124

需求

  • 用户通过微信扫码关注公众号获取红包

解决方案


步骤

  • 生成带参数的二维码
  • 扫码

    • 是否关注公众号

      • 已关注,则微信会将带场景值扫描事件推送给开发者,根据场景判断当前二维码是否失效

        • 失效,公众号推送消息:当前二维码已失效
        • 未失效,获取当前扫码用户openid查询是否已经领取过红包
          1. 已领取,则公众号推送消息到微信用户通知今日已领取
          2. 未领取
            • 则调用发送红包接口,发送红包,发送成功,则存储当前微信用户信息,包含唯一 openid。
            • 设置当前扫描二维码失效
      • 未关注,则用户可以关注公众号,关注后微信会将带场景值关注事件推送给开发者

        • 根据场景值查询二维码是否失效
          • 失效,公众号推送消息:当前二维码已失效
          • 未失效,获取当前扫码用户openid查询是否已经领取过红包
            1. 已领取,则公众号推送消息到微信用户通知今日已领取
            2. 未领取
              • 则调用发送红包接口,发送红包,发送成功,则存储当前微信用户信息,包含唯一 openid。
              • 设置当前扫描二维码失效

  • 生成带参数的二维码
    • 目前有2种类型的二维码:
      • 临时二维码,是有过期时间的,最长可以设置为在二维码生成后的30天(即2592000秒)后过期,但能够生成较多数量。临时二维码主要用于帐号绑定等不要求二维码永久保存的业务场景
      • 永久二维码,是无过期时间的,但数量较少(目前为最多10万个)。永久二维码主要用于适用于帐号绑定、用户来源统计等场景。
    • 用户扫描带场景值二维码时,可能推送以下两种事件:
      1. 用户还未关注公众号,则用户可以关注公众号,关注后微信会将带场景值关注事件推送给开发者。
      2. 用户已经关注公众号,在用户扫描后会自动进入会话,微信也会将带场景值扫描事件推送给开发者。
    • 获取带参数的二维码的过程包括两步
      1. 首先创建二维码ticket
      2. 然后凭借ticket到指定URL换取二维码

这里我们选择永久二维码


  • 然后,将TOKEN添加到请求ticket地址,进行请求
    • 成功返回说明
      {
      “ticket”:”xxx”,
      “expire_seconds”:60,
      “url”:”http:\/\/weixin.qq.com\/q\/xxx”
      }
    • 参数说明
      1. ticket :二维码ticket,凭借此ticket可以在有效时间内换取二维码
      2. expire_seconds:二维码有效时间,以秒为单位。 最大不超过2592000(即30天)
      3. url:二维码图片解析后的地址


  • 用户扫描带参数的二维码
    • 用户未关注时,进行关注后的事件推送,xml数据包
<xml>
    <ToUserName>接收人</ToUserName>
    <FromUserName>发送人</FromUserName>
    <CreateTime>创建时间,时间戳</CreateTime>
    <MsgType>消息类型【event】</MsgType>
    <Event>事件类型【subscribe】</Event>
    <EventKey>事件KEY值,qrscene_为前缀【scan事件无此前缀,】</EventKey>
    <Ticket>二维码的ticket</Ticket>
</xml>

    • 用户已关注时的事件推送
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[SCAN]]></Event>
<EventKey><![CDATA[SCENE_VALUE]]></EventKey>
<Ticket><![CDATA[TICKET]]></Ticket>
</xml>

*:EventKey也就是生成二维码时的场景ID,比如生成二维码时传的场景ID为 A,那么此处EventKey 则为 A


  • 知道了这个EventKEY值,
    • 查询当前二维码是否失效
  • 知道了ToUserName也就是接收人openid

    • 查询当前微信唯一openid是否存在
  • 如果当前二维码未失效,并且当前 openid 不存在数据库中,那么则可以调用发送红包接口进行红包发送。



<xml>
    <sign><![CDATA[红包签名key]]></sign>
    <mch_billno><![CDATA[订单号]]></mch_billno>
    <mch_id><![CDATA[商户号]]></mch_id>
    <wxappid><![CDATA[公众号APPID]]></wxappid>
    <send_name><![CDATA[发送方名称]]><send_name>
    <re_openid><![CDATA[接收人openid]]></re_openid>
    <total_amount><![CDATA[红包金额]]></total_amount>
    <total_num><![CDATA[红包数量]]></total_num>
    <wishing><![CDATA[恭喜发财]]></wishing>
    <client_ip><![CDATA[调用接口的机器Ip地址]]></client_ip>
    <act_name><![CDATA[活动名称]]></act_name>
    <remark><![CDATA[备注]]></remark>
    <scene_id><![CDATA[场景值,非必须]]></scene_id>
    <consume_mch_id><![CDATA[资金授权商户号,非必须]]></consume_mch_id>
    <nonce_str><![CDATA[随机字符串]]></nonce_str>
    <risk_info>活动信息,非必须</risk_info>
</xml>
  • 红包签名算法
  • 这里请求的时候要带上请求证书
    • 证书说明地址
    • 证书下载地址
      • 微信商户平台(pay.weixin.qq.com)–>账户设置–>API安全–>证书下载
  • 使用证书

    • 使用商户证书
      ◆ apiclient_cert.p12是商户证书文件,除PHP外的开发均使用此证书文件
      ◆ 商户如果使用.NET环境开发,请确认Framework版本大于2.0,必须在操作系统上双击安装证书apiclient_cert.p12后才能被正常调用。
      ◆ 商户证书调用或安装都需要使用到密码,该密码的值为微信商户号(mch_id)
  • 成功返回说明,xml

<xml>
    <return_code><![CDATA[SUCCESS]]></return_code>
    <return_msg><![CDATA[发放成功.]]></return_msg>
    <result_code><![CDATA[SUCCESS]]></result_code>
    <err_code><![CDATA[0]]></err_code>
    <err_code_des><![CDATA[发放成功.]]></err_code_des>
    <mch_billno><![CDATA[00100104xxx]]></mch_billno>
    <mch_id>10010404</mch_id>
    <wxappid><![CDATA[wx6fa7e3bab7e15415]]></wxappid>
    <re_openid><![CDATA[onqOjjmM1tad-3ROpncN-yUfa6uI]]></re_openid>
    <total_amount>1</total_amount>
</xml>

代码见第二章


2019-07-19 11:58:42 flysnownet 阅读数 12641

引言

本篇介绍的二维码登录不是微信开发平台的二维码登录,而是利用微信公众号临时二维码扫码事件关注公众号进行登录注册,

浏览器判断扫码状态有两种方式,

第一种是ajax每隔一秒进行轮询,如果用户扫码了则后台给个成功状态

第二种是进入页面后链接websocket等待服务器主动通知,

优缺点分析:

轮询:客户端定时向服务器发送Ajax请求,服务器接到请求后马上返回响应信息并关闭连接。 
优点:后端程序编写比较容易。 
缺点:请求中有大半是无用,浪费带宽和服务器资源。 

websocket

优点:在无消息的情况下不会频繁的请求,耗费资源小。 
缺点:服务器报纸连接会消耗资源

简单流程

  1. 后台监听websocket
  2. 浏览器请求登录页--
  3. 后台生成微信零时二维码(带参数uid)--
  4. 浏览器显示二维码并连接websocket---
  5. 用户扫码
  6. 微信服务器通知业务服务器
  7. 服务器获取到用户信息进行注册并生成token通过websocket通知浏览器登陆成功
  8. 浏览器携带token跳转到登陆成功页面

实现

1.后台开启websocket

这里以tp5集成的workman为例进行设置,跟自己下载的基本相同,其他框架或自己安装的请参考workman官方文档

1.安装workerman     

  1. tp5文档:https://www.kancloud.cn/manual/thinkphp5_1/354134
composer require topthink/think-worker=2.0.*

 2.配置 config/worker_server.php

因为tp5配置方式不支持自定义函数(或许是我没找到),这里使用自定义类作为Worker服务入口文件类,在配置文件里填写服务类名即可开启,

// 扩展自身需要的配置
    'protocol'       => 'websocket', // 协议 支持 tcp udp unix http websocket text
    'host'           => '0.0.0.0', // 监听地址
    'port'           => 2345, // 监听端口
    'socket'         => '', // 完整监听地址
    'context'        => [], // socket 上下文选项
    'worker_class'   => 'app\http\Worker', // 自定义Workerman服务类名 支持数组定义多个服务

    // 支持workerman的所有配置参数
    'name'           => 'thinkphp',
    'count'          => 4,
    'daemonize'      => false,
    'pidFile'        => Env::get('runtime_path') . 'worker.pid',

Worker.php

 

 

ps:这里把浏览器第一次发来的信息作为uid保存,为了后面主动给浏览器发送消息,

必须设置心跳,客户端每隔一定时间向服务器发送消息,不然超过超时时间服务器将断开连接,避免客户端异常时浪费资源

$inner_text_worker内部服务是为了后面扫码成功后接收通知,然后向特定客户端发送消息用

详细请参考:https://wenda.workerman.net/question/508

<?php
namespace app\http;

use think\worker\Server;
use Workerman\Lib\Timer;
use Workerman\Worker as W;

class Worker extends Server
{
    protected $socket = 'websocket://0.0.0.0:2345';

    /**
     * 每个进程启动
     * @param $worker
     */


    public function onWorkerStart($worker)
    {
        // 开启一个内部端口,方便内部系统推送数据,Text协议格式 文本+换行符
        $inner_text_worker = new W('Text://0.0.0.0:5678');
        $inner_text_worker->onMessage = function($connection, $buffer)
        {
            // $data数组格式,里面有uid,表示向那个uid的页面推送数据
            $data = json_decode($buffer, true);
            $uid = $data['uid'];
            // 通过workerman,向uid的页面推送数据
            $ret = $this->sendMessageByUid($uid, $buffer);
            if ($ret)
            {
                $msg['error'] = 0;
                $msg['msg'] = 'ok';
            }else{
                $msg['error'] = 1;
                $msg['msg'] = '异常';
            }
            $msg = json_encode($msg);

            // 返回推送结果
            $connection->send($msg);
        };
        $inner_text_worker->listen();

        // 心跳间隔55秒
        define('HEARTBEAT_TIME', 55);
        Timer::add(1, function()use($worker){
            $time_now = time();
            foreach($worker->connections as $connection) {
                // 有可能该connection还没收到过消息,则lastMessageTime设置为当前时间
                if (empty($connection->lastMessageTime)) {
                    $connection->lastMessageTime = $time_now;
                    continue;
                }
                // 上次通讯时间间隔大于心跳间隔,则认为客户端已经下线,关闭连接
                if ($time_now - $connection->lastMessageTime > HEARTBEAT_TIME) {
                    $connection->close();
                }
            }
        });
    }

    public function onMessage($connection,$data)
    {
        global $worker;
        // 判断当前客户端是否已经验证,即是否设置了uid
        if(!isset($connection->uid))
        {
            // 没验证的话把第一个包当做uid(这里为了方便演示,没做真正的验证)
            $connection->uid = $data;
            /* 保存uid到connection的映射,这样可以方便的通过uid查找connection,
             * 实现针对特定uid推送数据
             */
            $worker->uidConnections[$connection->uid] = $connection;
            //return $connection->send('login success, your uid is ' . $connection->uid);
        }
        // 给connection临时设置一个lastMessageTime属性,用来记录上次收到消息的时间
        $connection->lastMessageTime = time();
        //$connection->send('receive success');
        echo $data;
        echo "\n";
    }

    public function onConnect($connection)
    {

    }

    /**
     * 当连接断开时触发的回调函数
     * @param $connection
     */
    public function onClose($connection)
    {
        global $worker;
        if(isset($connection->uid))
        {
            // 连接断开时删除映射
            unset($worker->uidConnections[$connection->uid]);
        }
    }
    /**
     * 当客户端的连接上发生错误时触发
     * @param $connection
     * @param $code
     * @param $msg
     */
    public function onError($connection, $code, $msg)
    {
        echo "error $code $msg\n";
    }


    // 针对uid推送数据
    public function sendMessageByUid($uid, $message)
    {
        global $worker;
        if(isset($worker->uidConnections[$uid]))
        {
            $connection = $worker->uidConnections[$uid];
            $connection->send($message);
            return true;
        }
        return false;
    }


}

 启动服务器  如下图

php think worker:server

 

2.生成微信临时二维码  (这里使用easywechat,详细请看我其他相关博文)

生成唯一uid,并把uid当做二维码的场景值生成二维码

public function index()
    {
        $uid = make_uid();
        //$ticket = Cache::store('redis')->get('login_ticket');
        $ticket = $this->loginCode($uid);
        Cache::store('redis')->set('login'.$uid,1,600);
        $data = array(
            'uid' => $uid,
            'ticket' => $ticket
        );
        return view('index',$data);
    }



    public function loginCode($uid)
    {
        $config = [
            'app_id' => Option::get('app_id')->option_value,
            'secret' => Option::get('app_secret')->option_value,
            'token' => Option::get('app_token')->option_value,
            'response_type' => 'array',
            'log' => [
                'level' => 'debug',
                'file' => Env::get('runtime_path').'/wechat.log',
            ],
            'oauth' => [
                'scopes'   => ['snsapi_userinfo'],
                'callback' => '/index/login/oauth_callback',
            ],
        ];
        $app = Factory::officialAccount($config);
        $result = $app->qrcode->temporary($uid, 600);

        $url = $app->qrcode->url($result['ticket']);
        return $url;

    }

/**
 * Notes:生成UID
 * @auther: xxf
 * Date: 2019/7/17
 * Time: 17:28
 * @return string
 */
function make_uid()
{
    @date_default_timezone_set("PRC");
    //号码主体(YYYYMMDDHHIISSNNNNNNNN)
    $order_id_main = date('YmdHis') . rand(10000000,99999999);
    $order_id_len = strlen($order_id_main);
    $order_id_sum = 0;
    for($i=0; $i<$order_id_len; $i++){
        $order_id_sum += (int)(substr($order_id_main,$i,1));
    }
    //唯一号码(YYYYMMDDHHIISSNNNNNNNNCC)
    $uid = $order_id_main . str_pad((100 - $order_id_sum % 100) % 100,2,'0',STR_PAD_LEFT);
    return $uid;
}

 

3.浏览器显示二维码并向后台发送uid

前端页面index.html

 

ps:每隔30秒向服务器发送一条数据,避免超时,

onmessage里,后台返回token说明登录成功,进行跳转
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" type="text/css" href="__CSS__/common.css">
    <link rel="stylesheet" type="text/css" href="__CSS__/weLogin.css" />
    <title>微信登录</title>
</head>

<body>
<div class="login block">
    <div class="we-login">
        <p class="login-title">微信登录</p>
        <div class="login-img">
            <img src="{$ticket}" />
        </div>
        <div class="login-desc">
            <p><span>扫码</span>
                <span>></span>
                <span>关注</span>
                <span>></span>
                <span>登录</span></p>
        </div>
    </div>
</div>

<script>
    var wsServer = 'ws://127.0.0.1:2345';
    var websocket = new WebSocket(wsServer);
    websocket.onopen = function (evt) {
        console.log("Connected to WebSocket server.");
        //ws.send("Hello WebSockets!");
        websocket.send("{$uid}");
        //设置心跳,避免服务器断开
        setInterval(function () {
            websocket.send("{$uid}");
        }, 30000)
    };

    websocket.onclose = function (evt) {
        console.log("Disconnected");
    };

    websocket.onmessage = function (evt) {
        console.log('Retrieved data from server: ' + evt.data);
        var res = JSON.parse(evt.data);
        if(res.token !== '' && res.token !== undefined){
            window.location='/index/login/login?token='+res.token;
        }
    };

    websocket.onerror = function (evt, e) {
        console.log('Error occured: ' + evt.data);
    };
</script>
</body>

</html>

4.用户扫码,后台进行注册与通知

微信消息处理请看:https://blog.csdn.net/flysnownet/article/details/90239582

 

<?php

namespace app\api\controller;

use app\common\model\Option;
use EasyWeChat\Kernel\Messages\News;
use EasyWeChat\Kernel\Messages\NewsItem;
use think\Controller;
use think\facade\Env;
use think\Request;
use EasyWeChat\Factory;
use think\facade\Session;
use message\MessageHandler;

/**微信消息处理
 * Class Message
 * @package app\api\controller
 */
class Message extends Controller
{

    public function __construct(Request $request)
    {
        global $_W;
        $_W['config'] = [
            'app_id' => Option::get('app_id')->option_value,
            'secret' => Option::get('app_secret')->option_value,
            'token' => Option::get('app_token')->option_value,
            'response_type' => 'array',
            'log' => [
                'level' => 'debug',
                'file' => Env::get('runtime_path').'/wechat.log',
            ],
            'oauth' => [
                'scopes'   => ['snsapi_userinfo'],
                'callback' => '/index/login/oauth_callback',
            ],
        ];

        //微信首次接入验证
        if (!empty($_GET['echostr']) && $this->checkSignature($_W['config']['token'])) {
            header('content-type:text');
            echo $_GET['echostr'];
            exit;
        }
    }


    public function index(Request $request)
    {
        global $_W;
        $app = Factory::officialAccount($_W['config']);

        $app->server->push(function ($message) {
            // $message['FromUserName'] // 用户的 openid
            // $message['MsgType'] // 消息类型:event, text....

            $handler = new MessageHandler($message);
            switch ($message['MsgType']) {
                case 'event':
                    //return '收到事件消息';
                    return $handler->eventHandler($message['FromUserName']);
                    break;
                case 'text':
                    //return '收到文字消息';
                    return $handler->textHandler($message['FromUserName']);
                    break;
                case 'image':
                    return '收到图片消息';
                    break;
                case 'voice':
                    return '收到语音消息';
                    break;
                case 'video':
                    return '收到视频消息';
                    break;
                case 'location':
                    //return '收到坐标消息';
                    return $handler->test();
                    break;
                case 'link':
                    return '收到链接消息';
                    break;
                case 'file':
                    return '收到文件消息';
                // ... 其它消息
                default:
                    return '收到其它消息';
                    break;
            }
        });

        $response = $app->server->serve();
        $response->send();
        //return $response;

    }

    /*
     * 接入验签
     */
    private function checkSignature($token)
    {
        $signature = $_GET["signature"];
        $timestamp = $_GET["timestamp"];
        $nonce = $_GET["nonce"];
        $tmpArr = array($token, $timestamp, $nonce);
        sort($tmpArr);
        $tmpStr = implode($tmpArr);
        $tmpStr = sha1($tmpStr);
        if ($tmpStr == $signature) {
            return true;
        } else {
            return false;
        }
    }



}


 

<?php

namespace message;

use app\common\model\Users;
use EasyWeChat\Kernel\Messages\Link;
use EasyWeChat\Kernel\Messages\Message;
use EasyWeChat\Kernel\Messages\Text;
use EasyWeChat\Kernel\Messages\News;
use EasyWeChat\Kernel\Messages\NewsItem;
use think\Exception;
use think\facade\Cache;
use think\facade\Log;
use EasyWeChat\Factory;
use Token\Token;
/**微信消息
 * Class MessageHandler
 * @package message
 */
class MessageHandler
{
    /*
    * 消息对象
    */

    private $message;
    private $user_model;

    public function __construct($message)
    {
        $this->message = $message;
        $this->user_model = new Users();
    }

    /*
     * 事件响应函数
     */
    public function eventHandler()
    {
        // $message['FromUserName'] // 用户的 openid
        // $message['MsgType'] // 消息类型:event, text....
        global $_W;
        switch ($this->message['Event']) {
            //关注事件
            case 'subscribe':
                if (!empty($this->message['EventKey'])) {
                    $uid = substr($this->message['EventKey'],8);
                    $res = $this->loginEvent($uid);
                    return $res;
                }
                return '欢迎关注';
                break;
            //取消关注事件
            case 'unsubscribe':
                return $this->unSubscribe();
                break;
            //点击事件
            case 'CLICK':
                return '点击';
                break;
            //扫描事件
            case 'SCAN':
                $res = $this->loginEvent($this->message['EventKey']);
                return $res;
                //return '取关';
                break;
            default:
                return '收到其它消息';
                break;
        }
    }



    public function LoginEvent($uid)
    {
        global $_W;
        //注册
        $openid = $this->message['FromUserName'];
        $user_id = $this->addMember($openid);

        $jwtToken = new Token();
        $tokenData = array(
            'user_id' => $user_id,
        );
        $token = $jwtToken->createToken($tokenData, 86400)['token'];
        $time_out = Cache::store('redis')->get('login'.$uid);
        if (empty($time_out))
            return "二维码过期,请重新登陆";
        $data = array(
            'uid' => $uid,
            'token' => $token
        );
        $res = $this->sendSocket($data);
        if($res)
            return "登录成功";
        else
            return '登录异常';



    }



    private function addMember($openid)
    {
        global $_W;
        $user_info = $this->user_model->getInfoByOpenId($openid);
        if (empty($user_info))
        {
            $app = Factory::officialAccount($_W['config']);
            $user_detail = $app->user->get($openid);
            $data = array(
                'nickname' => $user_detail['nickname'] ?? '',
                'openid' => $openid,
                'gender' => $user_detail['sex'] ?? 0,
                'avatar' => $user_detail['headimgurl'] ?? '',
            );
            $user_id = $this->user_model->addUser($data);
        }else{
            $user_id = $user_info->id;
        }
        return $user_id;

    }



    /**
     * Notes:取消关注事件
     * Date: 2019/6/19
     * Time: 14:11
     * @return bool
     */
    private function unSubscribe()
    {
        global $_W;
        $member_model = new Members();
        if ($_W['user']['id'] > 0)
        $member_model->unSubscribe($_W['user']['id']);
        return true;

    }

    public function sendSocket($data)
    {
        try{
            // 建立socket连接到内部推送端口
            $client = stream_socket_client('tcp://127.0.0.1:5678', $errno, $errmsg, 1);
            // 推送的数据,包含uid字段,表示是给这个uid推送
            //$data = array('uid'=>'201907181703404867264713', 'percent'=>'88%');
            // 发送数据,注意5678端口是Text协议的端口,Text协议需要在数据末尾加上换行符
            fwrite($client, json_encode($data)."\n");

            // 读取推送结果
            $res = fread($client, 8192);
            fclose($client);
            $res = json_decode($res,true);
            if($res['error'] == 0){
                return true;
            }else{
                return false;
            }

        }catch (\Exception $e)
        {
            return $e->getMessage();
        }

    }





}

ps:

sendSocket() :注册成功后给内部服务推送消息,告知给$uid的客户端发送token,workerman收到推送后发送消息给浏览器

5.浏览器收到token,进行跳转

6.登陆成功

public function login(Request $request)
    {

        $token = $request->param('token', 0);
        if (!empty($token)) {
            $jwtToken = new Token();
            $checkToken = $jwtToken->checkToken($token);
            $data = (array)$checkToken['data']['data'];
            $uid = $data['uid'] ?? 0;
            $user_id = $data['user_id'] ?? 0;
            Session::set('user_id', $user_id);
            $this->error('登陆成功', 'index/index/index');
        }
    }

 

没有更多推荐了,返回首页