cnmksjs commited on
Commit
24fd742
·
verified ·
1 Parent(s): 9ad3bc2

Upload 49 files

Browse files
.dockerignore ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Git相关
2
+ .git
3
+ .gitignore
4
+
5
+ # 依赖目录
6
+ node_modules
7
+ */node_modules
8
+ npm-debug.log*
9
+ yarn-debug.log*
10
+ yarn-error.log*
11
+
12
+ # 构建输出
13
+ dist
14
+ build
15
+ */dist
16
+ */build
17
+
18
+ # 环境变量文件
19
+ .env
20
+ .env.local
21
+ .env.development.local
22
+ .env.test.local
23
+ .env.production.local
24
+ */.env
25
+
26
+ # 日志文件
27
+ logs
28
+ *.log
29
+
30
+ # 运行时数据
31
+ pids
32
+ *.pid
33
+ *.seed
34
+ *.pid.lock
35
+
36
+ # 覆盖率目录
37
+ coverage
38
+ .nyc_output
39
+
40
+ # IDE和编辑器
41
+ .vscode
42
+ .idea
43
+ *.swp
44
+ *.swo
45
+ *~
46
+
47
+ # 操作系统
48
+ .DS_Store
49
+ Thumbs.db
50
+
51
+ # Docker相关
52
+ Dockerfile*
53
+ docker-compose*.yml
54
+ .dockerignore
55
+
56
+ # 脚本文件(不需要在容器中)
57
+ *.sh
58
+ *.bat
59
+ Makefile
60
+
61
+ # 文档
62
+ README.md
63
+ DEPLOYMENT.md
64
+ *.md
65
+
66
+ # 备份文件
67
+ backups/
68
+ *.backup
69
+ *.bak
70
+
71
+ # 临时文件
72
+ tmp/
73
+ temp/
74
+ .tmp/
75
+
76
+ # 测试文件
77
+ test/
78
+ tests/
79
+ __tests__/
80
+ *.test.js
81
+ *.test.ts
82
+ *.spec.js
83
+ *.spec.ts
.gitignore ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 依赖
2
+ node_modules/
3
+ npm-debug.log*
4
+ yarn-debug.log*
5
+ yarn-error.log*
6
+
7
+ # 环境变量
8
+ .env
9
+ .env.local
10
+ .env.development.local
11
+ .env.test.local
12
+ .env.production.local
13
+
14
+ # 构建输出
15
+ dist/
16
+ build/
17
+
18
+ # 日志
19
+ logs
20
+ *.log
21
+
22
+ # 运行时数据
23
+ pids
24
+ *.pid
25
+ *.seed
26
+ *.pid.lock
27
+
28
+ # 覆盖率目录
29
+ coverage/
30
+ .nyc_output
31
+
32
+ # IDE
33
+ .vscode/
34
+ .idea/
35
+ *.swp
36
+ *.swo
37
+
38
+ # 操作系统
39
+ .DS_Store
40
+ Thumbs.db
41
+
42
+ # Docker
43
+ .dockerignore
44
+
45
+ # MongoDB数据
46
+ data/
DEPLOYMENT.md ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚀 Linux部署指南
2
+
3
+ 这是一个完整的聊天应用Linux部署指南,包含所有必要的脚本和配置文件。
4
+
5
+ ## 📋 部署清单
6
+
7
+ ### 🔧 必需软件
8
+ - [ ] Docker Engine
9
+ - [ ] Docker Compose
10
+ - [ ] Git (可选,用于版本控制)
11
+ - [ ] Nginx (用于反向代理,可选)
12
+ - [ ] Certbot (用于SSL证书,可选)
13
+
14
+ ### 📁 文件清单
15
+ - [ ] `docker-compose.yml` - 生产环境配置
16
+ - [ ] `docker-compose.dev.yml` - 开发环境配置
17
+ - [ ] `Dockerfile` (client & server) - 容器构建文件
18
+ - [ ] `start.sh` - Linux启动脚本
19
+ - [ ] `stop.sh` - Linux停止脚本
20
+ - [ ] `deploy.sh` - 自动部署脚本
21
+ - [ ] `monitor.sh` - 监控脚本
22
+ - [ ] `test.sh` - 功能测试脚本
23
+ - [ ] `Makefile` - 管理命令
24
+
25
+ ## 🚀 快速部署
26
+
27
+ ### 方法1: 一键部署(推荐)
28
+ ```bash
29
+ # 下载项目
30
+ git clone <your-repo-url>
31
+ cd chat-app
32
+
33
+ # 设置权限
34
+ chmod +x *.sh
35
+
36
+ # 一键部署
37
+ sudo ./deploy.sh
38
+ ```
39
+
40
+ ### 方法2: 手动部署
41
+ ```bash
42
+ # 1. 安装Docker
43
+ curl -fsSL https://get.docker.com -o get-docker.sh
44
+ sudo sh get-docker.sh
45
+
46
+ # 2. 启动应用
47
+ ./start.sh
48
+
49
+ # 3. 验证部署
50
+ ./test.sh
51
+ ```
52
+
53
+ ### 方法3: 使用Makefile
54
+ ```bash
55
+ make install # 设置权限
56
+ make start # 启动应用
57
+ make health # 检查状态
58
+ ```
59
+
60
+ ## 🔒 SSL配置(生产环境推荐)
61
+
62
+ ```bash
63
+ # 自动配置SSL和nginx反向代理
64
+ sudo ./setup-ssl.sh
65
+ ```
66
+
67
+ ## 📊 监控和维护
68
+
69
+ ```bash
70
+ # 查看实时监控
71
+ ./monitor.sh
72
+
73
+ # 自动刷新监控(每30秒)
74
+ watch -n 30 ./monitor.sh
75
+
76
+ # 查看日志
77
+ make logs
78
+
79
+ # 备份数据库
80
+ make backup
81
+
82
+ # 测试功能
83
+ ./test.sh
84
+ ```
85
+
86
+ ## 🔧 故障排除
87
+
88
+ ### 常见问题
89
+
90
+ 1. **端口被占用**
91
+ ```bash
92
+ # 检查端口使用情况
93
+ sudo netstat -tulpn | grep :3000
94
+ sudo netstat -tulpn | grep :5000
95
+ sudo netstat -tulpn | grep :27017
96
+ ```
97
+
98
+ 2. **Docker权限问题**
99
+ ```bash
100
+ # 将用户添加到docker组
101
+ sudo usermod -aG docker $USER
102
+ # 重新登录或执行
103
+ newgrp docker
104
+ ```
105
+
106
+ 3. **服务启动失败**
107
+ ```bash
108
+ # 查看详细日志
109
+ docker-compose logs -f
110
+
111
+ # 检查容器状态
112
+ docker-compose ps
113
+ ```
114
+
115
+ 4. **内存不足**
116
+ ```bash
117
+ # 检查系统资源
118
+ free -h
119
+ df -h
120
+
121
+ # 清理Docker资源
122
+ make clean
123
+ ```
124
+
125
+ ## 🔐 安全配置
126
+
127
+ ### 防火墙设置
128
+ ```bash
129
+ # Ubuntu/Debian
130
+ sudo ufw allow 22 # SSH
131
+ sudo ufw allow 80 # HTTP
132
+ sudo ufw allow 443 # HTTPS
133
+ sudo ufw enable
134
+
135
+ # CentOS/RHEL
136
+ sudo firewall-cmd --permanent --add-service=ssh
137
+ sudo firewall-cmd --permanent --add-service=http
138
+ sudo firewall-cmd --permanent --add-service=https
139
+ sudo firewall-cmd --reload
140
+ ```
141
+
142
+ ### 环境变量安全
143
+ 1. 修改 `server/.env` 中的JWT密钥
144
+ 2. 更改MongoDB默认密码
145
+ 3. 设置强密码策略
146
+
147
+ ### 定期维护
148
+ ```bash
149
+ # 每日备份(添加到crontab)
150
+ 0 2 * * * /path/to/chat-app/make backup
151
+
152
+ # 每周更新
153
+ 0 3 * * 0 /path/to/chat-app/make update
154
+
155
+ # 监控磁盘空间
156
+ 0 */6 * * * df -h | mail -s "Disk Usage Report" [email protected]
157
+ ```
158
+
159
+ ## 📈 性能优化
160
+
161
+ ### 系统级优化
162
+ ```bash
163
+ # 增加文件描述符限制
164
+ echo "* soft nofile 65536" >> /etc/security/limits.conf
165
+ echo "* hard nofile 65536" >> /etc/security/limits.conf
166
+
167
+ # 优化网络参数
168
+ echo "net.core.somaxconn = 65536" >> /etc/sysctl.conf
169
+ sysctl -p
170
+ ```
171
+
172
+ ### Docker优化
173
+ ```bash
174
+ # 限制日志大小
175
+ echo '{
176
+ "log-driver": "json-file",
177
+ "log-opts": {
178
+ "max-size": "10m",
179
+ "max-file": "3"
180
+ }
181
+ }' > /etc/docker/daemon.json
182
+
183
+ systemctl restart docker
184
+ ```
185
+
186
+ ## 🆘 紧急恢复
187
+
188
+ ### 快速恢复步骤
189
+ 1. 停止所有服务: `make stop`
190
+ 2. 恢复数据库备份: `make restore FILE=backup.gz`
191
+ 3. 重启服务: `make start`
192
+ 4. 验证功能: `./test.sh`
193
+
194
+ ### 数据备份策略
195
+ - 每日自动备份数据库
196
+ - 保留最近30天的备份
197
+ - 异地备份重要数据
198
+
199
+ ## 📞 支持联系
200
+
201
+ 如果遇到问题,请:
202
+ 1. 查看日志: `make logs`
203
+ 2. 运行测试: `./test.sh`
204
+ 3. 检查监控: `./monitor.sh`
205
+ 4. 提交Issue到项目仓库
DOCKER.md ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🐳 Docker部署完整指南
2
+
3
+ 本项目提供了多种Docker部署方案,适应不同的使用场景和需求。
4
+
5
+ ## 📦 Dockerfile文件说明
6
+
7
+ ### 主要Dockerfile文件
8
+
9
+ | 文件 | 用途 | 特点 |
10
+ |------|------|------|
11
+ | `Dockerfile` | 单容器部署 | 前端+后端在一个容器中,简化部署 |
12
+ | `Dockerfile.optimized` | 优化单容器 | 多阶段构建,镜像更小,性能更好 |
13
+ | `client/Dockerfile` | 前端容器 | React应用 + Nginx,支持多容器架构 |
14
+ | `server/Dockerfile` | 后端容器 | Node.js应用,支持多容器架构 |
15
+
16
+ ### Docker Compose配置
17
+
18
+ | 文件 | 用途 | 特点 |
19
+ |------|------|------|
20
+ | `docker-compose.yml` | 生产环境 | 多容器,包含健康检查和依赖管理 |
21
+ | `docker-compose.dev.yml` | 开发环境 | 支持热重载,便于开发调试 |
22
+ | `docker-compose.single.yml` | 单容器部署 | 简化的单容器 + MongoDB |
23
+
24
+ ## 🚀 部署方案选择
25
+
26
+ ### 方案1: 多容器部署(推荐生产环境)
27
+
28
+ **优点:**
29
+ - 服务分离,便于扩展
30
+ - 独立更新前端或后端
31
+ - 更好的资源管理
32
+ - 符合微服务架构
33
+
34
+ **部署命令:**
35
+ ```bash
36
+ # 自动部署
37
+ ./deploy.sh
38
+
39
+ # 手动部署
40
+ docker-compose up --build -d
41
+
42
+ # 使用Makefile
43
+ make start
44
+ ```
45
+
46
+ ### 方案2: 单容器部署(推荐小型部署)
47
+
48
+ **优点:**
49
+ - 部署简单
50
+ - 资源占用少
51
+ - 管理方便
52
+ - 适合小型应用
53
+
54
+ **部署命令:**
55
+ ```bash
56
+ # 使用标准版本
57
+ docker-compose -f docker-compose.single.yml up -d
58
+
59
+ # 使用优化版本
60
+ docker build -t chatapp:optimized -f Dockerfile.optimized .
61
+ docker run -d -p 80:80 --name chatapp chatapp:optimized
62
+
63
+ # 使用Makefile
64
+ make single
65
+ ```
66
+
67
+ ### 方案3: 开发环境
68
+
69
+ **特点:**
70
+ - 支持热重载
71
+ - 实时代码同步
72
+ - 便于调试
73
+ - 快速迭代
74
+
75
+ **部署命令:**
76
+ ```bash
77
+ ./start-dev.sh
78
+ # 或
79
+ make dev
80
+ ```
81
+
82
+ ## 🔨 构建Docker镜像
83
+
84
+ ### 交互式构建
85
+ ```bash
86
+ # 使用构建脚本(推荐)
87
+ ./build-docker.sh
88
+
89
+ # 选择构建类型:
90
+ # 1) 多容器构建
91
+ # 2) 单容器构建
92
+ # 3) 仅构建前端
93
+ # 4) 仅构建后端
94
+ ```
95
+
96
+ ### 手动构建
97
+ ```bash
98
+ # 构建所有镜像
99
+ make build
100
+
101
+ # 构建单个镜像
102
+ docker build -t chatapp-frontend ./client
103
+ docker build -t chatapp-backend ./server
104
+ docker build -t chatapp .
105
+
106
+ # 构建优化版本
107
+ docker build -t chatapp:optimized -f Dockerfile.optimized .
108
+ ```
109
+
110
+ ### 镜像比较
111
+ ```bash
112
+ # 比较不同版本的镜像大小和性能
113
+ ./docker-compare.sh
114
+ ```
115
+
116
+ ## 📊 镜像优化特性
117
+
118
+ ### 多阶段构建
119
+ - 分离构建环境和运行环境
120
+ - 减少最终镜像大小
121
+ - 提高安全性
122
+
123
+ ### 安全特性
124
+ - 非root用户运行
125
+ - 最小权限原则
126
+ - 安全的基础镜像
127
+ - 定期安全更新
128
+
129
+ ### 性能优化
130
+ - Alpine Linux基础镜像
131
+ - 优化的nginx配置
132
+ - Gzip压缩
133
+ - 静态资源缓存
134
+ - 健康检查
135
+
136
+ ## 🔧 配置说明
137
+
138
+ ### 环境变量
139
+
140
+ **后端环境变量:**
141
+ ```env
142
+ NODE_ENV=production
143
+ MONGODB_URI=mongodb://mongo:27017/chatapp
144
+ JWT_SECRET=your-secret-key
145
+ PORT=5000
146
+ CLIENT_URL=http://localhost:3000
147
+ ```
148
+
149
+ **前端环境变量:**
150
+ ```env
151
+ VITE_API_URL=http://localhost:5000
152
+ ```
153
+
154
+ ### 端口映射
155
+
156
+ | 服务 | 容器端口 | 主机端口 | 说明 |
157
+ |------|----------|----------|------|
158
+ | 前端 | 80 | 3000 | React应用 |
159
+ | 后端 | 5000 | 5000 | API服务 |
160
+ | MongoDB | 27017 | 27017 | 数据库 |
161
+
162
+ ### 数据卷
163
+
164
+ | 卷名 | 挂载点 | 用途 |
165
+ |------|--------|------|
166
+ | `mongo_data` | `/data/db` | MongoDB数据持久化 |
167
+ | `./server` | `/app` | 开发环境代码同步 |
168
+ | `./client` | `/app` | 开发环境代码同步 |
169
+
170
+ ## 🧪 测试和验证
171
+
172
+ ### 功能测试
173
+ ```bash
174
+ # 运行完整测试套件
175
+ ./test.sh
176
+
177
+ # 使用Makefile
178
+ make test
179
+ ```
180
+
181
+ ### 健康检查
182
+ ```bash
183
+ # 检查服务状态
184
+ make health
185
+
186
+ # 查看容器状态
187
+ docker-compose ps
188
+
189
+ # 查看健康检查日志
190
+ docker inspect <container_name> | jq '.[0].State.Health'
191
+ ```
192
+
193
+ ### 性能测试
194
+ ```bash
195
+ # 监控资源使用
196
+ docker stats
197
+
198
+ # 查看镜像大小
199
+ docker images | grep chatapp
200
+
201
+ # 网络性能测试
202
+ curl -w "@curl-format.txt" -o /dev/null -s http://localhost:3000
203
+ ```
204
+
205
+ ## 🔍 故障排除
206
+
207
+ ### 常见问题
208
+
209
+ **1. 容器启动失败**
210
+ ```bash
211
+ # 查看日志
212
+ docker-compose logs -f
213
+
214
+ # 检查容器状态
215
+ docker-compose ps
216
+
217
+ # 进入容器调试
218
+ docker exec -it <container_name> sh
219
+ ```
220
+
221
+ **2. 端口冲突**
222
+ ```bash
223
+ # 检查端口占用
224
+ netstat -tulpn | grep :3000
225
+ netstat -tulpn | grep :5000
226
+
227
+ # 修改端口映射
228
+ # 编辑 docker-compose.yml 中的 ports 配置
229
+ ```
230
+
231
+ **3. 镜像构建失败**
232
+ ```bash
233
+ # 清理构建缓存
234
+ docker builder prune -f
235
+
236
+ # 重新构建
237
+ docker-compose build --no-cache
238
+
239
+ # 查看构建日志
240
+ docker build --progress=plain .
241
+ ```
242
+
243
+ **4. 数据持久化问题**
244
+ ```bash
245
+ # 检查数据卷
246
+ docker volume ls
247
+
248
+ # 备份数据
249
+ make backup
250
+
251
+ # 恢复数据
252
+ make restore FILE=backup.gz
253
+ ```
254
+
255
+ ## 🚀 生产环境最佳实践
256
+
257
+ ### 1. 使用优化镜像
258
+ ```bash
259
+ # 使用多阶段构建的优化版本
260
+ docker build -t chatapp:optimized -f Dockerfile.optimized .
261
+ ```
262
+
263
+ ### 2. 配置资源限制
264
+ ```yaml
265
+ # 在docker-compose.yml中添加
266
+ services:
267
+ server:
268
+ deploy:
269
+ resources:
270
+ limits:
271
+ cpus: '0.5'
272
+ memory: 512M
273
+ reservations:
274
+ cpus: '0.25'
275
+ memory: 256M
276
+ ```
277
+
278
+ ### 3. 设置重启策略
279
+ ```yaml
280
+ services:
281
+ server:
282
+ restart: unless-stopped
283
+ ```
284
+
285
+ ### 4. 配置日志管理
286
+ ```yaml
287
+ services:
288
+ server:
289
+ logging:
290
+ driver: "json-file"
291
+ options:
292
+ max-size: "10m"
293
+ max-file: "3"
294
+ ```
295
+
296
+ ### 5. 使用健康检查
297
+ ```yaml
298
+ services:
299
+ server:
300
+ healthcheck:
301
+ test: ["CMD", "wget", "--spider", "http://localhost:5000/api/health"]
302
+ interval: 30s
303
+ timeout: 10s
304
+ retries: 3
305
+ ```
306
+
307
+ ## 📈 监控和维护
308
+
309
+ ### 日志管理
310
+ ```bash
311
+ # 查看日志
312
+ make logs
313
+
314
+ # 实时日志
315
+ docker-compose logs -f
316
+
317
+ # 特定服务日志
318
+ docker-compose logs -f server
319
+ ```
320
+
321
+ ### 备份策略
322
+ ```bash
323
+ # 自动备份脚本
324
+ make backup
325
+
326
+ # 定时备份(crontab)
327
+ 0 2 * * * /path/to/chatapp/make backup
328
+ ```
329
+
330
+ ### 更新部署
331
+ ```bash
332
+ # 更新应用
333
+ make update
334
+
335
+ # 滚动更新
336
+ docker-compose up -d --no-deps server
337
+ ```
338
+
339
+ 这个Docker部署指南提供了完整的容器化解决方案,适合各种部署场景和需求。
Dockerfile ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 多阶段构建 - 完整聊天应用Dockerfile
2
+ # 这个Dockerfile将前端和后端打包到一个容器中(可选方案)
3
+
4
+ # 阶段1: 构建前端
5
+ FROM node:18-alpine as frontend-build
6
+
7
+ WORKDIR /app/client
8
+
9
+ # 复制前端package.json
10
+ COPY client/package*.json ./
11
+
12
+ # 安装前端依赖
13
+ RUN npm ci
14
+
15
+ # 复制前端源代码
16
+ COPY client/ ./
17
+
18
+ # 构建前端
19
+ RUN npm run build
20
+
21
+ # 阶段2: 构建后端
22
+ FROM node:18-alpine as backend-build
23
+
24
+ WORKDIR /app/server
25
+
26
+ # 复制后端package.json
27
+ COPY server/package*.json ./
28
+
29
+ # 安装后端依赖
30
+ RUN npm ci --only=production
31
+
32
+ # 复制后端源代码
33
+ COPY server/ ./
34
+
35
+ # 阶段3: 生产环境
36
+ FROM node:18-alpine
37
+
38
+ # 安装必要的系统依赖
39
+ RUN apk add --no-cache \
40
+ nginx \
41
+ wget \
42
+ curl \
43
+ supervisor \
44
+ && rm -rf /var/cache/apk/*
45
+
46
+ # 创建应用用户
47
+ RUN addgroup -g 1001 -S appuser && \
48
+ adduser -S appuser -u 1001
49
+
50
+ # 设置工作目录
51
+ WORKDIR /app
52
+
53
+ # 复制后端文件
54
+ COPY --from=backend-build --chown=appuser:appuser /app/server ./server
55
+
56
+ # 复制前端构建文件
57
+ COPY --from=frontend-build --chown=appuser:appuser /app/client/dist ./client/dist
58
+
59
+ # 创建nginx配置
60
+ RUN mkdir -p /etc/nginx/conf.d
61
+ COPY --chown=appuser:appuser client/nginx.conf /etc/nginx/conf.d/default.conf
62
+
63
+ # 创建supervisor配置
64
+ RUN mkdir -p /etc/supervisor/conf.d
65
+ COPY --chown=appuser:appuser <<EOF /etc/supervisor/conf.d/supervisord.conf
66
+ [supervisord]
67
+ nodaemon=true
68
+ user=root
69
+ logfile=/var/log/supervisor/supervisord.log
70
+ pidfile=/var/run/supervisord.pid
71
+
72
+ [program:nginx]
73
+ command=nginx -g "daemon off;"
74
+ autostart=true
75
+ autorestart=true
76
+ stderr_logfile=/var/log/nginx/error.log
77
+ stdout_logfile=/var/log/nginx/access.log
78
+ user=appuser
79
+
80
+ [program:node]
81
+ command=node server/index.js
82
+ directory=/app
83
+ autostart=true
84
+ autorestart=true
85
+ stderr_logfile=/var/log/node/error.log
86
+ stdout_logfile=/var/log/node/access.log
87
+ user=appuser
88
+ environment=NODE_ENV=production
89
+ EOF
90
+
91
+ # 创建日志目录
92
+ RUN mkdir -p /var/log/nginx /var/log/node /var/log/supervisor && \
93
+ chown -R appuser:appuser /var/log/nginx /var/log/node /var/log/supervisor
94
+
95
+ # 修改nginx配置以适应单容器部署
96
+ RUN sed -i 's/proxy_pass http:\/\/localhost:5000/proxy_pass http:\/\/127.0.0.1:5000/g' /etc/nginx/conf.d/default.conf
97
+
98
+ # 暴露端口
99
+ EXPOSE 80 5000
100
+
101
+ # 环境变量
102
+ ENV NODE_ENV=production
103
+ ENV PORT=5000
104
+ ENV MONGODB_URI=mongodb://mongo:27017/chatapp
105
+
106
+ # 健康检查
107
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
108
+ CMD wget --no-verbose --tries=1 --spider http://localhost:80 && \
109
+ wget --no-verbose --tries=1 --spider http://localhost:5000/api/health || exit 1
110
+
111
+ # 切换到应用用户
112
+ USER appuser
113
+
114
+ # 启动supervisor
115
+ CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
Dockerfile.optimized ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 优化版多阶段构建Dockerfile
2
+ # 这个版本针对生产环境进行了优化,镜像更小,安全性更高
3
+
4
+ # ================================
5
+ # 阶段1: 前端构建
6
+ # ================================
7
+ FROM node:18-alpine as frontend-builder
8
+
9
+ # 设置工作目录
10
+ WORKDIR /app
11
+
12
+ # 只复制package文件,利用Docker缓存
13
+ COPY client/package*.json ./
14
+
15
+ # 安装依赖
16
+ RUN npm ci --only=production && \
17
+ npm cache clean --force
18
+
19
+ # 复制源代码
20
+ COPY client/ ./
21
+
22
+ # 构建前端
23
+ RUN npm run build
24
+
25
+ # ================================
26
+ # 阶段2: 后端构建
27
+ # ================================
28
+ FROM node:18-alpine as backend-builder
29
+
30
+ WORKDIR /app
31
+
32
+ # 复制package文件
33
+ COPY server/package*.json ./
34
+
35
+ # 安装依赖
36
+ RUN npm ci --only=production && \
37
+ npm cache clean --force
38
+
39
+ # 复制源代码
40
+ COPY server/ ./
41
+
42
+ # ================================
43
+ # 阶段3: 最终生产镜像
44
+ # ================================
45
+ FROM node:18-alpine
46
+
47
+ # 安装运行时依赖
48
+ RUN apk add --no-cache \
49
+ nginx \
50
+ supervisor \
51
+ wget \
52
+ curl \
53
+ dumb-init \
54
+ && rm -rf /var/cache/apk/*
55
+
56
+ # 创建应用用户
57
+ RUN addgroup -g 1001 -S appuser && \
58
+ adduser -S appuser -u 1001 -G appuser
59
+
60
+ # 创建必要的目录
61
+ RUN mkdir -p /app /var/log/nginx /var/log/supervisor /run/nginx && \
62
+ chown -R appuser:appuser /app /var/log/nginx /var/log/supervisor /run/nginx
63
+
64
+ # 设置工作目录
65
+ WORKDIR /app
66
+
67
+ # 复制后端文件
68
+ COPY --from=backend-builder --chown=appuser:appuser /app ./
69
+
70
+ # 复制前端构建文件
71
+ COPY --from=frontend-builder --chown=appuser:appuser /app/dist ./public
72
+
73
+ # 创建nginx配置
74
+ COPY --chown=appuser:appuser <<EOF /etc/nginx/nginx.conf
75
+ user appuser;
76
+ worker_processes auto;
77
+ pid /run/nginx/nginx.pid;
78
+
79
+ events {
80
+ worker_connections 1024;
81
+ use epoll;
82
+ multi_accept on;
83
+ }
84
+
85
+ http {
86
+ include /etc/nginx/mime.types;
87
+ default_type application/octet-stream;
88
+
89
+ # 日志格式
90
+ log_format main '\$remote_addr - \$remote_user [\$time_local] "\$request" '
91
+ '\$status \$body_bytes_sent "\$http_referer" '
92
+ '"\$http_user_agent" "\$http_x_forwarded_for"';
93
+
94
+ access_log /var/log/nginx/access.log main;
95
+ error_log /var/log/nginx/error.log warn;
96
+
97
+ # 性能优化
98
+ sendfile on;
99
+ tcp_nopush on;
100
+ tcp_nodelay on;
101
+ keepalive_timeout 65;
102
+ types_hash_max_size 2048;
103
+
104
+ # Gzip压缩
105
+ gzip on;
106
+ gzip_vary on;
107
+ gzip_min_length 1024;
108
+ gzip_comp_level 6;
109
+ gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
110
+
111
+ server {
112
+ listen 80;
113
+ server_name localhost;
114
+ root /app/public;
115
+ index index.html;
116
+
117
+ # 前端路由
118
+ location / {
119
+ try_files \$uri \$uri/ /index.html;
120
+ }
121
+
122
+ # API代理
123
+ location /api/ {
124
+ proxy_pass http://127.0.0.1:5000;
125
+ proxy_http_version 1.1;
126
+ proxy_set_header Upgrade \$http_upgrade;
127
+ proxy_set_header Connection 'upgrade';
128
+ proxy_set_header Host \$host;
129
+ proxy_set_header X-Real-IP \$remote_addr;
130
+ proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
131
+ proxy_set_header X-Forwarded-Proto \$scheme;
132
+ proxy_cache_bypass \$http_upgrade;
133
+ }
134
+
135
+ # Socket.IO代理
136
+ location /socket.io/ {
137
+ proxy_pass http://127.0.0.1:5000;
138
+ proxy_http_version 1.1;
139
+ proxy_set_header Upgrade \$http_upgrade;
140
+ proxy_set_header Connection "upgrade";
141
+ proxy_set_header Host \$host;
142
+ proxy_set_header X-Real-IP \$remote_addr;
143
+ proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
144
+ proxy_set_header X-Forwarded-Proto \$scheme;
145
+ }
146
+
147
+ # 静态资源缓存
148
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)\$ {
149
+ expires 1y;
150
+ add_header Cache-Control "public, immutable";
151
+ }
152
+ }
153
+ }
154
+ EOF
155
+
156
+ # 创建supervisor配置
157
+ COPY --chown=appuser:appuser <<EOF /etc/supervisor/conf.d/supervisord.conf
158
+ [supervisord]
159
+ nodaemon=true
160
+ user=appuser
161
+ logfile=/var/log/supervisor/supervisord.log
162
+ pidfile=/run/supervisord.pid
163
+ childlogdir=/var/log/supervisor
164
+
165
+ [program:nginx]
166
+ command=nginx -g "daemon off;"
167
+ autostart=true
168
+ autorestart=true
169
+ stderr_logfile=/var/log/supervisor/nginx_error.log
170
+ stdout_logfile=/var/log/supervisor/nginx_access.log
171
+ user=appuser
172
+
173
+ [program:node]
174
+ command=node index.js
175
+ directory=/app
176
+ autostart=true
177
+ autorestart=true
178
+ stderr_logfile=/var/log/supervisor/node_error.log
179
+ stdout_logfile=/var/log/supervisor/node_access.log
180
+ user=appuser
181
+ environment=NODE_ENV=production,PORT=5000
182
+ EOF
183
+
184
+ # 设置权限
185
+ RUN chown -R appuser:appuser /etc/nginx /etc/supervisor
186
+
187
+ # 切换到应用用户
188
+ USER appuser
189
+
190
+ # 暴露端口
191
+ EXPOSE 80
192
+
193
+ # 环境变量
194
+ ENV NODE_ENV=production
195
+ ENV PORT=5000
196
+
197
+ # 健康检查
198
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
199
+ CMD wget --no-verbose --tries=1 --spider http://localhost:80/api/health || exit 1
200
+
201
+ # 使用dumb-init作为PID 1
202
+ ENTRYPOINT ["/usr/bin/dumb-init", "--"]
203
+
204
+ # 启动supervisor
205
+ CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
Makefile ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 聊天应用 Makefile
2
+
3
+ .PHONY: help install start stop restart logs clean dev deploy health backup restore build single test
4
+
5
+ # 默认目标
6
+ help:
7
+ @echo "聊天应用管理命令:"
8
+ @echo ""
9
+ @echo "🚀 部署命令:"
10
+ @echo " make install - 安装依赖和设置权限"
11
+ @echo " make start - 启动生产环境(多容器)"
12
+ @echo " make single - 启动单容器版本"
13
+ @echo " make dev - 启动开发环境"
14
+ @echo " make deploy - 部署到生产环境"
15
+ @echo ""
16
+ @echo "🔧 管理命令:"
17
+ @echo " make stop - 停止所有服务"
18
+ @echo " make restart - 重启所有服务"
19
+ @echo " make logs - 查看服务日志"
20
+ @echo " make health - 检查服务健康状态"
21
+ @echo " make test - 运行功能测试"
22
+ @echo ""
23
+ @echo "🐳 Docker命令:"
24
+ @echo " make build - 构建Docker镜像"
25
+ @echo " make clean - 清理Docker资源"
26
+ @echo ""
27
+ @echo "💾 数据命令:"
28
+ @echo " make backup - 备份数据库"
29
+ @echo " make restore - 恢复数据库"
30
+ @echo ""
31
+
32
+ # 安装和设置
33
+ install:
34
+ @echo "🔧 设置脚本权限..."
35
+ chmod +x *.sh
36
+ @echo "✅ 安装完成"
37
+
38
+ # 启动生产环境
39
+ start:
40
+ @echo "🚀 启动生产环境..."
41
+ ./start.sh
42
+
43
+ # 启动开发环境
44
+ dev:
45
+ @echo "🚀 启动开发环境..."
46
+ ./start-dev.sh
47
+
48
+ # 启动单容器版本
49
+ single:
50
+ @echo "🚀 启动单容器版本..."
51
+ @if command -v docker-compose >/dev/null 2>&1; then \
52
+ docker-compose -f docker-compose.single.yml up --build -d; \
53
+ else \
54
+ docker compose -f docker-compose.single.yml up --build -d; \
55
+ fi
56
+ @echo "✅ 单容器版本启动完成"
57
+ @echo "🌐 访问地址: http://localhost"
58
+
59
+ # 构建Docker镜像
60
+ build:
61
+ @echo "🐳 构建Docker镜像..."
62
+ @if command -v docker-compose >/dev/null 2>&1; then \
63
+ docker-compose build --no-cache; \
64
+ else \
65
+ docker compose build --no-cache; \
66
+ fi
67
+ @echo "✅ 镜像构建完成"
68
+
69
+ # 运行测试
70
+ test:
71
+ @echo "🧪 运行功能测试..."
72
+ ./test.sh
73
+
74
+ # 停止服务
75
+ stop:
76
+ @echo "🛑 停止服务..."
77
+ ./stop.sh
78
+
79
+ # 重启服务
80
+ restart: stop start
81
+
82
+ # 查看日志
83
+ logs:
84
+ @if command -v docker-compose >/dev/null 2>&1; then \
85
+ docker-compose logs -f; \
86
+ else \
87
+ docker compose logs -f; \
88
+ fi
89
+
90
+ # 检查健康状态
91
+ health:
92
+ @echo "🔍 检查服务状态..."
93
+ @if command -v docker-compose >/dev/null 2>&1; then \
94
+ docker-compose ps; \
95
+ else \
96
+ docker compose ps; \
97
+ fi
98
+ @echo ""
99
+ @echo "🧪 测试服务连接..."
100
+ @curl -f http://localhost:5000/api/health 2>/dev/null && echo "✅ 后端服务正常" || echo "❌ 后端服务异常"
101
+ @curl -f http://localhost:3000 2>/dev/null && echo "✅ 前端服务正常" || echo "❌ 前端服务异常"
102
+
103
+ # 清理Docker资源
104
+ clean:
105
+ @echo "🧹 清理Docker资源..."
106
+ @if command -v docker-compose >/dev/null 2>&1; then \
107
+ docker-compose down -v --rmi all --remove-orphans; \
108
+ else \
109
+ docker compose down -v --rmi all --remove-orphans; \
110
+ fi
111
+ docker system prune -f
112
+ @echo "✅ 清理完成"
113
+
114
+ # 部署到生产环境
115
+ deploy:
116
+ @echo "🚀 部署到生产环境..."
117
+ ./deploy.sh
118
+
119
+ # 备份数据库
120
+ backup:
121
+ @echo "💾 备份数据库..."
122
+ @mkdir -p backups
123
+ @BACKUP_FILE="backups/chatapp-backup-$$(date +%Y%m%d-%H%M%S).gz"; \
124
+ docker exec chat-mongo mongodump --authenticationDatabase admin -u admin -p password123 --db chatapp --gzip --archive=$$BACKUP_FILE; \
125
+ echo "✅ 数据库已备份到: $$BACKUP_FILE"
126
+
127
+ # 恢复数据库
128
+ restore:
129
+ @echo "📥 恢复数据库..."
130
+ @if [ -z "$(FILE)" ]; then \
131
+ echo "❌ 请指定备份文件: make restore FILE=backups/backup-file.gz"; \
132
+ exit 1; \
133
+ fi
134
+ @if [ ! -f "$(FILE)" ]; then \
135
+ echo "❌ 备份文件不存在: $(FILE)"; \
136
+ exit 1; \
137
+ fi
138
+ docker exec chat-mongo mongorestore --authenticationDatabase admin -u admin -p password123 --db chatapp --gzip --archive=$(FILE) --drop
139
+ @echo "✅ 数据库恢复完成"
140
+
141
+ # 更新应用
142
+ update:
143
+ @echo "🔄 更新应用..."
144
+ git pull
145
+ @if command -v docker-compose >/dev/null 2>&1; then \
146
+ docker-compose pull; \
147
+ docker-compose up --build -d; \
148
+ else \
149
+ docker compose pull; \
150
+ docker compose up --build -d; \
151
+ fi
152
+ @echo "✅ 更新完成"
build-docker.sh ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Docker镜像构建脚本
4
+
5
+ set -e
6
+
7
+ echo "🐳 Docker镜像构建脚本"
8
+ echo "===================="
9
+ echo
10
+
11
+ # 检查Docker是否安装
12
+ if ! command -v docker &> /dev/null; then
13
+ echo "❌ 错误: Docker未安装"
14
+ echo "请先安装Docker: https://docs.docker.com/engine/install/"
15
+ exit 1
16
+ fi
17
+
18
+ # 检查Docker服务是否运行
19
+ if ! docker info &> /dev/null; then
20
+ echo "❌ 错误: Docker服务未运行"
21
+ echo "请启动Docker服务: sudo systemctl start docker"
22
+ exit 1
23
+ fi
24
+
25
+ echo "✅ Docker环境检查通过"
26
+ echo
27
+
28
+ # 设置镜像标签
29
+ IMAGE_TAG=${1:-latest}
30
+ REGISTRY=${2:-""}
31
+
32
+ if [ -n "$REGISTRY" ]; then
33
+ FULL_TAG="$REGISTRY/chatapp:$IMAGE_TAG"
34
+ else
35
+ FULL_TAG="chatapp:$IMAGE_TAG"
36
+ fi
37
+
38
+ echo "📦 构建配置:"
39
+ echo " 镜像标签: $FULL_TAG"
40
+ echo " 构建上下文: $(pwd)"
41
+ echo
42
+
43
+ # 选择构建方式
44
+ echo "请选择构建方式:"
45
+ echo "1) 多容器构建 (推荐)"
46
+ echo "2) 单容器构建"
47
+ echo "3) 仅构建前端"
48
+ echo "4) 仅构建后端"
49
+ echo
50
+
51
+ read -p "请输入选择 (1-4): " choice
52
+
53
+ case $choice in
54
+ 1)
55
+ echo "🔨 构建多容器版本..."
56
+
57
+ # 构建前端镜像
58
+ echo "📦 构建前端镜像..."
59
+ docker build -t "${FULL_TAG}-frontend" ./client
60
+
61
+ # 构建后端镜像
62
+ echo "📦 构建后端镜像..."
63
+ docker build -t "${FULL_TAG}-backend" ./server
64
+
65
+ echo "✅ 多容器镜像构建完成"
66
+ echo " 前端镜像: ${FULL_TAG}-frontend"
67
+ echo " 后端镜像: ${FULL_TAG}-backend"
68
+ ;;
69
+
70
+ 2)
71
+ echo "🔨 构建单容器版本..."
72
+ docker build -t "$FULL_TAG" .
73
+ echo "✅ 单容器镜像构建完成: $FULL_TAG"
74
+ ;;
75
+
76
+ 3)
77
+ echo "🔨 构建前端镜像..."
78
+ docker build -t "${FULL_TAG}-frontend" ./client
79
+ echo "✅ 前端镜像构建完成: ${FULL_TAG}-frontend"
80
+ ;;
81
+
82
+ 4)
83
+ echo "🔨 构建后端镜像..."
84
+ docker build -t "${FULL_TAG}-backend" ./server
85
+ echo "✅ 后端镜像构建完成: ${FULL_TAG}-backend"
86
+ ;;
87
+
88
+ *)
89
+ echo "❌ 无效选择"
90
+ exit 1
91
+ ;;
92
+ esac
93
+
94
+ echo
95
+
96
+ # 显示构建的镜像
97
+ echo "📋 构建的镜像:"
98
+ docker images | grep chatapp
99
+
100
+ echo
101
+
102
+ # 询问是否推送到仓库
103
+ if [ -n "$REGISTRY" ]; then
104
+ read -p "是否推送镜像到仓库? (y/N): " push_choice
105
+ if [[ $push_choice =~ ^[Yy]$ ]]; then
106
+ echo "📤 推送镜像到仓库..."
107
+
108
+ case $choice in
109
+ 1)
110
+ docker push "${FULL_TAG}-frontend"
111
+ docker push "${FULL_TAG}-backend"
112
+ ;;
113
+ 2)
114
+ docker push "$FULL_TAG"
115
+ ;;
116
+ 3)
117
+ docker push "${FULL_TAG}-frontend"
118
+ ;;
119
+ 4)
120
+ docker push "${FULL_TAG}-backend"
121
+ ;;
122
+ esac
123
+
124
+ echo "✅ 镜像推送完成"
125
+ fi
126
+ fi
127
+
128
+ # 询问是否运行测试
129
+ read -p "是否运行容器测试? (y/N): " test_choice
130
+ if [[ $test_choice =~ ^[Yy]$ ]]; then
131
+ echo "🧪 运行容器测试..."
132
+
133
+ case $choice in
134
+ 1)
135
+ echo "启动多容器测试环境..."
136
+ docker-compose up -d
137
+ sleep 30
138
+ ./test.sh
139
+ docker-compose down
140
+ ;;
141
+ 2)
142
+ echo "启动单容器测试环境..."
143
+ docker-compose -f docker-compose.single.yml up -d
144
+ sleep 30
145
+ ./test.sh
146
+ docker-compose -f docker-compose.single.yml down
147
+ ;;
148
+ 3|4)
149
+ echo "⚠️ 单独的前端或后端镜像需要完整环境才能测试"
150
+ ;;
151
+ esac
152
+ fi
153
+
154
+ echo
155
+ echo "🎉 构建完成!"
156
+ echo
157
+ echo "💡 使用提示:"
158
+ echo " - 查看镜像: docker images | grep chatapp"
159
+ echo " - 运行容器: docker run -p 3000:80 $FULL_TAG"
160
+ echo " - 清理镜像: docker rmi $FULL_TAG"
161
+ echo
162
+ echo "📚 更多命令:"
163
+ echo " - make build # 使用Makefile构建"
164
+ echo " - make start # 启动应用"
165
+ echo " - make test # 运行测试"
client/.dockerignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ npm-debug.log
3
+ .env
4
+ .git
5
+ .gitignore
6
+ README.md
7
+ Dockerfile
8
+ .dockerignore
9
+ dist
client/.env ADDED
@@ -0,0 +1 @@
 
 
1
+ VITE_API_URL=http://localhost:5000
client/Dockerfile ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 构建阶段
2
+ FROM node:18-alpine as build
3
+
4
+ # 安装构建依赖
5
+ RUN apk add --no-cache git
6
+
7
+ WORKDIR /app
8
+
9
+ # 创建非root用户
10
+ RUN addgroup -g 1001 -S nodejs && \
11
+ adduser -S nodejs -u 1001
12
+
13
+ # 复制package.json和package-lock.json
14
+ COPY --chown=nodejs:nodejs package*.json ./
15
+
16
+ # 切换到nodejs用户
17
+ USER nodejs
18
+
19
+ # 安装依赖
20
+ RUN npm ci && npm cache clean --force
21
+
22
+ # 复制源代码
23
+ COPY --chown=nodejs:nodejs . .
24
+
25
+ # 构建应用
26
+ RUN npm run build
27
+
28
+ # 生产阶段
29
+ FROM nginx:alpine
30
+
31
+ # 安装wget用于健康检查
32
+ RUN apk add --no-cache wget
33
+
34
+ # 创建nginx用户目录
35
+ RUN mkdir -p /var/cache/nginx && \
36
+ chown -R nginx:nginx /var/cache/nginx && \
37
+ chown -R nginx:nginx /var/log/nginx && \
38
+ chown -R nginx:nginx /etc/nginx/conf.d
39
+
40
+ # 复制构建的文件到nginx
41
+ COPY --from=build --chown=nginx:nginx /app/dist /usr/share/nginx/html
42
+
43
+ # 复制nginx配置
44
+ COPY --chown=nginx:nginx nginx.conf /etc/nginx/conf.d/default.conf
45
+
46
+ # 切换到nginx用户
47
+ USER nginx
48
+
49
+ # 暴露端口
50
+ EXPOSE 80
51
+
52
+ # 健康检查
53
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
54
+ CMD wget --no-verbose --tries=1 --spider http://localhost:80 || exit 1
55
+
56
+ # 启动nginx
57
+ CMD ["nginx", "-g", "daemon off;"]
client/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>完美聊天网站</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
client/nginx.conf ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ server {
2
+ listen 80;
3
+ server_name localhost;
4
+ root /usr/share/nginx/html;
5
+ index index.html;
6
+
7
+ # 处理React Router的路由
8
+ location / {
9
+ try_files $uri $uri/ /index.html;
10
+ }
11
+
12
+ # 静态资源缓存
13
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
14
+ expires 1y;
15
+ add_header Cache-Control "public, immutable";
16
+ }
17
+
18
+ # 安全头
19
+ add_header X-Frame-Options "SAMEORIGIN" always;
20
+ add_header X-Content-Type-Options "nosniff" always;
21
+ add_header X-XSS-Protection "1; mode=block" always;
22
+
23
+ # Gzip压缩
24
+ gzip on;
25
+ gzip_vary on;
26
+ gzip_min_length 1024;
27
+ gzip_types
28
+ text/plain
29
+ text/css
30
+ text/xml
31
+ text/javascript
32
+ application/javascript
33
+ application/xml+rss
34
+ application/json;
35
+ }
client/package.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chat-client",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "react": "^18.2.0",
14
+ "react-dom": "^18.2.0",
15
+ "socket.io-client": "^4.7.4",
16
+ "axios": "^1.6.2",
17
+ "react-router-dom": "^6.20.1",
18
+ "lucide-react": "^0.294.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/react": "^18.2.43",
22
+ "@types/react-dom": "^18.2.17",
23
+ "@typescript-eslint/eslint-plugin": "^6.14.0",
24
+ "@typescript-eslint/parser": "^6.14.0",
25
+ "@vitejs/plugin-react": "^4.2.1",
26
+ "autoprefixer": "^10.4.16",
27
+ "eslint": "^8.55.0",
28
+ "eslint-plugin-react-hooks": "^4.6.0",
29
+ "eslint-plugin-react-refresh": "^0.4.5",
30
+ "postcss": "^8.4.32",
31
+ "tailwindcss": "^3.3.6",
32
+ "typescript": "^5.2.2",
33
+ "vite": "^5.0.8"
34
+ }
35
+ }
client/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
client/public/vite.svg ADDED
client/src/App.tsx ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
3
+ import Login from './components/Login';
4
+ import Register from './components/Register';
5
+ import Chat from './components/Chat';
6
+ import { User } from './types';
7
+
8
+ function App() {
9
+ const [user, setUser] = useState<User | null>(null);
10
+ const [loading, setLoading] = useState(true);
11
+
12
+ useEffect(() => {
13
+ // 检查本地存储中的用户信息
14
+ const token = localStorage.getItem('token');
15
+ const userData = localStorage.getItem('user');
16
+
17
+ if (token && userData) {
18
+ try {
19
+ const parsedUser = JSON.parse(userData);
20
+ setUser(parsedUser);
21
+ } catch (error) {
22
+ console.error('解析用户数据失败:', error);
23
+ localStorage.removeItem('token');
24
+ localStorage.removeItem('user');
25
+ }
26
+ }
27
+
28
+ setLoading(false);
29
+ }, []);
30
+
31
+ const handleLogin = (userData: User, token: string) => {
32
+ setUser(userData);
33
+ localStorage.setItem('token', token);
34
+ localStorage.setItem('user', JSON.stringify(userData));
35
+ };
36
+
37
+ const handleLogout = () => {
38
+ setUser(null);
39
+ localStorage.removeItem('token');
40
+ localStorage.removeItem('user');
41
+ };
42
+
43
+ if (loading) {
44
+ return (
45
+ <div className="min-h-screen flex items-center justify-center bg-gray-50">
46
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
47
+ </div>
48
+ );
49
+ }
50
+
51
+ return (
52
+ <Router>
53
+ <div className="min-h-screen bg-gray-50">
54
+ <Routes>
55
+ <Route
56
+ path="/login"
57
+ element={
58
+ user ? (
59
+ <Navigate to="/chat" replace />
60
+ ) : (
61
+ <Login onLogin={handleLogin} />
62
+ )
63
+ }
64
+ />
65
+ <Route
66
+ path="/register"
67
+ element={
68
+ user ? (
69
+ <Navigate to="/chat" replace />
70
+ ) : (
71
+ <Register onRegister={handleLogin} />
72
+ )
73
+ }
74
+ />
75
+ <Route
76
+ path="/chat"
77
+ element={
78
+ user ? (
79
+ <Chat user={user} onLogout={handleLogout} />
80
+ ) : (
81
+ <Navigate to="/login" replace />
82
+ )
83
+ }
84
+ />
85
+ <Route
86
+ path="/"
87
+ element={
88
+ <Navigate to={user ? "/chat" : "/login"} replace />
89
+ }
90
+ />
91
+ </Routes>
92
+ </div>
93
+ </Router>
94
+ );
95
+ }
96
+
97
+ export default App;
client/src/components/Chat.tsx ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { Send, LogOut, Users, MessageCircle } from 'lucide-react';
3
+ import { User, Message, OnlineUser } from '../types';
4
+ import { messageAPI } from '../utils/api';
5
+ import { socketService } from '../utils/socket';
6
+
7
+ interface ChatProps {
8
+ user: User;
9
+ onLogout: () => void;
10
+ }
11
+
12
+ const Chat: React.FC<ChatProps> = ({ user, onLogout }) => {
13
+ const [messages, setMessages] = useState<Message[]>([]);
14
+ const [newMessage, setNewMessage] = useState('');
15
+ const [onlineUsers, setOnlineUsers] = useState<OnlineUser[]>([]);
16
+ const [loading, setLoading] = useState(true);
17
+ const messagesEndRef = useRef<HTMLDivElement>(null);
18
+
19
+ const scrollToBottom = () => {
20
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
21
+ };
22
+
23
+ useEffect(() => {
24
+ scrollToBottom();
25
+ }, [messages]);
26
+
27
+ useEffect(() => {
28
+ const initializeChat = async () => {
29
+ try {
30
+ // 获取历史消息
31
+ const historyMessages = await messageAPI.getMessages();
32
+ setMessages(historyMessages);
33
+
34
+ // 连接Socket
35
+ const token = localStorage.getItem('token');
36
+ if (token) {
37
+ const socket = socketService.connect(token);
38
+
39
+ // 监听新消息
40
+ socketService.onNewMessage((message: Message) => {
41
+ setMessages(prev => [...prev, message]);
42
+ });
43
+
44
+ // 监听用户上线
45
+ socketService.onUserJoined((userData) => {
46
+ console.log(`${userData.username} 加入了聊天室`);
47
+ });
48
+
49
+ // 监听用户下线
50
+ socketService.onUserLeft((userData) => {
51
+ console.log(`${userData.username} 离开了聊天室`);
52
+ });
53
+
54
+ // 监听在线用户列表
55
+ socketService.onOnlineUsers((users: OnlineUser[]) => {
56
+ setOnlineUsers(users);
57
+ });
58
+ }
59
+ } catch (error) {
60
+ console.error('初始化聊天失败:', error);
61
+ } finally {
62
+ setLoading(false);
63
+ }
64
+ };
65
+
66
+ initializeChat();
67
+
68
+ // 清理函数
69
+ return () => {
70
+ socketService.offAllListeners();
71
+ socketService.disconnect();
72
+ };
73
+ }, []);
74
+
75
+ const handleSendMessage = (e: React.FormEvent) => {
76
+ e.preventDefault();
77
+ if (newMessage.trim()) {
78
+ socketService.sendMessage(newMessage.trim());
79
+ setNewMessage('');
80
+ }
81
+ };
82
+
83
+ const handleLogout = () => {
84
+ socketService.disconnect();
85
+ onLogout();
86
+ };
87
+
88
+ const formatTime = (timestamp: Date) => {
89
+ return new Date(timestamp).toLocaleTimeString('zh-CN', {
90
+ hour: '2-digit',
91
+ minute: '2-digit',
92
+ });
93
+ };
94
+
95
+ if (loading) {
96
+ return (
97
+ <div className="min-h-screen flex items-center justify-center bg-gray-50">
98
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
99
+ </div>
100
+ );
101
+ }
102
+
103
+ return (
104
+ <div className="flex h-screen bg-gray-50">
105
+ {/* 侧边栏 */}
106
+ <div className="w-64 bg-white border-r border-gray-200 flex flex-col">
107
+ {/* 用户信息 */}
108
+ <div className="p-4 border-b border-gray-200">
109
+ <div className="flex items-center space-x-3">
110
+ <div className="w-10 h-10 bg-primary-600 rounded-full flex items-center justify-center">
111
+ <span className="text-white font-medium">
112
+ {user.username.charAt(0).toUpperCase()}
113
+ </span>
114
+ </div>
115
+ <div className="flex-1">
116
+ <h3 className="font-medium text-gray-900">{user.username}</h3>
117
+ <p className="text-sm text-gray-500">{user.email}</p>
118
+ </div>
119
+ <button
120
+ onClick={handleLogout}
121
+ className="p-2 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100"
122
+ title="退出登录"
123
+ >
124
+ <LogOut className="h-5 w-5" />
125
+ </button>
126
+ </div>
127
+ </div>
128
+
129
+ {/* 在线用户 */}
130
+ <div className="flex-1 p-4">
131
+ <div className="flex items-center space-x-2 mb-4">
132
+ <Users className="h-5 w-5 text-gray-500" />
133
+ <h4 className="font-medium text-gray-900">
134
+ 在线用户 ({onlineUsers.length})
135
+ </h4>
136
+ </div>
137
+ <div className="space-y-2">
138
+ {onlineUsers.map((onlineUser) => (
139
+ <div key={onlineUser.userId} className="flex items-center space-x-3">
140
+ <div className="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
141
+ <span className="text-white text-sm font-medium">
142
+ {onlineUser.username.charAt(0).toUpperCase()}
143
+ </span>
144
+ </div>
145
+ <span className="text-sm text-gray-700">{onlineUser.username}</span>
146
+ {onlineUser.userId === user.id && (
147
+ <span className="text-xs text-gray-500">(你)</span>
148
+ )}
149
+ </div>
150
+ ))}
151
+ </div>
152
+ </div>
153
+ </div>
154
+
155
+ {/* 主聊天区域 */}
156
+ <div className="flex-1 flex flex-col">
157
+ {/* 聊天头部 */}
158
+ <div className="bg-white border-b border-gray-200 p-4">
159
+ <div className="flex items-center space-x-2">
160
+ <MessageCircle className="h-6 w-6 text-primary-600" />
161
+ <h2 className="text-lg font-semibold text-gray-900">聊天室</h2>
162
+ </div>
163
+ </div>
164
+
165
+ {/* 消息列表 */}
166
+ <div className="flex-1 overflow-y-auto p-4 space-y-4 custom-scrollbar">
167
+ {messages.map((message) => (
168
+ <div
169
+ key={message.id}
170
+ className={`flex ${
171
+ message.sender.id === user.id ? 'justify-end' : 'justify-start'
172
+ }`}
173
+ >
174
+ <div
175
+ className={`message-bubble ${
176
+ message.sender.id === user.id ? 'message-own' : 'message-other'
177
+ }`}
178
+ >
179
+ {message.sender.id !== user.id && (
180
+ <div className="text-xs font-medium mb-1 text-gray-600">
181
+ {message.sender.username}
182
+ </div>
183
+ )}
184
+ <div className="text-sm">{message.content}</div>
185
+ <div
186
+ className={`text-xs mt-1 ${
187
+ message.sender.id === user.id
188
+ ? 'text-primary-200'
189
+ : 'text-gray-500'
190
+ }`}
191
+ >
192
+ {formatTime(message.timestamp)}
193
+ </div>
194
+ </div>
195
+ </div>
196
+ ))}
197
+ <div ref={messagesEndRef} />
198
+ </div>
199
+
200
+ {/* 消息输入 */}
201
+ <div className="bg-white border-t border-gray-200 p-4">
202
+ <form onSubmit={handleSendMessage} className="flex space-x-4">
203
+ <input
204
+ type="text"
205
+ value={newMessage}
206
+ onChange={(e) => setNewMessage(e.target.value)}
207
+ placeholder="输入消息..."
208
+ className="flex-1 input-field"
209
+ />
210
+ <button
211
+ type="submit"
212
+ disabled={!newMessage.trim()}
213
+ className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
214
+ >
215
+ <Send className="h-4 w-4" />
216
+ <span>发送</span>
217
+ </button>
218
+ </form>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ );
223
+ };
224
+
225
+ export default Chat;
client/src/components/Login.tsx ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import { LogIn, Mail, Lock, AlertCircle } from 'lucide-react';
4
+ import { authAPI } from '../utils/api';
5
+ import { User } from '../types';
6
+
7
+ interface LoginProps {
8
+ onLogin: (user: User, token: string) => void;
9
+ }
10
+
11
+ const Login: React.FC<LoginProps> = ({ onLogin }) => {
12
+ const [formData, setFormData] = useState({
13
+ email: '',
14
+ password: '',
15
+ });
16
+ const [loading, setLoading] = useState(false);
17
+ const [error, setError] = useState('');
18
+
19
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
20
+ setFormData({
21
+ ...formData,
22
+ [e.target.name]: e.target.value,
23
+ });
24
+ setError('');
25
+ };
26
+
27
+ const handleSubmit = async (e: React.FormEvent) => {
28
+ e.preventDefault();
29
+ setLoading(true);
30
+ setError('');
31
+
32
+ try {
33
+ const response = await authAPI.login(formData);
34
+ onLogin(response.user, response.token);
35
+ } catch (error: any) {
36
+ setError(error.response?.data?.message || '登录失败,请重试');
37
+ } finally {
38
+ setLoading(false);
39
+ }
40
+ };
41
+
42
+ return (
43
+ <div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
44
+ <div className="max-w-md w-full space-y-8">
45
+ <div className="text-center">
46
+ <div className="mx-auto h-12 w-12 bg-primary-600 rounded-full flex items-center justify-center">
47
+ <LogIn className="h-6 w-6 text-white" />
48
+ </div>
49
+ <h2 className="mt-6 text-3xl font-bold text-gray-900">
50
+ 登录到聊天室
51
+ </h2>
52
+ <p className="mt-2 text-sm text-gray-600">
53
+ 还没有账号?{' '}
54
+ <Link
55
+ to="/register"
56
+ className="font-medium text-primary-600 hover:text-primary-500"
57
+ >
58
+ 立即注册
59
+ </Link>
60
+ </p>
61
+ </div>
62
+
63
+ <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
64
+ {error && (
65
+ <div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center space-x-2">
66
+ <AlertCircle className="h-5 w-5 text-red-500" />
67
+ <span className="text-red-700 text-sm">{error}</span>
68
+ </div>
69
+ )}
70
+
71
+ <div className="space-y-4">
72
+ <div>
73
+ <label htmlFor="email" className="block text-sm font-medium text-gray-700">
74
+ 邮箱地址
75
+ </label>
76
+ <div className="mt-1 relative">
77
+ <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
78
+ <Mail className="h-5 w-5 text-gray-400" />
79
+ </div>
80
+ <input
81
+ id="email"
82
+ name="email"
83
+ type="email"
84
+ required
85
+ value={formData.email}
86
+ onChange={handleChange}
87
+ className="input-field pl-10"
88
+ placeholder="请输入邮箱地址"
89
+ />
90
+ </div>
91
+ </div>
92
+
93
+ <div>
94
+ <label htmlFor="password" className="block text-sm font-medium text-gray-700">
95
+ 密码
96
+ </label>
97
+ <div className="mt-1 relative">
98
+ <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
99
+ <Lock className="h-5 w-5 text-gray-400" />
100
+ </div>
101
+ <input
102
+ id="password"
103
+ name="password"
104
+ type="password"
105
+ required
106
+ value={formData.password}
107
+ onChange={handleChange}
108
+ className="input-field pl-10"
109
+ placeholder="请输入密码"
110
+ />
111
+ </div>
112
+ </div>
113
+ </div>
114
+
115
+ <button
116
+ type="submit"
117
+ disabled={loading}
118
+ className="w-full btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
119
+ >
120
+ {loading ? (
121
+ <div className="flex items-center justify-center">
122
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
123
+ 登录中...
124
+ </div>
125
+ ) : (
126
+ '登录'
127
+ )}
128
+ </button>
129
+ </form>
130
+ </div>
131
+ </div>
132
+ );
133
+ };
134
+
135
+ export default Login;
client/src/components/Register.tsx ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import { UserPlus, Mail, Lock, User as UserIcon, AlertCircle } from 'lucide-react';
4
+ import { authAPI } from '../utils/api';
5
+ import { User } from '../types';
6
+
7
+ interface RegisterProps {
8
+ onRegister: (user: User, token: string) => void;
9
+ }
10
+
11
+ const Register: React.FC<RegisterProps> = ({ onRegister }) => {
12
+ const [formData, setFormData] = useState({
13
+ username: '',
14
+ email: '',
15
+ password: '',
16
+ confirmPassword: '',
17
+ });
18
+ const [loading, setLoading] = useState(false);
19
+ const [error, setError] = useState('');
20
+
21
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
22
+ setFormData({
23
+ ...formData,
24
+ [e.target.name]: e.target.value,
25
+ });
26
+ setError('');
27
+ };
28
+
29
+ const handleSubmit = async (e: React.FormEvent) => {
30
+ e.preventDefault();
31
+ setLoading(true);
32
+ setError('');
33
+
34
+ if (formData.password !== formData.confirmPassword) {
35
+ setError('两次输入的密码不一致');
36
+ setLoading(false);
37
+ return;
38
+ }
39
+
40
+ if (formData.password.length < 6) {
41
+ setError('密码长度至少为6位');
42
+ setLoading(false);
43
+ return;
44
+ }
45
+
46
+ try {
47
+ const { confirmPassword, ...registerData } = formData;
48
+ const response = await authAPI.register(registerData);
49
+ onRegister(response.user, response.token);
50
+ } catch (error: any) {
51
+ setError(error.response?.data?.message || '注册失败,请重试');
52
+ } finally {
53
+ setLoading(false);
54
+ }
55
+ };
56
+
57
+ return (
58
+ <div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
59
+ <div className="max-w-md w-full space-y-8">
60
+ <div className="text-center">
61
+ <div className="mx-auto h-12 w-12 bg-primary-600 rounded-full flex items-center justify-center">
62
+ <UserPlus className="h-6 w-6 text-white" />
63
+ </div>
64
+ <h2 className="mt-6 text-3xl font-bold text-gray-900">
65
+ 创建新账号
66
+ </h2>
67
+ <p className="mt-2 text-sm text-gray-600">
68
+ 已有账号?{' '}
69
+ <Link
70
+ to="/login"
71
+ className="font-medium text-primary-600 hover:text-primary-500"
72
+ >
73
+ 立即登录
74
+ </Link>
75
+ </p>
76
+ </div>
77
+
78
+ <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
79
+ {error && (
80
+ <div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center space-x-2">
81
+ <AlertCircle className="h-5 w-5 text-red-500" />
82
+ <span className="text-red-700 text-sm">{error}</span>
83
+ </div>
84
+ )}
85
+
86
+ <div className="space-y-4">
87
+ <div>
88
+ <label htmlFor="username" className="block text-sm font-medium text-gray-700">
89
+ 用户名
90
+ </label>
91
+ <div className="mt-1 relative">
92
+ <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
93
+ <UserIcon className="h-5 w-5 text-gray-400" />
94
+ </div>
95
+ <input
96
+ id="username"
97
+ name="username"
98
+ type="text"
99
+ required
100
+ value={formData.username}
101
+ onChange={handleChange}
102
+ className="input-field pl-10"
103
+ placeholder="请输入用户名"
104
+ />
105
+ </div>
106
+ </div>
107
+
108
+ <div>
109
+ <label htmlFor="email" className="block text-sm font-medium text-gray-700">
110
+ 邮箱地址
111
+ </label>
112
+ <div className="mt-1 relative">
113
+ <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
114
+ <Mail className="h-5 w-5 text-gray-400" />
115
+ </div>
116
+ <input
117
+ id="email"
118
+ name="email"
119
+ type="email"
120
+ required
121
+ value={formData.email}
122
+ onChange={handleChange}
123
+ className="input-field pl-10"
124
+ placeholder="请输入邮箱地址"
125
+ />
126
+ </div>
127
+ </div>
128
+
129
+ <div>
130
+ <label htmlFor="password" className="block text-sm font-medium text-gray-700">
131
+ 密码
132
+ </label>
133
+ <div className="mt-1 relative">
134
+ <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
135
+ <Lock className="h-5 w-5 text-gray-400" />
136
+ </div>
137
+ <input
138
+ id="password"
139
+ name="password"
140
+ type="password"
141
+ required
142
+ value={formData.password}
143
+ onChange={handleChange}
144
+ className="input-field pl-10"
145
+ placeholder="请输入密码(至少6位)"
146
+ />
147
+ </div>
148
+ </div>
149
+
150
+ <div>
151
+ <label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700">
152
+ 确认密码
153
+ </label>
154
+ <div className="mt-1 relative">
155
+ <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
156
+ <Lock className="h-5 w-5 text-gray-400" />
157
+ </div>
158
+ <input
159
+ id="confirmPassword"
160
+ name="confirmPassword"
161
+ type="password"
162
+ required
163
+ value={formData.confirmPassword}
164
+ onChange={handleChange}
165
+ className="input-field pl-10"
166
+ placeholder="请再次输入密码"
167
+ />
168
+ </div>
169
+ </div>
170
+ </div>
171
+
172
+ <button
173
+ type="submit"
174
+ disabled={loading}
175
+ className="w-full btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
176
+ >
177
+ {loading ? (
178
+ <div className="flex items-center justify-center">
179
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
180
+ 注册中...
181
+ </div>
182
+ ) : (
183
+ '注册'
184
+ )}
185
+ </button>
186
+ </form>
187
+ </div>
188
+ </div>
189
+ );
190
+ };
191
+
192
+ export default Register;
client/src/index.css ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ html {
7
+ font-family: 'Inter', system-ui, sans-serif;
8
+ }
9
+
10
+ body {
11
+ @apply bg-gray-50 text-gray-900;
12
+ }
13
+ }
14
+
15
+ @layer components {
16
+ .btn-primary {
17
+ @apply bg-primary-600 hover:bg-primary-700 text-white font-medium py-2 px-4 rounded-lg transition-colors duration-200;
18
+ }
19
+
20
+ .btn-secondary {
21
+ @apply bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-4 rounded-lg transition-colors duration-200;
22
+ }
23
+
24
+ .input-field {
25
+ @apply w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent;
26
+ }
27
+
28
+ .message-bubble {
29
+ @apply max-w-xs lg:max-w-md px-4 py-2 rounded-lg break-words;
30
+ }
31
+
32
+ .message-own {
33
+ @apply bg-primary-600 text-white ml-auto;
34
+ }
35
+
36
+ .message-other {
37
+ @apply bg-white text-gray-800 border border-gray-200;
38
+ }
39
+ }
40
+
41
+ /* 自定义滚动条 */
42
+ .custom-scrollbar::-webkit-scrollbar {
43
+ width: 6px;
44
+ }
45
+
46
+ .custom-scrollbar::-webkit-scrollbar-track {
47
+ background: #f1f1f1;
48
+ border-radius: 3px;
49
+ }
50
+
51
+ .custom-scrollbar::-webkit-scrollbar-thumb {
52
+ background: #c1c1c1;
53
+ border-radius: 3px;
54
+ }
55
+
56
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover {
57
+ background: #a8a8a8;
58
+ }
client/src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App.tsx'
4
+ import './index.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ )
client/src/types/index.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface User {
2
+ id: string;
3
+ username: string;
4
+ email: string;
5
+ avatar?: string;
6
+ }
7
+
8
+ export interface Message {
9
+ id: string;
10
+ content: string;
11
+ sender: {
12
+ id: string;
13
+ username: string;
14
+ avatar?: string;
15
+ };
16
+ timestamp: Date;
17
+ room: string;
18
+ }
19
+
20
+ export interface AuthResponse {
21
+ message: string;
22
+ token: string;
23
+ user: User;
24
+ }
25
+
26
+ export interface LoginData {
27
+ email: string;
28
+ password: string;
29
+ }
30
+
31
+ export interface RegisterData {
32
+ username: string;
33
+ email: string;
34
+ password: string;
35
+ }
36
+
37
+ export interface OnlineUser {
38
+ userId: string;
39
+ username: string;
40
+ avatar?: string;
41
+ }
client/src/utils/api.ts ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from 'axios';
2
+ import { AuthResponse, LoginData, RegisterData, Message } from '../types';
3
+
4
+ const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';
5
+
6
+ const api = axios.create({
7
+ baseURL: `${API_BASE_URL}/api`,
8
+ headers: {
9
+ 'Content-Type': 'application/json',
10
+ },
11
+ });
12
+
13
+ // 请求拦截器 - 添加认证token
14
+ api.interceptors.request.use((config) => {
15
+ const token = localStorage.getItem('token');
16
+ if (token) {
17
+ config.headers.Authorization = `Bearer ${token}`;
18
+ }
19
+ return config;
20
+ });
21
+
22
+ // 响应拦截器 - 处理认证错误
23
+ api.interceptors.response.use(
24
+ (response) => response,
25
+ (error) => {
26
+ if (error.response?.status === 401) {
27
+ localStorage.removeItem('token');
28
+ localStorage.removeItem('user');
29
+ window.location.href = '/login';
30
+ }
31
+ return Promise.reject(error);
32
+ }
33
+ );
34
+
35
+ export const authAPI = {
36
+ login: async (data: LoginData): Promise<AuthResponse> => {
37
+ const response = await api.post('/login', data);
38
+ return response.data;
39
+ },
40
+
41
+ register: async (data: RegisterData): Promise<AuthResponse> => {
42
+ const response = await api.post('/register', data);
43
+ return response.data;
44
+ },
45
+ };
46
+
47
+ export const messageAPI = {
48
+ getMessages: async (room: string = 'general', limit: number = 50): Promise<Message[]> => {
49
+ const response = await api.get('/messages', {
50
+ params: { room, limit }
51
+ });
52
+ return response.data;
53
+ },
54
+ };
55
+
56
+ export default api;
client/src/utils/socket.ts ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { io, Socket } from 'socket.io-client';
2
+
3
+ const SOCKET_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';
4
+
5
+ class SocketService {
6
+ private socket: Socket | null = null;
7
+
8
+ connect(token: string): Socket {
9
+ if (this.socket?.connected) {
10
+ return this.socket;
11
+ }
12
+
13
+ this.socket = io(SOCKET_URL, {
14
+ autoConnect: false,
15
+ });
16
+
17
+ this.socket.connect();
18
+
19
+ // 连接成功后发送认证信息
20
+ this.socket.on('connect', () => {
21
+ console.log('Socket连接成功');
22
+ this.socket?.emit('join', { token });
23
+ });
24
+
25
+ this.socket.on('disconnect', () => {
26
+ console.log('Socket连接断开');
27
+ });
28
+
29
+ this.socket.on('error', (error) => {
30
+ console.error('Socket错误:', error);
31
+ });
32
+
33
+ return this.socket;
34
+ }
35
+
36
+ disconnect(): void {
37
+ if (this.socket) {
38
+ this.socket.disconnect();
39
+ this.socket = null;
40
+ }
41
+ }
42
+
43
+ getSocket(): Socket | null {
44
+ return this.socket;
45
+ }
46
+
47
+ sendMessage(content: string, room: string = 'general'): void {
48
+ if (this.socket?.connected) {
49
+ this.socket.emit('sendMessage', { content, room });
50
+ }
51
+ }
52
+
53
+ onNewMessage(callback: (message: any) => void): void {
54
+ this.socket?.on('newMessage', callback);
55
+ }
56
+
57
+ onUserJoined(callback: (user: any) => void): void {
58
+ this.socket?.on('userJoined', callback);
59
+ }
60
+
61
+ onUserLeft(callback: (user: any) => void): void {
62
+ this.socket?.on('userLeft', callback);
63
+ }
64
+
65
+ onOnlineUsers(callback: (users: any[]) => void): void {
66
+ this.socket?.on('onlineUsers', callback);
67
+ }
68
+
69
+ offAllListeners(): void {
70
+ this.socket?.removeAllListeners();
71
+ }
72
+ }
73
+
74
+ export const socketService = new SocketService();
75
+ export default socketService;
client/tailwind.config.js ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{js,ts,jsx,tsx}",
6
+ ],
7
+ theme: {
8
+ extend: {
9
+ colors: {
10
+ primary: {
11
+ 50: '#eff6ff',
12
+ 100: '#dbeafe',
13
+ 200: '#bfdbfe',
14
+ 300: '#93c5fd',
15
+ 400: '#60a5fa',
16
+ 500: '#3b82f6',
17
+ 600: '#2563eb',
18
+ 700: '#1d4ed8',
19
+ 800: '#1e40af',
20
+ 900: '#1e3a8a',
21
+ }
22
+ }
23
+ },
24
+ },
25
+ plugins: [],
26
+ }
client/tsconfig.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "noEmit": true,
15
+ "jsx": "react-jsx",
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "noFallthroughCasesInSwitch": true
22
+ },
23
+ "include": ["src"],
24
+ "references": [{ "path": "./tsconfig.node.json" }]
25
+ }
client/tsconfig.node.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true
8
+ },
9
+ "include": ["vite.config.ts"]
10
+ }
client/vite.config.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vitejs.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ server: {
8
+ host: '0.0.0.0',
9
+ port: 3000,
10
+ },
11
+ build: {
12
+ outDir: 'dist',
13
+ },
14
+ })
deploy.sh ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # 聊天应用生产环境部署脚本 - Linux版本
4
+
5
+ set -e # 遇到错误立即退出
6
+
7
+ echo "🚀 部署聊天应用到生产环境..."
8
+ echo
9
+
10
+ # 检查是否为root用户
11
+ if [ "$EUID" -ne 0 ]; then
12
+ echo "⚠️ 建议使用sudo运行此脚本以确保权限充足"
13
+ fi
14
+
15
+ # 检查Docker是否安装
16
+ if ! command -v docker &> /dev/null; then
17
+ echo "❌ 错误: Docker未安装"
18
+ echo "正在安装Docker..."
19
+ curl -fsSL https://get.docker.com -o get-docker.sh
20
+ sh get-docker.sh
21
+ rm get-docker.sh
22
+ systemctl enable docker
23
+ systemctl start docker
24
+ echo "✅ Docker安装完成"
25
+ fi
26
+
27
+ # 检查Docker Compose是否安装
28
+ if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
29
+ echo "❌ 错误: Docker Compose未安装"
30
+ echo "正在安装Docker Compose..."
31
+ curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
32
+ chmod +x /usr/local/bin/docker-compose
33
+ echo "✅ Docker Compose安装完成"
34
+ fi
35
+
36
+ # 检查Docker服务是否运行
37
+ if ! docker info &> /dev/null; then
38
+ echo "🔄 启动Docker服务..."
39
+ systemctl start docker
40
+ fi
41
+
42
+ echo "✅ Docker环境准备完成"
43
+ echo
44
+
45
+ # 设置脚本权限
46
+ chmod +x start.sh stop.sh start-dev.sh
47
+
48
+ # 创建生产环境配置
49
+ echo "⚙️ 配置生产环境..."
50
+
51
+ # 生成随机JWT密钥
52
+ JWT_SECRET=$(openssl rand -base64 32)
53
+
54
+ # 创建生产环境配置文件
55
+ cat > .env.production << EOF
56
+ # 生产环境配置
57
+ NODE_ENV=production
58
+ MONGODB_URI=mongodb://admin:$(openssl rand -base64 12)@mongo:27017/chatapp?authSource=admin
59
+ JWT_SECRET=${JWT_SECRET}
60
+ PORT=5000
61
+ CLIENT_URL=http://localhost:3000
62
+ EOF
63
+
64
+ echo "✅ 生产环境配置完成"
65
+ echo
66
+
67
+ # 停止现有服务
68
+ echo "🛑 停止现有服务..."
69
+ if command -v docker-compose &> /dev/null; then
70
+ docker-compose down 2>/dev/null || true
71
+ else
72
+ docker compose down 2>/dev/null || true
73
+ fi
74
+
75
+ # 清理旧镜像
76
+ echo "🧹 清理旧镜像..."
77
+ docker system prune -f
78
+
79
+ # 构建并启动服务
80
+ echo "📦 构建并启动生产服务..."
81
+ if command -v docker-compose &> /dev/null; then
82
+ docker-compose up --build -d
83
+ else
84
+ docker compose up --build -d
85
+ fi
86
+
87
+ # 等待服务启动
88
+ echo "⏳ 等待服务启动..."
89
+ sleep 30
90
+
91
+ # 检查服务状态
92
+ echo "🔍 检查服务状态..."
93
+ if command -v docker-compose &> /dev/null; then
94
+ docker-compose ps
95
+ else
96
+ docker compose ps
97
+ fi
98
+
99
+ # 测试服务
100
+ echo "🧪 测试服务..."
101
+ if curl -f http://localhost:5000/api/health > /dev/null 2>&1; then
102
+ echo "✅ 后端服务正常"
103
+ else
104
+ echo "❌ 后端服务异常"
105
+ fi
106
+
107
+ if curl -f http://localhost:3000 > /dev/null 2>&1; then
108
+ echo "✅ 前端服务正常"
109
+ else
110
+ echo "❌ 前端服务异常"
111
+ fi
112
+
113
+ echo
114
+ echo "🎉 部署完成!"
115
+ echo
116
+ echo "🌐 前端地址: http://localhost:3000"
117
+ echo "🔧 后端API: http://localhost:5000"
118
+ echo "📊 MongoDB: localhost:27017"
119
+ echo
120
+ echo "📋 常用命令:"
121
+ echo " - 查看日志: docker-compose logs -f"
122
+ echo " - 重启服务: docker-compose restart"
123
+ echo " - 停止服务: ./stop.sh"
124
+ echo " - 更新应用: ./deploy.sh"
125
+ echo
126
+ echo "🔒 安全提醒:"
127
+ echo " - 请修改默认密码"
128
+ echo " - 配置防火墙规则"
129
+ echo " - 启用HTTPS (推荐使用nginx反向代理)"
130
+ echo " - 定期备份数据库"
docker-compare.sh ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Docker镜像大小比较脚本
4
+
5
+ echo "🐳 Docker镜像构建和大小比较"
6
+ echo "=========================="
7
+ echo
8
+
9
+ # 检查Docker是否可用
10
+ if ! command -v docker &> /dev/null; then
11
+ echo "❌ Docker未安装"
12
+ exit 1
13
+ fi
14
+
15
+ echo "📦 构建不同版本的Docker镜像..."
16
+ echo
17
+
18
+ # 构建标准版本
19
+ echo "🔨 构建标准单容器版本..."
20
+ docker build -t chatapp:standard -f Dockerfile . --no-cache
21
+ echo
22
+
23
+ # 构建优化版本
24
+ echo "🔨 构建优化版本..."
25
+ docker build -t chatapp:optimized -f Dockerfile.optimized . --no-cache
26
+ echo
27
+
28
+ # 构建多容器版本
29
+ echo "🔨 构建前端容器..."
30
+ docker build -t chatapp:frontend ./client --no-cache
31
+
32
+ echo "🔨 构建后端容器..."
33
+ docker build -t chatapp:backend ./server --no-cache
34
+ echo
35
+
36
+ # 显示镜像大小比较
37
+ echo "📊 镜像大小比较:"
38
+ echo "=================="
39
+ echo
40
+
41
+ # 获取镜像信息
42
+ STANDARD_SIZE=$(docker images chatapp:standard --format "{{.Size}}")
43
+ OPTIMIZED_SIZE=$(docker images chatapp:optimized --format "{{.Size}}")
44
+ FRONTEND_SIZE=$(docker images chatapp:frontend --format "{{.Size}}")
45
+ BACKEND_SIZE=$(docker images chatapp:backend --format "{{.Size}}")
46
+
47
+ echo "📦 单容器版本:"
48
+ echo " 标准版本: $STANDARD_SIZE"
49
+ echo " 优化版本: $OPTIMIZED_SIZE"
50
+ echo
51
+
52
+ echo "📦 多容器版本:"
53
+ echo " 前端容器: $FRONTEND_SIZE"
54
+ echo " 后端容器: $BACKEND_SIZE"
55
+ echo
56
+
57
+ # 详细镜像信息
58
+ echo "📋 详细镜像信息:"
59
+ echo "=================="
60
+ docker images | grep chatapp | sort -k2
61
+
62
+ echo
63
+ echo "🔍 镜像层分析:"
64
+ echo "=============="
65
+
66
+ echo
67
+ echo "📊 标准版本镜像层:"
68
+ docker history chatapp:standard --format "table {{.CreatedBy}}\t{{.Size}}" | head -10
69
+
70
+ echo
71
+ echo "📊 优化版本镜像层:"
72
+ docker history chatapp:optimized --format "table {{.CreatedBy}}\t{{.Size}}" | head -10
73
+
74
+ echo
75
+ echo "💡 建议:"
76
+ echo "========"
77
+ echo "✅ 生产环境推荐使用优化版本 (chatapp:optimized)"
78
+ echo "✅ 开发环境可以使用多容器版本便于调试"
79
+ echo "✅ 如需最小镜像,考虑使用多阶段构建进一步优化"
80
+
81
+ echo
82
+ echo "🧪 性能测试建议:"
83
+ echo "================"
84
+ echo "1. 启动时间测试:"
85
+ echo " time docker run --rm chatapp:standard"
86
+ echo " time docker run --rm chatapp:optimized"
87
+ echo
88
+ echo "2. 内存使用测试:"
89
+ echo " docker stats --no-stream"
90
+ echo
91
+ echo "3. 网络性能测试:"
92
+ echo " 使用 ab 或 wrk 工具测试HTTP性能"
93
+
94
+ echo
95
+ echo "🧹 清理命令:"
96
+ echo "============"
97
+ echo "# 删除测试镜像"
98
+ echo "docker rmi chatapp:standard chatapp:optimized chatapp:frontend chatapp:backend"
99
+ echo
100
+ echo "# 清理构建缓存"
101
+ echo "docker builder prune -f"
102
+
103
+ # 询问是否清理
104
+ read -p "是否现在清理测试镜像? (y/N): " cleanup
105
+ if [[ $cleanup =~ ^[Yy]$ ]]; then
106
+ echo "🧹 清理镜像..."
107
+ docker rmi chatapp:standard chatapp:optimized chatapp:frontend chatapp:backend 2>/dev/null || true
108
+ echo "✅ 清理完成"
109
+ fi
docker-compose.dev.yml ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ # MongoDB数据库
5
+ mongo:
6
+ image: mongo:7.0
7
+ container_name: chat-mongo-dev
8
+ restart: unless-stopped
9
+ environment:
10
+ MONGO_INITDB_ROOT_USERNAME: admin
11
+ MONGO_INITDB_ROOT_PASSWORD: password123
12
+ MONGO_INITDB_DATABASE: chatapp
13
+ ports:
14
+ - "27017:27017"
15
+ volumes:
16
+ - mongo_data_dev:/data/db
17
+ - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
18
+ networks:
19
+ - chat-network-dev
20
+
21
+ # 后端开发服务
22
+ server-dev:
23
+ image: node:18-alpine
24
+ container_name: chat-server-dev
25
+ restart: unless-stopped
26
+ working_dir: /app
27
+ environment:
28
+ - NODE_ENV=development
29
+ - MONGODB_URI=mongodb://admin:password123@mongo:27017/chatapp?authSource=admin
30
+ - JWT_SECRET=dev-secret-key
31
+ - PORT=5000
32
+ - CLIENT_URL=http://localhost:3000
33
+ ports:
34
+ - "5000:5000"
35
+ depends_on:
36
+ - mongo
37
+ networks:
38
+ - chat-network-dev
39
+ volumes:
40
+ - ./server:/app
41
+ - /app/node_modules
42
+ command: sh -c "npm install && npm run dev"
43
+
44
+ # 前端开发服务
45
+ client-dev:
46
+ image: node:18-alpine
47
+ container_name: chat-client-dev
48
+ restart: unless-stopped
49
+ working_dir: /app
50
+ environment:
51
+ - VITE_API_URL=http://localhost:5000
52
+ ports:
53
+ - "3000:3000"
54
+ depends_on:
55
+ - server-dev
56
+ networks:
57
+ - chat-network-dev
58
+ volumes:
59
+ - ./client:/app
60
+ - /app/node_modules
61
+ command: sh -c "npm install && npm run dev"
62
+
63
+ volumes:
64
+ mongo_data_dev:
65
+
66
+ networks:
67
+ chat-network-dev:
68
+ driver: bridge
docker-compose.single.yml ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ # 单容器部署配置 - 前后端在同一个容器中
4
+ services:
5
+ # MongoDB数据库
6
+ mongo:
7
+ image: mongo:7.0
8
+ container_name: chat-mongo-single
9
+ restart: unless-stopped
10
+ environment:
11
+ MONGO_INITDB_ROOT_USERNAME: admin
12
+ MONGO_INITDB_ROOT_PASSWORD: password123
13
+ MONGO_INITDB_DATABASE: chatapp
14
+ ports:
15
+ - "27017:27017"
16
+ volumes:
17
+ - mongo_data_single:/data/db
18
+ - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
19
+ networks:
20
+ - chat-network-single
21
+ healthcheck:
22
+ test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
23
+ interval: 30s
24
+ timeout: 10s
25
+ retries: 3
26
+ start_period: 40s
27
+
28
+ # 聊天应用(前端+后端)
29
+ chat-app:
30
+ build:
31
+ context: .
32
+ dockerfile: Dockerfile
33
+ container_name: chat-app-single
34
+ restart: unless-stopped
35
+ environment:
36
+ - NODE_ENV=production
37
+ - MONGODB_URI=mongodb://admin:password123@mongo:27017/chatapp?authSource=admin
38
+ - JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
39
+ - PORT=5000
40
+ - CLIENT_URL=http://localhost
41
+ ports:
42
+ - "80:80" # 前端 (nginx)
43
+ - "5000:5000" # 后端API
44
+ depends_on:
45
+ mongo:
46
+ condition: service_healthy
47
+ networks:
48
+ - chat-network-single
49
+ healthcheck:
50
+ test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80"]
51
+ interval: 30s
52
+ timeout: 10s
53
+ retries: 3
54
+ start_period: 60s
55
+
56
+ volumes:
57
+ mongo_data_single:
58
+
59
+ networks:
60
+ chat-network-single:
61
+ driver: bridge
docker-compose.yml ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ # MongoDB数据库
5
+ mongo:
6
+ image: mongo:7.0
7
+ container_name: chat-mongo
8
+ restart: unless-stopped
9
+ environment:
10
+ MONGO_INITDB_ROOT_USERNAME: admin
11
+ MONGO_INITDB_ROOT_PASSWORD: password123
12
+ MONGO_INITDB_DATABASE: chatapp
13
+ ports:
14
+ - "27017:27017"
15
+ volumes:
16
+ - mongo_data:/data/db
17
+ - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
18
+ networks:
19
+ - chat-network
20
+ healthcheck:
21
+ test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
22
+ interval: 30s
23
+ timeout: 10s
24
+ retries: 3
25
+ start_period: 40s
26
+
27
+ # 后端服务
28
+ server:
29
+ build:
30
+ context: ./server
31
+ dockerfile: Dockerfile
32
+ container_name: chat-server
33
+ restart: unless-stopped
34
+ environment:
35
+ - NODE_ENV=production
36
+ - MONGODB_URI=mongodb://admin:password123@mongo:27017/chatapp?authSource=admin
37
+ - JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
38
+ - PORT=5000
39
+ - CLIENT_URL=http://localhost:3000
40
+ ports:
41
+ - "5000:5000"
42
+ depends_on:
43
+ mongo:
44
+ condition: service_healthy
45
+ networks:
46
+ - chat-network
47
+ healthcheck:
48
+ test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:5000/api/health"]
49
+ interval: 30s
50
+ timeout: 10s
51
+ retries: 3
52
+ start_period: 40s
53
+
54
+ # 前端服务
55
+ client:
56
+ build:
57
+ context: ./client
58
+ dockerfile: Dockerfile
59
+ container_name: chat-client
60
+ restart: unless-stopped
61
+ ports:
62
+ - "3000:80"
63
+ depends_on:
64
+ server:
65
+ condition: service_healthy
66
+ networks:
67
+ - chat-network
68
+ environment:
69
+ - VITE_API_URL=http://localhost:5000
70
+ healthcheck:
71
+ test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80"]
72
+ interval: 30s
73
+ timeout: 10s
74
+ retries: 3
75
+ start_period: 40s
76
+
77
+ volumes:
78
+ mongo_data:
79
+
80
+ networks:
81
+ chat-network:
82
+ driver: bridge
mongo-init.js ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // MongoDB初始化脚本
2
+ db = db.getSiblingDB('chatapp');
3
+
4
+ // 创建用户集合索引
5
+ db.users.createIndex({ "email": 1 }, { unique: true });
6
+ db.users.createIndex({ "username": 1 }, { unique: true });
7
+
8
+ // 创建消息集合索引
9
+ db.messages.createIndex({ "timestamp": -1 });
10
+ db.messages.createIndex({ "room": 1, "timestamp": -1 });
11
+
12
+ print('数据库初始化完成');
monitor.sh ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # 聊天应用监控脚本
4
+
5
+ echo "📊 聊天应用监控面板"
6
+ echo "===================="
7
+ echo
8
+
9
+ # 检查Docker服务
10
+ echo "🐳 Docker服务状态:"
11
+ if systemctl is-active --quiet docker; then
12
+ echo " ✅ Docker服务运行中"
13
+ else
14
+ echo " ❌ Docker服务未运行"
15
+ fi
16
+ echo
17
+
18
+ # 检查容器状态
19
+ echo "📦 容器状态:"
20
+ if command -v docker-compose >/dev/null 2>&1; then
21
+ docker-compose ps
22
+ else
23
+ docker compose ps
24
+ fi
25
+ echo
26
+
27
+ # 检查服务健康状态
28
+ echo "🏥 服务健康检查:"
29
+
30
+ # 检查后端API
31
+ if curl -f -s http://localhost:5000/api/health > /dev/null; then
32
+ echo " ✅ 后端API (http://localhost:5000)"
33
+ # 获取健康信息
34
+ HEALTH_INFO=$(curl -s http://localhost:5000/api/health)
35
+ echo " $(echo $HEALTH_INFO | jq -r '.status // "N/A"') - 运行时间: $(echo $HEALTH_INFO | jq -r '.uptime // "N/A"')秒"
36
+ echo " MongoDB: $(echo $HEALTH_INFO | jq -r '.mongodb // "N/A"')"
37
+ else
38
+ echo " ❌ 后端API (http://localhost:5000)"
39
+ fi
40
+
41
+ # 检查前端
42
+ if curl -f -s http://localhost:3000 > /dev/null; then
43
+ echo " ✅ 前端服务 (http://localhost:3000)"
44
+ else
45
+ echo " ❌ 前端服务 (http://localhost:3000)"
46
+ fi
47
+
48
+ # 检查MongoDB
49
+ if docker exec chat-mongo mongosh --eval "db.adminCommand('ping')" > /dev/null 2>&1; then
50
+ echo " ✅ MongoDB数据库"
51
+ else
52
+ echo " ❌ MongoDB数据库"
53
+ fi
54
+ echo
55
+
56
+ # 系统资源使用情况
57
+ echo "💻 系统资源使用:"
58
+ echo " CPU使用率: $(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | awk -F'%' '{print $1}')%"
59
+ echo " 内存使用: $(free -h | awk 'NR==2{printf "%.1f%%", $3*100/$2 }')"
60
+ echo " 磁盘使用: $(df -h / | awk 'NR==2{print $5}')"
61
+ echo
62
+
63
+ # Docker资源使用
64
+ echo "🐳 Docker资源使用:"
65
+ docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}"
66
+ echo
67
+
68
+ # 最近的日志
69
+ echo "📋 最近的日志 (最后10行):"
70
+ if command -v docker-compose >/dev/null 2>&1; then
71
+ docker-compose logs --tail=10
72
+ else
73
+ docker compose logs --tail=10
74
+ fi
75
+ echo
76
+
77
+ # 网络连接
78
+ echo "🌐 网络连接:"
79
+ echo " 端口3000状态: $(netstat -tuln | grep :3000 > /dev/null && echo "✅ 监听中" || echo "❌ 未监听")"
80
+ echo " 端口5000状态: $(netstat -tuln | grep :5000 > /dev/null && echo "✅ 监听中" || echo "❌ 未监听")"
81
+ echo " 端口27017状态: $(netstat -tuln | grep :27017 > /dev/null && echo "✅ 监听中" || echo "❌ 未监听")"
82
+ echo
83
+
84
+ # 数据库统计
85
+ echo "📊 数据库统计:"
86
+ if docker exec chat-mongo mongosh chatapp --eval "
87
+ print('用户数量: ' + db.users.countDocuments());
88
+ print('消息数量: ' + db.messages.countDocuments());
89
+ print('今日消息: ' + db.messages.countDocuments({timestamp: {\$gte: new Date(new Date().setHours(0,0,0,0))}}));
90
+ " 2>/dev/null; then
91
+ echo " ✅ 数据库统计获取成功"
92
+ else
93
+ echo " ❌ 无法获取数据库统计"
94
+ fi
95
+ echo
96
+
97
+ echo "🔄 监控完成 - $(date)"
98
+ echo "💡 提示: 使用 'watch -n 30 ./monitor.sh' 可以每30秒自动刷新监控信息"
nginx-proxy.conf ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Nginx反向代理配置 - 用于生产环境
2
+ # 将此文件复制到 /etc/nginx/sites-available/chatapp
3
+ # 然后创建软链接: sudo ln -s /etc/nginx/sites-available/chatapp /etc/nginx/sites-enabled/
4
+
5
+ upstream backend {
6
+ server localhost:5000;
7
+ keepalive 32;
8
+ }
9
+
10
+ upstream frontend {
11
+ server localhost:3000;
12
+ keepalive 32;
13
+ }
14
+
15
+ # HTTP重定向到HTTPS
16
+ server {
17
+ listen 80;
18
+ server_name your-domain.com www.your-domain.com;
19
+
20
+ # Let's Encrypt验证
21
+ location /.well-known/acme-challenge/ {
22
+ root /var/www/html;
23
+ }
24
+
25
+ # 重定向到HTTPS
26
+ location / {
27
+ return 301 https://$server_name$request_uri;
28
+ }
29
+ }
30
+
31
+ # HTTPS配置
32
+ server {
33
+ listen 443 ssl http2;
34
+ server_name your-domain.com www.your-domain.com;
35
+
36
+ # SSL证书配置
37
+ ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
38
+ ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
39
+
40
+ # SSL安全配置
41
+ ssl_protocols TLSv1.2 TLSv1.3;
42
+ ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
43
+ ssl_prefer_server_ciphers off;
44
+ ssl_session_cache shared:SSL:10m;
45
+ ssl_session_timeout 10m;
46
+
47
+ # 安全头
48
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
49
+ add_header X-Frame-Options "SAMEORIGIN" always;
50
+ add_header X-Content-Type-Options "nosniff" always;
51
+ add_header X-XSS-Protection "1; mode=block" always;
52
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
53
+
54
+ # 日志配置
55
+ access_log /var/log/nginx/chatapp_access.log;
56
+ error_log /var/log/nginx/chatapp_error.log;
57
+
58
+ # 客户端配置
59
+ client_max_body_size 10M;
60
+ client_body_timeout 60s;
61
+ client_header_timeout 60s;
62
+
63
+ # API代理
64
+ location /api/ {
65
+ proxy_pass http://backend;
66
+ proxy_http_version 1.1;
67
+ proxy_set_header Upgrade $http_upgrade;
68
+ proxy_set_header Connection 'upgrade';
69
+ proxy_set_header Host $host;
70
+ proxy_set_header X-Real-IP $remote_addr;
71
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
72
+ proxy_set_header X-Forwarded-Proto $scheme;
73
+ proxy_cache_bypass $http_upgrade;
74
+ proxy_connect_timeout 30s;
75
+ proxy_send_timeout 30s;
76
+ proxy_read_timeout 30s;
77
+ }
78
+
79
+ # Socket.IO代理
80
+ location /socket.io/ {
81
+ proxy_pass http://backend;
82
+ proxy_http_version 1.1;
83
+ proxy_set_header Upgrade $http_upgrade;
84
+ proxy_set_header Connection "upgrade";
85
+ proxy_set_header Host $host;
86
+ proxy_set_header X-Real-IP $remote_addr;
87
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
88
+ proxy_set_header X-Forwarded-Proto $scheme;
89
+ proxy_cache_bypass $http_upgrade;
90
+ proxy_connect_timeout 30s;
91
+ proxy_send_timeout 30s;
92
+ proxy_read_timeout 86400s; # 24小时,用于长连接
93
+ }
94
+
95
+ # 前端代理
96
+ location / {
97
+ proxy_pass http://frontend;
98
+ proxy_http_version 1.1;
99
+ proxy_set_header Upgrade $http_upgrade;
100
+ proxy_set_header Connection 'upgrade';
101
+ proxy_set_header Host $host;
102
+ proxy_set_header X-Real-IP $remote_addr;
103
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
104
+ proxy_set_header X-Forwarded-Proto $scheme;
105
+ proxy_cache_bypass $http_upgrade;
106
+ proxy_connect_timeout 30s;
107
+ proxy_send_timeout 30s;
108
+ proxy_read_timeout 30s;
109
+ }
110
+
111
+ # 静态资源缓存
112
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
113
+ proxy_pass http://frontend;
114
+ expires 1y;
115
+ add_header Cache-Control "public, immutable";
116
+ add_header Vary "Accept-Encoding";
117
+ }
118
+
119
+ # Gzip压缩
120
+ gzip on;
121
+ gzip_vary on;
122
+ gzip_min_length 1024;
123
+ gzip_proxied any;
124
+ gzip_comp_level 6;
125
+ gzip_types
126
+ text/plain
127
+ text/css
128
+ text/xml
129
+ text/javascript
130
+ application/javascript
131
+ application/xml+rss
132
+ application/json
133
+ application/xml
134
+ image/svg+xml;
135
+ }
server/.dockerignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ npm-debug.log
3
+ .env
4
+ .git
5
+ .gitignore
6
+ README.md
7
+ Dockerfile
8
+ .dockerignore
server/.env ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ MONGODB_URI=mongodb://mongo:27017/chatapp
2
+ JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
3
+ PORT=5000
4
+ CLIENT_URL=http://localhost:3000
server/Dockerfile ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 使用官方Node.js运行时作为基础镜像
2
+ FROM node:18-alpine
3
+
4
+ # 安装必要的系统依赖
5
+ RUN apk add --no-cache \
6
+ wget \
7
+ curl \
8
+ && rm -rf /var/cache/apk/*
9
+
10
+ # 设置工作目录
11
+ WORKDIR /app
12
+
13
+ # 创建非root用户
14
+ RUN addgroup -g 1001 -S nodejs && \
15
+ adduser -S nodejs -u 1001
16
+
17
+ # 复制package.json和package-lock.json(如果存在)
18
+ COPY --chown=nodejs:nodejs package*.json ./
19
+
20
+ # 切换到nodejs用户安装依赖
21
+ USER nodejs
22
+ RUN npm ci --only=production && npm cache clean --force
23
+
24
+ # 复制应用源代码
25
+ COPY --chown=nodejs:nodejs . .
26
+
27
+ # 暴露端口
28
+ EXPOSE 5000
29
+
30
+ # 健康检查
31
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
32
+ CMD wget --no-verbose --tries=1 --spider http://localhost:5000/api/health || exit 1
33
+
34
+ # 启动应用
35
+ CMD ["npm", "start"]
server/index.js ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const http = require('http');
3
+ const socketIo = require('socket.io');
4
+ const cors = require('cors');
5
+ const mongoose = require('mongoose');
6
+ const jwt = require('jsonwebtoken');
7
+ const bcrypt = require('bcryptjs');
8
+ require('dotenv').config();
9
+
10
+ const app = express();
11
+ const server = http.createServer(app);
12
+ const io = socketIo(server, {
13
+ cors: {
14
+ origin: process.env.CLIENT_URL || "http://localhost:3000",
15
+ methods: ["GET", "POST"]
16
+ }
17
+ });
18
+
19
+ // 中间件
20
+ app.use(cors());
21
+ app.use(express.json());
22
+
23
+ // MongoDB连接
24
+ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://mongo:27017/chatapp';
25
+ mongoose.connect(MONGODB_URI)
26
+ .then(() => console.log('MongoDB连接成功'))
27
+ .catch(err => console.error('MongoDB连接失败:', err));
28
+
29
+ // 用户模型
30
+ const userSchema = new mongoose.Schema({
31
+ username: { type: String, required: true, unique: true },
32
+ email: { type: String, required: true, unique: true },
33
+ password: { type: String, required: true },
34
+ avatar: { type: String, default: '' },
35
+ createdAt: { type: Date, default: Date.now }
36
+ });
37
+
38
+ const User = mongoose.model('User', userSchema);
39
+
40
+ // 消息模型
41
+ const messageSchema = new mongoose.Schema({
42
+ content: { type: String, required: true },
43
+ sender: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
44
+ room: { type: String, default: 'general' },
45
+ timestamp: { type: Date, default: Date.now }
46
+ });
47
+
48
+ const Message = mongoose.model('Message', messageSchema);
49
+
50
+ // JWT验证中间件
51
+ const authenticateToken = (req, res, next) => {
52
+ const authHeader = req.headers['authorization'];
53
+ const token = authHeader && authHeader.split(' ')[1];
54
+
55
+ if (!token) {
56
+ return res.sendStatus(401);
57
+ }
58
+
59
+ jwt.verify(token, process.env.JWT_SECRET || 'fallback-secret', (err, user) => {
60
+ if (err) return res.sendStatus(403);
61
+ req.user = user;
62
+ next();
63
+ });
64
+ };
65
+
66
+ // API路由
67
+ // 健康检查端点
68
+ app.get('/api/health', (req, res) => {
69
+ res.status(200).json({
70
+ status: 'ok',
71
+ timestamp: new Date().toISOString(),
72
+ uptime: process.uptime(),
73
+ mongodb: mongoose.connection.readyState === 1 ? 'connected' : 'disconnected'
74
+ });
75
+ });
76
+
77
+ // 用户注册
78
+ app.post('/api/register', async (req, res) => {
79
+ try {
80
+ const { username, email, password } = req.body;
81
+
82
+ // 检查用户是否已存在
83
+ const existingUser = await User.findOne({
84
+ $or: [{ email }, { username }]
85
+ });
86
+
87
+ if (existingUser) {
88
+ return res.status(400).json({ message: '用户名或邮箱已存在' });
89
+ }
90
+
91
+ // 加密密码
92
+ const hashedPassword = await bcrypt.hash(password, 10);
93
+
94
+ // 创建新用户
95
+ const user = new User({
96
+ username,
97
+ email,
98
+ password: hashedPassword
99
+ });
100
+
101
+ await user.save();
102
+
103
+ // 生成JWT token
104
+ const token = jwt.sign(
105
+ { userId: user._id, username: user.username },
106
+ process.env.JWT_SECRET || 'fallback-secret',
107
+ { expiresIn: '24h' }
108
+ );
109
+
110
+ res.status(201).json({
111
+ message: '注册成功',
112
+ token,
113
+ user: {
114
+ id: user._id,
115
+ username: user.username,
116
+ email: user.email,
117
+ avatar: user.avatar
118
+ }
119
+ });
120
+ } catch (error) {
121
+ console.error('注册错误:', error);
122
+ res.status(500).json({ message: '服务器错误' });
123
+ }
124
+ });
125
+
126
+ // 用户登录
127
+ app.post('/api/login', async (req, res) => {
128
+ try {
129
+ const { email, password } = req.body;
130
+
131
+ // 查找用户
132
+ const user = await User.findOne({ email });
133
+ if (!user) {
134
+ return res.status(400).json({ message: '邮箱或密码错误' });
135
+ }
136
+
137
+ // 验证密码
138
+ const isValidPassword = await bcrypt.compare(password, user.password);
139
+ if (!isValidPassword) {
140
+ return res.status(400).json({ message: '邮箱或密码错误' });
141
+ }
142
+
143
+ // 生成JWT token
144
+ const token = jwt.sign(
145
+ { userId: user._id, username: user.username },
146
+ process.env.JWT_SECRET || 'fallback-secret',
147
+ { expiresIn: '24h' }
148
+ );
149
+
150
+ res.json({
151
+ message: '登录成功',
152
+ token,
153
+ user: {
154
+ id: user._id,
155
+ username: user.username,
156
+ email: user.email,
157
+ avatar: user.avatar
158
+ }
159
+ });
160
+ } catch (error) {
161
+ console.error('登录错误:', error);
162
+ res.status(500).json({ message: '服务器错误' });
163
+ }
164
+ });
165
+
166
+ // 获取历史消息
167
+ app.get('/api/messages', authenticateToken, async (req, res) => {
168
+ try {
169
+ const { room = 'general', limit = 50 } = req.query;
170
+ const messages = await Message.find({ room })
171
+ .populate('sender', 'username avatar')
172
+ .sort({ timestamp: -1 })
173
+ .limit(parseInt(limit));
174
+
175
+ res.json(messages.reverse());
176
+ } catch (error) {
177
+ console.error('获取消息错误:', error);
178
+ res.status(500).json({ message: '服务器错误' });
179
+ }
180
+ });
181
+
182
+ // Socket.IO连接处理
183
+ const connectedUsers = new Map();
184
+
185
+ io.on('connection', (socket) => {
186
+ console.log('用户��接:', socket.id);
187
+
188
+ // 用户加入
189
+ socket.on('join', async (userData) => {
190
+ try {
191
+ // 验证token
192
+ const decoded = jwt.verify(userData.token, process.env.JWT_SECRET || 'fallback-secret');
193
+ const user = await User.findById(decoded.userId);
194
+
195
+ if (user) {
196
+ socket.userId = user._id.toString();
197
+ socket.username = user.username;
198
+ connectedUsers.set(socket.id, {
199
+ userId: user._id.toString(),
200
+ username: user.username,
201
+ avatar: user.avatar
202
+ });
203
+
204
+ socket.join('general');
205
+
206
+ // 广播用户上线
207
+ socket.broadcast.emit('userJoined', {
208
+ username: user.username,
209
+ avatar: user.avatar
210
+ });
211
+
212
+ // 发送在线用户列表
213
+ const onlineUsers = Array.from(connectedUsers.values());
214
+ io.emit('onlineUsers', onlineUsers);
215
+ }
216
+ } catch (error) {
217
+ console.error('用户加入错误:', error);
218
+ socket.emit('error', { message: '认证失败' });
219
+ }
220
+ });
221
+
222
+ // 发送消息
223
+ socket.on('sendMessage', async (messageData) => {
224
+ try {
225
+ if (!socket.userId) {
226
+ socket.emit('error', { message: '未认证用户' });
227
+ return;
228
+ }
229
+
230
+ const message = new Message({
231
+ content: messageData.content,
232
+ sender: socket.userId,
233
+ room: messageData.room || 'general'
234
+ });
235
+
236
+ await message.save();
237
+ await message.populate('sender', 'username avatar');
238
+
239
+ // 广播消息到房间
240
+ io.to(messageData.room || 'general').emit('newMessage', {
241
+ id: message._id,
242
+ content: message.content,
243
+ sender: {
244
+ id: message.sender._id,
245
+ username: message.sender.username,
246
+ avatar: message.sender.avatar
247
+ },
248
+ timestamp: message.timestamp,
249
+ room: message.room
250
+ });
251
+ } catch (error) {
252
+ console.error('发送消息错误:', error);
253
+ socket.emit('error', { message: '发送消息失败' });
254
+ }
255
+ });
256
+
257
+ // 用户断开连接
258
+ socket.on('disconnect', () => {
259
+ console.log('用户断开连接:', socket.id);
260
+
261
+ const userData = connectedUsers.get(socket.id);
262
+ if (userData) {
263
+ connectedUsers.delete(socket.id);
264
+
265
+ // 广播用户下线
266
+ socket.broadcast.emit('userLeft', {
267
+ username: userData.username
268
+ });
269
+
270
+ // 更新在线用户列表
271
+ const onlineUsers = Array.from(connectedUsers.values());
272
+ io.emit('onlineUsers', onlineUsers);
273
+ }
274
+ });
275
+ });
276
+
277
+ const PORT = process.env.PORT || 5000;
278
+ server.listen(PORT, () => {
279
+ console.log(`服务器运行在端口 ${PORT}`);
280
+ });
server/package.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chat-server",
3
+ "version": "1.0.0",
4
+ "description": "Chat application backend server",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "start": "node index.js",
8
+ "dev": "nodemon index.js",
9
+ "test": "echo \"Error: no test specified\" && exit 1"
10
+ },
11
+ "keywords": ["chat", "socket.io", "express", "mongodb"],
12
+ "author": "",
13
+ "license": "MIT",
14
+ "dependencies": {
15
+ "express": "^4.18.2",
16
+ "socket.io": "^4.7.4",
17
+ "mongoose": "^8.0.3",
18
+ "cors": "^2.8.5",
19
+ "jsonwebtoken": "^9.0.2",
20
+ "bcryptjs": "^2.4.3",
21
+ "dotenv": "^16.3.1"
22
+ },
23
+ "devDependencies": {
24
+ "nodemon": "^3.0.2"
25
+ }
26
+ }
setup-ssl.sh ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # SSL证书设置脚本 - 使用Let's Encrypt
4
+
5
+ set -e
6
+
7
+ echo "🔒 设置SSL证书..."
8
+ echo
9
+
10
+ # 检查是否为root用户
11
+ if [ "$EUID" -ne 0 ]; then
12
+ echo "❌ 请使用sudo运行此脚本"
13
+ exit 1
14
+ fi
15
+
16
+ # 获取域名
17
+ read -p "请输入您的域名 (例如: example.com): " DOMAIN
18
+ if [ -z "$DOMAIN" ]; then
19
+ echo "❌ 域名不能为空"
20
+ exit 1
21
+ fi
22
+
23
+ read -p "请输入您的邮箱地址: " EMAIL
24
+ if [ -z "$EMAIL" ]; then
25
+ echo "❌ 邮箱地址不能为空"
26
+ exit 1
27
+ fi
28
+
29
+ echo "域名: $DOMAIN"
30
+ echo "邮箱: $EMAIL"
31
+ echo
32
+
33
+ # 安装nginx
34
+ echo "📦 安装nginx..."
35
+ apt update
36
+ apt install -y nginx
37
+
38
+ # 安装certbot
39
+ echo "📦 安装certbot..."
40
+ apt install -y certbot python3-certbot-nginx
41
+
42
+ # 创建基本nginx配置
43
+ echo "⚙️ 创建nginx配置..."
44
+ cat > /etc/nginx/sites-available/chatapp << EOF
45
+ server {
46
+ listen 80;
47
+ server_name $DOMAIN www.$DOMAIN;
48
+
49
+ location /.well-known/acme-challenge/ {
50
+ root /var/www/html;
51
+ }
52
+
53
+ location / {
54
+ proxy_pass http://localhost:3000;
55
+ proxy_set_header Host \$host;
56
+ proxy_set_header X-Real-IP \$remote_addr;
57
+ proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
58
+ proxy_set_header X-Forwarded-Proto \$scheme;
59
+ }
60
+
61
+ location /api/ {
62
+ proxy_pass http://localhost:5000;
63
+ proxy_set_header Host \$host;
64
+ proxy_set_header X-Real-IP \$remote_addr;
65
+ proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
66
+ proxy_set_header X-Forwarded-Proto \$scheme;
67
+ }
68
+
69
+ location /socket.io/ {
70
+ proxy_pass http://localhost:5000;
71
+ proxy_http_version 1.1;
72
+ proxy_set_header Upgrade \$http_upgrade;
73
+ proxy_set_header Connection "upgrade";
74
+ proxy_set_header Host \$host;
75
+ proxy_set_header X-Real-IP \$remote_addr;
76
+ proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
77
+ proxy_set_header X-Forwarded-Proto \$scheme;
78
+ }
79
+ }
80
+ EOF
81
+
82
+ # 启用站点
83
+ ln -sf /etc/nginx/sites-available/chatapp /etc/nginx/sites-enabled/
84
+ rm -f /etc/nginx/sites-enabled/default
85
+
86
+ # 测试nginx配置
87
+ nginx -t
88
+
89
+ # 重启nginx
90
+ systemctl restart nginx
91
+ systemctl enable nginx
92
+
93
+ echo "✅ nginx配置完成"
94
+ echo
95
+
96
+ # 获取SSL证书
97
+ echo "🔒 获取SSL证书..."
98
+ certbot --nginx -d $DOMAIN -d www.$DOMAIN --email $EMAIL --agree-tos --no-eff-email
99
+
100
+ # 设置自动续期
101
+ echo "⏰ 设置证书自动续期..."
102
+ (crontab -l 2>/dev/null; echo "0 12 * * * /usr/bin/certbot renew --quiet") | crontab -
103
+
104
+ # 复制完整的nginx配置
105
+ echo "⚙️ 应用完整nginx配置..."
106
+ sed "s/your-domain.com/$DOMAIN/g" nginx-proxy.conf > /etc/nginx/sites-available/chatapp
107
+ nginx -t
108
+ systemctl reload nginx
109
+
110
+ echo
111
+ echo "🎉 SSL设置完成!"
112
+ echo
113
+ echo "🌐 您的网站现在可以通过以下地址访问:"
114
+ echo " - https://$DOMAIN"
115
+ echo " - https://www.$DOMAIN"
116
+ echo
117
+ echo "🔒 SSL证书信息:"
118
+ certbot certificates
119
+ echo
120
+ echo "📋 管理命令:"
121
+ echo " - 续期证书: sudo certbot renew"
122
+ echo " - 查看证书: sudo certbot certificates"
123
+ echo " - 测试续期: sudo certbot renew --dry-run"
start-dev.sh ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # 聊天应用开发环境启动脚本 - Linux版本
4
+
5
+ echo "🚀 启动聊天应用 (开发模式)..."
6
+ echo
7
+
8
+ # 检查Docker是否安装
9
+ if ! command -v docker &> /dev/null; then
10
+ echo "❌ 错误: Docker未安装"
11
+ echo "请先安装Docker: https://docs.docker.com/engine/install/"
12
+ exit 1
13
+ fi
14
+
15
+ # 检查Docker Compose是否安装
16
+ if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
17
+ echo "❌ 错误: Docker Compose未安装"
18
+ echo "请先安装Docker Compose: https://docs.docker.com/compose/install/"
19
+ exit 1
20
+ fi
21
+
22
+ # 检查Docker服务是否运行
23
+ if ! docker info &> /dev/null; then
24
+ echo "❌ 错误: Docker服务未运行"
25
+ echo "请启动Docker服务: sudo systemctl start docker"
26
+ exit 1
27
+ fi
28
+
29
+ echo "✅ Docker环境检查通过"
30
+ echo
31
+
32
+ # 构建并启动开发环境
33
+ echo "📦 构建并启动开发环境..."
34
+ if command -v docker-compose &> /dev/null; then
35
+ docker-compose -f docker-compose.dev.yml up --build
36
+ else
37
+ docker compose -f docker-compose.dev.yml up --build
38
+ fi
start.bat ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ echo 启动聊天应用...
3
+ echo.
4
+
5
+ echo 检查Docker是否运行...
6
+ docker --version >nul 2>&1
7
+ if %errorlevel% neq 0 (
8
+ echo 错误: Docker未安装或未运行
9
+ echo 请先安装Docker Desktop并确保其正在运行
10
+ pause
11
+ exit /b 1
12
+ )
13
+
14
+ echo Docker已就绪
15
+ echo.
16
+
17
+ echo 构建并启动服务...
18
+ docker-compose up --build -d
19
+
20
+ if %errorlevel% equ 0 (
21
+ echo.
22
+ echo ✅ 聊天应用启动成功!
23
+ echo.
24
+ echo 🌐 前端地址: http://localhost:3000
25
+ echo 🔧 后端API: http://localhost:5000
26
+ echo 📊 MongoDB: localhost:27017
27
+ echo.
28
+ echo 按任意键打开浏览器...
29
+ pause >nul
30
+ start http://localhost:3000
31
+ ) else (
32
+ echo.
33
+ echo ❌ 启动失败,请检查错误信息
34
+ pause
35
+ )
start.sh ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # 聊天应用启动脚本 - Linux版本
4
+
5
+ echo "🚀 启动聊天应用..."
6
+ echo
7
+
8
+ # 检查Docker是否安装
9
+ if ! command -v docker &> /dev/null; then
10
+ echo "❌ 错误: Docker未安装"
11
+ echo "请先安装Docker: https://docs.docker.com/engine/install/"
12
+ exit 1
13
+ fi
14
+
15
+ # 检查Docker Compose是否安装
16
+ if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
17
+ echo "❌ 错误: Docker Compose未安装"
18
+ echo "请先安装Docker Compose: https://docs.docker.com/compose/install/"
19
+ exit 1
20
+ fi
21
+
22
+ # 检查Docker服务是否运行
23
+ if ! docker info &> /dev/null; then
24
+ echo "❌ 错误: Docker服务未运行"
25
+ echo "请启动Docker服务: sudo systemctl start docker"
26
+ exit 1
27
+ fi
28
+
29
+ echo "✅ Docker环境检查通过"
30
+ echo
31
+
32
+ # 设置权限
33
+ chmod +x stop.sh
34
+
35
+ # 构建并启动服务
36
+ echo "📦 构建并启动服务..."
37
+ if command -v docker-compose &> /dev/null; then
38
+ docker-compose up --build -d
39
+ else
40
+ docker compose up --build -d
41
+ fi
42
+
43
+ if [ $? -eq 0 ]; then
44
+ echo
45
+ echo "🎉 聊天应用启动成功!"
46
+ echo
47
+ echo "🌐 前端地址: http://localhost:3000"
48
+ echo "🔧 后端API: http://localhost:5000"
49
+ echo "📊 MongoDB: localhost:27017"
50
+ echo
51
+ echo "📋 查看日志: docker-compose logs -f"
52
+ echo "🛑 停止应用: ./stop.sh"
53
+ echo
54
+
55
+ # 等待服务启动
56
+ echo "⏳ 等待服务启动..."
57
+ sleep 10
58
+
59
+ # 检查服务状态
60
+ echo "🔍 检查服务状态..."
61
+ if command -v docker-compose &> /dev/null; then
62
+ docker-compose ps
63
+ else
64
+ docker compose ps
65
+ fi
66
+
67
+ echo
68
+ echo "✨ 应用已就绪,请访问 http://localhost:3000"
69
+ else
70
+ echo
71
+ echo "❌ 启动失败,请检查错误信息"
72
+ echo "📋 查看日志: docker-compose logs"
73
+ exit 1
74
+ fi
stop.bat ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ echo 停止聊天应用...
3
+ echo.
4
+
5
+ docker-compose down
6
+
7
+ if %errorlevel% equ 0 (
8
+ echo.
9
+ echo ✅ 聊天应用已停止
10
+ ) else (
11
+ echo.
12
+ echo ❌ 停止失败,请检查错误信息
13
+ )
14
+
15
+ echo.
16
+ pause
stop.sh ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # 聊天应用停止脚本 - Linux版本
4
+
5
+ echo "🛑 停止聊天应用..."
6
+ echo
7
+
8
+ # 停止服务
9
+ if command -v docker-compose &> /dev/null; then
10
+ docker-compose down
11
+ else
12
+ docker compose down
13
+ fi
14
+
15
+ if [ $? -eq 0 ]; then
16
+ echo
17
+ echo "✅ 聊天应用已停止"
18
+ echo
19
+ echo "🧹 清理资源 (可选):"
20
+ echo " - 删除镜像: docker-compose down --rmi all"
21
+ echo " - 删除数据卷: docker-compose down -v"
22
+ echo " - 完全清理: docker-compose down -v --rmi all --remove-orphans"
23
+ else
24
+ echo
25
+ echo "❌ 停止失败,请检查错误信息"
26
+ exit 1
27
+ fi
test.sh ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # 聊天应用测试脚本
4
+
5
+ echo "🧪 聊天应用功能测试"
6
+ echo "=================="
7
+ echo
8
+
9
+ # 测试后端API
10
+ echo "🔧 测试后端API..."
11
+
12
+ # 健康检查
13
+ echo -n " 健康检查: "
14
+ if curl -f -s http://localhost:5000/api/health > /dev/null; then
15
+ echo "✅ 通过"
16
+ HEALTH=$(curl -s http://localhost:5000/api/health | jq -r '.status')
17
+ echo " 状态: $HEALTH"
18
+ else
19
+ echo "❌ 失败"
20
+ fi
21
+
22
+ # 测试注册API
23
+ echo -n " 注册API: "
24
+ REGISTER_RESPONSE=$(curl -s -X POST http://localhost:5000/api/register \
25
+ -H "Content-Type: application/json" \
26
+ -d '{
27
+ "username": "testuser",
28
+ "email": "[email protected]",
29
+ "password": "testpass123"
30
+ }')
31
+
32
+ if echo "$REGISTER_RESPONSE" | jq -e '.token' > /dev/null 2>&1; then
33
+ echo "✅ 通过"
34
+ TOKEN=$(echo "$REGISTER_RESPONSE" | jq -r '.token')
35
+ else
36
+ echo "⚠️ 用户可能已存在"
37
+ # 尝试登录
38
+ LOGIN_RESPONSE=$(curl -s -X POST http://localhost:5000/api/login \
39
+ -H "Content-Type: application/json" \
40
+ -d '{
41
+ "email": "[email protected]",
42
+ "password": "testpass123"
43
+ }')
44
+
45
+ if echo "$LOGIN_RESPONSE" | jq -e '.token' > /dev/null 2>&1; then
46
+ echo " 登录成功"
47
+ TOKEN=$(echo "$LOGIN_RESPONSE" | jq -r '.token')
48
+ else
49
+ echo " ❌ 登录失败"
50
+ TOKEN=""
51
+ fi
52
+ fi
53
+
54
+ # 测试获取消息API
55
+ if [ -n "$TOKEN" ]; then
56
+ echo -n " 消息API: "
57
+ if curl -f -s -H "Authorization: Bearer $TOKEN" http://localhost:5000/api/messages > /dev/null; then
58
+ echo "✅ 通过"
59
+ else
60
+ echo "❌ 失败"
61
+ fi
62
+ fi
63
+
64
+ echo
65
+
66
+ # 测试前端
67
+ echo "🌐 测试前端服务..."
68
+ echo -n " 前端可访问性: "
69
+ if curl -f -s http://localhost:3000 > /dev/null; then
70
+ echo "✅ 通过"
71
+ else
72
+ echo "❌ 失败"
73
+ fi
74
+
75
+ echo -n " 静态资源: "
76
+ if curl -f -s http://localhost:3000/vite.svg > /dev/null; then
77
+ echo "✅ 通过"
78
+ else
79
+ echo "❌ 失败"
80
+ fi
81
+
82
+ echo
83
+
84
+ # 测试数据库
85
+ echo "📊 测试数据库..."
86
+ echo -n " MongoDB连接: "
87
+ if docker exec chat-mongo mongosh --eval "db.adminCommand('ping')" > /dev/null 2>&1; then
88
+ echo "✅ 通过"
89
+
90
+ # 获取数据库统计
91
+ echo " 数据库统计:"
92
+ docker exec chat-mongo mongosh chatapp --eval "
93
+ print(' 用户数量: ' + db.users.countDocuments());
94
+ print(' 消息数量: ' + db.messages.countDocuments());
95
+ " 2>/dev/null
96
+ else
97
+ echo "❌ 失败"
98
+ fi
99
+
100
+ echo
101
+
102
+ # 测试Socket.IO连接
103
+ echo "🔌 测试Socket.IO..."
104
+ echo -n " WebSocket连接: "
105
+
106
+ # 使用Node.js测试Socket.IO连接
107
+ node -e "
108
+ const io = require('socket.io-client');
109
+ const socket = io('http://localhost:5000');
110
+
111
+ socket.on('connect', () => {
112
+ console.log('✅ 通过');
113
+ socket.disconnect();
114
+ process.exit(0);
115
+ });
116
+
117
+ socket.on('connect_error', (error) => {
118
+ console.log('❌ 失败:', error.message);
119
+ process.exit(1);
120
+ });
121
+
122
+ setTimeout(() => {
123
+ console.log('❌ 超时');
124
+ process.exit(1);
125
+ }, 5000);
126
+ " 2>/dev/null || echo "⚠️ 需要安装socket.io-client进行测试"
127
+
128
+ echo
129
+
130
+ # 性能测试
131
+ echo "⚡ 性能测试..."
132
+ echo -n " API响应时间: "
133
+ RESPONSE_TIME=$(curl -o /dev/null -s -w '%{time_total}' http://localhost:5000/api/health)
134
+ echo "${RESPONSE_TIME}s"
135
+
136
+ echo -n " 前端加载时间: "
137
+ FRONTEND_TIME=$(curl -o /dev/null -s -w '%{time_total}' http://localhost:3000)
138
+ echo "${FRONTEND_TIME}s"
139
+
140
+ echo
141
+
142
+ # 安全测试
143
+ echo "🔒 基础安全检查..."
144
+ echo -n " CORS配置: "
145
+ CORS_HEADER=$(curl -s -I http://localhost:5000/api/health | grep -i "access-control-allow-origin")
146
+ if [ -n "$CORS_HEADER" ]; then
147
+ echo "✅ 已配置"
148
+ else
149
+ echo "⚠️ 未检测到CORS头"
150
+ fi
151
+
152
+ echo -n " JWT验证: "
153
+ UNAUTH_RESPONSE=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:5000/api/messages)
154
+ if [ "$UNAUTH_RESPONSE" = "401" ]; then
155
+ echo "✅ 正常(未授权访问被拒绝)"
156
+ else
157
+ echo "⚠️ 可能存在安全问题"
158
+ fi
159
+
160
+ echo
161
+
162
+ # 总结
163
+ echo "📋 测试总结"
164
+ echo "==========="
165
+ echo "✅ 通过的测试将显示绿色勾号"
166
+ echo "❌ 失败的测试将显示红色叉号"
167
+ echo "⚠️ 警告或需要注意的项目将显示黄色感叹号"
168
+ echo
169
+ echo "💡 如果有测试失败,请检查:"
170
+ echo " 1. 服务是否正常启动"
171
+ echo " 2. 端口是否被占用"
172
+ echo " 3. 防火墙设置"
173
+ echo " 4. Docker容器状态"
174
+ echo
175
+ echo "🔧 查看详细日志: make logs"
176
+ echo "📊 查看监控信息: ./monitor.sh"