跳到主要内容

Task:APP 活跃、运行记录与查询优化

应用活跃、运行统计与归档​

“应用活跃”根据应用心跳/上报整理活跃设备和时间;“应用运行”处理设备明确上报的包运行时长与打开次数。两条链的设备键、日期维度和统计类型编码不同,做报表时不能只因都含包名和MAC就直接合并。

应用活跃:原始日志到日/月/年​

AppActivityConsumer接收Kafka活跃消息,AppActivityLogService生成雪花ID、补服务端create_time,写MySQL ik_thirdparty。原始表按包名+接收日期划分:app_activity_log_{包名中的点替换为下划线}_{yyyyMMdd}。设备时间time_stamp与服务端建表日期分开保留,避免把迟到报文理解为另一天建表依据。

表内partition_index由MAC与CPU拼接后的哈希取1024模计算,分区名为P{序号},建表代码使用(id,partition_index)复合主键。这是同一日表的分区,不是1024张业务表。tableName为@TableField(exist=false)路由属性,不能列为物理列。

AppActivityJob按分区读取原始日志形成设备活跃BO,insertByBo写入TDengine日、月、年三个库的app_activity_detail超级表。月记录的时间归一到月首、年记录归一到年首;不是简单把日表整行累计后就得到可证明的月/年时长。统计数值准确性及重复写语义本次未通过业务记录核验。

存储对象核心字段与设计目的本次结构结果
MySQL动态原始表mac/cpu标识设备,package_name/version_code标识应用,time_interval_config为上报间隔分钟,另保留设备时间、时区、IP、接收时间实测25张,覆盖5个包名、20260916—20260920;含预建日期不能推断已有业务数据
TD app_activity_day.app_activity_detailts时间戳+data_key复合键;record_time/package_name为TAG实测161个子表
TD app_activity_month.app_activity_detail同一模型,时间归一到月份;标签日期也是归一后的yyyyMMdd实测38个子表
TD app_activity_year.app_activity_detail同一模型,时间归一到年份实测12个子表
MySQL app_activity_count旧统计实体与CRUD仍在,后台统计页同时具有TD查询路径beginDate/endDate与实表不符,详见差异页

TD活跃子表为aad_{规范化包名}_{record_time},设备复合标识data_key=mac+cpu;普通列还含version_code/duration/ip/country。tbname是TD子表路由伪列,不是普通业务字段。代码统计接口type为0日、1月、2年。

应用运行:热数据、排行榜和归档​

Task统一设备事件入口将运行列表发Kafka,AppRuntimeConsumer过滤不合时长范围、超出配置写入期限或未启用运行采集的包,写入TD热库nebula_ids.flow_app_runtime。包名登记/开关查询通过device模块工具完成。运行上报还可能回填flow_task_device首次运行时间,是对投放效果的验证链。

运行子表名为flow_app_runtime_{规范化包名}_{day},day来自归一到日的record_time。data_key是去除冒号的MAC、下划线、CPU ID拼接;这与活跃链的直接拼接不同。record_time与data_key构成实测复合键;package_name/day是TAG,create_time保留接收时间,duration/open_time分别表达运行时长与打开次数。TAG不应按普通MySQL外键解释。

对象用途结构与关联
TD nebula_ids.flow_app_runtime近期运行明细、当前消费者写入实测1567子表;实体及Mapper标明tdAppRuntime
TD app_runtime_archive.flow_app_runtime归档运行明细、历史查询实测261子表;同一列与标签模型,部分压缩算法不同
MySQL nebula_ids.stats_app_runtime_top按包/版本/日期预计算运行排行榜,避免每次扫明细唯一键(date,package_name,type,version_code);type为0月、1年、2日,与活跃编码不同;duration分钟、open_time次数、device_count设备数、record_count记录数
MySQL ik_yudao_task.task_app_runtime_archive_record记录按天归档任务的执行结果day归档日期,state=0/1/2执行中/成功/失败,count迁移量,remark说明;仅主键,未有day唯一约束

AppRuntimeArchiveJob根据归档任务参数调用备份/恢复脚本并写归档记录;源码中存在SSH执行和脚本传输能力。本次仅阅读该代码,没有运行该任务、SSH脚本或归档操作。归档表结构已确认,不代表每一天归档成功,也不能凭count字段设计证明热库与归档库数据一致。

字段附录:应用日志MySQL、日活跃、月活跃、年活跃、运行热库、运行归档库。

源码证据:dal/dataobject/appactivity/AppActivityLogDO.java:22与:107、job/AppActivityJob.java:100与:200、service/appactivity/AppActivityCountServiceImpl.java:337、dal/dataobject/appactivity/AppActivityDetailDO.java:91、kafka/consumer/AppRuntimeConsumer.java:51、dal/dataobject/appruntime/AppRuntimeDO.java:119、dal/dataobject/appruntime/AppRuntimeTopDO.java:21、job/AppRuntimeJob.java:77、job/AppRuntimeArchiveJob.java:48、dal/dataobject/appruntime/AppRuntimeArchiveRecord.java:20。路径基准见总览。

Task 应用数据查询与优化指南​

当前 Task 的 APP 运行明细查询走 TDengine;Device 的设备安装列表页面走 ES。 两者都显示设备和应用信息,但“装了什么”与“某天运行多久、打开几次”是不同业务。不能把安装列表的 ES 月索引规则用于运行明细,也不能因为 MySQL 遗留库存在同名 flow_app_runtime 就直接查那张表作为当前运行记录来源。

本页依据 2026-09-18 工作区源码和测试环境结构快照。补采分区信息核验到当日 21:33(北京时间);未读取业务记录、执行查询计划或性能测试。下文“现状”来自代码或实测元数据,“建议”是后续改进方向,尚未修改业务实现或索引。

先按业务问题选存储​

要回答的问题当前入口或调用链实际存储与选取范围时间与关键约束
某应用、某设备每天运行多久/task/app-runtime-top/app-runtime/page → getAppRuntimePageTD 热库 nebula_ids.flow_app_runtime 或归档库 app_runtime_archive.flow_app_runtimerecord_time 时间范围+day IN;尽量给精确包名,再给 MAC/CPU
一个时间段内每台设备累计运行量同前缀 /app-runtime/unique/page → uniquePage同上,PARTITION BY mac,cpu_id 聚合后分页时间范围+day IN;该 SQL 不应用 versionCode 和模糊匹配开关,不能套用明细页语义
应用运行排行榜同前缀 /page → getAppRuntimeTopPageMySQL nebula_ids.stats_app_runtime_top,固定表date+type;是否按版本展示由 groupByVersionCode 决定
应用日/月/年活跃统计及设备清单AppActivityCountServiceImpl.getAppActivityCountPage/getAppActivityDetailPageTD app_activity_day/month/year.app_activity_detailtype=0/1/2 选择日/月/年库;包名必填,按周期生成 record_time TAG 集合
追查某条活跃上报、重新汇总原始心跳AppActivityLogMapper;后台 AppActivityJob 分区汇总MySQL ik_thirdparty.app_activity_log_{包名规范化}_{yyyyMMdd}按应用+服务端接收日选表,再按设备哈希选 P0…P1023
看设备上报的已安装应用列表Device 的 AppInstallDeviceController → AppInstallDeviceEsServiceServiceImplES app_install_device{yyyyMM}month 直接指定一个月索引;不是 Task 运行明细索引,详见 Device 文档

运行统计的 type 是 0月、1年、2日,活跃统计则是 0日、1月、2年。同名参数不能跨接口直接复用。活跃 MySQL 原始日志为日表;月维度活跃查询选择 TD 月库,不是查询 MySQL 月日志表。

APP 运行明细:先分热库和归档库,再限定应用和日期​

Mermaid Diagram Code:

flowchart LR
    Q[运行明细或设备累计请求] --> V[校验设备或包名及时间范围]
    V --> R{范围涉及哪些月份}
    R -->|当前月和上月| H[TD nebula_ids]
    R -->|更早月份| A[TD app_runtime_archive]
    R -->|跨冷热边界| E[拒绝请求,调用方拆分]
    H --> F[flow_app_runtime]
    A --> F
    F --> W[时间范围 + day TAG + 包名或设备条件]

AppRuntimeTopServiceImpl.java:447/479/519 按服务端当前日期计算路由。以核验日 2026-09-18 为例,8、9 月查热库,7 月及以前查归档库;7 月末至8月初必须拆成两次请求。该路由按日历决定,并不先查看归档任务是否完成,所以“已转归档库查询”不等于“该日期的业务记录已完整迁移”。缺数排查还要对照 运行归档业务,本次未验证实际记录完整性。

现有校验要求 MAC、CPU、包名至少一个,并限制日期差不超过两个月;这不是“任意两个整月都能混查”。只给一个设备而不给包名,仍可能覆盖该设备在很多应用子表中的记录。已知包名时精确限定包名和日期,通常比全应用范围内按设备筛选更明确;实际收益需查询计划验证。

时间列、TAG 与子表不是一回事​

结构当前含义与查询用途
record_time TIMESTAMP设备记录日期;AppRuntimeDO.setRecordTime 将其归一为当天零点。查询使用该列,不是入库时间 create_time
day INT TAG同一记录日期的 yyyyMMdd,用于限定对应日期的子表集合
package_name TAG子表的应用归属;精确包名限定应用,模糊条件会扩大候选范围
子表名flow_app_runtime_{包名中的点替换为下划线}_{yyyyMMdd},不是每设备一张子表
data_key去掉冒号的 MAC+下划线+CPU;与时间列形成实测复合键,设备列也单独保留
普通过滤列mac/cpu_id/version_code/duration;不能把普通列过滤写成已核验的 TAG 索引命中

页面实际查询超级表,由 record_time BETWEEN、day IN 和包名/设备条件缩小范围,不是页面先拼接子表名逐张查询。明细页还固定过滤 1 <= duration <= 144000,请求中 duration 字段并没有替换该范围。按小时输入范围可能漏掉当天零点的日记录;查询日业务应明确提供完整日期边界。

不同 SQL 的条件并不统一:

方法已有范围条件当前边界
getAppRuntimePagerecord_time BETWEEN+day IN;包名/MAC/CPU按开关精确或模糊匹配;可筛版本默认精确匹配。create_time 不参与时间范围
selectUniqueDeviceByConditionrecord_time >=/<=+day IN,精确包名/MAC/CPU,按 MAC+CPU 聚合不筛版本、不使用模糊开关;同设备多版本会汇总在一起
countByPackageNameAndRecordTimeBetweenrecord_time BETWEEN+包名集合+时长范围XML 中 day IN 块已注释,不能声称该汇总已有日期 TAG 条件
deviceCountByPInfoAndGroupByPInfo时间范围+包名集合+时长范围按包名/版本/MAC去重,再计数;不是按 MAC+CPU 去重,也无 day IN
countDeviceByPackageNameAndRecordTimeBetween时间范围、包名、版本、时长及可选 day INdistinct mac 与设备累计接口口径不同
归档 selectTbname/selectByTbname先用 day BETWEEN 找子表,随后按子表读取selectByTbname 整张子表读取;这是迁移链,不是分页接口

以上 XML 位于 mapper/appruntime/IFlowAppRuntimeMapper.xml:5/27/43/63;归档 SQL 位于 dal/tdengine/appruntime/AppRuntimeMapper.java:107/118。建议先统一统计的设备身份和版本语义,再评估是否给缺失 TAG 条件的汇总增加与时间范围一致的日期条件,避免只改查询速度却改变统计结果。

分页和导出目前有哪些限制​

普通明细走 BaseMapperX.selectPage,设备累计走 PageHelper 聚合分页;这不代表聚合前只扫描一页原始数据。源码没有在这两个方法中实现游标翻页。应先缩小应用和日期,再考虑深页优化;如果改成游标,需连同排序稳定性和复合键处理,不应仅把页码改大。

/app-runtime/export-excel 将 pageSize 设为 PAGE_SIZE_NONE,随后获取列表;异步导出逐日查询,但每一天仍用 selectList 将符合条件的记录整体载入内存。只有 /app-runtime/export-sync 显式增加两天范围检查,不能说所有导出都有两天上限。异步任务也不是按记录数量分批的流式读取。

明细与累计页还逐条调用 AppPackageNameApi 补应用名称;这是可见的逐条调用行为,未证明每次都会触发独立远程请求或造成多大耗时。后续可评估按包名去重后批量补充,并与数据库耗时分开观测。

有一项查询正确性问题应先于性能优化处理:AppRuntimePageReqVO.java:75 的 getEndDate() 在无 recordTime 数组时返回 startDate + " 23:59:59";getRecordTime() 又会按 startDate/endDate 构造并缓存数组。不同 getter 的先后调用可能使校验和查询使用的结束边界不同。当前调用应提供完整两元素 recordTime 数组,后续统一日期解析、数组长度及开始不晚于结束的校验。本轮仅记录,没有修复代码。

活跃数据:MySQL 原始日表与 TD 周期统计分开查询​

原始日志表名取服务端 create_time 的日期,不取设备 time_stamp。追查迟到报文时只按设备发生日选表可能漏报文;先明确接收日期范围,再定位对应日表。表名中包名的点转换为下划线;动态表名是 SQL 标识符,必须从内部规则和已知对象清单得到,不能把用户输入直接拼到 ${tableName}。

实测25张原始日表均为 LIST(partition_index)、1024个分区,总25600个分区;分区值0—1023。源码分区算法为 Java 字符串 (mac + cpu).hashCode() 取1024模后绝对值(空值按源码规则处理)。这不是数据库内置 HASH 分区,也不能换成另一语言的默认哈希函数后期望落到同一分区。

真实索引或分区已有查询如何使用局限与后续建议
所有原始日表仅 PRIMARY(id,partition_index),顺序为 ID 在前精确已知 ID 可按主键定位;AppActivityLogMapper.xml:18/28 显式指定 PARTITION(p${partitionIndex})设备查询条件是 MAC+CPU,不能把这组主键描述成设备检索索引
显式选择一个 P{n} 分区设备列表查询在分区内按多组 (mac,cpu) 过滤,排序 mac,cpu,create_time DESC选择分区只减少参与的数据范围;实测无 mac/cpu/create_time 二级索引,仍要处理分区内过滤和排序
后台汇总逐分区遍历同分区按 mac,cpu,version_code 分组,对 time_interval_config 求和一次只处理一个分区,但完成整天会遍历1024分区;不能称整天汇总只扫一个分区

针对经常按单设备追查的业务,可在获得查询计划、数据规模及写入成本后评估 (mac,cpu,create_time) 等候选二级索引;这只是候选,不是当前已有索引,也未执行建索引。按时间选日表、按设备选分区与分区内部索引优化是三个不同步骤,不能相互替代。

面向运营的活跃统计页使用 TD,而不是逐个扫描这些 MySQL 日表。AppActivityCountServiceImpl.java:111/234 按 type 选择日/月/年 Mapper;月和年的写入时间分别归一到月首和年首。record_time 是整数 TAG,ts 才是时间戳,二者不要与运行超级表中的 record_time TIMESTAMP 混淆。

TD 活跃查询当前 WHERE 与范围限制使用提示
汇总 listCount精确包名、ts BETWEEN、record_time IN、可选国家;按 ts,package_name 分组count(*) 命名为设备数;本次没有用业务记录证明与任意去重设备口径相同
设备明细精确包名、可选 MAC/CPU/版本/国家,record_time IN当前 Service 构造器没有追加 ts BETWEEN;不要因 Mapper 存在另一个带 between(ts) 的重载就写成页面已使用
周期范围校验包名与时间必填,日不超过30天,月不超过730天,年不超过3650天后两项代码按固定天数计算,不是按闰年精确计算2或10个日历年

月/年查询边界应与对应周期起点一致:记录时间存月首/年首,汇总 SQL 又过滤 ts BETWEEN,若从月中/年中开始,可能排除首个周期。日/月/年的子表均按 aad_{规范化包名}_{record_time} 命名;范围生成月 TAG 时用 yyyyMM01,年 TAG 用 yyyy0101。建议先明确要按“完整周期”还是任意时间段展示,再统一页面边界与后端校验。

运行排行榜:固定 MySQL 表的索引顺序决定了什么​

实测 stats_app_runtime_top 的 BTREE 索引如下,补采未发现该表的分区定义:

索引名类型与列顺序与页面查询的关系
PRIMARY唯一 (id)单条ID查询;不直接对应按日期排行榜
date非唯一 (date)页面固定日期筛选的现有候选索引
key唯一 (date,package_name,type,version_code)唯一粒度为日期+包名+周期类型+版本;不能重述为 (date,type,package_name,version_code)

页面查询要求 date,但包名、应用名当前使用包含匹配 LIKE '%值%';type 位于联合索引第二列包名之后。仅给日期和类型时,不能保证用该联合索引完整限定日期+类型范围。按包名包含匹配也不能视为包名等值定位。是否选择 date 索引、是否排序或临时聚合,必须以实际查询计划为准。

默认不分版本时,XML 按 date,type,package_name 分组并对 device_count 求和。一个设备跨版本出现时,版本设备数相加不等于跨版本去重设备数。查询排序字段需要显式传入;该 SQL 的默认排序分支已注释,不能保证未传排序时稳定按设备数降序。

建议将“已知包名精确查询”和“按名称查找应用”区分为不同使用场景,先定位应用再查榜;若确有大量按日期+类型读取的需求,再评估相应索引顺序。保留现有唯一约束的业务含义,不能为了查询顺序直接替换唯一键。

证据与待核验边界​

Java 路径以下述目录为基准:ik_project/yudao-cloud/yudao-module-task/yudao-module-task-biz/src/main/java/cn/iocoder/yudao/module/task/;XML 路径相对同模块 src/main/resources/。

证据支撑内容
controller/admin/appruntime/AppRuntimeTopController.java:89/101/123/156运行明细、全量Excel、同步导出、设备累计入口
service/appruntime/AppRuntimeTopServiceImpl.java:187/232/335/447/479/519明细筛选、聚合、逐日导出、范围校验、冷热路由
dal/tdengine/appruntime/AppRuntimeHotMapper.java:16、AppRuntimeArchiveMapper.java:16两个 TD 数据源选择;并非 ES Mapper
dal/dataobject/appruntime/AppRuntimeDO.java:112/119/130/139日期归零、子表名、day TAG、设备复合键生成
mapper/appruntime/IFlowAppRuntimeMapper.xml:5/27/43/63不同汇总的实际过滤和分组口径
mapper/appruntime/AppRuntimeTopMapper.xml:24MySQL 榜单筛选、跨版本聚合及排序
dal/dataobject/appactivity/AppActivityLogDO.java:29/107/118;mapper/appactivity/AppActivityLogMapper.xml:18/28日表、设备哈希分区及分区内查询
job/AppActivityJob.java:100/204建分区表与逐分区统计
service/appactivity/AppActivityCountServiceImpl.java:111/154/189/234/294/336;mapper/AppActivityDetailMapper.xml:7活跃周期选择、范围、实际明细过滤、归一化写入与聚合
Device模块 controller/admin/appinstalldevice/AppInstallDeviceController.java:65、service/appinstalldevice/AppInstallDeviceEsServiceServiceImpl.java:90已确认的安装列表 ES 页面链;其 Java 根包为 cn/iocoder/yudao/module/device

元数据证据为统一采集目录中的 mysql-nebula_ids.json、mysql-ik_thirdparty.json、各 TD 库快照及 query-layout/mysql-layout-ik_thirdparty.json、mysql-layout-nebula_ids.json。完整字段与索引见 遗留 MySQL 字典、活跃原始日志字典。

本轮沿 Controller→Service→Mapper/XML 确认运行查询走 TD,并在 Task 生产 Java 范围检索 ES 客户端/注解等符号,未找到运行记录 ES 查询实现。这一结论覆盖当前工作区 Task 实现,不代表线上其他版本或外围消费程序绝不存在运行记录 ES 副本。未测量数据量、耗时、执行计划、深分页上限或实际归档完成度;本页不承诺任何建议的性能收益。

用户文档
AI 助手
Agent 列表
请选择一个 Agent 开始对话
AI 问答