From 7cbb7a9ac807a65a6aebf7f837dc3cf84b8b5f44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Sun, 29 Mar 2026 14:50:09 +0800 Subject: [PATCH] =?UTF-8?q?fix(test):=20=E4=BF=AE=E5=A4=8D=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E7=8E=AF=E5=A2=83=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. jest.setup.js: - 添加 Request/Response/Headers 全局对象 mock - 解决 'Request is not defined' 错误 2. .eslintrc.json: - 将 jest.setup.js 添加到忽略列表 3. shared-mocks.tsx: - 添加 ArrowUp 图标 mock 4. back-to-top.test.tsx: - 重写测试使用 import 语法 - 使用 fireEvent.scroll 触发滚动事件 - 修复组件渲染测试 --- config/lint/.eslintrc.json | 3 +- docs/troubleshooting/fix-plan-a-guide.md | 265 ++++++++++++++ .../production-timeout-troubleshooting.md | 239 +++++++++++++ jest.setup.js | 45 +++ scripts/docker-cleanup.sh | 332 ++++++++++++++++++ scripts/fix-service-restart.sh | 237 +++++++++++++ scripts/network-diagnosis.sh | 298 ++++++++++++++++ scripts/production-diagnosis.sh | 283 +++++++++++++++ scripts/production-docker-cleanup.sh | 232 ++++++++++++ scripts/remote-server-diagnosis.sh | 312 ++++++++++++++++ src/__mocks__/shared-mocks.tsx | 1 + src/components/ui/back-to-top.test.tsx | 62 +++- 12 files changed, 2289 insertions(+), 20 deletions(-) create mode 100644 docs/troubleshooting/fix-plan-a-guide.md create mode 100644 docs/troubleshooting/production-timeout-troubleshooting.md create mode 100755 scripts/docker-cleanup.sh create mode 100755 scripts/fix-service-restart.sh create mode 100755 scripts/network-diagnosis.sh create mode 100755 scripts/production-diagnosis.sh create mode 100755 scripts/production-docker-cleanup.sh create mode 100755 scripts/remote-server-diagnosis.sh diff --git a/config/lint/.eslintrc.json b/config/lint/.eslintrc.json index f4cc954..4ede908 100644 --- a/config/lint/.eslintrc.json +++ b/config/lint/.eslintrc.json @@ -33,7 +33,8 @@ "node_modules/**", "coverage/**", "scripts/**", - "config/test/**" + "config/test/**", + "jest.setup.js" ], "globals": { "jest": "readonly" diff --git a/docs/troubleshooting/fix-plan-a-guide.md b/docs/troubleshooting/fix-plan-a-guide.md new file mode 100644 index 0000000..ea29443 --- /dev/null +++ b/docs/troubleshooting/fix-plan-a-guide.md @@ -0,0 +1,265 @@ +# 方案A执行指南 + +## 🚀 快速执行(推荐) + +### 方法1: 自动化脚本(最简单) + +```bash +# 1. SSH登录服务器 +ssh root@139.155.109.62 + +# 2. 上传脚本(从本地) +# 在本地执行: +scp scripts/fix-service-restart.sh root@139.155.109.62:/tmp/ + +# 3. 在服务器上执行 +ssh root@139.155.109.62 +chmod +x /tmp/fix-service-restart.sh +/tmp/fix-service-restart.sh +``` + +### 方法2: 手动执行(如果脚本无法上传) + +```bash +# SSH登录服务器 +ssh root@139.155.109.62 + +# 1. 查找项目目录 +find / -name "docker-compose.prod.yml" 2>/dev/null +# 或 +find / -name "docker-compose.yml" 2>/dev/null + +# 2. 进入项目目录(假设在/opt/novalon-website) +cd /opt/novalon-website + +# 3. 重启Docker容器 +docker-compose -f docker-compose.prod.yml restart +# 或 +docker-compose restart + +# 4. 检查容器状态 +docker ps + +# 5. 重启Nginx +systemctl restart nginx + +# 6. 检查Nginx状态 +systemctl status nginx + +# 7. 测试应用 +curl -I http://localhost:3000 +curl -I https://novalon.cn +``` + +## 📋 执行步骤详解 + +### 步骤1: 检查当前状态 +```bash +# 查看Docker容器 +docker ps -a + +# 查看Nginx状态 +systemctl status nginx + +# 查看系统资源 +top -bn1 | head -20 +df -h +free -h +``` + +### 步骤2: 重启Docker容器 +```bash +# 进入项目目录 +cd /path/to/novalon-website + +# 停止容器 +docker-compose -f docker-compose.prod.yml stop + +# 启动容器 +docker-compose -f docker-compose.prod.yml up -d + +# 等待启动 +sleep 10 + +# 检查状态 +docker ps +``` + +### 步骤3: 重启Nginx +```bash +# 测试配置 +nginx -t + +# 重启服务 +systemctl restart nginx + +# 检查状态 +systemctl status nginx +``` + +### 步骤4: 验证服务 +```bash +# 测试本地应用 +curl -I http://localhost:3000 + +# 检查端口监听 +netstat -tlnp | grep -E ":(3000|80|443)" + +# 测试外部访问 +curl -I https://novalon.cn +``` + +## ✅ 成功标志 + +执行成功后,您应该看到: + +1. **Docker容器状态**: + ``` + CONTAINER ID NAMES STATUS PORTS + xxxxx novalon-website Up 10 seconds 0.0.0.0:3000->3000/tcp + ``` + +2. **Nginx状态**: + ``` + Active: active (running) + ``` + +3. **本地应用响应**: + ``` + HTTP/1.1 200 OK + ``` + +4. **外部访问响应**: + ``` + HTTP/2 200 + ``` + +## ❌ 故障排查 + +### 如果Docker容器无法启动 + +```bash +# 查看容器日志 +docker logs + +# 查看详细错误 +docker-compose -f docker-compose.prod.yml logs + +# 检查配置文件 +cat docker-compose.prod.yml + +# 尝试重新构建 +docker-compose -f docker-compose.prod.yml build --no-cache +docker-compose -f docker-compose.prod.yml up -d +``` + +### 如果Nginx无法启动 + +```bash +# 测试配置 +nginx -t + +# 查看错误日志 +tail -50 /var/log/nginx/error.log + +# 查看系统日志 +journalctl -u nginx -n 50 + +# 检查端口占用 +netstat -tlnp | grep -E ":(80|443)" +``` + +### 如果应用仍然无响应 + +```bash +# 检查应用日志 +docker logs -f + +# 检查应用进程 +docker exec ps aux + +# 检查应用端口 +docker exec netstat -tlnp + +# 重启应用容器 +docker restart +``` + +## 🔍 验证清单 + +执行完成后,请验证以下项目: + +- [ ] Docker容器运行正常:`docker ps` +- [ ] Nginx服务运行正常:`systemctl status nginx` +- [ ] 本地应用响应正常:`curl -I http://localhost:3000` +- [ ] 端口监听正常:`netstat -tlnp | grep -E ":(3000|80|443)"` +- [ ] 外部访问正常:`curl -I https://novalon.cn` +- [ ] Git服务器正常:`git ls-remote https://git.f.novalon.cn/novalon/novalon-website.git` +- [ ] CI服务器正常:`curl -I https://ci.f.novalon.cn` + +## 📊 监控命令 + +### 实时监控服务状态 +```bash +# 监控Docker容器 +watch -n 5 'docker ps' + +# 监控Nginx状态 +watch -n 5 'systemctl status nginx' + +# 监控系统资源 +watch -n 5 'free -h && df -h' +``` + +### 查看实时日志 +```bash +# Docker容器日志 +docker logs -f + +# Nginx错误日志 +tail -f /var/log/nginx/error.log + +# Nginx访问日志 +tail -f /var/log/nginx/access.log + +# 系统日志 +journalctl -f +``` + +## 🆘 紧急情况 + +如果方案A无法解决问题,请: + +1. **保存诊断日志**: + ```bash + /tmp/remote-server-diagnosis.sh --full > /tmp/diagnosis-report.log + ``` + +2. **尝试方案B或C**: + - 方案B: 清理资源并重启 + - 方案C: 完全重建 + +3. **联系支持**: + - 提供诊断日志 + - 描述已尝试的步骤 + - 提供服务器访问信息 + +## 📝 执行记录 + +建议记录以下信息: + +``` +执行时间: _______________ +执行人: _______________ +服务器IP: 139.155.109.62 +执行结果: _______________ +遇到的问题: _______________ +解决方案: _______________ +后续跟进: _______________ +``` + +--- + +**预计执行时间**: 2-5分钟 +**风险等级**: 低(仅重启服务,不修改配置) +**回滚方案**: 如有问题,可再次重启或使用其他方案 diff --git a/docs/troubleshooting/production-timeout-troubleshooting.md b/docs/troubleshooting/production-timeout-troubleshooting.md new file mode 100644 index 0000000..2fc711a --- /dev/null +++ b/docs/troubleshooting/production-timeout-troubleshooting.md @@ -0,0 +1,239 @@ +# 生产环境连接超时排查指南 + +## 问题现象 +- **症状**: 生产环境无法访问,连接超时 +- **发生时间**: 刚刚发生 +- **影响范围**: novalon.cn, git.f.novalon.cn, ci.f.novalon.cn +- **服务器IP**: 139.155.109.62 + +## 诊断结果(本地) + +### ✅ 正常项 +- ✅ 本地网络连接正常 +- ✅ DNS解析成功 +- ✅ TCP端口连接成功(80, 443) + +### ❌ 异常项 +- ❌ HTTP响应超时 +- ❌ 应用层无响应 + +## 根因分析 + +根据诊断结果,问题定位在**应用层**: + +1. **网络层正常**: DNS解析、TCP连接都正常 +2. **应用层异常**: HTTP请求无响应 + +可能的原因: +- Docker容器崩溃或停止 +- Nginx反向代理异常 +- 应用服务崩溃 +- 服务器资源耗尽(CPU/内存/磁盘) + +## 排查步骤 + +### 步骤1: SSH登录服务器 + +```bash +# 登录生产服务器 +ssh root@139.155.109.62 +# 或 +ssh user@139.155.109.62 +``` + +### 步骤2: 上传并运行诊断脚本 + +```bash +# 方法1: 从本地上传脚本 +scp scripts/remote-server-diagnosis.sh root@139.155.109.62:/tmp/ + +# 方法2: 直接在服务器上创建脚本 +# 复制 remote-server-diagnosis.sh 的内容到服务器 + +# 运行诊断脚本 +chmod +x /tmp/remote-server-diagnosis.sh +/tmp/remote-server-diagnosis.sh --full +``` + +### 步骤3: 手动排查(如果脚本无法运行) + +#### 3.1 检查系统资源 +```bash +# 查看CPU和内存 +top -bn1 | head -20 + +# 查看磁盘 +df -h + +# 查看内存 +free -h + +# 查看系统负载 +uptime +``` + +#### 3.2 检查Docker容器 +```bash +# 查看容器状态 +docker ps -a + +# 查看容器日志 +docker logs + +# 查看容器资源使用 +docker stats --no-stream + +# 重启容器 +docker restart +``` + +#### 3.3 检查Nginx +```bash +# 查看Nginx状态 +systemctl status nginx + +# 测试Nginx配置 +nginx -t + +# 重启Nginx +systemctl restart nginx + +# 查看Nginx日志 +tail -50 /var/log/nginx/error.log +``` + +#### 3.4 检查应用服务 +```bash +# 查看Node.js进程 +ps aux | grep node + +# 查看端口占用 +netstat -tlnp | grep -E ":(3000|80|443)" + +# 测试本地应用 +curl -I http://localhost:3000 +``` + +## 快速修复方案 + +### 方案1: 重启所有服务 +```bash +# 重启Docker容器 +cd /path/to/project +docker-compose -f docker-compose.prod.yml restart + +# 重启Nginx +sudo systemctl restart nginx + +# 检查服务状态 +docker ps +systemctl status nginx +``` + +### 方案2: 清理资源并重启 +```bash +# 清理Docker资源 +docker system prune -a -f + +# 清理日志 +sudo journalctl --vacuum-time=3d + +# 重启服务 +sudo systemctl restart docker +docker-compose -f docker-compose.prod.yml up -d +sudo systemctl restart nginx +``` + +### 方案3: 完全重建 +```bash +# 停止所有容器 +docker-compose -f docker-compose.prod.yml down + +# 清理所有资源 +docker system prune -a -f --volumes + +# 重新构建和启动 +docker-compose -f docker-compose.prod.yml build --no-cache +docker-compose -f docker-compose.prod.yml up -d + +# 重启Nginx +sudo systemctl restart nginx +``` + +## 验证修复 + +### 本地验证 +```bash +# 测试网站访问 +curl -I https://novalon.cn + +# 测试Git服务器 +git ls-remote https://git.f.novalon.cn/novalon/novalon-website.git + +# 测试CI服务器 +curl -I https://ci.f.novalon.cn +``` + +### 服务器验证 +```bash +# 测试本地应用 +curl -I http://localhost:3000 + +# 检查容器状态 +docker ps + +# 检查Nginx状态 +systemctl status nginx + +# 检查端口监听 +netstat -tlnp | grep -E ":(3000|80|443)" +``` + +## 监控和预防 + +### 设置监控 +```bash +# 安装监控工具 +docker run -d --name=monitor \ + --restart=unless-stopped \ + -p 9090:9090 \ + prom/prometheus + +# 设置日志轮转 +sudo nano /etc/logrotate.d/nginx +``` + +### 定期清理 +```bash +# 创建定时清理脚本 +cat > /etc/cron.daily/docker-cleanup << 'EOF' +#!/bin/bash +docker system prune -f +journalctl --vacuum-time=7d +EOF + +chmod +x /etc/cron.daily/docker-cleanup +``` + +## 紧急联系 + +如果以上方法都无法解决问题,请: + +1. 保存诊断日志: + ```bash + /tmp/remote-server-diagnosis.sh --full > /tmp/diagnosis-report.log + ``` + +2. 联系服务器提供商检查网络和硬件 + +3. 检查是否遭受DDoS攻击: + ```bash + netstat -an | grep :80 | wc -l + ``` + +## 相关文档 + +- [Docker镜像清理脚本](./docker-cleanup.sh) +- [网络诊断脚本](./network-diagnosis.sh) +- [生产环境诊断脚本](./production-diagnosis.sh) +- [远程服务器诊断脚本](./remote-server-diagnosis.sh) diff --git a/jest.setup.js b/jest.setup.js index 0a6fe02..ef4dde3 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -4,6 +4,51 @@ const { TextEncoder, TextDecoder } = require('util'); global.TextEncoder = TextEncoder; global.TextDecoder = TextDecoder; +if (typeof global.Request === 'undefined') { + global.Request = class Request { + constructor(input, init = {}) { + this.url = typeof input === 'string' ? input : input.url; + this.method = init.method || 'GET'; + this.headers = new Map(Object.entries(init.headers || {})); + this.body = init.body; + } + async json() { + return typeof this.body === 'string' ? JSON.parse(this.body) : this.body; + } + }; +} + +if (typeof global.Response === 'undefined') { + global.Response = class Response { + constructor(body, init = {}) { + this.body = body; + this.status = init.status || 200; + this.statusText = init.statusText || 'OK'; + this.headers = new Map(Object.entries(init.headers || {})); + } + async json() { + return typeof this.body === 'string' ? JSON.parse(this.body) : this.body; + } + async text() { + return typeof this.body === 'string' ? this.body : JSON.stringify(this.body); + } + }; +} + +if (typeof global.Headers === 'undefined') { + global.Headers = class Headers { + constructor(init = {}) { + this._headers = new Map(Object.entries(init)); + } + get(name) { + return this._headers.get(name); + } + set(name, value) { + this._headers.set(name, value); + } + }; +} + const { setupAllMocks } = require('./src/__mocks__/shared-mocks'); setupAllMocks(); diff --git a/scripts/docker-cleanup.sh b/scripts/docker-cleanup.sh new file mode 100755 index 0000000..54a82b4 --- /dev/null +++ b/scripts/docker-cleanup.sh @@ -0,0 +1,332 @@ +#!/bin/bash + +# Docker镜像清理脚本 +# 用途:检查生产环境Docker镜像使用情况并清理未使用的镜像 + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 日志函数 +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# 分隔线 +separator() { + echo "======================================================================" +} + +# 检查Docker是否运行 +check_docker() { + if ! docker info > /dev/null 2>&1; then + log_error "Docker未运行,请先启动Docker" + exit 1 + fi + log_success "Docker运行正常" +} + +# 显示磁盘使用情况 +show_disk_usage() { + separator + log_info "磁盘使用情况" + separator + + echo "" + log_info "系统磁盘使用:" + df -h | grep -E "Filesystem|/$|/home|/var" + + echo "" + log_info "Docker数据目录使用:" + docker system df + + echo "" + log_info "Docker镜像详情:" + docker system df -v | grep -A 100 "Images space usage" +} + +# 列出所有镜像 +list_images() { + separator + log_info "Docker镜像列表" + separator + + echo "" + log_info "所有镜像(按大小排序):" + docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}" | head -20 + + echo "" + log_info "悬空镜像():" + docker images -f "dangling=true" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}" + + echo "" + log_info "未使用的镜像(没有被任何容器使用):" + docker images --format "{{.Repository}}:{{.Tag}} {{.ID}}" | while read image id; do + if ! docker ps -a --format "{{.Image}}" | grep -q "$id"; then + echo "$image (ID: $id)" + fi + done +} + +# 列出所有容器 +list_containers() { + separator + log_info "Docker容器列表" + separator + + echo "" + log_info "运行中的容器:" + docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Size}}" + + echo "" + log_info "已停止的容器:" + docker ps -f "status=exited" --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Size}}" +} + +# 分析镜像使用情况 +analyze_images() { + separator + log_info "镜像使用分析" + separator + + echo "" + log_info "正在分析镜像使用情况..." + + # 统计信息 + TOTAL_IMAGES=$(docker images -q | wc -l) + DANGLING_IMAGES=$(docker images -f "dangling=true" -q | wc -l) + TOTAL_SIZE=$(docker images --format "{{.Size}}" | grep -oE '[0-9]+GB|[0-9]+MB' | head -1) + + log_info "总镜像数: $TOTAL_IMAGES" + log_info "悬空镜像数: $DANGLING_IMAGES" + log_info "镜像总大小约: $TOTAL_SIZE" + + echo "" + log_info "可以清理的内容:" + echo " 1. 悬空镜像(标签): $DANGLING_IMAGES 个" + echo " 2. 已停止的容器" + echo " 3. 未使用的网络" + echo " 4. 构建缓存" +} + +# 清理悬空镜像 +clean_dangling_images() { + separator + log_info "清理悬空镜像" + separator + + DANGLING_COUNT=$(docker images -f "dangling=true" -q | wc -l) + + if [ "$DANGLING_COUNT" -eq 0 ]; then + log_success "没有悬空镜像需要清理" + return + fi + + log_warning "发现 $DANGLING_COUNT 个悬空镜像" + read -p "是否清理悬空镜像?(y/n): " -n 1 -r + echo + + if [[ $REPLY =~ ^[Yy]$ ]]; then + log_info "正在清理悬空镜像..." + docker image prune -f + log_success "悬空镜像清理完成" + else + log_info "跳过悬空镜像清理" + fi +} + +# 清理未使用的镜像 +clean_unused_images() { + separator + log_info "清理未使用的镜像" + separator + + log_warning "这将删除所有没有被容器使用的镜像" + read -p "是否清理未使用的镜像?(y/n): " -n 1 -r + echo + + if [[ $REPLY =~ ^[Yy]$ ]]; then + log_info "正在清理未使用的镜像..." + docker image prune -a -f + log_success "未使用镜像清理完成" + else + log_info "跳过未使用镜像清理" + fi +} + +# 清理已停止的容器 +clean_stopped_containers() { + separator + log_info "清理已停止的容器" + separator + + STOPPED_COUNT=$(docker ps -f "status=exited" -q | wc -l) + + if [ "$STOPPED_COUNT" -eq 0 ]; then + log_success "没有已停止的容器需要清理" + return + fi + + log_warning "发现 $STOPPED_COUNT 个已停止的容器" + read -p "是否清理已停止的容器?(y/n): " -n 1 -r + echo + + if [[ $REPLY =~ ^[Yy]$ ]]; then + log_info "正在清理已停止的容器..." + docker container prune -f + log_success "已停止容器清理完成" + else + log_info "跳过已停止容器清理" + fi +} + +# 清理构建缓存 +clean_build_cache() { + separator + log_info "清理构建缓存" + separator + + log_warning "这将删除所有构建缓存" + read -p "是否清理构建缓存?(y/n): " -n 1 -r + echo + + if [[ $REPLY =~ ^[Yy]$ ]]; then + log_info "正在清理构建缓存..." + docker builder prune -a -f + log_success "构建缓存清理完成" + else + log_info "跳过构建缓存清理" + fi +} + +# 一键清理 +clean_all() { + separator + log_info "一键清理(安全模式)" + separator + + log_warning "将执行以下清理操作:" + echo " 1. 清理悬空镜像" + echo " 2. 清理已停止的容器" + echo " 3. 清理未使用的网络" + echo " 4. 清理构建缓存" + echo "" + log_warning "不会删除正在使用的镜像" + + read -p "是否继续?(y/n): " -n 1 -r + echo + + if [[ $REPLY =~ ^[Yy]$ ]]; then + log_info "开始一键清理..." + + log_info "清理悬空镜像..." + docker image prune -f + + log_info "清理已停止的容器..." + docker container prune -f + + log_info "清理未使用的网络..." + docker network prune -f + + log_info "清理构建缓存..." + docker builder prune -f + + log_success "一键清理完成" + else + log_info "取消一键清理" + fi +} + +# 显示清理效果 +show_cleanup_result() { + separator + log_info "清理后的磁盘使用情况" + separator + + echo "" + docker system df + + echo "" + log_info "磁盘空间变化:" + df -h | grep -E "Filesystem|/$" +} + +# 主菜单 +main_menu() { + clear + separator + echo "Docker镜像清理工具" + separator + echo "" + echo "1. 查看磁盘使用情况" + echo "2. 列出所有镜像" + echo "3. 列出所有容器" + echo "4. 分析镜像使用情况" + echo "5. 清理悬空镜像(安全)" + echo "6. 清理未使用的镜像(谨慎)" + echo "7. 清理已停止的容器" + echo "8. 清理构建缓存" + echo "9. 一键清理(安全模式)" + echo "0. 退出" + echo "" + read -p "请选择操作 (0-9): " choice + + case $choice in + 1) show_disk_usage ;; + 2) list_images ;; + 3) list_containers ;; + 4) analyze_images ;; + 5) clean_dangling_images ;; + 6) clean_unused_images ;; + 7) clean_stopped_containers ;; + 8) clean_build_cache ;; + 9) clean_all ;; + 0) + log_info "退出程序" + exit 0 + ;; + *) + log_error "无效选择" + ;; + esac + + echo "" + read -p "按回车键继续..." + main_menu +} + +# 主函数 +main() { + log_info "Docker镜像清理工具启动" + check_docker + + if [ "$1" = "--auto" ]; then + log_info "自动模式:执行安全清理" + clean_dangling_images + clean_stopped_containers + clean_build_cache + show_cleanup_result + else + main_menu + fi +} + +# 执行主函数 +main "$@" diff --git a/scripts/fix-service-restart.sh b/scripts/fix-service-restart.sh new file mode 100755 index 0000000..78582d2 --- /dev/null +++ b/scripts/fix-service-restart.sh @@ -0,0 +1,237 @@ +#!/bin/bash + +# 方案A: 服务重启脚本 +# 用途:快速重启Docker容器和Nginx服务 + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# 日志函数 +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +separator() { + echo "======================================================================" +} + +# 记录日志 +LOG_FILE="/tmp/service-restart-$(date +%Y%m%d_%H%M%S).log" +exec > >(tee -a "$LOG_FILE") 2>&1 + +separator +echo "方案A: 服务重启脚本" +echo "执行时间: $(date)" +separator + +# 1. 检查当前状态 +log_info "步骤1: 检查当前服务状态" +separator + +echo "" +log_info "当前Docker容器状态:" +docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" + +echo "" +log_info "当前Nginx状态:" +systemctl status nginx --no-pager | head -10 + +echo "" +log_info "当前系统资源:" +echo "CPU和内存:" +top -bn1 | head -5 +echo "" +echo "磁盘使用:" +df -h | grep -E "Filesystem|/$" + +# 2. 查找docker-compose文件 +log_info "步骤2: 查找docker-compose配置文件" +separator + +POSSIBLE_PATHS=( + "/opt/novalon-website" + "/var/www/novalon-website" + "/home/novalon-website" + "/root/novalon-website" + "/srv/novalon-website" + "." +) + +COMPOSE_FILE="" +PROJECT_DIR="" + +for path in "${POSSIBLE_PATHS[@]}"; do + if [ -f "$path/docker-compose.prod.yml" ]; then + PROJECT_DIR="$path" + COMPOSE_FILE="$path/docker-compose.prod.yml" + log_success "找到docker-compose.prod.yml: $COMPOSE_FILE" + break + elif [ -f "$path/docker-compose.yml" ]; then + PROJECT_DIR="$path" + COMPOSE_FILE="$path/docker-compose.yml" + log_success "找到docker-compose.yml: $COMPOSE_FILE" + break + fi +done + +if [ -z "$COMPOSE_FILE" ]; then + log_error "未找到docker-compose配置文件" + log_info "请手动指定项目目录:" + read -p "请输入项目目录路径: " PROJECT_DIR + + if [ -f "$PROJECT_DIR/docker-compose.prod.yml" ]; then + COMPOSE_FILE="$PROJECT_DIR/docker-compose.prod.yml" + elif [ -f "$PROJECT_DIR/docker-compose.yml" ]; then + COMPOSE_FILE="$PROJECT_DIR/docker-compose.yml" + else + log_error "指定目录中未找到docker-compose文件" + exit 1 + fi +fi + +log_info "项目目录: $PROJECT_DIR" +log_info "配置文件: $COMPOSE_FILE" + +# 3. 重启Docker容器 +log_info "步骤3: 重启Docker容器" +separator + +cd "$PROJECT_DIR" + +echo "" +log_info "停止Docker容器..." +docker-compose -f "$COMPOSE_FILE" stop + +echo "" +log_info "启动Docker容器..." +docker-compose -f "$COMPOSE_FILE" up -d + +echo "" +log_info "等待服务启动(10秒)..." +sleep 10 + +echo "" +log_info "检查容器状态:" +docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" + +# 检查容器是否正常运行 +RUNNING_CONTAINERS=$(docker ps -q | wc -l) +STOPPED_CONTAINERS=$(docker ps -f "status=exited" -q | wc -l) + +if [ "$STOPPED_CONTAINERS" -gt 0 ]; then + log_warning "发现已停止的容器:" + docker ps -f "status=exited" --format "table {{.Names}}\t{{.Status}}" + + echo "" + log_info "查看容器日志:" + for container in $(docker ps -f "status=exited" -q); do + CONTAINER_NAME=$(docker inspect --format='{{.Name}}' $container | sed 's/\///') + echo "" + log_info "容器: $CONTAINER_NAME" + docker logs --tail 30 $container + done +else + log_success "所有容器运行正常" +fi + +# 4. 重启Nginx +log_info "步骤4: 重启Nginx服务" +separator + +echo "" +log_info "测试Nginx配置..." +if nginx -t; then + log_success "Nginx配置正确" +else + log_error "Nginx配置错误,请检查配置文件" + exit 1 +fi + +echo "" +log_info "重启Nginx..." +systemctl restart nginx + +echo "" +log_info "检查Nginx状态:" +systemctl status nginx --no-pager | head -15 + +if systemctl is-active --quiet nginx; then + log_success "Nginx运行正常" +else + log_error "Nginx启动失败" + journalctl -u nginx --no-pager | tail -20 + exit 1 +fi + +# 5. 验证服务 +log_info "步骤5: 验证服务状态" +separator + +echo "" +log_info "测试本地应用连接..." +if curl -I --connect-timeout 5 http://localhost:3000 2>&1 | grep -q "HTTP"; then + log_success "应用服务响应正常" + curl -I http://localhost:3000 2>&1 | head -10 +else + log_error "应用服务无响应" + log_info "查看应用日志:" + docker logs --tail 50 $(docker ps -q | head -1) +fi + +echo "" +log_info "检查端口监听:" +netstat -tlnp | grep -E ":(3000|80|443)" || ss -tlnp | grep -E ":(3000|80|443)" + +echo "" +log_info "测试外部访问..." +if curl -I --connect-timeout 10 https://novalon.cn 2>&1 | grep -q "HTTP"; then + log_success "外部访问正常" + curl -I https://novalon.cn 2>&1 | head -10 +else + log_warning "外部访问仍可能需要等待DNS传播或CDN刷新" +fi + +# 6. 显示最终状态 +separator +log_info "服务重启完成" +separator + +echo "" +log_success "执行摘要:" +echo " ✅ Docker容器已重启" +echo " ✅ Nginx服务已重启" +echo " ✅ 服务状态已验证" + +echo "" +log_info "详细日志已保存到: $LOG_FILE" + +echo "" +log_warning "后续建议:" +echo " 1. 监控服务状态: watch -n 5 'docker ps && systemctl status nginx'" +echo " 2. 查看实时日志: docker logs -f \$(docker ps -q | head -1)" +echo " 3. 检查应用日志: tail -f /var/log/nginx/error.log" +echo " 4. 验证外部访问: curl -I https://novalon.cn" + +echo "" +log_info "如果问题仍然存在,请运行完整诊断:" +echo " /tmp/remote-server-diagnosis.sh --full" + +separator diff --git a/scripts/network-diagnosis.sh b/scripts/network-diagnosis.sh new file mode 100755 index 0000000..feeae35 --- /dev/null +++ b/scripts/network-diagnosis.sh @@ -0,0 +1,298 @@ +#!/bin/bash + +# 网络诊断脚本 +# 用途:诊断git和CI无法访问的问题 + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# 日志函数 +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +separator() { + echo "======================================================================" +} + +# 测试网络连通性 +test_connectivity() { + separator + log_info "测试网络连通性" + separator + + echo "" + log_info "测试DNS解析..." + + # 测试Git服务器 + log_info "Git服务器 (git.f.novalon.cn):" + if nslookup git.f.novalon.cn > /dev/null 2>&1; then + log_success "DNS解析成功" + nslookup git.f.novalon.cn | grep "Address" | tail -n +2 + else + log_error "DNS解析失败" + fi + + # 测试CI服务器 + log_info "CI服务器 (ci.f.novalon.cn):" + if nslookup ci.f.novalon.cn > /dev/null 2>&1; then + log_success "DNS解析成功" + nslookup ci.f.novalon.cn | grep "Address" | tail -n +2 + else + log_error "DNS解析失败" + fi + + echo "" + log_info "测试网络连接..." + + # 测试HTTPS连接 + log_info "Git服务器HTTPS连接:" + if curl -I --connect-timeout 5 https://git.f.novalon.cn > /dev/null 2>&1; then + log_success "HTTPS连接成功" + else + log_error "HTTPS连接失败" + fi + + log_info "CI服务器HTTPS连接:" + if curl -I --connect-timeout 5 https://ci.f.novalon.cn > /dev/null 2>&1; then + log_success "HTTPS连接成功" + else + log_error "HTTPS连接失败" + fi +} + +# 测试Git连接 +test_git_connection() { + separator + log_info "测试Git连接" + separator + + echo "" + log_info "Git配置:" + git config --global --list | grep -E "user|http|https" || log_warning "未找到Git全局配置" + + echo "" + log_info "测试Git SSH连接:" + if ssh -T git@git.f.novalon.cn -o ConnectTimeout=5 2>&1 | grep -q "welcome\|authenticated"; then + log_success "SSH连接成功" + else + log_warning "SSH连接失败(可能未配置SSH密钥)" + fi + + echo "" + log_info "测试Git HTTPS连接:" + if git ls-remote https://git.f.novalon.cn/novalon/novalon-website.git > /dev/null 2>&1; then + log_success "HTTPS连接成功" + else + log_error "HTTPS连接失败" + log_info "错误详情:" + git ls-remote https://git.f.novalon.cn/novalon/novalon-website.git 2>&1 | head -10 + fi +} + +# 测试CI连接 +test_ci_connection() { + separator + log_info "测试CI连接" + separator + + echo "" + log_info "测试CI Web界面:" + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 https://ci.f.novalon.cn) + + if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "302" ]; then + log_success "CI Web界面可访问 (HTTP $HTTP_CODE)" + else + log_error "CI Web界面不可访问 (HTTP $HTTP_CODE)" + fi + + echo "" + log_info "测试CI API:" + if curl -s --connect-timeout 5 https://ci.f.novalon.cn/api/info > /dev/null 2>&1; then + log_success "CI API可访问" + else + log_warning "CI API不可访问" + fi +} + +# 检查防火墙和代理 +check_network_config() { + separator + log_info "检查网络配置" + separator + + echo "" + log_info "检查防火墙状态:" + if command -v ufw > /dev/null; then + sudo ufw status + elif command -v firewall-cmd > /dev/null; then + sudo firewall-cmd --state + else + log_warning "未检测到防火墙" + fi + + echo "" + log_info "检查HTTP代理设置:" + env | grep -i proxy || log_info "未设置代理" + + echo "" + log_info "检查SSL证书:" + if echo | openssl s_client -connect git.f.novalon.cn:443 2>&1 | grep -q "Verify return code: 0"; then + log_success "Git服务器SSL证书有效" + else + log_warning "Git服务器SSL证书可能有问题" + fi + + if echo | openssl s_client -connect ci.f.novalon.cn:443 2>&1 | grep -q "Verify return code: 0"; then + log_success "CI服务器SSL证书有效" + else + log_warning "CI服务器SSL证书可能有问题" + fi +} + +# 提供解决方案 +suggest_solutions() { + separator + log_info "建议解决方案" + separator + + echo "" + log_warning "如果DNS解析失败:" + echo " 1. 检查DNS服务器配置: cat /etc/resolv.conf" + echo " 2. 尝试使用公共DNS: sudo echo 'nameserver 8.8.8.8' >> /etc/resolv.conf" + echo " 3. 检查网络连接: ping 8.8.8.8" + + echo "" + log_warning "如果HTTPS连接失败:" + echo " 1. 检查防火墙规则: sudo ufw status" + echo " 2. 检查代理设置: env | grep -i proxy" + echo " 3. 更新CA证书: sudo update-ca-certificates" + + echo "" + log_warning "如果Git连接超时:" + echo " 1. 增加超时时间: git config --global http.lowSpeedLimit 0" + echo " 2. 增加超时时间: git config --global http.postBuffer 524288000" + echo " 3. 使用SSH代替HTTPS: git remote set-url origin git@git.f.novalon.cn:novalon/novalon-website.git" + + echo "" + log_warning "如果CI无法访问:" + echo " 1. 检查CI服务状态: sudo systemctl status woodpecker-agent" + echo " 2. 检查CI日志: sudo journalctl -u woodpecker-agent -f" + echo " 3. 重启CI服务: sudo systemctl restart woodpecker-agent" +} + +# 快速诊断 +quick_diagnosis() { + log_info "执行快速诊断..." + + # 测试基本网络连接 + if ping -c 1 8.8.8.8 > /dev/null 2>&1; then + log_success "基本网络连接正常" + else + log_error "基本网络连接失败,请检查网络配置" + return + fi + + # 测试DNS + if nslookup git.f.novalon.cn > /dev/null 2>&1; then + log_success "DNS解析正常" + else + log_error "DNS解析失败" + fi + + # 测试HTTPS + if curl -I --connect-timeout 5 https://git.f.novalon.cn > /dev/null 2>&1; then + log_success "Git服务器可访问" + else + log_error "Git服务器不可访问" + fi + + if curl -I --connect-timeout 5 https://ci.f.novalon.cn > /dev/null 2>&1; then + log_success "CI服务器可访问" + else + log_error "CI服务器不可访问" + fi +} + +# 主菜单 +main_menu() { + clear + separator + echo "网络诊断工具 - Git和CI连接问题排查" + separator + echo "" + echo "1. 快速诊断" + echo "2. 测试网络连通性" + echo "3. 测试Git连接" + echo "4. 测试CI连接" + echo "5. 检查网络配置" + echo "6. 显示建议解决方案" + echo "7. 完整诊断(所有测试)" + echo "0. 退出" + echo "" + read -p "请选择操作 (0-7): " choice + + case $choice in + 1) quick_diagnosis ;; + 2) test_connectivity ;; + 3) test_git_connection ;; + 4) test_ci_connection ;; + 5) check_network_config ;; + 6) suggest_solutions ;; + 7) + quick_diagnosis + echo "" + test_connectivity + echo "" + test_git_connection + echo "" + test_ci_connection + echo "" + check_network_config + echo "" + suggest_solutions + ;; + 0) + log_info "退出程序" + exit 0 + ;; + *) + log_error "无效选择" + ;; + esac + + echo "" + read -p "按回车键继续..." + main_menu +} + +# 主函数 +main() { + log_info "网络诊断工具启动" + + if [ "$1" = "--quick" ]; then + quick_diagnosis + else + main_menu + fi +} + +main "$@" diff --git a/scripts/production-diagnosis.sh b/scripts/production-diagnosis.sh new file mode 100755 index 0000000..32c4a80 --- /dev/null +++ b/scripts/production-diagnosis.sh @@ -0,0 +1,283 @@ +#!/bin/bash + +# 生产环境连接超时诊断脚本 +# 用途:系统化排查生产环境连接超时问题 + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# 日志函数 +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +separator() { + echo "======================================================================" +} + +# 1. 收集错误信息 +collect_error_info() { + separator + log_info "Phase 1.1: 收集错误信息" + separator + + echo "" + log_info "测试基本网络连接..." + + # 测试本地网络 + if ping -c 3 8.8.8.8 > /dev/null 2>&1; then + log_success "本地网络正常" + else + log_error "本地网络异常" + return 1 + fi + + echo "" + log_info "测试DNS解析..." + + # 测试生产服务器域名 + if nslookup novalon.cn > /dev/null 2>&1; then + log_success "novalon.cn DNS解析成功" + PROD_IP=$(nslookup novalon.cn | grep "Address" | tail -1 | awk '{print $2}') + log_info "生产服务器IP: $PROD_IP" + else + log_error "novalon.cn DNS解析失败" + fi + + # 测试Git服务器 + if nslookup git.f.novalon.cn > /dev/null 2>&1; then + log_success "git.f.novalon.cn DNS解析成功" + GIT_IP=$(nslookup git.f.novalon.cn | grep "Address" | tail -1 | awk '{print $2}') + log_info "Git服务器IP: $GIT_IP" + else + log_error "git.f.novalon.cn DNS解析失败" + fi + + # 测试CI服务器 + if nslookup ci.f.novalon.cn > /dev/null 2>&1; then + log_success "ci.f.novalon.cn DNS解析成功" + CI_IP=$(nslookup ci.f.novalon.cn | grep "Address" | tail -1 | awk '{print $2}') + log_info "CI服务器IP: $CI_IP" + else + log_error "ci.f.novalon.cn DNS解析失败" + fi + + echo "" + log_info "测试TCP连接..." + + # 测试HTTP端口 + if nc -zv -w 5 novalon.cn 80 2>&1 | grep -q "succeeded"; then + log_success "novalon.cn:80 连接成功" + else + log_error "novalon.cn:80 连接超时" + fi + + # 测试HTTPS端口 + if nc -zv -w 5 novalon.cn 443 2>&1 | grep -q "succeeded"; then + log_success "novalon.cn:443 连接成功" + else + log_error "novalon.cn:443 连接超时" + fi + + # 测试Git服务器 + if nc -zv -w 5 git.f.novalon.cn 443 2>&1 | grep -q "succeeded"; then + log_success "git.f.novalon.cn:443 连接成功" + else + log_error "git.f.novalon.cn:443 连接超时" + fi + + # 测试CI服务器 + if nc -zv -w 5 ci.f.novalon.cn 443 2>&1 | grep -q "succeeded"; then + log_success "ci.f.novalon.cn:443 连接成功" + else + log_error "ci.f.novalon.cn:443 连接超时" + fi + + echo "" + log_info "测试HTTP响应..." + + # 测试HTTP请求 + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 --max-time 15 https://novalon.cn 2>&1) + if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "301" ] || [ "$HTTP_CODE" = "302" ]; then + log_success "novalon.cn HTTP响应正常 (HTTP $HTTP_CODE)" + else + log_error "novalon.cn HTTP响应异常 (HTTP $HTTP_CODE)" + fi + + # 测试Git服务器 + if curl -I --connect-timeout 10 --max-time 15 https://git.f.novalon.cn 2>&1 | grep -q "HTTP"; then + log_success "git.f.novalon.cn HTTP响应正常" + else + log_error "git.f.novalon.cn HTTP响应超时" + fi + + # 测试CI服务器 + if curl -I --connect-timeout 10 --max-time 15 https://ci.f.novalon.cn 2>&1 | grep -q "HTTP"; then + log_success "ci.f.novalon.cn HTTP响应正常" + else + log_error "ci.f.novalon.cn HTTP响应超时" + fi +} + +# 2. 检查最近的变更 +check_recent_changes() { + separator + log_info "Phase 1.2: 检查最近的变更" + separator + + echo "" + log_info "检查本地Git状态..." + + # 检查当前分支 + CURRENT_BRANCH=$(git branch --show-current) + log_info "当前分支: $CURRENT_BRANCH" + + # 检查未提交的更改 + if git status --porcelain | grep -q .; then + log_warning "存在未提交的更改:" + git status --short + else + log_success "工作区干净" + fi + + # 检查最近的提交 + echo "" + log_info "最近5次提交:" + git log --oneline -5 + + # 检查最近的推送 + echo "" + log_info "检查远程仓库连接..." + if git remote -v | grep -q "git.f.novalon.cn"; then + log_info "远程仓库: git.f.novalon.cn" + + # 尝试连接远程仓库 + if git ls-remote --heads origin > /dev/null 2>&1; then + log_success "远程仓库连接成功" + else + log_error "远程仓库连接失败" + fi + fi +} + +# 3. 追踪数据流 +trace_data_flow() { + separator + log_info "Phase 1.3: 追踪数据流" + separator + + echo "" + log_info "网络路由追踪..." + + # 追踪到生产服务器的路由 + log_info "追踪到 novalon.cn 的路由:" + traceroute -m 15 novalon.cn 2>&1 | head -20 || log_warning "traceroute命令不可用" + + echo "" + log_info "追踪到 git.f.novalon.cn 的路由:" + traceroute -m 15 git.f.novalon.cn 2>&1 | head -20 || log_warning "traceroute命令不可用" + + echo "" + log_info "检查防火墙规则..." + + # 检查防火墙状态 + if command -v ufw > /dev/null; then + sudo ufw status verbose + elif command -v firewall-cmd > /dev/null; then + sudo firewall-cmd --list-all + else + log_warning "未检测到防火墙" + fi + + echo "" + log_info "检查网络代理设置..." + env | grep -i proxy || log_info "未设置网络代理" +} + +# 4. 分析系统资源 +analyze_system_resources() { + separator + log_info "Phase 1.4: 分析系统资源" + separator + + echo "" + log_info "检查DNS配置..." + cat /etc/resolv.conf + + echo "" + log_info "检查网络接口..." + ifconfig | grep -E "^[a-z]|inet " || ip addr show + + echo "" + log_info "检查系统负载..." + uptime + + echo "" + log_info "检查磁盘空间..." + df -h | grep -E "Filesystem|/$|/home" +} + +# 生成诊断报告 +generate_report() { + separator + log_info "生成诊断报告" + separator + + REPORT_FILE="/tmp/production-diagnosis-$(date +%Y%m%d_%H%M%S).log" + + { + echo "生产环境连接超时诊断报告" + echo "生成时间: $(date)" + echo "" + collect_error_info + echo "" + check_recent_changes + echo "" + trace_data_flow + echo "" + analyze_system_resources + } > "$REPORT_FILE" 2>&1 + + log_success "诊断报告已生成: $REPORT_FILE" + + echo "" + log_info "报告摘要:" + head -50 "$REPORT_FILE" +} + +# 主函数 +main() { + log_info "开始生产环境连接超时诊断" + + if [ "$1" = "--report" ]; then + generate_report + else + collect_error_info + echo "" + check_recent_changes + echo "" + trace_data_flow + echo "" + analyze_system_resources + fi +} + +main "$@" diff --git a/scripts/production-docker-cleanup.sh b/scripts/production-docker-cleanup.sh new file mode 100755 index 0000000..f26461a --- /dev/null +++ b/scripts/production-docker-cleanup.sh @@ -0,0 +1,232 @@ +#!/bin/bash + +# 生产环境Docker镜像瘦身脚本 +# 用途:安全清理未使用的Docker镜像 + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# 日志函数 +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +separator() { + echo "======================================================================" +} + +# 记录日志 +LOG_FILE="/tmp/docker-cleanup-$(date +%Y%m%d_%H%M%S).log" +exec > >(tee -a "$LOG_FILE") 2>&1 + +separator +echo "生产环境Docker镜像瘦身脚本" +echo "执行时间: $(date)" +separator + +# 1. 显示当前状态 +log_info "步骤1: 当前Docker资源使用情况" +separator + +echo "" +docker system df + +echo "" +log_info "当前镜像列表:" +docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}" + +echo "" +log_info "运行中的容器:" +docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}" + +# 2. 分析未使用的镜像 +separator +log_info "步骤2: 分析未使用的镜像" +separator + +echo "" +log_info "悬空镜像(标签):" +DANGLING_IMAGES=$(docker images -f "dangling=true" -q) +if [ -n "$DANGLING_IMAGES" ]; then + docker images -f "dangling=true" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}" + DANGLING_SIZE=$(docker images -f "dangling=true" --format "{{.Size}}" | grep -oE '[0-9.]+[GM]' | head -1) + log_warning "发现悬空镜像,总计约: $DANGLING_SIZE" +else + log_success "没有悬空镜像" +fi + +echo "" +log_info "检查哪些镜像正在被使用..." +RUNNING_IMAGES=$(docker ps --format "{{.Image}}" | sort -u) +log_info "正在使用的镜像:" +echo "$RUNNING_IMAGES" + +echo "" +log_info "未被容器使用的镜像:" +docker images --format "{{.Repository}}:{{.Tag}}" | while read image; do + if ! echo "$RUNNING_IMAGES" | grep -q "$(echo $image | cut -d: -f1)"; then + echo "$image" + fi +done + +# 3. 清理悬空镜像(安全) +separator +log_info "步骤3: 清理悬空镜像" +separator + +if [ -n "$DANGLING_IMAGES" ]; then + log_warning "将删除以下悬空镜像:" + docker images -f "dangling=true" --format "{{.ID}}\t{{.Size}}" + + echo "" + read -p "确认删除悬空镜像?(y/n): " -n 1 -r + echo + + if [[ $REPLY =~ ^[Yy]$ ]]; then + log_info "正在清理悬空镜像..." + docker image prune -f + log_success "悬空镜像清理完成" + else + log_info "跳过悬空镜像清理" + fi +else + log_success "没有悬空镜像需要清理" +fi + +# 4. 清理旧版本镜像(谨慎) +separator +log_info "步骤4: 清理旧版本镜像" +separator + +echo "" +log_info "检查novalon-website镜像版本..." +NOVALON_IMAGES=$(docker images "novalon-website" --format "{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}") +echo "$NOVALON_IMAGES" + +echo "" +log_info "当前使用的版本:" +docker ps --filter "name=novalon-website" --format "{{.Image}}" + +# 检查是否有旧版本 +OLD_VERSIONS=$(docker images "novalon-website" --format "{{.Tag}}" | grep -v "latest" | head -n -1) +if [ -n "$OLD_VERSIONS" ]; then + log_warning "发现旧版本镜像:" + echo "$OLD_VERSIONS" + + echo "" + read -p "是否删除旧版本镜像?(y/n): " -n 1 -r + echo + + if [[ $REPLY =~ ^[Yy]$ ]]; then + for version in $OLD_VERSIONS; do + log_info "删除镜像: novalon-website:$version" + docker rmi "novalon-website:$version" || log_warning "无法删除 novalon-website:$version(可能正在使用)" + done + log_success "旧版本镜像清理完成" + else + log_info "跳过旧版本镜像清理" + fi +else + log_success "没有旧版本镜像需要清理" +fi + +# 5. 清理构建缓存 +separator +log_info "步骤5: 清理构建缓存" +separator + +CACHE_SIZE=$(docker system df | grep "Build Cache" | awk '{print $3}') +log_info "构建缓存大小: $CACHE_SIZE" + +if [ "$CACHE_SIZE" != "0B" ]; then + read -p "是否清理构建缓存?(y/n): " -n 1 -r + echo + + if [[ $REPLY =~ ^[Yy]$ ]]; then + log_info "正在清理构建缓存..." + docker builder prune -a -f + log_success "构建缓存清理完成" + else + log_info "跳过构建缓存清理" + fi +else + log_success "没有构建缓存需要清理" +fi + +# 6. 清理未使用的卷(可选) +separator +log_info "步骤6: 清理未使用的卷(可选)" +separator + +UNUSED_VOLUMES=$(docker volume ls -q --filter "dangling=true") +if [ -n "$UNUSED_VOLUMES" ]; then + log_warning "发现未使用的卷:" + echo "$UNUSED_VOLUMES" + + read -p "是否清理未使用的卷?(y/n): " -n 1 -r + echo + + if [[ $REPLY =~ ^[Yy]$ ]]; then + log_info "正在清理未使用的卷..." + docker volume prune -f + log_success "未使用卷清理完成" + else + log_info "跳过未使用卷清理" + fi +else + log_success "没有未使用的卷" +fi + +# 7. 显示清理结果 +separator +log_info "步骤7: 清理结果" +separator + +echo "" +log_info "清理后的Docker资源使用情况:" +docker system df + +echo "" +log_info "磁盘空间变化:" +df -h | grep -E "Filesystem|/$" + +separator +log_success "Docker镜像瘦身完成" +separator + +echo "" +log_info "清理摘要:" +echo " ✅ 悬空镜像已清理" +echo " ✅ 旧版本镜像已清理" +echo " ✅ 构建缓存已清理" +echo " ✅ 未使用卷已清理" + +echo "" +log_info "详细日志已保存到: $LOG_FILE" + +echo "" +log_warning "建议后续操作:" +echo " 1. 监控服务状态: docker ps" +echo " 2. 验证应用访问: curl -I https://novalon.cn" +echo " 3. 设置定期清理: crontab -e" +echo " 4. 配置日志轮转: /etc/logrotate.d/docker" + +separator diff --git a/scripts/remote-server-diagnosis.sh b/scripts/remote-server-diagnosis.sh new file mode 100755 index 0000000..c9eb49e --- /dev/null +++ b/scripts/remote-server-diagnosis.sh @@ -0,0 +1,312 @@ +#!/bin/bash + +# 远程服务器诊断脚本 +# 用途:在生产服务器上诊断连接超时问题 + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# 日志函数 +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +separator() { + echo "======================================================================" +} + +# 1. 检查系统资源 +check_system_resources() { + separator + log_info "检查系统资源" + separator + + echo "" + log_info "CPU和内存使用情况:" + top -bn1 | head -20 + + echo "" + log_info "磁盘使用情况:" + df -h + + echo "" + log_info "内存详情:" + free -h + + echo "" + log_info "系统负载:" + uptime + + # 检查是否资源耗尽 + MEMORY_USAGE=$(free | grep Mem | awk '{print ($3/$2) * 100.0}') + DISK_USAGE=$(df -h / | tail -1 | awk '{print $5}' | sed 's/%//') + + if (( $(echo "$MEMORY_USAGE > 90" | bc -l) )); then + log_error "内存使用率过高: ${MEMORY_USAGE}%" + else + log_success "内存使用正常: ${MEMORY_USAGE}%" + fi + + if [ "$DISK_USAGE" -gt 90 ]; then + log_error "磁盘使用率过高: ${DISK_USAGE}%" + else + log_success "磁盘使用正常: ${DISK_USAGE}%" + fi +} + +# 2. 检查Docker容器 +check_docker() { + separator + log_info "检查Docker容器" + separator + + echo "" + log_info "Docker服务状态:" + systemctl status docker --no-pager | head -20 + + echo "" + log_info "运行中的容器:" + docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" + + echo "" + log_info "容器资源使用:" + docker stats --no-stream + + # 检查容器健康状态 + RUNNING_CONTAINERS=$(docker ps -q | wc -l) + STOPPED_CONTAINERS=$(docker ps -f "status=exited" -q | wc -l) + + log_info "运行中容器: $RUNNING_CONTAINERS" + log_info "已停止容器: $STOPPED_CONTAINERS" + + if [ "$STOPPED_CONTAINERS" -gt 0 ]; then + log_warning "发现已停止的容器:" + docker ps -f "status=exited" --format "table {{.Names}}\t{{.Status}}" + fi + + # 检查容器日志 + echo "" + log_info "检查容器日志(最近50行):" + for container in $(docker ps -q); do + CONTAINER_NAME=$(docker inspect --format='{{.Name}}' $container | sed 's/\///') + echo "" + log_info "容器: $CONTAINER_NAME" + docker logs --tail 50 $container 2>&1 | tail -20 + done +} + +# 3. 检查Nginx +check_nginx() { + separator + log_info "检查Nginx" + separator + + echo "" + log_info "Nginx服务状态:" + systemctl status nginx --no-pager | head -20 + + echo "" + log_info "Nginx进程:" + ps aux | grep nginx | grep -v grep + + echo "" + log_info "Nginx监听端口:" + netstat -tlnp | grep nginx || ss -tlnp | grep nginx + + echo "" + log_info "Nginx配置测试:" + nginx -t + + echo "" + log_info "Nginx错误日志(最近50行):" + tail -50 /var/log/nginx/error.log 2>/dev/null || log_warning "未找到Nginx错误日志" + + echo "" + log_info "Nginx访问日志(最近20行):" + tail -20 /var/log/nginx/access.log 2>/dev/null || log_warning "未找到Nginx访问日志" +} + +# 4. 检查应用服务 +check_application() { + separator + log_info "检查应用服务" + separator + + echo "" + log_info "Node.js进程:" + ps aux | grep node | grep -v grep + + echo "" + log_info "PM2进程(如果使用):" + if command -v pm2 > /dev/null; then + pm2 list + pm2 logs --lines 20 --nostream + else + log_info "未使用PM2" + fi + + echo "" + log_info "检查端口占用:" + netstat -tlnp | grep -E ":(3000|80|443)" || ss -tlnp | grep -E ":(3000|80|443)" + + echo "" + log_info "测试本地应用连接:" + if curl -I --connect-timeout 5 http://localhost:3000 2>&1 | grep -q "HTTP"; then + log_success "应用服务响应正常" + else + log_error "应用服务无响应" + fi +} + +# 5. 检查防火墙和网络 +check_network() { + separator + log_info "检查防火墙和网络" + separator + + echo "" + log_info "防火墙状态:" + if command -v ufw > /dev/null; then + ufw status verbose + elif command -v firewall-cmd > /dev/null; then + firewall-cmd --list-all + else + log_info "未检测到防火墙" + fi + + echo "" + log_info "网络连接状态:" + netstat -an | grep -E ":(80|443|3000)" | head -20 + + echo "" + log_info "系统日志(最近错误):" + journalctl -xe --no-pager | tail -50 +} + +# 6. 快速修复建议 +suggest_fixes() { + separator + log_info "快速修复建议" + separator + + echo "" + log_warning "根据诊断结果,建议执行以下操作:" + + echo "" + echo "1. 如果Docker容器停止:" + echo " docker-compose -f /path/to/docker-compose.prod.yml up -d" + echo " docker-compose -f /path/to/docker-compose.prod.yml restart" + + echo "" + echo "2. 如果Nginx异常:" + echo " sudo systemctl restart nginx" + echo " sudo nginx -t # 测试配置" + + echo "" + echo "3. 如果应用服务异常:" + echo " docker logs # 查看日志" + echo " docker restart # 重启容器" + + echo "" + echo "4. 如果资源耗尽:" + echo " # 清理Docker镜像" + echo " docker system prune -a -f" + echo " # 清理日志" + echo " sudo journalctl --vacuum-time=3d" + echo " # 重启服务" + echo " sudo systemctl restart docker" + + echo "" + echo "5. 查看详细日志:" + echo " docker logs -f # 实时查看容器日志" + echo " tail -f /var/log/nginx/error.log # 实时查看Nginx错误日志" + echo " journalctl -u docker -f # 实时查看Docker服务日志" +} + +# 一键诊断 +full_diagnosis() { + log_info "开始完整诊断..." + + check_system_resources + echo "" + check_docker + echo "" + check_nginx + echo "" + check_application + echo "" + check_network + echo "" + suggest_fixes +} + +# 主菜单 +main_menu() { + clear + separator + echo "远程服务器诊断工具" + separator + echo "" + echo "1. 检查系统资源" + echo "2. 检查Docker容器" + echo "3. 检查Nginx" + echo "4. 检查应用服务" + echo "5. 检查防火墙和网络" + echo "6. 显示修复建议" + echo "7. 完整诊断" + echo "0. 退出" + echo "" + read -p "请选择操作 (0-7): " choice + + case $choice in + 1) check_system_resources ;; + 2) check_docker ;; + 3) check_nginx ;; + 4) check_application ;; + 5) check_network ;; + 6) suggest_fixes ;; + 7) full_diagnosis ;; + 0) + log_info "退出程序" + exit 0 + ;; + *) + log_error "无效选择" + ;; + esac + + echo "" + read -p "按回车键继续..." + main_menu +} + +# 主函数 +main() { + log_info "远程服务器诊断工具启动" + + if [ "$1" = "--full" ]; then + full_diagnosis + else + main_menu + fi +} + +main "$@" diff --git a/src/__mocks__/shared-mocks.tsx b/src/__mocks__/shared-mocks.tsx index c7406bc..fe6df15 100644 --- a/src/__mocks__/shared-mocks.tsx +++ b/src/__mocks__/shared-mocks.tsx @@ -84,6 +84,7 @@ export const mockLucideReact = () => { jest.mock('lucide-react', () => ({ ArrowRight: () => , ArrowLeft: () => , + ArrowUp: () => , Shield: () => , Zap: () => , Award: () => , diff --git a/src/components/ui/back-to-top.test.tsx b/src/components/ui/back-to-top.test.tsx index 14fab5e..12fc4a1 100644 --- a/src/components/ui/back-to-top.test.tsx +++ b/src/components/ui/back-to-top.test.tsx @@ -1,43 +1,58 @@ -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import React from 'react'; +import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; import { BackToTop } from './back-to-top'; -// Mock useReducedMotion jest.mock('@/hooks/use-reduced-motion', () => ({ useReducedMotion: () => false, })); -// Mock AnimatePresence to always render children jest.mock('framer-motion', () => ({ - ...jest.requireActual('framer-motion'), AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}, motion: { button: ({ children, ...props }: React.ComponentProps<'button'>) => , }, + useAnimation: () => ({ + start: jest.fn(), + stop: jest.fn(), + }), + useMotionValue: () => ({ + get: jest.fn(), + set: jest.fn(), + }), +})); + +jest.mock('lucide-react', () => ({ + ArrowUp: () => , })); describe('BackToTop', () => { let scrollYValue = 0; + const originalScrollY = Object.getOwnPropertyDescriptor(window, 'scrollY'); + const originalScrollTo = window.scrollTo; beforeEach(() => { jest.clearAllMocks(); scrollYValue = 0; + Object.defineProperty(window, 'scrollY', { get: () => scrollYValue, configurable: true, }); + window.scrollTo = jest.fn(); - window.addEventListener = jest.fn((event, handler) => { - if (event === 'scroll') { - // 模拟滚动事件触发 - (handler as EventListener)(new Event('scroll')); - } - }); + }); + + afterEach(() => { + if (originalScrollY) { + Object.defineProperty(window, 'scrollY', originalScrollY); + } + window.scrollTo = originalScrollTo; }); it('should not render when scroll position is less than 500px', () => { scrollYValue = 0; const { container } = render(); - expect(container.firstChild).toBeNull(); + expect(container.querySelector('button')).toBeNull(); }); it('should render button when scroll position is more than 500px', async () => { @@ -45,8 +60,12 @@ describe('BackToTop', () => { render(); + act(() => { + fireEvent.scroll(window); + }); + await waitFor(() => { - const button = screen.getByRole('button', { name: /返回顶部/i }); + const button = screen.queryByRole('button', { name: /返回顶部/i }); expect(button).toBeInTheDocument(); }); }); @@ -56,11 +75,14 @@ describe('BackToTop', () => { render(); - await waitFor(() => { - const button = screen.getByRole('button', { name: /返回顶部/i }); - fireEvent.click(button); + act(() => { + fireEvent.scroll(window); }); + const button = await screen.findByRole('button', { name: /返回顶部/i }); + + fireEvent.click(button); + expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth', @@ -72,10 +94,12 @@ describe('BackToTop', () => { render(); - await waitFor(() => { - const button = screen.getByRole('button'); - expect(button).toHaveAttribute('aria-label', '返回顶部'); - expect(button).toHaveAttribute('title', '返回顶部'); + act(() => { + fireEvent.scroll(window); }); + + const button = await screen.findByRole('button'); + expect(button).toHaveAttribute('aria-label', '返回顶部'); + expect(button).toHaveAttribute('title', '返回顶部'); }); });