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

@@ -0,0 +1,9 @@
ALTER TABLE "users"
ADD COLUMN IF NOT EXISTS "username" TEXT,
ADD COLUMN IF NOT EXISTS "department_id" TEXT,
ADD COLUMN IF NOT EXISTS "role_id" TEXT NOT NULL DEFAULT 'member',
ADD COLUMN IF NOT EXISTS "phone" TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS "password" TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS "is_system" BOOLEAN NOT NULL DEFAULT false;
CREATE UNIQUE INDEX IF NOT EXISTS "users_username_key" ON "users"("username");

View File

@@ -8,12 +8,18 @@ datasource db {
}
model User {
id String @id @default(cuid())
email String @unique
name String
avatar String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
id String @id @default(cuid())
email String @unique
name String
avatar String?
username String? @unique
departmentId String? @map("department_id")
roleId String @default("member") @map("role_id")
phone String @default("")
password String @default("")
isSystem Boolean @default(false) @map("is_system")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
createdTasks Task[] @relation("TaskCreator")
assignedTasks Task[] @relation("TaskAssignee")

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({