tuqin
7 天以前 de754f277f7cef20206c5f30fee6e033c426a554
feat(safety): 安全生产五期 - 安全教育培训与持证管理

覆盖培训计划→组织→签到→考试→持证归档全链路。

三张表:
- mes_safety_training 培训批次(类型/层级/讲师/时间/学时/内容/状态)
- mes_safety_training_attendee 参训人员(签到状态 + 签到时间 + 考试成绩 + 是否通过)
- mes_safety_certificate 资格证书(类型/编号/发证机关/发证日期/有效期至)

后端:
- 培训:新增/修改/删除(删除时级联清理参训记录,避免孤儿数据)、
时间区间校验、编码唯一校验
- 参训人员:全量保存(先清后插,保证与前端列表一致)+ 单个更新签到/成绩 + 删除
- 冗余统计自动刷新:参训人数、实到人数(已签到+迟到)、平均分(只统计有成绩的)
- 资格证书:证书编号唯一、有效期必须晚于发证日期
- 到期计算由服务端完成:返回距到期天数、是否已过期、是否 30 天内到期
- 支持 expiringSoon 筛选(只看 30 天内到期含已过期)
- 错误码 1_040_907_xxx / 1_040_908_xxx;字典常量 MES_SAFETY_TRAINING_* / MES_SAFETY_CERT_*

前端:
- 培训列表(参训/实到用颜色区分是否到齐)+ 培训表单
- 参训人员弹窗:多选添加人员、逐行设置签到状态与成绩、实时统计人数
- 资格证书列表(已过期红色标记、30 天内到期橙色 + 剩余天数、长期有效单独标记)+ 表单

SQL(mes_safety_training_init.sql,可重复执行):
- 建表 3 张、菜单 2 个(培训管理/资格证书)、字典 5 组、10 个按钮权限

注:本期只提交代码,未部署。
已添加21个文件
已修改2个文件
1786 ■■■■■ 文件已修改
sql/mysql/mes_safety_training_init.sql 201 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/MesSafetyCertificateController.java 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/MesSafetyTrainingController.java 163 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/certificate/MesSafetyCertificatePageReqVO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/certificate/MesSafetyCertificateRespVO.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/certificate/MesSafetyCertificateSaveReqVO.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/training/MesSafetyTrainingAttendeeRespVO.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/training/MesSafetyTrainingAttendeeSaveReqVO.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/training/MesSafetyTrainingPageReqVO.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/training/MesSafetyTrainingRespVO.java 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/training/MesSafetyTrainingSaveReqVO.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/MesSafetyCertificateDO.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/MesSafetyTrainingAttendeeDO.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/MesSafetyTrainingDO.java 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/MesSafetyCertificateMapper.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/MesSafetyTrainingAttendeeMapper.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/MesSafetyTrainingMapper.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/DictTypeConstants.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/MesSafetyCertificateService.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/MesSafetyCertificateServiceImpl.java 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/MesSafetyTrainingService.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/MesSafetyTrainingServiceImpl.java 203 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_safety_training_init.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,201 @@
-- =====================================================================
-- å®‰å…¨ç”Ÿäº§æ¨¡å— â€”— äº”期:安全教育培训
--
-- å®šä½ï¼šåŸ¹è®­è®¡åˆ’ â†’ ç»„织培训 â†’ å‚训签到 â†’ è€ƒè¯•评分 â†’ æŒè¯ç®¡ç†ï¼Œå½¢æˆåŸ¹è®­æ¡£æ¡ˆé—­çŽ¯ã€‚
--       ç‰¹ç§ä½œä¸šäººå‘˜æ˜¯å¦æŒæœ‰æ•ˆè¯ä»¶ï¼Œå¯é€šè¿‡èµ„格证书表核查。
--
-- ä¸‰å¼ è¡¨ï¼š
--   mes_safety_training           åŸ¹è®­æ‰¹æ¬¡ï¼ˆä¸€æ¬¡åŸ¹è®­æ´»åŠ¨ï¼‰
--   mes_safety_training_attendee  å‚训人员(含签到与考试成绩)
--   mes_safety_certificate        å®‰å…¨èµ„格证书(特种作业证等,含有效期)
--
-- å†…容:
--   1. å»ºè¡¨ 3 å¼ 
--   2. å­—典:培训类型 / åŸ¹è®­çŠ¶æ€ / ç­¾åˆ°çŠ¶æ€ / è¯ä¹¦ç±»åž‹
--   3. èœå•:培训管理、资格证书,挂在「安全生产」目录下
--   4. æŽˆæƒè¶…级管理员
--
-- ç‰¹æ€§ï¼šå¯é‡å¤æ‰§è¡Œ
-- =====================================================================
-- ---------------------------------------------------------------------
-- 1. åŸ¹è®­æ‰¹æ¬¡
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `mes_safety_training` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
  `code` varchar(64) NOT NULL COMMENT '培训编号',
  `title` varchar(200) NOT NULL COMMENT '培训主题',
  `training_type` tinyint DEFAULT NULL COMMENT '培训类型(字典 mes_safety_training_type)',
  `training_level` tinyint DEFAULT NULL COMMENT '培训层级(字典 mes_safety_training_level:1公司级 2车间级 3班组级)',
  `trainer` varchar(100) DEFAULT NULL COMMENT '讲师/授课人',
  `trainer_org` varchar(200) DEFAULT NULL COMMENT '讲师单位(外部讲师时填写)',
  `location` varchar(255) DEFAULT NULL COMMENT '培训地点',
  `start_time` datetime DEFAULT NULL COMMENT '培训开始时间',
  `end_time` datetime DEFAULT NULL COMMENT '培训结束时间',
  `duration_hours` decimal(6,1) DEFAULT NULL COMMENT '学时',
  `content` varchar(2000) DEFAULT NULL COMMENT '培训内容',
  `material` varchar(500) DEFAULT NULL COMMENT '培训资料/教材',
  `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0计划中 10进行中 20已完成 30已取消',
  `attendee_count` int DEFAULT '0' COMMENT '参训人数(冗余统计)',
  `signed_count` int DEFAULT '0' COMMENT '实到人数(冗余统计)',
  `avg_score` decimal(6,2) DEFAULT NULL COMMENT '考试平均分(冗余统计)',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  `creator` varchar(64) DEFAULT '' COMMENT '创建者',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updater` varchar(64) DEFAULT '' COMMENT '更新者',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `uk_code` (`code`) USING BTREE,
  KEY `idx_status` (`status`) USING BTREE,
  KEY `idx_start` (`start_time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='安全生产-培训批次';
-- ---------------------------------------------------------------------
-- 2. å‚训人员(含签到与考试成绩)
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `mes_safety_training_attendee` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
  `training_id` bigint NOT NULL COMMENT '培训批次编号',
  `user_id` bigint NOT NULL COMMENT '参训人编号',
  `dept_id` bigint DEFAULT NULL COMMENT '所属部门编号',
  `sign_status` tinyint DEFAULT '0' COMMENT '签到状态:0未签到 1已签到 2迟到 3缺席',
  `sign_time` datetime DEFAULT NULL COMMENT '签到时间',
  `score` decimal(6,2) DEFAULT NULL COMMENT '考试成绩',
  `passed` tinyint DEFAULT '0' COMMENT '是否通过:0未考 1通过 2不通过',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  `creator` varchar(64) DEFAULT '' COMMENT '创建者',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updater` varchar(64) DEFAULT '' COMMENT '更新者',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_training` (`training_id`) USING BTREE,
  KEY `idx_user` (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='安全生产-培训参训人员';
-- ---------------------------------------------------------------------
-- 3. å®‰å…¨èµ„格证书
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `mes_safety_certificate` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
  `user_id` bigint NOT NULL COMMENT '持证人编号',
  `dept_id` bigint DEFAULT NULL COMMENT '所属部门编号',
  `cert_type` tinyint NOT NULL COMMENT '证书类型(字典 mes_safety_cert_type)',
  `cert_no` varchar(100) NOT NULL COMMENT '证书编号',
  `issue_org` varchar(200) DEFAULT NULL COMMENT '发证机关',
  `issue_date` date DEFAULT NULL COMMENT '发证日期',
  `expire_date` date DEFAULT NULL COMMENT '有效期至(空表示长期有效)',
  `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1有效 0已作废',
  `attachment_url` varchar(500) DEFAULT NULL COMMENT '证书扫描件地址',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  `creator` varchar(64) DEFAULT '' COMMENT '创建者',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updater` varchar(64) DEFAULT '' COMMENT '更新者',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `uk_cert_no` (`cert_no`) USING BTREE,
  KEY `idx_user` (`user_id`) USING BTREE,
  KEY `idx_expire` (`expire_date`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='安全生产-安全资格证书';
-- ---------------------------------------------------------------------
-- 4. å­—å…¸
-- ---------------------------------------------------------------------
DELETE FROM system_dict_data WHERE dict_type IN
  ('mes_safety_training_type', 'mes_safety_training_level', 'mes_safety_training_status',
   'mes_safety_attendee_status', 'mes_safety_cert_type');
DELETE FROM system_dict_type WHERE type IN
  ('mes_safety_training_type', 'mes_safety_training_level', 'mes_safety_training_status',
   'mes_safety_attendee_status', 'mes_safety_cert_type');
INSERT INTO system_dict_type (name, type, status, remark, creator, create_time, updater, update_time, deleted)
VALUES ('安全生产-培训类型', 'mes_safety_training_type', 0, '培训活动分类', '1', NOW(), '1', NOW(), b'0'),
       ('安全生产-培训层级', 'mes_safety_training_level', 0, '三级教育层级', '1', NOW(), '1', NOW(), b'0'),
       ('安全生产-培训状态', 'mes_safety_training_status', 0, '培训批次状态', '1', NOW(), '1', NOW(), b'0'),
       ('安全生产-签到状态', 'mes_safety_attendee_status', 0, '参训签到情况', '1', NOW(), '1', NOW(), b'0'),
       ('安全生产-证书类型', 'mes_safety_cert_type', 0, '安全资格证书类型', '1', NOW(), '1', NOW(), b'0');
INSERT INTO system_dict_data (sort, label, value, dict_type, status, color_type, creator, create_time, updater, update_time, deleted)
VALUES
-- åŸ¹è®­ç±»åž‹
(1, '三级安全教育', '1', 'mes_safety_training_type', 0, 'processing', '1', NOW(), '1', NOW(), b'0'),
(2, '专项培训',     '2', 'mes_safety_training_type', 0, 'default',    '1', NOW(), '1', NOW(), b'0'),
(3, '复训',         '3', 'mes_safety_training_type', 0, 'warning',    '1', NOW(), '1', NOW(), b'0'),
(4, '日常安全教育', '4', 'mes_safety_training_type', 0, 'default',    '1', NOW(), '1', NOW(), b'0'),
(5, '其他',         '5', 'mes_safety_training_type', 0, 'default',    '1', NOW(), '1', NOW(), b'0'),
-- åŸ¹è®­å±‚级
(1, '公司级', '1', 'mes_safety_training_level', 0, 'processing', '1', NOW(), '1', NOW(), b'0'),
(2, '车间级', '2', 'mes_safety_training_level', 0, 'default',    '1', NOW(), '1', NOW(), b'0'),
(3, '班组级', '3', 'mes_safety_training_level', 0, 'default',    '1', NOW(), '1', NOW(), b'0'),
-- åŸ¹è®­çŠ¶æ€
(1, '计划中', '0',  'mes_safety_training_status', 0, 'processing', '1', NOW(), '1', NOW(), b'0'),
(2, '进行中', '10', 'mes_safety_training_status', 0, 'warning',    '1', NOW(), '1', NOW(), b'0'),
(3, '已完成', '20', 'mes_safety_training_status', 0, 'success',    '1', NOW(), '1', NOW(), b'0'),
(4, '已取消', '30', 'mes_safety_training_status', 0, 'default',    '1', NOW(), '1', NOW(), b'0'),
-- ç­¾åˆ°çŠ¶æ€
(1, '未签到', '0', 'mes_safety_attendee_status', 0, 'default',    '1', NOW(), '1', NOW(), b'0'),
(2, '已签到', '1', 'mes_safety_attendee_status', 0, 'success',    '1', NOW(), '1', NOW(), b'0'),
(3, '迟到',   '2', 'mes_safety_attendee_status', 0, 'warning',    '1', NOW(), '1', NOW(), b'0'),
(4, '缺席',   '3', 'mes_safety_attendee_status', 0, 'error',      '1', NOW(), '1', NOW(), b'0'),
-- è¯ä¹¦ç±»åž‹
(1, '特种作业操作证',   '1', 'mes_safety_cert_type', 0, 'processing', '1', NOW(), '1', NOW(), b'0'),
(2, '安全管理人员证',   '2', 'mes_safety_cert_type', 0, 'default',    '1', NOW(), '1', NOW(), b'0'),
(3, '主要负责人证',     '3', 'mes_safety_cert_type', 0, 'default',    '1', NOW(), '1', NOW(), b'0'),
(4, '其他资格证',       '4', 'mes_safety_cert_type', 0, 'default',    '1', NOW(), '1', NOW(), b'0');
-- ---------------------------------------------------------------------
-- 5. èœå•(挂在 1065910 å®‰å…¨ç”Ÿäº§ä¸‹ï¼‰
-- ---------------------------------------------------------------------
DELETE FROM system_role_menu WHERE menu_id BETWEEN 1065941 AND 1065960;
DELETE FROM system_menu WHERE id BETWEEN 1065941 AND 1065960;
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon,
                         component, component_name, status, visible, keep_alive, always_show,
                         creator, create_time, updater, update_time, deleted)
VALUES
-- åŸ¹è®­ç®¡ç†
(1065941, '安全教育培训', '', 2, 6, 1065910, 'training', 'ep:reading',
 'safety/training/index', 'SafetyTraining', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065942, '培训查询',   'mes:safety-training:query',  3, 1, 1065941, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065943, '培训创建',   'mes:safety-training:create', 3, 2, 1065941, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065944, '培训修改',   'mes:safety-training:update', 3, 3, 1065941, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065945, '培训删除',   'mes:safety-training:delete', 3, 4, 1065941, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065946, '参训人员维护', 'mes:safety-training:attendee', 3, 5, 1065941, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
-- èµ„格证书
(1065947, '安全资格证书', '', 2, 7, 1065910, 'certificate', 'ep:medal',
 'safety/certificate/index', 'SafetyCertificate', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065948, '证书查询', 'mes:safety-certificate:query',  3, 1, 1065947, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065949, '证书创建', 'mes:safety-certificate:create', 3, 2, 1065947, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065950, '证书修改', 'mes:safety-certificate:update', 3, 3, 1065947, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065951, '证书删除', 'mes:safety-certificate:delete', 3, 4, 1065947, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
INSERT INTO system_role_menu (role_id, menu_id, creator, create_time, updater, update_time, deleted)
SELECT 1, m.id, '1', NOW(), '1', NOW(), b'0'
FROM system_menu m
WHERE m.id BETWEEN 1065941 AND 1065960;
-- ---------------------------------------------------------------------
-- 6. éªŒè¯
-- ---------------------------------------------------------------------
SELECT id, parent_id, name, path, component, permission, type
FROM system_menu WHERE id BETWEEN 1065910 AND 1065960 ORDER BY id;
SELECT dict_type, label, value FROM system_dict_data
WHERE dict_type LIKE 'mes_safety_training%' OR dict_type IN ('mes_safety_attendee_status', 'mes_safety_cert_type')
ORDER BY dict_type, sort;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/MesSafetyCertificateController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,144 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.certificate.MesSafetyCertificatePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.certificate.MesSafetyCertificateRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.certificate.MesSafetyCertificateSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.MesSafetyCertificateDO;
import cn.iocoder.yudao.module.mes.service.safety.MesSafetyCertificateService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * å®‰å…¨ç”Ÿäº§ - å®‰å…¨èµ„格证书 Controller
 *
 * @author è¶…级管理员
 */
@Tag(name = "管理后台 - å®‰å…¨ç”Ÿäº§-安全资格证书")
@RestController
@RequestMapping("/mes/safety/certificate")
@Validated
public class MesSafetyCertificateController {
    /** å³å°†åˆ°æœŸé˜ˆå€¼ï¼ˆå¤©ï¼‰ */
    private static final long EXPIRING_SOON_DAYS = 30L;
    @Resource
    private MesSafetyCertificateService certificateService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private DeptApi deptApi;
    @PostMapping("/create")
    @Operation(summary = "创建资格证书")
    @PreAuthorize("@ss.hasPermission('mes:safety-certificate:create')")
    public CommonResult<Long> createCertificate(@Valid @RequestBody MesSafetyCertificateSaveReqVO createReqVO) {
        return success(certificateService.createCertificate(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新资格证书")
    @PreAuthorize("@ss.hasPermission('mes:safety-certificate:update')")
    public CommonResult<Boolean> updateCertificate(@Valid @RequestBody MesSafetyCertificateSaveReqVO updateReqVO) {
        certificateService.updateCertificate(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除资格证书")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:safety-certificate:delete')")
    public CommonResult<Boolean> deleteCertificate(@RequestParam("id") Long id) {
        certificateService.deleteCertificate(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得资格证书")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('mes:safety-certificate:query')")
    public CommonResult<MesSafetyCertificateRespVO> getCertificate(@RequestParam("id") Long id) {
        MesSafetyCertificateDO certificate = certificateService.getCertificate(id);
        if (certificate == null) {
            return success(null);
        }
        List<MesSafetyCertificateRespVO> list = buildRespList(Collections.singletonList(certificate));
        return success(list.isEmpty() ? null : list.get(0));
    }
    @GetMapping("/page")
    @Operation(summary = "获得资格证书分页")
    @PreAuthorize("@ss.hasPermission('mes:safety-certificate:query')")
    public CommonResult<PageResult<MesSafetyCertificateRespVO>> getCertificatePage(@Valid MesSafetyCertificatePageReqVO pageReqVO) {
        PageResult<MesSafetyCertificateDO> pageResult = certificateService.getCertificatePage(pageReqVO);
        List<MesSafetyCertificateRespVO> list = buildRespList(pageResult.getList());
        return success(new PageResult<>(list, pageResult.getTotal()));
    }
    /**
     * è½¬æ¢ä¸º VO å¹¶å¡«å……持证人姓名、部门名称、到期天数与到期标记
     */
    private List<MesSafetyCertificateRespVO> buildRespList(List<MesSafetyCertificateDO> list) {
        if (list == null || list.isEmpty()) {
            return Collections.emptyList();
        }
        List<MesSafetyCertificateRespVO> respList = BeanUtils.toBean(list, MesSafetyCertificateRespVO.class);
        Set<Long> userIds = list.stream().map(MesSafetyCertificateDO::getUserId)
                .filter(Objects::nonNull).collect(Collectors.toSet());
        Map<Long, AdminUserRespDTO> userMap = userIds.isEmpty()
                ? Collections.emptyMap() : adminUserApi.getUserMap(userIds);
        Set<Long> deptIds = list.stream().map(MesSafetyCertificateDO::getDeptId)
                .filter(Objects::nonNull).collect(Collectors.toSet());
        Map<Long, String> deptNameMap = new HashMap<>();
        if (!deptIds.isEmpty()) {
            List<DeptRespDTO> depts = deptApi.getDeptList(deptIds);
            if (depts != null) {
                depts.forEach(d -> deptNameMap.put(d.getId(), d.getName()));
            }
        }
        LocalDate today = LocalDate.now();
        for (MesSafetyCertificateRespVO vo : respList) {
            AdminUserRespDTO user = userMap.get(vo.getUserId());
            if (user != null) {
                vo.setUserName(user.getNickname());
            }
            vo.setDeptName(deptNameMap.get(vo.getDeptId()));
            if (vo.getExpireDate() != null) {
                long days = ChronoUnit.DAYS.between(today, vo.getExpireDate());
                vo.setDaysToExpire(days);
                vo.setExpired(days < 0);
                vo.setExpiringSoon(days >= 0 && days <= EXPIRING_SOON_DAYS);
            } else {
                vo.setExpired(false);
                vo.setExpiringSoon(false);
            }
        }
        return respList;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/MesSafetyTrainingController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,163 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.training.MesSafetyTrainingAttendeeRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.training.MesSafetyTrainingAttendeeSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.training.MesSafetyTrainingPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.training.MesSafetyTrainingRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.training.MesSafetyTrainingSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.MesSafetyTrainingAttendeeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.MesSafetyTrainingDO;
import cn.iocoder.yudao.module.mes.service.safety.MesSafetyTrainingService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * å®‰å…¨ç”Ÿäº§ - å®‰å…¨æ•™è‚²åŸ¹è®­ Controller
 *
 * @author è¶…级管理员
 */
@Tag(name = "管理后台 - å®‰å…¨ç”Ÿäº§-安全教育培训")
@RestController
@RequestMapping("/mes/safety/training")
@Validated
public class MesSafetyTrainingController {
    @Resource
    private MesSafetyTrainingService trainingService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private DeptApi deptApi;
    @PostMapping("/create")
    @Operation(summary = "创建培训批次")
    @PreAuthorize("@ss.hasPermission('mes:safety-training:create')")
    public CommonResult<Long> createTraining(@Valid @RequestBody MesSafetyTrainingSaveReqVO createReqVO) {
        return success(trainingService.createTraining(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新培训批次")
    @PreAuthorize("@ss.hasPermission('mes:safety-training:update')")
    public CommonResult<Boolean> updateTraining(@Valid @RequestBody MesSafetyTrainingSaveReqVO updateReqVO) {
        trainingService.updateTraining(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除培训批次")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:safety-training:delete')")
    public CommonResult<Boolean> deleteTraining(@RequestParam("id") Long id) {
        trainingService.deleteTraining(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得培训批次")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('mes:safety-training:query')")
    public CommonResult<MesSafetyTrainingRespVO> getTraining(@RequestParam("id") Long id) {
        MesSafetyTrainingDO training = trainingService.getTraining(id);
        return success(training == null ? null : BeanUtils.toBean(training, MesSafetyTrainingRespVO.class));
    }
    @GetMapping("/page")
    @Operation(summary = "获得培训批次分页")
    @PreAuthorize("@ss.hasPermission('mes:safety-training:query')")
    public CommonResult<PageResult<MesSafetyTrainingRespVO>> getTrainingPage(@Valid MesSafetyTrainingPageReqVO pageReqVO) {
        PageResult<MesSafetyTrainingDO> pageResult = trainingService.getTrainingPage(pageReqVO);
        return success(new PageResult<>(
                BeanUtils.toBean(pageResult.getList(), MesSafetyTrainingRespVO.class),
                pageResult.getTotal()));
    }
    // ==================== å‚训人员 ====================
    @GetMapping("/attendee-list")
    @Operation(summary = "获得参训人员列表")
    @Parameter(name = "trainingId", description = "培训批次编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:safety-training:query')")
    public CommonResult<List<MesSafetyTrainingAttendeeRespVO>> getAttendeeList(@RequestParam("trainingId") Long trainingId) {
        return success(buildAttendeeRespList(trainingService.getAttendeeList(trainingId)));
    }
    @PostMapping("/save-attendees")
    @Operation(summary = "全量保存参训人员")
    @Parameter(name = "trainingId", description = "培训批次编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:safety-training:attendee')")
    public CommonResult<Boolean> saveAttendees(@RequestParam("trainingId") Long trainingId,
                                               @RequestBody List<MesSafetyTrainingAttendeeSaveReqVO> attendees) {
        trainingService.saveAttendees(trainingId, attendees);
        return success(true);
    }
    @PutMapping("/update-attendee")
    @Operation(summary = "更新参训人员签到/成绩")
    @PreAuthorize("@ss.hasPermission('mes:safety-training:attendee')")
    public CommonResult<Boolean> updateAttendee(@Valid @RequestBody MesSafetyTrainingAttendeeSaveReqVO reqVO) {
        trainingService.updateAttendee(reqVO);
        return success(true);
    }
    @DeleteMapping("/delete-attendee")
    @Operation(summary = "删除参训人员")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:safety-training:attendee')")
    public CommonResult<Boolean> deleteAttendee(@RequestParam("id") Long id) {
        trainingService.deleteAttendee(id);
        return success(true);
    }
    private List<MesSafetyTrainingAttendeeRespVO> buildAttendeeRespList(List<MesSafetyTrainingAttendeeDO> list) {
        if (list == null || list.isEmpty()) {
            return Collections.emptyList();
        }
        List<MesSafetyTrainingAttendeeRespVO> respList = BeanUtils.toBean(list, MesSafetyTrainingAttendeeRespVO.class);
        Set<Long> userIds = list.stream().map(MesSafetyTrainingAttendeeDO::getUserId)
                .filter(Objects::nonNull).collect(Collectors.toSet());
        Map<Long, AdminUserRespDTO> userMap = userIds.isEmpty()
                ? Collections.emptyMap() : adminUserApi.getUserMap(userIds);
        Set<Long> deptIds = list.stream().map(MesSafetyTrainingAttendeeDO::getDeptId)
                .filter(Objects::nonNull).collect(Collectors.toSet());
        Map<Long, String> deptNameMap = new HashMap<>();
        if (!deptIds.isEmpty()) {
            List<DeptRespDTO> depts = deptApi.getDeptList(deptIds);
            if (depts != null) {
                depts.forEach(d -> deptNameMap.put(d.getId(), d.getName()));
            }
        }
        for (MesSafetyTrainingAttendeeRespVO vo : respList) {
            AdminUserRespDTO user = userMap.get(vo.getUserId());
            if (user != null) {
                vo.setUserName(user.getNickname());
            }
            vo.setDeptName(deptNameMap.get(vo.getDeptId()));
        }
        return respList;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/certificate/MesSafetyCertificatePageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.certificate;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(description = "管理后台 - å®‰å…¨ç”Ÿäº§-资格证书分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesSafetyCertificatePageReqVO extends PageParam {
    @Schema(description = "持证人编号", example = "1")
    private Long userId;
    @Schema(description = "证书类型", example = "1")
    private Integer certType;
    @Schema(description = "证书编号", example = "T123456789")
    private String certNo;
    @Schema(description = "状态", example = "1")
    private Integer status;
    @Schema(description = "是否只看即将到期(30 å¤©å†…)", example = "false")
    private Boolean expiringSoon;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/certificate/MesSafetyCertificateRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.certificate;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - å®‰å…¨ç”Ÿäº§-资格证书 Response VO")
@Data
public class MesSafetyCertificateRespVO {
    @Schema(description = "编号", example = "1024")
    private Long id;
    @Schema(description = "持证人编号", example = "1")
    private Long userId;
    @Schema(description = "持证人姓名")
    private String userName;
    @Schema(description = "所属部门编号", example = "120")
    private Long deptId;
    @Schema(description = "所属部门名称")
    private String deptName;
    @Schema(description = "证书类型", example = "1")
    private Integer certType;
    @Schema(description = "证书编号", example = "T123456789")
    private String certNo;
    @Schema(description = "发证机关")
    private String issueOrg;
    @Schema(description = "发证日期")
    private LocalDate issueDate;
    @Schema(description = "有效期至")
    private LocalDate expireDate;
    @Schema(description = "距到期天数(负数表示已过期)", example = "180")
    private Long daysToExpire;
    @Schema(description = "是否已过期", example = "false")
    private Boolean expired;
    @Schema(description = "是否即将到期(30 å¤©å†…)", example = "false")
    private Boolean expiringSoon;
    @Schema(description = "状态:1有效 0已作废", example = "1")
    private Integer status;
    @Schema(description = "证书扫描件地址")
    private String attachmentUrl;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/certificate/MesSafetyCertificateSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.certificate;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
@Schema(description = "管理后台 - å®‰å…¨ç”Ÿäº§-资格证书新增/修改 Request VO")
@Data
public class MesSafetyCertificateSaveReqVO {
    @Schema(description = "编号", example = "1024")
    private Long id;
    @Schema(description = "持证人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "持证人不能为空")
    private Long userId;
    @Schema(description = "所属部门编号", example = "120")
    private Long deptId;
    @Schema(description = "证书类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "证书类型不能为空")
    private Integer certType;
    @Schema(description = "证书编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "T123456789")
    @NotEmpty(message = "证书编号不能为空")
    private String certNo;
    @Schema(description = "发证机关", example = "新疆应急管理厅")
    private String issueOrg;
    @Schema(description = "发证日期")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
    private LocalDate issueDate;
    @Schema(description = "有效期至(留空表示长期有效)")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
    private LocalDate expireDate;
    @Schema(description = "状态:1有效 0已作废", example = "1")
    private Integer status;
    @Schema(description = "证书扫描件地址")
    private String attachmentUrl;
    @Schema(description = "备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/training/MesSafetyTrainingAttendeeRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.training;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - å®‰å…¨ç”Ÿäº§-参训人员 Response VO")
@Data
public class MesSafetyTrainingAttendeeRespVO {
    @Schema(description = "编号", example = "1024")
    private Long id;
    @Schema(description = "培训批次编号", example = "1")
    private Long trainingId;
    @Schema(description = "参训人编号", example = "1")
    private Long userId;
    @Schema(description = "参训人姓名")
    private String userName;
    @Schema(description = "所属部门编号", example = "120")
    private Long deptId;
    @Schema(description = "所属部门名称")
    private String deptName;
    @Schema(description = "签到状态:0未签到 1已签到 2迟到 3缺席", example = "1")
    private Integer signStatus;
    @Schema(description = "签到时间")
    private LocalDateTime signTime;
    @Schema(description = "考试成绩", example = "88")
    private BigDecimal score;
    @Schema(description = "是否通过:0未考 1通过 2不通过", example = "1")
    private Integer passed;
    @Schema(description = "备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/training/MesSafetyTrainingAttendeeSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.training;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - å®‰å…¨ç”Ÿäº§-参训人员 ä¿å­˜ Request VO")
@Data
public class MesSafetyTrainingAttendeeSaveReqVO {
    @Schema(description = "编号", example = "1024")
    private Long id;
    @Schema(description = "培训批次编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "培训批次不能为空")
    private Long trainingId;
    @Schema(description = "参训人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "参训人不能为空")
    private Long userId;
    @Schema(description = "所属部门编号", example = "120")
    private Long deptId;
    @Schema(description = "签到状态:0未签到 1已签到 2迟到 3缺席", example = "0")
    private Integer signStatus;
    @Schema(description = "签到时间")
    private LocalDateTime signTime;
    @Schema(description = "考试成绩", example = "88")
    private BigDecimal score;
    @Schema(description = "是否通过:0未考 1通过 2不通过", example = "0")
    private Integer passed;
    @Schema(description = "备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/training/MesSafetyTrainingPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.training;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - å®‰å…¨ç”Ÿäº§-培训分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesSafetyTrainingPageReqVO extends PageParam {
    @Schema(description = "培训编号", example = "PX-2026-001")
    private String code;
    @Schema(description = "培训主题", example = "特种作业安全操作培训")
    private String title;
    @Schema(description = "培训类型", example = "2")
    private Integer trainingType;
    @Schema(description = "培训层级", example = "1")
    private Integer trainingLevel;
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "培训开始时间-èµ·")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime startTimeStart;
    @Schema(description = "培训开始时间-æ­¢")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime startTimeEnd;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/training/MesSafetyTrainingRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,70 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.training;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - å®‰å…¨ç”Ÿäº§-培训 Response VO")
@Data
public class MesSafetyTrainingRespVO {
    @Schema(description = "编号", example = "1024")
    private Long id;
    @Schema(description = "培训编号", example = "PX-2026-001")
    private String code;
    @Schema(description = "培训主题")
    private String title;
    @Schema(description = "培训类型", example = "2")
    private Integer trainingType;
    @Schema(description = "培训层级", example = "1")
    private Integer trainingLevel;
    @Schema(description = "讲师/授课人")
    private String trainer;
    @Schema(description = "讲师单位")
    private String trainerOrg;
    @Schema(description = "培训地点")
    private String location;
    @Schema(description = "培训开始时间")
    private LocalDateTime startTime;
    @Schema(description = "培训结束时间")
    private LocalDateTime endTime;
    @Schema(description = "学时", example = "4")
    private BigDecimal durationHours;
    @Schema(description = "培训内容")
    private String content;
    @Schema(description = "培训资料/教材")
    private String material;
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "参训人数", example = "10")
    private Integer attendeeCount;
    @Schema(description = "实到人数", example = "9")
    private Integer signedCount;
    @Schema(description = "考试平均分", example = "85.5")
    private BigDecimal avgScore;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/training/MesSafetyTrainingSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.training;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - å®‰å…¨ç”Ÿäº§-培训新增/修改 Request VO")
@Data
public class MesSafetyTrainingSaveReqVO {
    @Schema(description = "编号", example = "1024")
    private Long id;
    @Schema(description = "培训编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "PX-2026-001")
    @NotEmpty(message = "培训编号不能为空")
    private String code;
    @Schema(description = "培训主题", requiredMode = Schema.RequiredMode.REQUIRED, example = "特种作业安全操作培训")
    @NotEmpty(message = "培训主题不能为空")
    private String title;
    @Schema(description = "培训类型", example = "2")
    private Integer trainingType;
    @Schema(description = "培训层级", example = "1")
    private Integer trainingLevel;
    @Schema(description = "讲师/授课人", example = "å¼ å·¥")
    private String trainer;
    @Schema(description = "讲师单位")
    private String trainerOrg;
    @Schema(description = "培训地点", example = "一楼培训室")
    private String location;
    @Schema(description = "培训开始时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime startTime;
    @Schema(description = "培训结束时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime endTime;
    @Schema(description = "学时", example = "4")
    private BigDecimal durationHours;
    @Schema(description = "培训内容")
    private String content;
    @Schema(description = "培训资料/教材")
    private String material;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    @NotNull(message = "状态不能为空")
    private Integer status;
    @Schema(description = "备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/MesSafetyCertificateDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.safety;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDate;
/**
 * å®‰å…¨ç”Ÿäº§ - å®‰å…¨èµ„格证书 DO
 *
 * ç‰¹ç§ä½œä¸šæ“ä½œè¯ã€å®‰å…¨ç®¡ç†äººå‘˜è¯ç­‰ï¼Œå«æœ‰æ•ˆæœŸç”¨äºŽåˆ°æœŸæé†’
 *
 * @author è¶…级管理员
 */
@TableName("mes_safety_certificate")
@KeySequence("mes_safety_certificate_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesSafetyCertificateDO extends BaseDO {
    /** çŠ¶æ€ï¼šæœ‰æ•ˆ */
    public static final Integer STATUS_VALID = 1;
    /** çŠ¶æ€ï¼šå·²ä½œåºŸ */
    public static final Integer STATUS_INVALID = 0;
    @TableId
    private Long id;
    /**
     * æŒè¯äººç¼–号
     */
    private Long userId;
    /**
     * æ‰€å±žéƒ¨é—¨ç¼–号
     */
    private Long deptId;
    /**
     * è¯ä¹¦ç±»åž‹
     *
     * å­—å…¸ mes_safety_cert_type:1特种作业操作证 2安全管理人员证 3主要负责人证 4其他资格证
     */
    private Integer certType;
    /**
     * è¯ä¹¦ç¼–号
     */
    private String certNo;
    /**
     * å‘证机关
     */
    private String issueOrg;
    /**
     * å‘证日期
     */
    private LocalDate issueDate;
    /**
     * æœ‰æ•ˆæœŸè‡³ï¼ˆä¸ºç©ºè¡¨ç¤ºé•¿æœŸæœ‰æ•ˆï¼‰
     */
    private LocalDate expireDate;
    /**
     * çŠ¶æ€ï¼š1有效 0已作废
     */
    private Integer status;
    /**
     * è¯ä¹¦æ‰«æä»¶åœ°å€
     */
    private String attachmentUrl;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/MesSafetyTrainingAttendeeDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.safety;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - åŸ¹è®­å‚训人员 DO
 *
 * å«ç­¾åˆ°æƒ…况与考试成绩
 *
 * @author è¶…级管理员
 */
@TableName("mes_safety_training_attendee")
@KeySequence("mes_safety_training_attendee_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesSafetyTrainingAttendeeDO extends BaseDO {
    /** ç­¾åˆ°çŠ¶æ€ï¼šæœªç­¾åˆ° */
    public static final Integer SIGN_NONE = 0;
    /** ç­¾åˆ°çŠ¶æ€ï¼šå·²ç­¾åˆ° */
    public static final Integer SIGN_DONE = 1;
    /** ç­¾åˆ°çŠ¶æ€ï¼šè¿Ÿåˆ° */
    public static final Integer SIGN_LATE = 2;
    /** ç­¾åˆ°çŠ¶æ€ï¼šç¼ºå¸­ */
    public static final Integer SIGN_ABSENT = 3;
    /** è€ƒè¯•结果:未考 */
    public static final Integer PASS_NONE = 0;
    /** è€ƒè¯•结果:通过 */
    public static final Integer PASS_YES = 1;
    /** è€ƒè¯•结果:不通过 */
    public static final Integer PASS_NO = 2;
    @TableId
    private Long id;
    /**
     * åŸ¹è®­æ‰¹æ¬¡ç¼–号
     */
    private Long trainingId;
    /**
     * å‚训人编号
     */
    private Long userId;
    /**
     * æ‰€å±žéƒ¨é—¨ç¼–号
     */
    private Long deptId;
    /**
     * ç­¾åˆ°çŠ¶æ€ï¼š0未签到 1已签到 2迟到 3缺席
     */
    private Integer signStatus;
    /**
     * ç­¾åˆ°æ—¶é—´
     */
    private LocalDateTime signTime;
    /**
     * è€ƒè¯•成绩
     */
    private BigDecimal score;
    /**
     * æ˜¯å¦é€šè¿‡ï¼š0未考 1通过 2不通过
     */
    private Integer passed;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/MesSafetyTrainingDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,128 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.safety;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - åŸ¹è®­æ‰¹æ¬¡ DO
 *
 * @author è¶…级管理员
 */
@TableName("mes_safety_training")
@KeySequence("mes_safety_training_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesSafetyTrainingDO extends BaseDO {
    /** çŠ¶æ€ï¼šè®¡åˆ’ä¸­ */
    public static final Integer STATUS_PLANNED = 0;
    /** çŠ¶æ€ï¼šè¿›è¡Œä¸­ */
    public static final Integer STATUS_ONGOING = 10;
    /** çŠ¶æ€ï¼šå·²å®Œæˆ */
    public static final Integer STATUS_FINISHED = 20;
    /** çŠ¶æ€ï¼šå·²å–æ¶ˆ */
    public static final Integer STATUS_CANCELED = 30;
    @TableId
    private Long id;
    /**
     * åŸ¹è®­ç¼–号
     */
    private String code;
    /**
     * åŸ¹è®­ä¸»é¢˜
     */
    private String title;
    /**
     * åŸ¹è®­ç±»åž‹
     *
     * å­—å…¸ mes_safety_training_type:1三级安全教育 2专项培训 3复训 4日常安全教育 5其他
     */
    private Integer trainingType;
    /**
     * åŸ¹è®­å±‚级
     *
     * å­—å…¸ mes_safety_training_level:1公司级 2车间级 3班组级
     */
    private Integer trainingLevel;
    /**
     * è®²å¸ˆ / æŽˆè¯¾äºº
     */
    private String trainer;
    /**
     * è®²å¸ˆå•位(外部讲师时填写)
     */
    private String trainerOrg;
    /**
     * åŸ¹è®­åœ°ç‚¹
     */
    private String location;
    /**
     * åŸ¹è®­å¼€å§‹æ—¶é—´
     */
    private LocalDateTime startTime;
    /**
     * åŸ¹è®­ç»“束时间
     */
    private LocalDateTime endTime;
    /**
     * å­¦æ—¶
     */
    private BigDecimal durationHours;
    /**
     * åŸ¹è®­å†…容
     */
    private String content;
    /**
     * åŸ¹è®­èµ„æ–™ / æ•™æ
     */
    private String material;
    /**
     * çŠ¶æ€ï¼š0计划中 10进行中 20已完成 30已取消
     */
    private Integer status;
    /**
     * å‚训人数(冗余统计)
     */
    private Integer attendeeCount;
    /**
     * å®žåˆ°äººæ•°ï¼ˆå†—余统计)
     */
    private Integer signedCount;
    /**
     * è€ƒè¯•平均分(冗余统计)
     */
    private BigDecimal avgScore;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/MesSafetyCertificateMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
package cn.iocoder.yudao.module.mes.dal.mysql.safety;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.certificate.MesSafetyCertificatePageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.MesSafetyCertificateDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDate;
import java.util.List;
/**
 * å®‰å…¨ç”Ÿäº§ - å®‰å…¨èµ„格证书 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesSafetyCertificateMapper extends BaseMapperX<MesSafetyCertificateDO> {
    default PageResult<MesSafetyCertificateDO> selectPage(MesSafetyCertificatePageReqVO reqVO) {
        LambdaQueryWrapperX<MesSafetyCertificateDO> wrapper = new LambdaQueryWrapperX<MesSafetyCertificateDO>()
                .eqIfPresent(MesSafetyCertificateDO::getUserId, reqVO.getUserId())
                .eqIfPresent(MesSafetyCertificateDO::getCertType, reqVO.getCertType())
                .likeIfPresent(MesSafetyCertificateDO::getCertNo, reqVO.getCertNo())
                .eqIfPresent(MesSafetyCertificateDO::getStatus, reqVO.getStatus());
        // åªçœ‹å³å°†åˆ°æœŸï¼ˆå«å·²è¿‡æœŸï¼‰ï¼šexpireDate åœ¨ä»Šå¤©+30 ä¹‹å‰
        if (Boolean.TRUE.equals(reqVO.getExpiringSoon())) {
            LocalDate today = LocalDate.now();
            wrapper.isNotNull(MesSafetyCertificateDO::getExpireDate)
                    .le(MesSafetyCertificateDO::getExpireDate, today.plusDays(30));
        }
        wrapper.orderByAsc(MesSafetyCertificateDO::getExpireDate)
                .orderByDesc(MesSafetyCertificateDO::getId);
        return selectPage(reqVO, wrapper);
    }
    default MesSafetyCertificateDO selectByCertNo(String certNo) {
        return selectOne(MesSafetyCertificateDO::getCertNo, certNo);
    }
    /**
     * ç»Ÿè®¡æœ‰æ•ˆä½†å·²è¿‡æœŸçš„证书数量
     */
    default Long selectCountExpired(LocalDate today) {
        return selectCount(new LambdaQueryWrapperX<MesSafetyCertificateDO>()
                .eq(MesSafetyCertificateDO::getStatus, MesSafetyCertificateDO.STATUS_VALID)
                .isNotNull(MesSafetyCertificateDO::getExpireDate)
                .lt(MesSafetyCertificateDO::getExpireDate, today));
    }
    default List<MesSafetyCertificateDO> selectAllList() {
        return selectList(new LambdaQueryWrapperX<MesSafetyCertificateDO>()
                .orderByAsc(MesSafetyCertificateDO::getExpireDate));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/MesSafetyTrainingAttendeeMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.mes.dal.mysql.safety;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.MesSafetyTrainingAttendeeDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * å®‰å…¨ç”Ÿäº§ - åŸ¹è®­å‚训人员 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesSafetyTrainingAttendeeMapper extends BaseMapperX<MesSafetyTrainingAttendeeDO> {
    default List<MesSafetyTrainingAttendeeDO> selectListByTrainingId(Long trainingId) {
        return selectList(new LambdaQueryWrapperX<MesSafetyTrainingAttendeeDO>()
                .eq(MesSafetyTrainingAttendeeDO::getTrainingId, trainingId)
                .orderByAsc(MesSafetyTrainingAttendeeDO::getId));
    }
    default MesSafetyTrainingAttendeeDO selectByTrainingIdAndUserId(Long trainingId, Long userId) {
        return selectOne(new LambdaQueryWrapperX<MesSafetyTrainingAttendeeDO>()
                .eq(MesSafetyTrainingAttendeeDO::getTrainingId, trainingId)
                .eq(MesSafetyTrainingAttendeeDO::getUserId, userId));
    }
    default void deleteByTrainingId(Long trainingId) {
        delete(new LambdaQueryWrapperX<MesSafetyTrainingAttendeeDO>()
                .eq(MesSafetyTrainingAttendeeDO::getTrainingId, trainingId));
    }
    default Long selectCountByTrainingId(Long trainingId) {
        return selectCount(new LambdaQueryWrapperX<MesSafetyTrainingAttendeeDO>()
                .eq(MesSafetyTrainingAttendeeDO::getTrainingId, trainingId));
    }
    default Long selectCountSignedByTrainingId(Long trainingId) {
        return selectCount(new LambdaQueryWrapperX<MesSafetyTrainingAttendeeDO>()
                .eq(MesSafetyTrainingAttendeeDO::getTrainingId, trainingId)
                .in(MesSafetyTrainingAttendeeDO::getSignStatus,
                        MesSafetyTrainingAttendeeDO.SIGN_DONE, MesSafetyTrainingAttendeeDO.SIGN_LATE));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/MesSafetyTrainingMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.mes.dal.mysql.safety;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.training.MesSafetyTrainingPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.MesSafetyTrainingDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * å®‰å…¨ç”Ÿäº§ - åŸ¹è®­æ‰¹æ¬¡ Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesSafetyTrainingMapper extends BaseMapperX<MesSafetyTrainingDO> {
    default PageResult<MesSafetyTrainingDO> selectPage(MesSafetyTrainingPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesSafetyTrainingDO>()
                .likeIfPresent(MesSafetyTrainingDO::getCode, reqVO.getCode())
                .likeIfPresent(MesSafetyTrainingDO::getTitle, reqVO.getTitle())
                .eqIfPresent(MesSafetyTrainingDO::getTrainingType, reqVO.getTrainingType())
                .eqIfPresent(MesSafetyTrainingDO::getTrainingLevel, reqVO.getTrainingLevel())
                .eqIfPresent(MesSafetyTrainingDO::getStatus, reqVO.getStatus())
                .geIfPresent(MesSafetyTrainingDO::getStartTime, reqVO.getStartTimeStart())
                .leIfPresent(MesSafetyTrainingDO::getStartTime, reqVO.getStartTimeEnd())
                .orderByDesc(MesSafetyTrainingDO::getId));
    }
    default MesSafetyTrainingDO selectByCode(String code) {
        return selectOne(MesSafetyTrainingDO::getCode, code);
    }
    default Long selectCountByStatus(Integer status) {
        return selectCount(new LambdaQueryWrapperX<MesSafetyTrainingDO>()
                .eq(MesSafetyTrainingDO::getStatus, status));
    }
    default List<MesSafetyTrainingDO> selectAllList() {
        return selectList(new LambdaQueryWrapperX<MesSafetyTrainingDO>()
                .orderByDesc(MesSafetyTrainingDO::getId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/DictTypeConstants.java
@@ -135,4 +135,13 @@
    String MES_SAFETY_TICKET_TYPE = "mes_safety_ticket_type";       // ä½œä¸šç±»åž‹
    String MES_SAFETY_TICKET_LEVEL = "mes_safety_ticket_level";     // ä½œä¸šçº§åˆ«
    String MES_SAFETY_TICKET_STATUS = "mes_safety_ticket_status";   // ä½œä¸šç¥¨çŠ¶æ€
    // å®‰å…¨æ•™è‚²åŸ¹è®­
    String MES_SAFETY_TRAINING_TYPE = "mes_safety_training_type";   // åŸ¹è®­ç±»åž‹
    String MES_SAFETY_TRAINING_LEVEL = "mes_safety_training_level"; // åŸ¹è®­å±‚级(三级教育)
    String MES_SAFETY_TRAINING_STATUS = "mes_safety_training_status"; // åŸ¹è®­çŠ¶æ€
    String MES_SAFETY_ATTENDEE_STATUS = "mes_safety_attendee_status"; // ç­¾åˆ°çŠ¶æ€
    // å®‰å…¨èµ„格证书
    String MES_SAFETY_CERT_TYPE = "mes_safety_cert_type";           // è¯ä¹¦ç±»åž‹
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -869,4 +869,15 @@
    ErrorCode MES_SAFETY_TICKET_REJECT_REASON_REQUIRED = new ErrorCode(1_040_906_004, "审批驳回时必须填写驳回原因");
    ErrorCode MES_SAFETY_TICKET_HAS_REFERENCE = new ErrorCode(1_040_906_005, "该记录已被作业票引用,无法删除");
    // ========== MES å®‰å…¨ç”Ÿäº§ - å®‰å…¨æ•™è‚²åŸ¹è®­ï¼ˆ1-040-907-000) ==========
    ErrorCode MES_SAFETY_TRAINING_NOT_EXISTS = new ErrorCode(1_040_907_000, "培训批次不存在");
    ErrorCode MES_SAFETY_TRAINING_CODE_DUPLICATE = new ErrorCode(1_040_907_001, "培训编号已存在");
    ErrorCode MES_SAFETY_TRAINING_TIME_INVALID = new ErrorCode(1_040_907_002, "培训结束时间必须晚于开始时间");
    ErrorCode MES_SAFETY_TRAINING_ATTENDEE_NOT_EXISTS = new ErrorCode(1_040_907_003, "参训人员记录不存在");
    // ========== MES å®‰å…¨ç”Ÿäº§ - å®‰å…¨èµ„格证书(1-040-908-000) ==========
    ErrorCode MES_SAFETY_CERTIFICATE_NOT_EXISTS = new ErrorCode(1_040_908_000, "资格证书不存在");
    ErrorCode MES_SAFETY_CERTIFICATE_NO_DUPLICATE = new ErrorCode(1_040_908_001, "证书编号已存在");
    ErrorCode MES_SAFETY_CERTIFICATE_DATE_INVALID = new ErrorCode(1_040_908_002, "有效期必须晚于发证日期");
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/MesSafetyCertificateService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.mes.service.safety;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.certificate.MesSafetyCertificatePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.certificate.MesSafetyCertificateSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.MesSafetyCertificateDO;
/**
 * å®‰å…¨ç”Ÿäº§ - èµ„格证书 Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface MesSafetyCertificateService {
    PageResult<MesSafetyCertificateDO> getCertificatePage(MesSafetyCertificatePageReqVO pageReqVO);
    MesSafetyCertificateDO getCertificate(Long id);
    Long createCertificate(MesSafetyCertificateSaveReqVO createReqVO);
    void updateCertificate(MesSafetyCertificateSaveReqVO updateReqVO);
    void deleteCertificate(Long id);
    MesSafetyCertificateDO validateCertificateExists(Long id);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/MesSafetyCertificateServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,99 @@
package cn.iocoder.yudao.module.mes.service.safety;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.certificate.MesSafetyCertificatePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.certificate.MesSafetyCertificateSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.MesSafetyCertificateDO;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.MesSafetyCertificateMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
/**
 * å®‰å…¨ç”Ÿäº§ - èµ„格证书 Service å®žçް
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class MesSafetyCertificateServiceImpl implements MesSafetyCertificateService {
    @Resource
    private MesSafetyCertificateMapper certificateMapper;
    @Override
    public PageResult<MesSafetyCertificateDO> getCertificatePage(MesSafetyCertificatePageReqVO pageReqVO) {
        return certificateMapper.selectPage(pageReqVO);
    }
    @Override
    public MesSafetyCertificateDO getCertificate(Long id) {
        return certificateMapper.selectById(id);
    }
    @Override
    public Long createCertificate(MesSafetyCertificateSaveReqVO createReqVO) {
        validateCertNoUnique(null, createReqVO.getCertNo());
        validateDateRange(createReqVO);
        MesSafetyCertificateDO certificate = BeanUtils.toBean(createReqVO, MesSafetyCertificateDO.class);
        if (certificate.getStatus() == null) {
            certificate.setStatus(MesSafetyCertificateDO.STATUS_VALID);
        }
        certificateMapper.insert(certificate);
        return certificate.getId();
    }
    @Override
    public void updateCertificate(MesSafetyCertificateSaveReqVO updateReqVO) {
        validateCertificateExists(updateReqVO.getId());
        validateCertNoUnique(updateReqVO.getId(), updateReqVO.getCertNo());
        validateDateRange(updateReqVO);
        MesSafetyCertificateDO updateObj = BeanUtils.toBean(updateReqVO, MesSafetyCertificateDO.class);
        certificateMapper.updateById(updateObj);
    }
    @Override
    public void deleteCertificate(Long id) {
        validateCertificateExists(id);
        certificateMapper.deleteById(id);
    }
    @Override
    public MesSafetyCertificateDO validateCertificateExists(Long id) {
        if (id == null) {
            throw exception(MES_SAFETY_CERTIFICATE_NOT_EXISTS);
        }
        MesSafetyCertificateDO certificate = certificateMapper.selectById(id);
        if (certificate == null) {
            throw exception(MES_SAFETY_CERTIFICATE_NOT_EXISTS);
        }
        return certificate;
    }
    /**
     * æœ‰æ•ˆæœŸå¿…须晚于发证日期
     */
    private void validateDateRange(MesSafetyCertificateSaveReqVO reqVO) {
        if (reqVO.getIssueDate() != null && reqVO.getExpireDate() != null
                && !reqVO.getExpireDate().isAfter(reqVO.getIssueDate())) {
            throw exception(MES_SAFETY_CERTIFICATE_DATE_INVALID);
        }
    }
    private void validateCertNoUnique(Long id, String certNo) {
        MesSafetyCertificateDO exists = certificateMapper.selectByCertNo(certNo);
        if (exists == null) {
            return;
        }
        if (id == null || !Objects.equals(exists.getId(), id)) {
            throw exception(MES_SAFETY_CERTIFICATE_NO_DUPLICATE);
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/MesSafetyTrainingService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.mes.service.safety;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.training.MesSafetyTrainingAttendeeSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.training.MesSafetyTrainingPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.training.MesSafetyTrainingSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.MesSafetyTrainingAttendeeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.MesSafetyTrainingDO;
import java.util.List;
/**
 * å®‰å…¨ç”Ÿäº§ - åŸ¹è®­ Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface MesSafetyTrainingService {
    PageResult<MesSafetyTrainingDO> getTrainingPage(MesSafetyTrainingPageReqVO pageReqVO);
    MesSafetyTrainingDO getTraining(Long id);
    Long createTraining(MesSafetyTrainingSaveReqVO createReqVO);
    void updateTraining(MesSafetyTrainingSaveReqVO updateReqVO);
    void deleteTraining(Long id);
    MesSafetyTrainingDO validateTrainingExists(Long id);
    /**
     * èŽ·å¾—æŒ‡å®šåŸ¹è®­çš„å‚è®­äººå‘˜åˆ—è¡¨
     */
    List<MesSafetyTrainingAttendeeDO> getAttendeeList(Long trainingId);
    /**
     * å…¨é‡ä¿å­˜å‚训人员(以传入列表为准覆盖)
     */
    void saveAttendees(Long trainingId, List<MesSafetyTrainingAttendeeSaveReqVO> attendees);
    /**
     * æ›´æ–°å•个参训人员的签到 / æˆç»©
     */
    void updateAttendee(MesSafetyTrainingAttendeeSaveReqVO reqVO);
    /**
     * åˆ é™¤å‚训人员
     */
    void deleteAttendee(Long id);
    /**
     * åˆ·æ–°åŸ¹è®­æ‰¹æ¬¡çš„冗余统计(参训人数 / å®žåˆ°äººæ•° / å¹³å‡åˆ†ï¼‰
     */
    void refreshStatistics(Long trainingId);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/MesSafetyTrainingServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,203 @@
package cn.iocoder.yudao.module.mes.service.safety;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.training.MesSafetyTrainingAttendeeSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.training.MesSafetyTrainingPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.training.MesSafetyTrainingSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.MesSafetyTrainingAttendeeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.MesSafetyTrainingDO;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.MesSafetyTrainingAttendeeMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.MesSafetyTrainingMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
/**
 * å®‰å…¨ç”Ÿäº§ - åŸ¹è®­ Service å®žçް
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class MesSafetyTrainingServiceImpl implements MesSafetyTrainingService {
    @Resource
    private MesSafetyTrainingMapper trainingMapper;
    @Resource
    private MesSafetyTrainingAttendeeMapper attendeeMapper;
    @Override
    public PageResult<MesSafetyTrainingDO> getTrainingPage(MesSafetyTrainingPageReqVO pageReqVO) {
        return trainingMapper.selectPage(pageReqVO);
    }
    @Override
    public MesSafetyTrainingDO getTraining(Long id) {
        return trainingMapper.selectById(id);
    }
    @Override
    public Long createTraining(MesSafetyTrainingSaveReqVO createReqVO) {
        validateCodeUnique(null, createReqVO.getCode());
        validateTimeRange(createReqVO);
        MesSafetyTrainingDO training = BeanUtils.toBean(createReqVO, MesSafetyTrainingDO.class);
        training.setAttendeeCount(0);
        training.setSignedCount(0);
        trainingMapper.insert(training);
        return training.getId();
    }
    @Override
    public void updateTraining(MesSafetyTrainingSaveReqVO updateReqVO) {
        validateTrainingExists(updateReqVO.getId());
        validateCodeUnique(updateReqVO.getId(), updateReqVO.getCode());
        validateTimeRange(updateReqVO);
        MesSafetyTrainingDO updateObj = BeanUtils.toBean(updateReqVO, MesSafetyTrainingDO.class);
        // ç»Ÿè®¡å­—段由参训人员维护,不通过此接口修改
        updateObj.setAttendeeCount(null);
        updateObj.setSignedCount(null);
        updateObj.setAvgScore(null);
        trainingMapper.updateById(updateObj);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteTraining(Long id) {
        validateTrainingExists(id);
        // çº§è”删除参训记录,避免孤儿数据
        attendeeMapper.deleteByTrainingId(id);
        trainingMapper.deleteById(id);
    }
    @Override
    public MesSafetyTrainingDO validateTrainingExists(Long id) {
        if (id == null) {
            throw exception(MES_SAFETY_TRAINING_NOT_EXISTS);
        }
        MesSafetyTrainingDO training = trainingMapper.selectById(id);
        if (training == null) {
            throw exception(MES_SAFETY_TRAINING_NOT_EXISTS);
        }
        return training;
    }
    @Override
    public List<MesSafetyTrainingAttendeeDO> getAttendeeList(Long trainingId) {
        return attendeeMapper.selectListByTrainingId(trainingId);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveAttendees(Long trainingId, List<MesSafetyTrainingAttendeeSaveReqVO> attendees) {
        validateTrainingExists(trainingId);
        // å…¨é‡è¦†ç›–:先清空再插入,保证与前端列表一致
        attendeeMapper.deleteByTrainingId(trainingId);
        if (attendees != null && !attendees.isEmpty()) {
            List<MesSafetyTrainingAttendeeDO> list = new ArrayList<>(attendees.size());
            for (MesSafetyTrainingAttendeeSaveReqVO vo : attendees) {
                MesSafetyTrainingAttendeeDO attendee = BeanUtils.toBean(vo, MesSafetyTrainingAttendeeDO.class);
                attendee.setId(null);
                attendee.setTrainingId(trainingId);
                if (attendee.getSignStatus() == null) {
                    attendee.setSignStatus(MesSafetyTrainingAttendeeDO.SIGN_NONE);
                }
                if (attendee.getPassed() == null) {
                    attendee.setPassed(MesSafetyTrainingAttendeeDO.PASS_NONE);
                }
                list.add(attendee);
            }
            attendeeMapper.insertBatch(list);
        }
        refreshStatistics(trainingId);
    }
    @Override
    public void updateAttendee(MesSafetyTrainingAttendeeSaveReqVO reqVO) {
        if (reqVO.getId() == null) {
            throw exception(MES_SAFETY_TRAINING_ATTENDEE_NOT_EXISTS);
        }
        MesSafetyTrainingAttendeeDO exists = attendeeMapper.selectById(reqVO.getId());
        if (exists == null) {
            throw exception(MES_SAFETY_TRAINING_ATTENDEE_NOT_EXISTS);
        }
        MesSafetyTrainingAttendeeDO updateObj = new MesSafetyTrainingAttendeeDO();
        updateObj.setId(reqVO.getId());
        updateObj.setSignStatus(reqVO.getSignStatus());
        updateObj.setSignTime(reqVO.getSignTime());
        updateObj.setScore(reqVO.getScore());
        updateObj.setPassed(reqVO.getPassed());
        updateObj.setRemark(reqVO.getRemark());
        attendeeMapper.updateById(updateObj);
        refreshStatistics(exists.getTrainingId());
    }
    @Override
    public void deleteAttendee(Long id) {
        MesSafetyTrainingAttendeeDO exists = attendeeMapper.selectById(id);
        if (exists == null) {
            throw exception(MES_SAFETY_TRAINING_ATTENDEE_NOT_EXISTS);
        }
        attendeeMapper.deleteById(id);
        refreshStatistics(exists.getTrainingId());
    }
    @Override
    public void refreshStatistics(Long trainingId) {
        Long total = attendeeMapper.selectCountByTrainingId(trainingId);
        Long signed = attendeeMapper.selectCountSignedByTrainingId(trainingId);
        List<MesSafetyTrainingAttendeeDO> list = attendeeMapper.selectListByTrainingId(trainingId);
        // å¹³å‡åˆ†åªç»Ÿè®¡æœ‰æˆç»©çš„
        BigDecimal sum = BigDecimal.ZERO;
        int scored = 0;
        for (MesSafetyTrainingAttendeeDO a : list) {
            if (a.getScore() != null) {
                sum = sum.add(a.getScore());
                scored++;
            }
        }
        BigDecimal avg = scored > 0
                ? sum.divide(BigDecimal.valueOf(scored), 2, RoundingMode.HALF_UP)
                : null;
        MesSafetyTrainingDO updateObj = new MesSafetyTrainingDO();
        updateObj.setId(trainingId);
        updateObj.setAttendeeCount(total == null ? 0 : total.intValue());
        updateObj.setSignedCount(signed == null ? 0 : signed.intValue());
        updateObj.setAvgScore(avg);
        trainingMapper.updateById(updateObj);
    }
    /**
     * æ ¡éªŒåŸ¹è®­æ—¶é—´åŒºé—´
     */
    private void validateTimeRange(MesSafetyTrainingSaveReqVO reqVO) {
        LocalDateTime start = reqVO.getStartTime();
        LocalDateTime end = reqVO.getEndTime();
        if (start != null && end != null && !end.isAfter(start)) {
            throw exception(MES_SAFETY_TRAINING_TIME_INVALID);
        }
    }
    private void validateCodeUnique(Long id, String code) {
        MesSafetyTrainingDO exists = trainingMapper.selectByCode(code);
        if (exists == null) {
            return;
        }
        if (id == null || !Objects.equals(exists.getId(), id)) {
            throw exception(MES_SAFETY_TRAINING_CODE_DUPLICATE);
        }
    }
}