feat(v2.4): 完成领域主写迁移

This commit is contained in:
2026-07-08 15:41:10 +08:00
parent 2358dbd0b5
commit da71128ccd
42 changed files with 1410 additions and 81 deletions

View File

@@ -10,6 +10,10 @@ import { VersionPlanModule } from './modules/version-plan/version-plan.module';
import { DevTaskModule } from './modules/dev-task/dev-task.module';
import { TestCaseModule } from './modules/test-case/test-case.module';
import { BugModule } from './modules/bug/bug.module';
import { MemberModule } from './modules/member/member.module';
import { TaskCategoryModule } from './modules/task-category/task-category.module';
import { TaskWorklogModule } from './modules/task-worklog/task-worklog.module';
import { OvertimeModule } from './modules/overtime/overtime.module';
import { AiModule } from './modules/ai/ai.module';
import { ConfigModule } from './modules/config/config.module';
import { DataModule } from './modules/data/data.module';
@@ -27,6 +31,10 @@ import { HealthModule } from './modules/health/health.module';
DevTaskModule,
TestCaseModule,
BugModule,
MemberModule,
TaskCategoryModule,
TaskWorklogModule,
OvertimeModule,
RequirementModule,
ConfigModule,
DataModule,

View File

@@ -0,0 +1,34 @@
import { IsBoolean, IsOptional, IsString } from 'class-validator';
export class CreateMemberDto {
@IsString()
name!: string;
@IsString()
@IsOptional()
username?: string;
@IsString()
@IsOptional()
departmentId?: string;
@IsString()
@IsOptional()
roleId?: string;
@IsString()
@IsOptional()
phone?: string;
@IsString()
@IsOptional()
email?: string;
@IsString()
@IsOptional()
password?: string;
@IsBoolean()
@IsOptional()
isSystem?: boolean;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateMemberDto } from './create-member.dto';
export class UpdateMemberDto extends PartialType(CreateMemberDto) {}

View File

@@ -0,0 +1,29 @@
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common';
import { CreateMemberDto } from './dto/create-member.dto';
import { UpdateMemberDto } from './dto/update-member.dto';
import { MemberService } from './member.service';
@Controller('members')
export class MemberController {
constructor(private readonly memberService: MemberService) {}
@Get()
findAll() {
return this.memberService.findAll();
}
@Post()
create(@Body() dto: CreateMemberDto) {
return this.memberService.create(dto);
}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateMemberDto) {
return this.memberService.update(id, dto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.memberService.remove(id);
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { MemberController } from './member.controller';
import { MemberService } from './member.service';
@Module({
controllers: [MemberController],
providers: [MemberService],
exports: [MemberService],
})
export class MemberModule {}

View File

@@ -0,0 +1,94 @@
import { BadRequestException } from '@nestjs/common';
import { MemberService } from './member.service';
describe('MemberService domain writes', () => {
const makeService = () => {
const prisma = {
user: {
create: jest.fn(),
delete: jest.fn(),
findMany: jest.fn(),
update: jest.fn(),
},
};
return { prisma, service: new MemberService(prisma as any) };
};
it('creates members with UI role and department fields in users', async () => {
const { prisma, service } = makeService();
prisma.user.create.mockResolvedValue({
id: 'member-1',
name: '张三',
username: 'zhangsan',
departmentId: 'dept-2',
roleId: 'role-dev',
phone: '13000000000',
email: 'zhangsan@example.com',
password: 'Ftb12345',
isSystem: false,
createdAt: new Date('2026-07-08T00:00:00.000Z'),
});
await service.create({
name: '张三',
username: 'zhangsan',
departmentId: 'dept-2',
roleId: 'role-dev',
phone: '13000000000',
email: 'zhangsan@example.com',
password: 'Ftb12345',
});
expect(prisma.user.create).toHaveBeenCalledWith({
data: expect.objectContaining({
name: '张三',
username: 'zhangsan',
departmentId: 'dept-2',
roleId: 'role-dev',
phone: '13000000000',
email: 'zhangsan@example.com',
password: 'Ftb12345',
}),
});
});
it('protects the built-in admin from deletion and privileged field edits', async () => {
const { prisma, service } = makeService();
prisma.user.update.mockResolvedValue({
id: 'm-8',
name: '超级管理员',
username: 'admin',
roleId: 'role-admin',
departmentId: '',
phone: '13200132009',
email: 'admin@example.com',
password: 'Ftb12345',
isSystem: true,
createdAt: new Date('2026-07-08T00:00:00.000Z'),
});
await expect(service.remove('m-8')).rejects.toBeInstanceOf(BadRequestException);
await service.update('m-8', {
name: '误改',
username: 'other',
roleId: 'role-viewer',
departmentId: 'dept-1',
phone: '13200132009',
});
expect(prisma.user.delete).not.toHaveBeenCalled();
expect(prisma.user.update).toHaveBeenCalledWith({
where: { id: 'm-8' },
data: expect.not.objectContaining({
name: '误改',
username: 'other',
roleId: 'role-viewer',
departmentId: 'dept-1',
}),
});
expect(prisma.user.update).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({ phone: '13200132009', isSystem: true }),
}));
});
});

View File

@@ -0,0 +1,95 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { CreateMemberDto } from './dto/create-member.dto';
import { UpdateMemberDto } from './dto/update-member.dto';
const SYSTEM_ADMIN_MEMBER_ID = 'm-8';
const SYSTEM_ADMIN_NAME = '超级管理员';
const SYSTEM_ADMIN_USERNAME = 'admin';
const SYSTEM_ADMIN_ROLE_ID = 'role-admin';
@Injectable()
export class MemberService {
constructor(private readonly prisma: PrismaService) {}
async findAll() {
const rows = await (this.prisma.user as any).findMany({ orderBy: [{ isSystem: 'desc' }, { name: 'asc' }] });
return rows.map(toMember);
}
async create(dto: CreateMemberDto) {
const data = toUserData(dto);
const row = await (this.prisma.user as any).create({ data });
return toMember(row);
}
async update(id: string, dto: UpdateMemberDto) {
const data = id === SYSTEM_ADMIN_MEMBER_ID
? { ...sanitizeSystemAdminPatch(dto), isSystem: true }
: toUserData(dto, { partial: true });
const row = await (this.prisma.user as any).update({ where: { id }, data });
return toMember(row);
}
async remove(id: string) {
if (id === SYSTEM_ADMIN_MEMBER_ID) {
throw new BadRequestException('系统内置超级管理员不可删除');
}
await (this.prisma.user as any).delete({ where: { id } });
return { deleted: true };
}
}
function sanitizeSystemAdminPatch(dto: UpdateMemberDto) {
const data = toUserData(dto, { partial: true });
delete (data as any).name;
delete (data as any).username;
delete (data as any).departmentId;
delete (data as any).roleId;
delete (data as any).email;
return data;
}
function toUserData(dto: Partial<CreateMemberDto>, options: { partial?: boolean } = {}) {
const id = (dto as any).id as string | undefined;
const email = dto.email?.trim() || (options.partial ? undefined : `${id || dto.username || Date.now()}@local.ftb`);
return {
...(id !== undefined && { id }),
...(dto.name !== undefined && { name: dto.name }),
...(dto.username !== undefined && { username: emptyToNull(dto.username) }),
...(dto.departmentId !== undefined && { departmentId: emptyToNull(dto.departmentId) }),
...(dto.roleId !== undefined && { roleId: dto.roleId || 'member' }),
...(dto.phone !== undefined && { phone: dto.phone ?? '' }),
...(email !== undefined && { email }),
...(dto.password !== undefined && { password: dto.password ?? '' }),
...(dto.isSystem !== undefined && { isSystem: dto.isSystem }),
};
}
function toMember(row: any) {
return {
id: row.id,
name: row.id === SYSTEM_ADMIN_MEMBER_ID ? SYSTEM_ADMIN_NAME : row.name,
username: row.id === SYSTEM_ADMIN_MEMBER_ID ? SYSTEM_ADMIN_USERNAME : row.username ?? undefined,
departmentId: row.id === SYSTEM_ADMIN_MEMBER_ID ? '' : row.departmentId ?? '',
roleId: row.id === SYSTEM_ADMIN_MEMBER_ID ? SYSTEM_ADMIN_ROLE_ID : row.roleId ?? 'member',
phone: row.phone ?? '',
email: row.email?.endsWith('@local.ftb') ? '' : row.email ?? '',
password: row.password ?? '',
createdAt: toIsoDate(row.createdAt),
isSystem: Boolean(row.isSystem) || row.id === SYSTEM_ADMIN_MEMBER_ID,
};
}
function emptyToNull(value: string | null | undefined): string | null {
if (value === null) return null;
if (value === undefined) return null;
const trimmed = value.trim();
return trimmed ? trimmed : null;
}
function toIsoDate(value: unknown): string {
if (value instanceof Date) return value.toISOString().slice(0, 10);
if (typeof value === 'string' && value) return value.slice(0, 10);
return new Date().toISOString().slice(0, 10);
}

View File

@@ -0,0 +1,34 @@
import { IsOptional, IsString } from 'class-validator';
export class CreateOvertimeDto {
@IsString()
@IsOptional()
productId?: string;
@IsString()
@IsOptional()
projectId?: string;
@IsString()
@IsOptional()
versionId?: string;
@IsString()
person!: string;
@IsString()
startTime!: string;
@IsString()
endTime!: string;
@IsOptional()
duration?: number;
@IsString()
reasonId!: string;
@IsString()
@IsOptional()
remark?: string;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateOvertimeDto } from './create-overtime.dto';
export class UpdateOvertimeDto extends PartialType(CreateOvertimeDto) {}

View File

@@ -0,0 +1,29 @@
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common';
import { CreateOvertimeDto } from './dto/create-overtime.dto';
import { UpdateOvertimeDto } from './dto/update-overtime.dto';
import { OvertimeService } from './overtime.service';
@Controller('overtime')
export class OvertimeController {
constructor(private readonly overtimeService: OvertimeService) {}
@Get()
findAll() {
return this.overtimeService.findAll();
}
@Post()
create(@Body() dto: CreateOvertimeDto) {
return this.overtimeService.create(dto);
}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateOvertimeDto) {
return this.overtimeService.update(id, dto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.overtimeService.remove(id);
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { WorkActivityModule } from '../work-activity/work-activity.module';
import { OvertimeController } from './overtime.controller';
import { OvertimeService } from './overtime.service';
@Module({
imports: [WorkActivityModule],
controllers: [OvertimeController],
providers: [OvertimeService],
exports: [OvertimeService],
})
export class OvertimeModule {}

View File

@@ -0,0 +1,37 @@
import { OvertimeService } from './overtime.service';
describe('OvertimeService domain writes', () => {
it('writes overtime records to relation table', async () => {
const prisma = {
overtimeRecord: {
create: jest.fn().mockResolvedValue({ id: 'ot-1' }),
delete: jest.fn(),
findMany: jest.fn(),
update: jest.fn(),
},
};
const service = new OvertimeService(prisma as any);
await service.create({
projectId: 'project-1',
versionId: 'version-1',
person: 'member-1',
startTime: '2026-07-08T19:00:00.000Z',
endTime: '2026-07-08T21:00:00.000Z',
duration: 2,
reasonId: 'reason-4',
});
expect(prisma.overtimeRecord.create).toHaveBeenCalledWith({
data: expect.objectContaining({
projectId: 'project-1',
versionId: 'version-1',
userId: 'member-1',
reason: 'reason-4',
startAt: new Date('2026-07-08T19:00:00.000Z'),
endAt: new Date('2026-07-08T21:00:00.000Z'),
hours: 2,
}),
});
});
});

View File

@@ -0,0 +1,56 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { WorkActivityService } from '../work-activity/work-activity.service';
import { CreateOvertimeDto } from './dto/create-overtime.dto';
import { UpdateOvertimeDto } from './dto/update-overtime.dto';
@Injectable()
export class OvertimeService {
constructor(
private readonly prisma: PrismaService,
private readonly workActivity?: WorkActivityService,
) {}
async create(dto: CreateOvertimeDto) {
const item = await this.prisma.overtimeRecord.create({
data: {
productId: dto.productId || null,
projectId: dto.projectId || null,
versionId: dto.versionId || null,
userId: dto.person,
reason: dto.remark ? `${dto.reasonId}:${dto.remark}` : dto.reasonId,
startAt: new Date(dto.startTime),
endAt: new Date(dto.endTime),
hours: dto.duration ?? 0,
},
});
if (dto.versionId) await this.workActivity?.markXiaobaoSummaryDirty(dto.versionId);
return item;
}
findAll() {
return this.prisma.overtimeRecord.findMany({ orderBy: { createdAt: 'desc' } });
}
update(id: string, dto: UpdateOvertimeDto) {
return this.prisma.overtimeRecord.updateMany({ where: { id }, data: toData(dto) });
}
async remove(id: string) {
await this.prisma.overtimeRecord.deleteMany({ where: { id } });
return { deleted: true };
}
}
function toData(dto: UpdateOvertimeDto) {
return {
...(dto.productId !== undefined && { productId: dto.productId || null }),
...(dto.projectId !== undefined && { projectId: dto.projectId || null }),
...(dto.versionId !== undefined && { versionId: dto.versionId || null }),
...(dto.person !== undefined && { userId: dto.person }),
...(dto.reasonId !== undefined && { reason: dto.remark ? `${dto.reasonId}:${dto.remark}` : dto.reasonId }),
...(dto.startTime !== undefined && { startAt: new Date(dto.startTime) }),
...(dto.endTime !== undefined && { endAt: new Date(dto.endTime) }),
...(dto.duration !== undefined && { hours: dto.duration ?? 0 }),
};
}

View File

@@ -0,0 +1,24 @@
import { IsBoolean, IsOptional, IsString } from 'class-validator';
export class CreateTaskCategoryDto {
@IsString()
name!: string;
@IsString()
group!: string;
@IsString()
@IsOptional()
code?: string;
@IsString()
@IsOptional()
color?: string;
@IsOptional()
sortOrder?: number;
@IsBoolean()
@IsOptional()
isSystem?: boolean;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateTaskCategoryDto } from './create-task-category.dto';
export class UpdateTaskCategoryDto extends PartialType(CreateTaskCategoryDto) {}

View File

@@ -0,0 +1,29 @@
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common';
import { CreateTaskCategoryDto } from './dto/create-task-category.dto';
import { UpdateTaskCategoryDto } from './dto/update-task-category.dto';
import { TaskCategoryService } from './task-category.service';
@Controller('task-categories')
export class TaskCategoryController {
constructor(private readonly taskCategoryService: TaskCategoryService) {}
@Get()
findAll() {
return this.taskCategoryService.findAll();
}
@Post()
create(@Body() dto: CreateTaskCategoryDto) {
return this.taskCategoryService.create(dto);
}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateTaskCategoryDto) {
return this.taskCategoryService.update(id, dto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.taskCategoryService.remove(id);
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { TaskCategoryController } from './task-category.controller';
import { TaskCategoryService } from './task-category.service';
@Module({
controllers: [TaskCategoryController],
providers: [TaskCategoryService],
exports: [TaskCategoryService],
})
export class TaskCategoryModule {}

View File

@@ -0,0 +1,41 @@
import { TaskCategoryService } from './task-category.service';
describe('TaskCategoryService domain writes', () => {
const makeService = () => {
const prisma = {
taskCategory: {
create: jest.fn(),
delete: jest.fn(),
findMany: jest.fn(),
update: jest.fn(),
},
};
return { prisma, service: new TaskCategoryService(prisma as any) };
};
it('creates task categories with uniqueness scoped by group and name', async () => {
const { prisma, service } = makeService();
prisma.taskCategory.create.mockResolvedValue({ id: 'cat-1', name: '前端开发', group: 'development' });
await service.create({ name: '前端开发', group: 'development', code: 'frontend_development' });
expect(prisma.taskCategory.create).toHaveBeenCalledWith({
data: expect.objectContaining({
name: '前端开发',
group: 'development',
code: 'frontend_development',
isSystem: false,
}),
});
});
it('does not delete system categories', async () => {
const { prisma, service } = makeService();
prisma.taskCategory.findMany.mockResolvedValue([{ id: 'cat-system', isSystem: true }]);
const result = await service.remove('cat-system');
expect(result).toEqual({ deleted: false });
expect(prisma.taskCategory.delete).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,43 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { CreateTaskCategoryDto } from './dto/create-task-category.dto';
import { UpdateTaskCategoryDto } from './dto/update-task-category.dto';
@Injectable()
export class TaskCategoryService {
constructor(private readonly prisma: PrismaService) {}
findAll() {
return this.prisma.taskCategory.findMany({ orderBy: [{ group: 'asc' }, { name: 'asc' }] });
}
create(dto: CreateTaskCategoryDto) {
return this.prisma.taskCategory.create({
data: {
name: dto.name,
group: dto.group,
code: dto.code,
isSystem: dto.isSystem ?? false,
},
});
}
update(id: string, dto: UpdateTaskCategoryDto) {
return this.prisma.taskCategory.update({
where: { id },
data: {
...(dto.name !== undefined && { name: dto.name }),
...(dto.group !== undefined && { group: dto.group }),
...(dto.code !== undefined && { code: dto.code }),
...(dto.isSystem !== undefined && { isSystem: dto.isSystem }),
},
});
}
async remove(id: string) {
const [target] = await this.prisma.taskCategory.findMany({ where: { id }, take: 1 });
if (!target || target.isSystem) return { deleted: false };
await this.prisma.taskCategory.delete({ where: { id } });
return { deleted: true };
}
}

View File

@@ -0,0 +1,35 @@
import { IsOptional, IsString } from 'class-validator';
export class CreateTaskWorklogDto {
@IsString()
taskId!: string;
@IsString()
userId!: string;
@IsString()
date!: string;
@IsOptional()
hours?: number;
@IsString()
@IsOptional()
workContent?: string;
@IsString()
@IsOptional()
sourceType?: string;
@IsString()
@IsOptional()
versionId?: string;
@IsString()
@IsOptional()
productId?: string;
@IsString()
@IsOptional()
projectId?: string;
}

View File

@@ -0,0 +1,23 @@
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common';
import { CreateTaskWorklogDto } from './dto/create-task-worklog.dto';
import { TaskWorklogService } from './task-worklog.service';
@Controller('task-worklogs')
export class TaskWorklogController {
constructor(private readonly taskWorklogService: TaskWorklogService) {}
@Get()
findAll() {
return this.taskWorklogService.findAll();
}
@Post()
create(@Body() dto: CreateTaskWorklogDto) {
return this.taskWorklogService.create(dto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.taskWorklogService.remove(id);
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { WorkActivityModule } from '../work-activity/work-activity.module';
import { TaskWorklogController } from './task-worklog.controller';
import { TaskWorklogService } from './task-worklog.service';
@Module({
imports: [WorkActivityModule],
controllers: [TaskWorklogController],
providers: [TaskWorklogService],
exports: [TaskWorklogService],
})
export class TaskWorklogModule {}

View File

@@ -0,0 +1,36 @@
import { TaskWorklogService } from './task-worklog.service';
describe('TaskWorklogService append writes', () => {
it('appends task worklogs as relational evidence rows', async () => {
const prisma = {
taskWorklog: {
create: jest.fn().mockResolvedValue({ id: 'wl-1' }),
deleteMany: jest.fn(),
findMany: jest.fn(),
},
};
const service = new TaskWorklogService(prisma as any);
await service.create({
taskId: 'task-1',
userId: 'member-1',
date: '2026-07-08',
hours: 2,
workContent: '补充日报',
versionId: 'version-1',
});
expect(prisma.taskWorklog.create).toHaveBeenCalledWith({
data: expect.objectContaining({
sourceType: 'dev_task',
sourceId: 'task-1',
sourceVersionId: 'version-1',
versionId: 'version-1',
userId: 'member-1',
workDate: new Date('2026-07-08T00:00:00.000Z'),
hours: 2,
content: '补充日报',
}),
});
});
});

View File

@@ -0,0 +1,44 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { WorkActivityService } from '../work-activity/work-activity.service';
import { CreateTaskWorklogDto } from './dto/create-task-worklog.dto';
@Injectable()
export class TaskWorklogService {
constructor(
private readonly prisma: PrismaService,
private readonly workActivity?: WorkActivityService,
) {}
async create(dto: CreateTaskWorklogDto) {
const item = await this.prisma.taskWorklog.create({
data: {
versionId: dto.versionId ?? null,
productId: dto.productId ?? null,
projectId: dto.projectId ?? null,
userId: dto.userId,
sourceType: dto.sourceType ?? 'dev_task',
sourceId: dto.taskId,
sourceVersionId: dto.versionId ?? null,
workDate: parseDateOnly(dto.date),
hours: dto.hours ?? 0,
content: dto.workContent ?? '',
},
});
if (dto.versionId) await this.workActivity?.markXiaobaoSummaryDirty(dto.versionId);
return item;
}
findAll() {
return this.prisma.taskWorklog.findMany({ orderBy: { createdAt: 'desc' } });
}
async remove(id: string) {
await this.prisma.taskWorklog.deleteMany({ where: { id } });
return { deleted: true };
}
}
function parseDateOnly(value: string): Date {
return new Date(`${value.slice(0, 10)}T00:00:00.000Z`);
}

View File

@@ -0,0 +1,22 @@
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common';
import { WorkActivityRecordInput, WorkActivityService } from './work-activity.service';
@Controller('work-activities')
export class WorkActivityController {
constructor(private readonly workActivityService: WorkActivityService) {}
@Get()
findAll() {
return this.workActivityService.findAll();
}
@Post()
create(@Body() dto: WorkActivityRecordInput) {
return this.workActivityService.record(dto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.workActivityService.remove(id);
}
}

View File

@@ -1,7 +1,9 @@
import { Module } from '@nestjs/common';
import { WorkActivityController } from './work-activity.controller';
import { WorkActivityService } from './work-activity.service';
@Module({
controllers: [WorkActivityController],
providers: [WorkActivityService],
exports: [WorkActivityService],
})

View File

@@ -22,6 +22,10 @@ export interface WorkActivityRecordInput {
export class WorkActivityService {
constructor(private readonly prisma: PrismaService) {}
findAll() {
return this.prisma.workActivity.findMany({ orderBy: { occurredAt: 'desc' } });
}
async record(input: WorkActivityRecordInput) {
const versionId = input.versionId ?? input.sourceVersionId ?? null;
const metadata = {
@@ -54,6 +58,11 @@ export class WorkActivityService {
return activity;
}
async remove(id: string) {
await this.prisma.workActivity.deleteMany({ where: { id } });
return { deleted: true };
}
async markXiaobaoSummaryDirty(versionId: string) {
const riskSignature = `dirty:${versionId}`;
await this.prisma.xiaobaoRiskSummary.upsert({