diff --git a/app/Http/Controllers/Api/AppsController.php b/app/Http/Controllers/Api/AppsController.php index 2b594e0db..05e0eea64 100644 --- a/app/Http/Controllers/Api/AppsController.php +++ b/app/Http/Controllers/Api/AppsController.php @@ -13,6 +13,7 @@ use Request; * 动态路由(routes/web.php): * api/apps/badge/set -> badge__set() 应用密钥鉴权,绝对设置/清除角标 * api/apps/badge/clear -> badge__clear() 当前用户 token 鉴权,清除自己的角标 + * api/apps/badge/list -> badge__list() 当前用户 token 鉴权,拉取自己全部角标(初始同步) */ class AppsController extends AbstractController { @@ -71,4 +72,23 @@ class AppsController extends AbstractController trim(Request::input('menu_key', '')) )); } + + /** + * @api {get} api/apps/badge/list 拉取自己全部角标 + * + * @apiDescription 供前端初始同步:返回当前用户全部应用(插件 + 自定义微应用)的角标快照。 + * 数据结构 app_id => menu_key => {count, dot},与前端 store 的 map 结构一致。 + * @apiVersion 1.0.0 + * @apiGroup apps + * @apiName badge__list + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息 + * @apiSuccess {Object} data 返回数据 + */ + public function badge__list() + { + $user = User::auth(); + return Base::retSuccess('success', Badge::userBadges((int)$user->userid)); + } } diff --git a/app/Http/Controllers/Api/SystemController.php b/app/Http/Controllers/Api/SystemController.php index 576aa7c30..dcac260c3 100755 --- a/app/Http/Controllers/Api/SystemController.php +++ b/app/Http/Controllers/Api/SystemController.php @@ -23,7 +23,6 @@ use Symfony\Component\Mailer\Transport; use Symfony\Component\Mime\Email; use App\Models\UserCheckinRecord; use App\Module\Apps; -use App\Module\Badge; use App\Module\BillMultipleExport; use LdapRecord\LdapRecordException; use Swoole\Coroutine; @@ -787,7 +786,6 @@ class SystemController extends AbstractController } $setting = Setting::filterCustomMicroAppsForUser($setting, $user); $setting = Setting::formatCustomMicroAppsForResponse($setting); - $setting = Badge::attachMenuBadges($setting, $user ? (int)$user->userid : 0); } return Base::retSuccess($type == 'save' ? '保存成功' : 'success', $setting); } diff --git a/app/Module/Badge.php b/app/Module/Badge.php index 19cf80d84..775e471d5 100644 --- a/app/Module/Badge.php +++ b/app/Module/Badge.php @@ -273,33 +273,6 @@ class Badge return $map; } - /** - * 为微应用菜单列表附带当前用户的角标 {count,dot},作为前端初始同步来源(零额外请求)。 - * - * @param array $apps 微应用列表(每项含 id 与 menu_items) - * @param int $userid - * @return array - */ - public static function attachMenuBadges(array $apps, int $userid): array - { - $map = self::userBadges($userid); - foreach ($apps as &$app) { - $appId = (string)($app['id'] ?? ''); - if (empty($app['menu_items']) || !is_array($app['menu_items'])) { - continue; - } - foreach ($app['menu_items'] as &$menu) { - $menuKey = (string)($menu['key'] ?? ''); - $badge = $map[$appId][$menuKey] ?? ['count' => 0, 'dot' => false]; - $menu['count'] = (int)$badge['count']; - $menu['dot'] = (bool)$badge['dot']; - } - unset($menu); - } - unset($app); - return $apps; - } - /** * 向在线用户实时推送角标变更(仅投递,不补发离线)。 * diff --git a/resources/assets/js/store/actions.js b/resources/assets/js/store/actions.js index 1377df840..7c1709068 100644 --- a/resources/assets/js/store/actions.js +++ b/resources/assets/js/store/actions.js @@ -5376,8 +5376,13 @@ export default { // 忽略自定义菜单加载失败 } commit("microApps/data", apps || []) - // 用应用菜单返回的角标初始化(自定义应用由 microapp_menu 提供,插件应用由 appstore installed 提供) - commit("appBadges/hydrate", apps || []) + // 角标初始同步:一次拉取当前用户全部应用(插件 + 自定义微应用)的角标快照 + try { + const {data: badgeMap} = await dispatch('call', {url: 'apps/badge/list'}); + commit("appBadges/hydrateMap", badgeMap || {}) + } catch (e) { + // 忽略角标初始同步失败 + } } }, diff --git a/resources/assets/js/store/modules/appBadges.js b/resources/assets/js/store/modules/appBadges.js index b5e1d5525..9fd920683 100644 --- a/resources/assets/js/store/modules/appBadges.js +++ b/resources/assets/js/store/modules/appBadges.js @@ -8,7 +8,7 @@ const EMPTY_BADGE = Object.freeze({count: 0, dot: false}) * * 结构:map[app_id][menu_key] = { count, dot } * - menu_key 为空串表示该应用的第一个菜单 - * - 初始值由 updateMicroAppsStatus 通过 hydrate 填充(microapp_menu / appstore installed) + * - 初始值由 updateMicroAppsStatus 通过 hydrateMap 填充(apps/badge/list 一次返回插件 + 自定义全部角标) * - 运行时由 websocket 消息 appBadge 经 set 增量更新 */ export default { @@ -42,29 +42,32 @@ export default { }, /** - * 由应用菜单列表整体初始化角标 + * 由后端角标快照整体初始化(apps/badge/list 返回,含插件 + 自定义微应用) * @param state - * @param apps 形如 [{id, menu_items:[{key,count,dot}]}] + * @param map 形如 { app_id: { menu_key: {count, dot} } } */ - hydrate(state, apps) { - const map = {} - ;(Array.isArray(apps) ? apps : []).forEach(app => { - const appid = app && app.id - if (!appid || !Array.isArray(app.menu_items)) { - return - } - app.menu_items.forEach(menu => { - const count = Number(menu && menu.count) || 0 - const dot = !!(menu && menu.dot) - if (count > 0 || dot) { - if (!map[appid]) { - map[appid] = {} - } - map[appid][(menu && menu.key) || ''] = {count, dot} + hydrateMap(state, map) { + const next = {} + if (map && typeof map === 'object') { + Object.keys(map).forEach(appid => { + const menus = map[appid] + if (!appid || !menus || typeof menus !== 'object') { + return } + Object.keys(menus).forEach(key => { + const item = menus[key] || {} + const count = Number(item.count) || 0 + const dot = !!item.dot + if (count > 0 || dot) { + if (!next[appid]) { + next[appid] = {} + } + next[appid][key || ''] = {count, dot} + } + }) }) - }) - state.map = map + } + state.map = next }, /** diff --git a/routes/api-map.md b/routes/api-map.md index a79ac3f7f..bdcbb779c 100644 --- a/routes/api-map.md +++ b/routes/api-map.md @@ -2,7 +2,7 @@ > 此文件由 `php artisan doc:api-map` 生成,勿手改。 -接口总数:306 +接口总数:307 ## 路由规则 @@ -377,6 +377,7 @@ API 使用动态路由(见 `routes/web.php`),URL 段映射为控制器方 | --- | --- | --- | --- | | api/apps/badge/set | badge__set() | post | 设置角标(应用密钥鉴权) | | api/apps/badge/clear | badge__clear() | post | 清除角标(当前用户 token 鉴权) | +| api/apps/badge/list | badge__list() | get | 拉取自己全部角标 | ## test(TestController) diff --git a/routes/web.php b/routes/web.php index fe611dc7c..d0fd77742 100644 --- a/routes/web.php +++ b/routes/web.php @@ -33,44 +33,44 @@ use App\Http\Controllers\Api\AppsController; */ Route::prefix('api')->middleware(['webapi'])->group(function () { // 会员 - Route::any('users/{method}', UsersController::class); - Route::any('users/{method}/{action}', UsersController::class); + Route::any('users/{method}', UsersController::class); + Route::any('users/{method}/{action}', UsersController::class); // 项目 - Route::any('project/{method}', ProjectController::class); - Route::any('project/{method}/{action}', ProjectController::class); + Route::any('project/{method}', ProjectController::class); + Route::any('project/{method}/{action}', ProjectController::class); // 系统 - Route::any('system/{method}', SystemController::class); - Route::any('system/{method}/{action}', SystemController::class); + Route::any('system/{method}', SystemController::class); + Route::any('system/{method}/{action}', SystemController::class); // 在线授权 - Route::any('license/{method}', LicenseController::class); - Route::any('license/{method}/{action}', LicenseController::class); + Route::any('license/{method}', LicenseController::class); + Route::any('license/{method}/{action}', LicenseController::class); // 对话 - Route::any('dialog/{method}', DialogController::class); - Route::any('dialog/{method}/{action}', DialogController::class); + Route::any('dialog/{method}', DialogController::class); + Route::any('dialog/{method}/{action}', DialogController::class); // 文件 - Route::any('file/{method}', FileController::class); - Route::any('file/{method}/{action}', FileController::class); + Route::any('file/{method}', FileController::class); + Route::any('file/{method}/{action}', FileController::class); // 报告 - Route::any('report/{method}', ReportController::class); - Route::any('report/{method}/{action}', ReportController::class); + Route::any('report/{method}', ReportController::class); + Route::any('report/{method}/{action}', ReportController::class); // 公开接口 - Route::any('public/{method}', PublicController::class); - Route::any('public/{method}/{action}', PublicController::class); + Route::any('public/{method}', PublicController::class); + Route::any('public/{method}/{action}', PublicController::class); // 助手 - Route::any('assistant/{method}', AssistantController::class); - Route::any('assistant/{method}/{action}', AssistantController::class); + Route::any('assistant/{method}', AssistantController::class); + Route::any('assistant/{method}/{action}', AssistantController::class); // 投诉 - Route::any('complaint/{method}', ComplaintController::class); - Route::any('complaint/{method}/{action}', ComplaintController::class); + Route::any('complaint/{method}', ComplaintController::class); + Route::any('complaint/{method}/{action}', ComplaintController::class); // 智能搜索 - Route::any('search/{method}', SearchController::class); - Route::any('search/{method}/{action}', SearchController::class); + Route::any('search/{method}', SearchController::class); + Route::any('search/{method}/{action}', SearchController::class); // 应用相关 - Route::any('apps/{method}', AppsController::class); - Route::any('apps/{method}/{action}', AppsController::class); + Route::any('apps/{method}', AppsController::class); + Route::any('apps/{method}/{action}', AppsController::class); // 测试 - Route::any('test/{method}', TestController::class); - Route::any('test/{method}/{action}', TestController::class); + Route::any('test/{method}', TestController::class); + Route::any('test/{method}/{action}', TestController::class); }); /**