diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..fa0a6ba --- /dev/null +++ b/.drone.yml @@ -0,0 +1,183 @@ +--- +kind: pipeline +type: docker +name: 测试环境部署 + +trigger: + branch: + - develop + event: + - push + +steps: + - name: 部署到测试服务器 + image: appleboy/drone-ssh + settings: + host: 119.91.201.181 + username: root + password: + from_secret: server_password + port: 22 + command_timeout: 10m + envs: + - gitea_password + - mysql_root_password + - mysql_password + - secret_key + script: + - echo "========== 开始部署测试环境 ==========" + - export DEPLOY_ENV=test + - export APP_NAME=pricing + - export DEPLOY_DIR=/opt/apps/pricing-test + + # 创建部署目录 + - mkdir -p $DEPLOY_DIR + - cd $DEPLOY_DIR + + # 克隆或更新代码 + - | + if [ -d ".git" ]; then + git fetch origin develop + git reset --hard origin/develop + else + git clone -b develop "https://kuzma:$GITEA_PASSWORD@git.ai.ireborn.com.cn/kuzma/smart-project-pricing.git" . + fi + + # 创建环境配置文件 + - | + cat > .env << 'ENVEOF' + APP_NAME=智能项目定价模型 + APP_VERSION=1.0.0 + APP_ENV=test + DEBUG=true + SECRET_KEY=$SECRET_KEY + DATABASE_URL=mysql+aiomysql://pricing_user:$MYSQL_PASSWORD@pricing-mysql-test:3306/pricing_model_test?charset=utf8mb4 + MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD + MYSQL_USER=pricing_user + MYSQL_PASSWORD=$MYSQL_PASSWORD + DB_POOL_SIZE=5 + DB_MAX_OVERFLOW=10 + DB_POOL_RECYCLE=3600 + PORTAL_CONFIG_API=http://portal-backend:8000/api/ai/internal/config + AI_MODULE_CODE=pricing_model + TIMEZONE=Asia/Shanghai + CORS_ORIGINS=["https://pricing.test.zhicheng.ireborn.com.cn"] + API_V1_PREFIX=/api/v1 + ENVEOF + + # 替换环境变量 + - sed -i "s|\$MYSQL_ROOT_PASSWORD|$MYSQL_ROOT_PASSWORD|g" .env + - sed -i "s|\$MYSQL_PASSWORD|$MYSQL_PASSWORD|g" .env + - sed -i "s|\$SECRET_KEY|$SECRET_KEY|g" .env + + # 创建外部网络(如果不存在) + - docker network create traefik_network 2>/dev/null || true + - docker network create scrm_network 2>/dev/null || true + - docker network create ai_ai-strategy-network 2>/dev/null || true + + # 停止旧容器并重新部署 + - docker compose -f docker-compose.test.yml down --remove-orphans || true + - docker compose -f docker-compose.test.yml pull || true + - docker compose -f docker-compose.test.yml build --no-cache + - docker compose -f docker-compose.test.yml up -d + + # 等待服务启动 + - sleep 15 + + # 健康检查 + - docker compose -f docker-compose.test.yml ps + - echo "========== 测试环境部署完成 ==========" + - echo "前端地址: https://pricing.test.zhicheng.ireborn.com.cn" + - echo "后端API: https://pricing-api.test.zhicheng.ireborn.com.cn" + +--- +kind: pipeline +type: docker +name: 生产环境部署 + +trigger: + branch: + - main + event: + - push + +steps: + - name: 部署到生产服务器 + image: appleboy/drone-ssh + settings: + host: 119.91.201.181 + username: root + password: + from_secret: server_password + port: 22 + command_timeout: 10m + envs: + - gitea_password + - mysql_root_password + - mysql_password + - secret_key + script: + - echo "========== 开始部署生产环境 ==========" + - export DEPLOY_ENV=prod + - export APP_NAME=pricing + - export DEPLOY_DIR=/opt/apps/pricing-prod + + # 创建部署目录 + - mkdir -p $DEPLOY_DIR + - cd $DEPLOY_DIR + + # 克隆或更新代码 + - | + if [ -d ".git" ]; then + git fetch origin main + git reset --hard origin/main + else + git clone -b main "https://kuzma:$GITEA_PASSWORD@git.ai.ireborn.com.cn/kuzma/smart-project-pricing.git" . + fi + + # 创建环境配置文件 + - | + cat > .env << 'ENVEOF' + APP_NAME=智能项目定价模型 + APP_VERSION=1.0.0 + APP_ENV=production + DEBUG=false + SECRET_KEY=$SECRET_KEY + DATABASE_URL=mysql+aiomysql://pricing_user:$MYSQL_PASSWORD@pricing-mysql-prod:3306/pricing_model_prod?charset=utf8mb4 + MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD + MYSQL_USER=pricing_user + MYSQL_PASSWORD=$MYSQL_PASSWORD + DB_POOL_SIZE=5 + DB_MAX_OVERFLOW=10 + DB_POOL_RECYCLE=3600 + PORTAL_CONFIG_API=http://portal-backend:8000/api/ai/internal/config + AI_MODULE_CODE=pricing_model + TIMEZONE=Asia/Shanghai + CORS_ORIGINS=["https://pricing.zhicheng.ireborn.com.cn"] + API_V1_PREFIX=/api/v1 + ENVEOF + + # 替换环境变量 + - sed -i "s|\$MYSQL_ROOT_PASSWORD|$MYSQL_ROOT_PASSWORD|g" .env + - sed -i "s|\$MYSQL_PASSWORD|$MYSQL_PASSWORD|g" .env + - sed -i "s|\$SECRET_KEY|$SECRET_KEY|g" .env + + # 创建外部网络(如果不存在) + - docker network create traefik_network 2>/dev/null || true + - docker network create scrm_network 2>/dev/null || true + - docker network create ai_ai-strategy-network 2>/dev/null || true + + # 停止旧容器并重新部署 + - docker compose -f docker-compose.prod.yml down --remove-orphans || true + - docker compose -f docker-compose.prod.yml pull || true + - docker compose -f docker-compose.prod.yml build --no-cache + - docker compose -f docker-compose.prod.yml up -d + + # 等待服务启动 + - sleep 15 + + # 健康检查 + - docker compose -f docker-compose.prod.yml ps + - echo "========== 生产环境部署完成 ==========" + - echo "前端地址: https://pricing.zhicheng.ireborn.com.cn" + - echo "后端API: https://pricing-api.zhicheng.ireborn.com.cn" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..2f68ec7 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,154 @@ +# 智能项目定价模型 - 生产环境 Docker Compose 配置 +# 域名: pricing.zhicheng.ireborn.com.cn + +name: pricing-model-prod + +services: + # ============ 前端服务 ============ + pricing-frontend-prod: + build: + context: ./前端应用 + dockerfile: Dockerfile + args: + - VITE_API_BASE_URL=https://pricing-api.zhicheng.ireborn.com.cn/api/v1 + container_name: pricing-frontend-prod + restart: unless-stopped + labels: + - "traefik.enable=true" + - "traefik.http.routers.pricing-frontend-prod.rule=Host(`pricing.zhicheng.ireborn.com.cn`)" + - "traefik.http.routers.pricing-frontend-prod.entrypoints=websecure" + - "traefik.http.routers.pricing-frontend-prod.tls=true" + - "traefik.http.routers.pricing-frontend-prod.tls.certresolver=letsencrypt" + - "traefik.http.services.pricing-frontend-prod.loadbalancer.server.port=80" + networks: + - pricing_network_prod + - traefik_network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + cpus: '0.5' + memory: 256M + reservations: + cpus: '0.1' + memory: 64M + + # ============ 后端服务 ============ + pricing-backend-prod: + build: + context: ./后端服务 + dockerfile: Dockerfile + container_name: pricing-backend-prod + restart: unless-stopped + env_file: + - .env + environment: + - DATABASE_URL=mysql+aiomysql://pricing_user:${MYSQL_PASSWORD}@pricing-mysql-prod:3306/pricing_model_prod?charset=utf8mb4 + - PORTAL_CONFIG_API=${PORTAL_CONFIG_API} + - APP_ENV=production + - DEBUG=false + - SECRET_KEY=${SECRET_KEY} + - CORS_ORIGINS=["https://pricing.zhicheng.ireborn.com.cn"] + labels: + - "traefik.enable=true" + - "traefik.http.routers.pricing-backend-prod.rule=Host(`pricing-api.zhicheng.ireborn.com.cn`)" + - "traefik.http.routers.pricing-backend-prod.entrypoints=websecure" + - "traefik.http.routers.pricing-backend-prod.tls=true" + - "traefik.http.routers.pricing-backend-prod.tls.certresolver=letsencrypt" + - "traefik.http.services.pricing-backend-prod.loadbalancer.server.port=8000" + depends_on: + pricing-mysql-prod: + condition: service_healthy + networks: + - pricing_network_prod + - traefik_network + - scrm_network + - ai_strategy_network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + cpus: '1' + memory: 512M + reservations: + cpus: '0.25' + memory: 128M + + # ============ 数据库服务 ============ + pricing-mysql-prod: + image: mysql:8.0.36 + container_name: pricing-mysql-prod + restart: unless-stopped + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --default-time-zone=+08:00 + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: pricing_model_prod + MYSQL_USER: pricing_user + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + volumes: + - pricing_mysql_data_prod:/var/lib/mysql + - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro + networks: + - pricing_network_prod + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + cpus: '1' + memory: 1G + reservations: + cpus: '0.25' + memory: 256M + +# ============ 网络配置 ============ +networks: + pricing_network_prod: + driver: bridge + name: pricing_network_prod + traefik_network: + external: true + name: traefik_network + scrm_network: + external: true + name: scrm_network + ai_strategy_network: + external: true + name: ai_ai-strategy-network + +# ============ 数据卷 ============ +volumes: + pricing_mysql_data_prod: + name: pricing_mysql_data_prod diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000..02679cf --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,154 @@ +# 智能项目定价模型 - 测试环境 Docker Compose 配置 +# 域名: pricing.test.zhicheng.ireborn.com.cn + +name: pricing-model-test + +services: + # ============ 前端服务 ============ + pricing-frontend-test: + build: + context: ./前端应用 + dockerfile: Dockerfile + args: + - VITE_API_BASE_URL=https://pricing-api.test.zhicheng.ireborn.com.cn/api/v1 + container_name: pricing-frontend-test + restart: unless-stopped + labels: + - "traefik.enable=true" + - "traefik.http.routers.pricing-frontend-test.rule=Host(`pricing.test.zhicheng.ireborn.com.cn`)" + - "traefik.http.routers.pricing-frontend-test.entrypoints=websecure" + - "traefik.http.routers.pricing-frontend-test.tls=true" + - "traefik.http.routers.pricing-frontend-test.tls.certresolver=letsencrypt" + - "traefik.http.services.pricing-frontend-test.loadbalancer.server.port=80" + networks: + - pricing_network_test + - traefik_network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + cpus: '0.5' + memory: 256M + reservations: + cpus: '0.1' + memory: 64M + + # ============ 后端服务 ============ + pricing-backend-test: + build: + context: ./后端服务 + dockerfile: Dockerfile + container_name: pricing-backend-test + restart: unless-stopped + env_file: + - .env + environment: + - DATABASE_URL=mysql+aiomysql://pricing_user:pricing_test_123@pricing-mysql-test:3306/pricing_model_test?charset=utf8mb4 + - PORTAL_CONFIG_API=${PORTAL_CONFIG_API} + - APP_ENV=test + - DEBUG=true + - SECRET_KEY=${SECRET_KEY} + - CORS_ORIGINS=["https://pricing.test.zhicheng.ireborn.com.cn"] + labels: + - "traefik.enable=true" + - "traefik.http.routers.pricing-backend-test.rule=Host(`pricing-api.test.zhicheng.ireborn.com.cn`)" + - "traefik.http.routers.pricing-backend-test.entrypoints=websecure" + - "traefik.http.routers.pricing-backend-test.tls=true" + - "traefik.http.routers.pricing-backend-test.tls.certresolver=letsencrypt" + - "traefik.http.services.pricing-backend-test.loadbalancer.server.port=8000" + depends_on: + pricing-mysql-test: + condition: service_healthy + networks: + - pricing_network_test + - traefik_network + - scrm_network + - ai_strategy_network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + cpus: '1' + memory: 512M + reservations: + cpus: '0.25' + memory: 128M + + # ============ 数据库服务 ============ + pricing-mysql-test: + image: mysql:8.0.36 + container_name: pricing-mysql-test + restart: unless-stopped + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --default-time-zone=+08:00 + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root_test_123} + MYSQL_DATABASE: pricing_model_test + MYSQL_USER: pricing_user + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-pricing_test_123} + volumes: + - pricing_mysql_data_test:/var/lib/mysql + - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro + networks: + - pricing_network_test + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-root_test_123}"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + cpus: '1' + memory: 1G + reservations: + cpus: '0.25' + memory: 256M + +# ============ 网络配置 ============ +networks: + pricing_network_test: + driver: bridge + name: pricing_network_test + traefik_network: + external: true + name: traefik_network + scrm_network: + external: true + name: scrm_network + ai_strategy_network: + external: true + name: ai_ai-strategy-network + +# ============ 数据卷 ============ +volumes: + pricing_mysql_data_test: + name: pricing_mysql_data_test diff --git a/前端应用/Dockerfile b/前端应用/Dockerfile index e4e2021..6eb8e48 100644 --- a/前端应用/Dockerfile +++ b/前端应用/Dockerfile @@ -4,6 +4,9 @@ # 构建阶段 FROM node:20.11-alpine AS builder +# 构建参数 - API 地址 +ARG VITE_API_BASE_URL=/api/v1 + # 设置工作目录 WORKDIR /app @@ -23,6 +26,9 @@ RUN pnpm install # 复制源代码 COPY . . +# 设置构建时环境变量 +ENV VITE_API_BASE_URL=$VITE_API_BASE_URL + # 构建 RUN pnpm build diff --git a/前端应用/src/api/request.ts b/前端应用/src/api/request.ts index 9059c6e..78fa8d5 100644 --- a/前端应用/src/api/request.ts +++ b/前端应用/src/api/request.ts @@ -38,9 +38,12 @@ export const ErrorCode = { AI_SERVICE_TIMEOUT: 40002, } +// API 基础地址 - 支持环境变量配置 +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api/v1' + // 创建 Axios 实例 const instance: AxiosInstance = axios.create({ - baseURL: '/api/v1', + baseURL: API_BASE_URL, timeout: 30000, headers: { 'Content-Type': 'application/json', diff --git a/前端应用/src/vite-env.d.ts b/前端应用/src/vite-env.d.ts index 323c78a..f96b942 100644 --- a/前端应用/src/vite-env.d.ts +++ b/前端应用/src/vite-env.d.ts @@ -1,5 +1,13 @@ /// +interface ImportMetaEnv { + readonly VITE_API_BASE_URL: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} + declare module '*.vue' { import type { DefineComponent } from 'vue' const component: DefineComponent<{}, {}, any>