- 后端
- 前端
- 协同办公
- 即时通讯
- 企业应用
【免费下载链接】dootask
DooTask是一款开源在线项目任务管理工具,提供各类文档协作工具、在线思维导图、在线流程图、项目管理、任务分发、即时IM,文件管理等工具;同时消息功能使用非对称加密技术让你的沟通更安全。
导读
DooTask 的插件/微应用可以在「应用中心卡片」「主导航平铺入口」以及父级「应用」入口上展示角标——一个提醒数字或一个小红点,用于提示用户有待处理事项。本文以 插件/微应用菜单角标概念文档 为骨架,结合仓库中app_badges表、Badge 业务模块、AppsController 接口 与前端appBadgesstore 的源码实现,完整讲清角标的定义、展示规则、菜单配置属性、三个后端接口的调用方式、自动清零与多端实时同步机制,以及卸载/离职时的生命周期清理。读完本文,你将能独立为自有插件接入 DooTask 角标能力,或从源码层面理解角标的完整数据流。
一、角标是什么:真值归插件,主程序只做存储与分发
角标(Badge)是插件/微应用菜单上显示的一个数字或小红点,用来提醒用户「这里有待处理事项」。它的核心设计原则是:
角标真值归插件。DooTask 主程序不自己产生角标数字,只负责存储、下发与实时推送。插件没有接入角标接口,就不会有角标出现。
这意味着角标不是消息未读数,其具体含义由各插件自行定义——可以代表「待审批数量」「待办提醒」或任意业务计数;自定义微应用菜单(纯外链)默认也不会有角标,除非其后端接入了角标接口。
二、展示位置与聚合规则
角标出现在三个层级:
| 展示位置 | 说明 |
|---|---|
| 应用中心卡片 | 对应插件/微应用卡片右上角显示数字或红点 |
| 主导航平铺入口 | 被平铺到左侧主菜单的单个插件入口显示其角标 |
| 父「应用」入口(聚合) | 左侧「应用」一级入口与移动端底部「应用」标签显示聚合角标 |
父级「应用」入口的聚合规则:
- 聚合数字= 工作报告未读数 + 所有插件角标数字之和
- 总数 > 0 时显示数字;超过 999 显示
999+ - 总数为 0 时,若任一插件存在红点则显示红点
该聚合逻辑在前端 appBadges store 中有直接实现:
// 所有应用角标数字之和 totalCount: (state) => { /* 遍历 map 累加 count */ }, // 是否存在任一红点 anyDot: (state) => { /* 遍历 map 判断任意 dot 为 true */ }, // 父『应用』入口聚合数字:工作报告未读 + 所有插件角标数字之和 applicationCount: (state, getters, rootState) => (Number(rootState.reportUnreadNumber) || 0) + getters.totalCount, // 父『应用』入口聚合红点:总数为 0 时,任一插件存在红点则显示 applicationDot: (state, getters) => getters.anyDot,从源码可见,聚合数字是「工作报告未读数 + 全部插件角标数字之和」,聚合红点则是在总数为 0 时由任意插件的红点决定,与概念文档完全一致。
三、菜单配置关键属性:key 与 badge_clear_on_open
角标的挂载目标是「菜单」,因此两个配置属性定义在菜单配置menu_items中(自定义微应用在管理员配置页填写,插件应用在插件包config.yml的menu_items中声明):
3.1key:菜单稳定标识
- 用于定位某个菜单的角标,即角标绑定到哪个菜单;
- 留空时表示该应用的第一个菜单。
后端在解析时遵循同样的规则,见 Badge.php 的 resolveMenu:menu_key为空时直接取menus[0](若列表为空则退化为['key' => '', 'visible' => ['all'], 'badge_clear_on_open' => false]);非空时必须在已声明的菜单 key 中命中,否则判定「菜单不存在」。
3.2badge_clear_on_open:打开时是否自动清零
true:打开对应应用菜单时角标自动清除;false(默认):永不自动清零,角标的增减完全由插件控制。
在 Setting.php 的菜单规范化逻辑 中,自定义微应用菜单保存时会强制规范化这两个字段:
// 角标:菜单稳定标识 与 打开时是否自动清零 $payload['key'] = Base::newTrim($menu['key'] ?? ''); $payload['badge_clear_on_open'] = (bool)($menu['badge_clear_on_open'] ?? false);插件应用的菜单声明同样会经过 Apps.php 的 mapMenuItem 归一为['key', 'visible', 'badge_clear_on_open']三元组,供角标模块解析。
四、数据存储:app_badges 表
角标持久化在独立的app_badges表中,由迁移 2026_06_28_000001_create_app_badges_table.php 创建,对应的模型为 AppBadge:
| 字段 | 类型 | 说明 |
|---|---|---|
id | bigint | 主键 |
app_id | string(100) | 应用 ID(appstore 插件 appid 或自定义微应用 id) |
menu_key | string(100) | 菜单稳定标识;空串表示该应用的第一个菜单 |
userid | bigint | 用户 ID |
count | int | 角标数字,默认 0 |
dot | boolean | 是否显示红点,默认 false |
updated_at | timestamp | 更新时间 |
表的关键约束:
- 唯一键
(app_id, menu_key, userid):同一用户在同一应用的同一菜单上只有一行角标记录,天然支持「绝对设置(幂等覆盖)」; - 索引
userid:支撑「用户离职清空」「按用户拉取快照」这类按用户维度的高频查询。
值得注意的存储语义:app_badges仅存非清除态。当count=0 且 dot=false时该行会被直接删除,而不是写入一条「零值记录」,见 Badge.php 的 applySet:
// 清除态:一条 whereIn 删除 if ($count === 0 && !$dot) { AppBadge::whereAppId($appId)->whereMenuKey($menuKey)->whereIn('userid', $userids)->delete(); return; } // 非清除态:依赖唯一键 (app_id,menu_key,userid) 批量 upsert AppBadge::upsert($rows, ['app_id', 'menu_key', 'userid'], ['count', 'dot', 'updated_at']);五、三个后端接口:set / clear / list
角标能力对外暴露为三个接口,全部定义在 AppsController 中,通过动态路由api/apps/{method}映射(见 routes/web.php):
5.1POST api/apps/badge/set:设置角标(应用密钥鉴权)
由插件服务端使用应用密钥调用,对(appid, 菜单, 每个 userid)绝对设置角标(幂等覆盖)。
| 参数 | 类型 | 说明 |
|---|---|---|
appid | String | 应用 ID |
secret | String | 应用密钥(APP_SECRET) |
userid | Number | Number[] | 目标用户 ID(单个或数组) |
menu_key | String(可选) | 菜单稳定标识;留空表示该应用第一个菜单 |
count | Number(可选,默认 0) | 角标数字 |
dot | Boolean(可选,默认 false) | 是否显示红点(count=0 且 dot=false 即清除) |
该接口的完整执行链在 Badge::set:
- 参数校验:
appid、secret非空,userid归一化为去重去零的整型数组; - 密钥校验:
Apps::appSecret($appId)取出该应用独立密钥,用hash_equals常量时间比较,避免时序攻击(见 Apps::appSecret)。APP_SECRET 与全局 APP_KEY 不同——每个已安装应用独立、唯一,持久化在docker/appstore/config/{appid}/config.yml的app_secret字段,首次需要时惰性生成; - 菜单解析:
resolveAppMenu校验应用已安装、菜单存在(见 Badge::resolveAppMenu),同时兼容两类来源——插件应用读取docker/appstore/apps/{appid}/{version}/config.yml的menu_items,自定义微应用读取microapp_menu系统设置(见 Apps::appMenuConfig); - 可见性过滤:只保留对该应用菜单有可见权限的用户(见下文第六节);
- 落库 + 推送:
applySet幂等写库,push通过 WebSocket 实时下发。
响应示例:
{ "ret": 1, "msg": "success", "data": { "appid": "approve", "menu_key": "todo", "count": 3, "dot": false, "affected": 25 } }affected为实际被写入并推送的用户数——当过滤后目标用户为空时,接口直接返回affected: 0,不会产生任何写入。
5.2POST api/apps/badge/clear:清除角标(当前用户 token 鉴权)
供前端在badge_clear_on_open=true的菜单打开时调用,清除当前用户在该应用该菜单的角标:
| 参数 | 类型 | 说明 |
|---|---|---|
appid | String | 应用 ID |
menu_key | String(可选) | 菜单稳定标识;留空表示该应用第一个菜单 |
对应实现 Badge::clearForUser:按(app_id, menu_key, userid)删除该行,然后以count=0, dot=false的 payload 推送给该用户的所有在线端,保证多端一致。
5.3GET api/apps/badge/list:拉取自己全部角标(初始同步)
供前端初始同步:返回当前用户全部应用(插件 + 自定义微应用)的角标快照,数据结构为app_id => menu_key => {count, dot},与前端 store 的 map 结构一一对应(见 AppsController::badge__list)。
对应实现 Badge::userBadges 有一个值得一提的细节:返回快照时会过滤掉应用已不存在(卸载 / 自定义微应用被删除)的行,避免父级聚合统计残留脏数据;同时通过按app_id缓存存在性判定结果、一次性收集自定义微应用 id 集合,避免逐行重复读 yaml / 遍历设置项。
六、可见性:角标只发给对该应用菜单可见的用户
角标与 微应用权限 联动:角标只发给对该应用菜单可见的用户。过滤逻辑在 Badge::filterVisibleUserids:
- 菜单
visible包含all时,目标用户全部保留; - 否则按用户身份逐一调用
Setting::isCustomMicroVisibleTo($visible, $user->isAdmin(), $userid)判定(见 Setting.php):admin仅管理员可见,具体 userid 则按白名单匹配。
可见性来源同样是菜单配置:插件应用取插件包config.yml中的应用级visible_to(默认all),自定义微应用取应用级visible_to(默认admin),菜单自身也可覆盖,见 Apps::readPluginMenus 与 Apps::mapMenuItem。
七、自动清零:本地先清,服务端持久化
当菜单设置了badge_clear_on_open: true,用户打开该菜单时:
- 前端立即本地清零:先更新本地 store,避免网络延迟造成角标残留闪烁;
- 调用接口持久化清除:同步请求
apps/badge/clear; - 多端实时推送保持一致:后端清除成功后推送给该用户的所有在线端。
前端实现见 MicroApps/index.vue 的 onOpen:
// 角标:打开 badge_clear_on_open=true 的菜单时自动清零(本地 + 服务端持久化) if (config.badge_clear_on_open === true && config.id) { const menuKey = typeof config.key === 'string' ? config.key : ''; this.$store.commit('appBadges/clearMenu', {appid: config.id, menu_key: menuKey}); this.$store.dispatch('call', { url: 'apps/badge/clear', method: 'post', data: {appid: config.id, menu_key: menuKey}, }).catch(() => {}); }若badge_clear_on_open为false(默认),则角标的增减完全由插件通过badge/set接口控制,主程序永不自动清零。
八、实时性:WebSocket 增量推送 + 初始快照同步
角标的数据流分两条路径:
8.1 在线用户:WebSocket 实时推送
插件调用badge/set或用户调用badge/clear后,后端通过 Badge::push 向目标用户推送type: appBadge的消息:
PushTask::push([ 'userid' => $userids, 'msg' => [ 'type' => 'appBadge', 'data' => [ 'appid' => $appId, 'menu_key' => $menuKey, 'count' => $count, 'dot' => $dot, ], ], ], false);PushTask::push依赖 Swoole 运行时绑定(见 PushTask)——非 Swoole 运行时(artisan/测试)无法推送,会直接跳过,与观察者模式的守卫逻辑一致。前端收到appBadge消息后,在 store/actions.js 中增量更新:
case "appBadge": commit("appBadges/set", msgDetail.data || {});8.2 离线用户:进入时随初始数据同步
离线用户不会收到推送(PushTask::push的$retryOffline参数在角标场景传false,即仅投递、不补发离线)。离线用户下次进入时,由应用菜单接口在拉取微应用列表后顺带调用apps/badge/list完成角标初始同步,无需额外请求,见 store/actions.js 的 updateMicroAppsStatus:
// 角标初始同步:一次拉取当前用户全部应用(插件 + 自定义微应用)的角标快照 const {data: badgeMap} = await dispatch('call', {url: 'apps/badge/list'}); commit("appBadges/hydrateMap", badgeMap || {})前端appBadgesstore 的完整结构(见 appBadges.js):
map[app_id][menu_key] = { count, dot },menu_key为空串表示该应用第一个菜单;- 无角标时返回冻结的
{count: 0, dot: false}共享只读对象,避免 getter 每次调用新建对象; hydrateMap快照整体初始化,只保留count > 0 || dot的非清除态;set增量更新、clearMenu清除单个菜单、clearApp清除应用全部角标(卸载/更新时)。
九、生命周期:卸载清空与离职清空
角标的生命周期与「应用」和「用户」强绑定:
| 场景 | 行为 | 对应实现 |
|---|---|---|
| 应用被卸载 | 该应用的全部角标清除 | Badge::clearByApp:AppBadge::whereAppId($appId)->delete() |
| 用户离职 | 该用户的全部角标清除 | Badge::clearByUser:AppBadge::whereUserid($userid)->delete() |
同时,前端userBadges快照与hydrateMap都会过滤掉应用已不存在的行,保证卸载后父级「应用」入口的聚合统计不会残留脏数据。
十、边界与常见误区
结合概念文档的negative声明与源码实现,以下边界值得插件开发者注意:
- 角标真值归插件,主程序不自己产生数字——插件未接入则不会有角标;
- 角标不是消息未读数,具体含义由各插件自行定义;
- 自定义微应用菜单(纯外链)默认不会有角标,除非其后端接入了角标接口——即需要调用
apps/badge/set,且自定义微应用也可在microapp_menu配置中为菜单声明key与badge_clear_on_open; - 角标受菜单可见性过滤,只发给对该应用菜单可见的用户,即使接口传入了无权用户也会被过滤掉(
affected会相应减少); - 清除态不落库:
count=0 且 dot=false即删除行,app_badges仅存非清除态; - 离线不补发:实时推送仅投递给在线用户,离线用户在下次进入时通过
apps/badge/list初始同步兜底。
十一、给插件开发者的接入清单
要将角标能力接入自己的插件,最小步骤为:
- 在插件包
config.yml的menu_items中为需要角标的菜单声明key(可留空表示第一个菜单)与badge_clear_on_open; - 插件服务端在业务产生未读/待办时,调用
POST api/apps/badge/set,携带appid、secret(即 APP_SECRET,与主程序写入docker/appstore/config/{appid}/config.yml的app_secret一致)、userid(单个或数组)、menu_key、count/dot; - 需要「打开即清零」时,在菜单配置中置
badge_clear_on_open: true,前端会自动完成本地清零与apps/badge/clear调用; - 无需额外处理多端一致性与离线同步——主程序通过 WebSocket
appBadge消息实时推送,离线用户由apps/badge/list快照兜底。
相关阅读
- 微应用整体概念:微应用是什么、与系统应用的区别
- 自定义微应用菜单:
microapp_menu配置、菜单属性与配置入口 - 微应用权限:可见性过滤与登录态传递
- 后端实现:Badge 业务模块、角标 API 控制器、AppBadge 模型、app_badges 表迁移
- 前端实现:appBadges store、微应用组件 onOpen、角标消息处理与初始同步
- 后端
- 前端
- 协同办公
- 即时通讯
- 企业应用
【免费下载链接】dootask
DooTask是一款开源在线项目任务管理工具,提供各类文档协作工具、在线思维导图、在线流程图、项目管理、任务分发、即时IM,文件管理等工具;同时消息功能使用非对称加密技术让你的沟通更安全。
相关推荐
DooTask 微应用(Micro App)体系详解:插件安装、菜单挂载、登录态继承与角标联动
DooTask 微应用(Micro App)体系详解:插件安装、菜单挂载、登录态继承与角标联动 本文以 DooTask 知识库( resources/ai kb
后端前端协同办公即时通讯企业应用x64dbg GuiMenuClear 接口详解:清空菜单项的跨进程实现与插件菜单生命周期
x64dbg GuiMenuClear 接口详解:清空菜单项的跨进程实现与插件菜单生命周期 本文讲解 x64dbg GUI 插件接口中的 GuiMenuClea
逆向工程调试器插件系统x64dbg GUI 接口详解:GuiMenuSetIcon 为插件菜单设置图标
x64dbg GUI 接口详解:GuiMenuSetIcon 为插件菜单设置图标 本文围绕 x64dbg 开发者文档中的 GuiMenuSetIcon 接口展开
逆向工程调试器插件系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考