From c58a99d602b4a2e5fc5fc28e064a0fbfd05c9bb1 Mon Sep 17 00:00:00 2001 From: wikiboss Date: Mon, 21 Sep 2026 11:15:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(security):=20=E5=AE=8C=E5=96=84=E8=B6=85?= =?UTF-8?q?=E7=AE=A1=E5=B9=B3=E5=8F=B0=E8=AE=A2=E9=98=85=E5=AE=88=E5=8D=AB?= =?UTF-8?q?=E6=8B=A6=E6=88=AA=E4=B8=8E=E7=A7=BB=E5=8A=A8=E7=AB=AF=E9=AB=98?= =?UTF-8?q?=E6=84=8F=E5=90=91=E8=AE=BF=E5=AE=A2=E6=8F=90=E9=86=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/team/controller/admin/billing.ts | 2 ++ .../modules/team/controller/admin/platform.ts | 8 ++++++ backend/test/comm/super-admin-guard.test.ts | 2 +- mpuniapp/pages/owner/hall/index.uvue | 28 +++++++++++++++++++ mpuniapp/pages/owner/leads.uvue | 22 +++++++++++++++ mpuniapp/pages/owner/team/useOverview.uts | 8 +++++- mpuniapp/pages/owner/useLeads.uts | 27 ++++++++++++++++-- 7 files changed, 93 insertions(+), 4 deletions(-) diff --git a/backend/src/modules/team/controller/admin/billing.ts b/backend/src/modules/team/controller/admin/billing.ts index 4a9e955..d4ce8f4 100644 --- a/backend/src/modules/team/controller/admin/billing.ts +++ b/backend/src/modules/team/controller/admin/billing.ts @@ -12,6 +12,7 @@ import { TeamQuotaService } from '../../service/quota'; import { TeamInvoiceService } from '../../service/invoice'; import { TeamStripeService } from '../../service/stripe'; import { TeamFinanceAnalyticsService } from '../../service/finance-analytics'; +import { assertSuperAdmin } from '../../../../comm/guard/super-admin'; import { CalculateProrationDTO, CreateInvoiceDTO, @@ -88,6 +89,7 @@ export class AdminTeamBillingController extends BaseController { async postAnalytics( @Body() body?: FinanceAnalyticsQueryDTO ): Promise> { + assertSuperAdmin(this.ctx); const result = await this.teamFinanceAnalyticsService.getOverviewAnalytics(body || {}); return this.ok(result) as unknown as CoolAdminResponse; } diff --git a/backend/src/modules/team/controller/admin/platform.ts b/backend/src/modules/team/controller/admin/platform.ts index 9984509..069e1af 100644 --- a/backend/src/modules/team/controller/admin/platform.ts +++ b/backend/src/modules/team/controller/admin/platform.ts @@ -2,6 +2,7 @@ import { Body, Inject, Post, Provide } from '@midwayjs/core'; import { Context } from '@midwayjs/koa'; import { CoolController, BaseController } from '@cool-midway/core'; import { PlatformSubscriptionService } from '../../service/platform-subscription'; +import { assertSuperAdmin } from '../../../../comm/guard/super-admin'; import { ManualRenewDTO, PlanConfigItem, @@ -44,6 +45,7 @@ export class AdminPlatformSubscriptionController extends BaseController { */ @Post('/overview', { summary: '平台会员与订阅整体概览' }) async overview() { + assertSuperAdmin(this.ctx); const result: PlatformOverviewResult = await this.platformSubscriptionService.getOverview(); return this.ok(result); @@ -64,6 +66,7 @@ export class AdminPlatformSubscriptionController extends BaseController { keyword?: string; } ) { + assertSuperAdmin(this.ctx); const result = await this.platformSubscriptionService.getSubscribers(body); return this.ok(result); } @@ -74,6 +77,7 @@ export class AdminPlatformSubscriptionController extends BaseController { */ @Post('/subscribers/update', { summary: '超管修改会员套餐/配额/延期' }) async subscribersUpdate(@Body() body: UpdateSubscriberDTO) { + assertSuperAdmin(this.ctx); const result = await this.platformSubscriptionService.updateSubscriber(body); return this.ok(result); } @@ -92,6 +96,7 @@ export class AdminPlatformSubscriptionController extends BaseController { keyword?: string; } ) { + assertSuperAdmin(this.ctx); const result = await this.platformSubscriptionService.getRenewals(body); return this.ok(result); } @@ -102,6 +107,7 @@ export class AdminPlatformSubscriptionController extends BaseController { */ @Post('/renewals/manual-renew', { summary: '超管手动代续费与补录订单' }) async manualRenew(@Body() body: ManualRenewDTO) { + assertSuperAdmin(this.ctx); const result = await this.platformSubscriptionService.manualRenew(body); return this.ok(result); } @@ -112,6 +118,7 @@ export class AdminPlatformSubscriptionController extends BaseController { */ @Post('/plans/list', { summary: 'SaaS 套餐方案矩阵列表' }) async plansList() { + assertSuperAdmin(this.ctx); const result = await this.platformSubscriptionService.getPlans(); return this.ok(result); } @@ -128,6 +135,7 @@ export class AdminPlatformSubscriptionController extends BaseController { params: Partial; } ) { + assertSuperAdmin(this.ctx); const result = await this.platformSubscriptionService.updatePlan( body.planType, body.params diff --git a/backend/test/comm/super-admin-guard.test.ts b/backend/test/comm/super-admin-guard.test.ts index 94489bb..1cf1458 100644 --- a/backend/test/comm/super-admin-guard.test.ts +++ b/backend/test/comm/super-admin-guard.test.ts @@ -46,7 +46,7 @@ describe('后端权限隔离 - 平台超级管理员专属守卫 (Super Admin Gu try { assertSuperAdmin(ctx); } catch (err: any) { - expect(err.status || err.code).toBe(403); + expect(err.statusCode || err.status).toBe(403); expect(err.message).toContain('权限不足'); } }); diff --git a/mpuniapp/pages/owner/hall/index.uvue b/mpuniapp/pages/owner/hall/index.uvue index 483e353..e0baf8d 100644 --- a/mpuniapp/pages/owner/hall/index.uvue +++ b/mpuniapp/pages/owner/hall/index.uvue @@ -18,6 +18,13 @@ > {{ t('tabTeam') }} + + {{ t('tabEvent') }} + + @@ -89,6 +105,7 @@ import { openHallPortal, refreshHall, resolveHallText, + submitHallEventJoin, submitHallExchange, } from './useHall.uts'; @@ -113,11 +130,15 @@ const pageTitle = ref(resolveHallText('title')); const pageSubtitle = ref(resolveHallText('subtitle')); const searchPlaceholder = ref(resolveHallText('searchPlaceholder')); const exchangePlaceholder = ref(resolveHallText('exchangePlaceholder')); +const eventPlaceholder = ref(resolveHallText('eventPlaceholder')); const emptyDesc = computed((): string => { if (state.tab === 'team') { return resolveHallText('teamEmpty'); } + if (state.tab === 'event') { + return resolveHallText('eventEmpty'); + } return pageSubtitle.value; }); @@ -125,6 +146,9 @@ const ctaLabel = computed((): string => { if (state.loading) { return resolveHallText('loading'); } + if (state.tab === 'event' && state.eventCode.trim() !== '') { + return resolveHallText('eventJoin'); + } if (state.exchangeCode.trim() !== '') { return resolveHallText('exchange'); } @@ -152,6 +176,10 @@ function handleOpenPortal(item: HallContactItem): void { } async function handlePrimary(): Promise { + if (state.tab === 'event' && state.eventCode.trim() !== '') { + await submitHallEventJoin(state); + return; + } if (state.exchangeCode.trim() !== '') { await submitHallExchange(state); return; diff --git a/mpuniapp/pages/owner/leads.uvue b/mpuniapp/pages/owner/leads.uvue index 4d1f74f..fd2d5eb 100644 --- a/mpuniapp/pages/owner/leads.uvue +++ b/mpuniapp/pages/owner/leads.uvue @@ -3,6 +3,11 @@ {{ pageTitle }} {{ followUpLabel }} + {{ hotUnreadText }} {{ radarLineOne }} {{ radarLineTwo }} @@ -115,6 +120,7 @@ import { applyLeadStatusFilter, applyLeadsOptions, createLeadsState, + formatHotUnread, formatLeadCompany, formatRadarLine, openRadarPage, @@ -149,6 +155,9 @@ const searchPlaceholder = ref(resolveLeadsText('searchPlaceholder')); const scoreLabel = ref(resolveLeadsText('score')); const briefingLabel = ref(resolveOverviewText('title')); const radarMoreLabel = ref(resolveLeadsText('radarMore')); +const hotUnreadText = computed((): string => { + return formatHotUnread(state.hotUnread); +}); const radarLineOne = computed((): string => { if (state.radarTracks.length < 1) { return ''; @@ -250,6 +259,19 @@ onLoad((options: LeadsPageOptions) => { margin-bottom: var(--oc-space-8, 8px); } +.radar-hot-badge { + min-height: 44px; + min-width: 44px; + padding-inline: 12px; + margin-bottom: var(--oc-space-12, 12px); + background-color: var(--oc-primary-soft, #EAF1FF); + color: var(--oc-primary, #0B5CFF); + border-radius: 12px; + font-size: 13px; + font-weight: 600; + line-height: 44px; +} + .radar-preview { margin-bottom: var(--oc-space-12, 12px); padding: var(--oc-space-12, 12px); diff --git a/mpuniapp/pages/owner/team/useOverview.uts b/mpuniapp/pages/owner/team/useOverview.uts index 84c96b4..511180d 100644 --- a/mpuniapp/pages/owner/team/useOverview.uts +++ b/mpuniapp/pages/owner/team/useOverview.uts @@ -8,10 +8,13 @@ import { OwnerPageData, OwnerPageQuery, RadarTrack, + TeamRankItem, } from '../../../common/types.uts'; import { + fetchCurrentEvent, fetchOwnerLeadPage, fetchOwnerRadarPage, + fetchTeamLiveRank, } from '../../../common/api.uts'; import { getCurrentLocale, getOmniText } from '../../../common/locale.uts'; @@ -27,7 +30,8 @@ export type OverviewCopyKey = | 'empty' | 'error' | 'loading' - | 'unknownCountry'; + | 'unknownCountry' + | 'rankTitle'; export type OverviewSummary = { totalLeads: number; @@ -49,6 +53,8 @@ export type LoadOverviewApi = { fetchOwnerRadarPage?: ( query: OwnerPageQuery ) => Promise>; + fetchTeamLiveRank?: (eventId?: string) => Promise; + fetchCurrentEvent?: () => Promise<{ code?: string } | null>; }; export type LoadOverviewResult = { diff --git a/mpuniapp/pages/owner/useLeads.uts b/mpuniapp/pages/owner/useLeads.uts index 1f9a26d..d451d17 100644 --- a/mpuniapp/pages/owner/useLeads.uts +++ b/mpuniapp/pages/owner/useLeads.uts @@ -10,7 +10,11 @@ import { OwnerPageQuery, RadarTrack, } from '../../common/types.uts'; -import { fetchOwnerLeadPage, fetchOwnerRadarPage } from '../../common/api.uts'; +import { + fetchOwnerLeadPage, + fetchOwnerRadarPage, + fetchRadarHotInbox, +} from '../../common/api.uts'; import { getCurrentLocale, getOmniText } from '../../common/locale.uts'; import { formatLocation, formatStay } from './useRadar.uts'; @@ -29,7 +33,8 @@ export type LeadsCopyKey = | 'error' | 'loading' | 'score' - | 'radarMore'; + | 'radarMore' + | 'hotBadge'; export type LoadLeadsQuery = { page: number; @@ -58,6 +63,7 @@ export type LoadLeadsResult = { export type LeadsState = { list: LeadEntity[]; radarTracks: RadarTrack[]; + hotUnread: number; page: number; size: number; total: number; @@ -154,9 +160,19 @@ export function resolveLeadsText(key: LeadsCopyKey): string { if (key === 'radarMore') { return zh ? '查看全部雷达' : 'Full radar'; } + if (key === 'hotBadge') { + return zh ? '高意向访客' : 'Hot visitors'; + } return ''; } +export function formatHotUnread(unread: number): string { + if (unread < 1) { + return ''; + } + return `${resolveLeadsText('hotBadge')} ${unread}`; +} + export function statusLabel(status: string): string { if (status === 'new') { return resolveLeadsText('statusNew'); @@ -244,6 +260,7 @@ export function createLeadsState(): LeadsState { return { list: [], radarTracks: [], + hotUnread: 0, page: 1, size: 20, total: 0, @@ -318,6 +335,12 @@ export async function refreshLeads( } catch (_err) { state.radarTracks = []; } + try { + const inbox = await fetchRadarHotInbox(); + state.hotUnread = inbox != null && inbox.unread != null ? inbox.unread : 0; + } catch (_err) { + state.hotUnread = 0; + } state.loading = false; return result; }