公众号管理

重要

本文中含有需要您注意的重要提示信息,忽略该信息可能对您的业务造成影响,请务必仔细阅读。

功能概述

支持授权公众号给Quick Tracking,可以将公众号行为事件(如:关注、菜单访问、取消关注等)上报到Quick Tracking中用于公众号的用户行为分析。

重要

微信公众号采集依赖于微信平台定制的规则,比较重要的内容总结如下,若您需要采集此类事件前,请详细阅读(点击可查看微信官方说明):

  1. 微信公众号事件是由微信定义、生成和发送的,开发者仅为订阅这些数据,无法进行自定义。具体可订阅的事件列表可点击2 事件及事件属性查看。

  2. 若需要采集微信公众号事件,需成为公众号开发者,成为后,公众号平台中的「自动回复、自定义菜单」将无法使用,已经配置的信息也将失效,需要您自主维护。操作流程可见微信官方说明

  3. 微信平台最终会将开发者订阅的事件发送至开发者维护的服务器中,开发者需要将该事件再转发至Quick Tracking中。

1 操作说明

下述表格为公众号事件采集的必要步骤,请按照顺序操作:

步骤序号

描述

操作明细

1

微信公众号平台开启开发者配置

image.png

微信公众号平台可点击跳转

警告

再次提示:若需要采集微信公众号事件,需成为公众号开发者,成为开发者后,公众号平台中的「自动回复、自定义菜单」将无法使用,已经配置的信息也将失效,需要您自主维护。操作流程可见微信官方说明

2

QuickTracking平台配置公众号采集

image.png

接入的操作明细可点击查看

3

将微信公众号的事件转发至QuickTracking中

该步骤的操作说明可点击查看

4

在QuickTracking中进行数据分析

具体可分析的事件列表可点击2 事件及事件属性查看

1.1 接入公众号

产品路径:「数据采集」-「数据源管理」-「公众号管理」

image.png

点击「接入公众号」按钮,弹出右侧「接入数据源」页面完成新建接入公众号,完成接入需要填写如下信息:

「基础信息」

  • 公众号名称:填写要接入的公众号名称

  • 公众号ID:填写公众号ID,公众号ID不可重复。获取方式见下图:image.png

「分析设置」

  • 归属组织:默认为当前组织不可修改,如需在不同组织下接入公众号,需在左侧组下拉框切换组织。

  • 归属应用:在下拉选择框选择当前归属组织下的应用。

「接收微信消息」

  • 加密模式:分为安全模式与明文模式;

安全模式:EncodingAESKey 用于消息体加解密密钥,与微信公众号平台填写一致。

明文模式:不使用消息体加解密功能,安全系数较低。

  • EncodingAESKey 获取地址:公众号后台-基础配置-复制EncodingAESKey。

  • Token获取地址:公众号后台-基础配置-复制Token

(注:微信公众号后台URL为用户填写地址,微信公众号将数据发至填写地址后,再同步至Quick Tracking。参考文档

image.png

「匿名用户唯一标识」

必选项,设置以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表达式,点击「测试」,测试是否解析成功,解析成功则可正常获取。

image.png

完成以上步骤点击保存完成创建。

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 服务端埋点参数获取

  1. 进入管理控制台-->点击采集信息

  2. 主域名:服务端埋点数据上报地址

  3. 副域名:默认和主域名一致,私部署客户可以提供副域名作为备用域名

  4. ServiceSecret和ServiceID为校验参数,注意保密不可泄漏。

  5. code为开发者ID(AppID),必填来自微信公号平台,且需要在采集管理-数据源管理-公众号管理中配置。

注意:因为上述信息涉及系统安全,所以只有管理员权限才可以看到!

image.pngimage.png

image.png

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

<xml><ToUserName><![CDATA[gh_0b03a4dd7455]]></ToUserName>
<FromUserName><![CDATA[o884561i2yjDrOhC6IqzEzXtPKUo]]></FromUserName>
<CreateTime>1676613107</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[测试]]></Content>
<MsgId>24003327368861869</MsgId>
</xml>

3.3 鉴权sign生成规则

  1. 先将除了sign字段的其他所有请求字段的key和value封装成一个map对象

  2. 将这个map对象序列化成JSON字符,生成时需要将key按照自然排序即ascii码排序,java可使用JSONObject.toJSONString(mapObject, SerializerFeature.MapSortField),

  3. 获取serviceSecret

  4. sign = MD5(排序完生成的JSON字符 + serviceSecret)

  5. 将计算得到的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参考

image.png

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("加密"));
    }
}