Dridft commited on
Commit
d9e5dfd
·
verified ·
1 Parent(s): d25b3ea

Upload 8 files

Browse files
Files changed (8) hide show
  1. .dockerignore +18 -0
  2. Dockerfile +42 -0
  3. README.md +120 -6
  4. db-config.js +709 -0
  5. package-lock.json +29 -0
  6. package.json +30 -0
  7. r2-storage.js +270 -0
  8. 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: Campusloop
3
- emoji: 🐢
4
- colorFrom: indigo
5
- colorTo: green
6
  sdk: docker
7
  pinned: false
8
  license: mit
9
- short_description: campusloop-backend
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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();