本文档详细描述了校园论坛系统的所有 API 接口,供客户端开发者参考。
相关仓库:服务端本仓库 XEKernel/school-forum · Android 客户端 XEKernel/school-forum-android
本文档的接口路径已全部更新为带
/api前缀(2026-08-02 批量同步,共 141 处):
- 服务端对不带
/api的旧路径提供 307 重定向兼容,但仅限 GET 请求与显式 JSON 请求;新客户端请一律使用/api前缀。- 认证方式:文档中要求 body 传
userId/viewerId的接口,代码已改为从 JWT 取身份(Authorization: Bearer <token>),body/query 中传入的 userId 会被忽略(安全修复)。- 响应格式:文档示例为
data嵌套;实际成功响应为{ success, message, ...业务字段 }(业务字段直接展开在顶层,如user、token)。- 已补齐文档(2026-08-07):忘记密码(
/api/forgot-password/*)、回复点赞(/api/posts/:id/comments/:commentId/replies/:replyId/like)、群发广播消息(/api/admin/broadcast-message)、QQ 快捷登录(/api/auth/qq/*,可选功能)。- 文档仍未收录(均为代码中已存在):
POST /api/unfollow、GET /api/blocked/:userId、用户数据导出/导入(/api/user/export-data、/api/user/import-data)、公开 IP 统计已移除(/api/ip-stats*现需管理员认证,同/api/admin/ip-stats*)。
- Base URL:
http://your-domain:2080(默认端口 2080,请以 .env 中 PORT 为准) - Content-Type:
application/json - 响应格式: JSON
实际实现(
src/utils/validationUtils.js):业务字段直接展开在顶层,不使用data嵌套。 下方章节的响应示例若为旧格式"data": {...},请将data内的字段上提一层,即{"success": true, "message": "...", ...data 内字段}。
成功响应(实际格式):
{
"success": true,
"message": "操作成功",
"token": "...",
"user": { "...": "..." }
}实际实现:身份一律取自 JWT,body/query 中传入的
userId/viewerId/senderId会被忽略(安全修复,防身份伪造)。 下方章节若要求 body 传userId等身份参数,请忽略该参数,改为携带令牌。
需登录接口请求头:
Authorization: Bearer <accessToken>
令牌说明:
accessToken:短期令牌(默认 7 天),通过POST /api/login或POST /api/register获取,响应字段token。refreshToken:长期令牌(默认 30 天),通过POST /api/refresh-token换取新 accessToken。- 管理员接口额外需要管理员身份(登录管理员账号获得),
Authorization同 user 令牌但要求isAdmin。 - 令牌失效场景:登出(加入黑名单)、修改密码(access 立即失效,refresh 同步失效)、达到有效期。
错误响应(实际格式):
{
"success": false,
"message": "错误信息",
"code": 400
}HTTP 状态码约定:成功 200/201;参数错误 400;未认证 401;无权限 403;资源不存在 404;服务器内部错误 500(部分接口对未登录访问返回 401)。
GET /api/health
响应示例:
{
"success": true,
"message": "服务器运行正常",
"timestamp": "2024-01-01T00:00:00.000Z"
}POST /api/send-verification-code
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| string | 是 | 邮箱地址 |
请求示例:
{
"email": "user@example.com"
}响应示例:
{
"success": true,
"message": "验证码已发送到您的邮箱"
}POST /api/send-login-verification-code
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| string | 是 | 邮箱地址 |
POST /api/send-password-change-code
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
| currentPassword | string | 是 | 当前密码 |
响应示例:
{
"success": true,
"message": "验证码已发送到您的邮箱",
"data": {
"email": "u***@example.com"
}
}POST /api/verify-password-change-code
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
| verificationCode | string | 是 | 6位验证码 |
POST /api/change-password
🔑 需要认证
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
| verificationCode | string | 是 | 已验证的验证码 |
| newPassword | string | 是 | 新密码 |
POST /api/send-email-change-code
🔑 需要认证
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
| newEmail | string | 是 | 新邮箱地址 |
| currentPassword | string | 是 | 当前密码 |
POST /api/verify-email-change
🔑 需要认证
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
| verificationCode | string | 是 | 6位验证码 |
POST /api/change-qq
🔑 需要认证
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
| newQq | string | 是 | 新 QQ 号 |
| currentPassword | string | 是 | 当前密码 |
POST /api/send-deletion-code
🔑 需要认证
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
POST /api/delete-account
🔑 需要认证
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
| verificationCode | string | 是 | 6位验证码 |
GET /api/captcha
获取一次性图形验证码(SVG 格式),用于登录、注册和管理员登录时的人机验证。
响应示例:
{
"success": true,
"data": {
"captchaId": "uuid-string",
"svg": "<svg>...</svg>"
}
}字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
| captchaId | string | 验证码唯一标识,提交时需携带 |
| svg | string | SVG 图片字符串,直接嵌入 DOM 显示 |
使用规则:
- 验证码为 4 位随机数字,大小写不敏感
- 有效期 5 分钟,过期自动失效
- 一次性使用:验证后立即删除,不可重复使用
- 点击验证码图片可刷新获取新的验证码
POST /api/register
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| string | 是 | QQ号 | |
| username | string | 是 | 用户名(2-20字符) |
| password | string | 是 | 密码(至少6位) |
| string | 是 | 邮箱地址 | |
| verificationCode | string | 是 | 6位邮箱验证码 |
| captchaId | string | 是 | 图形验证码ID(从 GET /captcha 获取) |
| captchaCode | string | 是 | 图形验证码(图片中的4位数字) |
| school | string | 是 | 学校ID |
| enrollmentYear | number | 是 | 入学年份 |
| className | string | 是 | 班级名称 |
请求示例:
{
"qq": "12345678",
"username": "张三",
"password": "password123",
"email": "user@example.com",
"verificationCode": "123456",
"captchaId": "uuid-from-captcha-api",
"captchaCode": "3847",
"school": "XXXX",
"enrollmentYear": 2024,
"className": "高一(1)班"
}响应示例:
{
"success": true,
"message": "注册成功",
"data": {
"user": {
"id": "uuid-string",
"qq": "12345678",
"username": "张三",
"email": "user@example.com",
"school": "XXXX",
"enrollmentYear": 2024,
"className": "高一(1)班",
"grade": "高一",
"createdAt": "2024-01-01T00:00:00.000Z",
"postCount": 0,
"commentCount": 0,
"isActive": true
}
}
}POST /api/login
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| string | 是 | 邮箱地址 | |
| string | 是 | QQ号 | |
| password | string | 是 | 密码 |
| verificationCode | string | 是 | 6位邮箱验证码 |
| captchaId | string | 是 | 图形验证码ID(从 GET /captcha 获取) |
| captchaCode | string | 是 | 图形验证码(图片中的4位数字) |
请求示例:
{
"email": "user@example.com",
"qq": "12345678",
"password": "password123",
"verificationCode": "123456",
"captchaId": "uuid-from-captcha-api",
"captchaCode": "5621"
}响应示例:
{
"success": true,
"message": "登录成功",
"data": {
"user": {
"id": "uuid-string",
"qq": "12345678",
"username": "张三",
"email": "user@example.com",
"school": "XXXX",
"grade": "高一",
"className": "高一(1)班",
"avatar": "/images/avatars/xxx.jpg",
"lastLogin": "2024-01-01T00:00:00.000Z"
},
"isAdmin": false
}
}需先在 QQ 互联 申请应用,并在
.env配置:QQ_APP_ID、QQ_APP_SECRET、QQ_REDIRECT_URI(回调地址需与 QQ 互联后台一致)。 未配置时接口返回 400「QQ登录未配置」。
GET /api/auth/qq/status
未登录可用。返回 { configured: true|false },登录页据此决定是否显示「QQ 快捷登录」按钮。
GET /api/auth/qq/authorize-url?type=login
未登录可用。返回 { url, state },前端跳转 url 进入 QQ 授权页。
GET /api/auth/qq/authorize-url-bind?type=bind
需登录(Authorization: Bearer token)。用于设置页绑定 QQ 到当前账号。
GET /api/auth/qq/callback?code=xxx&state=yyy
QQ 授权完成后由 QQ 服务器调用(即 QQ_REDIRECT_URI)。服务端换取
openid/用户信息后,302 重定向到前端 /qq-callback.html?state=yyy。
GET /api/auth/qq/result?state=yyy
前端回调页调用,state 为一次性凭证(会话 10 分钟有效)。返回:
{
"success": true,
"type": "login",
"result": {
"needProfile": false,
"user": { "id": "xxx", "username": "xxx" },
"token": "JWT访问令牌",
"refreshToken": "JWT刷新令牌",
"isAdmin": false,
"isNewDevice": false
},
"prefill": { "nickname": "QQ昵称", "avatar": "头像URL", "gender": "male" }
}result.needProfile = false:已有账号,直接登录成功(前端保存 token 跳首页)result.needProfile = true:新用户,跳转/qq-register.html?state=yyy补全资料type = "bind":绑定场景,result.bound表示绑定结果
POST /api/auth/qq/complete-profile
Content-Type: application/json
{
"state": "yyy",
"username": "用户名(默认预填QQ昵称,重名自动加后缀)",
"school": "学校名称",
"enrollmentYear": 2024,
"className": "1班",
"birthday": "2000-01-01",
"gender": "male"
}
创建账号并直接登录(返回 token/refreshToken)。QQ 快捷注册账号使用占位
邮箱(@qq-oauth.local,不可收信),不发送任何邮件通知;用户可在设置页
修改 QQ 号/绑定真实邮箱后恢复邮件功能。
POST /api/auth/qq/unbind
Authorization: Bearer token
需登录。QQ 快捷注册账号(占位 QQ 号)禁止解绑,返回 400。
GET /api/auth/qq/bind-status
Authorization: Bearer token
需登录。返回 { qqBound, qqPlaceholder, configured },设置页渲染绑定卡片。
POST /api/forgot-password/send-code
Content-Type: application/json
{
"qq": "123456789",
"email": "user@example.com",
"captchaId": "图形验证码ID",
"captchaCode": "1234"
}
未登录可用。校验 QQ 号 + 邮箱是否匹配注册信息 + 图形验证码(一次性), 匹配后向该邮箱发送密码重置验证码。统一错误文案("信息不匹配")防止 账号枚举;接口纳入严格限流(5 次/分钟)。
{
"success": true,
"message": "验证码已发送"
}
POST /api/forgot-password/reset
Content-Type: application/json
{
"email": "user@example.com",
"verificationCode": "123456",
"newPassword": "NewPass123!"
}
未登录可用。校验邮箱验证码后重置密码。重置成功后该用户所有旧登录 Token(含其他设备)立即失效(passwordChangedAt 机制),需用新密码重新登录。
{
"success": true,
"message": "密码重置成功"
}
POST /api/auth/verify
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
响应示例:
{
"success": true,
"message": "用户验证通过",
"data": {
"user": { ... },
"isAdmin": false,
"isBanned": false,
"valid": true
}
}GET /api/users/:id
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| id | string | 用户ID |
响应示例:
{
"success": true,
"data": {
"user": {
"id": "uuid-string",
"qq": "12345678",
"username": "张三",
"school": "XXXX",
"grade": "高一",
"className": "高一(1)班",
"avatar": "/images/avatars/xxx.jpg",
"createdAt": "2024-01-01T00:00:00.000Z"
},
"stats": {
"postCount": 10,
"commentCount": 25,
"totalLikes": 50,
"totalViews": 200,
"joinDate": "2024-01-01T00:00:00.000Z",
"lastLogin": "2024-01-15T00:00:00.000Z"
},
"recentPosts": [ ... ]
}
}PUT /api/users/:id
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| id | string | 用户ID |
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| currentPassword | string | 否 | 当前密码(修改密码时必填) |
| newPassword | string | 否 | 新密码 |
| username | string | 否 | 新用户名 |
| settings | object | 否 | 用户设置 |
请求示例:
{
"currentPassword": "oldpassword",
"newPassword": "newpassword123",
"username": "新用户名"
}PUT /api/users/:id/settings
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| settings | object | 是 | 设置对象 |
请求示例:
{
"settings": {
"theme": "dark",
"notifications": true
}
}POST /api/users/:id/avatar
请求格式: multipart/form-data
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| avatar | file | 是 | 头像图片(JPG/PNG/GIF/WebP,最大2MB) |
响应示例:
{
"success": true,
"message": "头像上传成功",
"data": {
"user": { ... },
"avatarUrl": "/images/avatars/xxx.jpg"
}
}DELETE /api/users/:id/avatar
请求参数:无(通过路径参数指定用户)
图形验证码用于登录、注册和管理员登录时的人机验证,防止自动化脚本攻击。
- 纯 SVG 生成:零依赖,服务端字符串拼接生成 SVG 图片
- 验证码内容:4 位随机数字 + 干扰线(4 条)+ 噪点(30 个)
- 存储:Redis 优先,不可用时自动降级到内存 Map
- 有效期:5 分钟(TTL),过期自动清除
- 一次性:验证成功后立即删除,防止重用
GET /api/captcha
获取新的图形验证码,返回 captchaId 和 SVG 图片。
响应示例:
{
"success": true,
"data": {
"captchaId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"130\" height=\"48\">...</svg>"
}
}字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
| captchaId | string | 验证码唯一标识(UUID v4),提交表单时需携带 |
| svg | string | SVG 图片字符串,可直接嵌入 HTML DOM 显示 |
使用流程:
- 客户端调用
GET /captcha获取 captchaId 和 SVG - 将 SVG 渲染到页面(innerHTML),存储 captchaId
- 用户输入图片中的 4 位数字
- 提交表单时携带
captchaId和captchaCode - 验证失败后应重新获取验证码(点击图片刷新或提交失败自动刷新)
注意事项:
- 验证码比较大小写不敏感
- 每个 captchaId 只能验证一次,无论成功或失败
- 5 分钟内未验证自动过期
- 建议在登录/注册表单提交失败后自动刷新验证码
GET /api/posts
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | number | 否 | 1 | 页码 |
| limit | number | 否 | 100 | 每页数量 |
| search | string | 否 | - | 搜索关键词 |
| sortBy | string | 否 | latest | 排序方式 |
| categoryId | string | 否 | - | 按栏目筛选 |
| viewerId | string | 否 | - | 当前查看者ID(用于个性化推荐) |
sortBy 可选值:
| 值 | 说明 |
|---|---|
| latest | 最新发布(默认) |
| recommended | 推荐排序(防信息茧房混合算法) |
| relevance | 综合热度 |
| likes | 点赞数排序 |
| favorites | 收藏数排序 |
| views | 浏览量排序 |
| comments | 评论数排序 |
推荐算法说明:推荐排序采用混合策略,平衡热度、新鲜度、关注动态和随机探索,避免信息茧房。
- 40% 热门内容 + 25% 关注动态 + 20% 新鲜内容 + 15% 随机探索
- 关注用户的帖子权重提升 1.5 倍
- 48 小时内的新帖子有额外新鲜度加分
响应示例:
{
"success": true,
"data": {
"posts": [
{
"id": "uuid-string",
"userId": "user-uuid",
"username": "张三",
"userAvatar": "/images/avatars/xxx.jpg",
"school": "XXXX",
"grade": "高一",
"className": "高一(1)班",
"content": "帖子内容...",
"images": [
{ "url": "/images/xxx.jpg", "filename": "xxx.jpg" }
],
"anonymous": false,
"timestamp": "2024-01-01T00:00:00.000Z",
"likes": 10,
"dislikes": 0,
"viewCount": 100,
"category": {
"id": "category-uuid",
"name": "学习交流",
"icon": "fa-book",
"color": "#4361ee"
},
"comments": [ ... ]
}
],
"categories": [
{
"id": "category-uuid",
"name": "学习交流",
"description": "学习相关的讨论区",
"icon": "fa-book",
"color": "#4361ee",
"postCount": 25
}
],
"pagination": {
"currentPage": 1,
"totalPages": 10,
"totalPosts": 100,
"hasNext": true,
"hasPrev": false
}
}
}POST /api/posts
请求格式: multipart/form-data
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
| username | string | 是 | 用户名 |
| school | string | 是 | 学校ID |
| grade | string | 是 | 年级 |
| className | string | 是 | 班级 |
| content | string | 否* | 帖子内容(无图片时必填) |
| anonymous | string | 否 | 是否匿名("true") |
| images | file[] | 否* | 图片文件(最多20张) |
| categoryId | string | 否 | 所属栏目ID(可选) |
*内容和图片至少提供一项
响应示例:
{
"success": true,
"message": "帖子发布成功",
"data": {
"post": {
"id": "uuid-string",
"userId": "user-uuid",
"username": "张三",
"content": "帖子内容...",
"images": [ ... ],
"timestamp": "2024-01-01T00:00:00.000Z",
"likes": 0,
"likedBy": [],
"comments": [],
"viewCount": 0,
"isDeleted": false
}
}
}GET /api/posts/:id
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| id | string | 帖子ID |
PUT /api/posts/:id
请求格式: multipart/form-data
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
| content | string | 否 | 新内容 |
| deletedImages | string | 否 | JSON数组,要删除的图片URL |
| images | file[] | 否 | 新增图片 |
DELETE /api/posts/:id
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID(必须是帖子作者) |
POST /api/posts/:id/view
响应示例:
{
"success": true,
"data": {
"viewCount": 101
}
}POST /api/posts/:id/like
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
响应示例:
{
"success": true,
"message": "点赞成功",
"data": {
"likes": 11,
"liked": true,
"dislikes": 0,
"disliked": false
}
}POST /api/posts/:id/dislike
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
响应示例:
{
"success": true,
"message": "点踩成功",
"data": {
"dislikes": 1,
"disliked": true,
"likes": 10,
"liked": false
}
}POST /api/posts/:id/comments
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| id | string | 帖子ID |
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
| username | string | 是 | 用户名 |
| content | string | 是 | 评论内容(最多500字) |
| anonymous | boolean | 否 | 是否匿名 |
响应示例:
{
"success": true,
"message": "评论添加成功",
"data": {
"comment": {
"id": "uuid-string",
"userId": "user-uuid",
"username": "张三",
"content": "评论内容",
"anonymous": false,
"timestamp": "2024-01-01T00:00:00.000Z"
}
}
}POST /api/posts/:id/comments/:commentId/like
Authorization: Bearer token
需登录。点赞/取消点赞评论(重复点击切换),返回最新点赞数与点赞状态。
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| id | string | 帖子ID |
| commentId | string | 评论ID |
响应:
{ "success": true, "likes": 1, "liked": true, "message": "点赞成功" }回复点赞(任意嵌套层级)见「点赞回复(任意层级)」一节。
POST /api/posts/:id/comments/:commentId/replies
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| id | string | 帖子ID |
| commentId | string | 评论ID |
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
| username | string | 是 | 用户名 |
| content | string | 是 | 回复内容 |
| anonymous | boolean | 否 | 是否匿名 |
| replyToId | string | 否 | 被回复的回复ID(嵌套回复) |
DELETE /api/posts/:id/comments/:commentId
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
| replyId | string | 否 | 回复ID(删除回复时) |
| nestedReplyId | string | 否 | 嵌套回复ID |
POST /api/posts/:id/comments/:commentId/replies/:replyId/like
Authorization: Bearer token
需登录。递归定位任意嵌套层级的回复进行点赞/取消点赞(重复点击切换),
支持 6 层嵌套回复中的任意层。点赞时通知回复作者(复用 comment_like 通知)。
请求参数(URL):
| 参数 | 类型 | 说明 |
|---|---|---|
| id | string | 帖子ID |
| commentId | string | 顶层评论ID |
| replyId | string | 目标回复ID(可为任意层级) |
响应:
{ "success": true, "likes": 1, "liked": true, "message": "点赞成功" }本系统所有 API 响应均为 JSON 格式,大多数接口采用嵌套式响应:
{
"success": true,
"message": "操作成功",
"data": { ... }
}部分接口(如管理后台、运行模式相关)采用展开式响应,数据直接展开到根级别:
{
"success": true,
"message": "操作成功",
"mode": "normal",
"maintenanceMessage": "",
...其他数据字段
}
⚠️ 客户端注意:调用接口时请根据实际路由判断响应格式:
/admin/*和/run-mode相关接口 → 展开式响应- 其他接口 → 嵌套式响应(
data字段)- 不确定时,可同时检查
response.data和response.mode等字段
GET /api/categories
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | number | 否 | 1 | 页码 |
| limit | number | 否 | 20 | 每页数量 |
响应示例:
{
"success": true,
"data": {
"categories": [
{
"id": "uuid-string",
"name": "学习交流",
"description": "学习方法和经验分享",
"icon": "book",
"color": "#4CAF50",
"order": 1,
"isActive": true,
"postCount": 156
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 5
}
}
}GET /api/categories/:id
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| id | string | 栏目ID |
响应示例:
{
"success": true,
"data": {
"id": "uuid-string",
"name": "学习交流",
"description": "学习方法和经验分享",
"icon": "book",
"color": "#4CAF50",
"order": 1,
"isActive": true,
"postCount": 156
}
}GET /api/categories/:id/posts
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| id | string | 栏目ID |
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | number | 否 | 1 | 页码 |
| limit | number | 否 | 20 | 每页数量 |
响应示例:
{
"success": true,
"data": {
"posts": [...],
"pagination": {
"page": 1,
"limit": 20,
"total": 156
},
"category": {
"id": "uuid-string",
"name": "学习交流"
}
}
}POST /api/category-applications
需要认证
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| categoryName | string | 是 | 栏目名称(最多30字符) |
| description | string | 否 | 申请理由(最多500字符) |
响应示例:
{
"success": true,
"message": "申请已提交,请等待管理员审核"
}GET /api/admin/categories
需要管理员权限
响应示例:
{
"success": true,
"data": {
"categories": [
{
"id": "uuid-string",
"name": "学习交流",
"description": "学习方法和经验分享",
"icon": "book",
"color": "#4CAF50",
"order": 1,
"isActive": true,
"postCount": 156,
"createdAt": "2024-01-01T00:00:00.000Z"
}
]
}
}POST /api/admin/categories
需要管理员权限
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| name | string | 是 | 栏目名称 |
| description | string | 否 | 栏目描述 |
| icon | string | 否 | 图标名称(FontAwesome) |
| color | string | 否 | 主题颜色(十六进制) |
| order | number | 否 | 排序序号 |
响应示例:
{
"success": true,
"message": "栏目创建成功",
"data": {
"id": "uuid-string",
"name": "新栏目",
"description": "栏目描述",
"icon": "folder",
"color": "#2196F3",
"order": 10,
"isActive": true,
"postCount": 0
}
}PUT /api/admin/categories/:id
需要管理员权限
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| name | string | 否 | 栏目名称 |
| description | string | 否 | 栏目描述 |
| icon | string | 否 | 图标名称 |
| color | string | 否 | 主题颜色 |
| order | number | 否 | 排序序号 |
DELETE /api/admin/categories/:id
需要管理员权限
响应示例:
{
"success": true,
"message": "栏目已删除"
}PATCH /api/admin/categories/:id/toggle-status
需要管理员权限
响应示例:
{
"success": true,
"message": "栏目已禁用",
"data": {
"isActive": false
}
}GET /api/admin/category-applications
需要管理员权限
响应示例:
{
"success": true,
"data": {
"applications": [
{
"id": "uuid-string",
"userId": "user-uuid",
"categoryName": "游戏讨论",
"description": "希望开设游戏讨论区",
"status": "pending",
"createdAt": "2024-01-01T00:00:00.000Z"
}
]
}
}POST /api/admin/category-applications/:id/approve
需要管理员权限
响应示例:
{
"success": true,
"message": "申请已批准,栏目已创建"
}POST /api/admin/category-applications/:id/reject
需要管理员权限
响应示例:
{
"success": true,
"message": "申请已拒绝"
}POST /api/follow
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| followerId | string | 是 | 关注者ID |
| followingId | string | 是 | 被关注者ID |
响应示例:
{
"success": true,
"message": "关注成功",
"data": {
"following": true
}
}DELETE /api/follow
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| followerId | string | 是 | 关注者ID |
| followingId | string | 是 | 被关注者ID |
GET /api/follow/status
查询参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| followerId | string | 是 | 关注者ID |
| followingId | string | 是 | 被关注者ID |
响应示例:
{
"success": true,
"data": {
"isFollowing": true
}
}GET /api/follow/stats/:userId
响应示例:
{
"success": true,
"data": {
"followingCount": 10,
"followerCount": 25
}
}GET /api/following/:userId
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | number | 否 | 1 | 页码 |
| limit | number | 否 | 20 | 每页数量 |
| currentUserId | string | 否 | - | 当前用户ID |
响应示例:
{
"success": true,
"data": {
"list": [
{
"id": "user-uuid",
"username": "张三",
"avatar": "/images/avatars/xxx.jpg",
"school": "XXXX",
"grade": "高一",
"className": "高一(1)班",
"followedAt": "2024-01-01T00:00:00.000Z",
"isAdmin": false,
"isFollowing": true
}
],
"pagination": {
"currentPage": 1,
"totalPages": 5,
"total": 100,
"hasNext": true,
"hasPrev": false
}
}
}GET /api/followers/:userId
参数同关注列表。
GET /api/following/posts/:userId
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | number | 否 | 1 | 页码 |
| limit | number | 否 | 10 | 每页数量 |
GET /api/follow/new-posts/:userId
响应示例:
{
"success": true,
"data": {
"count": 5
}
}POST /api/follow/mark-viewed
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
POST /api/favorites/:postId
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| postId | string | 帖子ID |
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
| tagId | string | 否 | 标签ID |
响应示例:
{
"success": true,
"message": "收藏成功",
"data": {
"favorited": true,
"favorite": {
"userId": "user-uuid",
"postId": "post-uuid",
"tagId": null,
"createdAt": "2024-01-01T00:00:00.000Z"
}
}
}DELETE /api/favorites/:postId
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
GET /api/favorites/:postId/check
查询参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
响应示例:
{
"success": true,
"data": {
"favorited": true,
"favoriteCount": 10,
"tagId": "tag-uuid"
}
}GET /api/favorites/user/:userId
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | number | 否 | 1 | 页码 |
| limit | number | 否 | 10 | 每页数量 |
| tagId | string | 否 | - | 按标签筛选 |
响应示例:
{
"success": true,
"data": {
"posts": [
{
"id": "post-uuid",
"content": "帖子内容...",
"isDeleted": false,
"userAvatar": "/images/avatars/xxx.jpg",
"favoriteAt": "2024-01-01T00:00:00.000Z",
"tagId": "tag-uuid"
}
],
"pagination": { ... }
}
}GET /api/favorites/user/:userId/count
PUT /api/favorites/:postId/tag
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
| tagId | string | 否 | 标签ID(null表示取消标签) |
POST /api/favorites/batch/delete
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
| postIds | string[] | 是 | 帖子ID数组 |
POST /api/favorites/batch/move
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
| postIds | string[] | 是 | 帖子ID数组 |
| tagId | string | 否 | 目标标签ID |
GET /api/favorites/tags/:userId
响应示例:
{
"success": true,
"data": {
"tags": [
{
"_id": "tag-uuid",
"userId": "user-uuid",
"name": "学习资料",
"color": "#4361ee",
"order": 0,
"favoriteCount": 5
}
]
}
}POST /api/favorites/tags
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
| name | string | 是 | 标签名称 |
| color | string | 否 | 标签颜色(默认 #4361ee) |
PUT /api/favorites/tags/:tagId
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
| name | string | 否 | 标签名称 |
| color | string | 否 | 标签颜色 |
DELETE /api/favorites/tags/:tagId
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
PUT /api/favorites/tags/order
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
| tagOrders | array | 是 | 排序数据 [{id, order}, ...] |
GET /api/notifications
查询参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
响应示例:
{
"success": true,
"data": {
"notifications": [
{
"id": "notification-uuid",
"userId": "user-uuid",
"type": "like",
"postId": "post-uuid",
"postTitle": "帖子标题...",
"fromUserId": "from-user-uuid",
"fromUsername": "张三",
"timestamp": "2024-01-01T00:00:00.000Z",
"read": false,
"postExists": true
}
]
}
}通知类型:
| type | 说明 |
|---|---|
| like | 点赞通知 |
| comment | 评论通知 |
| comment_reply | 回复通知 |
| follow | 关注通知 |
| system | 系统通知 |
POST /api/notifications/:id/read
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| id | string | 通知ID |
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
POST /api/notifications/read-all
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
GET /api/reports/types
响应示例:
{
"success": true,
"data": {
"types": {
"SPAM": "垃圾广告",
"HARASSMENT": "骚扰辱骂",
"INAPPROPRIATE": "不当内容",
"FALSE_INFO": "虚假信息",
"COPYRIGHT": "侵权内容",
"OTHER": "其他"
}
}
}POST /api/reports
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| reporterId | string | 是 | 举报人ID |
| targetType | string | 是 | 目标类型:post/comment |
| targetId | string | 是 | 目标ID |
| reason | string | 是 | 举报原因(见类型列表) |
| description | string | 否 | 详细描述 |
请求示例:
{
"reporterId": "user-uuid",
"targetType": "post",
"targetId": "post-uuid",
"reason": "SPAM",
"description": "这是垃圾广告"
}GET /api/reports/user/:userId
GET /api/stats
响应示例:
{
"success": true,
"data": {
"stats": {
"totalUsers": 1000,
"totalPosts": 5000,
"todayPosts": 50,
"totalComments": 10000,
"totalLikes": 25000,
"activeUsers": 500,
"anonymousPosts": 200
}
}
}GET /api/search
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| q | string | 是 | - | 搜索关键词 |
| type | string | 否 | posts | 搜索类型:posts/users |
| page | number | 否 | 1 | 页码 |
| limit | number | 否 | 100 | 每页数量 |
GET /api/schools
响应示例:
{
"success": true,
"data": {
"schools": [
{
"id": "XXXX",
"name": "XX学校",
"classInfo": [
{ "year": 2024, "classCount": 25 }
]
}
]
}
}GET /api/config/public
响应示例:
{
"success": true,
"data": {
"config": {
"contentLimits": {
"post": 10000,
"comment": 500,
"username": { "min": 2, "max": 20 }
},
"upload": {
"maxFiles": 32,
"maxFileSize": 33554432,
"allowedTypes": ["image/jpeg", "image/png", ...]
}
}
}
}POST /api/messages
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| senderId | string | 是 | 发送者ID |
| receiverId | string | 是 | 接收者ID |
| content | string | 是 | 消息内容(最多2000字) |
请求示例:
{
"senderId": "user-uuid-1",
"receiverId": "user-uuid-2",
"content": "你好!"
}响应示例:
{
"success": true,
"message": "消息发送成功",
"data": {
"message": {
"id": "message-uuid",
"conversationId": "conversation-uuid",
"senderId": "user-uuid-1",
"receiverId": "user-uuid-2",
"content": "你好!",
"type": "text",
"read": false,
"createdAt": "2024-01-01T00:00:00.000Z",
"senderUsername": "张三",
"senderAvatar": "/images/avatars/xxx.jpg"
}
}
}发送规则:
- 互相关注的用户可以无限次互发消息
- 非互关用户:A发消息给B后,必须等B回复才能继续发送
- 被对方拉黑或自己拉黑对方时无法发送
GET /api/messages
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| userId | string | 是 | - | 当前用户ID |
| otherUserId | string | 是 | - | 对方用户ID |
| limit | number | 否 | 50 | 每页数量 |
| before | string | 否 | - | 获取此时间之前的消息 |
响应示例:
{
"success": true,
"data": {
"messages": [
{
"id": "message-uuid",
"conversationId": "conversation-uuid",
"senderId": "user-uuid-1",
"receiverId": "user-uuid-2",
"content": "你好!",
"type": "text",
"read": true,
"createdAt": "2024-01-01T00:00:00.000Z",
"senderUsername": "张三",
"senderAvatar": "/images/avatars/xxx.jpg"
}
]
}
}GET /api/messages/unread
查询参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
响应示例:
{
"success": true,
"data": {
"unreadCount": 5
}
}GET /api/messages/check-permission
查询参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| senderId | string | 是 | 发送者ID |
| receiverId | string | 是 | 接收者ID |
响应示例:
{
"success": true,
"data": {
"canSend": true,
"reason": "互相关注用户",
"relation": {
"isFollowing": true,
"isFollower": true,
"isMutualFollow": true
},
"blockStatus": {
"isBlocked": false,
"isBlockedBy": false
}
}
}GET /api/messages/contactable-users
查询参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
响应示例:
{
"success": true,
"data": {
"users": [
{
"id": "user-uuid",
"username": "张三",
"avatar": "/images/avatars/xxx.jpg",
"school": "XXXX",
"isFollowing": true,
"isFollower": true
}
]
}
}DELETE /api/messages/:messageId
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| messageId | string | 消息ID |
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
GET /api/conversations
查询参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
响应示例:
{
"success": true,
"data": {
"conversations": [
{
"id": "conversation-uuid",
"otherUser": {
"id": "user-uuid",
"username": "张三",
"avatar": "/images/avatars/xxx.jpg",
"school": "XXXX"
},
"lastMessage": {
"content": "好的",
"senderId": "user-uuid",
"createdAt": "2024-01-01T00:00:00.000Z"
},
"updatedAt": "2024-01-01T00:00:00.000Z",
"unreadCount": 2
}
]
}
}DELETE /api/conversations/:conversationId
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| conversationId | string | 会话ID |
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | string | 是 | 用户ID |
POST /api/block
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| blockerId | string | 是 | 拉黑者ID |
| blockedId | string | 是 | 被拉黑者ID |
响应示例:
{
"success": true,
"message": "拉黑成功",
"data": {
"blocked": true
}
}注意:拉黑用户时会自动取消双方的关注关系
POST /api/unblock
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| blockerId | string | 是 | 拉黑者ID |
| blockedId | string | 是 | 被拉黑者ID |
响应示例:
{
"success": true,
"message": "取消拉黑成功",
"data": {
"blocked": false
}
}GET /api/block/status
查询参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| blockerId | string | 是 | 用户ID |
| blockedId | string | 是 | 目标用户ID |
响应示例:
{
"success": true,
"data": {
"isBlocked": true,
"isBlockedBy": false
}
}GET /api/block/relation
查询参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId1 | string | 是 | 用户1 ID |
| userId2 | string | 是 | 用户2 ID |
响应示例:
{
"success": true,
"data": {
"hasBlockRelation": true
}
}GET /api/blocked/:userId
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| userId | string | 用户ID |
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | number | 否 | 1 | 页码 |
| limit | number | 否 | 20 | 每页数量 |
响应示例:
{
"success": true,
"data": {
"list": [
{
"id": "user-uuid",
"username": "张三",
"avatar": "/images/avatars/xxx.jpg",
"school": "XXXX",
"grade": "高一",
"className": "高一(1)班",
"blockedAt": "2024-01-01T00:00:00.000Z",
"isAdmin": false
}
],
"pagination": {
"currentPage": 1,
"totalPages": 5,
"total": 100,
"hasNext": true,
"hasPrev": false
}
}
}GET /api/blocked/count/:userId
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| userId | string | 用户ID |
响应示例:
{
"success": true,
"data": {
"count": 10
}
}GET /api/announcements/active
响应示例:
{
"success": true,
"data": {
"announcements": [
{
"id": "announcement-uuid",
"title": "系统公告",
"content": "公告内容...",
"type": "info",
"pinned": true,
"active": true,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
}
]
}
}GET /api/announcements/:id
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| id | string | 公告ID |
POST /api/admin/broadcast-message
Authorization: Bearer adminToken
Content-Type: application/json
{
"title": "周末活动通知",
"content": "本周六下午学校篮球场举行社团招新活动,欢迎大家参加!"
}
需管理员登录。创建一条广播通知(target: 'all'),所有用户的「消息」
列表均可看到(单文档共享,不逐用户插入,海量用户也不卡)。适合活动、
维护等全体通知。
响应:
{ "success": true, "message": "广播消息已发送", "notificationId": "..." }广播通知的已读机制:广播是全体共享文档,已读状态按用户独立记录
(readBy 数组),read 字段在响应层按当前用户计算——A 用户已读不影响
B 用户的未读状态;"全部已读"与未读数统计均包含广播通知。
GET /api/run-mode
响应示例(展开式响应):
{
"success": true,
"message": "获取成功",
"mode": "normal",
"message": null
}mode 可选值:
| 值 | 说明 |
|---|---|
normal |
正常运行 |
maintenance |
维护模式(普通用户无法访问) |
readonly |
只读模式(无法发帖/评论) |
GET /api/maintenance-message
响应示例:
{
"success": true,
"data": {
"message": "系统维护中,预计恢复时间:2024-01-01 12:00"
}
}以下接口需要管理员权限,通过中间件验证。
GET /api/admin/posts
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | number | 否 | 1 | 页码 |
| limit | number | 否 | 100 | 每页数量 |
| search | string | 否 | - | 搜索关键词 |
DELETE /api/admin/posts/:id
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| adminId | string | 是 | 管理员ID |
| reason | string | 否 | 删除原因 |
GET /api/admin/users
GET /api/admin/banned-users
POST /api/admin/users/:id/ban
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| id | string | 用户ID |
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| adminId | string | 是 | 管理员ID |
| duration | number | 否 | 封禁天数(默认7,365为永久) |
| reason | string | 否 | 封禁原因 |
POST /api/admin/users/:id/unban
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| adminId | string | 是 | 管理员ID |
GET /api/admin/stats
响应示例:
{
"success": true,
"data": {
"stats": {
"totalUsers": 1000,
"totalPosts": 5000,
"bannedUsers": 10,
"activeUsers": 800,
"todayPosts": 50,
"weekPosts": 300,
"monthPosts": 1000,
"totalComments": 10000,
"totalLikes": 25000,
"anonymousPosts": 200,
"gradeDistribution": { "高一": 300, "高二": 350 },
"schoolDistribution": { "XXXX": 500, "YYYY": 300 },
"topActiveUsers": [ ... ]
}
}
}GET /api/admin/recent-activity
GET /api/admin/comments
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | number | 否 | 1 | 页码 |
| limit | number | 否 | 100 | 每页数量 |
| search | string | 否 | - | 搜索关键词 |
DELETE /api/admin/comments/:id
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| adminId | string | 是 | 管理员ID |
| postId | string | 是 | 帖子ID |
| reason | string | 否 | 删除原因 |
GET /api/admin/logs
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | number | 否 | 1 | 页码 |
| limit | number | 否 | 50 | 每页数量 |
| level | string | 否 | ALL | 日志级别 |
| search | string | 否 | - | 搜索关键词 |
| date | string | 否 | - | 日期(YYYY-MM-DD) |
GET /api/admin/logs/dates
DELETE /api/admin/logs
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| adminId | string | 是 | 管理员ID |
| date | string | 否 | 日期(不填则清空全部) |
DELETE /api/admin/logs/date
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| adminId | string | 是 | 管理员ID |
| date | string | 是 | 日期(YYYY-MM-DD) |
GET /api/admin/reports
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | number | 否 | 1 | 页码 |
| limit | number | 否 | 100 | 每页数量 |
| status | string | 否 | - | 状态:pending/processed/rejected |
GET /api/admin/reports/stats
POST /api/admin/reports/:reportId/process
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| adminId | string | 是 | 管理员ID |
| action | string | 是 | 操作:approve/reject |
| banDuration | number | 否 | 封禁天数 |
| note | string | 否 | 备注 |
GET /api/admin/config
PUT /api/admin/config
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| adminId | string | 是 | 管理员ID |
| updates | object | 是 | 配置更新对象 |
GET /api/admin/admins
POST /api/admin/admins
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| adminId | string | 是 | 当前管理员ID |
| newAdminId | string | 是 | 新管理员QQ号或用户ID |
DELETE /api/admin/admins
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| adminId | string | 是 | 当前管理员ID |
| targetAdminId | string | 是 | 目标管理员ID |
GET /api/admin/announcements
POST /api/admin/announcements
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| title | string | 是 | 公告标题 |
| content | string | 是 | 公告内容(支持 Markdown) |
| type | string | 否 | 类型:info/warning/error(默认 info) |
| pinned | boolean | 否 | 是否置顶 |
| active | boolean | 否 | 是否启用(默认 true) |
PUT /api/admin/announcements/:id
参数同创建公告(均为选填)。
DELETE /api/admin/announcements/:id
PATCH /api/admin/announcements/:id/toggle-status
PATCH /api/admin/announcements/:id/toggle-pinned
PATCH /api/admin/announcements/batch-status
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| ids | string[] | 是 | 公告ID数组 |
| active | boolean | 是 | 目标状态 |
GET /api/admin/ip-stats
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | number | 否 | 1 | 页码 |
| limit | number | 否 | 100 | 每页数量 |
GET /api/admin/ip-stats/summary
DELETE /api/admin/ip-stats/:ip
DELETE /api/admin/ip-stats
POST /api/admin/run-mode
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| mode | string | 是 | normal / maintenance / readonly |
| message | string | 否 | 维护时显示的消息 |
GET /api/admin/categories
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | number | 否 | 1 | 页码 |
| limit | number | 否 | 50 | 每页数量 |
| isActive | boolean | 否 | - | 按启用状态筛选 |
POST /api/admin/categories
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| name | string | 是 | 栏目名称 |
| description | string | 否 | 栏目描述 |
| icon | string | 否 | 图标(FontAwesome类名,默认 fa-folder) |
| color | string | 否 | 颜色(十六进制,默认 #4361ee) |
| order | number | 否 | 排序权重(默认0) |
PUT /api/admin/categories/:id
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| id | string | 栏目ID |
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| name | string | 否 | 栏目名称 |
| description | string | 否 | 栏目描述 |
| icon | string | 否 | 图标 |
| color | string | 否 | 颜色 |
| order | number | 否 | 排序权重 |
| isActive | boolean | 否 | 是否启用 |
DELETE /api/admin/categories/:id
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| id | string | 栏目ID |
注意:删除栏目后,该栏目的帖子将移至"无栏目"状态,不会被删除。
PATCH /api/admin/categories/:id/toggle-status
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| id | string | 栏目ID |
响应示例:
{
"success": true,
"message": "栏目已启用",
"data": {
"category": {
"id": "uuid-string",
"name": "学习交流",
"isActive": true
}
}
}GET /api/admin/category-applications
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | number | 否 | 1 | 页码 |
| limit | number | 否 | 20 | 每页数量 |
| status | string | 否 | - | 状态:pending/approved/rejected |
POST /api/admin/category-applications/:id/approve
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| id | string | 申请ID |
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| reviewNote | string | 否 | 审核备注 |
响应示例:
{
"success": true,
"message": "栏目「游戏讨论」创建成功",
"data": {
"category": {
"id": "uuid-string",
"name": "游戏讨论"
},
"application": {
"id": "uuid-string",
"status": "approved"
}
}
}POST /api/admin/category-applications/:id/reject
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| id | string | 申请ID |
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| reviewNote | string | 否 | 拒绝原因 |
POST /api/admin/self-destruct/level3
⚠️ 危险操作:删除所有帖子、评论、私信数据
POST /api/admin/self-destruct/level2
⚠️ 危险操作:清空整个数据库
POST /api/admin/self-destruct/level1
⚠️ 极危险操作:删除论坛所有文件
| HTTP状态码 | 说明 |
|---|---|
| 200 | 成功 |
| 201 | 创建成功 |
| 400 | 请求参数错误 |
| 401 | 未授权(密码错误等) |
| 403 | 禁止访问(无权限) |
| 404 | 资源不存在 |
| 500 | 服务器内部错误 |
/**
* @typedef {Object} User
* @property {string} id - UUID
* @property {string} qq - QQ号
* @property {string} username - 用户名
* @property {string} email - 邮箱
* @property {string} password - 加密密码(不返回)
* @property {string} school - 学校ID
* @property {number} enrollmentYear - 入学年份
* @property {string} className - 班级
* @property {string} grade - 年级
* @property {string} [avatar] - 头像URL
* @property {string} createdAt - 注册时间
* @property {string} [lastLogin] - 最后登录
* @property {number} postCount - 发帖数
* @property {number} commentCount - 评论数
* @property {boolean} isActive - 是否激活
* @property {Object} [settings] - 用户设置
*//**
* @typedef {Object} Post
* @property {string} id - UUID
* @property {string} userId - 作者ID
* @property {string} username - 作者名
* @property {string} school - 学校
* @property {string} grade - 年级
* @property {string} className - 班级
* @property {string} content - 内容
* @property {Image[]} images - 图片
* @property {boolean} anonymous - 是否匿名
* @property {string} timestamp - 发布时间
* @property {string} [updatedAt] - 更新时间
* @property {number} likes - 点赞数
* @property {string[]} likedBy - 点赞用户ID
* @property {number} dislikes - 点踩数
* @property {string[]} dislikedBy - 点踩用户ID
* @property {number} viewCount - 浏览量
* @property {Comment[]} comments - 评论
* @property {boolean} isDeleted - 是否删除
*//**
* @typedef {Object} Image
* @property {string} url - 图片URL
* @property {string} filename - 文件名
*//**
* @typedef {Object} Comment
* @property {string} id - UUID
* @property {string} userId - 作者ID
* @property {string} username - 作者名
* @property {string} content - 内容
* @property {boolean} anonymous - 是否匿名
* @property {string} timestamp - 时间
* @property {Reply[]} [replies] - 回复
*/
/**
* @typedef {Object} Reply
* @property {string} id - UUID
* @property {string} userId - 作者ID
* @property {string} username - 作者名
* @property {string} content - 内容
* @property {boolean} anonymous - 是否匿名
* @property {string} replyTo - 回复目标ID
* @property {string} timestamp - 时间
* @property {Reply[]} [replies] - 嵌套回复
*/本系统采用 JWT 双 Token 认证:
| Token 类型 | 用途 | 有效期 | 存储位置 |
|---|---|---|---|
accessToken |
API 请求认证 | 7 天 | 内存 / localStorage |
refreshToken |
刷新 accessToken | 30 天 | localStorage |
| 管理员 Token | 管理后台认证 | 24 小时 | 内存 |
Android 客户端内置 Token 刷新拦截器(TokenRefreshInterceptor),在收到 401 响应时自动:
- 清除过期的 accessToken
- 使用 refreshToken 请求刷新
- 重试原始请求
- 若刷新失败,引导用户重新登录
前端通过 userManager.checkAutoLogin() 自动检查登录状态:
- 有效 Token → 自动登录,初始化用户信息
- 无效/过期 Token → 保持未登录状态
- 401 响应 → 清除 Token,跳转登录页
| 防护类型 | 实现方式 |
|---|---|
| XSS 过滤 | 输入内容经过 sanitize-html 处理 |
| MongoDB 注入防护 | SQL 关键词检测 + 参数化查询 |
| 请求限流 | 基于 IP 的滑动窗口限流 |
| 登录锁定 | 连续 5 次登录失败,锁定 30 分钟 |
| 恶意 User-Agent | 检测并记录可疑请求 |
| 图形验证码 | 登录/注册/管理员登录需通过人机验证(4位随机数字 SVG,5分钟有效,一次性使用) |
- 开发环境:允许
localhost和内网 IP(192.168.x.x / 10.x.x.x / 172.16-31.x.x) - 生产环境:通过
CORS_ORIGIN环境变量配置,允许多个来源(逗号分隔) - HTTP 环境:禁用 HSTS 和高级安全响应头
- 独立的
ADMIN_JWT_SECRET密钥(与用户 JWT 分离) - 所有
/admin/*接口必须携带管理员 Token - 维护模式验证使用 JWT Token,不接受伪造的请求头
| 规则 | 说明 |
|---|---|
| 最小长度 | 8 位(可通过环境变量配置) |
| 大小写字母 | 必须包含 |
| 数字 | 必须包含 |
| 特殊字符 | 可选(默认关闭) |
| 加密方式 | bcryptjs,10 轮盐加密 |
以下操作需要通过邮件验证码确认:
- 修改密码
- 修改邮箱
- 注销账户
- 管理员操作(如自毁模式)
| 限制 | 值 |
|---|---|
| 文件类型 | image/jpeg, image/png, image/gif, image/webp |
| 单文件大小 | 32 MB |
| 单次最多文件 | 32 张 |
| 存储路径 | public/images/ |
| 头像大小 | 最大 2 MB |
/**
* @typedef {Object} Notification
* @property {string} id - UUID
* @property {string} [userId] - 接收者ID(广播通知为 null)
* @property {'like'|'comment'|'comment_reply'|'follow'|'system'} type - 通知类型
* @property {string} [systemType] - 系统通知子类型:'new_device'(新设备登录提醒)、
* 'broadcast'(管理员群发广播消息)
* @property {'user'|'all'} [target] - 通知范围:'user' 单用户 / 'all' 全体广播
* @property {string} [title] - 标题(广播消息/新设备提醒)
* @property {string} [message] - 正文(system 通知使用;like/comment 等互动通知
* 用 content 字段)
* @property {string} [postId] - 帖子ID
* @property {string} [commentId] - 评论ID
* @property {string} [fromUserId] - 发送者ID
* @property {string} [fromUsername] - 发送者用户名
* @property {string} [content] - 内容
* @property {string} timestamp - 时间
* @property {boolean} read - 是否已读(广播通知在响应层按 readBy 计算)
* @property {string[]} [readBy] - 广播通知已读用户ID列表
*/说明:系统/广播类通知(
system)不使用postTitle/fromUsername交互字段,客户端应展示title+message。
/**
* @typedef {Object} Favorite
* @property {string} id - UUID
* @property {string} userId - 用户ID
* @property {string} postId - 帖子ID
* @property {string} [tagId] - 标签ID
* @property {string} createdAt - 收藏时间
*//**
* @typedef {Object} FavoriteTag
* @property {string} _id - MongoDB ID
* @property {string} userId - 用户ID
* @property {string} name - 标签名称
* @property {string} color - 标签颜色
* @property {number} order - 排序
*//**
* @typedef {Object} Follow
* @property {string} id - UUID
* @property {string} followerId - 关注者ID
* @property {string} followingId - 被关注者ID
* @property {string} createdAt - 关注时间
*//**
* @typedef {Object} Conversation
* @property {string} id - 会话ID
* @property {string[]} participants - 参与者ID数组
* @property {Object} lastMessage - 最后一条消息
* @property {string} lastMessage.content - 消息内容
* @property {string} lastMessage.senderId - 发送者ID
* @property {string} lastMessage.createdAt - 发送时间
* @property {string} [canInitiateFrom] - 可发起消息的用户ID(非互关时)
* @property {string} createdAt - 创建时间
* @property {string} updatedAt - 更新时间
* @property {boolean} lastMessageRead - 最后消息是否已读
*//**
* @typedef {Object} Message
* @property {string} id - UUID
* @property {string} conversationId - 会话ID
* @property {string} senderId - 发送者ID
* @property {string} receiverId - 接收者ID
* @property {string} content - 消息内容
* @property {string} type - 消息类型:text
* @property {boolean} read - 是否已读
* @property {string[]} deletedBy - 删除此消息的用户ID列表
* @property {string} createdAt - 创建时间
*//**
* @typedef {Object} Blacklist
* @property {string} id - UUID
* @property {string} blockerId - 拉黑者ID
* @property {string} blockedId - 被拉黑者ID
* @property {string} createdAt - 拉黑时间
*//**
* @typedef {Object} Report
* @property {string} id - UUID
* @property {string} reporterId - 举报人ID
* @property {'post'|'comment'} targetType - 目标类型
* @property {string} targetId - 目标ID
* @property {string} targetUserId - 被举报用户ID
* @property {string} reason - 举报原因
* @property {string} reasonText - 举报原因文本
* @property {string} [description] - 详细描述
* @property {'pending'|'processed'|'rejected'} status - 状态
* @property {string} createdAt - 创建时间
*//**
* @typedef {Object} Announcement
* @property {string} id - UUID
* @property {string} title - 公告标题
* @property {string} content - 公告内容(支持 Markdown)
* @property {'info'|'warning'|'error'} type - 公告类型
* @property {boolean} pinned - 是否置顶
* @property {boolean} active - 是否启用
* @property {string} createdAt - 创建时间
* @property {string} updatedAt - 更新时间
*//**
* @typedef {Object} Category
* @property {string} id - UUID
* @property {string} name - 栏目名称
* @property {string} description - 栏目描述
* @property {string} icon - 图标(FontAwesome 类名)
* @property {string} color - 颜色(十六进制)
* @property {number} order - 排序权重
* @property {boolean} isActive - 是否启用
* @property {number} postCount - 帖子数量
* @property {string} createdBy - 创建者ID
* @property {string} createdAt - 创建时间
*//**
* @typedef {Object} CategoryApplication
* @property {string} id - UUID
* @property {string} categoryName - 申请栏目名称
* @property {string} description - 申请理由
* @property {string} applicantId - 申请人ID
* @property {string} applicantUsername - 申请人用户名
* @property {'pending'|'approved'|'rejected'} status - 申请状态
* @property {string} reviewedBy - 审核者ID
* @property {string} reviewedAt - 审核时间
* @property {string} reviewNote - 审核备注
* @property {string} createdAt - 创建时间
*/本系统已实施多层安全防护措施,包括但不限于:
| 安全措施 | 说明 |
|---|---|
| Helmet 安全头 | 设置 Content-Security-Policy、X-Frame-Options、HSTS 等安全头 |
| CORS 白名单 | 仅允许配置的域名进行跨域请求 |
| XSS 过滤 | 自动过滤请求体中的 XSS 攻击代码 |
| SQL/NoSQL 注入防护 | 检测并阻止潜在的注入攻击,使用 mongo-sanitize |
| HPP 防护 | 防止 HTTP 参数污染攻击 |
| Rate Limiting | 基于 Redis 的滑动窗口限流机制 |
| JWT 认证 | 基于 Token 的身份认证,支持令牌刷新和注销 |
| 登录锁定 | 多次登录失败后锁定账户 |
| 请求 ID 追踪 | 每个请求分配唯一 ID,便于安全审计 |
| 图形验证码 | 登录/注册/管理员登录人机验证,SVG 零依赖生成,Redis+内存双存储 |
- 用户登录成功后,服务器返回
token(访问令牌)和refreshToken(刷新令牌) - 客户端在后续请求中通过
Authorization: Bearer <token>头携带令牌 - 访问令牌默认有效期 7 天,刷新令牌有效期 30 天
- 访问令牌过期后,使用刷新令牌获取新的访问令牌
POST /api/refresh-token
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| refreshToken | string | 是 | 刷新令牌 |
响应示例:
{
"success": true,
"data": {
"token": "新的访问令牌"
}
}POST /api/logout
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| token | string | 是 | 当前访问令牌 |
管理员在登录时会额外获得 adminToken,用于管理员操作的认证。
管理员认证支持两种方式:
- JWT Token(推荐):通过
Authorization: Bearer <adminToken>头携带 - 传统方式(过渡期兼容):通过请求体或查询参数传递
adminId
- 连续 5 次登录失败后,账户将被锁定 30 分钟
- 锁定期间任何登录尝试都会被拒绝
- 成功登录后,失败计数自动清零
安全配置通过环境变量管理,请参考 .env.example 文件:
| 环境变量 | 说明 | 默认值 |
|---|---|---|
| JWT_SECRET | JWT 密钥 | - |
| JWT_EXPIRES_IN | 访问令牌有效期 | 7d |
| CORS_ORIGIN | 允许的跨域来源(逗号分隔) | localhost:2080 |
| LOGIN_MAX_ATTEMPTS | 最大登录尝试次数 | 5 |
| LOGIN_LOCK_TIME | 锁定时间(毫秒) | 1800000 |
| MAX_REQUEST_SIZE | 请求体最大大小(MB) | 10 |
每个 API 响应都会包含以下安全相关头:
| 响应头 | 说明 |
|---|---|
| X-Request-ID | 请求唯一标识,用于追踪和审计 |
| X-Content-Type-Options | 设置为 nosniff,防止 MIME 类型嗅探 |
| X-Frame-Options | 设置为 DENY,防止点击劫持 |
| Strict-Transport-Security | HSTS 配置,强制使用 HTTPS |
| Content-Security-Policy | 内容安全策略 |
-
生产环境必须:
- 设置强随机的
JWT_SECRET和ADMIN_JWT_SECRET - 配置正确的
CORS_ORIGIN白名单 - 启用 HTTPS 并配置 HSTS
- 设置强随机的
-
客户端建议:
- 安全存储令牌(避免 localStorage,推荐 httpOnly Cookie)
- 实现令牌自动刷新机制
- 处理 401 响应时清除本地令牌并跳转登录
-
敏感操作:
- 修改密码、邮箱等敏感操作需要验证码二次验证
- 管理员操作需要独立的管理员令牌认证