本文中含有需要您注意的重要提示信息,忽略该信息可能对您的业务造成影响,请务必仔细阅读。
功能概述
支持授权公众号给Quick Tracking,可以将公众号行为事件(如:关注、菜单访问、取消关注等)上报到Quick Tracking中用于公众号的用户行为分析。
1 操作说明
下述表格为公众号事件采集的必要步骤,请按照顺序操作:
步骤序号 | 描述 | 操作明细 |
1 | 在微信公众号平台开启开发者配置 | 微信公众号平台可点击跳转 警告 再次提示:若需要采集微信公众号事件,需成为公众号开发者,成为开发者后,公众号平台中的「自动回复、自定义菜单」将无法使用,已经配置的信息也将失效,需要您自主维护。操作流程可见微信官方说明 |
2 | 在QuickTracking平台配置公众号采集 | 接入的操作明细可点击查看 |
3 | 将微信公众号的事件转发至QuickTracking中 | 该步骤的操作说明可点击查看 |
4 | 在QuickTracking中进行数据分析 | 具体可分析的事件列表可点击2 事件及事件属性查看 |
1.1 接入公众号
产品路径:「数据采集」-「数据源管理」-「公众号管理」
点击「接入公众号」按钮,弹出右侧「接入数据源」页面完成新建接入公众号,完成接入需要填写如下信息:
「基础信息」
公众号名称:填写要接入的公众号名称
公众号ID:填写公众号ID,公众号ID不可重复。获取方式见下图:
「分析设置」
归属组织:默认为当前组织不可修改,如需在不同组织下接入公众号,需在左侧组下拉框切换组织。
归属应用:在下拉选择框选择当前归属组织下的应用。
「接收微信消息」
加密模式:分为安全模式与明文模式;
安全模式:EncodingAESKey 用于消息体加解密密钥,与微信公众号平台填写一致。
明文模式:不使用消息体加解密功能,安全系数较低。
EncodingAESKey 获取地址:公众号后台-基础配置-复制EncodingAESKey。
Token获取地址:公众号后台-基础配置-复制Token
(注:微信公众号后台URL为用户填写地址,微信公众号将数据发至填写地址后,再同步至Quick Tracking。参考文档)
「匿名用户唯一标识」
必选项,设置以Union或OpenID来作为匿名用户的唯一标识。
OpenID:用户唯一标识;
UnionID:用于区分用户的唯一值,设置为UnionID后,需获取access_token。
「获取access_token」
如果有其他业务同时使用了Token,用户需自搭建的中控服务,来统一获取、维护access_token,在分析平台中可从中控服务来获取access_token。(注:选择unionID为匿名用户唯一标识,用户需配置一个控制中心。Quick Tracking通过中控中心来获取token,在微信公众号获取对应的unionID(因每次调用获取token后token会进行刷新,其他位置使用的token也会刷新改变。且获取token次数微信开发平台有限制,用户可以从控制中心获取token,且可控制token的周期))
中控服务器地址:填写自建的中控服务地址可以获取到token中控服务地址;
access_token解析:从中控服务器获取的返回值中解析access_token,通过JSONPah表达式解析,填写 JSONPath表达式,点击「测试」,测试是否解析成功,解析成功则可正常获取。
完成以上步骤点击保存完成创建。
1.2 公众号管理
公众号操作区主要由以下几个部分组成:
左侧组织筛选区:切换不同组织下接入的公众号
数据展示区:展示已接入的公众号列表,数据包含公众号名称、公众号APPID、创建人、归属应用和操作。注意:分析设置中,一个公众号只能选择归属一个应用。
操作区:操作区包含「编辑」与「操作」,点击「编辑」后和新增的界面一致,点击删除则表示从应用中移除了公众号的关联。
1.3 数据分析
接入公众号后的应用,以及有“接入公众号后的应用”的分析视图和小站,系统属性中需要有公众号事件参与分析,其中参与分析的系统属性如下表格:
系统属性 | 属性值 |
平台 | 公众号AppID@wechatoa |
平台类型 | 公众号 |
事件类型 | 自定义事件 |
事件名称&事件编码 | 具体可点击2 事件及事件属性查看 |
设备ID | openid(默认),可以在公众号管理中改为Unionid |
设备是否首日启动 | 根据归属应用内的数据一同打标 |
透出公众号事件的分析模型有:事件分析、漏斗分析、留存分析、分布分析、间隔分析、路径分析、生命周期、归因分析、人群管理。
2 事件及事件属性
微信公众号事件是由微信定义、生成和发送的,开发者仅为订阅这些数据,无法进行自定义。具体可订阅的事件列表如下:
事件名称 | 事件编码 | 事件属性名称 (分析展示) | 属性Key | 属性类型 | 说明 | |
菜单点击(预置) | $$_menu_click | 消息类型 | MsgType | string | 消息类型:event | |
事件类型 | Event | string | 事件类型:CLICK | |||
事件key | EventKey | string | 事件 KEY 值,与自定义菜单接口中 KEY 值对应 | |||
菜单ID | MenuID | string | 指菜单ID,如果是个性化菜单,则可以通过这个字段,知道是哪个规则的菜单被点击了。 | |||
扫描信息 | ScanCodeInfo | string | 扫描信息 | |||
扫描类型 | ScanType | string | 扫描类型:一般是qrcode | |||
扫描结果 | ScanResult | string | 扫描结果,即二维码对应的字符串信息 | |||
发送的图片信息 | SendPicsInfo | string | 发送的图片信息 | |||
发送的图片数量 | Count | number | 发送的图片数量 | |||
发送的位置信息 | SendLocationInfo | string | 发送的位置信息 | |||
X坐标信息 | Location_X | string | X坐标信息 | |||
Y坐标信息 | Location_Y | string | Y坐标信息 | |||
精度 | Scale | number | 精度,可理解为精度或者比例尺、越精细的话 scale越高 | |||
地理位置 | Label | string | 地理位置的字符串信息 | |||
朋友圈POI名称 | Poiname | string | 朋友圈 POI 的名字,可能为空 | |||
关注公众号 (预置) | $$_official_account_follow | 是否是扫码关注 | is_scan | string | 是否是扫码关注(将原始事件的关注公众号和扫码关注公众号融合) | |
事件key | EventKey | string | 事件 KEY 值,qrscene_为前缀,后面为二维码的参数值 | |||
事件类型 | Event | string | 事件类型:subscribe(订阅) | |||
二维码ticket | Ticket | string | 二维码的ticket,可用来换取二维码图片 | |||
取消关注公众号 (预置) | $$_official_account_unfollow | 消息类型 | MsgType | string | 消息类型:event | |
事件类型 | Event | string | 事件类型:unsubscribe(取消订阅) | |||
扫码打开公众号 (预置) | $$_scan_open_official_account | 消息类型 | MsgType | string | 消息类型:event | |
事件类型 | Event | string | 事件类型:SCAN | |||
事件key | EventKey | string | 事件 KEY 值,是一个32位无符号整数,即创建二维码时的二维码scene_id | |||
二维码ticket | Ticket | string | 二维码的ticket,可用来换取二维码图片 | |||
上报地理位置 (预置) | $$_send_localtion | 事件类型 | Event | string | 事件类型:LOCATION | |
纬度(地理位置) | Latitude | number | 地理位置纬度 | |||
经度(地理位置) | Longitude | number | 地理位置经度 | |||
精度(地理位置) | Precision | number | 地理位置精度 | |||
订阅消息发送 (预置) | $$_send_msg_popup | 模板 id | TemplateId | string | 模板 id(一次订阅可能有多条通知,带有多个 id) | |
消息id | MsgID | string | 消息 id | |||
推送结果码 | ErrorCode | string | 推送结果状态码(0表示成功) | |||
推送结果 | ErrorStatus | string | 推送结果状态码文字含义 | |||
订阅弹窗点击 (预置) | $$_click_msg_popup | 模板 id | TemplateId | string | 模板 id(一次订阅可能有多条通知,带有多个 id) | |
操作行为 | SubscribeStatusString | string | 用户点击行为(同意“accept”、取消“reject”发送通知) | |||
操作场景 | PopupScene | string | 1 :弹窗来自 H5 页面 2 :弹窗来自图文消息 | |||
订阅消息管理 (预置) | $$_manager_msg_popup | 模板 id | TemplateId | string | 模板 id(一次订阅可能有多条通知,带有多个 id) | |
操作行为 | SubscribeStatusString | string | 用户点击行为(仅推送用户拒收通知) | |||
模板消息送达 (预置) | $$_template_send_finish | 消息类型 | MsgType | string | 消息类型:event | |
事件类型 | Event | string | 事件为模板消息发送结束 | |||
消息id | MsgID | string | 消息id | |||
发送状态 | Status | string | 发送状态为成功(success)/拒绝(user block)/失败(system failed) | |||
群发消息送达 (预置) | $$_mass_send_finish | 消息类型 | MsgType | string | 消息类型:event | |
事件类型 | Event | string | 事件类型:MASSSENDJOBFINISH | |||
消息id | MsgID | string | 群发的消息ID | |||
发送状态 | Status | string | 群发的结果,为“send success”或“send fail”或“err(num)”。但send success时,也有可能因用户拒收公众号的消息、系统错误等原因造成少量用户接收失败。err(num)是审核失败的具体原因,可能的情况如下:err(10001):涉嫌广告, err(20001):涉嫌政治, err(20004):涉嫌社会, err(20002):涉嫌色情, err(20006):涉嫌违法犯罪, err(20008):涉嫌欺诈, err(20013):涉嫌版权, err(22000):涉嫌互推(互相宣传), err(21000):涉嫌其他, err(30001):原创校验出现系统错误且用户选择了被判为转载就不群发, err(30002): 原创校验被判定为不能群发, err(30003): 原创校验被判定为转载文且用户选择了被判为转载就不群发, err(40001):管理员拒绝, err(40002):管理员30分钟内无响应、超时 | |||
粉丝总数 | TotalCount | number | tag_id下粉丝数;或者openid_list中的粉丝数 | |||
实际发送粉丝数 | FilterCount | number | 过滤(过滤是指特定地区、性别的过滤、用户设置拒收的过滤,用户接收已超4条的过滤)后,准备发送的粉丝数,原则上,FilterCount 约等于 SentCount + ErrorCount | |||
发送成功粉丝数 | SentCount | number | 发送成功的粉丝数 | |||
发送失败粉丝数 | ErrorCount | number | 发送失败的粉丝数 | |||
文章序号 | ArticleIdx | string | 群发文章的序号,从1开始 | |||
文章状态 | UserDeclareState | string | 用户声明文章的状态 | |||
系统校验状态 | AuditState | string | 系统校验的状态 | |||
相似原创文URL | OriginalArticleUrl | string | 相似原创文的URL | |||
相似原创文类型 | OriginalArticleType | string | 相似原创文的类型 | |||
是否能转载 | CanReprint | string | 是否能转载 | |||
是否需要替换为原创文内容 | NeedReplaceContent | string | 是否需要替换成原创文内容 | |||
是否需要注明转载来源 | NeedShowReprintSource | string | 是否需要注明转载来源 | |||
整体校验结果 | CheckState | string | 整体校验结果1-未被判为转载,可以群发,2-被判为转载,可以群发,3-被判为转载,不能群发 | |||
文章URL | ArticleUrl | string | 群发文章的URL | |||
公众号推文发布 (预置) | $$_publish_job_finish | 发布任务id | publish_id | string | 发布任务id | |
发布状态 | publish_status | string | 发布状态,0:成功、1:发布中、2:原创失败,、3: 常规失败、4:平台审核不通过、 5:成功后用户删除所有文章、6: 成功后系统封禁所有文章 | |||
文章id | article_id | string | 当发布状态为0时(即成功)时,返回图文的 article_id,可用于“客服消息”场景 | |||
文章数量 | count | number | 当发布状态为0时(即成功)时,返回文章数量 | |||
文章编号 | idx | string | 当发布状态为0时(即成功)时,返回文章对应的编号 | |||
文章URL | article_URL | string | 当发布状态为0时(即成功)时,返回图文的永久链接 | |||
发布失败文章编号 | fail_idx | string | 当发布状态为2或4时,返回不通过的文章编号,第一篇为 1;其他发布状态则为空 | |||
公众号消息接收 (预置) | $$_receive_msg | 消息类型 | MsgType | string | 消息类型:text | |
文本消息内容 | Content | string | 文本消息内容 | |||
消息id | MsgId | string | 消息id,64位整型 | |||
消息的数据ID | MsgDataId | string | 消息的数据ID(消息如果来自文章时才有) | |||
文章序号 | Idx | string | 多图文时第几篇文章,从1开始(消息如果来自文章时才有) | |||
图片链接 | PicUrl | string | 图片链接(由系统生成) | |||
媒体id | MediaId | string | 图片消息媒体id,可以调用获取临时素材接口拉取数据。 | |||
语音格式 | Format | string | 语音格式:amr | |||
语音识别结果 | Recognition | string | 语音识别结果,UTF8编码 | |||
缩略图媒体id | ThumbMediaId | string | 视频消息缩略图的媒体id,可以调用多媒体文件下载接口拉取数据。 | |||
X坐标信息 | Location_X | number | 地理位置纬度 | |||
Y坐标信息 | Location_Y | number | 地理位置经度 | |||
精度 | Scale | number | 地图缩放大小 | |||
地理位置 | Label | string | 地理位置信息 | |||
消息标题 | Title | string | 消息标题 | |||
消息描述 | Description | string | 消息描述 | |||
消息链接 | Url | string | 消息链接 |
3 事件采集
3.1 服务端埋点参数获取
进入管理控制台-->点击采集信息
主域名:服务端埋点数据上报地址
副域名:默认和主域名一致,私部署客户可以提供副域名作为备用域名
ServiceSecret和ServiceID为校验参数,注意保密不可泄漏。
code为开发者ID(AppID),必填来自微信公号平台,且需要在采集管理-数据源管理-公众号管理中配置。
注意:因为上述信息涉及系统安全,所以只有管理员权限才可以看到!
3.2 请求参数
接口地址:https://<收数域名+端口>/server/thirdPartyDataSource
请求参数content-type:application/json
请求JSON参数:
JSON字段名 | 数据类型 | 是否必传 | 字段描述 | 示例 |
sign | string | 是 | 校验签名 | f564cae6a8ad458648id9d607a124322 |
app_id | string | 是 | 对应服务端埋点中的serviceID | OA8kI9Jis7YJNh5uh |
appkey | string | 是 | 应用key,从QT管理后台获取 | 9moqdsuia8hvxm7k8shf82n |
ts | string | 是 | 上报的毫秒级时间戳 | 1659493170125 |
code | string | 是 | 开发者ID(AppID),来着微信官方 | wxd66151142ae9b9ae |
weChatOAData | string | 是 | 微信公众号传来的xml |
|
3.3 鉴权sign生成规则
先将除了sign字段的其他所有请求字段的key和value封装成一个map对象
将这个map对象序列化成JSON字符,生成时需要将key按照自然排序即ascii码排序,java可使用JSONObject.toJSONString(mapObject, SerializerFeature.MapSortField),
获取serviceSecret
sign = MD5(排序完生成的JSON字符 + serviceSecret)
将计算得到的sign放到原始的map对象中,再序列化成JSON字符即为最终完整的JSON请求报文
3.4 响应说明
响应数据content-type:application/json
响应的JSON字符中有两个字段,分别为code、message
code | message | 描述 |
Httpapi_300_200 | 上报成功 | 成功 |
Httpapi_300_101 | 非法的签名 | 签名校验失败,请参考sign生成规则说明 |
Httpapi_300_102 | 上报的数据类型非JSON格式 | 请求参数的数据格式不是JSON格式 |
Httpapi_300_103 | 缺少必要字段 | 参考请求参数表,检查是否遗漏了必填的参数 |
Httpapi_300_104 | 用户属性缺少必要字段 | 参考请求参数表,检查是否遗漏了必填的参数 |
Httpapi_300_105 | 非法事件id | 事件ID不正确 |
Httpapi_300_106 | ak/sk不正确 | |
Httpapi_300_107 | 微信公众号上报日志非标准xml格式 | |
Httpapi_300_108 | 上报的微信公众号消息不合法 | |
Httpapi_300_109 | 不可识别的微信公众加密类型 | |
Httpapi_300_110 | 获取unionid失败 |
3.5 java demo参考
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.google.gson.Gson;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
/**
* ThirdPartyDataSourceApiDemo 接入第三方数据源demo
*
* @author chengtao
*/
public class ThirdPartyDataSourceApiDemo {
private static final Logger logger = LoggerFactory.getLogger(ThirdPartyDataSourceApiDemo.class);
/**
* 主域名
*/
private static final String API_URL = "*****";
/**
* 对应服务端埋点信息中的:ServiceID
*/
private static final String SERVICE_ID = "*****";
/**
* 对应服务端埋点信息中的:ServiceSecret
*/
private static final String SERVICE_SECRET = "*****";
/**
* client日志
*/
private static final OkHttpClient client = new OkHttpClient();
/**
* 获取微信公众号日志信息并转发到QT收数(需要在微信公众号管理后台中配置该接口)
*
* @param request
* @throws IOException
*/
@RequestMapping("/token")
@ResponseBody
public void token(HttpServletRequest request) throws IOException {
//获取微信公众号官方请求中的全部参数信息,并封装成json
Map<String, String[]> paramMap = request.getParameterMap();
JSONObject json = paramMapToJson(paramMap);
//向报文中添加服务ID
json.put("app_id",SERVICE_ID);
//向报文中添加ts
json.put("ts", String.valueOf(System.currentTimeMillis()));
//填写公众号ID
String code = "******";
json.put("code", code);
//获取微信公众号xml数据
String weChatOAData = new String(InputStreamUtil.getBytes(request));
json.put("weChatOAData", weChatOAData);
//计算签名
String sign = MD5Util.md5(JSONObject.toJSONString(json, SerializerFeature.MapSortField) + SERVICE_SECRET);
json.put("sign", sign);
String desJsonString = JSON.toJSONString(json, SerializerFeature.DisableCircularReferenceDetect);
Request newRequest = new Request.Builder()
.URL(API_URL + "/server")
.post(RequestBody.create(MediaType.parse("application/json"),
desJsonString))
.build();
try {
Response response = client.newCall(newRequest).execute();
if (!response.isSuccessful()) {
System.out.printf("[DEMO] 发送日志失败 %s%n", response);
} else {
System.out.printf("[DEMO] 发送成功 %s%n",
response.body().string());
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static JSONObject paramMapToJson(Map<String, String[]> paramMap) {
Map<String, Object> bodyMap = new HashMap<>();
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
String key = entry.getKey();
String[] value = entry.getValue();
if (value.length == 1) {
bodyMap.put(key, value[0]);
} else {
bodyMap.put(key, value);
}
}
return JSON.parseObject(new Gson().toJson(bodyMap));
}
private static String getBodyString(HttpServletRequest request) {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = request.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line = "";
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
logger.error("read body error", e);
throw new RuntimeException("read body error");
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
logger.error("close inputStream error", e);
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
logger.error("close reader error", e);
}
}
}
return sb.toString();
}
}
public class MD5Util {
public static String md5(String s) {
char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
try {
byte[] btInput = s.getBytes();
// 获得MD5摘要算法的 MessageDigest 对象
MessageDigest mdInst = MessageDigest.getInstance("MD5");
// 使用指定的字节更新摘要
mdInst.update(btInput);
// 获得密文
byte[] md = mdInst.digest();
// 把密文转换成十六进制的字符串形式
int j = md.length;
char str[] = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str);
} catch (Exception e) {
return null;
}
}
/**
* 单元测试
*/
public static void main(String[] args) {
StringBuilder dtskey = new StringBuilder("abc12390");
dtskey.append('\0').append("~!@#$%^&*()_+");
System.out.println(MD5Util.md5(dtskey.toString()));
System.out.println(MD5Util.md5("20121221"));
System.out.println(MD5Util.md5("加密"));
}
}