APK 推送历史与检索索引
apk_push_history 在 MySQL 保存独立于任务的设备应用历史;同名 ES 索引用于列表、分页、组合条件和聚合检索。任务不存在不等于历史应删除,MySQL 与 ES 也不是同一个物理对象。
源码实际支持同设备同应用的多条历史,以版本号和推送时间区分;实体头部“只保留最新版本”的旧注释已与当前实现不一致。本页采用 Service 与 Mapper 的实际判断规则。
业务流程与关系
- 推送消息先按
(mac,cpuid,packageName)找占位或完全匹配记录。占位记录的version_code、push_task_time均为 NULL,随后补齐;相同版本和时间的完全匹配记录跳过,否则插入新历史。 - 卸载消息没有卸载时间时跳过;有历史时选
push_task_time DESC, id DESC最新一条更新uninstall_time;无历史时先插入占位,允许卸载消息先到。当前 blacklist 链路将消费消息时的服务端时间放入事件,因此这里保存的不是设备反馈device_time。证据:blacklist 的BlackCallbackConsumer.java:57–64使用LocalDateTime.now()构造事件,report 的ApkUninstallUpdateConsumer.java:78使用事件时间。 cpuid=null在服务写入前归一化为空字符串。当前匹配 Mapper 又将 blank 值按空串查询,不把老注释中的“允许 NULL”当作当前物理约束。- 内部匹配优先 ES,ES 未找到或异常时再向 MySQL 确认;列表 ES 计数超过 10000 时使用 MySQL,分页异常时降级 MySQL。
- MySQL 插入/更新后尝试同步 ES,ES 失败被捕获,不能宣称两个存储在一个原子事务内提交。删除服务当前只调用 MySQL Mapper,没有同路径 ES 删除,因此不能把查询索引当作绝对 同步的事实源。
- Kafka 消费者和按 ID 区间的
apkPushHistoryMysqlToEs任务是数据流入口/补同步入口,本轮只读分析未执行消费或同步。
证据:ik_project/yudao-cloud/yudao-module-report/yudao-module-report-biz/src/main/java/cn/iocoder/yudao/module/report/service/apkpushhistory/ApkPushHistoryServiceImpl.java:40、:74、:110、:125、:158、:179、:188;同模块 dal/mysql/apkpushhistory/ApkPushHistoryMapper.java:101、:129,job/ApkPushHistoryEsJob.java:53,kafka/consumer/ApkPushHistoryConsumer.java:38、kafka/consumer/ApkUninstallUpdateConsumer.java:46。
MySQL 完整字段与物理约束
来源:ik_project/yudao-cloud/yudao-module-report/yudao-module-report-biz/src/main/java/cn/iocoder/yudao/module/report/dal/dataobject/apkpushhistory/ApkPushHistoryDO.java:26。本 DO 只实现 Serializable,没有租户、逻辑删除、审计继承字段。
实测:2026-09-18 20:51,MySQL 8.0.35,ik_yudao_report,InnoDB,utf8mb4_general_ci。
| 列 | Java 类型 / 声明行 | 实际 SQL 类型 | 可空 / 默认 | 业务意义 |
|---|---|---|---|---|
id | Long / 43 | bigint | 否 / 无默认,auto_increment | ORM 标识,也用于 ES 文档 ID |
mac | String / 48 | varchar(20) | 否 / 无默认 | 设备 MAC |
cpuid | String / 58 | varchar(32) | 否 / 空字符串 | 设备 CPU 序列,空串表示未提供 |
package_name | String / 63 | varchar(255) | 否 / 无默认 | 应用包名 |
version_code | Long / 70 | bigint | 是 / NULL | 版本号;占位时为空 |
push_task_time | LocalDateTime / 77 | datetime | 是 / NULL | 推送事实时间;参与幂等识别 |
uninstall_time | LocalDateTime / 86 | datetime | 是 / NULL | 卸载事件时间;当前 blacklist 链路为服务端消费时间 |
version_code_key | 无实体字段 | bigint | 是 / 虚拟生成 | IFNULL(version_code,0),空版本参与唯一约束 |
push_task_time_key | 无实体字段 | datetime | 是 / 虚拟生成 | IFNULL(push_task_time,'1970-01-01 00:00:01'),空时间参与唯一约束 |
实测只有两个唯一索引:
| 索引名 | 有序字段 | 解释 |
|---|---|---|
id | id,mac,cpuid | UNIQUE;不是 PRIMARY,也不是 id 单列唯一约束 |
uk_mac_cpuid_package_version | mac,cpuid,package_name,version_code_key,push_task_time_key | UNIQUE;名称未写出时间,但实际包含生成时间列 |
元数据未返回 PRIMARY 或外键。@TableId(type=AUTO) 是 ORM 声明,不能覆盖上述数据库事实。唯一键以 0 和特定时间替代 NULL,因此真实值恰好等于这两个占位值时会共用唯一键值;本轮未读取业务数据评估是否发生碰撞。补采确认该表为 KEY(mac,cpuid)、16 个分区。设备维度追查应同时保留 MAC 与 CPUID,组合唯一索引还涉及包名和生成列;是否裁剪分区及选用索引需要执行计划,本轮未执行。
mac、cpuid 是跨业务标识,package_name 是应用标识;本表没有 task_id 或实体声明的外键。数据库中额外 apk_push_history11111、apk_push_history_new 也已纳入 完整物理字典,不能按名字认定为正在使用的历史源。
ES 完整映射
实体:ik_project/yudao-cloud/yudao-module-report/yudao-module-report-biz/src/main/java/cn/iocoder/yudao/module/report/dal/es/apkpushhistory/ApkPushHistoryEsEntity.java:31。实测 ES 7.17.6 的 7 个字段完整匹配实体;列名为下划线形式。全局命名配置证据:doc/数据库设计管理/nacos/application-common.yaml:337。
| ES 字段 | Java 类型 / 声明行 | 实测 mapping | 说明 |
|---|---|---|---|
id | Long / 42 | long | 自定义文档 ID 来源为 MySQL id;字段本身也存 long |
mac | String / 48 | text,analyzer=mac_analyzer | 子字段 keyword 为 keyword,ignore_above=255 |
cpuid | String / 54 | keyword | CPU 序列精确值 |
package_name | String / 62 | text,analyzer=standard | 子字段 keyword 为 keyword,ignore_above=255 |
version_code | Long / 68 | long | 版本号 |
push_task_time | LocalDateTime / 74 | date,format=yyyy-MM-dd HH:mm:ss | 推送时间 |
uninstall_time | LocalDateTime / 80 | date,format=yyyy-MM-dd HH:mm:ss | 同 MySQL 卸载事件时间口径 |
ES 不含 MySQL 的两个虚拟生成列。采集 mapping 没有显式 search_analyzer;源码声明查询分析器与索引分析器相同,应区分“注解意图”和“mapping 显式返回项”。ES mapping 不表达 MySQL 的 UNIQUE/NOT NULL,不能把关系库约束照搬到索引。
更多映射见 ES apk_push_history 字典。