未读消息
概述
未读消息模块基于设备本地 SQLite 数据库实现,用于记录业务消息并同步至云控后台,在投屏界面实时展示未读数量及关键状态。
适用于:
- 任务执行状态跟踪
- 异常 / 错误上报
- 收益或数据统计展示
- 多设备集中监控

架构说明
本地存储(SQLite)
消息数据存储在设备本地数据库:
- 每台设备独立存储
- 支持自定义表结构(需包含
id、isRead)
脚本写入消息
通过脚本向 SQLite 写入或更新消息数据:
- 添加未读消息(
isRead = 0)
通知云端刷新
调用接口通知云控后台更新数据
云控后台聚合
后台接收通知并处理数据:
- 聚合设备消息
- 统计未读数量
投屏界面展示
投屏界面根据数据字段实时渲染未读消息内容:
- 未读消息数量
- 点击可打开消息窗口
- 消息详情列表
- 用于展示消息数据
- 支持配置 显示数量
消息操作
在投屏界面点击未读消息图标,可打开消息窗口并进行管理:
- 查看消息详情
- 删除或批量删除消息
- 标记为已读(
isRead = 1,支持批量操作) - 触发关联脚本执行
脚本执行
在消息列表中点击「执行」后,将触发设备端执行对应脚本:
- 自动获取当前消息数据(作为执行上下文)
- 按预设逻辑处理业务(如跳转、操作、请求等)
- 可在执行过程中更新任务状态(
taskStatus)
数据库结构
meta_actions 表为系统必选核心表,结构固定且不可修改,必须存在于数据库中,用于系统任务调度与执行控制。
业务表支持按需创建,可创建多个,表名完全自定义,用于承载具体业务数据。
字段结构需遵循以下规则:
- 必选字段与推荐字段必须保留
- 其余字段均为业务扩展字段,支持自定义(包含中文字段名)
- 字段命名需保持语义清晰,不得与系统保留字段冲突
未读消息.db(数据库文件)
├── meta_actions(必选表 / 系统任务调度表)
│ ├── id(主键)
│ ├── activity(启动 Activity 页面)
│ ├── broadcast(执行时发送广播)
│ ├── scriptContent(脚本内容代码)
│ ├── scriptName(脚本名称)
│ ├── scriptPath(脚本文件路径)
│ ├── scriptConfig(脚本执行配置参数)
│ ├── targetTable(关联业务表名)
│ └── targetId(关联业务表数据 ID)
│
├── 设备消息(业务表)
│ ├── id
│ ├── 标题
│ ├── 内容
│ ├── 今日收益
│ ├── 错误信息
│ ├── 创建时间
│ ├── isRead
│ ├── taskStatus
│ └── taskUpdateTime
│
├── 盈利汇总(业务表)
│ ├── id
│ ├── 日期
│ ├── 总收益
│ ├── 备注
│ └── isRead
│
├── 任务汇总(业务表)
│ ├── id
│ ├── 任务名称
│ ├── 执行状态
│ ├── 更新时间
│ └── isRead
├── ...(其他业务表)
meta_actions(系统任务调度核心表)
meta_actions 用于定义任务执行行为,是系统的控制中心表,不可删除,不可修改结构。
该表负责:
- 启动 Activity 页面
- 发送广播通知
- 执行脚本代码
- 关联业务数据表(targetTable + targetId)
字段说明
| 字段 | 类型 | 描述 |
|---|---|---|
| id | INTEGER | 主键 |
| activity | TEXT | 用于定义需要启动的 Android Activity 页面信息,通常包含 packageName 与 className,用于在执行任务时跳转到指定页面 |
| broadcast | TEXT | 用于定义广播触发配置,在任务执行过程中发送系统或自定义广播,用于通知其他模块或应用进行联动处理 |
| scriptContent | TEXT | 存储可直接执行的脚本代码内容,脚本执行时优先使用该字段内容作为运行逻辑主体,详情参阅脚本引擎 |
| scriptName | TEXT | 脚本名称标识,运行脚本代码内容时得名称,详情参阅脚本引擎 |
| scriptPath | TEXT | 脚本文件存储路径,当脚本内容不直接内嵌时,通过该路径加载外部脚本文件执行,详情参阅脚本引擎 |
| scriptConfig | TEXT | 脚本运行时配置参数,通常为 JSON 结构,用于控制执行频率、循环次数、延迟等运行行为,详情参阅脚本引擎 |
| targetTable | TEXT | 关联的业务数据表名称,用于指定当前任务操作的数据来源表,实现任务与业务数据解耦 |
| targetId | INTEGER | 关联业务表中的具体数据 ID,用于定位当前任务对应的具体业务记录 |
业务表
支持完全自定义表结构(字段名支持中文),但需满足以下约束:
必选字段
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INTEGER | 主键(自增) |
| isRead | INTEGER | 是否已读(0=未读,1=已读) |
推荐字段
| 字段名 | 类型 | 说明 |
|---|---|---|
| taskStatus | INTEGER | 任务状态 |
| taskUpdateTime | INTEGER | 状态更新时间(时间戳) |
taskStatus 用于表示任务执行状态,取值如下:
0:待执行(初始状态)1:开始执行2:执行中(任务已被脚本获取)3:已完成(执行成功)4:执行失败(执行异常)
扩展字段
除上述字段外,可根据业务需求自定义扩展字段(支持中文字段名)。
建议:
- 字段命名保持语义清晰、统一规范
- 数据类型与实际业务含义保持一致
- 避免与系统字段(如
id、isRead、taskStatus、taskUpdateTime)冲突
数据库创建
可在后台创建并编辑 SQLite 模板,详情参阅 SQLite 数据库
完成数据库设计后,数据库可通过以下两种方式在设备端实现:
数据库文件分发
适用于已设计完成的 SQLite 数据库模板。
数据库文件(.db)可通过任意方式分发至设备,例如本地传输或远程下载等,实现快速部署与同步。
存放路径要求:
/sdcard/cloud/SQLite/
将数据库文件放入上述目录后,系统即可自动识别并使用。
脚本动态创建
适用于运行时初始化或动态生成数据库结构的场景。
无需依赖预置 .db 文件,可通过脚本在设备端直接创建表结构。
// 👉 示例调用
createUnreadMessageDB("/sdcard/cloud/SQLite/未读消息.db");
/**
* 创建【未读消息数据库模板】
*
* 📌 功能说明:
* 该函数用于在指定路径生成一个完整的 SQLite 数据库文件,
* 包含:
* 1. 表结构(meta_actions / 设备消息)
* 2. 初始化数据(任务数据 + 示例消息)
*
* 📌 使用场景:
* - 云控设备初始化数据库
* - 未读消息模板下发
* - 本地测试数据构建
* - 数据库恢复 / 克隆
*
* 📌 特点:
* - 自动删除旧数据库(避免结构冲突)
* - 自动创建表
* - 自动插入初始化数据
* - 支持中文表名 / 字段
*
* @param {string} dbPath 数据库文件路径(如:/sdcard/cloud/SQLite/未读消息.db)
*/
function createUnreadMessageDB(dbPath) {
// =========================
// 0️⃣ 如果已存在数据库 → 先删除
// =========================
var file = new java.io.File(dbPath);
if (file.exists()) {
file.delete();
console.log("🗑️ 已删除旧数据库:", dbPath);
}
// 打开(不存在会自动创建)
var db = sqlite.open(dbPath);
try {
// =========================
// 1️⃣ 创建表结构
// =========================
// 👉 脚本任务配置表
db.execSQL(
"CREATE TABLE meta_actions (" +
"id INTEGER NOT NULL PRIMARY KEY, " +
"activity TEXT, " +
"broadcast TEXT, " +
"scriptContent TEXT, " +
"scriptName TEXT, " +
"scriptPath TEXT, " +
"scriptConfig TEXT, " +
"targetTable TEXT, " +
"targetId INTEGER" +
")"
);
// 👉 未读消息表(核心)
db.execSQL(
"CREATE TABLE 设备消息 (" +
"id INTEGER NOT NULL PRIMARY KEY, " +
"标题 TEXT, " +
"内容 TEXT, " +
"今日收益 REAL, " +
"错误信息 TEXT, " +
"创建时间 TEXT, " +
"isRead INTEGER, " +
"taskStatus INTEGER, " +
"taskUpdateTime INTEGER" +
")"
);
// =========================
// 2️⃣ 插入初始化数据
// =========================
// 👉 脚本任务配置(用于联动执行)
db.insert("meta_actions", {
id: 1,
activity: '{\n "packageName": "bin.mt.plus",\n "className": "bin.mt.plus.Main"\n}',
broadcast:
'{\n "broadcastType": "global",\n "action": "com.example.TEST_ACTION",\n "packageName": "com.example.app",\n "className": "com.example.app.MyReceiver",\n "dataUri": "content://com.example.provider/test",\n "mimeType": "text/plain",\n "categories": [\n "android.intent.category.DEFAULT",\n "android.intent.category.BROWSABLE"\n ],\n "flags": 268435456,\n "permission": "com.example.permission.TEST",\n "extras": {\n "stringKey": "字符串",\n "intKey": 123,\n "longKey": 123456789,\n "doubleKey": 3.14,\n "booleanKey": true\n }\n}',
scriptContent:
'\n\nvar messageConfig = {\n DB_PATH: "/sdcard/cloud/SQLite/未读消息.db",\n TABLE_TASK: "设备消息",\n TABLE_META: "meta_actions"\n};\n\nvar task = getNextTask(messageConfig);\nif (task) {\n console.log("🎯 任务:", task);\n\n try {\n\n // =====================\n // 你的业务逻辑\n // =====================\n // doSomething(task);\n\n taskSuccess(messageConfig, task.id);\n\n } catch (e) {\n console.error(e);\n taskFail(messageConfig, task.id);\n }\n\n} else {\n console.log("❌ 没任务");\n}\n\n\n\n\n\n/**\n * 获取“未读消息任务”\n * \n * 功能说明:\n * 1. 打开 SQLite 数据库\n * 2. 从“设备消息”表中获取一条可执行任务\n * 3. 支持 taskStatus / taskUpdateTime 字段动态判断\n * 4. 支持 meta_actions 兜底获取任务\n * 5. 自动抢占任务(taskStatus = 2)\n * 6. 返回完整任务行数据\n * 7. 自动关闭数据库连接\n * \n * 任务状态说明:\n * 0 = 待执行\n * 1 = 开始执行(已被抢占)\n * 2 = 执行中\n * 3 = 已完成\n * 4 = 执行失败\n * \n * @param {Object} config 配置参数\n * @param {string} config.DB_PATH 数据库路径\n * @param {string} config.TABLE_TASK 任务表名(如:设备消息)\n * @param {string} config.TABLE_META 关联表(如:meta_actions)\n * \n * @returns {Object|null} 返回一条任务数据(整行记录),没有则返回 null\n */\nfunction getNextTask(config) {\n\n // =============================\n // 打开数据库\n // =============================\n var db = sqlite.open(config.DB_PATH);\n\n try {\n\n var TABLE_TASK = config.TABLE_TASK; // 任务表(如:设备消息)\n var TABLE_META = config.TABLE_META; // 关联表(如:meta_actions)\n\n // =============================\n // 判断某个字段是否存在\n // 👉 因为不同数据库结构可能不一样\n // =============================\n function hasColumn(table, column) {\n var cursor = db.rawQuery("PRAGMA table_info(" + table + ")", null);\n var exists = false;\n\n while (cursor.moveToNext()) {\n var row = cursor.pick();\n\n // 如果字段名匹配说明存在\n if (row.name === column) {\n exists = true;\n break;\n }\n }\n\n cursor.close();\n return exists;\n }\n\n // 判断任务表是否有这些字段\n var hasTaskStatus = hasColumn(TABLE_TASK, "taskStatus"); // 是否有状态字段\n var hasTaskUpdateTime = hasColumn(TABLE_TASK, "taskUpdateTime"); // 是否有时间字段\n\n // =============================\n // 抢占任务(非常关键)\n // 👉 防止多个脚本同时执行同一条任务\n // =============================\n function take(id) {\n\n if (!id) return null;\n\n var affect = 1;\n\n // 如果有 taskStatus 字段,就先“抢占任务”\n // 把状态从 1(开始执行) 改成 2(执行中)\n if (hasTaskStatus) {\n affect = db.update(\n TABLE_TASK,\n { taskStatus: 2 },\n "id = ? AND taskStatus = 1", // 只有未执行的才能被抢\n [id]\n );\n }\n\n // 如果 update 失败,说明已经被别人抢走了\n if (affect === 0) return null;\n\n // 返回完整任务数据\n return db.rawQuery(\n "SELECT * FROM " + TABLE_TASK + " WHERE id = ?",\n [id]\n ).single();\n }\n\n var task = null;\n\n // =============================\n // 方法1:从任务表获取\n // =============================\n var sql = "SELECT id FROM " + TABLE_TASK + " WHERE isRead = 1";\n\n // 如果有状态字段,只取“待执行”的任务\n if (hasTaskStatus) {\n sql += " AND taskStatus = 1";\n }\n\n // 如果有时间字段,就取最新的一条\n if (hasTaskUpdateTime) {\n sql += " ORDER BY taskUpdateTime DESC LIMIT 1";\n } else {\n sql += " LIMIT 1";\n }\n\n // 执行查询(只拿 id,减少数据量)\n var row1 = db.rawQuery(sql, null).single();\n\n // 如果查到了,就尝试抢占任务\n if (row1 && row1.id) {\n task = take(row1.id);\n }\n\n // =============================\n // 方法2:兜底来源 meta_actions\n // =============================\n // 👉 如果方法1没拿到任务,再从关联表找\n if (!task) {\n\n var row2 = db.rawQuery(\n "SELECT targetId FROM " + TABLE_META +\n " WHERE targetTable = ? LIMIT 1",\n [TABLE_TASK]\n ).single();\n\n // 用 targetId 再去抢占一次任务\n if (row2 && row2.targetId) {\n task = take(row2.targetId);\n }\n }\n\n // 返回最终任务(可能为 null)\n return task;\n\n } catch (e) {\n\n // 出错日志\n console.error("getNextTask 执行失败:", e);\n return null;\n\n } finally {\n\n // 一定要关闭数据库,释放资源\n db.close();\n }\n}\n\n/**\n * =============== 任务成功 ===============\n * taskStatus = 3\n */\nfunction taskSuccess(config, id) {\n\n var db = sqlite.open(config.DB_PATH);\n\n try {\n\n db.update(\n config.TABLE_TASK,\n { taskStatus: 3 },\n "id = ?",\n [id]\n );\n\n } catch (e) {\n\n console.error(\n "【任务成功更新失败】\\n" +\n "数据库路径: " + config.DB_PATH + "\\n" +\n "表名: " + config.TABLE_TASK + "\\n" +\n "任务ID: " + id + "\\n" +\n "错误信息: " + e\n );\n\n } finally {\n db.close();\n }\n}\n\n/**\n * =============== 任务失败 ===============\n * taskStatus = 4\n */\nfunction taskFail(config, id) {\n\n var db = sqlite.open(config.DB_PATH);\n\n try {\n\n db.update(\n config.TABLE_TASK,\n { taskStatus: 4 },\n "id = ?",\n [id]\n );\n\n } catch (e) {\n\n console.error(\n "【任务失败状态更新失败】\\n" +\n "数据库路径: " + config.DB_PATH + "\\n" +\n "表名: " + config.TABLE_TASK + "\\n" +\n "任务ID: " + id + "\\n" +\n "错误信息: " + e\n );\n\n } finally {\n db.close();\n }\n}\n ',
scriptName: "脚本名称-未读消息",
scriptPath: "",
scriptConfig:
'{\n "delay": 1000,\n "loopTimes": 1,\n "interval": 200,\n "path": [\n "/sdcard/cloud/script"\n ]\n}',
targetTable: "设备消息",
});
// 👉 未读消息模拟数据(业务示例,更易理解)
db.insert("设备消息", {
标题: "新任务待执行",
内容: "任务已下发,等待设备执行",
今日收益: 0.0,
错误信息: "",
创建时间: "2026/04/10 16:10:56",
isRead: 0,
taskStatus: 0,
taskUpdateTime: 0,
});
db.insert("设备消息", {
标题: "任务执行失败",
内容: "请求接口超时,任务中断",
今日收益: 0.0,
错误信息: "网络请求超时:5000ms",
创建时间: "2026/04/10 16:11:22",
isRead: 0,
taskStatus: 4,
taskUpdateTime: 1712736682000,
});
db.insert("设备消息", {
标题: "今日收益结算",
内容: "本次任务收益 23.50 元",
今日收益: 23.5,
错误信息: "",
创建时间: formatDateTime(),
isRead: 1,
taskStatus: 3,
taskUpdateTime: new Date().getTime(),
});
db.insert("设备消息", {
标题: "任务执行完成",
内容: "自动化任务已正常执行完毕",
今日收益: 0.0,
错误信息: "",
创建时间: formatDateTime(),
isRead: 1,
taskStatus: 3,
taskUpdateTime: new Date().getTime(),
});
db.insert("设备消息", {
标题: "设备运行异常",
内容: "应用意外退出,任务未完成",
今日收益: 0.0,
错误信息: "进程异常终止:CODE:9",
创建时间: formatDateTime(),
isRead: 1,
taskStatus: 4,
taskUpdateTime: new Date().getTime(),
});
console.log("✅ 未读消息数据库创建完成:", dbPath);
} catch (e) {
console.error("❌ 数据库创建失败:", e);
} finally {
db.close();
}
}
/**
* 格式化时间为:yyyy/MM/dd HH:mm:ss
*
* @param {number} [timestamp] 可选时间戳(毫秒)
* @returns {string} 格式化后的时间字符串
*/
function formatDateTime(timestamp) {
var date = timestamp ? new Date(timestamp) : new Date();
return (
date.getFullYear() +
"/" +
(date.getMonth() + 1) +
"/" +
date.getDate() +
" " +
date.getHours() +
":" +
date.getMinutes() +
":" +
date.getSeconds()
);
}
消息更新与同步
通过脚本向手机本地的 SQLite 数据库文件里添加数据并通知云控后台更新未读消息
添加消息
在脚本中向 SQLite 数据库文件里添加消息:
// 👉 示例
insertMessage(
"/sdcard/cloud/SQLite/未读消息_副本.db",
"设备消息",
{
标题: "新任务",
内容: "执行中...",
今日收益: 0,
错误信息: "",
创建时间: formatDateTime(),
isRead: 0,
taskStatus: 1,
taskUpdateTime: new Date().getTime()
}
);
/**
* 向指定表添加一条消息数据
*
* 📌 功能说明:
* 向 SQLite 数据库中的指定表插入一条记录(消息对象)。
*
* 📌 使用场景:
* - 添加未读消息
* - 记录任务执行结果
* - 动态写入业务数据
*
* 📌 特点:
* - 自动打开 / 关闭数据库
* - 支持任意表(不局限“设备消息”)
* - 传入对象即插入(键值对应字段)
*
* ⚠️ 注意:
* - message 对象的字段必须和表结构一致
* - 如果包含主键 id,请确保不冲突
*
* @param {string} dbPath 数据库路径
* @param {string} tableName 表名(如:设备消息)
* @param {Object} message 要插入的数据(对象形式)
*
* @returns {number} 插入成功返回 rowId,失败返回 -1
*/
function insertMessage(dbPath, tableName, message) {
var db = sqlite.open(dbPath);
try {
// 👉 插入数据
var rowId = db.insert(tableName, message);
if (rowId === -1) {
console.error("❌ 插入失败:", message);
} else {
console.log("✅ 插入成功,rowId:", rowId);
}
return rowId;
} catch (e) {
console.error("❌ 插入异常:", e);
return -1;
} finally {
db.close();
}
}
/**
* 格式化时间为:yyyy/MM/dd HH:mm:ss
*
* @param {number} [timestamp] 可选时间戳(毫秒)
* @returns {string} 格式化后的时间字符串
*/
function formatDateTime(timestamp) {
var date = timestamp ? new Date(timestamp) : new Date();
return date.getFullYear() + "/" +
(date.getMonth() + 1) + "/" +
date.getDate() + " " +
date.getHours() + ":" +
date.getMinutes() + ":" +
date.getSeconds();
}
未读消息同步接口
$cloud.notifyUnreadMessages() 用于在脚本向本地 SQLite 写入未读消息数据后,通知云控后台刷新数据,实现投屏界面未读消息的实时同步展示。
该接口仅用于触发同步机制,不参与具体业务数据处理。
$cloud.notifyUnreadMessages();
显示数量
在 全局配置 → 投屏配置 → 未读任务消息显示 中设置
- 建议:1~2 条
- 说明:数量过多会影响悬浮日志显示效果