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

@@ -102,7 +102,7 @@ async function main() {
// ============================================
console.log('📌 创建超级管理员...');
const superAdmin = await prisma.User.upsert({
const superAdmin = await prisma.user.upsert({
where: { phone: '13800000001' },
update: {},
create: {
@@ -123,7 +123,7 @@ async function main() {
// ============================================
console.log('📌 创建Prompt工程师账号...');
const promptEngineer = await prisma.User.upsert({
const promptEngineer = await prisma.user.upsert({
where: { phone: '13800000002' },
update: {},
create: {
@@ -232,7 +232,7 @@ async function main() {
// ============================================
console.log('📌 创建医院管理员...');
const hospitalAdmin = await prisma.User.upsert({
const hospitalAdmin = await prisma.user.upsert({
where: { phone: '13800138001' },
update: {},
create: {
@@ -266,7 +266,7 @@ async function main() {
// ============================================
console.log('📌 创建普通医生用户...');
const doctor1 = await prisma.User.upsert({
const doctor1 = await prisma.user.upsert({
where: { phone: '13800138002' },
update: {},
create: {
@@ -282,7 +282,7 @@ async function main() {
},
});
const doctor2 = await prisma.User.upsert({
const doctor2 = await prisma.user.upsert({
where: { phone: '13800138003' },
update: {},
create: {
@@ -343,6 +343,8 @@ async function main() {
// 审计日志权限
{ code: 'audit:view', name: '查看审计日志', description: '查看操作审计日志', module: 'audit' },
// 用户运营权限(可访问租户管理/用户管理/运营日志)
{ code: 'ops:user-ops', name: '用户运营', description: '运营管理端用户运营视图权限', module: 'ops' },
];
for (const perm of permissionsData) {

View File

@@ -394,6 +394,64 @@ export async function logout(
});
}
/**
* 记录前端行为埋点(如顶部导航点击)
*
* POST /api/v1/auth/activity
*/
export async function logActivity(
request: FastifyRequest<{
Body: {
module: 'SYSTEM' | 'AIA' | 'PKB' | 'ASL' | 'DC' | 'RVW' | 'IIT' | 'SSA' | 'ST';
feature: string;
action?: 'LOGIN' | 'USE' | 'EXPORT' | 'CREATE' | 'DELETE' | 'CLICK' | 'START' | 'COMPLETE' | 'ERROR';
info?: string;
};
}>,
reply: FastifyReply
) {
try {
if (!request.user) {
return reply.status(401).send({
success: false,
error: 'Unauthorized',
message: '未认证',
});
}
const { module, feature, action = 'USE', info } = request.body;
if (!module || !feature?.trim()) {
return reply.status(400).send({
success: false,
error: 'BadRequest',
message: 'module 和 feature 不能为空',
});
}
const user = await authService.getCurrentUser(request.user.userId);
activityService.log(
user.tenantId,
user.tenantName || null,
user.id,
user.name,
module,
feature.trim(),
action,
info,
);
return reply.status(200).send({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : '埋点记录失败';
logger.warn('记录埋点失败', { error: message, userId: request.user?.userId });
return reply.status(500).send({
success: false,
error: 'InternalServerError',
message,
});
}
}
/**
* <20><>ȡ<EFBFBD><C8A1>ǰ<EFBFBD>û<EFBFBD><C3BB>ɷ<EFBFBD><C9B7>ʵ<EFBFBD>ģ<EFBFBD><C4A3>

View File

@@ -183,20 +183,73 @@ export function requirePermission(requiredPermission: string): preHandlerHookHan
});
}
// TODO: 从缓存或数据库获取用户权限
// 目前简化处理:超级管理员拥有所有权限
if (request.user.role === 'SUPER_ADMIN') {
return;
}
// TODO: 实现权限检查逻辑
// const hasPermission = await checkUserPermission(request.user.userId, requiredPermission);
// if (!hasPermission) {
// return reply.status(403).send({
// error: 'Forbidden',
// message: `需要权限: ${requiredPermission}`,
// });
// }
const allowed = await prisma.role_permissions.findFirst({
where: {
role: request.user.role as any,
permissions: {
code: requiredPermission,
},
},
select: { permission_id: true },
});
if (!allowed) {
logger.warn('权限不足', {
userId: request.user.userId,
role: request.user.role,
requiredPermission,
});
return reply.status(403).send({
error: 'Forbidden',
message: `需要权限: ${requiredPermission}`,
});
}
};
}
/**
* 任一权限检查中间件工厂
*
* @param requiredPermissions 允许的权限code列表命中任意一个即放行
*/
export function requireAnyPermission(...requiredPermissions: string[]): preHandlerHookHandler {
return async (request: FastifyRequest, reply: FastifyReply) => {
if (!request.user) {
return reply.status(401).send({
error: 'Unauthorized',
message: '未认证',
});
}
if (request.user.role === 'SUPER_ADMIN') {
return;
}
const allowed = await prisma.role_permissions.findFirst({
where: {
role: request.user.role as any,
permissions: {
code: { in: requiredPermissions },
},
},
select: { permission_id: true },
});
if (!allowed) {
logger.warn('权限不足(任一权限检查失败)', {
userId: request.user.userId,
role: request.user.role,
requiredPermissions,
});
return reply.status(403).send({
error: 'Forbidden',
message: `需要权限之一: ${requiredPermissions.join(', ')}`,
});
}
};
}

View File

@@ -16,6 +16,7 @@ import {
uploadAvatar,
refreshToken,
logout,
logActivity,
} from './auth.controller.js';
import { authenticate } from './auth.middleware.js';
@@ -87,6 +88,26 @@ const refreshTokenSchema = {
},
};
const logActivitySchema = {
body: {
type: 'object',
required: ['module', 'feature'],
properties: {
module: {
type: 'string',
enum: ['SYSTEM', 'AIA', 'PKB', 'ASL', 'DC', 'RVW', 'IIT', 'SSA', 'ST'],
},
feature: { type: 'string', minLength: 1, maxLength: 120 },
action: {
type: 'string',
enum: ['LOGIN', 'USE', 'EXPORT', 'CREATE', 'DELETE', 'CLICK', 'START', 'COMPLETE', 'ERROR'],
default: 'USE',
},
info: { type: 'string', maxLength: 2000 },
},
},
};
/**
* 注册认证路由
*/
@@ -170,6 +191,14 @@ export async function authRoutes(
fastify.post('/logout', {
preHandler: [authenticate],
}, logout);
/**
* 前端行为埋点上报(如顶部导航点击)
*/
fastify.post('/activity', {
preHandler: [authenticate],
schema: logActivitySchema,
}, logActivity as any);
}
export default authRoutes;

View File

@@ -34,6 +34,9 @@ export type ActionType =
| 'EXPORT' // 导出/下载
| 'CREATE' // 创建资源
| 'DELETE' // 删除资源
| 'CLICK' // 点击行为
| 'START' // 启动任务
| 'COMPLETE'// 完成任务
| 'ERROR'; // 错误记录
/**
@@ -111,22 +114,35 @@ export const activityService = {
*/
async getTodayOverview(): Promise<{
dau: number;
mau: number;
dat: number;
exportCount: number;
apiTokenTotal: number;
topActiveUser: { userId: string; userName: string | null; actionCount: number } | null;
moduleStats: Record<string, number>;
}> {
const todayStart = new Date();
todayStart.setHours(0, 0, 0, 0);
const monthStart = new Date(todayStart);
monthStart.setDate(monthStart.getDate() - 29);
// 查询 DAU/DAT/导出数
// 查询 DAU/MAU/DAT/导出数/Token总量从 info 文本中提取 tokens: N
const stats = await prisma.$queryRaw`
SELECT
COUNT(DISTINCT user_id) as dau,
COUNT(DISTINCT CASE WHEN created_at >= ${monthStart} THEN user_id END) as mau,
COUNT(DISTINCT tenant_id) as dat,
COUNT(CASE WHEN action = 'EXPORT' THEN 1 END) as export_count
COUNT(CASE WHEN action = 'EXPORT' THEN 1 END) as export_count,
COALESCE(SUM(
CASE
WHEN info ~* 'tokens?\\D*\\d+'
THEN (regexp_match(info, '(?i)tokens?\\D*(\\d+)'))[1]::bigint
ELSE 0
END
), 0) as api_token_total
FROM admin_schema.simple_logs
WHERE created_at >= ${todayStart}
` as Array<{ dau: bigint; dat: bigint; export_count: bigint }>;
` as Array<{ dau: bigint; mau: bigint; dat: bigint; export_count: bigint; api_token_total: bigint }>;
// 查询模块使用统计
const moduleStats = await prisma.$queryRaw`
@@ -141,10 +157,31 @@ export const activityService = {
moduleMap[m.module] = Number(m.count);
});
// 今日最活跃用户(按行为数)
const topUsers = await prisma.$queryRaw`
SELECT user_id, user_name, COUNT(*) as action_count
FROM admin_schema.simple_logs
WHERE created_at >= ${todayStart}
GROUP BY user_id, user_name
ORDER BY action_count DESC
LIMIT 1
` as Array<{ user_id: string; user_name: string | null; action_count: bigint }>;
const topActiveUser = topUsers[0]
? {
userId: topUsers[0].user_id,
userName: topUsers[0].user_name,
actionCount: Number(topUsers[0].action_count),
}
: null;
return {
dau: Number(stats[0]?.dau || 0),
mau: Number(stats[0]?.mau || 0),
dat: Number(stats[0]?.dat || 0),
exportCount: Number(stats[0]?.export_count || 0),
apiTokenTotal: Number(stats[0]?.api_token_total || 0),
topActiveUser,
moduleStats: moduleMap,
};
},

View File

@@ -20,6 +20,7 @@ import { createSkillRunner } from '../../iit-manager/engines/SkillRunner.js';
import { QcExecutor } from '../../iit-manager/engines/QcExecutor.js';
import { QcReportService } from '../../iit-manager/services/QcReportService.js';
import { dailyQcOrchestrator } from '../../iit-manager/services/DailyQcOrchestrator.js';
import { activityService } from '../../../common/services/activity.service.js';
const prisma = new PrismaClient();
@@ -45,10 +46,30 @@ export class IitBatchController {
) {
const { projectId } = request.params;
const startTime = Date.now();
const requestUserId = (request as any).user?.userId as string | undefined;
try {
logger.info('[V3.1] Batch QC started', { projectId });
if (requestUserId) {
const actor = await prisma.user.findUnique({
where: { id: requestUserId },
include: { tenants: true },
});
if (actor) {
activityService.log(
actor.tenant_id,
actor.tenants?.name || null,
actor.id,
actor.name,
'IIT',
'CRA质控',
'START',
`projectId:${projectId}`,
);
}
}
const project = await prisma.iitProject.findUnique({
where: { id: projectId },
select: { id: true },
@@ -87,6 +108,25 @@ export class IitBatchController {
projectId, totalRecords, totalEvents, passed, failed, warnings, durationMs,
});
if (requestUserId) {
const actor = await prisma.user.findUnique({
where: { id: requestUserId },
include: { tenants: true },
});
if (actor) {
activityService.log(
actor.tenant_id,
actor.tenants?.name || null,
actor.id,
actor.name,
'IIT',
'CRA质控',
'COMPLETE',
`projectId:${projectId}, total:${totalEvents}, pass:${passed}, fail:${failed}, warn:${warnings}`,
);
}
}
return reply.send({
success: true,
message: '事件级全量质控完成V3.1 QcExecutor',

View File

@@ -9,7 +9,7 @@
import type { FastifyInstance } from 'fastify';
import * as statsController from '../controllers/statsController.js';
import { authenticate, requirePermission } from '../../../common/auth/auth.middleware.js';
import { authenticate, requireAnyPermission, requirePermission } from '../../../common/auth/auth.middleware.js';
export async function statsRoutes(fastify: FastifyInstance) {
// ==================== 运营统计 ====================
@@ -21,7 +21,7 @@ export async function statsRoutes(fastify: FastifyInstance) {
* 权限: SUPER_ADMIN, PROMPT_ENGINEER
*/
fastify.get('/overview', {
preHandler: [authenticate, requirePermission('tenant:view')],
preHandler: [authenticate, requireAnyPermission('tenant:view', 'ops:user-ops')],
handler: statsController.getOverview,
});
@@ -32,7 +32,7 @@ export async function statsRoutes(fastify: FastifyInstance) {
* 权限: SUPER_ADMIN, PROMPT_ENGINEER
*/
fastify.get('/live-feed', {
preHandler: [authenticate, requirePermission('tenant:view')],
preHandler: [authenticate, requireAnyPermission('tenant:view', 'ops:user-ops')],
handler: statsController.getLiveFeed,
});
@@ -43,7 +43,7 @@ export async function statsRoutes(fastify: FastifyInstance) {
* 权限: SUPER_ADMIN, PROMPT_ENGINEER
*/
fastify.get('/logs', {
preHandler: [authenticate, requirePermission('tenant:view')],
preHandler: [authenticate, requireAnyPermission('tenant:view', 'ops:user-ops')],
handler: statsController.getActivityLogs,
});
@@ -72,7 +72,7 @@ export async function userOverviewRoute(fastify: FastifyInstance) {
* 权限: SUPER_ADMIN
*/
fastify.get('/:id/overview', {
preHandler: [authenticate, requirePermission('user:view')],
preHandler: [authenticate, requireAnyPermission('user:view', 'ops:user-ops')],
handler: statsController.getUserOverview,
});
}

View File

@@ -6,7 +6,7 @@
import type { FastifyInstance } from 'fastify';
import * as tenantController from '../controllers/tenantController.js';
import { authenticate, requirePermission } from '../../../common/auth/auth.middleware.js';
import { authenticate, requireAnyPermission, requirePermission } from '../../../common/auth/auth.middleware.js';
export async function tenantRoutes(fastify: FastifyInstance) {
// ==================== 租户管理 ====================
@@ -14,14 +14,14 @@ export async function tenantRoutes(fastify: FastifyInstance) {
// 获取租户列表
// GET /api/admin/tenants?type=&status=&search=&page=1&limit=20
fastify.get('/', {
preHandler: [authenticate, requirePermission('tenant:view')],
preHandler: [authenticate, requireAnyPermission('tenant:view', 'ops:user-ops')],
handler: tenantController.listTenants,
});
// 获取租户详情
// GET /api/admin/tenants/:id
fastify.get('/:id', {
preHandler: [authenticate, requirePermission('tenant:view')],
preHandler: [authenticate, requireAnyPermission('tenant:view', 'ops:user-ops')],
handler: tenantController.getTenant,
});
@@ -70,7 +70,7 @@ export async function moduleRoutes(fastify: FastifyInstance) {
// 获取所有可用模块列表
// GET /api/admin/modules
fastify.get('/', {
preHandler: [authenticate, requirePermission('tenant:view')],
preHandler: [authenticate, requireAnyPermission('tenant:view', 'ops:user-ops')],
handler: tenantController.listModules,
});
}

View File

@@ -5,7 +5,7 @@
*/
import type { FastifyInstance } from 'fastify';
import { authenticate, requireRoles, requirePermission } from '../../../common/auth/auth.middleware.js';
import { authenticate, requireRoles, requirePermission, requireAnyPermission } from '../../../common/auth/auth.middleware.js';
import * as userController from '../controllers/userController.js';
/**
@@ -17,14 +17,14 @@ export async function userRoutes(fastify: FastifyInstance) {
// 获取用户列表
// GET /api/admin/users?page=1&pageSize=20&search=&role=&tenantId=&status=&departmentId=
fastify.get('/', {
preHandler: [authenticate, requirePermission('user:view')],
preHandler: [authenticate, requireAnyPermission('user:view', 'ops:user-ops')],
handler: userController.listUsers,
});
// 获取用户详情
// GET /api/admin/users/:id
fastify.get('/:id', {
preHandler: [authenticate, requirePermission('user:view')],
preHandler: [authenticate, requireAnyPermission('user:view', 'ops:user-ops')],
handler: userController.getUserById,
});
@@ -95,21 +95,21 @@ export async function userRoutes(fastify: FastifyInstance) {
// 获取所有租户列表(用于下拉选择)
// GET /api/admin/users/options/tenants
fastify.get('/options/tenants', {
preHandler: [authenticate, requirePermission('user:view')],
preHandler: [authenticate, requireAnyPermission('user:view', 'ops:user-ops')],
handler: userController.getAllTenants,
});
// 获取租户的科室列表
// GET /api/admin/users/options/tenants/:tenantId/departments
fastify.get('/options/tenants/:tenantId/departments', {
preHandler: [authenticate, requirePermission('user:view')],
preHandler: [authenticate, requireAnyPermission('user:view', 'ops:user-ops')],
handler: userController.getDepartmentsByTenant,
});
// 获取租户的模块列表(用于模块配置)
// GET /api/admin/users/options/tenants/:tenantId/modules
fastify.get('/options/tenants/:tenantId/modules', {
preHandler: [authenticate, requirePermission('user:view')],
preHandler: [authenticate, requireAnyPermission('user:view', 'ops:user-ops')],
handler: userController.getModulesByTenant,
});
}

View File

@@ -11,6 +11,7 @@ import { FastifyRequest, FastifyReply } from 'fastify';
import { prisma } from '../../../config/database.js';
import { jobQueue } from '../../../common/jobs/index.js';
import { logger } from '../../../common/logging/index.js';
import { activityService } from '../../../common/services/activity.service.js';
import { requirementExpansionService } from '../services/requirementExpansionService.js';
import { wordExportService } from '../services/wordExportService.js';
import { DEEP_RESEARCH_DATA_SOURCES } from '../config/dataSources.js';
@@ -63,6 +64,28 @@ export async function generateRequirement(
filters,
});
// 埋点ASL 意图识别(需求扩写)
try {
const user = await prisma.user.findUnique({
where: { id: userId },
include: { tenants: true },
});
if (user) {
activityService.log(
user.tenant_id,
user.tenants?.name || null,
user.id,
user.name,
'ASL',
'意图识别',
'USE',
`queryLen:${originalQuery.trim().length}`,
);
}
} catch {
// 埋点失败不影响主流程
}
return reply.send({ success: true, data: result });
} catch (error: any) {
logger.error('[DeepResearchController] generateRequirement failed', {
@@ -120,6 +143,28 @@ export async function executeTask(
await jobQueue.push('asl_deep_research_v2', { taskId });
// 埋点:启动 Deep Research
try {
const user = await prisma.user.findUnique({
where: { id: userId },
include: { tenants: true },
});
if (user) {
activityService.log(
user.tenant_id,
user.tenants?.name || null,
user.id,
user.name,
'ASL',
'启动Deep Research',
'START',
`taskId:${taskId}`,
);
}
} catch {
// 埋点失败不影响主流程
}
logger.info('[DeepResearchController] Task pushed to queue', { taskId });
return reply.send({ success: true });

View File

@@ -172,6 +172,25 @@ export class AIController {
error: '重试次数必须在1-5之间'
});
}
// 埋点:记录 Tool C AI清洗启动
try {
const user = (request as any).user;
if (user) {
activityService.log(
user.tenantId,
user.tenantName || null,
user.id || user.userId,
user.name || null,
'DC',
'智能数据清洗',
'START',
`session:${sessionId}`,
);
}
} catch {
// 埋点失败不影响主业务
}
// 生成并执行(带重试)
const result = await aiCodeService.generateAndExecute(

View File

@@ -14,6 +14,8 @@ import { logger } from '../../../common/logging/index.js';
import { ModelType } from '../../../common/llm/adapters/types.js';
import * as reviewService from '../services/reviewService.js';
import { AgentType } from '../types/index.js';
import { activityService } from '../../../common/services/activity.service.js';
import { prisma } from '../../../config/database.js';
/**
* 获取用户ID从JWT Token中获取
@@ -119,6 +121,28 @@ export async function createTask(
// 创建任务
const task = await reviewService.createTask(file, filename, userId, tenantId, modelType);
// 埋点:稿件上传
try {
const user = await prisma.user.findUnique({
where: { id: userId },
include: { tenants: true },
});
if (user) {
activityService.log(
user.tenant_id,
user.tenants?.name || null,
user.id,
user.name,
'RVW',
'稿件上传',
'CREATE',
`taskId:${task.id}, file:${filename}, size:${fileSizeBytes}`,
);
}
} catch {
// 埋点失败不影响主流程
}
logger.info('[RVW:Controller] 任务已创建', { taskId: task.id });
return reply.send({