fix(channel-monitor): drop soft delete, refactor feature flag to declarative form
### 后端修复:日志表不该用软删除 channel_monitor_histories / channel_monitor_daily_rollups 都是日志/聚合表, 没有恢复需求。110 里加的 SoftDeleteMixin 会让 DELETE 自动变成 UPDATE deleted_at, 导致行和索引只增不减,徒增磁盘占用和查询成本。 改回分批物理删(参考 OpsCleanupService.deleteOldRowsByID 模板): - ent schema 移除 SoftDeleteMixin,重新 go generate - repo 新增 deleteChannelMonitorBatched 辅助 + 两条 prune SQL 常量 (WITH batch AS SELECT id LIMIT 5000 → DELETE IN batch) - DeleteHistoryBefore / DeleteRollupsBefore 改调分批 raw SQL - 移除 ComputeAvailability / ComputeAvailabilityForMonitors / UpsertDailyRollupsFor / ListLatestPerModel / ListLatestForMonitorIDs / ListRecentHistoryForMonitors 等 raw SQL 中的 deleted_at IS NULL 过滤 - UpsertDailyRollupsFor 的 ON CONFLICT 去掉 deleted_at = NULL 重置 - migration 111 DROP COLUMN deleted_at + 对应索引(110 已部署但 maintenance 首跑在次日 02:00,此时尚无业务数据在依赖软删除) ### 前端重构:feature flag 声明式 + 复用 AppSidebar.vue 里 7 处 `...(flag ? [item] : [])` 样板代码删光,改为 NavItem 加 featureFlag?: () => boolean | undefined 字段,加一个 applyFeatureFlags 递归 过滤(含 children)。语义统一为 `!== false`(宽容策略,undefined 时默认显示, 避免 public settings 未加载完成时菜单闪烁消失 — 对应用户反馈"刷新后菜单消失 要去保存设置才回来")。 - 集中声明 4 个 flag getter:flagChannelMonitor / flagPayment / flagOpsMonitoring / flagAdminPayment - 提取 buildSelfNavItems 复用用户端主菜单和管理员"我的账户"子菜单 - 未来新增开关:在统一位置加一个 flag getter + 给对应 NavItem 加字段 (不用再动渲染逻辑) bump 0.1.114.29
This commit is contained in:
@@ -199,6 +199,28 @@ interface NavItem {
|
||||
* does NOT navigate to its `path`. The `path` is purely a stable key.
|
||||
*/
|
||||
expandOnly?: boolean
|
||||
/**
|
||||
* 可选的功能开关 getter。返回 false 时菜单项被隐藏;返回 undefined/true 时显示。
|
||||
* 宽容策略(undefined → 显示)避免 public settings 未加载完成时菜单闪烁消失。
|
||||
* Getter 里访问的 reactive 来源(store / composable)会被 computed 自动追踪,
|
||||
* 开关切换时菜单自动更新。
|
||||
*/
|
||||
featureFlag?: () => boolean | undefined
|
||||
}
|
||||
|
||||
// applyFeatureFlags 递归过滤掉 featureFlag() === false 的节点(含子节点)。
|
||||
// 使用 `!== false` 宽容语义:undefined(设置未加载)或 true 都视为显示。
|
||||
function applyFeatureFlags(items: NavItem[]): NavItem[] {
|
||||
const out: NavItem[] = []
|
||||
for (const item of items) {
|
||||
if (item.featureFlag && item.featureFlag() === false) continue
|
||||
if (item.children) {
|
||||
out.push({ ...item, children: applyFeatureFlags(item.children) })
|
||||
} else {
|
||||
out.push(item)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -605,36 +627,27 @@ const ChevronDownIcon = {
|
||||
)
|
||||
}
|
||||
|
||||
// User navigation items (for regular users)
|
||||
const userNavItems = computed((): NavItem[] => {
|
||||
const items: NavItem[] = [
|
||||
{ path: '/dashboard', label: t('nav.dashboard'), icon: DashboardIcon },
|
||||
// 各个开关集中声明:所有菜单项引用这里的 getter,未来加新开关只需在此加一个常量。
|
||||
// getter 返回 false = 隐藏;undefined/true = 显示(宽容策略,避免 public settings 未加载闪烁)。
|
||||
const flagChannelMonitor = () => appStore.cachedPublicSettings?.channel_monitor_enabled
|
||||
const flagPayment = () => appStore.cachedPublicSettings?.payment_enabled
|
||||
const flagOpsMonitoring = () => adminSettingsStore.opsMonitoringEnabled
|
||||
const flagAdminPayment = () => adminSettingsStore.paymentEnabled
|
||||
|
||||
// buildSelfNavItems 构造用户自己的导航项(用户端主菜单和管理员的"我的账户"子菜单共享这组声明)。
|
||||
// withDashboard=true 时包含仪表盘(用户端),false 时不含(管理员的个人区已经有独立仪表盘入口)。
|
||||
function buildSelfNavItems(withDashboard: boolean): NavItem[] {
|
||||
const items: NavItem[] = []
|
||||
if (withDashboard) {
|
||||
items.push({ path: '/dashboard', label: t('nav.dashboard'), icon: DashboardIcon })
|
||||
}
|
||||
items.push(
|
||||
{ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon },
|
||||
{ path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true },
|
||||
...(appStore.cachedPublicSettings?.channel_monitor_enabled
|
||||
? [{ path: '/monitor', label: t('nav.channelStatus'), icon: SignalIcon }]
|
||||
: []),
|
||||
{ path: '/monitor', label: t('nav.channelStatus'), icon: SignalIcon, featureFlag: flagChannelMonitor },
|
||||
{ path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
|
||||
...(appStore.cachedPublicSettings?.payment_enabled
|
||||
? [
|
||||
{
|
||||
path: '/purchase',
|
||||
label: t('nav.buySubscription'),
|
||||
icon: RechargeSubscriptionIcon,
|
||||
hideInSimpleMode: true
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(appStore.cachedPublicSettings?.payment_enabled
|
||||
? [
|
||||
{
|
||||
path: '/orders',
|
||||
label: t('nav.myOrders'),
|
||||
icon: OrderListIcon,
|
||||
hideInSimpleMode: true
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ path: '/purchase', label: t('nav.buySubscription'), icon: RechargeSubscriptionIcon, hideInSimpleMode: true, featureFlag: flagPayment },
|
||||
{ path: '/orders', label: t('nav.myOrders'), icon: OrderListIcon, hideInSimpleMode: true, featureFlag: flagPayment },
|
||||
{ path: '/redeem', label: t('nav.redeem'), icon: GiftIcon, hideInSimpleMode: true },
|
||||
{ path: '/profile', label: t('nav.profile'), icon: UserIcon },
|
||||
...customMenuItemsForUser.value.map((item): NavItem => ({
|
||||
@@ -643,50 +656,21 @@ const userNavItems = computed((): NavItem[] => {
|
||||
icon: null,
|
||||
iconSvg: item.icon_svg,
|
||||
})),
|
||||
]
|
||||
return authStore.isSimpleMode ? items.filter(item => !item.hideInSimpleMode) : items
|
||||
})
|
||||
)
|
||||
return items
|
||||
}
|
||||
|
||||
// finalizeNav 合并三重过滤:featureFlag 过滤 + simple 模式过滤。
|
||||
function finalizeNav(items: NavItem[]): NavItem[] {
|
||||
const visible = applyFeatureFlags(items)
|
||||
return authStore.isSimpleMode ? visible.filter(item => !item.hideInSimpleMode) : visible
|
||||
}
|
||||
|
||||
// User navigation items (for regular users)
|
||||
const userNavItems = computed((): NavItem[] => finalizeNav(buildSelfNavItems(true)))
|
||||
|
||||
// Personal navigation items (for admin's "My Account" section, without Dashboard)
|
||||
const personalNavItems = computed((): NavItem[] => {
|
||||
const items: NavItem[] = [
|
||||
{ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon },
|
||||
{ path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true },
|
||||
...(appStore.cachedPublicSettings?.channel_monitor_enabled
|
||||
? [{ path: '/monitor', label: t('nav.channelStatus'), icon: SignalIcon }]
|
||||
: []),
|
||||
{ path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
|
||||
...(appStore.cachedPublicSettings?.payment_enabled
|
||||
? [
|
||||
{
|
||||
path: '/purchase',
|
||||
label: t('nav.buySubscription'),
|
||||
icon: RechargeSubscriptionIcon,
|
||||
hideInSimpleMode: true
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(appStore.cachedPublicSettings?.payment_enabled
|
||||
? [
|
||||
{
|
||||
path: '/orders',
|
||||
label: t('nav.myOrders'),
|
||||
icon: OrderListIcon,
|
||||
hideInSimpleMode: true
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ path: '/redeem', label: t('nav.redeem'), icon: GiftIcon, hideInSimpleMode: true },
|
||||
{ path: '/profile', label: t('nav.profile'), icon: UserIcon },
|
||||
...customMenuItemsForUser.value.map((item): NavItem => ({
|
||||
path: `/custom/${item.id}`,
|
||||
label: item.label,
|
||||
icon: null,
|
||||
iconSvg: item.icon_svg,
|
||||
})),
|
||||
]
|
||||
return authStore.isSimpleMode ? items.filter(item => !item.hideInSimpleMode) : items
|
||||
})
|
||||
const personalNavItems = computed((): NavItem[] => finalizeNav(buildSelfNavItems(false)))
|
||||
|
||||
// Custom menu items filtered by visibility
|
||||
const customMenuItemsForUser = computed(() => {
|
||||
@@ -706,9 +690,7 @@ const customMenuItemsForAdmin = computed(() => {
|
||||
const adminNavItems = computed((): NavItem[] => {
|
||||
const baseItems: NavItem[] = [
|
||||
{ path: '/admin/dashboard', label: t('nav.dashboard'), icon: DashboardIcon },
|
||||
...(adminSettingsStore.opsMonitoringEnabled
|
||||
? [{ path: '/admin/ops', label: t('nav.ops'), icon: ChartIcon }]
|
||||
: []),
|
||||
{ path: '/admin/ops', label: t('nav.ops'), icon: ChartIcon, featureFlag: flagOpsMonitoring },
|
||||
{ path: '/admin/users', label: t('nav.users'), icon: UsersIcon, hideInSimpleMode: true },
|
||||
{ path: '/admin/groups', label: t('nav.groups'), icon: FolderIcon, hideInSimpleMode: true },
|
||||
{
|
||||
@@ -719,9 +701,7 @@ const adminNavItems = computed((): NavItem[] => {
|
||||
expandOnly: true,
|
||||
children: [
|
||||
{ path: '/admin/channels/pricing', label: t('nav.channelPricing'), icon: PriceTagIcon },
|
||||
...(appStore.cachedPublicSettings?.channel_monitor_enabled
|
||||
? [{ path: '/admin/channels/monitor', label: t('nav.channelMonitor'), icon: SignalIcon }]
|
||||
: []),
|
||||
{ path: '/admin/channels/monitor', label: t('nav.channelMonitor'), icon: SignalIcon, featureFlag: flagChannelMonitor },
|
||||
],
|
||||
},
|
||||
{ path: '/admin/subscriptions', label: t('nav.subscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
|
||||
@@ -730,43 +710,40 @@ const adminNavItems = computed((): NavItem[] => {
|
||||
{ path: '/admin/proxies', label: t('nav.proxies'), icon: ServerIcon },
|
||||
{ path: '/admin/redeem', label: t('nav.redeemCodes'), icon: TicketIcon, hideInSimpleMode: true },
|
||||
{ path: '/admin/promo-codes', label: t('nav.promoCodes'), icon: GiftIcon, hideInSimpleMode: true },
|
||||
...(adminSettingsStore.paymentEnabled
|
||||
? [
|
||||
{
|
||||
path: '/admin/orders',
|
||||
label: t('nav.orderManagement'),
|
||||
icon: OrderIcon,
|
||||
hideInSimpleMode: true,
|
||||
expandOnly: true,
|
||||
children: [
|
||||
{ path: '/admin/orders/dashboard', label: t('nav.paymentDashboard'), icon: ChartIcon },
|
||||
{ path: '/admin/orders', label: t('nav.orderManagement'), icon: OrderIcon },
|
||||
{ path: '/admin/orders/plans', label: t('nav.paymentPlans'), icon: CreditCardIcon },
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
path: '/admin/orders',
|
||||
label: t('nav.orderManagement'),
|
||||
icon: OrderIcon,
|
||||
hideInSimpleMode: true,
|
||||
expandOnly: true,
|
||||
featureFlag: flagAdminPayment,
|
||||
children: [
|
||||
{ path: '/admin/orders/dashboard', label: t('nav.paymentDashboard'), icon: ChartIcon },
|
||||
{ path: '/admin/orders', label: t('nav.orderManagement'), icon: OrderIcon },
|
||||
{ path: '/admin/orders/plans', label: t('nav.paymentPlans'), icon: CreditCardIcon },
|
||||
],
|
||||
},
|
||||
{ path: '/admin/usage', label: t('nav.usage'), icon: ChartIcon }
|
||||
]
|
||||
|
||||
const visible = applyFeatureFlags(baseItems)
|
||||
|
||||
// 简单模式下,在系统设置前插入 API密钥
|
||||
if (authStore.isSimpleMode) {
|
||||
const filtered = baseItems.filter(item => !item.hideInSimpleMode)
|
||||
const filtered = visible.filter(item => !item.hideInSimpleMode)
|
||||
filtered.push({ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon })
|
||||
filtered.push({ path: '/admin/settings', label: t('nav.settings'), icon: CogIcon })
|
||||
// Add admin custom menu items after settings
|
||||
for (const cm of customMenuItemsForAdmin.value) {
|
||||
filtered.push({ path: `/custom/${cm.id}`, label: cm.label, icon: null, iconSvg: cm.icon_svg })
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
baseItems.push({ path: '/admin/settings', label: t('nav.settings'), icon: CogIcon })
|
||||
// Add admin custom menu items after settings
|
||||
visible.push({ path: '/admin/settings', label: t('nav.settings'), icon: CogIcon })
|
||||
for (const cm of customMenuItemsForAdmin.value) {
|
||||
baseItems.push({ path: `/custom/${cm.id}`, label: cm.label, icon: null, iconSvg: cm.icon_svg })
|
||||
visible.push({ path: `/custom/${cm.id}`, label: cm.label, icon: null, iconSvg: cm.icon_svg })
|
||||
}
|
||||
return baseItems
|
||||
return visible
|
||||
})
|
||||
|
||||
function toggleSidebar() {
|
||||
|
||||
Reference in New Issue
Block a user