diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8bc3289 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,44 @@ +# Git +.git/ +.gitignore + +# IDE +.idea/ +*.iml +*.iws +*.ipr +.vscode/ +*.swp +*.swo +*~ +.DS_Store + +# 构建产物 +backend/build/ +backend/.gradle/ +backend/out/ +backend/bin/ +frontend/dist/ +frontend/node_modules/ +frontend/.vite/ +frontend/.cache/ + +# 日志 +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# 环境配置 +.env +.env.local +.env.*.local + +# 其他 +*.bak +*.backup +*.old +clob-client/ +polymarket-demo/ + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3f03d17 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,79 @@ +# 多阶段构建:前后端一体化部署 +# 阶段1:构建前端 +FROM node:18-alpine AS frontend-build + +WORKDIR /app/frontend + +# 复制前端文件 +COPY frontend/package*.json ./ +RUN npm ci + +COPY frontend/ ./ + +# 构建前端(使用相对路径,通过 Nginx 代理) +RUN npm run build + +# 阶段2:构建后端 +FROM gradle:8.5-jdk17 AS backend-build + +WORKDIR /app/backend + +# 复制 Gradle 配置文件 +COPY backend/build.gradle.kts backend/settings.gradle.kts ./ +COPY backend/gradle ./gradle + +# 下载依赖(利用 Docker 缓存) +RUN gradle dependencies --no-daemon || true + +# 复制源代码 +COPY backend/src ./src + +# 构建应用 +RUN gradle bootJar --no-daemon + +# 阶段3:运行环境 +FROM openjdk:17-jre-slim + +WORKDIR /app + +# 安装 Nginx 和必要的工具 +RUN apt-get update && \ + apt-get install -y nginx curl && \ + rm -rf /var/lib/apt/lists/* && \ + rm -rf /etc/nginx/sites-enabled/default + +# 从构建阶段复制文件 +COPY --from=frontend-build /app/frontend/dist /usr/share/nginx/html +COPY --from=backend-build /app/backend/build/libs/*.jar app.jar + +# 复制 Nginx 配置 +COPY docker/nginx.conf /etc/nginx/nginx.conf + +# 创建启动脚本 +COPY docker/start.sh /app/start.sh +RUN chmod +x /app/start.sh + +# 创建 Nginx 运行目录 +RUN mkdir -p /var/log/nginx /var/lib/nginx /var/cache/nginx && \ + chown -R appuser:appuser /var/log/nginx /var/lib/nginx /var/cache/nginx + +# 创建非 root 用户 +RUN useradd -m -u 1000 appuser && \ + chown -R appuser:appuser /app && \ + chown -R appuser:appuser /usr/share/nginx/html && \ + chown -R appuser:appuser /var/log/nginx && \ + chown -R appuser:appuser /var/lib/nginx && \ + chown -R appuser:appuser /etc/nginx + +USER appuser + +# 暴露端口 +EXPOSE 80 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ + CMD curl -f http://localhost/api/health || exit 1 + +# 启动服务(同时启动 Nginx 和后端) +ENTRYPOINT ["/app/start.sh"] + diff --git a/README.md b/README.md index 55ab1ff..6eec3a1 100644 --- a/README.md +++ b/README.md @@ -124,25 +124,33 @@ npm run dev #### 快速部署 -**后端(Java 方式)**: +**一体化部署(推荐)** - 前后端一起部署到一个容器: +```bash +# 在项目根目录 +./deploy.sh +``` + +**分别部署**: + +后端(Java 方式): ```bash cd backend ./deploy.sh java ``` -**后端(Docker 方式)**: +后端(Docker 方式): ```bash cd backend ./deploy.sh docker ``` -**前端**: +前端: ```bash cd frontend -# 使用默认后端地址 +# 使用默认后端地址(相对路径) ./build.sh -# 或指定自定义后端地址 +# 或指定自定义后端地址(跨域场景) ./build.sh --api-url http://your-backend-server.com:8000 ``` diff --git a/backend/deploy.sh b/backend/deploy.sh index 69f6488..fa4fe3d 100755 --- a/backend/deploy.sh +++ b/backend/deploy.sh @@ -150,7 +150,12 @@ JWT_SECRET=your-jwt-secret-key-change-in-production # 管理员密码重置密钥(生产环境必须修改) ADMIN_RESET_PASSWORD_KEY=your-admin-reset-key-change-in-production EOF - warn "请编辑 .env 文件配置相关参数" + error ".env 文件已创建,请先编辑配置相关参数后再运行部署脚本" + error "特别是以下参数必须修改:" + error " - DB_PASSWORD: 数据库密码" + error " - JWT_SECRET: JWT 密钥(生产环境)" + error " - ADMIN_RESET_PASSWORD_KEY: 管理员密码重置密钥(生产环境)" + exit 1 fi # 构建并启动 diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..011fb81 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,114 @@ +#!/bin/bash + +# PolyHermes 一体化部署脚本 +# 将前后端一起部署到一个 Docker 容器中 + +set -e + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# 打印信息 +info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# 检查 Docker 环境 +check_docker() { + if ! command -v docker &> /dev/null; then + error "Docker 未安装,请先安装 Docker" + exit 1 + fi + + if ! command -v docker-compose &> /dev/null; then + error "Docker Compose 未安装,请先安装 Docker Compose" + exit 1 + fi + + info "Docker 环境检查通过" +} + +# 创建 .env 文件(如果不存在) +create_env_file() { + if [ ! -f ".env" ]; then + warn ".env 文件不存在,创建示例文件..." + cat > .env </dev/null || true + fi + nginx -s quit 2>/dev/null || true + exit 0 +} + +# 注册信号处理 +trap cleanup SIGTERM SIGINT + +# 启动后端服务(后台运行) +echo "启动后端服务..." +java -jar /app/app.jar --spring.profiles.active=${SPRING_PROFILES_ACTIVE:-prod} & +BACKEND_PID=$! + +# 等待后端服务启动 +echo "等待后端服务启动..." +for i in {1..60}; do + if curl -f http://localhost:8000/api/health > /dev/null 2>&1; then + echo "后端服务已启动" + break + fi + if [ $i -eq 60 ]; then + echo "后端服务启动超时" + exit 1 + fi + sleep 1 +done + +# 启动 Nginx(前台运行,作为主进程) +echo "启动 Nginx..." +exec nginx -g "daemon off;" + diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index fe579b6..c5ab7ee 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -4,6 +4,7 @@ ## 目录 +- [一体化部署(推荐)](#一体化部署推荐) - [后端部署](#后端部署) - [Java 直接部署](#java-直接部署) - [Docker 部署](#docker-部署) @@ -11,6 +12,82 @@ - [环境配置](#环境配置) - [常见问题](#常见问题) +## 一体化部署(推荐) + +将前后端一起部署到一个 Docker 容器中,使用 Nginx 提供前端静态文件并代理后端 API。 + +### 前置要求 + +- Docker 20.10+ +- Docker Compose 2.0+ + +### 部署步骤 + +1. **使用部署脚本(推荐)** + +```bash +# 在项目根目录 +./deploy.sh +``` + +脚本会自动: +- 检查 Docker 环境 +- 创建 `.env` 配置文件(如果不存在) +- 构建 Docker 镜像(包含前后端) +- 启动服务(应用 + MySQL) + +2. **手动部署** + +```bash +# 创建 .env 文件 +cat > .env < .env.production < .env.production < 后端地址,/ws -> 后端 WebSocket 地址" + fi + info " 3. 使用 serve 预览:npx serve -s dist -l 3000" + echo "" + info "提示:可通过环境变量配置后端地址:" + info " VITE_API_URL=http://your-backend.com:8000 ./build.sh" + info " 或使用参数:./build.sh --api-url http://your-backend.com:8000" } main "$@" diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 173524b..f7daf9f 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -5,17 +5,16 @@ import { wsManager } from './websocket' /** * API 基础配置 - * 支持通过环境变量 VITE_API_URL 配置后端地址 - * 默认使用相对路径 /api(适用于同域部署) - * 如果设置了 VITE_API_URL,则使用完整 URL + * 默认使用相对路径 /api(通过反向代理转发) + * 如果设置了 VITE_API_URL 环境变量,则使用完整 URL(用于跨域场景) */ const getBaseURL = (): string => { const envApiUrl = import.meta.env.VITE_API_URL if (envApiUrl) { - // 如果设置了环境变量,使用完整 URL + // 如果设置了环境变量,使用完整 URL(支持跨域) return `${envApiUrl}/api` } - // 否则使用相对路径(适用于开发环境代理或同域部署) + // 否则使用相对路径(通过反向代理转发) return '/api' } diff --git a/frontend/src/services/websocket.ts b/frontend/src/services/websocket.ts index d79a476..2af25d4 100644 --- a/frontend/src/services/websocket.ts +++ b/frontend/src/services/websocket.ts @@ -325,17 +325,18 @@ class WebSocketManager { /** * 获取 WebSocket URL(带token认证) - * 支持通过环境变量 VITE_WS_URL 配置 WebSocket 地址 + * 默认使用相对路径 /ws(通过反向代理转发) + * 如果设置了 VITE_WS_URL 环境变量,则使用完整 URL(用于跨域场景) */ private getWebSocketUrl(): string { const envWsUrl = import.meta.env.VITE_WS_URL let wsBaseUrl: string if (envWsUrl) { - // 如果设置了环境变量,使用完整 URL + // 如果设置了环境变量,使用完整 URL(支持跨域) wsBaseUrl = envWsUrl } else { - // 否则使用相对路径(适用于开发环境代理或同域部署) + // 否则使用相对路径(通过反向代理转发) const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' const host = window.location.host wsBaseUrl = `${protocol}//${host}` diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 9faaf41..6168db4 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -6,7 +6,8 @@ export default defineConfig(({ mode }) => { // 加载环境变量 const env = loadEnv(mode, process.cwd(), '') - // 从环境变量获取 API 地址,默认使用 localhost:8000 + // 从环境变量获取后端地址,用于开发环境代理 + // 如果未设置,使用默认值 localhost:8000 const API_URL = env.VITE_API_URL || 'http://localhost:8000' const WS_URL = env.VITE_WS_URL || 'ws://localhost:8000' @@ -17,8 +18,7 @@ export default defineConfig(({ mode }) => { proxy: { '/api': { target: API_URL, - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api/, '/api') + changeOrigin: true }, '/ws': { target: WS_URL, @@ -26,11 +26,6 @@ export default defineConfig(({ mode }) => { changeOrigin: true } } - }, - // 定义环境变量,在构建时注入 - define: { - 'import.meta.env.VITE_API_URL': JSON.stringify(API_URL), - 'import.meta.env.VITE_WS_URL': JSON.stringify(WS_URL) } } })