feat(security): 完善超管平台订阅守卫拦截与移动端高意向访客提醒

这个提交包含在:
wikiboss
2026-09-21 11:15:59 +08:00
父节点 654f317c59
当前提交 c58a99d602
共有 7 个文件被更改,包括 93 次插入 和 4 次删除

查看文件

@@ -12,6 +12,7 @@ import { TeamQuotaService } from '../../service/quota';
import { TeamInvoiceService } from '../../service/invoice'; import { TeamInvoiceService } from '../../service/invoice';
import { TeamStripeService } from '../../service/stripe'; import { TeamStripeService } from '../../service/stripe';
import { TeamFinanceAnalyticsService } from '../../service/finance-analytics'; import { TeamFinanceAnalyticsService } from '../../service/finance-analytics';
import { assertSuperAdmin } from '../../../../comm/guard/super-admin';
import { import {
CalculateProrationDTO, CalculateProrationDTO,
CreateInvoiceDTO, CreateInvoiceDTO,
@@ -88,6 +89,7 @@ export class AdminTeamBillingController extends BaseController {
async postAnalytics( async postAnalytics(
@Body() body?: FinanceAnalyticsQueryDTO @Body() body?: FinanceAnalyticsQueryDTO
): Promise<CoolAdminResponse<FinanceAnalyticsResult>> { ): Promise<CoolAdminResponse<FinanceAnalyticsResult>> {
assertSuperAdmin(this.ctx);
const result = await this.teamFinanceAnalyticsService.getOverviewAnalytics(body || {}); const result = await this.teamFinanceAnalyticsService.getOverviewAnalytics(body || {});
return this.ok(result) as unknown as CoolAdminResponse<FinanceAnalyticsResult>; return this.ok(result) as unknown as CoolAdminResponse<FinanceAnalyticsResult>;
} }

查看文件

@@ -2,6 +2,7 @@ import { Body, Inject, Post, Provide } from '@midwayjs/core';
import { Context } from '@midwayjs/koa'; import { Context } from '@midwayjs/koa';
import { CoolController, BaseController } from '@cool-midway/core'; import { CoolController, BaseController } from '@cool-midway/core';
import { PlatformSubscriptionService } from '../../service/platform-subscription'; import { PlatformSubscriptionService } from '../../service/platform-subscription';
import { assertSuperAdmin } from '../../../../comm/guard/super-admin';
import { import {
ManualRenewDTO, ManualRenewDTO,
PlanConfigItem, PlanConfigItem,
@@ -44,6 +45,7 @@ export class AdminPlatformSubscriptionController extends BaseController {
*/ */
@Post('/overview', { summary: '平台会员与订阅整体概览' }) @Post('/overview', { summary: '平台会员与订阅整体概览' })
async overview() { async overview() {
assertSuperAdmin(this.ctx);
const result: PlatformOverviewResult = const result: PlatformOverviewResult =
await this.platformSubscriptionService.getOverview(); await this.platformSubscriptionService.getOverview();
return this.ok(result); return this.ok(result);
@@ -64,6 +66,7 @@ export class AdminPlatformSubscriptionController extends BaseController {
keyword?: string; keyword?: string;
} }
) { ) {
assertSuperAdmin(this.ctx);
const result = await this.platformSubscriptionService.getSubscribers(body); const result = await this.platformSubscriptionService.getSubscribers(body);
return this.ok(result); return this.ok(result);
} }
@@ -74,6 +77,7 @@ export class AdminPlatformSubscriptionController extends BaseController {
*/ */
@Post('/subscribers/update', { summary: '超管修改会员套餐/配额/延期' }) @Post('/subscribers/update', { summary: '超管修改会员套餐/配额/延期' })
async subscribersUpdate(@Body() body: UpdateSubscriberDTO) { async subscribersUpdate(@Body() body: UpdateSubscriberDTO) {
assertSuperAdmin(this.ctx);
const result = await this.platformSubscriptionService.updateSubscriber(body); const result = await this.platformSubscriptionService.updateSubscriber(body);
return this.ok(result); return this.ok(result);
} }
@@ -92,6 +96,7 @@ export class AdminPlatformSubscriptionController extends BaseController {
keyword?: string; keyword?: string;
} }
) { ) {
assertSuperAdmin(this.ctx);
const result = await this.platformSubscriptionService.getRenewals(body); const result = await this.platformSubscriptionService.getRenewals(body);
return this.ok(result); return this.ok(result);
} }
@@ -102,6 +107,7 @@ export class AdminPlatformSubscriptionController extends BaseController {
*/ */
@Post('/renewals/manual-renew', { summary: '超管手动代续费与补录订单' }) @Post('/renewals/manual-renew', { summary: '超管手动代续费与补录订单' })
async manualRenew(@Body() body: ManualRenewDTO) { async manualRenew(@Body() body: ManualRenewDTO) {
assertSuperAdmin(this.ctx);
const result = await this.platformSubscriptionService.manualRenew(body); const result = await this.platformSubscriptionService.manualRenew(body);
return this.ok(result); return this.ok(result);
} }
@@ -112,6 +118,7 @@ export class AdminPlatformSubscriptionController extends BaseController {
*/ */
@Post('/plans/list', { summary: 'SaaS 套餐方案矩阵列表' }) @Post('/plans/list', { summary: 'SaaS 套餐方案矩阵列表' })
async plansList() { async plansList() {
assertSuperAdmin(this.ctx);
const result = await this.platformSubscriptionService.getPlans(); const result = await this.platformSubscriptionService.getPlans();
return this.ok(result); return this.ok(result);
} }
@@ -128,6 +135,7 @@ export class AdminPlatformSubscriptionController extends BaseController {
params: Partial<PlanConfigItem>; params: Partial<PlanConfigItem>;
} }
) { ) {
assertSuperAdmin(this.ctx);
const result = await this.platformSubscriptionService.updatePlan( const result = await this.platformSubscriptionService.updatePlan(
body.planType, body.planType,
body.params body.params

查看文件

@@ -46,7 +46,7 @@ describe('后端权限隔离 - 平台超级管理员专属守卫 (Super Admin Gu
try { try {
assertSuperAdmin(ctx); assertSuperAdmin(ctx);
} catch (err: any) { } catch (err: any) {
expect(err.status || err.code).toBe(403); expect(err.statusCode || err.status).toBe(403);
expect(err.message).toContain('权限不足'); expect(err.message).toContain('权限不足');
} }
}); });

查看文件

@@ -18,6 +18,13 @@
> >
<text class="hall-tab-text">{{ t('tabTeam') }}</text> <text class="hall-tab-text">{{ t('tabTeam') }}</text>
</view> </view>
<view
class="hall-tab"
:class="{ 'hall-tab-active': state.tab === 'event' }"
@click="handleTab('event')"
>
<text class="hall-tab-text">{{ t('tabEvent') }}</text>
</view>
</view> </view>
<input <input
class="hall-search" class="hall-search"
@@ -35,6 +42,15 @@
:placeholder="exchangePlaceholder" :placeholder="exchangePlaceholder"
:disabled="state.loading" :disabled="state.loading"
/> />
<input
v-if="state.tab === 'event'"
class="hall-search"
type="text"
maxlength="6"
v-model="state.eventCode"
:placeholder="eventPlaceholder"
:disabled="state.loading"
/>
</view> </view>
<view v-if="state.loading" class="hall-skeleton"> <view v-if="state.loading" class="hall-skeleton">
@@ -89,6 +105,7 @@ import {
openHallPortal, openHallPortal,
refreshHall, refreshHall,
resolveHallText, resolveHallText,
submitHallEventJoin,
submitHallExchange, submitHallExchange,
} from './useHall.uts'; } from './useHall.uts';
@@ -113,11 +130,15 @@ const pageTitle = ref(resolveHallText('title'));
const pageSubtitle = ref(resolveHallText('subtitle')); const pageSubtitle = ref(resolveHallText('subtitle'));
const searchPlaceholder = ref(resolveHallText('searchPlaceholder')); const searchPlaceholder = ref(resolveHallText('searchPlaceholder'));
const exchangePlaceholder = ref(resolveHallText('exchangePlaceholder')); const exchangePlaceholder = ref(resolveHallText('exchangePlaceholder'));
const eventPlaceholder = ref(resolveHallText('eventPlaceholder'));
const emptyDesc = computed((): string => { const emptyDesc = computed((): string => {
if (state.tab === 'team') { if (state.tab === 'team') {
return resolveHallText('teamEmpty'); return resolveHallText('teamEmpty');
} }
if (state.tab === 'event') {
return resolveHallText('eventEmpty');
}
return pageSubtitle.value; return pageSubtitle.value;
}); });
@@ -125,6 +146,9 @@ const ctaLabel = computed((): string => {
if (state.loading) { if (state.loading) {
return resolveHallText('loading'); return resolveHallText('loading');
} }
if (state.tab === 'event' && state.eventCode.trim() !== '') {
return resolveHallText('eventJoin');
}
if (state.exchangeCode.trim() !== '') { if (state.exchangeCode.trim() !== '') {
return resolveHallText('exchange'); return resolveHallText('exchange');
} }
@@ -152,6 +176,10 @@ function handleOpenPortal(item: HallContactItem): void {
} }
async function handlePrimary(): Promise<void> { async function handlePrimary(): Promise<void> {
if (state.tab === 'event' && state.eventCode.trim() !== '') {
await submitHallEventJoin(state);
return;
}
if (state.exchangeCode.trim() !== '') { if (state.exchangeCode.trim() !== '') {
await submitHallExchange(state); await submitHallExchange(state);
return; return;

查看文件

@@ -3,6 +3,11 @@
<view class="leads-header"> <view class="leads-header">
<text class="leads-title">{{ pageTitle }}</text> <text class="leads-title">{{ pageTitle }}</text>
<text class="leads-subtitle">{{ followUpLabel }}</text> <text class="leads-subtitle">{{ followUpLabel }}</text>
<text
v-if="hotUnreadText !== ''"
class="radar-hot-badge"
@click="openRadar"
>{{ hotUnreadText }}</text>
<view v-if="radarLineOne !== ''" class="radar-preview"> <view v-if="radarLineOne !== ''" class="radar-preview">
<text class="radar-preview-text">{{ radarLineOne }}</text> <text class="radar-preview-text">{{ radarLineOne }}</text>
<text v-if="radarLineTwo !== ''" class="radar-preview-text">{{ radarLineTwo }}</text> <text v-if="radarLineTwo !== ''" class="radar-preview-text">{{ radarLineTwo }}</text>
@@ -115,6 +120,7 @@ import {
applyLeadStatusFilter, applyLeadStatusFilter,
applyLeadsOptions, applyLeadsOptions,
createLeadsState, createLeadsState,
formatHotUnread,
formatLeadCompany, formatLeadCompany,
formatRadarLine, formatRadarLine,
openRadarPage, openRadarPage,
@@ -149,6 +155,9 @@ const searchPlaceholder = ref(resolveLeadsText('searchPlaceholder'));
const scoreLabel = ref(resolveLeadsText('score')); const scoreLabel = ref(resolveLeadsText('score'));
const briefingLabel = ref(resolveOverviewText('title')); const briefingLabel = ref(resolveOverviewText('title'));
const radarMoreLabel = ref(resolveLeadsText('radarMore')); const radarMoreLabel = ref(resolveLeadsText('radarMore'));
const hotUnreadText = computed((): string => {
return formatHotUnread(state.hotUnread);
});
const radarLineOne = computed((): string => { const radarLineOne = computed((): string => {
if (state.radarTracks.length < 1) { if (state.radarTracks.length < 1) {
return ''; return '';
@@ -250,6 +259,19 @@ onLoad((options: LeadsPageOptions) => {
margin-bottom: var(--oc-space-8, 8px); 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 { .radar-preview {
margin-bottom: var(--oc-space-12, 12px); margin-bottom: var(--oc-space-12, 12px);
padding: var(--oc-space-12, 12px); padding: var(--oc-space-12, 12px);

查看文件

@@ -8,10 +8,13 @@ import {
OwnerPageData, OwnerPageData,
OwnerPageQuery, OwnerPageQuery,
RadarTrack, RadarTrack,
TeamRankItem,
} from '../../../common/types.uts'; } from '../../../common/types.uts';
import { import {
fetchCurrentEvent,
fetchOwnerLeadPage, fetchOwnerLeadPage,
fetchOwnerRadarPage, fetchOwnerRadarPage,
fetchTeamLiveRank,
} from '../../../common/api.uts'; } from '../../../common/api.uts';
import { getCurrentLocale, getOmniText } from '../../../common/locale.uts'; import { getCurrentLocale, getOmniText } from '../../../common/locale.uts';
@@ -27,7 +30,8 @@ export type OverviewCopyKey =
| 'empty' | 'empty'
| 'error' | 'error'
| 'loading' | 'loading'
| 'unknownCountry'; | 'unknownCountry'
| 'rankTitle';
export type OverviewSummary = { export type OverviewSummary = {
totalLeads: number; totalLeads: number;
@@ -49,6 +53,8 @@ export type LoadOverviewApi = {
fetchOwnerRadarPage?: ( fetchOwnerRadarPage?: (
query: OwnerPageQuery query: OwnerPageQuery
) => Promise<OwnerPageData<RadarTrack>>; ) => Promise<OwnerPageData<RadarTrack>>;
fetchTeamLiveRank?: (eventId?: string) => Promise<TeamRankItem[]>;
fetchCurrentEvent?: () => Promise<{ code?: string } | null>;
}; };
export type LoadOverviewResult = { export type LoadOverviewResult = {

查看文件

@@ -10,7 +10,11 @@ import {
OwnerPageQuery, OwnerPageQuery,
RadarTrack, RadarTrack,
} from '../../common/types.uts'; } 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 { getCurrentLocale, getOmniText } from '../../common/locale.uts';
import { formatLocation, formatStay } from './useRadar.uts'; import { formatLocation, formatStay } from './useRadar.uts';
@@ -29,7 +33,8 @@ export type LeadsCopyKey =
| 'error' | 'error'
| 'loading' | 'loading'
| 'score' | 'score'
| 'radarMore'; | 'radarMore'
| 'hotBadge';
export type LoadLeadsQuery = { export type LoadLeadsQuery = {
page: number; page: number;
@@ -58,6 +63,7 @@ export type LoadLeadsResult = {
export type LeadsState = { export type LeadsState = {
list: LeadEntity[]; list: LeadEntity[];
radarTracks: RadarTrack[]; radarTracks: RadarTrack[];
hotUnread: number;
page: number; page: number;
size: number; size: number;
total: number; total: number;
@@ -154,9 +160,19 @@ export function resolveLeadsText(key: LeadsCopyKey): string {
if (key === 'radarMore') { if (key === 'radarMore') {
return zh ? '查看全部雷达' : 'Full radar'; return zh ? '查看全部雷达' : 'Full radar';
} }
if (key === 'hotBadge') {
return zh ? '高意向访客' : 'Hot visitors';
}
return ''; return '';
} }
export function formatHotUnread(unread: number): string {
if (unread < 1) {
return '';
}
return `${resolveLeadsText('hotBadge')} ${unread}`;
}
export function statusLabel(status: string): string { export function statusLabel(status: string): string {
if (status === 'new') { if (status === 'new') {
return resolveLeadsText('statusNew'); return resolveLeadsText('statusNew');
@@ -244,6 +260,7 @@ export function createLeadsState(): LeadsState {
return { return {
list: [], list: [],
radarTracks: [], radarTracks: [],
hotUnread: 0,
page: 1, page: 1,
size: 20, size: 20,
total: 0, total: 0,
@@ -318,6 +335,12 @@ export async function refreshLeads(
} catch (_err) { } catch (_err) {
state.radarTracks = []; 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; state.loading = false;
return result; return result;
} }