feat(backend): Day 5 - backend basic architecture setup completed

This commit is contained in:
AI Clinical Dev Team
2025-10-10 15:56:45 +08:00
parent 1fac0b5cbf
commit 39e14cfb97
14 changed files with 3616 additions and 26 deletions

5
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules
# Keep environment variables out of version control
.env
/src/generated/prisma

102
backend/README.md Normal file
View File

@@ -0,0 +1,102 @@
# AI临床研究平台 - 后端服务
## 快速开始
### 1. 环境要求
- Node.js >= 18
- PostgreSQL 15+
- Redis 7+
### 2. 安装依赖
```bash
npm install
```
### 3. 配置环境变量
复制`.env.example``.env`并配置:
```bash
cp .env.example .env
```
### 4. 初始化数据库
```bash
# 生成Prisma Client
npm run prisma:generate
# 执行数据库迁移
npm run prisma:migrate
# 可选打开Prisma Studio查看数据
npm run prisma:studio
```
### 5. 启动开发服务器
```bash
npm run dev
```
服务器将在 http://localhost:3001 启动
### 6. 验证服务
访问以下端点验证服务是否正常:
- 健康检查: http://localhost:3001/health
- API入口: http://localhost:3001/api/v1
## 项目结构
```
backend/
├── prisma/
│ └── schema.prisma # 数据库模型定义
├── src/
│ ├── config/
│ │ ├── env.ts # 环境变量配置
│ │ └── database.ts # 数据库连接
│ ├── controllers/ # 控制器层
│ ├── services/ # 业务逻辑层
│ ├── routes/ # 路由定义
│ ├── types/ # TypeScript类型定义
│ ├── utils/ # 工具函数
│ └── index.ts # 应用入口
├── .env # 环境变量不提交到Git
├── .env.example # 环境变量模板
├── package.json # 项目配置
└── tsconfig.json # TypeScript配置
```
## NPM Scripts
- `npm run dev` - 启动开发服务器hot reload
- `npm run build` - 构建生产版本
- `npm run start` - 启动生产服务器
- `npm run prisma:generate` - 生成Prisma Client
- `npm run prisma:migrate` - 执行数据库迁移
- `npm run prisma:studio` - 打开Prisma Studio
## 数据库设计
详见:`../docs/01-设计文档/数据库设计文档.md`
## API文档
详见:`../docs/01-设计文档/API设计规范.md`
## 技术栈
- **框架**: Fastify
- **ORM**: Prisma
- **数据库**: PostgreSQL
- **缓存**: Redis
- **语言**: TypeScript
- **日志**: Pino
## Day 5 完成情况
✅ 初始化后端项目Node.js + TypeScript
✅ 配置Fastify框架
✅ 配置Prisma ORM
✅ 创建数据库表(执行迁移)
✅ 数据库连接测试通过
所有核心功能已完成!🎉

2469
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

40
backend/package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "ai-clinical-backend",
"version": "1.0.0",
"description": "AI Clinical Research Platform - Backend API",
"main": "dist/index.js",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:studio": "prisma studio",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"ai",
"clinical",
"research",
"medical"
],
"author": "AI Clinical Dev Team",
"license": "ISC",
"dependencies": {
"@fastify/cors": "^11.1.0",
"@fastify/jwt": "^10.0.0",
"@prisma/client": "^6.17.0",
"dotenv": "^17.2.3",
"fastify": "^5.6.1",
"prisma": "^6.17.0"
},
"devDependencies": {
"@types/node": "^24.7.1",
"nodemon": "^3.1.10",
"pino-pretty": "^13.1.1",
"ts-node": "^10.9.2",
"tsx": "^4.20.6",
"typescript": "^5.9.3"
}
}

View File

@@ -0,0 +1,203 @@
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
"name" TEXT,
"avatar_url" TEXT,
"role" TEXT NOT NULL DEFAULT 'user',
"status" TEXT NOT NULL DEFAULT 'active',
"kb_quota" INTEGER NOT NULL DEFAULT 3,
"kb_used" INTEGER NOT NULL DEFAULT 0,
"trial_ends_at" TIMESTAMP(3),
"is_trial" BOOLEAN NOT NULL DEFAULT true,
"last_login_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "projects" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT NOT NULL,
"conversation_count" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "projects_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "conversations" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"project_id" TEXT,
"agent_id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"model_name" TEXT NOT NULL DEFAULT 'deepseek-v3',
"message_count" INTEGER NOT NULL DEFAULT 0,
"total_tokens" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "conversations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "messages" (
"id" TEXT NOT NULL,
"conversation_id" TEXT NOT NULL,
"role" TEXT NOT NULL,
"content" TEXT NOT NULL,
"metadata" JSONB,
"tokens" INTEGER,
"is_pinned" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "messages_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "knowledge_bases" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"dify_dataset_id" TEXT NOT NULL,
"file_count" INTEGER NOT NULL DEFAULT 0,
"total_size_bytes" BIGINT NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "knowledge_bases_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "documents" (
"id" TEXT NOT NULL,
"kb_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"filename" TEXT NOT NULL,
"file_type" TEXT NOT NULL,
"file_size_bytes" BIGINT NOT NULL,
"file_url" TEXT NOT NULL,
"dify_document_id" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'uploading',
"progress" INTEGER NOT NULL DEFAULT 0,
"error_message" TEXT,
"segments_count" INTEGER,
"tokens_count" INTEGER,
"uploaded_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"processed_at" TIMESTAMP(3),
CONSTRAINT "documents_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "admin_logs" (
"id" SERIAL NOT NULL,
"admin_id" TEXT NOT NULL,
"action" TEXT NOT NULL,
"resource_type" TEXT,
"resource_id" TEXT,
"details" JSONB,
"ip_address" TEXT,
"user_agent" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "admin_logs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE INDEX "users_email_idx" ON "users"("email");
-- CreateIndex
CREATE INDEX "users_status_idx" ON "users"("status");
-- CreateIndex
CREATE INDEX "users_created_at_idx" ON "users"("created_at");
-- CreateIndex
CREATE INDEX "projects_user_id_idx" ON "projects"("user_id");
-- CreateIndex
CREATE INDEX "projects_created_at_idx" ON "projects"("created_at");
-- CreateIndex
CREATE INDEX "conversations_user_id_idx" ON "conversations"("user_id");
-- CreateIndex
CREATE INDEX "conversations_project_id_idx" ON "conversations"("project_id");
-- CreateIndex
CREATE INDEX "conversations_agent_id_idx" ON "conversations"("agent_id");
-- CreateIndex
CREATE INDEX "conversations_created_at_idx" ON "conversations"("created_at");
-- CreateIndex
CREATE INDEX "messages_conversation_id_idx" ON "messages"("conversation_id");
-- CreateIndex
CREATE INDEX "messages_created_at_idx" ON "messages"("created_at");
-- CreateIndex
CREATE INDEX "messages_is_pinned_idx" ON "messages"("is_pinned");
-- CreateIndex
CREATE INDEX "knowledge_bases_user_id_idx" ON "knowledge_bases"("user_id");
-- CreateIndex
CREATE INDEX "knowledge_bases_dify_dataset_id_idx" ON "knowledge_bases"("dify_dataset_id");
-- CreateIndex
CREATE INDEX "documents_kb_id_idx" ON "documents"("kb_id");
-- CreateIndex
CREATE INDEX "documents_user_id_idx" ON "documents"("user_id");
-- CreateIndex
CREATE INDEX "documents_status_idx" ON "documents"("status");
-- CreateIndex
CREATE INDEX "documents_dify_document_id_idx" ON "documents"("dify_document_id");
-- CreateIndex
CREATE INDEX "admin_logs_admin_id_idx" ON "admin_logs"("admin_id");
-- CreateIndex
CREATE INDEX "admin_logs_created_at_idx" ON "admin_logs"("created_at");
-- CreateIndex
CREATE INDEX "admin_logs_action_idx" ON "admin_logs"("action");
-- AddForeignKey
ALTER TABLE "projects" ADD CONSTRAINT "projects_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "conversations" ADD CONSTRAINT "conversations_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "conversations" ADD CONSTRAINT "conversations_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "messages" ADD CONSTRAINT "messages_conversation_id_fkey" FOREIGN KEY ("conversation_id") REFERENCES "conversations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "knowledge_bases" ADD CONSTRAINT "knowledge_bases_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "documents" ADD CONSTRAINT "documents_kb_id_fkey" FOREIGN KEY ("kb_id") REFERENCES "knowledge_bases"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "documents" ADD CONSTRAINT "documents_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "admin_logs" ADD CONSTRAINT "admin_logs_admin_id_fkey" FOREIGN KEY ("admin_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View File

@@ -0,0 +1,182 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ==================== 用户模块 ====================
model User {
id String @id @default(uuid())
email String @unique
password String
name String?
avatarUrl String? @map("avatar_url")
role String @default("user")
status String @default("active")
kbQuota Int @default(3) @map("kb_quota")
kbUsed Int @default(0) @map("kb_used")
trialEndsAt DateTime? @map("trial_ends_at")
isTrial Boolean @default(true) @map("is_trial")
lastLoginAt DateTime? @map("last_login_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
projects Project[]
conversations Conversation[]
knowledgeBases KnowledgeBase[]
documents Document[]
adminLogs AdminLog[]
@@index([email])
@@index([status])
@@index([createdAt])
@@map("users")
}
// ==================== 项目模块 ====================
model Project {
id String @id @default(uuid())
userId String @map("user_id")
name String
description String @db.Text
conversationCount Int @default(0) @map("conversation_count")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
conversations Conversation[]
@@index([userId])
@@index([createdAt])
@@map("projects")
}
// ==================== 对话模块 ====================
model Conversation {
id String @id @default(uuid())
userId String @map("user_id")
projectId String? @map("project_id")
agentId String @map("agent_id")
title String
modelName String @default("deepseek-v3") @map("model_name")
messageCount Int @default(0) @map("message_count")
totalTokens Int @default(0) @map("total_tokens")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
messages Message[]
@@index([userId])
@@index([projectId])
@@index([agentId])
@@index([createdAt])
@@map("conversations")
}
model Message {
id String @id @default(uuid())
conversationId String @map("conversation_id")
role String
content String @db.Text
metadata Json?
tokens Int?
isPinned Boolean @default(false) @map("is_pinned")
createdAt DateTime @default(now()) @map("created_at")
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
@@index([conversationId])
@@index([createdAt])
@@index([isPinned])
@@map("messages")
}
// ==================== 知识库模块 ====================
model KnowledgeBase {
id String @id @default(uuid())
userId String @map("user_id")
name String
description String?
difyDatasetId String @map("dify_dataset_id")
fileCount Int @default(0) @map("file_count")
totalSizeBytes BigInt @default(0) @map("total_size_bytes")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
documents Document[]
@@index([userId])
@@index([difyDatasetId])
@@map("knowledge_bases")
}
model Document {
id String @id @default(uuid())
kbId String @map("kb_id")
userId String @map("user_id")
filename String
fileType String @map("file_type")
fileSizeBytes BigInt @map("file_size_bytes")
fileUrl String @map("file_url")
difyDocumentId String @map("dify_document_id")
status String @default("uploading")
progress Int @default(0)
errorMessage String? @map("error_message")
segmentsCount Int? @map("segments_count")
tokensCount Int? @map("tokens_count")
uploadedAt DateTime @default(now()) @map("uploaded_at")
processedAt DateTime? @map("processed_at")
knowledgeBase KnowledgeBase @relation(fields: [kbId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([kbId])
@@index([userId])
@@index([status])
@@index([difyDocumentId])
@@map("documents")
}
// ==================== 运营管理模块 ====================
model AdminLog {
id Int @id @default(autoincrement())
adminId String @map("admin_id")
action String
resourceType String? @map("resource_type")
resourceId String? @map("resource_id")
details Json?
ipAddress String? @map("ip_address")
userAgent String? @map("user_agent")
createdAt DateTime @default(now()) @map("created_at")
admin User @relation(fields: [adminId], references: [id], onDelete: Cascade)
@@index([adminId])
@@index([createdAt])
@@index([action])
@@map("admin_logs")
}

View File

@@ -0,0 +1,35 @@
import { PrismaClient } from '@prisma/client';
// 创建Prisma Client实例
export const prisma = new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'info', 'warn', 'error'] : ['error'],
});
// 数据库连接测试
export async function testDatabaseConnection(): Promise<boolean> {
try {
await prisma.$connect();
console.log('✅ 数据库连接成功!');
// 获取数据库信息
const result = await prisma.$queryRaw<Array<{ version: string }>>`SELECT version()`;
console.log('📊 数据库版本:', result[0]?.version.split(' ')[0], result[0]?.version.split(' ')[1]);
return true;
} catch (error) {
console.error('❌ 数据库连接失败:', error);
return false;
}
}
// 优雅关闭数据库连接
export async function closeDatabaseConnection() {
await prisma.$disconnect();
console.log('👋 数据库连接已关闭');
}
// 进程退出时关闭连接
process.on('beforeExit', async () => {
await closeDatabaseConnection();
});

36
backend/src/config/env.ts Normal file
View File

@@ -0,0 +1,36 @@
import { config as dotenvConfig } from 'dotenv';
dotenvConfig();
export const config = {
// 服务器配置
nodeEnv: process.env.NODE_ENV || 'development',
port: parseInt(process.env.PORT || '3001', 10),
host: process.env.HOST || '0.0.0.0',
// 数据库配置
databaseUrl: process.env.DATABASE_URL || '',
// Redis配置
redisUrl: process.env.REDIS_URL || 'redis://localhost:6379',
// JWT配置
jwtSecret: process.env.JWT_SECRET || 'your-secret-key',
// 大模型API Keys
deepseekApiKey: process.env.DEEPSEEK_API_KEY || '',
qwenApiKey: process.env.QWEN_API_KEY || '',
geminiApiKey: process.env.GEMINI_API_KEY || '',
// Dify配置
difyApiUrl: process.env.DIFY_API_URL || 'http://localhost:5001',
difyApiKey: process.env.DIFY_API_KEY || '',
// 文件上传配置
uploadMaxSize: parseInt(process.env.UPLOAD_MAX_SIZE || '10485760', 10),
uploadDir: process.env.UPLOAD_DIR || './uploads',
// 日志配置
logLevel: process.env.LOG_LEVEL || 'info',
};

87
backend/src/index.ts Normal file
View File

@@ -0,0 +1,87 @@
import Fastify from 'fastify';
import cors from '@fastify/cors';
import { config } from './config/env.js';
import { testDatabaseConnection, prisma } from './config/database.js';
const fastify = Fastify({
logger: {
level: config.logLevel,
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'HH:MM:ss Z',
ignore: 'pid,hostname',
},
},
},
});
// 注册CORS插件
await fastify.register(cors, {
origin: true, // 开发环境允许所有来源
credentials: true,
});
// 健康检查路由
fastify.get('/health', async () => {
// 检查数据库连接
let dbStatus = 'unknown';
try {
await prisma.$queryRaw`SELECT 1`;
dbStatus = 'connected';
} catch {
dbStatus = 'disconnected';
}
return {
status: 'ok',
database: dbStatus,
timestamp: new Date().toISOString(),
uptime: process.uptime(),
};
});
// API路由前缀
fastify.get('/api/v1', async () => {
return {
message: 'AI Clinical Research Platform API',
version: '1.0.0',
environment: config.nodeEnv,
};
});
// 启动服务器
const start = async () => {
try {
// 测试数据库连接
console.log('🔍 正在测试数据库连接...');
const dbConnected = await testDatabaseConnection();
if (!dbConnected) {
console.error('❌ 数据库连接失败,无法启动服务器');
process.exit(1);
}
// 启动Fastify服务器
await fastify.listen({
port: config.port,
host: config.host,
});
console.log('\n' + '='.repeat(60));
console.log('🚀 AI临床研究平台 - 后端服务器启动成功!');
console.log('='.repeat(60));
console.log(`📍 服务地址: http://${config.host === '0.0.0.0' ? 'localhost' : config.host}:${config.port}`);
console.log(`🔍 健康检查: http://localhost:${config.port}/health`);
console.log(`📡 API入口: http://localhost:${config.port}/api/v1`);
console.log(`🌍 运行环境: ${config.nodeEnv}`);
console.log('='.repeat(60) + '\n');
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();

38
backend/tsconfig.json Normal file
View File

@@ -0,0 +1,38 @@
{
"compilerOptions": {
// File Layout
"rootDir": "./src",
"outDir": "./dist",
// Environment Settings
"module": "NodeNext",
"target": "ES2022",
"lib": ["ES2022"],
"types": ["node"],
// Module Resolution
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
// Other Outputs
"sourceMap": true,
"declaration": true,
// Stricter Typechecking Options
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
// Advanced Options
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

10
backend/启动后端.bat Normal file
View File

@@ -0,0 +1,10 @@
@echo off
chcp 65001 >nul
echo ====================================
echo AI临床研究平台 - 后端服务器
echo ====================================
echo.
cd /d %~dp0
call npm run dev
pause