引言:为什么需要一个个性化留言板?

在当今的互联网环境中,用户互动是网站和应用成功的关键。无论是个人博客、企业官网还是社区论坛,一个功能完善、个性化的留言板都能显著提升用户参与度和粘性。传统的留言板往往功能单一、界面陈旧,无法满足现代用户对个性化、即时互动的需求。

本文将从零开始,教你如何搭建一个功能丰富、可定制化的个性化留言系统。我们将使用现代Web技术栈(Node.js + Express + MongoDB + React)来构建这个系统,确保它既易于扩展又具备良好的用户体验。通过这个教程,你将学会:

  1. 设计留言系统的数据结构和API接口
  2. 实现用户认证和权限管理
  3. 构建响应式的前端界面
  4. 添加实时通知和互动功能
  5. 部署和优化你的留言系统

第一部分:系统设计与技术选型

1.1 系统架构设计

一个完整的留言板系统通常包含以下几个核心模块:

  • 用户模块:注册、登录、个人资料管理
  • 留言模块:创建、编辑、删除、回复留言
  • 互动模块:点赞、评论、@提及
  • 通知模块:实时消息推送
  • 管理模块:后台管理、数据统计

我们将采用前后端分离的架构:

前端 (React) → API网关 → 后端服务 (Node.js/Express) → 数据库 (MongoDB)

1.2 技术栈选择

  • 前端:React 18 + TypeScript + Tailwind CSS
  • 后端:Node.js 18 + Express 4 + MongoDB + Mongoose
  • 实时通信:Socket.io
  • 部署:Docker + Nginx

1.3 数据库设计

使用MongoDB作为文档型数据库,设计以下集合:

// 用户集合 (users)
{
  _id: ObjectId,
  username: String,      // 用户名
  email: String,         // 邮箱
  password: String,      // 加密后的密码
  avatar: String,        // 头像URL
  bio: String,           // 个人简介
  createdAt: Date,
  updatedAt: Date
}

// 留言集合 (messages)
{
  _id: ObjectId,
  userId: ObjectId,      // 发布者ID
  content: String,       // 留言内容
  parentId: ObjectId,    // 父留言ID(用于回复)
  likes: [ObjectId],     // 点赞用户ID数组
  mentions: [ObjectId],  // @提及的用户ID数组
  createdAt: Date,
  updatedAt: Date
}

// 通知集合 (notifications)
{
  _id: ObjectId,
  userId: ObjectId,      // 接收者ID
  type: String,          // 通知类型(like, reply, mention)
  sourceId: ObjectId,    // 来源ID(留言ID)
  read: Boolean,         // 是否已读
  createdAt: Date
}

第二部分:后端开发

2.1 项目初始化

首先创建项目目录并初始化Node.js项目:

mkdir message-board-backend
cd message-board-backend
npm init -y
npm install express mongoose bcryptjs jsonwebtoken socket.io cors dotenv
npm install --save-dev nodemon typescript @types/node @types/express

创建项目结构:

message-board-backend/
├── src/
│   ├── config/          # 配置文件
│   ├── controllers/     # 控制器
│   ├── models/          # 数据模型
│   ├── routes/          # 路由
│   ├── middleware/      # 中间件
│   ├── utils/           # 工具函数
│   └── app.ts          # 应用入口
├── .env                # 环境变量
└── package.json

2.2 数据库连接与模型定义

创建数据库连接配置:

// src/config/database.ts
import mongoose from 'mongoose';

const connectDB = async () => {
  try {
    const conn = await mongoose.connect(process.env.MONGODB_URI as string);
    console.log(`MongoDB Connected: ${conn.connection.host}`);
  } catch (error) {
    console.error('Database connection error:', error);
    process.exit(1);
  }
};

export default connectDB;

定义用户模型:

// src/models/User.ts
import mongoose, { Schema, Document } from 'mongoose';
import bcrypt from 'bcryptjs';

export interface IUser extends Document {
  username: string;
  email: string;
  password: string;
  avatar: string;
  bio: string;
  comparePassword(candidatePassword: string): Promise<boolean>;
}

const UserSchema: Schema = new Schema({
  username: { type: String, required: true, unique: true, trim: true },
  email: { type: String, required: true, unique: true, lowercase: true },
  password: { type: String, required: true },
  avatar: { 
    type: String, 
    default: 'https://api.dicebear.com/7.x/avataaars/svg?seed=' 
  },
  bio: { type: String, default: '' },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

// 密码加密中间件
UserSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  const salt = await bcrypt.genSalt(10);
  this.password = await bcrypt.hash(this.password, salt);
  next();
});

// 密码比较方法
UserSchema.methods.comparePassword = async function(candidatePassword: string) {
  return await bcrypt.compare(candidatePassword, this.password);
};

export default mongoose.model<IUser>('User', UserSchema);

定义留言模型:

// src/models/Message.ts
import mongoose, { Schema, Document } from 'mongoose';

export interface IMessage extends Document {
  userId: mongoose.Types.ObjectId;
  content: string;
  parentId?: mongoose.Types.ObjectId;
  likes: mongoose.Types.ObjectId[];
  mentions: mongoose.Types.ObjectId[];
  createdAt: Date;
  updatedAt: Date;
}

const MessageSchema: Schema = new Schema({
  userId: { type: Schema.Types.ObjectId, ref: 'User', required: true },
  content: { type: String, required: true, trim: true },
  parentId: { type: Schema.Types.ObjectId, ref: 'Message' },
  likes: [{ type: Schema.Types.ObjectId, ref: 'User' }],
  mentions: [{ type: Schema.Types.ObjectId, ref: 'User' }],
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

// 索引优化
MessageSchema.index({ parentId: 1 });
MessageSchema.index({ createdAt: -1 });
MessageSchema.index({ userId: 1 });

export default mongoose.model<IMessage>('Message', MessageSchema);

2.3 用户认证中间件

创建JWT认证中间件:

// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

export interface AuthenticatedRequest extends Request {
  user?: any;
}

export const authenticateToken = (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

  if (!token) {
    return res.status(401).json({ message: 'Access token required' });
  }

  jwt.verify(token, process.env.JWT_SECRET as string, (err, user) => {
    if (err) {
      return res.status(403).json({ message: 'Invalid token' });
    }
    req.user = user;
    next();
  });
};

2.4 控制器实现

用户控制器:

// src/controllers/userController.ts
import { Request, Response } from 'express';
import User from '../models/User';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';

// 用户注册
export const register = async (req: Request, res: Response) => {
  try {
    const { username, email, password } = req.body;

    // 检查用户是否已存在
    const existingUser = await User.findOne({ $or: [{ email }, { username }] });
    if (existingUser) {
      return res.status(400).json({ 
        message: '用户已存在' 
      });
    }

    // 创建新用户
    const user = new User({ username, email, password });
    await user.save();

    // 生成JWT
    const token = jwt.sign(
      { userId: user._id, username: user.username },
      process.env.JWT_SECRET as string,
      { expiresIn: '7d' }
    );

    res.status(201).json({
      message: '注册成功',
      token,
      user: {
        id: user._id,
        username: user.username,
        email: user.email,
        avatar: user.avatar
      }
    });
  } catch (error) {
    res.status(500).json({ message: '服务器错误', error });
  }
};

// 用户登录
export const login = async (req: Request, res: Response) => {
  try {
    const { email, password } = req.body;

    const user = await User.findOne({ email });
    if (!user) {
      return res.status(401).json({ message: '邮箱或密码错误' });
    }

    const isMatch = await user.comparePassword(password);
    if (!isMatch) {
      return res.status(401).json({ message: '邮箱或密码错误' });
    }

    const token = jwt.sign(
      { userId: user._id, username: user.username },
      process.env.JWT_SECRET as string,
      { expiresIn: '7d' }
    );

    res.json({
      message: '登录成功',
      token,
      user: {
        id: user._id,
        username: user.username,
        email: user.email,
        avatar: user.avatar
      }
    });
  } catch (error) {
    res.status(500).json({ message: '服务器错误', error });
  }
};

// 获取用户信息
export const getUserProfile = async (req: AuthenticatedRequest, res: Response) => {
  try {
    const user = await User.findById(req.user.userId).select('-password');
    if (!user) {
      return res.status(404).json({ message: '用户不存在' });
    }
    res.json(user);
  } catch (error) {
    res.status(500).json({ message: '服务器错误', error });
  }
};

留言控制器:

// src/controllers/messageController.ts
import { Request, Response } from 'express';
import Message from '../models/Message';
import User from '../models/User';
import { AuthenticatedRequest } from '../middleware/auth';
import Notification from '../models/Notification';

// 创建留言
export const createMessage = async (req: AuthenticatedRequest, res: Response) => {
  try {
    const { content, parentId } = req.body;
    const userId = req.user.userId;

    // 解析@提及
    const mentions = this.parseMentions(content);

    const message = new Message({
      userId,
      content,
      parentId: parentId || null,
      mentions
    });

    await message.save();

    // 如果是回复,创建通知
    if (parentId) {
      const parentMessage = await Message.findById(parentId);
      if (parentMessage && parentMessage.userId.toString() !== userId) {
        await Notification.create({
          userId: parentMessage.userId,
          type: 'reply',
          sourceId: message._id,
          read: false
        });
      }
    }

    // 如果有@提及,创建通知
    if (mentions.length > 0) {
      for (const mentionId of mentions) {
        if (mentionId.toString() !== userId) {
          await Notification.create({
            userId: mentionId,
            type: 'mention',
            sourceId: message._id,
            read: false
          });
        }
      }
    }

    // 获取完整消息数据(包含用户信息)
    const populatedMessage = await Message.findById(message._id)
      .populate('userId', 'username avatar')
      .populate('mentions', 'username');

    res.status(201).json(populatedMessage);
  } catch (error) {
    res.status(500).json({ message: '服务器错误', error });
  }
};

// 解析@提及的辅助函数
const parseMentions = (content: string): mongoose.Types.ObjectId[] => {
  const mentionRegex = /@(\w+)/g;
  const mentions: mongoose.Types.ObjectId[] = [];
  let match;
  
  while ((match = mentionRegex.exec(content)) !== null) {
    const username = match[1];
    const user = await User.findOne({ username });
    if (user) {
      mentions.push(user._id);
    }
  }
  
  return mentions;
};

// 获取留言列表
export const getMessages = async (req: Request, res: Response) => {
  try {
    const page = parseInt(req.query.page as string) || 1;
    const limit = parseInt(req.query.limit as string) || 20;
    const skip = (page - 1) * limit;

    // 获取根留言(非回复)
    const messages = await Message.find({ parentId: null })
      .sort({ createdAt: -1 })
      .skip(skip)
      .limit(limit)
      .populate('userId', 'username avatar')
      .populate({
        path: 'replies',
        options: { sort: { createdAt: 1 } },
        populate: { path: 'userId', select: 'username avatar' }
      });

    const total = await Message.countDocuments({ parentId: null });

    res.json({
      messages,
      pagination: {
        page,
        limit,
        total,
        pages: Math.ceil(total / limit)
      }
    });
  } catch (error) {
    res.status(500).json({ message: '服务器错误', error });
  }
};

// 点赞/取消点赞
export const toggleLike = async (req: AuthenticatedRequest, res: Response) => {
  try {
    const { messageId } = req.params;
    const userId = req.user.userId;

    const message = await Message.findById(messageId);
    if (!message) {
      return res.status(404).json({ message: '留言不存在' });
    }

    const likeIndex = message.likes.indexOf(userId);
    if (likeIndex > -1) {
      // 取消点赞
      message.likes.splice(likeIndex, 1);
    } else {
      // 点赞
      message.likes.push(userId);
      
      // 如果不是自己给自己点赞,创建通知
      if (message.userId.toString() !== userId) {
        await Notification.create({
          userId: message.userId,
          type: 'like',
          sourceId: message._id,
          read: false
        });
      }
    }

    await message.save();
    res.json({ likes: message.likes.length });
  } catch (error) {
    res.status(500).json({ message: '服务器错误', error });
  }
};

2.5 路由配置

// src/routes/userRoutes.ts
import express from 'express';
import { register, login, getUserProfile } from '../controllers/userController';
import { authenticateToken } from '../middleware/auth';

const router = express.Router();

router.post('/register', register);
router.post('/login', login);
router.get('/profile', authenticateToken, getUserProfile);

export default router;
// src/routes/messageRoutes.ts
import express from 'express';
import { 
  createMessage, 
  getMessages, 
  toggleLike 
} from '../controllers/messageController';
import { authenticateToken } from '../middleware/auth';

const router = express.Router();

router.post('/', authenticateToken, createMessage);
router.get('/', getMessages);
router.post('/:messageId/like', authenticateToken, toggleLike);

export default router;

2.6 应用入口与Socket.io集成

// src/app.ts
import express from 'express';
import http from 'http';
import cors from 'cors';
import dotenv from 'dotenv';
import { Server } from 'socket.io';
import connectDB from './config/database';
import userRoutes from './routes/userRoutes';
import messageRoutes from './routes/messageRoutes';
import notificationRoutes from './routes/notificationRoutes';

dotenv.config();

const app = express();
const server = http.createServer(app);
const io = new Server(server, {
  cors: {
    origin: process.env.FRONTEND_URL || 'http://localhost:3000',
    methods: ['GET', 'POST']
  }
});

// 中间件
app.use(cors());
app.use(express.json());

// 路由
app.use('/api/users', userRoutes);
app.use('/api/messages', messageRoutes);
app.use('/api/notifications', notificationRoutes);

// Socket.io 实时通信
io.on('connection', (socket) => {
  console.log('用户连接:', socket.id);

  // 加入房间(基于用户ID)
  socket.on('join', (userId) => {
    socket.join(userId);
    console.log(`用户 ${userId} 加入房间`);
  });

  // 处理新留言
  socket.on('newMessage', (message) => {
    // 广播给所有连接的用户
    io.emit('messageAdded', message);
    
    // 如果有@提及,发送给特定用户
    if (message.mentions && message.mentions.length > 0) {
      message.mentions.forEach((userId: string) => {
        io.to(userId).emit('mentionNotification', {
          message,
          type: 'mention'
        });
      });
    }
  });

  // 处理点赞
  socket.on('like', (data) => {
    io.emit('likeUpdated', data);
  });

  socket.on('disconnect', () => {
    console.log('用户断开连接:', socket.id);
  });
});

// 连接数据库
connectDB();

const PORT = process.env.PORT || 5000;

server.listen(PORT, () => {
  console.log(`服务器运行在端口 ${PORT}`);
});

第三部分:前端开发

3.1 项目初始化

使用Create React App创建前端项目:

npx create-react-app message-board-frontend --template typescript
cd message-board-frontend
npm install axios socket.io-client react-router-dom tailwindcss
npm install --save-dev @types/socket.io-client

配置Tailwind CSS:

npx tailwindcss init -p

修改 tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{js,jsx,ts,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

src/index.css 中添加:

@tailwind base;
@tailwind components;
@tailwind utilities;

3.2 项目结构

message-board-frontend/
├── src/
│   ├── components/      # 可复用组件
│   ├── pages/           # 页面组件
│   ├── hooks/           # 自定义Hook
│   ├── services/        # API服务
│   ├── types/           # TypeScript类型定义
│   ├── utils/           # 工具函数
│   ├── App.tsx          # 应用入口
│   └── index.tsx

3.3 类型定义

// src/types/index.ts
export interface User {
  id: string;
  username: string;
  email: string;
  avatar: string;
  bio?: string;
}

export interface Message {
  _id: string;
  userId: User;
  content: string;
  parentId?: string;
  likes: string[];
  mentions: User[];
  replies?: Message[];
  createdAt: string;
  updatedAt: string;
}

export interface Notification {
  _id: string;
  userId: string;
  type: 'like' | 'reply' | 'mention';
  sourceId: string;
  read: boolean;
  createdAt: string;
}

export interface AuthState {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;
}

3.4 API服务层

// src/services/api.ts
import axios from 'axios';

const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000/api';

const api = axios.create({
  baseURL: API_BASE_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

// 请求拦截器,添加JWT
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 响应拦截器,处理错误
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Token过期或无效,清除本地存储并重定向到登录页
      localStorage.removeItem('token');
      localStorage.removeItem('user');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

export const authAPI = {
  register: (data: { username: string; email: string; password: string }) =>
    api.post('/users/register', data),
  
  login: (data: { email: string; password: string }) =>
    api.post('/users/login', data),
  
  getProfile: () => api.get('/users/profile'),
};

export const messageAPI = {
  create: (data: { content: string; parentId?: string }) =>
    api.post('/messages', data),
  
  getMessages: (page = 1, limit = 20) =>
    api.get(`/messages?page=${page}&limit=${limit}`),
  
  toggleLike: (messageId: string) =>
    api.post(`/messages/${messageId}/like`),
};

export const notificationAPI = {
  getNotifications: () => api.get('/notifications'),
  markAsRead: (notificationId: string) =>
    api.patch(`/notifications/${notificationId}/read`),
};

3.5 自定义Hook

// src/hooks/useAuth.ts
import { useState, useEffect, useContext, createContext } from 'react';
import { authAPI } from '../services/api';
import { User, AuthState } from '../types';

interface AuthContextType {
  user: User | null;
  isAuthenticated: boolean;
  login: (email: string, password: string) => Promise<void>;
  register: (username: string, email: string, password: string) => Promise<void>;
  logout: () => void;
  loading: boolean;
}

const AuthContext = createContext<AuthContextType | null>(null);

export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [authState, setAuthState] = useState<AuthState>({
    user: null,
    token: null,
    isAuthenticated: false,
  });
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 检查本地存储中的token
    const token = localStorage.getItem('token');
    const userStr = localStorage.getItem('user');
    
    if (token && userStr) {
      const user = JSON.parse(userStr);
      setAuthState({
        user,
        token,
        isAuthenticated: true,
      });
    }
    setLoading(false);
  }, []);

  const login = async (email: string, password: string) => {
    setLoading(true);
    try {
      const response = await authAPI.login({ email, password });
      const { token, user } = response.data;
      
      localStorage.setItem('token', token);
      localStorage.setItem('user', JSON.stringify(user));
      
      setAuthState({
        user,
        token,
        isAuthenticated: true,
      });
    } catch (error) {
      console.error('登录失败:', error);
      throw error;
    } finally {
      setLoading(false);
    }
  };

  const register = async (username: string, email: string, password: string) => {
    setLoading(true);
    try {
      const response = await authAPI.register({ username, email, password });
      const { token, user } = response.data;
      
      localStorage.setItem('token', token);
      localStorage.setItem('user', JSON.stringify(user));
      
      setAuthState({
        user,
        token,
        isAuthenticated: true,
      });
    } catch (error) {
      console.error('注册失败:', error);
      throw error;
    } finally {
      setLoading(false);
    }
  };

  const logout = () => {
    localStorage.removeItem('token');
    localStorage.removeItem('user');
    setAuthState({
      user: null,
      token: null,
      isAuthenticated: false,
    });
  };

  return (
    <AuthContext.Provider value={{
      user: authState.user,
      isAuthenticated: authState.isAuthenticated,
      login,
      register,
      logout,
      loading,
    }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth必须在AuthProvider内使用');
  }
  return context;
};

3.6 主要页面组件

登录/注册页面

// src/pages/AuthPage.tsx
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';

const AuthPage: React.FC = () => {
  const [isLogin, setIsLogin] = useState(true);
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
  });
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);
  
  const { login, register } = useAuth();
  const navigate = useNavigate();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError('');
    setLoading(true);

    try {
      if (isLogin) {
        await login(formData.email, formData.password);
      } else {
        await register(formData.username, formData.email, formData.password);
      }
      navigate('/');
    } catch (err: any) {
      setError(err.response?.data?.message || '操作失败');
    } finally {
      setLoading(false);
    }
  };

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setFormData({
      ...formData,
      [e.target.name]: e.target.value,
    });
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-100">
      <div className="bg-white p-8 rounded-lg shadow-lg w-full max-w-md">
        <h2 className="text-2xl font-bold text-center mb-6">
          {isLogin ? '登录' : '注册'}
        </h2>
        
        {error && (
          <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
            {error}
          </div>
        )}

        <form onSubmit={handleSubmit}>
          {!isLogin && (
            <div className="mb-4">
              <label className="block text-gray-700 text-sm font-bold mb-2">
                用户名
              </label>
              <input
                type="text"
                name="username"
                value={formData.username}
                onChange={handleChange}
                className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
                required
              />
            </div>
          )}
          
          <div className="mb-4">
            <label className="block text-gray-700 text-sm font-bold mb-2">
              邮箱
            </label>
            <input
              type="email"
              name="email"
              value={formData.email}
              onChange={handleChange}
              className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
              required
            />
          </div>
          
          <div className="mb-6">
            <label className="block text-gray-700 text-sm font-bold mb-2">
              密码
            </label>
            <input
              type="password"
              name="password"
              value={formData.password}
              onChange={handleChange}
              className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
              required
            />
          </div>
          
          <button
            type="submit"
            disabled={loading}
            className={`w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition ${
              loading ? 'opacity-50 cursor-not-allowed' : ''
            }`}
          >
            {loading ? '处理中...' : (isLogin ? '登录' : '注册')}
          </button>
        </form>
        
        <div className="mt-4 text-center">
          <button
            onClick={() => setIsLogin(!isLogin)}
            className="text-blue-500 hover:text-blue-700"
          >
            {isLogin ? '没有账号?立即注册' : '已有账号?立即登录'}
          </button>
        </div>
      </div>
    </div>
  );
};

export default AuthPage;

留言板主页面

// src/pages/MessageBoardPage.tsx
import React, { useState, useEffect, useRef } from 'react';
import { useAuth } from '../hooks/useAuth';
import { messageAPI } from '../services/api';
import { Message } from '../types';
import MessageCard from '../components/MessageCard';
import MessageForm from '../components/MessageForm';
import { io, Socket } from 'socket.io-client';

const MessageBoardPage: React.FC = () => {
  const { user, isAuthenticated } = useAuth();
  const [messages, setMessages] = useState<Message[]>([]);
  const [loading, setLoading] = useState(false);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);
  const [socket, setSocket] = useState<Socket | null>(null);
  const [notification, setNotification] = useState<string | null>(null);
  
  const socketRef = useRef<Socket | null>(null);

  useEffect(() => {
    // 初始化Socket连接
    const newSocket = io(process.env.REACT_APP_API_URL || 'http://localhost:5000');
    socketRef.current = newSocket;
    setSocket(newSocket);

    // 加入用户房间
    if (user?.id) {
      newSocket.emit('join', user.id);
    }

    // 监听新留言
    newSocket.on('messageAdded', (newMessage: Message) => {
      setMessages((prev) => [newMessage, ...prev]);
      showNotification('新留言已添加');
    });

    // 监听点赞更新
    newSocket.on('likeUpdated', (data: { messageId: string; likes: number }) => {
      setMessages((prev) =>
        prev.map((msg) =>
          msg._id === data.messageId
            ? { ...msg, likes: Array(data.likes).fill('') }
            : msg
        )
      );
    });

    // 监听@提及通知
    newSocket.on('mentionNotification', (data) => {
      showNotification(`@${data.message.userId.username} 提及了你`);
    });

    return () => {
      newSocket.disconnect();
    };
  }, [user]);

  useEffect(() => {
    loadMessages();
  }, []);

  const loadMessages = async () => {
    if (loading || !hasMore) return;
    
    setLoading(true);
    try {
      const response = await messageAPI.getMessages(page, 10);
      const { messages: newMessages, pagination } = response.data;
      
      setMessages((prev) => [...prev, ...newMessages]);
      setPage(page + 1);
      setHasMore(pagination.page < pagination.pages);
    } catch (error) {
      console.error('加载留言失败:', error);
    } finally {
      setLoading(false);
    }
  };

  const handleNewMessage = (newMessage: Message) => {
    setMessages((prev) => [newMessage, ...prev]);
    // 通过Socket发送新留言
    if (socket) {
      socket.emit('newMessage', newMessage);
    }
  };

  const handleLike = async (messageId: string) => {
    try {
      const response = await messageAPI.toggleLike(messageId);
      const likesCount = response.data.likes;
      
      // 更新本地状态
      setMessages((prev) =>
        prev.map((msg) =>
          msg._id === messageId
            ? { ...msg, likes: Array(likesCount).fill('') }
            : msg
        )
      );
      
      // 通过Socket发送点赞更新
      if (socket) {
        socket.emit('like', { messageId, likes: likesCount });
      }
    } catch (error) {
      console.error('点赞失败:', error);
    }
  };

  const showNotification = (message: string) => {
    setNotification(message);
    setTimeout(() => setNotification(null), 3000);
  };

  return (
    <div className="min-h-screen bg-gray-50">
      {/* 通知提示 */}
      {notification && (
        <div className="fixed top-4 right-4 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg z-50 animate-fade-in">
          {notification}
        </div>
      )}

      <div className="max-w-3xl mx-auto p-4">
        <header className="mb-8">
          <h1 className="text-3xl font-bold text-gray-800">留言板</h1>
          <p className="text-gray-600 mt-2">
            {isAuthenticated 
              ? `欢迎回来,${user?.username}!` 
              : '请先登录以参与互动'}
          </p>
        </header>

        {/* 留言表单 */}
        {isAuthenticated && (
          <div className="mb-8">
            <MessageForm 
              onSubmit={handleNewMessage} 
              parentId={null}
            />
          </div>
        )}

        {/* 留言列表 */}
        <div className="space-y-4">
          {messages.map((message) => (
            <MessageCard
              key={message._id}
              message={message}
              onLike={handleLike}
              isAuthenticated={isAuthenticated}
              onReply={(parentId) => {
                // 滚动到回复表单
                const replyForm = document.getElementById(`reply-form-${parentId}`);
                if (replyForm) {
                  replyForm.scrollIntoView({ behavior: 'smooth' });
                }
              }}
            />
          ))}
        </div>

        {/* 加载更多 */}
        {hasMore && (
          <div className="text-center mt-6">
            <button
              onClick={loadMessages}
              disabled={loading}
              className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50"
            >
              {loading ? '加载中...' : '加载更多'}
            </button>
          </div>
        )}

        {!loading && messages.length === 0 && (
          <div className="text-center py-12 text-gray-500">
            <p>暂无留言,快来发表你的第一条评论吧!</p>
          </div>
        )}
      </div>
    </div>
  );
};

export default MessageBoardPage;

3.7 可复用组件

留言卡片组件

// src/components/MessageCard.tsx
import React, { useState } from 'react';
import { Message } from '../types';
import MessageForm from './MessageForm';

interface MessageCardProps {
  message: Message;
  onLike: (messageId: string) => void;
  isAuthenticated: boolean;
  onReply: (parentId: string) => void;
}

const MessageCard: React.FC<MessageCardProps> = ({
  message,
  onLike,
  isAuthenticated,
  onReply,
}) => {
  const [showReplyForm, setShowReplyForm] = useState(false);
  const [showReplies, setShowReplies] = useState(false);

  const handleLikeClick = () => {
    if (!isAuthenticated) {
      alert('请先登录才能点赞');
      return;
    }
    onLike(message._id);
  };

  const handleReplyClick = () => {
    if (!isAuthenticated) {
      alert('请先登录才能回复');
      return;
    }
    setShowReplyForm(!showReplyForm);
    onReply(message._id);
  };

  const handleNewReply = (newReply: Message) => {
    // 更新本地状态,添加新回复
    // 这里需要更复杂的逻辑来更新嵌套的回复
    setShowReplyForm(false);
  };

  return (
    <div className="bg-white rounded-lg shadow-md p-4 border border-gray-100">
      <div className="flex items-start space-x-3">
        {/* 头像 */}
        <div className="flex-shrink-0">
          <img
            src={message.userId.avatar}
            alt={message.userId.username}
            className="w-10 h-10 rounded-full object-cover"
          />
        </div>

        {/* 内容区域 */}
        <div className="flex-1 min-w-0">
          <div className="flex items-center space-x-2">
            <span className="font-semibold text-gray-800">
              {message.userId.username}
            </span>
            <span className="text-xs text-gray-500">
              {new Date(message.createdAt).toLocaleString()}
            </span>
          </div>

          <div className="mt-2 text-gray-700 whitespace-pre-wrap">
            {message.content}
          </div>

          {/* @提及显示 */}
          {message.mentions && message.mentions.length > 0 && (
            <div className="mt-2 text-sm text-blue-600">
              提及: {message.mentions.map(m => `@${m.username}`).join(' ')}
            </div>
          )}

          {/* 操作按钮 */}
          <div className="mt-3 flex items-center space-x-4 text-sm">
            <button
              onClick={handleLikeClick}
              className={`flex items-center space-x-1 hover:text-red-500 transition ${
                message.likes.includes('') ? 'text-red-500' : 'text-gray-500'
              }`}
            >
              <span>❤️</span>
              <span>{message.likes.length}</span>
            </button>

            <button
              onClick={handleReplyClick}
              className="text-gray-500 hover:text-blue-500 transition"
            >
              💬 回复
            </button>

            {message.replies && message.replies.length > 0 && (
              <button
                onClick={() => setShowReplies(!showReplies)}
                className="text-gray-500 hover:text-blue-500 transition"
              >
                {showReplies ? '收起回复' : `查看 ${message.replies.length} 条回复`}
              </button>
            )}
          </div>

          {/* 回复表单 */}
          {showReplyForm && (
            <div className="mt-4 p-3 bg-gray-50 rounded-lg" id={`reply-form-${message._id}`}>
              <MessageForm
                parentId={message._id}
                onSubmit={handleNewReply}
                onCancel={() => setShowReplyForm(false)}
              />
            </div>
          )}

          {/* 回复列表 */}
          {showReplies && message.replies && (
            <div className="mt-4 space-y-3 pl-4 border-l-2 border-gray-200">
              {message.replies.map((reply) => (
                <MessageCard
                  key={reply._id}
                  message={reply}
                  onLike={onLike}
                  isAuthenticated={isAuthenticated}
                  onReply={onReply}
                />
              ))}
            </div>
          )}
        </div>
      </div>
    </div>
  );
};

export default MessageCard;

留言表单组件

// src/components/MessageForm.tsx
import React, { useState, useEffect, useRef } from 'react';
import { messageAPI } from '../services/api';
import { Message } from '../types';

interface MessageFormProps {
  parentId?: string | null;
  onSubmit: (message: Message) => void;
  onCancel?: () => void;
  initialContent?: string;
}

const MessageForm: React.FC<MessageFormProps> = ({
  parentId,
  onSubmit,
  onCancel,
  initialContent = '',
}) => {
  const [content, setContent] = useState(initialContent);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const textareaRef = useRef<HTMLTextAreaElement>(null);

  useEffect(() => {
    // 自动聚焦
    if (textareaRef.current) {
      textareaRef.current.focus();
    }
  }, []);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError('');
    
    if (!content.trim()) {
      setError('留言内容不能为空');
      return;
    }

    setLoading(true);
    try {
      const response = await messageAPI.create({
        content,
        parentId: parentId || undefined,
      });
      
      setContent('');
      onSubmit(response.data);
      
      if (onCancel) {
        onCancel();
      }
    } catch (err: any) {
      setError(err.response?.data?.message || '提交失败');
    } finally {
      setLoading(false);
    }
  };

  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    // Ctrl+Enter 提交
    if (e.ctrlKey && e.key === 'Enter') {
      handleSubmit(e);
    }
  };

  return (
    <div className="bg-white rounded-lg shadow-md p-4 border border-gray-100">
      <form onSubmit={handleSubmit}>
        <div className="mb-3">
          <textarea
            ref={textareaRef}
            value={content}
            onChange={(e) => setContent(e.target.value)}
            onKeyDown={handleKeyDown}
            placeholder={parentId ? '回复留言...' : '分享你的想法...'}
            className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
            rows={3}
            maxLength={500}
          />
          <div className="text-right text-sm text-gray-500 mt-1">
            {content.length}/500
          </div>
        </div>

        {error && (
          <div className="text-red-500 text-sm mb-2">{error}</div>
        )}

        <div className="flex justify-end space-x-2">
          {onCancel && (
            <button
              type="button"
              onClick={onCancel}
              className="px-4 py-2 text-gray-600 hover:text-gray-800 transition"
            >
              取消
            </button>
          )}
          <button
            type="submit"
            disabled={loading}
            className={`px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition ${
              loading ? 'opacity-50 cursor-not-allowed' : ''
            }`}
          >
            {loading ? '提交中...' : parentId ? '回复' : '发布'}
          </button>
        </div>
      </form>
    </div>
  );
};

export default MessageForm;

3.8 应用路由配置

// src/App.tsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './hooks/useAuth';
import AuthPage from './pages/AuthPage';
import MessageBoardPage from './pages/MessageBoardPage';
import NotificationPage from './pages/NotificationPage';
import Navbar from './components/Navbar';

// 受保护的路由组件
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const { isAuthenticated, loading } = useAuth();
  
  if (loading) {
    return <div className="flex items-center justify-center min-h-screen">加载中...</div>;
  }
  
  return isAuthenticated ? <>{children}</> : <Navigate to="/login" />;
};

function App() {
  return (
    <AuthProvider>
      <Router>
        <Navbar />
        <Routes>
          <Route path="/login" element={<AuthPage />} />
          <Route path="/register" element={<AuthPage />} />
          <Route 
            path="/" 
            element={
              <ProtectedRoute>
                <MessageBoardPage />
              </ProtectedRoute>
            } 
          />
          <Route 
            path="/notifications" 
            element={
              <ProtectedRoute>
                <NotificationPage />
              </ProtectedRoute>
            } 
          />
          <Route path="*" element={<Navigate to="/" />} />
        </Routes>
      </Router>
    </AuthProvider>
  );
}

export default App;

第四部分:高级功能实现

4.1 实时通知系统

通知控制器:

// src/controllers/notificationController.ts
import { Request, Response } from 'express';
import Notification from '../models/Notification';
import { AuthenticatedRequest } from '../middleware/auth';

// 获取用户通知
export const getUserNotifications = async (req: AuthenticatedRequest, res: Response) => {
  try {
    const notifications = await Notification.find({ userId: req.user.userId })
      .sort({ createdAt: -1 })
      .limit(50)
      .populate('sourceId', 'content userId')
      .populate('sourceId.userId', 'username');

    res.json(notifications);
  } catch (error) {
    res.status(500).json({ message: '服务器错误', error });
  }
};

// 标记通知为已读
export const markAsRead = async (req: AuthenticatedRequest, res: Response) => {
  try {
    const { notificationId } = req.params;
    const notification = await Notification.findOneAndUpdate(
      { _id: notificationId, userId: req.user.userId },
      { read: true },
      { new: true }
    );

    if (!notification) {
      return res.status(404).json({ message: '通知不存在' });
    }

    res.json(notification);
  } catch (error) {
    res.status(500).json({ message: '服务器错误', error });
  }
};

// 标记所有通知为已读
export const markAllAsRead = async (req: AuthenticatedRequest, res: Response) => {
  try {
    await Notification.updateMany(
      { userId: req.user.userId, read: false },
      { read: true }
    );

    res.json({ message: '所有通知已标记为已读' });
  } catch (error) {
    res.status(500).json({ message: '服务器错误', error });
  }
};

4.2 搜索功能

// src/controllers/searchController.ts
import { Request, Response } from 'express';
import Message from '../models/Message';
import User from '../models/User';

export const searchMessages = async (req: Request, res: Response) => {
  try {
    const { q, page = 1, limit = 20 } = req.query;
    const skip = (parseInt(page as string) - 1) * parseInt(limit as string);

    if (!q) {
      return res.status(400).json({ message: '搜索关键词不能为空' });
    }

    // 使用MongoDB的全文搜索(需要创建文本索引)
    const messages = await Message.find(
      { $text: { $search: q as string } },
      { score: { $meta: 'textScore' } }
    )
      .sort({ score: { $meta: 'textScore' } })
      .skip(skip)
      .limit(parseInt(limit as string))
      .populate('userId', 'username avatar');

    const total = await Message.countDocuments({ $text: { $search: q as string } });

    res.json({
      messages,
      pagination: {
        page: parseInt(page as string),
        limit: parseInt(limit as string),
        total,
        pages: Math.ceil(total / parseInt(limit as string))
      }
    });
  } catch (error) {
    res.status(500).json({ message: '服务器错误', error });
  }
};

4.3 数据统计与分析

// src/controllers/analyticsController.ts
import { Request, Response } from 'express';
import Message from '../models/Message';
import User from '../models/User';
import Notification from '../models/Notification';

export const getDashboardStats = async (req: Request, res: Response) => {
  try {
    const totalUsers = await User.countDocuments();
    const totalMessages = await Message.countDocuments();
    const totalReplies = await Message.countDocuments({ parentId: { $ne: null } });
    const totalNotifications = await Notification.countDocuments();

    // 最近7天的留言趋势
    const sevenDaysAgo = new Date();
    sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);

    const recentMessages = await Message.aggregate([
      {
        $match: {
          createdAt: { $gte: sevenDaysAgo }
        }
      },
      {
        $group: {
          _id: { $dateToString: { format: '%Y-%m-%d', date: '$createdAt' } },
          count: { $sum: 1 }
        }
      },
      { $sort: { _id: 1 } }
    ]);

    // 最活跃用户
    const activeUsers = await Message.aggregate([
      {
        $group: {
          _id: '$userId',
          messageCount: { $sum: 1 }
        }
      },
      { $sort: { messageCount: -1 } },
      { $limit: 5 },
      {
        $lookup: {
          from: 'users',
          localField: '_id',
          foreignField: '_id',
          as: 'user'
        }
      },
      { $unwind: '$user' },
      {
        $project: {
          username: '$user.username',
          avatar: '$user.avatar',
          messageCount: 1
        }
      }
    ]);

    res.json({
      totalUsers,
      totalMessages,
      totalReplies,
      totalNotifications,
      recentMessages,
      activeUsers
    });
  } catch (error) {
    res.status(500).json({ message: '服务器错误', error });
  }
};

第五部分:部署与优化

5.1 Docker化部署

创建Dockerfile:

# 后端Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 5000

CMD ["node", "dist/app.js"]

创建docker-compose.yml:

version: '3.8'

services:
  mongodb:
    image: mongo:6
    container_name: message-board-mongo
    ports:
      - "27017:27017"
    volumes:
      - mongodb_data:/data/db
    environment:
      - MONGO_INITDB_ROOT_USERNAME=admin
      - MONGO_INITDB_ROOT_PASSWORD=password

  backend:
    build: ./backend
    container_name: message-board-backend
    ports:
      - "5000:5000"
    environment:
      - MONGODB_URI=mongodb://admin:password@mongodb:27017/messageboard
      - JWT_SECRET=your-super-secret-jwt-key
      - NODE_ENV=production
    depends_on:
      - mongodb

  frontend:
    build: ./frontend
    container_name: message-board-frontend
    ports:
      - "3000:3000"
    environment:
      - REACT_APP_API_URL=http://localhost:5000/api
    depends_on:
      - backend

volumes:
  mongodb_data:

5.2 性能优化

后端优化

  1. 缓存策略
// src/utils/cache.ts
import NodeCache from 'node-cache';

const cache = new NodeCache({ stdTTL: 300 }); // 5分钟缓存

export const cacheMiddleware = (key: string, ttl: number = 300) => {
  return async (req: Request, res: Response, next: NextFunction) => {
    const cacheKey = `${key}:${JSON.stringify(req.query)}`;
    const cached = cache.get(cacheKey);
    
    if (cached) {
      return res.json(cached);
    }
    
    // 重写res.json来缓存结果
    const originalJson = res.json.bind(res);
    res.json = function(data) {
      cache.set(cacheKey, data, ttl);
      return originalJson(data);
    };
    
    next();
  };
};
  1. 数据库索引优化
// 在模型定义中添加复合索引
MessageSchema.index({ userId: 1, createdAt: -1 });
MessageSchema.index({ parentId: 1, createdAt: -1 });
MessageSchema.index({ mentions: 1 });

前端优化

  1. 代码分割
// 使用React.lazy进行代码分割
const NotificationPage = React.lazy(() => import('./pages/NotificationPage'));

// 在路由中使用Suspense
<Suspense fallback={<div>加载中...</div>}>
  <NotificationPage />
</Suspense>
  1. 虚拟滚动
// 使用react-window进行长列表优化
import { FixedSizeList as List } from 'react-window';

const MessageList: React.FC<{ messages: Message[] }> = ({ messages }) => {
  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
    <div style={style}>
      <MessageCard message={messages[index]} />
    </div>
  );

  return (
    <List
      height={600}
      itemCount={messages.length}
      itemSize={150}
      width="100%"
    >
      {Row}
    </List>
  );
};

5.3 安全加固

  1. 输入验证
// src/middleware/validation.ts
import { Request, Response, NextFunction } from 'express';
import { body, validationResult } from 'express-validator';

export const validateMessage = [
  body('content')
    .trim()
    .isLength({ min: 1, max: 500 })
    .withMessage('留言内容长度必须在1-500字符之间')
    .escape(), // 防止XSS攻击
    
  (req: Request, res: Response, next: NextFunction) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    next();
  }
];
  1. 速率限制
// src/middleware/rateLimit.ts
import rateLimit from 'express-rate-limit';

export const createAccountLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分钟
  max: 5, // 每个IP最多5次请求
  message: '请求过于频繁,请稍后再试'
});

export const messageLimiter = rateLimit({
  windowMs: 60 * 1000, // 1分钟
  max: 10, // 每分钟最多10条留言
  message: '留言过于频繁,请稍后再试'
});

第六部分:扩展与定制

6.1 主题定制系统

// src/contexts/ThemeContext.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';

type Theme = 'light' | 'dark' | 'auto';

interface ThemeContextType {
  theme: Theme;
  setTheme: (theme: Theme) => void;
  isDark: boolean;
}

const ThemeContext = createContext<ThemeContextType | null>(null);

export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [theme, setTheme] = useState<Theme>('light');

  useEffect(() => {
    // 从localStorage加载主题
    const savedTheme = localStorage.getItem('theme') as Theme;
    if (savedTheme) {
      setTheme(savedTheme);
    } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
      setTheme('auto');
    }
  }, []);

  useEffect(() => {
    // 应用主题
    const root = document.documentElement;
    
    if (theme === 'auto') {
      const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
      root.classList.toggle('dark', isDark);
    } else {
      root.classList.toggle('dark', theme === 'dark');
    }
    
    localStorage.setItem('theme', theme);
  }, [theme]);

  const isDark = theme === 'dark' || (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches);

  return (
    <ThemeContext.Provider value={{ theme, setTheme, isDark }}>
      {children}
    </ThemeContext.Provider>
  );
};

export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme必须在ThemeProvider内使用');
  }
  return context;
};

6.2 插件系统架构

// src/plugins/PluginManager.ts
interface Plugin {
  name: string;
  version: string;
  description: string;
  init: (app: any) => void;
  cleanup?: () => void;
}

class PluginManager {
  private plugins: Map<string, Plugin> = new Map();

  register(plugin: Plugin) {
    if (this.plugins.has(plugin.name)) {
      console.warn(`插件 ${plugin.name} 已存在`);
      return;
    }
    this.plugins.set(plugin.name, plugin);
    plugin.init(this);
  }

  unregister(pluginName: string) {
    const plugin = this.plugins.get(pluginName);
    if (plugin && plugin.cleanup) {
      plugin.cleanup();
    }
    this.plugins.delete(pluginName);
  }

  // 示例:表情符号插件
  static emojiPlugin: Plugin = {
    name: 'emoji-plugin',
    version: '1.0.0',
    description: '添加表情符号支持',
    init: (app) => {
      // 扩展消息模型,添加emoji字段
      app.MessageSchema.add({
        emojis: [{ type: String }]
      });
      
      // 添加emoji选择器到前端
      console.log('表情符号插件已初始化');
    }
  };
}

export default PluginManager;

第七部分:测试与监控

7.1 单元测试示例

// __tests__/messageController.test.ts
import request from 'supertest';
import app from '../src/app';
import mongoose from 'mongoose';
import User from '../src/models/User';
import Message from '../src/models/Message';

describe('Message Controller', () => {
  let token: string;
  let userId: string;

  beforeAll(async () => {
    await mongoose.connect(process.env.MONGODB_TEST_URI as string);
    
    // 创建测试用户
    const user = new User({
      username: 'testuser',
      email: 'test@example.com',
      password: 'password123'
    });
    await user.save();
    userId = user._id.toString();

    // 获取token
    const response = await request(app)
      .post('/api/users/login')
      .send({ email: 'test@example.com', password: 'password123' });
    
    token = response.body.token;
  });

  afterAll(async () => {
    await mongoose.connection.close();
  });

  beforeEach(async () => {
    await Message.deleteMany({});
  });

  test('创建留言', async () => {
    const response = await request(app)
      .post('/api/messages')
      .set('Authorization', `Bearer ${token}`)
      .send({ content: '测试留言' });

    expect(response.status).toBe(201);
    expect(response.body.content).toBe('测试留言');
    expect(response.body.userId).toBe(userId);
  });

  test('获取留言列表', async () => {
    // 先创建一些留言
    await Message.create([
      { userId, content: '留言1' },
      { userId, content: '留言2' }
    ]);

    const response = await request(app)
      .get('/api/messages')
      .query({ page: 1, limit: 10 });

    expect(response.status).toBe(200);
    expect(response.body.messages).toHaveLength(2);
    expect(response.body.pagination.total).toBe(2);
  });

  test('点赞留言', async () => {
    const message = await Message.create({ userId, content: '测试留言' });
    
    const response = await request(app)
      .post(`/api/messages/${message._id}/like`)
      .set('Authorization', `Bearer ${token}`);

    expect(response.status).toBe(200);
    expect(response.body.likes).toBe(1);
  });
});

7.2 监控与日志

// src/utils/logger.ts
import winston from 'winston';
import path from 'path';

const logger = winston.createLogger({
  level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    // 错误日志
    new winston.transports.File({
      filename: path.join(__dirname, '../../logs/error.log'),
      level: 'error'
    }),
    // 所有日志
    new winston.transports.File({
      filename: path.join(__dirname, '../../logs/combined.log')
    })
  ]
});

// 开发环境添加控制台输出
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.combine(
      winston.format.colorize(),
      winston.format.simple()
    )
  }));
}

export default logger;

第八部分:总结与展望

通过本教程,我们从零开始构建了一个功能完整的个性化留言系统。这个系统包含了:

  1. 用户认证系统:安全的注册、登录和JWT令牌管理
  2. 留言核心功能:创建、回复、编辑、删除留言
  3. 互动功能:点赞、@提及、实时通知
  4. 高级功能:搜索、数据统计、主题定制
  5. 部署方案:Docker容器化部署
  6. 性能优化:缓存、索引、代码分割
  7. 安全措施:输入验证、速率限制、XSS防护

未来扩展方向

  1. AI集成:使用自然语言处理自动分类留言,智能回复建议
  2. 多媒体支持:图片、视频、文件上传
  3. 社交集成:分享到社交媒体,第三方登录
  4. 移动端优化:PWA应用,离线功能
  5. 数据分析:用户行为分析,情感分析
  6. 多语言支持:国际化(i18n)支持

部署检查清单

  • [ ] 环境变量配置完成
  • [ ] 数据库备份策略
  • [ ] SSL证书配置
  • [ ] 监控告警设置
  • [ ] 性能基准测试
  • [ ] 安全扫描
  • [ ] 用户文档编写

这个留言系统不仅解决了用户互动的基本需求,还提供了丰富的扩展可能性。你可以根据具体需求,添加更多个性化功能,打造独一无二的互动体验。

记住,最好的系统是持续迭代和优化的系统。定期收集用户反馈,分析使用数据,不断改进你的留言系统,让它成为用户真正喜爱的互动平台。