diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 55b41246..fe146ada 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -50,17 +50,20 @@ model Project { id String @id @default(uuid()) userId String @map("user_id") name String - description String @db.Text + background String @default("") @db.Text + researchType String @default("observational") @map("research_type") conversationCount Int @default(0) @map("conversation_count") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) conversations Conversation[] @@index([userId]) @@index([createdAt]) + @@index([deletedAt]) @@map("projects") } diff --git a/backend/src/controllers/projectController.ts b/backend/src/controllers/projectController.ts new file mode 100644 index 00000000..5eb9579a --- /dev/null +++ b/backend/src/controllers/projectController.ts @@ -0,0 +1,183 @@ +import { FastifyRequest, FastifyReply } from 'fastify'; +import { projectService } from '../services/projectService.js'; + +interface ProjectParams { + id: string; +} + +interface CreateProjectBody { + name: string; + background: string; + researchType: 'observational' | 'interventional'; +} + +interface UpdateProjectBody { + name?: string; + background?: string; + researchType?: 'observational' | 'interventional'; +} + +class ProjectController { + // 获取项目列表 + async getProjects(request: FastifyRequest, reply: FastifyReply) { + try { + // TODO: 从JWT token中获取真实的userId + // 目前使用模拟用户ID + const userId = 'user-mock-001'; + + const projects = await projectService.getProjectsByUserId(userId); + + return reply.code(200).send({ + success: true, + data: projects, + }); + } catch (error) { + request.log.error(error); + return reply.code(500).send({ + success: false, + message: '获取项目列表失败', + error: error instanceof Error ? error.message : 'Unknown error', + }); + } + } + + // 获取单个项目详情 + async getProjectById( + request: FastifyRequest<{ Params: ProjectParams }>, + reply: FastifyReply + ) { + try { + const { id } = request.params; + const userId = 'user-mock-001'; // TODO: 从JWT获取 + + const project = await projectService.getProjectById(id, userId); + + if (!project) { + return reply.code(404).send({ + success: false, + message: '项目不存在或无权访问', + }); + } + + return reply.code(200).send({ + success: true, + data: project, + }); + } catch (error) { + request.log.error(error); + return reply.code(500).send({ + success: false, + message: '获取项目详情失败', + error: error instanceof Error ? error.message : 'Unknown error', + }); + } + } + + // 创建项目 + async createProject(request: FastifyRequest, reply: FastifyReply) { + try { + const body = request.body as CreateProjectBody; + const userId = 'user-mock-001'; // TODO: 从JWT获取 + + // 检查用户项目数量限制(可选) + const projectCount = await projectService.countUserProjects(userId); + const MAX_PROJECTS = 50; // 可以配置到环境变量 + + if (projectCount >= MAX_PROJECTS) { + return reply.code(400).send({ + success: false, + message: `最多只能创建${MAX_PROJECTS}个项目`, + }); + } + + const project = await projectService.createProject({ + name: body.name, + background: body.background, + researchType: body.researchType, + userId, + }); + + return reply.code(201).send({ + success: true, + message: '项目创建成功', + data: project, + }); + } catch (error) { + request.log.error(error); + return reply.code(500).send({ + success: false, + message: '创建项目失败', + error: error instanceof Error ? error.message : 'Unknown error', + }); + } + } + + // 更新项目 + async updateProject( + request: FastifyRequest<{ Params: ProjectParams }>, + reply: FastifyReply + ) { + try { + const { id } = request.params; + const body = request.body as UpdateProjectBody; + const userId = 'user-mock-001'; // TODO: 从JWT获取 + + const project = await projectService.updateProject(id, userId, body); + + if (!project) { + return reply.code(404).send({ + success: false, + message: '项目不存在或无权访问', + }); + } + + return reply.code(200).send({ + success: true, + message: '项目更新成功', + data: project, + }); + } catch (error) { + request.log.error(error); + return reply.code(500).send({ + success: false, + message: '更新项目失败', + error: error instanceof Error ? error.message : 'Unknown error', + }); + } + } + + // 删除项目 + async deleteProject( + request: FastifyRequest<{ Params: ProjectParams }>, + reply: FastifyReply + ) { + try { + const { id } = request.params; + const userId = 'user-mock-001'; // TODO: 从JWT获取 + + const project = await projectService.deleteProject(id, userId); + + if (!project) { + return reply.code(404).send({ + success: false, + message: '项目不存在或无权访问', + }); + } + + return reply.code(200).send({ + success: true, + message: '项目删除成功', + }); + } catch (error) { + request.log.error(error); + return reply.code(500).send({ + success: false, + message: '删除项目失败', + error: error instanceof Error ? error.message : 'Unknown error', + }); + } + } +} + +export const projectController = new ProjectController(); + diff --git a/backend/src/index.ts b/backend/src/index.ts index e41772e1..b1ede82b 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -2,6 +2,7 @@ import Fastify from 'fastify'; import cors from '@fastify/cors'; import { config } from './config/env.js'; import { testDatabaseConnection, prisma } from './config/database.js'; +import { projectRoutes } from './routes/projects.js'; const fastify = Fastify({ logger: { @@ -51,6 +52,9 @@ fastify.get('/api/v1', async () => { }; }); +// 注册项目管理路由 +await fastify.register(projectRoutes, { prefix: '/api/v1' }); + // 启动服务器 const start = async () => { try { diff --git a/backend/src/middleware/validateProject.ts b/backend/src/middleware/validateProject.ts new file mode 100644 index 00000000..2ac9b4b1 --- /dev/null +++ b/backend/src/middleware/validateProject.ts @@ -0,0 +1,110 @@ +import { FastifyRequest, FastifyReply } from 'fastify'; + +interface CreateProjectBody { + name: string; + background?: string; + researchType: 'observational' | 'interventional'; +} + +interface UpdateProjectBody { + name?: string; + background?: string; + researchType?: 'observational' | 'interventional'; +} + +// 验证创建项目请求 +export async function validateProjectCreate(request: FastifyRequest, reply: FastifyReply) { + const body = request.body as CreateProjectBody; + + // 验证必填字段 + if (!body.name || typeof body.name !== 'string') { + return reply.code(400).send({ + success: false, + message: '项目名称为必填项', + }); + } + + if (body.name.trim().length === 0) { + return reply.code(400).send({ + success: false, + message: '项目名称不能为空', + }); + } + + if (body.name.length > 100) { + return reply.code(400).send({ + success: false, + message: '项目名称不能超过100个字符', + }); + } + + // 验证研究类型 + if (!body.researchType) { + return reply.code(400).send({ + success: false, + message: '研究类型为必填项', + }); + } + + if (!['observational', 'interventional'].includes(body.researchType)) { + return reply.code(400).send({ + success: false, + message: '研究类型必须是observational或interventional', + }); + } + + // 验证项目背景(可选,但有长度限制) + if (body.background && body.background.length > 2000) { + return reply.code(400).send({ + success: false, + message: '项目背景不能超过2000个字符', + }); + } +} + +// 验证更新项目请求 +export async function validateProjectUpdate(request: FastifyRequest, reply: FastifyReply) { + const body = request.body as UpdateProjectBody; + + // 至少需要更新一个字段 + if (!body.name && !body.background && !body.researchType) { + return reply.code(400).send({ + success: false, + message: '至少需要提供一个要更新的字段', + }); + } + + // 验证项目名称 + if (body.name !== undefined) { + if (typeof body.name !== 'string' || body.name.trim().length === 0) { + return reply.code(400).send({ + success: false, + message: '项目名称不能为空', + }); + } + + if (body.name.length > 100) { + return reply.code(400).send({ + success: false, + message: '项目名称不能超过100个字符', + }); + } + } + + // 验证研究类型 + if (body.researchType && !['observational', 'interventional'].includes(body.researchType)) { + return reply.code(400).send({ + success: false, + message: '研究类型必须是observational或interventional', + }); + } + + // 验证项目背景 + if (body.background && body.background.length > 2000) { + return reply.code(400).send({ + success: false, + message: '项目背景不能超过2000个字符', + }); + } +} + diff --git a/backend/src/routes/projects.ts b/backend/src/routes/projects.ts new file mode 100644 index 00000000..061d4f0a --- /dev/null +++ b/backend/src/routes/projects.ts @@ -0,0 +1,53 @@ +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { projectController } from '../controllers/projectController.js'; +import { validateProjectCreate, validateProjectUpdate } from '../middleware/validateProject.js'; + +interface ProjectParams { + id: string; +} + +export async function projectRoutes(fastify: FastifyInstance) { + // 获取项目列表 + fastify.get('/projects', async (request: FastifyRequest, reply: FastifyReply) => { + return projectController.getProjects(request, reply); + }); + + // 获取单个项目详情 + fastify.get<{ Params: ProjectParams }>( + '/projects/:id', + async (request: FastifyRequest<{ Params: ProjectParams }>, reply: FastifyReply) => { + return projectController.getProjectById(request, reply); + } + ); + + // 创建项目 + fastify.post( + '/projects', + { + preHandler: validateProjectCreate, + }, + async (request: FastifyRequest, reply: FastifyReply) => { + return projectController.createProject(request, reply); + } + ); + + // 更新项目 + fastify.put<{ Params: ProjectParams }>( + '/projects/:id', + { + preHandler: validateProjectUpdate, + }, + async (request: FastifyRequest<{ Params: ProjectParams }>, reply: FastifyReply) => { + return projectController.updateProject(request, reply); + } + ); + + // 删除项目 + fastify.delete<{ Params: ProjectParams }>( + '/projects/:id', + async (request: FastifyRequest<{ Params: ProjectParams }>, reply: FastifyReply) => { + return projectController.deleteProject(request, reply); + } + ); +} + diff --git a/backend/src/services/projectService.ts b/backend/src/services/projectService.ts new file mode 100644 index 00000000..7ed26993 --- /dev/null +++ b/backend/src/services/projectService.ts @@ -0,0 +1,102 @@ +import { prisma } from '../config/database.js'; + +export interface CreateProjectDTO { + name: string; + background: string; + researchType: 'observational' | 'interventional'; + userId: string; +} + +export interface UpdateProjectDTO { + name?: string; + background?: string; + researchType?: 'observational' | 'interventional'; +} + +class ProjectService { + // 获取用户的所有项目 + async getProjectsByUserId(userId: string) { + return prisma.project.findMany({ + where: { + userId, + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + }); + } + + // 根据ID获取项目 + async getProjectById(projectId: string, userId: string) { + return prisma.project.findFirst({ + where: { + id: projectId, + userId, + deletedAt: null, + }, + }); + } + + // 创建项目 + async createProject(data: CreateProjectDTO) { + return prisma.project.create({ + data: { + name: data.name, + background: data.background, + researchType: data.researchType, + userId: data.userId, + }, + }); + } + + // 更新项目 + async updateProject(projectId: string, userId: string, data: UpdateProjectDTO) { + // 先检查项目是否存在且属于该用户 + const project = await this.getProjectById(projectId, userId); + if (!project) { + return null; + } + + return prisma.project.update({ + where: { + id: projectId, + }, + data: { + ...data, + updatedAt: new Date(), + }, + }); + } + + // 软删除项目 + async deleteProject(projectId: string, userId: string) { + // 先检查项目是否存在且属于该用户 + const project = await this.getProjectById(projectId, userId); + if (!project) { + return null; + } + + return prisma.project.update({ + where: { + id: projectId, + }, + data: { + deletedAt: new Date(), + }, + }); + } + + // 统计用户的项目数量 + async countUserProjects(userId: string) { + return prisma.project.count({ + where: { + userId, + deletedAt: null, + }, + }); + } +} + +export const projectService = new ProjectService(); + diff --git a/frontend/src/api/projectApi.ts b/frontend/src/api/projectApi.ts new file mode 100644 index 00000000..4ba284b0 --- /dev/null +++ b/frontend/src/api/projectApi.ts @@ -0,0 +1,57 @@ +import request from './request'; +import type { Project } from '../stores/useProjectStore'; + +export interface ApiResponse { + success: boolean; + data?: T; + message?: string; + error?: string; +} + +export interface CreateProjectRequest { + name: string; + background: string; + researchType: 'observational' | 'interventional'; +} + +export interface UpdateProjectRequest { + name?: string; + background?: string; + researchType?: 'observational' | 'interventional'; +} + +export const projectApi = { + // 获取项目列表 + getProjects: async (): Promise> => { + const response = await request.get('/projects'); + return response.data; + }, + + // 获取单个项目详情 + getProjectById: async (id: string): Promise> => { + const response = await request.get(`/projects/${id}`); + return response.data; + }, + + // 创建项目 + createProject: async (data: CreateProjectRequest): Promise> => { + const response = await request.post('/projects', data); + return response.data; + }, + + // 更新项目 + updateProject: async ( + id: string, + data: UpdateProjectRequest + ): Promise> => { + const response = await request.put(`/projects/${id}`, data); + return response.data; + }, + + // 删除项目 + deleteProject: async (id: string): Promise => { + const response = await request.delete(`/projects/${id}`); + return response.data; + }, +}; + diff --git a/frontend/src/components/CreateProjectDialog.tsx b/frontend/src/components/CreateProjectDialog.tsx index 1b66dcc2..ba53fcab 100644 --- a/frontend/src/components/CreateProjectDialog.tsx +++ b/frontend/src/components/CreateProjectDialog.tsx @@ -1,32 +1,40 @@ +import { useState } from 'react'; import { Modal, Form, Input, Radio, message } from 'antd'; -import { useProjectStore, Project } from '../stores/useProjectStore'; +import { useProjectStore } from '../stores/useProjectStore'; +import { projectApi } from '../api/projectApi'; const { TextArea } = Input; export const CreateProjectDialog = () => { const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); const { showCreateDialog, setShowCreateDialog, addProject } = useProjectStore(); const handleOk = async () => { try { const values = await form.validateFields(); + setLoading(true); - // 模拟创建项目(后续会调用真实API) - const newProject: Project = { - id: `proj-${Date.now()}`, + // 调用真实API创建项目 + const response = await projectApi.createProject({ name: values.name, background: values.background || '', researchType: values.researchType, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; + }); - addProject(newProject); - message.success('项目创建成功'); - form.resetFields(); - setShowCreateDialog(false); + if (response.success && response.data) { + addProject(response.data); + message.success(response.message || '项目创建成功'); + form.resetFields(); + setShowCreateDialog(false); + } else { + message.error(response.message || '项目创建失败'); + } } catch (error) { - console.error('表单验证失败:', error); + console.error('创建项目失败:', error); + message.error('项目创建失败'); + } finally { + setLoading(false); } }; @@ -44,6 +52,7 @@ export const CreateProjectDialog = () => { width={600} okText="创建" cancelText="取消" + confirmLoading={loading} >
{ const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); const { currentProject, showEditDialog, @@ -29,19 +31,27 @@ export const EditProjectDialog = () => { try { const values = await form.validateFields(); + setLoading(true); - // 模拟更新项目(后续会调用真实API) - updateProject(currentProject.id, { + // 调用真实API更新项目 + const response = await projectApi.updateProject(currentProject.id, { name: values.name, background: values.background || '', researchType: values.researchType, - updatedAt: new Date().toISOString(), }); - message.success('项目更新成功'); - setShowEditDialog(false); + if (response.success && response.data) { + updateProject(currentProject.id, response.data); + message.success(response.message || '项目更新成功'); + setShowEditDialog(false); + } else { + message.error(response.message || '项目更新失败'); + } } catch (error) { - console.error('表单验证失败:', error); + console.error('更新项目失败:', error); + message.error('项目更新失败'); + } finally { + setLoading(false); } }; @@ -59,6 +69,7 @@ export const EditProjectDialog = () => { width={600} okText="保存" cancelText="取消" + confirmLoading={loading} > { const { currentProject, projects, + loading, setCurrentProject, setShowCreateDialog, setShowEditDialog, + fetchProjects, } = useProjectStore(); + // 组件挂载时获取项目列表 + useEffect(() => { + fetchProjects(); + }, [fetchProjects]); + const handleProjectChange = (projectId: string) => { if (projectId === 'global') { setCurrentProject(null); @@ -29,23 +37,25 @@ export const ProjectSelector = () => { 当前项目 - - + + 全局快速问答 - ))} - + + {projects.map((project) => ( + + {project.name} + + ))} +