feat(admin,rvw,asl,frontend): Batch import redesign + RVW parallel skills + UI improvements

Backend:
- Redesign batch user import: add autoInheritModules param, users auto-inherit tenant modules when true
- Add module validation: reject modules not subscribed by the tenant
- Soften department validation: skip instead of fail when department name not found
- Fix RVW skill status semantics: review findings (ERROR issues) no longer mark skill as error
- Add parallel execution support to SkillExecutor via parallelGroup
- Configure Editorial + Methodology skills to run in parallel (~240s -> ~130s)
- Update legacy bridge error message to user-friendly text

Frontend:
- Redesign ImportUserModal: 4-step flow (select tenant -> upload -> preview -> result)
- Simplify import template: remove tenant code and module columns
- Show tenant subscribed modules before import with auto-inherit option
- Fix isLegacyEmbed modules bypassing RouteGuard and TopNavigation permission checks
- Hide ASL fulltext screening (step 3), renumber subsequent nav items
- Add ExtractionWorkbenchGuide page when no taskId provided
- Update legacy system error message to network-friendly text

Docs:
- Update deployment changelog with BE-9, FE-11 entries

Made-with: Cursor
This commit is contained in:
2026-03-05 22:04:36 +08:00
parent 0677d42345
commit 91ae80888e
19 changed files with 576 additions and 274 deletions

View File

@@ -407,12 +407,17 @@ export async function updateUserModules(
*/
export async function importUsers(
request: FastifyRequest<{
Body: { users: ImportUserRow[]; defaultTenantId?: string };
Body: {
users: ImportUserRow[];
defaultTenantId?: string;
autoInheritModules?: boolean;
};
}>,
reply: FastifyReply
) {
try {
const importer = request.user!;
const autoInheritModules = request.body.autoInheritModules ?? true;
// 确定默认租户
let defaultTenantId = request.body.defaultTenantId;
@@ -440,7 +445,8 @@ export async function importUsers(
const result = await userService.importUsers(
request.body.users,
defaultTenantId,
importer.userId
importer.userId,
autoInheritModules
);
return reply.send({

View File

@@ -722,11 +722,13 @@ export async function updateUserModules(
/**
* 批量导入用户
* @param autoInheritModules 为 true 时忽略 Excel 中的模块列,用户自动继承租户全部已开通模块
*/
export async function importUsers(
rows: ImportUserRow[],
defaultTenantId: string,
importerId: string
importerId: string,
autoInheritModules: boolean = true
): Promise<ImportResult> {
const result: ImportResult = {
success: 0,
@@ -734,25 +736,34 @@ export async function importUsers(
errors: [],
};
// 预加载租户已开通模块(用于校验手动指定的模块)
const tenantModulesCache = new Map<string, Set<string>>();
async function getTenantModuleCodes(tenantId: string): Promise<Set<string>> {
if (!tenantModulesCache.has(tenantId)) {
const tenantModules = await prisma.tenant_modules.findMany({
where: { tenant_id: tenantId, is_enabled: true },
select: { module_code: true },
});
tenantModulesCache.set(tenantId, new Set(tenantModules.map((m) => m.module_code)));
}
return tenantModulesCache.get(tenantId)!;
}
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const rowNumber = i + 2; // Excel行号跳过表头
const rowNumber = i + 2;
try {
// 验证手机号
if (!row.phone || !/^1[3-9]\d{9}$/.test(row.phone)) {
throw new Error('手机号格式不正确');
}
// 验证姓名
if (!row.name || row.name.trim().length === 0) {
throw new Error('姓名不能为空');
}
// 解析角色
const role = parseRole(row.role);
// 解析租户
let tenantId = defaultTenantId;
if (row.tenantCode) {
const tenant = await prisma.tenants.findUnique({
@@ -764,7 +775,6 @@ export async function importUsers(
tenantId = tenant.id;
}
// 解析科室
let departmentId: string | undefined;
if (row.departmentName) {
const department = await prisma.departments.findFirst({
@@ -773,18 +783,23 @@ export async function importUsers(
tenant_id: tenantId,
},
});
if (!department) {
throw new Error(`科室 ${row.departmentName} 不存在`);
}
departmentId = department.id;
departmentId = department?.id;
}
// 解析模块
const modules = row.modules
? row.modules.split(',').map((m) => m.trim().toUpperCase())
: undefined;
let modules: string[] | undefined;
if (autoInheritModules) {
// 不写 user_modules用户自动继承租户全部已开通模块
modules = undefined;
} else if (row.modules) {
modules = row.modules.split(',').map((m) => m.trim().toUpperCase());
// 校验模块是否在租户已开通范围内
const subscribedModules = await getTenantModuleCodes(tenantId);
const invalidModules = modules.filter((m) => !subscribedModules.has(m));
if (invalidModules.length > 0) {
throw new Error(`模块 ${invalidModules.join(',')} 不在租户已开通范围内`);
}
}
// 创建用户
await createUser(
{
phone: row.phone,
@@ -812,6 +827,7 @@ export async function importUsers(
logger.info('[UserService] Batch import completed', {
success: result.success,
failed: result.failed,
autoInheritModules,
importedBy: importerId,
});