Upload 49 files
Browse files- .dockerignore +83 -0
- .gitignore +46 -0
- DEPLOYMENT.md +205 -0
- DOCKER.md +339 -0
- Dockerfile +115 -0
- Dockerfile.optimized +205 -0
- Makefile +152 -0
- build-docker.sh +165 -0
- client/.dockerignore +9 -0
- client/.env +1 -0
- client/Dockerfile +57 -0
- client/index.html +13 -0
- client/nginx.conf +35 -0
- client/package.json +35 -0
- client/postcss.config.js +6 -0
- client/public/vite.svg +1 -0
- client/src/App.tsx +97 -0
- client/src/components/Chat.tsx +225 -0
- client/src/components/Login.tsx +135 -0
- client/src/components/Register.tsx +192 -0
- client/src/index.css +58 -0
- client/src/main.tsx +10 -0
- client/src/types/index.ts +41 -0
- client/src/utils/api.ts +56 -0
- client/src/utils/socket.ts +75 -0
- client/tailwind.config.js +26 -0
- client/tsconfig.json +25 -0
- client/tsconfig.node.json +10 -0
- client/vite.config.ts +14 -0
- deploy.sh +130 -0
- docker-compare.sh +109 -0
- docker-compose.dev.yml +68 -0
- docker-compose.single.yml +61 -0
- docker-compose.yml +82 -0
- mongo-init.js +12 -0
- monitor.sh +98 -0
- nginx-proxy.conf +135 -0
- server/.dockerignore +8 -0
- server/.env +4 -0
- server/Dockerfile +35 -0
- server/index.js +280 -0
- server/package.json +26 -0
- setup-ssl.sh +123 -0
- start-dev.sh +38 -0
- start.bat +35 -0
- start.sh +74 -0
- stop.bat +16 -0
- stop.sh +27 -0
- test.sh +176 -0
.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"
|