Files
fdesk/app/common/service/MenuBadgeService.php
T
2025-10-12 06:22:29 +00:00

178 lines
6.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\common\service;
use support\Db;
class MenuBadgeService
{
/**
* 构造某个节点向上的角标链路(根->叶),每一级返回“完整url + 角标”
* @param int $menuId 叶子或中间节点ID(directory/menu均可)
* @param int $leafCount 该节点需要向上累加的消息数(正数可表示新增)
* @param bool $includeHidden 是否包含隐藏/禁用菜单(默认 false:过滤 status != 1)
* @return array<int, array{id:int,name:string,type:string,url:string,badge:int}>
*/
public static function buildBadgeChain(
int $menuId,
int $leafCount,
bool $includeHidden = false,
bool $forceIncludeSelf = true
): array {
// 取到从“叶->根”的节点列表
$nodes = self::getAncestorNodesIterative($menuId, $includeHidden, $forceIncludeSelf);
if (empty($nodes)) {
return [];
}
// 调整为“根->叶”
$nodes = array_reverse($nodes);
$segments = [];
$result = [];
$leafId = $menuId;
foreach ($nodes as $n) {
$isLeaf = ((int)$n['id'] === $leafId);
// 中间层的 permission 不展示(但若有内部url段,仍需累计到segments)
if (($n['type'] ?? '') === 'permission' && !$isLeaf) {
// 若这是 internal 且有 url 段,仍拼到段里,便于子级正确拼接
$openType = strtolower((string)($n['open_type'] ?? 'internal'));
if ($openType === 'internal') {
$seg = trim((string)($n['url'] ?? ''), '/');
if ($seg !== '') {
$segments[] = $seg;
}
}
continue;
}
// 计算完整URL(internal 按段拼接;external/iframe 直接用 link/url)
$fullUrl = self::composeFullUrl($segments, $n);
// 叶子(传入ID)必须返回;其他节点照常
if ($isLeaf || ($n['type'] ?? '') !== 'permission') {
$result[] = [
'id' => (int)$n['id'],
'name' => (string)$n['name'],
'type' => (string)$n['type'],
'url' => $fullUrl,
'badge' => (int)$leafCount,
];
}
}
return $result;
}
/**
* 批量聚合多个叶子节点的角标,把同一路径处的角标相加(父级=所有子级之和)
* @param array<int, array{menu_id:int,count:int}> $items
* @param bool $includeHidden
* @return array<int, array{url:string,badge:int}>
*/
public static function aggregateChains(array $items, bool $includeHidden = false): array
{
$acc = []; // key = url
foreach ($items as $it) {
$menuId = (int)($it['menu_id'] ?? 0);
$leafCount = (int)($it['count'] ?? 0);
if ($menuId <= 0 || $leafCount === 0) {
continue;
}
$chain = self::buildBadgeChain($menuId, $leafCount, $includeHidden);
foreach ($chain as $node) {
$key = $node['url'];
if (!isset($acc[$key])) {
$acc[$key] = ['url' => $node['url'], 'badge' => 0];
}
$acc[$key]['badge'] += $node['badge'];
}
}
// 父级在前(更短的路径在前),便于前端逐级渲染
usort($acc, function ($a, $b) {
return substr_count($a['url'], '/') <=> substr_count($b['url'], '/');
});
return array_values($acc);
}
/**
* 迭代法:从叶子一路查到根(MySQL版本无关)
* $forceIncludeSelf=true 时,无论叶子 status 是否为1,都会把叶子纳入;
* 但祖先节点仍按 $includeHidden 规则过滤。
*/
private static function getAncestorNodesIterative(
int $id,
bool $includeHidden = false,
bool $forceIncludeSelf = true
): array {
$cols = ['id', 'parent_id', 'name', 'type', 'url', 'open_type', 'link', 'is_hide', 'status'];
$nodes = [];
$seen = [];
$cur = $id;
$maxHop = 32; // 防环
$isFirst = true;
for ($i = 0; $i < $maxHop && $cur > 0; $i++) {
/** @var object|null $row */
$row = Db::table('resources')->select($cols)->where('id', $cur)->first();
if (!$row) break;
$rowArr = (array)$row;
if (!$includeHidden) {
// 叶子节点:若 $forceIncludeSelf=true 则无视 status 过滤;祖先照常过滤
if (!$isFirst && (int)$rowArr['status'] !== 1) {
break;
}
if ($isFirst && !$forceIncludeSelf && (int)$rowArr['status'] !== 1) {
// 如果明确不强制包含叶子,叶子也按规则过滤
break;
}
}
$nodes[] = $rowArr;
if (in_array($cur, $seen, true)) {
break; // 防环
}
$seen[] = $cur;
$cur = (int)$rowArr['parent_id'];
$isFirst = false;
}
return $nodes; // 叶 -> 根
}
/**
* 组装“完整路由”
* - internal:把父级段 + 自身 url 段拼成形如 /a/b/c
* - external/iframe:直接用 link(若无则退回自身 url)
* - 忽略空段(避免出现重复斜杠)
*/
private static function composeFullUrl(array &$segments, array $node): string
{
$openType = strtolower((string)($node['open_type'] ?? 'internal'));
// 外链或 iframe:直接返回 link/url,不再参与分段拼接
if ($openType !== 'internal') {
$target = trim((string)($node['link'] ?? ''), " \t\r\n");
if ($target === '') {
$target = (string)($node['url'] ?? '');
}
return $target;
}
// internal:把自身 url 作为一个“段”拼到前面来的 segments
$seg = trim((string)($node['url'] ?? ''), '/');
if ($seg !== '') {
$segments[] = $seg;
}
return '/' . implode('/', $segments);
}
}