From fdf6d19ed76d9afb86ac027e9b0d306b63d593c4 Mon Sep 17 00:00:00 2001 From: caoyuchun <335003032@qq.com> Date: Wed, 14 Jan 2026 19:30:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=8E=AF=E5=A2=83=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81docker=20=E5=AE=89=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DOCKER_SETUP.md | 54 ++++++ JENKINS_DOCKER_SETUP.md | 219 ++++++++++++++++++++++ backend/Dockerfile | 23 ++- backend/prisma/schema.prisma | 5 + frontend/package.json | 1 + frontend/src/components/ErrorBoundary.tsx | 2 +- frontend/src/pages/PathDetailPage.tsx | 52 ++++- nginx/docker-compose.yml | 8 +- nginx/nginx.conf | 5 +- scripts/start-local.sh | 86 +++++++++ scripts/stop-local.sh | 20 ++ 11 files changed, 457 insertions(+), 18 deletions(-) create mode 100644 DOCKER_SETUP.md create mode 100644 JENKINS_DOCKER_SETUP.md create mode 100755 scripts/start-local.sh create mode 100755 scripts/stop-local.sh diff --git a/DOCKER_SETUP.md b/DOCKER_SETUP.md new file mode 100644 index 0000000..5261dca --- /dev/null +++ b/DOCKER_SETUP.md @@ -0,0 +1,54 @@ +# Docker 镜像加速器配置 + +## 问题 +无法从 Docker Hub 拉取镜像,出现网络超时错误。 + +## 解决方案 + +### 方法一:配置 Docker Desktop 镜像加速器(macOS) + +1. 打开 Docker Desktop +2. 点击设置(Settings/Preferences) +3. 选择 Docker Engine +4. 添加以下配置: + +```json +{ + "registry-mirrors": [ + "https://docker.mirrors.ustc.edu.cn", + "https://hub-mirror.c.163.com", + "https://mirror.baidubce.com" + ] +} +``` + +5. 点击 Apply & Restart + +### 方法二:使用代理(如果有) + +如果有代理,可以在 Docker Desktop 中配置代理: +- Settings → Resources → Proxies +- 配置 HTTP/HTTPS 代理 + +### 方法三:等待网络恢复 + +如果网络暂时有问题,可以稍后重试。 + +## 验证配置 + +配置完成后,测试拉取镜像: + +```bash +docker pull node:18-alpine +``` + +## 临时解决方案 + +如果急需启动服务,可以先启动 nginx(已有镜像): + +```bash +cd nginx +docker compose up nginx -d +``` + +等网络恢复后再启动后端。 diff --git a/JENKINS_DOCKER_SETUP.md b/JENKINS_DOCKER_SETUP.md new file mode 100644 index 0000000..866cb11 --- /dev/null +++ b/JENKINS_DOCKER_SETUP.md @@ -0,0 +1,219 @@ +# Jenkins Docker Pipeline 插件安装指南 + +## 安装步骤 + +### 1. 进入 Jenkins 插件管理 + +1. 登录 Jenkins +2. 点击 **系统管理** (Manage Jenkins) +3. 点击 **插件管理** (Manage Plugins) + +### 2. 安装 Docker Pipeline 插件 + +#### 方式一:通过界面安装(推荐) + +1. 在插件管理页面,点击 **可选插件** (Available) +2. 在搜索框输入 `Docker Pipeline` +3. 找到 **Docker Pipeline** 插件 +4. 勾选插件 +5. 点击 **直接安装** (Install without restart) 或 **下载待重启后安装** (Download now and install after restart) +6. 等待安装完成 +7. 如果提示重启,点击 **重启 Jenkins** (Restart Jenkins) + +#### 方式二:通过命令行安装 + +```bash +# 在 Jenkins 服务器上执行 +jenkins-plugin-cli install docker-workflow +``` + +### 3. 验证安装 + +安装完成后,可以通过以下方式验证: + +#### 方式一:在 Jenkins 中测试 + +创建一个测试 Pipeline: + +```groovy +pipeline { + agent any + stages { + stage('Test Docker') { + steps { + script { + def nodeImage = docker.image("node:18") + nodeImage.inside() { + sh 'node --version' + } + } + } + } + } +} +``` + +如果能够成功执行,说明插件安装成功。 + +#### 方式二:检查插件列表 + +1. 进入 **系统管理** → **插件管理** → **已安装** +2. 搜索 `Docker Pipeline` +3. 确认插件已安装并启用 + +## 配置 Docker 访问权限 + +### 1. 确保 Docker 服务运行 + +```bash +# 检查 Docker 状态 +sudo systemctl status docker + +# 如果未运行,启动 Docker +sudo systemctl start docker +sudo systemctl enable docker +``` + +### 2. 配置 Jenkins 用户权限 + +```bash +# 将 Jenkins 用户添加到 docker 组 +sudo usermod -aG docker jenkins + +# 重启 Jenkins +sudo systemctl restart jenkins +``` + +### 3. 验证 Docker 访问 + +```bash +# 切换到 Jenkins 用户测试 +sudo -u jenkins docker ps + +# 如果成功,说明权限配置正确 +``` + +## 常见问题 + +### 1. 插件安装失败 + +**问题**: 插件下载失败或安装超时 + +**解决**: +- 检查网络连接 +- 更换 Jenkins 更新中心镜像源 +- 手动下载插件并上传安装 + +### 2. Docker 命令权限被拒绝 + +**错误**: `permission denied while trying to connect to the Docker daemon socket` + +**解决**: +```bash +# 将 Jenkins 用户添加到 docker 组 +sudo usermod -aG docker jenkins + +# 重启 Jenkins +sudo systemctl restart jenkins +``` + +### 3. Docker 镜像拉取失败 + +**错误**: `Error pulling image` + +**解决**: +- 检查网络连接 +- 配置 Docker 镜像加速器(如阿里云、腾讯云) +- 手动拉取镜像:`docker pull node:18` + +### 4. 插件已安装但 docker 对象不可用 + +**问题**: 安装插件后仍然报错 `No such property: docker` + +**解决**: +1. 确认插件已启用(插件管理 → 已安装 → 检查状态) +2. 重启 Jenkins +3. 检查 Jenkins 日志:`/var/log/jenkins/jenkins.log` + +## 安装后的效果 + +安装 Docker Pipeline 插件后: + +1. ✅ **可以使用 `docker.image()` 方法** + ```groovy + def nodeImage = docker.image("node:18") + ``` + +2. ✅ **可以在容器中执行构建** + ```groovy + nodeImage.inside() { + sh 'npm install' + } + ``` + +3. ✅ **可以构建 Docker 镜像** + ```groovy + docker.build("myapp:${env.BUILD_NUMBER}") + ``` + +4. ✅ **可以推送镜像到仓库** + ```groovy + docker.image("myapp:${env.BUILD_NUMBER}").push() + ``` + +## 当前 Jenkinsfile 说明 + +当前 Jenkinsfile 已经配置了 Docker 构建: + +```groovy +stage('Build') { + steps { + script { + try { + def nodeImage = docker.image("node:18") + nodeImage.inside() { + // 构建步骤 + } + } catch (Exception e) { + // 回退到主机构建 + } + } + } +} +``` + +**特点**: +- 优先使用 Docker 构建(环境隔离) +- 如果 Docker 插件不可用,自动回退到主机构建 +- 确保在任何情况下都能正常构建 + +## 推荐配置 + +安装插件后,建议: + +1. **固定 Node.js 版本**: 使用 `node:18` 而不是 `node:latest` +2. **使用 Alpine 镜像**: 更小的镜像体积,如 `node:18-alpine` +3. **缓存依赖**: 使用 Docker layer caching 加速构建 +4. **定期更新镜像**: 定期拉取最新的基础镜像 + +## 测试构建 + +安装插件后,可以运行一次构建测试: + +1. 在 Jenkins 中触发构建 +2. 查看构建日志,应该看到: + ``` + 使用 Docker 容器构建... + 在 Docker 容器中构建... + ``` +3. 如果看到这些日志,说明 Docker 构建正常工作 + +## 总结 + +安装 **Docker Pipeline** 插件后,Jenkinsfile 中的 Docker 构建功能就可以正常使用了。插件提供了: +- Docker 镜像管理 +- 容器内执行命令 +- Docker 镜像构建和推送 +- 完整的 Docker 集成 + +安装完成后,重新运行构建即可! diff --git a/backend/Dockerfile b/backend/Dockerfile index bda2b80..e3fb7db 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,16 +1,27 @@ FROM node:18-alpine +# 安装 OpenSSL(Prisma 需要) +# Alpine 3.19+ 使用 openssl3,旧版本使用 openssl1.1-compat +RUN apk add --no-cache openssl libc6-compat || \ + apk add --no-cache openssl1.1-compat || \ + apk add --no-cache openssl + WORKDIR /app -# 复制 package 文件 -COPY package*.json ./ -COPY prisma ./prisma/ +# 先复制 shared 包(workspace 依赖) +COPY shared ./shared -# 安装依赖(包括生产依赖) -RUN npm ci --production +# 复制 backend 的 package 文件 +COPY backend/package*.json ./ +COPY backend/prisma ./prisma/ + +# 安装依赖(shared 包直接使用源码,不需要安装) +# 修改 package.json 中的 shared 依赖为 file:./shared +RUN sed -i 's|"@ai-learning/shared": "\*"|"@ai-learning/shared": "file:./shared"|' package.json && \ + npm install --production # 复制构建产物 -COPY dist ./dist +COPY backend/dist ./dist # 生成 Prisma Client RUN npx prisma generate diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 8d789c7..8df4198 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -21,6 +21,7 @@ model Course { updatedAt DateTime @updatedAt chapters Chapter[] pathItems PathItem[] + userProgress UserProgress[] } model Chapter { @@ -33,6 +34,7 @@ model Chapter { updatedAt DateTime @updatedAt course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) pathItems PathItem[] + userProgress UserProgress[] } model LearningPath { @@ -69,5 +71,8 @@ model UserProgress { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: Cascade) + @@unique([userId, courseId, chapterId]) } diff --git a/frontend/package.json b/frontend/package.json index b4f3fc4..1720a27 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", + "build:with-lint": "npm run lint && tsc && vite build", "preview": "vite preview", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" }, diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx index 57e6111..add1479 100644 --- a/frontend/src/components/ErrorBoundary.tsx +++ b/frontend/src/components/ErrorBoundary.tsx @@ -1,4 +1,4 @@ -import React, { Component, ErrorInfo, ReactNode } from 'react'; +import { Component, ErrorInfo, ReactNode } from 'react'; interface Props { children: ReactNode; diff --git a/frontend/src/pages/PathDetailPage.tsx b/frontend/src/pages/PathDetailPage.tsx index 52a9fcf..82b6942 100644 --- a/frontend/src/pages/PathDetailPage.tsx +++ b/frontend/src/pages/PathDetailPage.tsx @@ -1,11 +1,23 @@ import { useEffect, useState } from 'react'; import { useParams, Link } from 'react-router-dom'; -import { pathApi } from '../services/api'; -import type { LearningPath } from '@ai-learning/shared'; +import { pathApi, courseApi, chapterApi } from '../services/api'; +import type { LearningPath, Course, Chapter } from '@ai-learning/shared'; + +interface PathItemWithDetails { + id: string; + pathId: string; + courseId: string; + chapterId?: string; + order: number; + type: 'course' | 'chapter'; + course?: Course; + chapter?: Chapter; +} export default function PathDetailPage() { const { id } = useParams<{ id: string }>(); const [path, setPath] = useState(null); + const [pathItems, setPathItems] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { @@ -20,7 +32,37 @@ export default function PathDetailPage() { try { setLoading(true); const response = await pathApi.getById(id); - setPath(response.data); + const pathData = response.data; + setPath(pathData); + + // 加载每个 pathItem 的详细信息 + const itemsWithDetails = await Promise.all( + pathData.pathItems.map(async (item) => { + const details: PathItemWithDetails = { ...item }; + + // 加载课程信息 + try { + const courseResponse = await courseApi.getById(item.courseId); + details.course = courseResponse.data; + } catch (error) { + console.error(`Failed to load course ${item.courseId}:`, error); + } + + // 如果是章节类型,加载章节信息 + if (item.type === 'chapter' && item.chapterId) { + try { + const chapterResponse = await chapterApi.getById(item.chapterId); + details.chapter = chapterResponse.data; + } catch (error) { + console.error(`Failed to load chapter ${item.chapterId}:`, error); + } + } + + return details; + }) + ); + + setPathItems(itemsWithDetails); } catch (error) { console.error('Failed to load path:', error); } finally { @@ -62,11 +104,11 @@ export default function PathDetailPage() {

学习路径

- {!path.pathItems || path.pathItems.length === 0 ? ( + {!pathItems || pathItems.length === 0 ? (

路径为空

) : (
- {path.pathItems.map((item, index) => ( + {pathItems.map((item, index) => (
{index + 1} diff --git a/nginx/docker-compose.yml b/nginx/docker-compose.yml index cef8d88..4d0325a 100644 --- a/nginx/docker-compose.yml +++ b/nginx/docker-compose.yml @@ -6,8 +6,8 @@ services: image: nginx:alpine container_name: ai-learning-nginx ports: - - "80:80" - - "443:443" + - "8080:80" + - "8443:443" volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro - ../frontend/dist:/usr/share/nginx/html:ro @@ -21,8 +21,8 @@ services: # 后端服务 backend: build: - context: ../backend - dockerfile: Dockerfile + context: .. # 使用项目根目录作为构建上下文 + dockerfile: backend/Dockerfile container_name: ai-learning-backend ports: - "3001:3001" diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 519f2ae..48e132d 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -1,8 +1,9 @@ # Nginx 配置文件 - AI学习平台 # 上游后端服务器 +# 在 Docker 网络中使用服务名,本地开发使用 localhost upstream backend { - server localhost:3001; + server backend:3001; keepalive 64; } @@ -20,7 +21,7 @@ server { # 前端静态文件 location / { - root /opt/nginx/html/ai/current; + root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; diff --git a/scripts/start-local.sh b/scripts/start-local.sh new file mode 100755 index 0000000..908b7d8 --- /dev/null +++ b/scripts/start-local.sh @@ -0,0 +1,86 @@ +#!/bin/bash + +# 本地 Docker 启动脚本 + +set -e + +echo "=== 检查 Docker ===" +if ! docker info > /dev/null 2>&1; then + echo "错误: Docker daemon 未运行" + echo "请先启动 Docker Desktop 或 Docker daemon" + exit 1 +fi + +echo "Docker 运行正常" +echo "" + +# 进入项目根目录 +cd "$(dirname "$0")/.." + +echo "=== 检查并构建项目 ===" + +# 检查前端是否已构建 +if [ ! -d "frontend/dist" ] || [ -z "$(ls -A frontend/dist 2>/dev/null)" ]; then + echo "前端未构建,开始构建..." + npm install + npm run install:all + npm run build --workspace=frontend +else + echo "前端已构建,跳过..." +fi + +# 检查后端是否已构建 +if [ ! -d "backend/dist" ] || [ -z "$(ls -A backend/dist 2>/dev/null)" ]; then + echo "后端未构建,开始构建..." + cd backend + npm install + npm run build + npm run prisma:generate + cd .. +else + echo "后端已构建,跳过..." +fi + +echo "" +echo "=== 启动 Docker 容器 ===" +cd nginx + +# 检查 docker-compose 命令(支持新版本的 docker compose) +if command -v docker-compose &> /dev/null; then + COMPOSE_CMD="docker-compose" +elif docker compose version &> /dev/null 2>&1; then + COMPOSE_CMD="docker compose" +else + echo "错误: 未找到 docker-compose 或 docker compose 命令" + exit 1 +fi + +# 停止旧容器(如果存在) +echo "停止旧容器..." +$COMPOSE_CMD down 2>/dev/null || true + +# 创建必要的目录 +mkdir -p logs + +# 启动容器 +echo "启动容器..." +$COMPOSE_CMD up -d --build + +echo "" +echo "=== 等待服务启动 ===" +sleep 5 + +# 检查容器状态 +echo "容器状态:" +$COMPOSE_CMD ps + +echo "" +echo "=== 服务信息 ===" +echo "前端: http://localhost" +echo "后端 API: http://localhost:3001" +echo "" +echo "查看日志:" +echo " $COMPOSE_CMD logs -f" +echo "" +echo "停止服务:" +echo " $COMPOSE_CMD down" diff --git a/scripts/stop-local.sh b/scripts/stop-local.sh new file mode 100755 index 0000000..de96ed9 --- /dev/null +++ b/scripts/stop-local.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# 停止本地 Docker 服务 + +cd "$(dirname "$0")/../nginx" + +# 检查 docker-compose 命令 +if command -v docker-compose &> /dev/null; then + COMPOSE_CMD="docker-compose" +elif docker compose version &> /dev/null 2>&1; then + COMPOSE_CMD="docker compose" +else + echo "错误: 未找到 docker-compose 或 docker compose 命令" + exit 1 +fi + +echo "停止 Docker 容器..." +$COMPOSE_CMD down + +echo "服务已停止"