diff --git a/docs/TrainingOutlineApi.md b/docs/TrainingOutlineApi.md new file mode 100644 index 0000000..6b219f1 --- /dev/null +++ b/docs/TrainingOutlineApi.md @@ -0,0 +1,217 @@ +# 培训大纲接口说明 + +## 功能概述 + +培训大纲使用统一层级表维护,支持三种层级: + +- `1` 年度 +- `2` 月度 +- `3` 周度 + +层级关系如下: + +- 年度大纲没有上级,`parentId=0` +- 月度大纲的上级必须是年度大纲 +- 周度大纲的上级必须是月度大纲 + +培训类型字段 `trainingType` 为字典值,对应字典类型 `hot_training_type`,前端按项目现有字典接口获取可选项。 + +## 数据字段 + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| id | Long | 否 | 主键,编辑/删除/详情时使用 | +| companyId | Long | 否 | 公司ID;不传时后端优先取当前登录公司 | +| parentId | Long | 条件必填 | 上级大纲ID;年度传 `0` 或不传,月度/周度必传 | +| outlineLevel | Long | 是 | 大纲层级:`1=年度`、`2=月度`、`3=周度` | +| outlineName | String | 是 | 大纲名称 | +| outlineFile | String | 是 | 大纲文件,建议传 OSS ID 或文件 URL | +| trainingType | String | 是 | 培训类型字典值 | +| isEnabled | Long | 否 | 是否启用:`0=禁用`、`1=启用`,默认 `1` | +| sortNo | Long | 否 | 排序号,越小越靠前,默认 `0` | +| remark | String | 否 | 备注 | + +## 返回字段补充说明 + +除上述字段外,列表/详情接口还会返回: + +- `parentName`:上级大纲名称 +- `outlineLevelName`:层级名称,取值为 `年度`、`月度`、`周度` +- `trainingTypeLabel`:培训类型字典标签 +- `isEnabled`:是否启用,`0=禁用`、`1=启用` +- `hasChildren`:是否存在直属子级 + +## 可见性规则 + +- 总部端可新增、修改 `isEnabled` +- 企业端查询列表、年度列表、直属子级、详情、导出时,只能看到 `isEnabled=1` 的大纲 +- 企业端即使知道禁用大纲的 ID,也无法通过详情或子级接口查看 + +## 1. 分页查询培训大纲 + +- 路径:`GET /securityManagement/trainingOutline/list` +- 说明:后台通用分页查询,可按名称、层级、父级、培训类型筛选 + +### 请求参数 + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| pageNum | Integer | 页码 | +| pageSize | Integer | 每页条数 | +| companyId | Long | 公司ID | +| parentId | Long | 上级大纲ID | +| outlineLevel | Long | 层级 | +| outlineName | String | 大纲名称,模糊匹配 | +| trainingType | String | 培训类型字典值 | +| isEnabled | Long | 是否启用 | + +## 2. 查询全部年度大纲 + +- 路径:`GET /securityManagement/trainingOutline/annuals` +- 说明:返回所有年度层级的大纲列表 + +### 可选参数 + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| companyId | Long | 公司ID | +| trainingType | String | 培训类型字典值 | +| outlineName | String | 大纲名称,模糊匹配 | +| isEnabled | Long | 是否启用,总部端可用 | + +### 返回示例 + +```json +[ + { + "id": 1, + "companyId": 1, + "parentId": 0, + "outlineLevel": 1, + "outlineLevelName": "年度", + "outlineName": "2026年度培训大纲", + "outlineFile": "197845612345678901", + "trainingType": "pre-job", + "trainingTypeLabel": "岗前培训", + "isEnabled": 1, + "sortNo": 1, + "remark": "", + "hasChildren": true + } +] +``` + +## 3. 查询直属子大纲列表 + +- 路径:`GET /securityManagement/trainingOutline/children/{parentId}` +- 说明:前端传入某个大纲ID,返回该大纲的直属子级列表 + +### 场景说明 + +- 传入年度大纲ID:返回该年度下全部月度大纲 +- 传入月度大纲ID:返回该月度下全部周度大纲 +- 传入周度大纲ID:返回空数组 + +## 4. 查询详情 + +- 路径:`GET /securityManagement/trainingOutline/{id}` + +## 5. 新增培训大纲 + +- 路径:`POST /securityManagement/trainingOutline` +- `Content-Type`:`application/json` + +### 年度新增示例 + +```json +{ + "companyId": 1, + "outlineLevel": 1, + "outlineName": "2026年度培训大纲", + "outlineFile": "197845612345678901", + "trainingType": "pre-job", + "isEnabled": 1, + "sortNo": 1, + "remark": "年度培训总纲" +} +``` + +### 月度新增示例 + +```json +{ + "companyId": 1, + "parentId": 1, + "outlineLevel": 2, + "outlineName": "2026年12月培训大纲", + "outlineFile": "197845612345678902", + "trainingType": "pre-job", + "isEnabled": 1, + "sortNo": 12 +} +``` + +### 周度新增示例 + +```json +{ + "companyId": 1, + "parentId": 2, + "outlineLevel": 3, + "outlineName": "2026年12月第1周培训大纲", + "outlineFile": "197845612345678903", + "trainingType": "pre-job", + "isEnabled": 1, + "sortNo": 1 +} +``` + +### 后端校验规则 + +- 年度大纲自动按 `parentId=0` 处理 +- 月度大纲的上级必须是年度大纲 +- 周度大纲的上级必须是月度大纲 +- 同一公司、同一父级、同一层级下不允许同名 +- `isEnabled` 仅总部端可控制;未传时默认按启用处理 + +## 6. 修改培训大纲 + +- 路径:`PUT /securityManagement/trainingOutline` +- `Content-Type`:`application/json` + +### 请求示例 + +```json +{ + "id": 3, + "companyId": 1, + "parentId": 2, + "outlineLevel": 3, + "outlineName": "2026年12月第2周培训大纲", + "outlineFile": "197845612345678904", + "trainingType": "daily", + "isEnabled": 0, + "sortNo": 2, + "remark": "更新后的周度大纲" +} +``` + +## 7. 删除培训大纲 + +- 路径:`DELETE /securityManagement/trainingOutline/{ids}` +- 示例:`DELETE /securityManagement/trainingOutline/3` +- 说明:支持批量,多个ID使用英文逗号分隔 + +### 删除限制 + +- 存在直属子级时不可删除 + +## 8. 导出列表 + +- 路径:`POST /securityManagement/trainingOutline/export` +- 说明:按查询条件导出 Excel + +## 9. 导出 PDF + +- 路径:`POST /securityManagement/trainingOutline/exportPdf` +- 说明:默认导出年度大纲;如传 `id`,则导出该节点所属年度的大纲内容 diff --git a/sql/1289/3_training_outline.sql b/sql/1289/3_training_outline.sql new file mode 100644 index 0000000..9cd7e2b --- /dev/null +++ b/sql/1289/3_training_outline.sql @@ -0,0 +1,33 @@ +-- 培训大纲统一层级表 +-- 如历史环境已存在旧版 hot_training_outline 表,请先确认是否需要备份历史数据后再执行本脚本。 + +DROP TABLE IF EXISTS `hot_training_outline`; + +CREATE TABLE `hot_training_outline` +( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `company_id` BIGINT UNSIGNED NULL COMMENT '公司ID', + `parent_id` BIGINT UNSIGNED NULL DEFAULT 0 COMMENT '上级大纲ID,年度为0', + `outline_level` TINYINT NULL COMMENT '大纲层级:1=年度,2=月度,3=周度', + `outline_name` VARCHAR(200) NULL COMMENT '大纲名称', + `outline_file` VARCHAR(1024) NULL COMMENT '大纲文件,支持存储OSS ID或URL', + `training_type` VARCHAR(100) NULL COMMENT '培训类型(字典值)', + `is_enabled` TINYINT NULL DEFAULT 1 COMMENT '是否启用:0=禁用,1=启用', + `sort_no` BIGINT NULL DEFAULT 0 COMMENT '排序号,越小越靠前', + `remark` VARCHAR(500) NULL COMMENT '备注', + `create_dept` BIGINT NULL COMMENT '创建部门', + `create_by` BIGINT NULL COMMENT '创建者', + `create_by_name` VARCHAR(64) NULL COMMENT '创建者姓名', + `create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` BIGINT NULL COMMENT '更新者', + `update_by_name` VARCHAR(64) NULL COMMENT '更新者姓名', + `update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `is_deleted` TINYINT NULL DEFAULT 0 COMMENT '0=正常, 1=已删除', + PRIMARY KEY (`id`), + KEY `idx_company_id` (`company_id`), + KEY `idx_parent_id` (`parent_id`), + KEY `idx_level_parent` (`outline_level`, `parent_id`), + KEY `idx_training_type` (`training_type`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci COMMENT ='培训大纲统一层级表'; diff --git a/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/controller/HotTrainingOutlineController.java b/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/controller/HotTrainingOutlineController.java index 75629cc..a6d4d50 100644 --- a/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/controller/HotTrainingOutlineController.java +++ b/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/controller/HotTrainingOutlineController.java @@ -52,6 +52,26 @@ public class HotTrainingOutlineController extends BaseController { return hotTrainingOutlineService.queryPageList(bo, pageQuery); } + /** + * 查询全部年度培训大纲列表 + */ + @GetMapping("/annuals") + @Operation(summary = "查询全部年度培训大纲列表") + public R> annuals(HotTrainingOutlineBo bo) { + return R.ok(hotTrainingOutlineService.queryAnnualList(bo)); + } + + /** + * 查询直属子级大纲列表 + */ + @GetMapping("/children/{parentId}") + @Operation(summary = "根据父级大纲ID查询直属子级大纲列表") + public R> children(@NotNull(message = "父级大纲ID不能为空") + @Parameter(name = "parentId", description = "父级大纲ID", required = true, example = "1") + @PathVariable Long parentId) { + return R.ok(hotTrainingOutlineService.queryChildrenByParentId(parentId)); + } + /** * 导出培训大纲列表 */ diff --git a/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/domain/HotTrainingOutline.java b/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/domain/HotTrainingOutline.java index 0651f80..f85f63c 100644 --- a/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/domain/HotTrainingOutline.java +++ b/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/domain/HotTrainingOutline.java @@ -12,8 +12,10 @@ import java.io.Serial; /** * 培训大纲对象 hot_training_outline * + * 统一承载年度、月度、周度三层培训大纲。 + * * @author shihongwei - * @date 2026-01-14 + * @date 2026-05-14 */ @Data @EqualsAndHashCode(callSuper = true) @@ -30,24 +32,44 @@ public class HotTrainingOutline extends BaseEntity { private Long id; /** - * 年度 + * 公司ID */ - private Long outlineYear; + private Long companyId; /** - * 培训类型组合,逗号分隔存储 + * 上级大纲ID,年度为0 */ - private String outlineTypes; + private Long parentId; /** - * 是否启用 0=否,1=是 + * 大纲层级:1=年度,2=月度,3=周度 + */ + private Long outlineLevel; + + /** + * 大纲名称 + */ + private String outlineName; + + /** + * 大纲文件 + */ + private String outlineFile; + + /** + * 培训类型(字典值) + */ + private String trainingType; + + /** + * 是否启用:0=禁用,1=启用 */ private Long isEnabled; /** - * 月度培训计划ID组合,逗号分隔存储 + * 排序号,越小越靠前 */ - private String monthPlanIds; + private Long sortNo; /** * 备注 diff --git a/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/domain/bo/HotTrainingOutlineBo.java b/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/domain/bo/HotTrainingOutlineBo.java index 572dde7..195a57c 100644 --- a/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/domain/bo/HotTrainingOutlineBo.java +++ b/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/domain/bo/HotTrainingOutlineBo.java @@ -2,6 +2,7 @@ package com.hotwj.platform.securityManagement.trainingOutline.domain.bo; import com.hotwj.platform.securityManagement.trainingOutline.domain.HotTrainingOutline; import io.github.linpeilie.annotations.AutoMapper; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.EqualsAndHashCode; @@ -13,7 +14,7 @@ import org.dromara.common.mybatis.core.domain.BaseEntity; * 培训大纲业务对象 hot_training_outline * * @author shihongwei - * @date 2026-01-14 + * @date 2026-05-14 */ @Data @EqualsAndHashCode(callSuper = true) @@ -27,25 +28,48 @@ public class HotTrainingOutlineBo extends BaseEntity { private Long id; /** - * 年度 + * 公司ID */ - @NotNull(message = "年度不能为空", groups = {AddGroup.class, EditGroup.class}) - private Long outlineYear; + private Long companyId; /** - * 培训类型组合,逗号分隔存储 + * 上级大纲ID,年度为0 */ - private String outlineTypes; + private Long parentId; /** - * 是否启用 0=否,1=是 + * 大纲层级:1=年度,2=月度,3=周度 + */ + @NotNull(message = "大纲层级不能为空", groups = {AddGroup.class, EditGroup.class}) + private Long outlineLevel; + + /** + * 大纲名称 + */ + @NotBlank(message = "大纲名称不能为空", groups = {AddGroup.class, EditGroup.class}) + private String outlineName; + + /** + * 大纲文件 + */ + @NotBlank(message = "大纲文件不能为空", groups = {AddGroup.class, EditGroup.class}) + private String outlineFile; + + /** + * 培训类型(字典值) + */ + @NotBlank(message = "培训类型不能为空", groups = {AddGroup.class, EditGroup.class}) + private String trainingType; + + /** + * 是否启用:0=禁用,1=启用 */ private Long isEnabled; /** - * 月度培训计划ID组合,逗号分隔存储 + * 排序号,越小越靠前 */ - private String monthPlanIds; + private Long sortNo; /** * 备注 diff --git a/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/domain/vo/HotTrainingOutlineVo.java b/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/domain/vo/HotTrainingOutlineVo.java index 554854f..e79b6c7 100644 --- a/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/domain/vo/HotTrainingOutlineVo.java +++ b/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/domain/vo/HotTrainingOutlineVo.java @@ -14,7 +14,7 @@ import java.io.Serializable; * 培训大纲视图对象 hot_training_outline * * @author shihongwei - * @date 2026-01-14 + * @date 2026-05-14 */ @Data @ExcelIgnoreUnannotated @@ -31,28 +31,76 @@ public class HotTrainingOutlineVo implements Serializable { private Long id; /** - * 年度 + * 公司ID */ - @ExcelProperty(value = "年度") - private Long outlineYear; + @ExcelProperty(value = "公司ID") + private Long companyId; /** - * 培训类型组合,逗号分隔存储 + * 上级大纲ID,年度为0 */ - @ExcelProperty(value = "培训类型组合,逗号分隔存储") - private String outlineTypes; + @ExcelProperty(value = "上级大纲ID") + private Long parentId; /** - * 是否启用 0=否,1=是 + * 上级大纲名称 */ - @ExcelProperty(value = "是否启用 0=否,1=是") + @ExcelProperty(value = "上级大纲名称") + private String parentName; + + /** + * 大纲层级:1=年度,2=月度,3=周度 + */ + @ExcelProperty(value = "大纲层级") + private Long outlineLevel; + + /** + * 大纲层级名称 + */ + @ExcelProperty(value = "大纲层级名称") + private String outlineLevelName; + + /** + * 大纲名称 + */ + @ExcelProperty(value = "大纲名称") + private String outlineName; + + /** + * 大纲文件 + */ + @ExcelProperty(value = "大纲文件") + private String outlineFile; + + /** + * 培训类型(字典值) + */ + @ExcelProperty(value = "培训类型") + private String trainingType; + + /** + * 是否启用:0=禁用,1=启用 + */ + @ExcelProperty(value = "是否启用") private Long isEnabled; /** - * 月度培训计划ID组合,逗号分隔存储 + * 培训类型名称 */ - @ExcelProperty(value = "月度培训计划ID组合,逗号分隔存储") - private String monthPlanIds; + @ExcelProperty(value = "培训类型名称") + private String trainingTypeLabel; + + /** + * 排序号,越小越靠前 + */ + @ExcelProperty(value = "排序号") + private Long sortNo; + + /** + * 是否存在子节点 + */ + @ExcelProperty(value = "是否存在子节点") + private Boolean hasChildren; /** * 备注 diff --git a/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/service/IHotTrainingOutlineService.java b/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/service/IHotTrainingOutlineService.java index c9a8b3c..164df5c 100644 --- a/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/service/IHotTrainingOutlineService.java +++ b/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/service/IHotTrainingOutlineService.java @@ -41,6 +41,22 @@ public interface IHotTrainingOutlineService { */ List queryList(HotTrainingOutlineBo bo); + /** + * 查询全部年度大纲列表 + * + * @param bo 查询条件 + * @return 年度大纲列表 + */ + List queryAnnualList(HotTrainingOutlineBo bo); + + /** + * 根据父级大纲ID查询直属子级列表 + * + * @param parentId 父级大纲ID + * @return 直属子级列表 + */ + List queryChildrenByParentId(Long parentId); + /** * 新增培训大纲 * diff --git a/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/service/impl/HotTrainingOutlinePrintServiceImpl.java b/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/service/impl/HotTrainingOutlinePrintServiceImpl.java index 69c0c7b..d21ca1d 100644 --- a/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/service/impl/HotTrainingOutlinePrintServiceImpl.java +++ b/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/service/impl/HotTrainingOutlinePrintServiceImpl.java @@ -2,29 +2,18 @@ package com.hotwj.platform.securityManagement.trainingOutline.service.impl; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateUtil; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.hotwj.platform.common.utils.PdfWatermarkUtil; import com.hotwj.platform.reportStatistics.integration.GotenbergClient; import com.hotwj.platform.reportStatistics.template.VelocityTemplateRenderer; -import com.hotwj.platform.securityManagement.trainingOutline.domain.HotTrainingOutline; import com.hotwj.platform.securityManagement.trainingOutline.domain.bo.HotTrainingOutlineBo; import com.hotwj.platform.securityManagement.trainingOutline.domain.vo.HotTrainingOutlineVo; -import com.hotwj.platform.securityManagement.trainingOutline.mapper.HotTrainingOutlineMapper; import com.hotwj.platform.securityManagement.trainingOutline.service.IHotTrainingOutlinePrintService; -import com.hotwj.platform.securityManagement.trainingOutlineCourse.domain.HotTrainingOutlineCourse; -import com.hotwj.platform.securityManagement.trainingOutlineCourse.domain.vo.HotTrainingOutlineCourseVo; -import com.hotwj.platform.securityManagement.trainingOutlineCourse.mapper.HotTrainingOutlineCourseMapper; -import com.hotwj.platform.securityManagement.trainingOutlineMonth.domain.HotTrainingOutlineMonth; -import com.hotwj.platform.securityManagement.trainingOutlineMonth.domain.vo.HotTrainingOutlineMonthVo; -import com.hotwj.platform.securityManagement.trainingOutlineMonth.mapper.HotTrainingOutlineMonthMapper; +import com.hotwj.platform.securityManagement.trainingOutline.service.IHotTrainingOutlineService; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.velocity.VelocityContext; import org.dromara.common.core.exception.ServiceException; -import org.dromara.common.core.utils.MapstructUtils; -import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.file.FileUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -37,15 +26,17 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; @Slf4j @Service @RequiredArgsConstructor public class HotTrainingOutlinePrintServiceImpl implements IHotTrainingOutlinePrintService { - private final HotTrainingOutlineMapper outlineMapper; - private final HotTrainingOutlineMonthMapper outlineMonthMapper; - private final HotTrainingOutlineCourseMapper outlineCourseMapper; + private static final long LEVEL_ANNUAL = 1L; + private static final long LEVEL_MONTHLY = 2L; + + private final IHotTrainingOutlineService trainingOutlineService; private final VelocityTemplateRenderer velocityTemplateRenderer; private final GotenbergClient gotenbergClient; @@ -54,82 +45,39 @@ public class HotTrainingOutlinePrintServiceImpl implements IHotTrainingOutlinePr @Override public void exportPdf(HotTrainingOutlineBo bo, HttpServletResponse response) { - // 1. 查询大纲列表 - LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); - lqw.orderByAsc(HotTrainingOutline::getId); - lqw.eq(bo.getOutlineYear() != null, HotTrainingOutline::getOutlineYear, bo.getOutlineYear()); - lqw.eq(StringUtils.isNotBlank(bo.getOutlineTypes()), HotTrainingOutline::getOutlineTypes, bo.getOutlineTypes()); - lqw.eq(bo.getIsEnabled() != null, HotTrainingOutline::getIsEnabled, bo.getIsEnabled()); - lqw.eq(StringUtils.isNotBlank(bo.getMonthPlanIds()), HotTrainingOutline::getMonthPlanIds, bo.getMonthPlanIds()); - lqw.eq(bo.getIsDeleted() != null, HotTrainingOutline::getIsDeleted, bo.getIsDeleted()); - - List outlineList = outlineMapper.selectVoList(lqw); + List outlineList = resolveAnnualOutlines(bo); if (CollUtil.isEmpty(outlineList)) { throw new ServiceException("未查询到培训大纲数据"); } - // 假设只打印第一个匹配的大纲,或者需要循环打印。通常"导出PDF"如果是列表查询,可能导出列表。 - // 但根据前端逻辑,它似乎是在查看详情时打印,或者是针对特定年份的打印。 - // 如果是列表导出,我们可能需要一个包含多个大纲的 PDF。 - // 这里假设按照 vehicleThreeInspect 的逻辑,它导出的是一个列表。 - - // 但前端传参是 queryParams,可能包含 id。 - // 如果 bo.getId() 不为空,则只导出一个。 - List> printDataList = new ArrayList<>(); - for (HotTrainingOutlineVo outline : outlineList) { Map outlineData = new HashMap<>(); - outlineData.put("outlineYear", outline.getOutlineYear()); - - // 查询月度计划 - List months = outlineMonthMapper.selectList( - Wrappers.lambdaQuery() - .eq(HotTrainingOutlineMonth::getOutlineId, outline.getId()) - .eq(HotTrainingOutlineMonth::getIsDeleted, 0) - .orderByAsc(HotTrainingOutlineMonth::getSortNo) // 假设有排序 - ); - List monthVos = MapstructUtils.convert(months, HotTrainingOutlineMonthVo.class); - + outlineData.put("outlineYear", outline.getOutlineName()); List> flatRows = new ArrayList<>(); - + List monthVos = trainingOutlineService.queryChildrenByParentId(outline.getId()); if (CollUtil.isNotEmpty(monthVos)) { - for (HotTrainingOutlineMonthVo month : monthVos) { - // 查询课程 - List courses = outlineCourseMapper.selectList( - Wrappers.lambdaQuery() - .eq(HotTrainingOutlineCourse::getMonthOutlineId, month.getId()) - .eq(HotTrainingOutlineCourse::getIsDeleted, 0) - .orderByAsc(HotTrainingOutlineCourse::getSortNo) - ); - List courseVos = MapstructUtils.convert(courses, HotTrainingOutlineCourseVo.class); - - if (CollUtil.isNotEmpty(courseVos)) { - for (int i = 0; i < courseVos.size(); i++) { - HotTrainingOutlineCourseVo course = courseVos.get(i); + for (HotTrainingOutlineVo month : monthVos) { + List weekVos = trainingOutlineService.queryChildrenByParentId(month.getId()); + if (CollUtil.isNotEmpty(weekVos)) { + for (int i = 0; i < weekVos.size(); i++) { + HotTrainingOutlineVo week = weekVos.get(i); Map row = new HashMap<>(); - row.put("planMonth", month.getPlanMonth()); - row.put("topic", month.getTopic()); - row.put("courseName", course.getCourseName()); - row.put("durationMinute", course.getDurationMinute()); - row.put("courseDesc", course.getCourseDesc()); - - // 设置 rowSpan - if (i == 0) { - row.put("monthRowSpan", courseVos.size()); - } else { - row.put("monthRowSpan", 0); - } + row.put("planMonth", month.getOutlineName()); + row.put("topic", defaultString(month.getTrainingTypeLabel(), month.getTrainingType())); + row.put("courseName", week.getOutlineName()); + row.put("durationMinute", ""); + row.put("courseDesc", week.getOutlineFile()); + row.put("monthRowSpan", i == 0 ? weekVos.size() : 0); flatRows.add(row); } } else { - // 如果没有课程,也显示月度信息 Map row = new HashMap<>(); - row.put("planMonth", month.getPlanMonth()); - row.put("topic", month.getTopic()); + row.put("planMonth", month.getOutlineName()); + row.put("topic", defaultString(month.getTrainingTypeLabel(), month.getTrainingType())); row.put("courseName", ""); row.put("durationMinute", ""); - row.put("courseDesc", ""); + row.put("courseDesc", month.getOutlineFile()); row.put("monthRowSpan", 1); flatRows.add(row); } @@ -139,7 +87,6 @@ public class HotTrainingOutlinePrintServiceImpl implements IHotTrainingOutlinePr printDataList.add(outlineData); } - // 渲染 HTML VelocityContext context = new VelocityContext(); context.put("printTime", DateUtil.now()); context.put("list", printDataList); @@ -189,4 +136,39 @@ public class HotTrainingOutlinePrintServiceImpl implements IHotTrainingOutlinePr } } } + + private List resolveAnnualOutlines(HotTrainingOutlineBo bo) { + if (bo != null && bo.getId() != null) { + HotTrainingOutlineVo current = trainingOutlineService.queryById(bo.getId()); + if (current == null) { + throw new ServiceException("培训大纲不存在"); + } + HotTrainingOutlineVo annual = resolveAnnualOutline(current); + return annual == null ? List.of() : List.of(annual); + } + HotTrainingOutlineBo queryBo = bo == null ? new HotTrainingOutlineBo() : bo; + queryBo.setOutlineLevel(LEVEL_ANNUAL); + return trainingOutlineService.queryAnnualList(queryBo); + } + + private HotTrainingOutlineVo resolveAnnualOutline(HotTrainingOutlineVo current) { + if (current == null) { + return null; + } + if (Objects.equals(current.getOutlineLevel(), LEVEL_ANNUAL)) { + return current; + } + if (Objects.equals(current.getOutlineLevel(), LEVEL_MONTHLY)) { + return trainingOutlineService.queryById(current.getParentId()); + } + HotTrainingOutlineVo parent = trainingOutlineService.queryById(current.getParentId()); + if (parent == null) { + return null; + } + return trainingOutlineService.queryById(parent.getParentId()); + } + + private String defaultString(String preferred, String fallback) { + return preferred != null && !preferred.isBlank() ? preferred : (fallback == null ? "" : fallback); + } } diff --git a/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/service/impl/HotTrainingOutlineServiceImpl.java b/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/service/impl/HotTrainingOutlineServiceImpl.java index 2a0832b..d9b003d 100644 --- a/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/service/impl/HotTrainingOutlineServiceImpl.java +++ b/src/main/java/com/hotwj/platform/securityManagement/trainingOutline/service/impl/HotTrainingOutlineServiceImpl.java @@ -3,6 +3,7 @@ package com.hotwj.platform.securityManagement.trainingOutline.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.hotwj.platform.resourceManagement.company.service.ISysUserLoginPortService; import com.hotwj.platform.securityManagement.trainingOutline.domain.HotTrainingOutline; import com.hotwj.platform.securityManagement.trainingOutline.domain.bo.HotTrainingOutlineBo; import com.hotwj.platform.securityManagement.trainingOutline.domain.vo.HotTrainingOutlineVo; @@ -10,87 +11,127 @@ import com.hotwj.platform.securityManagement.trainingOutline.mapper.HotTrainingO import com.hotwj.platform.securityManagement.trainingOutline.service.IHotTrainingOutlineService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.domain.model.LoginUser; +import org.dromara.common.core.exception.ServiceException; +import org.dromara.common.core.service.DictService; import org.dromara.common.core.utils.MapstructUtils; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.satoken.utils.LoginHelper; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; /** * 培训大纲Service业务层处理 * * @author shihongwei - * @date 2026-01-14 + * @date 2026-05-14 */ @Slf4j @RequiredArgsConstructor @Service public class HotTrainingOutlineServiceImpl implements IHotTrainingOutlineService { - private final HotTrainingOutlineMapper baseMapper; + private static final long LEVEL_ANNUAL = 1L; + private static final long LEVEL_MONTHLY = 2L; + private static final long LEVEL_WEEKLY = 3L; + + private final HotTrainingOutlineMapper baseMapper; + private final DictService dictService; - /** - * 查询培训大纲 - * - * @param id 主键 - * @return 培训大纲 - */ @Override public HotTrainingOutlineVo queryById(Long id) { - return baseMapper.selectVoById(id); + HotTrainingOutlineVo vo = baseMapper.selectVoById(id); + if (vo == null) { + return null; + } + if (shouldFilterDisabledForCurrentUser() && !Long.valueOf(1L).equals(vo.getIsEnabled())) { + return null; + } + fillExtraFields(List.of(vo)); + return vo; } - /** - * 分页查询培训大纲列表 - * - * @param bo 查询条件 - * @param pageQuery 分页参数 - * @return 培训大纲分页列表 - */ @Override public TableDataInfo queryPageList(HotTrainingOutlineBo bo, PageQuery pageQuery) { LambdaQueryWrapper lqw = buildQueryWrapper(bo); Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + List records = result.getRecords(); + if (records != null && !records.isEmpty()) { + fillExtraFields(records); + } return TableDataInfo.build(result); } - /** - * 查询符合条件的培训大纲列表 - * - * @param bo 查询条件 - * @return 培训大纲列表 - */ @Override public List queryList(HotTrainingOutlineBo bo) { - LambdaQueryWrapper lqw = buildQueryWrapper(bo); - return baseMapper.selectVoList(lqw); + List list = baseMapper.selectVoList(buildQueryWrapper(bo)); + fillExtraFields(list); + return list; + } + + @Override + public List queryAnnualList(HotTrainingOutlineBo bo) { + HotTrainingOutlineBo queryBo = bo == null ? new HotTrainingOutlineBo() : bo; + queryBo.setOutlineLevel(LEVEL_ANNUAL); + queryBo.setParentId(0L); + List list = baseMapper.selectVoList(buildQueryWrapper(queryBo)); + fillExtraFields(list); + return list; + } + + @Override + public List queryChildrenByParentId(Long parentId) { + HotTrainingOutline parent = baseMapper.selectById(parentId); + if (parent == null || Long.valueOf(1L).equals(parent.getIsDeleted())) { + throw new ServiceException("父级培训大纲不存在"); + } + List children = baseMapper.selectVoList( + Wrappers.lambdaQuery() + .eq(HotTrainingOutline::getParentId, parentId) + .eq(shouldFilterDisabledForCurrentUser(), HotTrainingOutline::getIsEnabled, 1L) + .eq(HotTrainingOutline::getIsDeleted, 0L) + .orderByAsc(HotTrainingOutline::getSortNo) + .orderByAsc(HotTrainingOutline::getId) + ); + fillExtraFields(children); + return children; } private LambdaQueryWrapper buildQueryWrapper(HotTrainingOutlineBo bo) { - Map params = bo.getParams(); LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); - lqw.orderByAsc(HotTrainingOutline::getId); - lqw.eq(bo.getOutlineYear() != null, HotTrainingOutline::getOutlineYear, bo.getOutlineYear()); - lqw.eq(StringUtils.isNotBlank(bo.getOutlineTypes()), HotTrainingOutline::getOutlineTypes, bo.getOutlineTypes()); + lqw.eq(bo.getId() != null, HotTrainingOutline::getId, bo.getId()); + lqw.eq(bo.getCompanyId() != null, HotTrainingOutline::getCompanyId, bo.getCompanyId()); + lqw.eq(bo.getParentId() != null, HotTrainingOutline::getParentId, bo.getParentId()); + lqw.eq(bo.getOutlineLevel() != null, HotTrainingOutline::getOutlineLevel, bo.getOutlineLevel()); + lqw.like(StringUtils.isNotBlank(bo.getOutlineName()), HotTrainingOutline::getOutlineName, bo.getOutlineName()); + lqw.eq(StringUtils.isNotBlank(bo.getTrainingType()), HotTrainingOutline::getTrainingType, bo.getTrainingType()); lqw.eq(bo.getIsEnabled() != null, HotTrainingOutline::getIsEnabled, bo.getIsEnabled()); - lqw.eq(StringUtils.isNotBlank(bo.getMonthPlanIds()), HotTrainingOutline::getMonthPlanIds, bo.getMonthPlanIds()); lqw.eq(bo.getIsDeleted() != null, HotTrainingOutline::getIsDeleted, bo.getIsDeleted()); + if (shouldFilterDisabledForCurrentUser()) { + lqw.eq(HotTrainingOutline::getIsEnabled, 1L); + } + lqw.orderByAsc(HotTrainingOutline::getSortNo) + .orderByAsc(HotTrainingOutline::getId); return lqw; } - /** - * 新增培训大纲 - * - * @param bo 培训大纲 - * @return 是否新增成功 - */ @Override + @Transactional(rollbackFor = Exception.class) public Boolean insertByBo(HotTrainingOutlineBo bo) { HotTrainingOutline add = MapstructUtils.convert(bo, HotTrainingOutline.class); + prepareEntity(add, null); validEntityBeforeSave(add); boolean flag = baseMapper.insert(add) > 0; if (flag) { @@ -99,38 +140,211 @@ public class HotTrainingOutlineServiceImpl implements IHotTrainingOutlineService return flag; } - /** - * 修改培训大纲 - * - * @param bo 培训大纲 - * @return 是否修改成功 - */ @Override + @Transactional(rollbackFor = Exception.class) public Boolean updateByBo(HotTrainingOutlineBo bo) { + HotTrainingOutline current = baseMapper.selectById(bo.getId()); + if (current == null || Long.valueOf(1L).equals(current.getIsDeleted())) { + throw new ServiceException("培训大纲不存在"); + } HotTrainingOutline update = MapstructUtils.convert(bo, HotTrainingOutline.class); + prepareEntity(update, current); validEntityBeforeSave(update); return baseMapper.updateById(update) > 0; } - /** - * 保存前的数据校验 - */ - private void validEntityBeforeSave(HotTrainingOutline entity) { - //TODO 做一些数据校验,如唯一约束 + private void prepareEntity(HotTrainingOutline entity, HotTrainingOutline current) { + if (entity.getCompanyId() == null) { + entity.setCompanyId(current != null ? current.getCompanyId() : resolveCurrentCompanyId()); + } + if (entity.getSortNo() == null) { + entity.setSortNo(current != null && current.getSortNo() != null ? current.getSortNo() : 0L); + } + if (!canManageEnabled()) { + entity.setIsEnabled(current != null && current.getIsEnabled() != null ? current.getIsEnabled() : 1L); + } else if (entity.getIsEnabled() == null) { + entity.setIsEnabled(current != null && current.getIsEnabled() != null ? current.getIsEnabled() : 1L); + } + if (entity.getIsDeleted() == null) { + entity.setIsDeleted(current != null && current.getIsDeleted() != null ? current.getIsDeleted() : 0L); + } + if (Objects.equals(entity.getOutlineLevel(), LEVEL_ANNUAL)) { + entity.setParentId(0L); + } else if (entity.getParentId() == null && current != null) { + entity.setParentId(current.getParentId()); + } + } + + private void validEntityBeforeSave(HotTrainingOutline entity) { + if (!isSupportedLevel(entity.getOutlineLevel())) { + throw new ServiceException("大纲层级仅支持年度、月度、周度"); + } + if (StringUtils.isBlank(entity.getOutlineName())) { + throw new ServiceException("大纲名称不能为空"); + } + if (StringUtils.isBlank(entity.getOutlineFile())) { + throw new ServiceException("大纲文件不能为空"); + } + if (StringUtils.isBlank(entity.getTrainingType())) { + throw new ServiceException("培训类型不能为空"); + } + if (entity.getIsEnabled() != null && !Objects.equals(entity.getIsEnabled(), 0L) && !Objects.equals(entity.getIsEnabled(), 1L)) { + throw new ServiceException("是否启用取值仅支持0和1"); + } + + if (Objects.equals(entity.getOutlineLevel(), LEVEL_MONTHLY)) { + HotTrainingOutline parent = requireParent(entity.getParentId(), "月度大纲"); + if (!Objects.equals(parent.getOutlineLevel(), LEVEL_ANNUAL)) { + throw new ServiceException("月度大纲的上级必须是年度大纲"); + } + inheritCompanyFromParent(entity, parent); + } else if (Objects.equals(entity.getOutlineLevel(), LEVEL_WEEKLY)) { + HotTrainingOutline parent = requireParent(entity.getParentId(), "周度大纲"); + if (!Objects.equals(parent.getOutlineLevel(), LEVEL_MONTHLY)) { + throw new ServiceException("周度大纲的上级必须是月度大纲"); + } + inheritCompanyFromParent(entity, parent); + } + + LambdaQueryWrapper duplicateWrapper = Wrappers.lambdaQuery(); + duplicateWrapper.eq(HotTrainingOutline::getOutlineLevel, entity.getOutlineLevel()) + .eq(HotTrainingOutline::getParentId, entity.getParentId()) + .eq(HotTrainingOutline::getOutlineName, entity.getOutlineName()) + .eq(HotTrainingOutline::getIsDeleted, 0L) + .ne(entity.getId() != null, HotTrainingOutline::getId, entity.getId()); + if (entity.getCompanyId() != null) { + duplicateWrapper.eq(HotTrainingOutline::getCompanyId, entity.getCompanyId()); + } + if (baseMapper.selectCount(duplicateWrapper) > 0) { + throw new ServiceException("同级下已存在同名培训大纲"); + } + } + + private HotTrainingOutline requireParent(Long parentId, String currentLevelName) { + if (parentId == null || parentId <= 0) { + throw new ServiceException(currentLevelName + "必须选择上级大纲"); + } + HotTrainingOutline parent = baseMapper.selectById(parentId); + if (parent == null || Long.valueOf(1L).equals(parent.getIsDeleted())) { + throw new ServiceException("上级培训大纲不存在"); + } + if (shouldFilterDisabledForCurrentUser() && !Long.valueOf(1L).equals(parent.getIsEnabled())) { + throw new ServiceException("上级培训大纲不存在"); + } + return parent; + } + + private void inheritCompanyFromParent(HotTrainingOutline entity, HotTrainingOutline parent) { + if (entity.getCompanyId() == null) { + entity.setCompanyId(parent.getCompanyId()); + } + if (entity.getCompanyId() != null && parent.getCompanyId() != null + && !Objects.equals(entity.getCompanyId(), parent.getCompanyId())) { + throw new ServiceException("子级大纲与上级大纲的公司归属必须一致"); + } } - /** - * 校验并批量删除培训大纲信息 - * - * @param ids 待删除的主键集合 - * @param isValid 是否进行有效性校验 - * @return 是否删除成功 - */ @Override + @Transactional(rollbackFor = Exception.class) public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if (ids == null || ids.isEmpty()) { + return false; + } if (isValid) { - //TODO 做一些业务上的校验,判断是否需要校验 + Long childCount = baseMapper.selectCount( + Wrappers.lambdaQuery() + .in(HotTrainingOutline::getParentId, ids) + .eq(HotTrainingOutline::getIsDeleted, 0L) + ); + if (childCount != null && childCount > 0) { + throw new ServiceException("存在下级培训大纲,无法删除"); + } } return baseMapper.deleteByIds(ids) > 0; } + + private void fillExtraFields(List list) { + if (list == null || list.isEmpty()) { + return; + } + Set parentIds = list.stream() + .map(HotTrainingOutlineVo::getParentId) + .filter(Objects::nonNull) + .filter(parentId -> parentId > 0) + .collect(Collectors.toCollection(LinkedHashSet::new)); + Map parentNameMap = new HashMap<>(); + if (!parentIds.isEmpty()) { + parentNameMap = baseMapper.selectBatchIds(parentIds).stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap(HotTrainingOutline::getId, HotTrainingOutline::getOutlineName, (a, b) -> a)); + } + + Set currentIds = list.stream() + .map(HotTrainingOutlineVo::getId) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(LinkedHashSet::new)); + Map childCountMap = Collections.emptyMap(); + if (!currentIds.isEmpty()) { + childCountMap = baseMapper.selectList( + Wrappers.lambdaQuery() + .select(HotTrainingOutline::getId, HotTrainingOutline::getParentId) + .in(HotTrainingOutline::getParentId, currentIds) + .eq(HotTrainingOutline::getIsDeleted, 0L) + ).stream().collect(Collectors.groupingBy(HotTrainingOutline::getParentId, Collectors.counting())); + } + + for (HotTrainingOutlineVo vo : list) { + vo.setParentName(parentNameMap.get(vo.getParentId())); + vo.setOutlineLevelName(resolveLevelName(vo.getOutlineLevel())); + if (StringUtils.isNotBlank(vo.getTrainingType())) { + String label = dictService.getDictLabel("hot_training_type", vo.getTrainingType()); + vo.setTrainingTypeLabel(StringUtils.isNotBlank(label) ? label : vo.getTrainingType()); + } + vo.setHasChildren(childCountMap.getOrDefault(vo.getId(), 0L) > 0); + } + } + + private String resolveLevelName(Long outlineLevel) { + if (Objects.equals(outlineLevel, LEVEL_ANNUAL)) { + return "年度"; + } + if (Objects.equals(outlineLevel, LEVEL_MONTHLY)) { + return "月度"; + } + if (Objects.equals(outlineLevel, LEVEL_WEEKLY)) { + return "周度"; + } + return ""; + } + + private boolean isSupportedLevel(Long outlineLevel) { + return Objects.equals(outlineLevel, LEVEL_ANNUAL) + || Objects.equals(outlineLevel, LEVEL_MONTHLY) + || Objects.equals(outlineLevel, LEVEL_WEEKLY); + } + + private Long resolveCurrentCompanyId() { + try { + return LoginHelper.getCompanyId(); + } catch (Exception e) { + log.debug("resolveCurrentCompanyId failed", e); + return null; + } + } + + private boolean shouldFilterDisabledForCurrentUser() { + if (LoginHelper.isSuperAdmin() || LoginHelper.isTenantAdmin()) { + return false; + } + LoginUser loginUser = LoginHelper.getLoginUser(); + return loginUser != null && ISysUserLoginPortService.ENTERPRISE_PORT.equals(loginUser.getLoginPort()); + } + + private boolean canManageEnabled() { + if (LoginHelper.isSuperAdmin() || LoginHelper.isTenantAdmin()) { + return true; + } + LoginUser loginUser = LoginHelper.getLoginUser(); + return loginUser != null && ISysUserLoginPortService.HEADQUARTERS_PORT.equals(loginUser.getLoginPort()); + } }