chore(deploy): finalize 0309 SAE rollout updates

Sync deployment documentation to the final successful SAE state and clear pending deployment checklist items. Include backend/frontend/R hardening and diagnostics improvements required for stable production behavior.

Made-with: Cursor
This commit is contained in:
2026-03-09 22:27:11 +08:00
parent d30bf95815
commit 971e903acf
23 changed files with 810 additions and 180 deletions

View File

@@ -34,7 +34,7 @@ const PRIMARY_COLOR = '#10b981'
* - 权限检查SUPER_ADMIN / PROMPT_ENGINEER
*/
const AdminLayout = () => {
const { isAuthenticated, isLoading, user, logout } = useAuth()
const { isAuthenticated, isLoading, user, logout, hasPermission } = useAuth()
const location = useLocation()
const navigate = useNavigate()
const [collapsed, setCollapsed] = useState(false)
@@ -55,8 +55,9 @@ const AdminLayout = () => {
// 权限检查:可进入管理端的角色
const adminAllowedRoles = ['SUPER_ADMIN', 'PROMPT_ENGINEER', 'IIT_OPERATOR', 'PHARMA_ADMIN', 'HOSPITAL_ADMIN']
const hasUserOps = hasPermission('ops:user-ops')
const userRole = user?.role || ''
if (!adminAllowedRoles.includes(userRole)) {
if (!adminAllowedRoles.includes(userRole) && !hasUserOps) {
return (
<div className="h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
@@ -133,6 +134,10 @@ const AdminLayout = () => {
} else if (userRole === 'PHARMA_ADMIN' || userRole === 'HOSPITAL_ADMIN') {
items.push(projectGroup)
}
if (hasUserOps && userRole !== 'SUPER_ADMIN') {
if (items.length > 0) items.push({ type: 'divider' })
items.push(bizGroup)
}
return items
})()

View File

@@ -11,6 +11,7 @@ import {
import type { MenuProps } from 'antd'
import { MODULES } from '../modules/moduleRegistry'
import { useAuth } from '../auth'
import apiClient from '../../common/api/axios'
/**
* 顶部导航栏组件
@@ -25,7 +26,7 @@ import { useAuth } from '../auth'
const TopNavigation = () => {
const navigate = useNavigate()
const location = useLocation()
const { user, logout: authLogout, hasModule } = useAuth()
const { user, logout: authLogout, hasModule, hasPermission } = useAuth()
// 根据用户模块权限过滤可显示的模块
const availableModules = MODULES.filter(module => {
@@ -40,7 +41,20 @@ const TopNavigation = () => {
// 检查用户权限,决定显示哪些切换入口
const userRole = user?.role || ''
const canAccessAdmin = ['SUPER_ADMIN', 'PROMPT_ENGINEER', 'IIT_OPERATOR'].includes(userRole)
const canAccessAdmin = ['SUPER_ADMIN', 'PROMPT_ENGINEER', 'IIT_OPERATOR'].includes(userRole) || hasPermission('ops:user-ops')
const reportTopNavClick = async (moduleName: string): Promise<void> => {
try {
await apiClient.post('/api/v1/auth/activity', {
module: 'SYSTEM',
feature: '顶部导航点击',
action: 'CLICK',
info: moduleName,
})
} catch {
// 埋点失败不影响导航
}
}
const canAccessOrg = ['HOSPITAL_ADMIN', 'PHARMA_ADMIN', 'DEPARTMENT_ADMIN', 'SUPER_ADMIN'].includes(userRole)
// 用户菜单 - 动态构建
@@ -121,6 +135,7 @@ const TopNavigation = () => {
<div
key={module.id}
onClick={() => {
void reportTopNavClick(module.name)
if (module.isExternal && module.externalUrl) {
window.open(module.externalUrl, '_blank', 'noopener');
} else {

View File

@@ -2,14 +2,21 @@
* 运营统计 API
*/
import { authRequest } from '@/framework/request';
import apiClient from '@/common/api/axios';
// ==================== 类型定义 ====================
export interface OverviewData {
dau: number;
mau: number;
dat: number;
exportCount: number;
apiTokenTotal: number;
topActiveUser: {
userId: string;
userName: string | null;
actionCount: number;
} | null;
moduleStats: Record<string, number>;
}
@@ -54,29 +61,29 @@ export interface UserOverview {
* 获取今日大盘数据
*/
export async function getOverview(): Promise<OverviewData> {
const res = await authRequest.get<{ success: boolean; data: OverviewData }>(
const res = await apiClient.get<{ success: boolean; data: OverviewData }>(
'/api/admin/stats/overview'
);
return res.data;
return res.data.data;
}
/**
* 获取实时流水账
*/
export async function getLiveFeed(limit = 100): Promise<ActivityLog[]> {
const res = await authRequest.get<{ success: boolean; data: ActivityLog[] }>(
const res = await apiClient.get<{ success: boolean; data: ActivityLog[] }>(
`/api/admin/stats/live-feed?limit=${limit}`
);
return res.data;
return res.data.data;
}
/**
* 获取用户360画像
*/
export async function getUserOverview(userId: string): Promise<UserOverview> {
const res = await authRequest.get<{ success: boolean; data: UserOverview }>(
const res = await apiClient.get<{ success: boolean; data: UserOverview }>(
`/api/admin/users/${userId}/overview`
);
return res.data;
return res.data.data;
}

View File

@@ -11,6 +11,8 @@ import {
UserOutlined,
BankOutlined,
ExportOutlined,
FireOutlined,
ApiOutlined,
MessageOutlined,
BookOutlined,
SearchOutlined,
@@ -23,7 +25,7 @@ import {
ReloadOutlined,
} from '@ant-design/icons';
import { getOverview, getLiveFeed } from '../api/statsApi';
import type { OverviewData, ActivityLog } from '../api/statsApi';
import type { ActivityLog } from '../api/statsApi';
// ==================== 模块图标映射 ====================
@@ -174,7 +176,7 @@ export default function StatsDashboardPage() {
{/* 核心指标卡片 */}
<Row gutter={[16, 16]} className="mb-6">
<Col xs={24} sm={8}>
<Col xs={24} sm={12} md={6}>
<Card className="shadow-sm hover:shadow-md transition-shadow">
<Statistic
title={<span className="text-gray-600"> (DAU)</span>}
@@ -185,7 +187,18 @@ export default function StatsDashboardPage() {
/>
</Card>
</Col>
<Col xs={24} sm={8}>
<Col xs={24} sm={12} md={6}>
<Card className="shadow-sm hover:shadow-md transition-shadow">
<Statistic
title={<span className="text-gray-600">30 (MAU)</span>}
value={overview?.mau ?? 0}
prefix={<UserOutlined className="text-indigo-500" />}
loading={overviewLoading}
valueStyle={{ color: '#4f46e5', fontWeight: 'bold' }}
/>
</Card>
</Col>
<Col xs={24} sm={12} md={6}>
<Card className="shadow-sm hover:shadow-md transition-shadow">
<Statistic
title={<span className="text-gray-600"> (DAT)</span>}
@@ -196,7 +209,7 @@ export default function StatsDashboardPage() {
/>
</Card>
</Col>
<Col xs={24} sm={8}>
<Col xs={24} sm={12} md={6}>
<Card className="shadow-sm hover:shadow-md transition-shadow">
<Statistic
title={<span className="text-gray-600"></span>}
@@ -209,6 +222,31 @@ export default function StatsDashboardPage() {
</Col>
</Row>
<Row gutter={[16, 16]} className="mb-6">
<Col xs={24} md={12}>
<Card className="shadow-sm hover:shadow-md transition-shadow">
<Statistic
title={<span className="text-gray-600"></span>}
value={overview?.topActiveUser?.userName || '暂无'}
prefix={<FireOutlined className="text-orange-500" />}
suffix={overview?.topActiveUser ? `${overview.topActiveUser.actionCount}` : ''}
loading={overviewLoading}
/>
</Card>
</Col>
<Col xs={24} md={12}>
<Card className="shadow-sm hover:shadow-md transition-shadow">
<Statistic
title={<span className="text-gray-600"> API Token </span>}
value={overview?.apiTokenTotal ?? 0}
prefix={<ApiOutlined className="text-cyan-500" />}
loading={overviewLoading}
valueStyle={{ color: '#0891b2', fontWeight: 'bold' }}
/>
</Card>
</Col>
</Row>
{/* 模块使用统计 */}
{overview?.moduleStats && Object.keys(overview.moduleStats).length > 0 && (
<Card