Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ed5631969 | ||
|
|
65ee61c6c9 | ||
|
|
09e67cd834 | ||
|
|
fd317b7932 | ||
|
|
859dab9401 | ||
|
|
4f99f31475 | ||
|
|
43580dd1fd | ||
|
|
d5d9b7aa99 | ||
|
|
6eca6de249 | ||
|
|
a42df9d521 | ||
|
|
015ecd13d8 | ||
|
|
8abd750dfb | ||
|
|
f8e24ffdf0 | ||
|
|
688be6b65f | ||
|
|
a6e75f6e97 | ||
|
|
6cc5212eba | ||
|
|
2d87b96a09 | ||
|
|
eb2869a71d | ||
|
|
1789141a7b | ||
|
|
b80e4c01f5 | ||
|
|
b70d90d18a | ||
|
|
73fdaf82e7 | ||
|
|
5418f23111 | ||
|
|
58e9e4db22 | ||
|
|
a6dab438d5 | ||
|
|
174b4265d7 | ||
|
|
3d157ed30c |
@@ -0,0 +1,150 @@
|
|||||||
|
name: lpt-be Build & Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
description: '部署环境(dev 仅允许 dev 分支;prod 仅允许 master/main)'
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- dev
|
||||||
|
- prod
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: 192.168.123.199:5000
|
||||||
|
APP: lpt-be
|
||||||
|
REPO_URL: http://git.cat-shark.xyz/cat-shark/lpt-be.git
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Validate branch ↔ environment
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
REF="${{ gitea.ref }}"
|
||||||
|
BRANCH="${REF#refs/heads/}"
|
||||||
|
if [[ "$REF" == refs/tags/* ]]; then
|
||||||
|
echo "ERROR: 请从分支触发部署,不要用 tag。当前 ref=$REF"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ENV="${{ inputs.environment }}"
|
||||||
|
echo "branch=$BRANCH environment=$ENV sha=${{ gitea.sha }}"
|
||||||
|
case "$ENV" in
|
||||||
|
prod)
|
||||||
|
case "$BRANCH" in
|
||||||
|
master|main) echo "OK: prod 允许从 $BRANCH 部署" ;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: prod 只能从 master/main 部署,当前分支是 '$BRANCH'"
|
||||||
|
echo "请切换到 master 后再 Run workflow,并选择 environment=prod"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
dev)
|
||||||
|
case "$BRANCH" in
|
||||||
|
dev) echo "OK: dev 允许从 $BRANCH 部署" ;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: dev 只能从 dev 分支部署,当前分支是 '$BRANCH'"
|
||||||
|
echo "请切换到 dev 后再 Run workflow,并选择 environment=dev"
|
||||||
|
echo "(禁止用 master 代码部署到 lpt-dev,避免环境错配)"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: 未知 environment=$ENV"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git clone "$REPO_URL" .
|
||||||
|
git checkout "${{ gitea.sha }}"
|
||||||
|
|
||||||
|
- name: Login to Docker Registry
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "$REGISTRY" -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
|
||||||
|
|
||||||
|
- name: Build & Push Docker image
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ENV="${{ inputs.environment }}"
|
||||||
|
TAG="${{ gitea.sha }}-$(date +%s)"
|
||||||
|
echo "Building $REGISTRY/$APP:$TAG (env tag=$ENV)"
|
||||||
|
docker build -t "$REGISTRY/$APP:$TAG" -t "$REGISTRY/$APP:$ENV" .
|
||||||
|
docker push "$REGISTRY/$APP:$TAG"
|
||||||
|
docker push "$REGISTRY/$APP:$ENV"
|
||||||
|
echo "IMAGE_TAG=$TAG"
|
||||||
|
echo "$TAG" > image_tag.txt
|
||||||
|
mkdir -p /tmp/lpt-ci
|
||||||
|
echo "$TAG" > /tmp/lpt-ci/image_tag.txt
|
||||||
|
|
||||||
|
- name: Setup kubectl
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
curl -sLO "https://dl.k8s.io/release/v1.30.0/bin/linux/amd64/kubectl"
|
||||||
|
chmod +x kubectl
|
||||||
|
mv kubectl /usr/local/bin/
|
||||||
|
mkdir -p ~/.kube
|
||||||
|
echo "${{ secrets.KUBECONFIG_B64 }}" | base64 -d > ~/.kube/config
|
||||||
|
|
||||||
|
- name: Create/Update imagePullSecret
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
kubectl create secret docker-registry regcred \
|
||||||
|
--docker-server="$REGISTRY" \
|
||||||
|
--docker-username="${{ secrets.REGISTRY_USERNAME }}" \
|
||||||
|
--docker-password="${{ secrets.REGISTRY_PASSWORD }}" \
|
||||||
|
-n "lpt-${{ inputs.environment }}" \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
|
- name: Deploy to K8s
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ENV="${{ inputs.environment }}"
|
||||||
|
IMAGE_TAG="$(cat image_tag.txt 2>/dev/null || cat /tmp/lpt-ci/image_tag.txt 2>/dev/null || echo '')"
|
||||||
|
if [ -z "$IMAGE_TAG" ]; then
|
||||||
|
echo "ERROR: IMAGE_TAG 为空,无法部署"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Deploying $REGISTRY/$APP:$IMAGE_TAG -> namespace lpt-$ENV"
|
||||||
|
kubectl set image "deployment/$APP" \
|
||||||
|
"$APP=$REGISTRY/$APP:$IMAGE_TAG" \
|
||||||
|
-n "lpt-$ENV" --record
|
||||||
|
kubectl rollout status "deployment/$APP" \
|
||||||
|
-n "lpt-$ENV" --timeout=5m
|
||||||
|
|
||||||
|
- name: Debug on failure
|
||||||
|
if: failure()
|
||||||
|
run: |
|
||||||
|
ENV="${{ inputs.environment }}"
|
||||||
|
echo "=== Deployment Status ==="
|
||||||
|
kubectl get deployment "$APP" -n "lpt-$ENV" || true
|
||||||
|
echo ""
|
||||||
|
echo "=== Pod Status ==="
|
||||||
|
kubectl get pods -n "lpt-$ENV" -l "app=$APP" || true
|
||||||
|
echo ""
|
||||||
|
echo "=== Recent Events ==="
|
||||||
|
kubectl get events -n "lpt-$ENV" --sort-by='.lastTimestamp' | tail -20 || true
|
||||||
|
echo ""
|
||||||
|
echo "=== Pod Describe (latest) ==="
|
||||||
|
POD=$(kubectl get pods -n "lpt-$ENV" -l "app=$APP" --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1].metadata.name}' 2>/dev/null || true)
|
||||||
|
if [ -n "${POD:-}" ]; then
|
||||||
|
kubectl describe pod "$POD" -n "lpt-$ENV" || true
|
||||||
|
echo ""
|
||||||
|
echo "=== Pod Logs (latest) ==="
|
||||||
|
kubectl logs "$POD" -n "lpt-$ENV" --tail=50 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Rollback on failure
|
||||||
|
if: failure()
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ENV="${{ inputs.environment }}"
|
||||||
|
kubectl rollout undo "deployment/$APP" -n "lpt-$ENV" || true
|
||||||
|
kubectl rollout status "deployment/$APP" -n "lpt-$ENV" || true
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
# LPT 后端服务(lpt-be)
|
||||||
|
|
||||||
|
> Java Spring Boot 后端,提供 REST API、数据库访问、认证鉴权
|
||||||
|
|
||||||
|
## Git 提交规范
|
||||||
|
- 提交信息使用**中文**,简洁描述变更内容
|
||||||
|
- 格式:`类型: 简述`,如 `feat: 新增用户登录接口`、`fix: 修复多租户拦截器空指针`、`docs: 补充API文档`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
- **框架**: Spring Boot 3.2.5
|
||||||
|
- **ORM**: MyBatis-Plus 3.5.5
|
||||||
|
- **数据库**: MySQL 8.0(端口 8109,服务器 39.105.149.197)
|
||||||
|
- **认证**: Sa-Token 1.38.0(Cookie 名 `satoken`)
|
||||||
|
- **数据库迁移**: Flyway
|
||||||
|
- **对象映射**: MapStruct 1.5.5
|
||||||
|
- **连接池**: Druid 1.2.8
|
||||||
|
- **密码加密**: jBCrypt 0.4
|
||||||
|
- **API 文档**: Knife4j (OpenAPI 3)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
```
|
||||||
|
lpt-be/src/main/java/com/guo/learningprogresstracker/
|
||||||
|
├── controller/ → REST 接口层(6个控制器)
|
||||||
|
├── service/ → 业务逻辑层(12个服务接口 + impl)
|
||||||
|
├── mapper/ → MyBatis-Plus BaseMapper(10个)
|
||||||
|
├── entity/ → 数据库实体(10个)
|
||||||
|
├── dto/ → 请求/响应 DTO
|
||||||
|
│ ├── request/ → 请求体
|
||||||
|
│ └── response/ → 响应体
|
||||||
|
├── config/ → Spring 配置
|
||||||
|
├── common/ → 全局异常处理、Ops
|
||||||
|
├── mapStruct/ → MapStruct Converter
|
||||||
|
├── enums/ → 枚举类
|
||||||
|
├── exception/ → 自定义异常
|
||||||
|
└── utils/ → 工具类
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API 端点总览(32 个)
|
||||||
|
|
||||||
|
| 模块 | 端点数 | 端点 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 学习会话 | 9 | `POST /study-sessions/{taskNum}/start`, `PUT .../pause`, `PUT .../resume`, `PUT .../end`, `POST .../fragments`, `GET .../history`, `PUT .../expectation`, `GET .../expectation`, `GET .../report-draft` |
|
||||||
|
| 标准思维导图 | 6 | `GET /review/standard-mind-map/{taskNum}`, `POST .../regenerate`, `PUT ...`, `POST .../recall`, `POST .../find-node`, `GET .../recall-records` |
|
||||||
|
| 复习模块 | 6 | `GET /review/feed`, `GET /review/task/{taskNum}`, `GET /review/report/{id}`, `GET /review/fragment/{id}`, `GET /review/standard-mind-map/recall-records/{recordId}`, `GET /review/tasks` |
|
||||||
|
| 任务管理 | 6 | `GET /tasks`, `POST /tasks`, `PUT /tasks/{taskNum}`, `DELETE /tasks/{taskNum}`, `GET /tasks/priority-weights`, `PUT /tasks/priority-weights` |
|
||||||
|
| 应用场景 | 4 | `GET/POST/PUT/DELETE /tasks/{taskNum}/applications[/{id}]` |
|
||||||
|
| 工具 | 1 | `GET /utils/fetch-title?url=...` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 数据库(10 个核心表)
|
||||||
|
`users`, `tasks`, `study_sessions`, `study_reports`, `study_report_fragments`, `study_expectations`, `task_applications`, `review_standard_mind_maps`, `review_recall_records`, `user_priority_weights`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关键机制
|
||||||
|
- **多租户隔离**: `TenantLineInnerInterceptor` 自动注入 `WHERE created_by = #{当前用户}`,排除 `user`/`flyway_schema_history` 表
|
||||||
|
- **自动填充**: `MetaObjectHandler` 自动填充 `created_by`/`updated_by`/`created_time`/`updated_time`
|
||||||
|
- **认证鉴权**: Sa-Token 拦截 `/**` 排除 `/login`,Cookie `satoken`,`@SaCheckPermission` 注解式权限
|
||||||
|
- **响应格式**: `CommonResult<T>` 统一包装 `{ code, message, data }`
|
||||||
|
- **CORS**: 可配置,local profile 允许所有来源
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置文件
|
||||||
|
|
||||||
|
| Profile | 文件 |
|
||||||
|
|---------|------|
|
||||||
|
| local | `application-local.yml` |
|
||||||
|
| dev | `application-dev.yml` |
|
||||||
|
| uat | `application-uat.yml` |
|
||||||
|
| prod | `application-prod.yml` |
|
||||||
|
| 公共 | `application.yml` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AI 服务依赖
|
||||||
|
- 配置: `lpt.ai-service.url=http://localhost:5199`
|
||||||
|
- 超时: 600s
|
||||||
|
- 不可用时自动降级到内置规则引擎(`BuiltinMindMapGenerator`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 启动命令
|
||||||
|
```bash
|
||||||
|
mvn clean compile -DskipTests # 编译(需要 JDK 17)
|
||||||
|
mvn spring-boot:run # 启动(默认 profile: local)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关联项目
|
||||||
|
| 项目 | 路径 | 端口 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| lpt-fe | `../lpt-fe/` | 5158 | Vue 3 前端,通过 `/api` 代理调用本服务 |
|
||||||
|
| lpt-ai | `../lpt-ai/` | 5199 | AI 服务,本服务通过 HTTP 调用其异步任务接口 |
|
||||||
|
|
||||||
|
## 调用关系
|
||||||
|
```
|
||||||
|
浏览器 → lpt-fe (5158) ──/api──→ lpt-be (5157) ──HTTP──→ lpt-ai (5199)
|
||||||
|
│
|
||||||
|
└── MySQL (8109)
|
||||||
|
```
|
||||||
+17
-10
@@ -1,17 +1,24 @@
|
|||||||
# 使用官方 OpenJDK 作为基础镜像
|
# Stage 1: Maven 构建(Docker 层缓存自动缓存依赖)
|
||||||
FROM eclipse-temurin:17-jre
|
FROM maven:3.9-eclipse-temurin-17 AS builder
|
||||||
|
|
||||||
# 设置工作目录
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 拷贝打包好的 JAR 文件到容器
|
# 先复制 pom.xml,单独一层 —— 只要依赖不变,这层就被缓存
|
||||||
COPY target/LPT.jar LPT.jar
|
COPY pom.xml .
|
||||||
|
RUN mvn dependency:go-offline -B -q
|
||||||
|
|
||||||
|
# 再复制源码构建
|
||||||
|
COPY src/ ./src/
|
||||||
|
RUN mvn package -DskipTests -B -q
|
||||||
|
|
||||||
|
# Stage 2: 运行时(精简 JRE)
|
||||||
|
FROM eclipse-temurin:17-jre
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /app/target/LPT.jar LPT.jar
|
||||||
|
|
||||||
# 暴露应用运行的端口
|
|
||||||
EXPOSE 8888
|
EXPOSE 8888
|
||||||
|
|
||||||
# 启动应用
|
|
||||||
ENTRYPOINT ["java", "-jar", "LPT.jar"]
|
ENTRYPOINT ["java", "-jar", "LPT.jar"]
|
||||||
|
|
||||||
# 健康检查
|
HEALTHCHECK --interval=10s --timeout=5s \
|
||||||
HEALTHCHECK --interval=10s --timeout=5s CMD curl --fail http://localhost:8888/actuator/health || exit 1
|
CMD curl --fail http://localhost:8888/actuator/health || exit 1
|
||||||
|
|||||||
Vendored
+27
-19
@@ -1,7 +1,7 @@
|
|||||||
pipeline {
|
pipeline {
|
||||||
agent none // 全局不指定,局部自己声明
|
agent none
|
||||||
environment {
|
environment {
|
||||||
IMAGE_NAME = 'lpt-prod:0.0'
|
IMAGE_NAME = "lpt-prod:${env.GIT_COMMIT?.take(8) ?: '0.0'}"
|
||||||
CONTAINER_NAME = 'LPT-prod'
|
CONTAINER_NAME = 'LPT-prod'
|
||||||
CONTAINER_PORT = '8888'
|
CONTAINER_PORT = '8888'
|
||||||
}
|
}
|
||||||
@@ -31,23 +31,24 @@ pipeline {
|
|||||||
sh '''
|
sh '''
|
||||||
docker network create traefik-public || true
|
docker network create traefik-public || true
|
||||||
docker rm -f $CONTAINER_NAME || true
|
docker rm -f $CONTAINER_NAME || true
|
||||||
docker run -d --name $CONTAINER_NAME --network mysql-prod_mysql-prod \\
|
docker run -d --name $CONTAINER_NAME \
|
||||||
--restart=always \\
|
--network traefik-public \
|
||||||
-e SPRING_PROFILES_ACTIVE=prod \\
|
--restart=always \
|
||||||
--label "traefik.enable=true" \\
|
-e SPRING_PROFILES_ACTIVE=prod \
|
||||||
--label "traefik.docker.network=traefik-public" \\
|
--label "traefik.enable=true" \
|
||||||
--label 'traefik.http.routers.lpt-api.rule=Host(`lpt.cat-shark.xyz`) && PathPrefix(`/api`)' \\
|
--label "traefik.docker.network=traefik-public" \
|
||||||
--label "traefik.http.routers.lpt-api.entrypoints=websecure" \\
|
--label 'traefik.http.routers.lpt-api.rule=Host(`lpt.cat-shark.xyz`) && PathPrefix(`/api`)' \
|
||||||
--label "traefik.http.routers.lpt-api.tls.certresolver=le" \\
|
--label "traefik.http.routers.lpt-api.entrypoints=websecure" \
|
||||||
--label "traefik.http.routers.lpt-api.priority=100" \\
|
--label "traefik.http.routers.lpt-api.tls.certresolver=le" \
|
||||||
--label "traefik.http.routers.lpt-api.service=lpt-api" \\
|
--label "traefik.http.routers.lpt-api.priority=100" \
|
||||||
--label "traefik.http.routers.lpt-api.middlewares=lpt-api-strip" \\
|
--label "traefik.http.routers.lpt-api.service=lpt-api" \
|
||||||
--label "traefik.http.middlewares.lpt-api-strip.stripprefix.prefixes=/api" \\
|
--label "traefik.http.routers.lpt-api.middlewares=lpt-api-strip" \
|
||||||
--label "traefik.http.services.lpt-api.loadbalancer.server.port=$CONTAINER_PORT" \\
|
--label "traefik.http.middlewares.lpt-api-strip.stripprefix.prefixes=/api" \
|
||||||
--log-driver=loki \\
|
--label "traefik.http.services.lpt-api.loadbalancer.server.port=$CONTAINER_PORT" \
|
||||||
--log-opt loki-url="http://192.168.123.199:3100/loki/api/v1/push" \\
|
--log-driver=loki \
|
||||||
|
--log-opt loki-url="http://192.168.123.199:3100/loki/api/v1/push" \
|
||||||
$IMAGE_NAME
|
$IMAGE_NAME
|
||||||
docker network connect traefik-public $CONTAINER_NAME || true
|
docker network connect mysql-prod_mysql-prod $CONTAINER_NAME || true
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +58,7 @@ pipeline {
|
|||||||
steps {
|
steps {
|
||||||
script {
|
script {
|
||||||
def lastStatus = ''
|
def lastStatus = ''
|
||||||
timeout(time: 60, unit: 'SECONDS') {
|
timeout(time: 120, unit: 'SECONDS') {
|
||||||
waitUntil {
|
waitUntil {
|
||||||
def status = sh(
|
def status = sh(
|
||||||
script: "docker inspect -f '{{.State.Health.Status}}' $CONTAINER_NAME || echo 'unhealthy'",
|
script: "docker inspect -f '{{.State.Health.Status}}' $CONTAINER_NAME || echo 'unhealthy'",
|
||||||
@@ -74,5 +75,12 @@ pipeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stage('清理旧镜像') {
|
||||||
|
agent any
|
||||||
|
steps {
|
||||||
|
sh 'docker image prune -af --filter "until=168h" || true'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user