Spaces:
Runtime error
Runtime error
Upload 8 files
Browse files- .dockerignore +18 -0
- Dockerfile +42 -0
- README.md +120 -6
- db-config.js +709 -0
- package-lock.json +29 -0
- package.json +30 -0
- r2-storage.js +270 -0
- server-mysql.js +2327 -0
.dockerignore
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
npm-debug.log
|
| 3 |
+
.env*
|
| 4 |
+
.git
|
| 5 |
+
.gitignore
|
| 6 |
+
*.md
|
| 7 |
+
coverage
|
| 8 |
+
.nyc_output
|
| 9 |
+
*.log
|
| 10 |
+
logs/*
|
| 11 |
+
!logs/.gitkeep
|
| 12 |
+
uploads/*
|
| 13 |
+
!uploads/.gitkeep
|
| 14 |
+
campus_circle.db
|
| 15 |
+
.vscode/
|
| 16 |
+
.idea/
|
| 17 |
+
.DS_Store
|
| 18 |
+
Thumbs.db
|
Dockerfile
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 使用官方Node.js 18 LTS镜像
|
| 2 |
+
FROM node:18-slim
|
| 3 |
+
|
| 4 |
+
# 设置工作目录
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# 安装系统依赖
|
| 8 |
+
RUN apt-get update && apt-get install -y \
|
| 9 |
+
python3 \
|
| 10 |
+
make \
|
| 11 |
+
g++ \
|
| 12 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
+
|
| 14 |
+
# 复制package.json和package-lock.json
|
| 15 |
+
COPY package*.json ./
|
| 16 |
+
|
| 17 |
+
# 安装npm依赖
|
| 18 |
+
RUN npm install --omit=dev && npm cache clean --force
|
| 19 |
+
|
| 20 |
+
# 复制应用代码
|
| 21 |
+
COPY . .
|
| 22 |
+
|
| 23 |
+
# 创建必要的目录
|
| 24 |
+
RUN mkdir -p uploads logs images
|
| 25 |
+
|
| 26 |
+
# 设置权限并切换用户
|
| 27 |
+
RUN chown -R 1000:1000 /app
|
| 28 |
+
USER 1000
|
| 29 |
+
|
| 30 |
+
# 暴露Hugging Face Spaces标准端口
|
| 31 |
+
EXPOSE 7860
|
| 32 |
+
|
| 33 |
+
# 设置环境变量
|
| 34 |
+
ENV NODE_ENV=production
|
| 35 |
+
ENV PORT=7860
|
| 36 |
+
|
| 37 |
+
# 健康检查
|
| 38 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
| 39 |
+
CMD node -e "require('http').get('http://localhost:7860/api/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) }).on('error', () => process.exit(1))"
|
| 40 |
+
|
| 41 |
+
# 启动应用
|
| 42 |
+
CMD ["node", "server-mysql.js"]
|
README.md
CHANGED
|
@@ -1,12 +1,126 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
-
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: CampusLoop Backend
|
| 3 |
+
emoji: 🎓
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
+
app_port: 7860
|
| 10 |
+
startup_duration_timeout: 60s
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# CampusLoop Backend API
|
| 14 |
+
|
| 15 |
+
校园圈社交应用后端服务,提供用户管理、动态发布、论坛讨论、活动管理等功能。
|
| 16 |
+
|
| 17 |
+
## 🚀 功能特性
|
| 18 |
+
|
| 19 |
+
- 🔐 **用户认证系统** - 注册、登录、JWT认证
|
| 20 |
+
- 📝 **动态发布** - 文字、图片动态发布与评论
|
| 21 |
+
- 💬 **论坛讨论** - 多话题论坛交流
|
| 22 |
+
- 🎉 **活动管理** - 校园活动发布与参与
|
| 23 |
+
- 📚 **课程表管理** - 个人课程安排
|
| 24 |
+
- 🔍 **失物招领** - 校园失物发布与寻找
|
| 25 |
+
- 📊 **成绩管理** - 学生成绩查询
|
| 26 |
+
- 🔔 **消息通知** - 实时消息推送
|
| 27 |
+
|
| 28 |
+
## 📡 API 接口文档
|
| 29 |
+
|
| 30 |
+
### 认证接口
|
| 31 |
+
- `POST /api/auth/register` - 用户注册
|
| 32 |
+
- `POST /api/auth/login` - 用户登录
|
| 33 |
+
- `GET /api/user/profile` - 获取用户信息
|
| 34 |
+
|
| 35 |
+
### 动态接口
|
| 36 |
+
- `GET /api/posts` - 获取动态列表
|
| 37 |
+
- `POST /api/posts` - 发布动态
|
| 38 |
+
- `GET /api/posts/:id/comments` - 获取动态评论
|
| 39 |
+
- `POST /api/posts/:id/comments` - 添加评论
|
| 40 |
+
- `POST /api/posts/:id/like` - 点赞动态
|
| 41 |
+
|
| 42 |
+
### 论坛接口
|
| 43 |
+
- `GET /api/forum/posts` - 获取论坛帖子
|
| 44 |
+
- `POST /api/forum/posts` - 创建论坛帖子
|
| 45 |
+
- `GET /api/forum/posts/:id` - 获取帖子详情
|
| 46 |
+
|
| 47 |
+
### 活动接口
|
| 48 |
+
- `GET /api/activities` - 获取活动列表
|
| 49 |
+
- `POST /api/activities` - 创建活动
|
| 50 |
+
- `POST /api/activities/:id/join` - 参加活动
|
| 51 |
+
|
| 52 |
+
### 课程表接口
|
| 53 |
+
- `GET /api/schedule` - 获取课程表
|
| 54 |
+
- `POST /api/schedule` - 添加课程
|
| 55 |
+
|
| 56 |
+
### 失物招领接口
|
| 57 |
+
- `GET /api/lostfound` - 获取失物信息
|
| 58 |
+
- `POST /api/lostfound` - 发布失物信息
|
| 59 |
+
|
| 60 |
+
### 成绩接口
|
| 61 |
+
- `GET /api/grades` - 获取成绩
|
| 62 |
+
- `POST /api/grades` - 添加成绩
|
| 63 |
+
|
| 64 |
+
### 通知接口
|
| 65 |
+
- `GET /api/notifications` - 获取通知列表
|
| 66 |
+
- `POST /api/notifications/:id/read` - 标记通知已读
|
| 67 |
+
|
| 68 |
+
## 🏥 健康检查
|
| 69 |
+
|
| 70 |
+
访问 `/api/health` 查看服务状态:
|
| 71 |
+
|
| 72 |
+
```json
|
| 73 |
+
{
|
| 74 |
+
"status": "ok",
|
| 75 |
+
"timestamp": "2024-11-14T03:00:00.000Z",
|
| 76 |
+
"service": "campusloop-backend"
|
| 77 |
+
}
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
## 🛠 技术栈
|
| 81 |
+
|
| 82 |
+
- **后端框架**: Node.js + Express.js
|
| 83 |
+
- **数据库**: MySQL
|
| 84 |
+
- **认证**: JWT (JSON Web Tokens)
|
| 85 |
+
- **文件上传**: Multer
|
| 86 |
+
- **日志系统**: Winston
|
| 87 |
+
- **实时通信**: Socket.io
|
| 88 |
+
- **安全**: Helmet + CORS
|
| 89 |
+
- **容器化**: Docker
|
| 90 |
+
|
| 91 |
+
## 🔧 环境配置
|
| 92 |
+
|
| 93 |
+
应用需要以下环境变量:
|
| 94 |
+
|
| 95 |
+
```bash
|
| 96 |
+
NODE_ENV=production
|
| 97 |
+
PORT=7860
|
| 98 |
+
MYSQL_HOST=your_mysql_host
|
| 99 |
+
MYSQL_PORT=your_mysql_port
|
| 100 |
+
MYSQL_USER=your_mysql_user
|
| 101 |
+
MYSQL_PASSWORD=your_mysql_password
|
| 102 |
+
MYSQL_DATABASE=campus_circle
|
| 103 |
+
JWT_SECRET=your_jwt_secret
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
## 📝 使用说明
|
| 107 |
+
|
| 108 |
+
1. **健康检查**: `GET /api/health`
|
| 109 |
+
2. **用户注册**: `POST /api/auth/register`
|
| 110 |
+
3. **用户登录**: `POST /api/auth/login`
|
| 111 |
+
4. **获取动态**: `GET /api/posts`
|
| 112 |
+
|
| 113 |
+
## 🎯 部署信息
|
| 114 |
+
|
| 115 |
+
- **平台**: Hugging Face Spaces
|
| 116 |
+
- **端口**: 7860
|
| 117 |
+
- **协议**: HTTPS
|
| 118 |
+
- **域名**: 自动分配 `.hf.space` 域名
|
| 119 |
+
|
| 120 |
+
## 📄 许可证
|
| 121 |
+
|
| 122 |
+
MIT License - 详见 LICENSE 文件
|
| 123 |
+
|
| 124 |
+
---
|
| 125 |
+
|
| 126 |
+
由响指AI开发 🤖
|
db-config.js
ADDED
|
@@ -0,0 +1,709 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// db-config.js - 数据库配置和连接管理
|
| 2 |
+
require('dotenv').config();
|
| 3 |
+
const mysql = require('mysql2/promise');
|
| 4 |
+
|
| 5 |
+
// MySQL 连接池配置
|
| 6 |
+
const pool = mysql.createPool({
|
| 7 |
+
host: process.env.MYSQL_HOST || 'gz-cdb-1xrcr3dt.sql.tencentcdb.com',
|
| 8 |
+
port: process.env.MYSQL_PORT || 23767,
|
| 9 |
+
user: process.env.MYSQL_USER || 'root',
|
| 10 |
+
password: process.env.MYSQL_PASSWORD,
|
| 11 |
+
database: process.env.MYSQL_DATABASE || 'campus_circle',
|
| 12 |
+
waitForConnections: true,
|
| 13 |
+
connectionLimit: 10,
|
| 14 |
+
queueLimit: 0,
|
| 15 |
+
enableKeepAlive: true,
|
| 16 |
+
keepAliveInitialDelay: 0,
|
| 17 |
+
connectTimeout: 60000, // 60秒连接超时
|
| 18 |
+
acquireTimeout: 60000, // 60秒获取连接超时
|
| 19 |
+
timeout: 60000, // 60秒查询超时
|
| 20 |
+
timezone: '+08:00' // 设置为北京时间(东八区)
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
// 初始化数据库表
|
| 24 |
+
async function initDatabase() {
|
| 25 |
+
const connection = await pool.getConnection();
|
| 26 |
+
|
| 27 |
+
try {
|
| 28 |
+
console.log('开始初始化MySQL数据库...');
|
| 29 |
+
|
| 30 |
+
// 创建用户表
|
| 31 |
+
await connection.query(`
|
| 32 |
+
CREATE TABLE IF NOT EXISTS users (
|
| 33 |
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
| 34 |
+
username VARCHAR(255) UNIQUE NOT NULL,
|
| 35 |
+
email VARCHAR(255) UNIQUE NOT NULL,
|
| 36 |
+
password VARCHAR(255) NOT NULL,
|
| 37 |
+
avatar LONGTEXT,
|
| 38 |
+
bio TEXT,
|
| 39 |
+
role VARCHAR(50) DEFAULT 'user',
|
| 40 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 41 |
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
| 42 |
+
`);
|
| 43 |
+
|
| 44 |
+
// 为现有用户表添加role字段(如果不存在)
|
| 45 |
+
try {
|
| 46 |
+
await connection.query(`
|
| 47 |
+
ALTER TABLE users ADD COLUMN role VARCHAR(50) DEFAULT 'user'
|
| 48 |
+
`);
|
| 49 |
+
console.log('✅ role字段添加成功');
|
| 50 |
+
} catch (error) {
|
| 51 |
+
if (error.code === 'ER_DUP_FIELDNAME') {
|
| 52 |
+
console.log('role字段已存在,跳过添加');
|
| 53 |
+
} else {
|
| 54 |
+
console.log('添加role字段时出错:', error.message);
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// 为现有用户表添加student_id字段(如果不存在)
|
| 59 |
+
try {
|
| 60 |
+
await connection.query(`
|
| 61 |
+
ALTER TABLE users ADD COLUMN student_id VARCHAR(50) DEFAULT NULL
|
| 62 |
+
`);
|
| 63 |
+
console.log('✅ student_id字段添加成功');
|
| 64 |
+
} catch (error) {
|
| 65 |
+
if (error.code === 'ER_DUP_FIELDNAME') {
|
| 66 |
+
console.log('student_id字段已存在,跳过添加');
|
| 67 |
+
} else {
|
| 68 |
+
console.log('添加student_id字段时出错:', error.message);
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// 为现有用户表添加phone字段(如果不存在)
|
| 73 |
+
try {
|
| 74 |
+
await connection.query(`
|
| 75 |
+
ALTER TABLE users ADD COLUMN phone VARCHAR(20) DEFAULT NULL
|
| 76 |
+
`);
|
| 77 |
+
console.log('✅ phone字段添加成功');
|
| 78 |
+
} catch (error) {
|
| 79 |
+
if (error.code === 'ER_DUP_FIELDNAME') {
|
| 80 |
+
console.log('phone字段已存在,跳过添加');
|
| 81 |
+
} else {
|
| 82 |
+
console.log('添加phone字段时出错:', error.message);
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// 为现有用户表添加major字段(如果不存在)
|
| 87 |
+
try {
|
| 88 |
+
await connection.query(`
|
| 89 |
+
ALTER TABLE users ADD COLUMN major VARCHAR(100) DEFAULT NULL
|
| 90 |
+
`);
|
| 91 |
+
console.log('✅ major字段添加成功');
|
| 92 |
+
} catch (error) {
|
| 93 |
+
if (error.code === 'ER_DUP_FIELDNAME') {
|
| 94 |
+
console.log('major字段已存在,跳过添加');
|
| 95 |
+
} else {
|
| 96 |
+
console.log('添加major字段时出错:', error.message);
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// 为现有用户表添加grade字段(如果不存在)
|
| 101 |
+
try {
|
| 102 |
+
await connection.query(`
|
| 103 |
+
ALTER TABLE users ADD COLUMN grade VARCHAR(20) DEFAULT NULL
|
| 104 |
+
`);
|
| 105 |
+
console.log('✅ grade字段添加成功');
|
| 106 |
+
} catch (error) {
|
| 107 |
+
if (error.code === 'ER_DUP_FIELDNAME') {
|
| 108 |
+
console.log('grade字段已存在,跳过添加');
|
| 109 |
+
} else {
|
| 110 |
+
console.log('添加grade字段时出错:', error.message);
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// 为现有用户表添加gender字段(如果不存在)
|
| 115 |
+
try {
|
| 116 |
+
await connection.query(`
|
| 117 |
+
ALTER TABLE users ADD COLUMN gender VARCHAR(10) DEFAULT NULL
|
| 118 |
+
`);
|
| 119 |
+
console.log('✅ gender字段添加成功');
|
| 120 |
+
} catch (error) {
|
| 121 |
+
if (error.code === 'ER_DUP_FIELDNAME') {
|
| 122 |
+
console.log('gender字段已存在,跳过添加');
|
| 123 |
+
} else {
|
| 124 |
+
console.log('添加gender字段时出错:', error.message);
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
// 升级avatar字段类型以支持base64存储
|
| 129 |
+
try {
|
| 130 |
+
await connection.query(`
|
| 131 |
+
ALTER TABLE users MODIFY COLUMN avatar LONGTEXT
|
| 132 |
+
`);
|
| 133 |
+
console.log('✅ avatar字段类型已升级为LONGTEXT');
|
| 134 |
+
} catch (error) {
|
| 135 |
+
console.log('升级avatar字段类型时出错:', error.message);
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
// 为现有失物招领表添加images字段(如果不存在)
|
| 139 |
+
try {
|
| 140 |
+
await connection.query(`
|
| 141 |
+
ALTER TABLE lost_found ADD COLUMN images LONGTEXT DEFAULT NULL
|
| 142 |
+
`);
|
| 143 |
+
console.log('✅ lost_found表images字段添加成功');
|
| 144 |
+
} catch (error) {
|
| 145 |
+
if (error.code === 'ER_DUP_FIELDNAME') {
|
| 146 |
+
console.log('lost_found表images字段已存在,尝试升级字段类型');
|
| 147 |
+
// 尝试���级字段类型为LONGTEXT
|
| 148 |
+
try {
|
| 149 |
+
await connection.query(`
|
| 150 |
+
ALTER TABLE lost_found MODIFY COLUMN images LONGTEXT DEFAULT NULL
|
| 151 |
+
`);
|
| 152 |
+
console.log('✅ lost_found表images字段升级为LONGTEXT成功');
|
| 153 |
+
} catch (modifyError) {
|
| 154 |
+
console.log('升级lost_found表images字段类型时出错:', modifyError.message);
|
| 155 |
+
}
|
| 156 |
+
} else {
|
| 157 |
+
console.log('添加lost_found表images字段时出错:', error.message);
|
| 158 |
+
// 如果是其他错误,继续执行但记录日志
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
// 创建动态表
|
| 163 |
+
await connection.query(`
|
| 164 |
+
CREATE TABLE IF NOT EXISTS posts (
|
| 165 |
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
| 166 |
+
user_id INT NOT NULL,
|
| 167 |
+
content TEXT,
|
| 168 |
+
image LONGTEXT,
|
| 169 |
+
likes INT DEFAULT 0,
|
| 170 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 171 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 172 |
+
INDEX idx_user_id (user_id),
|
| 173 |
+
INDEX idx_created_at (created_at)
|
| 174 |
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
| 175 |
+
`);
|
| 176 |
+
|
| 177 |
+
// 升级posts表的image字段类型
|
| 178 |
+
try {
|
| 179 |
+
await connection.query(`
|
| 180 |
+
ALTER TABLE posts MODIFY COLUMN image LONGTEXT
|
| 181 |
+
`);
|
| 182 |
+
console.log('✅ posts表image字段已升级为LONGTEXT');
|
| 183 |
+
} catch (error) {
|
| 184 |
+
console.log('升级posts表image字段时出错:', error.message);
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
// 修改content字段为可选
|
| 188 |
+
try {
|
| 189 |
+
await connection.query(`
|
| 190 |
+
ALTER TABLE posts MODIFY COLUMN content TEXT
|
| 191 |
+
`);
|
| 192 |
+
console.log('✅ posts表content字段已改为可选');
|
| 193 |
+
} catch (error) {
|
| 194 |
+
console.log('修改posts表content字段时出错:', error.message);
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
// 创建评论表
|
| 198 |
+
await connection.query(`
|
| 199 |
+
CREATE TABLE IF NOT EXISTS comments (
|
| 200 |
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
| 201 |
+
post_id INT NOT NULL,
|
| 202 |
+
user_id INT NOT NULL,
|
| 203 |
+
content TEXT NOT NULL,
|
| 204 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 205 |
+
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
|
| 206 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 207 |
+
INDEX idx_post_id (post_id)
|
| 208 |
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
| 209 |
+
`);
|
| 210 |
+
|
| 211 |
+
// 创建点赞表
|
| 212 |
+
await connection.query(`
|
| 213 |
+
CREATE TABLE IF NOT EXISTS likes (
|
| 214 |
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
| 215 |
+
post_id INT NOT NULL,
|
| 216 |
+
user_id INT NOT NULL,
|
| 217 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 218 |
+
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
|
| 219 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 220 |
+
UNIQUE KEY unique_like (post_id, user_id),
|
| 221 |
+
INDEX idx_post_id (post_id),
|
| 222 |
+
INDEX idx_user_id (user_id)
|
| 223 |
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
| 224 |
+
`);
|
| 225 |
+
|
| 226 |
+
// 创建活动表
|
| 227 |
+
await connection.query(`
|
| 228 |
+
CREATE TABLE IF NOT EXISTS activities (
|
| 229 |
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
| 230 |
+
title VARCHAR(255) NOT NULL,
|
| 231 |
+
description TEXT NOT NULL,
|
| 232 |
+
location VARCHAR(255) NOT NULL,
|
| 233 |
+
start_time DATETIME NOT NULL,
|
| 234 |
+
end_time DATETIME NOT NULL,
|
| 235 |
+
organizer_id INT NOT NULL,
|
| 236 |
+
participants INT DEFAULT 0,
|
| 237 |
+
status VARCHAR(50) DEFAULT 'active',
|
| 238 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 239 |
+
FOREIGN KEY (organizer_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 240 |
+
INDEX idx_status (status),
|
| 241 |
+
INDEX idx_start_time (start_time)
|
| 242 |
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
| 243 |
+
`);
|
| 244 |
+
|
| 245 |
+
// 创建活动参与表
|
| 246 |
+
await connection.query(`
|
| 247 |
+
CREATE TABLE IF NOT EXISTS activity_participants (
|
| 248 |
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
| 249 |
+
activity_id INT NOT NULL,
|
| 250 |
+
user_id INT NOT NULL,
|
| 251 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 252 |
+
FOREIGN KEY (activity_id) REFERENCES activities(id) ON DELETE CASCADE,
|
| 253 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 254 |
+
UNIQUE KEY unique_participant (activity_id, user_id)
|
| 255 |
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
| 256 |
+
`);
|
| 257 |
+
|
| 258 |
+
// 创建好友关系表
|
| 259 |
+
await connection.query(`
|
| 260 |
+
CREATE TABLE IF NOT EXISTS friendships (
|
| 261 |
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
| 262 |
+
user_id INT NOT NULL,
|
| 263 |
+
friend_id INT NOT NULL,
|
| 264 |
+
status VARCHAR(20) DEFAULT 'pending',
|
| 265 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 266 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 267 |
+
FOREIGN KEY (friend_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 268 |
+
INDEX idx_user_id (user_id),
|
| 269 |
+
INDEX idx_friend_id (friend_id),
|
| 270 |
+
INDEX idx_status (status)
|
| 271 |
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
| 272 |
+
`);
|
| 273 |
+
|
| 274 |
+
// 创建聊天消息表
|
| 275 |
+
await connection.query(`
|
| 276 |
+
CREATE TABLE IF NOT EXISTS chat_messages (
|
| 277 |
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
| 278 |
+
sender_id INT NOT NULL,
|
| 279 |
+
receiver_id INT NOT NULL,
|
| 280 |
+
content TEXT NOT NULL,
|
| 281 |
+
type VARCHAR(20) DEFAULT 'text',
|
| 282 |
+
is_read BOOLEAN DEFAULT FALSE,
|
| 283 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 284 |
+
FOREIGN KEY (sender_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 285 |
+
FOREIGN KEY (receiver_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 286 |
+
INDEX idx_sender_id (sender_id),
|
| 287 |
+
INDEX idx_receiver_id (receiver_id),
|
| 288 |
+
INDEX idx_created_at (created_at)
|
| 289 |
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
| 290 |
+
`);
|
| 291 |
+
|
| 292 |
+
// 创建课程表表
|
| 293 |
+
await connection.query(`
|
| 294 |
+
CREATE TABLE IF NOT EXISTS schedules (
|
| 295 |
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
| 296 |
+
user_id INT NOT NULL,
|
| 297 |
+
course_name VARCHAR(255) NOT NULL,
|
| 298 |
+
teacher VARCHAR(255) NOT NULL,
|
| 299 |
+
weekday INT NOT NULL,
|
| 300 |
+
start_time VARCHAR(10) NOT NULL,
|
| 301 |
+
end_time VARCHAR(10) NOT NULL,
|
| 302 |
+
classroom VARCHAR(255) DEFAULT '',
|
| 303 |
+
week_start INT DEFAULT 1,
|
| 304 |
+
week_end INT DEFAULT 16,
|
| 305 |
+
week_type VARCHAR(20) DEFAULT 'all',
|
| 306 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 307 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 308 |
+
INDEX idx_user_id (user_id)
|
| 309 |
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
| 310 |
+
`);
|
| 311 |
+
|
| 312 |
+
// 创建论坛帖子表
|
| 313 |
+
await connection.query(`
|
| 314 |
+
CREATE TABLE IF NOT EXISTS forum_posts (
|
| 315 |
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
| 316 |
+
user_id INT NOT NULL,
|
| 317 |
+
title VARCHAR(255) NOT NULL,
|
| 318 |
+
content TEXT NOT NULL,
|
| 319 |
+
topic VARCHAR(100) NOT NULL,
|
| 320 |
+
replies INT DEFAULT 0,
|
| 321 |
+
views INT DEFAULT 0,
|
| 322 |
+
likes INT DEFAULT 0,
|
| 323 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 324 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 325 |
+
INDEX idx_topic (topic),
|
| 326 |
+
INDEX idx_created_at (created_at)
|
| 327 |
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
| 328 |
+
`);
|
| 329 |
+
|
| 330 |
+
// 创建论坛帖子点赞表
|
| 331 |
+
await connection.query(`
|
| 332 |
+
CREATE TABLE IF NOT EXISTS forum_post_likes (
|
| 333 |
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
| 334 |
+
post_id INT NOT NULL,
|
| 335 |
+
user_id INT NOT NULL,
|
| 336 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 337 |
+
FOREIGN KEY (post_id) REFERENCES forum_posts(id) ON DELETE CASCADE,
|
| 338 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 339 |
+
UNIQUE KEY unique_like (post_id, user_id)
|
| 340 |
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
| 341 |
+
`);
|
| 342 |
+
|
| 343 |
+
// 创建论坛帖子评论表
|
| 344 |
+
await connection.query(`
|
| 345 |
+
CREATE TABLE IF NOT EXISTS forum_post_comments (
|
| 346 |
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
| 347 |
+
post_id INT NOT NULL,
|
| 348 |
+
user_id INT NOT NULL,
|
| 349 |
+
content TEXT NOT NULL,
|
| 350 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 351 |
+
FOREIGN KEY (post_id) REFERENCES forum_posts(id) ON DELETE CASCADE,
|
| 352 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 353 |
+
INDEX idx_post_id (post_id)
|
| 354 |
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
| 355 |
+
`);
|
| 356 |
+
|
| 357 |
+
// 创建失物招领表
|
| 358 |
+
await connection.query(`
|
| 359 |
+
CREATE TABLE IF NOT EXISTS lost_found (
|
| 360 |
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
| 361 |
+
user_id INT NOT NULL,
|
| 362 |
+
title VARCHAR(255) NOT NULL,
|
| 363 |
+
description TEXT NOT NULL,
|
| 364 |
+
category VARCHAR(100) NOT NULL,
|
| 365 |
+
status VARCHAR(50) DEFAULT 'active',
|
| 366 |
+
contact VARCHAR(255) DEFAULT '',
|
| 367 |
+
images LONGTEXT DEFAULT NULL,
|
| 368 |
+
likes INT DEFAULT 0,
|
| 369 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 370 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 371 |
+
INDEX idx_category (category),
|
| 372 |
+
INDEX idx_status (status)
|
| 373 |
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
| 374 |
+
`);
|
| 375 |
+
|
| 376 |
+
// 创建失物招领点赞表
|
| 377 |
+
await connection.query(`
|
| 378 |
+
CREATE TABLE IF NOT EXISTS lost_found_likes (
|
| 379 |
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
| 380 |
+
item_id INT NOT NULL,
|
| 381 |
+
user_id INT NOT NULL,
|
| 382 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 383 |
+
FOREIGN KEY (item_id) REFERENCES lost_found(id) ON DELETE CASCADE,
|
| 384 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 385 |
+
UNIQUE KEY unique_like (item_id, user_id)
|
| 386 |
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
| 387 |
+
`);
|
| 388 |
+
|
| 389 |
+
// 创建成绩表
|
| 390 |
+
await connection.query(`
|
| 391 |
+
CREATE TABLE IF NOT EXISTS grades (
|
| 392 |
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
| 393 |
+
user_id INT NOT NULL,
|
| 394 |
+
course_name VARCHAR(255) NOT NULL,
|
| 395 |
+
score DECIMAL(5,2) NOT NULL,
|
| 396 |
+
semester VARCHAR(100) NOT NULL,
|
| 397 |
+
credit INT DEFAULT 3,
|
| 398 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 399 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 400 |
+
INDEX idx_user_id (user_id)
|
| 401 |
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
| 402 |
+
`);
|
| 403 |
+
|
| 404 |
+
// 创建失物招领点赞表
|
| 405 |
+
await connection.query(`
|
| 406 |
+
CREATE TABLE IF NOT EXISTS lost_found_likes (
|
| 407 |
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
| 408 |
+
item_id INT NOT NULL,
|
| 409 |
+
user_id INT NOT NULL,
|
| 410 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 411 |
+
FOREIGN KEY (item_id) REFERENCES lost_found(id) ON DELETE CASCADE,
|
| 412 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 413 |
+
UNIQUE KEY unique_like (item_id, user_id)
|
| 414 |
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
| 415 |
+
`);
|
| 416 |
+
|
| 417 |
+
// 创建通知表
|
| 418 |
+
await connection.query(`
|
| 419 |
+
CREATE TABLE IF NOT EXISTS notifications (
|
| 420 |
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
| 421 |
+
user_id INT NOT NULL,
|
| 422 |
+
type VARCHAR(100) NOT NULL,
|
| 423 |
+
title VARCHAR(255) NOT NULL,
|
| 424 |
+
message TEXT NOT NULL,
|
| 425 |
+
read_status TINYINT DEFAULT 0,
|
| 426 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 427 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 428 |
+
INDEX idx_user_id (user_id),
|
| 429 |
+
INDEX idx_read_status (read_status)
|
| 430 |
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
| 431 |
+
`);
|
| 432 |
+
|
| 433 |
+
// 创建默认管理员账户
|
| 434 |
+
const bcrypt = require('bcrypt');
|
| 435 |
+
const hashedPassword = await bcrypt.hash('admin', 10);
|
| 436 |
+
|
| 437 |
+
// 创建或更新管理员账户
|
| 438 |
+
try {
|
| 439 |
+
// 首先检查是否已存在admin用户
|
| 440 |
+
const [existingAdmin] = await connection.query(
|
| 441 |
+
'SELECT id FROM users WHERE username = ? OR email = ?',
|
| 442 |
+
['admin', 'admin@campus.edu']
|
| 443 |
+
);
|
| 444 |
+
|
| 445 |
+
if (existingAdmin.length > 0) {
|
| 446 |
+
// 管理员用户已存在,更新密码和角色(使用默认头像)
|
| 447 |
+
await connection.query(`
|
| 448 |
+
UPDATE users SET password = ?, role = 'admin', avatar = '/images/default-avatar.svg'
|
| 449 |
+
WHERE username = 'admin' OR email = 'admin@campus.edu'
|
| 450 |
+
`, [hashedPassword]);
|
| 451 |
+
console.log('👑 管理员账户更新成功: admin/admin');
|
| 452 |
+
} else {
|
| 453 |
+
// 管理员用户不存在,创建新的
|
| 454 |
+
try {
|
| 455 |
+
await connection.query(`
|
| 456 |
+
INSERT INTO users (username, email, password, role, avatar)
|
| 457 |
+
VALUES ('admin', 'admin@campus.edu', ?, 'admin', '/images/default-avatar.svg')
|
| 458 |
+
`, [hashedPassword]);
|
| 459 |
+
console.log('👑 管理员账户创建成功: admin/admin (新建)');
|
| 460 |
+
} catch (insertError) {
|
| 461 |
+
if (insertError.code === 'ER_BAD_FIELD_ERROR') {
|
| 462 |
+
// role字段不存在,使用不带role的插入语句
|
| 463 |
+
await connection.query(`
|
| 464 |
+
INSERT INTO users (username, email, password, avatar)
|
| 465 |
+
VALUES ('admin', 'admin@campus.edu', ?, '/images/default-avatar.svg')
|
| 466 |
+
`, [hashedPassword]);
|
| 467 |
+
console.log('👑 管理员账户创建成功: admin/admin (无role字段)');
|
| 468 |
+
} else {
|
| 469 |
+
throw insertError;
|
| 470 |
+
}
|
| 471 |
+
}
|
| 472 |
+
}
|
| 473 |
+
} catch (error) {
|
| 474 |
+
console.error('❌ 管理员账户创建/更新失败:', error.message);
|
| 475 |
+
// 不抛出错误,继续执行
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
// 插入示例数据
|
| 479 |
+
try {
|
| 480 |
+
console.log('🎯 开始插入示例数据...');
|
| 481 |
+
|
| 482 |
+
// 插入论坛帖子样例数据
|
| 483 |
+
await connection.query(`
|
| 484 |
+
INSERT IGNORE INTO forum_posts (id, user_id, title, content, topic, replies, views, likes, created_at)
|
| 485 |
+
VALUES (1, 2, '如何高效学习数据结构与算法?', '最近在学数据结构,感觉有些吃力,有没有好的学习方法和资源推荐?', 'study', 15, 128, 8, '2024-01-15 10:30:00')
|
| 486 |
+
`);
|
| 487 |
+
|
| 488 |
+
await connection.query(`
|
| 489 |
+
INSERT IGNORE INTO forum_posts (id, user_id, title, content, topic, replies, views, likes, created_at)
|
| 490 |
+
VALUES (2, 2, '校园食堂新菜品试吃活动', '听说食堂要推出新菜品了,有没有同学想一起去试吃的?', 'life', 8, 95, 12, '2024-01-14 15:20:00')
|
| 491 |
+
`);
|
| 492 |
+
|
| 493 |
+
await connection.query(`
|
| 494 |
+
INSERT IGNORE INTO forum_posts (id, user_id, title, content, topic, replies, views, likes, created_at)
|
| 495 |
+
VALUES (3, 2, 'React开发经验分享', '分享一些React开发中的最佳实践和常见问题解决方案', 'tech', 22, 186, 25, '2024-01-13 09:15:00')
|
| 496 |
+
`);
|
| 497 |
+
|
| 498 |
+
// 插入活动样例数据
|
| 499 |
+
await connection.query(`
|
| 500 |
+
INSERT IGNORE INTO activities (id, title, description, location, start_time, end_time, organizer_id, participants, status, created_at)
|
| 501 |
+
VALUES (1, '校园科技节开幕式', '第十届校园科技节即将开幕,欢迎广大师生参与!本次科技节将展示最新的科技成果和学生创新项目。', '学校大礼堂', '2024-12-20 09:00:00', '2024-12-20 12:00:00', 2, 156, 'active', '2024-11-15 10:30:00')
|
| 502 |
+
`);
|
| 503 |
+
|
| 504 |
+
await connection.query(`
|
| 505 |
+
INSERT IGNORE INTO activities (id, title, description, location, start_time, end_time, organizer_id, participants, status, created_at)
|
| 506 |
+
VALUES (2, '春季篮球联赛', '校园春季篮球联赛火热开赛,各学院代表队将展开激烈角逐,精彩不容错过!', '���育馆', '2024-12-25 14:00:00', '2024-12-25 18:00:00', 2, 89, 'active', '2024-11-14 15:20:00')
|
| 507 |
+
`);
|
| 508 |
+
|
| 509 |
+
await connection.query(`
|
| 510 |
+
INSERT IGNORE INTO activities (id, title, description, location, start_time, end_time, organizer_id, participants, status, created_at)
|
| 511 |
+
VALUES (3, '迎新文艺晚会', '迎新文艺晚会正在紧张彩排中,各社团精心准备的节目即将与大家见面。', '艺术中心', '2024-12-18 19:00:00', '2024-12-18 21:00:00', 2, 45, 'active', '2024-11-13 09:15:00')
|
| 512 |
+
`);
|
| 513 |
+
|
| 514 |
+
await connection.query(`
|
| 515 |
+
INSERT IGNORE INTO activities (id, title, description, location, start_time, end_time, organizer_id, participants, status, created_at)
|
| 516 |
+
VALUES (4, '编程马拉松大赛', '24小时编程挑战赛,展示你的编程技能,与顶尖程序员同台竞技!奖品丰厚,欢迎报名参加。', '计算机学院实验楼', '2025-01-01 09:00:00', '2025-01-02 09:00:00', 2, 78, 'active', '2024-11-16 14:30:00')
|
| 517 |
+
`);
|
| 518 |
+
|
| 519 |
+
await connection.query(`
|
| 520 |
+
INSERT IGNORE INTO activities (id, title, description, location, start_time, end_time, organizer_id, participants, status, created_at)
|
| 521 |
+
VALUES (5, '校园招聘会', '知名企业校园招聘会,提供实习和就业机会,涵盖IT、金融、制造等多个行业。', '学生活动中心', '2025-01-05 10:00:00', '2025-01-05 17:00:00', 2, 234, 'active', '2024-11-17 11:20:00')
|
| 522 |
+
`);
|
| 523 |
+
|
| 524 |
+
await connection.query(`
|
| 525 |
+
INSERT IGNORE INTO activities (id, title, description, location, start_time, end_time, organizer_id, participants, status, created_at)
|
| 526 |
+
VALUES (6, '环保志愿活动', '校园环保志愿活动,一起为美丽校园贡献力量!清理校园垃圾,种植绿色植物。', '校园各区域', '2025-01-08 08:00:00', '2025-01-08 12:00:00', 2, 67, 'active', '2024-11-18 16:45:00')
|
| 527 |
+
`);
|
| 528 |
+
|
| 529 |
+
await connection.query(`
|
| 530 |
+
INSERT IGNORE INTO activities (id, title, description, location, start_time, end_time, organizer_id, participants, status, created_at)
|
| 531 |
+
VALUES (7, '学术讲座:人工智能前沿', '邀请知名AI专家分享人工智能最新发展趋势,探讨未来科技发展方向。', '学术报告厅', '2025-01-12 15:00:00', '2025-01-12 17:00:00', 2, 123, 'active', '2024-11-19 10:15:00')
|
| 532 |
+
`);
|
| 533 |
+
|
| 534 |
+
await connection.query(`
|
| 535 |
+
INSERT IGNORE INTO activities (id, title, description, location, start_time, end_time, organizer_id, participants, status, created_at)
|
| 536 |
+
VALUES (8, '社团嘉年华', '各大社团展示活动,精彩表演、互动游戏、美食品尝,体验丰富多彩的社团文化!', '中央广场', '2025-01-15 14:00:00', '2025-01-15 20:00:00', 2, 189, 'active', '2024-11-20 13:30:00')
|
| 537 |
+
`);
|
| 538 |
+
|
| 539 |
+
await connection.query(`
|
| 540 |
+
INSERT IGNORE INTO activities (id, title, description, location, start_time, end_time, organizer_id, participants, status, created_at)
|
| 541 |
+
VALUES (9, '创业项目路演', '学生创业项目路演大赛,展示创新创业成果,获得投资机会和创业指导。', '创新创业中心', '2025-01-18 13:30:00', '2025-01-18 17:30:00', 2, 56, 'active', '2024-11-21 09:45:00')
|
| 542 |
+
`);
|
| 543 |
+
|
| 544 |
+
await connection.query(`
|
| 545 |
+
INSERT IGNORE INTO activities (id, title, description, location, start_time, end_time, organizer_id, participants, status, created_at)
|
| 546 |
+
VALUES (10, '春游踏青活动', '春暖花开,组织全校师生春游踏青,亲近自然,放松身心,增进友谊。', '郊外公园', '2025-01-22 08:00:00', '2025-01-22 18:00:00', 2, 145, 'active', '2024-11-22 12:20:00')
|
| 547 |
+
`);
|
| 548 |
+
|
| 549 |
+
// 插入活动参与者样例数据
|
| 550 |
+
await connection.query(`
|
| 551 |
+
INSERT IGNORE INTO activity_participants (activity_id, user_id, created_at)
|
| 552 |
+
VALUES (1, 2, '2024-01-16 10:30:00')
|
| 553 |
+
`);
|
| 554 |
+
|
| 555 |
+
await connection.query(`
|
| 556 |
+
INSERT IGNORE INTO activity_participants (activity_id, user_id, created_at)
|
| 557 |
+
VALUES (2, 2, '2024-01-15 15:20:00')
|
| 558 |
+
`);
|
| 559 |
+
|
| 560 |
+
await connection.query(`
|
| 561 |
+
INSERT IGNORE INTO activity_participants (activity_id, user_id, created_at)
|
| 562 |
+
VALUES (4, 2, '2024-01-17 14:30:00')
|
| 563 |
+
`);
|
| 564 |
+
|
| 565 |
+
await connection.query(`
|
| 566 |
+
INSERT IGNORE INTO activity_participants (activity_id, user_id, created_at)
|
| 567 |
+
VALUES (5, 2, '2024-01-18 11:20:00')
|
| 568 |
+
`);
|
| 569 |
+
|
| 570 |
+
await connection.query(`
|
| 571 |
+
INSERT IGNORE INTO activity_participants (activity_id, user_id, created_at)
|
| 572 |
+
VALUES (7, 2, '2024-01-20 10:15:00')
|
| 573 |
+
`);
|
| 574 |
+
|
| 575 |
+
// 插入课程表样例数据
|
| 576 |
+
await connection.query(`
|
| 577 |
+
INSERT IGNORE INTO schedules (id, user_id, course_name, teacher, weekday, start_time, end_time, classroom, created_at)
|
| 578 |
+
VALUES (1, 2, '高等数学A', '张教授', 1, '08:00', '09:40', '教学楼A201', '2024-01-15 00:00:00')
|
| 579 |
+
`);
|
| 580 |
+
|
| 581 |
+
await connection.query(`
|
| 582 |
+
INSERT IGNORE INTO schedules (id, user_id, course_name, teacher, weekday, start_time, end_time, classroom, created_at)
|
| 583 |
+
VALUES (2, 2, '大学英语', '李老师', 2, '10:00', '11:40', '教学楼B305', '2024-01-15 00:00:00')
|
| 584 |
+
`);
|
| 585 |
+
|
| 586 |
+
await connection.query(`
|
| 587 |
+
INSERT IGNORE INTO schedules (id, user_id, course_name, teacher, weekday, start_time, end_time, classroom, created_at)
|
| 588 |
+
VALUES (3, 2, '程序设计基础', '王老师', 3, '14:00', '15:40', '实验楼C102', '2024-01-15 00:00:00')
|
| 589 |
+
`);
|
| 590 |
+
|
| 591 |
+
// 插入失物招领样例数据
|
| 592 |
+
await connection.query(`
|
| 593 |
+
INSERT IGNORE INTO lost_found (id, user_id, title, description, category, status, contact, created_at)
|
| 594 |
+
VALUES (1, 2, '苹果手机 iPhone 14', '黑色iPhone 14,在图书馆三楼遗失,手机壳是透明的', 'electronics', 'active', '微信:abc123', '2024-01-15 10:30:00')
|
| 595 |
+
`);
|
| 596 |
+
|
| 597 |
+
await connection.query(`
|
| 598 |
+
INSERT IGNORE INTO lost_found (id, user_id, title, description, category, status, contact, created_at)
|
| 599 |
+
VALUES (2, 2, '高等数学教材', '同济版高等数学上册,封面有些磨损,内有笔记', 'books', 'active', 'QQ:123456789', '2024-01-14 15:20:00')
|
| 600 |
+
`);
|
| 601 |
+
|
| 602 |
+
await connection.query(`
|
| 603 |
+
INSERT IGNORE INTO lost_found (id, user_id, title, description, category, status, contact, created_at)
|
| 604 |
+
VALUES (3, 2, '黑色钥匙串', '有宿舍钥匙和自行车钥匙,钥匙串上有小熊挂件', 'keys', 'resolved', '电话:138****5678', '2024-01-13 09:15:00')
|
| 605 |
+
`);
|
| 606 |
+
|
| 607 |
+
// 插入动态样例数据
|
| 608 |
+
await connection.query(`
|
| 609 |
+
INSERT IGNORE INTO posts (id, user_id, content, likes, created_at)
|
| 610 |
+
VALUES
|
| 611 |
+
(1, 2, '欢迎来到校园圈!这里是同学们分享校园生活的地方。', 5, '2024-01-15 08:00:00'),
|
| 612 |
+
(2, 2, '今天天气真不错,适合在校园里走走拍照。', 8, '2024-01-14 16:30:00'),
|
| 613 |
+
(3, 2, '图书馆新到了一批好书,推荐大家去看看!📚', 12, '2024-01-13 14:20:00'),
|
| 614 |
+
(4, 2, '食堂今天的新菜品超好吃!强烈推荐麻辣香锅🍲', 15, '2024-01-12 12:30:00'),
|
| 615 |
+
(5, 2, '期末考试加油!大家一起努力💪', 20, '2024-01-11 09:00:00'),
|
| 616 |
+
(6, 2, '校园篮球赛精彩瞬间,我们班赢了!🏀', 18, '2024-01-10 17:45:00'),
|
| 617 |
+
(7, 2, '分享一个学习小技巧:番茄工作法真的很有效!', 25, '2024-01-09 20:15:00'),
|
| 618 |
+
(8, 2, '校园的樱花开了,太美了!春天来了🌸', 30, '2024-01-08 15:30:00'),
|
| 619 |
+
(9, 2, '求推荐好用的学习APP,有没有同学分享一下?', 10, '2024-01-07 11:00:00'),
|
| 620 |
+
(10, 2, '今天参加了社团活动,认识了很多新朋友😊', 22, '2024-01-06 18:20:00')
|
| 621 |
+
`);
|
| 622 |
+
|
| 623 |
+
// 插入成绩样例数据
|
| 624 |
+
await connection.query(`
|
| 625 |
+
INSERT IGNORE INTO grades (id, user_id, course_name, score, semester, credit, created_at)
|
| 626 |
+
VALUES (1, 2, '高等数学A', 92, '2024春季', 4, '2024-01-15 00:00:00')
|
| 627 |
+
`);
|
| 628 |
+
|
| 629 |
+
await connection.query(`
|
| 630 |
+
INSERT IGNORE INTO grades (id, user_id, course_name, score, semester, credit, created_at)
|
| 631 |
+
VALUES (2, 2, '大学英语', 88, '2024春季', 3, '2024-01-15 00:00:00')
|
| 632 |
+
`);
|
| 633 |
+
|
| 634 |
+
await connection.query(`
|
| 635 |
+
INSERT IGNORE INTO grades (id, user_id, course_name, score, semester, credit, created_at)
|
| 636 |
+
VALUES (3, 2, '程序设计基础', 95, '2024春季', 3, '2024-01-15 00:00:00')
|
| 637 |
+
`);
|
| 638 |
+
|
| 639 |
+
await connection.query(`
|
| 640 |
+
INSERT IGNORE INTO grades (id, user_id, course_name, score, semester, credit, created_at)
|
| 641 |
+
VALUES (4, 2, '线性代数', 85, '2024春季', 3, '2024-01-15 00:00:00')
|
| 642 |
+
`);
|
| 643 |
+
|
| 644 |
+
await connection.query(`
|
| 645 |
+
INSERT IGNORE INTO grades (id, user_id, course_name, score, semester, credit, created_at)
|
| 646 |
+
VALUES (5, 2, '大学物理', 90, '2024春季', 4, '2024-01-15 00:00:00')
|
| 647 |
+
`);
|
| 648 |
+
|
| 649 |
+
await connection.query(`
|
| 650 |
+
INSERT IGNORE INTO grades (id, user_id, course_name, score, semester, credit, created_at)
|
| 651 |
+
VALUES (6, 2, '数据结构', 93, '2024秋季', 4, '2024-09-01 00:00:00')
|
| 652 |
+
`);
|
| 653 |
+
|
| 654 |
+
await connection.query(`
|
| 655 |
+
INSERT IGNORE INTO grades (id, user_id, course_name, score, semester, credit, created_at)
|
| 656 |
+
VALUES (7, 2, '计算机网络', 87, '2024秋季', 3, '2024-09-01 00:00:00')
|
| 657 |
+
`);
|
| 658 |
+
|
| 659 |
+
await connection.query(`
|
| 660 |
+
INSERT IGNORE INTO grades (id, user_id, course_name, score, semester, credit, created_at)
|
| 661 |
+
VALUES (8, 2, '操作系统', 91, '2024秋季', 4, '2024-09-01 00:00:00')
|
| 662 |
+
`);
|
| 663 |
+
|
| 664 |
+
await connection.query(`
|
| 665 |
+
INSERT IGNORE INTO grades (id, user_id, course_name, score, semester, credit, created_at)
|
| 666 |
+
VALUES (9, 2, '数据库原理', 89, '2024秋季', 3, '2024-09-01 00:00:00')
|
| 667 |
+
`);
|
| 668 |
+
|
| 669 |
+
await connection.query(`
|
| 670 |
+
INSERT IGNORE INTO grades (id, user_id, course_name, score, semester, credit, created_at)
|
| 671 |
+
VALUES (10, 2, '软件工程', 94, '2024秋季', 3, '2024-09-01 00:00:00')
|
| 672 |
+
`);
|
| 673 |
+
|
| 674 |
+
console.log('🎉 示例数据插入完成');
|
| 675 |
+
} catch (error) {
|
| 676 |
+
console.error('❌ 示例数据插入失败:', error.message);
|
| 677 |
+
// 不抛出错误,继续执行
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
console.log('✅ 数据库初始化完成');
|
| 681 |
+
} catch (error) {
|
| 682 |
+
console.error('❌ 数据库初始化失败:', error);
|
| 683 |
+
throw error;
|
| 684 |
+
} finally {
|
| 685 |
+
connection.release();
|
| 686 |
+
}
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
// 测试数据库连接
|
| 690 |
+
async function testConnection() {
|
| 691 |
+
try {
|
| 692 |
+
const connection = await pool.getConnection();
|
| 693 |
+
console.log('✅ MySQL数据库连接成功!');
|
| 694 |
+
console.log('主机:', process.env.MYSQL_HOST);
|
| 695 |
+
console.log('端口:', process.env.MYSQL_PORT);
|
| 696 |
+
console.log('数据库:', process.env.MYSQL_DATABASE);
|
| 697 |
+
connection.release();
|
| 698 |
+
return true;
|
| 699 |
+
} catch (error) {
|
| 700 |
+
console.error('❌ MySQL数据库连接失败:', error.message);
|
| 701 |
+
return false;
|
| 702 |
+
}
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
module.exports = {
|
| 706 |
+
pool,
|
| 707 |
+
initDatabase,
|
| 708 |
+
testConnection
|
| 709 |
+
};
|
package-lock.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "campusloop-backend-hf",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"lockfileVersion": 2,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "campusloop-backend-hf",
|
| 9 |
+
"version": "1.0.0",
|
| 10 |
+
"license": "MIT",
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"bcrypt": "^5.1.0",
|
| 13 |
+
"cors": "^2.8.5",
|
| 14 |
+
"dotenv": "^16.0.0",
|
| 15 |
+
"express": "^4.18.0",
|
| 16 |
+
"express-validator": "^6.14.0",
|
| 17 |
+
"helmet": "^6.0.0",
|
| 18 |
+
"jsonwebtoken": "^9.0.0",
|
| 19 |
+
"multer": "^1.4.4",
|
| 20 |
+
"mysql2": "^3.6.0",
|
| 21 |
+
"winston": "^3.8.0"
|
| 22 |
+
},
|
| 23 |
+
"engines": {
|
| 24 |
+
"node": ">=18.0.0",
|
| 25 |
+
"npm": ">=8.0.0"
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
}
|
package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "campusloop-backend-hf",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "CampusLoop Backend API for Hugging Face Spaces",
|
| 5 |
+
"main": "server-mysql.js",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"start": "node server-mysql.js",
|
| 8 |
+
"dev": "node server-mysql.js",
|
| 9 |
+
"test": "echo \"Error: no test specified\" && exit 1"
|
| 10 |
+
},
|
| 11 |
+
"keywords": ["campus", "social", "backend", "api", "mysql"],
|
| 12 |
+
"author": "响指AI",
|
| 13 |
+
"license": "MIT",
|
| 14 |
+
"dependencies": {
|
| 15 |
+
"bcrypt": "^5.1.0",
|
| 16 |
+
"cors": "^2.8.5",
|
| 17 |
+
"dotenv": "^16.0.0",
|
| 18 |
+
"express": "^4.18.0",
|
| 19 |
+
"express-validator": "^6.14.0",
|
| 20 |
+
"helmet": "^6.0.0",
|
| 21 |
+
"jsonwebtoken": "^9.0.0",
|
| 22 |
+
"multer": "1.4.4-lts.1",
|
| 23 |
+
"mysql2": "^3.6.0",
|
| 24 |
+
"winston": "^3.8.0"
|
| 25 |
+
},
|
| 26 |
+
"engines": {
|
| 27 |
+
"node": ">=18.0.0",
|
| 28 |
+
"npm": ">=8.0.0"
|
| 29 |
+
}
|
| 30 |
+
}
|
r2-storage.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Cloudflare R2 图片存储模块
|
| 3 |
+
*
|
| 4 |
+
* 配置说明:
|
| 5 |
+
* 在 .env 文件中添加以下环境变量:
|
| 6 |
+
* R2_ACCOUNT_ID=你的Cloudflare账户ID
|
| 7 |
+
* R2_ACCESS_KEY_ID=你的R2访问密钥ID
|
| 8 |
+
* R2_SECRET_ACCESS_KEY=你的R2秘密访问密钥
|
| 9 |
+
* R2_BUCKET_NAME=campusloop-images
|
| 10 |
+
* R2_PUBLIC_URL=https://你的公开访问域名(可选)
|
| 11 |
+
*/
|
| 12 |
+
|
| 13 |
+
const crypto = require('crypto');
|
| 14 |
+
const https = require('https');
|
| 15 |
+
const http = require('http');
|
| 16 |
+
|
| 17 |
+
class R2Storage {
|
| 18 |
+
constructor() {
|
| 19 |
+
this.accountId = process.env.R2_ACCOUNT_ID;
|
| 20 |
+
this.accessKeyId = process.env.R2_ACCESS_KEY_ID;
|
| 21 |
+
this.secretAccessKey = process.env.R2_SECRET_ACCESS_KEY;
|
| 22 |
+
this.bucketName = process.env.R2_BUCKET_NAME || 'campusloop-images';
|
| 23 |
+
this.publicUrl = process.env.R2_PUBLIC_URL;
|
| 24 |
+
|
| 25 |
+
// R2 端点
|
| 26 |
+
this.endpoint = `${this.accountId}.r2.cloudflarestorage.com`;
|
| 27 |
+
this.region = 'auto';
|
| 28 |
+
this.service = 's3';
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
/**
|
| 32 |
+
* 检查 R2 是否已配置
|
| 33 |
+
*/
|
| 34 |
+
isConfigured() {
|
| 35 |
+
return !!(this.accountId && this.accessKeyId && this.secretAccessKey);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
/**
|
| 39 |
+
* 生成 AWS Signature V4 签名
|
| 40 |
+
*/
|
| 41 |
+
sign(key, msg) {
|
| 42 |
+
return crypto.createHmac('sha256', key).update(msg, 'utf8').digest();
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
getSignatureKey(dateStamp) {
|
| 46 |
+
const kDate = this.sign('AWS4' + this.secretAccessKey, dateStamp);
|
| 47 |
+
const kRegion = this.sign(kDate, this.region);
|
| 48 |
+
const kService = this.sign(kRegion, this.service);
|
| 49 |
+
const kSigning = this.sign(kService, 'aws4_request');
|
| 50 |
+
return kSigning;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
/**
|
| 54 |
+
* 上传文件到 R2
|
| 55 |
+
* @param {Buffer} fileBuffer - 文件内容
|
| 56 |
+
* @param {string} fileName - 文件名
|
| 57 |
+
* @param {string} contentType - MIME 类型
|
| 58 |
+
* @param {Object} options - 可选配置
|
| 59 |
+
* @param {string} options.customKey - 自定义存储路径(用于覆盖上传)
|
| 60 |
+
* @returns {Promise<{success: boolean, url: string, key: string}>}
|
| 61 |
+
*/
|
| 62 |
+
async uploadFile(fileBuffer, fileName, contentType, options = {}) {
|
| 63 |
+
if (!this.isConfigured()) {
|
| 64 |
+
throw new Error('R2 存储未配置,请设置环境变量');
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
let key;
|
| 68 |
+
if (options.customKey) {
|
| 69 |
+
// 使用自定义路径(用于头像等需要覆盖的场景)
|
| 70 |
+
key = options.customKey;
|
| 71 |
+
} else {
|
| 72 |
+
// 生成唯一的文件名
|
| 73 |
+
const timestamp = Date.now();
|
| 74 |
+
const randomStr = crypto.randomBytes(8).toString('hex');
|
| 75 |
+
const ext = fileName.split('.').pop() || 'jpg';
|
| 76 |
+
key = `uploads/${timestamp}-${randomStr}.${ext}`;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
const now = new Date();
|
| 80 |
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '');
|
| 81 |
+
const dateStamp = amzDate.slice(0, 8);
|
| 82 |
+
|
| 83 |
+
const method = 'PUT';
|
| 84 |
+
const canonicalUri = `/${this.bucketName}/${key}`;
|
| 85 |
+
const host = this.endpoint;
|
| 86 |
+
|
| 87 |
+
// 计算内容哈希
|
| 88 |
+
const payloadHash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
|
| 89 |
+
|
| 90 |
+
// 构建规范请求
|
| 91 |
+
const canonicalHeaders = [
|
| 92 |
+
`content-type:${contentType}`,
|
| 93 |
+
`host:${host}`,
|
| 94 |
+
`x-amz-content-sha256:${payloadHash}`,
|
| 95 |
+
`x-amz-date:${amzDate}`
|
| 96 |
+
].join('\n') + '\n';
|
| 97 |
+
|
| 98 |
+
const signedHeaders = 'content-type;host;x-amz-content-sha256;x-amz-date';
|
| 99 |
+
|
| 100 |
+
const canonicalRequest = [
|
| 101 |
+
method,
|
| 102 |
+
canonicalUri,
|
| 103 |
+
'', // 查询字符串为空
|
| 104 |
+
canonicalHeaders,
|
| 105 |
+
signedHeaders,
|
| 106 |
+
payloadHash
|
| 107 |
+
].join('\n');
|
| 108 |
+
|
| 109 |
+
// 构建待签名字符串
|
| 110 |
+
const algorithm = 'AWS4-HMAC-SHA256';
|
| 111 |
+
const credentialScope = `${dateStamp}/${this.region}/${this.service}/aws4_request`;
|
| 112 |
+
const canonicalRequestHash = crypto.createHash('sha256').update(canonicalRequest).digest('hex');
|
| 113 |
+
|
| 114 |
+
const stringToSign = [
|
| 115 |
+
algorithm,
|
| 116 |
+
amzDate,
|
| 117 |
+
credentialScope,
|
| 118 |
+
canonicalRequestHash
|
| 119 |
+
].join('\n');
|
| 120 |
+
|
| 121 |
+
// 计算签名
|
| 122 |
+
const signingKey = this.getSignatureKey(dateStamp);
|
| 123 |
+
const signature = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex');
|
| 124 |
+
|
| 125 |
+
// 构建授权头
|
| 126 |
+
const authorization = `${algorithm} Credential=${this.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
| 127 |
+
|
| 128 |
+
// 发送请求
|
| 129 |
+
return new Promise((resolve, reject) => {
|
| 130 |
+
const options = {
|
| 131 |
+
hostname: host,
|
| 132 |
+
port: 443,
|
| 133 |
+
path: canonicalUri,
|
| 134 |
+
method: method,
|
| 135 |
+
headers: {
|
| 136 |
+
'Content-Type': contentType,
|
| 137 |
+
'Content-Length': fileBuffer.length,
|
| 138 |
+
'Host': host,
|
| 139 |
+
'X-Amz-Content-Sha256': payloadHash,
|
| 140 |
+
'X-Amz-Date': amzDate,
|
| 141 |
+
'Authorization': authorization
|
| 142 |
+
}
|
| 143 |
+
};
|
| 144 |
+
|
| 145 |
+
const req = https.request(options, (res) => {
|
| 146 |
+
let data = '';
|
| 147 |
+
res.on('data', chunk => data += chunk);
|
| 148 |
+
res.on('end', () => {
|
| 149 |
+
if (res.statusCode === 200 || res.statusCode === 201) {
|
| 150 |
+
// 构建公开访问 URL
|
| 151 |
+
let publicUrl;
|
| 152 |
+
if (this.publicUrl) {
|
| 153 |
+
publicUrl = `${this.publicUrl}/${key}`;
|
| 154 |
+
} else {
|
| 155 |
+
// 使用 R2 公开访问 URL(需要在 Cloudflare 配置)
|
| 156 |
+
publicUrl = `https://pub-${this.accountId}.r2.dev/${this.bucketName}/${key}`;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
resolve({
|
| 160 |
+
success: true,
|
| 161 |
+
url: publicUrl,
|
| 162 |
+
key: key,
|
| 163 |
+
size: fileBuffer.length
|
| 164 |
+
});
|
| 165 |
+
} else {
|
| 166 |
+
reject(new Error(`R2 上传失败: ${res.statusCode} - ${data}`));
|
| 167 |
+
}
|
| 168 |
+
});
|
| 169 |
+
});
|
| 170 |
+
|
| 171 |
+
req.on('error', (error) => {
|
| 172 |
+
reject(new Error(`R2 请求错误: ${error.message}`));
|
| 173 |
+
});
|
| 174 |
+
|
| 175 |
+
req.write(fileBuffer);
|
| 176 |
+
req.end();
|
| 177 |
+
});
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
/**
|
| 181 |
+
* 删除文件
|
| 182 |
+
* @param {string} key - 文件键名
|
| 183 |
+
*/
|
| 184 |
+
async deleteFile(key) {
|
| 185 |
+
if (!this.isConfigured()) {
|
| 186 |
+
throw new Error('R2 存储未配置');
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
const now = new Date();
|
| 190 |
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '');
|
| 191 |
+
const dateStamp = amzDate.slice(0, 8);
|
| 192 |
+
|
| 193 |
+
const method = 'DELETE';
|
| 194 |
+
const canonicalUri = `/${this.bucketName}/${key}`;
|
| 195 |
+
const host = this.endpoint;
|
| 196 |
+
|
| 197 |
+
const payloadHash = crypto.createHash('sha256').update('').digest('hex');
|
| 198 |
+
|
| 199 |
+
const canonicalHeaders = [
|
| 200 |
+
`host:${host}`,
|
| 201 |
+
`x-amz-content-sha256:${payloadHash}`,
|
| 202 |
+
`x-amz-date:${amzDate}`
|
| 203 |
+
].join('\n') + '\n';
|
| 204 |
+
|
| 205 |
+
const signedHeaders = 'host;x-amz-content-sha256;x-amz-date';
|
| 206 |
+
|
| 207 |
+
const canonicalRequest = [
|
| 208 |
+
method,
|
| 209 |
+
canonicalUri,
|
| 210 |
+
'',
|
| 211 |
+
canonicalHeaders,
|
| 212 |
+
signedHeaders,
|
| 213 |
+
payloadHash
|
| 214 |
+
].join('\n');
|
| 215 |
+
|
| 216 |
+
const algorithm = 'AWS4-HMAC-SHA256';
|
| 217 |
+
const credentialScope = `${dateStamp}/${this.region}/${this.service}/aws4_request`;
|
| 218 |
+
const canonicalRequestHash = crypto.createHash('sha256').update(canonicalRequest).digest('hex');
|
| 219 |
+
|
| 220 |
+
const stringToSign = [
|
| 221 |
+
algorithm,
|
| 222 |
+
amzDate,
|
| 223 |
+
credentialScope,
|
| 224 |
+
canonicalRequestHash
|
| 225 |
+
].join('\n');
|
| 226 |
+
|
| 227 |
+
const signingKey = this.getSignatureKey(dateStamp);
|
| 228 |
+
const signature = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex');
|
| 229 |
+
|
| 230 |
+
const authorization = `${algorithm} Credential=${this.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
| 231 |
+
|
| 232 |
+
return new Promise((resolve, reject) => {
|
| 233 |
+
const options = {
|
| 234 |
+
hostname: host,
|
| 235 |
+
port: 443,
|
| 236 |
+
path: canonicalUri,
|
| 237 |
+
method: method,
|
| 238 |
+
headers: {
|
| 239 |
+
'Host': host,
|
| 240 |
+
'X-Amz-Content-Sha256': payloadHash,
|
| 241 |
+
'X-Amz-Date': amzDate,
|
| 242 |
+
'Authorization': authorization
|
| 243 |
+
}
|
| 244 |
+
};
|
| 245 |
+
|
| 246 |
+
const req = https.request(options, (res) => {
|
| 247 |
+
let data = '';
|
| 248 |
+
res.on('data', chunk => data += chunk);
|
| 249 |
+
res.on('end', () => {
|
| 250 |
+
if (res.statusCode === 204 || res.statusCode === 200) {
|
| 251 |
+
resolve({ success: true });
|
| 252 |
+
} else {
|
| 253 |
+
reject(new Error(`R2 删除失败: ${res.statusCode}`));
|
| 254 |
+
}
|
| 255 |
+
});
|
| 256 |
+
});
|
| 257 |
+
|
| 258 |
+
req.on('error', reject);
|
| 259 |
+
req.end();
|
| 260 |
+
});
|
| 261 |
+
}
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
// 导出单例
|
| 265 |
+
const r2Storage = new R2Storage();
|
| 266 |
+
|
| 267 |
+
module.exports = {
|
| 268 |
+
R2Storage,
|
| 269 |
+
r2Storage
|
| 270 |
+
};
|
server-mysql.js
ADDED
|
@@ -0,0 +1,2327 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// server-mysql.js - 使用MySQL数据库的校园圈后端服务器 (Hugging Face Spaces版本)
|
| 2 |
+
require('dotenv').config();
|
| 3 |
+
const express = require('express');
|
| 4 |
+
const cors = require('cors');
|
| 5 |
+
const helmet = require('helmet');
|
| 6 |
+
const multer = require('multer');
|
| 7 |
+
const path = require('path');
|
| 8 |
+
const fs = require('fs');
|
| 9 |
+
const bcrypt = require('bcrypt');
|
| 10 |
+
const jwt = require('jsonwebtoken');
|
| 11 |
+
const { body, validationResult } = require('express-validator');
|
| 12 |
+
const winston = require('winston');
|
| 13 |
+
const { pool, initDatabase } = require('./db-config');
|
| 14 |
+
const { r2Storage } = require('./r2-storage');
|
| 15 |
+
|
| 16 |
+
// JWT密钥
|
| 17 |
+
const JWT_SECRET = process.env.JWT_SECRET || 'campus-circle-secret-key-2024';
|
| 18 |
+
|
| 19 |
+
// 服务器端口 - Hugging Face Spaces 使用 7860
|
| 20 |
+
const PORT = process.env.PORT || 7860;
|
| 21 |
+
|
| 22 |
+
// 创建日志记录器
|
| 23 |
+
const logger = winston.createLogger({
|
| 24 |
+
level: 'info',
|
| 25 |
+
format: winston.format.combine(
|
| 26 |
+
winston.format.timestamp(),
|
| 27 |
+
winston.format.json()
|
| 28 |
+
),
|
| 29 |
+
transports: [
|
| 30 |
+
new winston.transports.Console(),
|
| 31 |
+
new winston.transports.File({ filename: 'server.log' })
|
| 32 |
+
]
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
const app = express();
|
| 36 |
+
|
| 37 |
+
// 确保uploads目录存在 - 使用Hugging Face Spaces持久化目录
|
| 38 |
+
const uploadsDir = process.env.HF_SPACE ?
|
| 39 |
+
path.join('/tmp', 'uploads') : // Hugging Face Spaces临时目录
|
| 40 |
+
path.join(__dirname, 'uploads'); // 本地开发目录
|
| 41 |
+
|
| 42 |
+
if (!fs.existsSync(uploadsDir)) {
|
| 43 |
+
fs.mkdirSync(uploadsDir, { recursive: true });
|
| 44 |
+
console.log('📁 创建uploads目录:', uploadsDir);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// 创建静态文件目录(用于持久化存储)
|
| 48 |
+
const staticDir = path.join(__dirname, 'static');
|
| 49 |
+
if (!fs.existsSync(staticDir)) {
|
| 50 |
+
fs.mkdirSync(staticDir, { recursive: true });
|
| 51 |
+
console.log('📁 创建static目录:', staticDir);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// 添加调试信息 - 列出现有文件
|
| 55 |
+
try {
|
| 56 |
+
const files = fs.readdirSync(uploadsDir);
|
| 57 |
+
console.log('📂 uploads目录现有文件:', files);
|
| 58 |
+
} catch (error) {
|
| 59 |
+
console.log('📂 无法读取uploads目录:', error.message);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// 配置multer用于文件上传
|
| 63 |
+
const storage = multer.diskStorage({
|
| 64 |
+
destination: function (req, file, cb) {
|
| 65 |
+
// 使用/tmp目录,Hugging Face Spaces中有写权限
|
| 66 |
+
const uploadPath = uploadsDir;
|
| 67 |
+
|
| 68 |
+
// 确保目录存在
|
| 69 |
+
if (!fs.existsSync(uploadPath)) {
|
| 70 |
+
try {
|
| 71 |
+
fs.mkdirSync(uploadPath, { recursive: true });
|
| 72 |
+
console.log('📁 创建上传目录:', uploadPath);
|
| 73 |
+
} catch (error) {
|
| 74 |
+
console.error('❌ 创建上传目录失败:', error);
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
cb(null, uploadPath);
|
| 79 |
+
},
|
| 80 |
+
filename: function (req, file, cb) {
|
| 81 |
+
// 生成唯一文件名
|
| 82 |
+
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
| 83 |
+
const ext = path.extname(file.originalname);
|
| 84 |
+
const filename = file.fieldname + '-' + uniqueSuffix + ext;
|
| 85 |
+
console.log('📸 生成文件名:', filename);
|
| 86 |
+
cb(null, filename);
|
| 87 |
+
}
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
const upload = multer({
|
| 91 |
+
storage: storage,
|
| 92 |
+
limits: {
|
| 93 |
+
fileSize: 2 * 1024 * 1024 // 2MB限制,减少base64大小
|
| 94 |
+
},
|
| 95 |
+
fileFilter: function (req, file, cb) {
|
| 96 |
+
console.log('📸 文件过滤检查:', file.mimetype);
|
| 97 |
+
// 只允许图片文件
|
| 98 |
+
if (file.mimetype.startsWith('image/')) {
|
| 99 |
+
cb(null, true);
|
| 100 |
+
} else {
|
| 101 |
+
cb(new Error('只允许上传图片文件'));
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
});
|
| 105 |
+
|
| 106 |
+
// 中间件
|
| 107 |
+
app.use(helmet());
|
| 108 |
+
app.use(cors());
|
| 109 |
+
app.use(express.json({ limit: '10mb' }));
|
| 110 |
+
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
| 111 |
+
|
| 112 |
+
// 静态文件服务 - 配置CORS和缓存
|
| 113 |
+
app.use('/uploads', express.static(uploadsDir, {
|
| 114 |
+
setHeaders: (res, path, stat) => {
|
| 115 |
+
res.set('Access-Control-Allow-Origin', '*');
|
| 116 |
+
res.set('Cross-Origin-Resource-Policy', 'cross-origin');
|
| 117 |
+
}
|
| 118 |
+
}));
|
| 119 |
+
|
| 120 |
+
app.use('/static', express.static(staticDir, {
|
| 121 |
+
setHeaders: (res, path, stat) => {
|
| 122 |
+
res.set('Access-Control-Allow-Origin', '*');
|
| 123 |
+
res.set('Cross-Origin-Resource-Policy', 'cross-origin');
|
| 124 |
+
}
|
| 125 |
+
}));
|
| 126 |
+
|
| 127 |
+
app.use('/images', express.static('images', {
|
| 128 |
+
setHeaders: (res, path, stat) => {
|
| 129 |
+
res.set('Access-Control-Allow-Origin', '*');
|
| 130 |
+
res.set('Cross-Origin-Resource-Policy', 'cross-origin');
|
| 131 |
+
}
|
| 132 |
+
}));
|
| 133 |
+
|
| 134 |
+
// JWT验证中间件
|
| 135 |
+
const authenticateToken = (req, res, next) => {
|
| 136 |
+
const authHeader = req.headers['authorization'];
|
| 137 |
+
const token = authHeader && authHeader.split(' ')[1];
|
| 138 |
+
|
| 139 |
+
if (!token) {
|
| 140 |
+
return res.status(401).json({ error: '访问令牌缺失' });
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
jwt.verify(token, JWT_SECRET, (err, user) => {
|
| 144 |
+
if (err) {
|
| 145 |
+
return res.status(403).json({ error: '令牌无效' });
|
| 146 |
+
}
|
| 147 |
+
req.user = user;
|
| 148 |
+
next();
|
| 149 |
+
});
|
| 150 |
+
};
|
| 151 |
+
|
| 152 |
+
// 图片上传接口 - 文件存储方案
|
| 153 |
+
app.post('/api/upload', authenticateToken, upload.single('file'), (req, res) => {
|
| 154 |
+
try {
|
| 155 |
+
if (!req.file) {
|
| 156 |
+
return res.status(400).json({
|
| 157 |
+
success: false,
|
| 158 |
+
error: '没有上传文件'
|
| 159 |
+
});
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
// 检查文件大小
|
| 163 |
+
if (req.file.size > 5 * 1024 * 1024) { // 5MB限制
|
| 164 |
+
// 删除上传的文件
|
| 165 |
+
if (fs.existsSync(req.file.path)) {
|
| 166 |
+
fs.unlinkSync(req.file.path);
|
| 167 |
+
}
|
| 168 |
+
return res.status(400).json({
|
| 169 |
+
success: false,
|
| 170 |
+
error: '图片文件过大,请选择小于5MB的图片'
|
| 171 |
+
});
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
// 生成文件URL
|
| 175 |
+
const fileUrl = `/static/${req.file.filename}`;
|
| 176 |
+
|
| 177 |
+
logger.info(`图片上传成功: ${req.file.originalname}, 大小: ${req.file.size}字节, 路径: ${req.file.path}`);
|
| 178 |
+
|
| 179 |
+
res.json({
|
| 180 |
+
success: true,
|
| 181 |
+
message: '文件上传成功',
|
| 182 |
+
url: fileUrl,
|
| 183 |
+
filename: req.file.filename,
|
| 184 |
+
originalname: req.file.originalname,
|
| 185 |
+
size: req.file.size
|
| 186 |
+
});
|
| 187 |
+
} catch (error) {
|
| 188 |
+
logger.error('文件上传失败:', error);
|
| 189 |
+
|
| 190 |
+
// 清理上传的文件
|
| 191 |
+
if (req.file && req.file.path && fs.existsSync(req.file.path)) {
|
| 192 |
+
try {
|
| 193 |
+
fs.unlinkSync(req.file.path);
|
| 194 |
+
} catch (cleanupError) {
|
| 195 |
+
logger.error('清理上传文件失败:', cleanupError);
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
res.status(500).json({
|
| 200 |
+
success: false,
|
| 201 |
+
error: '文件上传失败: ' + error.message
|
| 202 |
+
});
|
| 203 |
+
}
|
| 204 |
+
});
|
| 205 |
+
|
| 206 |
+
// 健康检查端点
|
| 207 |
+
app.get('/api/health', (req, res) => {
|
| 208 |
+
res.status(200).json({
|
| 209 |
+
status: 'ok',
|
| 210 |
+
timestamp: new Date().toISOString(),
|
| 211 |
+
service: 'campusloop-backend',
|
| 212 |
+
port: PORT,
|
| 213 |
+
platform: 'Hugging Face Spaces'
|
| 214 |
+
});
|
| 215 |
+
});
|
| 216 |
+
|
| 217 |
+
// 获取平台统计数据(注册用户数、动态数、活跃聊天数)
|
| 218 |
+
app.get('/api/stats', async (req, res) => {
|
| 219 |
+
try {
|
| 220 |
+
const connection = await pool.getConnection();
|
| 221 |
+
|
| 222 |
+
try {
|
| 223 |
+
// 查询注册用户总数
|
| 224 |
+
const [usersResult] = await connection.query(
|
| 225 |
+
'SELECT COUNT(*) as count FROM users'
|
| 226 |
+
);
|
| 227 |
+
const totalUsers = usersResult[0].count || 0;
|
| 228 |
+
|
| 229 |
+
// 查询动态总数
|
| 230 |
+
const [postsResult] = await connection.query(
|
| 231 |
+
'SELECT COUNT(*) as count FROM posts'
|
| 232 |
+
);
|
| 233 |
+
const totalPosts = postsResult[0].count || 0;
|
| 234 |
+
|
| 235 |
+
// 查询活跃聊天数(最近7天有发送消息的用户数)
|
| 236 |
+
const [chatsResult] = await connection.query(
|
| 237 |
+
`SELECT COUNT(DISTINCT sender_id) as count FROM chat_messages
|
| 238 |
+
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)`
|
| 239 |
+
);
|
| 240 |
+
const activeChats = chatsResult[0].count || 0;
|
| 241 |
+
|
| 242 |
+
res.json({
|
| 243 |
+
success: true,
|
| 244 |
+
stats: {
|
| 245 |
+
totalUsers,
|
| 246 |
+
totalPosts,
|
| 247 |
+
activeChats
|
| 248 |
+
}
|
| 249 |
+
});
|
| 250 |
+
} finally {
|
| 251 |
+
connection.release();
|
| 252 |
+
}
|
| 253 |
+
} catch (error) {
|
| 254 |
+
console.error('获取统计数据失败:', error);
|
| 255 |
+
logger.error('获取统计数据失败:', error);
|
| 256 |
+
// 返回默认值,避免前端显示错误
|
| 257 |
+
res.json({
|
| 258 |
+
success: true,
|
| 259 |
+
stats: {
|
| 260 |
+
totalUsers: 0,
|
| 261 |
+
totalPosts: 0,
|
| 262 |
+
activeChats: 0
|
| 263 |
+
}
|
| 264 |
+
});
|
| 265 |
+
}
|
| 266 |
+
});
|
| 267 |
+
|
| 268 |
+
// 根路径
|
| 269 |
+
app.get('/', (req, res) => {
|
| 270 |
+
res.json({
|
| 271 |
+
message: '🎓 CampusLoop Backend API',
|
| 272 |
+
version: '1.0.0',
|
| 273 |
+
platform: 'Hugging Face Spaces',
|
| 274 |
+
endpoints: {
|
| 275 |
+
health: '/api/health',
|
| 276 |
+
auth: {
|
| 277 |
+
register: 'POST /api/auth/register',
|
| 278 |
+
login: 'POST /api/auth/login'
|
| 279 |
+
},
|
| 280 |
+
posts: {
|
| 281 |
+
list: 'GET /api/posts',
|
| 282 |
+
create: 'POST /api/posts',
|
| 283 |
+
comments: 'GET /api/posts/:id/comments'
|
| 284 |
+
},
|
| 285 |
+
forum: {
|
| 286 |
+
posts: 'GET /api/forum/posts',
|
| 287 |
+
create: 'POST /api/forum/posts'
|
| 288 |
+
},
|
| 289 |
+
activities: {
|
| 290 |
+
list: 'GET /api/activities',
|
| 291 |
+
create: 'POST /api/activities'
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
});
|
| 295 |
+
});
|
| 296 |
+
|
| 297 |
+
// 用户注册接口
|
| 298 |
+
app.post('/api/auth/register', [
|
| 299 |
+
body('username').isLength({ min: 3 }).withMessage('用户名至少3个字符'),
|
| 300 |
+
body('email').isEmail().withMessage('请输入有效的邮箱地址'),
|
| 301 |
+
body('password').isLength({ min: 6 }).withMessage('密码至少6个字符')
|
| 302 |
+
], async (req, res) => {
|
| 303 |
+
const errors = validationResult(req);
|
| 304 |
+
if (!errors.isEmpty()) {
|
| 305 |
+
return res.status(400).json({ errors: errors.array() });
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
const { username, email, password } = req.body;
|
| 309 |
+
|
| 310 |
+
try {
|
| 311 |
+
const connection = await pool.getConnection();
|
| 312 |
+
|
| 313 |
+
try {
|
| 314 |
+
// 检查用户名和邮箱是否已存在
|
| 315 |
+
const [existingUsers] = await connection.query(
|
| 316 |
+
'SELECT id FROM users WHERE username = ? OR email = ?',
|
| 317 |
+
[username, email]
|
| 318 |
+
);
|
| 319 |
+
|
| 320 |
+
if (existingUsers.length > 0) {
|
| 321 |
+
return res.status(400).json({ error: '用户名或邮箱已存在' });
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
// 加密密码
|
| 325 |
+
const hashedPassword = await bcrypt.hash(password, 10);
|
| 326 |
+
|
| 327 |
+
// 插入新用户(设置默认头像)
|
| 328 |
+
const [result] = await connection.query(
|
| 329 |
+
'INSERT INTO users (username, email, password, avatar) VALUES (?, ?, ?, ?)',
|
| 330 |
+
[username, email, hashedPassword, '/images/default-avatar.svg']
|
| 331 |
+
);
|
| 332 |
+
|
| 333 |
+
const userId = result.insertId;
|
| 334 |
+
const token = jwt.sign({ userId, username }, JWT_SECRET, { expiresIn: '24h' });
|
| 335 |
+
|
| 336 |
+
res.status(201).json({
|
| 337 |
+
message: '注册成功',
|
| 338 |
+
token,
|
| 339 |
+
user: { id: userId, username, email }
|
| 340 |
+
});
|
| 341 |
+
|
| 342 |
+
} finally {
|
| 343 |
+
connection.release();
|
| 344 |
+
}
|
| 345 |
+
} catch (error) {
|
| 346 |
+
logger.error('注册失败:', error);
|
| 347 |
+
res.status(500).json({ error: '注册失败,请稍后重试' });
|
| 348 |
+
}
|
| 349 |
+
});
|
| 350 |
+
|
| 351 |
+
// 用户登录接口
|
| 352 |
+
app.post('/api/auth/login', [
|
| 353 |
+
body('username').notEmpty().withMessage('用户名不能为空'),
|
| 354 |
+
body('password').notEmpty().withMessage('密码不能为空')
|
| 355 |
+
], async (req, res) => {
|
| 356 |
+
const errors = validationResult(req);
|
| 357 |
+
if (!errors.isEmpty()) {
|
| 358 |
+
return res.status(400).json({ errors: errors.array() });
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
const { username, password } = req.body;
|
| 362 |
+
|
| 363 |
+
try {
|
| 364 |
+
const connection = await pool.getConnection();
|
| 365 |
+
|
| 366 |
+
try {
|
| 367 |
+
// 尝试查询包含role字段,如果失败则查询不包含role字段
|
| 368 |
+
let users;
|
| 369 |
+
try {
|
| 370 |
+
[users] = await connection.query(
|
| 371 |
+
'SELECT id, username, email, password, role FROM users WHERE username = ? OR email = ?',
|
| 372 |
+
[username, username]
|
| 373 |
+
);
|
| 374 |
+
} catch (error) {
|
| 375 |
+
if (error.code === 'ER_BAD_FIELD_ERROR') {
|
| 376 |
+
// role字段不存在,使用不带role的查询
|
| 377 |
+
[users] = await connection.query(
|
| 378 |
+
'SELECT id, username, email, password FROM users WHERE username = ? OR email = ?',
|
| 379 |
+
[username, username]
|
| 380 |
+
);
|
| 381 |
+
} else {
|
| 382 |
+
throw error;
|
| 383 |
+
}
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
if (users.length === 0) {
|
| 387 |
+
return res.status(401).json({ error: '用户名或密码错误' });
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
const user = users[0];
|
| 391 |
+
const isValidPassword = await bcrypt.compare(password, user.password);
|
| 392 |
+
|
| 393 |
+
if (!isValidPassword) {
|
| 394 |
+
return res.status(401).json({ error: '用户名或密码错误' });
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
const token = jwt.sign(
|
| 398 |
+
{ userId: user.id, username: user.username },
|
| 399 |
+
JWT_SECRET,
|
| 400 |
+
{ expiresIn: '24h' }
|
| 401 |
+
);
|
| 402 |
+
|
| 403 |
+
res.json({
|
| 404 |
+
message: '登录成功',
|
| 405 |
+
token,
|
| 406 |
+
user: {
|
| 407 |
+
id: user.id,
|
| 408 |
+
username: user.username,
|
| 409 |
+
email: user.email,
|
| 410 |
+
role: user.role || 'user'
|
| 411 |
+
}
|
| 412 |
+
});
|
| 413 |
+
|
| 414 |
+
} finally {
|
| 415 |
+
connection.release();
|
| 416 |
+
}
|
| 417 |
+
} catch (error) {
|
| 418 |
+
logger.error('登录失败:', error);
|
| 419 |
+
res.status(500).json({ error: '登录失败,请稍后重试' });
|
| 420 |
+
}
|
| 421 |
+
});
|
| 422 |
+
|
| 423 |
+
// 获取个人资料接口
|
| 424 |
+
app.get('/api/profile', authenticateToken, async (req, res) => {
|
| 425 |
+
try {
|
| 426 |
+
const connection = await pool.getConnection();
|
| 427 |
+
|
| 428 |
+
try {
|
| 429 |
+
// 查询用户信息,包含所有字段
|
| 430 |
+
const [users] = await connection.query(
|
| 431 |
+
'SELECT id, username, email, avatar, bio, role, phone, major, grade, gender, student_id, created_at FROM users WHERE id = ?',
|
| 432 |
+
[req.user.userId]
|
| 433 |
+
);
|
| 434 |
+
|
| 435 |
+
if (users.length === 0) {
|
| 436 |
+
return res.status(404).json({ error: '用户不存在' });
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
const user = users[0];
|
| 440 |
+
// 不返回密码字段
|
| 441 |
+
delete user.password;
|
| 442 |
+
res.json({ success: true, user });
|
| 443 |
+
} finally {
|
| 444 |
+
connection.release();
|
| 445 |
+
}
|
| 446 |
+
} catch (error) {
|
| 447 |
+
logger.error('获取用户信息失败:', error);
|
| 448 |
+
res.status(500).json({ error: '获取用户信息失败' });
|
| 449 |
+
}
|
| 450 |
+
});
|
| 451 |
+
|
| 452 |
+
// 获取主页成绩信息接口
|
| 453 |
+
app.get('/api/grades/home', authenticateToken, async (req, res) => {
|
| 454 |
+
try {
|
| 455 |
+
const connection = await pool.getConnection();
|
| 456 |
+
|
| 457 |
+
try {
|
| 458 |
+
const [grades] = await connection.query(
|
| 459 |
+
'SELECT * FROM grades WHERE user_id = ? ORDER BY created_at DESC LIMIT 5',
|
| 460 |
+
[req.user.userId]
|
| 461 |
+
);
|
| 462 |
+
|
| 463 |
+
res.json({ grades });
|
| 464 |
+
} finally {
|
| 465 |
+
connection.release();
|
| 466 |
+
}
|
| 467 |
+
} catch (error) {
|
| 468 |
+
logger.error('获取成绩失败:', error);
|
| 469 |
+
res.status(500).json({ error: '获取成绩失败' });
|
| 470 |
+
}
|
| 471 |
+
});
|
| 472 |
+
|
| 473 |
+
// 获取校园地图接口
|
| 474 |
+
app.get('/api/map', (req, res) => {
|
| 475 |
+
// 返回校园地图数据
|
| 476 |
+
const mapData = {
|
| 477 |
+
id: 1,
|
| 478 |
+
name: '校园地图',
|
| 479 |
+
imageUrl: '/images/campus-map.jpg',
|
| 480 |
+
locations: [
|
| 481 |
+
{ id: 1, name: '图书馆', x: 100, y: 150, type: 'library' },
|
| 482 |
+
{ id: 2, name: '教学楼A', x: 200, y: 100, type: 'building' },
|
| 483 |
+
{ id: 3, name: '食堂', x: 150, y: 200, type: 'restaurant' },
|
| 484 |
+
{ id: 4, name: '体育馆', x: 300, y: 180, type: 'gym' }
|
| 485 |
+
]
|
| 486 |
+
};
|
| 487 |
+
|
| 488 |
+
res.json({ map: mapData });
|
| 489 |
+
});
|
| 490 |
+
|
| 491 |
+
// 获取动态接口
|
| 492 |
+
app.get('/api/posts', async (req, res) => {
|
| 493 |
+
try {
|
| 494 |
+
const connection = await pool.getConnection();
|
| 495 |
+
|
| 496 |
+
try {
|
| 497 |
+
const [posts] = await connection.query(`
|
| 498 |
+
SELECT p.*, u.username, u.avatar
|
| 499 |
+
FROM posts p
|
| 500 |
+
JOIN users u ON p.user_id = u.id
|
| 501 |
+
ORDER BY p.created_at DESC
|
| 502 |
+
LIMIT 50
|
| 503 |
+
`);
|
| 504 |
+
|
| 505 |
+
res.json({ success: true, posts });
|
| 506 |
+
} finally {
|
| 507 |
+
connection.release();
|
| 508 |
+
}
|
| 509 |
+
} catch (error) {
|
| 510 |
+
console.error('获取动态失败:', error);
|
| 511 |
+
logger.error('获取动态失败:', error);
|
| 512 |
+
res.status(500).json({ success: false, error: '获取动态失败' });
|
| 513 |
+
}
|
| 514 |
+
});
|
| 515 |
+
|
| 516 |
+
// 发布动态接口(支持图片上传)
|
| 517 |
+
// 优先使用 Cloudflare R2 存储,如未配置则使用 Base64 存数据库
|
| 518 |
+
app.post('/api/posts', authenticateToken, upload.single('image'), async (req, res) => {
|
| 519 |
+
const { content } = req.body;
|
| 520 |
+
|
| 521 |
+
// 验证:至少要有内容或图片
|
| 522 |
+
if (!content && !req.file) {
|
| 523 |
+
return res.status(400).json({ error: '请输入内容或上传图片' });
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
try {
|
| 527 |
+
const connection = await pool.getConnection();
|
| 528 |
+
|
| 529 |
+
try {
|
| 530 |
+
let imageUrl = null;
|
| 531 |
+
|
| 532 |
+
// 如果有上传图片
|
| 533 |
+
if (req.file) {
|
| 534 |
+
const fileBuffer = fs.readFileSync(req.file.path);
|
| 535 |
+
|
| 536 |
+
// 优先使用 R2 存储
|
| 537 |
+
if (r2Storage.isConfigured()) {
|
| 538 |
+
try {
|
| 539 |
+
logger.info('使用 Cloudflare R2 存储图片...');
|
| 540 |
+
const uploadResult = await r2Storage.uploadFile(
|
| 541 |
+
fileBuffer,
|
| 542 |
+
req.file.originalname,
|
| 543 |
+
req.file.mimetype
|
| 544 |
+
);
|
| 545 |
+
imageUrl = uploadResult.url;
|
| 546 |
+
logger.info(`R2 上传成功: ${imageUrl}`);
|
| 547 |
+
} catch (r2Error) {
|
| 548 |
+
logger.error('R2 上传失败,回退到 Base64:', r2Error.message);
|
| 549 |
+
// R2 失败时回退到 Base64
|
| 550 |
+
imageUrl = `data:${req.file.mimetype};base64,${fileBuffer.toString('base64')}`;
|
| 551 |
+
}
|
| 552 |
+
} else {
|
| 553 |
+
// 未配置 R2,使用 Base64 存储
|
| 554 |
+
logger.info('R2 未配置,使用 Base64 存储图片');
|
| 555 |
+
imageUrl = `data:${req.file.mimetype};base64,${fileBuffer.toString('base64')}`;
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
// 删除临时文件
|
| 559 |
+
try {
|
| 560 |
+
fs.unlinkSync(req.file.path);
|
| 561 |
+
} catch (unlinkError) {
|
| 562 |
+
logger.warn('删除临时文件失败:', unlinkError.message);
|
| 563 |
+
}
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
const [result] = await connection.query(
|
| 567 |
+
'INSERT INTO posts (user_id, content, image) VALUES (?, ?, ?)',
|
| 568 |
+
[req.user.userId, content || '', imageUrl]
|
| 569 |
+
);
|
| 570 |
+
|
| 571 |
+
res.status(201).json({
|
| 572 |
+
success: true,
|
| 573 |
+
message: '动态发布成功',
|
| 574 |
+
postId: result.insertId,
|
| 575 |
+
imageStorage: r2Storage.isConfigured() ? 'r2' : 'base64'
|
| 576 |
+
});
|
| 577 |
+
} finally {
|
| 578 |
+
connection.release();
|
| 579 |
+
}
|
| 580 |
+
} catch (error) {
|
| 581 |
+
console.error('发布动态失败:', error);
|
| 582 |
+
logger.error('发布动态失败:', error);
|
| 583 |
+
res.status(500).json({ error: '发布动态失败' });
|
| 584 |
+
}
|
| 585 |
+
});
|
| 586 |
+
|
| 587 |
+
// 编辑动态接口
|
| 588 |
+
app.put('/api/posts/:id', authenticateToken, async (req, res) => {
|
| 589 |
+
const { id } = req.params;
|
| 590 |
+
const { content } = req.body;
|
| 591 |
+
|
| 592 |
+
if (!content || !content.trim()) {
|
| 593 |
+
return res.status(400).json({ error: '动态内容不能为空' });
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
try {
|
| 597 |
+
const connection = await pool.getConnection();
|
| 598 |
+
|
| 599 |
+
try {
|
| 600 |
+
// 检查动态是否存在以及当前用户是否有权限编辑
|
| 601 |
+
const [posts] = await connection.execute(
|
| 602 |
+
'SELECT * FROM posts WHERE id = ? AND user_id = ?',
|
| 603 |
+
[id, req.user.userId]
|
| 604 |
+
);
|
| 605 |
+
|
| 606 |
+
if (posts.length === 0) {
|
| 607 |
+
return res.status(404).json({ error: '动态不存在或无权限编辑' });
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
// 执行更新操作
|
| 611 |
+
await connection.execute(
|
| 612 |
+
'UPDATE posts SET content = ? WHERE id = ?',
|
| 613 |
+
[content.trim(), id]
|
| 614 |
+
);
|
| 615 |
+
|
| 616 |
+
res.json({ success: true, message: '动态编辑成功' });
|
| 617 |
+
} finally {
|
| 618 |
+
connection.release();
|
| 619 |
+
}
|
| 620 |
+
} catch (error) {
|
| 621 |
+
logger.error('编辑动态失败:', error);
|
| 622 |
+
res.status(500).json({ error: '编辑动态失败' });
|
| 623 |
+
}
|
| 624 |
+
});
|
| 625 |
+
|
| 626 |
+
// 删除动态接口(支持管理员删除任何动态)
|
| 627 |
+
app.delete('/api/posts/:id', authenticateToken, async (req, res) => {
|
| 628 |
+
const { id } = req.params;
|
| 629 |
+
|
| 630 |
+
try {
|
| 631 |
+
const connection = await pool.getConnection();
|
| 632 |
+
|
| 633 |
+
try {
|
| 634 |
+
// 先获取当前用户的角色
|
| 635 |
+
const [users] = await connection.execute(
|
| 636 |
+
'SELECT role FROM users WHERE id = ?',
|
| 637 |
+
[req.user.userId]
|
| 638 |
+
);
|
| 639 |
+
|
| 640 |
+
const isAdmin = users.length > 0 && users[0].role === 'admin';
|
| 641 |
+
|
| 642 |
+
// 检查动态是否存在
|
| 643 |
+
const [posts] = await connection.execute(
|
| 644 |
+
'SELECT * FROM posts WHERE id = ?',
|
| 645 |
+
[id]
|
| 646 |
+
);
|
| 647 |
+
|
| 648 |
+
if (posts.length === 0) {
|
| 649 |
+
return res.status(404).json({ error: '动态不存在' });
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
// 检查权限:只有动态作者或管理员可以删除
|
| 653 |
+
if (posts[0].user_id !== req.user.userId && !isAdmin) {
|
| 654 |
+
return res.status(403).json({ error: '无权限删除此动态' });
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
// 删除动态(会级联删除相关的评论和点赞)
|
| 658 |
+
await connection.execute('DELETE FROM posts WHERE id = ?', [id]);
|
| 659 |
+
|
| 660 |
+
logger.info(`动态删除成功: postId=${id}, 操作者=${req.user.username}, 是否管理员=${isAdmin}`);
|
| 661 |
+
res.json({ success: true, message: '动态删除成功' });
|
| 662 |
+
} finally {
|
| 663 |
+
connection.release();
|
| 664 |
+
}
|
| 665 |
+
} catch (error) {
|
| 666 |
+
logger.error('删除动态失败:', error);
|
| 667 |
+
res.status(500).json({ error: '删除动态失败' });
|
| 668 |
+
}
|
| 669 |
+
});
|
| 670 |
+
|
| 671 |
+
// 点赞动态接口
|
| 672 |
+
app.post('/api/posts/:id/like', authenticateToken, async (req, res) => {
|
| 673 |
+
const { id } = req.params;
|
| 674 |
+
|
| 675 |
+
try {
|
| 676 |
+
const connection = await pool.getConnection();
|
| 677 |
+
|
| 678 |
+
try {
|
| 679 |
+
// 检查是否已经点赞
|
| 680 |
+
const [existingLikes] = await connection.execute(
|
| 681 |
+
'SELECT * FROM likes WHERE post_id = ? AND user_id = ?',
|
| 682 |
+
[id, req.user.userId]
|
| 683 |
+
);
|
| 684 |
+
|
| 685 |
+
if (existingLikes.length > 0) {
|
| 686 |
+
// 取消点赞
|
| 687 |
+
await connection.execute(
|
| 688 |
+
'DELETE FROM likes WHERE post_id = ? AND user_id = ?',
|
| 689 |
+
[id, req.user.userId]
|
| 690 |
+
);
|
| 691 |
+
|
| 692 |
+
res.json({ success: true, message: '取消点赞成功', liked: false });
|
| 693 |
+
} else {
|
| 694 |
+
// 添加点赞
|
| 695 |
+
await connection.execute(
|
| 696 |
+
'INSERT INTO likes (post_id, user_id) VALUES (?, ?)',
|
| 697 |
+
[id, req.user.userId]
|
| 698 |
+
);
|
| 699 |
+
|
| 700 |
+
res.json({ success: true, message: '点赞成功', liked: true });
|
| 701 |
+
}
|
| 702 |
+
} finally {
|
| 703 |
+
connection.release();
|
| 704 |
+
}
|
| 705 |
+
} catch (error) {
|
| 706 |
+
logger.error('点赞失败:', error);
|
| 707 |
+
res.status(500).json({ error: '点赞失败' });
|
| 708 |
+
}
|
| 709 |
+
});
|
| 710 |
+
|
| 711 |
+
// 获取动态评论接口
|
| 712 |
+
app.get('/api/posts/:id/comments', async (req, res) => {
|
| 713 |
+
const { id } = req.params;
|
| 714 |
+
|
| 715 |
+
try {
|
| 716 |
+
const connection = await pool.getConnection();
|
| 717 |
+
|
| 718 |
+
try {
|
| 719 |
+
const [comments] = await connection.execute(`
|
| 720 |
+
SELECT c.*, u.username, u.avatar
|
| 721 |
+
FROM comments c
|
| 722 |
+
JOIN users u ON c.user_id = u.id
|
| 723 |
+
WHERE c.post_id = ?
|
| 724 |
+
ORDER BY c.created_at ASC
|
| 725 |
+
`, [id]);
|
| 726 |
+
|
| 727 |
+
res.json({ success: true, comments });
|
| 728 |
+
} finally {
|
| 729 |
+
connection.release();
|
| 730 |
+
}
|
| 731 |
+
} catch (error) {
|
| 732 |
+
logger.error('获取评论失败:', error);
|
| 733 |
+
res.status(500).json({ error: '获取评论失败' });
|
| 734 |
+
}
|
| 735 |
+
});
|
| 736 |
+
|
| 737 |
+
// 发表评论接口
|
| 738 |
+
app.post('/api/posts/:id/comments', authenticateToken, async (req, res) => {
|
| 739 |
+
const { id } = req.params;
|
| 740 |
+
const { content } = req.body;
|
| 741 |
+
|
| 742 |
+
if (!content || !content.trim()) {
|
| 743 |
+
return res.status(400).json({ error: '评论内容不能为空' });
|
| 744 |
+
}
|
| 745 |
+
|
| 746 |
+
try {
|
| 747 |
+
const connection = await pool.getConnection();
|
| 748 |
+
|
| 749 |
+
try {
|
| 750 |
+
const [result] = await connection.execute(
|
| 751 |
+
'INSERT INTO comments (post_id, user_id, content) VALUES (?, ?, ?)',
|
| 752 |
+
[id, req.user.userId, content.trim()]
|
| 753 |
+
);
|
| 754 |
+
|
| 755 |
+
res.json({
|
| 756 |
+
success: true,
|
| 757 |
+
comment: {
|
| 758 |
+
id: result.insertId,
|
| 759 |
+
post_id: id,
|
| 760 |
+
user_id: req.user.userId,
|
| 761 |
+
username: req.user.username,
|
| 762 |
+
content: content.trim(),
|
| 763 |
+
created_at: new Date().toISOString()
|
| 764 |
+
}
|
| 765 |
+
});
|
| 766 |
+
} finally {
|
| 767 |
+
connection.release();
|
| 768 |
+
}
|
| 769 |
+
} catch (error) {
|
| 770 |
+
logger.error('发表评论失败:', error);
|
| 771 |
+
res.status(500).json({ error: '发表评论失败' });
|
| 772 |
+
}
|
| 773 |
+
});
|
| 774 |
+
|
| 775 |
+
// 删除评论接口
|
| 776 |
+
app.delete('/api/posts/:postId/comments/:commentId', authenticateToken, async (req, res) => {
|
| 777 |
+
const { postId, commentId } = req.params;
|
| 778 |
+
|
| 779 |
+
try {
|
| 780 |
+
const connection = await pool.getConnection();
|
| 781 |
+
|
| 782 |
+
try {
|
| 783 |
+
// 检查评论是否存在以及是否属于当前用户
|
| 784 |
+
const [comments] = await connection.execute(
|
| 785 |
+
'SELECT * FROM comments WHERE id = ? AND post_id = ? AND user_id = ?',
|
| 786 |
+
[commentId, postId, req.user.userId]
|
| 787 |
+
);
|
| 788 |
+
|
| 789 |
+
if (comments.length === 0) {
|
| 790 |
+
return res.status(404).json({ error: '评论不存在或无权限删除' });
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
// 删除评论
|
| 794 |
+
await connection.execute('DELETE FROM comments WHERE id = ?', [commentId]);
|
| 795 |
+
|
| 796 |
+
res.json({ success: true, message: '评论删除成功' });
|
| 797 |
+
} finally {
|
| 798 |
+
connection.release();
|
| 799 |
+
}
|
| 800 |
+
} catch (error) {
|
| 801 |
+
logger.error('删除评论失败:', error);
|
| 802 |
+
res.status(500).json({ error: '删除评论失败' });
|
| 803 |
+
}
|
| 804 |
+
});
|
| 805 |
+
|
| 806 |
+
// 获取论坛帖子接口
|
| 807 |
+
app.get('/api/forum/posts', async (req, res) => {
|
| 808 |
+
const { topic } = req.query;
|
| 809 |
+
|
| 810 |
+
try {
|
| 811 |
+
const connection = await pool.getConnection();
|
| 812 |
+
|
| 813 |
+
try {
|
| 814 |
+
let query = `
|
| 815 |
+
SELECT fp.*, u.username, u.avatar
|
| 816 |
+
FROM forum_posts fp
|
| 817 |
+
JOIN users u ON fp.user_id = u.id
|
| 818 |
+
`;
|
| 819 |
+
let params = [];
|
| 820 |
+
|
| 821 |
+
if (topic) {
|
| 822 |
+
query += ' WHERE fp.topic = ?';
|
| 823 |
+
params.push(topic);
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
query += ' ORDER BY fp.created_at DESC LIMIT 50';
|
| 827 |
+
|
| 828 |
+
const [posts] = await connection.query(query, params);
|
| 829 |
+
res.json({ posts });
|
| 830 |
+
} finally {
|
| 831 |
+
connection.release();
|
| 832 |
+
}
|
| 833 |
+
} catch (error) {
|
| 834 |
+
logger.error('获取论坛帖子失败:', error);
|
| 835 |
+
res.status(500).json({ error: '获取论坛帖子失败' });
|
| 836 |
+
}
|
| 837 |
+
});
|
| 838 |
+
|
| 839 |
+
// 获取课程表接口
|
| 840 |
+
app.get('/api/schedule', authenticateToken, async (req, res) => {
|
| 841 |
+
try {
|
| 842 |
+
const connection = await pool.getConnection();
|
| 843 |
+
|
| 844 |
+
try {
|
| 845 |
+
const [schedules] = await connection.query(
|
| 846 |
+
'SELECT * FROM schedules WHERE user_id = ? ORDER BY weekday, start_time',
|
| 847 |
+
[req.user.userId]
|
| 848 |
+
);
|
| 849 |
+
|
| 850 |
+
res.json({ success: true, schedules });
|
| 851 |
+
} finally {
|
| 852 |
+
connection.release();
|
| 853 |
+
}
|
| 854 |
+
} catch (error) {
|
| 855 |
+
logger.error('获取课程表失败:', error);
|
| 856 |
+
res.status(500).json({ success: false, error: '获取课程表失败' });
|
| 857 |
+
}
|
| 858 |
+
});
|
| 859 |
+
|
| 860 |
+
// 添加课程接口
|
| 861 |
+
app.post('/api/schedule', authenticateToken, [
|
| 862 |
+
body('course_name').notEmpty().withMessage('课程名称不能为空'),
|
| 863 |
+
body('teacher').notEmpty().withMessage('教师姓名不能为空'),
|
| 864 |
+
body('weekday').isInt({ min: 1, max: 7 }).withMessage('星期必须是1-7之间的数字')
|
| 865 |
+
], async (req, res) => {
|
| 866 |
+
const errors = validationResult(req);
|
| 867 |
+
if (!errors.isEmpty()) {
|
| 868 |
+
return res.status(400).json({ errors: errors.array() });
|
| 869 |
+
}
|
| 870 |
+
|
| 871 |
+
const { course_name, teacher, weekday, start_time, end_time, classroom } = req.body;
|
| 872 |
+
|
| 873 |
+
try {
|
| 874 |
+
const connection = await pool.getConnection();
|
| 875 |
+
|
| 876 |
+
try {
|
| 877 |
+
const [result] = await connection.query(
|
| 878 |
+
'INSERT INTO schedules (user_id, course_name, teacher, weekday, start_time, end_time, classroom) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
| 879 |
+
[req.user.userId, course_name, teacher, weekday, start_time, end_time, classroom || '']
|
| 880 |
+
);
|
| 881 |
+
|
| 882 |
+
res.status(201).json({
|
| 883 |
+
message: '课程添加成功',
|
| 884 |
+
scheduleId: result.insertId
|
| 885 |
+
});
|
| 886 |
+
} finally {
|
| 887 |
+
connection.release();
|
| 888 |
+
}
|
| 889 |
+
} catch (error) {
|
| 890 |
+
logger.error('添加课程失败:', error);
|
| 891 |
+
res.status(500).json({ error: '添加课程失败' });
|
| 892 |
+
}
|
| 893 |
+
});
|
| 894 |
+
|
| 895 |
+
// 初始化测试课程数据接口
|
| 896 |
+
app.post('/api/schedule/init-test-data', authenticateToken, async (req, res) => {
|
| 897 |
+
try {
|
| 898 |
+
const connection = await pool.getConnection();
|
| 899 |
+
|
| 900 |
+
try {
|
| 901 |
+
// 检查是否已有课程
|
| 902 |
+
const [existing] = await connection.query(
|
| 903 |
+
'SELECT COUNT(*) as count FROM schedules WHERE user_id = ?',
|
| 904 |
+
[req.user.userId]
|
| 905 |
+
);
|
| 906 |
+
|
| 907 |
+
if (existing[0].count > 0) {
|
| 908 |
+
return res.json({ success: true, message: '已有课程数据,无需初始化', count: existing[0].count });
|
| 909 |
+
}
|
| 910 |
+
|
| 911 |
+
// 测试课程数据
|
| 912 |
+
const courses = [
|
| 913 |
+
{ course_name: '高等数学', teacher: '张教授', weekday: 1, start_time: '08:00', end_time: '09:45', classroom: '教学楼A201', week_start: 1, week_end: 16 },
|
| 914 |
+
{ course_name: '大学物理', teacher: '刘教授', weekday: 1, start_time: '10:00', end_time: '11:45', classroom: '理学楼B102', week_start: 1, week_end: 16 },
|
| 915 |
+
{ course_name: '思想政治', teacher: '陈老师', weekday: 1, start_time: '14:00', end_time: '15:45', classroom: '综合楼C301', week_start: 1, week_end: 18 },
|
| 916 |
+
{ course_name: '大学英语', teacher: '李老师', weekday: 2, start_time: '08:00', end_time: '09:45', classroom: '外语楼A305', week_start: 1, week_end: 16 },
|
| 917 |
+
{ course_name: '线性代数', teacher: '王教授', weekday: 2, start_time: '10:00', end_time: '11:45', classroom: '教学楼A203', week_start: 1, week_end: 14 },
|
| 918 |
+
{ course_name: '体育', teacher: '赵老师', weekday: 2, start_time: '14:00', end_time: '15:45', classroom: '体育馆', week_start: 1, week_end: 16 },
|
| 919 |
+
{ course_name: '程序设计', teacher: '孙老师', weekday: 3, start_time: '08:00', end_time: '09:45', classroom: '实验楼C102', week_start: 1, week_end: 16 },
|
| 920 |
+
{ course_name: '高等数学', teacher: '张教授', weekday: 3, start_time: '10:00', end_time: '11:45', classroom: '教学楼A201', week_start: 1, week_end: 16 },
|
| 921 |
+
{ course_name: '数据结构', teacher: '周教授', weekday: 3, start_time: '14:00', end_time: '15:45', classroom: '实验楼C201', week_start: 3, week_end: 18 },
|
| 922 |
+
{ course_name: '大学英语', teacher: '李老师', weekday: 4, start_time: '08:00', end_time: '09:45', classroom: '外语楼A305', week_start: 1, week_end: 16 },
|
| 923 |
+
{ course_name: '物理实验', teacher: '刘教授', weekday: 4, start_time: '14:00', end_time: '15:45', classroom: '物理实验楼', week_start: 2, week_end: 16 },
|
| 924 |
+
{ course_name: '计算机网络', teacher: '吴老师', weekday: 4, start_time: '16:00', end_time: '17:45', classroom: '实验楼C301', week_start: 5, week_end: 18 },
|
| 925 |
+
{ course_name: '程序设计', teacher: '孙老师', weekday: 5, start_time: '08:00', end_time: '09:45', classroom: '实验楼C102', week_start: 1, week_end: 16 },
|
| 926 |
+
{ course_name: '概率统计', teacher: '钱教授', weekday: 5, start_time: '10:00', end_time: '11:45', classroom: '教学楼A302', week_start: 1, week_end: 14 },
|
| 927 |
+
{ course_name: '创新创业', teacher: '郑老师', weekday: 5, start_time: '14:00', end_time: '15:45', classroom: '创业楼D101', week_start: 1, week_end: 10 }
|
| 928 |
+
];
|
| 929 |
+
|
| 930 |
+
// 批量插入课程
|
| 931 |
+
for (const course of courses) {
|
| 932 |
+
await connection.query(
|
| 933 |
+
'INSERT INTO schedules (user_id, course_name, teacher, weekday, start_time, end_time, classroom, week_start, week_end, week_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
| 934 |
+
[req.user.userId, course.course_name, course.teacher, course.weekday, course.start_time, course.end_time, course.classroom, course.week_start, course.week_end, 'all']
|
| 935 |
+
);
|
| 936 |
+
}
|
| 937 |
+
|
| 938 |
+
res.json({ success: true, message: '测试课程数据初始化成功', count: courses.length });
|
| 939 |
+
} finally {
|
| 940 |
+
connection.release();
|
| 941 |
+
}
|
| 942 |
+
} catch (error) {
|
| 943 |
+
logger.error('初始化测试课程失败:', error);
|
| 944 |
+
res.status(500).json({ error: '初始化测试课程失败' });
|
| 945 |
+
}
|
| 946 |
+
});
|
| 947 |
+
|
| 948 |
+
// 获取失物招领接口
|
| 949 |
+
app.get('/api/lostfound', async (req, res) => {
|
| 950 |
+
const { category, status } = req.query;
|
| 951 |
+
|
| 952 |
+
try {
|
| 953 |
+
const connection = await pool.getConnection();
|
| 954 |
+
|
| 955 |
+
try {
|
| 956 |
+
let query = `
|
| 957 |
+
SELECT lf.*, u.username
|
| 958 |
+
FROM lost_found lf
|
| 959 |
+
JOIN users u ON lf.user_id = u.id
|
| 960 |
+
`;
|
| 961 |
+
let conditions = [];
|
| 962 |
+
let params = [];
|
| 963 |
+
|
| 964 |
+
if (category) {
|
| 965 |
+
conditions.push('lf.category = ?');
|
| 966 |
+
params.push(category);
|
| 967 |
+
}
|
| 968 |
+
|
| 969 |
+
if (status) {
|
| 970 |
+
conditions.push('lf.status = ?');
|
| 971 |
+
params.push(status);
|
| 972 |
+
}
|
| 973 |
+
|
| 974 |
+
if (conditions.length > 0) {
|
| 975 |
+
query += ' WHERE ' + conditions.join(' AND ');
|
| 976 |
+
}
|
| 977 |
+
|
| 978 |
+
query += ' ORDER BY lf.created_at DESC LIMIT 50';
|
| 979 |
+
|
| 980 |
+
const [items] = await connection.query(query, params);
|
| 981 |
+
res.json({ items });
|
| 982 |
+
} finally {
|
| 983 |
+
connection.release();
|
| 984 |
+
}
|
| 985 |
+
} catch (error) {
|
| 986 |
+
logger.error('获取失物招领失败:', error);
|
| 987 |
+
res.status(500).json({ error: '获取失物招领失败' });
|
| 988 |
+
}
|
| 989 |
+
});
|
| 990 |
+
|
| 991 |
+
// 发布失物招领接口
|
| 992 |
+
app.post('/api/lostfound', authenticateToken, [
|
| 993 |
+
body('title').notEmpty().withMessage('标题不能为空'),
|
| 994 |
+
body('description').notEmpty().withMessage('描述不能为空'),
|
| 995 |
+
body('category').notEmpty().withMessage('分类不能为空')
|
| 996 |
+
], async (req, res) => {
|
| 997 |
+
const errors = validationResult(req);
|
| 998 |
+
if (!errors.isEmpty()) {
|
| 999 |
+
return res.status(400).json({ errors: errors.array() });
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
const { title, description, category, contact, images } = req.body;
|
| 1003 |
+
|
| 1004 |
+
try {
|
| 1005 |
+
const connection = await pool.getConnection();
|
| 1006 |
+
|
| 1007 |
+
try {
|
| 1008 |
+
const imagesJson = images && images.length > 0 ? JSON.stringify(images) : null;
|
| 1009 |
+
|
| 1010 |
+
// 先尝试包含images字段的插入
|
| 1011 |
+
let result;
|
| 1012 |
+
try {
|
| 1013 |
+
[result] = await connection.query(
|
| 1014 |
+
'INSERT INTO lost_found (user_id, title, description, category, contact, images) VALUES (?, ?, ?, ?, ?, ?)',
|
| 1015 |
+
[req.user.userId, title, description, category, contact || '', imagesJson]
|
| 1016 |
+
);
|
| 1017 |
+
} catch (insertError) {
|
| 1018 |
+
if (insertError.code === 'ER_BAD_FIELD_ERROR') {
|
| 1019 |
+
// images字段不存在,使用不包含images的插入
|
| 1020 |
+
console.log('images字段不存在,使用兼容模式插入');
|
| 1021 |
+
[result] = await connection.query(
|
| 1022 |
+
'INSERT INTO lost_found (user_id, title, description, category, contact) VALUES (?, ?, ?, ?, ?)',
|
| 1023 |
+
[req.user.userId, title, description, category, contact || '']
|
| 1024 |
+
);
|
| 1025 |
+
} else {
|
| 1026 |
+
throw insertError;
|
| 1027 |
+
}
|
| 1028 |
+
}
|
| 1029 |
+
|
| 1030 |
+
res.status(201).json({
|
| 1031 |
+
message: '失物招领发布成功',
|
| 1032 |
+
itemId: result.insertId
|
| 1033 |
+
});
|
| 1034 |
+
} finally {
|
| 1035 |
+
connection.release();
|
| 1036 |
+
}
|
| 1037 |
+
} catch (error) {
|
| 1038 |
+
logger.error('发布失物招领失败:', error);
|
| 1039 |
+
res.status(500).json({ error: '发布失物招领失败' });
|
| 1040 |
+
}
|
| 1041 |
+
});
|
| 1042 |
+
|
| 1043 |
+
// 获取成绩接口
|
| 1044 |
+
app.get('/api/grades', authenticateToken, async (req, res) => {
|
| 1045 |
+
const { semester } = req.query;
|
| 1046 |
+
|
| 1047 |
+
try {
|
| 1048 |
+
const connection = await pool.getConnection();
|
| 1049 |
+
|
| 1050 |
+
try {
|
| 1051 |
+
let query = 'SELECT * FROM grades WHERE user_id = ?';
|
| 1052 |
+
let params = [req.user.userId];
|
| 1053 |
+
|
| 1054 |
+
if (semester) {
|
| 1055 |
+
query += ' AND semester = ?';
|
| 1056 |
+
params.push(semester);
|
| 1057 |
+
}
|
| 1058 |
+
|
| 1059 |
+
query += ' ORDER BY created_at DESC';
|
| 1060 |
+
|
| 1061 |
+
const [grades] = await connection.query(query, params);
|
| 1062 |
+
res.json({ grades });
|
| 1063 |
+
} finally {
|
| 1064 |
+
connection.release();
|
| 1065 |
+
}
|
| 1066 |
+
} catch (error) {
|
| 1067 |
+
logger.error('获取成绩失败:', error);
|
| 1068 |
+
res.status(500).json({ error: '获取成绩失败' });
|
| 1069 |
+
}
|
| 1070 |
+
});
|
| 1071 |
+
|
| 1072 |
+
// 添加成绩接口
|
| 1073 |
+
app.post('/api/grades', authenticateToken, [
|
| 1074 |
+
body('course_name').notEmpty().withMessage('课程名称不能为空'),
|
| 1075 |
+
body('score').isFloat({ min: 0, max: 100 }).withMessage('成绩必须是0-100之间的数字'),
|
| 1076 |
+
body('semester').notEmpty().withMessage('学期不能为空')
|
| 1077 |
+
], async (req, res) => {
|
| 1078 |
+
const errors = validationResult(req);
|
| 1079 |
+
if (!errors.isEmpty()) {
|
| 1080 |
+
return res.status(400).json({ errors: errors.array() });
|
| 1081 |
+
}
|
| 1082 |
+
|
| 1083 |
+
const { course_name, score, semester, credit } = req.body;
|
| 1084 |
+
|
| 1085 |
+
try {
|
| 1086 |
+
const connection = await pool.getConnection();
|
| 1087 |
+
|
| 1088 |
+
try {
|
| 1089 |
+
const [result] = await connection.query(
|
| 1090 |
+
'INSERT INTO grades (user_id, course_name, score, semester, credit) VALUES (?, ?, ?, ?, ?)',
|
| 1091 |
+
[req.user.userId, course_name, score, semester, credit || 3]
|
| 1092 |
+
);
|
| 1093 |
+
|
| 1094 |
+
res.status(201).json({
|
| 1095 |
+
message: '成绩添加成功',
|
| 1096 |
+
gradeId: result.insertId
|
| 1097 |
+
});
|
| 1098 |
+
} finally {
|
| 1099 |
+
connection.release();
|
| 1100 |
+
}
|
| 1101 |
+
} catch (error) {
|
| 1102 |
+
logger.error('添加成绩失败:', error);
|
| 1103 |
+
res.status(500).json({ error: '添加成绩失败' });
|
| 1104 |
+
}
|
| 1105 |
+
});
|
| 1106 |
+
|
| 1107 |
+
// 获取通知接口
|
| 1108 |
+
app.get('/api/notifications', authenticateToken, async (req, res) => {
|
| 1109 |
+
try {
|
| 1110 |
+
const connection = await pool.getConnection();
|
| 1111 |
+
|
| 1112 |
+
try {
|
| 1113 |
+
const [notifications] = await connection.query(
|
| 1114 |
+
'SELECT * FROM notifications WHERE user_id = ? ORDER BY created_at DESC LIMIT 50',
|
| 1115 |
+
[req.user.userId]
|
| 1116 |
+
);
|
| 1117 |
+
|
| 1118 |
+
res.json({ notifications });
|
| 1119 |
+
} finally {
|
| 1120 |
+
connection.release();
|
| 1121 |
+
}
|
| 1122 |
+
} catch (error) {
|
| 1123 |
+
logger.error('获取通知失败:', error);
|
| 1124 |
+
res.status(500).json({ error: '获取通知失败' });
|
| 1125 |
+
}
|
| 1126 |
+
});
|
| 1127 |
+
|
| 1128 |
+
// 标记通知为已读接口
|
| 1129 |
+
app.put('/api/notifications/:id', authenticateToken, async (req, res) => {
|
| 1130 |
+
const { id } = req.params;
|
| 1131 |
+
|
| 1132 |
+
try {
|
| 1133 |
+
const connection = await pool.getConnection();
|
| 1134 |
+
|
| 1135 |
+
try {
|
| 1136 |
+
await connection.query(
|
| 1137 |
+
'UPDATE notifications SET read_status = 1 WHERE id = ? AND user_id = ?',
|
| 1138 |
+
[id, req.user.userId]
|
| 1139 |
+
);
|
| 1140 |
+
|
| 1141 |
+
res.json({ success: true, message: '通知标记为已读' });
|
| 1142 |
+
} finally {
|
| 1143 |
+
connection.release();
|
| 1144 |
+
}
|
| 1145 |
+
} catch (error) {
|
| 1146 |
+
logger.error('标记通知为已读失败:', error);
|
| 1147 |
+
res.status(500).json({ error: '标记通知为已读失败' });
|
| 1148 |
+
}
|
| 1149 |
+
});
|
| 1150 |
+
|
| 1151 |
+
// 获取活动接口
|
| 1152 |
+
app.get('/api/activities', async (req, res) => {
|
| 1153 |
+
// 从请求头获取用户信息(可选)
|
| 1154 |
+
const authHeader = req.headers['authorization'];
|
| 1155 |
+
const token = authHeader && authHeader.split(' ')[1];
|
| 1156 |
+
let userId = null;
|
| 1157 |
+
|
| 1158 |
+
if (token) {
|
| 1159 |
+
try {
|
| 1160 |
+
const decoded = jwt.verify(token, JWT_SECRET);
|
| 1161 |
+
userId = decoded.userId;
|
| 1162 |
+
} catch (error) {
|
| 1163 |
+
// 忽略token错误,继续执行
|
| 1164 |
+
}
|
| 1165 |
+
}
|
| 1166 |
+
|
| 1167 |
+
try {
|
| 1168 |
+
const connection = await pool.getConnection();
|
| 1169 |
+
|
| 1170 |
+
try {
|
| 1171 |
+
const [activities] = await connection.query(`
|
| 1172 |
+
SELECT a.*, u.username as organizer_name,
|
| 1173 |
+
CASE WHEN ap.user_id IS NOT NULL THEN 1 ELSE 0 END as is_registered
|
| 1174 |
+
FROM activities a
|
| 1175 |
+
JOIN users u ON a.organizer_id = u.id
|
| 1176 |
+
LEFT JOIN activity_participants ap ON a.id = ap.activity_id AND ap.user_id = ?
|
| 1177 |
+
WHERE a.status = 'active'
|
| 1178 |
+
ORDER BY a.start_time ASC
|
| 1179 |
+
LIMIT 50
|
| 1180 |
+
`, [userId]);
|
| 1181 |
+
|
| 1182 |
+
res.json({ success: true, activities });
|
| 1183 |
+
} finally {
|
| 1184 |
+
connection.release();
|
| 1185 |
+
}
|
| 1186 |
+
} catch (error) {
|
| 1187 |
+
logger.error('获取活动失败:', error);
|
| 1188 |
+
res.status(500).json({ error: '获取活动失败' });
|
| 1189 |
+
}
|
| 1190 |
+
});
|
| 1191 |
+
|
| 1192 |
+
// 报名活动接口
|
| 1193 |
+
app.post('/api/activities/:id/register', authenticateToken, async (req, res) => {
|
| 1194 |
+
const activityId = req.params.id;
|
| 1195 |
+
const userId = req.user.userId;
|
| 1196 |
+
|
| 1197 |
+
try {
|
| 1198 |
+
const connection = await pool.getConnection();
|
| 1199 |
+
|
| 1200 |
+
try {
|
| 1201 |
+
// 检查是否已经报名
|
| 1202 |
+
const [existing] = await connection.query(
|
| 1203 |
+
'SELECT * FROM activity_participants WHERE activity_id = ? AND user_id = ?',
|
| 1204 |
+
[activityId, userId]
|
| 1205 |
+
);
|
| 1206 |
+
|
| 1207 |
+
if (existing.length > 0) {
|
| 1208 |
+
return res.status(400).json({ error: '您已经报名了这个活动' });
|
| 1209 |
+
}
|
| 1210 |
+
|
| 1211 |
+
// 添加报名记录
|
| 1212 |
+
await connection.query(
|
| 1213 |
+
'INSERT INTO activity_participants (activity_id, user_id) VALUES (?, ?)',
|
| 1214 |
+
[activityId, userId]
|
| 1215 |
+
);
|
| 1216 |
+
|
| 1217 |
+
// 更新活动参与人数
|
| 1218 |
+
await connection.query(
|
| 1219 |
+
'UPDATE activities SET participants = participants + 1 WHERE id = ?',
|
| 1220 |
+
[activityId]
|
| 1221 |
+
);
|
| 1222 |
+
|
| 1223 |
+
res.json({ success: true, message: '报名成功' });
|
| 1224 |
+
} finally {
|
| 1225 |
+
connection.release();
|
| 1226 |
+
}
|
| 1227 |
+
} catch (error) {
|
| 1228 |
+
logger.error('报名活动失败:', error);
|
| 1229 |
+
res.status(500).json({ error: '报名失败' });
|
| 1230 |
+
}
|
| 1231 |
+
});
|
| 1232 |
+
|
| 1233 |
+
// 取消报名活动接口
|
| 1234 |
+
app.post('/api/activities/:id/cancel', authenticateToken, async (req, res) => {
|
| 1235 |
+
const activityId = req.params.id;
|
| 1236 |
+
const userId = req.user.userId;
|
| 1237 |
+
|
| 1238 |
+
try {
|
| 1239 |
+
const connection = await pool.getConnection();
|
| 1240 |
+
|
| 1241 |
+
try {
|
| 1242 |
+
// 检查是否已经报名
|
| 1243 |
+
const [existing] = await connection.query(
|
| 1244 |
+
'SELECT * FROM activity_participants WHERE activity_id = ? AND user_id = ?',
|
| 1245 |
+
[activityId, userId]
|
| 1246 |
+
);
|
| 1247 |
+
|
| 1248 |
+
if (existing.length === 0) {
|
| 1249 |
+
return res.status(400).json({ error: '您尚未报名此活动' });
|
| 1250 |
+
}
|
| 1251 |
+
|
| 1252 |
+
// 删除报名记录
|
| 1253 |
+
await connection.query(
|
| 1254 |
+
'DELETE FROM activity_participants WHERE activity_id = ? AND user_id = ?',
|
| 1255 |
+
[activityId, userId]
|
| 1256 |
+
);
|
| 1257 |
+
|
| 1258 |
+
// 更新活动参与人数
|
| 1259 |
+
await connection.query(
|
| 1260 |
+
'UPDATE activities SET participants = GREATEST(0, participants - 1) WHERE id = ?',
|
| 1261 |
+
[activityId]
|
| 1262 |
+
);
|
| 1263 |
+
|
| 1264 |
+
res.json({ success: true, message: '取消报名成功' });
|
| 1265 |
+
} finally {
|
| 1266 |
+
connection.release();
|
| 1267 |
+
}
|
| 1268 |
+
} catch (error) {
|
| 1269 |
+
logger.error('取消报名失败:', error);
|
| 1270 |
+
res.status(500).json({ error: '取消报名失败' });
|
| 1271 |
+
}
|
| 1272 |
+
});
|
| 1273 |
+
|
| 1274 |
+
// 获取单个活动详情接口
|
| 1275 |
+
app.get('/api/activities/:id', async (req, res) => {
|
| 1276 |
+
const activityId = req.params.id;
|
| 1277 |
+
|
| 1278 |
+
try {
|
| 1279 |
+
const connection = await pool.getConnection();
|
| 1280 |
+
|
| 1281 |
+
try {
|
| 1282 |
+
const [activities] = await connection.query(`
|
| 1283 |
+
SELECT a.*, u.username as organizer_name
|
| 1284 |
+
FROM activities a
|
| 1285 |
+
JOIN users u ON a.organizer_id = u.id
|
| 1286 |
+
WHERE a.id = ?
|
| 1287 |
+
`, [activityId]);
|
| 1288 |
+
|
| 1289 |
+
if (activities.length === 0) {
|
| 1290 |
+
return res.status(404).json({ error: '活动不存在' });
|
| 1291 |
+
}
|
| 1292 |
+
|
| 1293 |
+
res.json({ success: true, activity: activities[0] });
|
| 1294 |
+
} finally {
|
| 1295 |
+
connection.release();
|
| 1296 |
+
}
|
| 1297 |
+
} catch (error) {
|
| 1298 |
+
logger.error('获取活动详情失败:', error);
|
| 1299 |
+
res.status(500).json({ error: '获取活动详情失败' });
|
| 1300 |
+
}
|
| 1301 |
+
});
|
| 1302 |
+
|
| 1303 |
+
// 更新活动接口
|
| 1304 |
+
app.put('/api/activities/:id', authenticateToken, [
|
| 1305 |
+
body('title').notEmpty().withMessage('活动标题不能为空'),
|
| 1306 |
+
body('description').notEmpty().withMessage('活动描述不能为空'),
|
| 1307 |
+
body('location').notEmpty().withMessage('活动地点不能为空'),
|
| 1308 |
+
body('start_time').notEmpty().withMessage('开始时间不能为空'),
|
| 1309 |
+
body('end_time').notEmpty().withMessage('结束时间不能为空')
|
| 1310 |
+
], async (req, res) => {
|
| 1311 |
+
const errors = validationResult(req);
|
| 1312 |
+
if (!errors.isEmpty()) {
|
| 1313 |
+
return res.status(400).json({ errors: errors.array() });
|
| 1314 |
+
}
|
| 1315 |
+
|
| 1316 |
+
const activityId = req.params.id;
|
| 1317 |
+
const userId = req.user.userId;
|
| 1318 |
+
const { title, description, location, start_time, end_time } = req.body;
|
| 1319 |
+
|
| 1320 |
+
try {
|
| 1321 |
+
const connection = await pool.getConnection();
|
| 1322 |
+
|
| 1323 |
+
try {
|
| 1324 |
+
console.log('更新活动请求:', { activityId, userId, title, description, location, start_time, end_time });
|
| 1325 |
+
|
| 1326 |
+
// 检查用户是否为管理员或活动创建者
|
| 1327 |
+
const [activity] = await connection.query(
|
| 1328 |
+
'SELECT organizer_id FROM activities WHERE id = ?',
|
| 1329 |
+
[activityId]
|
| 1330 |
+
);
|
| 1331 |
+
|
| 1332 |
+
if (activity.length === 0) {
|
| 1333 |
+
console.log('活动不存在:', activityId);
|
| 1334 |
+
return res.status(404).json({ error: '活动不存在' });
|
| 1335 |
+
}
|
| 1336 |
+
|
| 1337 |
+
// 检查用户权限
|
| 1338 |
+
const [user] = await connection.query(
|
| 1339 |
+
'SELECT role FROM users WHERE id = ?',
|
| 1340 |
+
[userId]
|
| 1341 |
+
);
|
| 1342 |
+
|
| 1343 |
+
const isAdmin = user.length > 0 && user[0].role === 'admin';
|
| 1344 |
+
const isOrganizer = activity[0].organizer_id === userId;
|
| 1345 |
+
|
| 1346 |
+
console.log('权限检查:', { isAdmin, isOrganizer, userId, organizerId: activity[0].organizer_id });
|
| 1347 |
+
|
| 1348 |
+
if (!isAdmin && !isOrganizer) {
|
| 1349 |
+
return res.status(403).json({ error: '您没有权限编辑此活动' });
|
| 1350 |
+
}
|
| 1351 |
+
|
| 1352 |
+
// 更新活动
|
| 1353 |
+
const [result] = await connection.query(
|
| 1354 |
+
'UPDATE activities SET title = ?, description = ?, location = ?, start_time = ?, end_time = ? WHERE id = ?',
|
| 1355 |
+
[title, description, location, start_time, end_time, activityId]
|
| 1356 |
+
);
|
| 1357 |
+
|
| 1358 |
+
console.log('更新结果:', result);
|
| 1359 |
+
|
| 1360 |
+
res.json({ success: true, message: '活动更新成功' });
|
| 1361 |
+
} finally {
|
| 1362 |
+
connection.release();
|
| 1363 |
+
}
|
| 1364 |
+
} catch (error) {
|
| 1365 |
+
console.error('更新活动失败详细错误:', error);
|
| 1366 |
+
logger.error('更新活动失败:', error);
|
| 1367 |
+
res.status(500).json({ error: '更新活动失败', details: error.message });
|
| 1368 |
+
}
|
| 1369 |
+
});
|
| 1370 |
+
|
| 1371 |
+
// 删除活动接口
|
| 1372 |
+
app.delete('/api/activities/:id', authenticateToken, async (req, res) => {
|
| 1373 |
+
const activityId = req.params.id;
|
| 1374 |
+
const userId = req.user.userId;
|
| 1375 |
+
|
| 1376 |
+
try {
|
| 1377 |
+
const connection = await pool.getConnection();
|
| 1378 |
+
|
| 1379 |
+
try {
|
| 1380 |
+
// 检查用户是否为管理员或活动创建者
|
| 1381 |
+
const [activity] = await connection.query(
|
| 1382 |
+
'SELECT organizer_id FROM activities WHERE id = ?',
|
| 1383 |
+
[activityId]
|
| 1384 |
+
);
|
| 1385 |
+
|
| 1386 |
+
if (activity.length === 0) {
|
| 1387 |
+
return res.status(404).json({ error: '活动不存在' });
|
| 1388 |
+
}
|
| 1389 |
+
|
| 1390 |
+
// 检查用户权限
|
| 1391 |
+
const [user] = await connection.query(
|
| 1392 |
+
'SELECT role FROM users WHERE id = ?',
|
| 1393 |
+
[userId]
|
| 1394 |
+
);
|
| 1395 |
+
|
| 1396 |
+
const isAdmin = user.length > 0 && user[0].role === 'admin';
|
| 1397 |
+
const isOrganizer = activity[0].organizer_id === userId;
|
| 1398 |
+
|
| 1399 |
+
if (!isAdmin && !isOrganizer) {
|
| 1400 |
+
return res.status(403).json({ error: '您没有权限删除此活动' });
|
| 1401 |
+
}
|
| 1402 |
+
|
| 1403 |
+
// 删除活动(会自动删除相关的参与记录,因为有外键约束)
|
| 1404 |
+
await connection.query('DELETE FROM activities WHERE id = ?', [activityId]);
|
| 1405 |
+
|
| 1406 |
+
res.json({ success: true, message: '活动删除成功' });
|
| 1407 |
+
} finally {
|
| 1408 |
+
connection.release();
|
| 1409 |
+
}
|
| 1410 |
+
} catch (error) {
|
| 1411 |
+
logger.error('删除活动失败:', error);
|
| 1412 |
+
res.status(500).json({ error: '删除活动失败' });
|
| 1413 |
+
}
|
| 1414 |
+
});
|
| 1415 |
+
|
| 1416 |
+
// 创建活动接口
|
| 1417 |
+
app.post('/api/activities', authenticateToken, [
|
| 1418 |
+
body('title').notEmpty().withMessage('活动标题不能为空'),
|
| 1419 |
+
body('description').notEmpty().withMessage('活动描述不能为空'),
|
| 1420 |
+
body('location').notEmpty().withMessage('活动地点不能为空'),
|
| 1421 |
+
body('start_time').notEmpty().withMessage('开始时间不能为空'),
|
| 1422 |
+
body('end_time').notEmpty().withMessage('结束时间不能为空')
|
| 1423 |
+
], async (req, res) => {
|
| 1424 |
+
const errors = validationResult(req);
|
| 1425 |
+
if (!errors.isEmpty()) {
|
| 1426 |
+
return res.status(400).json({ errors: errors.array() });
|
| 1427 |
+
}
|
| 1428 |
+
|
| 1429 |
+
const { title, description, location, start_time, end_time } = req.body;
|
| 1430 |
+
|
| 1431 |
+
try {
|
| 1432 |
+
const connection = await pool.getConnection();
|
| 1433 |
+
|
| 1434 |
+
try {
|
| 1435 |
+
const [result] = await connection.query(
|
| 1436 |
+
'INSERT INTO activities (title, description, location, start_time, end_time, organizer_id) VALUES (?, ?, ?, ?, ?, ?)',
|
| 1437 |
+
[title, description, location, start_time, end_time, req.user.userId]
|
| 1438 |
+
);
|
| 1439 |
+
|
| 1440 |
+
res.status(201).json({
|
| 1441 |
+
success: true,
|
| 1442 |
+
message: '活动创建成功',
|
| 1443 |
+
activityId: result.insertId
|
| 1444 |
+
});
|
| 1445 |
+
} finally {
|
| 1446 |
+
connection.release();
|
| 1447 |
+
}
|
| 1448 |
+
} catch (error) {
|
| 1449 |
+
logger.error('创建活动失败:', error);
|
| 1450 |
+
res.status(500).json({ error: '创建活动失败' });
|
| 1451 |
+
}
|
| 1452 |
+
});
|
| 1453 |
+
|
| 1454 |
+
// ============ 好友相关接口 ============
|
| 1455 |
+
|
| 1456 |
+
// 获取好友列表
|
| 1457 |
+
app.get('/api/friends', authenticateToken, async (req, res) => {
|
| 1458 |
+
const userId = req.user.userId;
|
| 1459 |
+
|
| 1460 |
+
try {
|
| 1461 |
+
const connection = await pool.getConnection();
|
| 1462 |
+
|
| 1463 |
+
try {
|
| 1464 |
+
// 查询好友列表(双向好友关系)
|
| 1465 |
+
const [friends] = await connection.query(`
|
| 1466 |
+
SELECT DISTINCT u.id, u.username, u.avatar, u.student_id
|
| 1467 |
+
FROM users u
|
| 1468 |
+
INNER JOIN (
|
| 1469 |
+
SELECT friend_id as user_id FROM friendships WHERE user_id = ? AND status = 'accepted'
|
| 1470 |
+
UNION
|
| 1471 |
+
SELECT user_id FROM friendships WHERE friend_id = ? AND status = 'accepted'
|
| 1472 |
+
) f ON u.id = f.user_id
|
| 1473 |
+
ORDER BY u.username
|
| 1474 |
+
`, [userId, userId]);
|
| 1475 |
+
|
| 1476 |
+
res.json({
|
| 1477 |
+
success: true,
|
| 1478 |
+
friends: friends || []
|
| 1479 |
+
});
|
| 1480 |
+
} finally {
|
| 1481 |
+
connection.release();
|
| 1482 |
+
}
|
| 1483 |
+
} catch (error) {
|
| 1484 |
+
console.error('获取好友列表失败:', error);
|
| 1485 |
+
logger.error('获取好友列表失败:', error);
|
| 1486 |
+
res.status(500).json({ error: '获取好友列表失败' });
|
| 1487 |
+
}
|
| 1488 |
+
});
|
| 1489 |
+
|
| 1490 |
+
// 搜索用户
|
| 1491 |
+
app.get('/api/users/search', authenticateToken, async (req, res) => {
|
| 1492 |
+
const { keyword } = req.query;
|
| 1493 |
+
const currentUserId = req.user.userId;
|
| 1494 |
+
|
| 1495 |
+
if (!keyword || keyword.trim() === '') {
|
| 1496 |
+
return res.json({ success: true, users: [] });
|
| 1497 |
+
}
|
| 1498 |
+
|
| 1499 |
+
try {
|
| 1500 |
+
const connection = await pool.getConnection();
|
| 1501 |
+
|
| 1502 |
+
try {
|
| 1503 |
+
// 搜索用户(排除自己)
|
| 1504 |
+
const [users] = await connection.query(`
|
| 1505 |
+
SELECT id, username, avatar, student_id
|
| 1506 |
+
FROM users
|
| 1507 |
+
WHERE (username LIKE ? OR student_id LIKE ?) AND id != ?
|
| 1508 |
+
LIMIT 20
|
| 1509 |
+
`, [`%${keyword}%`, `%${keyword}%`, currentUserId]);
|
| 1510 |
+
|
| 1511 |
+
// 检查每个用户是否已是好友
|
| 1512 |
+
const usersWithFriendStatus = await Promise.all(users.map(async (user) => {
|
| 1513 |
+
const [friendship] = await connection.query(`
|
| 1514 |
+
SELECT status FROM friendships
|
| 1515 |
+
WHERE (user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)
|
| 1516 |
+
`, [currentUserId, user.id, user.id, currentUserId]);
|
| 1517 |
+
|
| 1518 |
+
return {
|
| 1519 |
+
...user,
|
| 1520 |
+
isFriend: friendship.length > 0 && friendship[0].status === 'accepted',
|
| 1521 |
+
friendshipStatus: friendship.length > 0 ? friendship[0].status : null
|
| 1522 |
+
};
|
| 1523 |
+
}));
|
| 1524 |
+
|
| 1525 |
+
res.json({
|
| 1526 |
+
success: true,
|
| 1527 |
+
users: usersWithFriendStatus
|
| 1528 |
+
});
|
| 1529 |
+
} finally {
|
| 1530 |
+
connection.release();
|
| 1531 |
+
}
|
| 1532 |
+
} catch (error) {
|
| 1533 |
+
console.error('搜索用户失败:', error);
|
| 1534 |
+
logger.error('搜索用户失败:', error);
|
| 1535 |
+
res.status(500).json({ error: '搜索用户失败' });
|
| 1536 |
+
}
|
| 1537 |
+
});
|
| 1538 |
+
|
| 1539 |
+
// ============ 用户资料相关接口 ============
|
| 1540 |
+
|
| 1541 |
+
// 上传头像
|
| 1542 |
+
// 优先使用 Cloudflare R2 存储,如未配置则使用 Base64 存数据库
|
| 1543 |
+
app.post('/api/profile/avatar', authenticateToken, upload.single('avatar'), async (req, res) => {
|
| 1544 |
+
try {
|
| 1545 |
+
console.log('📸 收到头像上传请求');
|
| 1546 |
+
console.log('📸 用户ID:', req.user.userId);
|
| 1547 |
+
console.log('📸 文件信息:', req.file);
|
| 1548 |
+
|
| 1549 |
+
if (!req.file) {
|
| 1550 |
+
console.error('❌ 没有收到文件');
|
| 1551 |
+
return res.status(400).json({ error: '请选择头像文件' });
|
| 1552 |
+
}
|
| 1553 |
+
|
| 1554 |
+
const userId = req.user.userId;
|
| 1555 |
+
|
| 1556 |
+
console.log('📸 文件路径:', req.file.path);
|
| 1557 |
+
console.log('📸 文件大小:', req.file.size);
|
| 1558 |
+
|
| 1559 |
+
// 检查文件是否存在
|
| 1560 |
+
if (!fs.existsSync(req.file.path)) {
|
| 1561 |
+
console.error('❌ 文件不存在:', req.file.path);
|
| 1562 |
+
return res.status(500).json({ error: '文件上传失败,请重试' });
|
| 1563 |
+
}
|
| 1564 |
+
|
| 1565 |
+
// 读取文件
|
| 1566 |
+
const fileBuffer = fs.readFileSync(req.file.path);
|
| 1567 |
+
let avatarUrl = null;
|
| 1568 |
+
let storageType = 'base64';
|
| 1569 |
+
|
| 1570 |
+
// 优先使用 R2 存储
|
| 1571 |
+
if (r2Storage.isConfigured()) {
|
| 1572 |
+
try {
|
| 1573 |
+
console.log('📸 使用 Cloudflare R2 存储头像...');
|
| 1574 |
+
|
| 1575 |
+
// 获取文件扩展名
|
| 1576 |
+
const ext = req.file.originalname.split('.').pop() || 'jpg';
|
| 1577 |
+
|
| 1578 |
+
// 使用固定路径:avatars/用户ID/avatar.扩展名
|
| 1579 |
+
// 每个用户有独立文件夹,每次上传覆盖同一文件
|
| 1580 |
+
const customKey = `avatars/user-${userId}/avatar.${ext}`;
|
| 1581 |
+
|
| 1582 |
+
const uploadResult = await r2Storage.uploadFile(
|
| 1583 |
+
fileBuffer,
|
| 1584 |
+
req.file.originalname,
|
| 1585 |
+
req.file.mimetype,
|
| 1586 |
+
{ customKey: customKey }
|
| 1587 |
+
);
|
| 1588 |
+
|
| 1589 |
+
// 添加时间戳参数避免浏览器缓存
|
| 1590 |
+
avatarUrl = `${uploadResult.url}?t=${Date.now()}`;
|
| 1591 |
+
storageType = 'r2';
|
| 1592 |
+
console.log('📸 R2 上传成功:', avatarUrl);
|
| 1593 |
+
console.log('📸 存储路径:', customKey);
|
| 1594 |
+
} catch (r2Error) {
|
| 1595 |
+
console.error('📸 R2 上传失败,回退到 Base64:', r2Error.message);
|
| 1596 |
+
// R2 失败时回退到 Base64
|
| 1597 |
+
avatarUrl = `data:${req.file.mimetype};base64,${fileBuffer.toString('base64')}`;
|
| 1598 |
+
}
|
| 1599 |
+
} else {
|
| 1600 |
+
// 未配置 R2,使用 Base64 存储
|
| 1601 |
+
console.log('📸 R2 未配置,使用 Base64 存储头像');
|
| 1602 |
+
avatarUrl = `data:${req.file.mimetype};base64,${fileBuffer.toString('base64')}`;
|
| 1603 |
+
}
|
| 1604 |
+
|
| 1605 |
+
console.log('📸 头像URL长度:', avatarUrl.length);
|
| 1606 |
+
console.log('📸 存储类型:', storageType);
|
| 1607 |
+
|
| 1608 |
+
// 删除临时文件
|
| 1609 |
+
try {
|
| 1610 |
+
fs.unlinkSync(req.file.path);
|
| 1611 |
+
console.log('📸 临时文件已删除');
|
| 1612 |
+
} catch (unlinkError) {
|
| 1613 |
+
console.error('⚠️ 删除临时文件失败:', unlinkError.message);
|
| 1614 |
+
}
|
| 1615 |
+
|
| 1616 |
+
const connection = await pool.getConnection();
|
| 1617 |
+
|
| 1618 |
+
try {
|
| 1619 |
+
// 更新用户头像
|
| 1620 |
+
await connection.query(
|
| 1621 |
+
'UPDATE users SET avatar = ? WHERE id = ?',
|
| 1622 |
+
[avatarUrl, userId]
|
| 1623 |
+
);
|
| 1624 |
+
|
| 1625 |
+
console.log('✅ 头像上传成功');
|
| 1626 |
+
|
| 1627 |
+
res.json({
|
| 1628 |
+
success: true,
|
| 1629 |
+
avatarUrl: avatarUrl,
|
| 1630 |
+
storageType: storageType,
|
| 1631 |
+
message: '头像上传成功'
|
| 1632 |
+
});
|
| 1633 |
+
} finally {
|
| 1634 |
+
connection.release();
|
| 1635 |
+
}
|
| 1636 |
+
} catch (error) {
|
| 1637 |
+
console.error('❌ 上传头像失败:', error);
|
| 1638 |
+
console.error('❌ 错误堆栈:', error.stack);
|
| 1639 |
+
logger.error('上传头像失败:', error);
|
| 1640 |
+
res.status(500).json({
|
| 1641 |
+
error: '上传头像失败',
|
| 1642 |
+
message: error.message
|
| 1643 |
+
});
|
| 1644 |
+
}
|
| 1645 |
+
});
|
| 1646 |
+
|
| 1647 |
+
// 更新用户资料
|
| 1648 |
+
app.put('/api/profile', authenticateToken, async (req, res) => {
|
| 1649 |
+
const userId = req.user.userId;
|
| 1650 |
+
const { username, email, bio, phone, major, grade, gender, avatar } = req.body;
|
| 1651 |
+
|
| 1652 |
+
try {
|
| 1653 |
+
const connection = await pool.getConnection();
|
| 1654 |
+
|
| 1655 |
+
try {
|
| 1656 |
+
// 构建更新字段
|
| 1657 |
+
const updates = [];
|
| 1658 |
+
const values = [];
|
| 1659 |
+
|
| 1660 |
+
if (username !== undefined) {
|
| 1661 |
+
updates.push('username = ?');
|
| 1662 |
+
values.push(username);
|
| 1663 |
+
}
|
| 1664 |
+
if (email !== undefined) {
|
| 1665 |
+
updates.push('email = ?');
|
| 1666 |
+
values.push(email);
|
| 1667 |
+
}
|
| 1668 |
+
if (bio !== undefined) {
|
| 1669 |
+
updates.push('bio = ?');
|
| 1670 |
+
values.push(bio);
|
| 1671 |
+
}
|
| 1672 |
+
if (phone !== undefined) {
|
| 1673 |
+
updates.push('phone = ?');
|
| 1674 |
+
values.push(phone);
|
| 1675 |
+
}
|
| 1676 |
+
if (major !== undefined) {
|
| 1677 |
+
updates.push('major = ?');
|
| 1678 |
+
values.push(major);
|
| 1679 |
+
}
|
| 1680 |
+
if (grade !== undefined) {
|
| 1681 |
+
updates.push('grade = ?');
|
| 1682 |
+
values.push(grade);
|
| 1683 |
+
}
|
| 1684 |
+
if (gender !== undefined) {
|
| 1685 |
+
updates.push('gender = ?');
|
| 1686 |
+
values.push(gender);
|
| 1687 |
+
}
|
| 1688 |
+
if (avatar !== undefined) {
|
| 1689 |
+
updates.push('avatar = ?');
|
| 1690 |
+
values.push(avatar);
|
| 1691 |
+
}
|
| 1692 |
+
|
| 1693 |
+
if (updates.length === 0) {
|
| 1694 |
+
return res.status(400).json({ error: '没有要更新的字段' });
|
| 1695 |
+
}
|
| 1696 |
+
|
| 1697 |
+
values.push(userId);
|
| 1698 |
+
|
| 1699 |
+
await connection.query(
|
| 1700 |
+
`UPDATE users SET ${updates.join(', ')} WHERE id = ?`,
|
| 1701 |
+
values
|
| 1702 |
+
);
|
| 1703 |
+
|
| 1704 |
+
res.json({
|
| 1705 |
+
success: true,
|
| 1706 |
+
message: '资料更新成功'
|
| 1707 |
+
});
|
| 1708 |
+
} finally {
|
| 1709 |
+
connection.release();
|
| 1710 |
+
}
|
| 1711 |
+
} catch (error) {
|
| 1712 |
+
console.error('更新资料失败:', error);
|
| 1713 |
+
logger.error('更新资料失败:', error);
|
| 1714 |
+
res.status(500).json({ error: '更新资料失败' });
|
| 1715 |
+
}
|
| 1716 |
+
});
|
| 1717 |
+
|
| 1718 |
+
// 添加好友
|
| 1719 |
+
app.post('/api/friends', authenticateToken, async (req, res) => {
|
| 1720 |
+
const userId = req.user.userId;
|
| 1721 |
+
const { friendId } = req.body;
|
| 1722 |
+
|
| 1723 |
+
if (!friendId) {
|
| 1724 |
+
return res.status(400).json({ error: '好友ID不能为空' });
|
| 1725 |
+
}
|
| 1726 |
+
|
| 1727 |
+
if (userId === friendId) {
|
| 1728 |
+
return res.status(400).json({ error: '不能添加自己为好友' });
|
| 1729 |
+
}
|
| 1730 |
+
|
| 1731 |
+
try {
|
| 1732 |
+
const connection = await pool.getConnection();
|
| 1733 |
+
|
| 1734 |
+
try {
|
| 1735 |
+
// 检查是否已经是好友
|
| 1736 |
+
const [existing] = await connection.query(`
|
| 1737 |
+
SELECT * FROM friendships
|
| 1738 |
+
WHERE (user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)
|
| 1739 |
+
`, [userId, friendId, friendId, userId]);
|
| 1740 |
+
|
| 1741 |
+
if (existing.length > 0) {
|
| 1742 |
+
return res.status(400).json({ error: '已经是好友或已发送好友请求' });
|
| 1743 |
+
}
|
| 1744 |
+
|
| 1745 |
+
// 添加好友关系(直接设为accepted,简化流程)
|
| 1746 |
+
await connection.query(
|
| 1747 |
+
'INSERT INTO friendships (user_id, friend_id, status) VALUES (?, ?, ?)',
|
| 1748 |
+
[userId, friendId, 'accepted']
|
| 1749 |
+
);
|
| 1750 |
+
|
| 1751 |
+
res.json({
|
| 1752 |
+
success: true,
|
| 1753 |
+
message: '添加好友成功'
|
| 1754 |
+
});
|
| 1755 |
+
} finally {
|
| 1756 |
+
connection.release();
|
| 1757 |
+
}
|
| 1758 |
+
} catch (error) {
|
| 1759 |
+
console.error('添加好友失败:', error);
|
| 1760 |
+
logger.error('添加好友失败:', error);
|
| 1761 |
+
res.status(500).json({ error: '添加好友失败' });
|
| 1762 |
+
}
|
| 1763 |
+
});
|
| 1764 |
+
|
| 1765 |
+
// ============ 聊天相关接口 ============
|
| 1766 |
+
|
| 1767 |
+
// 获取聊天消息
|
| 1768 |
+
app.get('/api/chat/:friendId', authenticateToken, async (req, res) => {
|
| 1769 |
+
const userId = req.user.userId;
|
| 1770 |
+
const friendId = req.params.friendId;
|
| 1771 |
+
|
| 1772 |
+
try {
|
| 1773 |
+
const connection = await pool.getConnection();
|
| 1774 |
+
|
| 1775 |
+
try {
|
| 1776 |
+
// 获取聊天消息
|
| 1777 |
+
const [messages] = await connection.query(`
|
| 1778 |
+
SELECT m.*, u.username, u.avatar
|
| 1779 |
+
FROM chat_messages m
|
| 1780 |
+
JOIN users u ON m.sender_id = u.id
|
| 1781 |
+
WHERE (m.sender_id = ? AND m.receiver_id = ?)
|
| 1782 |
+
OR (m.sender_id = ? AND m.receiver_id = ?)
|
| 1783 |
+
ORDER BY m.created_at ASC
|
| 1784 |
+
LIMIT 100
|
| 1785 |
+
`, [userId, friendId, friendId, userId]);
|
| 1786 |
+
|
| 1787 |
+
res.json({
|
| 1788 |
+
success: true,
|
| 1789 |
+
messages: messages || []
|
| 1790 |
+
});
|
| 1791 |
+
} finally {
|
| 1792 |
+
connection.release();
|
| 1793 |
+
}
|
| 1794 |
+
} catch (error) {
|
| 1795 |
+
console.error('获取聊天消息失败:', error);
|
| 1796 |
+
logger.error('获取聊天消息失败:', error);
|
| 1797 |
+
res.status(500).json({ error: '获取聊天消息失败' });
|
| 1798 |
+
}
|
| 1799 |
+
});
|
| 1800 |
+
|
| 1801 |
+
// 发送聊天消息
|
| 1802 |
+
app.post('/api/chat', authenticateToken, async (req, res) => {
|
| 1803 |
+
const senderId = req.user.userId;
|
| 1804 |
+
const { receiverId, content, type = 'text' } = req.body;
|
| 1805 |
+
|
| 1806 |
+
if (!receiverId || !content) {
|
| 1807 |
+
return res.status(400).json({ error: '接收者ID和消息内容不能为空' });
|
| 1808 |
+
}
|
| 1809 |
+
|
| 1810 |
+
try {
|
| 1811 |
+
const connection = await pool.getConnection();
|
| 1812 |
+
|
| 1813 |
+
try {
|
| 1814 |
+
// 插入消息
|
| 1815 |
+
const [result] = await connection.query(
|
| 1816 |
+
'INSERT INTO chat_messages (sender_id, receiver_id, content, type) VALUES (?, ?, ?, ?)',
|
| 1817 |
+
[senderId, receiverId, content, type]
|
| 1818 |
+
);
|
| 1819 |
+
|
| 1820 |
+
// 获取刚插入的消息
|
| 1821 |
+
const [messages] = await connection.query(`
|
| 1822 |
+
SELECT m.*, u.username, u.avatar
|
| 1823 |
+
FROM chat_messages m
|
| 1824 |
+
JOIN users u ON m.sender_id = u.id
|
| 1825 |
+
WHERE m.id = ?
|
| 1826 |
+
`, [result.insertId]);
|
| 1827 |
+
|
| 1828 |
+
res.json({
|
| 1829 |
+
success: true,
|
| 1830 |
+
message: messages[0]
|
| 1831 |
+
});
|
| 1832 |
+
} finally {
|
| 1833 |
+
connection.release();
|
| 1834 |
+
}
|
| 1835 |
+
} catch (error) {
|
| 1836 |
+
console.error('发送消息失败:', error);
|
| 1837 |
+
logger.error('发送消息失败:', error);
|
| 1838 |
+
res.status(500).json({ error: '发送消息失败' });
|
| 1839 |
+
}
|
| 1840 |
+
});
|
| 1841 |
+
|
| 1842 |
+
// ============ 兼容性路由 ============
|
| 1843 |
+
// 失物招领兼容性路由 (前端调用的是 /api/lost-found)
|
| 1844 |
+
app.get('/api/lost-found', async (req, res) => {
|
| 1845 |
+
const { category, status } = req.query;
|
| 1846 |
+
|
| 1847 |
+
try {
|
| 1848 |
+
const connection = await pool.getConnection();
|
| 1849 |
+
|
| 1850 |
+
try {
|
| 1851 |
+
let query = `
|
| 1852 |
+
SELECT lf.*, u.username
|
| 1853 |
+
FROM lost_found lf
|
| 1854 |
+
JOIN users u ON lf.user_id = u.id
|
| 1855 |
+
`;
|
| 1856 |
+
let conditions = [];
|
| 1857 |
+
let params = [];
|
| 1858 |
+
|
| 1859 |
+
if (category) {
|
| 1860 |
+
conditions.push('lf.category = ?');
|
| 1861 |
+
params.push(category);
|
| 1862 |
+
}
|
| 1863 |
+
|
| 1864 |
+
if (status) {
|
| 1865 |
+
conditions.push('lf.status = ?');
|
| 1866 |
+
params.push(status);
|
| 1867 |
+
}
|
| 1868 |
+
|
| 1869 |
+
if (conditions.length > 0) {
|
| 1870 |
+
query += ' WHERE ' + conditions.join(' AND ');
|
| 1871 |
+
}
|
| 1872 |
+
|
| 1873 |
+
query += ' ORDER BY lf.created_at DESC LIMIT 50';
|
| 1874 |
+
|
| 1875 |
+
const [items] = await connection.query(query, params);
|
| 1876 |
+
res.json({ items });
|
| 1877 |
+
} finally {
|
| 1878 |
+
connection.release();
|
| 1879 |
+
}
|
| 1880 |
+
} catch (error) {
|
| 1881 |
+
logger.error('获取失物招领失败:', error);
|
| 1882 |
+
res.status(500).json({ error: '获取失物招领失败' });
|
| 1883 |
+
}
|
| 1884 |
+
});
|
| 1885 |
+
|
| 1886 |
+
app.post('/api/lost-found', authenticateToken, [
|
| 1887 |
+
body('title').notEmpty().withMessage('标题不能为空'),
|
| 1888 |
+
body('description').notEmpty().withMessage('描述不能为空'),
|
| 1889 |
+
body('category').notEmpty().withMessage('分类不能为空')
|
| 1890 |
+
], async (req, res) => {
|
| 1891 |
+
const errors = validationResult(req);
|
| 1892 |
+
if (!errors.isEmpty()) {
|
| 1893 |
+
return res.status(400).json({ errors: errors.array() });
|
| 1894 |
+
}
|
| 1895 |
+
|
| 1896 |
+
const { title, description, category, contact, images } = req.body;
|
| 1897 |
+
|
| 1898 |
+
try {
|
| 1899 |
+
const connection = await pool.getConnection();
|
| 1900 |
+
|
| 1901 |
+
try {
|
| 1902 |
+
const imagesJson = images && images.length > 0 ? JSON.stringify(images) : null;
|
| 1903 |
+
|
| 1904 |
+
// 先尝试包含images字段的插入
|
| 1905 |
+
let result;
|
| 1906 |
+
try {
|
| 1907 |
+
[result] = await connection.query(
|
| 1908 |
+
'INSERT INTO lost_found (user_id, title, description, category, contact, images) VALUES (?, ?, ?, ?, ?, ?)',
|
| 1909 |
+
[req.user.userId, title, description, category, contact || '', imagesJson]
|
| 1910 |
+
);
|
| 1911 |
+
} catch (insertError) {
|
| 1912 |
+
if (insertError.code === 'ER_BAD_FIELD_ERROR') {
|
| 1913 |
+
// images字段不存在,使用不包含images的插入
|
| 1914 |
+
console.log('images字段不存在,使用兼容模式插入');
|
| 1915 |
+
[result] = await connection.query(
|
| 1916 |
+
'INSERT INTO lost_found (user_id, title, description, category, contact) VALUES (?, ?, ?, ?, ?)',
|
| 1917 |
+
[req.user.userId, title, description, category, contact || '']
|
| 1918 |
+
);
|
| 1919 |
+
} else {
|
| 1920 |
+
throw insertError;
|
| 1921 |
+
}
|
| 1922 |
+
}
|
| 1923 |
+
|
| 1924 |
+
res.status(201).json({
|
| 1925 |
+
message: '失物招领发布成功',
|
| 1926 |
+
itemId: result.insertId
|
| 1927 |
+
});
|
| 1928 |
+
} finally {
|
| 1929 |
+
connection.release();
|
| 1930 |
+
}
|
| 1931 |
+
} catch (error) {
|
| 1932 |
+
logger.error('发布失物招领失败:', error);
|
| 1933 |
+
res.status(500).json({ error: '发布失物招领失败' });
|
| 1934 |
+
}
|
| 1935 |
+
});
|
| 1936 |
+
|
| 1937 |
+
// 获取失物招领详情接口
|
| 1938 |
+
app.get('/api/lost-found/:id', async (req, res) => {
|
| 1939 |
+
const { id } = req.params;
|
| 1940 |
+
|
| 1941 |
+
try {
|
| 1942 |
+
const connection = await pool.getConnection();
|
| 1943 |
+
|
| 1944 |
+
try {
|
| 1945 |
+
const [items] = await connection.query(`
|
| 1946 |
+
SELECT lf.*, u.username
|
| 1947 |
+
FROM lost_found lf
|
| 1948 |
+
JOIN users u ON lf.user_id = u.id
|
| 1949 |
+
WHERE lf.id = ?
|
| 1950 |
+
`, [id]);
|
| 1951 |
+
|
| 1952 |
+
if (items.length === 0) {
|
| 1953 |
+
return res.status(404).json({ error: '失物招领信息不存在' });
|
| 1954 |
+
}
|
| 1955 |
+
|
| 1956 |
+
res.json({ item: items[0] });
|
| 1957 |
+
} finally {
|
| 1958 |
+
connection.release();
|
| 1959 |
+
}
|
| 1960 |
+
} catch (error) {
|
| 1961 |
+
logger.error('获取失物招领详情失败:', error);
|
| 1962 |
+
res.status(500).json({ error: '获取失物招领详情失败' });
|
| 1963 |
+
}
|
| 1964 |
+
});
|
| 1965 |
+
|
| 1966 |
+
// 更新失物招领状态接口
|
| 1967 |
+
app.put('/api/lost-found/:id/status', authenticateToken, async (req, res) => {
|
| 1968 |
+
const { id } = req.params;
|
| 1969 |
+
const { status } = req.body;
|
| 1970 |
+
|
| 1971 |
+
try {
|
| 1972 |
+
const connection = await pool.getConnection();
|
| 1973 |
+
|
| 1974 |
+
try {
|
| 1975 |
+
const [result] = await connection.query(
|
| 1976 |
+
'UPDATE lost_found SET status = ? WHERE id = ?',
|
| 1977 |
+
[status, id]
|
| 1978 |
+
);
|
| 1979 |
+
|
| 1980 |
+
if (result.affectedRows === 0) {
|
| 1981 |
+
return res.status(404).json({ error: '失物招领信息不存在' });
|
| 1982 |
+
}
|
| 1983 |
+
|
| 1984 |
+
res.json({ success: true, message: '状态更新成功' });
|
| 1985 |
+
} finally {
|
| 1986 |
+
connection.release();
|
| 1987 |
+
}
|
| 1988 |
+
} catch (error) {
|
| 1989 |
+
logger.error('更新失物招领状态失败:', error);
|
| 1990 |
+
res.status(500).json({ error: '更新失物招领状态失败' });
|
| 1991 |
+
}
|
| 1992 |
+
});
|
| 1993 |
+
|
| 1994 |
+
// 编辑失物招领接口
|
| 1995 |
+
app.put('/api/lost-found/:id', authenticateToken, [
|
| 1996 |
+
body('title').notEmpty().withMessage('标题不能为空'),
|
| 1997 |
+
body('description').notEmpty().withMessage('描述不能为空'),
|
| 1998 |
+
body('category').notEmpty().withMessage('分类不能为空')
|
| 1999 |
+
], async (req, res) => {
|
| 2000 |
+
const errors = validationResult(req);
|
| 2001 |
+
if (!errors.isEmpty()) {
|
| 2002 |
+
return res.status(400).json({ errors: errors.array() });
|
| 2003 |
+
}
|
| 2004 |
+
|
| 2005 |
+
const { id } = req.params;
|
| 2006 |
+
const { title, description, category, contact, images } = req.body;
|
| 2007 |
+
|
| 2008 |
+
try {
|
| 2009 |
+
const connection = await pool.getConnection();
|
| 2010 |
+
|
| 2011 |
+
try {
|
| 2012 |
+
const imagesJson = images && images.length > 0 ? JSON.stringify(images) : null;
|
| 2013 |
+
|
| 2014 |
+
// 先尝试包含images字段的更新
|
| 2015 |
+
let result;
|
| 2016 |
+
try {
|
| 2017 |
+
[result] = await connection.query(
|
| 2018 |
+
'UPDATE lost_found SET title = ?, description = ?, category = ?, contact = ?, images = ? WHERE id = ? AND user_id = ?',
|
| 2019 |
+
[title, description, category, contact || '', imagesJson, id, req.user.userId]
|
| 2020 |
+
);
|
| 2021 |
+
} catch (updateError) {
|
| 2022 |
+
if (updateError.code === 'ER_BAD_FIELD_ERROR') {
|
| 2023 |
+
// images字段不存在,使用不包含images的更新
|
| 2024 |
+
console.log('images字段不存在,使用兼容模式更新');
|
| 2025 |
+
[result] = await connection.query(
|
| 2026 |
+
'UPDATE lost_found SET title = ?, description = ?, category = ?, contact = ? WHERE id = ? AND user_id = ?',
|
| 2027 |
+
[title, description, category, contact || '', id, req.user.userId]
|
| 2028 |
+
);
|
| 2029 |
+
} else {
|
| 2030 |
+
throw updateError;
|
| 2031 |
+
}
|
| 2032 |
+
}
|
| 2033 |
+
|
| 2034 |
+
if (result.affectedRows === 0) {
|
| 2035 |
+
return res.status(404).json({ error: '失物招领信息不存在或无权限编辑' });
|
| 2036 |
+
}
|
| 2037 |
+
|
| 2038 |
+
res.json({ success: true, message: '编辑成功' });
|
| 2039 |
+
} finally {
|
| 2040 |
+
connection.release();
|
| 2041 |
+
}
|
| 2042 |
+
} catch (error) {
|
| 2043 |
+
logger.error('编辑失物招领失败:', error);
|
| 2044 |
+
res.status(500).json({ error: '编辑失物招领失败' });
|
| 2045 |
+
}
|
| 2046 |
+
});
|
| 2047 |
+
|
| 2048 |
+
// 删除失物招领接口
|
| 2049 |
+
app.delete('/api/lost-found/:id', authenticateToken, async (req, res) => {
|
| 2050 |
+
const { id } = req.params;
|
| 2051 |
+
|
| 2052 |
+
try {
|
| 2053 |
+
const connection = await pool.getConnection();
|
| 2054 |
+
|
| 2055 |
+
try {
|
| 2056 |
+
const [result] = await connection.query(
|
| 2057 |
+
'DELETE FROM lost_found WHERE id = ? AND user_id = ?',
|
| 2058 |
+
[id, req.user.userId]
|
| 2059 |
+
);
|
| 2060 |
+
|
| 2061 |
+
if (result.affectedRows === 0) {
|
| 2062 |
+
return res.status(404).json({ error: '失物招领信息不存在或无权限删除' });
|
| 2063 |
+
}
|
| 2064 |
+
|
| 2065 |
+
res.json({ success: true, message: '删除成功' });
|
| 2066 |
+
} finally {
|
| 2067 |
+
connection.release();
|
| 2068 |
+
}
|
| 2069 |
+
} catch (error) {
|
| 2070 |
+
logger.error('删除失物招领失败:', error);
|
| 2071 |
+
res.status(500).json({ error: '删除失物招领失败' });
|
| 2072 |
+
}
|
| 2073 |
+
});
|
| 2074 |
+
|
| 2075 |
+
// 失物招领点赞接口
|
| 2076 |
+
app.post('/api/lost-found/:id/like', authenticateToken, async (req, res) => {
|
| 2077 |
+
const { id } = req.params;
|
| 2078 |
+
|
| 2079 |
+
try {
|
| 2080 |
+
const connection = await pool.getConnection();
|
| 2081 |
+
|
| 2082 |
+
try {
|
| 2083 |
+
// 检查是否已经点赞
|
| 2084 |
+
const [existingLikes] = await connection.query(
|
| 2085 |
+
'SELECT id FROM lost_found_likes WHERE item_id = ? AND user_id = ?',
|
| 2086 |
+
[id, req.user.userId]
|
| 2087 |
+
);
|
| 2088 |
+
|
| 2089 |
+
if (existingLikes.length > 0) {
|
| 2090 |
+
// 已经点赞,取消点赞
|
| 2091 |
+
await connection.query(
|
| 2092 |
+
'DELETE FROM lost_found_likes WHERE item_id = ? AND user_id = ?',
|
| 2093 |
+
[id, req.user.userId]
|
| 2094 |
+
);
|
| 2095 |
+
|
| 2096 |
+
await connection.query(
|
| 2097 |
+
'UPDATE lost_found SET likes = likes - 1 WHERE id = ?',
|
| 2098 |
+
[id]
|
| 2099 |
+
);
|
| 2100 |
+
|
| 2101 |
+
res.json({ success: true, message: '取消点赞成功', liked: false });
|
| 2102 |
+
} else {
|
| 2103 |
+
// 未点赞,添加点赞
|
| 2104 |
+
await connection.query(
|
| 2105 |
+
'INSERT INTO lost_found_likes (item_id, user_id) VALUES (?, ?)',
|
| 2106 |
+
[id, req.user.userId]
|
| 2107 |
+
);
|
| 2108 |
+
|
| 2109 |
+
await connection.query(
|
| 2110 |
+
'UPDATE lost_found SET likes = likes + 1 WHERE id = ?',
|
| 2111 |
+
[id]
|
| 2112 |
+
);
|
| 2113 |
+
|
| 2114 |
+
res.json({ success: true, message: '点赞成功', liked: true });
|
| 2115 |
+
}
|
| 2116 |
+
} finally {
|
| 2117 |
+
connection.release();
|
| 2118 |
+
}
|
| 2119 |
+
} catch (error) {
|
| 2120 |
+
logger.error('失物招领点赞失败:', error);
|
| 2121 |
+
res.status(500).json({ error: '操作失败' });
|
| 2122 |
+
}
|
| 2123 |
+
});
|
| 2124 |
+
|
| 2125 |
+
// 调试接口 - 检查文件是否存在
|
| 2126 |
+
app.get('/api/debug/files', (req, res) => {
|
| 2127 |
+
try {
|
| 2128 |
+
// 检查uploads目录
|
| 2129 |
+
const uploadsFiles = fs.existsSync(uploadsDir) ? fs.readdirSync(uploadsDir) : [];
|
| 2130 |
+
const uploadsDetails = uploadsFiles.map(filename => {
|
| 2131 |
+
const filePath = path.join(uploadsDir, filename);
|
| 2132 |
+
const stats = fs.statSync(filePath);
|
| 2133 |
+
return {
|
| 2134 |
+
filename,
|
| 2135 |
+
size: stats.size,
|
| 2136 |
+
created: stats.birthtime,
|
| 2137 |
+
modified: stats.mtime,
|
| 2138 |
+
path: filePath
|
| 2139 |
+
};
|
| 2140 |
+
});
|
| 2141 |
+
|
| 2142 |
+
// 检查static目录
|
| 2143 |
+
const staticFiles = fs.existsSync(staticDir) ? fs.readdirSync(staticDir) : [];
|
| 2144 |
+
const staticDetails = staticFiles.map(filename => {
|
| 2145 |
+
const filePath = path.join(staticDir, filename);
|
| 2146 |
+
const stats = fs.statSync(filePath);
|
| 2147 |
+
return {
|
| 2148 |
+
filename,
|
| 2149 |
+
size: stats.size,
|
| 2150 |
+
created: stats.birthtime,
|
| 2151 |
+
modified: stats.mtime,
|
| 2152 |
+
path: filePath
|
| 2153 |
+
};
|
| 2154 |
+
});
|
| 2155 |
+
|
| 2156 |
+
res.json({
|
| 2157 |
+
uploadsDir,
|
| 2158 |
+
staticDir,
|
| 2159 |
+
uploads: {
|
| 2160 |
+
totalFiles: uploadsFiles.length,
|
| 2161 |
+
files: uploadsDetails
|
| 2162 |
+
},
|
| 2163 |
+
static: {
|
| 2164 |
+
totalFiles: staticFiles.length,
|
| 2165 |
+
files: staticDetails
|
| 2166 |
+
}
|
| 2167 |
+
});
|
| 2168 |
+
} catch (error) {
|
| 2169 |
+
res.status(500).json({ error: error.message });
|
| 2170 |
+
}
|
| 2171 |
+
});
|
| 2172 |
+
|
| 2173 |
+
// 调试接口 - 查看用户数据
|
| 2174 |
+
app.get('/api/debug/users', async (req, res) => {
|
| 2175 |
+
try {
|
| 2176 |
+
const connection = await pool.getConnection();
|
| 2177 |
+
|
| 2178 |
+
try {
|
| 2179 |
+
const [users] = await connection.query('SELECT id, username, email, role, created_at FROM users');
|
| 2180 |
+
res.json({
|
| 2181 |
+
users: users.map(user => ({
|
| 2182 |
+
id: user.id,
|
| 2183 |
+
username: user.username,
|
| 2184 |
+
email: user.email,
|
| 2185 |
+
role: user.role,
|
| 2186 |
+
created_at: user.created_at
|
| 2187 |
+
})),
|
| 2188 |
+
count: users.length,
|
| 2189 |
+
message: '调试信息 - 用户列表'
|
| 2190 |
+
});
|
| 2191 |
+
} finally {
|
| 2192 |
+
connection.release();
|
| 2193 |
+
}
|
| 2194 |
+
} catch (error) {
|
| 2195 |
+
res.status(500).json({ error: error.message, message: '查询用户数据失败' });
|
| 2196 |
+
}
|
| 2197 |
+
});
|
| 2198 |
+
|
| 2199 |
+
// 调试接口 - 测试管理员登录
|
| 2200 |
+
app.post('/api/debug/test-admin', async (req, res) => {
|
| 2201 |
+
try {
|
| 2202 |
+
const connection = await pool.getConnection();
|
| 2203 |
+
|
| 2204 |
+
try {
|
| 2205 |
+
const [users] = await connection.query(
|
| 2206 |
+
'SELECT id, username, email, password, role FROM users WHERE username = ? OR email = ?',
|
| 2207 |
+
['admin', 'admin']
|
| 2208 |
+
);
|
| 2209 |
+
|
| 2210 |
+
if (users.length === 0) {
|
| 2211 |
+
return res.json({
|
| 2212 |
+
success: false,
|
| 2213 |
+
message: '管理员用户不存在',
|
| 2214 |
+
query: 'SELECT * FROM users WHERE username = admin OR email = admin'
|
| 2215 |
+
});
|
| 2216 |
+
}
|
| 2217 |
+
|
| 2218 |
+
const user = users[0];
|
| 2219 |
+
const bcrypt = require('bcrypt');
|
| 2220 |
+
const isValidPassword = await bcrypt.compare('admin', user.password);
|
| 2221 |
+
|
| 2222 |
+
res.json({
|
| 2223 |
+
success: isValidPassword,
|
| 2224 |
+
message: isValidPassword ? '密码验证成功' : '密码验证失败',
|
| 2225 |
+
user: {
|
| 2226 |
+
id: user.id,
|
| 2227 |
+
username: user.username,
|
| 2228 |
+
email: user.email,
|
| 2229 |
+
role: user.role,
|
| 2230 |
+
passwordHash: user.password.substring(0, 20) + '...'
|
| 2231 |
+
}
|
| 2232 |
+
});
|
| 2233 |
+
} finally {
|
| 2234 |
+
connection.release();
|
| 2235 |
+
}
|
| 2236 |
+
} catch (error) {
|
| 2237 |
+
res.status(500).json({ error: error.message, message: '测试管理员登录失败' });
|
| 2238 |
+
}
|
| 2239 |
+
});
|
| 2240 |
+
|
| 2241 |
+
// Multer错误处理中间件
|
| 2242 |
+
app.use((error, req, res, next) => {
|
| 2243 |
+
if (error instanceof multer.MulterError) {
|
| 2244 |
+
console.error('❌ Multer错误:', error);
|
| 2245 |
+
if (error.code === 'LIMIT_FILE_SIZE') {
|
| 2246 |
+
return res.status(400).json({
|
| 2247 |
+
error: '文件太大',
|
| 2248 |
+
message: '文件大小不能超过2MB'
|
| 2249 |
+
});
|
| 2250 |
+
}
|
| 2251 |
+
return res.status(400).json({
|
| 2252 |
+
error: '文件上传错误',
|
| 2253 |
+
message: error.message
|
| 2254 |
+
});
|
| 2255 |
+
}
|
| 2256 |
+
|
| 2257 |
+
if (error) {
|
| 2258 |
+
console.error('❌ 服务器错误:', error);
|
| 2259 |
+
return res.status(500).json({
|
| 2260 |
+
error: '服务器错误',
|
| 2261 |
+
message: error.message
|
| 2262 |
+
});
|
| 2263 |
+
}
|
| 2264 |
+
|
| 2265 |
+
next();
|
| 2266 |
+
});
|
| 2267 |
+
|
| 2268 |
+
// 404处理
|
| 2269 |
+
app.use('*', (req, res) => {
|
| 2270 |
+
res.status(404).json({
|
| 2271 |
+
error: 'API endpoint not found',
|
| 2272 |
+
message: 'Please check the API documentation at /'
|
| 2273 |
+
});
|
| 2274 |
+
});
|
| 2275 |
+
|
| 2276 |
+
// 启动服务器
|
| 2277 |
+
async function startServer() {
|
| 2278 |
+
try {
|
| 2279 |
+
console.log('🔄 正在连接MySQL数据库...');
|
| 2280 |
+
console.log(`📍 数据库地址: ${process.env.MYSQL_HOST || 'gz-cdb-1xrcr3dt.sql.tencentcdb.com'}:${process.env.MYSQL_PORT || 23767}`);
|
| 2281 |
+
|
| 2282 |
+
// 测试数据库连接
|
| 2283 |
+
const connection = await pool.getConnection();
|
| 2284 |
+
console.log('✅ MySQL数据库连接成功!');
|
| 2285 |
+
connection.release();
|
| 2286 |
+
|
| 2287 |
+
// 初始化数据库表
|
| 2288 |
+
await initDatabase();
|
| 2289 |
+
|
| 2290 |
+
// 启动服务器
|
| 2291 |
+
app.listen(PORT, '0.0.0.0', () => {
|
| 2292 |
+
logger.info(`校园圈服务器运行在端口 ${PORT} - Hugging Face Spaces版本`);
|
| 2293 |
+
console.log(`\n🚀 CampusLoop Backend 启动成功!`);
|
| 2294 |
+
console.log(`🌐 服务器地址: http://0.0.0.0:${PORT}`);
|
| 2295 |
+
console.log(`🏥 健康检查: http://0.0.0.0:${PORT}/api/health`);
|
| 2296 |
+
console.log(`📅 当前时间: ${new Date().toLocaleString()}`);
|
| 2297 |
+
console.log(`🎯 平台: Hugging Face Spaces`);
|
| 2298 |
+
console.log(`\n📝 主要接口:`);
|
| 2299 |
+
console.log(` GET / - API文档`);
|
| 2300 |
+
console.log(` GET /api/health - 健康检查`);
|
| 2301 |
+
console.log(` POST /api/auth/register - 用户注册`);
|
| 2302 |
+
console.log(` POST /api/auth/login - 用户登录`);
|
| 2303 |
+
console.log(` GET /api/posts - 获取动态`);
|
| 2304 |
+
console.log(` POST /api/posts - 发布动态`);
|
| 2305 |
+
console.log(` GET /api/forum/posts - 获取论坛帖子`);
|
| 2306 |
+
console.log(` GET /api/activities - 获取活动`);
|
| 2307 |
+
});
|
| 2308 |
+
} catch (error) {
|
| 2309 |
+
logger.error('服务器启动失败:', error);
|
| 2310 |
+
console.error('❌ 服务器启动失败:', error.message);
|
| 2311 |
+
|
| 2312 |
+
if (error.code === 'ETIMEDOUT') {
|
| 2313 |
+
console.error('\n⚠️ 数据库连接超时!可能的原因:');
|
| 2314 |
+
console.error(' 1. 数据库服务器防火墙未开放外部访问');
|
| 2315 |
+
console.error(' 2. Hugging Face Spaces IP未加入数据库白名单');
|
| 2316 |
+
console.error(' 3. 数据库地址或端口配置错误');
|
| 2317 |
+
console.error('\n💡 解决方案:');
|
| 2318 |
+
console.error(' 1. 在腾讯云MySQL控制台添加 0.0.0.0/0 到白名单(测试用)');
|
| 2319 |
+
console.error(' 2. 检查Hugging Face Spaces环境变量配置');
|
| 2320 |
+
console.error(' 3. 确认数据库服务正常运行');
|
| 2321 |
+
}
|
| 2322 |
+
|
| 2323 |
+
process.exit(1);
|
| 2324 |
+
}
|
| 2325 |
+
}
|
| 2326 |
+
|
| 2327 |
+
startServer();
|