feat(admin): add user-level direct permission system and enhance activity tracking

Features:
- Add user_permissions table for direct user-to-permission grants (ops:user-ops)
- Merge role_permissions + user_permissions in auth chain (login, middleware, getCurrentUser)
- Add getUserQueryScope support for USER role with ops:user-ops (cross-tenant access)
- Unify cross-tenant operation checks via getUserQueryScope (remove hardcoded SUPER_ADMIN checks)
- Add 3 new API endpoints: GET/PUT /:id/permissions, GET /options/permissions
- Support ops:user-ops as alternative permission on all user/tenant management routes
- Frontend: add user-ops permission toggle on UserFormPage and UserDetailPage
- Enhance DC module activity tracking (StreamAIController, SessionController, QuickActionController)
- Fix DC AIController user ID extraction and feature name consistency
- Add verify-activity-tracking.ts validation script
- Update deployment checklist and admin module documentation

DB Migration: 20260309_add_user_permissions_table

Made-with: Cursor
This commit is contained in:
2026-03-10 09:02:35 +08:00
parent 971e903acf
commit 097e7920ab
19 changed files with 693 additions and 87 deletions

View File

@@ -13,6 +13,7 @@
"prisma:studio": "prisma studio",
"prisma:seed": "tsx prisma/seed.ts",
"test:sms": "tsx scripts/test-aliyun-sms.ts",
"test:tracking": "tsx scripts/verify-activity-tracking.ts",
"iit:equery:dedupe": "tsx scripts/dedupe_open_equeries.ts",
"iit:equery:dedupe:apply": "tsx scripts/dedupe_open_equeries.ts --apply",
"iit:guard:check": "tsx scripts/validate_guard_types_for_project.ts",

View File

@@ -0,0 +1,21 @@
-- CreateTable: 用户直授权限表(不依赖角色,单独给用户授予权限,如 ops:user-ops
CREATE TABLE "platform_schema"."user_permissions" (
"id" SERIAL NOT NULL,
"user_id" TEXT NOT NULL,
"permission_id" INTEGER NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "user_permissions_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "user_permissions_user_id_idx" ON "platform_schema"."user_permissions"("user_id");
-- CreateIndex: 唯一约束,同一用户不重复授同一权限
CREATE UNIQUE INDEX "user_permissions_user_id_permission_id_key" ON "platform_schema"."user_permissions"("user_id", "permission_id");
-- AddForeignKey
ALTER TABLE "platform_schema"."user_permissions" ADD CONSTRAINT "user_permissions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "platform_schema"."users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "platform_schema"."user_permissions" ADD CONSTRAINT "user_permissions_permission_id_fkey" FOREIGN KEY ("permission_id") REFERENCES "platform_schema"."permissions"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -47,6 +47,7 @@ model User {
updatedAt DateTime @updatedAt @map("updated_at")
tenant_members tenant_members[]
user_modules user_modules[]
user_permissions user_permissions[]
iitUserMappings IitUserMapping[]
departments departments? @relation(fields: [department_id], references: [id])
tenants tenants @relation(fields: [tenant_id], references: [id])
@@ -1775,6 +1776,7 @@ model permissions {
module String?
created_at DateTime @default(now())
role_permissions role_permissions[]
user_permissions user_permissions[]
@@schema("platform_schema")
}
@@ -1790,6 +1792,20 @@ model role_permissions {
@@schema("platform_schema")
}
/// 用户直授权限表(不依赖角色,单独给用户授予权限)
model user_permissions {
id Int @id @default(autoincrement())
user_id String
permission_id Int
created_at DateTime @default(now())
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
permissions permissions @relation(fields: [permission_id], references: [id], onDelete: Cascade)
@@unique([user_id, permission_id])
@@index([user_id])
@@schema("platform_schema")
}
model tenant_members {
id String @id
tenant_id String

View File

@@ -0,0 +1,137 @@
/**
* 埋点验证脚本
*
* 检查 simple_logs 表中是否存在各关键埋点,
* 并汇总每个模块/功能的记录数量。
*
* 用法: npx tsx scripts/verify-activity-tracking.ts [--days N]
*/
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
interface TrackingPoint {
module: string;
feature: string;
label: string;
}
const EXPECTED_TRACKING_POINTS: TrackingPoint[] = [
// 系统级
{ module: 'SYSTEM', feature: '用户登录', label: '用户登录' },
{ module: 'SYSTEM', feature: '顶部导航点击', label: '顶部导航点击' },
// ASL
{ module: 'ASL', feature: '意图识别', label: 'ASL 意图识别(需求扩写)' },
{ module: 'ASL', feature: 'Deep Research', label: 'ASL 启动 Deep Research' },
// AIA (各智能体名称作为 feature)
{ module: 'AIA', feature: '科学问题梳理', label: 'AIA 智能体 - 科学问题梳理(示例)' },
// PKB
{ module: 'PKB', feature: '创建知识库', label: 'PKB 创建知识库' },
// DC
{ module: 'DC', feature: '智能数据清洗', label: 'DC 智能数据清洗' },
// IIT/CRA
{ module: 'IIT', feature: 'CRA质控', label: 'IIT CRA质控' },
// RVW
{ module: 'RVW', feature: '稿', label: 'RVW 稿件审查相关' },
];
async function main() {
const daysArg = process.argv.findIndex(a => a === '--days');
const days = daysArg >= 0 ? parseInt(process.argv[daysArg + 1], 10) || 7 : 7;
const since = new Date();
since.setDate(since.getDate() - days);
console.log(`\n📊 埋点验证报告 (最近 ${days}since ${since.toISOString().slice(0, 10)})\n`);
console.log('='.repeat(80));
// 1. 总记录数
const totalCount = await prisma.simple_logs.count({
where: { created_at: { gte: since } },
});
console.log(`\n📈 总记录数: ${totalCount}\n`);
// 2. 按模块统计
const moduleStats = await prisma.$queryRaw`
SELECT module, COUNT(*) as count
FROM admin_schema.simple_logs
WHERE created_at >= ${since}
GROUP BY module
ORDER BY count DESC
` as Array<{ module: string; count: bigint }>;
console.log('📦 模块统计:');
console.log('-'.repeat(40));
for (const row of moduleStats) {
console.log(` ${row.module.padEnd(12)} ${Number(row.count).toString().padStart(6)}`);
}
// 3. 检查每个关键埋点是否存在
console.log('\n🔍 关键埋点覆盖检查:');
console.log('-'.repeat(80));
let coveredCount = 0;
let missingCount = 0;
for (const tp of EXPECTED_TRACKING_POINTS) {
const count = await prisma.simple_logs.count({
where: {
module: tp.module,
feature: { contains: tp.feature },
created_at: { gte: since },
},
});
const status = count > 0 ? '✅' : '❌';
if (count > 0) coveredCount++;
else missingCount++;
console.log(` ${status} ${tp.label.padEnd(35)} ${count > 0 ? `${count}` : '缺失'}`);
}
console.log('\n' + '='.repeat(80));
console.log(`\n📋 结果: ${coveredCount}/${EXPECTED_TRACKING_POINTS.length} 已覆盖, ${missingCount} 缺失\n`);
// 4. 按 feature 统计 Top 20
console.log('🏆 Top 20 Feature (按记录数):');
console.log('-'.repeat(80));
const topFeatures = await prisma.$queryRaw`
SELECT module, feature, action, COUNT(*) as count
FROM admin_schema.simple_logs
WHERE created_at >= ${since}
GROUP BY module, feature, action
ORDER BY count DESC
LIMIT 20
` as Array<{ module: string; feature: string; action: string; count: bigint }>;
for (const row of topFeatures) {
console.log(` ${row.module.padEnd(10)} ${row.feature.padEnd(25)} ${row.action.padEnd(10)} ${Number(row.count).toString().padStart(6)}`);
}
// 5. DAU/MAU
const dauResult = await prisma.$queryRaw`
SELECT COUNT(DISTINCT user_id) as dau
FROM admin_schema.simple_logs
WHERE created_at >= CURRENT_DATE
` as Array<{ dau: bigint }>;
const mauResult = await prisma.$queryRaw`
SELECT COUNT(DISTINCT user_id) as mau
FROM admin_schema.simple_logs
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
` as Array<{ mau: bigint }>;
console.log(`\n👤 DAU (今日活跃用户): ${Number(dauResult[0]?.dau || 0)}`);
console.log(`👥 MAU (30日活跃用户): ${Number(mauResult[0]?.mau || 0)}`);
console.log('\n✅ 验证完成\n');
}
main()
.then(() => prisma.$disconnect())
.catch(async (e) => {
console.error('❌ 验证失败:', e);
await prisma.$disconnect();
process.exit(1);
});

View File

@@ -187,27 +187,35 @@ export function requirePermission(requiredPermission: string): preHandlerHookHan
return;
}
const allowed = await prisma.role_permissions.findFirst({
// 1. 检查角色权限
const roleAllowed = await prisma.role_permissions.findFirst({
where: {
role: request.user.role as any,
permissions: {
code: requiredPermission,
},
permissions: { code: requiredPermission },
},
select: { permission_id: true },
});
if (roleAllowed) return;
if (!allowed) {
logger.warn('权限不足', {
userId: request.user.userId,
role: request.user.role,
requiredPermission,
});
return reply.status(403).send({
error: 'Forbidden',
message: `需要权限: ${requiredPermission}`,
});
}
// 2. 检查用户直授权限
const userAllowed = await prisma.user_permissions.findFirst({
where: {
user_id: request.user.userId,
permissions: { code: requiredPermission },
},
select: { permission_id: true },
});
if (userAllowed) return;
logger.warn('权限不足', {
userId: request.user.userId,
role: request.user.role,
requiredPermission,
});
return reply.status(403).send({
error: 'Forbidden',
message: `需要权限: ${requiredPermission}`,
});
};
}
@@ -229,27 +237,35 @@ export function requireAnyPermission(...requiredPermissions: string[]): preHandl
return;
}
const allowed = await prisma.role_permissions.findFirst({
// 1. 检查角色权限
const roleAllowed = await prisma.role_permissions.findFirst({
where: {
role: request.user.role as any,
permissions: {
code: { in: requiredPermissions },
},
permissions: { code: { in: requiredPermissions } },
},
select: { permission_id: true },
});
if (roleAllowed) return;
if (!allowed) {
logger.warn('权限不足(任一权限检查失败)', {
userId: request.user.userId,
role: request.user.role,
requiredPermissions,
});
return reply.status(403).send({
error: 'Forbidden',
message: `需要权限之一: ${requiredPermissions.join(', ')}`,
});
}
// 2. 检查用户直授权限
const userAllowed = await prisma.user_permissions.findFirst({
where: {
user_id: request.user.userId,
permissions: { code: { in: requiredPermissions } },
},
select: { permission_id: true },
});
if (userAllowed) return;
logger.warn('权限不足(任一权限检查失败)', {
userId: request.user.userId,
role: request.user.role,
requiredPermissions,
});
return reply.status(403).send({
error: 'Forbidden',
message: `需要权限之一: ${requiredPermissions.join(', ')}`,
});
};
}

View File

@@ -112,8 +112,8 @@ export class AuthService {
throw new Error('账号已被禁用,请联系管理员');
}
// 4. 获取用户权限和模块列表
const permissions = await this.getUserPermissions(user.role);
// 4. 获取用户权限(角色权限 + 用户直授权限合并)和模块列表
const permissions = await this.getUserPermissions(user.role, user.id);
const modules = await this.getUserModules(user.id);
// 4.5 原子递增 token 版本号(数据库强一致,避免并发登录竞态)
@@ -163,7 +163,7 @@ export class AuthService {
departmentName: user.departments?.name,
isDefaultPassword: user.is_default_password,
permissions,
modules, // 新增:返回模块列表
modules,
},
tokens,
};
@@ -218,8 +218,8 @@ export class AuthService {
throw new Error('账号已被禁用,请联系管理员');
}
// 5. 获取用户权限和模块列表
const permissions = await this.getUserPermissions(user.role);
// 5. 获取用户权限(角色权限 + 用户直授权限合并)和模块列表
const permissions = await this.getUserPermissions(user.role, user.id);
const modules = await this.getUserModules(user.id);
// 5.5 原子递增 token 版本号(数据库强一致,避免并发登录竞态)
@@ -290,7 +290,7 @@ export class AuthService {
throw new Error('用户不存在');
}
const permissions = await this.getUserPermissions(user.role);
const permissions = await this.getUserPermissions(user.role, userId);
const modules = await this.getUserModules(userId);
return {
@@ -306,7 +306,7 @@ export class AuthService {
departmentName: user.departments?.name,
isDefaultPassword: user.is_default_password,
permissions,
modules, // 新增:返回模块列表
modules,
// 2026-01-28: 个人中心扩展字段
avatarUrl: user.avatarUrl,
status: user.status,
@@ -471,13 +471,25 @@ export class AuthService {
/**
* 获取用户权限列表
*/
private async getUserPermissions(role: string): Promise<string[]> {
private async getUserPermissions(role: string, userId?: string): Promise<string[]> {
const rolePermissions = await prisma.role_permissions.findMany({
where: { role: role as any },
include: { permissions: true },
});
return rolePermissions.map(rp => rp.permissions.code);
const merged = new Set(rolePermissions.map(rp => rp.permissions.code));
if (userId) {
const directPermissions = await prisma.user_permissions.findMany({
where: { user_id: userId },
include: { permissions: true },
});
for (const dp of directPermissions) {
merged.add(dp.permissions.code);
}
}
return Array.from(merged).sort();
}
/**

View File

@@ -84,9 +84,11 @@ export async function createUser(
try {
const creator = request.user!;
// HOSPITAL_ADMIN 和 PHARMA_ADMIN 只能在自己的租户内创建用
// 跨租户创建检查SUPER_ADMIN 和持有 ops:user-ops 的用户可跨租
const scope = await userService.getUserQueryScope(creator.role, creator.tenantId, creator.userId);
const canCrossTenant = Object.keys(scope).length === 0;
if (
(creator.role === 'HOSPITAL_ADMIN' || creator.role === 'PHARMA_ADMIN') &&
!canCrossTenant &&
request.body.tenantId !== creator.tenantId
) {
return reply.status(403).send({
@@ -360,10 +362,11 @@ export async function updateUserModules(
try {
const updater = request.user!;
// SUPER_ADMIN 可操作任意租户
// HOSPITAL_ADMIN/PHARMA_ADMIN 只能操作自己租户的用户
// 跨租户操作检查:SUPER_ADMIN 和持有 ops:user-ops 的用户可操作任意租户
const scope = await userService.getUserQueryScope(updater.role, updater.tenantId, updater.userId);
const canCrossTenant = Object.keys(scope).length === 0; // 空 scope = 无限制
if (
updater.role !== 'SUPER_ADMIN' &&
!canCrossTenant &&
request.body.tenantId !== updater.tenantId
) {
return reply.status(403).send({
@@ -463,6 +466,92 @@ export async function importUsers(
}
}
/**
* 更新用户直授权限
* PUT /api/admin/users/:id/permissions
*/
export async function updateUserPermissions(
request: FastifyRequest<{
Params: { id: string };
Body: { permissions: string[] };
}>,
reply: FastifyReply
) {
try {
const updater = request.user!;
const result = await userService.updateUserDirectPermissions(
request.params.id,
request.body.permissions,
updater.userId
);
return reply.send({
code: 0,
message: '用户权限更新成功',
data: { permissions: result },
});
} catch (error: any) {
logger.error('[UserController] updateUserPermissions error:', error);
if (error.message.includes('不存在')) {
return reply.status(404).send({ code: 404, message: error.message });
}
return reply.status(500).send({
code: 500,
message: error.message || '更新权限失败',
});
}
}
/**
* 获取用户直授权限
* GET /api/admin/users/:id/permissions
*/
export async function getUserPermissions(
request: FastifyRequest<{ Params: { id: string } }>,
reply: FastifyReply
) {
try {
const directPerms = await userService.getUserDirectPermissions(request.params.id);
return reply.send({
code: 0,
message: 'success',
data: { permissions: directPerms },
});
} catch (error: any) {
logger.error('[UserController] getUserPermissions error:', error);
return reply.status(500).send({
code: 500,
message: error.message || '获取权限失败',
});
}
}
/**
* 获取所有可直授权限列表
* GET /api/admin/users/options/permissions
*/
export async function getPermissionOptions(
_request: FastifyRequest,
reply: FastifyReply
) {
try {
const permissions = await userService.getAllDirectGrantablePermissions();
return reply.send({
code: 0,
message: 'success',
data: permissions,
});
} catch (error: any) {
logger.error('[UserController] getPermissionOptions error:', error);
return reply.status(500).send({
code: 500,
message: error.message || '获取权限列表失败',
});
}
}
/**
* 获取所有租户列表(用于下拉选择)
*/

View File

@@ -28,25 +28,25 @@ export async function tenantRoutes(fastify: FastifyInstance) {
// 创建租户
// POST /api/admin/tenants
fastify.post('/', {
preHandler: [authenticate, requirePermission('tenant:create')],
preHandler: [authenticate, requireAnyPermission('tenant:create', 'ops:user-ops')],
handler: tenantController.createTenant,
});
// 更新租户信息
// PUT /api/admin/tenants/:id
fastify.put('/:id', {
preHandler: [authenticate, requirePermission('tenant:edit')],
preHandler: [authenticate, requireAnyPermission('tenant:edit', 'ops:user-ops')],
handler: tenantController.updateTenant,
});
// 更新租户状态
// PUT /api/admin/tenants/:id/status
fastify.put('/:id/status', {
preHandler: [authenticate, requirePermission('tenant:edit')],
preHandler: [authenticate, requireAnyPermission('tenant:edit', 'ops:user-ops')],
handler: tenantController.updateTenantStatus,
});
// 删除租户
// 删除租户(仅保留严格权限,运营人员不应删除租户)
// DELETE /api/admin/tenants/:id
fastify.delete('/:id', {
preHandler: [authenticate, requirePermission('tenant:delete')],
@@ -56,7 +56,7 @@ export async function tenantRoutes(fastify: FastifyInstance) {
// 配置租户模块
// PUT /api/admin/tenants/:id/modules
fastify.put('/:id/modules', {
preHandler: [authenticate, requirePermission('tenant:edit')],
preHandler: [authenticate, requireAnyPermission('tenant:edit', 'ops:user-ops')],
handler: tenantController.configureModules,
});
}

View File

@@ -31,28 +31,28 @@ export async function userRoutes(fastify: FastifyInstance) {
// 创建用户
// POST /api/admin/users
fastify.post('/', {
preHandler: [authenticate, requirePermission('user:create')],
preHandler: [authenticate, requireAnyPermission('user:create', 'ops:user-ops')],
handler: userController.createUser,
});
// 更新用户
// PUT /api/admin/users/:id
fastify.put('/:id', {
preHandler: [authenticate, requirePermission('user:edit')],
preHandler: [authenticate, requireAnyPermission('user:edit', 'ops:user-ops')],
handler: userController.updateUser,
});
// 更新用户状态(启用/禁用)
// PUT /api/admin/users/:id/status
fastify.put('/:id/status', {
preHandler: [authenticate, requirePermission('user:edit')],
preHandler: [authenticate, requireAnyPermission('user:edit', 'ops:user-ops')],
handler: userController.updateUserStatus,
});
// 重置用户密码
// POST /api/admin/users/:id/reset-password
fastify.post('/:id/reset-password', {
preHandler: [authenticate, requirePermission('user:edit')],
preHandler: [authenticate, requireAnyPermission('user:edit', 'ops:user-ops')],
handler: userController.resetUserPassword,
});
@@ -77,7 +77,7 @@ export async function userRoutes(fastify: FastifyInstance) {
// 更新用户在指定租户的模块权限
// PUT /api/admin/users/:id/modules
fastify.put('/:id/modules', {
preHandler: [authenticate, requirePermission('user:edit')],
preHandler: [authenticate, requireAnyPermission('user:edit', 'ops:user-ops')],
handler: userController.updateUserModules,
});
@@ -86,12 +86,35 @@ export async function userRoutes(fastify: FastifyInstance) {
// 批量导入用户
// POST /api/admin/users/import
fastify.post('/import', {
preHandler: [authenticate, requirePermission('user:create')],
preHandler: [authenticate, requireAnyPermission('user:create', 'ops:user-ops')],
handler: userController.importUsers,
});
// ==================== 直授权限管理 ====================
// 获取用户直授权限
// GET /api/admin/users/:id/permissions
fastify.get('/:id/permissions', {
preHandler: [authenticate, requireAnyPermission('user:view', 'ops:user-ops')],
handler: userController.getUserPermissions,
});
// 更新用户直授权限
// PUT /api/admin/users/:id/permissions
fastify.put('/:id/permissions', {
preHandler: [authenticate, requireAnyPermission('user:edit', 'ops:user-ops')],
handler: userController.updateUserPermissions,
});
// ==================== 辅助接口 ====================
// 获取所有可直授权限列表
// GET /api/admin/users/options/permissions
fastify.get('/options/permissions', {
preHandler: [authenticate, requireAnyPermission('user:view', 'ops:user-ops')],
handler: userController.getPermissionOptions,
});
// 获取所有租户列表(用于下拉选择)
// GET /api/admin/users/options/tenants
fastify.get('/options/tenants', {

View File

@@ -60,6 +60,19 @@ export async function getUserQueryScope(
const departmentId = userId ? await getUserDepartmentId(userId) : undefined;
return { tenantId, departmentId };
}
case 'USER': {
// USER 角色持有 ops:user-ops 权限时可访问管理端
// 检查是否有直授权限(有则给予跨租户查看能力,否则限制本租户)
if (userId) {
const directPerms = await prisma.user_permissions.findMany({
where: { user_id: userId },
include: { permissions: { select: { code: true } } },
});
const hasOps = directPerms.some(dp => dp.permissions.code === 'ops:user-ops');
if (hasOps) return {}; // 运营人员可查看全部用户
}
return { tenantId }; // 无 ops 权限则限制本租户
}
default:
throw new Error('无权限访问用户管理');
}
@@ -280,12 +293,20 @@ export async function getUserById(userId: string, scope: UserQueryScope): Promis
})
);
// 获取用户权限(基于角色)
// 获取用户权限(角色权限 + 用户直授权限合并
const rolePermissions = await prisma.role_permissions.findMany({
where: { role: user.role },
include: { permissions: true },
});
const permissions = rolePermissions.map((rp) => rp.permissions.code);
const directPermissions = await prisma.user_permissions.findMany({
where: { user_id: user.id },
include: { permissions: true },
});
const permSet = new Set([
...rolePermissions.map((rp) => rp.permissions.code),
...directPermissions.map((dp) => dp.permissions.code),
]);
const permissions = Array.from(permSet).sort();
return {
id: user.id,
@@ -885,6 +906,72 @@ export async function getModulesByTenant(tenantId: string) {
}));
}
/**
* 更新用户直授权限(不依赖角色,直接给用户授予/移除权限)
*/
export async function updateUserDirectPermissions(
userId: string,
permissionCodes: string[],
updaterId: string
): Promise<string[]> {
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user) {
throw new Error('用户不存在');
}
// 查询权限 ID
const perms = await prisma.permissions.findMany({
where: { code: { in: permissionCodes } },
});
const validCodes = perms.map((p) => p.code);
const invalidCodes = permissionCodes.filter((c) => !validCodes.includes(c));
if (invalidCodes.length > 0) {
throw new Error(`以下权限代码不存在: ${invalidCodes.join(', ')}`);
}
await prisma.$transaction(async (tx) => {
await tx.user_permissions.deleteMany({ where: { user_id: userId } });
if (perms.length > 0) {
await tx.user_permissions.createMany({
data: perms.map((p) => ({
user_id: userId,
permission_id: p.id,
})),
});
}
});
logger.info('[UserService] User direct permissions updated', {
userId,
permissionCodes,
updatedBy: updaterId,
});
return validCodes;
}
/**
* 获取用户直授权限列表(不含角色权限)
*/
export async function getUserDirectPermissions(userId: string): Promise<string[]> {
const directPerms = await prisma.user_permissions.findMany({
where: { user_id: userId },
include: { permissions: true },
});
return directPerms.map((dp) => dp.permissions.code);
}
/**
* 获取系统所有可直授的权限列表
*/
export async function getAllDirectGrantablePermissions(): Promise<Array<{ code: string; name: string; description: string | null }>> {
return prisma.permissions.findMany({
select: { code: true, name: true, description: true },
orderBy: { code: 'asc' },
});
}
// ============ 辅助函数 ============
function getModuleName(code: string): string {

View File

@@ -180,7 +180,7 @@ export class AIController {
activityService.log(
user.tenantId,
user.tenantName || null,
user.id || user.userId,
user.userId || user.id,
user.name || null,
'DC',
'智能数据清洗',
@@ -208,11 +208,11 @@ export class AIController {
activityService.log(
user.tenantId,
user.tenantName || null,
user.id,
user.name,
user.userId || user.id,
user.name || null,
'DC',
'Tool C AI代码',
'USE',
'智能数据清洗',
'COMPLETE',
`session:${sessionId}, retries:${result.retryCount}`
);
}

View File

@@ -20,6 +20,7 @@ import { sessionService } from '../services/SessionService.js';
// @ts-ignore - uuid 类型定义
import { v4 as uuidv4 } from 'uuid';
import { prisma } from '../../../../config/database.js';
import { activityService } from '../../../../common/services/activity.service.js';
/**
* 获取用户ID从JWT Token中获取
@@ -71,6 +72,23 @@ export class QuickActionController {
const userId = getUserId(request);
logger.info(`[QuickAction] 执行快速操作: action=${action}, sessionId=${sessionId}`);
// 埋点DC 快速操作
try {
const user = (request as any).user;
if (user) {
activityService.log(
user.tenantId,
user.tenantName || null,
user.userId || user.id,
user.name || null,
'DC',
'智能数据清洗',
'USE',
`quick-action:${action}, session:${sessionId}`,
);
}
} catch { /* 埋点失败不影响主业务 */ }
// 1. 验证参数
if (!sessionId || !action || !params) {

View File

@@ -19,6 +19,7 @@ import { sessionService } from '../services/SessionService.js';
import { dataProcessService } from '../services/DataProcessService.js';
import { jobQueue } from '../../../../common/jobs/index.js';
import * as xlsx from 'xlsx';
import { activityService } from '../../../../common/services/activity.service.js';
/**
* 获取用户ID从JWT Token中获取
@@ -92,6 +93,23 @@ export class SessionController {
logger.info(`[SessionController] Session创建成功: ${sessionResult.id}, jobId: ${sessionResult.jobId}`);
// 埋点DC 上传数据文件
try {
const user = (request as any).user;
if (user) {
activityService.log(
user.tenantId,
user.tenantName || null,
user.userId || user.id,
user.name || null,
'DC',
'智能数据清洗',
'USE',
`upload:${fileName}, session:${sessionResult.id}`,
);
}
} catch { /* 埋点失败不影响主业务 */ }
// 6. 返回Session信息 + jobId用于前端轮询
return reply.code(201).send({
success: true,

View File

@@ -16,6 +16,7 @@ import { FastifyRequest, FastifyReply } from 'fastify';
import { logger } from '../../../../common/logging/index.js';
import { aiCodeService } from '../services/AICodeService.js';
import { sessionService } from '../services/SessionService.js';
import { activityService } from '../../../../common/services/activity.service.js';
// ==================== 类型定义 ====================
@@ -49,6 +50,23 @@ export class StreamAIController {
const { sessionId, message, maxRetries = 3 } = request.body as StreamProcessBody;
logger.info(`[StreamAI] 收到流式处理请求: sessionId=${sessionId}`);
// 埋点:记录 DC 智能数据清洗启动
try {
const user = (request as any).user;
if (user) {
activityService.log(
user.tenantId,
user.tenantName || null,
user.userId || user.id,
user.name || null,
'DC',
'智能数据清洗',
'START',
`session:${sessionId}`,
);
}
} catch { /* 埋点失败不影响主业务 */ }
// 参数验证
if (!sessionId || !message) {
@@ -151,6 +169,23 @@ export class StreamAIController {
retryCount: attempt,
});
// 埋点:记录 DC 智能数据清洗完成
try {
const user = (request as any).user;
if (user) {
activityService.log(
user.tenantId,
user.tenantName || null,
user.userId || user.id,
user.name || null,
'DC',
'智能数据清洗',
'COMPLETE',
`session:${sessionId}, retries:${attempt}`,
);
}
} catch { /* 埋点失败不影响主业务 */ }
// 发送结束标记
reply.raw.write('data: [DONE]\n\n');
reply.raw.end();