Add demo mode factory reset
- Align the backend seeded default surgery template with the report editor's default report content. - Add backend demo defaults for the default template, Kimi provider, and Xunfei speech proxy configuration. - Change system reset into a super-admin demo mode factory reset that clears reports, audit logs, files, custom templates, and non-default users. - Keep only the default admin, manager, doctor, and default surgery template after demo reset. - Replace the old local-only reset all data button with a two-confirmation backend reset flow. - Add tests covering demo default alignment and database-backed demo reset behavior. - Update docs to describe demo mode reset semantics and production credential cautions.
This commit is contained in:
@@ -1,29 +1,35 @@
|
||||
import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import argon2 from 'argon2';
|
||||
import { AuditService } from '../audit/audit.service.js';
|
||||
import type { SafeUser } from '../auth/auth.types.js';
|
||||
import {
|
||||
DEMO_DEFAULT_REPORT_CONTENT,
|
||||
DEMO_SYSTEM_SETTINGS,
|
||||
DEMO_TEMPLATE_ID,
|
||||
} from '../demo/demo-defaults.js';
|
||||
import { isSuper } from '../permissions/permissions.policy.js';
|
||||
import { PrismaService } from '../prisma/prisma.service.js';
|
||||
import { systemSettingsSchema, type SystemSettingsInput } from './settings.schemas.js';
|
||||
|
||||
const DEFAULT_AI_PROVIDERS = {
|
||||
kimi: { endpoint: 'https://api.moonshot.cn/v1', apiKey: '', modelName: 'moonshot-v1-32k-vision-preview' },
|
||||
kimi: { endpoint: 'https://api.moonshot.cn/v1', apiKey: DEMO_SYSTEM_SETTINGS.aiProviders.kimi.apiKey, modelName: 'moonshot-v1-32k-vision-preview' },
|
||||
deepseek: { endpoint: 'https://api.deepseek.com/v1', apiKey: '', modelName: 'deepseek-chat' },
|
||||
openai: { endpoint: 'https://api.openai.com/v1', apiKey: '', modelName: 'gpt-4o' },
|
||||
custom: { endpoint: '', apiKey: '', modelName: '' },
|
||||
};
|
||||
|
||||
const DEFAULT_SETTINGS: SystemSettingsInput = {
|
||||
frameCount: 12,
|
||||
framePositions: [7.9, 9.3, 46.2, 49.1, 63.9, 64.8, 68.8, 73.7, 80.2, 85, 96.3, 98.6],
|
||||
defaultTemplate: '',
|
||||
frameCount: DEMO_SYSTEM_SETTINGS.frameCount,
|
||||
framePositions: [...DEMO_SYSTEM_SETTINGS.framePositions],
|
||||
defaultTemplate: DEMO_SYSTEM_SETTINGS.defaultTemplate,
|
||||
frameMode: 'keep',
|
||||
activeAiProvider: 'kimi',
|
||||
activeAiProvider: DEMO_SYSTEM_SETTINGS.activeAiProvider,
|
||||
aiProviders: DEFAULT_AI_PROVIDERS,
|
||||
autoInsertFrames: true,
|
||||
autoInsertDelay: 1,
|
||||
autoInsertFrameIndices: [0, 2, 4, 6, 8, 10],
|
||||
xfSpeechConfig: { appId: '', apiKey: '', apiSecret: '' },
|
||||
autoInsertFrames: DEMO_SYSTEM_SETTINGS.autoInsertFrames,
|
||||
autoInsertDelay: DEMO_SYSTEM_SETTINGS.autoInsertDelay,
|
||||
autoInsertFrameIndices: [...DEMO_SYSTEM_SETTINGS.autoInsertFrameIndices],
|
||||
xfSpeechConfig: DEMO_SYSTEM_SETTINGS.xfSpeechConfig,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -106,17 +112,160 @@ export class SettingsService {
|
||||
if (!isSuper(this.actorToPolicy(actor))) {
|
||||
throw new ForbiddenException('只有超级管理员可以重置系统设置');
|
||||
}
|
||||
await this.setSettingValue(actor.tenantId, 'global', 'systemSettings', this.toJson(DEFAULT_SETTINGS));
|
||||
await this.audit?.record({
|
||||
actor,
|
||||
action: 'settings.system.reset',
|
||||
targetType: 'SystemSetting',
|
||||
targetId: 'systemSettings',
|
||||
metadata: { scope: 'global' },
|
||||
});
|
||||
|
||||
await this.resetDemoData(actor.tenantId);
|
||||
return this.getSystemSettings(actor);
|
||||
}
|
||||
|
||||
private async resetDemoData(tenantId: string) {
|
||||
const passwordHash = await argon2.hash('123456');
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
const tenant = await tx.tenant.upsert({
|
||||
where: { code: 'default' },
|
||||
update: {},
|
||||
create: { code: 'default', name: '默认医院' },
|
||||
});
|
||||
const effectiveTenantId = tenantId || tenant.id;
|
||||
const demoTemplateId = effectiveTenantId === tenant.id
|
||||
? DEMO_TEMPLATE_ID
|
||||
: `${DEMO_TEMPLATE_ID}_${effectiveTenantId}`;
|
||||
|
||||
const adminDepartment = await tx.department.upsert({
|
||||
where: { tenantId_code: { tenantId: effectiveTenantId, code: 'admin' } },
|
||||
update: { name: '管理部门' },
|
||||
create: { tenantId: effectiveTenantId, code: 'admin', name: '管理部门' },
|
||||
});
|
||||
const surgeryDepartment = await tx.department.upsert({
|
||||
where: { tenantId_code: { tenantId: effectiveTenantId, code: 'surgery' } },
|
||||
update: { name: '外科' },
|
||||
create: { tenantId: effectiveTenantId, code: 'surgery', name: '外科' },
|
||||
});
|
||||
|
||||
await tx.report.deleteMany({ where: { tenantId: effectiveTenantId } });
|
||||
await tx.fileResource.deleteMany({ where: { tenantId: effectiveTenantId } });
|
||||
await tx.templateDepartmentPermission.deleteMany({
|
||||
where: { template: { tenantId: effectiveTenantId } },
|
||||
});
|
||||
await tx.template.deleteMany({ where: { tenantId: effectiveTenantId } });
|
||||
await tx.auditLog.deleteMany({ where: { tenantId: effectiveTenantId } });
|
||||
await tx.systemSetting.deleteMany({ where: { tenantId: effectiveTenantId } });
|
||||
await tx.userSession.deleteMany({ where: { user: { tenantId: effectiveTenantId } } });
|
||||
|
||||
await tx.user.deleteMany({
|
||||
where: {
|
||||
tenantId: effectiveTenantId,
|
||||
username: { notIn: ['admin', 'manager', '0001'] },
|
||||
},
|
||||
});
|
||||
|
||||
await tx.user.upsert({
|
||||
where: { tenantId_username: { tenantId: effectiveTenantId, username: 'admin' } },
|
||||
update: {
|
||||
departmentId: adminDepartment.id,
|
||||
passwordHash,
|
||||
role: 'SUPER',
|
||||
name: '超级管理员',
|
||||
status: 'ACTIVE',
|
||||
phone: null,
|
||||
email: null,
|
||||
signatureFileId: null,
|
||||
},
|
||||
create: {
|
||||
tenantId: effectiveTenantId,
|
||||
departmentId: adminDepartment.id,
|
||||
username: 'admin',
|
||||
passwordHash,
|
||||
role: 'SUPER',
|
||||
name: '超级管理员',
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
await tx.user.upsert({
|
||||
where: { tenantId_username: { tenantId: effectiveTenantId, username: 'manager' } },
|
||||
update: {
|
||||
departmentId: surgeryDepartment.id,
|
||||
passwordHash,
|
||||
role: 'ADMIN',
|
||||
name: '科室管理员',
|
||||
status: 'ACTIVE',
|
||||
phone: null,
|
||||
email: null,
|
||||
signatureFileId: null,
|
||||
},
|
||||
create: {
|
||||
tenantId: effectiveTenantId,
|
||||
departmentId: surgeryDepartment.id,
|
||||
username: 'manager',
|
||||
passwordHash,
|
||||
role: 'ADMIN',
|
||||
name: '科室管理员',
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
await tx.user.upsert({
|
||||
where: { tenantId_username: { tenantId: effectiveTenantId, username: '0001' } },
|
||||
update: {
|
||||
departmentId: surgeryDepartment.id,
|
||||
passwordHash,
|
||||
role: 'DOCTOR',
|
||||
name: '张医生',
|
||||
status: 'ACTIVE',
|
||||
phone: null,
|
||||
email: null,
|
||||
signatureFileId: null,
|
||||
},
|
||||
create: {
|
||||
tenantId: effectiveTenantId,
|
||||
departmentId: surgeryDepartment.id,
|
||||
username: '0001',
|
||||
passwordHash,
|
||||
role: 'DOCTOR',
|
||||
name: '张医生',
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
|
||||
await tx.department.deleteMany({
|
||||
where: {
|
||||
tenantId: effectiveTenantId,
|
||||
code: { notIn: ['admin', 'surgery'] },
|
||||
},
|
||||
});
|
||||
|
||||
const template = await tx.template.create({
|
||||
data: {
|
||||
id: demoTemplateId,
|
||||
tenantId: effectiveTenantId,
|
||||
name: '腹腔镜胆囊切除术报告',
|
||||
description: '标准手术记录模板',
|
||||
content: DEMO_DEFAULT_REPORT_CONTENT,
|
||||
fields: [],
|
||||
scope: 'DEPARTMENT',
|
||||
ownerDepartmentId: surgeryDepartment.id,
|
||||
ownerUserId: null,
|
||||
},
|
||||
});
|
||||
await tx.templateDepartmentPermission.create({
|
||||
data: {
|
||||
templateId: template.id,
|
||||
departmentId: surgeryDepartment.id,
|
||||
canUse: true,
|
||||
canManage: true,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.systemSetting.create({
|
||||
data: {
|
||||
tenantId: effectiveTenantId,
|
||||
scope: 'global',
|
||||
key: 'systemSettings',
|
||||
value: this.toJson({ ...DEFAULT_SETTINGS, defaultTemplate: demoTemplateId }),
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private normalize(input: SystemSettingsInput): SystemSettingsInput {
|
||||
const aiProviders = {
|
||||
...DEFAULT_AI_PROVIDERS,
|
||||
|
||||
Reference in New Issue
Block a user