Commit c6f4ee01 authored by soilchalk's avatar soilchalk
Browse files

feat: 聊天室项目核心代码 v1.0.0

- 实现完整的全栈聊天室应用
- 支持用户认证、多房间聊天、实时消息推送
- 包含前端(Next.js)和后端(API Routes)代码
- 支持Docker部署和数据库管理
- 包含管理员功能和测试用户
parent dacd5539
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;
// 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 = "sqlite"
url = "file:./dev.db"
}
// 用户表
model User {
id String @id @default(cuid())
username String @unique
email String @unique
password String
nickname String
avatar String?
status UserStatus @default(OFFLINE)
role UserRole @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
}
// 房间表
model Room {
roomId Int @id @default(autoincrement())
roomName String
creator String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 关系
messages Message[]
@@map("rooms")
}
// 消息表
model Message {
messageId Int @id @default(autoincrement())
roomId Int
sender String
content String
time BigInt // 时间戳
createdAt DateTime @default(now())
// 关系
room Room @relation(fields: [roomId], references: [roomId], onDelete: Cascade)
@@map("messages")
}
// 枚举类型
enum UserStatus {
ONLINE
OFFLINE
AWAY
}
enum UserRole {
ADMIN
USER
}
import { PrismaClient, UserRole } from '@prisma/client';
import bcrypt from 'bcryptjs';
const prisma = new PrismaClient();
async function main() {
console.log('🌱 开始创建种子数据...');
// 创建管理员账户
const adminPassword = await bcrypt.hash('admin123', 12);
const admin = await prisma.user.upsert({
where: { username: 'admin' },
update: {},
create: {
username: 'admin',
email: 'admin@chatroom.com',
password: adminPassword,
nickname: '系统管理员',
role: UserRole.ADMIN,
status: 'OFFLINE',
},
});
// 创建测试用户
const testUsers = [
{
username: 'alice',
email: 'alice@example.com',
password: 'alice123',
nickname: '爱丽丝',
role: UserRole.USER,
},
{
username: 'bob',
email: 'bob@example.com',
password: 'bob123',
nickname: '鲍勃',
role: UserRole.USER,
},
{
username: 'charlie',
email: 'charlie@example.com',
password: 'charlie123',
nickname: '查理',
role: UserRole.USER,
},
{
username: 'diana',
email: 'diana@example.com',
password: 'diana123',
nickname: '黛安娜',
role: UserRole.USER,
},
{
username: 'eve',
email: 'eve@example.com',
password: 'eve123',
nickname: '夏娃',
role: UserRole.USER,
},
];
for (const userData of testUsers) {
const hashedPassword = await bcrypt.hash(userData.password, 12);
await prisma.user.upsert({
where: { username: userData.username },
update: {},
create: {
username: userData.username,
email: userData.email,
password: hashedPassword,
nickname: userData.nickname,
role: userData.role,
status: 'OFFLINE',
},
});
}
// 创建测试房间
const testRooms = [
{
roomName: '欢迎大厅',
creator: 'admin',
},
{
roomName: '技术讨论',
creator: 'alice',
},
{
roomName: '闲聊天地',
creator: 'bob',
},
{
roomName: '游戏交流',
creator: 'charlie',
},
];
for (const roomData of testRooms) {
const existingRoom = await prisma.room.findFirst({
where: { roomName: roomData.roomName },
});
if (!existingRoom) {
await prisma.room.create({
data: {
roomName: roomData.roomName,
creator: roomData.creator,
},
});
}
}
// 创建一些测试消息
const welcomeRoom = await prisma.room.findFirst({
where: { roomName: '欢迎大厅' },
});
if (welcomeRoom) {
const welcomeMessages = [
{
roomId: welcomeRoom.roomId,
sender: 'admin',
content: '欢迎来到聊天室!',
time: BigInt(Date.now() - 3600000), // 1小时前
},
{
roomId: welcomeRoom.roomId,
sender: 'alice',
content: '大家好,我是爱丽丝!',
time: BigInt(Date.now() - 3000000), // 50分钟前
},
{
roomId: welcomeRoom.roomId,
sender: 'bob',
content: '很高兴认识大家!',
time: BigInt(Date.now() - 2400000), // 40分钟前
},
];
for (const messageData of welcomeMessages) {
await prisma.message.create({
data: messageData,
});
}
}
console.log('✅ 种子数据创建完成!');
console.log('\n📋 测试账户列表:');
console.log('┌─────────────┬─────────────┬─────────────┬─────────────┐');
console.log('│ 用户名 │ 密码 │ 昵称 │ 角色 │');
console.log('├─────────────┼─────────────┼─────────────┼─────────────┤');
console.log('│ admin │ admin123 │ 系统管理员 │ 管理员 │');
console.log('│ alice │ alice123 │ 爱丽丝 │ 普通用户 │');
console.log('│ bob │ bob123 │ 鲍勃 │ 普通用户 │');
console.log('│ charlie │ charlie123 │ 查理 │ 普通用户 │');
console.log('│ diana │ diana123 │ 黛安娜 │ 普通用户 │');
console.log('│ eve │ eve123 │ 夏娃 │ 普通用户 │');
console.log('└─────────────┴─────────────┴─────────────┴─────────────┘');
console.log('\n🏠 测试房间列表:');
console.log('• 欢迎大厅 (管理员创建)');
console.log('• 技术讨论 (爱丽丝创建)');
console.log('• 闲聊天地 (鲍勃创建)');
console.log('• 游戏交流 (查理创建)');
console.log('\n💡 使用说明:');
console.log('1. 管理员账户可以删除任何房间和消息');
console.log('2. 普通用户只能删除自己创建的房间');
console.log('3. 所有用户都可以发送消息');
console.log('4. 使用上述账户登录测试功能');
}
main()
.catch((e) => {
console.error('❌ 种子数据创建失败:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
#!/bin/bash
# 管理员账户设置脚本
# 用于快速设置内置管理员账户和测试数据
echo "🚀 开始设置管理员账户和测试数据..."
# 检查是否在正确的目录
if [ ! -f "package.json" ]; then
echo "❌ 请在项目根目录运行此脚本"
exit 1
fi
# 安装依赖
echo "📦 安装依赖..."
pnpm install
# 生成 Prisma 客户端
echo "🔧 生成 Prisma 客户端..."
pnpm db:generate
# 重置数据库并运行迁移
echo "🗄️ 重置数据库..."
pnpm db:reset --force
# 运行种子数据
echo "🌱 创建种子数据..."
pnpm db:seed
# 运行管理员功能测试
echo "🧪 测试管理员功能..."
pnpm tsx scripts/test-admin.ts
echo ""
echo "✅ 设置完成!"
echo ""
echo "📋 测试账户信息:"
echo "┌─────────────┬─────────────┬─────────────┬─────────────┐"
echo "│ 用户名 │ 密码 │ 昵称 │ 角色 │"
echo "├─────────────┼─────────────┼─────────────┼─────────────┤"
echo "│ admin │ admin123 │ 系统管理员 │ 管理员 │"
echo "│ alice │ alice123 │ 爱丽丝 │ 普通用户 │"
echo "│ bob │ bob123 │ 鲍勃 │ 普通用户 │"
echo "│ charlie │ charlie123 │ 查理 │ 普通用户 │"
echo "│ diana │ diana123 │ 黛安娜 │ 普通用户 │"
echo "│ eve │ eve123 │ 夏娃 │ 普通用户 │"
echo "└─────────────┴─────────────┴─────────────┴─────────────┘"
echo ""
echo "🏠 测试房间:"
echo "• 欢迎大厅 (管理员创建)"
echo "• 技术讨论 (爱丽丝创建)"
echo "• 闲聊天地 (鲍勃创建)"
echo "• 游戏交流 (查理创建)"
echo ""
echo "🚀 启动开发服务器:"
echo "pnpm dev"
echo ""
echo "🌐 访问地址:"
echo "• 主应用: http://localhost:3000"
echo "• 管理员页面: http://localhost:3000/admin"
echo "• 数据库管理: pnpm db:studio"
echo ""
echo "💡 使用说明:"
echo "1. 使用 admin/admin123 登录系统"
echo "2. 管理员可以删除任何房间和消息"
echo "3. 普通用户只能管理自己创建的内容"
echo "4. 访问 /admin 页面查看用户管理界面"
#!/usr/bin/env tsx
/**
* 管理员功能测试脚本
* 用于验证内置管理员账户和权限功能
*/
import { PrismaClient, UserRole } from '@prisma/client';
import bcrypt from 'bcryptjs';
const prisma = new PrismaClient();
async function testAdminFeatures() {
console.log('🔐 测试管理员功能...\n');
try {
// 1. 验证管理员账户存在
console.log('1️⃣ 验证管理员账户...');
const admin = await prisma.user.findUnique({
where: { username: 'admin' },
});
if (!admin) {
console.log('❌ 管理员账户不存在,请先运行: pnpm db:seed');
return;
}
console.log('✅ 管理员账户存在');
console.log(` 用户名: ${admin.username}`);
console.log(` 昵称: ${admin.nickname}`);
console.log(` 角色: ${admin.role}`);
console.log(` 邮箱: ${admin.email}`);
// 2. 验证密码
console.log('\n2️⃣ 验证管理员密码...');
const isPasswordValid = await bcrypt.compare('admin123', admin.password);
console.log(isPasswordValid ? '✅ 密码验证通过' : '❌ 密码验证失败');
// 3. 显示所有测试用户
console.log('\n3️⃣ 显示所有测试用户...');
const allUsers = await prisma.user.findMany({
select: {
username: true,
nickname: true,
email: true,
role: true,
status: true,
createdAt: true,
},
orderBy: { createdAt: 'asc' },
});
console.log('┌─────────────┬─────────────┬─────────────────────────┬─────────────┬─────────────┐');
console.log('│ 用户名 │ 昵称 │ 邮箱 │ 角色 │ 状态 │');
console.log('├─────────────┼─────────────┼─────────────────────────┼─────────────┼─────────────┤');
allUsers.forEach(user => {
const roleIcon = user.role === UserRole.ADMIN ? '👑' : '👤';
const statusIcon = user.status === 'ONLINE' ? '🟢' : '';
console.log(`│ ${user.username.padEnd(11)}${user.nickname.padEnd(11)}${user.email.padEnd(23)}${roleIcon} ${user.role.padEnd(9)}${statusIcon} ${user.status.padEnd(9)} │`);
});
console.log('└─────────────┴─────────────┴─────────────────────────┴─────────────┴─────────────┘');
// 4. 显示所有房间
console.log('\n4️⃣ 显示所有房间...');
const allRooms = await prisma.room.findMany({
include: {
messages: {
take: 1,
orderBy: { createdAt: 'desc' },
},
},
orderBy: { createdAt: 'asc' },
});
console.log('┌─────────────┬─────────────┬─────────────┬─────────────┐');
console.log('│ 房间ID │ 房间名称 │ 创建者 │ 最后消息 │');
console.log('├─────────────┼─────────────┼─────────────┼─────────────┤');
allRooms.forEach(room => {
const lastMessage = room.messages[0]?.content || '暂无消息';
const truncatedMessage = lastMessage.length > 15 ? lastMessage.substring(0, 15) + '...' : lastMessage;
console.log(`│ ${room.roomId.toString().padEnd(11)}${room.roomName.padEnd(11)}${room.creator.padEnd(11)}${truncatedMessage.padEnd(11)} │`);
});
console.log('└─────────────┴─────────────┴─────────────┴─────────────┘');
// 5. 统计信息
console.log('\n5️⃣ 系统统计信息...');
const userCount = await prisma.user.count();
const roomCount = await prisma.room.count();
const messageCount = await prisma.message.count();
const adminCount = await prisma.user.count({
where: { role: UserRole.ADMIN },
});
console.log(`👥 总用户数: ${userCount}`);
console.log(`👑 管理员数: ${adminCount}`);
console.log(`🏠 总房间数: ${roomCount}`);
console.log(`💬 总消息数: ${messageCount}`);
// 6. 测试管理员权限
console.log('\n6️⃣ 测试管理员权限...');
// 管理员可以删除任何房间
const testRoom = await prisma.room.findFirst({
where: { roomName: '技术讨论' },
});
if (testRoom) {
console.log(`✅ 管理员可以访问房间: ${testRoom.roomName} (ID: ${testRoom.roomId})`);
}
// 管理员可以查看所有消息
const allMessages = await prisma.message.findMany({
take: 5,
orderBy: { createdAt: 'desc' },
include: {
room: {
select: { roomName: true },
},
},
});
console.log('\n📝 最近5条消息:');
allMessages.forEach((msg, index) => {
console.log(` ${index + 1}. [${msg.room.roomName}] ${msg.sender}: ${msg.content}`);
});
console.log('\n✅ 管理员功能测试完成!');
console.log('\n💡 使用说明:');
console.log('1. 使用 admin/admin123 登录系统');
console.log('2. 管理员可以删除任何房间和消息');
console.log('3. 管理员可以查看所有用户信息');
console.log('4. 普通用户只能管理自己创建的内容');
} catch (error) {
console.error('❌ 测试过程中出现错误:', error);
} finally {
await prisma.$disconnect();
}
}
// 运行测试
testAdminFeatures();
'use client';
import { useState, useEffect } from 'react';
import { Table, Card, Tag, Button, Space, message, Modal } from 'antd';
import { UserOutlined, CrownOutlined, DeleteOutlined } from '@ant-design/icons';
import { User, UserRole, UserStatus } from '@/types/chat';
interface UserWithActions extends User {
key: string;
}
export default function AdminPage() {
const [users, setUsers] = useState<UserWithActions[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUsers();
}, []);
const fetchUsers = async () => {
try {
setLoading(true);
// 这里应该调用实际的API,暂时使用模拟数据
const mockUsers: UserWithActions[] = [
{
id: '1',
username: 'admin',
email: 'admin@chatroom.com',
nickname: '系统管理员',
status: UserStatus.OFFLINE,
role: UserRole.ADMIN,
createdAt: new Date(),
updatedAt: new Date(),
key: '1',
},
{
id: '2',
username: 'alice',
email: 'alice@example.com',
nickname: '爱丽丝',
status: UserStatus.ONLINE,
role: UserRole.USER,
createdAt: new Date(),
updatedAt: new Date(),
key: '2',
},
{
id: '3',
username: 'bob',
email: 'bob@example.com',
nickname: '鲍勃',
status: UserStatus.OFFLINE,
role: UserRole.USER,
createdAt: new Date(),
updatedAt: new Date(),
key: '3',
},
{
id: '4',
username: 'charlie',
email: 'charlie@example.com',
nickname: '查理',
status: UserStatus.AWAY,
role: UserRole.USER,
createdAt: new Date(),
updatedAt: new Date(),
key: '4',
},
{
id: '5',
username: 'diana',
email: 'diana@example.com',
nickname: '黛安娜',
status: UserStatus.ONLINE,
role: UserRole.USER,
createdAt: new Date(),
updatedAt: new Date(),
key: '5',
},
{
id: '6',
username: 'eve',
email: 'eve@example.com',
nickname: '夏娃',
status: UserStatus.OFFLINE,
role: UserRole.USER,
createdAt: new Date(),
updatedAt: new Date(),
key: '6',
},
];
setUsers(mockUsers);
} catch (error) {
message.error('获取用户列表失败');
} finally {
setLoading(false);
}
};
const getStatusColor = (status: UserStatus) => {
switch (status) {
case UserStatus.ONLINE:
return 'success';
case UserStatus.AWAY:
return 'warning';
case UserStatus.OFFLINE:
return 'default';
default:
return 'default';
}
};
const getStatusText = (status: UserStatus) => {
switch (status) {
case UserStatus.ONLINE:
return '在线';
case UserStatus.AWAY:
return '离开';
case UserStatus.OFFLINE:
return '离线';
default:
return '未知';
}
};
const getRoleColor = (role: UserRole) => {
return role === UserRole.ADMIN ? 'red' : 'blue';
};
const getRoleText = (role: UserRole) => {
return role === UserRole.ADMIN ? '管理员' : '普通用户';
};
const handleDeleteUser = (user: UserWithActions) => {
if (user.role === UserRole.ADMIN) {
message.warning('不能删除管理员账户');
return;
}
Modal.confirm({
title: '确认删除',
content: `确定要删除用户 "${user.nickname}" 吗?此操作不可撤销。`,
okText: '确认删除',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
// 这里应该调用实际的删除API
message.success('用户删除成功');
fetchUsers();
} catch (error) {
message.error('删除用户失败');
}
},
});
};
const columns = [
{
title: '用户信息',
key: 'userInfo',
render: (record: UserWithActions) => (
<Space>
{record.role === UserRole.ADMIN ? (
<CrownOutlined style={{ color: '#ff4d4f' }} />
) : (
<UserOutlined />
)}
<div>
<div style={{ fontWeight: 'bold' }}>{record.nickname}</div>
<div style={{ fontSize: '12px', color: '#666' }}>@{record.username}</div>
</div>
</Space>
),
},
{
title: '邮箱',
dataIndex: 'email',
key: 'email',
},
{
title: '角色',
dataIndex: 'role',
key: 'role',
render: (role: UserRole) => (
<Tag color={getRoleColor(role)}>
{getRoleText(role)}
</Tag>
),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: UserStatus) => (
<Tag color={getStatusColor(status)}>
{getStatusText(status)}
</Tag>
),
},
{
title: '注册时间',
dataIndex: 'createdAt',
key: 'createdAt',
render: (date: Date) => new Date(date).toLocaleDateString(),
},
{
title: '操作',
key: 'actions',
render: (record: UserWithActions) => (
<Space>
<Button
type="link"
size="small"
onClick={() => message.info('查看详情功能待实现')}
>
查看详情
</Button>
{record.role !== UserRole.ADMIN && (
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => handleDeleteUser(record)}
>
删除
</Button>
)}
</Space>
),
},
];
return (
<div style={{ padding: '24px' }}>
<Card title="用户管理" extra={
<Button type="primary" onClick={fetchUsers}>
刷新
</Button>
}>
<Table
columns={columns}
dataSource={users}
loading={loading}
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `共 ${total} 个用户`,
}}
/>
</Card>
<Card title="测试账户信息" style={{ marginTop: '24px' }}>
<div style={{ marginBottom: '16px' }}>
<h4>👑 管理员账户</h4>
<p><strong>用户名:</strong> admin</p>
<p><strong>密码:</strong> admin123</p>
<p><strong>权限:</strong> 可以删除任何房间和消息,查看所有用户信息</p>
</div>
<div style={{ marginBottom: '16px' }}>
<h4>👤 测试用户账户</h4>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '16px' }}>
<div>
<p><strong>alice / alice123</strong> - 爱丽丝</p>
<p><strong>bob / bob123</strong> - 鲍勃</p>
<p><strong>charlie / charlie123</strong> - 查理</p>
</div>
<div>
<p><strong>diana / diana123</strong> - 黛安娜</p>
<p><strong>eve / eve123</strong> - 夏娃</p>
</div>
</div>
</div>
<div>
<h4>🏠 测试房间</h4>
<ul>
<li>欢迎大厅 (管理员创建)</li>
<li>技术讨论 (爱丽丝创建)</li>
<li>闲聊天地 (鲍勃创建)</li>
<li>游戏交流 (查理创建)</li>
</ul>
</div>
</Card>
</div>
);
}
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/db';
import { verifyPassword, generateAccessToken, generateRefreshToken } from '@/lib/auth';
import { LoginRequest, ApiResponse, AuthResponse, UserStatus } from '@/types/auth';
/**
* 用户登录 API
* POST /api/auth/login
*/
export async function POST(request: NextRequest) {
try {
// 解析请求体
const body: LoginRequest = await request.json();
const { username, password } = body;
// 参数验证
if (!username || !password) {
return NextResponse.json(
{
message: '用户名和密码不能为空',
code: 400,
data: null
} as ApiResponse,
{ status: 400 }
);
}
// 查找用户
const user = await prisma.user.findFirst({
where: {
OR: [
{ username },
{ email: username } // 支持邮箱登录
]
}
});
if (!user) {
return NextResponse.json(
{
message: '用户名或密码错误',
code: 401,
data: null
} as ApiResponse,
{ status: 401 }
);
}
// 验证密码
const isPasswordValid = await verifyPassword(password, user.password);
if (!isPasswordValid) {
return NextResponse.json(
{
message: '用户名或密码错误',
code: 401,
data: null
} as ApiResponse,
{ status: 401 }
);
}
// 更新用户状态为在线
await prisma.user.update({
where: { id: user.id },
data: { status: 'ONLINE' }
});
// 生成令牌
const accessToken = generateAccessToken({
userId: user.id,
username: user.username,
email: user.email
});
const refreshToken = generateRefreshToken(user.id);
// 构建响应数据
const authResponse: AuthResponse = {
user: {
id: user.id,
username: user.username,
email: user.email,
nickname: user.nickname,
avatar: user.avatar || undefined,
status: UserStatus.ONLINE,
createdAt: user.createdAt,
updatedAt: user.updatedAt
},
accessToken,
refreshToken
};
console.log(`✅ 用户登录成功: ${user.username} (${user.id})`);
return NextResponse.json(
{
message: '登录成功',
code: 0,
data: authResponse
} as ApiResponse<AuthResponse>,
{ status: 200 }
);
} catch (error) {
console.error('用户登录失败:', error);
return NextResponse.json(
{
message: '服务器内部错误',
code: 500,
data: null
} as ApiResponse,
{ status: 500 }
);
}
}
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/db';
import { Prisma } from '@prisma/client';
import { hashPassword, generateAccessToken, generateRefreshToken } from '@/lib/auth';
import { RegisterRequest, ApiResponse, AuthResponse, UserStatus } from '@/types/auth';
/**
* 用户注册 API
* POST /api/auth/register
*/
export async function POST(request: NextRequest) {
try {
// 开发期邮箱白名单(仅用于调试场景)
const EMAIL_WHITELIST = ['210914403@qq.com'];
// 解析请求体
const body: RegisterRequest = await request.json();
const { username, email, password, nickname } = body;
// 参数验证
if (!username || !email || !password || !nickname) {
return NextResponse.json(
{
message: '缺少必要参数',
code: 400,
data: null
} as ApiResponse,
{ status: 400 }
);
}
// 用户名长度验证
if (username.length < 3 || username.length > 20) {
return NextResponse.json(
{
message: '用户名长度必须在3-20个字符之间',
code: 400,
data: null
} as ApiResponse,
{ status: 400 }
);
}
// 邮箱格式验证
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return NextResponse.json(
{
message: '邮箱格式不正确',
code: 400,
data: null
} as ApiResponse,
{ status: 400 }
);
}
// 密码强度验证
if (password.length < 6) {
return NextResponse.json(
{
message: '密码长度至少6个字符',
code: 400,
data: null
} as ApiResponse,
{ status: 400 }
);
}
// 昵称长度验证
if (nickname.length < 2 || nickname.length > 20) {
return NextResponse.json(
{
message: '昵称长度必须在2-20个字符之间',
code: 400,
data: null
} as ApiResponse,
{ status: 400 }
);
}
// 检查用户名是否为admin(不区分大小写)
if (username.toLowerCase() === 'admin') {
return NextResponse.json(
{
message: 'admin用户名已被保留,请选择其他用户名',
code: 400,
data: null
} as ApiResponse,
{ status: 400 }
);
}
// 检查昵称是否为admin(不区分大小写)
if (nickname.toLowerCase() === 'admin') {
return NextResponse.json(
{
message: 'admin昵称已被保留,请选择其他昵称',
code: 400,
data: null
} as ApiResponse,
{ status: 400 }
);
}
// 测试账户保留:禁止注册 test/test@qq.com(无论是否已存在)
if (username.toLowerCase() === 'test') {
return NextResponse.json(
{
message: '用户名已存在',
code: 409,
data: null
} as ApiResponse,
{ status: 409 }
);
}
if (email.toLowerCase() === 'test@qq.com') {
return NextResponse.json(
{
message: '邮箱已被注册',
code: 409,
data: null
} as ApiResponse,
{ status: 409 }
);
}
// 检查用户名/邮箱是否已存在
const existingUser = await prisma.user.findFirst({
where: {
OR: [
{ username },
{ email }
]
}
});
if (existingUser) {
// 用户名冲突
if (existingUser.username === username) {
return NextResponse.json(
{
message: '用户名已存在',
code: 409,
data: null
} as ApiResponse,
{ status: 409 }
);
}
// 邮箱冲突
if (existingUser.email === email) {
// 如果是白名单邮箱,直接签发登录态返回(方便调试)
if (EMAIL_WHITELIST.includes(email)) {
const accessTokenForExisting = generateAccessToken({
userId: existingUser.id,
username: existingUser.username,
email: existingUser.email
});
const refreshTokenForExisting = generateRefreshToken(existingUser.id);
const authResponseExisting: AuthResponse = {
user: {
id: existingUser.id,
username: existingUser.username,
email: existingUser.email,
nickname: existingUser.nickname,
avatar: existingUser.avatar || undefined,
status: existingUser.status as UserStatus,
createdAt: existingUser.createdAt,
updatedAt: existingUser.updatedAt
},
accessToken: accessTokenForExisting,
refreshToken: refreshTokenForExisting
};
return NextResponse.json(
{
message: '白名单邮箱已存在,已为您自动登录',
code: 0,
data: authResponseExisting
} as ApiResponse<AuthResponse>,
{ status: 200 }
);
}
return NextResponse.json(
{
message: '邮箱已被注册',
code: 409,
data: null
} as ApiResponse,
{ status: 409 }
);
}
}
// 加密密码
const hashedPassword = await hashPassword(password);
// 创建用户
const user = await prisma.user.create({
data: {
username,
email,
password: hashedPassword,
nickname,
status: 'OFFLINE'
}
});
// 生成令牌
const accessToken = generateAccessToken({
userId: user.id,
username: user.username,
email: user.email
});
const refreshToken = generateRefreshToken(user.id);
// 构建响应数据
const authResponse: AuthResponse = {
user: {
id: user.id,
username: user.username,
email: user.email,
nickname: user.nickname,
avatar: user.avatar || undefined,
status: user.status as UserStatus,
createdAt: user.createdAt,
updatedAt: user.updatedAt
},
accessToken,
refreshToken
};
console.log(`✅ 用户注册成功: ${username} (${user.id})`);
return NextResponse.json(
{
message: '用户注册成功',
code: 0,
data: authResponse
} as ApiResponse<AuthResponse>,
{ status: 201 }
);
} catch (error) {
console.error('用户注册失败:', error);
// 捕获数据库唯一约束错误,确保重复检测来自DB
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
const target = (error.meta?.target as string[]) || [];
// 根据冲突字段返回精确提示
if (target.includes('username') || target.join(',').includes('username')) {
return NextResponse.json(
{ message: '用户名已存在', code: 409, data: null } as ApiResponse,
{ status: 409 }
);
}
if (target.includes('email') || target.join(',').includes('email')) {
return NextResponse.json(
{ message: '邮箱已被注册', code: 409, data: null } as ApiResponse,
{ status: 409 }
);
}
// 未知字段冲突,通用重复提示
return NextResponse.json(
{ message: '信息已存在', code: 409, data: null } as ApiResponse,
{ status: 409 }
);
}
return NextResponse.json(
{
message: '服务器内部错误',
code: 500,
data: null
} as ApiResponse,
{ status: 500 }
);
}
}
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/db';
import { hashPassword, generateAccessToken, generateRefreshToken } from '@/lib/auth';
import { ApiResponse, AuthResponse, UserStatus } from '@/types/auth';
/**
* 开发/运维用:内置管理员账户
* 账号信息(按需求):
* 用户名:soilchalk;邮箱:210914403@qq.com;昵称:admin;密码:Xzr@210914403
* POST /api/auth/seed-admin
*/
export async function POST(_request: NextRequest) {
try {
// 环境开关:默认关闭,避免误暴露
if (process.env.ALLOW_ADMIN_SEED !== 'true') {
return NextResponse.json({ message: 'Not Found', code: 404, data: null } as ApiResponse, { status: 404 });
}
// 从环境变量读取管理员信息,避免硬编码凭据
const username = process.env.ADMIN_USERNAME || '';
const email = process.env.ADMIN_EMAIL || '';
const nickname = process.env.ADMIN_NICKNAME || 'admin';
const passwordPlain = process.env.ADMIN_PASSWORD || '';
if (!username || !email || !passwordPlain) {
return NextResponse.json(
{ message: '管理员种子变量未配置', code: 403, data: null } as ApiResponse,
{ status: 403 }
);
}
// 如果已存在则复用
let user = await prisma.user.findFirst({ where: { OR: [{ username }, { email }] } });
if (!user) {
const hashedPassword = await hashPassword(passwordPlain);
user = await prisma.user.create({
data: {
username,
email,
password: hashedPassword,
nickname,
status: 'OFFLINE',
},
});
console.log(`✅ 已创建内置管理员: ${username} (${user.id})`);
} else {
// 确保昵称为 admin
if (user.nickname !== nickname) {
user = await prisma.user.update({ where: { id: user.id }, data: { nickname } });
}
console.log(`ℹ️ 管理员已存在: ${username} (${user.id})`);
}
const accessToken = generateAccessToken({ userId: user.id, username: user.username, email: user.email });
const refreshToken = generateRefreshToken(user.id);
const authResponse: AuthResponse = {
user: {
id: user.id,
username: user.username,
email: user.email,
nickname: user.nickname,
avatar: user.avatar || undefined,
status: user.status as UserStatus,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
},
accessToken,
refreshToken,
};
return NextResponse.json(
{ message: '管理员账户就绪', code: 0, data: authResponse } as ApiResponse<AuthResponse>,
{ status: 200 }
);
} catch (error) {
console.error('创建管理员账户失败:', error);
return NextResponse.json(
{ message: '服务器内部错误', code: 500, data: null } as ApiResponse,
{ status: 500 }
);
}
}
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/db';
import { hashPassword, generateAccessToken, generateRefreshToken } from '@/lib/auth';
import { ApiResponse, AuthResponse, UserStatus } from '@/types/auth';
/**
* 开发用:创建测试账户
* POST /api/auth/seed-test-user
*/
export async function POST(_request: NextRequest) {
try {
if (process.env.ALLOW_TEST_SEED !== 'true') {
return NextResponse.json({ message: 'Not Found', code: 404, data: null } as ApiResponse, { status: 404 });
}
const username = 'test';
const email = 'test@qq.com';
const nickname = 'test';
const passwordPlain = 'test000';
const existing = await prisma.user.findFirst({ where: { OR: [{ username }, { email }] } });
let user = existing;
if (!existing) {
const hashedPassword = await hashPassword(passwordPlain);
user = await prisma.user.create({
data: {
username,
email,
password: hashedPassword,
nickname,
status: 'OFFLINE',
},
});
console.log(`✅ 已创建测试用户: ${username} (${user.id})`);
} else {
console.log(`ℹ️ 测试用户已存在: ${username} (${existing.id})`);
}
if (!user) {
return NextResponse.json(
{ message: '创建测试用户失败', code: 500, data: null } as ApiResponse,
{ status: 500 }
);
}
const accessToken = generateAccessToken({ userId: user.id, username: user.username, email: user.email });
const refreshToken = generateRefreshToken(user.id);
const authResponse: AuthResponse = {
user: {
id: user.id,
username: user.username,
email: user.email,
nickname: user.nickname,
avatar: user.avatar || undefined,
status: user.status as UserStatus,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
},
accessToken,
refreshToken,
};
return NextResponse.json(
{ message: '测试用户就绪', code: 0, data: authResponse } as ApiResponse<AuthResponse>,
{ status: 200 }
);
} catch (error) {
console.error('创建测试用户失败:', error);
return NextResponse.json(
{ message: '服务器内部错误', code: 500, data: null } as ApiResponse,
{ status: 500 }
);
}
}
import { NextRequest, NextResponse } from 'next/server';
import { checkDatabaseHealth } from '@/lib/db';
/**
* 健康检查 API
* 用于验证项目是否正常运行
*/
export async function GET(request: NextRequest) {
try {
// 获取当前时间
const now = new Date();
// 检查数据库健康状态
const dbHealth = await checkDatabaseHealth();
// 构建响应数据
const healthData = {
status: dbHealth.status === 'healthy' ? 'healthy' : 'degraded',
timestamp: now.toISOString(),
uptime: process.uptime(),
environment: process.env.NODE_ENV || 'development',
version: process.env.npm_package_version || '1.0.0',
memory: {
used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024),
external: Math.round(process.memoryUsage().external / 1024 / 1024),
},
services: {
database: dbHealth.status,
redis: 'unknown', // 后续可以添加Redis连接检查
},
database: dbHealth
};
return NextResponse.json(healthData, { status: 200 });
} catch (error) {
console.error('Health check error:', error);
return NextResponse.json(
{
status: 'unhealthy',
timestamp: new Date().toISOString(),
error: 'Health check failed',
details: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
);
}
}
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/db';
import { publishToRoom } from '@/lib/sse';
import { MessageAddArgs, ApiResponse } from '@/types/chat';
/**
* 添加消息 API
* POST /api/message/add
*/
export async function POST(request: NextRequest) {
try {
// 解析请求体
const body: MessageAddArgs = await request.json();
const { roomId, content, sender } = body;
// 参数验证
if (!roomId || !content || !sender) {
return NextResponse.json(
{
message: '缺少必要参数',
code: 400,
data: null
} as ApiResponse,
{ status: 400 }
);
}
// 检查房间是否存在
const room = await prisma.room.findUnique({
where: { roomId }
});
if (!room) {
return NextResponse.json(
{
message: '房间不存在',
code: 404,
data: null
} as ApiResponse,
{ status: 404 }
);
}
// 消息内容长度验证
if (content.length === 0 || content.length > 500) {
return NextResponse.json(
{
message: '消息内容长度必须在1-500个字符之间',
code: 400,
data: null
} as ApiResponse,
{ status: 400 }
);
}
// 创建消息
const message = await prisma.message.create({
data: {
roomId,
sender,
content,
time: BigInt(Date.now())
}
});
// 更新房间的更新时间
await prisma.room.update({
where: { roomId },
data: { updatedAt: new Date() }
});
// 推送SSE给订阅者
publishToRoom(roomId, {
type: 'message',
payload: {
messageId: message.messageId,
roomId: message.roomId,
sender: message.sender,
content: message.content,
time: Number(message.time),
createdAt: message.createdAt,
},
});
console.log(`✅ 消息发送成功: ${sender} -> ${room.roomName} (${message.messageId})`);
return NextResponse.json(
{
message: '消息发送成功',
code: 0,
data: null
} as ApiResponse,
{ status: 201 }
);
} catch (error) {
console.error('消息发送失败:', error);
return NextResponse.json(
{
message: '服务器内部错误',
code: 500,
data: null
} as ApiResponse,
{ status: 500 }
);
}
}
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/db';
import { RoomAddArgs, ApiResponse, RoomAddRes } from '@/types/chat';
/**
* 创建房间 API
* POST /api/room/add
*/
export async function POST(request: NextRequest) {
try {
// 解析请求体
const body: RoomAddArgs = await request.json();
const { user, roomName } = body;
// 参数验证
if (!user || !roomName) {
return NextResponse.json(
{
message: '缺少必要参数',
code: 400,
data: null
} as ApiResponse,
{ status: 400 }
);
}
// 房间名称长度验证
if (roomName.length < 2 || roomName.length > 50) {
return NextResponse.json(
{
message: '房间名称长度必须在2-50个字符之间',
code: 400,
data: null
} as ApiResponse,
{ status: 400 }
);
}
// 检查房间名称是否已存在
const existingRoom = await prisma.room.findFirst({
where: { roomName }
});
if (existingRoom) {
return NextResponse.json(
{
message: '房间名称已存在',
code: 409,
data: null
} as ApiResponse,
{ status: 409 }
);
}
// 创建房间
const room = await prisma.room.create({
data: {
roomName,
creator: user
}
});
console.log(`✅ 房间创建成功: ${roomName} (${room.roomId})`);
const response: RoomAddRes = {
roomId: room.roomId
};
return NextResponse.json(
{
message: '房间创建成功',
code: 0,
data: response
} as ApiResponse<RoomAddRes>,
{ status: 201 }
);
} catch (error) {
console.error('房间创建失败:', error);
return NextResponse.json(
{
message: '服务器内部错误',
code: 500,
data: null
} as ApiResponse,
{ status: 500 }
);
}
}
This diff is collapsed.
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/db';
import { ApiResponse, RoomListRes } from '@/types/chat';
/**
* 获取房间列表 API
* GET /api/room/list
*/
export async function GET(request: NextRequest) {
try {
// 获取所有房间
const rooms = await prisma.room.findMany({
orderBy: { updatedAt: 'desc' }
});
// 获取每个房间的最后一条消息
const roomsWithLastMessage = await Promise.all(
rooms.map(async (room) => {
const lastMessage = await prisma.message.findFirst({
where: { roomId: room.roomId },
orderBy: { time: 'desc' }
});
return {
roomId: room.roomId,
roomName: room.roomName,
lastMessage: lastMessage ? {
messageId: lastMessage.messageId,
roomId: lastMessage.roomId,
sender: lastMessage.sender,
content: lastMessage.content,
time: Number(lastMessage.time) // 将 BigInt 转换为 Number
} : null
};
})
);
const response: RoomListRes = {
rooms: roomsWithLastMessage
};
return NextResponse.json(
{
message: '获取房间列表成功',
code: 0,
data: response
} as ApiResponse<RoomListRes>,
{ status: 200 }
);
} catch (error) {
console.error('获取房间列表失败:', error);
return NextResponse.json(
{
message: '服务器内部错误',
code: 500,
data: null
} as ApiResponse,
{ status: 500 }
);
}
}
This diff is collapsed.
This diff is collapsed.
.chatLayout {
height: 100vh;
background: #fff;
}
.sider {
background: #fafafa;
border-right: 1px solid #e8e8e8;
overflow: hidden;
}
.content {
background: #fff;
overflow: hidden;
display: flex;
flex-direction: column;
}
This diff is collapsed.
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment