# 实战业务培训 — 技术方案

> 依据：同目录 `实战业务培训功能需求.md`。培训主档 **CRUD** + **发布/下架** + **`training_status` 持久化** + **小程序报名/取消**（`biz_practical_training_member`）；封面复用若依 `/common/upload`。

---

## 1. 技术架构

| 项 | 说明 |
| --- | --- |
| **后端** | RuoYi **v3.9.2**（**springboot2** 分支）：JDK 8、Spring Boot 2.x、Spring MVC、MyBatis、Druid |
| **数据库** | MySQL **5.7.39**，InnoDB，`utf8mb4` |
| **前端** | 若依 Vue2；时间 `yyyy-MM-dd HH:mm:ss`；封面上传后提交 URL、存储路径 |
| **代码包（建议）** | `com.ruoyi.web.modules.techservice` |

**分层**：`Controller` → `Service`（`PracticalTrainingValidation`、**报名状态计算**、发布状态机、替换封面删旧文件）→ `Mapper`/XML → `domain`。

**业务摘要**

| 场景 | 行为 |
| --- | --- |
| 新增「仅保存」 | `publish_status=0`，`publish_time=NULL` |
| 新增「保存并发布」 | Body `saveAndPublish=true` 或调发布接口；`publish_status=1`，`publish_time=now()` |
| 修改 / 删除 / 发布 | **仅** `publish_status=0` 且 `del_flag=0` |
| 下架 | **仅** `publish_status=1` 且 `now() < registration_start_time` → `publish_status=0`，写 `offline_time`；**保留** `publish_time` |
| 报名状态 | 列表/详情由 `PracticalTrainingEnrollStatusResolver` 按 **§3.4** 计算，**不入库** |
| 实际报名人数 | 字段 `actual_enrolled_count`，默认 `0`；后续报名模块 `UPDATE` 汇总 |
| 列表排序 | `training_time DESC, id DESC` |
| 列表筛选 | `trainingTopic` **精确**；`startDate`/`endDate` 对 `DATE(training_time)` 闭区间 |

---

## 2. 数据库设计

### 2.1 表 `biz_practical_training`（实战业务培训）

| 字段 | 类型 | 非空 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `id` | `bigint(20)` | Y | 自增 | 主键 |
| `training_topic` | `varchar(64)` | Y | — | 培训主题（**1～20** 字） |
| `training_time` | `datetime` | Y | — | 培训时间 |
| `training_intro` | `varchar(500)` | Y | — | 培训简介 |
| `planned_head_count` | `int(11)` | Y | — | 计划人数（正整数） |
| `training_location` | `varchar(200)` | Y | — | 培训地点 |
| `expected_outcome` | `text` | N | NULL | 预期成果 |
| `registration_start_time` | `datetime` | Y | — | 报名开始时间 |
| `registration_end_time` | `datetime` | Y | — | 报名截止时间 |
| `cover_file_url` | `varchar(512)` | N | NULL | 封面 URL |
| `cover_file_path` | `varchar(512)` | N | NULL | 封面存储路径 |
| `actual_enrolled_count` | `int(11)` | Y | `0` | 实际报名人数（成员表汇总） |
| `training_status` | `varchar(32)` | Y | `006001` | 培训状态字典 `006` |
| `publish_status` | `tinyint(4)` | Y | `0` | `0` 未发布 `1` 已发布 |
| `publish_time` | `datetime` | N | NULL | 发布时间（列表「发布日期」取日期部分） |
| `offline_time` | `datetime` | N | NULL | 下架时间 |
| `del_flag` | `char(1)` | Y | `'0'` | `0` 存在 `2` 删除 |
| `create_by` / `create_time` / `update_by` / `update_time` | 若依惯例 | — | — | 审计 |
| `remark` | `varchar(500)` | N | NULL | 备注 |

**索引**：`PRIMARY KEY (id)`；`KEY idx_training_time (training_time)`；`KEY idx_training_topic (training_topic)`；`KEY idx_publish (publish_status)`；`KEY idx_reg_start (registration_start_time)`；`KEY idx_del_flag (del_flag)`。

### 2.2 DDL（MySQL 5.7）

```sql
CREATE TABLE `biz_practical_training` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `training_topic` varchar(64) NOT NULL COMMENT '培训主题',
  `training_time` datetime NOT NULL COMMENT '培训时间',
  `training_intro` varchar(500) NOT NULL COMMENT '培训简介',
  `planned_head_count` int(11) NOT NULL COMMENT '计划人数',
  `training_location` varchar(200) NOT NULL COMMENT '培训地点',
  `expected_outcome` text COMMENT '预期成果',
  `registration_start_time` datetime NOT NULL COMMENT '报名开始时间',
  `registration_end_time` datetime NOT NULL COMMENT '报名截止时间',
  `cover_file_url` varchar(512) DEFAULT NULL COMMENT '封面URL',
  `cover_file_path` varchar(512) DEFAULT NULL COMMENT '封面存储路径',
  `actual_enrolled_count` int(11) NOT NULL DEFAULT '0' COMMENT '实际报名人数',
  `publish_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0未发布 1已发布',
  `publish_time` datetime DEFAULT NULL COMMENT '发布时间',
  `offline_time` datetime DEFAULT NULL COMMENT '下架时间',
  `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '0存在 2删除',
  `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  KEY `idx_training_time` (`training_time`),
  KEY `idx_training_topic` (`training_topic`),
  KEY `idx_publish` (`publish_status`),
  KEY `idx_reg_start` (`registration_start_time`),
  KEY `idx_del_flag` (`del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='实战业务培训';
```

### 2.3 枚举

**发布状态 `publish_status`**

| 值 | 标签 |
| --- | --- |
| 0 | 未发布 |
| 1 | 已发布 |

**培训状态 `training_status`**

| 编码 | 标签 |
| --- | --- |
| `006001` | 未开始 |
| `006002` | 报名中 |
| `006003` | 已报满 |
| `006004` | 已截止 |

管理端列表仍返回 `enrollStatus`（1–4）与 `enrollStatusName`，由 `training_status` 映射，兼容原前端。

### 2.4 表 `biz_practical_training_member`

| 字段 | 说明 |
| --- | --- |
| `training_id` | 关联主表 |
| `member_name` | 报名人姓名 |
| `sys_user_id` | 小程序用户 |
| `enroll_status` | `0` 已报名 `1` 已取消 |
| `enroll_time` / `cancel_time` | 报名/取消时间 |

唯一约束：`uk_training_user (training_id, sys_user_id)`。

### 2.5 移动端接口

见 `doc/app/实战业务培训/实战业务培训接口说明.md`：`GET /myEnroll/list`、`GET /myEnroll/{id}`、`POST /{id}/enroll`、`POST /{id}/cancelEnroll`。

---

## 3. 接口设计

**统一响应**：`AjaxResult` / `TableDataInfo`。

**权限标识**：`techService:practicalTraining:list|query|add|edit|remove|publish|offline`

**Base Path**：`/techService/practicalTraining`

| # | 说明 | Method | URI | 权限 |
| --- | --- | --- | --- | --- |
| 3.1 | 分页列表 | GET | `/techService/practicalTraining/list` | `list` |
| 3.2 | 详情 | GET | `/techService/practicalTraining/{id}` | `query` |
| 3.3 | 新增 | POST | `/techService/practicalTraining` | `add` |
| 3.4 | 修改 | PUT | `/techService/practicalTraining` | `edit` |
| 3.5 | 删除 | DELETE | `/techService/practicalTraining/{ids}` | `remove` |
| 3.6 | 发布 | POST | `/techService/practicalTraining/publish/{id}` | `publish` |
| 3.7 | 下架 | POST | `/techService/practicalTraining/offline/{id}` | `offline` |

#### 3.1.1 列表 Query

| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `pageNum` / `pageSize` | int | N | 默认 `1` / `20` |
| `trainingTopic` | string | N | 培训主题**精确**匹配 |
| `startDate` / `endDate` | string | N | `yyyy-MM-dd`；`DATE(training_time)` 闭区间；起 > 止抛业务异常 |

**列表行扩展（驼峰）**：除表字段外含 `enrollStatus`、`enrollStatusName`（**§3.4**）；`publishTime` 格式化为日期供「发布日期」列。

#### 3.2.1 详情 `data`

全业务字段 + `enrollStatus` / `enrollStatusName` + 审计字段；`canOffline`（boolean，是否允许下架，供前端控制按钮）。

#### 3.3.1 新增 / 修改 Body（驼峰）

| 字段 | 新增 | 修改 | 说明 |
| --- | --- | --- | --- |
| `id` | — | Y | 主键 |
| `trainingTopic` | Y | Y | 1～20 字 |
| `trainingTime` | Y | Y | `yyyy-MM-dd HH:mm:ss` |
| `trainingIntro` | Y | Y | trim 后非空；≤500 字 |
| `plannedHeadCount` | Y | Y | 整数 ≥1 |
| `trainingLocation` | Y | Y | 1～200 字 |
| `expectedOutcome` | N | N | 选填 |
| `registrationStartTime` | Y | Y | 须满足 **§3.5** 时间链 |
| `registrationEndTime` | Y | Y | 同上 |
| `coverFileUrl` / `coverFilePath` | N | N | 同时有或同时无 |
| `saveAndPublish` | N | — | 仅新增；`true` 时保存后发布 |
| `remark` | N | N | ≤500 字 |

#### 3.4 培训状态计算（服务端）

```
IF publish_status = 0 → 006001
ELSE IF now >= registration_end_time → 006004
ELSE IF now < registration_start_time → 006001
ELSE IF actual_enrolled_count >= planned_head_count → 006003
ELSE IF registration_start <= now < registration_end → 006002
```

实现类：`PracticalTrainingEnrollStatusResolver.resolveTrainingStatus(row, now)`；持久化：`PracticalTrainingStatusSupport.refreshAndPersist`。

#### 3.5 服务端校验（`PracticalTrainingValidation`）

| 项 | 规则 |
| --- | --- |
| 时间链 | `registrationStartTime < registrationEndTime < trainingTime` |
| 计划人数 | `plannedHeadCount >= 1` |
| 封面 | 可空；有则 `jpg,jpeg,png`，≤10MB（与上传接口一致） |
| 发布/修改/删除 | 目标记录 `publish_status=0` 且 `del_flag=0` |
| 下架 | `publish_status=1` 且 `now() < registration_start_time`；否则 `ServiceException`「报名已开始，不允许下架」 |
| 删除 | 若 `actual_enrolled_count > 0` 宜禁止（可选，评审） |
| 列表日期 | `startDate ≤ endDate`（均填时） |

#### 3.6 发布 / 下架要点

| 操作 | 结果 |
| --- | --- |
| 发布 | `publish_status=1`，`publish_time=now()`；再次校验 **§3.5** |
| 下架 | `publish_status=0`，`offline_time=now()`；**不删**封面；**不清** `publish_time` |

#### 3.7 Mapper 约定

- CRUD 列表/详情带 `del_flag='0'`。  
- 逻辑删除：`del_flag='2'`。  
- 修改/发布/删除条件附加 `publish_status=0`（下架除外）。

---

## 4. 菜单与权限（示例）

| 类型 | 名称 | 权限标识 |
| --- | --- | --- |
| 菜单 | 实战业务培训 | `techService:practicalTraining:list` |
| 按钮 | 查询 | `techService:practicalTraining:query` |
| 按钮 | 新增 | `techService:practicalTraining:add` |
| 按钮 | 修改 | `techService:practicalTraining:edit` |
| 按钮 | 删除 | `techService:practicalTraining:remove` |
| 按钮 | 发布 | `techService:practicalTraining:publish` |
| 按钮 | 下架 | `techService:practicalTraining:offline` |

组件路径：`techService/practicalTraining/index`（「科技平台与服务」目录下）。

---

## 5. 交付清单

- [ ] `sql/biz_practical_training.sql`、菜单权限 SQL  
- [ ] `BizPracticalTraining`、Mapper/XML、Service、Controller  
- [ ] `PracticalTrainingValidation`、`PracticalTrainingEnrollStatusResolver`、`PracticalTrainingRules`  
- [ ] 单元测试（时间链、报名状态四档、下架前置）  
- [ ] MockMvc（list 筛选、CRUD、发布/下架、已发布不可改）  

---

## 6. 修订记录

| 版本 | 说明 |
| --- | --- |
| 1.0 | 初稿：`biz_practical_training`；CRUD+发布下架；报名状态计算；下架须早于报名开始；封面复用通用上传 |
