feat(product): 实现产品需求管理模块(后端 + 前端)

后端:Product Module(CRUD)、Requirement Module(CRUD + 状态机流转)、PrismaService
前端:产品列表页、产品详情页(含需求池 Tab)、Zustand Store、API 封装

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Script Generator
2026-06-08 11:06:16 +08:00
parent 8de1f93fd3
commit e261c3d0b8
27 changed files with 6993 additions and 3 deletions

View File

@@ -0,0 +1,21 @@
import { IsString, IsNotEmpty, IsOptional, IsInt, Min, Max } from 'class-validator';
export class CreateRequirementDto {
@IsString()
@IsNotEmpty()
title!: string;
@IsString()
@IsOptional()
description?: string;
@IsInt()
@Min(0)
@Max(4)
@IsOptional()
priority?: number;
@IsString()
@IsNotEmpty()
creatorId!: string;
}

View File

@@ -0,0 +1,7 @@
import { IsEnum } from 'class-validator';
import { RequirementStatus } from '@ftb/shared';
export class UpdateRequirementStatusDto {
@IsEnum(RequirementStatus)
status!: RequirementStatus;
}

View File

@@ -0,0 +1,9 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateRequirementDto } from './create-requirement.dto';
import { IsOptional, IsString } from 'class-validator';
export class UpdateRequirementDto extends PartialType(CreateRequirementDto) {
@IsString()
@IsOptional()
creatorId?: never;
}

View File

@@ -0,0 +1,60 @@
import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from '@nestjs/common';
import { RequirementService } from './requirement.service';
import { CreateRequirementDto } from './dto/create-requirement.dto';
import { UpdateRequirementDto } from './dto/update-requirement.dto';
import { UpdateRequirementStatusDto } from './dto/update-requirement-status.dto';
@Controller('products/:productId/requirements')
export class RequirementController {
constructor(private readonly requirementService: RequirementService) {}
@Post()
create(
@Param('productId') productId: string,
@Body() dto: CreateRequirementDto,
) {
return this.requirementService.create(productId, dto);
}
@Get()
findAll(
@Param('productId') productId: string,
@Query('status') status?: string,
) {
return this.requirementService.findAll(productId, status);
}
@Get(':id')
findOne(
@Param('productId') productId: string,
@Param('id') id: string,
) {
return this.requirementService.findOne(productId, id);
}
@Patch(':id')
update(
@Param('productId') productId: string,
@Param('id') id: string,
@Body() dto: UpdateRequirementDto,
) {
return this.requirementService.update(productId, id, dto);
}
@Patch(':id/status')
updateStatus(
@Param('productId') productId: string,
@Param('id') id: string,
@Body() dto: UpdateRequirementStatusDto,
) {
return this.requirementService.updateStatus(productId, id, dto.status);
}
@Delete(':id')
remove(
@Param('productId') productId: string,
@Param('id') id: string,
) {
return this.requirementService.remove(productId, id);
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { RequirementController } from './requirement.controller';
import { RequirementService } from './requirement.service';
@Module({
controllers: [RequirementController],
providers: [RequirementService],
exports: [RequirementService],
})
export class RequirementModule {}

View File

@@ -0,0 +1,81 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { RequirementStatus } from '@ftb/shared';
import { CreateRequirementDto } from './dto/create-requirement.dto';
import { UpdateRequirementDto } from './dto/update-requirement.dto';
const VALID_TRANSITIONS: Record<string, string[]> = {
[RequirementStatus.DRAFT]: [RequirementStatus.REVIEWING],
[RequirementStatus.REVIEWING]: [RequirementStatus.APPROVED, RequirementStatus.REJECTED],
[RequirementStatus.APPROVED]: [RequirementStatus.DELIVERED],
[RequirementStatus.REJECTED]: [RequirementStatus.DRAFT],
[RequirementStatus.DELIVERED]: [],
};
@Injectable()
export class RequirementService {
constructor(private prisma: PrismaService) {}
create(productId: string, dto: CreateRequirementDto) {
return this.prisma.requirement.create({
data: {
productId,
title: dto.title,
description: dto.description || '',
priority: dto.priority ?? 0,
creatorId: dto.creatorId,
},
});
}
findAll(productId: string, status?: string) {
return this.prisma.requirement.findMany({
where: {
productId,
...(status ? { status } : {}),
},
orderBy: { createdAt: 'desc' },
include: { creator: { select: { id: true, name: true } } },
});
}
async findOne(productId: string, id: string) {
const req = await this.prisma.requirement.findFirst({
where: { id, productId },
include: { creator: { select: { id: true, name: true } } },
});
if (!req) throw new NotFoundException('需求不存在');
return req;
}
async update(productId: string, id: string, dto: UpdateRequirementDto) {
await this.findOne(productId, id);
return this.prisma.requirement.update({
where: { id },
data: {
...(dto.title !== undefined && { title: dto.title }),
...(dto.description !== undefined && { description: dto.description }),
...(dto.priority !== undefined && { priority: dto.priority }),
},
});
}
async updateStatus(productId: string, id: string, newStatus: string) {
const req = await this.findOne(productId, id);
const allowed = VALID_TRANSITIONS[req.status] || [];
if (!allowed.includes(newStatus)) {
throw new BadRequestException(
`无法从 "${req.status}" 转换到 "${newStatus}"`,
);
}
return this.prisma.requirement.update({
where: { id },
data: { status: newStatus },
});
}
async remove(productId: string, id: string) {
await this.findOne(productId, id);
return this.prisma.requirement.delete({ where: { id } });
}
}