tuqin
6 天以前 5ea3ccadce91cb9e7a760da8d0d34b316a977d78
feat(safety): 安全生产四期 - 特种作业票证管理

高风险作业必须先办票、审批通过后才可作业,完工后登记并归档,形成可追溯的作业许可记录。

状态流转:
0 待审批 --审批通过--> 10 已批准 --完工登记--> 20 已完工 --归档--> 30 已归档
└--审批驳回--> 90 已驳回

作业类型:动火 / 受限空间 / 高处 / 吊装 / 临时用电 / 断路 / 盲板抽堵 / 设备检修
作业级别:特殊 / 一级 / 二级 / 三级

后端:
- 作业票 MesSafetyWorkTicket:作业信息、计划/实际时间、申请人、监护人、
作业人员、施工单位、安全措施、安全交底、审批与归档信息
- 关键校验:
* 计划结束时间必须晚于开始时间
* 审批通过后不允许再改作业信息(需作废重开),已归档只读
* 只有「待审批」可审批;驳回必须填原因
* 只有「已批准」可完工、「已完工」可归档
* 已批准及之后的作业票不允许删除(保留追溯记录)
* 被驳回的作业票重新提交时自动回到待审批并清空驳回原因
- 超期判定:已批准但计划结束时间已过
- 反向引用保护:风险单元被作业票引用后禁止删除
- 错误码 1_040_906_xxx;字典常量 MES_SAFETY_TICKET_*

前端:
- 作业票列表:类型/级别字典渲染、计划时间超期红色标记、按状态动态显示可用操作
- 申请表单(含安全措施、监护人、作业人员)
- 审批弹窗(批准/驳回,驳回必填原因)
- 完工登记弹窗(可勾选「完工后直接归档」),已完工时自动切换为归档模式

SQL(mes_safety_ticket_init.sql,可重复执行):
- 建表 1 张、菜单挂到「安全生产」目录、字典 3 组、7 个按钮权限

注:本期只提交代码,未部署。
已添加11个文件
已修改3个文件
1137 ■■■■■ 文件已修改
sql/mysql/mes_safety_ticket_init.sql 150 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/MesSafetyWorkTicketController.java 179 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/ticket/MesSafetyWorkTicketApproveReqVO.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/ticket/MesSafetyWorkTicketCloseReqVO.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/ticket/MesSafetyWorkTicketPageReqVO.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/ticket/MesSafetyWorkTicketRespVO.java 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/ticket/MesSafetyWorkTicketSaveReqVO.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/MesSafetyWorkTicketDO.java 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/MesSafetyWorkTicketMapper.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/DictTypeConstants.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/MesSafetyRiskUnitServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/MesSafetyWorkTicketService.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/MesSafetyWorkTicketServiceImpl.java 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_safety_ticket_init.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,150 @@
-- =====================================================================
-- å®‰å…¨ç”Ÿäº§æ¨¡å— â€”— å››æœŸï¼šç‰¹ç§ä½œä¸šç¥¨è¯ç®¡ç†
--
-- å®šä½ï¼šé«˜é£Žé™©ä½œä¸šå¿…须先办票、审批通过后才可作业,作业期间需监护人确认,
--       å®Œå·¥åŽç™»è®°å¹¶å½’档,形成可追溯的作业许可记录。
--
-- æ”¯æŒä½œä¸šç±»åž‹ï¼ˆå¯æŒ‰å®¢æˆ·å®žé™…增减字典即可):
--   1 åŠ¨ç«ä½œä¸š / 2 å—限空间作业 / 3 é«˜å¤„作业 / 4 åŠè£…作业
--   5 ä¸´æ—¶ç”¨ç”µä½œä¸š / 6 æ–­è·¯ä½œä¸š / 7 ç›²æ¿æŠ½å µä½œä¸š / 8 è®¾å¤‡æ£€ä¿®ä½œä¸š
--
-- çŠ¶æ€æµè½¬ï¼š
--   0 å¾…审批 --审批通过--> 10 å·²æ‰¹å‡† --完工登记--> 20 å·²å®Œå·¥ --归档--> 30 å·²å½’æ¡£
--      â”‚
--      â””--审批驳回--> 90 å·²é©³å›ž
--
-- ä½¿ç”¨çº¦æŸï¼ˆæœåŠ¡ç«¯å¼ºæ ¡éªŒï¼‰ï¼š
--   * ä½œä¸šç»“束时间必须晚于开始时间
--   * å®¡æ‰¹é€šè¿‡åŽä¸å…è®¸å†æ”¹ä½œä¸šä¿¡æ¯ï¼ˆéœ€ä½œåºŸé‡å¼€ï¼‰
--   * å·²å½’档的作业票只读
--
-- å†…容:
--   1. å»ºè¡¨ mes_safety_work_ticket
--   2. å­—典:作业类型 / ä½œä¸šçº§åˆ« / ä½œä¸šç¥¨çŠ¶æ€
--   3. èœå•挂在「安全生产」目录下
--   4. æŽˆæƒè¶…级管理员
--
-- ç‰¹æ€§ï¼šå¯é‡å¤æ‰§è¡Œ
-- =====================================================================
CREATE TABLE IF NOT EXISTS `mes_safety_work_ticket` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
  `code` varchar(64) NOT NULL COMMENT '作业票号',
  `ticket_type` tinyint NOT NULL COMMENT '作业类型(字典 mes_safety_ticket_type)',
  `ticket_level` tinyint DEFAULT NULL COMMENT '作业级别(字典 mes_safety_ticket_level)',
  `unit_id` bigint DEFAULT NULL COMMENT '关联风险单元编号',
  `workshop_id` bigint DEFAULT NULL COMMENT '所属车间编号',
  `work_location` varchar(255) DEFAULT NULL COMMENT '作业地点',
  `work_content` varchar(1000) DEFAULT NULL COMMENT '作业内容',
  `plan_start_time` datetime DEFAULT NULL COMMENT '计划开始时间',
  `plan_end_time` datetime DEFAULT NULL COMMENT '计划结束时间',
  `actual_start_time` datetime DEFAULT NULL COMMENT '实际开始时间',
  `actual_end_time` datetime DEFAULT NULL COMMENT '实际结束时间',
  `applicant_user_id` bigint DEFAULT NULL COMMENT '申请人编号',
  `applicant_dept_id` bigint DEFAULT NULL COMMENT '申请部门编号',
  `contractor` varchar(200) DEFAULT NULL COMMENT '施工单位/承包商',
  `worker_names` varchar(500) DEFAULT NULL COMMENT '作业人员(多个用逗号分隔)',
  `guardian_user_id` bigint DEFAULT NULL COMMENT '监护人编号',
  `guardian_confirm` bit(1) DEFAULT b'0' COMMENT '监护人是否已确认',
  `safety_measures` varchar(2000) DEFAULT NULL COMMENT '安全措施',
  `safety_check_result` varchar(1000) DEFAULT NULL COMMENT '安全交底/检查结果',
  `approver_user_id` bigint DEFAULT NULL COMMENT '审批人编号',
  `approve_time` datetime DEFAULT NULL COMMENT '审批时间',
  `approve_comment` varchar(500) DEFAULT NULL COMMENT '审批意见',
  `reject_reason` varchar(500) DEFAULT NULL COMMENT '驳回原因',
  `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0待审批 10已批准 20已完工 30已归档 90已驳回',
  `close_time` datetime DEFAULT NULL COMMENT '归档时间',
  `close_user_id` bigint DEFAULT NULL COMMENT '归档人编号',
  `close_remark` 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_code` (`code`) USING BTREE,
  KEY `idx_status` (`status`) USING BTREE,
  KEY `idx_type` (`ticket_type`) USING BTREE,
  KEY `idx_plan_start` (`plan_start_time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='安全生产-特种作业票证';
-- ---------------------------------------------------------------------
-- å­—å…¸
-- ---------------------------------------------------------------------
DELETE FROM system_dict_data WHERE dict_type IN
  ('mes_safety_ticket_type', 'mes_safety_ticket_level', 'mes_safety_ticket_status');
DELETE FROM system_dict_type WHERE type IN
  ('mes_safety_ticket_type', 'mes_safety_ticket_level', 'mes_safety_ticket_status');
INSERT INTO system_dict_type (name, type, status, remark, creator, create_time, updater, update_time, deleted)
VALUES ('安全生产-作业类型', 'mes_safety_ticket_type', 0, '特种作业票证类型', '1', NOW(), '1', NOW(), b'0'),
       ('安全生产-作业级别', 'mes_safety_ticket_level', 0, '动火/高处等按风险分级的作业级别', '1', NOW(), '1', NOW(), b'0'),
       ('安全生产-作业票状态', 'mes_safety_ticket_status', 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_ticket_type', 0, 'error',      '1', NOW(), '1', NOW(), b'0'),
(2, '受限空间作业', '2', 'mes_safety_ticket_type', 0, 'warning',    '1', NOW(), '1', NOW(), b'0'),
(3, '高处作业',     '3', 'mes_safety_ticket_type', 0, 'warning',    '1', NOW(), '1', NOW(), b'0'),
(4, '吊装作业',     '4', 'mes_safety_ticket_type', 0, 'processing', '1', NOW(), '1', NOW(), b'0'),
(5, '临时用电作业', '5', 'mes_safety_ticket_type', 0, 'processing', '1', NOW(), '1', NOW(), b'0'),
(6, '断路作业',     '6', 'mes_safety_ticket_type', 0, 'default',    '1', NOW(), '1', NOW(), b'0'),
(7, '盲板抽堵作业', '7', 'mes_safety_ticket_type', 0, 'warning',    '1', NOW(), '1', NOW(), b'0'),
(8, '设备检修作业', '8', 'mes_safety_ticket_type', 0, 'default',    '1', NOW(), '1', NOW(), b'0'),
-- ä½œä¸šçº§åˆ«
(1, '特殊作业', '1', 'mes_safety_ticket_level', 0, 'error',      '1', NOW(), '1', NOW(), b'0'),
(2, '一级作业', '2', 'mes_safety_ticket_level', 0, 'warning',    '1', NOW(), '1', NOW(), b'0'),
(3, '二级作业', '3', 'mes_safety_ticket_level', 0, 'processing', '1', NOW(), '1', NOW(), b'0'),
(4, '三级作业', '4', 'mes_safety_ticket_level', 0, 'success',    '1', NOW(), '1', NOW(), b'0'),
-- ä½œä¸šç¥¨çŠ¶æ€
(1, '待审批', '0',  'mes_safety_ticket_status', 0, 'processing', '1', NOW(), '1', NOW(), b'0'),
(2, '已批准', '10', 'mes_safety_ticket_status', 0, 'success',    '1', NOW(), '1', NOW(), b'0'),
(3, '已完工', '20', 'mes_safety_ticket_status', 0, 'default',    '1', NOW(), '1', NOW(), b'0'),
(4, '已归档', '30', 'mes_safety_ticket_status', 0, 'default',    '1', NOW(), '1', NOW(), b'0'),
(5, '已驳回', '90', 'mes_safety_ticket_status', 0, 'error',      '1', NOW(), '1', NOW(), b'0');
-- ---------------------------------------------------------------------
-- èœå•(挂在 1065910 å®‰å…¨ç”Ÿäº§ä¸‹ï¼‰
-- ---------------------------------------------------------------------
DELETE FROM system_role_menu WHERE menu_id BETWEEN 1065934 AND 1065950;
DELETE FROM system_menu WHERE id BETWEEN 1065934 AND 1065950;
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
(1065934, '特种作业票证', '', 2, 5, 1065910, 'work-ticket', 'ep:tickets',
 'safety/ticket/index', 'SafetyWorkTicket', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065935, '作业票查询', 'mes:safety-ticket:query',   3, 1, 1065934, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065936, '作业票申请', 'mes:safety-ticket:create',  3, 2, 1065934, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065937, '作业票修改', 'mes:safety-ticket:update',  3, 3, 1065934, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065938, '作业票删除', 'mes:safety-ticket:delete',  3, 4, 1065934, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065939, '作业票审批', 'mes:safety-ticket:approve', 3, 5, 1065934, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065940, '完工归档',   'mes:safety-ticket:close',   3, 6, 1065934, '', '', 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 1065934 AND 1065950;
-- ---------------------------------------------------------------------
-- éªŒè¯
-- ---------------------------------------------------------------------
SELECT id, parent_id, name, path, component, permission, type
FROM system_menu WHERE id BETWEEN 1065910 AND 1065950 ORDER BY id;
SELECT dict_type, label, value FROM system_dict_data
WHERE dict_type LIKE 'mes_safety_ticket%' ORDER BY dict_type, sort;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/MesSafetyWorkTicketController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,179 @@
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.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ticket.MesSafetyWorkTicketApproveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ticket.MesSafetyWorkTicketCloseReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ticket.MesSafetyWorkTicketPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ticket.MesSafetyWorkTicketRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ticket.MesSafetyWorkTicketSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkshopDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.MesSafetyRiskUnitDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.MesSafetyWorkTicketDO;
import cn.iocoder.yudao.module.mes.dal.mysql.md.workstation.MesMdWorkshopMapper;
import cn.iocoder.yudao.module.mes.service.safety.MesSafetyRiskUnitService;
import cn.iocoder.yudao.module.mes.service.safety.MesSafetyWorkTicketService;
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.LocalDateTime;
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/work-ticket")
@Validated
public class MesSafetyWorkTicketController {
    @Resource
    private MesSafetyWorkTicketService ticketService;
    @Resource
    private MesSafetyRiskUnitService riskUnitService;
    @Resource
    private MesMdWorkshopMapper workshopMapper;
    @Resource
    private AdminUserApi adminUserApi;
    @PostMapping("/create")
    @Operation(summary = "申请作业票")
    @PreAuthorize("@ss.hasPermission('mes:safety-ticket:create')")
    public CommonResult<Long> createTicket(@Valid @RequestBody MesSafetyWorkTicketSaveReqVO createReqVO) {
        return success(ticketService.createTicket(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "修改作业票")
    @PreAuthorize("@ss.hasPermission('mes:safety-ticket:update')")
    public CommonResult<Boolean> updateTicket(@Valid @RequestBody MesSafetyWorkTicketSaveReqVO updateReqVO) {
        ticketService.updateTicket(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除作业票")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:safety-ticket:delete')")
    public CommonResult<Boolean> deleteTicket(@RequestParam("id") Long id) {
        ticketService.deleteTicket(id);
        return success(true);
    }
    @PutMapping("/approve")
    @Operation(summary = "审批作业票")
    @PreAuthorize("@ss.hasPermission('mes:safety-ticket:approve')")
    public CommonResult<Boolean> approveTicket(@Valid @RequestBody MesSafetyWorkTicketApproveReqVO reqVO) {
        ticketService.approveTicket(SecurityFrameworkUtils.getLoginUserId(), reqVO);
        return success(true);
    }
    @PutMapping("/close")
    @Operation(summary = "完工登记 / å½’æ¡£")
    @PreAuthorize("@ss.hasPermission('mes:safety-ticket:close')")
    public CommonResult<Boolean> closeTicket(@Valid @RequestBody MesSafetyWorkTicketCloseReqVO reqVO) {
        ticketService.closeTicket(SecurityFrameworkUtils.getLoginUserId(), reqVO);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得作业票")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('mes:safety-ticket:query')")
    public CommonResult<MesSafetyWorkTicketRespVO> getTicket(@RequestParam("id") Long id) {
        MesSafetyWorkTicketDO ticket = ticketService.getTicket(id);
        if (ticket == null) {
            return success(null);
        }
        List<MesSafetyWorkTicketRespVO> list = buildRespList(Collections.singletonList(ticket));
        return success(list.isEmpty() ? null : list.get(0));
    }
    @GetMapping("/page")
    @Operation(summary = "获得作业票分页")
    @PreAuthorize("@ss.hasPermission('mes:safety-ticket:query')")
    public CommonResult<PageResult<MesSafetyWorkTicketRespVO>> getTicketPage(@Valid MesSafetyWorkTicketPageReqVO pageReqVO) {
        PageResult<MesSafetyWorkTicketDO> pageResult = ticketService.getTicketPage(pageReqVO);
        List<MesSafetyWorkTicketRespVO> list = buildRespList(pageResult.getList());
        return success(new PageResult<>(list, pageResult.getTotal()));
    }
    /**
     * è½¬æ¢ä¸º VO å¹¶å¡«å……车间、人员、风险单元名称、超期标记
     */
    private List<MesSafetyWorkTicketRespVO> buildRespList(List<MesSafetyWorkTicketDO> list) {
        if (list == null || list.isEmpty()) {
            return Collections.emptyList();
        }
        List<MesSafetyWorkTicketRespVO> respList = BeanUtils.toBean(list, MesSafetyWorkTicketRespVO.class);
        // è½¦é—´åç§°
        Set<Long> workshopIds = list.stream().map(MesSafetyWorkTicketDO::getWorkshopId)
                .filter(Objects::nonNull).collect(Collectors.toSet());
        Map<Long, String> workshopNameMap = new HashMap<>();
        if (!workshopIds.isEmpty()) {
            List<MesMdWorkshopDO> workshops = workshopMapper.selectByIds(workshopIds);
            workshops.forEach(w -> workshopNameMap.put(w.getId(), w.getName()));
        }
        // äººå‘˜å§“名
        Set<Long> userIds = new HashSet<>();
        list.forEach(t -> {
            if (t.getApplicantUserId() != null) userIds.add(t.getApplicantUserId());
            if (t.getGuardianUserId() != null) userIds.add(t.getGuardianUserId());
            if (t.getApproverUserId() != null) userIds.add(t.getApproverUserId());
        });
        Map<Long, AdminUserRespDTO> userMap = userIds.isEmpty()
                ? Collections.emptyMap() : adminUserApi.getUserMap(userIds);
        // é£Žé™©å•元名称
        Set<Long> unitIds = list.stream().map(MesSafetyWorkTicketDO::getUnitId)
                .filter(Objects::nonNull).collect(Collectors.toSet());
        Map<Long, MesSafetyRiskUnitDO> unitMap = riskUnitService.getRiskUnitMap(unitIds);
        LocalDateTime now = LocalDateTime.now();
        for (MesSafetyWorkTicketRespVO vo : respList) {
            vo.setWorkshopName(workshopNameMap.get(vo.getWorkshopId()));
            vo.setApplicantUserName(nickname(userMap, vo.getApplicantUserId()));
            vo.setGuardianUserName(nickname(userMap, vo.getGuardianUserId()));
            vo.setApproverUserName(nickname(userMap, vo.getApproverUserId()));
            MesSafetyRiskUnitDO unit = unitMap.get(vo.getUnitId());
            if (unit != null) {
                vo.setUnitName(unit.getName());
            }
            // è¶…期:已批准但计划结束时间已过
            vo.setOverdue(MesSafetyWorkTicketDO.STATUS_APPROVED.equals(vo.getStatus())
                    && vo.getPlanEndTime() != null
                    && vo.getPlanEndTime().isBefore(now));
        }
        return respList;
    }
    private String nickname(Map<Long, AdminUserRespDTO> userMap, Long userId) {
        if (userId == null) {
            return null;
        }
        AdminUserRespDTO user = userMap.get(userId);
        return user != null ? user.getNickname() : null;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/ticket/MesSafetyWorkTicketApproveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ticket;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - å®‰å…¨ç”Ÿäº§-作业票审批 Request VO")
@Data
public class MesSafetyWorkTicketApproveReqVO {
    @Schema(description = "作业票编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @NotNull(message = "作业票编号不能为空")
    private Long id;
    @Schema(description = "是否通过", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
    @NotNull(message = "审批结论不能为空")
    private Boolean pass;
    @Schema(description = "审批意见", example = "安全措施到位,同意作业")
    private String approveComment;
    @Schema(description = "驳回原因(不通过时必填)", example = "安全措施不完善,缺少气体检测")
    private String rejectReason;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/ticket/MesSafetyWorkTicketCloseReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ticket;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
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
public class MesSafetyWorkTicketCloseReqVO {
    @Schema(description = "作业票编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @NotNull(message = "作业票编号不能为空")
    private Long id;
    @Schema(description = "是否直接归档:false=仅登记完工(已批准→已完工);true=完工并归档(→已归档)",
            example = "false")
    private Boolean archive;
    @Schema(description = "实际开始时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime actualStartTime;
    @Schema(description = "实际结束时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime actualEndTime;
    @Schema(description = "完工 / å½’档说明", example = "作业完成,现场已清理")
    private String closeRemark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/ticket/MesSafetyWorkTicketPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ticket;
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 MesSafetyWorkTicketPageReqVO extends PageParam {
    @Schema(description = "作业票号", example = "ZY-2026-001")
    private String code;
    @Schema(description = "作业类型", example = "1")
    private Integer ticketType;
    @Schema(description = "作业级别", example = "2")
    private Integer ticketLevel;
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "所属车间编号", example = "7")
    private Long workshopId;
    @Schema(description = "申请人编号", example = "1")
    private Long applicantUserId;
    @Schema(description = "作业地点", example = "造纸车间")
    private String workLocation;
    @Schema(description = "计划开始时间-èµ·")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime planStartTimeStart;
    @Schema(description = "计划开始时间-æ­¢")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime planStartTimeEnd;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/ticket/MesSafetyWorkTicketRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,120 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ticket;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - å®‰å…¨ç”Ÿäº§-作业票 Response VO")
@Data
public class MesSafetyWorkTicketRespVO {
    @Schema(description = "编号", example = "1024")
    private Long id;
    @Schema(description = "作业票号", example = "ZY-2026-001")
    private String code;
    @Schema(description = "作业类型", example = "1")
    private Integer ticketType;
    @Schema(description = "作业级别", example = "2")
    private Integer ticketLevel;
    @Schema(description = "关联风险单元编号", example = "1")
    private Long unitId;
    @Schema(description = "关联风险单元名称")
    private String unitName;
    @Schema(description = "所属车间编号", example = "7")
    private Long workshopId;
    @Schema(description = "所属车间名称")
    private String workshopName;
    @Schema(description = "作业地点")
    private String workLocation;
    @Schema(description = "作业内容")
    private String workContent;
    @Schema(description = "计划开始时间")
    private LocalDateTime planStartTime;
    @Schema(description = "计划结束时间")
    private LocalDateTime planEndTime;
    @Schema(description = "实际开始时间")
    private LocalDateTime actualStartTime;
    @Schema(description = "实际结束时间")
    private LocalDateTime actualEndTime;
    @Schema(description = "申请人编号", example = "1")
    private Long applicantUserId;
    @Schema(description = "申请人姓名")
    private String applicantUserName;
    @Schema(description = "申请部门编号", example = "120")
    private Long applicantDeptId;
    @Schema(description = "施工单位 / æ‰¿åŒ…商")
    private String contractor;
    @Schema(description = "作业人员")
    private String workerNames;
    @Schema(description = "监护人编号", example = "1")
    private Long guardianUserId;
    @Schema(description = "监护人姓名")
    private String guardianUserName;
    @Schema(description = "监护人是否已确认")
    private Boolean guardianConfirm;
    @Schema(description = "安全措施")
    private String safetyMeasures;
    @Schema(description = "安全交底 / æ£€æŸ¥ç»“æžœ")
    private String safetyCheckResult;
    @Schema(description = "审批人编号", example = "1")
    private Long approverUserId;
    @Schema(description = "审批人姓名")
    private String approverUserName;
    @Schema(description = "审批时间")
    private LocalDateTime approveTime;
    @Schema(description = "审批意见")
    private String approveComment;
    @Schema(description = "驳回原因")
    private String rejectReason;
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "是否已超过计划结束时间仍未完工", example = "false")
    private Boolean overdue;
    @Schema(description = "归档时间")
    private LocalDateTime closeTime;
    @Schema(description = "归档人编号", example = "1")
    private Long closeUserId;
    @Schema(description = "完工 / å½’档说明")
    private String closeRemark;
    @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/ticket/MesSafetyWorkTicketSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ticket;
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.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 MesSafetyWorkTicketSaveReqVO {
    @Schema(description = "编号", example = "1024")
    private Long id;
    @Schema(description = "作业票号", requiredMode = Schema.RequiredMode.REQUIRED, example = "ZY-2026-001")
    @NotEmpty(message = "作业票号不能为空")
    private String code;
    @Schema(description = "作业类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "作业类型不能为空")
    private Integer ticketType;
    @Schema(description = "作业级别", example = "2")
    private Integer ticketLevel;
    @Schema(description = "关联风险单元编号", example = "1")
    private Long unitId;
    @Schema(description = "所属车间编号", example = "7")
    private Long workshopId;
    @Schema(description = "作业地点", example = "造纸车间东侧管道")
    private String workLocation;
    @Schema(description = "作业内容", example = "管道焊接")
    private String workContent;
    @Schema(description = "计划开始时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime planStartTime;
    @Schema(description = "计划结束时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime planEndTime;
    @Schema(description = "申请部门编号", example = "120")
    private Long applicantDeptId;
    @Schema(description = "施工单位 / æ‰¿åŒ…商", example = "XX å®‰è£…公司")
    private String contractor;
    @Schema(description = "作业人员(多个用逗号分隔)", example = "张三,李四")
    private String workerNames;
    @Schema(description = "监护人编号", example = "1")
    private Long guardianUserId;
    @Schema(description = "安全措施", example = "1.清理动火点周边可燃物;2.配备灭火器")
    private String safetyMeasures;
    @Schema(description = "安全交底 / æ£€æŸ¥ç»“æžœ")
    private String safetyCheckResult;
    @Schema(description = "备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/MesSafetyWorkTicketDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,188 @@
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.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰¹ç§ä½œä¸šç¥¨è¯ DO
 *
 * çŠ¶æ€æµè½¬ï¼š
 * 0 å¾…审批 --审批通过--> 10 å·²æ‰¹å‡† --完工登记--> 20 å·²å®Œå·¥ --归档--> 30 å·²å½’æ¡£
 *    â””--审批驳回--> 90 å·²é©³å›ž
 *
 * @author è¶…级管理员
 */
@TableName("mes_safety_work_ticket")
@KeySequence("mes_safety_work_ticket_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesSafetyWorkTicketDO extends BaseDO {
    /** çŠ¶æ€ï¼šå¾…å®¡æ‰¹ */
    public static final Integer STATUS_PENDING = 0;
    /** çŠ¶æ€ï¼šå·²æ‰¹å‡†ï¼ˆå¯ä½œä¸šï¼‰ */
    public static final Integer STATUS_APPROVED = 10;
    /** çŠ¶æ€ï¼šå·²å®Œå·¥ */
    public static final Integer STATUS_FINISHED = 20;
    /** çŠ¶æ€ï¼šå·²å½’æ¡£ */
    public static final Integer STATUS_ARCHIVED = 30;
    /** çŠ¶æ€ï¼šå·²é©³å›ž */
    public static final Integer STATUS_REJECTED = 90;
    @TableId
    private Long id;
    /**
     * ä½œä¸šç¥¨å·
     */
    private String code;
    /**
     * ä½œä¸šç±»åž‹
     *
     * å­—å…¸ mes_safety_ticket_type:1动火 2受限空间 3高处 4吊装 5临时用电 6断路 7盲板抽堵 8设备检修
     */
    private Integer ticketType;
    /**
     * ä½œä¸šçº§åˆ«
     *
     * å­—å…¸ mes_safety_ticket_level:1特殊 2一级 3二级 4三级
     */
    private Integer ticketLevel;
    /**
     * å…³è”风险单元编号
     */
    private Long unitId;
    /**
     * æ‰€å±žè½¦é—´ç¼–号
     */
    private Long workshopId;
    /**
     * ä½œä¸šåœ°ç‚¹
     */
    private String workLocation;
    /**
     * ä½œä¸šå†…容
     */
    private String workContent;
    /**
     * è®¡åˆ’开始时间
     */
    private LocalDateTime planStartTime;
    /**
     * è®¡åˆ’结束时间
     */
    private LocalDateTime planEndTime;
    /**
     * å®žé™…开始时间
     */
    private LocalDateTime actualStartTime;
    /**
     * å®žé™…结束时间
     */
    private LocalDateTime actualEndTime;
    /**
     * ç”³è¯·äººç¼–号
     */
    private Long applicantUserId;
    /**
     * ç”³è¯·éƒ¨é—¨ç¼–号
     */
    private Long applicantDeptId;
    /**
     * æ–½å·¥å•位 / æ‰¿åŒ…商
     */
    private String contractor;
    /**
     * ä½œä¸šäººå‘˜ï¼ˆå¤šä¸ªç”¨é€—号分隔)
     */
    private String workerNames;
    /**
     * ç›‘护人编号
     */
    private Long guardianUserId;
    /**
     * ç›‘护人是否已确认
     */
    private Boolean guardianConfirm;
    /**
     * å®‰å…¨æŽªæ–½
     */
    private String safetyMeasures;
    /**
     * å®‰å…¨äº¤åº• / æ£€æŸ¥ç»“æžœ
     */
    private String safetyCheckResult;
    /**
     * å®¡æ‰¹äººç¼–号
     */
    private Long approverUserId;
    /**
     * å®¡æ‰¹æ—¶é—´
     */
    private LocalDateTime approveTime;
    /**
     * å®¡æ‰¹æ„è§
     */
    private String approveComment;
    /**
     * é©³å›žåŽŸå› 
     */
    private String rejectReason;
    /**
     * çŠ¶æ€ï¼š0待审批 10已批准 20已完工 30已归档 90已驳回
     */
    private Integer status;
    /**
     * å½’档时间
     */
    private LocalDateTime closeTime;
    /**
     * å½’档人编号
     */
    private Long closeUserId;
    /**
     * å®Œå·¥ / å½’档说明
     */
    private String closeRemark;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/MesSafetyWorkTicketMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
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.ticket.MesSafetyWorkTicketPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.MesSafetyWorkTicketDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
import java.util.List;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰¹ç§ä½œä¸šç¥¨è¯ Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesSafetyWorkTicketMapper extends BaseMapperX<MesSafetyWorkTicketDO> {
    default PageResult<MesSafetyWorkTicketDO> selectPage(MesSafetyWorkTicketPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesSafetyWorkTicketDO>()
                .likeIfPresent(MesSafetyWorkTicketDO::getCode, reqVO.getCode())
                .eqIfPresent(MesSafetyWorkTicketDO::getTicketType, reqVO.getTicketType())
                .eqIfPresent(MesSafetyWorkTicketDO::getTicketLevel, reqVO.getTicketLevel())
                .eqIfPresent(MesSafetyWorkTicketDO::getStatus, reqVO.getStatus())
                .eqIfPresent(MesSafetyWorkTicketDO::getWorkshopId, reqVO.getWorkshopId())
                .eqIfPresent(MesSafetyWorkTicketDO::getApplicantUserId, reqVO.getApplicantUserId())
                .likeIfPresent(MesSafetyWorkTicketDO::getWorkLocation, reqVO.getWorkLocation())
                .geIfPresent(MesSafetyWorkTicketDO::getPlanStartTime, reqVO.getPlanStartTimeStart())
                .leIfPresent(MesSafetyWorkTicketDO::getPlanStartTime, reqVO.getPlanStartTimeEnd())
                .orderByDesc(MesSafetyWorkTicketDO::getId));
    }
    default MesSafetyWorkTicketDO selectByCode(String code) {
        return selectOne(MesSafetyWorkTicketDO::getCode, code);
    }
    /**
     * æŸ¥è¯¢å½“前处于「已批准」但已超过计划结束时间的作业票(用于超期未完工提醒)
     */
    default List<MesSafetyWorkTicketDO> selectApprovedOverdueList(LocalDateTime now) {
        return selectList(new LambdaQueryWrapperX<MesSafetyWorkTicketDO>()
                .eq(MesSafetyWorkTicketDO::getStatus, MesSafetyWorkTicketDO.STATUS_APPROVED)
                .lt(MesSafetyWorkTicketDO::getPlanEndTime, now)
                .orderByAsc(MesSafetyWorkTicketDO::getPlanEndTime));
    }
    default Long selectCountByStatus(Integer status) {
        return selectCount(new LambdaQueryWrapperX<MesSafetyWorkTicketDO>()
                .eq(MesSafetyWorkTicketDO::getStatus, status));
    }
    default Long selectCountByUnitId(Long unitId) {
        return selectCount(new LambdaQueryWrapperX<MesSafetyWorkTicketDO>()
                .eq(MesSafetyWorkTicketDO::getUnitId, unitId));
    }
    default List<MesSafetyWorkTicketDO> selectAllList() {
        return selectList(new LambdaQueryWrapperX<MesSafetyWorkTicketDO>()
                .orderByDesc(MesSafetyWorkTicketDO::getId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/DictTypeConstants.java
@@ -130,4 +130,9 @@
    String MES_SAFETY_HAZARD_SOURCE = "mes_safety_hazard_source";   // é𐿂£æ¥æº
    String MES_SAFETY_HAZARD_LEVEL = "mes_safety_hazard_level";     // é𐿂£ç­‰çº§ï¼ˆ1一般 2重大)
    String MES_SAFETY_HAZARD_STATUS = "mes_safety_hazard_status";   // é𐿂£çŠ¶æ€
    // ç‰¹ç§ä½œä¸šç¥¨è¯
    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";   // ä½œä¸šç¥¨çŠ¶æ€
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -861,4 +861,12 @@
    ErrorCode MES_SAFETY_HAZARD_REJECT_REASON_REQUIRED = new ErrorCode(1_040_905_003, "验收不通过时必须填写驳回原因");
    ErrorCode MES_SAFETY_HAZARD_HAS_REFERENCE = new ErrorCode(1_040_905_004, "该记录已被隐患引用,无法删除");
    // ========== MES å®‰å…¨ç”Ÿäº§ - ç‰¹ç§ä½œä¸šç¥¨è¯ï¼ˆ1-040-906-000) ==========
    ErrorCode MES_SAFETY_TICKET_NOT_EXISTS = new ErrorCode(1_040_906_000, "作业票不存在");
    ErrorCode MES_SAFETY_TICKET_CODE_DUPLICATE = new ErrorCode(1_040_906_001, "作业票号已存在");
    ErrorCode MES_SAFETY_TICKET_STATUS_INVALID = new ErrorCode(1_040_906_002, "当前作业票状态不允许该操作");
    ErrorCode MES_SAFETY_TICKET_TIME_INVALID = new ErrorCode(1_040_906_003, "计划结束时间必须晚于计划开始时间");
    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, "该记录已被作业票引用,无法删除");
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/MesSafetyRiskUnitServiceImpl.java
@@ -8,6 +8,7 @@
import cn.iocoder.yudao.module.mes.dal.mysql.safety.MesSafetyHazardMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.MesSafetyRiskMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.MesSafetyRiskUnitMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.MesSafetyWorkTicketMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@@ -39,6 +40,9 @@
    @Resource
    private MesSafetyHazardMapper hazardMapper;
    @Resource
    private MesSafetyWorkTicketMapper ticketMapper;
    @Override
    public PageResult<MesSafetyRiskUnitDO> getRiskUnitPage(MesSafetyRiskUnitPageReqVO pageReqVO) {
@@ -80,6 +84,11 @@
        if (hazardCount != null && hazardCount > 0) {
            throw exception(MES_SAFETY_HAZARD_HAS_REFERENCE);
        }
        // å·²è¢«ä½œä¸šç¥¨å¼•用的单元也不允许删除
        Long ticketCount = ticketMapper.selectCountByUnitId(id);
        if (ticketCount != null && ticketCount > 0) {
            throw exception(MES_SAFETY_TICKET_HAS_REFERENCE);
        }
        riskUnitMapper.deleteById(id);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/MesSafetyWorkTicketService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
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.ticket.MesSafetyWorkTicketApproveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ticket.MesSafetyWorkTicketCloseReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ticket.MesSafetyWorkTicketPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ticket.MesSafetyWorkTicketSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.MesSafetyWorkTicketDO;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰¹ç§ä½œä¸šç¥¨è¯ Service æŽ¥å£
 *
 * çŠ¶æ€æµè½¬ï¼š0待审批 â†’ 10已批准 â†’ 20已完工 â†’ 30已归档;0 â†’ 90已驳回
 *
 * @author è¶…级管理员
 */
public interface MesSafetyWorkTicketService {
    PageResult<MesSafetyWorkTicketDO> getTicketPage(MesSafetyWorkTicketPageReqVO pageReqVO);
    MesSafetyWorkTicketDO getTicket(Long id);
    Long createTicket(MesSafetyWorkTicketSaveReqVO createReqVO);
    void updateTicket(MesSafetyWorkTicketSaveReqVO updateReqVO);
    void deleteTicket(Long id);
    /**
     * å®¡æ‰¹ï¼š0待审批 â†’ 10已批准,或 â†’ 90已驳回
     *
     * @param userId å®¡æ‰¹äººç¼–号
     */
    void approveTicket(Long userId, MesSafetyWorkTicketApproveReqVO reqVO);
    /**
     * å®Œå·¥ç™»è®° / å½’档:10已批准 â†’ 20已完工,20已完工 â†’ 30已归档
     *
     * @param userId æ“ä½œäººç¼–号
     */
    void closeTicket(Long userId, MesSafetyWorkTicketCloseReqVO reqVO);
    MesSafetyWorkTicketDO validateTicketExists(Long id);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/MesSafetyWorkTicketServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,188 @@
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.ticket.MesSafetyWorkTicketApproveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ticket.MesSafetyWorkTicketCloseReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ticket.MesSafetyWorkTicketPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ticket.MesSafetyWorkTicketSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.MesSafetyWorkTicketDO;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.MesSafetyWorkTicketMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
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 MesSafetyWorkTicketServiceImpl implements MesSafetyWorkTicketService {
    @Resource
    private MesSafetyWorkTicketMapper ticketMapper;
    @Override
    public PageResult<MesSafetyWorkTicketDO> getTicketPage(MesSafetyWorkTicketPageReqVO pageReqVO) {
        return ticketMapper.selectPage(pageReqVO);
    }
    @Override
    public MesSafetyWorkTicketDO getTicket(Long id) {
        return ticketMapper.selectById(id);
    }
    @Override
    public Long createTicket(MesSafetyWorkTicketSaveReqVO createReqVO) {
        validateCodeUnique(null, createReqVO.getCode());
        validateTimeRange(createReqVO.getPlanStartTime(), createReqVO.getPlanEndTime());
        MesSafetyWorkTicketDO ticket = BeanUtils.toBean(createReqVO, MesSafetyWorkTicketDO.class);
        // æ–°å»ºå³å¾…审批
        ticket.setStatus(MesSafetyWorkTicketDO.STATUS_PENDING);
        ticket.setGuardianConfirm(false);
        ticketMapper.insert(ticket);
        return ticket.getId();
    }
    @Override
    public void updateTicket(MesSafetyWorkTicketSaveReqVO updateReqVO) {
        MesSafetyWorkTicketDO exists = validateTicketExists(updateReqVO.getId());
        // å®¡æ‰¹é€šè¿‡åŽä¸å…è®¸å†æ”¹ä½œä¸šä¿¡æ¯ï¼Œéœ€ä½œåºŸé‡å¼€
        if (!MesSafetyWorkTicketDO.STATUS_PENDING.equals(exists.getStatus())
                && !MesSafetyWorkTicketDO.STATUS_REJECTED.equals(exists.getStatus())) {
            throw exception(MES_SAFETY_TICKET_STATUS_INVALID);
        }
        validateCodeUnique(updateReqVO.getId(), updateReqVO.getCode());
        validateTimeRange(updateReqVO.getPlanStartTime(), updateReqVO.getPlanEndTime());
        MesSafetyWorkTicketDO updateObj = BeanUtils.toBean(updateReqVO, MesSafetyWorkTicketDO.class);
        // çŠ¶æ€ä¸Žå®¡æ‰¹/归档信息不走此接口
        updateObj.setStatus(null);
        updateObj.setApproverUserId(null);
        updateObj.setApproveTime(null);
        updateObj.setApproveComment(null);
        updateObj.setRejectReason(null);
        updateObj.setCloseTime(null);
        updateObj.setCloseUserId(null);
        updateObj.setCloseRemark(null);
        updateObj.setActualStartTime(null);
        updateObj.setActualEndTime(null);
        // é‡æ–°æäº¤æ—¶æ¸…空驳回原因
        if (MesSafetyWorkTicketDO.STATUS_REJECTED.equals(exists.getStatus())) {
            updateObj.setStatus(MesSafetyWorkTicketDO.STATUS_PENDING);
            updateObj.setRejectReason("");
        }
        ticketMapper.updateById(updateObj);
    }
    @Override
    public void deleteTicket(Long id) {
        MesSafetyWorkTicketDO exists = validateTicketExists(id);
        // å·²æ‰¹å‡†åŠä¹‹åŽçš„作业票不允许删除(需保留追溯记录)
        if (MesSafetyWorkTicketDO.STATUS_APPROVED.equals(exists.getStatus())
                || MesSafetyWorkTicketDO.STATUS_FINISHED.equals(exists.getStatus())
                || MesSafetyWorkTicketDO.STATUS_ARCHIVED.equals(exists.getStatus())) {
            throw exception(MES_SAFETY_TICKET_STATUS_INVALID);
        }
        ticketMapper.deleteById(id);
    }
    @Override
    public void approveTicket(Long userId, MesSafetyWorkTicketApproveReqVO reqVO) {
        MesSafetyWorkTicketDO ticket = validateTicketExists(reqVO.getId());
        if (!MesSafetyWorkTicketDO.STATUS_PENDING.equals(ticket.getStatus())) {
            throw exception(MES_SAFETY_TICKET_STATUS_INVALID);
        }
        MesSafetyWorkTicketDO updateObj = new MesSafetyWorkTicketDO();
        updateObj.setId(reqVO.getId());
        updateObj.setApproverUserId(userId);
        updateObj.setApproveTime(LocalDateTime.now());
        updateObj.setApproveComment(reqVO.getApproveComment());
        if (Boolean.TRUE.equals(reqVO.getPass())) {
            updateObj.setStatus(MesSafetyWorkTicketDO.STATUS_APPROVED);
            updateObj.setRejectReason("");
        } else {
            if (reqVO.getRejectReason() == null || reqVO.getRejectReason().trim().isEmpty()) {
                throw exception(MES_SAFETY_TICKET_REJECT_REASON_REQUIRED);
            }
            updateObj.setStatus(MesSafetyWorkTicketDO.STATUS_REJECTED);
            updateObj.setRejectReason(reqVO.getRejectReason());
        }
        ticketMapper.updateById(updateObj);
    }
    @Override
    public void closeTicket(Long userId, MesSafetyWorkTicketCloseReqVO reqVO) {
        MesSafetyWorkTicketDO ticket = validateTicketExists(reqVO.getId());
        Integer status = ticket.getStatus();
        // å·²æ‰¹å‡† â†’ å®Œå·¥ï¼›å·²å®Œå·¥ â†’ å½’æ¡£
        boolean canFinish = MesSafetyWorkTicketDO.STATUS_APPROVED.equals(status);
        boolean canArchive = MesSafetyWorkTicketDO.STATUS_FINISHED.equals(status);
        if (!canFinish && !canArchive) {
            throw exception(MES_SAFETY_TICKET_STATUS_INVALID);
        }
        MesSafetyWorkTicketDO updateObj = new MesSafetyWorkTicketDO();
        updateObj.setId(reqVO.getId());
        updateObj.setActualStartTime(reqVO.getActualStartTime());
        updateObj.setActualEndTime(reqVO.getActualEndTime());
        if (reqVO.getCloseRemark() != null) {
            updateObj.setCloseRemark(reqVO.getCloseRemark());
        }
        if (canFinish) {
            // å®Œå·¥ç™»è®°ï¼›è‹¥åŒæ—¶é€‰æ‹©å½’档则直接进入已归档
            boolean archiveNow = Boolean.TRUE.equals(reqVO.getArchive());
            updateObj.setStatus(archiveNow
                    ? MesSafetyWorkTicketDO.STATUS_ARCHIVED
                    : MesSafetyWorkTicketDO.STATUS_FINISHED);
            if (archiveNow) {
                updateObj.setCloseTime(LocalDateTime.now());
                updateObj.setCloseUserId(userId);
            }
        } else {
            // å·²å®Œå·¥ â†’ å½’æ¡£
            updateObj.setStatus(MesSafetyWorkTicketDO.STATUS_ARCHIVED);
            updateObj.setCloseTime(LocalDateTime.now());
            updateObj.setCloseUserId(userId);
        }
        ticketMapper.updateById(updateObj);
    }
    @Override
    public MesSafetyWorkTicketDO validateTicketExists(Long id) {
        if (id == null) {
            throw exception(MES_SAFETY_TICKET_NOT_EXISTS);
        }
        MesSafetyWorkTicketDO ticket = ticketMapper.selectById(id);
        if (ticket == null) {
            throw exception(MES_SAFETY_TICKET_NOT_EXISTS);
        }
        return ticket;
    }
    /**
     * æ ¡éªŒè®¡åˆ’时间区间合法
     */
    private void validateTimeRange(LocalDateTime start, LocalDateTime end) {
        if (start != null && end != null && !end.isAfter(start)) {
            throw exception(MES_SAFETY_TICKET_TIME_INVALID);
        }
    }
    private void validateCodeUnique(Long id, String code) {
        MesSafetyWorkTicketDO exists = ticketMapper.selectByCode(code);
        if (exists == null) {
            return;
        }
        if (id == null || !Objects.equals(exists.getId(), id)) {
            throw exception(MES_SAFETY_TICKET_CODE_DUPLICATE);
        }
    }
}