Compare commits

...

4 Commits

Author SHA1 Message Date
yuliang_guo
98fc8b8eea feat(position): 岗位课程分配增加全选功能和checkbox样式优化
Some checks failed
continuous-integration/drone/push Build is failing
- 添加表格多选功能,支持全选/取消全选
- 增加批量设为必修/选修操作
- 优化 checkbox 样式(渐变色、圆角、动画)
- 新增批量操作栏UI
2026-01-28 13:10:47 +08:00
yuliang_guo
f52c8fde10 docs: 更新测试环境配置文档 - 新分支结构
Some checks failed
continuous-integration/drone/push Build is failing
2026-01-28 12:01:44 +08:00
yuliang_guo
22241fb5b6 feat: 前端独立部署 - 测试/预生产/生产环境隔离
Some checks failed
continuous-integration/drone/push Build is failing
- 创建三个独立 dist 目录 (dist-test, dist-staging, dist-prod)
- test 分支 -> kpl 测试环境
- staging 分支 -> aiedu 预生产
- main 分支 -> 生产租户
- 更新 Drone CI/CD 配置
2026-01-28 12:01:07 +08:00
yuliang_guo
e406110af2 feat: 新增灵活部署配置
All checks were successful
continuous-integration/drone/push Build is passing
- 新增 dev2 分支部署开发2环境 (aiedu.ireborn.com.cn)
- 生产部署支持选择性更新租户 [all]/[hua]/[cxw,yy]
- 新增测试环境配置文档
2026-01-28 10:39:55 +08:00
3 changed files with 702 additions and 155 deletions

View File

@@ -1,138 +1,256 @@
kind: pipeline kind: pipeline
type: docker type: docker
name: deploy-test name: deploy-test
# 测试环境test 分支触发,部署到生产服务器的开发环境 # 测试环境test 分支触发,部署到 kpl
trigger: trigger:
branch: branch:
- test - test
event: event:
- push - push
steps: steps:
- name: sync-code - name: sync-code
image: appleboy/drone-ssh image: appleboy/drone-ssh
settings: settings:
host: 120.79.247.16 host: 120.79.247.16
username: root username: root
password: password:
from_secret: prod_ssh_password from_secret: prod_ssh_password
port: 22 port: 22
command_timeout: 5m command_timeout: 5m
script: script:
- echo "=== 同步代码到测试环境 ===" - echo "=== [测试环境] 同步代码 ==="
- cd /root/aiedu - cd /root/aiedu
- git fetch origin - git fetch origin
- git checkout test 2>/dev/null || git checkout -b test origin/test - git checkout test 2>/dev/null || git checkout -b test origin/test
- git reset --hard origin/test - git reset --hard origin/test
- echo "代码同步完成" - echo "代码同步完成"
- name: rebuild-dev - name: build-frontend-test
image: appleboy/drone-ssh image: appleboy/drone-ssh
settings: settings:
host: 120.79.247.16 host: 120.79.247.16
username: root username: root
password: password:
from_secret: prod_ssh_password from_secret: prod_ssh_password
port: 22 port: 22
command_timeout: 10m command_timeout: 10m
script: script:
- echo "=== 重建测试环境 ===" - echo "=== [测试环境] 编译前端到 dist-test ==="
- cd /root/aiedu - cd /root/aiedu/kaopeilian-frontend
- docker-compose -f docker-compose.dev.yml up -d --build - npm install --silent
- sleep 5 - npm run build
- docker ps | grep kpl- - rm -rf /root/aiedu/dist-test/*
- echo "=== 测试环境部署完成: https://kpl.ireborn.com.cn ===" - cp -r dist/* /root/aiedu/dist-test/
- echo "前端编译完成 -> dist-test"
---
kind: pipeline - name: restart-backend
type: docker image: appleboy/drone-ssh
name: deploy-prod settings:
host: 120.79.247.16
# 生产环境main 分支触发,批量更新所有租户 username: root
trigger: password:
branch: from_secret: prod_ssh_password
- main port: 22
event: command_timeout: 5m
- push script:
- echo "=== [测试环境] 重启后端 ==="
steps: - docker restart kpl-backend-dev
- name: sync-code-to-server - sleep 5
image: appleboy/drone-ssh - docker ps | grep kpl-
settings: - echo "=== 测试环境部署完成: https://kpl.ireborn.com.cn ==="
host: 120.79.247.16
username: root ---
password: kind: pipeline
from_secret: prod_ssh_password type: docker
port: 22 name: deploy-staging
command_timeout: 10m
script: # 预生产环境staging 分支触发,部署到 aiedu
- echo "=== 同步代码到生产服务器 ===" trigger:
- cd /root/aiedu branch:
- git fetch origin - staging
- git reset --hard origin/main event:
- echo "代码同步完成" - push
- name: build-frontend steps:
image: appleboy/drone-ssh - name: sync-code
settings: image: appleboy/drone-ssh
host: 120.79.247.16 settings:
username: root host: 120.79.247.16
password: username: root
from_secret: prod_ssh_password password:
port: 22 from_secret: prod_ssh_password
command_timeout: 10m port: 22
script: command_timeout: 5m
- echo "=== 编译前端(所有租户共享)===" script:
- cd /root/aiedu/kaopeilian-frontend - echo "=== [预生产] 同步代码 ==="
- npm install --silent - cd /root/aiedu
- npm run build - git fetch origin
- echo "前端编译完成,所有租户已更新" - git checkout staging 2>/dev/null || git checkout -b staging origin/staging
- git reset --hard origin/staging
- name: rebuild-backends - echo "代码同步完成"
image: appleboy/drone-ssh
settings: - name: build-frontend-staging
host: 120.79.247.16 image: appleboy/drone-ssh
username: root settings:
password: host: 120.79.247.16
from_secret: prod_ssh_password username: root
port: 22 password:
command_timeout: 15m from_secret: prod_ssh_password
script: port: 22
- echo "=== 重建所有后端服务 ===" command_timeout: 10m
- cd /root/aiedu script:
- docker-compose -f docker-compose.prod-multi.yml up -d --build --no-deps hua-backend yy-backend hl-backend xy-backend fw-backend ex-backend cxw-backend - echo "=== [预生产] 编译前端到 dist-staging ==="
- sleep 10 - cd /root/aiedu/kaopeilian-frontend
- docker ps | grep backend - npm install --silent
- echo "=== 生产环境批量更新完成 ===" - npm run build
- echo "租户列表: hua, yy, hl, xy, fw, ex, cxw" - rm -rf /root/aiedu/dist-staging/*
- cp -r dist/* /root/aiedu/dist-staging/
volumes: - echo "前端编译完成 -> dist-staging"
- name: docker-sock
host: - name: restart-backend
path: /var/run/docker.sock image: appleboy/drone-ssh
settings:
--- host: 120.79.247.16
kind: pipeline username: root
type: docker password:
name: code-check from_secret: prod_ssh_password
port: 22
trigger: command_timeout: 5m
event: script:
- push - echo "=== [预生产] 重启后端 ==="
- pull_request - docker restart kaopeilian-backend
- sleep 5
steps: - docker ps | grep kaopeilian-
- name: python-lint - echo "=== 预生产部署完成: https://aiedu.ireborn.com.cn ==="
image: python:3.9-slim
commands: ---
- cd backend kind: pipeline
- pip install flake8 -q type: docker
- flake8 app --count --select=E9,F63,F7,F82 --show-source --statistics || true name: deploy-prod
- echo "Python lint completed"
# 生产环境main 分支触发,部署到所有租户
- name: frontend-check #
image: node:18-alpine # 使用方法:
commands: # git commit -m "feat: xxx [all]" - 部署所有租户
- cd frontend # git commit -m "feat: xxx [hua]" - 仅部署 hua
- echo "Frontend check completed" # git commit -m "feat: xxx [cxw,yy,hl]" - 部署指定多个租户
# git commit -m "feat: xxx" - 默认部署所有租户
#
trigger:
branch:
- main
event:
- push
steps:
- name: sync-code-to-server
image: appleboy/drone-ssh
settings:
host: 120.79.247.16
username: root
password:
from_secret: prod_ssh_password
port: 22
command_timeout: 10m
script:
- echo "=== [生产] 同步代码 ==="
- cd /root/aiedu
- git fetch origin
- git reset --hard origin/main
- echo "代码同步完成"
- name: build-frontend-prod
image: appleboy/drone-ssh
settings:
host: 120.79.247.16
username: root
password:
from_secret: prod_ssh_password
port: 22
command_timeout: 10m
script:
- echo "=== [生产] 编译前端到 dist-prod ==="
- cd /root/aiedu/kaopeilian-frontend
- npm install --silent
- npm run build
- rm -rf /root/aiedu/dist-prod/*
- cp -r dist/* /root/aiedu/dist-prod/
- echo "前端编译完成 -> dist-prod所有生产租户已更新"
- name: deploy-tenants
image: appleboy/drone-ssh
settings:
host: 120.79.247.16
username: root
password:
from_secret: prod_ssh_password
port: 22
command_timeout: 15m
script:
- echo "=== [生产] 部署租户后端 ==="
- cd /root/aiedu
- |
# 获取 commit message
COMMIT_MSG="${DRONE_COMMIT_MESSAGE}"
echo "Commit: $COMMIT_MSG"
# 所有可用租户
ALL_TENANTS="hua yy hl xy fw ex cxw"
# 解析要部署的租户
if echo "$COMMIT_MSG" | grep -q '\[all\]'; then
TENANTS="$ALL_TENANTS"
echo "部署所有租户: $TENANTS"
elif echo "$COMMIT_MSG" | grep -oP '\[\K[a-z,]+(?=\])' > /tmp/tenants.txt 2>/dev/null; then
TENANTS=$(cat /tmp/tenants.txt | tr ',' ' ')
echo "部署指定租户: $TENANTS"
else
TENANTS="$ALL_TENANTS"
echo "默认部署所有租户: $TENANTS"
fi
# 构建 docker compose 命令
BACKEND_SERVICES=""
for t in $TENANTS; do
BACKEND_SERVICES="$BACKEND_SERVICES ${t}-backend"
done
echo "重建后端服务: $BACKEND_SERVICES"
docker compose -f docker-compose.prod-multi.yml up -d --build --no-deps $BACKEND_SERVICES
sleep 10
docker ps | grep backend
echo "=== [生产] 部署完成 ==="
echo "已更新租户: $TENANTS"
volumes:
- name: docker-sock
host:
path: /var/run/docker.sock
---
kind: pipeline
type: docker
name: code-check
trigger:
event:
- push
- pull_request
steps:
- name: python-lint
image: python:3.9-slim
commands:
- cd backend
- pip install flake8 -q
- flake8 app --count --select=E9,F63,F7,F82 --show-source --statistics || true
- echo "Python lint completed"
- name: frontend-check
image: node:18-alpine
commands:
- cd frontend
- echo "Frontend check completed"

217
docs/测试环境配置.md Normal file
View File

@@ -0,0 +1,217 @@
# 考培练系统 - 环境配置与部署指南
> 最后更新2026-01-28
## 一、环境总览
| 环境 | 分支 | 域名 | dist 目录 | 用途 |
|------|------|------|-----------|------|
| **测试环境** | `test` | https://kpl.ireborn.com.cn | dist-test | 功能开发测试 |
| **预生产** | `staging` | https://aiedu.ireborn.com.cn | dist-staging | 集成测试/预发布 |
| **生产环境** | `main` | 各租户域名 | dist-prod | 正式生产 |
| **超级管理后台** | - | https://admin.kpl.ireborn.com.cn | - | SaaS管理 |
---
## 二、租户列表
| 租户代码 | 名称 | 域名 | 前端端口 | 后端端口 |
|----------|------|------|----------|----------|
| hua | 华尔倍丽 | https://hua.ireborn.com.cn | 3010 | 8010 |
| yy | 杨扬宠物 | https://yy.ireborn.com.cn | 3011 | 8011 |
| hl | 武汉禾丽 | https://hl.ireborn.com.cn | 3012 | 8012 |
| xy | 芯颜定制 | https://xy.ireborn.com.cn | 3013 | 8013 |
| fw | 飞沃 | https://fw.ireborn.com.cn | 3014 | 8014 |
| ex | 恩喜成都总院 | https://ex.ireborn.com.cn | 3015 | 8015 |
| cxw | 崔曦文 | https://cxw.kpl.ireborn.com.cn | 3016 | 8016 |
---
## 三、CI/CD 部署方式
### 3.1 开发1环境 (kpl-dev)
```bash
# 推送到 test 分支自动部署
git push cicd test
```
- **触发条件**`test` 分支 push
- **部署目标**kpl-dev 容器组
- **访问地址**https://kpl.ireborn.com.cn
---
### 3.2 开发2环境 (主站)
```bash
# 推送到 dev2 分支自动部署
git push cicd dev2
```
- **触发条件**`dev2` 分支 push
- **部署目标**kaopeilian 主站容器
- **访问地址**https://aiedu.ireborn.com.cn
---
### 3.3 生产环境 (租户)
```bash
# 推送到 main 分支,通过 commit message 控制部署范围
git push cicd main
```
#### 部署所有租户
```bash
git commit -m "feat: 新功能上线 [all]"
git push cicd main
```
#### 部署单个租户
```bash
git commit -m "fix: 修复问题 [hua]"
git push cicd main
```
#### 部署多个租户
```bash
git commit -m "feat: 功能更新 [cxw,yy,hl]"
git push cicd main
```
#### 默认行为
```bash
# 不带标签默认部署所有租户
git commit -m "feat: 常规更新"
git push cicd main
```
---
## 四、手动部署命令
### 4.1 SSH 登录服务器
```bash
ssh root@120.79.247.16
# 密码: Rxm88808
```
### 4.2 重启单个租户后端
```bash
cd /root/aiedu
docker restart cxw-backend
```
### 4.3 重建单个租户后端
```bash
cd /root/aiedu
docker-compose -f docker-compose.prod-multi.yml up -d --build --no-deps cxw-backend
```
### 4.4 查看日志
```bash
docker logs -f cxw-backend --tail 100
```
### 4.5 重新编译前端(所有租户共享)
```bash
cd /root/aiedu/kaopeilian-frontend
npm run build
```
---
## 五、数据库连接
### 5.1 生产共享 MySQL (prod-mysql)
- **端口**3309
- **用户**root
- **密码**ProdMySQL2025!@#
- **数据库**kaopeilian_hua, kaopeilian_yy, kaopeilian_hl, kaopeilian_xy, kaopeilian_fw, kaopeilian_ex, kaopeilian_cxw
### 5.2 开发测试 MySQL (kpl-mysql-dev)
- **端口**3308
- **用户**root
- **密码**nj861021
- **数据库**kaopeilian
### 5.3 主站 MySQL (kaopeilian-mysql)
- **端口**3307
- **用户**root
- **密码**nj861021
- **数据库**kaopeilian
---
## 六、容器管理
### 当前运行容器统计
| 类型 | 数量 |
|------|------|
| 前端容器 | 11 |
| 后端容器 | 11 |
| Redis | 10 |
| MySQL | 4 |
| Nginx | 1 |
| **总计** | **37** |
### 查看所有容器
```bash
docker ps --format 'table {{.Names}}\t{{.Status}}'
```
---
## 七、测试账户
| 角色 | 用户名 | 密码 |
|------|--------|------|
| 系统管理员 | admin | Admin123! |
| 培训经理 | manager | Admin123! |
| 测试学员 | testuser | Admin123! |
---
## 八、注意事项
1. **前端共享**:所有生产租户共享同一套前端代码,编译一次全部更新
2. **后端独立**:每个租户有独立的后端容器和数据库
3. **域名解析**
- `*.ireborn.com.cn` 解析到 120.79.242.43SCRM服务器
- `*.kpl.ireborn.com.cn` 解析到 120.79.247.16(考培练服务器)
4. **SSL证书**:使用 Let's Encrypt自动续期
---
## 九、Git 仓库配置
```bash
# 查看远程仓库
git remote -v
# origin: GitHub 源代码仓库
# cicd: Gitea CI/CD 触发仓库
# 常规开发
git push origin main
# 触发部署
git push cicd test # 部署开发1
git push cicd dev2 # 部署开发2
git push cicd main # 部署生产
```

View File

@@ -338,40 +338,76 @@
<div class="course-section"> <div class="course-section">
<div class="section-header"> <div class="section-header">
<h4>可用课程列表</h4> <h4>可用课程列表</h4>
<el-input <div class="header-actions">
v-model="courseSearchText" <el-input
placeholder="搜索课程名称" v-model="courseSearchText"
style="width: 200px" placeholder="搜索课程名称"
clearable style="width: 200px"
> clearable
<template #prefix> >
<el-icon><Search /></el-icon> <template #prefix>
</template> <el-icon><Search /></el-icon>
</el-input> </template>
</el-input>
</div>
</div> </div>
<el-table :data="filteredAllCourses" style="width: 100%" max-height="300">
<el-table-column prop="name" label="课程名称" min-width="200" /> <!-- 批量操作栏 -->
<el-table-column prop="category" label="分类" width="120"> <div class="batch-actions" v-if="selectedCourses.length > 0">
<span class="selected-count">已选择 {{ selectedCourses.length }} 门课程</span>
<el-button type="danger" size="small" @click="batchAssignCourse('required')">
<el-icon><Star /></el-icon>
批量设为必修
</el-button>
<el-button type="warning" size="small" @click="batchAssignCourse('optional')">
<el-icon><CollectionTag /></el-icon>
批量设为选修
</el-button>
<el-button size="small" @click="clearSelection">
取消选择
</el-button>
</div>
<el-table
ref="courseTableRef"
:data="filteredAllCourses"
style="width: 100%"
max-height="300"
@selection-change="handleSelectionChange"
class="course-table"
>
<el-table-column type="selection" width="55">
<template #header>
<el-checkbox
v-model="isAllSelected"
:indeterminate="isIndeterminate"
@change="handleSelectAll"
class="custom-checkbox"
/>
</template>
</el-table-column>
<el-table-column prop="name" label="课程名称" min-width="180" />
<el-table-column prop="category" label="分类" width="100">
<template #default="scope"> <template #default="scope">
<el-tag size="small">{{ scope.row.category }}</el-tag> <el-tag size="small">{{ scope.row.category }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="duration" label="学时" width="80" /> <el-table-column prop="duration" label="学时" width="70" />
<el-table-column prop="difficulty" label="难度" width="100"> <el-table-column prop="difficulty" label="难度" width="80">
<template #default="scope"> <template #default="scope">
<el-tag :type="getDifficultyTagType(scope.row.difficulty)" size="small"> <el-tag :type="getDifficultyTagType(scope.row.difficulty)" size="small">
{{ getDifficultyText(scope.row.difficulty) }} {{ getDifficultyText(scope.row.difficulty) }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="当前状态" width="120"> <el-table-column label="当前状态" width="100">
<template #default="scope"> <template #default="scope">
<el-tag v-if="isCourseRequired(scope.row.id)" type="danger" size="small">必修</el-tag> <el-tag v-if="isCourseRequired(scope.row.id)" type="danger" size="small">必修</el-tag>
<el-tag v-else-if="isCourseOptional(scope.row.id)" type="warning" size="small">选修</el-tag> <el-tag v-else-if="isCourseOptional(scope.row.id)" type="warning" size="small">选修</el-tag>
<span v-else class="text-muted">未分配</span> <span v-else class="text-muted">未分配</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="200"> <el-table-column label="操作" width="180">
<template #default="scope"> <template #default="scope">
<el-button <el-button
v-if="!isCourseRequired(scope.row.id)" v-if="!isCourseRequired(scope.row.id)"
@@ -452,6 +488,12 @@ const requiredCourses = ref<any[]>([])
const optionalCourses = ref<any[]>([]) const optionalCourses = ref<any[]>([])
const allCourses = ref<any[]>([]) const allCourses = ref<any[]>([])
// 全选相关
const courseTableRef = ref<any>(null)
const selectedCourses = ref<any[]>([])
const isAllSelected = ref(false)
const isIndeterminate = ref(false)
// 岗位表单 // 岗位表单
const positionForm = reactive({ const positionForm = reactive({
id: '', id: '',
@@ -941,6 +983,67 @@ const filteredAllCourses = computed(() => {
) )
}) })
/**
* 处理表格选择变化
*/
const handleSelectionChange = (selection: any[]) => {
selectedCourses.value = selection
const total = filteredAllCourses.value.length
isAllSelected.value = selection.length === total && total > 0
isIndeterminate.value = selection.length > 0 && selection.length < total
}
/**
* 全选/取消全选
*/
const handleSelectAll = (val: boolean) => {
if (courseTableRef.value) {
if (val) {
filteredAllCourses.value.forEach(row => {
courseTableRef.value.toggleRowSelection(row, true)
})
} else {
courseTableRef.value.clearSelection()
}
}
}
/**
* 清除选择
*/
const clearSelection = () => {
if (courseTableRef.value) {
courseTableRef.value.clearSelection()
}
selectedCourses.value = []
isAllSelected.value = false
isIndeterminate.value = false
}
/**
* 批量分配课程
*/
const batchAssignCourse = async (type: 'required' | 'optional') => {
if (!currentPosition.value || selectedCourses.value.length === 0) return
try {
const courseIds = selectedCourses.value.map(c => c.id)
const response = await request.post(`/api/v1/admin/positions/${currentPosition.value.id}/courses`, {
course_ids: courseIds,
course_type: type
})
if (response && response.code === 200) {
await loadPositionCourses(currentPosition.value.id)
clearSelection()
ElMessage.success(`已将 ${courseIds.length} 门课程设为${type === 'required' ? '必修' : '选修'}`)
}
} catch (error) {
console.error('批量分配课程失败:', error)
ElMessage.error('批量分配课程失败')
}
}
/** /**
* 检查课程是否为必修 * 检查课程是否为必修
*/ */
@@ -1326,11 +1429,120 @@ onMounted(() => {
color: #333; color: #333;
margin: 0; margin: 0;
} }
.header-actions {
display: flex;
gap: 12px;
align-items: center;
}
}
// 批量操作栏样式
.batch-actions {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
margin-bottom: 12px;
background: linear-gradient(135deg, #e8f4fd 0%, #f0f7ff 100%);
border-radius: 8px;
border: 1px solid #d4e8fc;
.selected-count {
font-size: 14px;
font-weight: 500;
color: #409eff;
margin-right: 8px;
}
.el-button {
&.el-button--danger {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
border: none;
&:hover {
background: linear-gradient(135deg, #ff5252 0%, #d63031 100%);
}
}
&.el-button--warning {
background: linear-gradient(135deg, #ffa726 0%, #ff9800 100%);
border: none;
&:hover {
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
}
}
}
} }
.text-muted { .text-muted {
color: #909399; color: #909399;
} }
// 课程表格样式优化
.course-table {
// Checkbox 样式优化
:deep(.el-checkbox) {
.el-checkbox__inner {
width: 18px;
height: 18px;
border-radius: 4px;
border: 2px solid #dcdfe6;
transition: all 0.2s ease;
&::after {
width: 4px;
height: 8px;
left: 5px;
top: 1px;
border-width: 2px;
}
}
&:hover .el-checkbox__inner {
border-color: #409eff;
}
&.is-checked .el-checkbox__inner {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-color: #667eea;
}
&.is-indeterminate .el-checkbox__inner {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-color: #667eea;
&::before {
height: 3px;
top: 6px;
left: 3px;
right: 3px;
}
}
}
// 表格行选中效果
:deep(.el-table__row) {
&.current-row,
&:hover {
background-color: #f5f7fa;
}
&.selection-row {
background-color: #ecf5ff;
}
}
// 表头样式
:deep(.el-table__header-wrapper) {
th {
background-color: #fafafa;
font-weight: 600;
color: #333;
}
}
}
} }
} }