diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..92e84a9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,32 @@ +# 忽略不需要的文件和目录 +node_modules +npm-debug.log +.git +.gitignore +README.md +.env +.nyc_output +coverage +.cache +.vscode +.idea + +# 只保留dist目录和配置文件 +!dist/ +!nginx.conf +!Dockerfile +!docker-compose.yml + +# 忽略开发相关文件 +src/ +public/ +*.md +*.log +.DS_Store +Thumbs.db + +# 忽略构建缓存 +.next +.nuxt +.output +.vercel diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..0036cd4 --- /dev/null +++ b/.env.production @@ -0,0 +1,36 @@ +# 生产环境配置文件 +NODE_ENV=production + +# 应用配置 +APP_NAME=AI Chat +APP_VERSION=1.0.0 +APP_PORT=80 + +# 域名配置(请修改为您的实际域名) +DOMAIN=your-domain.com +API_BASE_URL=https://your-api-domain.com + +# SSL配置 +SSL_CERT_PATH=/etc/nginx/ssl/fullchain.pem +SSL_KEY_PATH=/etc/nginx/ssl/privkey.pem + +# 日志配置 +LOG_LEVEL=warn +LOG_PATH=/var/log/nginx + +# 安全配置 +ENABLE_SECURITY_HEADERS=true +ENABLE_RATE_LIMITING=true + +# 缓存配置 +ENABLE_GZIP=true +STATIC_CACHE_TIME=31536000 # 1年 +HTML_CACHE_TIME=0 + +# 监控配置 +ENABLE_HEALTH_CHECK=true +HEALTH_CHECK_PATH=/health + +# 备份配置 +BACKUP_RETENTION_DAYS=7 +AUTO_BACKUP=true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..713d831 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +# 使用官方nginx镜像作为基础镜像 +FROM nginx:alpine + +# 设置工作目录 +WORKDIR /usr/share/nginx/html + +# 删除nginx默认的静态文件 +RUN rm -rf /usr/share/nginx/html/* + +# 复制dist目录下的所有文件到nginx的html目录 +COPY dist/ /usr/share/nginx/html/ + +# 创建自定义nginx配置文件 +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# 暴露80端口 +EXPOSE 80 + +# 启动nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000..087ed71 --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,70 @@ +# 生产环境多阶段构建 Dockerfile +# 第一阶段:构建应用 +FROM node:18-alpine AS builder + +# 设置工作目录 +WORKDIR /app + +# 复制package文件 +COPY package*.json pnpm-lock.yaml ./ + +# 安装pnpm +RUN npm install -g pnpm + +# 安装依赖 +RUN pnpm install --frozen-lockfile + +# 复制源代码 +COPY . . + +# 构建应用 +RUN pnpm run build:h5 + +# 第二阶段:生产环境镜像 +FROM nginx:1.25-alpine AS production + +# 安装必要的工具 +RUN apk add --no-cache \ + curl \ + tzdata \ + && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ + && echo "Asia/Shanghai" > /etc/timezone \ + && apk del tzdata + +# 创建nginx用户和组 +RUN addgroup -g 1001 -S nginx-app && \ + adduser -S -D -H -u 1001 -h /var/cache/nginx -s /sbin/nologin -G nginx-app -g nginx-app nginx-app + +# 设置工作目录 +WORKDIR /usr/share/nginx/html + +# 删除nginx默认文件 +RUN rm -rf /usr/share/nginx/html/* + +# 从构建阶段复制构建产物 +COPY --from=builder /app/dist/ /usr/share/nginx/html/ + +# 复制nginx配置 +COPY nginx.prod.conf /etc/nginx/conf.d/default.conf + +# 创建日志目录 +RUN mkdir -p /var/log/nginx && \ + chown -R nginx-app:nginx-app /var/log/nginx && \ + chown -R nginx-app:nginx-app /usr/share/nginx/html && \ + chown -R nginx-app:nginx-app /var/cache/nginx + +# 设置正确的权限 +RUN chmod -R 755 /usr/share/nginx/html + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:80/ || exit 1 + +# 暴露端口 +EXPOSE 80 + +# 使用非root用户运行 +USER nginx-app + +# 启动nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/README-Docker.md b/README-Docker.md new file mode 100644 index 0000000..df429ae --- /dev/null +++ b/README-Docker.md @@ -0,0 +1,143 @@ +# AI Chat Docker 部署指南 + +## 📦 文件说明 + +- `Dockerfile` - Docker镜像构建文件 +- `docker-compose.yml` - Docker Compose配置文件 +- `nginx.conf` - Nginx服务器配置 +- `proxy.conf` - 反向代理配置(可选) +- `.dockerignore` - Docker构建忽略文件 + +## 🚀 快速启动 + +### 1. 基础部署(推荐) + +```bash +# 构建并启动服务 +docker-compose up -d + +# 查看服务状态 +docker-compose ps + +# 查看日志 +docker-compose logs -f web +``` + +访问地址:http://localhost:3000 + +### 2. 使用反向代理 + +```bash +# 启动包含反向代理的完整服务 +docker-compose --profile proxy up -d +``` + +访问地址:http://localhost + +### 3. 手动构建镜像 + +```bash +# 构建镜像 +docker build -t ai-chat:latest . + +# 运行容器 +docker run -d -p 3000:80 --name ai-chat-container ai-chat:latest +``` + +## 🔧 配置说明 + +### 端口配置 +- 默认端口:3000 (可在docker-compose.yml中修改) +- 反向代理端口:80/443 + +### 环境变量 +- `TZ=Asia/Shanghai` - 时区设置 + +### 数据卷 +- `./logs:/var/log/nginx` - Nginx日志持久化 + +## 📝 常用命令 + +```bash +# 停止服务 +docker-compose down + +# 重新构建并启动 +docker-compose up -d --build + +# 查看容器状态 +docker-compose ps + +# 进入容器 +docker-compose exec web sh + +# 查看实时日志 +docker-compose logs -f + +# 清理未使用的镜像 +docker image prune -f +``` + +## 🔒 生产环境配置 + +### 1. HTTPS配置 +1. 将SSL证书放入 `./ssl/` 目录 +2. 取消注释 `proxy.conf` 中的HTTPS配置 +3. 重启服务 + +### 2. 性能优化 +- 启用gzip压缩 ✅ +- 静态资源缓存 ✅ +- 安全头设置 ✅ + +### 3. 监控和日志 +```bash +# 查看资源使用情况 +docker stats ai-chat-web + +# 查看详细信息 +docker inspect ai-chat-web +``` + +## 🐛 故障排除 + +### 常见问题 + +1. **端口被占用** + ```bash + # 修改docker-compose.yml中的端口映射 + ports: + - "8080:80" # 改为其他端口 + ``` + +2. **权限问题** + ```bash + # 确保dist目录有正确权限 + chmod -R 755 dist/ + ``` + +3. **容器无法启动** + ```bash + # 查看详细错误信息 + docker-compose logs web + ``` + +## 📊 监控指标 + +- 容器状态:`docker-compose ps` +- 资源使用:`docker stats` +- 访问日志:`./logs/access.log` +- 错误日志:`./logs/error.log` + +## 🔄 更新部署 + +```bash +# 1. 重新构建应用 +npm run build + +# 2. 重新构建Docker镜像 +docker-compose build + +# 3. 重启服务 +docker-compose up -d +``` diff --git a/README-Production.md b/README-Production.md new file mode 100644 index 0000000..12d1115 --- /dev/null +++ b/README-Production.md @@ -0,0 +1,300 @@ +# 🚀 AI Chat 生产环境部署指南 + +## 📋 概述 + +本指南提供了完整的生产环境Docker部署方案,包括安全配置、性能优化、监控和自动化部署。 + +## 🏗️ 架构说明 + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 用户请求 │───▶│ Nginx Proxy │───▶│ AI Chat App │ +│ │ │ (HTTPS/SSL) │ │ (Frontend) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ 日志 & 监控 │ + │ (可选组件) │ + └─────────────────┘ +``` + +## 🚀 快速部署 + +### 1. 基础HTTP部署 + +```bash +# 一键部署(推荐) +./deploy-prod.sh + +# 或手动部署 +docker-compose -f docker-compose.prod.yml up -d --build +``` + +访问地址:http://your-server-ip + +### 2. HTTPS部署 + +```bash +# 1. 配置SSL证书 +./ssl-setup.sh + +# 2. HTTPS部署 +./deploy-prod.sh https +``` + +访问地址:https://your-domain.com + +## 🔧 详细配置 + +### 环境要求 + +- **操作系统**: Linux (Ubuntu 20.04+ / CentOS 8+ 推荐) +- **Docker**: 20.10+ +- **Docker Compose**: 2.0+ +- **内存**: 最低 1GB,推荐 2GB+ +- **存储**: 最低 10GB 可用空间 +- **网络**: 80/443 端口开放 + +### 文件说明 + +| 文件 | 说明 | +|------|------| +| `Dockerfile.prod` | 生产环境多阶段构建 | +| `docker-compose.prod.yml` | 生产环境编排配置 | +| `nginx.prod.conf` | 生产级Nginx配置 | +| `nginx-proxy.conf` | HTTPS反向代理配置 | +| `deploy-prod.sh` | 自动化部署脚本 | +| `ssl-setup.sh` | SSL证书配置脚本 | +| `.env.production` | 生产环境变量 | + +## 🔒 SSL/HTTPS 配置 + +### Let's Encrypt (推荐) + +```bash +# 自动配置Let's Encrypt证书 +./ssl-setup.sh +# 选择选项1,输入域名和邮箱 +``` + +### 自定义证书 + +```bash +# 将证书文件放入ssl目录 +cp your-cert.pem ssl/fullchain.pem +cp your-key.pem ssl/privkey.pem + +# 部署HTTPS +./deploy-prod.sh https +``` + +## 📊 监控和日志 + +### 启用监控模式 + +```bash +./deploy-prod.sh monitoring +``` + +包含以下组件: +- **Watchtower**: 自动更新容器 +- **Promtail**: 日志收集(可选) + +### 日志查看 + +```bash +# 应用日志 +docker-compose -f docker-compose.prod.yml logs -f ai-chat-web + +# Nginx访问日志 +tail -f logs/nginx/access.log + +# 错误日志 +tail -f logs/nginx/error.log +``` + +## 🔧 运维命令 + +### 基础操作 + +```bash +# 查看服务状态 +docker-compose -f docker-compose.prod.yml ps + +# 重启服务 +docker-compose -f docker-compose.prod.yml restart + +# 停止服务 +docker-compose -f docker-compose.prod.yml down + +# 查看资源使用 +docker stats +``` + +### 更新部署 + +```bash +# 1. 拉取最新代码 +git pull + +# 2. 重新部署 +./deploy-prod.sh + +# 3. 清理旧镜像 +docker image prune -f +``` + +### 备份和恢复 + +```bash +# 自动备份(部署时自动执行) +tar -czf backup_$(date +%Y%m%d_%H%M%S).tar.gz dist/ logs/ + +# 恢复备份 +tar -xzf backup_20240101_120000.tar.gz +``` + +## 🛡️ 安全配置 + +### 已启用的安全特性 + +✅ **HTTPS强制重定向** +✅ **安全HTTP头** +✅ **HSTS (HTTP Strict Transport Security)** +✅ **XSS保护** +✅ **内容类型嗅探保护** +✅ **点击劫持保护** +✅ **CSP (Content Security Policy)** +✅ **非root用户运行** + +### 额外安全建议 + +1. **防火墙配置** +```bash +# 只开放必要端口 +ufw allow 22 # SSH +ufw allow 80 # HTTP +ufw allow 443 # HTTPS +ufw enable +``` + +2. **定期更新** +```bash +# 系统更新 +sudo apt update && sudo apt upgrade -y + +# Docker镜像更新(Watchtower自动处理) +``` + +## 🚨 故障排除 + +### 常见问题 + +1. **容器启动失败** +```bash +# 查看详细错误 +docker-compose -f docker-compose.prod.yml logs ai-chat-web + +# 检查端口占用 +netstat -tlnp | grep :80 +``` + +2. **SSL证书问题** +```bash +# 检查证书有效性 +openssl x509 -in ssl/fullchain.pem -text -noout + +# 重新生成证书 +./ssl-setup.sh +``` + +3. **性能问题** +```bash +# 查看资源使用 +docker stats + +# 查看系统负载 +htop +``` + +### 健康检查 + +```bash +# 应用健康检查 +curl http://localhost/health + +# 容器健康状态 +docker-compose -f docker-compose.prod.yml ps +``` + +## 📈 性能优化 + +### 已启用的优化 + +✅ **Gzip压缩** - 减少传输大小 +✅ **静态资源缓存** - 1年缓存期 +✅ **HTTP/2支持** - 提升加载速度 +✅ **连接复用** - 减少连接开销 +✅ **多阶段构建** - 减小镜像大小 + +### 进一步优化 + +1. **CDN配置** +```bash +# 配置CDN加速静态资源 +# 修改nginx.prod.conf中的静态资源处理 +``` + +2. **缓存策略** +```bash +# 根据业务需求调整缓存时间 +# 编辑nginx.prod.conf +``` + +## 🔄 CI/CD 集成 + +### GitHub Actions 示例 + +```yaml +# .github/workflows/deploy.yml +name: Deploy to Production + +on: + push: + branches: [ main ] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Deploy to server + run: | + ssh user@server 'cd /path/to/app && git pull && ./deploy-prod.sh' +``` + +## 📞 技术支持 + +如果遇到问题,请检查: + +1. **日志文件**: `logs/nginx/error.log` +2. **容器状态**: `docker-compose ps` +3. **系统资源**: `htop`, `df -h` +4. **网络连接**: `netstat -tlnp` + +--- + +## 🎯 生产环境检查清单 + +- [ ] 域名DNS解析正确 +- [ ] SSL证书配置完成 +- [ ] 防火墙规则设置 +- [ ] 备份策略配置 +- [ ] 监控告警设置 +- [ ] 性能测试通过 +- [ ] 安全扫描通过 +- [ ] 文档更新完成 + +**🎉 恭喜!您的AI Chat应用已成功部署到生产环境!** diff --git a/deploy-prod.sh b/deploy-prod.sh new file mode 100755 index 0000000..8e0460e --- /dev/null +++ b/deploy-prod.sh @@ -0,0 +1,220 @@ +#!/bin/bash + +# AI Chat 生产环境部署脚本 +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" +} + +# 检查必要的工具 +check_requirements() { + log_info "检查部署环境..." + + if ! command -v docker &> /dev/null; then + log_error "Docker 未安装,请先安装 Docker" + exit 1 + fi + + if ! command -v docker-compose &> /dev/null; then + log_error "Docker Compose 未安装,请先安装 Docker Compose" + exit 1 + fi + + log_success "环境检查通过" +} + +# 创建必要的目录 +create_directories() { + log_info "创建必要的目录..." + + mkdir -p logs/nginx + mkdir -p logs/proxy + mkdir -p ssl + mkdir -p backups + + log_success "目录创建完成" +} + +# 备份当前部署 +backup_current() { + if [ -d "dist" ]; then + log_info "备份当前部署..." + timestamp=$(date +%Y%m%d_%H%M%S) + tar -czf "backups/backup_${timestamp}.tar.gz" dist/ logs/ 2>/dev/null || true + log_success "备份完成: backups/backup_${timestamp}.tar.gz" + fi +} + +# 构建应用 +build_app() { + log_info "构建应用..." + + if [ ! -f "package.json" ]; then + log_error "package.json 不存在,请确保在项目根目录运行此脚本" + exit 1 + fi + + # 检查是否有pnpm + if command -v pnpm &> /dev/null; then + log_info "使用 pnpm 安装依赖..." + pnpm install --frozen-lockfile + pnpm run build:h5 + elif command -v npm &> /dev/null; then + log_info "使用 npm 安装依赖..." + npm ci + npm run build:h5 + else + log_error "未找到 npm 或 pnpm" + exit 1 + fi + + if [ ! -d "dist" ]; then + log_error "构建失败,dist 目录不存在" + exit 1 + fi + + log_success "应用构建完成" +} + +# 部署应用 +deploy_app() { + local mode=$1 + + log_info "开始部署应用 (模式: $mode)..." + + case $mode in + "basic") + docker-compose -f docker-compose.prod.yml down 2>/dev/null || true + docker-compose -f docker-compose.prod.yml up -d --build + ;; + "https") + if [ ! -f "ssl/fullchain.pem" ] || [ ! -f "ssl/privkey.pem" ]; then + log_warning "SSL证书不存在,请先配置SSL证书" + log_info "将使用基础模式部署..." + docker-compose -f docker-compose.prod.yml down 2>/dev/null || true + docker-compose -f docker-compose.prod.yml up -d --build + else + docker-compose -f docker-compose.prod.yml --profile https down 2>/dev/null || true + docker-compose -f docker-compose.prod.yml --profile https up -d --build + fi + ;; + "monitoring") + docker-compose -f docker-compose.prod.yml --profile monitoring down 2>/dev/null || true + docker-compose -f docker-compose.prod.yml --profile monitoring up -d --build + ;; + *) + log_error "未知的部署模式: $mode" + exit 1 + ;; + esac + + log_success "应用部署完成" +} + +# 等待服务启动 +wait_for_service() { + log_info "等待服务启动..." + + local max_attempts=30 + local attempt=1 + + while [ $attempt -le $max_attempts ]; do + if curl -s http://localhost/health > /dev/null 2>&1; then + log_success "服务启动成功" + return 0 + fi + + log_info "等待服务启动... ($attempt/$max_attempts)" + sleep 2 + ((attempt++)) + done + + log_warning "服务启动超时,请检查日志" + return 1 +} + +# 显示部署信息 +show_deployment_info() { + log_success "🎉 部署完成!" + echo "" + echo "📋 部署信息:" + echo " - 访问地址: http://localhost" + echo " - 健康检查: http://localhost/health" + echo " - 日志目录: ./logs/" + echo "" + echo "🔧 管理命令:" + echo " - 查看状态: docker-compose -f docker-compose.prod.yml ps" + echo " - 查看日志: docker-compose -f docker-compose.prod.yml logs -f" + echo " - 停止服务: docker-compose -f docker-compose.prod.yml down" + echo " - 重启服务: docker-compose -f docker-compose.prod.yml restart" + echo "" + echo "📊 监控:" + echo " - 容器状态: docker stats" + echo " - 系统资源: htop" + echo "" +} + +# 主函数 +main() { + echo "🚀 AI Chat 生产环境部署脚本" + echo "================================" + + # 检查参数 + if [ $# -eq 0 ]; then + echo "请选择部署模式:" + echo "1) 基础模式 (HTTP)" + echo "2) HTTPS模式" + echo "3) 监控模式" + echo "4) 仅构建" + echo "" + read -p "请输入选择 (1-4): " choice + + case $choice in + 1) MODE="basic" ;; + 2) MODE="https" ;; + 3) MODE="monitoring" ;; + 4) MODE="build-only" ;; + *) log_error "无效选择"; exit 1 ;; + esac + else + MODE=$1 + fi + + # 执行部署步骤 + check_requirements + create_directories + backup_current + build_app + + if [ "$MODE" != "build-only" ]; then + deploy_app $MODE + wait_for_service + show_deployment_info + else + log_success "构建完成,跳过部署" + fi +} + +# 运行主函数 +main "$@" diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..4dc2ca6 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# AI Chat Docker 部署脚本 +set -e + +echo "🚀 开始部署 AI Chat 应用..." + +# 检查Docker是否安装 +if ! command -v docker &> /dev/null; then + echo "❌ Docker 未安装,请先安装 Docker" + exit 1 +fi + +# 检查Docker Compose是否安装 +if ! command -v docker-compose &> /dev/null; then + echo "❌ Docker Compose 未安装,请先安装 Docker Compose" + exit 1 +fi + +# 检查dist目录是否存在 +if [ ! -d "dist" ]; then + echo "❌ dist 目录不存在,请先构建项目" + echo "运行: npm run build" + exit 1 +fi + +# 创建日志目录 +mkdir -p logs + +# 选择部署模式 +echo "请选择部署模式:" +echo "1) 简单模式 (端口 3000)" +echo "2) 完整模式 (端口 3000)" +echo "3) 带反向代理 (端口 80)" + +read -p "请输入选择 (1-3): " choice + +case $choice in + 1) + echo "📦 使用简单模式部署..." + docker-compose -f docker-compose.simple.yml down 2>/dev/null || true + docker-compose -f docker-compose.simple.yml up -d --build + echo "✅ 部署完成!访问地址: http://localhost:3000" + ;; + 2) + echo "📦 使用完整模式部署..." + docker-compose down 2>/dev/null || true + docker-compose up -d --build + echo "✅ 部署完成!访问地址: http://localhost:3000" + ;; + 3) + echo "📦 使用反向代理模式部署..." + docker-compose --profile proxy down 2>/dev/null || true + docker-compose --profile proxy up -d --build + echo "✅ 部署完成!访问地址: http://localhost" + ;; + *) + echo "❌ 无效选择" + exit 1 + ;; +esac + +# 等待服务启动 +echo "⏳ 等待服务启动..." +sleep 5 + +# 检查服务状态 +if [ "$choice" = "1" ]; then + docker-compose -f docker-compose.simple.yml ps +else + docker-compose ps +fi + +echo "" +echo "🎉 部署完成!" +echo "" +echo "📋 常用命令:" +echo " 查看日志: docker-compose logs -f" +echo " 停止服务: docker-compose down" +echo " 重启服务: docker-compose restart" +echo "" +echo "🔍 健康检查:" +if curl -s http://localhost:3000 > /dev/null 2>&1 || curl -s http://localhost > /dev/null 2>&1; then + echo "✅ 服务运行正常" +else + echo "⚠️ 服务可能还在启动中,请稍等片刻" +fi diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..c8598fa --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,105 @@ +version: '3.8' + +services: + # 前端应用 + ai-chat-web: + build: + context: . + dockerfile: Dockerfile.prod + args: + - NODE_ENV=production + image: ai-chat:latest + container_name: ai-chat-web-prod + restart: unless-stopped + ports: + - "80:80" + environment: + - TZ=Asia/Shanghai + - NODE_ENV=production + volumes: + - ./logs/nginx:/var/log/nginx + - ./ssl:/etc/nginx/ssl:ro # SSL证书目录(如果需要HTTPS) + networks: + - ai-chat-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:80/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + labels: + - "traefik.enable=true" + - "traefik.http.routers.ai-chat.rule=Host(`your-domain.com`)" + - "traefik.http.services.ai-chat.loadbalancer.server.port=80" + deploy: + resources: + limits: + cpus: '1.0' + memory: 512M + reservations: + cpus: '0.5' + memory: 256M + + # 反向代理(可选,用于HTTPS和负载均衡) + nginx-proxy: + image: nginx:1.25-alpine + container_name: ai-chat-proxy-prod + restart: unless-stopped + ports: + - "443:443" + volumes: + - ./nginx-proxy.conf:/etc/nginx/conf.d/default.conf:ro + - ./ssl:/etc/nginx/ssl:ro + - ./logs/proxy:/var/log/nginx + depends_on: + - ai-chat-web + networks: + - ai-chat-network + profiles: + - https # 使用profile控制HTTPS代理 + healthcheck: + test: ["CMD", "nginx", "-t"] + interval: 30s + timeout: 10s + retries: 3 + + # 监控服务(可选) + watchtower: + image: containrrr/watchtower + container_name: ai-chat-watchtower + restart: unless-stopped + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + - WATCHTOWER_CLEANUP=true + - WATCHTOWER_POLL_INTERVAL=3600 # 每小时检查一次更新 + - WATCHTOWER_INCLUDE_STOPPED=true + profiles: + - monitoring + command: ai-chat-web-prod + + # 日志收集(可选) + promtail: + image: grafana/promtail:latest + container_name: ai-chat-promtail + restart: unless-stopped + volumes: + - ./logs:/var/log/app:ro + - ./promtail-config.yml:/etc/promtail/config.yml:ro + networks: + - ai-chat-network + profiles: + - logging + +networks: + ai-chat-network: + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/16 + +volumes: + nginx-logs: + driver: local + ssl-certs: + driver: local diff --git a/docker-compose.simple.yml b/docker-compose.simple.yml new file mode 100644 index 0000000..4753c29 --- /dev/null +++ b/docker-compose.simple.yml @@ -0,0 +1,19 @@ +version: '3.8' + +services: + ai-chat: + build: + context: . + dockerfile: Dockerfile + container_name: ai-chat-app + ports: + - "3000:80" + restart: unless-stopped + environment: + - TZ=Asia/Shanghai + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..abe3918 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +version: '3.8' + +services: + web: + build: + context: . + dockerfile: Dockerfile + container_name: ai-chat-web + ports: + - "3000:80" + restart: unless-stopped + environment: + - TZ=Asia/Shanghai + volumes: + # 可选:如果需要持久化nginx日志 + - ./logs:/var/log/nginx + networks: + - ai-chat-network + labels: + - "traefik.enable=true" + - "traefik.http.routers.ai-chat.rule=Host(`localhost`)" + - "traefik.http.services.ai-chat.loadbalancer.server.port=80" + + # 可选:如果需要反向代理 + nginx-proxy: + image: nginx:alpine + container_name: ai-chat-proxy + ports: + - "80:80" + - "443:443" + volumes: + - ./proxy.conf:/etc/nginx/conf.d/default.conf + - ./ssl:/etc/nginx/ssl # SSL证书目录(如果需要HTTPS) + depends_on: + - web + restart: unless-stopped + networks: + - ai-chat-network + profiles: + - proxy # 使用profile控制是否启动代理 + +networks: + ai-chat-network: + driver: bridge + +volumes: + logs: + driver: local diff --git a/monitoring.yml b/monitoring.yml new file mode 100644 index 0000000..77aafca --- /dev/null +++ b/monitoring.yml @@ -0,0 +1,84 @@ +# 监控配置文件 +version: '3.8' + +services: + # Prometheus监控 + prometheus: + image: prom/prometheus:latest + container_name: ai-chat-prometheus + restart: unless-stopped + ports: + - "9090:9090" + volumes: + - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus-data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--web.console.libraries=/etc/prometheus/console_libraries' + - '--web.console.templates=/etc/prometheus/consoles' + - '--storage.tsdb.retention.time=200h' + - '--web.enable-lifecycle' + networks: + - monitoring + + # Grafana仪表板 + grafana: + image: grafana/grafana:latest + container_name: ai-chat-grafana + restart: unless-stopped + ports: + - "3001:3000" + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin123 + volumes: + - grafana-data:/var/lib/grafana + - ./monitoring/grafana/provisioning:/etc/grafana/provisioning + networks: + - monitoring + + # Node Exporter (系统监控) + node-exporter: + image: prom/node-exporter:latest + container_name: ai-chat-node-exporter + restart: unless-stopped + ports: + - "9100:9100" + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: + - '--path.procfs=/host/proc' + - '--path.rootfs=/rootfs' + - '--path.sysfs=/host/sys' + - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' + networks: + - monitoring + + # cAdvisor (容器监控) + cadvisor: + image: gcr.io/cadvisor/cadvisor:latest + container_name: ai-chat-cadvisor + restart: unless-stopped + ports: + - "8080:8080" + volumes: + - /:/rootfs:ro + - /var/run:/var/run:ro + - /sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:ro + - /dev/disk/:/dev/disk:ro + privileged: true + devices: + - /dev/kmsg + networks: + - monitoring + +networks: + monitoring: + driver: bridge + +volumes: + prometheus-data: + grafana-data: diff --git a/nginx-proxy.conf b/nginx-proxy.conf new file mode 100644 index 0000000..b57dc40 --- /dev/null +++ b/nginx-proxy.conf @@ -0,0 +1,84 @@ +# HTTPS反向代理配置 +upstream ai-chat-backend { + server ai-chat-web:80; + keepalive 32; +} + +# HTTP重定向到HTTPS +server { + listen 80; + server_name your-domain.com www.your-domain.com; + + # Let's Encrypt验证 + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # 重定向到HTTPS + location / { + return 301 https://$server_name$request_uri; + } +} + +# HTTPS主服务器 +server { + listen 443 ssl http2; + server_name your-domain.com www.your-domain.com; + + # SSL配置 + ssl_certificate /etc/nginx/ssl/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/privkey.pem; + + # SSL安全配置 + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # HSTS + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + # 其他安全头 + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # 日志配置 + access_log /var/log/nginx/access.log combined; + error_log /var/log/nginx/error.log warn; + + # 代理配置 + location / { + proxy_pass http://ai-chat-backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket支持 + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # 超时设置 + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # 缓冲设置 + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 4k; + + # 连接复用 + proxy_set_header Connection ""; + } + + # 健康检查 + location /health { + access_log off; + proxy_pass http://ai-chat-backend/health; + } +} diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..0860b39 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,57 @@ +server { + listen 80; + server_name localhost; + + # 设置根目录 + root /usr/share/nginx/html; + index index.html index.htm; + + # 启用gzip压缩 + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied any; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/json + application/javascript + application/xml+rss + application/atom+xml + image/svg+xml; + + # 静态资源缓存设置 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # HTML文件不缓存 + location ~* \.html$ { + expires -1; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + } + + # SPA路由支持 - 所有路由都返回index.html + location / { + try_files $uri $uri/ /index.html; + } + + # 安全头设置 + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # 错误页面 + error_page 404 /index.html; + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/nginx.prod.conf b/nginx.prod.conf new file mode 100644 index 0000000..5aa1d9b --- /dev/null +++ b/nginx.prod.conf @@ -0,0 +1,129 @@ +# 生产环境 Nginx 配置 +server { + listen 80; + server_name _; + + # 安全配置 + server_tokens off; + + # 设置根目录 + root /usr/share/nginx/html; + index index.html index.htm; + + # 日志配置 + access_log /var/log/nginx/access.log combined; + error_log /var/log/nginx/error.log warn; + + # 启用gzip压缩 + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied any; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/json + application/javascript + application/xml+rss + application/atom+xml + image/svg+xml + application/x-font-ttf + application/vnd.ms-fontobject + font/opentype; + + # Brotli压缩(如果支持) + # brotli on; + # brotli_comp_level 6; + # brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + # 静态资源缓存设置 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|webp|avif)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + add_header X-Content-Type-Options "nosniff"; + access_log off; + + # 预压缩文件支持 + location ~* \.(js|css)$ { + gzip_static on; + } + } + + # HTML文件不缓存 + location ~* \.html$ { + expires -1; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header X-Content-Type-Options "nosniff"; + } + + # API代理(如果需要) + location /api/ { + # 替换为您的后端API地址 + proxy_pass http://backend-api:8080/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket支持 + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # 超时设置 + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # 缓冲设置 + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 4k; + } + + # SPA路由支持 - 所有路由都返回index.html + location / { + try_files $uri $uri/ /index.html; + + # 安全头 + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' wss: https:;" always; + } + + # 健康检查端点 + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # 禁止访问敏感文件 + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + location ~ \.(env|log|config)$ { + deny all; + access_log off; + log_not_found off; + } + + # 限制请求大小 + client_max_body_size 10M; + + # 错误页面 + error_page 404 /index.html; + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/proxy.conf b/proxy.conf new file mode 100644 index 0000000..9e3d211 --- /dev/null +++ b/proxy.conf @@ -0,0 +1,51 @@ +# 反向代理配置(可选) +upstream ai-chat-backend { + server web:80; +} + +server { + listen 80; + server_name localhost; + + # 重定向到HTTPS(如果需要) + # return 301 https://$server_name$request_uri; + + location / { + proxy_pass http://ai-chat-backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket支持 + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # 超时设置 + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } +} + +# HTTPS配置(如果需要) +# server { +# listen 443 ssl http2; +# server_name localhost; +# +# ssl_certificate /etc/nginx/ssl/cert.pem; +# ssl_certificate_key /etc/nginx/ssl/key.pem; +# +# location / { +# proxy_pass http://ai-chat-backend; +# proxy_set_header Host $host; +# proxy_set_header X-Real-IP $remote_addr; +# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +# proxy_set_header X-Forwarded-Proto $scheme; +# +# proxy_http_version 1.1; +# proxy_set_header Upgrade $http_upgrade; +# proxy_set_header Connection "upgrade"; +# } +# } diff --git a/src/app.config.ts b/src/app.config.ts index b762d2b..8e22a18 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -64,6 +64,13 @@ export default defineAppConfig({ "detail" ] }, + { + "root": "yxtwjr", + "pages": [ + "index", + "detail" + ] + }, ], window: { backgroundTextStyle: 'dark', diff --git a/src/expert/detail.tsx b/src/expert/detail.tsx index 79c56aa..66cd700 100644 --- a/src/expert/detail.tsx +++ b/src/expert/detail.tsx @@ -13,11 +13,10 @@ function Detail() { // 文章详情 const [item, setItem] = useState() - const reload = () => { + const reload = async () => { getCmsArticle(Number(params.id)).then(data => { if (data) { setItem(data) - // Taro.setNavigationBarTitle({title: `${data?.categoryName} - 文章详情`}) } }) } diff --git a/src/expert/index.tsx b/src/expert/index.tsx index a0c7b38..006d650 100644 --- a/src/expert/index.tsx +++ b/src/expert/index.tsx @@ -5,7 +5,7 @@ import Taro from '@tarojs/taro' import {useRouter} from '@tarojs/taro' import {Image} from '@nutui/nutui-react-taro' import {InfiniteLoading} from '@nutui/nutui-react-taro' -import {getCmsNavigation} from "@/api/cms/cmsNavigation"; +import {getCmsNavigation, pageCmsNavigation} from "@/api/cms/cmsNavigation"; import {CmsNavigation} from "@/api/cms/cmsNavigation/model"; /** @@ -15,6 +15,7 @@ import {CmsNavigation} from "@/api/cms/cmsNavigation/model"; const Index = () => { const {params} = useRouter(); const [navigation, setNavigation] = useState() + const [childCategory, setChildCategory] = useState([]) const [page, setPage] = useState(1) const [hasMore, setHasMore] = useState(true) const [list, setList] = useState([]) @@ -30,6 +31,12 @@ const Index = () => { // 当前栏目信息 const navs = await getCmsNavigation(categoryId); + // 二级栏目 + const childCateogry = await pageCmsNavigation({parentId: categoryId}); + // 获取子级栏目 + if (childCateogry) { + setChildCategory(childCateogry.list) + } if (navs) { setNavigation(navs); Taro.setNavigationBarTitle({title: `${navs.title}`}) @@ -64,12 +71,17 @@ const Index = () => { }).catch(error => { console.error('获取数据失败:', error); setHasMore(false); + }).finally(() => { + Taro.hideLoading() }); } const reloadMore = async () => { const nextPage = page + 1; console.log('加载更多 - 下一页:', nextPage); + Taro.showLoading({ + title: '加载中...', + }) setPage(nextPage); getList(nextPage); } @@ -99,6 +111,29 @@ const Index = () => {
+
+ { + // 子级栏目 + childCategory.map((item, index) => { + return ( +
Taro.navigateTo({url: `/${item.model}/index?id=${item.navigationId}`})} + > + {/* 图片容器 */} +
+ {item.title +
+
+ ) + }) + } +
{ // 终极文章列表 list.map((item, index) => { diff --git a/src/honor/list.tsx b/src/honor/list.tsx index e7cd56e..35f3a78 100644 --- a/src/honor/list.tsx +++ b/src/honor/list.tsx @@ -62,8 +62,13 @@ const List = () => { const reloadMore = async () => { const nextPage = page + 1; console.log('honor/list 加载更多 - 下一页:', nextPage); + Taro.showLoading({ + title: '加载中...', + }) setPage(nextPage); - reload(nextPage).then(); + reload(nextPage).then().finally(() => { + Taro.hideLoading(); + }); } useEffect(() => { diff --git a/src/pages/index/Video.tsx b/src/pages/index/Video.tsx new file mode 100644 index 0000000..c74246a --- /dev/null +++ b/src/pages/index/Video.tsx @@ -0,0 +1,17 @@ +import { useEffect } from 'react' + +const MyPage = () => { + + useEffect(() => { + }, []) + + return ( +
+ +
+ ) +} +export default MyPage diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index ea9d23d..de996af 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -3,10 +3,11 @@ import Taro from '@tarojs/taro'; import {useEffect, useState} from "react"; import {getSiteInfo} from "@/api/layout"; import Login from "./Login"; -import Banner from "./Banner"; +// import Banner from "./Banner"; import Menu from "./Menu"; import Image from "./Image"; import SimpleH5TabBar from "@/components/SimpleH5TabBar"; +import Video from "./Video"; function Home() { const [loading, setLoading] = useState(false) @@ -44,7 +45,8 @@ function Home() { {!IsLogin && search ? () : (<> - + {/**/} +