feat: 从长辉移植新高缺失的六个业务模块(安全生产/能耗/数采/备件/质检台账/售后问题回访)

## 移植范围(199 个 java 文件 + 建表脚本)

| 模块 | 文件数 | 说明 |
|---|---|---|
| 安全生产 safety | 100 | 安全档案、隐患整改、风险监测、巡检、培训、作业许可、证件 |
| 数采 collect | 37 | MQTT 设备数据采集、实时数据、报警、周期、技术参数 |
| 能耗 energy | 19 | 能耗记录、报警、统计分析 |
| 设备备件 spare | 10 | 备件台账、出入库流水 |
| 质检台账 ledger | 7 | 检验台账汇总 |
| 售后问题 problem | 8 | 问题记录全生命周期 |
| 售后回访 visit | 8 | 回访登记、满意度、复发跟踪 |
| 公司配置/数据库备份 | 10 | 系统级辅助功能 |

## 前置依赖补齐

1. 新增 HrmEmployeeApi + HrmEmployeeSimpleDTO + HrmEmployeeApiImpl
(安全生产的特种作业证需关联员工档案,新高原本没有这个跨模块 API)
2. mes/pom.xml 新增 yudao-module-hrm-api 依赖
3. yudao-dependencies 与 mes/pom.xml 新增 paho MQTT 客户端(数采连接设备网关)

## 既有类增量补齐

- MesDvMachineryDO:新增 collectDeviceCode / collectEnabled 字段
- MesDvMachineryMapper:新增 selectByCollectDeviceCode / selectListWithCollectDeviceCode
- MesProTaskMapper:新增 selectRunningTaskByWorkstationId(数采定位当前任务)
- MesDvRepairMapper:新增 lockForSpare(备件出库加行锁)
- AfterSaleTicketDO:新增 visitStatus / lastVisitTime / lastVisitSatisfied / lastVisitRecurred / problemId / problemNo
- AfterSaleNoRedisDAO:新增 PROBLEM_NO_PREFIX
- mes ErrorCodeConstants:新增 COLLECT_ALARM_*
- aftersales ErrorCodeConstants:新增 PROBLEM_* / VISIT_*
- aftersales enums:新增 AfterSaleProblemStatusEnum / AfterSaleVisitStatusEnum

## 数据库变更(已在新高生产执行,事前全库备份)

- 新建 12 张 safety_* 表
- 新建 mes_energy_record、7 张 mes_collect_* 表、2 张 mes_dv_spare_* 表
- mes_dv_machinery 补 2 列
- after_sale_ticket 补 6 列
- 新建 after_sale_problem、after_sale_visit 表

## 验证

全项目 mvn compile 通过(EXIT=0),打包成功。待部署后做接口实测。
已添加207个文件
已修改10个文件
13563 ■■■■■ 文件已修改
yudao-dependencies/pom.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/AfterSaleProblemController.java 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/vo/AfterSaleProblemPageReqVO.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/vo/AfterSaleProblemRespVO.java 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/vo/AfterSaleProblemSaveReqVO.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/AfterSaleVisitController.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/vo/AfterSaleVisitCreateReqVO.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/vo/AfterSaleVisitPageReqVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/vo/AfterSaleVisitRespVO.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/problem/AfterSaleProblemDO.java 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketDO.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/visit/AfterSaleVisitDO.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/problem/AfterSaleProblemMapper.java 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/visit/AfterSaleVisitMapper.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/no/AfterSaleNoRedisDAO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleProblemStatusEnum.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleVisitStatusEnum.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/ErrorCodeConstants.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/problem/AfterSaleProblemService.java 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/problem/AfterSaleProblemServiceImpl.java 211 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/visit/AfterSaleVisitService.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/visit/AfterSaleVisitServiceImpl.java 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/api/employee/HrmEmployeeApi.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/api/employee/dto/HrmEmployeeSimpleDTO.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/api/employee/HrmEmployeeApiImpl.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/pom.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/MesCollectController.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectAlarmPageReqVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectAlarmRespVO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectCyclePageReqVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectCycleRespVO.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectDeviceRespVO.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectOperateLogPageReqVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectOperateLogRespVO.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectRealtimeHistoryPageReqVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectRealtimeRespVO.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectTechRespVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/spare/MesDvSpareController.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/spare/vo/MesDvSpareActionReqVO.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/spare/vo/MesDvSpareMovementRespVO.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/spare/vo/MesDvSparePageReqVO.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/spare/vo/MesDvSparePartSaveReqVO.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/energy/EnergyAlertController.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/energy/EnergyRecordController.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/energy/vo/EnergyAlertRuleSaveReqVO.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/energy/vo/EnergyAnalysisReqVO.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/energy/vo/EnergyAnalysisRespVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/energy/vo/EnergyRecordPageReqVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/energy/vo/EnergyRecordRespVO.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/energy/vo/EnergyRecordSaveReqVO.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ledger/MesQcInspectionLedgerController.java 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ledger/vo/MesQcInspectionLedgerExportVO.java 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ledger/vo/MesQcInspectionLedgerPageReqVO.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ledger/vo/MesQcInspectionLedgerRespVO.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/vo/MesQcImportRespVO.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/vo/MesQcQualityExportVO.java 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/SafetyCertificateController.java 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/SafetyFileController.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/SafetyHazardController.java 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/SafetyInspectionController.java 162 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/SafetyLedgerController.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/SafetyMaterialController.java 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/SafetyOrgController.java 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/SafetyTrainingController.java 187 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/SafetyWorkPermitController.java 147 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/cert/SafetyCertificatePageReqVO.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/cert/SafetyCertificateRespVO.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/cert/SafetyCertificateSaveReqVO.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/hazard/SafetyHazardAssignReqVO.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/hazard/SafetyHazardPageReqVO.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/hazard/SafetyHazardRecheckReqVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/hazard/SafetyHazardRectifyReqVO.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/hazard/SafetyHazardRespVO.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/hazard/SafetyHazardSaveReqVO.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/inspection/SafetyInspectionPageReqVO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/inspection/SafetyInspectionPointPageReqVO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/inspection/SafetyInspectionPointRespVO.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/inspection/SafetyInspectionPointSaveReqVO.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/inspection/SafetyInspectionRespVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/inspection/SafetyInspectionSaveReqVO.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/ledger/SafetyLedgerExportReqVO.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/ledger/SafetyOverviewRespVO.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/material/SafetyMaterialPageReqVO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/material/SafetyMaterialRespVO.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/material/SafetyMaterialSaveReqVO.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/material/SafetyMaterialStockPageReqVO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/material/SafetyMaterialStockRespVO.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/material/SafetyMaterialStockSaveReqVO.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/org/SafetyOrgRespVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/org/SafetyOrgSaveReqVO.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/permit/SafetyWorkPermitApproveReqVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/permit/SafetyWorkPermitMonitorRespVO.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/permit/SafetyWorkPermitMonitorSaveReqVO.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/permit/SafetyWorkPermitPageReqVO.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/permit/SafetyWorkPermitRespVO.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/permit/SafetyWorkPermitSaveReqVO.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/train/SafetyAccidentPageReqVO.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/train/SafetyAccidentRespVO.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/train/SafetyAccidentSaveReqVO.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/train/SafetyDrillPageReqVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/train/SafetyDrillRespVO.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/train/SafetyDrillSaveReqVO.java 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/train/SafetyTrainingPageReqVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/train/SafetyTrainingRespVO.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/train/SafetyTrainingSaveReqVO.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectAlarmDO.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectCycleDO.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectMessageDO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectOperateLogDO.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectRealtimeDO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectRealtimeHistoryDO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectTechDO.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/machinery/MesDvMachineryDO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/spare/MesDvSpareMovementDO.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/spare/MesDvSparePartDO.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/energy/EnergyAlertLogDO.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/energy/EnergyAlertRuleDO.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/energy/EnergyRecordDO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyAccidentDO.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyCertificateDO.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyDrillDO.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyHazardDO.java 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyInspectionDO.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyInspectionPointDO.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyMaterialDO.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyMaterialStockDO.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyOrgDO.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyTrainingDO.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyWorkPermitDO.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyWorkPermitMonitorDO.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectAlarmMapper.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectCycleMapper.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectMessageMapper.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectOperateLogMapper.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectRealtimeHistoryMapper.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectRealtimeMapper.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectTechMapper.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/machinery/MesDvMachineryMapper.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/repair/MesDvRepairMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/spare/MesDvSpareMovementMapper.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/spare/MesDvSparePartMapper.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/energy/EnergyAlertLogMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/energy/EnergyAlertRuleMapper.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/energy/EnergyRecordMapper.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/task/MesProTaskMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/ledger/MesQcInspectionLedgerMapper.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyAccidentMapper.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyCertificateMapper.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyDrillMapper.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyHazardMapper.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyInspectionMapper.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyInspectionPointMapper.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyMaterialMapper.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyMaterialStockMapper.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyOrgMapper.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyTrainingMapper.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyWorkPermitMapper.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyWorkPermitMonitorMapper.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/SafetyErrorCodeConstants.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/collect/MesCollectAlarmStatusEnum.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/collect/MesCollectMsgTypeEnum.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/collect/MesCollectOperateModeEnum.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/collect/MesCollectRunStatusEnum.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyAccidentLevelEnum.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyAccidentStatusEnum.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyAccidentTypeEnum.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyCertificateStatusEnum.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyCertificateTypeEnum.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyDrillTypeEnum.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyHazardLevelEnum.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyHazardSourceEnum.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyHazardStatusEnum.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyInspectCycleEnum.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyInspectionPointTypeEnum.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyMaterialCategoryEnum.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyPermitStatusEnum.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyPermitTypeEnum.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyRiskLevelEnum.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyTrainingTypeEnum.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/collect/config/MesCollectProperties.java 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/collect/mqtt/MesCollectMqttClient.java 198 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/job/collect/MesCollectCleanupJob.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/collect/MesCollectDataParser.java 237 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/collect/MesCollectDataService.java 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/collect/MesCollectMessageDispatcher.java 204 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/collect/MesCollectQueryService.java 204 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/collect/MesCollectTaskResolver.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/spare/MesDvSpareService.java 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/energy/EnergyAlertListener.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/energy/EnergyAlertService.java 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/energy/EnergyExportService.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/energy/EnergyRecordChangedEvent.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/energy/EnergyRecordService.java 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ledger/MesQcInspectionLedgerService.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ledger/MesQcInspectionLedgerServiceImpl.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyCertificateService.java 234 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyFileService.java 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyHazardService.java 220 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyInspectionService.java 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyLedgerExportService.java 334 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyMaterialService.java 192 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyOrgService.java 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyOverviewService.java 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyRefResolver.java 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyTrainingService.java 273 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyWorkPermitService.java 286 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/companyconfig/SystemCompanyConfigController.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/companyconfig/vo/SystemCompanyConfigRespVO.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/companyconfig/vo/SystemCompanyConfigSaveReqVO.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/database/DatabaseBackupController.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/companyconfig/SystemCompanyConfigDO.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/companyconfig/SystemCompanyConfigMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/companyconfig/SystemCompanyConfigService.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/companyconfig/SystemCompanyConfigServiceImpl.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/database/DatabaseBackupService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/database/DatabaseBackupServiceImpl.java 460 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-dependencies/pom.xml
@@ -606,6 +606,13 @@
                <version>${vertx.version}</version>
            </dependency>
            <!-- MQTT:MES æ•°é‡‡å®¢æˆ·ç«¯ä½¿ç”¨ paho -->
            <dependency>
                <groupId>org.eclipse.paho</groupId>
                <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
                <version>${mqtt.version}</version>
            </dependency>
            <!-- MQTT -->
            <dependency>
                <groupId>org.eclipse.paho</groupId>
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/AfterSaleProblemController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,141 @@
package cn.iocoder.yudao.module.aftersales.controller.admin.problem;
import cn.hutool.core.collection.CollUtil;
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.aftersales.controller.admin.problem.vo.AfterSaleProblemPageReqVO;
import cn.iocoder.yudao.module.aftersales.controller.admin.problem.vo.AfterSaleProblemRespVO;
import cn.iocoder.yudao.module.aftersales.controller.admin.problem.vo.AfterSaleProblemSaveReqVO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.problem.AfterSaleProblemDO;
import cn.iocoder.yudao.module.aftersales.service.problem.AfterSaleProblemService;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
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 static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - å”®åŽé—®é¢˜åº“")
@RestController
@RequestMapping("/aftersales/problem")
@Validated
public class AfterSaleProblemController {
    @Resource
    private AfterSaleProblemService problemService;
    @Resource
    private CrmCustomerApi customerApi;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @PostMapping("/create")
    @Operation(summary = "创建售后问题")
    @PreAuthorize("@ss.hasPermission('aftersales:problem:create')")
    public CommonResult<Long> createProblem(@Valid @RequestBody AfterSaleProblemSaveReqVO createReqVO) {
        return success(problemService.createProblem(createReqVO, getLoginUserId()));
    }
    @PutMapping("/update")
    @Operation(summary = "更新售后问题")
    @PreAuthorize("@ss.hasPermission('aftersales:problem:update')")
    public CommonResult<Boolean> updateProblem(@Valid @RequestBody AfterSaleProblemSaveReqVO updateReqVO) {
        problemService.updateProblem(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除售后问题")
    @Parameter(name = "id", description = "问题编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('aftersales:problem:delete')")
    public CommonResult<Boolean> deleteProblem(@RequestParam("id") Long id) {
        problemService.deleteProblem(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得售后问题")
    @Parameter(name = "id", description = "问题编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('aftersales:problem:query')")
    public CommonResult<AfterSaleProblemRespVO> getProblem(@RequestParam("id") Long id) {
        AfterSaleProblemDO problemDO = problemService.getProblem(id);
        AfterSaleProblemRespVO respVO = buildProblemDetailList(List.of(problemDO)).get(0);
        respVO.setAttachmentList(storageAttachmentApi.listAttachments("aftersales_after_sale_problem", id));
        return success(respVO);
    }
    @GetMapping("/page")
    @Operation(summary = "获得售后问题分页")
    @PreAuthorize("@ss.hasPermission('aftersales:problem:query')")
    public CommonResult<PageResult<AfterSaleProblemRespVO>> getProblemPage(@Valid AfterSaleProblemPageReqVO pageReqVO) {
        PageResult<AfterSaleProblemDO> pageResult = problemService.getProblemPage(pageReqVO);
        return success(new PageResult<>(buildProblemDetailList(pageResult.getList()), pageResult.getTotal()));
    }
    @PutMapping("/publish")
    @Operation(summary = "发布售后问题")
    @Parameter(name = "id", description = "问题编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('aftersales:problem:publish')")
    public CommonResult<Boolean> publishProblem(@RequestParam("id") Long id) {
        problemService.publishProblem(id);
        return success(true);
    }
    @PutMapping("/disable")
    @Operation(summary = "停用售后问题")
    @Parameter(name = "id", description = "问题编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('aftersales:problem:disable')")
    public CommonResult<Boolean> disableProblem(@RequestParam("id") Long id) {
        problemService.disableProblem(id);
        return success(true);
    }
    @GetMapping("/reference")
    @Operation(summary = "新工单受理参考检索(已发布问题)")
    @PreAuthorize("@ss.hasPermission('aftersales:problem:query')")
    public CommonResult<List<AfterSaleProblemRespVO>> getReferenceList(@Valid AfterSaleProblemPageReqVO pageReqVO) {
        return success(buildProblemDetailList(problemService.getReferenceList(pageReqVO)));
    }
    // ========== å†…部方法:批量拼装关联名称 ==========
    private List<AfterSaleProblemRespVO> buildProblemDetailList(List<AfterSaleProblemDO> problemList) {
        if (CollUtil.isEmpty(problemList)) {
            return Collections.emptyList();
        }
        // 1. æ‰¹é‡åŠ è½½å®¢æˆ·åç§°
        Set<Long> customerIds = convertSet(problemList, AfterSaleProblemDO::getCustomerId);
        Map<Long, CrmCustomerRespDTO> customerMap = customerApi.getCustomerMap(customerIds);
        // 2. æ‰¹é‡åŠ è½½æ²‰æ·€äººåç§°
        Set<Long> userIds = convertSet(problemList, AfterSaleProblemDO::getOwnerUserId);
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
        // 3. æ‹¼è£…名称字段
        return BeanUtils.toBean(problemList, AfterSaleProblemRespVO.class, vo -> {
            CrmCustomerRespDTO customer = customerMap.get(vo.getCustomerId());
            if (customer != null) {
                vo.setCustomerName(customer.getName());
            }
            AdminUserRespDTO ownerUser = userMap.get(vo.getOwnerUserId());
            if (ownerUser != null) {
                vo.setOwnerUserName(ownerUser.getNickname());
            }
        });
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/vo/AfterSaleProblemPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.aftersales.controller.admin.problem.vo;
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;
/**
 * å”®åŽé—®é¢˜åº“ - åˆ†é¡µæŸ¥è¯¢ Request VO
 */
@Schema(description = "售后问题库 - åˆ†é¡µæŸ¥è¯¢ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class AfterSaleProblemPageReqVO extends PageParam {
    @Schema(description = "问题单号", example = "AP20260813000001")
    private String no;
    @Schema(description = "关键字(单号/标题/描述/解决方案)", example = "传感器")
    private String keyword;
    @Schema(description = "来源工单编号", example = "1")
    private Long sourceTicketId;
    @Schema(description = "物料编号", example = "1")
    private Long itemId;
    @Schema(description = "物料编码(模糊)", example = "MAT-001")
    private String itemCode;
    @Schema(description = "问题分类", example = "功能故障")
    private String issueCategory;
    @Schema(description = "问题类型", example = "1")
    private Integer issueType;
    @Schema(description = "根因分类", example = "3")
    private Integer rootCauseCategory;
    @Schema(description = "状态: 0-草稿 1-已发布 2-已停用", example = "1")
    private Integer status;
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/vo/AfterSaleProblemRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,104 @@
package cn.iocoder.yudao.module.aftersales.controller.admin.problem.vo;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
 * å”®åŽé—®é¢˜åº“ - å“åº” VO
 */
@Schema(description = "售后问题库 - å“åº” VO")
@Data
public class AfterSaleProblemRespVO {
    @Schema(description = "问题编号", example = "1")
    private Long id;
    @Schema(description = "问题单号", example = "AP20260813000001")
    private String no;
    @Schema(description = "问题标题", example = "拉力机传感器无输出")
    private String title;
    @Schema(description = "来源工单编号", example = "1")
    private Long sourceTicketId;
    @Schema(description = "来源工单单号(冗余)", example = "AS20260813000001")
    private String sourceTicketNo;
    @Schema(description = "客户编号", example = "1")
    private Long customerId;
    @Schema(description = "客户名称")
    private String customerName;
    @Schema(description = "物料编号", example = "1")
    private Long itemId;
    @Schema(description = "物料编码", example = "MAT-001")
    private String itemCode;
    @Schema(description = "物料名称", example = "电子元器件")
    private String itemName;
    @Schema(description = "生产批次编号", example = "1")
    private Long batchId;
    @Schema(description = "生产批次号", example = "BATCH-2024-001")
    private String batchCode;
    @Schema(description = "问题类型", example = "1")
    private Integer issueType;
    @Schema(description = "问题分类", example = "功能故障")
    private String issueCategory;
    @Schema(description = "严重程度", example = "2")
    private Integer severityLevel;
    @Schema(description = "问题描述")
    private String description;
    @Schema(description = "根因分类", example = "3")
    private Integer rootCauseCategory;
    @Schema(description = "根因分析")
    private String rootCause;
    @Schema(description = "解决方案")
    private String solution;
    @Schema(description = "状态: 0-草稿 1-已发布 2-已停用", example = "1")
    private Integer status;
    @Schema(description = "沉淀人用户编号", example = "1")
    private Long ownerUserId;
    @Schema(description = "沉淀人名称")
    private String ownerUserName;
    @Schema(description = "发布时间")
    private LocalDateTime publishTime;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "创建者")
    private String creator;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
    @Schema(description = "更新者")
    private String updater;
    @Schema(description = "更新时间")
    private LocalDateTime updateTime;
    @Schema(description = "附件列表")
    private List<StorageBlobRespDTO> attachmentList;
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/problem/vo/AfterSaleProblemSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
package cn.iocoder.yudao.module.aftersales.controller.admin.problem.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
/**
 * å”®åŽé—®é¢˜åº“ - ä¿å­˜ Request VO
 */
@Schema(description = "售后问题库 - ä¿å­˜ Request VO")
@Data
public class AfterSaleProblemSaveReqVO {
    @Schema(description = "问题编号", example = "1")
    private Long id;
    @Schema(description = "问题标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "拉力机传感器无输出")
    @NotEmpty(message = "问题标题不能为空")
    private String title;
    @Schema(description = "来源工单编号(可选,便于追溯)", example = "1")
    private Long sourceTicketId;
    @Schema(description = "客户编号", example = "1")
    private Long customerId;
    @Schema(description = "物料编号", example = "1")
    private Long itemId;
    @Schema(description = "物料编码", example = "MAT-001")
    private String itemCode;
    @Schema(description = "物料名称", example = "电子元器件")
    private String itemName;
    @Schema(description = "生产批次编号", example = "1")
    private Long batchId;
    @Schema(description = "生产批次号", example = "BATCH-2024-001")
    private String batchCode;
    @Schema(description = "问题类型: 0-未判定 1-一般问题 2-维修问题 3-退货问题", example = "1")
    private Integer issueType;
    @Schema(description = "问题分类", example = "功能故障")
    private String issueCategory;
    @Schema(description = "严重程度: 0-轻微 1-一般 2-严重 3-致命", example = "2")
    private Integer severityLevel;
    @Schema(description = "问题描述")
    private String description;
    @Schema(description = "根因分类: 1-设计 2-物料 3-工艺 4-装配 5-运输 6-包装 7-使用 8-软件 9-其他", example = "3")
    private Integer rootCauseCategory;
    @Schema(description = "根因分析")
    private String rootCause;
    @Schema(description = "解决方案")
    private String solution;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "附件 blobId åˆ—表")
    private List<Long> blobIds;
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/AfterSaleVisitController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
package cn.iocoder.yudao.module.aftersales.controller.admin.visit;
import cn.hutool.core.collection.CollUtil;
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.aftersales.controller.admin.visit.vo.AfterSaleVisitCreateReqVO;
import cn.iocoder.yudao.module.aftersales.controller.admin.visit.vo.AfterSaleVisitPageReqVO;
import cn.iocoder.yudao.module.aftersales.controller.admin.visit.vo.AfterSaleVisitRespVO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.visit.AfterSaleVisitDO;
import cn.iocoder.yudao.module.aftersales.service.visit.AfterSaleVisitService;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
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 static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - å”®åŽå›žè®¿è®°å½•")
@RestController
@RequestMapping("/aftersales/visit")
@Validated
public class AfterSaleVisitController {
    @Resource
    private AfterSaleVisitService visitService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @PostMapping("/create")
    @Operation(summary = "登记售后回访")
    @PreAuthorize("@ss.hasPermission('aftersales:visit:create')")
    public CommonResult<Long> createVisit(@Valid @RequestBody AfterSaleVisitCreateReqVO createReqVO) {
        return success(visitService.createVisit(createReqVO, getLoginUserId()));
    }
    @GetMapping("/get")
    @Operation(summary = "获得售后回访记录")
    @Parameter(name = "id", description = "回访记录编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('aftersales:visit:query')")
    public CommonResult<AfterSaleVisitRespVO> getVisit(@RequestParam("id") Long id) {
        AfterSaleVisitDO visitDO = visitService.getVisit(id);
        AfterSaleVisitRespVO respVO = buildVisitDetailList(List.of(visitDO)).get(0);
        respVO.setAttachmentList(storageAttachmentApi.listAttachments("aftersales_after_sale_visit", id));
        return success(respVO);
    }
    @GetMapping("/list")
    @Operation(summary = "获得某工单的回访记录列表")
    @Parameter(name = "ticketId", description = "工单编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('aftersales:visit:query')")
    public CommonResult<List<AfterSaleVisitRespVO>> getVisitListByTicketId(@RequestParam("ticketId") Long ticketId) {
        List<AfterSaleVisitDO> visitList = visitService.getVisitListByTicketId(ticketId);
        return success(buildVisitDetailList(visitList));
    }
    @GetMapping("/page")
    @Operation(summary = "获得售后回访记录分页")
    @PreAuthorize("@ss.hasPermission('aftersales:visit:query')")
    public CommonResult<PageResult<AfterSaleVisitRespVO>> getVisitPage(@Valid AfterSaleVisitPageReqVO pageReqVO) {
        PageResult<AfterSaleVisitDO> pageResult = visitService.getVisitPage(pageReqVO);
        return success(new PageResult<>(buildVisitDetailList(pageResult.getList()), pageResult.getTotal()));
    }
    // ========== å†…部方法:批量拼装关联名称 ==========
    private List<AfterSaleVisitRespVO> buildVisitDetailList(List<AfterSaleVisitDO> visitList) {
        if (CollUtil.isEmpty(visitList)) {
            return Collections.emptyList();
        }
        // æ‰¹é‡åŠ è½½å›žè®¿äººåç§°
        Set<Long> userIds = convertSet(visitList, AfterSaleVisitDO::getVisitorUserId);
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
        return BeanUtils.toBean(visitList, AfterSaleVisitRespVO.class, vo -> {
            AdminUserRespDTO visitorUser = userMap.get(vo.getVisitorUserId());
            if (visitorUser != null) {
                vo.setVisitorUserName(visitorUser.getNickname());
            }
        });
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/vo/AfterSaleVisitCreateReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.aftersales.controller.admin.visit.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
 * å”®åŽå›žè®¿è®°å½• - ç™»è®° Request VO
 */
@Schema(description = "售后回访记录 - ç™»è®° Request VO")
@Data
public class AfterSaleVisitCreateReqVO {
    @Schema(description = "工单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "工单不能为空")
    private Long ticketId;
    @Schema(description = "回访方式: 0-电话 1-上门 2-远程 3-其他", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    @NotNull(message = "回访方式不能为空")
    private Integer visitType;
    @Schema(description = "回访内容", example = "客户反馈已正常使用")
    private String content;
    @Schema(description = "是否满意", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
    @NotNull(message = "是否满意不能为空")
    private Boolean satisfied;
    @Schema(description = "问题是否复发", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
    @NotNull(message = "问题是否复发不能为空")
    private Boolean recurred;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "附件 blobId åˆ—表")
    private List<Long> blobIds;
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/vo/AfterSaleVisitPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.aftersales.controller.admin.visit.vo;
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;
/**
 * å”®åŽå›žè®¿è®°å½• - åˆ†é¡µæŸ¥è¯¢ Request VO
 */
@Schema(description = "售后回访记录 - åˆ†é¡µæŸ¥è¯¢ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class AfterSaleVisitPageReqVO extends PageParam {
    @Schema(description = "工单编号", example = "1")
    private Long ticketId;
    @Schema(description = "回访方式", example = "0")
    private Integer visitType;
    @Schema(description = "是否满意", example = "true")
    private Boolean satisfied;
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/visit/vo/AfterSaleVisitRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.aftersales.controller.admin.visit.vo;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
 * å”®åŽå›žè®¿è®°å½• - å“åº” VO
 */
@Schema(description = "售后回访记录 - å“åº” VO")
@Data
public class AfterSaleVisitRespVO {
    @Schema(description = "回访记录编号", example = "1")
    private Long id;
    @Schema(description = "工单编号", example = "1")
    private Long ticketId;
    @Schema(description = "回访方式: 0-电话 1-上门 2-远程 3-其他", example = "0")
    private Integer visitType;
    @Schema(description = "回访人用户编号", example = "1")
    private Long visitorUserId;
    @Schema(description = "回访人名称")
    private String visitorUserName;
    @Schema(description = "回访内容")
    private String content;
    @Schema(description = "是否满意", example = "true")
    private Boolean satisfied;
    @Schema(description = "问题是否复发", example = "false")
    private Boolean recurred;
    @Schema(description = "回访时间")
    private LocalDateTime visitTime;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "创建者")
    private String creator;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
    @Schema(description = "附件列表")
    private List<StorageBlobRespDTO> attachmentList;
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/problem/AfterSaleProblemDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,118 @@
package cn.iocoder.yudao.module.aftersales.dal.dataobject.problem;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleProblemStatusEnum;
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
 * <p>
 * ç‹¬ç«‹çš„知识沉淀实体:工单归档时按工单快照自动生成草稿,人工补充根因/方案后发布;
 * æ–°å·¥å•受理时可按物料/分类/关键字命中已发布问题作参考。
 */
@TableName("after_sale_problem")
@KeySequence("after_sale_problem_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AfterSaleProblemDO extends BaseDO {
    /**
     * é—®é¢˜ç¼–号
     */
    @TableId
    private Long id;
    /**
     * é—®é¢˜å•号,格式 AP+yyyyMMdd+6位
     */
    private String no;
    /**
     * é—®é¢˜æ ‡é¢˜
     */
    private String title;
    /**
     * æ¥æºå·¥å•编号,关联 after_sale_ticket.id
     */
    private Long sourceTicketId;
    /**
     * æ¥æºå·¥å•单号(冗余)
     */
    private String sourceTicketNo;
    /**
     * å®¢æˆ·ç¼–号(来源工单带入)
     */
    private Long customerId;
    /**
     * ç‰©æ–™ç¼–号(默认取来源工单首条明细)
     */
    private Long itemId;
    /**
     * ç‰©æ–™ç¼–码(冗余)
     */
    private String itemCode;
    /**
     * ç‰©æ–™åç§°(冗余)
     */
    private String itemName;
    /**
     * ç”Ÿäº§æ‰¹æ¬¡ç¼–号(可选)
     */
    private Long batchId;
    /**
     * ç”Ÿäº§æ‰¹æ¬¡å·(可选)
     */
    private String batchCode;
    /**
     * é—®é¢˜ç±»åž‹ï¼Œæžšä¸¾ {@link cn.iocoder.yudao.module.aftersales.enums.AfterSaleIssueTypeEnum}
     */
    private Integer issueType;
    /**
     * é—®é¢˜åˆ†ç±»
     */
    private String issueCategory;
    /**
     * ä¸¥é‡ç¨‹åº¦: 0-轻微 1-一般 2-严重 3-致命
     */
    private Integer severityLevel;
    /**
     * é—®é¢˜æè¿°
     */
    private String description;
    /**
     * æ ¹å› åˆ†ç±»ï¼Œæžšä¸¾ {@link cn.iocoder.yudao.module.aftersales.enums.AfterSaleRootCauseCategoryEnum}
     */
    private Integer rootCauseCategory;
    /**
     * æ ¹å› åˆ†æž
     */
    private String rootCause;
    /**
     * è§£å†³æ–¹æ¡ˆ
     */
    private String solution;
    /**
     * çŠ¶æ€ï¼Œæžšä¸¾ {@link AfterSaleProblemStatusEnum}
     */
    private Integer status;
    /**
     * æ²‰æ·€äººç”¨æˆ·ç¼–号
     */
    private Long ownerUserId;
    /**
     * å‘布时间
     */
    private LocalDateTime publishTime;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketDO.java
@@ -112,5 +112,33 @@
     * å¤‡æ³¨
     */
    private String remark;
    /**
     * å›žè®¿çŠ¶æ€
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.aftersales.enums.AfterSaleVisitStatusEnum}
     */
    private Integer visitStatus;
    /**
     * æœ€åŽå›žè®¿æ—¶é—´
     */
    private LocalDateTime lastVisitTime;
    /**
     * æœ€åŽå›žè®¿æ˜¯å¦æ»¡æ„
     */
    private Boolean lastVisitSatisfied;
    /**
     * æœ€åŽå›žè®¿æ˜¯å¦å¤å‘
     */
    private Boolean lastVisitRecurred;
    /**
     * å…³è”问题记录编号
     *
     * å…³è” {@link cn.iocoder.yudao.module.aftersales.dal.dataobject.problem.AfterSaleProblemDO#getId()}
     */
    private Long problemId;
    /**
     * å…³è”问题单号
     */
    private String problemNo;
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/visit/AfterSaleVisitDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
package cn.iocoder.yudao.module.aftersales.dal.dataobject.visit;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
 * å”®åŽå›žè®¿è®°å½• DO
 * <p>
 * æŒ‚在售后工单下的回访子记录,可多条;登记回访时同步推进工单回访状态。
 */
@TableName("after_sale_visit")
@KeySequence("after_sale_visit_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AfterSaleVisitDO extends BaseDO {
    /**
     * å›žè®¿è®°å½•编号
     */
    @TableId
    private Long id;
    /**
     * å·¥å•编号,关联 after_sale_ticket.id
     */
    private Long ticketId;
    /**
     * å›žè®¿æ–¹å¼: 0-电话 1-上门 2-远程 3-其他
     */
    private Integer visitType;
    /**
     * å›žè®¿äººç”¨æˆ·ç¼–号
     */
    private Long visitorUserId;
    /**
     * å›žè®¿å†…容
     */
    private String content;
    /**
     * æ˜¯å¦æ»¡æ„: 0-否 1-是
     */
    @TableField("is_satisfied")
    private Boolean satisfied;
    /**
     * é—®é¢˜æ˜¯å¦å¤å‘: 0-否 1-是
     */
    @TableField("is_recurred")
    private Boolean recurred;
    /**
     * å›žè®¿æ—¶é—´(登记即当时)
     */
    private LocalDateTime visitTime;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/problem/AfterSaleProblemMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,101 @@
package cn.iocoder.yudao.module.aftersales.dal.mysql.problem;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
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.aftersales.dal.dataobject.problem.AfterSaleProblemDO;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleProblemStatusEnum;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * å”®åŽé—®é¢˜åº“ Mapper
 */
@Mapper
public interface AfterSaleProblemMapper extends BaseMapperX<AfterSaleProblemDO> {
    default AfterSaleProblemDO selectByNo(String no) {
        return selectOne(AfterSaleProblemDO::getNo, no);
    }
    default AfterSaleProblemDO selectBySourceTicketId(Long sourceTicketId) {
        return selectOne(AfterSaleProblemDO::getSourceTicketId, sourceTicketId);
    }
    default PageResult<AfterSaleProblemDO> selectPage(PageParam pageParam, String no, String keyword,
                                                      Long sourceTicketId, Long itemId, String itemCode,
                                                      String issueCategory,
                                                      Integer issueType, Integer rootCauseCategory, Integer status) {
        LambdaQueryWrapperX<AfterSaleProblemDO> query = new LambdaQueryWrapperX<AfterSaleProblemDO>()
                .eqIfPresent(AfterSaleProblemDO::getNo, no)
                .eqIfPresent(AfterSaleProblemDO::getSourceTicketId, sourceTicketId)
                .eqIfPresent(AfterSaleProblemDO::getItemId, itemId)
                .likeIfPresent(AfterSaleProblemDO::getItemCode, itemCode)
                .eqIfPresent(AfterSaleProblemDO::getIssueCategory, issueCategory)
                .eqIfPresent(AfterSaleProblemDO::getIssueType, issueType)
                .eqIfPresent(AfterSaleProblemDO::getRootCauseCategory, rootCauseCategory)
                .eqIfPresent(AfterSaleProblemDO::getStatus, status);
        // å…³é”®å­—检索:单号/标题/描述/解决方案
        if (StrUtil.isNotBlank(keyword)) {
            query.and(w -> w.like(AfterSaleProblemDO::getNo, keyword)
                    .or().like(AfterSaleProblemDO::getTitle, keyword)
                    .or().like(AfterSaleProblemDO::getDescription, keyword)
                    .or().like(AfterSaleProblemDO::getSolution, keyword));
        }
        query.orderByDesc(AfterSaleProblemDO::getUpdateTime);
        return selectPage(pageParam, query);
    }
    /**
     * æ–°å·¥å•受理参考检索:只返回已发布,按物料/分类/关键字命中,取最近更新 10 æ¡
     */
    default List<AfterSaleProblemDO> selectReferenceList(Long itemId, String issueCategory, Integer issueType,
                                                         Integer rootCauseCategory, String keyword) {
        LambdaQueryWrapperX<AfterSaleProblemDO> query = new LambdaQueryWrapperX<AfterSaleProblemDO>()
                .eq(AfterSaleProblemDO::getStatus, AfterSaleProblemStatusEnum.PUBLISHED.getStatus());
        query.and(w -> {
                    boolean hasCond = false;
                    if (itemId != null) {
                        w.eq(AfterSaleProblemDO::getItemId, itemId);
                        hasCond = true;
                    }
                    if (StrUtil.isNotBlank(issueCategory)) {
                        if (hasCond) {
                            w.or();
                        }
                        w.eq(AfterSaleProblemDO::getIssueCategory, issueCategory);
                        hasCond = true;
                    }
                    if (issueType != null) {
                        if (hasCond) {
                            w.or();
                        }
                        w.eq(AfterSaleProblemDO::getIssueType, issueType);
                        hasCond = true;
                    }
                    if (rootCauseCategory != null) {
                        if (hasCond) {
                            w.or();
                        }
                        w.eq(AfterSaleProblemDO::getRootCauseCategory, rootCauseCategory);
                        hasCond = true;
                    }
                    if (StrUtil.isNotBlank(keyword)) {
                        if (hasCond) {
                            w.or();
                        }
                        w.like(AfterSaleProblemDO::getTitle, keyword);
                        hasCond = true;
                    }
                    if (!hasCond) {
                        w.eq(AfterSaleProblemDO::getId, -1L); // æ— æ¡ä»¶æ—¶è¿”回空,避免全表拉取
                    }
                });
        query.orderByDesc(AfterSaleProblemDO::getUpdateTime).last("LIMIT 10");
        return selectList(query);
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/visit/AfterSaleVisitMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.aftersales.dal.mysql.visit;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
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.aftersales.dal.dataobject.visit.AfterSaleVisitDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * å”®åŽå›žè®¿è®°å½• Mapper
 */
@Mapper
public interface AfterSaleVisitMapper extends BaseMapperX<AfterSaleVisitDO> {
    default List<AfterSaleVisitDO> selectListByTicketId(Long ticketId) {
        return selectList(new LambdaQueryWrapperX<AfterSaleVisitDO>()
                .eq(AfterSaleVisitDO::getTicketId, ticketId)
                .orderByDesc(AfterSaleVisitDO::getVisitTime));
    }
    default PageResult<AfterSaleVisitDO> selectPage(PageParam pageParam, Long ticketId, Integer visitType,
                                                    Boolean satisfied) {
        return selectPage(pageParam, new LambdaQueryWrapperX<AfterSaleVisitDO>()
                .eqIfPresent(AfterSaleVisitDO::getTicketId, ticketId)
                .eqIfPresent(AfterSaleVisitDO::getVisitType, visitType)
                .eqIfPresent(AfterSaleVisitDO::getSatisfied, satisfied)
                .orderByDesc(AfterSaleVisitDO::getVisitTime));
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/no/AfterSaleNoRedisDAO.java
@@ -26,6 +26,11 @@
     */
    public static final String RETURN_NO_PREFIX = "RT";
    /**
     * é—®é¢˜è®°å½•编号前缀
     */
    public static final String PROBLEM_NO_PREFIX = "AP";
    @Resource
    private StringRedisTemplate stringRedisTemplate;
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleProblemStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.aftersales.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * å”®åŽé—®é¢˜åº“状态枚举
 */
@Getter
@AllArgsConstructor
public enum AfterSaleProblemStatusEnum implements ArrayValuable<Integer> {
    DRAFT(0, "草稿"),
    PUBLISHED(1, "已发布"),
    DISABLED(2, "已停用");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(AfterSaleProblemStatusEnum::getStatus).toArray(Integer[]::new);
    /**
     * é—®é¢˜çŠ¶æ€
     */
    private final Integer status;
    /**
     * é—®é¢˜çŠ¶æ€åç§°
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
    public static String getNameByStatus(Integer status) {
        return Arrays.stream(values())
                .filter(item -> item.getStatus().equals(status))
                .map(AfterSaleProblemStatusEnum::getName)
                .findFirst().orElse(null);
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleVisitStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.aftersales.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * å”®åŽå·¥å•回访状态枚举
 * <p>
 * å·¥å•完结(COMPLETED)后进入「待回访」;登记「满意且未复发」的回访后置为「已回访通过」,方可归档。
 */
@Getter
@AllArgsConstructor
public enum AfterSaleVisitStatusEnum implements ArrayValuable<Integer> {
    WAITING(0, "待回访"),
    PASSED(1, "已回访通过");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(AfterSaleVisitStatusEnum::getStatus).toArray(Integer[]::new);
    /**
     * å›žè®¿çŠ¶æ€
     */
    private final Integer status;
    /**
     * å›žè®¿çŠ¶æ€åç§°
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
    public static String getNameByStatus(Integer status) {
        return Arrays.stream(values())
                .filter(item -> item.getStatus().equals(status))
                .map(AfterSaleVisitStatusEnum::getName)
                .findFirst().orElse(null);
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/ErrorCodeConstants.java
@@ -52,4 +52,16 @@
    ErrorCode COMPENSATION_AUDIT_REJECT_REASON_REQUIRED = new ErrorCode(1_022_004_006, "审核不通过时必须填写不通过原因");
    ErrorCode COMPENSATION_UPDATE_FAIL_NOT_EDITABLE = new ErrorCode(1_022_004_007, "只有未提交或审核不通过状态的赔付单才能修改");
    // ========== å”®åŽæœåŠ¡-问题记录(1-022-005-000) ==========
    ErrorCode PROBLEM_NOT_EXISTS = new ErrorCode(1_022_005_000, "问题记录不存在");
    ErrorCode PROBLEM_NO_EXISTS = new ErrorCode(1_022_005_001, "问题单号重复,请重试");
    ErrorCode PROBLEM_UPDATE_FAIL_STATUS = new ErrorCode(1_022_005_002, "问题修改失败,原因:仅草稿状态可编辑");
    ErrorCode PROBLEM_DELETE_FAIL_STATUS = new ErrorCode(1_022_005_003, "问题删除失败,原因:仅草稿状态可删除");
    ErrorCode PROBLEM_PUBLISH_FAIL_STATUS = new ErrorCode(1_022_005_004, "问题发布失败,原因:当前状态不可发布");
    ErrorCode PROBLEM_DISABLE_FAIL_STATUS = new ErrorCode(1_022_005_005, "问题停用失败,原因:当前状态不可停用");
    // ========== å”®åŽæœåŠ¡-回访(1-022-006-000) ==========
    ErrorCode VISIT_NOT_EXISTS = new ErrorCode(1_022_006_000, "回访记录不存在");
    ErrorCode VISIT_CREATE_FAIL_STATUS = new ErrorCode(1_022_006_001, "登记回访失败,原因:工单不是已完结(待回访)状态");
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/problem/AfterSaleProblemService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,95 @@
package cn.iocoder.yudao.module.aftersales.service.problem;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.aftersales.controller.admin.problem.vo.AfterSaleProblemPageReqVO;
import cn.iocoder.yudao.module.aftersales.controller.admin.problem.vo.AfterSaleProblemSaveReqVO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.problem.AfterSaleProblemDO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
/**
 * å”®åŽé—®é¢˜åº“ Service æŽ¥å£
 */
public interface AfterSaleProblemService {
    /**
     * åˆ›å»ºé—®é¢˜(人工建档),初始状态草稿
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @param userId      å½“前用户编号(沉淀人)
     * @return é—®é¢˜ç¼–号
     */
    Long createProblem(@Valid AfterSaleProblemSaveReqVO createReqVO, Long userId);
    /**
     * æ›´æ–°é—®é¢˜
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateProblem(@Valid AfterSaleProblemSaveReqVO updateReqVO);
    /**
     * åˆ é™¤é—®é¢˜(仅草稿)
     *
     * @param id é—®é¢˜ç¼–号
     */
    void deleteProblem(Long id);
    /**
     * èŽ·å¾—é—®é¢˜
     *
     * @param id é—®é¢˜ç¼–号
     * @return é—®é¢˜
     */
    AfterSaleProblemDO getProblem(Long id);
    /**
     * èŽ·å¾—é—®é¢˜åˆ—è¡¨
     *
     * @param ids ç¼–号列表
     * @return é—®é¢˜åˆ—表
     */
    List<AfterSaleProblemDO> getProblemList(Collection<Long> ids);
    /**
     * èŽ·å¾—é—®é¢˜åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return é—®é¢˜åˆ†é¡µ
     */
    PageResult<AfterSaleProblemDO> getProblemPage(AfterSaleProblemPageReqVO pageReqVO);
    /**
     * å‘布问题
     *
     * @param id é—®é¢˜ç¼–号
     */
    void publishProblem(Long id);
    /**
     * åœç”¨é—®é¢˜
     *
     * @param id é—®é¢˜ç¼–号
     */
    void disableProblem(Long id);
    /**
     * æ–°å·¥å•受理参考检索:命中已发布历史问题
     *
     * @param pageReqVO å‚考检索条件(取 itemId/issueCategory/issueType/rootCauseCategory/keyword)
     * @return å·²å‘布问题列表(至多10条)
     */
    List<AfterSaleProblemDO> getReferenceList(AfterSaleProblemPageReqVO pageReqVO);
    /**
     * å·¥å•归档时自动沉淀问题(草稿态);同一工单仅沉淀一次(幂等)
     *
     * @param ticket å”®åŽå·¥å•
     * @return æ²‰æ·€å‡ºçš„问题
     */
    AfterSaleProblemDO createProblemFromTicket(AfterSaleTicketDO ticket);
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/problem/AfterSaleProblemServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,211 @@
package cn.iocoder.yudao.module.aftersales.service.problem;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.aftersales.controller.admin.problem.vo.AfterSaleProblemPageReqVO;
import cn.iocoder.yudao.module.aftersales.controller.admin.problem.vo.AfterSaleProblemSaveReqVO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.problem.AfterSaleProblemDO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketItemDO;
import cn.iocoder.yudao.module.aftersales.dal.mysql.problem.AfterSaleProblemMapper;
import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketItemMapper;
import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketMapper;
import cn.iocoder.yudao.module.aftersales.dal.redis.no.AfterSaleNoRedisDAO;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleProblemStatusEnum;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.aftersales.enums.ErrorCodeConstants.*;
/**
 * å”®åŽé—®é¢˜åº“ Service å®žçŽ°ç±»
 */
@Service
@Validated
@Slf4j
public class AfterSaleProblemServiceImpl implements AfterSaleProblemService {
    @Resource
    private AfterSaleProblemMapper problemMapper;
    @Resource
    private AfterSaleTicketMapper ticketMapper;
    @Resource
    private AfterSaleTicketItemMapper ticketItemMapper;
    @Resource
    private AfterSaleNoRedisDAO noRedisDAO;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createProblem(AfterSaleProblemSaveReqVO createReqVO, Long userId) {
        AfterSaleProblemDO problem = BeanUtils.toBean(createReqVO, AfterSaleProblemDO.class);
        // æ¥æºå·¥å•存在时补全冗余单号与默认客户
        if (createReqVO.getSourceTicketId() != null) {
            AfterSaleTicketDO ticket = ticketMapper.selectById(createReqVO.getSourceTicketId());
            if (ticket == null) {
                throw exception(TICKET_NOT_EXISTS);
            }
            problem.setSourceTicketNo(ticket.getNo());
            if (problem.getCustomerId() == null) {
                problem.setCustomerId(ticket.getCustomerId());
            }
        }
        problem.setNo(generateNo())
                .setStatus(AfterSaleProblemStatusEnum.DRAFT.getStatus())
                .setOwnerUserId(userId);
        problemMapper.insert(problem);
        // ç»‘定附件
        if (CollUtil.isNotEmpty(createReqVO.getBlobIds())) {
            storageAttachmentApi.bindAttachments("file", "aftersales_after_sale_problem",
                    problem.getId(), createReqVO.getBlobIds());
        }
        return problem.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateProblem(AfterSaleProblemSaveReqVO updateReqVO) {
        AfterSaleProblemDO problem = validateProblemExists(updateReqVO.getId());
        if (!AfterSaleProblemStatusEnum.DRAFT.getStatus().equals(problem.getStatus())) {
            throw exception(PROBLEM_UPDATE_FAIL_STATUS);
        }
        AfterSaleProblemDO update = BeanUtils.toBean(updateReqVO, AfterSaleProblemDO.class);
        problemMapper.updateById(update);
        if (CollUtil.isNotEmpty(updateReqVO.getBlobIds())) {
            storageAttachmentApi.updateAttachments("file", "aftersales_after_sale_problem",
                    updateReqVO.getId(), updateReqVO.getBlobIds());
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteProblem(Long id) {
        AfterSaleProblemDO problem = validateProblemExists(id);
        if (!AfterSaleProblemStatusEnum.DRAFT.getStatus().equals(problem.getStatus())) {
            throw exception(PROBLEM_DELETE_FAIL_STATUS);
        }
        problemMapper.deleteById(id);
        storageAttachmentApi.deleteAttachmentsByRecord("aftersales_after_sale_problem", id);
        // è§£é™¤å·¥å•对问题的关联
        ticketMapper.update(null, new LambdaUpdateWrapper<AfterSaleTicketDO>()
                .eq(AfterSaleTicketDO::getProblemId, id)
                .set(AfterSaleTicketDO::getProblemId, null)
                .set(AfterSaleTicketDO::getProblemNo, null));
    }
    @Override
    public AfterSaleProblemDO getProblem(Long id) {
        return validateProblemExists(id);
    }
    @Override
    public List<AfterSaleProblemDO> getProblemList(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return List.of();
        }
        return problemMapper.selectBatchIds(ids);
    }
    @Override
    public PageResult<AfterSaleProblemDO> getProblemPage(AfterSaleProblemPageReqVO pageReqVO) {
        return problemMapper.selectPage(pageReqVO, pageReqVO.getNo(), pageReqVO.getKeyword(),
                pageReqVO.getSourceTicketId(), pageReqVO.getItemId(), pageReqVO.getItemCode(),
                pageReqVO.getIssueCategory(),
                pageReqVO.getIssueType(), pageReqVO.getRootCauseCategory(), pageReqVO.getStatus());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void publishProblem(Long id) {
        AfterSaleProblemDO problem = validateProblemExists(id);
        if (!(AfterSaleProblemStatusEnum.DRAFT.getStatus().equals(problem.getStatus())
                || AfterSaleProblemStatusEnum.DISABLED.getStatus().equals(problem.getStatus()))) {
            throw exception(PROBLEM_PUBLISH_FAIL_STATUS);
        }
        problemMapper.updateById(new AfterSaleProblemDO().setId(id)
                .setStatus(AfterSaleProblemStatusEnum.PUBLISHED.getStatus())
                .setPublishTime(LocalDateTime.now()));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void disableProblem(Long id) {
        AfterSaleProblemDO problem = validateProblemExists(id);
        if (!AfterSaleProblemStatusEnum.PUBLISHED.getStatus().equals(problem.getStatus())) {
            throw exception(PROBLEM_DISABLE_FAIL_STATUS);
        }
        problemMapper.updateById(new AfterSaleProblemDO().setId(id)
                .setStatus(AfterSaleProblemStatusEnum.DISABLED.getStatus()));
    }
    @Override
    public List<AfterSaleProblemDO> getReferenceList(AfterSaleProblemPageReqVO pageReqVO) {
        return problemMapper.selectReferenceList(pageReqVO.getItemId(), pageReqVO.getIssueCategory(),
                pageReqVO.getIssueType(), pageReqVO.getRootCauseCategory(), pageReqVO.getKeyword());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AfterSaleProblemDO createProblemFromTicket(AfterSaleTicketDO ticket) {
        // å¹‚等:同一工单已沉淀过则直接复用
        AfterSaleProblemDO exist = problemMapper.selectBySourceTicketId(ticket.getId());
        if (exist != null) {
            return exist;
        }
        AfterSaleProblemDO problem = new AfterSaleProblemDO()
                .setNo(generateNo())
                .setTitle(ticket.getName())
                .setSourceTicketId(ticket.getId())
                .setSourceTicketNo(ticket.getNo())
                .setCustomerId(ticket.getCustomerId())
                .setIssueType(ticket.getIssueType())
                .setIssueCategory(ticket.getIssueCategory())
                .setSeverityLevel(ticket.getSeverityLevel())
                .setDescription(ticket.getIssueDescription())
                .setStatus(AfterSaleProblemStatusEnum.DRAFT.getStatus())
                .setOwnerUserId(getLoginUserId());
        // å–工单首条明细作为物料快照(允许无明细行)
        AfterSaleTicketItemDO firstItem = CollUtil.getFirst(ticketItemMapper.selectListByTicketId(ticket.getId()));
        if (firstItem != null) {
            problem.setItemId(firstItem.getItemId())
                    .setItemCode(firstItem.getItemCode())
                    .setItemName(firstItem.getItemName())
                    .setBatchId(firstItem.getBatchId())
                    .setBatchCode(firstItem.getBatchCode());
        }
        problemMapper.insert(problem);
        return problem;
    }
    // ========== å†…部方法 ==========
    private AfterSaleProblemDO validateProblemExists(Long id) {
        AfterSaleProblemDO problem = problemMapper.selectById(id);
        if (problem == null) {
            throw exception(PROBLEM_NOT_EXISTS);
        }
        return problem;
    }
    private String generateNo() {
        String no = noRedisDAO.generate(AfterSaleNoRedisDAO.PROBLEM_NO_PREFIX);
        if (problemMapper.selectByNo(no) != null) {
            throw exception(PROBLEM_NO_EXISTS);
        }
        return no;
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/visit/AfterSaleVisitService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.aftersales.service.visit;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.aftersales.controller.admin.visit.vo.AfterSaleVisitCreateReqVO;
import cn.iocoder.yudao.module.aftersales.controller.admin.visit.vo.AfterSaleVisitPageReqVO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.visit.AfterSaleVisitDO;
import jakarta.validation.Valid;
import java.util.List;
/**
 * å”®åŽå›žè®¿è®°å½• Service æŽ¥å£
 */
public interface AfterSaleVisitService {
    /**
     * ç™»è®°å›žè®¿
     * <p>
     * å·¥å•须处于已完结(待回访)状态。登记时同步推进工单:
     * æ»¡æ„ä¸”未复发 â†’ å·¥å•回访通过(可归档);不满意或复发 â†’ å·¥å•回退处理中重新处理。
     *
     * @param createReqVO ç™»è®°ä¿¡æ¯
     * @param userId      å½“前用户编号(回访人)
     * @return å›žè®¿è®°å½•编号
     */
    Long createVisit(@Valid AfterSaleVisitCreateReqVO createReqVO, Long userId);
    /**
     * èŽ·å¾—å›žè®¿è®°å½•
     *
     * @param id ç¼–号
     * @return å›žè®¿è®°å½•
     */
    AfterSaleVisitDO getVisit(Long id);
    /**
     * èŽ·å¾—æŸå·¥å•çš„å›žè®¿è®°å½•åˆ—è¡¨
     *
     * @param ticketId å·¥å•编号
     * @return å›žè®¿è®°å½•列表
     */
    List<AfterSaleVisitDO> getVisitListByTicketId(Long ticketId);
    /**
     * èŽ·å¾—å›žè®¿è®°å½•åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return å›žè®¿è®°å½•分页
     */
    PageResult<AfterSaleVisitDO> getVisitPage(AfterSaleVisitPageReqVO pageReqVO);
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/visit/AfterSaleVisitServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,106 @@
package cn.iocoder.yudao.module.aftersales.service.visit;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.aftersales.controller.admin.visit.vo.AfterSaleVisitCreateReqVO;
import cn.iocoder.yudao.module.aftersales.controller.admin.visit.vo.AfterSaleVisitPageReqVO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.visit.AfterSaleVisitDO;
import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketMapper;
import cn.iocoder.yudao.module.aftersales.dal.mysql.visit.AfterSaleVisitMapper;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleTicketStatusEnum;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleVisitStatusEnum;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.aftersales.enums.ErrorCodeConstants.TICKET_NOT_EXISTS;
import static cn.iocoder.yudao.module.aftersales.enums.ErrorCodeConstants.VISIT_CREATE_FAIL_STATUS;
import static cn.iocoder.yudao.module.aftersales.enums.ErrorCodeConstants.VISIT_NOT_EXISTS;
/**
 * å”®åŽå›žè®¿è®°å½• Service å®žçŽ°ç±»
 */
@Service
@Validated
@Slf4j
public class AfterSaleVisitServiceImpl implements AfterSaleVisitService {
    @Resource
    private AfterSaleVisitMapper visitMapper;
    @Resource
    private AfterSaleTicketMapper ticketMapper;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createVisit(AfterSaleVisitCreateReqVO createReqVO, Long userId) {
        // 1. æ ¡éªŒå·¥å•:必须存在且处于已完结(待回访)状态
        AfterSaleTicketDO ticket = ticketMapper.selectById(createReqVO.getTicketId());
        if (ticket == null) {
            throw exception(TICKET_NOT_EXISTS);
        }
        if (!AfterSaleTicketStatusEnum.COMPLETED.getStatus().equals(ticket.getStatus())) {
            throw exception(VISIT_CREATE_FAIL_STATUS);
        }
        // 2. æ’入回访记录
        AfterSaleVisitDO visitDO = BeanUtils.toBean(createReqVO, AfterSaleVisitDO.class)
                .setVisitorUserId(userId)
                .setVisitTime(LocalDateTime.now());
        visitMapper.insert(visitDO);
        // 3. ç»‘定附件
        if (CollUtil.isNotEmpty(createReqVO.getBlobIds())) {
            storageAttachmentApi.bindAttachments("file", "aftersales_after_sale_visit",
                    visitDO.getId(), createReqVO.getBlobIds());
        }
        // 4. æŽ¨è¿›å·¥å•回访状态:满意且未复发 â†’ é€šè¿‡ï¼›ä¸æ»¡æ„æˆ–复发 â†’ å›žé€€å¤„理中重新处理
        boolean satisfied = Boolean.TRUE.equals(createReqVO.getSatisfied());
        boolean recurred = Boolean.TRUE.equals(createReqVO.getRecurred());
        AfterSaleTicketDO update = new AfterSaleTicketDO().setId(ticket.getId())
                .setLastVisitTime(visitDO.getVisitTime())
                .setLastVisitSatisfied(satisfied)
                .setLastVisitRecurred(recurred);
        if (satisfied && !recurred) {
            update.setVisitStatus(AfterSaleVisitStatusEnum.PASSED.getStatus());
        } else {
            update.setStatus(AfterSaleTicketStatusEnum.PROCESSING.getStatus())
                    .setResolvedTime(null)
                    .setVisitStatus(AfterSaleVisitStatusEnum.WAITING.getStatus());
        }
        ticketMapper.updateById(update);
        return visitDO.getId();
    }
    @Override
    public AfterSaleVisitDO getVisit(Long id) {
        AfterSaleVisitDO visitDO = visitMapper.selectById(id);
        if (visitDO == null) {
            throw exception(VISIT_NOT_EXISTS);
        }
        return visitDO;
    }
    @Override
    public List<AfterSaleVisitDO> getVisitListByTicketId(Long ticketId) {
        return visitMapper.selectListByTicketId(ticketId);
    }
    @Override
    public PageResult<AfterSaleVisitDO> getVisitPage(AfterSaleVisitPageReqVO pageReqVO) {
        return visitMapper.selectPage(pageReqVO, pageReqVO.getTicketId(),
                pageReqVO.getVisitType(), pageReqVO.getSatisfied());
    }
}
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/api/employee/HrmEmployeeApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.hrm.api.employee;
import cn.iocoder.yudao.module.hrm.api.employee.dto.HrmEmployeeSimpleDTO;
import java.util.Collection;
import java.util.List;
/**
 * å‘˜å·¥æ¡£æ¡ˆ API æŽ¥å£
 *
 * <p>提供给其他模块跨模块查询员工精简信息,
 * ä¾‹å¦‚安全生产模块的特种作业证需要关联持证人。</p>
 *
 * @author è¶…级管理员
 */
public interface HrmEmployeeApi {
    /**
     * èŽ·å–å‘˜å·¥ç²¾ç®€ä¿¡æ¯
     *
     * @param id å‘˜å·¥ç¼–号
     * @return å‘˜å·¥ä¿¡æ¯ï¼Œä¸å­˜åœ¨è¿”回 null
     */
    HrmEmployeeSimpleDTO getEmployee(Long id);
    /**
     * æ‰¹é‡èŽ·å–å‘˜å·¥ç²¾ç®€ä¿¡æ¯
     *
     * @param ids å‘˜å·¥ç¼–号集合
     * @return å‘˜å·¥ä¿¡æ¯åˆ—表
     */
    List<HrmEmployeeSimpleDTO> getEmployeeList(Collection<Long> ids);
    /**
     * æ ¡éªŒå‘˜å·¥æ˜¯å¦å­˜åœ¨ï¼ˆä¸”未删除)
     *
     * @param id å‘˜å·¥ç¼–号
     * @return æ˜¯å¦å­˜åœ¨
     */
    boolean validateEmployeeExists(Long id);
}
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/api/employee/dto/HrmEmployeeSimpleDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
package cn.iocoder.yudao.module.hrm.api.employee.dto;
import lombok.Data;
/**
 * å‘˜å·¥ç²¾ç®€ä¿¡æ¯ DTO
 *
 * <p>供其他模块(如安全生产模块的特种作业证)关联员工档案时使用。</p>
 *
 * @author è¶…级管理员
 */
@Data
public class HrmEmployeeSimpleDTO {
    /**
     * å‘˜å·¥ç¼–号
     */
    private Long id;
    /**
     * å…³è”的系统用户编号
     *
     * å…³è” AdminUserDO#getId()
     */
    private Long userId;
    /**
     * å‘˜å·¥ç¼–号(工号)
     */
    private String employeeNo;
    /**
     * å‘˜å·¥å§“名
     */
    private String name;
    /**
     * éƒ¨é—¨ç¼–号
     */
    private Long deptId;
    /**
     * éƒ¨é—¨åç§°
     */
    private String deptName;
    /**
     * æ‰‹æœºå·ç 
     */
    private String phone;
    /**
     * å‘˜å·¥çŠ¶æ€ï¼š1-在职,2-试用,3-离职
     */
    private Integer employeeStatus;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/api/employee/HrmEmployeeApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,85 @@
package cn.iocoder.yudao.module.hrm.api.employee;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.hrm.api.employee.dto.HrmEmployeeSimpleDTO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
 * å‘˜å·¥æ¡£æ¡ˆ API å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
public class HrmEmployeeApiImpl implements HrmEmployeeApi {
    @Resource
    private HrmEmployeeMapper employeeMapper;
    @Resource
    private DeptApi deptApi;
    @Override
    public HrmEmployeeSimpleDTO getEmployee(Long id) {
        if (id == null) {
            return null;
        }
        HrmEmployeeDO employee = employeeMapper.selectById(id);
        if (employee == null) {
            return null;
        }
        return build(employee, getDeptName(employee.getDeptId()));
    }
    @Override
    public List<HrmEmployeeSimpleDTO> getEmployeeList(Collection<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            return List.of();
        }
        List<HrmEmployeeDO> employees = employeeMapper.selectBatchIds(ids);
        if (employees.isEmpty()) {
            return List.of();
        }
        // æ‰¹é‡æŸ¥éƒ¨é—¨ï¼Œé¿å… N+1
        List<Long> deptIds = employees.stream().map(HrmEmployeeDO::getDeptId)
                .filter(Objects::nonNull).distinct().toList();
        // ç”¨ new HashMap<>() è€Œéž Map.of():员工的 deptId å…è®¸ä¸ºç©ºï¼Œ
        // åŽé¢ deptMap.get(e.getDeptId()) ä¼š get(null),Map.of() çš„ get(null) æŠ› NPE。
        Map<Long, DeptRespDTO> deptMap = deptIds.isEmpty() ? new HashMap<>() : deptApi.getDeptMap(deptIds);
        return employees.stream()
                .map(e -> {
                    DeptRespDTO dept = deptMap.get(e.getDeptId());
                    return build(e, dept == null ? null : dept.getName());
                })
                .toList();
    }
    @Override
    public boolean validateEmployeeExists(Long id) {
        return id != null && employeeMapper.selectById(id) != null;
    }
    private String getDeptName(Long deptId) {
        if (deptId == null) {
            return null;
        }
        DeptRespDTO dept = deptApi.getDept(deptId);
        return dept == null ? null : dept.getName();
    }
    private static HrmEmployeeSimpleDTO build(HrmEmployeeDO employee, String deptName) {
        HrmEmployeeSimpleDTO dto = BeanUtils.toBean(employee, HrmEmployeeSimpleDTO.class);
        dto.setDeptName(deptName);
        return dto;
    }
}
yudao-module-mes/pom.xml
@@ -31,6 +31,13 @@
            <version>${revision}</version>
        </dependency>
        <!-- HRM äººåŠ›èµ„æº API:安全生产模块的特种作业证需关联员工档案 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-hrm-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- MDM ä¸»æ•°æ®ç®¡ç† API -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
@@ -115,6 +122,12 @@
            <version>3.3.3</version>
        </dependency>
        <!-- MQTT å®¢æˆ·ç«¯ï¼šMES æ•°é‡‡ï¼ˆcollect)模块连接设备网关 -->
        <dependency>
            <groupId>org.eclipse.paho</groupId>
            <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
        </dependency>
        <!-- Test æµ‹è¯•相关 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/MesCollectController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,103 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.collect.vo.*;
import cn.iocoder.yudao.module.mes.service.collect.MesCollectQueryService;
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.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - MES æ•°é‡‡ç›‘控")
@RestController
@RequestMapping("/mes/collect")
@Validated
public class MesCollectController {
    @Resource
    private MesCollectQueryService queryService;
    @GetMapping("/device/list")
    @Operation(summary = "获取数采设备状态列表")
    @PreAuthorize("@ss.hasPermission('mes:collect:query')")
    public CommonResult<List<MesCollectDeviceRespVO>> getDeviceList() {
        return success(queryService.getDeviceList());
    }
    @GetMapping("/realtime/get")
    @Operation(summary = "获取设备最新实时状态")
    @Parameter(name = "deviceCode", description = "数采设备编号", required = true, example = "C01")
    @PreAuthorize("@ss.hasPermission('mes:collect:query')")
    public CommonResult<MesCollectRealtimeRespVO> getRealtime(@RequestParam("deviceCode") String deviceCode) {
        return success(queryService.getRealtime(deviceCode));
    }
    @GetMapping("/realtime/history-page")
    @Operation(summary = "分页查询实时状态历史")
    @PreAuthorize("@ss.hasPermission('mes:collect:query')")
    public CommonResult<PageResult<MesCollectRealtimeRespVO>> getRealtimeHistoryPage(
            @Valid MesCollectRealtimeHistoryPageReqVO reqVO) {
        return success(queryService.getRealtimeHistoryPage(reqVO));
    }
    @GetMapping("/cycle/page")
    @Operation(summary = "分页查询周期过程数据")
    @PreAuthorize("@ss.hasPermission('mes:collect:query')")
    public CommonResult<PageResult<MesCollectCycleRespVO>> getCyclePage(@Valid MesCollectCyclePageReqVO reqVO) {
        return success(queryService.getCyclePage(reqVO));
    }
    @GetMapping("/cycle/get")
    @Operation(summary = "获取周期过程数据明细")
    @Parameter(name = "id", description = "编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('mes:collect:query')")
    public CommonResult<MesCollectCycleRespVO> getCycle(@RequestParam("id") Long id) {
        return success(queryService.getCycle(id));
    }
    @GetMapping("/tech/get")
    @Operation(summary = "获取设备最新工艺参数快照")
    @Parameter(name = "deviceCode", description = "数采设备编号", required = true, example = "C01")
    @PreAuthorize("@ss.hasPermission('mes:collect:query')")
    public CommonResult<MesCollectTechRespVO> getTech(@RequestParam("deviceCode") String deviceCode) {
        return success(queryService.getTech(deviceCode));
    }
    @GetMapping("/operate-log/page")
    @Operation(summary = "分页查询参数变更日志")
    @PreAuthorize("@ss.hasPermission('mes:collect:query')")
    public CommonResult<PageResult<MesCollectOperateLogRespVO>> getOperateLogPage(
            @Valid MesCollectOperateLogPageReqVO reqVO) {
        return success(queryService.getOperateLogPage(reqVO));
    }
    @GetMapping("/alarm/page")
    @Operation(summary = "分页查询报警记录")
    @PreAuthorize("@ss.hasPermission('mes:collect:query')")
    public CommonResult<PageResult<MesCollectAlarmRespVO>> getAlarmPage(@Valid MesCollectAlarmPageReqVO reqVO) {
        return success(queryService.getAlarmPage(reqVO));
    }
    @PutMapping("/alarm/ack")
    @Operation(summary = "人工确认消除报警")
    @Parameter(name = "id", description = "报警记录编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('mes:collect-alarm:ack')")
    public CommonResult<Boolean> ackAlarm(@RequestParam("id") Long id) {
        queryService.ackAlarm(id);
        return success(true);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectAlarmPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect.vo;
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 = "管理后台 - MES æ•°é‡‡æŠ¥è­¦åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesCollectAlarmPageReqVO extends PageParam {
    @Schema(description = "数采设备编号", example = "C01")
    private String deviceCode;
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "状态:0未消除 1已消除", example = "0")
    private Integer status;
    @Schema(description = "报警时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] wmTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectAlarmRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES æ•°é‡‡æŠ¥è­¦ Response VO")
@Data
public class MesCollectAlarmRespVO {
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "数采设备编号", example = "C01")
    private String deviceCode;
    @Schema(description = "生产任务编号", example = "1")
    private Long taskId;
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "报警标识:2产生 0消失", example = "2")
    private Integer wmId;
    @Schema(description = "报警内容", example = "安全门未关")
    private String wmMsg;
    @Schema(description = "报警时间")
    private LocalDateTime wmTime;
    @Schema(description = "状态:0未消除 1已消除", example = "0")
    private Integer status;
    @Schema(description = "状态名称", example = "未消除")
    private String statusName;
    @Schema(description = "消除时间")
    private LocalDateTime clearTime;
    @Schema(description = "报文采集时间")
    private LocalDateTime collectTime;
    @Schema(description = "完整数据 JSON")
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectCyclePageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect.vo;
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 = "管理后台 - MES æ•°é‡‡å‘¨æœŸè¿‡ç¨‹åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesCollectCyclePageReqVO extends PageParam {
    @Schema(description = "数采设备编号", example = "C01")
    private String deviceCode;
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "生产任务编号", example = "1")
    private Long taskId;
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "采集时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] collectTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectCycleRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES æ•°é‡‡å‘¨æœŸè¿‡ç¨‹æ•°æ® Response VO")
@Data
public class MesCollectCycleRespVO {
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "数采设备编号", example = "C01")
    private String deviceCode;
    @Schema(description = "生产任务编号", example = "1")
    private Long taskId;
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "周期序号(CYCN)", example = "100")
    private Long cycleNo;
    @Schema(description = "采集时间")
    private LocalDateTime collectTime;
    @Schema(description = "周期时间(ECYCT)")
    private BigDecimal ecyct;
    @Schema(description = "射出起点(EISS)")
    private BigDecimal eiss;
    @Schema(description = "最大射速(EIVM)")
    private BigDecimal eivm;
    @Schema(description = "最大射压(EIPM)")
    private BigDecimal eipm;
    @Schema(description = "转保压时间(ESIPT)")
    private BigDecimal esipt;
    @Schema(description = "转保压压力(ESIPP)")
    private BigDecimal esipp;
    @Schema(description = "转保压位置(ESIPS)")
    private BigDecimal esips;
    @Schema(description = "射出时间(EIPT)")
    private BigDecimal eipt;
    @Schema(description = "射出终点位置(EIPSE)")
    private BigDecimal eipse;
    @Schema(description = "储料时间(EPLST)")
    private BigDecimal eplst;
    @Schema(description = "储料终点(EPLSSE)")
    private BigDecimal eplsse;
    @Schema(description = "最大储料压力(EPLSPM)")
    private BigDecimal eplspm;
    @Schema(description = "生产温度 JSON(ET1..ETn)")
    private String etTemps;
    @Schema(description = "完整数据 JSON")
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectDeviceRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,65 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES æ•°é‡‡è®¾å¤‡çŠ¶æ€ Response VO")
@Data
public class MesCollectDeviceRespVO {
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "设备编码", example = "MC_20260818001")
    private String machineryCode;
    @Schema(description = "设备名称", example = "注塑机 1 å·")
    private String machineryName;
    @Schema(description = "工作站编号", example = "1")
    private Long workstationId;
    @Schema(description = "工作站名称", example = "注塑工作站")
    private String workstationName;
    @Schema(description = "数采设备编号", example = "C01")
    private String collectDeviceCode;
    @Schema(description = "是否启用数采", example = "true")
    private Boolean collectEnabled;
    @Schema(description = "最近采集时间")
    private LocalDateTime lastCollectTime;
    @Schema(description = "是否在线(5 åˆ†é’Ÿå†…有数据)", example = "true")
    private Boolean online;
    @Schema(description = "运行状态:1待机 2生产", example = "2")
    private Integer sts;
    @Schema(description = "运行状态名称", example = "生产")
    private String stsName;
    @Schema(description = "运行模式:0手动 1半自动 2电眼自动 3时间自动 4调模使用", example = "2")
    private Integer opm;
    @Schema(description = "运行模式名称", example = "电眼自动")
    private String opmName;
    @Schema(description = "油温", example = "26.5")
    private BigDecimal ot;
    @Schema(description = "各段温度 JSON")
    private String temps;
    @Schema(description = "生产任务编号", example = "1")
    private Long taskId;
    @Schema(description = "生产任务编码", example = "PT202610200001")
    private String taskCode;
    @Schema(description = "生产任务名称", example = "注塑任务")
    private String taskName;
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "生产工单编码", example = "PO202610200001")
    private String workOrderCode;
    @Schema(description = "未消除报警数量", example = "1")
    private Long activeAlarmCount;
    /**
     * æœ€è¿‘一次采集的完整数据 JSON。
     *
     * <p>设备网关上报的是中文键的瞬时值(如 {"计米":268.35,"线速度":0.0}),
     * ä¸Žæ³¨å¡‘机协议的 STS/OPM/OT ç»“构不同,结构化字段(sts/opm/ot/temps)会是空的,
     * å‰ç«¯å±•示「当前数据」需要这个原始 JSON。</p>
     */
    @Schema(description = "最近采集数据 JSON", example = "{\"计米\":268.35}")
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectOperateLogPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect.vo;
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 = "管理后台 - MES æ•°é‡‡å‚数变更分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesCollectOperateLogPageReqVO extends PageParam {
    @Schema(description = "数采设备编号", example = "C01")
    private String deviceCode;
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "变量编码", example = "SIPS")
    private String varId;
    @Schema(description = "修改时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] modifyTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectOperateLogRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES æ•°é‡‡å‚数变更 Response VO")
@Data
public class MesCollectOperateLogRespVO {
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "数采设备编号", example = "C01")
    private String deviceCode;
    @Schema(description = "生产任务编号", example = "1")
    private Long taskId;
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "变量编码", example = "SIPS")
    private String varId;
    @Schema(description = "修改前的值", example = "37")
    private String oldValue;
    @Schema(description = "修改后的值", example = "37.5")
    private String newValue;
    @Schema(description = "参数修改时间")
    private LocalDateTime modifyTime;
    @Schema(description = "报文采集时间")
    private LocalDateTime collectTime;
    @Schema(description = "完整数据 JSON")
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectRealtimeHistoryPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect.vo;
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 = "管理后台 - MES æ•°é‡‡å®žæ—¶åŽ†å²åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesCollectRealtimeHistoryPageReqVO extends PageParam {
    @Schema(description = "数采设备编号", example = "C01")
    private String deviceCode;
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "生产任务编号", example = "1")
    private Long taskId;
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "采集时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] collectTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectRealtimeRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES æ•°é‡‡å®žæ—¶æ•°æ® Response VO")
@Data
public class MesCollectRealtimeRespVO {
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "数采设备编号", example = "C01")
    private String deviceCode;
    @Schema(description = "生产任务编号", example = "1")
    private Long taskId;
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "采集时间")
    private LocalDateTime collectTime;
    @Schema(description = "运行状态:1待机 2生产", example = "2")
    private Integer sts;
    @Schema(description = "运行状态名称", example = "生产")
    private String stsName;
    @Schema(description = "运行模式:0手动 1半自动 2电眼自动 3时间自动 4调模使用", example = "2")
    private Integer opm;
    @Schema(description = "运行模式名称", example = "电眼自动")
    private String opmName;
    @Schema(description = "附加状态", example = "0")
    private Integer asts;
    @Schema(description = "油温", example = "26.5")
    private BigDecimal ot;
    @Schema(description = "各段温度 JSON")
    private String temps;
    @Schema(description = "完整数据 JSON")
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/collect/vo/MesCollectTechRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.mes.controller.admin.collect.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES æ•°é‡‡å·¥è‰ºå‚æ•° Response VO")
@Data
public class MesCollectTechRespVO {
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "数采设备编号", example = "C01")
    private String deviceCode;
    @Schema(description = "生产任务编号", example = "1")
    private Long taskId;
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "采集时间")
    private LocalDateTime collectTime;
    @Schema(description = "工艺参数完整快照 JSON")
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/spare/MesDvSpareController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.mes.controller.admin.dv.spare;
import cn.iocoder.yudao.framework.common.pojo.*;
import cn.iocoder.yudao.module.mes.controller.admin.dv.spare.vo.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.spare.MesDvSparePartDO;
import cn.iocoder.yudao.module.mes.service.dv.spare.MesDvSpareService;
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 io.swagger.v3.oas.annotations.tags.Tag;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name="管理后台 - è®¾å¤‡å¤‡ä»¶ç®¡ç†")
@RestController @RequestMapping("/mes/dv/spare") @Validated
public class MesDvSpareController {
    @Resource private MesDvSpareService service;
    @GetMapping("/page") @PreAuthorize("@ss.hasPermission('mes:dv-spare:query')")
    public CommonResult<PageResult<MesDvSparePartDO>> page(@Valid MesDvSparePageReqVO req) { return success(service.page(req)); }
    @GetMapping("/get") @PreAuthorize("@ss.hasPermission('mes:dv-spare:query')")
    public CommonResult<MesDvSparePartDO> get(@RequestParam Long id) { return success(service.get(id)); }
    @PostMapping("/create") @PreAuthorize("@ss.hasPermission('mes:dv-spare:create')")
    public CommonResult<Long> create(@Valid @RequestBody MesDvSparePartSaveReqVO req) {
        req.setId(null); return success(service.create(req,getLoginUserId()));
    }
    @PutMapping("/update") @PreAuthorize("@ss.hasPermission('mes:dv-spare:update')")
    public CommonResult<Boolean> update(@Valid @RequestBody MesDvSparePartSaveReqVO req) { service.update(req); return success(true); }
    @DeleteMapping("/delete") @PreAuthorize("@ss.hasPermission('mes:dv-spare:delete')")
    public CommonResult<Boolean> delete(@RequestParam Long id) { service.delete(id); return success(true); }
    @GetMapping("/movement-page") @PreAuthorize("@ss.hasPermission('mes:dv-spare:record')")
    public CommonResult<PageResult<MesDvSpareMovementRespVO>> movements(@Valid MesDvSparePageReqVO req) { return success(service.movements(req)); }
    @PostMapping("/receive") @PreAuthorize("@ss.hasPermission('mes:dv-spare:receive')")
    public CommonResult<Long> receive(@Valid @RequestBody MesDvSpareActionReqVO req) { return success(service.receive(req,getLoginUserId())); }
    @PostMapping("/issue") @PreAuthorize("@ss.hasPermission('mes:dv-spare:issue')")
    public CommonResult<Long> issue(@Valid @RequestBody MesDvSpareActionReqVO req) { return success(service.issue(req,getLoginUserId())); }
    @PostMapping("/return") @PreAuthorize("@ss.hasPermission('mes:dv-spare:return')")
    public CommonResult<Long> returnPart(@Valid @RequestBody MesDvSpareActionReqVO req) { return success(service.returnPart(req,getLoginUserId())); }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/spare/vo/MesDvSpareActionReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package cn.iocoder.yudao.module.mes.controller.admin.dv.spare.vo;
import jakarta.validation.constraints.*;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class MesDvSpareActionReqVO {
    @NotBlank @Pattern(regexp="[a-zA-Z0-9-]{16,64}") private String requestKey;
    @Positive private Long sparePartId;
    @Positive private Long repairId;
    @Positive private Long originalId;
    @Positive private Long receiverUserId;
    @NotNull @DecimalMin(value="0",inclusive=false) @Digits(integer=15,fraction=3) private BigDecimal quantity;
    @NotBlank @Size(max=500) private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/spare/vo/MesDvSpareMovementRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,10 @@
package cn.iocoder.yudao.module.mes.controller.admin.dv.spare.vo;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.spare.MesDvSpareMovementDO;
import java.math.BigDecimal;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data @EqualsAndHashCode(callSuper=true)
public class MesDvSpareMovementRespVO extends MesDvSpareMovementDO {
    private BigDecimal returnedQuantity;
    private BigDecimal returnableQuantity;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/spare/vo/MesDvSparePageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.mes.controller.admin.dv.spare.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.*;
@Data @EqualsAndHashCode(callSuper=true)
public class MesDvSparePageReqVO extends PageParam {
    private String code;
    private String name;
    private Integer status;
    private Boolean shortageOnly;
    private Long sparePartId;
    private Long repairId;
    private String repairCode;
    @Min(1) @Max(4) private Integer type;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/spare/vo/MesDvSparePartSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
package cn.iocoder.yudao.module.mes.controller.admin.dv.spare.vo;
import jakarta.validation.constraints.*;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class MesDvSparePartSaveReqVO {
    private Long id;
    @NotBlank @Size(max=64) private String code;
    @NotBlank @Size(max=128) private String name;
    @Size(max=255) private String specification;
    @NotBlank @Size(max=32) private String unit;
    @Size(max=128) private String location;
    @NotNull @DecimalMin("0") @Digits(integer=15,fraction=3) private BigDecimal initialQuantity;
    @NotNull @DecimalMin("0") @Digits(integer=15,fraction=3) private BigDecimal safetyQuantity;
    @NotNull @Min(0) @Max(1) private Integer status;
    @Size(max=500) private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/energy/EnergyAlertController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.mes.controller.admin.energy;
import cn.iocoder.yudao.framework.common.pojo.*;
import cn.iocoder.yudao.module.mes.controller.admin.energy.vo.EnergyAlertRuleSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.energy.*;
import cn.iocoder.yudao.module.mes.service.energy.EnergyAlertService;
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import org.springframework.format.annotation.DateTimeFormat;
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.util.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@RestController @RequestMapping("/energy/alert") @Validated
public class EnergyAlertController {
    @Resource private EnergyAlertService energyAlertService;
    @Resource private AdminUserService adminUserService;
    @GetMapping("/page") @PreAuthorize("@ss.hasPermission('energy:alert:query')")
    public CommonResult<PageResult<EnergyAlertRuleDO>> page(@Valid PageParam page) { return success(energyAlertService.page(page)); }
    @GetMapping("/logs") @PreAuthorize("@ss.hasPermission('energy:alert:query')")
    public CommonResult<PageResult<EnergyAlertLogDO>> logs(@Valid PageParam page,
            @RequestParam(required=false) @DateTimeFormat(iso=DateTimeFormat.ISO.DATE) LocalDate date,
            @RequestParam(required=false) @Pattern(regexp="SENT|FAILED") String status) {
        return success(energyAlertService.logs(page, date, status));
    }
    @GetMapping("/recipients") @PreAuthorize("@ss.hasPermission('energy:alert:query')")
    public CommonResult<List<Map<String, Object>>> recipients() {
        return success(adminUserService.getUserListByStatus(0).stream()
                .map(user -> Map.<String,Object>of("id", user.getId(), "nickname", user.getNickname())).toList());
    }
    @PostMapping("/create") @PreAuthorize("@ss.hasPermission('energy:alert:create')")
    public CommonResult<Long> create(@Valid @RequestBody EnergyAlertRuleSaveReqVO req) { req.setId(null); return success(energyAlertService.save(req)); }
    @PutMapping("/update") @PreAuthorize("@ss.hasPermission('energy:alert:update')")
    public CommonResult<Boolean> update(@Valid @RequestBody EnergyAlertRuleSaveReqVO req) {
        if (req.getId() == null) throw cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception(
                new cn.iocoder.yudao.framework.common.exception.ErrorCode(1_040_951_001, "修改预警规则必须提供编号"));
        energyAlertService.save(req); return success(true);
    }
    @DeleteMapping("/delete") @PreAuthorize("@ss.hasPermission('energy:alert:delete')")
    public CommonResult<Boolean> delete(@RequestParam Long id) { energyAlertService.delete(id); return success(true); }
    @PostMapping("/check") @PreAuthorize("@ss.hasPermission('energy:alert:check')")
    public CommonResult<Map<String,Integer>> check(@RequestParam @NotNull @PastOrPresent
            @DateTimeFormat(iso=DateTimeFormat.ISO.DATE) LocalDate date) { return success(energyAlertService.check(date)); }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/energy/EnergyRecordController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,80 @@
package cn.iocoder.yudao.module.mes.controller.admin.energy;
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.energy.vo.*;
import cn.iocoder.yudao.module.mes.service.energy.EnergyRecordService;
import io.swagger.v3.oas.annotations.Operation;
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 static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - ç‹¬ç«‹èƒ½è€—管理")
@RestController
@RequestMapping("/energy/record")
@Validated
public class EnergyRecordController {
    @Resource private cn.iocoder.yudao.module.mes.service.energy.EnergyExportService energyExportService;
    @Resource(name = "energyRecordService")
    private EnergyRecordService energyRecordService;
    @GetMapping("/export-excel")
    @PreAuthorize("@ss.hasPermission('energy:record:export')")
    public void export(@Valid EnergyRecordPageReqVO req, jakarta.servlet.http.HttpServletResponse response) throws java.io.IOException {
        energyExportService.records(req, response);
    }
    @GetMapping("/analysis-export")
    @PreAuthorize("@ss.hasPermission('energy:analysis:export')")
    public void exportAnalysis(@Valid EnergyAnalysisReqVO req, jakarta.servlet.http.HttpServletResponse response) throws java.io.IOException {
        energyExportService.analysis(req, response);
    }
    @PostMapping("/create")
    @Operation(summary = "手动新增能耗记录")
    @PreAuthorize("@ss.hasPermission('energy:record:create')")
    public CommonResult<Long> create(@Valid @RequestBody EnergyRecordSaveReqVO req) {
        return success(energyRecordService.create(req));
    }
    @PutMapping("/update")
    @Operation(summary = "修改能耗记录")
    @PreAuthorize("@ss.hasPermission('energy:record:update')")
    public CommonResult<Boolean> update(@Valid @RequestBody EnergyRecordSaveReqVO req) {
        energyRecordService.update(req);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除能耗记录")
    @PreAuthorize("@ss.hasPermission('energy:record:delete')")
    public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
        energyRecordService.delete(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "查看能耗记录")
    @PreAuthorize("@ss.hasPermission('energy:record:query')")
    public CommonResult<EnergyRecordRespVO> get(@RequestParam("id") Long id) {
        return success(BeanUtils.toBean(energyRecordService.get(id), EnergyRecordRespVO.class));
    }
    @GetMapping("/page")
    @Operation(summary = "分页查询能耗记录")
    @PreAuthorize("@ss.hasPermission('energy:record:query')")
    public CommonResult<PageResult<EnergyRecordRespVO>> page(@Valid EnergyRecordPageReqVO req) {
        return success(BeanUtils.toBean(energyRecordService.page(req), EnergyRecordRespVO.class));
    }
    @GetMapping("/analysis")
    @Operation(summary = "能耗数据分析(按能源分别汇总用量)")
    @PreAuthorize("@ss.hasPermission('energy:analysis:query')")
    public CommonResult<EnergyAnalysisRespVO> analysis(@Valid EnergyAnalysisReqVO req) {
        return success(energyRecordService.analysis(req));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/energy/vo/EnergyAlertRuleSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.mes.controller.admin.energy.vo;
import jakarta.validation.constraints.*;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class EnergyAlertRuleSaveReqVO {
    private Long id;
    @NotBlank @Size(max = 100) private String name;
    @NotNull @Pattern(regexp = "WATER|ELECTRICITY|STEAM") private String energyType;
    @Size(max = 100) private String usageArea;
    @Size(max = 50) private String productCategory;
    @NotNull @Pattern(regexp = "QUANTITY|AMOUNT|MONTH_QUANTITY|MONTH_AMOUNT") private String metric;
    @NotNull @DecimalMin(value = "0", inclusive = false) @Digits(integer = 26, fraction = 3)
    private BigDecimal threshold;
    @NotNull private Boolean enabled;
    @NotEmpty @Size(max = 50) private List<@NotNull @Positive Long> recipientIds;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/energy/vo/EnergyAnalysisReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.mes.controller.admin.energy.vo;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
@Data
public class EnergyAnalysisReqVO {
    @NotNull
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate startDate;
    @NotNull
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate endDate;
    @Pattern(regexp = "WATER|ELECTRICITY|STEAM")
    private String energyType;
    private String usageArea;
    private String productCategory;
    @NotNull
    @Pattern(regexp = "DAY|MONTH")
    private String groupBy = "DAY";
    @AssertTrue(message = "统计日期范围必须在两年以内,且开始日期不能晚于结束日期")
    public boolean isDateRangeValid() {
        return startDate == null || endDate == null
                || (!startDate.isAfter(endDate) && !endDate.isAfter(startDate.plusYears(2)));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/energy/vo/EnergyAnalysisRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.mes.controller.admin.energy.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Data
public class EnergyAnalysisRespVO {
    private BigDecimal totalAmount = BigDecimal.ZERO;
    private long recordCount;
    private List<Summary> summary = new ArrayList<>();
    private List<Bucket> trend = new ArrayList<>();
    private List<Bucket> areas = new ArrayList<>();
    @Data
    public static class Summary {
        private String energyType;
        private BigDecimal quantity = BigDecimal.ZERO;
        private BigDecimal amount = BigDecimal.ZERO;
        private long recordCount;
    }
    @Data
    public static class Bucket extends Summary {
        /** è¶‹åŠ¿ä¸ºæ—¥æœŸ/月,区域分布为区域名称。 */
        private String label;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/energy/vo/EnergyRecordPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.mes.controller.admin.energy.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
@Data
@EqualsAndHashCode(callSuper = true)
public class EnergyRecordPageReqVO extends PageParam {
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate startDate;
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate endDate;
    @Pattern(regexp = "WATER|ELECTRICITY|STEAM")
    private String energyType;
    private String usageArea;
    private String productCategory;
    @AssertTrue(message = "开始日期不能晚于结束日期")
    public boolean isDateRangeValid() {
        return startDate == null || endDate == null || !startDate.isAfter(endDate);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/energy/vo/EnergyRecordRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.mes.controller.admin.energy.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
public class EnergyRecordRespVO {
    private Long id;
    private LocalDate recordDate;
    private String energyType;
    private String usageArea;
    private String productCategory;
    private String usagePurpose;
    private BigDecimal quantity;
    private BigDecimal unitPrice;
    private BigDecimal amount;
    private String recorder;
    private String remark;
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/energy/vo/EnergyRecordSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.mes.controller.admin.energy.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
@Data
@Schema(description = "管理后台 - èƒ½è€—记录新增/修改")
public class EnergyRecordSaveReqVO {
    @Positive
    private Long id;
    @NotNull(message = "统计日期不能为空")
    @PastOrPresent(message = "统计日期不能晚于今天")
    private LocalDate recordDate;
    @NotBlank
    @Pattern(regexp = "WATER|ELECTRICITY|STEAM", message = "能源类型只能为水、电、蒸汽")
    private String energyType;
    @NotBlank(message = "使用区域不能为空")
    @Size(max = 100)
    private String usageArea;
    @NotBlank(message = "产品类别不能为空")
    @Size(max = 50)
    private String productCategory;
    @NotBlank(message = "用途不能为空")
    @Size(max = 100)
    private String usagePurpose;
    @NotNull
    @DecimalMin(value = "0", inclusive = false, message = "用量必须大于零")
    @Digits(integer = 15, fraction = 3, message = "用量最多15位整数、3位小数")
    private BigDecimal quantity;
    @NotNull
    @DecimalMin(value = "0", message = "单价不能为负数")
    @Digits(integer = 8, fraction = 4, message = "单价最多8位整数、4位小数")
    private BigDecimal unitPrice;
    @NotBlank(message = "录入人不能为空")
    @Size(max = 50)
    private String recorder;
    @Size(max = 500)
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ledger/MesQcInspectionLedgerController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,113 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.ledger;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ledger.vo.MesQcInspectionLedgerExportVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ledger.vo.MesQcInspectionLedgerPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ledger.vo.MesQcInspectionLedgerRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.vo.MesQcQualityExportVO;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
import cn.iocoder.yudao.module.mes.service.qc.ledger.MesQcInspectionLedgerService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - MES æ£€æµ‹ç»“果台账")
@RestController
@RequestMapping("/mes/qc/inspection-ledger")
@Validated
public class MesQcInspectionLedgerController {
    @Resource
    private MesQcInspectionLedgerService ledgerService;
    @GetMapping("/page")
    @Operation(summary = "获得检测结果台账分页(四类检验单统一查询)")
    @PreAuthorize("@ss.hasPermission('mes:qc-inspection-ledger:query')")
    public CommonResult<PageResult<MesQcInspectionLedgerRespVO>> getInspectionLedgerPage(
            @Valid MesQcInspectionLedgerPageReqVO pageReqVO) {
        return success(ledgerService.getInspectionLedgerPage(pageReqVO));
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出检测结果台账 Excel")
    @PreAuthorize("@ss.hasPermission('mes:qc-inspection-ledger:export')")
    @ApiAccessLog(operateType = EXPORT)
    public void exportInspectionLedgerExcel(@Valid MesQcInspectionLedgerPageReqVO pageReqVO,
                                            HttpServletResponse response) throws IOException {
        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        List<MesQcInspectionLedgerRespVO> list = ledgerService.getInspectionLedgerPage(pageReqVO).getList();
        ExcelUtils.write(response, "检测结果台账.xls", "检测结果台账",
                MesQcInspectionLedgerExportVO.class, buildExportList(list));
    }
    private List<MesQcInspectionLedgerExportVO> buildExportList(List<MesQcInspectionLedgerRespVO> list) {
        if (CollUtil.isEmpty(list)) {
            return new ArrayList<>();
        }
        List<MesQcInspectionLedgerExportVO> exportList = new ArrayList<>(list.size());
        for (MesQcInspectionLedgerRespVO vo : list) {
            MesQcInspectionLedgerExportVO export = new MesQcInspectionLedgerExportVO();
            export.setQcTypeName(qcTypeName(vo.getQcType()));
            export.setCode(vo.getCode());
            export.setName(vo.getName());
            export.setItemCode(vo.getItemCode());
            export.setItemName(vo.getItemName());
            export.setItemSpecification(vo.getItemSpecification());
            export.setBatchNo(vo.getBatchNo());
            export.setIndicatorName(vo.getIndicatorName());
            export.setIndicatorValue(vo.getIndicatorValue());
            export.setJudgementBasis(vo.getJudgementBasis());
            export.setCheckResultText(parseDictLabel(DictTypeConstants.MES_QC_CHECK_RESULT, vo.getCheckResult()));
            export.setCheckQuantity(MesQcQualityExportVO.formatDecimal(vo.getCheckQuantity()));
            export.setQualifiedQuantity(MesQcQualityExportVO.formatDecimal(vo.getQualifiedQuantity()));
            export.setUnqualifiedQuantity(MesQcQualityExportVO.formatDecimal(vo.getUnqualifiedQuantity()));
            export.setInspectorName(vo.getInspectorName());
            export.setInspectDate(MesQcQualityExportVO.formatDateTime(vo.getInspectDate()));
            export.setStatusText(parseDictLabel(DictTypeConstants.MES_ORDER_STATUS, vo.getStatus()));
            export.setRemark(vo.getRemark());
            exportList.add(export);
        }
        return exportList;
    }
    private static String qcTypeName(Integer qcType) {
        for (MesQcTypeEnum e : MesQcTypeEnum.values()) {
            if (Objects.equals(e.getType(), qcType)) {
                return e.getName();
            }
        }
        return "";
    }
    private static String parseDictLabel(String dictType, Integer value) {
        if (value == null) {
            return "";
        }
        String label = DictFrameworkUtils.parseDictDataLabel(dictType, String.valueOf(value));
        return StrUtil.nullToEmpty(label);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ledger/vo/MesQcInspectionLedgerExportVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,70 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.ledger.vo;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.Data;
/**
 * MES æ£€æµ‹ç»“果台账导出 Excel VO
 *
 * <p>一行 = ä¸€ä¸ªæ£€æµ‹é¡¹ç»“果(检验单未录入检测项时该单占一行,检测项相关列为空),用于查询、打印与存档。
 *
 * @author è¶…级管理员
 */
@Data
public class MesQcInspectionLedgerExportVO {
    @ExcelProperty("分类")
    private String qcTypeName;
    @ExcelProperty("检验单编号")
    private String code;
    @ExcelProperty("检验单名称")
    private String name;
    @ExcelProperty("物料编码")
    private String itemCode;
    @ExcelProperty("物料名称")
    private String itemName;
    @ExcelProperty("规格型号")
    private String itemSpecification;
    @ExcelProperty("批次号")
    private String batchNo;
    @ExcelProperty("检测项")
    private String indicatorName;
    @ExcelProperty("检测值")
    private String indicatorValue;
    @ExcelProperty("判定依据")
    private String judgementBasis;
    @ExcelProperty("检测结果")
    private String checkResultText;
    @ExcelProperty("检测数量")
    private String checkQuantity;
    @ExcelProperty("合格数量")
    private String qualifiedQuantity;
    @ExcelProperty("不合格数量")
    private String unqualifiedQuantity;
    @ExcelProperty("检测人员")
    private String inspectorName;
    @ExcelProperty("检测日期")
    private String inspectDate;
    @ExcelProperty("单据状态")
    private String statusText;
    @ExcelProperty("备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ledger/vo/MesQcInspectionLedgerPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.ledger.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
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 = "管理后台 - MES æ£€æµ‹ç»“果台账分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class MesQcInspectionLedgerPageReqVO extends PageParam {
    @Schema(description = "分类(检验类型):1 æ¥æ–™æ£€éªŒ / 2 è¿‡ç¨‹æ£€éªŒ / 3 å‡ºè´§æ£€éªŒ / 4 é€€è´§æ£€éªŒï¼Œä¸ä¼ ä¸ºå…¨éƒ¨", example = "1")
    private Integer qcType;
    @Schema(description = "检测日期区间,传 [开始时间, ç»“束时间]",
            example = "[\"2026-01-01 00:00:00\", \"2026-12-31 23:59:59\"]")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] inspectDate;
    @Schema(description = "检验单编号(模糊匹配)", example = "IQC1787563511197")
    private String code;
    @Schema(description = "物料编号", example = "28")
    private Long itemId;
    @Schema(description = "批次号(模糊匹配;来料检验取供应商批次号,出货/退货检验取批次号)", example = "20260920a")
    private String batchNo;
    @Schema(description = "检测结果", example = "1")
    private Integer checkResult;
    @Schema(description = "检测人员编号", example = "1")
    private Long inspectorUserId;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ledger/vo/MesQcInspectionLedgerRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.ledger.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES æ£€æµ‹ç»“果台账 Response VO")
@Data
public class MesQcInspectionLedgerRespVO {
    // ========== æ£€éªŒå• ==========
    @Schema(description = "分类(检验类型):1 æ¥æ–™æ£€éªŒ / 2 è¿‡ç¨‹æ£€éªŒ / 3 å‡ºè´§æ£€éªŒ / 4 é€€è´§æ£€éªŒï¼›å­—å…¸ mes_qc_type", example = "1")
    private Integer qcType;
    @Schema(description = "检验单 ID", example = "1")
    private Long qcId;
    @Schema(description = "检验单编号", example = "IQC1787563511197")
    private String code;
    @Schema(description = "检验单名称", example = "AN1787563511157-物料检验")
    private String name;
    @Schema(description = "单据状态;字典 mes_order_status", example = "4")
    private Integer status;
    // ========== ç‰©æ–™ ==========
    @Schema(description = "物料编号", example = "28")
    private Long itemId;
    @Schema(description = "物料编码", example = "WL0001")
    private String itemCode;
    @Schema(description = "物料名称", example = "物料A")
    private String itemName;
    @Schema(description = "规格型号", example = "100mm*50mm")
    private String itemSpecification;
    @Schema(description = "计量单位名称", example = "个")
    private String unitName;
    @Schema(description = "批次号", example = "20260920a")
    private String batchNo;
    // ========== æ•°é‡ä¸Žç»“æžœ ==========
    @Schema(description = "检测数量", example = "100.00")
    private BigDecimal checkQuantity;
    @Schema(description = "合格数量", example = "99.00")
    private BigDecimal qualifiedQuantity;
    @Schema(description = "不合格数量", example = "1.00")
    private BigDecimal unqualifiedQuantity;
    @Schema(description = "检测结果;字典 mes_qc_check_result", example = "1")
    private Integer checkResult;
    @Schema(description = "检测日期")
    private LocalDateTime inspectDate;
    @Schema(description = "检测人员编号", example = "1")
    private Long inspectorUserId;
    @Schema(description = "检测人员昵称", example = "张三")
    private String inspectorName;
    // ========== æ£€æµ‹é¡¹ç»“果(一条明细一行;检验单未录入检测项时该组字段为空) ==========
    @Schema(description = "检测项编号", example = "2")
    private Long indicatorId;
    @Schema(description = "检测项名称", example = "长度")
    private String indicatorName;
    @Schema(description = "检测值", example = "15")
    private String indicatorValue;
    @Schema(description = "检测项备注", example = "抽检")
    private String indicatorRemark;
    @Schema(description = "判定依据,由检测项标准值/误差上下限拼装", example = "10~20")
    private String judgementBasis;
    @Schema(description = "检测项标准值(判定依据原始值,前端展示用 judgementBasis å³å¯ï¼‰", example = "15")
    private BigDecimal standardValue;
    @Schema(description = "检测项误差下限(判定依据原始值)", example = "10")
    private BigDecimal minThreshold;
    @Schema(description = "检测项误差上限(判定依据原始值)", example = "20")
    private BigDecimal maxThreshold;
    @Schema(description = "检验单备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/vo/MesQcImportRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
 * ç®¡ç†åŽå° - è´¨æ£€å•导入 Response VO(IQC / IPQC / OQC å…±ç”¨ï¼‰
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - è´¨æ£€å•导入 Response VO")
@Data
@Builder
public class MesQcImportRespVO {
    @Schema(description = "导入成功的检验单编号数组", example = "IQC20260101000001")
    private List<String> createList;
    @Schema(description = "导入失败集合,key ä¸ºè¡Œæ ‡è¯†ï¼ˆæ£€éªŒå•编号或第 N è¡Œï¼‰ï¼Œvalue ä¸ºå¤±è´¥åŽŸå› ")
    private Map<String, String> failureList;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/vo/MesQcQualityExportVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,92 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.vo;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
 * MES è´¨æ£€æ£€éªŒå•导出 Excel VO(IQC/IPQC/OQC/RQC å…±ç”¨ï¼‰
 *
 * <p>导出列:检验单名称 / ç‰©æ–™åç§° / æ‰¹å· / æ£€æµ‹æ—¥æœŸ / æ£€éªŒæŒ‡æ ‡ / åˆ¤å®šä¾æ® / æ£€éªŒç»“æžœ / æ£€éªŒäºº / å®¡æ ¸äºº / å¤‡æ³¨ã€‚
 * æ£€éªŒæŒ‡æ ‡ã€åˆ¤å®šä¾æ®å–自检验单行明细;审核人暂无数据来源,导出为空列。
 *
 * @author è¶…级管理员
 */
@Data
public class MesQcQualityExportVO {
    @ExcelProperty("检验单名称")
    private String docName;
    @ExcelProperty("物料名称")
    private String itemName;
    @ExcelProperty("批号")
    private String batchNo;
    @ExcelProperty("检测日期")
    private String inspectDate;
    @ExcelProperty("检验指标")
    private String indicatorNames;
    @ExcelProperty("判定依据")
    private String judgementBasis;
    @ExcelProperty("检验结果")
    private String checkResultText;
    @ExcelProperty("检验人")
    private String inspectorName;
    @ExcelProperty("审核人")
    private String auditorName;
    @ExcelProperty("备注")
    private String remark;
    /**
     * æ ¼å¼åŒ–检测日期(仅日期),null è¿”回空串
     */
    public static String formatDateTime(LocalDateTime dateTime) {
        if (dateTime == null) {
            return "";
        }
        return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    }
    /**
     * åŽ»æŽ‰æ•°å€¼å°¾éƒ¨æ— æ„ä¹‰çš„ 0,如 35.000000 -> 35、2.500000 -> 2.5
     */
    public static String formatDecimal(BigDecimal value) {
        if (value == null) {
            return "";
        }
        return value.stripTrailingZeros().toPlainString();
    }
    /**
     * ç»„装单个检测项的判定依据文本:
     * ä¸Šä¸‹é™å‡æœ‰ -> "下限~上限";仅有上限 -> "≤上限";仅有下限 -> "≥下限";
     * å‡æ— ä½†ç»™äº†æ ‡å‡†å€¼ -> "标准值 æ ‡å‡†å€¼";否则返回空串。
     */
    public static String buildRangeText(BigDecimal standardValue, BigDecimal minThreshold, BigDecimal maxThreshold) {
        if (maxThreshold != null && minThreshold != null) {
            return formatDecimal(minThreshold) + "~" + formatDecimal(maxThreshold);
        }
        if (maxThreshold != null) {
            return "≤" + formatDecimal(maxThreshold);
        }
        if (minThreshold != null) {
            return "≥" + formatDecimal(minThreshold);
        }
        if (standardValue != null) {
            return "标准值 " + formatDecimal(standardValue);
        }
        return "";
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/SafetyCertificateController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,98 @@
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.cert.SafetyCertificatePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.cert.SafetyCertificateRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.cert.SafetyCertificateSaveReqVO;
import cn.iocoder.yudao.module.mes.service.safety.SafetyCertificateService;
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.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰¹ç§ä½œä¸šè¯å°è´¦ï¼ˆå«åˆ°æœŸè‡ªåŠ¨é¢„è­¦ï¼‰
 *
 * @author è¶…级管理员
 */
@Tag(name = "管理后台 - å®‰å…¨ç”Ÿäº§-特种作业证")
@RestController
@RequestMapping("/safety/certificate")
@Validated
public class SafetyCertificateController {
    @Resource
    private SafetyCertificateService certificateService;
    @PostMapping("/create")
    @Operation(summary = "新增特种作业证")
    @PreAuthorize("@ss.hasPermission('safety:cert:create')")
    public CommonResult<Long> create(@Valid @RequestBody SafetyCertificateSaveReqVO reqVO) {
        return success(certificateService.createCertificate(reqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "修改特种作业证")
    @PreAuthorize("@ss.hasPermission('safety:cert:update')")
    public CommonResult<Boolean> update(@Valid @RequestBody SafetyCertificateSaveReqVO reqVO) {
        certificateService.updateCertificate(reqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除特种作业证")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:cert:delete')")
    public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
        certificateService.deleteCertificate(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得特种作业证")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:cert:query')")
    public CommonResult<SafetyCertificateRespVO> get(@RequestParam("id") Long id) {
        var cert = certificateService.getCertificate(id);
        var resp = BeanUtils.toBean(cert, SafetyCertificateRespVO.class);
        certificateService.fillDisplayFields(cert, resp);
        return success(resp);
    }
    @GetMapping("/page")
    @Operation(summary = "获得特种作业证分页")
    @PreAuthorize("@ss.hasPermission('safety:cert:query')")
    public CommonResult<PageResult<SafetyCertificateRespVO>> page(@Valid SafetyCertificatePageReqVO reqVO) {
        var page = certificateService.getCertificatePage(reqVO);
        // æ‰¹é‡è¡¥å…¨æŒè¯äººæœ€æ–°æ¡£æ¡ˆï¼ˆå§“名/工号/部门),一次性查好避免 N+1
        certificateService.fillEmployeeInfo(page.getList());
        var result = BeanUtils.toBean(page, SafetyCertificateRespVO.class);
        certificateService.fillDisplayFieldsBatch(page.getList(), result.getList());
        return success(result);
    }
    @GetMapping("/expiring-list")
    @Operation(summary = "获得即将到期/已过期的特种作业证(到期自动预警)")
    @Parameter(name = "days", description = "提前预警天数,默认 30 å¤©")
    @PreAuthorize("@ss.hasPermission('safety:cert:query')")
    public CommonResult<List<SafetyCertificateRespVO>> expiringList(
            @RequestParam(value = "days", required = false) Integer days) {
        List<cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyCertificateDO> certs =
                certificateService.getExpiringCertificates(days);
        certificateService.fillEmployeeInfo(certs);
        var resps = BeanUtils.toBean(certs, SafetyCertificateRespVO.class);
        certificateService.fillDisplayFieldsBatch(certs, resps);
        return success(resps);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/SafetyFileController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mes.service.safety.SafetyFileService;
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 org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * å®‰å…¨ç”Ÿäº§ - ç§»åŠ¨ç«¯çŽ°åœºæ‹ç…§ä¸Šä¼ 
 *
 * <p>巡检、隐患上报、作业票、演练等环节的照片统一走此接口,
 * è¿”回可访问 URL,前端拼成逗号串写入业务表。</p>
 *
 * @author è¶…级管理员
 */
@Tag(name = "管理后台 - å®‰å…¨ç”Ÿäº§-附件上传")
@RestController
@RequestMapping("/safety/file")
@Validated
public class SafetyFileController {
    @Resource
    private SafetyFileService fileService;
    @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @Operation(summary = "上传单个附件(移动端拍照)")
    @Parameter(name = "file", description = "文件", required = true)
    @PreAuthorize("@ss.hasPermission('safety:file:upload')")
    public CommonResult<String> upload(@RequestPart("file") MultipartFile file,
                                       @RequestParam(value = "bizType", required = false) String bizType) {
        return success(fileService.upload(file, bizType));
    }
    @PostMapping(value = "/upload-batch", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @Operation(summary = "批量上传附件(移动端一次多张)")
    @PreAuthorize("@ss.hasPermission('safety:file:upload')")
    public CommonResult<List<String>> uploadBatch(@RequestPart("files") List<MultipartFile> files,
                                                  @RequestParam(value = "bizType", required = false) String bizType) {
        return success(fileService.uploadBatch(files, bizType));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/SafetyHazardController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,106 @@
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.hazard.*;
import cn.iocoder.yudao.module.mes.service.safety.SafetyHazardService;
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 static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * å®‰å…¨ç”Ÿäº§ - é𐿂£ä¸ŠæŠ¥ä¸Žé—­çŽ¯ï¼ˆä¸ŠæŠ¥-分派-整改-复查)
 *
 * @author è¶…级管理员
 */
@Tag(name = "管理后台 - å®‰å…¨ç”Ÿäº§-隐患闭环")
@RestController
@RequestMapping("/safety/hazard")
@Validated
public class SafetyHazardController {
    @Resource
    private SafetyHazardService hazardService;
    @PostMapping("/create")
    @Operation(summary = "隐患上报(支持移动端拍照上传的 photoUrls)")
    @PreAuthorize("@ss.hasPermission('safety:hazard:create')")
    public CommonResult<Long> create(@Valid @RequestBody SafetyHazardSaveReqVO reqVO) {
        return success(hazardService.createHazard(reqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "修改隐患基础信息")
    @PreAuthorize("@ss.hasPermission('safety:hazard:update')")
    public CommonResult<Boolean> update(@Valid @RequestBody SafetyHazardSaveReqVO reqVO) {
        hazardService.updateHazard(reqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除隐患")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:hazard:delete')")
    public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
        hazardService.deleteHazard(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得隐患")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:hazard:query')")
    public CommonResult<SafetyHazardRespVO> get(@RequestParam("id") Long id) {
        var hazard = hazardService.getHazard(id);
        hazardService.fillPersonNames(java.util.List.of(hazard));
        var resp = BeanUtils.toBean(hazard, SafetyHazardRespVO.class);
        SafetyHazardService.fillHazardDisplay(hazard, resp);
        return success(resp);
    }
    @GetMapping("/page")
    @Operation(summary = "获得隐患分页")
    @PreAuthorize("@ss.hasPermission('safety:hazard:query')")
    public CommonResult<PageResult<SafetyHazardRespVO>> page(@Valid SafetyHazardPageReqVO reqVO) {
        var page = hazardService.getHazardPage(reqVO);
        hazardService.fillPersonNames(page.getList());
        var result = BeanUtils.toBean(page, SafetyHazardRespVO.class);
        for (int i = 0; i < page.getList().size(); i++) {
            SafetyHazardService.fillHazardDisplay(page.getList().get(i), result.getList().get(i));
        }
        return success(result);
    }
    @PutMapping("/assign")
    @Operation(summary = "分派隐患整改责任人")
    @PreAuthorize("@ss.hasPermission('safety:hazard:assign')")
    public CommonResult<Boolean> assign(@Valid @RequestBody SafetyHazardAssignReqVO reqVO) {
        hazardService.assignHazard(reqVO);
        return success(true);
    }
    @PutMapping("/rectify")
    @Operation(summary = "提交隐患整改结果")
    @PreAuthorize("@ss.hasPermission('safety:hazard:rectify')")
    public CommonResult<Boolean> rectify(@Valid @RequestBody SafetyHazardRectifyReqVO reqVO) {
        hazardService.rectifyHazard(reqVO);
        return success(true);
    }
    @PutMapping("/recheck")
    @Operation(summary = "隐患复查(通过则闭环,不通过退回整改)")
    @PreAuthorize("@ss.hasPermission('safety:hazard:recheck')")
    public CommonResult<Boolean> recheck(@Valid @RequestBody SafetyHazardRecheckReqVO reqVO) {
        hazardService.recheckHazard(reqVO);
        return success(true);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/SafetyInspectionController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,162 @@
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.inspection.*;
import cn.iocoder.yudao.module.mes.service.safety.SafetyInspectionService;
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.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * å®‰å…¨ç”Ÿäº§ - å·¡æ£€ç‚¹ä½ä¸Žå·¡æ£€è®°å½•
 *
 * @author è¶…级管理员
 */
@Tag(name = "管理后台 - å®‰å…¨ç”Ÿäº§-安全巡检")
@RestController
@RequestMapping("/safety/inspection")
@Validated
public class SafetyInspectionController {
    @Resource
    private SafetyInspectionService inspectionService;
    // ==================== å·¡æ£€ç‚¹ä½ ====================
    @PostMapping("/point/create")
    @Operation(summary = "新增巡检点位")
    @PreAuthorize("@ss.hasPermission('safety:inspection-point:create')")
    public CommonResult<Long> createPoint(@Valid @RequestBody SafetyInspectionPointSaveReqVO reqVO) {
        return success(inspectionService.createPoint(reqVO));
    }
    @PutMapping("/point/update")
    @Operation(summary = "修改巡检点位")
    @PreAuthorize("@ss.hasPermission('safety:inspection-point:update')")
    public CommonResult<Boolean> updatePoint(@Valid @RequestBody SafetyInspectionPointSaveReqVO reqVO) {
        inspectionService.updatePoint(reqVO);
        return success(true);
    }
    @DeleteMapping("/point/delete")
    @Operation(summary = "删除巡检点位")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:inspection-point:delete')")
    public CommonResult<Boolean> deletePoint(@RequestParam("id") Long id) {
        inspectionService.deletePoint(id);
        return success(true);
    }
    @GetMapping("/point/get")
    @Operation(summary = "获得巡检点位")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:inspection-point:query')")
    public CommonResult<SafetyInspectionPointRespVO> getPoint(@RequestParam("id") Long id) {
        var point = inspectionService.getPoint(id);
        // å•条查询也要回填主数据最新值,不能只返回快照
        inspectionService.fillPointRefs(java.util.List.of(point));
        var resp = BeanUtils.toBean(point, SafetyInspectionPointRespVO.class);
        SafetyInspectionService.fillPointDisplay(point, resp);
        resp.setLastInspectTime(inspectionService.getLastInspectTime(id));
        return success(resp);
    }
    @GetMapping("/point/page")
    @Operation(summary = "获得巡检点位分页")
    @PreAuthorize("@ss.hasPermission('safety:inspection-point:query')")
    public CommonResult<PageResult<SafetyInspectionPointRespVO>> getPointPage(
            @Valid SafetyInspectionPointPageReqVO reqVO) {
        var page = inspectionService.getPointPage(reqVO);
        inspectionService.fillPointRefs(page.getList());
        var result = BeanUtils.toBean(page, SafetyInspectionPointRespVO.class);
        for (int i = 0; i < page.getList().size(); i++) {
            SafetyInspectionService.fillPointDisplay(page.getList().get(i), result.getList().get(i));
        }
        return success(result);
    }
    @GetMapping("/point/simple-list")
    @Operation(summary = "获得启用中的巡检点位精简列表(移动端巡检选择)")
    @PreAuthorize("@ss.hasPermission('safety:inspection-point:query')")
    public CommonResult<List<SafetyInspectionPointRespVO>> getPointSimpleList() {
        var points = inspectionService.getEnabledPointList();
        inspectionService.fillPointRefs(points);
        List<SafetyInspectionPointRespVO> list = points.stream()
                .map(point -> {
                    var resp = BeanUtils.toBean(point, SafetyInspectionPointRespVO.class);
                    SafetyInspectionService.fillPointDisplay(point, resp);
                    return resp;
                }).toList();
        return success(list);
    }
    // ==================== å·¡æ£€è®°å½• ====================
    @PostMapping("/record/create")
    @Operation(summary = "新增巡检记录")
    @PreAuthorize("@ss.hasPermission('safety:inspection:create')")
    public CommonResult<Long> createRecord(@Valid @RequestBody SafetyInspectionSaveReqVO reqVO) {
        return success(inspectionService.createInspection(reqVO));
    }
    @PutMapping("/record/update")
    @Operation(summary = "修改巡检记录")
    @PreAuthorize("@ss.hasPermission('safety:inspection:update')")
    public CommonResult<Boolean> updateRecord(@Valid @RequestBody SafetyInspectionSaveReqVO reqVO) {
        inspectionService.updateInspection(reqVO);
        return success(true);
    }
    @DeleteMapping("/record/delete")
    @Operation(summary = "删除巡检记录")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:inspection:delete')")
    public CommonResult<Boolean> deleteRecord(@RequestParam("id") Long id) {
        inspectionService.deleteInspection(id);
        return success(true);
    }
    @GetMapping("/record/get")
    @Operation(summary = "获得巡检记录")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:inspection:query')")
    public CommonResult<SafetyInspectionRespVO> getRecord(@RequestParam("id") Long id) {
        var record = inspectionService.getInspection(id);
        inspectionService.fillInspectorNames(java.util.List.of(record));
        var resp = BeanUtils.toBean(record, SafetyInspectionRespVO.class);
        resp.setPointTypeName(cn.iocoder.yudao.module.mes.enums.safety.SafetyInspectionPointTypeEnum
                .getNameByType(record.getPointType()));
        resp.setResultName("NORMAL".equals(record.getResult()) ? "正常" : "异常");
        return success(resp);
    }
    @GetMapping("/record/page")
    @Operation(summary = "获得巡检记录分页")
    @PreAuthorize("@ss.hasPermission('safety:inspection:query')")
    public CommonResult<PageResult<SafetyInspectionRespVO>> getRecordPage(
            @Valid SafetyInspectionPageReqVO reqVO) {
        var page = inspectionService.getInspectionPage(reqVO);
        inspectionService.fillInspectorNames(page.getList());
        var result = BeanUtils.toBean(page, SafetyInspectionRespVO.class);
        for (int i = 0; i < page.getList().size(); i++) {
            var record = page.getList().get(i);
            var resp = result.getList().get(i);
            resp.setPointTypeName(cn.iocoder.yudao.module.mes.enums.safety.SafetyInspectionPointTypeEnum
                    .getNameByType(record.getPointType()));
            resp.setResultName("NORMAL".equals(record.getResult()) ? "正常" : "异常");
        }
        return success(result);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/SafetyLedgerController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ledger.SafetyLedgerExportReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ledger.SafetyOverviewRespVO;
import cn.iocoder.yudao.module.mes.service.safety.SafetyLedgerExportService;
import cn.iocoder.yudao.module.mes.service.safety.SafetyOverviewService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * å®‰å…¨ç”Ÿäº§ - æ€»è§ˆç»Ÿè®¡ä¸Žæ ‡å‡†åŒ–台账导出
 *
 * @author è¶…级管理员
 */
@Tag(name = "管理后台 - å®‰å…¨ç”Ÿäº§-台账与统计")
@RestController
@RequestMapping("/safety/ledger")
@Validated
public class SafetyLedgerController {
    @Resource
    private SafetyOverviewService overviewService;
    @Resource
    private SafetyLedgerExportService ledgerExportService;
    @GetMapping("/overview")
    @Operation(summary = "获得安全生产总览统计")
    @PreAuthorize("@ss.hasPermission('safety:ledger:query')")
    public CommonResult<SafetyOverviewRespVO> overview() {
        return success(overviewService.getOverview());
    }
    @GetMapping("/export")
    @Operation(summary = "导出电力行业安全标准化台账(全套或多选其一)")
    @PreAuthorize("@ss.hasPermission('safety:ledger:export')")
    public void export(@Valid SafetyLedgerExportReqVO reqVO, HttpServletResponse response) throws IOException {
        ledgerExportService.export(reqVO, response);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/SafetyMaterialController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,138 @@
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.material.*;
import cn.iocoder.yudao.module.mes.service.safety.SafetyMaterialService;
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.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * å®‰å…¨ç”Ÿäº§ - åº”急物资台账与出入库(绝缘用具 / å±åŒ–品 / æ¶ˆé˜²ç‰©èµ„)
 *
 * @author è¶…级管理员
 */
@Tag(name = "管理后台 - å®‰å…¨ç”Ÿäº§-应急物资")
@RestController
@RequestMapping("/safety/material")
@Validated
public class SafetyMaterialController {
    @Resource
    private SafetyMaterialService materialService;
    @PostMapping("/create")
    @Operation(summary = "新增物资")
    @PreAuthorize("@ss.hasPermission('safety:material:create')")
    public CommonResult<Long> create(@Valid @RequestBody SafetyMaterialSaveReqVO reqVO) {
        return success(materialService.createMaterial(reqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "修改物资(库存数量只能通过出入库变动)")
    @PreAuthorize("@ss.hasPermission('safety:material:update')")
    public CommonResult<Boolean> update(@Valid @RequestBody SafetyMaterialSaveReqVO reqVO) {
        materialService.updateMaterial(reqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除物资")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:material:delete')")
    public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
        materialService.deleteMaterial(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得物资")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:material:query')")
    public CommonResult<SafetyMaterialRespVO> get(@RequestParam("id") Long id) {
        var material = materialService.getMaterial(id);
        materialService.fillManagers(java.util.List.of(material));
        var resp = BeanUtils.toBean(material, SafetyMaterialRespVO.class);
        SafetyMaterialService.fillMaterialDisplay(material, resp);
        return success(resp);
    }
    @GetMapping("/page")
    @Operation(summary = "获得物资分页")
    @PreAuthorize("@ss.hasPermission('safety:material:query')")
    public CommonResult<PageResult<SafetyMaterialRespVO>> page(@Valid SafetyMaterialPageReqVO reqVO) {
        var page = materialService.getMaterialPage(reqVO);
        materialService.fillManagers(page.getList());
        var result = BeanUtils.toBean(page, SafetyMaterialRespVO.class);
        for (int i = 0; i < page.getList().size(); i++) {
            SafetyMaterialService.fillMaterialDisplay(page.getList().get(i), result.getList().get(i));
        }
        return success(result);
    }
    @GetMapping("/below-safe-stock")
    @Operation(summary = "获得低于安全库存的物资列表(预警)")
    @PreAuthorize("@ss.hasPermission('safety:material:query')")
    public CommonResult<List<SafetyMaterialRespVO>> belowSafeStock() {
        var materials = materialService.getBelowSafeStockList();
        materialService.fillManagers(materials);
        List<SafetyMaterialRespVO> list = materials.stream()
                .map(material -> {
                    var resp = BeanUtils.toBean(material, SafetyMaterialRespVO.class);
                    SafetyMaterialService.fillMaterialDisplay(material, resp);
                    return resp;
                }).toList();
        return success(list);
    }
    @GetMapping("/expiring-list")
    @Operation(summary = "获得即将到期的物资列表(预警)")
    @Parameter(name = "days", description = "提前预警天数,默认 30 å¤©")
    @PreAuthorize("@ss.hasPermission('safety:material:query')")
    public CommonResult<List<SafetyMaterialRespVO>> expiringList(
            @RequestParam(value = "days", required = false) Integer days) {
        var materials = materialService.getExpiringList(days);
        materialService.fillManagers(materials);
        List<SafetyMaterialRespVO> list = materials.stream()
                .map(material -> {
                    var resp = BeanUtils.toBean(material, SafetyMaterialRespVO.class);
                    SafetyMaterialService.fillMaterialDisplay(material, resp);
                    return resp;
                }).toList();
        return success(list);
    }
    // ==================== å‡ºå…¥åº“ ====================
    @PostMapping("/stock/create")
    @Operation(summary = "出入库登记(自动更新库存)")
    @PreAuthorize("@ss.hasPermission('safety:material-stock:in') or @ss.hasPermission('safety:material-stock:out')")
    public CommonResult<Long> createStock(@Valid @RequestBody SafetyMaterialStockSaveReqVO reqVO) {
        return success(materialService.createStock(reqVO));
    }
    @GetMapping("/stock/page")
    @Operation(summary = "获得出入库流水分页")
    @PreAuthorize("@ss.hasPermission('safety:material-stock:query')")
    public CommonResult<PageResult<SafetyMaterialStockRespVO>> stockPage(
            @Valid SafetyMaterialStockPageReqVO reqVO) {
        var page = materialService.getStockPage(reqVO);
        var result = BeanUtils.toBean(page, SafetyMaterialStockRespVO.class);
        for (int i = 0; i < page.getList().size(); i++) {
            SafetyMaterialService.fillStockDisplay(page.getList().get(i), result.getList().get(i));
        }
        return success(result);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/SafetyOrgController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,99 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.org.SafetyOrgRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.org.SafetyOrgSaveReqVO;
import cn.iocoder.yudao.module.mes.service.safety.SafetyOrgService;
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.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * å®‰å…¨ç”Ÿäº§ - å®‰å…¨ç»„织架构
 *
 * @author è¶…级管理员
 */
@Tag(name = "管理后台 - å®‰å…¨ç”Ÿäº§-组织架构")
@RestController
@RequestMapping("/safety/org")
@Validated
public class SafetyOrgController {
    @Resource
    private SafetyOrgService orgService;
    @PostMapping("/create")
    @Operation(summary = "新增安全组织")
    @PreAuthorize("@ss.hasPermission('safety:org:create')")
    public CommonResult<Long> create(@Valid @RequestBody SafetyOrgSaveReqVO reqVO) {
        return success(orgService.createOrg(reqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "修改安全组织")
    @PreAuthorize("@ss.hasPermission('safety:org:update')")
    public CommonResult<Boolean> update(@Valid @RequestBody SafetyOrgSaveReqVO reqVO) {
        orgService.updateOrg(reqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除安全组织")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:org:delete')")
    public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
        orgService.deleteOrg(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得安全组织")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:org:query')")
    public CommonResult<SafetyOrgRespVO> get(@RequestParam("id") Long id) {
        var org = orgService.getOrg(id);
        var resp = new SafetyOrgRespVO();
        org2resp(org, resp);
        // è´Ÿè´£äººç”¨å‘˜å·¥æ¡£æ¡ˆæœ€æ–°å§“名覆盖快照
        if (org.getLeaderId() != null) {
            var employee = orgService.getLeader(org.getLeaderId());
            if (employee != null) {
                resp.setLeaderName(employee.getName());
                resp.setLeaderPhone(employee.getPhone());
            }
        }
        return success(resp);
    }
    @GetMapping("/tree")
    @Operation(summary = "获得安全组织树")
    @PreAuthorize("@ss.hasPermission('safety:org:query')")
    public CommonResult<List<SafetyOrgRespVO>> tree() {
        return success(orgService.getOrgTree());
    }
    private static void org2resp(cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyOrgDO org,
                                 SafetyOrgRespVO resp) {
        resp.setId(org.getId());
        resp.setParentId(org.getParentId());
        resp.setName(org.getName());
        resp.setLeaderName(org.getLeaderName());
        resp.setLeaderPhone(org.getLeaderPhone());
        resp.setMemberNames(org.getMemberNames());
        resp.setDuty(org.getDuty());
        resp.setSort(org.getSort());
        resp.setStatus(org.getStatus());
        resp.setRemark(org.getRemark());
        resp.setCreateTime(org.getCreateTime());
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/SafetyTrainingController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,187 @@
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.train.*;
import cn.iocoder.yudao.module.mes.service.safety.SafetyTrainingService;
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 static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * å®‰å…¨ç”Ÿäº§ - åŸ¹è®­ã€æ¼”练、事故登记电子化归档
 *
 * @author è¶…级管理员
 */
@Tag(name = "管理后台 - å®‰å…¨ç”Ÿäº§-培训与事故")
@RestController
@RequestMapping("/safety/training")
@Validated
public class SafetyTrainingController {
    @Resource
    private SafetyTrainingService trainingService;
    // ==================== åŸ¹è®­ ====================
    @PostMapping("/create")
    @Operation(summary = "新增安全培训记录")
    @PreAuthorize("@ss.hasPermission('safety:training:create')")
    public CommonResult<Long> create(@Valid @RequestBody SafetyTrainingSaveReqVO reqVO) {
        return success(trainingService.createTraining(reqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "修改安全培训记录")
    @PreAuthorize("@ss.hasPermission('safety:training:update')")
    public CommonResult<Boolean> update(@Valid @RequestBody SafetyTrainingSaveReqVO reqVO) {
        trainingService.updateTraining(reqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除安全培训记录")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:training:delete')")
    public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
        trainingService.deleteTraining(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得安全培训记录")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:training:query')")
    public CommonResult<SafetyTrainingRespVO> get(@RequestParam("id") Long id) {
        var training = trainingService.getTraining(id);
        trainingService.fillTrainers(java.util.List.of(training));
        var resp = BeanUtils.toBean(training, SafetyTrainingRespVO.class);
        SafetyTrainingService.fillTrainingDisplay(training, resp);
        return success(resp);
    }
    @GetMapping("/page")
    @Operation(summary = "获得安全培训记录分页")
    @PreAuthorize("@ss.hasPermission('safety:training:query')")
    public CommonResult<PageResult<SafetyTrainingRespVO>> page(@Valid SafetyTrainingPageReqVO reqVO) {
        var page = trainingService.getTrainingPage(reqVO);
        trainingService.fillTrainers(page.getList());
        var result = BeanUtils.toBean(page, SafetyTrainingRespVO.class);
        for (int i = 0; i < page.getList().size(); i++) {
            SafetyTrainingService.fillTrainingDisplay(page.getList().get(i), result.getList().get(i));
        }
        return success(result);
    }
    // ==================== æ¼”练 ====================
    @PostMapping("/drill/create")
    @Operation(summary = "新增消防演练记录")
    @PreAuthorize("@ss.hasPermission('safety:drill:create')")
    public CommonResult<Long> createDrill(@Valid @RequestBody SafetyDrillSaveReqVO reqVO) {
        return success(trainingService.createDrill(reqVO));
    }
    @PutMapping("/drill/update")
    @Operation(summary = "修改消防演练记录")
    @PreAuthorize("@ss.hasPermission('safety:drill:update')")
    public CommonResult<Boolean> updateDrill(@Valid @RequestBody SafetyDrillSaveReqVO reqVO) {
        trainingService.updateDrill(reqVO);
        return success(true);
    }
    @DeleteMapping("/drill/delete")
    @Operation(summary = "删除消防演练记录")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:drill:delete')")
    public CommonResult<Boolean> deleteDrill(@RequestParam("id") Long id) {
        trainingService.deleteDrill(id);
        return success(true);
    }
    @GetMapping("/drill/get")
    @Operation(summary = "获得消防演练记录")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:drill:query')")
    public CommonResult<SafetyDrillRespVO> getDrill(@RequestParam("id") Long id) {
        var drill = trainingService.getDrill(id);
        trainingService.fillOrganizers(java.util.List.of(drill));
        var resp = BeanUtils.toBean(drill, SafetyDrillRespVO.class);
        SafetyTrainingService.fillDrillDisplay(drill, resp);
        return success(resp);
    }
    @GetMapping("/drill/page")
    @Operation(summary = "获得消防演练记录分页")
    @PreAuthorize("@ss.hasPermission('safety:drill:query')")
    public CommonResult<PageResult<SafetyDrillRespVO>> drillPage(@Valid SafetyDrillPageReqVO reqVO) {
        var page = trainingService.getDrillPage(reqVO);
        trainingService.fillOrganizers(page.getList());
        var result = BeanUtils.toBean(page, SafetyDrillRespVO.class);
        for (int i = 0; i < page.getList().size(); i++) {
            SafetyTrainingService.fillDrillDisplay(page.getList().get(i), result.getList().get(i));
        }
        return success(result);
    }
    // ==================== äº‹æ•… ====================
    @PostMapping("/accident/create")
    @Operation(summary = "新增事故登记")
    @PreAuthorize("@ss.hasPermission('safety:accident:create')")
    public CommonResult<Long> createAccident(@Valid @RequestBody SafetyAccidentSaveReqVO reqVO) {
        return success(trainingService.createAccident(reqVO));
    }
    @PutMapping("/accident/update")
    @Operation(summary = "修改事故登记")
    @PreAuthorize("@ss.hasPermission('safety:accident:update')")
    public CommonResult<Boolean> updateAccident(@Valid @RequestBody SafetyAccidentSaveReqVO reqVO) {
        trainingService.updateAccident(reqVO);
        return success(true);
    }
    @DeleteMapping("/accident/delete")
    @Operation(summary = "删除事故登记")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:accident:delete')")
    public CommonResult<Boolean> deleteAccident(@RequestParam("id") Long id) {
        trainingService.deleteAccident(id);
        return success(true);
    }
    @GetMapping("/accident/get")
    @Operation(summary = "获得事故登记")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:accident:query')")
    public CommonResult<SafetyAccidentRespVO> getAccident(@RequestParam("id") Long id) {
        var accident = trainingService.getAccident(id);
        trainingService.fillAccidentRefs(java.util.List.of(accident));
        var resp = BeanUtils.toBean(accident, SafetyAccidentRespVO.class);
        SafetyTrainingService.fillAccidentDisplay(accident, resp);
        return success(resp);
    }
    @GetMapping("/accident/page")
    @Operation(summary = "获得事故登记分页")
    @PreAuthorize("@ss.hasPermission('safety:accident:query')")
    public CommonResult<PageResult<SafetyAccidentRespVO>> accidentPage(
            @Valid SafetyAccidentPageReqVO reqVO) {
        var page = trainingService.getAccidentPage(reqVO);
        trainingService.fillAccidentRefs(page.getList());
        var result = BeanUtils.toBean(page, SafetyAccidentRespVO.class);
        for (int i = 0; i < page.getList().size(); i++) {
            SafetyTrainingService.fillAccidentDisplay(page.getList().get(i), result.getList().get(i));
        }
        return success(result);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/SafetyWorkPermitController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,147 @@
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.permit.*;
import cn.iocoder.yudao.module.mes.service.safety.SafetyWorkPermitService;
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.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * å®‰å…¨ç”Ÿäº§ - é«˜å±ä½œä¸šç¥¨ï¼ˆåŠ¨ç« / é«˜åŽ‹å¸¦ç”µ / æœ‰é™ç©ºé—´ç­‰ï¼‰å®¡æ‰¹ä¸Žç”µå­ç•™å­˜
 *
 * @author è¶…级管理员
 */
@Tag(name = "管理后台 - å®‰å…¨ç”Ÿäº§-高危作业票")
@RestController
@RequestMapping("/safety/permit")
@Validated
public class SafetyWorkPermitController {
    @Resource
    private SafetyWorkPermitService permitService;
    @PostMapping("/create")
    @Operation(summary = "提交高危作业申请")
    @PreAuthorize("@ss.hasPermission('safety:permit:create')")
    public CommonResult<Long> create(@Valid @RequestBody SafetyWorkPermitSaveReqVO reqVO) {
        return success(permitService.createPermit(reqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "修改作业票")
    @PreAuthorize("@ss.hasPermission('safety:permit:update')")
    public CommonResult<Boolean> update(@Valid @RequestBody SafetyWorkPermitSaveReqVO reqVO) {
        permitService.updatePermit(reqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除作业票(连同监护记录)")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:permit:delete')")
    public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
        permitService.deletePermit(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得作业票")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:permit:query')")
    public CommonResult<SafetyWorkPermitRespVO> get(@RequestParam("id") Long id) {
        var permit = permitService.getPermit(id);
        permitService.fillPermitRefs(java.util.List.of(permit));
        var resp = BeanUtils.toBean(permit, SafetyWorkPermitRespVO.class);
        SafetyWorkPermitService.fillPermitDisplay(permit, resp);
        return success(resp);
    }
    @GetMapping("/page")
    @Operation(summary = "获得作业票分页")
    @PreAuthorize("@ss.hasPermission('safety:permit:query')")
    public CommonResult<PageResult<SafetyWorkPermitRespVO>> page(@Valid SafetyWorkPermitPageReqVO reqVO) {
        var page = permitService.getPermitPage(reqVO);
        permitService.fillPermitRefs(page.getList());
        var result = BeanUtils.toBean(page, SafetyWorkPermitRespVO.class);
        for (int i = 0; i < page.getList().size(); i++) {
            SafetyWorkPermitService.fillPermitDisplay(page.getList().get(i), result.getList().get(i));
        }
        return success(result);
    }
    @PutMapping("/approve")
    @Operation(summary = "审批作业票(通过/驳回)")
    @PreAuthorize("@ss.hasPermission('safety:permit:approve')")
    public CommonResult<Boolean> approve(@Valid @RequestBody SafetyWorkPermitApproveReqVO reqVO) {
        permitService.approvePermit(reqVO);
        return success(true);
    }
    @PutMapping("/start")
    @Operation(summary = "开始作业")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:permit:update')")
    public CommonResult<Boolean> start(@RequestParam("id") Long id) {
        permitService.startPermit(id);
        return success(true);
    }
    @PutMapping("/close")
    @Operation(summary = "结束作业并归档")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:permit:update')")
    public CommonResult<Boolean> close(@RequestParam("id") Long id) {
        permitService.closePermit(id);
        return success(true);
    }
    @PutMapping("/cancel")
    @Operation(summary = "作废作业票")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:permit:update')")
    public CommonResult<Boolean> cancel(@RequestParam("id") Long id) {
        permitService.cancelPermit(id);
        return success(true);
    }
    // ==================== ç›‘护记录 ====================
    @GetMapping("/monitor/list")
    @Operation(summary = "获得作业监护记录列表")
    @Parameter(name = "permitId", description = "作业票编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:permit:query')")
    public CommonResult<List<SafetyWorkPermitMonitorRespVO>> monitorList(@RequestParam("permitId") Long permitId) {
        var monitors = permitService.getMonitorList(permitId);
        permitService.fillMonitorGuardians(monitors);
        return success(BeanUtils.toBean(monitors, SafetyWorkPermitMonitorRespVO.class));
    }
    @PostMapping("/monitor/create")
    @Operation(summary = "新增作业监护记录")
    @PreAuthorize("@ss.hasPermission('safety:permit:update')")
    public CommonResult<Long> createMonitor(@Valid @RequestBody SafetyWorkPermitMonitorSaveReqVO reqVO) {
        return success(permitService.createMonitor(reqVO));
    }
    @DeleteMapping("/monitor/delete")
    @Operation(summary = "删除作业监护记录")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('safety:permit:update')")
    public CommonResult<Boolean> deleteMonitor(@RequestParam("id") Long id) {
        permitService.deleteMonitor(id);
        return success(true);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/cert/SafetyCertificatePageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.cert;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰¹ç§ä½œä¸šè¯åˆ†é¡µ Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ç‰¹ç§ä½œä¸šè¯åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyCertificatePageReqVO extends PageParam {
    @Schema(description = "持证人(员工编号)")
    private Long employeeId;
    @Schema(description = "持证人姓名(模糊匹配)")
    private String employeeName;
    @Schema(description = "工号")
    private String employeeNo;
    @Schema(description = "所属部门")
    private String deptName;
    @Schema(description = "证件类型")
    private String certType;
    @Schema(description = "证件编号")
    private String certNo;
    @Schema(description = "状态")
    private String status;
    @Schema(description = "有效期开始")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate expireDateStart;
    @Schema(description = "有效期截止")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate expireDateEnd;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/cert/SafetyCertificateRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.cert;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰¹ç§ä½œä¸šè¯ Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ç‰¹ç§ä½œä¸šè¯ Response VO")
@Data
public class SafetyCertificateRespVO {
    private Long id;
    @Schema(description = "持证人(员工编号)")
    private Long employeeId;
    @Schema(description = "持证人关联的系统用户编号")
    private Long userId;
    private String employeeName;
    private String employeeNo;
    private String deptName;
    private String phone;
    private String certType;
    @Schema(description = "证件类型名称")
    private String certTypeName;
    private String certNo;
    private String issuer;
    private LocalDate issueDate;
    private LocalDate expireDate;
    private LocalDate reviewDate;
    private Integer warnDays;
    private String status;
    @Schema(description = "状态名称")
    private String statusName;
    @Schema(description = "距到期天数,负数表示已过期")
    private Long remainingDays;
    private String attachment;
    private String remark;
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/cert/SafetyCertificateSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,73 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.cert;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰¹ç§ä½œä¸šè¯æ–°å¢ž/修改 Request VO
 *
 * <p>持证人通过 employeeId å…³è”员工档案,姓名与部门由后端从档案带出,
 * å‰ç«¯åªéœ€é€‰äººï¼Œä¸æ‰‹å·¥å¡«å†™ï¼Œé¿å…ä¸ŽäººåŠ›èµ„æºæ•°æ®ä¸ä¸€è‡´ã€‚</p>
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ç‰¹ç§ä½œä¸šè¯æ–°å¢ž/修改 Request VO")
@Data
public class SafetyCertificateSaveReqVO {
    private Long id;
    @Schema(description = "持证人(员工编号)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "请选择持证人")
    private Long employeeId;
    @Schema(description = "证件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "HIGH_VOLTAGE_OPERATION")
    @NotBlank(message = "证件类型不能为空")
    @Pattern(regexp = "LOW_VOLTAGE_ELECTRICIAN|HIGH_VOLTAGE_OPERATION|WELDING|HOIST|HEIGHT_WORK|LIMITED_SPACE|FORKLIFT|BOILER|OTHER",
            message = "证件类型不合法")
    private String certType;
    @Schema(description = "证件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "T330102199001011234")
    @NotBlank(message = "证件编号不能为空")
    @Size(max = 64)
    private String certNo;
    @Schema(description = "发证机关")
    @Size(max = 100)
    private String issuer;
    @Schema(description = "发证日期")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate issueDate;
    @Schema(description = "有效期至", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "有效期不能为空")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate expireDate;
    @Schema(description = "下次复审日期")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate reviewDate;
    @Schema(description = "到期预警提前天数", example = "30")
    @Min(value = 0, message = "预警天数不能为负数")
    @Max(value = 365, message = "预警天数不能超过 365 å¤©")
    private Integer warnDays;
    @Schema(description = "状态", example = "VALID")
    @Pattern(regexp = "VALID|EXPIRING|EXPIRED|REVOKED", message = "状态不合法")
    private String status;
    @Schema(description = "证件扫描件 URL,逗号分隔")
    @Size(max = 500)
    private String attachment;
    @Schema(description = "备注")
    @Size(max = 500)
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/hazard/SafetyHazardAssignReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.hazard;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
/**
 * å®‰å…¨ç”Ÿäº§ - é𐿂£åˆ†æ´¾ Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - é𐿂£åˆ†æ´¾ Request VO")
@Data
public class SafetyHazardAssignReqVO {
    @Schema(description = "隐患编号", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "隐患编号不能为空")
    private Long id;
    @Schema(description = "整改责任人(员工编号,关联 hrm_employee)", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "请选择整改责任人")
    private Long assigneeId;
    @Schema(description = "整改期限")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate rectifyDeadline;
    @Schema(description = "分派说明")
    @Size(max = 500)
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/hazard/SafetyHazardPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.hazard;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - é𐿂£åˆ†é¡µ Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - é𐿂£åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyHazardPageReqVO extends PageParam {
    private String code;
    private String title;
    private String level;
    private String source;
    private String status;
    private String assignee;
    private String location;
    @Schema(description = "上报时间范围")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime[] reportTime;
    @Schema(description = "上报时间开始(内部使用)", hidden = true)
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    private LocalDateTime reportTimeStart;
    @Schema(description = "上报时间结束(内部使用)", hidden = true)
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    private LocalDateTime reportTimeEnd;
    public void normalizeTimeRange() {
        if (reportTime != null && reportTime.length == 2) {
            this.reportTimeStart = reportTime[0];
            this.reportTimeEnd = reportTime[1];
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/hazard/SafetyHazardRecheckReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.hazard;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
 * å®‰å…¨ç”Ÿäº§ - é𐿂£å¤æŸ¥ Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - é𐿂£å¤æŸ¥ Request VO")
@Data
public class SafetyHazardRecheckReqVO {
    @Schema(description = "隐患编号", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "隐患编号不能为空")
    private Long id;
    @Schema(description = "复查人(员工编号,关联 hrm_employee)", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "请选择复查人")
    private Long recheckerId;
    @Schema(description = "复查结论:PASSED-通过,REJECTED-不通过", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotBlank(message = "复查结论不能为空")
    @Pattern(regexp = "PASSED|REJECTED", message = "复查结论只能为通过或不通过")
    private String recheckResult;
    @Schema(description = "复查意见")
    @Size(max = 500)
    private String recheckRemark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/hazard/SafetyHazardRectifyReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.hazard;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
 * å®‰å…¨ç”Ÿäº§ - é𐿂£æ•´æ”¹ Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - é𐿂£æ•´æ”¹ Request VO")
@Data
public class SafetyHazardRectifyReqVO {
    @Schema(description = "隐患编号", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "隐患编号不能为空")
    private Long id;
    @Schema(description = "整改措施与结果", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotBlank(message = "整改措施与结果不能为空")
    @Size(max = 2000)
    private String rectifyDesc;
    @Schema(description = "整改后照片 URL,逗号分隔")
    @Size(max = 2000)
    private String rectifyPhotoUrls;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/hazard/SafetyHazardRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.hazard;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - é𐿂£ Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - é𐿂£ Response VO")
@Data
public class SafetyHazardRespVO {
    private Long id;
    private String code;
    private String title;
    private String level;
    @Schema(description = "隐患等级名称")
    private String levelName;
    private String source;
    @Schema(description = "来源名称")
    private String sourceName;
    private Long inspectionId;
    private Long pointId;
    private String pointName;
    private String location;
    private String description;
    private String photoUrls;
    @Schema(description = "上报人编号")
    private Long reporterId;
    private String reporter;
    private LocalDateTime reportTime;
    @Schema(description = "整改责任人编号")
    private Long assigneeId;
    private String assignee;
    private LocalDateTime assignTime;
    private LocalDate rectifyDeadline;
    @Schema(description = "是否已超期")
    private Boolean overdue;
    private String rectifyDesc;
    private String rectifyPhotoUrls;
    private LocalDateTime rectifyTime;
    @Schema(description = "复查人编号")
    private Long recheckerId;
    private String rechecker;
    private LocalDateTime recheckTime;
    private String recheckResult;
    @Schema(description = "复查结论名称")
    private String recheckResultName;
    private String recheckRemark;
    private String status;
    @Schema(description = "状态名称")
    private String statusName;
    private LocalDateTime closeTime;
    private String remark;
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/hazard/SafetyHazardSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,73 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.hazard;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - é𐿂£ä¸ŠæŠ¥/修改 Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - é𐿂£ä¸ŠæŠ¥/修改 Request VO")
@Data
public class SafetyHazardSaveReqVO {
    private Long id;
    @Schema(description = "隐患标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "配电室门未上锁")
    @NotBlank(message = "隐患标题不能为空")
    @Size(max = 200)
    private String title;
    @Schema(description = "隐患等级", example = "GENERAL")
    @Pattern(regexp = "GENERAL|MAJOR", message = "隐患等级不合法")
    private String level;
    @Schema(description = "来源", example = "INSPECTION")
    @Pattern(regexp = "INSPECTION|SELF_CHECK|REPORT|EXTERNAL", message = "隐患来源不合法")
    private String source;
    @Schema(description = "关联巡检记录编号")
    private Long inspectionId;
    @Schema(description = "关联巡检点位编号")
    private Long pointId;
    @Schema(description = "点位名称")
    @Size(max = 100)
    private String pointName;
    @Schema(description = "隐患位置")
    @Size(max = 200)
    private String location;
    @Schema(description = "隐患描述")
    @Size(max = 2000)
    private String description;
    @Schema(description = "现场照片 URL,逗号分隔(移动端拍照上传)")
    @Size(max = 2000)
    private String photoUrls;
    @Schema(description = "上报人(员工编号,关联 hrm_employee)", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "请选择上报人")
    private Long reporterId;
    @Schema(description = "上报时间")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    private LocalDateTime reportTime;
    @Schema(description = "整改期限")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate rectifyDeadline;
    @Schema(description = "备注")
    @Size(max = 500)
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/inspection/SafetyInspectionPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.inspection;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - å·¡æ£€è®°å½•分页 Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - å·¡æ£€è®°å½•分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyInspectionPageReqVO extends PageParam {
    private Long pointId;
    private String pointType;
    private String inspector;
    private String result;
    @Schema(description = "巡检时间范围")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime[] inspectTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/inspection/SafetyInspectionPointPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.inspection;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * å®‰å…¨ç”Ÿäº§ - å·¡æ£€ç‚¹ä½åˆ†é¡µ Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - å·¡æ£€ç‚¹ä½åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyInspectionPointPageReqVO extends PageParam {
    private String code;
    private String name;
    private String pointType;
    private String workshopName;
    private Integer status;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/inspection/SafetyInspectionPointRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.inspection;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - å·¡æ£€ç‚¹ä½ Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - å·¡æ£€ç‚¹ä½ Response VO")
@Data
public class SafetyInspectionPointRespVO {
    private Long id;
    private String code;
    private String name;
    private String pointType;
    @Schema(description = "点位类型名称")
    private String pointTypeName;
    @Schema(description = "所属车间编号")
    private Long workshopId;
    private String workshopName;
    private String location;
    private String inspectItems;
    private String inspectCycle;
    @Schema(description = "巡检周期名称")
    private String inspectCycleName;
    @Schema(description = "责任人编号")
    private Long ownerId;
    private String ownerName;
    private Integer status;
    @Schema(description = "最近一次巡检时间")
    private LocalDateTime lastInspectTime;
    private String remark;
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/inspection/SafetyInspectionPointSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.inspection;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.Data;
/**
 * å®‰å…¨ç”Ÿäº§ - å·¡æ£€ç‚¹ä½æ–°å¢ž/修改 Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - å·¡æ£€ç‚¹ä½æ–°å¢ž/修改 Request VO")
@Data
public class SafetyInspectionPointSaveReqVO {
    private Long id;
    @Schema(description = "点位编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "PD-001")
    @NotBlank(message = "点位编码不能为空")
    @Size(max = 64)
    private String code;
    @Schema(description = "点位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "1#高压配电室")
    @NotBlank(message = "点位名称不能为空")
    @Size(max = 100)
    private String name;
    @Schema(description = "点位类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "HIGH_VOLTAGE_ROOM")
    @NotBlank(message = "点位类型不能为空")
    @Pattern(regexp = "HIGH_VOLTAGE_ROOM|UNIT_AREA|DANGER_CHEMICAL|FIRE_AREA|WORKSHOP|OTHER", message = "点位类型不合法")
    private String pointType;
    @Schema(description = "所属车间(关联 mes_md_workshop)")
    private Long workshopId;
    @Schema(description = "具体位置")
    @Size(max = 200)
    private String location;
    @Schema(description = "巡检项目清单,按行分隔")
    @Size(max = 2000)
    private String inspectItems;
    @Schema(description = "巡检周期", example = "DAILY")
    @Pattern(regexp = "SHIFT|DAILY|WEEKLY|MONTHLY", message = "巡检周期不合法")
    private String inspectCycle;
    @Schema(description = "责任人(员工编号,关联 hrm_employee)")
    private Long ownerId;
    @Schema(description = "状态:0-启用,1-停用", example = "0")
    @NotNull(message = "状态不能为空")
    private Integer status;
    @Schema(description = "备注")
    @Size(max = 500)
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/inspection/SafetyInspectionRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.inspection;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - å·¡æ£€è®°å½• Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - å·¡æ£€è®°å½• Response VO")
@Data
public class SafetyInspectionRespVO {
    private Long id;
    private Long pointId;
    private String pointName;
    private String pointType;
    @Schema(description = "点位类型名称")
    private String pointTypeName;
    private LocalDateTime inspectTime;
    @Schema(description = "巡检人编号")
    private Long inspectorId;
    private String inspector;
    private String result;
    @Schema(description = "巡检结果名称")
    private String resultName;
    private String content;
    private String photoUrls;
    private Integer hazardCount;
    private String remark;
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/inspection/SafetyInspectionSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.inspection;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - å·¡æ£€è®°å½•新增/修改 Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - å·¡æ£€è®°å½•新增/修改 Request VO")
@Data
public class SafetyInspectionSaveReqVO {
    private Long id;
    @Schema(description = "巡检点位编号", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "巡检点位不能为空")
    private Long pointId;
    @Schema(description = "巡检时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "巡检时间不能为空")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    private LocalDateTime inspectTime;
    @Schema(description = "巡检人(员工编号,关联 hrm_employee)", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "请选择巡检人")
    private Long inspectorId;
    @Schema(description = "巡检结果", example = "NORMAL")
    @Pattern(regexp = "NORMAL|ABNORMAL", message = "巡检结果只能为正常或异常")
    private String result;
    @Schema(description = "巡检内容与情况")
    @Size(max = 2000)
    private String content;
    @Schema(description = "现场照片 URL,逗号分隔")
    @Size(max = 2000)
    private String photoUrls;
    @Schema(description = "本次发现的隐患数", example = "0")
    @Min(value = 0, message = "隐患数不能为负数")
    private Integer hazardCount;
    @Schema(description = "备注")
    @Size(max = 500)
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/ledger/SafetyLedgerExportReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ledger;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
/**
 * å®‰å…¨ç”Ÿäº§ - æ ‡å‡†åŒ–台账导出 Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - æ ‡å‡†åŒ–台账导出 Request VO")
@Data
public class SafetyLedgerExportReqVO {
    @Schema(description = "台账类型:ALL-全套,CERT-特种作业证,HAZARD-隐患,PERMIT-作业票,MATERIAL-物资,TRAINING-培训,DRILL-演练,ACCIDENT-事故,INSPECTION-巡检",
            example = "ALL")
    @Pattern(regexp = "ALL|CERT|HAZARD|PERMIT|MATERIAL|TRAINING|DRILL|ACCIDENT|INSPECTION", message = "台账类型不合法")
    private String type;
    @Schema(description = "开始日期")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate startDate;
    @Schema(description = "结束日期")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate endDate;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/ledger/SafetyOverviewRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ledger;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
 * å®‰å…¨ç”Ÿäº§ - æ€»è§ˆç»Ÿè®¡ Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - å®‰å…¨ç”Ÿäº§æ€»è§ˆç»Ÿè®¡ Response VO")
@Data
public class SafetyOverviewRespVO {
    @Schema(description = "特种作业证总数")
    private Long certTotal;
    @Schema(description = "即将到期证件数")
    private Long certExpiring;
    @Schema(description = "已过期证件数")
    private Long certExpired;
    @Schema(description = "隐患总数")
    private Long hazardTotal;
    @Schema(description = "未闭环隐患数")
    private Long hazardOpen;
    @Schema(description = "已超期未闭环隐患数")
    private Long hazardOverdue;
    @Schema(description = "隐患闭环率(0-100)")
    private Integer hazardCloseRate;
    @Schema(description = "待审批作业票数")
    private Long permitPending;
    @Schema(description = "进行中作业票数")
    private Long permitInProgress;
    @Schema(description = "物资低于安全库存数")
    private Long materialBelowSafeStock;
    @Schema(description = "物资临近有效期数")
    private Long materialExpiring;
    @Schema(description = "本月培训场次")
    private Long trainingThisMonth;
    @Schema(description = "本月演练场次")
    private Long drillThisMonth;
    @Schema(description = "本月事故数")
    private Long accidentThisMonth;
    @Schema(description = "隐患趋势(近 6 ä¸ªæœˆï¼ŒæŒ‰æœˆï¼‰")
    private List<MonthlyCount> hazardTrend;
    @Schema(description = "事故趋势(近 6 ä¸ªæœˆï¼ŒæŒ‰æœˆï¼‰")
    private List<MonthlyCount> accidentTrend;
    /**
     * æŒ‰æœˆè®¡æ•°
     */
    @Data
    public static class MonthlyCount {
        @Schema(description = "月份,格式 yyyy-MM", example = "2026-10")
        private String month;
        @Schema(description = "数量")
        private Long count;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/material/SafetyMaterialPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.material;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰©èµ„台账分页 Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ç‰©èµ„台账分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyMaterialPageReqVO extends PageParam {
    private String code;
    private String name;
    private String category;
    private String storageLocation;
    private Integer status;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/material/SafetyMaterialRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.material;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰©èµ„台账 Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ç‰©èµ„台账 Response VO")
@Data
public class SafetyMaterialRespVO {
    private Long id;
    private String code;
    private String name;
    private String category;
    @Schema(description = "类别名称")
    private String categoryName;
    private String spec;
    private String unit;
    private BigDecimal stockQty;
    private BigDecimal safeStock;
    @Schema(description = "是否低于安全库存")
    private Boolean belowSafeStock;
    private String storageLocation;
    private LocalDate expireDate;
    @Schema(description = "距有效期天数,负数表示已过期")
    private Long expireRemainingDays;
    private Integer validityMonths;
    @Schema(description = "保管人编号")
    private Long managerId;
    private String manager;
    private Integer status;
    private String remark;
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/material/SafetyMaterialSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,76 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.material;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰©èµ„台账新增/修改 Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ç‰©èµ„台账新增/修改 Request VO")
@Data
public class SafetyMaterialSaveReqVO {
    private Long id;
    @Schema(description = "物资编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "PM-001")
    @NotBlank(message = "物资编码不能为空")
    @Size(max = 64)
    private String code;
    @Schema(description = "物资名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "10kV ç»ç¼˜æ‰‹å¥—")
    @NotBlank(message = "物资名称不能为空")
    @Size(max = 100)
    private String name;
    @Schema(description = "类别", requiredMode = Schema.RequiredMode.REQUIRED, example = "INSULATING")
    @NotBlank(message = "物资类别不能为空")
    @Pattern(regexp = "INSULATING|HAZARD_CHEMICAL|FIRE|PPE|OTHER", message = "物资类别不合法")
    private String category;
    @Schema(description = "规格型号")
    @Size(max = 100)
    private String spec;
    @Schema(description = "计量单位", example = "双")
    @Size(max = 20)
    private String unit;
    @Schema(description = "当前库存数量")
    @DecimalMin(value = "0", message = "库存数量不能为负数")
    private BigDecimal stockQty;
    @Schema(description = "安全库存")
    @DecimalMin(value = "0", message = "安全库存不能为负数")
    private BigDecimal safeStock;
    @Schema(description = "存放位置")
    @Size(max = 100)
    private String storageLocation;
    @Schema(description = "有效期至")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate expireDate;
    @Schema(description = "有效期月数")
    @Min(value = 0, message = "有效期月数不能为负数")
    private Integer validityMonths;
    @Schema(description = "保管人(员工编号,关联 hrm_employee)")
    private Long managerId;
    @Schema(description = "状态:0-启用,1-停用", example = "0")
    @NotNull(message = "状态不能为空")
    private Integer status;
    @Schema(description = "备注")
    @Size(max = 500)
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/material/SafetyMaterialStockPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.material;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰©èµ„出入库流水分页 Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ç‰©èµ„出入库流水分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyMaterialStockPageReqVO extends PageParam {
    private Long materialId;
    private String materialName;
    private String category;
    private String bizType;
    @Schema(description = "业务日期范围")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate[] bizDate;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/material/SafetyMaterialStockRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.material;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰©èµ„出入库流水 Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ç‰©èµ„出入库流水 Response VO")
@Data
public class SafetyMaterialStockRespVO {
    private Long id;
    private Long materialId;
    private String materialName;
    private String category;
    @Schema(description = "类别名称")
    private String categoryName;
    private String bizType;
    @Schema(description = "业务类型名称")
    private String bizTypeName;
    private BigDecimal quantity;
    private BigDecimal stockBefore;
    private BigDecimal stockAfter;
    private LocalDate bizDate;
    private String operator;
    private String receiver;
    private String purpose;
    private String remark;
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/material/SafetyMaterialStockSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.material;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰©èµ„出入库登记 Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ç‰©èµ„出入库登记 Request VO")
@Data
public class SafetyMaterialStockSaveReqVO {
    @Schema(description = "物资编号", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "物资不能为空")
    private Long materialId;
    @Schema(description = "业务类型:IN-入库,OUT-出库", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotBlank(message = "业务类型不能为空")
    @Pattern(regexp = "IN|OUT", message = "业务类型只能为入库或出库")
    private String bizType;
    @Schema(description = "数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    @NotNull(message = "数量不能为空")
    @DecimalMin(value = "0", inclusive = false, message = "数量必须大于零")
    @Digits(integer = 15, fraction = 3)
    private BigDecimal quantity;
    @Schema(description = "业务日期")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate bizDate;
    @Schema(description = "经办人", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotBlank(message = "经办人不能为空")
    @Size(max = 50)
    private String operator;
    @Schema(description = "领用人(出库填写)")
    @Size(max = 50)
    private String receiver;
    @Schema(description = "用途/去向")
    @Size(max = 200)
    private String purpose;
    @Schema(description = "备注")
    @Size(max = 500)
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/org/SafetyOrgRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.org;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
 * å®‰å…¨ç”Ÿäº§ - å®‰å…¨ç»„织 Response VO(树形)
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - å®‰å…¨ç»„织 Response VO")
@Data
public class SafetyOrgRespVO {
    private Long id;
    private Long parentId;
    private String name;
    @Schema(description = "负责人(员工编号)")
    private Long leaderId;
    @Schema(description = "负责人姓名(快照)")
    private String leaderName;
    private String leaderPhone;
    private String memberNames;
    private String duty;
    private Integer sort;
    private Integer status;
    private String remark;
    private LocalDateTime createTime;
    @Schema(description = "子组织")
    private List<SafetyOrgRespVO> children;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/org/SafetyOrgSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.org;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
 * å®‰å…¨ç”Ÿäº§ - å®‰å…¨ç»„织新增/修改 Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - å®‰å…¨ç»„织新增/修改 Request VO")
@Data
public class SafetyOrgSaveReqVO {
    private Long id;
    @Schema(description = "父级编号,0 ä¸ºé¡¶çº§", example = "0")
    private Long parentId;
    @Schema(description = "组织名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "安全生产委员会")
    @NotBlank(message = "组织名称不能为空")
    @Size(max = 100, message = "组织名称长度不能超过 100 å­—")
    private String name;
    @Schema(description = "负责人(员工编号,关联 hrm_employee)")
    private Long leaderId;
    @Schema(description = "成员姓名,逗号分隔")
    @Size(max = 500)
    private String memberNames;
    @Schema(description = "安全职责")
    @Size(max = 500)
    private String duty;
    @Schema(description = "显示顺序", example = "1")
    @NotNull(message = "显示顺序不能为空")
    private Integer sort;
    @Schema(description = "状态:0-启用,1-停用", example = "0")
    @NotNull(message = "状态不能为空")
    private Integer status;
    @Schema(description = "备注")
    @Size(max = 500)
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/permit/SafetyWorkPermitApproveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.permit;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
 * å®‰å…¨ç”Ÿäº§ - é«˜å±ä½œä¸šç¥¨å®¡æ‰¹ Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - é«˜å±ä½œä¸šç¥¨å®¡æ‰¹ Request VO")
@Data
public class SafetyWorkPermitApproveReqVO {
    @Schema(description = "作业票编号", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "作业票编号不能为空")
    private Long id;
    @Schema(description = "审批人(员工编号,关联 hrm_employee)", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "请选择审批人")
    private Long approverId;
    @Schema(description = "审批结论:APPROVED-通过,REJECTED-驳回", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotBlank(message = "审批结论不能为空")
    @Pattern(regexp = "APPROVED|REJECTED", message = "审批结论只能为通过或驳回")
    private String decision;
    @Schema(description = "审批意见")
    @Size(max = 500)
    private String approveComment;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/permit/SafetyWorkPermitMonitorRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.permit;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - ä½œä¸šç›‘护记录 Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ä½œä¸šç›‘护记录 Response VO")
@Data
public class SafetyWorkPermitMonitorRespVO {
    private Long id;
    private Long permitId;
    private LocalDateTime monitorTime;
    @Schema(description = "监护人编号")
    private Long guardianId;
    private String guardian;
    private String monitorContent;
    private String abnormal;
    private String photoUrls;
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/permit/SafetyWorkPermitMonitorSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.permit;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - ä½œä¸šç›‘护记录新增 Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ä½œä¸šç›‘护记录新增 Request VO")
@Data
public class SafetyWorkPermitMonitorSaveReqVO {
    private Long id;
    @Schema(description = "作业票编号", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "作业票编号不能为空")
    private Long permitId;
    @Schema(description = "监护时间")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    private LocalDateTime monitorTime;
    @Schema(description = "监护人(员工编号,关联 hrm_employee)", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "请选择监护人")
    private Long guardianId;
    @Schema(description = "监护内容与情况")
    @Size(max = 1000)
    private String monitorContent;
    @Schema(description = "异常情况与处置")
    @Size(max = 500)
    private String abnormal;
    @Schema(description = "现场照片 URL,逗号分隔")
    @Size(max = 2000)
    private String photoUrls;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/permit/SafetyWorkPermitPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.permit;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - é«˜å±ä½œä¸šç¥¨åˆ†é¡µ Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - é«˜å±ä½œä¸šç¥¨åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyWorkPermitPageReqVO extends PageParam {
    private String code;
    private String title;
    private String permitType;
    private String status;
    private String riskLevel;
    private String applicant;
    private String workshopName;
    @Schema(description = "计划开始时间范围")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime[] planStartTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/permit/SafetyWorkPermitRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.permit;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - é«˜å±ä½œä¸šç¥¨ Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - é«˜å±ä½œä¸šç¥¨ Response VO")
@Data
public class SafetyWorkPermitRespVO {
    private Long id;
    private String code;
    private String permitType;
    @Schema(description = "作业类型名称")
    private String permitTypeName;
    private String title;
    @Schema(description = "作业车间编号")
    private Long workshopId;
    private String workshopName;
    private String location;
    private String workContent;
    @Schema(description = "申请人编号")
    private Long applicantId;
    private String applicant;
    private LocalDateTime applyTime;
    private LocalDateTime planStartTime;
    private LocalDateTime planEndTime;
    private LocalDateTime actualStartTime;
    private LocalDateTime actualEndTime;
    private String riskLevel;
    @Schema(description = "风险等级名称")
    private String riskLevelName;
    private String safetyMeasures;
    @Schema(description = "审批人编号")
    private Long approverId;
    private String approver;
    private LocalDateTime approveTime;
    private String approveComment;
    @Schema(description = "监护人编号")
    private Long guardianId;
    private String guardian;
    private String status;
    @Schema(description = "状态名称")
    private String statusName;
    private String attachment;
    private String photoUrls;
    @Schema(description = "监护人是否必填(动火/高压带电/有限空间)")
    private Boolean guardianRequired;
    private String remark;
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/permit/SafetyWorkPermitSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,84 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.permit;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - é«˜å±ä½œä¸šç¥¨ç”³è¯·/修改 Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - é«˜å±ä½œä¸šç¥¨ç”³è¯·/修改 Request VO")
@Data
public class SafetyWorkPermitSaveReqVO {
    private Long id;
    @Schema(description = "作业类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "HOT_WORK")
    @NotBlank(message = "作业类型不能为空")
    @Pattern(regexp = "HOT_WORK|LIVE_HIGH_VOLTAGE|LIMITED_SPACE|WORK_AT_HEIGHT|LIFTING|TEMP_ELECTRIC|OTHER",
            message = "作业类型不合法")
    private String permitType;
    @Schema(description = "作业名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "2#主变检修动火")
    @NotBlank(message = "作业名称不能为空")
    @Size(max = 200)
    private String title;
    @Schema(description = "作业车间(关联 mes_md_workshop)")
    private Long workshopId;
    @Schema(description = "作业地点")
    @Size(max = 200)
    private String location;
    @Schema(description = "作业内容与范围")
    @Size(max = 2000)
    private String workContent;
    @Schema(description = "申请人(员工编号,关联 hrm_employee)", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "请选择申请人")
    private Long applicantId;
    @Schema(description = "申请时间")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    private LocalDateTime applyTime;
    @Schema(description = "计划开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "计划开始时间不能为空")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    private LocalDateTime planStartTime;
    @Schema(description = "计划结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "计划结束时间不能为空")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    private LocalDateTime planEndTime;
    @Schema(description = "风险等级", example = "HIGH")
    @Pattern(regexp = "LOW|NORMAL|HIGH|VERY_HIGH", message = "风险等级不合法")
    private String riskLevel;
    @Schema(description = "安全措施")
    @Size(max = 2000)
    private String safetyMeasures;
    @Schema(description = "监护人(员工编号,关联 hrm_employee)")
    private Long guardianId;
    @Schema(description = "作业票附件 URL,逗号分隔")
    @Size(max = 2000)
    private String attachment;
    @Schema(description = "现场照片 URL,逗号分隔")
    @Size(max = 2000)
    private String photoUrls;
    @Schema(description = "备注")
    @Size(max = 500)
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/train/SafetyAccidentPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.train;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - äº‹æ•…登记分页 Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - äº‹æ•…登记分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyAccidentPageReqVO extends PageParam {
    private String code;
    private String title;
    private String accidentType;
    private String level;
    private String status;
    @Schema(description = "发生时间范围")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime[] occurTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/train/SafetyAccidentRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.train;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - äº‹æ•…登记 Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - äº‹æ•…登记 Response VO")
@Data
public class SafetyAccidentRespVO {
    private Long id;
    private String code;
    private String title;
    private String accidentType;
    @Schema(description = "事故类型名称")
    private String accidentTypeName;
    private String level;
    @Schema(description = "事故等级名称")
    private String levelName;
    private LocalDateTime occurTime;
    private String location;
    @Schema(description = "所属车间编号")
    private Long workshopId;
    private String workshopName;
    private String casualtyDesc;
    private BigDecimal economicLoss;
    private String involvedPersons;
    private String description;
    private String causeAnalysis;
    private String measures;
    @Schema(description = "登记人编号")
    private Long reporterId;
    private String reporter;
    private LocalDateTime reportTime;
    private String status;
    @Schema(description = "状态名称")
    private String statusName;
    private String attachment;
    private String remark;
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/train/SafetyAccidentSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,91 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.train;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - äº‹æ•…登记新增/修改 Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - äº‹æ•…登记新增/修改 Request VO")
@Data
public class SafetyAccidentSaveReqVO {
    private Long id;
    @Schema(description = "事故名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "10kV å¼€å…³æŸœè¯¯æ“ä½œè§¦ç”µ")
    @NotBlank(message = "事故名称不能为空")
    @Size(max = 200)
    private String title;
    @Schema(description = "事故类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "ELECTRIC_SHOCK")
    @NotBlank(message = "事故类型不能为空")
    @Pattern(regexp = "ELECTRIC_SHOCK|FIRE|FALL|OBJECT_STRIKE|MECHANICAL|CHEMICAL|VEHICLE|OTHER", message = "事故类型不合法")
    private String accidentType;
    @Schema(description = "事故等级", example = "GENERAL")
    @Pattern(regexp = "SLIGHT|GENERAL|SEVERE|MAJOR", message = "事故等级不合法")
    private String level;
    @Schema(description = "发生时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "发生时间不能为空")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    private LocalDateTime occurTime;
    @Schema(description = "发生地点")
    @Size(max = 200)
    private String location;
    @Schema(description = "所属车间(关联 mes_md_workshop)")
    private Long workshopId;
    @Schema(description = "伤亡情况")
    @Size(max = 500)
    private String casualtyDesc;
    @Schema(description = "直接经济损失(元)")
    @DecimalMin(value = "0", message = "经济损失不能为负数")
    private BigDecimal economicLoss;
    @Schema(description = "涉事人员姓名,逗号分隔")
    @Size(max = 500)
    private String involvedPersons;
    @Schema(description = "事故经过")
    @Size(max = 2000)
    private String description;
    @Schema(description = "原因分析")
    @Size(max = 2000)
    private String causeAnalysis;
    @Schema(description = "处理措施与整改要求")
    @Size(max = 2000)
    private String measures;
    @Schema(description = "登记人(员工编号,关联 hrm_employee)")
    private Long reporterId;
    @Schema(description = "登记时间")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    private LocalDateTime reportTime;
    @Schema(description = "状态", example = "REPORTED")
    @Pattern(regexp = "REPORTED|INVESTIGATING|HANDLED|CLOSED", message = "状态不合法")
    private String status;
    @Schema(description = "现场照片/调查报告 URL,逗号分隔")
    @Size(max = 2000)
    private String attachment;
    @Schema(description = "备注")
    @Size(max = 500)
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/train/SafetyDrillPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.train;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
/**
 * å®‰å…¨ç”Ÿäº§ - æ¼”练记录分页 Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - æ¼”练记录分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyDrillPageReqVO extends PageParam {
    private String title;
    private String drillType;
    private String organizer;
    @Schema(description = "演练日期范围")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate[] drillDate;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/train/SafetyDrillRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.train;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - æ¼”练记录 Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - æ¼”练记录 Response VO")
@Data
public class SafetyDrillRespVO {
    private Long id;
    private String title;
    private String drillType;
    @Schema(description = "演练类型名称")
    private String drillTypeName;
    private LocalDate drillDate;
    private String location;
    @Schema(description = "组织人编号")
    private Long organizerId;
    private String organizer;
    private String participants;
    private Integer participantCount;
    private String planDesc;
    private String processDesc;
    private String summary;
    private Integer durationMinutes;
    private String attachment;
    private String remark;
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/train/SafetyDrillSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,74 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.train;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
/**
 * å®‰å…¨ç”Ÿäº§ - æ¶ˆé˜²æ¼”练记录新增/修改 Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - æ¶ˆé˜²æ¼”练记录新增/修改 Request VO")
@Data
public class SafetyDrillSaveReqVO {
    private Long id;
    @Schema(description = "演练名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026 å¹´åº¦æ¶ˆé˜²ç–æ•£æ¼”练")
    @NotBlank(message = "演练名称不能为空")
    @Size(max = 200)
    private String title;
    @Schema(description = "演练类型", example = "FIRE")
    @Pattern(regexp = "FIRE|EMERGENCY_EVAC|ELECTRIC_ACCIDENT|CHEMICAL_LEAK|OTHER", message = "演练类型不合法")
    private String drillType;
    @Schema(description = "演练日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "演练日期不能为空")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate drillDate;
    @Schema(description = "演练地点")
    @Size(max = 200)
    private String location;
    @Schema(description = "组织人(员工编号,关联 hrm_employee)")
    private Long organizerId;
    @Schema(description = "参加人员姓名,逗号分隔")
    @Size(max = 2000)
    private String participants;
    @Schema(description = "参加人数")
    @Min(value = 0, message = "参加人数不能为负数")
    private Integer participantCount;
    @Schema(description = "演练方案")
    @Size(max = 2000)
    private String planDesc;
    @Schema(description = "演练过程记录")
    @Size(max = 2000)
    private String processDesc;
    @Schema(description = "演练总结与改进")
    @Size(max = 2000)
    private String summary;
    @Schema(description = "演练时长(分钟)")
    @Min(value = 0, message = "演练时长不能为负数")
    private Integer durationMinutes;
    @Schema(description = "演练照片/记录表 URL,逗号分隔")
    @Size(max = 2000)
    private String attachment;
    @Schema(description = "备注")
    @Size(max = 500)
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/train/SafetyTrainingPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.train;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
/**
 * å®‰å…¨ç”Ÿäº§ - åŸ¹è®­è®°å½•分页 Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - åŸ¹è®­è®°å½•分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyTrainingPageReqVO extends PageParam {
    private String title;
    private String trainingType;
    private String trainer;
    @Schema(description = "培训日期范围")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate[] trainDate;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/train/SafetyTrainingRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.train;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - åŸ¹è®­è®°å½• Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - åŸ¹è®­è®°å½• Response VO")
@Data
public class SafetyTrainingRespVO {
    private Long id;
    private String title;
    private String trainingType;
    @Schema(description = "培训类型名称")
    private String trainingTypeName;
    private LocalDate trainDate;
    private BigDecimal durationHours;
    @Schema(description = "讲师/组织人编号")
    private Long trainerId;
    private String trainer;
    private String location;
    private String participants;
    private Integer participantCount;
    private String content;
    private String examResult;
    private String attachment;
    private String remark;
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/train/SafetyTrainingSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.train;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * å®‰å…¨ç”Ÿäº§ - åŸ¹è®­è®°å½•新增/修改 Request VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - åŸ¹è®­è®°å½•新增/修改 Request VO")
@Data
public class SafetyTrainingSaveReqVO {
    private Long id;
    @Schema(description = "培训主题", requiredMode = Schema.RequiredMode.REQUIRED, example = "年度安全规程培训")
    @NotBlank(message = "培训主题不能为空")
    @Size(max = 200)
    private String title;
    @Schema(description = "培训类型", example = "SAFETY")
    @Pattern(regexp = "SAFETY|SKILL|LAW|EMERGENCY|INDUCTION|OTHER", message = "培训类型不合法")
    private String trainingType;
    @Schema(description = "培训日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "培训日期不能为空")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate trainDate;
    @Schema(description = "培训学时", example = "2.0")
    @DecimalMin(value = "0", message = "培训学时不能为负数")
    private BigDecimal durationHours;
    @Schema(description = "讲师/组织人(员工编号,关联 hrm_employee)")
    private Long trainerId;
    @Schema(description = "培训地点")
    @Size(max = 200)
    private String location;
    @Schema(description = "参加人员姓名,逗号分隔")
    @Size(max = 2000)
    private String participants;
    @Schema(description = "参加人数")
    @Min(value = 0, message = "参加人数不能为负数")
    private Integer participantCount;
    @Schema(description = "培训内容")
    @Size(max = 2000)
    private String content;
    @Schema(description = "考核情况")
    @Size(max = 2000)
    private String examResult;
    @Schema(description = "签到表/课件/照片 URL,逗号分隔")
    @Size(max = 2000)
    private String attachment;
    @Schema(description = "备注")
    @Size(max = 500)
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectAlarmDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.collect;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡-报警记录 DO
 *
 * @author è¶…级管理员
 */
@TableName("mes_collect_alarm")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesCollectAlarmDO extends BaseDO {
    @TableId
    private Long id;
    private Long machineryId;
    private String deviceCode;
    private Long taskId;
    private Long workOrderId;
    private Integer wmId;
    private String wmMsg;
    private LocalDateTime wmTime;
    private Integer status;
    private LocalDateTime clearTime;
    private LocalDateTime collectTime;
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectCycleDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.collect;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡-周期过程数据 DO
 *
 * @author è¶…级管理员
 */
@TableName("mes_collect_cycle")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesCollectCycleDO extends BaseDO {
    @TableId
    private Long id;
    private Long machineryId;
    private String deviceCode;
    private Long taskId;
    private Long workOrderId;
    private Long cycleNo;
    private LocalDateTime collectTime;
    private BigDecimal ecyct;
    private BigDecimal eiss;
    private BigDecimal eivm;
    private BigDecimal eipm;
    private BigDecimal esipt;
    private BigDecimal esipp;
    private BigDecimal esips;
    private BigDecimal eipt;
    private BigDecimal eipse;
    private BigDecimal eplst;
    private BigDecimal eplsse;
    private BigDecimal eplspm;
    private String etTemps;
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectMessageDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.collect;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡-原始报文 DO
 *
 * @author è¶…级管理员
 */
@TableName("mes_collect_message")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesCollectMessageDO extends BaseDO {
    @TableId
    private Long id;
    private Long machineryId;
    private String deviceCode;
    private String mqttTopic;
    private String msgType;
    private LocalDateTime eventTime;
    private String rawPayload;
    private Integer processStatus;
    private String errorMsg;
    private LocalDateTime receiveTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectOperateLogDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.collect;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡-参数变更日志 DO
 *
 * @author è¶…级管理员
 */
@TableName("mes_collect_operate_log")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesCollectOperateLogDO extends BaseDO {
    @TableId
    private Long id;
    private Long machineryId;
    private String deviceCode;
    private Long taskId;
    private Long workOrderId;
    private String varId;
    private String oldValue;
    private String newValue;
    private LocalDateTime modifyTime;
    private LocalDateTime collectTime;
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectRealtimeDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.collect;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡-设备实时状态 DO
 *
 * @author è¶…级管理员
 */
@TableName("mes_collect_realtime")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesCollectRealtimeDO extends BaseDO {
    @TableId
    private Long id;
    private Long machineryId;
    private String deviceCode;
    private Long taskId;
    private Long workOrderId;
    private LocalDateTime collectTime;
    private Integer sts;
    private Integer opm;
    private Integer asts;
    private BigDecimal ot;
    private String temps;
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectRealtimeHistoryDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.collect;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡-实时状态历史 DO
 *
 * @author è¶…级管理员
 */
@TableName("mes_collect_realtime_history")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesCollectRealtimeHistoryDO extends BaseDO {
    @TableId
    private Long id;
    private Long machineryId;
    private String deviceCode;
    private Long taskId;
    private Long workOrderId;
    private LocalDateTime collectTime;
    private Integer sts;
    private Integer opm;
    private Integer asts;
    private BigDecimal ot;
    private String temps;
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/collect/MesCollectTechDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.collect;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡-工艺参数快照 DO
 *
 * @author è¶…级管理员
 */
@TableName("mes_collect_tech")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesCollectTechDO extends BaseDO {
    @TableId
    private Long id;
    private Long machineryId;
    private String deviceCode;
    private Long taskId;
    private Long workOrderId;
    private LocalDateTime collectTime;
    private String dataJson;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/machinery/MesDvMachineryDO.java
@@ -101,5 +101,13 @@
     * è®¾å¤‡åˆ›å»ºæ—¶è‡ªåŠ¨åˆ›å»ºå¯¹åº”ç‰©æ–™ï¼Œå¹¶ä»¥æ­¤å…³è”è®¾å¤‡ä»“åº“å­˜
     */
    private Long itemId;
    /**
     * æ•°é‡‡è®¾å¤‡ç¼–号(MQTT devId,如 C01)
     */
    private String collectDeviceCode;
    /**
     * æ˜¯å¦å¯ç”¨æ•°é‡‡
     */
    private Boolean collectEnabled;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/spare/MesDvSpareMovementDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.dv.spare;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.*;
import lombok.*;
import java.math.BigDecimal;
@TableName("mes_dv_spare_movement")
@KeySequence("mes_dv_spare_movement_seq")
@Data @EqualsAndHashCode(callSuper = true)
public class MesDvSpareMovementDO extends BaseDO {
    @TableId private Long id;
    private String requestKey;
    private Long sparePartId;
    private String sparePartCode;
    private String sparePartName;
    private String specification;
    private String unit;
    /** 1 åˆå§‹åº“存;2 å…¥åº“ï¼›3 ç»´ä¿®é¢†ç”¨ï¼›4 é€€å›ž */
    private Integer type;
    private BigDecimal quantity;
    private BigDecimal beforeQuantity;
    private BigDecimal afterQuantity;
    private Long repairId;
    private String repairCode;
    private Long machineryId;
    private String machineryName;
    private Long originalId;
    private Long receiverUserId;
    private String receiverName;
    private Long operatorUserId;
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/spare/MesDvSparePartDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.dv.spare;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.*;
import lombok.*;
import java.math.BigDecimal;
@TableName("mes_dv_spare_part")
@KeySequence("mes_dv_spare_part_seq")
@Data @EqualsAndHashCode(callSuper = true)
public class MesDvSparePartDO extends BaseDO {
    @TableId private Long id;
    private String code;
    private String name;
    private String specification;
    private String unit;
    private String location;
    private BigDecimal stockQuantity;
    private BigDecimal safetyQuantity;
    private Integer status;
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/energy/EnergyAlertLogDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.energy;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.*;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDate;
@TableName("mes_energy_alert_log") @Data @EqualsAndHashCode(callSuper = true)
public class EnergyAlertLogDO extends TenantBaseDO {
    @TableId private Long id;
    private Long ruleId;
    private String ruleName;
    private LocalDate recordDate;
    private String periodType;
    private String energyType;
    private String scope;
    private String metric;
    private BigDecimal actualValue;
    private BigDecimal threshold;
    private Long recipientId;
    private String recipientName;
    private Long messageId;
    private String status;
    private String errorMessage;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/energy/EnergyAlertRuleDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.energy;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.*;
import lombok.*;
import java.math.BigDecimal;
@TableName("mes_energy_alert_rule") @Data @EqualsAndHashCode(callSuper = true)
public class EnergyAlertRuleDO extends TenantBaseDO {
    @TableId private Long id;
    private String name;
    private String energyType;
    private String usageArea;
    private String productCategory;
    private String metric;
    private BigDecimal threshold;
    private Boolean enabled;
    /** Comma-separated validated administrator IDs, at most 50. */
    private String recipientIds;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/energy/EnergyRecordDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.energy;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
/** ç‹¬ç«‹èƒ½è€—台账,不关联设备、工单、产品或部门主数据。 */
@TableName("mes_energy_record")
@Data
@EqualsAndHashCode(callSuper = true)
public class EnergyRecordDO extends TenantBaseDO {
    @TableId
    private Long id;
    private LocalDate recordDate;
    /** WATER / ELECTRICITY / STEAM,单位分别为 m³ / kWh / t。 */
    private String energyType;
    private String usageArea;
    private String productCategory;
    private String usagePurpose;
    private BigDecimal quantity;
    private BigDecimal unitPrice;
    private BigDecimal amount;
    private String recorder;
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyAccidentDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.safety;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - äº‹æ•…登记 DO
 *
 * @author è¶…级管理员
 */
@TableName("safety_accident")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyAccidentDO extends BaseDO {
    @TableId
    private Long id;
    /** äº‹æ•…编号 */
    private String code;
    /** äº‹æ•…名称 */
    private String title;
    /** äº‹æ•…类型,见 SafetyAccidentTypeEnum */
    private String accidentType;
    /** äº‹æ•…等级,见 SafetyAccidentLevelEnum */
    private String level;
    /** å‘生时间 */
    private LocalDateTime occurTime;
    /** å‘生地点 */
    private String location;
    /** æ‰€å±žè½¦é—´ï¼ˆå…³è” mes_md_workshop.id) */
    private Long workshopId;
    /** æ‰€å±žè½¦é—´åç§°ï¼ˆå¿«ç…§ï¼Œç”±è½¦é—´ä¸»æ•°æ®å¸¦å‡ºï¼‰ */
    private String workshopName;
    /** ä¼¤äº¡æƒ…况 */
    private String casualtyDesc;
    /** ç›´æŽ¥ç»æµŽæŸå¤±ï¼ˆå…ƒï¼‰ */
    private BigDecimal economicLoss;
    /** æ¶‰äº‹äººå‘˜å§“名,逗号分隔 */
    private String involvedPersons;
    /** äº‹æ•…经过 */
    private String description;
    /** åŽŸå› åˆ†æž */
    private String causeAnalysis;
    /** å¤„理措施与整改要求 */
    private String measures;
    /** ç™»è®°äººï¼ˆå‘˜å·¥ç¼–号,关联 hrm_employee.id) */
    private Long reporterId;
    /** ç™»è®°äººå§“名(快照,由员工档案带出) */
    private String reporter;
    /** ç™»è®°æ—¶é—´ */
    private LocalDateTime reportTime;
    /** çŠ¶æ€ï¼Œè§ SafetyAccidentStatusEnum */
    private String status;
    /** çŽ°åœºç…§ç‰‡/调查报告 URL,逗号分隔 */
    private String attachment;
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyCertificateDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.safety;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDate;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰¹ç§ä½œä¸šè¯å°è´¦ DO
 *
 * <p>覆盖电工证、高压操作证等特种作业资格证件,按有效期自动判断状态并预警。</p>
 *
 * @author è¶…级管理员
 */
@TableName("safety_certificate")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyCertificateDO extends BaseDO {
    @TableId
    private Long id;
    /**
     * æŒè¯äººï¼ˆå‘˜å·¥ç¼–号)
     *
     * å…³è” HrmEmployeeDO#getId(),姓名/工号/部门均由此关联带出
     */
    private Long employeeId;
    /** æŒè¯äººå§“名(快照,由员工档案带出,用于员工档案变更后追溯开证时信息) */
    private String employeeName;
    /** å·¥å·ï¼ˆå¿«ç…§ï¼‰ */
    private String employeeNo;
    /** æ‰€å±žéƒ¨é—¨ï¼ˆå¿«ç…§ï¼‰ */
    private String deptName;
    /** è¯ä»¶ç±»åž‹ï¼Œè§ SafetyCertificateTypeEnum */
    private String certType;
    /** è¯ä»¶ç¼–号 */
    private String certNo;
    /** å‘证机关 */
    private String issuer;
    /** å‘证日期 */
    private LocalDate issueDate;
    /** æœ‰æ•ˆæœŸè‡³ */
    private LocalDate expireDate;
    /** ä¸‹æ¬¡å¤å®¡æ—¥æœŸ */
    private LocalDate reviewDate;
    /** åˆ°æœŸé¢„警提前天数 */
    private Integer warnDays;
    /** çŠ¶æ€ï¼Œè§ SafetyCertificateStatusEnum */
    private String status;
    /** è¯ä»¶æ‰«æä»¶ URL,逗号分隔 */
    private String attachment;
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyDrillDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.safety;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDate;
/**
 * å®‰å…¨ç”Ÿäº§ - æ¶ˆé˜²æ¼”练记录 DO
 *
 * @author è¶…级管理员
 */
@TableName("safety_drill")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyDrillDO extends BaseDO {
    @TableId
    private Long id;
    /** æ¼”练名称 */
    private String title;
    /** æ¼”练类型,见 SafetyDrillTypeEnum */
    private String drillType;
    /** æ¼”练日期 */
    private LocalDate drillDate;
    /** æ¼”练地点 */
    private String location;
    /** ç»„织人(员工编号,关联 hrm_employee.id) */
    private Long organizerId;
    /** ç»„织人姓名(快照,由员工档案带出) */
    private String organizer;
    /** å‚加人员姓名,逗号分隔 */
    private String participants;
    /** å‚加人数 */
    private Integer participantCount;
    /** æ¼”练方案 */
    private String planDesc;
    /** æ¼”练过程记录 */
    private String processDesc;
    /** æ¼”练总结与改进 */
    private String summary;
    /** æ¼”练时长(分钟) */
    private Integer durationMinutes;
    /** æ¼”练照片/记录表 URL,逗号分隔 */
    private String attachment;
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyHazardDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.safety;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - é𐿂£ç™»è®°ä¸Žé—­çޝ DO
 *
 * <p>完整闭环:上报 â†’ åˆ†æ´¾ â†’ æ•´æ”¹ â†’ å¤æŸ¥ï¼ˆé€šè¿‡åˆ™é—­çŽ¯ï¼Œä¸é€šè¿‡é€€å›žæ•´æ”¹ï¼‰ã€‚</p>
 *
 * @author è¶…级管理员
 */
@TableName("safety_hazard")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyHazardDO extends BaseDO {
    @TableId
    private Long id;
    /** é𐿂£ç¼–号 */
    private String code;
    /** é𐿂£æ ‡é¢˜ */
    private String title;
    /** é𐿂£ç­‰çº§ï¼Œè§ SafetyHazardLevelEnum */
    private String level;
    /** æ¥æºï¼Œè§ SafetyHazardSourceEnum */
    private String source;
    /** å…³è”巡检记录编号 */
    private Long inspectionId;
    /** å…³è”巡检点位编号 */
    private Long pointId;
    /** ç‚¹ä½åç§° */
    private String pointName;
    /** é𐿂£ä½ç½® */
    private String location;
    /** é𐿂£æè¿° */
    private String description;
    /** çŽ°åœºç…§ç‰‡ URL,逗号分隔(移动端拍照上传) */
    private String photoUrls;
    /** ä¸ŠæŠ¥äººï¼ˆå‘˜å·¥ç¼–号,关联 hrm_employee.id) */
    private Long reporterId;
    /** ä¸ŠæŠ¥äººå§“名(快照,由员工档案带出) */
    private String reporter;
    /** ä¸ŠæŠ¥æ—¶é—´ */
    private LocalDateTime reportTime;
    /** æ•´æ”¹è´£ä»»äººï¼ˆå‘˜å·¥ç¼–号,关联 hrm_employee.id) */
    private Long assigneeId;
    /** æ•´æ”¹è´£ä»»äººå§“名(快照,由员工档案带出) */
    private String assignee;
    /** åˆ†æ´¾æ—¶é—´ */
    private LocalDateTime assignTime;
    /** æ•´æ”¹æœŸé™ */
    private LocalDate rectifyDeadline;
    /** æ•´æ”¹æŽªæ–½ä¸Žç»“æžœ */
    private String rectifyDesc;
    /** æ•´æ”¹åŽç…§ç‰‡ URL,逗号分隔 */
    private String rectifyPhotoUrls;
    /** æ•´æ”¹å®Œæˆæ—¶é—´ */
    private LocalDateTime rectifyTime;
    /** å¤æŸ¥äººï¼ˆå‘˜å·¥ç¼–号,关联 hrm_employee.id) */
    private Long recheckerId;
    /** å¤æŸ¥äººå§“名(快照,由员工档案带出) */
    private String rechecker;
    /** å¤æŸ¥æ—¶é—´ */
    private LocalDateTime recheckTime;
    /** å¤æŸ¥ç»“论:PASSED-通过,REJECTED-不通过 */
    private String recheckResult;
    /** å¤æŸ¥æ„è§ */
    private String recheckRemark;
    /** çŠ¶æ€ï¼Œè§ SafetyHazardStatusEnum */
    private String status;
    /** é—­çŽ¯æ—¶é—´ */
    private LocalDateTime closeTime;
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyInspectionDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.safety;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - å·¡æ£€è®°å½• DO
 *
 * @author è¶…级管理员
 */
@TableName("safety_inspection")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyInspectionDO extends BaseDO {
    @TableId
    private Long id;
    /** å·¡æ£€ç‚¹ä½ç¼–号 */
    private Long pointId;
    /** ç‚¹ä½åç§°ï¼ˆå¿«ç…§ï¼‰ */
    private String pointName;
    /** ç‚¹ä½ç±»åž‹ï¼ˆå¿«ç…§ï¼‰ */
    private String pointType;
    /** å·¡æ£€æ—¶é—´ */
    private LocalDateTime inspectTime;
    /** å·¡æ£€äººï¼ˆå‘˜å·¥ç¼–号,关联 hrm_employee.id) */
    private Long inspectorId;
    /** å·¡æ£€äººå§“名(快照,由员工档案带出) */
    private String inspector;
    /** å·¡æ£€ç»“果:NORMAL-正常,ABNORMAL-异常 */
    private String result;
    /** å·¡æ£€å†…容与情况 */
    private String content;
    /** çŽ°åœºç…§ç‰‡ URL,逗号分隔 */
    private String photoUrls;
    /** æœ¬æ¬¡å‘现的隐患数 */
    private Integer hazardCount;
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyInspectionPointDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.safety;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * å®‰å…¨ç”Ÿäº§ - å·¡æ£€ç‚¹ä½ DO
 *
 * <p>高压配电室、机组区等日常安全巡检点位。</p>
 *
 * @author è¶…级管理员
 */
@TableName("safety_inspection_point")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyInspectionPointDO extends BaseDO {
    @TableId
    private Long id;
    /** ç‚¹ä½ç¼–码 */
    private String code;
    /** ç‚¹ä½åç§° */
    private String name;
    /** ç‚¹ä½ç±»åž‹ï¼Œè§ SafetyInspectionPointTypeEnum */
    private String pointType;
    /** æ‰€å±žè½¦é—´ï¼ˆå…³è” mes_md_workshop.id) */
    private Long workshopId;
    /** æ‰€å±žè½¦é—´åç§°ï¼ˆå¿«ç…§ï¼Œç”±è½¦é—´ä¸»æ•°æ®å¸¦å‡ºï¼‰ */
    private String workshopName;
    /** å…·ä½“位置 */
    private String location;
    /** å·¡æ£€é¡¹ç›®æ¸…单,按行分隔 */
    private String inspectItems;
    /** å·¡æ£€å‘¨æœŸï¼Œè§ SafetyInspectCycleEnum */
    private String inspectCycle;
    /** è´£ä»»äººï¼ˆå‘˜å·¥ç¼–号,关联 hrm_employee.id) */
    private Long ownerId;
    /** è´£ä»»äººå§“名(快照,由员工档案带出) */
    private String ownerName;
    /** çŠ¶æ€ï¼š0-启用,1-停用 */
    private Integer status;
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyMaterialDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.safety;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * å®‰å…¨ç”Ÿäº§ - åº”急/安全物资台账 DO
 *
 * <p>电力绝缘用具、危化品、消防物资等。</p>
 *
 * @author è¶…级管理员
 */
@TableName("safety_material")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyMaterialDO extends BaseDO {
    @TableId
    private Long id;
    /** ç‰©èµ„编码 */
    private String code;
    /** ç‰©èµ„名称 */
    private String name;
    /** ç±»åˆ«ï¼Œè§ SafetyMaterialCategoryEnum */
    private String category;
    /** è§„格型号 */
    private String spec;
    /** è®¡é‡å•位 */
    private String unit;
    /** å½“前库存数量 */
    private BigDecimal stockQty;
    /** å®‰å…¨åº“存(低于则预警) */
    private BigDecimal safeStock;
    /** å­˜æ”¾ä½ç½® */
    private String storageLocation;
    /** æœ‰æ•ˆæœŸè‡³ï¼ˆç»ç¼˜/危化品必填) */
    private LocalDate expireDate;
    /** æœ‰æ•ˆæœŸæœˆæ•° */
    private Integer validityMonths;
    /** ä¿ç®¡äººï¼ˆå‘˜å·¥ç¼–号,关联 hrm_employee.id) */
    private Long managerId;
    /** ä¿ç®¡äººå§“名(快照,由员工档案带出) */
    private String manager;
    /** çŠ¶æ€ï¼š0-启用,1-停用 */
    private Integer status;
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyMaterialStockDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.safety;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰©èµ„出入库流水 DO
 *
 * @author è¶…级管理员
 */
@TableName("safety_material_stock")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyMaterialStockDO extends BaseDO {
    @TableId
    private Long id;
    /** ç‰©èµ„编号 */
    private Long materialId;
    /** ç‰©èµ„名称(快照) */
    private String materialName;
    /** ç‰©èµ„类别(快照) */
    private String category;
    /** ä¸šåŠ¡ç±»åž‹ï¼šIN-入库,OUT-出库 */
    private String bizType;
    /** æ•°é‡ */
    private BigDecimal quantity;
    /** æ“ä½œå‰åº“å­˜ */
    private BigDecimal stockBefore;
    /** æ“ä½œåŽåº“å­˜ */
    private BigDecimal stockAfter;
    /** ä¸šåŠ¡æ—¥æœŸ */
    private LocalDate bizDate;
    /** ç»åŠžäºº */
    private String operator;
    /** é¢†ç”¨äººï¼ˆå‡ºåº“) */
    private String receiver;
    /** ç”¨é€”/去向 */
    private String purpose;
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyOrgDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.safety;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * å®‰å…¨ç”Ÿäº§ - å®‰å…¨ç»„织架构 DO
 *
 * @author è¶…级管理员
 */
@TableName("safety_org")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyOrgDO extends BaseDO {
    @TableId
    private Long id;
    /** çˆ¶çº§ç¼–号,0 ä¸ºé¡¶çº§ */
    private Long parentId;
    /** ç»„织名称,如安全生产委员会、安全管理部 */
    private String name;
    /** è´Ÿè´£äººï¼ˆå‘˜å·¥ç¼–号,关联 hrm_employee.id) */
    private Long leaderId;
    /** è´Ÿè´£äººå§“名(快照,由员工档案带出) */
    private String leaderName;
    /** è´Ÿè´£äººç”µè¯ */
    private String leaderPhone;
    /** æˆå‘˜å§“名,逗号分隔 */
    private String memberNames;
    /** å®‰å…¨èŒè´£ */
    private String duty;
    /** æ˜¾ç¤ºé¡ºåº */
    private Integer sort;
    /** çŠ¶æ€ï¼š0-启用,1-停用 */
    private Integer status;
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyTrainingDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.safety;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * å®‰å…¨ç”Ÿäº§ - å®‰å…¨åŸ¹è®­è®°å½• DO
 *
 * @author è¶…级管理员
 */
@TableName("safety_training")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyTrainingDO extends BaseDO {
    @TableId
    private Long id;
    /** åŸ¹è®­ä¸»é¢˜ */
    private String title;
    /** åŸ¹è®­ç±»åž‹ï¼Œè§ SafetyTrainingTypeEnum */
    private String trainingType;
    /** åŸ¹è®­æ—¥æœŸ */
    private LocalDate trainDate;
    /** åŸ¹è®­å­¦æ—¶ */
    private BigDecimal durationHours;
    /** è®²å¸ˆ/组织人(员工编号,关联 hrm_employee.id) */
    private Long trainerId;
    /** è®²å¸ˆ/组织人姓名(快照,由员工档案带出) */
    private String trainer;
    /** åŸ¹è®­åœ°ç‚¹ */
    private String location;
    /** å‚加人员姓名,逗号分隔 */
    private String participants;
    /** å‚加人数 */
    private Integer participantCount;
    /** åŸ¹è®­å†…容 */
    private String content;
    /** è€ƒæ ¸æƒ…况 */
    private String examResult;
    /** ç­¾åˆ°è¡¨/课件/照片 URL,逗号分隔 */
    private String attachment;
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyWorkPermitDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,77 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.safety;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - é«˜å±ä½œä¸šç¥¨ DO
 *
 * <p>动火、高压带电、有限空间等作业的线上审批与电子留存。</p>
 *
 * @author è¶…级管理员
 */
@TableName("safety_work_permit")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyWorkPermitDO extends BaseDO {
    @TableId
    private Long id;
    /** ä½œä¸šç¥¨ç¼–号 */
    private String code;
    /** ä½œä¸šç±»åž‹ï¼Œè§ SafetyPermitTypeEnum */
    private String permitType;
    /** ä½œä¸šåç§° */
    private String title;
    /** ä½œä¸šè½¦é—´ï¼ˆå…³è” mes_md_workshop.id) */
    private Long workshopId;
    /** ä½œä¸šè½¦é—´åç§°ï¼ˆå¿«ç…§ï¼Œç”±è½¦é—´ä¸»æ•°æ®å¸¦å‡ºï¼‰ */
    private String workshopName;
    /** ä½œä¸šåœ°ç‚¹ */
    private String location;
    /** ä½œä¸šå†…容与范围 */
    private String workContent;
    /** ç”³è¯·äººï¼ˆå‘˜å·¥ç¼–号,关联 hrm_employee.id) */
    private Long applicantId;
    /** ç”³è¯·äººå§“名(快照,由员工档案带出) */
    private String applicant;
    /** ç”³è¯·æ—¶é—´ */
    private LocalDateTime applyTime;
    /** è®¡åˆ’开始时间 */
    private LocalDateTime planStartTime;
    /** è®¡åˆ’结束时间 */
    private LocalDateTime planEndTime;
    /** å®žé™…开始时间 */
    private LocalDateTime actualStartTime;
    /** å®žé™…结束时间 */
    private LocalDateTime actualEndTime;
    /** é£Žé™©ç­‰çº§ï¼Œè§ SafetyRiskLevelEnum */
    private String riskLevel;
    /** å®‰å…¨æŽªæ–½ */
    private String safetyMeasures;
    /** å®¡æ‰¹äººï¼ˆå‘˜å·¥ç¼–号,关联 hrm_employee.id) */
    private Long approverId;
    /** å®¡æ‰¹äººå§“名(快照,由员工档案带出) */
    private String approver;
    /** å®¡æ‰¹æ—¶é—´ */
    private LocalDateTime approveTime;
    /** å®¡æ‰¹æ„è§ */
    private String approveComment;
    /** ç›‘护人(员工编号,关联 hrm_employee.id) */
    private Long guardianId;
    /** ç›‘护人姓名(快照,由员工档案带出) */
    private String guardian;
    /** çŠ¶æ€ï¼Œè§ SafetyPermitStatusEnum */
    private String status;
    /** ä½œä¸šç¥¨é™„ä»¶ URL,逗号分隔 */
    private String attachment;
    /** çŽ°åœºç…§ç‰‡ URL,逗号分隔 */
    private String photoUrls;
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyWorkPermitMonitorDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.safety;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
 * å®‰å…¨ç”Ÿäº§ - ä½œä¸šç›‘护记录 DO
 *
 * @author è¶…级管理员
 */
@TableName("safety_work_permit_monitor")
@Data
@EqualsAndHashCode(callSuper = true)
public class SafetyWorkPermitMonitorDO extends BaseDO {
    @TableId
    private Long id;
    /** ä½œä¸šç¥¨ç¼–号 */
    private Long permitId;
    /** ç›‘护时间 */
    private LocalDateTime monitorTime;
    /** ç›‘护人(员工编号,关联 hrm_employee.id) */
    private Long guardianId;
    /** ç›‘护人姓名(快照,由员工档案带出) */
    private String guardian;
    /** ç›‘护内容与情况 */
    private String monitorContent;
    /** å¼‚常情况与处置 */
    private String abnormal;
    /** çŽ°åœºç…§ç‰‡ URL,逗号分隔 */
    private String photoUrls;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectAlarmMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.mes.dal.mysql.collect;
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.collect.vo.MesCollectAlarmPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectAlarmDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡-报警记录 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesCollectAlarmMapper extends BaseMapperX<MesCollectAlarmDO> {
    default PageResult<MesCollectAlarmDO> selectPage(MesCollectAlarmPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesCollectAlarmDO>()
                .eqIfPresent(MesCollectAlarmDO::getDeviceCode, reqVO.getDeviceCode())
                .eqIfPresent(MesCollectAlarmDO::getMachineryId, reqVO.getMachineryId())
                .eqIfPresent(MesCollectAlarmDO::getWorkOrderId, reqVO.getWorkOrderId())
                .eqIfPresent(MesCollectAlarmDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(MesCollectAlarmDO::getWmTime, reqVO.getWmTime())
                .orderByDesc(MesCollectAlarmDO::getWmTime));
    }
    default Long selectActiveCountByDeviceCode(String deviceCode) {
        return selectCount(new LambdaQueryWrapperX<MesCollectAlarmDO>()
                .eq(MesCollectAlarmDO::getDeviceCode, deviceCode)
                .eq(MesCollectAlarmDO::getStatus, 0));
    }
    default int clearActiveByDeviceCode(String deviceCode, LocalDateTime clearTime) {
        return update(null, new LambdaUpdateWrapper<MesCollectAlarmDO>()
                .eq(MesCollectAlarmDO::getDeviceCode, deviceCode)
                .eq(MesCollectAlarmDO::getStatus, 0)
                .set(MesCollectAlarmDO::getStatus, 1)
                .set(MesCollectAlarmDO::getClearTime, clearTime));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectCycleMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.mes.dal.mysql.collect;
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.collect.vo.MesCollectCyclePageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectCycleDO;
import org.apache.ibatis.annotations.Mapper;
/**
 * MES æ•°é‡‡-周期过程数据 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesCollectCycleMapper extends BaseMapperX<MesCollectCycleDO> {
    default PageResult<MesCollectCycleDO> selectPage(MesCollectCyclePageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesCollectCycleDO>()
                .eqIfPresent(MesCollectCycleDO::getDeviceCode, reqVO.getDeviceCode())
                .eqIfPresent(MesCollectCycleDO::getMachineryId, reqVO.getMachineryId())
                .eqIfPresent(MesCollectCycleDO::getTaskId, reqVO.getTaskId())
                .eqIfPresent(MesCollectCycleDO::getWorkOrderId, reqVO.getWorkOrderId())
                .betweenIfPresent(MesCollectCycleDO::getCollectTime, reqVO.getCollectTime())
                .orderByDesc(MesCollectCycleDO::getCollectTime));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectMessageMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.mes.dal.mysql.collect;
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.collect.MesCollectMessageDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡-原始报文 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesCollectMessageMapper extends BaseMapperX<MesCollectMessageDO> {
    default int deleteBefore(LocalDateTime before, int limit) {
        return delete(new LambdaQueryWrapperX<MesCollectMessageDO>()
                .lt(MesCollectMessageDO::getReceiveTime, before)
                .last("LIMIT " + limit));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectOperateLogMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.mes.dal.mysql.collect;
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.collect.vo.MesCollectOperateLogPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectOperateLogDO;
import org.apache.ibatis.annotations.Mapper;
/**
 * MES æ•°é‡‡-参数变更日志 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesCollectOperateLogMapper extends BaseMapperX<MesCollectOperateLogDO> {
    default PageResult<MesCollectOperateLogDO> selectPage(MesCollectOperateLogPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesCollectOperateLogDO>()
                .eqIfPresent(MesCollectOperateLogDO::getDeviceCode, reqVO.getDeviceCode())
                .eqIfPresent(MesCollectOperateLogDO::getMachineryId, reqVO.getMachineryId())
                .eqIfPresent(MesCollectOperateLogDO::getWorkOrderId, reqVO.getWorkOrderId())
                .eqIfPresent(MesCollectOperateLogDO::getVarId, reqVO.getVarId())
                .betweenIfPresent(MesCollectOperateLogDO::getModifyTime, reqVO.getModifyTime())
                .orderByDesc(MesCollectOperateLogDO::getModifyTime));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectRealtimeHistoryMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.dal.mysql.collect;
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.collect.vo.MesCollectRealtimeHistoryPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectRealtimeHistoryDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡-实时状态历史 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesCollectRealtimeHistoryMapper extends BaseMapperX<MesCollectRealtimeHistoryDO> {
    default PageResult<MesCollectRealtimeHistoryDO> selectPage(MesCollectRealtimeHistoryPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesCollectRealtimeHistoryDO>()
                .eqIfPresent(MesCollectRealtimeHistoryDO::getDeviceCode, reqVO.getDeviceCode())
                .eqIfPresent(MesCollectRealtimeHistoryDO::getMachineryId, reqVO.getMachineryId())
                .eqIfPresent(MesCollectRealtimeHistoryDO::getTaskId, reqVO.getTaskId())
                .eqIfPresent(MesCollectRealtimeHistoryDO::getWorkOrderId, reqVO.getWorkOrderId())
                .betweenIfPresent(MesCollectRealtimeHistoryDO::getCollectTime, reqVO.getCollectTime())
                .orderByDesc(MesCollectRealtimeHistoryDO::getCollectTime));
    }
    default int deleteBefore(LocalDateTime before, int limit) {
        return delete(new LambdaQueryWrapperX<MesCollectRealtimeHistoryDO>()
                .lt(MesCollectRealtimeHistoryDO::getCollectTime, before)
                .last("LIMIT " + limit));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectRealtimeMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.mes.dal.mysql.collect;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectRealtimeDO;
import org.apache.ibatis.annotations.Mapper;
/**
 * MES æ•°é‡‡-设备实时状态 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesCollectRealtimeMapper extends BaseMapperX<MesCollectRealtimeDO> {
    default MesCollectRealtimeDO selectByDeviceCode(String deviceCode) {
        return selectOne(MesCollectRealtimeDO::getDeviceCode, deviceCode);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/collect/MesCollectTechMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.mes.dal.mysql.collect;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectTechDO;
import org.apache.ibatis.annotations.Mapper;
/**
 * MES æ•°é‡‡-工艺参数快照 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesCollectTechMapper extends BaseMapperX<MesCollectTechDO> {
    default MesCollectTechDO selectByDeviceCode(String deviceCode) {
        return selectOne(MesCollectTechDO::getDeviceCode, deviceCode);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/machinery/MesDvMachineryMapper.java
@@ -37,4 +37,20 @@
        return selectCount(MesDvMachineryDO::getMachineryTypeId, machineryTypeId);
    }
    /**
     * æŒ‰æ•°é‡‡è®¾å¤‡ç¼–号查询设备(MQTT devId)
     */
    default MesDvMachineryDO selectByCollectDeviceCode(String collectDeviceCode) {
        return selectOne(MesDvMachineryDO::getCollectDeviceCode, collectDeviceCode);
    }
    /**
     * æŸ¥è¯¢æ‰€æœ‰å·²é…ç½®æ•°é‡‡è®¾å¤‡ç¼–号的设备
     */
    default java.util.List<MesDvMachineryDO> selectListWithCollectDeviceCode() {
        return selectList(new LambdaQueryWrapperX<MesDvMachineryDO>()
                .isNotNull(MesDvMachineryDO::getCollectDeviceCode)
                .orderByAsc(MesDvMachineryDO::getId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/repair/MesDvRepairMapper.java
@@ -15,6 +15,14 @@
@Mapper
public interface MesDvRepairMapper extends BaseMapperX<MesDvRepairDO> {
    /**
     * åŠ è¡Œé”æŸ¥è¯¢ç»´ä¿®å·¥å•ï¼šå¤‡ä»¶å‡ºåº“æ—¶é˜²æ­¢å¹¶å‘æ‰£å‡
     */
    default MesDvRepairDO lockForSpare(Long id) {
        return selectOne(new LambdaQueryWrapperX<MesDvRepairDO>()
                .eq(MesDvRepairDO::getId, id).last("FOR UPDATE"));
    }
    default PageResult<MesDvRepairDO> selectPage(MesDvRepairPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesDvRepairDO>()
                .likeIfPresent(MesDvRepairDO::getCode, reqVO.getCode())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/spare/MesDvSpareMovementMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.mes.dal.mysql.dv.spare;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.spare.MesDvSpareMovementDO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.spare.vo.MesDvSparePageReqVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.*;
@Mapper
public interface MesDvSpareMovementMapper extends BaseMapperX<MesDvSpareMovementDO> {
    default MesDvSpareMovementDO byKey(String key) {
        return selectOne(MesDvSpareMovementDO::getRequestKey,key);
    }
    default MesDvSpareMovementDO lock(Long id) {
        return selectOne(new LambdaQueryWrapperX<MesDvSpareMovementDO>().eq(MesDvSpareMovementDO::getId,id).last("FOR UPDATE"));
    }
    default List<MesDvSpareMovementDO> returns(Collection<Long> ids) {
        if (ids.isEmpty()) return List.of();
        return selectList(new LambdaQueryWrapperX<MesDvSpareMovementDO>()
                .eq(MesDvSpareMovementDO::getType,4).in(MesDvSpareMovementDO::getOriginalId,ids));
    }
    default PageResult<MesDvSpareMovementDO> page(MesDvSparePageReqVO req) {
        return selectPage(req,new LambdaQueryWrapperX<MesDvSpareMovementDO>()
                .eqIfPresent(MesDvSpareMovementDO::getSparePartId,req.getSparePartId())
                .eqIfPresent(MesDvSpareMovementDO::getRepairId,req.getRepairId())
                .eqIfPresent(MesDvSpareMovementDO::getRepairCode,req.getRepairCode())
                .eqIfPresent(MesDvSpareMovementDO::getType,req.getType())
                .orderByDesc(MesDvSpareMovementDO::getId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/spare/MesDvSparePartMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.mes.dal.mysql.dv.spare;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.spare.MesDvSparePartDO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.spare.vo.MesDvSparePageReqVO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MesDvSparePartMapper extends BaseMapperX<MesDvSparePartDO> {
    default MesDvSparePartDO lock(Long id) {
        return selectOne(new LambdaQueryWrapperX<MesDvSparePartDO>().eq(MesDvSparePartDO::getId,id).last("FOR UPDATE"));
    }
    default PageResult<MesDvSparePartDO> page(MesDvSparePageReqVO req) {
        return selectPage(req,new LambdaQueryWrapperX<MesDvSparePartDO>()
                .eqIfPresent(MesDvSparePartDO::getCode,req.getCode())
                .likeIfPresent(MesDvSparePartDO::getName,req.getName())
                .eqIfPresent(MesDvSparePartDO::getStatus,req.getStatus())
                .apply(Boolean.TRUE.equals(req.getShortageOnly()),"stock_quantity < safety_quantity")
                .orderByDesc(MesDvSparePartDO::getId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/energy/EnergyAlertLogMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
package cn.iocoder.yudao.module.mes.dal.mysql.energy;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.energy.EnergyAlertLogDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface EnergyAlertLogMapper extends BaseMapperX<EnergyAlertLogDO> {}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/energy/EnergyAlertRuleMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
package cn.iocoder.yudao.module.mes.dal.mysql.energy;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.energy.EnergyAlertRuleDO;
import org.apache.ibatis.annotations.*;
@Mapper
public interface EnergyAlertRuleMapper extends BaseMapperX<EnergyAlertRuleDO> {
    // Serialize checks for one rule, including concurrent record submissions.
    @Select("SELECT * FROM mes_energy_alert_rule WHERE id=#{id} AND deleted=0 FOR UPDATE")
    EnergyAlertRuleDO lock(@Param("id") Long id);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/energy/EnergyRecordMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
package cn.iocoder.yudao.module.mes.dal.mysql.energy;
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.energy.vo.EnergyRecordPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.energy.vo.EnergyAnalysisReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.energy.EnergyRecordDO;
import lombok.Data;
import org.apache.ibatis.annotations.*;
import java.math.BigDecimal;
import java.util.List;
@Mapper
public interface EnergyRecordMapper extends BaseMapperX<EnergyRecordDO> {
    @Select("""
        <script>
        SELECT COALESCE(SUM(<choose><when test="rule.metric == 'AMOUNT' or rule.metric == 'MONTH_AMOUNT'">amount</when><otherwise>quantity</otherwise></choose>),0)
        FROM mes_energy_record WHERE deleted=0 AND energy_type=#{rule.energyType}
        <choose><when test="rule.metric == 'MONTH_QUANTITY' or rule.metric == 'MONTH_AMOUNT'">
        AND record_date &gt;= #{date} AND record_date &lt; DATE_ADD(#{date}, INTERVAL 1 MONTH)
        </when><otherwise>AND record_date=#{date}</otherwise></choose>
        <if test="rule.usageArea != null and rule.usageArea != ''">AND usage_area=#{rule.usageArea}</if>
        <if test="rule.productCategory != null and rule.productCategory != ''">AND product_category=#{rule.productCategory}</if>
        </script>
        """)
    BigDecimal selectDailyValue(@Param("date") java.time.LocalDate date,
            @Param("rule") cn.iocoder.yudao.module.mes.dal.dataobject.energy.EnergyAlertRuleDO rule);
    default PageResult<EnergyRecordDO> selectPage(EnergyRecordPageReqVO req) {
        return selectPage(req, new LambdaQueryWrapperX<EnergyRecordDO>()
                .geIfPresent(EnergyRecordDO::getRecordDate, req.getStartDate())
                .leIfPresent(EnergyRecordDO::getRecordDate, req.getEndDate())
                .eqIfPresent(EnergyRecordDO::getEnergyType, req.getEnergyType())
                .likeIfPresent(EnergyRecordDO::getUsageArea, req.getUsageArea())
                .eqIfPresent(EnergyRecordDO::getProductCategory, req.getProductCategory())
                .orderByDesc(EnergyRecordDO::getRecordDate, EnergyRecordDO::getId));
    }
    // æŒ‰æ•°æ®åº“分组,避免加载全部台账;租户拦截器自动添加 tenant_id æ¡ä»¶ã€‚
    @Select("""
        <script>
        SELECT energy_type AS energyType,
          <choose><when test="req.groupBy == 'MONTH'">DATE_FORMAT(record_date, '%Y-%m')</when>
          <otherwise>DATE_FORMAT(record_date, '%Y-%m-%d')</otherwise></choose> AS period,
          usage_area AS usageArea, SUM(quantity) AS quantity, SUM(amount) AS amount,
          COUNT(*) AS recordCount
        FROM mes_energy_record
        WHERE deleted = 0 AND record_date BETWEEN #{req.startDate} AND #{req.endDate}
        <if test="req.energyType != null and req.energyType != ''">AND energy_type = #{req.energyType}</if>
        <if test="req.usageArea != null and req.usageArea != ''">AND usage_area LIKE CONCAT('%', #{req.usageArea}, '%')</if>
        <if test="req.productCategory != null and req.productCategory != ''">AND product_category = #{req.productCategory}</if>
        GROUP BY energy_type, period, usage_area
        ORDER BY period, energy_type, usage_area
        </script>
        """)
    List<Aggregate> selectAnalysis(@Param("req") EnergyAnalysisReqVO req);
    @Data
    class Aggregate {
        private String energyType;
        private String period;
        private String usageArea;
        private BigDecimal quantity;
        private BigDecimal amount;
        private long recordCount;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/task/MesProTaskMapper.java
@@ -133,4 +133,19 @@
                .setSql("last_feedback_time = NOW()"));
    }
    /**
     * æŸ¥è¯¢å·¥ä½œç«™ä¸Šæ­£åœ¨è¿è¡Œçš„任务:生产中、部分完成、已停工
     *
     * <p>数采模块根据设备上报的数据定位当前任务时使用。</p>
     */
    default MesProTaskDO selectRunningTaskByWorkstationId(Long workstationId) {
        return selectOne(new LambdaQueryWrapperX<MesProTaskDO>()
                .eq(MesProTaskDO::getWorkstationId, workstationId)
                .in(MesProTaskDO::getStatus, MesProTaskStatusEnum.PRODUCING.getStatus(),
                        MesProTaskStatusEnum.PARTIAL_FINISHED.getStatus(), MesProTaskStatusEnum.PAUSED.getStatus())
                .orderByDesc(MesProTaskDO::getLastFeedbackTime)
                .orderByDesc(MesProTaskDO::getId)
                .last("LIMIT 1"));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/ledger/MesQcInspectionLedgerMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.mes.dal.mysql.qc.ledger;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ledger.vo.MesQcInspectionLedgerPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ledger.vo.MesQcInspectionLedgerRespVO;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
 * MES æ£€æµ‹ç»“果台账 Mapper
 *
 * ä½¿ç”¨ UNION ALL SQL ä»Žå››ç±»æ£€éªŒå•(IQC/IPQC/OQC/RQC)拉取检验单,再关联检验结果明细与检测项,一行一个检测项结果
 */
@Mapper
public interface MesQcInspectionLedgerMapper {
    IPage<MesQcInspectionLedgerRespVO> selectInspectionLedgerPage(IPage<MesQcInspectionLedgerRespVO> page,
                                                                 @Param("reqVO") MesQcInspectionLedgerPageReqVO reqVO);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyAccidentMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
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.train.SafetyAccidentPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyAccidentDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
 * å®‰å…¨ç”Ÿäº§ - äº‹æ•…登记 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface SafetyAccidentMapper extends BaseMapperX<SafetyAccidentDO> {
    default PageResult<SafetyAccidentDO> selectPage(SafetyAccidentPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<SafetyAccidentDO>()
                .likeIfPresent(SafetyAccidentDO::getCode, reqVO.getCode())
                .likeIfPresent(SafetyAccidentDO::getTitle, reqVO.getTitle())
                .eqIfPresent(SafetyAccidentDO::getAccidentType, reqVO.getAccidentType())
                .eqIfPresent(SafetyAccidentDO::getLevel, reqVO.getLevel())
                .eqIfPresent(SafetyAccidentDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(SafetyAccidentDO::getOccurTime, reqVO.getOccurTime())
                .orderByDesc(SafetyAccidentDO::getOccurTime)
                .orderByDesc(SafetyAccidentDO::getId));
    }
    default List<SafetyAccidentDO> selectListBetween(java.time.LocalDateTime begin, java.time.LocalDateTime end) {
        return selectList(new LambdaQueryWrapperX<SafetyAccidentDO>()
                .geIfPresent(SafetyAccidentDO::getOccurTime, begin)
                .leIfPresent(SafetyAccidentDO::getOccurTime, end)
                .orderByAsc(SafetyAccidentDO::getOccurTime));
    }
    @Select("SELECT CONCAT(#{prefix}, LPAD(COUNT(*) + 1, 4, '0')) FROM safety_accident WHERE code LIKE CONCAT(#{prefix}, '%')")
    String selectNextCode(@Param("prefix") String prefix);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyCertificateMapper.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.cert.SafetyCertificatePageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyCertificateDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDate;
import java.util.List;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰¹ç§ä½œä¸šè¯ Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface SafetyCertificateMapper extends BaseMapperX<SafetyCertificateDO> {
    default PageResult<SafetyCertificateDO> selectPage(SafetyCertificatePageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<SafetyCertificateDO>()
                .likeIfPresent(SafetyCertificateDO::getEmployeeName, reqVO.getEmployeeName())
                .likeIfPresent(SafetyCertificateDO::getEmployeeNo, reqVO.getEmployeeNo())
                .likeIfPresent(SafetyCertificateDO::getDeptName, reqVO.getDeptName())
                .eqIfPresent(SafetyCertificateDO::getCertType, reqVO.getCertType())
                .eqIfPresent(SafetyCertificateDO::getStatus, reqVO.getStatus())
                .likeIfPresent(SafetyCertificateDO::getCertNo, reqVO.getCertNo())
                .geIfPresent(SafetyCertificateDO::getExpireDate, reqVO.getExpireDateStart())
                .leIfPresent(SafetyCertificateDO::getExpireDate, reqVO.getExpireDateEnd())
                .orderByAsc(SafetyCertificateDO::getExpireDate)
                .orderByDesc(SafetyCertificateDO::getId));
    }
    default SafetyCertificateDO selectByCertNo(String certNo) {
        return selectOne(SafetyCertificateDO::getCertNo, certNo);
    }
    /** æŸ¥è¯¢å³å°†åˆ°æœŸæˆ–已过期的证件(expireDate <= æˆªæ­¢æ—¥æœŸï¼‰ */
    default List<SafetyCertificateDO> selectExpiringBefore(LocalDate deadline) {
        return selectList(new LambdaQueryWrapperX<SafetyCertificateDO>()
                .le(SafetyCertificateDO::getExpireDate, deadline)
                .orderByAsc(SafetyCertificateDO::getExpireDate));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyDrillMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
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.train.SafetyDrillPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyDrillDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * å®‰å…¨ç”Ÿäº§ - æ¼”练记录 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface SafetyDrillMapper extends BaseMapperX<SafetyDrillDO> {
    default PageResult<SafetyDrillDO> selectPage(SafetyDrillPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<SafetyDrillDO>()
                .likeIfPresent(SafetyDrillDO::getTitle, reqVO.getTitle())
                .eqIfPresent(SafetyDrillDO::getDrillType, reqVO.getDrillType())
                .likeIfPresent(SafetyDrillDO::getOrganizer, reqVO.getOrganizer())
                .betweenIfPresent(SafetyDrillDO::getDrillDate, reqVO.getDrillDate())
                .orderByDesc(SafetyDrillDO::getDrillDate)
                .orderByDesc(SafetyDrillDO::getId));
    }
    default List<SafetyDrillDO> selectListBetween(java.time.LocalDate begin, java.time.LocalDate end) {
        return selectList(new LambdaQueryWrapperX<SafetyDrillDO>()
                .geIfPresent(SafetyDrillDO::getDrillDate, begin)
                .leIfPresent(SafetyDrillDO::getDrillDate, end)
                .orderByAsc(SafetyDrillDO::getDrillDate));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyHazardMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
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.hazard.SafetyHazardPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyHazardDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
 * å®‰å…¨ç”Ÿäº§ - é𐿂£ Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface SafetyHazardMapper extends BaseMapperX<SafetyHazardDO> {
    default PageResult<SafetyHazardDO> selectPage(SafetyHazardPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<SafetyHazardDO>()
                .likeIfPresent(SafetyHazardDO::getCode, reqVO.getCode())
                .likeIfPresent(SafetyHazardDO::getTitle, reqVO.getTitle())
                .eqIfPresent(SafetyHazardDO::getLevel, reqVO.getLevel())
                .eqIfPresent(SafetyHazardDO::getSource, reqVO.getSource())
                .eqIfPresent(SafetyHazardDO::getStatus, reqVO.getStatus())
                .likeIfPresent(SafetyHazardDO::getAssignee, reqVO.getAssignee())
                .likeIfPresent(SafetyHazardDO::getLocation, reqVO.getLocation())
                .geIfPresent(SafetyHazardDO::getReportTime, reqVO.getReportTimeStart())
                .leIfPresent(SafetyHazardDO::getReportTime, reqVO.getReportTimeEnd())
                .orderByDesc(SafetyHazardDO::getReportTime)
                .orderByDesc(SafetyHazardDO::getId));
    }
    /** è¶…过整改期限且未闭环的隐患 */
    default List<SafetyHazardDO> selectOverdue(java.time.LocalDate today) {
        return selectList(new LambdaQueryWrapperX<SafetyHazardDO>()
                .lt(SafetyHazardDO::getRectifyDeadline, today)
                .notIn(SafetyHazardDO::getStatus, "CLOSED")
                .orderByAsc(SafetyHazardDO::getRectifyDeadline));
    }
    /** ç”Ÿæˆé𐿂£ç¼–号:HZ + yyyyMMdd + 4 ä½æµæ°´ */
    @Select("SELECT CONCAT(#{prefix}, LPAD(COUNT(*) + 1, 4, '0')) FROM safety_hazard WHERE code LIKE CONCAT(#{prefix}, '%')")
    String selectNextCode(@Param("prefix") String prefix);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyInspectionMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
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.inspection.SafetyInspectionPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyInspectionDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
import java.util.List;
/**
 * å®‰å…¨ç”Ÿäº§ - å·¡æ£€è®°å½• Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface SafetyInspectionMapper extends BaseMapperX<SafetyInspectionDO> {
    default PageResult<SafetyInspectionDO> selectPage(SafetyInspectionPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<SafetyInspectionDO>()
                .eqIfPresent(SafetyInspectionDO::getPointId, reqVO.getPointId())
                .eqIfPresent(SafetyInspectionDO::getPointType, reqVO.getPointType())
                .likeIfPresent(SafetyInspectionDO::getInspector, reqVO.getInspector())
                .eqIfPresent(SafetyInspectionDO::getResult, reqVO.getResult())
                .betweenIfPresent(SafetyInspectionDO::getInspectTime, reqVO.getInspectTime())
                .orderByDesc(SafetyInspectionDO::getInspectTime)
                .orderByDesc(SafetyInspectionDO::getId));
    }
    default List<SafetyInspectionDO> selectListByTimeRange(LocalDateTime begin, LocalDateTime end) {
        return selectList(new LambdaQueryWrapperX<SafetyInspectionDO>()
                .geIfPresent(SafetyInspectionDO::getInspectTime, begin)
                .leIfPresent(SafetyInspectionDO::getInspectTime, end)
                .orderByAsc(SafetyInspectionDO::getInspectTime));
    }
    default Long selectCountByPointId(Long pointId) {
        return selectCount(SafetyInspectionDO::getPointId, pointId);
    }
    /** æŸ¥è¯¢æŸç‚¹ä½æœ€è¿‘一次巡检记录 */
    default SafetyInspectionDO selectLatestByPointId(Long pointId) {
        return selectOne(new LambdaQueryWrapperX<SafetyInspectionDO>()
                .eq(SafetyInspectionDO::getPointId, pointId)
                .orderByDesc(SafetyInspectionDO::getInspectTime)
                .last("LIMIT 1"));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyInspectionPointMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
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.inspection.SafetyInspectionPointPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyInspectionPointDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * å®‰å…¨ç”Ÿäº§ - å·¡æ£€ç‚¹ä½ Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface SafetyInspectionPointMapper extends BaseMapperX<SafetyInspectionPointDO> {
    default PageResult<SafetyInspectionPointDO> selectPage(SafetyInspectionPointPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<SafetyInspectionPointDO>()
                .likeIfPresent(SafetyInspectionPointDO::getCode, reqVO.getCode())
                .likeIfPresent(SafetyInspectionPointDO::getName, reqVO.getName())
                .eqIfPresent(SafetyInspectionPointDO::getPointType, reqVO.getPointType())
                .likeIfPresent(SafetyInspectionPointDO::getWorkshopName, reqVO.getWorkshopName())
                .eqIfPresent(SafetyInspectionPointDO::getStatus, reqVO.getStatus())
                .orderByAsc(SafetyInspectionPointDO::getCode));
    }
    default SafetyInspectionPointDO selectByCode(String code) {
        return selectOne(SafetyInspectionPointDO::getCode, code);
    }
    default List<SafetyInspectionPointDO> selectListByStatus(Integer status) {
        return selectList(new LambdaQueryWrapperX<SafetyInspectionPointDO>()
                .eq(SafetyInspectionPointDO::getStatus, status)
                .orderByAsc(SafetyInspectionPointDO::getCode));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyMaterialMapper.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.material.SafetyMaterialPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyMaterialDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDate;
import java.util.List;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰©èµ„台账 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface SafetyMaterialMapper extends BaseMapperX<SafetyMaterialDO> {
    default PageResult<SafetyMaterialDO> selectPage(SafetyMaterialPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<SafetyMaterialDO>()
                .likeIfPresent(SafetyMaterialDO::getCode, reqVO.getCode())
                .likeIfPresent(SafetyMaterialDO::getName, reqVO.getName())
                .eqIfPresent(SafetyMaterialDO::getCategory, reqVO.getCategory())
                .eqIfPresent(SafetyMaterialDO::getStatus, reqVO.getStatus())
                .likeIfPresent(SafetyMaterialDO::getStorageLocation, reqVO.getStorageLocation())
                .orderByAsc(SafetyMaterialDO::getCategory)
                .orderByAsc(SafetyMaterialDO::getCode));
    }
    default SafetyMaterialDO selectByCode(String code) {
        return selectOne(SafetyMaterialDO::getCode, code);
    }
    /** é”å®šåº“存行,避免并发出入库导致库存错乱 */
    @Select("SELECT * FROM safety_material WHERE id = #{id} AND deleted = b'0' FOR UPDATE")
    SafetyMaterialDO selectByIdForUpdate(@Param("id") Long id);
    /** ä½ŽäºŽå®‰å…¨åº“存的物资 */
    default List<SafetyMaterialDO> selectBelowSafeStock() {
        return selectList(new LambdaQueryWrapperX<SafetyMaterialDO>()
                .apply("stock_qty < safe_stock")
                .orderByAsc(SafetyMaterialDO::getCode));
    }
    /** æœ‰æ•ˆæœŸä¸´è¿‘的物资 */
    default List<SafetyMaterialDO> selectExpiringBefore(LocalDate deadline) {
        return selectList(new LambdaQueryWrapperX<SafetyMaterialDO>()
                .isNotNull(SafetyMaterialDO::getExpireDate)
                .le(SafetyMaterialDO::getExpireDate, deadline)
                .orderByAsc(SafetyMaterialDO::getExpireDate));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyMaterialStockMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
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.material.SafetyMaterialStockPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyMaterialStockDO;
import org.apache.ibatis.annotations.Mapper;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰©èµ„出入库流水 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface SafetyMaterialStockMapper extends BaseMapperX<SafetyMaterialStockDO> {
    default PageResult<SafetyMaterialStockDO> selectPage(SafetyMaterialStockPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<SafetyMaterialStockDO>()
                .eqIfPresent(SafetyMaterialStockDO::getMaterialId, reqVO.getMaterialId())
                .likeIfPresent(SafetyMaterialStockDO::getMaterialName, reqVO.getMaterialName())
                .eqIfPresent(SafetyMaterialStockDO::getCategory, reqVO.getCategory())
                .eqIfPresent(SafetyMaterialStockDO::getBizType, reqVO.getBizType())
                .betweenIfPresent(SafetyMaterialStockDO::getBizDate, reqVO.getBizDate())
                .orderByDesc(SafetyMaterialStockDO::getBizDate)
                .orderByDesc(SafetyMaterialStockDO::getId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyOrgMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
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.SafetyOrgDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * å®‰å…¨ç”Ÿäº§ - ç»„织架构 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface SafetyOrgMapper extends BaseMapperX<SafetyOrgDO> {
    default List<SafetyOrgDO> selectListOrdered() {
        return selectList(new LambdaQueryWrapperX<SafetyOrgDO>()
                .orderByAsc(SafetyOrgDO::getSort)
                .orderByAsc(SafetyOrgDO::getId));
    }
    default List<SafetyOrgDO> selectListByParentId(Long parentId) {
        return selectList(new LambdaQueryWrapperX<SafetyOrgDO>()
                .eq(SafetyOrgDO::getParentId, parentId)
                .orderByAsc(SafetyOrgDO::getSort));
    }
    default Long selectCountByParentId(Long parentId) {
        return selectCount(SafetyOrgDO::getParentId, parentId);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyTrainingMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
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.train.SafetyTrainingPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyTrainingDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * å®‰å…¨ç”Ÿäº§ - åŸ¹è®­è®°å½• Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface SafetyTrainingMapper extends BaseMapperX<SafetyTrainingDO> {
    default PageResult<SafetyTrainingDO> selectPage(SafetyTrainingPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<SafetyTrainingDO>()
                .likeIfPresent(SafetyTrainingDO::getTitle, reqVO.getTitle())
                .eqIfPresent(SafetyTrainingDO::getTrainingType, reqVO.getTrainingType())
                .likeIfPresent(SafetyTrainingDO::getTrainer, reqVO.getTrainer())
                .betweenIfPresent(SafetyTrainingDO::getTrainDate, reqVO.getTrainDate())
                .orderByDesc(SafetyTrainingDO::getTrainDate)
                .orderByDesc(SafetyTrainingDO::getId));
    }
    default List<SafetyTrainingDO> selectListBetween(java.time.LocalDate begin, java.time.LocalDate end) {
        return selectList(new LambdaQueryWrapperX<SafetyTrainingDO>()
                .geIfPresent(SafetyTrainingDO::getTrainDate, begin)
                .leIfPresent(SafetyTrainingDO::getTrainDate, end)
                .orderByAsc(SafetyTrainingDO::getTrainDate));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyWorkPermitMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
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.permit.SafetyWorkPermitPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyWorkPermitDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
 * å®‰å…¨ç”Ÿäº§ - é«˜å±ä½œä¸šç¥¨ Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface SafetyWorkPermitMapper extends BaseMapperX<SafetyWorkPermitDO> {
    default PageResult<SafetyWorkPermitDO> selectPage(SafetyWorkPermitPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<SafetyWorkPermitDO>()
                .likeIfPresent(SafetyWorkPermitDO::getCode, reqVO.getCode())
                .likeIfPresent(SafetyWorkPermitDO::getTitle, reqVO.getTitle())
                .eqIfPresent(SafetyWorkPermitDO::getPermitType, reqVO.getPermitType())
                .eqIfPresent(SafetyWorkPermitDO::getStatus, reqVO.getStatus())
                .eqIfPresent(SafetyWorkPermitDO::getRiskLevel, reqVO.getRiskLevel())
                .likeIfPresent(SafetyWorkPermitDO::getApplicant, reqVO.getApplicant())
                .likeIfPresent(SafetyWorkPermitDO::getWorkshopName, reqVO.getWorkshopName())
                .betweenIfPresent(SafetyWorkPermitDO::getPlanStartTime, reqVO.getPlanStartTime())
                .orderByDesc(SafetyWorkPermitDO::getApplyTime)
                .orderByDesc(SafetyWorkPermitDO::getId));
    }
    @Select("SELECT CONCAT(#{prefix}, LPAD(COUNT(*) + 1, 4, '0')) FROM safety_work_permit WHERE code LIKE CONCAT(#{prefix}, '%')")
    String selectNextCode(@Param("prefix") String prefix);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/safety/SafetyWorkPermitMonitorMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
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.SafetyWorkPermitMonitorDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * å®‰å…¨ç”Ÿäº§ - ä½œä¸šç›‘护记录 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface SafetyWorkPermitMonitorMapper extends BaseMapperX<SafetyWorkPermitMonitorDO> {
    default List<SafetyWorkPermitMonitorDO> selectListByPermitId(Long permitId) {
        return selectList(new LambdaQueryWrapperX<SafetyWorkPermitMonitorDO>()
                .eq(SafetyWorkPermitMonitorDO::getPermitId, permitId)
                .orderByDesc(SafetyWorkPermitMonitorDO::getMonitorTime));
    }
    default int deleteByPermitId(Long permitId) {
        return delete(SafetyWorkPermitMonitorDO::getPermitId, permitId);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -1001,4 +1001,8 @@
    ErrorCode PD_CTC_REEL_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_040_960_010, "导入盘具计算数据不能为空!");
    ErrorCode PD_TECH_CONFIRM_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_040_960_011, "导入技术确认函数据不能为空!");
    // ========== MES æ•°é‡‡-报警(1-040-920-000) ==========
    ErrorCode COLLECT_ALARM_NOT_EXISTS = new ErrorCode(1_040_920_001, "报警记录不存在");
    ErrorCode COLLECT_ALARM_ALREADY_CLEARED = new ErrorCode(1_040_920_002, "该报警已消除,无需重复确认");
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/SafetyErrorCodeConstants.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
package cn.iocoder.yudao.module.mes.enums;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
/**
 * MES æ¨¡å—错误码 - å®‰å…¨ç”Ÿäº§å­æ¨¡å—
 *
 * å®‰å…¨ç”Ÿäº§æ¨¡å—错误码区间:1_040_960_xxx
 *
 * @author è¶…级管理员
 */
public interface SafetyErrorCodeConstants {
    // ========== å®‰å…¨ç»„织架构 1_040_961_xxx ==========
    ErrorCode SAFETY_ORG_NOT_EXISTS = new ErrorCode(1_040_961_000, "安全组织不存在");
    ErrorCode SAFETY_ORG_PARENT_NOT_EXISTS = new ErrorCode(1_040_961_001, "父级安全组织不存在");
    ErrorCode SAFETY_ORG_PARENT_IS_SELF = new ErrorCode(1_040_961_002, "上级组织不能选择自己");
    ErrorCode SAFETY_ORG_PARENT_IS_CHILD = new ErrorCode(1_040_961_003, "上级组织不能选择自己的子组织");
    ErrorCode SAFETY_ORG_HAS_CHILDREN = new ErrorCode(1_040_961_004, "存在子组织,无法删除");
    // ========== ç‰¹ç§ä½œä¸šè¯ 1_040_962_xxx ==========
    ErrorCode SAFETY_CERT_NOT_EXISTS = new ErrorCode(1_040_962_000, "特种作业证不存在");
    ErrorCode SAFETY_CERT_NO_DUPLICATE = new ErrorCode(1_040_962_001, "证件编号已存在");
    ErrorCode SAFETY_CERT_EXPIRE_BEFORE_ISSUE = new ErrorCode(1_040_962_002, "有效期不能早于发证日期");
    ErrorCode SAFETY_CERT_EMPLOYEE_NOT_EXISTS = new ErrorCode(1_040_962_003, "持证人不存在,请从员工档案中选择");
    // ========== å·¡æ£€ç‚¹ä½ / å·¡æ£€è®°å½• 1_040_963_xxx ==========
    ErrorCode SAFETY_POINT_NOT_EXISTS = new ErrorCode(1_040_963_000, "巡检点位不存在");
    ErrorCode SAFETY_POINT_CODE_DUPLICATE = new ErrorCode(1_040_963_001, "点位编码已存在");
    ErrorCode SAFETY_POINT_DISABLED = new ErrorCode(1_040_963_002, "巡检点位已停用");
    ErrorCode SAFETY_INSPECTION_NOT_EXISTS = new ErrorCode(1_040_963_010, "巡检记录不存在");
    // ========== é𐿂£é—­çޝ 1_040_964_xxx ==========
    ErrorCode SAFETY_HAZARD_NOT_EXISTS = new ErrorCode(1_040_964_000, "隐患记录不存在");
    ErrorCode SAFETY_HAZARD_STATUS_ERROR = new ErrorCode(1_040_964_001, "当前隐患状态不允许该操作");
    ErrorCode SAFETY_HAZARD_ASSIGNEE_REQUIRED = new ErrorCode(1_040_964_002, "分派必须指定整改责任人");
    ErrorCode SAFETY_HAZARD_SAME_OPERATOR = new ErrorCode(1_040_964_003, "整改人与复查人不能是同一人");
    // ========== é«˜å±ä½œä¸šç¥¨ 1_040_965_xxx ==========
    ErrorCode SAFETY_PERMIT_NOT_EXISTS = new ErrorCode(1_040_965_000, "作业票不存在");
    ErrorCode SAFETY_PERMIT_STATUS_ERROR = new ErrorCode(1_040_965_001, "当前作业票状态不允许该操作");
    ErrorCode SAFETY_PERMIT_TIME_INVALID = new ErrorCode(1_040_965_002, "计划结束时间必须晚于开始时间");
    ErrorCode SAFETY_PERMIT_NOT_APPROVED = new ErrorCode(1_040_965_003, "作业票未审批通过,不能开始作业");
    ErrorCode SAFETY_PERMIT_GUARDIAN_REQUIRED = new ErrorCode(1_040_965_004, "高危作业必须指定监护人");
    ErrorCode SAFETY_PERMIT_SELF_APPROVE = new ErrorCode(1_040_965_005, "申请人不能审批自己的作业票");
    ErrorCode SAFETY_PERMIT_MONITOR_NOT_EXISTS = new ErrorCode(1_040_965_010, "监护记录不存在");
    // ========== ç‰©èµ„台账 1_040_966_xxx ==========
    ErrorCode SAFETY_MATERIAL_NOT_EXISTS = new ErrorCode(1_040_966_000, "物资不存在");
    ErrorCode SAFETY_MATERIAL_CODE_DUPLICATE = new ErrorCode(1_040_966_001, "物资编码已存在");
    ErrorCode SAFETY_MATERIAL_STOCK_NOT_ENOUGH = new ErrorCode(1_040_966_002, "库存不足,无法出库");
    ErrorCode SAFETY_MATERIAL_STOCK_RECORD_NOT_EXISTS = new ErrorCode(1_040_966_010, "出入库流水不存在");
    // ========== åŸ¹è®­ / æ¼”练 / äº‹æ•… 1_040_967_xxx ==========
    ErrorCode SAFETY_TRAINING_NOT_EXISTS = new ErrorCode(1_040_967_000, "培训记录不存在");
    ErrorCode SAFETY_DRILL_NOT_EXISTS = new ErrorCode(1_040_967_010, "演练记录不存在");
    ErrorCode SAFETY_ACCIDENT_NOT_EXISTS = new ErrorCode(1_040_967_020, "事故记录不存在");
    // ========== ä¸»æ•°æ®å¼•用 1_040_969_xxx ==========
    ErrorCode SAFETY_WORKSHOP_NOT_EXISTS = new ErrorCode(1_040_969_000, "车间不存在,请从车间主数据中选择");
    ErrorCode SAFETY_EMPLOYEE_REQUIRED = new ErrorCode(1_040_969_001, "请选择员工");
    // ========== å°è´¦å¯¼å‡º 1_040_968_xxx ==========
    ErrorCode SAFETY_LEDGER_EXPORT_ITEM_UNKNOWN = new ErrorCode(1_040_968_000, "未知的台账导出项");
    ErrorCode SAFETY_LEDGER_DATE_RANGE_INVALID = new ErrorCode(1_040_968_001, "开始日期不能晚于结束日期");
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/collect/MesCollectAlarmStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.mes.enums.collect;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * MES æ•°é‡‡æŠ¥è­¦çŠ¶æ€
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesCollectAlarmStatusEnum {
    ACTIVE(0, "未消除"),
    CLEARED(1, "已消除");
    private final Integer status;
    private final String name;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/collect/MesCollectMsgTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.enums.collect;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * MES æ•°é‡‡æŠ¥æ–‡ç±»åž‹
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesCollectMsgTypeEnum {
    REALTIME("realtime", "实时数据"),
    SPC("spc", "周期过程数据"),
    TECH("tech", "工艺参数"),
    OP_LOG("opLog", "参数变更日志"),
    ALARM("alarm", "报警"),
    CONNECT("connect", "设备上线"),
    DISCONNECT("disconnect", "设备离线"),
    UNKNOWN("unknown", "未知类型");
    private final String type;
    private final String name;
    public static MesCollectMsgTypeEnum getByType(String type) {
        for (MesCollectMsgTypeEnum value : values()) {
            if (value.type.equalsIgnoreCase(type)) {
                return value;
            }
        }
        return UNKNOWN;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/collect/MesCollectOperateModeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.enums.collect;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * MES æ•°é‡‡è®¾å¤‡è¿è¡Œæ¨¡å¼
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesCollectOperateModeEnum {
    MANUAL(0, "手动"),
    SEMI_AUTO(1, "半自动"),
    EYE_AUTO(2, "电眼自动"),
    TIME_AUTO(3, "时间自动"),
    MOLD_ADJUST(4, "调模使用");
    private final Integer mode;
    private final String name;
    public static String getNameByMode(Integer mode) {
        if (mode == null) {
            return null;
        }
        for (MesCollectOperateModeEnum value : values()) {
            if (value.mode.equals(mode)) {
                return value.name;
            }
        }
        return null;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/collect/MesCollectRunStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.mes.enums.collect;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * MES æ•°é‡‡è®¾å¤‡è¿è¡ŒçŠ¶æ€
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesCollectRunStatusEnum {
    STANDBY(1, "待机"),
    PRODUCING(2, "生产");
    private final Integer status;
    private final String name;
    public static String getNameByStatus(Integer status) {
        if (status == null) {
            return null;
        }
        for (MesCollectRunStatusEnum value : values()) {
            if (value.status.equals(status)) {
                return value.name;
            }
        }
        return null;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyAccidentLevelEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.mes.enums.safety;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * å®‰å…¨ç”Ÿäº§ - äº‹æ•…等级枚举
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum SafetyAccidentLevelEnum {
    SLIGHT("SLIGHT", "轻伤事故"),
    GENERAL("GENERAL", "一般事故"),
    SEVERE("SEVERE", "重伤事故"),
    MAJOR("MAJOR", "重大事故");
    private final String level;
    private final String name;
    public static String getNameByLevel(String level) {
        for (SafetyAccidentLevelEnum value : values()) {
            if (value.getLevel().equals(level)) {
                return value.getName();
            }
        }
        return level;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyAccidentStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.mes.enums.safety;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * å®‰å…¨ç”Ÿäº§ - äº‹æ•…处理状态枚举
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum SafetyAccidentStatusEnum {
    REPORTED("REPORTED", "已登记"),
    INVESTIGATING("INVESTIGATING", "调查中"),
    HANDLED("HANDLED", "已处理"),
    CLOSED("CLOSED", "已结案");
    private final String status;
    private final String name;
    public static String getNameByStatus(String status) {
        for (SafetyAccidentStatusEnum value : values()) {
            if (value.getStatus().equals(status)) {
                return value.getName();
            }
        }
        return status;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyAccidentTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.enums.safety;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * å®‰å…¨ç”Ÿäº§ - äº‹æ•…类型枚举
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum SafetyAccidentTypeEnum {
    ELECTRIC_SHOCK("ELECTRIC_SHOCK", "触电"),
    FIRE("FIRE", "火灾"),
    FALL("FALL", "高处坠落"),
    OBJECT_STRIKE("OBJECT_STRIKE", "物体打击"),
    MECHANICAL("MECHANICAL", "机械伤害"),
    CHEMICAL("CHEMICAL", "中毒窒息"),
    VEHICLE("VEHICLE", "车辆伤害"),
    OTHER("OTHER", "其他");
    private final String type;
    private final String name;
    public static String getNameByType(String type) {
        for (SafetyAccidentTypeEnum value : values()) {
            if (value.getType().equals(type)) {
                return value.getName();
            }
        }
        return type;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyCertificateStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.mes.enums.safety;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰¹ç§ä½œä¸šè¯çŠ¶æ€æžšä¸¾
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum SafetyCertificateStatusEnum {
    VALID("VALID", "有效"),
    EXPIRING("EXPIRING", "即将到期"),
    EXPIRED("EXPIRED", "已过期"),
    REVOKED("REVOKED", "已注销");
    private final String status;
    private final String name;
    public static String getNameByStatus(String status) {
        for (SafetyCertificateStatusEnum value : values()) {
            if (value.getStatus().equals(status)) {
                return value.getName();
            }
        }
        return status;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyCertificateTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.mes.enums.safety;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰¹ç§ä½œä¸šè¯ç±»åž‹æžšä¸¾
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum SafetyCertificateTypeEnum {
    LOW_VOLTAGE_ELECTRICIAN("LOW_VOLTAGE_ELECTRICIAN", "低压电工证"),
    HIGH_VOLTAGE_OPERATION("HIGH_VOLTAGE_OPERATION", "高压操作证"),
    WELDING("WELDING", "焊接与热切割作业证"),
    HOIST("HOIST", "起重机械作业证"),
    HEIGHT_WORK("HEIGHT_WORK", "高处作业证"),
    LIMITED_SPACE("LIMITED_SPACE", "有限空间作业证"),
    FORKLIFT("FORKLIFT", "叉车作业证"),
    BOILER("BOILER", "锅炉作业证"),
    OTHER("OTHER", "其他特种作业证");
    private final String type;
    private final String name;
    public static String getNameByType(String type) {
        for (SafetyCertificateTypeEnum value : values()) {
            if (value.getType().equals(type)) {
                return value.getName();
            }
        }
        return type;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyDrillTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.mes.enums.safety;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * å®‰å…¨ç”Ÿäº§ - æ¼”练类型枚举
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum SafetyDrillTypeEnum {
    FIRE("FIRE", "消防演练"),
    EMERGENCY_EVAC("EMERGENCY_EVAC", "应急疏散演练"),
    ELECTRIC_ACCIDENT("ELECTRIC_ACCIDENT", "触电事故演练"),
    CHEMICAL_LEAK("CHEMICAL_LEAK", "危化品泄漏演练"),
    OTHER("OTHER", "其他演练");
    private final String type;
    private final String name;
    public static String getNameByType(String type) {
        for (SafetyDrillTypeEnum value : values()) {
            if (value.getType().equals(type)) {
                return value.getName();
            }
        }
        return type;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyHazardLevelEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.mes.enums.safety;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * å®‰å…¨ç”Ÿäº§ - é𐿂£ç­‰çº§æžšä¸¾
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum SafetyHazardLevelEnum {
    GENERAL("GENERAL", "一般隐患"),
    MAJOR("MAJOR", "重大隐患");
    private final String level;
    private final String name;
    public static String getNameByLevel(String level) {
        for (SafetyHazardLevelEnum value : values()) {
            if (value.getLevel().equals(level)) {
                return value.getName();
            }
        }
        return level;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyHazardSourceEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.mes.enums.safety;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * å®‰å…¨ç”Ÿäº§ - é𐿂£æ¥æºæžšä¸¾
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum SafetyHazardSourceEnum {
    INSPECTION("INSPECTION", "巡检发现"),
    SELF_CHECK("SELF_CHECK", "自查上报"),
    REPORT("REPORT", "员工上报"),
    EXTERNAL("EXTERNAL", "外部检查");
    private final String source;
    private final String name;
    public static String getNameBySource(String source) {
        for (SafetyHazardSourceEnum value : values()) {
            if (value.getSource().equals(source)) {
                return value.getName();
            }
        }
        return source;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyHazardStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.mes.enums.safety;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * å®‰å…¨ç”Ÿäº§ - é𐿂£çŠ¶æ€æžšä¸¾
 *
 * <p>流转顺序:上报 â†’ åˆ†æ´¾ â†’ æ•´æ”¹ä¸­ â†’ å¾…复查 â†’ å·²é—­çޝ / å¤æŸ¥ä¸é€šè¿‡ï¼ˆé€€å›žæ•´æ”¹ï¼‰ã€‚</p>
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum SafetyHazardStatusEnum {
    REPORTED("REPORTED", "已上报"),
    ASSIGNED("ASSIGNED", "已分派"),
    RECTIFYING("RECTIFYING", "整改中"),
    RECHECKING("RECHECKING", "待复查"),
    REJECTED("REJECTED", "复查不通过"),
    CLOSED("CLOSED", "已闭环");
    private final String status;
    private final String name;
    public static String getNameByStatus(String status) {
        for (SafetyHazardStatusEnum value : values()) {
            if (value.getStatus().equals(status)) {
                return value.getName();
            }
        }
        return status;
    }
    public static boolean isFinished(String status) {
        return CLOSED.getStatus().equals(status);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyInspectCycleEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.mes.enums.safety;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * å®‰å…¨ç”Ÿäº§ - å·¡æ£€å‘¨æœŸæžšä¸¾
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum SafetyInspectCycleEnum {
    SHIFT("SHIFT", "每班"),
    DAILY("DAILY", "每日"),
    WEEKLY("WEEKLY", "每周"),
    MONTHLY("MONTHLY", "每月");
    private final String cycle;
    private final String name;
    public static String getNameByCycle(String cycle) {
        for (SafetyInspectCycleEnum value : values()) {
            if (value.getCycle().equals(cycle)) {
                return value.getName();
            }
        }
        return cycle;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyInspectionPointTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.mes.enums.safety;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * å®‰å…¨ç”Ÿäº§ - å·¡æ£€ç‚¹ä½ç±»åž‹æžšä¸¾
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum SafetyInspectionPointTypeEnum {
    HIGH_VOLTAGE_ROOM("HIGH_VOLTAGE_ROOM", "高压配电室"),
    UNIT_AREA("UNIT_AREA", "机组区"),
    DANGER_CHEMICAL("DANGER_CHEMICAL", "危化品库"),
    FIRE_AREA("FIRE_AREA", "消防重点部位"),
    WORKSHOP("WORKSHOP", "生产车间"),
    OTHER("OTHER", "其他");
    private final String type;
    private final String name;
    public static String getNameByType(String type) {
        for (SafetyInspectionPointTypeEnum value : values()) {
            if (value.getType().equals(type)) {
                return value.getName();
            }
        }
        return type;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyMaterialCategoryEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.mes.enums.safety;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰©èµ„类别枚举
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum SafetyMaterialCategoryEnum {
    INSULATING("INSULATING", "电力绝缘用具"),
    HAZARD_CHEMICAL("HAZARD_CHEMICAL", "危化品"),
    FIRE("FIRE", "消防物资"),
    PPE("PPE", "劳保用品"),
    OTHER("OTHER", "其他");
    private final String category;
    private final String name;
    public static String getNameByCategory(String category) {
        for (SafetyMaterialCategoryEnum value : values()) {
            if (value.getCategory().equals(category)) {
                return value.getName();
            }
        }
        return category;
    }
    /** æ˜¯å¦éœ€è¦æœ‰æ•ˆæœŸç®¡ç†ï¼ˆç»ç¼˜ç”¨å…·ä¸Žå±åŒ–品有保质期) */
    public static boolean requiresExpireDate(String category) {
        return INSULATING.getCategory().equals(category) || HAZARD_CHEMICAL.getCategory().equals(category);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyPermitStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.mes.enums.safety;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * å®‰å…¨ç”Ÿäº§ - é«˜å±ä½œä¸šç¥¨çŠ¶æ€æžšä¸¾
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum SafetyPermitStatusEnum {
    DRAFT("DRAFT", "草稿"),
    PENDING("PENDING", "待审批"),
    APPROVED("APPROVED", "已批准"),
    REJECTED("REJECTED", "已驳回"),
    IN_PROGRESS("IN_PROGRESS", "作业中"),
    CLOSED("CLOSED", "已完结"),
    CANCELLED("CANCELLED", "已作废");
    private final String status;
    private final String name;
    public static String getNameByStatus(String status) {
        for (SafetyPermitStatusEnum value : values()) {
            if (value.getStatus().equals(status)) {
                return value.getName();
            }
        }
        return status;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyPermitTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.mes.enums.safety;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * å®‰å…¨ç”Ÿäº§ - é«˜å±ä½œä¸šç¥¨ç±»åž‹æžšä¸¾
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum SafetyPermitTypeEnum {
    HOT_WORK("HOT_WORK", "动火作业"),
    LIVE_HIGH_VOLTAGE("LIVE_HIGH_VOLTAGE", "高压带电作业"),
    LIMITED_SPACE("LIMITED_SPACE", "有限空间作业"),
    WORK_AT_HEIGHT("WORK_AT_HEIGHT", "高处作业"),
    LIFTING("LIFTING", "吊装作业"),
    TEMP_ELECTRIC("TEMP_ELECTRIC", "临时用电作业"),
    OTHER("OTHER", "其他作业");
    private final String type;
    private final String name;
    public static String getNameByType(String type) {
        for (SafetyPermitTypeEnum value : values()) {
            if (value.getType().equals(type)) {
                return value.getName();
            }
        }
        return type;
    }
    /** æ˜¯å¦å¿…须指定监护人 */
    public static boolean requiresGuardian(String type) {
        return HOT_WORK.getType().equals(type) || LIVE_HIGH_VOLTAGE.getType().equals(type)
                || LIMITED_SPACE.getType().equals(type);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyRiskLevelEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.mes.enums.safety;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * å®‰å…¨ç”Ÿäº§ - é£Žé™©ç­‰çº§æžšä¸¾
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum SafetyRiskLevelEnum {
    LOW("LOW", "低"),
    NORMAL("NORMAL", "一般"),
    HIGH("HIGH", "高"),
    VERY_HIGH("VERY_HIGH", "极高");
    private final String level;
    private final String name;
    public static String getNameByLevel(String level) {
        for (SafetyRiskLevelEnum value : values()) {
            if (value.getLevel().equals(level)) {
                return value.getName();
            }
        }
        return level;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/safety/SafetyTrainingTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.mes.enums.safety;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * å®‰å…¨ç”Ÿäº§ - åŸ¹è®­ç±»åž‹æžšä¸¾
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum SafetyTrainingTypeEnum {
    SAFETY("SAFETY", "安全培训"),
    SKILL("SKILL", "技能培训"),
    LAW("LAW", "法规宣贯"),
    EMERGENCY("EMERGENCY", "应急演练培训"),
    INDUCTION("INDUCTION", "三级安全教育"),
    OTHER("OTHER", "其他");
    private final String type;
    private final String name;
    public static String getNameByType(String type) {
        for (SafetyTrainingTypeEnum value : values()) {
            if (value.getType().equals(type)) {
                return value.getName();
            }
        }
        return type;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/collect/config/MesCollectProperties.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,93 @@
package cn.iocoder.yudao.module.mes.framework.collect.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
 * MES æ•°é‡‡é…ç½®
 *
 * @author è¶…级管理员
 */
@Component
@ConfigurationProperties(prefix = "yudao.mes.collect")
@Data
public class MesCollectProperties {
    /**
     * æŠ¥æ–‡æ ¼å¼ï¼šæ³¨å¡‘机 /YLCY/IMM åè®®ï¼ˆæŠ¥æ–‡å¸¦ topic å­—段,主题末段为报文类型)
     */
    public static final String FORMAT_YLCY = "ylcy";
    /**
     * æŠ¥æ–‡æ ¼å¼ï¼šè®¾å¤‡ç½‘关协议(主题固定 gateway/xxx,报文首层 key ä¸ºè®¾å¤‡åï¼Œå€¼ä¸ºå±žæ€§å­—典)
     */
    public static final String FORMAT_GATEWAY = "gateway";
    private Mqtt mqtt = new Mqtt();
    private Retention retention = new Retention();
    @Data
    public static class Mqtt {
        /**
         * æ˜¯å¦å¯ç”¨æ•°é‡‡ MQTT è®¢é˜…
         */
        private Boolean enabled = false;
        /**
         * Broker åœ°å€ï¼Œä¾‹å¦‚ tcp://36.213.90.123:1883
         */
        private String url;
        private String username;
        private String password;
        /**
         * å®¢æˆ·ç«¯ ID å‰ç¼€
         */
        private String clientIdPrefix = "mes-collect";
        /**
         * ä¸»é¢˜å‰ç¼€ï¼Œä¾‹å¦‚ /YLCY/IMM、gateway
         */
        private String topicPrefix = "/YLCY/IMM";
        /**
         * æŠ¥æ–‡æ ¼å¼ï¼Œè§ {@link MesCollectProperties#FORMAT_YLCY}、{@link MesCollectProperties#FORMAT_GATEWAY}
         */
        private String format = FORMAT_YLCY;
        /**
         * æ˜¾å¼æŒ‡å®šè®¢é˜…主题,多个用逗号分隔;为空时按 format + topicPrefix è‡ªåŠ¨æŽ¨å¯¼
         */
        private String topics;
        private Integer qos = 0;
        private Integer keepAlive = 65;
        private Integer connectionTimeout = 10;
        /**
         * æŠ¥è­¦ä¸»é¢˜åŽç¼€ï¼ŒæŠ¥æ–‡ topic å­—段或主题末段命中时按报警处理
         */
        private String alarmTopicSuffix = "wm";
        /**
         * æ˜¯å¦ä¸ºè®¾å¤‡ç½‘关格式报文
         */
        public boolean isGatewayFormat() {
            return FORMAT_GATEWAY.equalsIgnoreCase(format == null ? "" : format.trim());
        }
    }
    @Data
    public static class Retention {
        /**
         * åŽŸå§‹æŠ¥æ–‡ä¿ç•™å¤©æ•°
         */
        private Integer rawDays = 7;
        /**
         * å®žæ—¶åŽ†å²ä¿ç•™å¤©æ•°
         */
        private Integer realtimeDays = 30;
        /**
         * å•次清理条数
         */
        private Integer batchSize = 50000;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/collect/mqtt/MesCollectMqttClient.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,198 @@
package cn.iocoder.yudao.module.mes.framework.collect.mqtt;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.mes.framework.collect.config.MesCollectProperties;
import cn.iocoder.yudao.module.mes.service.collect.MesCollectMessageDispatcher;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.context.SmartLifecycle;
import org.springframework.stereotype.Component;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
 * MES æ•°é‡‡ MQTT å®¢æˆ·ç«¯
 *
 * @author è¶…级管理员
 */
@Component
@Slf4j
public class MesCollectMqttClient implements SmartLifecycle {
    private static final long RECONNECT_DELAY_SECONDS = 30L;
    @Resource
    private MesCollectProperties properties;
    @Resource
    private MesCollectMessageDispatcher dispatcher;
    private volatile boolean running = false;
    private MqttClient client;
    private ScheduledExecutorService scheduler;
    @Override
    public void start() {
        MesCollectProperties.Mqtt mqtt = properties.getMqtt();
        if (!Boolean.TRUE.equals(mqtt.getEnabled())) {
            log.info("[start][数采 MQTT æœªå¯ç”¨]");
            return;
        }
        if (StrUtil.isBlank(mqtt.getUrl())) {
            log.warn("[start][数采 MQTT å·²å¯ç”¨ä½† url ä¸ºç©ºï¼Œè·³è¿‡è¿žæŽ¥]");
            return;
        }
        running = true;
        scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
            Thread thread = new Thread(runnable, "mes-collect-mqtt-reconnect");
            thread.setDaemon(true);
            return thread;
        });
        scheduler.scheduleWithFixedDelay(this::ensureConnected, 0, RECONNECT_DELAY_SECONDS, TimeUnit.SECONDS);
    }
    private void ensureConnected() {
        if (!running) {
            return;
        }
        MesCollectProperties.Mqtt mqtt = properties.getMqtt();
        try {
            if (client == null) {
                client = new MqttClient(mqtt.getUrl(), buildClientId(), new MemoryPersistence());
                client.setCallback(new MqttCallbackExtended() {
                    @Override
                    public void connectComplete(boolean reconnect, String serverURI) {
                        subscribe();
                    }
                    @Override
                    public void connectionLost(Throwable cause) {
                        log.warn("[connectionLost][数采 MQTT è¿žæŽ¥æ–­å¼€ï¼š{}]", cause == null ? "未知原因" : cause.getMessage());
                    }
                    @Override
                    public void messageArrived(String topic, MqttMessage message) {
                        String payload = new String(message.getPayload(), StandardCharsets.UTF_8);
                        try {
                            dispatcher.dispatch(topic, payload);
                        } catch (Exception e) {
                            log.error("[messageArrived][主题({}) å…¥é˜Ÿå¤±è´¥]", topic, e);
                        }
                    }
                    @Override
                    public void deliveryComplete(IMqttDeliveryToken token) {
                        // å½“前只订阅,不发布
                    }
                });
            }
            if (!client.isConnected()) {
                MqttConnectOptions options = new MqttConnectOptions();
                options.setAutomaticReconnect(true);
                options.setCleanSession(true);
                options.setKeepAliveInterval(mqtt.getKeepAlive());
                options.setConnectionTimeout(mqtt.getConnectionTimeout());
                if (StrUtil.isNotBlank(mqtt.getUsername())) {
                    options.setUserName(mqtt.getUsername());
                }
                if (mqtt.getPassword() != null) {
                    options.setPassword(mqtt.getPassword().toCharArray());
                }
                client.connect(options);
                log.info("[ensureConnected][数采 MQTT å·²è¿žæŽ¥ï¼š{}]", mqtt.getUrl());
            }
        } catch (Exception e) {
            log.warn("[ensureConnected][数采 MQTT è¿žæŽ¥/订阅失败,{} ç§’后重试:{}]",
                    RECONNECT_DELAY_SECONDS, e.getMessage());
        }
    }
    private void subscribe() {
        MesCollectProperties.Mqtt mqtt = properties.getMqtt();
        for (String topic : resolveSubscribeTopics(mqtt)) {
            try {
                client.subscribe(topic, mqtt.getQos());
                log.info("[subscribe][数采 MQTT è®¢é˜…成功:{}]", topic);
            } catch (Exception e) {
                log.error("[subscribe][数采 MQTT è®¢é˜…失败:{}]", topic, e);
            }
        }
    }
    /**
     * è§£æžéœ€è¦è®¢é˜…的主题。
     *
     * <p>显式配置 topics ä¼˜å…ˆï¼›ç½‘关格式订阅整个前缀(gateway/#),
     * å› ä¸ºç½‘关除 attributes å¤–还会发 connect / disconnect,后续也可能新增主题;
     * å…¶ä½™æ ¼å¼æ²¿ç”¨ /YLCY/IMM/+/+ ä¸¤çº§é€šé…ã€‚</p>
     */
    private List<String> resolveSubscribeTopics(MesCollectProperties.Mqtt mqtt) {
        if (StrUtil.isNotBlank(mqtt.getTopics())) {
            return Arrays.stream(mqtt.getTopics().split(","))
                    .map(String::trim)
                    .filter(StrUtil::isNotBlank)
                    .toList();
        }
        String prefix = normalizeTopicPrefix(mqtt.getTopicPrefix());
        return Collections.singletonList(mqtt.isGatewayFormat() ? prefix + "#" : prefix + "+/+");
    }
    private String normalizeTopicPrefix(String prefix) {
        if (StrUtil.isBlank(prefix)) {
            return "";
        }
        return prefix.endsWith("/") ? prefix : prefix + "/";
    }
    private String buildClientId() {
        String host;
        try {
            host = InetAddress.getLocalHost().getHostName();
        } catch (Exception e) {
            host = "host";
        }
        host = host.replaceAll("[^a-zA-Z0-9_-]", "_");
        String clientId = properties.getMqtt().getClientIdPrefix() + "-" + host + "-"
                + Long.toHexString(System.nanoTime());
        return StrUtil.maxLength(clientId, 64);
    }
    @Override
    public void stop() {
        running = false;
        if (scheduler != null) {
            scheduler.shutdownNow();
            scheduler = null;
        }
        if (client != null) {
            try {
                if (client.isConnected()) {
                    client.disconnect();
                }
            } catch (Exception e) {
                log.warn("[stop][数采 MQTT æ–­å¼€å¤±è´¥ï¼š{}]", e.getMessage());
            }
            try {
                client.close(true);
            } catch (Exception e) {
                log.warn("[stop][数采 MQTT å…³é—­å¤±è´¥ï¼š{}]", e.getMessage());
            }
            client = null;
        }
        log.info("[stop][数采 MQTT å·²åœæ­¢]");
    }
    @Override
    public boolean isRunning() {
        return running;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/job/collect/MesCollectCleanupJob.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
package cn.iocoder.yudao.module.mes.job.collect;
import cn.iocoder.yudao.module.mes.dal.mysql.collect.MesCollectMessageMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.collect.MesCollectRealtimeHistoryMapper;
import cn.iocoder.yudao.module.mes.framework.collect.config.MesCollectProperties;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡åŽ†å²æ•°æ®æ¸…ç†ä»»åŠ¡
 *
 * <p>原始报文保留 7 å¤©ï¼Œå®žæ—¶åŽ†å²ä¿ç•™ 30 å¤©ï¼Œæ¯æ—¥ 03:30 æ‰§è¡Œã€‚</p>
 *
 * @author è¶…级管理员
 */
@Component
@Slf4j
public class MesCollectCleanupJob {
    @Resource
    private MesCollectProperties properties;
    @Resource
    private MesCollectMessageMapper messageMapper;
    @Resource
    private MesCollectRealtimeHistoryMapper realtimeHistoryMapper;
    @Scheduled(cron = "0 30 3 * * ?")
    public void execute() {
        MesCollectProperties.Retention retention = properties.getRetention();
        int batchSize = retention.getBatchSize() == null || retention.getBatchSize() <= 0
                ? 50000 : retention.getBatchSize();
        int rawDeleted = deleteInBatches(() -> messageMapper.deleteBefore(
                LocalDateTime.now().minusDays(retention.getRawDays()), batchSize), batchSize);
        int realtimeDeleted = deleteInBatches(() -> realtimeHistoryMapper.deleteBefore(
                LocalDateTime.now().minusDays(retention.getRealtimeDays()), batchSize), batchSize);
        log.info("[execute][数采历史清理完成:原始报文 {} æ¡ï¼Œå®žæ—¶åŽ†å² {} æ¡]", rawDeleted, realtimeDeleted);
    }
    private int deleteInBatches(DeleteAction action, int batchSize) {
        int total = 0;
        int deleted;
        do {
            deleted = action.delete();
            total += deleted;
        } while (deleted >= batchSize);
        return total;
    }
    @FunctionalInterface
    private interface DeleteAction {
        int delete();
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/collect/MesCollectDataParser.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,237 @@
package cn.iocoder.yudao.module.mes.service.collect;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.mes.enums.collect.MesCollectMsgTypeEnum;
import tools.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.Map;
/**
 * MES æ•°é‡‡æŠ¥æ–‡è§£æžå·¥å…·
 *
 * @author è¶…级管理员
 */
@Component
@Slf4j
public class MesCollectDataParser {
    private static final DateTimeFormatter DATE_TIME_FORMATTER =
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss[.SSS]");
    public JsonNode parsePayload(String payload) {
        return JsonUtils.parseTree(payload);
    }
    public JsonNode parseData(JsonNode root) {
        JsonNode data = root.get("Data");
        if (data != null && data.isTextual()) {
            try {
                data = JsonUtils.parseTree(data.asText());
            } catch (Exception e) {
                log.warn("[parseData][Data å­—段不是合法 JSON,按原文处理:{}]", StrUtil.maxLength(data.asText(), 200));
                return root;
            }
        }
        return data != null && data.isObject() ? data : root;
    }
    public String getText(JsonNode node, String field) {
        JsonNode value = node.get(field);
        return value == null || value.isNull() ? null : value.asText();
    }
    public Integer getInteger(JsonNode node, String field) {
        JsonNode value = node.get(field);
        if (value == null || value.isNull()) {
            return null;
        }
        if (value.isNumber()) {
            return value.intValue();
        }
        try {
            return Integer.valueOf(value.asText().trim());
        } catch (NumberFormatException e) {
            return null;
        }
    }
    public Long getLong(JsonNode node, String field) {
        JsonNode value = node.get(field);
        if (value == null || value.isNull()) {
            return null;
        }
        if (value.isNumber()) {
            return value.longValue();
        }
        try {
            return Long.valueOf(value.asText().trim());
        } catch (NumberFormatException e) {
            return null;
        }
    }
    public BigDecimal getDecimal(JsonNode node, String field) {
        JsonNode value = node.get(field);
        if (value == null || value.isNull()) {
            return null;
        }
        if (value.isNumber()) {
            return value.decimalValue();
        }
        try {
            return new BigDecimal(value.asText().trim());
        } catch (NumberFormatException e) {
            return null;
        }
    }
    /**
     * æå–指定前缀 + æ•°å­—的字段,如 T1..Tn、ET1..ETn
     */
    public String extractNumberedFields(JsonNode data, String prefix) {
        Map<String, Object> result = new LinkedHashMap<>();
        data.properties().forEach(entry -> {
            String name = entry.getKey();
            if (!name.startsWith(prefix) || name.length() == prefix.length()) {
                return;
            }
            String suffix = name.substring(prefix.length());
            if (!suffix.chars().allMatch(Character::isDigit)) {
                return;
            }
            JsonNode value = entry.getValue();
            result.put(name, value != null && value.isNumber() ? value.decimalValue() : value == null ? null : value.asText());
        });
        if (result.isEmpty()) {
            return null;
        }
        return JsonUtils.toJsonString(result);
    }
    public LocalDateTime parseEventTime(JsonNode root) {
        LocalDateTime time = parseTimeText(getText(root, "time"));
        if (time != null) {
            return time;
        }
        Long timestamp = getLong(root, "timestamp");
        if (timestamp == null) {
            timestamp = getLong(root, "sendStamp");
        }
        if (timestamp != null && timestamp > 0) {
            Instant instant = timestamp > 1_000_000_000_000L
                    ? Instant.ofEpochMilli(timestamp) : Instant.ofEpochSecond(timestamp);
            return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
        }
        return LocalDateTime.now();
    }
    public LocalDateTime parseTimeText(String text) {
        if (StrUtil.isBlank(text)) {
            return null;
        }
        try {
            return LocalDateTime.parse(text.trim(), DATE_TIME_FORMATTER);
        } catch (Exception e) {
            return null;
        }
    }
    public String valueToString(JsonNode value) {
        if (value == null || value.isNull()) {
            return null;
        }
        return value.isValueNode() ? value.asText() : value.toString();
    }
    public MesCollectMsgTypeEnum resolveMsgType(String topicField, String mqttTopic,
                                                JsonNode data, String alarmTopicSuffix) {
        MesCollectMsgTypeEnum type = MesCollectMsgTypeEnum.getByType(topicField);
        if (type != MesCollectMsgTypeEnum.UNKNOWN) {
            return type;
        }
        if (StrUtil.isNotBlank(topicField) && StrUtil.equalsIgnoreCase(topicField, alarmTopicSuffix)) {
            return MesCollectMsgTypeEnum.ALARM;
        }
        String suffix = extractTopicSuffix(mqttTopic);
        if (StrUtil.isNotBlank(suffix)) {
            type = MesCollectMsgTypeEnum.getByType(suffix);
            if (type != MesCollectMsgTypeEnum.UNKNOWN) {
                return type;
            }
            if (StrUtil.equalsIgnoreCase(suffix, alarmTopicSuffix)) {
                return MesCollectMsgTypeEnum.ALARM;
            }
        }
        if (data == null) {
            return MesCollectMsgTypeEnum.UNKNOWN;
        }
        if (data.has("wmId")) {
            return MesCollectMsgTypeEnum.ALARM;
        }
        if (data.has("varId")) {
            return MesCollectMsgTypeEnum.OP_LOG;
        }
        if (data.has("CYCN")) {
            return MesCollectMsgTypeEnum.SPC;
        }
        if (data.has("STS") || data.has("OPM")) {
            return MesCollectMsgTypeEnum.REALTIME;
        }
        if (data.has("TS1") || data.has("CP1M")) {
            return MesCollectMsgTypeEnum.TECH;
        }
        return MesCollectMsgTypeEnum.UNKNOWN;
    }
    public String resolveDeviceId(JsonNode root, String mqttTopic) {
        String deviceId = getText(root, "devId");
        if (StrUtil.isNotBlank(deviceId)) {
            return deviceId.trim();
        }
        String[] parts = mqttTopic == null ? new String[0] : mqttTopic.split("/");
        return parts.length >= 2 ? parts[parts.length - 2] : null;
    }
    /**
     * ç½‘关格式报文拆包:报文首层 key æ˜¯è®¾å¤‡åï¼Œvalue æ˜¯è¯¥è®¾å¤‡çš„属性字典。
     *
     * <p>例如 {"630框绞机2":{"计米":268.3499}} æ‹†æˆ {630框绞机2 -> {"计米":268.3499}};
     * è¿žæŽ¥äº‹ä»¶ {"device":"大拉"} è¿™ç±»æ‰å¹³æŠ¥æ–‡æ²¡æœ‰åµŒå¥—对象,返回空 Map ç”±è°ƒç”¨æ–¹å¦è¡Œå¤„理。</p>
     */
    public Map<String, JsonNode> splitGatewayDevices(JsonNode root) {
        Map<String, JsonNode> devices = new LinkedHashMap<>();
        if (root == null || !root.isObject()) {
            return devices;
        }
        root.properties().forEach(entry -> {
            String deviceCode = StrUtil.trim(entry.getKey());
            JsonNode data = entry.getValue();
            if (StrUtil.isBlank(deviceCode) || data == null || !data.isObject()) {
                return;
            }
            devices.put(deviceCode, data);
        });
        return devices;
    }
    /**
     * å–主题末段作为报文类型,如 gateway/attributes -> attributes、gateway/disconnect -> disconnect
     */
    public String extractTopicSuffix(String mqttTopic) {
        if (StrUtil.isBlank(mqttTopic)) {
            return null;
        }
        String[] parts = mqttTopic.split("/");
        return parts.length == 0 ? null : parts[parts.length - 1];
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/collect/MesCollectDataService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,188 @@
package cn.iocoder.yudao.module.mes.service.collect;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.mes.dal.dataobject.collect.*;
import cn.iocoder.yudao.module.mes.dal.mysql.collect.*;
import cn.iocoder.yudao.module.mes.enums.collect.MesCollectAlarmStatusEnum;
import cn.iocoder.yudao.module.mes.enums.collect.MesCollectMsgTypeEnum;
import tools.jackson.databind.JsonNode;
import jakarta.annotation.Resource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
/**
 * MES æ•°é‡‡æ•°æ®è½åº“ Service
 *
 * @author è¶…级管理员
 */
@Service
public class MesCollectDataService {
    @Resource
    private MesCollectDataParser parser;
    @Resource
    private MesCollectMessageMapper messageMapper;
    @Resource
    private MesCollectRealtimeMapper realtimeMapper;
    @Resource
    private MesCollectRealtimeHistoryMapper realtimeHistoryMapper;
    @Resource
    private MesCollectCycleMapper cycleMapper;
    @Resource
    private MesCollectTechMapper techMapper;
    @Resource
    private MesCollectOperateLogMapper collectOperateLogMapper;
    @Resource
    private MesCollectAlarmMapper alarmMapper;
    public Long saveRawMessage(String deviceCode, Long machineryId, String mqttTopic,
                               MesCollectMsgTypeEnum msgType, LocalDateTime eventTime, String payload) {
        MesCollectMessageDO message = MesCollectMessageDO.builder()
                .deviceCode(deviceCode).machineryId(machineryId).mqttTopic(mqttTopic)
                .msgType(msgType.getType()).eventTime(eventTime).rawPayload(payload)
                .processStatus(0).receiveTime(LocalDateTime.now()).build();
        messageMapper.insert(message);
        return message.getId();
    }
    public void markRawMessageFailed(Long id, String error) {
        if (id == null) {
            return;
        }
        messageMapper.updateById(MesCollectMessageDO.builder()
                .id(id).processStatus(1).errorMsg(StrUtil.maxLength(error, 1000)).build());
    }
    public void saveRealtime(String deviceCode, Long machineryId, Long taskId, Long workOrderId,
                             LocalDateTime eventTime, JsonNode data) {
        MesCollectRealtimeDO snapshot = MesCollectRealtimeDO.builder()
                .machineryId(machineryId).deviceCode(deviceCode).taskId(taskId).workOrderId(workOrderId)
                .collectTime(eventTime)
                .sts(parser.getInteger(data, "STS"))
                .opm(parser.getInteger(data, "OPM"))
                .asts(parser.getInteger(data, "ASTS"))
                .ot(parser.getDecimal(data, "OT"))
                .temps(parser.extractNumberedFields(data, "T"))
                .dataJson(data.toString())
                .build();
        MesCollectRealtimeDO existing = realtimeMapper.selectByDeviceCode(deviceCode);
        if (existing != null) {
            snapshot.setId(existing.getId());
            realtimeMapper.updateById(snapshot);
        } else {
            try {
                realtimeMapper.insert(snapshot);
            } catch (DuplicateKeyException e) {
                MesCollectRealtimeDO current = realtimeMapper.selectByDeviceCode(deviceCode);
                if (current != null) {
                    snapshot.setId(current.getId());
                    realtimeMapper.updateById(snapshot);
                }
            }
        }
        MesCollectRealtimeHistoryDO history = MesCollectRealtimeHistoryDO.builder()
                .machineryId(machineryId).deviceCode(deviceCode).taskId(taskId).workOrderId(workOrderId)
                .collectTime(eventTime)
                .sts(snapshot.getSts())
                .opm(snapshot.getOpm())
                .asts(snapshot.getAsts())
                .ot(snapshot.getOt())
                .temps(snapshot.getTemps())
                .dataJson(snapshot.getDataJson())
                .build();
        try {
            realtimeHistoryMapper.insert(history);
        } catch (DuplicateKeyException ignored) {
            // åŒä¸€è®¾å¤‡åŒä¸€ç§’的重复报文,直接忽略
        }
    }
    public void saveCycle(String deviceCode, Long machineryId, Long taskId, Long workOrderId,
                          LocalDateTime eventTime, JsonNode data) {
        Long cycleNo = parser.getLong(data, "CYCN");
        MesCollectCycleDO cycle = MesCollectCycleDO.builder()
                .machineryId(machineryId).deviceCode(deviceCode).taskId(taskId).workOrderId(workOrderId)
                .cycleNo(cycleNo == null ? 0L : cycleNo)
                .collectTime(eventTime)
                .ecyct(parser.getDecimal(data, "ECYCT"))
                .eiss(parser.getDecimal(data, "EISS"))
                .eivm(parser.getDecimal(data, "EIVM"))
                .eipm(parser.getDecimal(data, "EIPM"))
                .esipt(parser.getDecimal(data, "ESIPT"))
                .esipp(parser.getDecimal(data, "ESIPP"))
                .esips(parser.getDecimal(data, "ESIPS"))
                .eipt(parser.getDecimal(data, "EIPT"))
                .eipse(parser.getDecimal(data, "EIPSE"))
                .eplst(parser.getDecimal(data, "EPLST"))
                .eplsse(parser.getDecimal(data, "EPLSSE"))
                .eplspm(parser.getDecimal(data, "EPLSPM"))
                .etTemps(parser.extractNumberedFields(data, "ET"))
                .dataJson(data.toString())
                .build();
        try {
            cycleMapper.insert(cycle);
        } catch (DuplicateKeyException ignored) {
            // åŒä¸€å‘¨æœŸé‡å¤ä¸ŠæŠ¥ï¼Œç›´æŽ¥å¿½ç•¥
        }
    }
    public void saveTech(String deviceCode, Long machineryId, Long taskId, Long workOrderId,
                         LocalDateTime eventTime, JsonNode data) {
        MesCollectTechDO tech = MesCollectTechDO.builder()
                .machineryId(machineryId).deviceCode(deviceCode).taskId(taskId).workOrderId(workOrderId)
                .collectTime(eventTime).dataJson(data.toString()).build();
        MesCollectTechDO existing = techMapper.selectByDeviceCode(deviceCode);
        if (existing != null) {
            tech.setId(existing.getId());
            techMapper.updateById(tech);
        } else {
            try {
                techMapper.insert(tech);
            } catch (DuplicateKeyException e) {
                MesCollectTechDO current = techMapper.selectByDeviceCode(deviceCode);
                if (current != null) {
                    tech.setId(current.getId());
                    techMapper.updateById(tech);
                }
            }
        }
    }
    public void saveOperateLog(String deviceCode, Long machineryId, Long taskId, Long workOrderId,
                               LocalDateTime eventTime, JsonNode data) {
        LocalDateTime modifyTime = parser.parseTimeText(parser.getText(data, "modifyTime"));
        collectOperateLogMapper.insert(MesCollectOperateLogDO.builder()
                .machineryId(machineryId).deviceCode(deviceCode).taskId(taskId).workOrderId(workOrderId)
                .varId(parser.getText(data, "varId"))
                .oldValue(StrUtil.maxLength(parser.valueToString(data.get("lastValue")), 500))
                .newValue(StrUtil.maxLength(parser.valueToString(data.get("value")), 500))
                .modifyTime(modifyTime == null ? eventTime : modifyTime)
                .collectTime(eventTime).dataJson(data.toString())
                .build());
    }
    public void saveAlarm(String deviceCode, Long machineryId, Long taskId, Long workOrderId,
                          LocalDateTime eventTime, JsonNode data) {
        Integer wmId = parser.getInteger(data, "wmId");
        LocalDateTime wmTime = parser.parseTimeText(parser.getText(data, "wmTime"));
        if (wmTime == null) {
            wmTime = eventTime;
        }
        if (wmId != null && wmId == 0) {
            alarmMapper.clearActiveByDeviceCode(deviceCode, wmTime);
            return;
        }
        alarmMapper.insert(MesCollectAlarmDO.builder()
                .machineryId(machineryId).deviceCode(deviceCode).taskId(taskId).workOrderId(workOrderId)
                .wmId(wmId)
                .wmMsg(StrUtil.maxLength(parser.getText(data, "wmMsg"), 500))
                .wmTime(wmTime)
                .status(MesCollectAlarmStatusEnum.ACTIVE.getStatus())
                .collectTime(eventTime).dataJson(data.toString())
                .build());
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/collect/MesCollectMessageDispatcher.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,204 @@
package cn.iocoder.yudao.module.mes.service.collect;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryMapper;
import cn.iocoder.yudao.module.mes.enums.collect.MesCollectMsgTypeEnum;
import cn.iocoder.yudao.module.mes.framework.collect.config.MesCollectProperties;
import tools.jackson.databind.JsonNode;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * MES æ•°é‡‡æŠ¥æ–‡åˆ†å‘器
 *
 * <p>Paho å›žè°ƒçº¿ç¨‹åªè´Ÿè´£å…¥é˜Ÿï¼Œè§£æžä¸Žè½åº“在单线程中串行执行,保证同一设备报警等状态按顺序处理。</p>
 *
 * @author è¶…级管理员
 */
@Component
@Slf4j
public class MesCollectMessageDispatcher {
    /**
     * ç½‘关格式里承载设备属性快照的主题末段
     */
    private static final String ATTR_TOPIC_SUFFIX = "attributes";
    @Resource
    private MesCollectProperties properties;
    @Resource
    private MesCollectDataParser parser;
    @Resource
    private MesCollectDataService dataService;
    @Resource
    private MesDvMachineryMapper machineryMapper;
    @Resource
    private MesCollectTaskResolver taskResolver;
    private final ExecutorService executor = new ThreadPoolExecutor(1, 1,
            0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(10_000),
            new ThreadFactory() {
                private final AtomicInteger index = new AtomicInteger();
                @Override
                public Thread newThread(Runnable runnable) {
                    Thread thread = new Thread(runnable, "mes-collect-dispatch-" + index.incrementAndGet());
                    thread.setDaemon(true);
                    return thread;
                }
            }, new ThreadPoolExecutor.CallerRunsPolicy());
    /**
     * è®¾å¤‡æœªç»‘定台账时的告警缓存,避免每条报文重复刷日志
     */
    private final ConcurrentHashMap<String, Long> unboundWarnCache = new ConcurrentHashMap<>();
    public void dispatch(String topic, String payload) {
        executor.execute(() -> process(topic, payload));
    }
    private void process(String topic, String payload) {
        // ç½‘关格式报文结构完全不同(主题固定、设备以报文首层 key åŒºåˆ†ï¼‰ï¼Œå•独处理
        if (properties.getMqtt().isGatewayFormat()) {
            processGateway(topic, payload);
            return;
        }
        JsonNode root;
        try {
            root = parser.parsePayload(payload);
        } catch (Exception e) {
            log.warn("[process][主题({}) æŠ¥æ–‡ä¸æ˜¯åˆæ³• JSON:{}]", topic, StrUtil.maxLength(payload, 200));
            dataService.markRawMessageFailed(
                    dataService.saveRawMessage(null, null, topic, MesCollectMsgTypeEnum.UNKNOWN,
                            LocalDateTime.now(), payload),
                    "报文不是合法 JSON:" + e.getMessage());
            return;
        }
        JsonNode data = parser.parseData(root);
        MesCollectMsgTypeEnum msgType = parser.resolveMsgType(parser.getText(root, "topic"), topic, data,
                properties.getMqtt().getAlarmTopicSuffix());
        String deviceCode = parser.resolveDeviceId(root, topic);
        LocalDateTime eventTime = parser.parseEventTime(root);
        MesDvMachineryDO machinery = StrUtil.isBlank(deviceCode) ? null
                : machineryMapper.selectByCollectDeviceCode(deviceCode);
        if (machinery == null && StrUtil.isNotBlank(deviceCode)) {
            warnUnboundDevice(deviceCode);
        }
        Long machineryId = machinery == null ? null : machinery.getId();
        Long taskId = null;
        Long workOrderId = null;
        if (machinery != null && machinery.getWorkstationId() != null) {
            MesProTaskDO task = taskResolver.resolve(machinery.getWorkstationId());
            if (task != null) {
                taskId = task.getId();
                workOrderId = task.getWorkOrderId();
            }
        }
        Long rawId = dataService.saveRawMessage(deviceCode, machineryId, topic, msgType, eventTime, payload);
        try {
            switch (msgType) {
                case REALTIME -> dataService.saveRealtime(deviceCode, machineryId, taskId, workOrderId, eventTime, data);
                case SPC -> dataService.saveCycle(deviceCode, machineryId, taskId, workOrderId, eventTime, data);
                case TECH -> dataService.saveTech(deviceCode, machineryId, taskId, workOrderId, eventTime, data);
                case OP_LOG -> dataService.saveOperateLog(deviceCode, machineryId, taskId, workOrderId, eventTime, data);
                case ALARM -> dataService.saveAlarm(deviceCode, machineryId, taskId, workOrderId, eventTime, data);
                default -> log.debug("[process][忽略未知类型报文,主题({})]", topic);
            }
        } catch (Exception e) {
            dataService.markRawMessageFailed(rawId, e.getMessage());
            log.error("[process][主题({}) è®¾å¤‡({}) å¤„理失败]", topic, deviceCode, e);
        }
    }
    /**
     * ç½‘关格式报文处理。
     *
     * <p>attributes:报文首层 key æ˜¯è®¾å¤‡åã€value æ˜¯å±žæ€§å­—典,按设备逐台落实时数据;
     * connect / disconnect:连接事件,只留痕不落实时数据,便于排查设备离线。</p>
     */
    private void processGateway(String topic, String payload) {
        JsonNode root;
        try {
            root = parser.parsePayload(payload);
        } catch (Exception e) {
            log.warn("[processGateway][主题({}) æŠ¥æ–‡ä¸æ˜¯åˆæ³• JSON:{}]", topic, StrUtil.maxLength(payload, 200));
            dataService.markRawMessageFailed(
                    dataService.saveRawMessage(null, null, topic, MesCollectMsgTypeEnum.UNKNOWN,
                            LocalDateTime.now(), payload),
                    "报文不是合法 JSON:" + e.getMessage());
            return;
        }
        // è¿žæŽ¥äº‹ä»¶ï¼š{"device":"大拉"},没有属性字典
        if (!ATTR_TOPIC_SUFFIX.equalsIgnoreCase(parser.extractTopicSuffix(topic))) {
            String deviceCode = StrUtil.trim(parser.getText(root, "device"));
            MesDvMachineryDO machinery = StrUtil.isBlank(deviceCode) ? null
                    : machineryMapper.selectByCollectDeviceCode(deviceCode);
            dataService.saveRawMessage(deviceCode, machinery == null ? null : machinery.getId(), topic,
                    MesCollectMsgTypeEnum.getByType(parser.extractTopicSuffix(topic)),
                    LocalDateTime.now(), payload);
            return;
        }
        Map<String, JsonNode> devices = parser.splitGatewayDevices(root);
        if (devices.isEmpty()) {
            log.debug("[processGateway][主题({}) æŠ¥æ–‡é‡Œæ²¡æœ‰è®¾å¤‡å±žæ€§ï¼š{}]", topic, StrUtil.maxLength(payload, 200));
            dataService.saveRawMessage(null, null, topic, MesCollectMsgTypeEnum.UNKNOWN, LocalDateTime.now(), payload);
            return;
        }
        // ç½‘关报文不带时间戳,统一按接收时间落库
        LocalDateTime eventTime = parser.parseEventTime(root);
        devices.forEach((deviceCode, data) -> {
            MesDvMachineryDO machinery = machineryMapper.selectByCollectDeviceCode(deviceCode);
            if (machinery == null) {
                warnUnboundDevice(deviceCode);
            }
            Long machineryId = machinery == null ? null : machinery.getId();
            Long taskId = null;
            Long workOrderId = null;
            if (machinery != null && machinery.getWorkstationId() != null) {
                MesProTaskDO task = taskResolver.resolve(machinery.getWorkstationId());
                if (task != null) {
                    taskId = task.getId();
                    workOrderId = task.getWorkOrderId();
                }
            }
            Long rawId = dataService.saveRawMessage(deviceCode, machineryId, topic,
                    MesCollectMsgTypeEnum.REALTIME, eventTime, payload);
            try {
                dataService.saveRealtime(deviceCode, machineryId, taskId, workOrderId, eventTime, data);
            } catch (Exception e) {
                dataService.markRawMessageFailed(rawId, e.getMessage());
                log.error("[processGateway][主题({}) è®¾å¤‡({}) å¤„理失败]", topic, deviceCode, e);
            }
        });
    }
    private void warnUnboundDevice(String deviceCode) {
        long now = System.currentTimeMillis();
        Long last = unboundWarnCache.get(deviceCode);
        if (last != null && now - last < 60_000L) {
            return;
        }
        unboundWarnCache.put(deviceCode, now);
        log.warn("[process][数采设备({}) æœªç»‘定任何设备台账,数据将按设备编号落库,machineryId ä¸ºç©º]", deviceCode);
    }
    @PreDestroy
    public void destroy() {
        executor.shutdownNow();
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/collect/MesCollectQueryService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,204 @@
package cn.iocoder.yudao.module.mes.service.collect;
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.collect.vo.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.collect.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.mysql.collect.*;
import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderMapper;
import cn.iocoder.yudao.module.mes.enums.collect.MesCollectAlarmStatusEnum;
import cn.iocoder.yudao.module.mes.enums.collect.MesCollectOperateModeEnum;
import cn.iocoder.yudao.module.mes.enums.collect.MesCollectRunStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.COLLECT_ALARM_ALREADY_CLEARED;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.COLLECT_ALARM_NOT_EXISTS;
/**
 * MES æ•°é‡‡æ•°æ®æŸ¥è¯¢ Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
public class MesCollectQueryService {
    /**
     * è¶…过该时长未采集数据视为离线
     */
    private static final long ONLINE_TIMEOUT_MINUTES = 5L;
    @Resource
    private MesDvMachineryMapper machineryMapper;
    @Resource
    private MesCollectRealtimeMapper realtimeMapper;
    @Resource
    private MesCollectRealtimeHistoryMapper realtimeHistoryMapper;
    @Resource
    private MesCollectCycleMapper cycleMapper;
    @Resource
    private MesCollectTechMapper techMapper;
    @Resource
    private MesCollectOperateLogMapper collectOperateLogMapper;
    @Resource
    private MesCollectAlarmMapper alarmMapper;
    @Resource
    private MesProWorkOrderMapper workOrderMapper;
    @Resource
    private MesCollectTaskResolver taskResolver;
    @Resource
    private MesMdWorkstationService workstationService;
    /**
     * èŽ·å–å·²é…ç½®æ•°é‡‡ç¼–ç çš„è®¾å¤‡å®žæ—¶çŠ¶æ€åˆ—è¡¨
     */
    public List<MesCollectDeviceRespVO> getDeviceList() {
        List<MesDvMachineryDO> machineries = machineryMapper.selectListWithCollectDeviceCode();
        Map<Long, MesMdWorkstationDO> workstationMap = workstationService.getWorkstationMap(
                machineries.stream().map(MesDvMachineryDO::getWorkstationId).filter(Objects::nonNull).toList());
        return machineries.stream().map(machinery -> {
            MesCollectDeviceRespVO vo = new MesCollectDeviceRespVO();
            vo.setMachineryId(machinery.getId());
            vo.setMachineryCode(machinery.getCode());
            vo.setMachineryName(machinery.getName());
            vo.setCollectDeviceCode(machinery.getCollectDeviceCode());
            vo.setCollectEnabled(Boolean.TRUE.equals(machinery.getCollectEnabled()));
            MesMdWorkstationDO workstation = workstationMap.get(machinery.getWorkstationId());
            if (workstation != null) {
                vo.setWorkstationId(workstation.getId());
                vo.setWorkstationName(workstation.getName());
            }
            MesCollectRealtimeDO realtime = realtimeMapper.selectByDeviceCode(machinery.getCollectDeviceCode());
            if (realtime != null) {
                vo.setLastCollectTime(realtime.getCollectTime());
                vo.setOnline(realtime.getCollectTime() != null
                        && realtime.getCollectTime().isAfter(LocalDateTime.now().minusMinutes(ONLINE_TIMEOUT_MINUTES)));
                vo.setSts(realtime.getSts());
                vo.setStsName(MesCollectRunStatusEnum.getNameByStatus(realtime.getSts()));
                vo.setOpm(realtime.getOpm());
                vo.setOpmName(MesCollectOperateModeEnum.getNameByMode(realtime.getOpm()));
                vo.setOt(realtime.getOt());
                vo.setTemps(realtime.getTemps());
                // ç½‘关格式报文的结构化字段为空,前端「当前数据」列直接展示原始 JSON
                vo.setDataJson(realtime.getDataJson());
            } else {
                vo.setOnline(false);
            }
            MesProTaskDO task = taskResolver.resolve(machinery.getWorkstationId());
            if (task != null) {
                vo.setTaskId(task.getId());
                vo.setTaskCode(task.getCode());
                vo.setTaskName(task.getName());
                vo.setWorkOrderId(task.getWorkOrderId());
                MesProWorkOrderDO workOrder = task.getWorkOrderId() != null
                        ? workOrderMapper.selectById(task.getWorkOrderId()) : null;
                if (workOrder != null) {
                    vo.setWorkOrderCode(workOrder.getCode());
                }
            }
            vo.setActiveAlarmCount(alarmMapper.selectActiveCountByDeviceCode(machinery.getCollectDeviceCode()));
            return vo;
        }).toList();
    }
    /**
     * èŽ·å–æŒ‡å®šæ•°é‡‡è®¾å¤‡çš„å®žæ—¶çŠ¶æ€å¿«ç…§
     */
    public MesCollectRealtimeRespVO getRealtime(String deviceCode) {
        MesCollectRealtimeDO realtime = realtimeMapper.selectByDeviceCode(deviceCode);
        if (realtime == null) {
            return null;
        }
        MesCollectRealtimeRespVO vo = BeanUtils.toBean(realtime, MesCollectRealtimeRespVO.class);
        vo.setStsName(MesCollectRunStatusEnum.getNameByStatus(realtime.getSts()));
        vo.setOpmName(MesCollectOperateModeEnum.getNameByMode(realtime.getOpm()));
        return vo;
    }
    /**
     * åˆ†é¡µæŸ¥è¯¢å®žæ—¶çŠ¶æ€åŽ†å²
     */
    public PageResult<MesCollectRealtimeRespVO> getRealtimeHistoryPage(MesCollectRealtimeHistoryPageReqVO reqVO) {
        PageResult<MesCollectRealtimeHistoryDO> pageResult = realtimeHistoryMapper.selectPage(reqVO);
        PageResult<MesCollectRealtimeRespVO> result = BeanUtils.toBean(pageResult, MesCollectRealtimeRespVO.class);
        result.getList().forEach(vo -> {
            vo.setStsName(MesCollectRunStatusEnum.getNameByStatus(vo.getSts()));
            vo.setOpmName(MesCollectOperateModeEnum.getNameByMode(vo.getOpm()));
        });
        return result;
    }
    /**
     * åˆ†é¡µæŸ¥è¯¢å‘¨æœŸè¿‡ç¨‹æ•°æ®
     */
    public PageResult<MesCollectCycleRespVO> getCyclePage(MesCollectCyclePageReqVO reqVO) {
        return BeanUtils.toBean(cycleMapper.selectPage(reqVO), MesCollectCycleRespVO.class);
    }
    /**
     * èŽ·å–å•æ¡å‘¨æœŸè¿‡ç¨‹æ•°æ®
     */
    public MesCollectCycleRespVO getCycle(Long id) {
        MesCollectCycleDO cycle = cycleMapper.selectById(id);
        return cycle == null ? null : BeanUtils.toBean(cycle, MesCollectCycleRespVO.class);
    }
    /**
     * èŽ·å–æŒ‡å®šæ•°é‡‡è®¾å¤‡çš„æœ€æ–°å·¥è‰ºå‚æ•°å¿«ç…§
     */
    public MesCollectTechRespVO getTech(String deviceCode) {
        MesCollectTechDO tech = techMapper.selectByDeviceCode(deviceCode);
        return tech == null ? null : BeanUtils.toBean(tech, MesCollectTechRespVO.class);
    }
    /**
     * åˆ†é¡µæŸ¥è¯¢å‚数变更日志
     */
    public PageResult<MesCollectOperateLogRespVO> getOperateLogPage(MesCollectOperateLogPageReqVO reqVO) {
        return BeanUtils.toBean(collectOperateLogMapper.selectPage(reqVO), MesCollectOperateLogRespVO.class);
    }
    /**
     * åˆ†é¡µæŸ¥è¯¢æŠ¥è­¦è®°å½•
     */
    public PageResult<MesCollectAlarmRespVO> getAlarmPage(MesCollectAlarmPageReqVO reqVO) {
        PageResult<MesCollectAlarmDO> pageResult = alarmMapper.selectPage(reqVO);
        PageResult<MesCollectAlarmRespVO> result = BeanUtils.toBean(pageResult, MesCollectAlarmRespVO.class);
        result.getList().forEach(vo -> vo.setStatusName(MesCollectAlarmStatusEnum.ACTIVE.getStatus().equals(vo.getStatus())
                ? MesCollectAlarmStatusEnum.ACTIVE.getName() : MesCollectAlarmStatusEnum.CLEARED.getName()));
        return result;
    }
    /**
     * äººå·¥ç¡®è®¤æ¶ˆé™¤æŠ¥è­¦
     */
    public void ackAlarm(Long id) {
        MesCollectAlarmDO alarm = alarmMapper.selectById(id);
        if (alarm == null) {
            throw exception(COLLECT_ALARM_NOT_EXISTS);
        }
        if (MesCollectAlarmStatusEnum.CLEARED.getStatus().equals(alarm.getStatus())) {
            throw exception(COLLECT_ALARM_ALREADY_CLEARED);
        }
        MesCollectAlarmDO updateObj = new MesCollectAlarmDO();
        updateObj.setId(id);
        updateObj.setStatus(MesCollectAlarmStatusEnum.CLEARED.getStatus());
        updateObj.setClearTime(LocalDateTime.now());
        alarmMapper.updateById(updateObj);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/collect/MesCollectTaskResolver.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.mes.service.collect;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.concurrent.ConcurrentHashMap;
/**
 * MES æ•°é‡‡-生产任务关联解析
 *
 * @author è¶…级管理员
 */
@Service
public class MesCollectTaskResolver {
    private static final long CACHE_MILLIS = 30_000L;
    @Resource
    private MesProTaskMapper taskMapper;
    private final ConcurrentHashMap<Long, CachedTask> cache = new ConcurrentHashMap<>();
    /**
     * æŸ¥è¯¢å·¥ä½œç«™ä¸Šæ­£åœ¨è¿è¡Œçš„生产任务(生产中/部分完成/已停工)
     */
    public MesProTaskDO resolve(Long workstationId) {
        if (workstationId == null) {
            return null;
        }
        long now = System.currentTimeMillis();
        CachedTask cached = cache.get(workstationId);
        if (cached != null && now - cached.time < CACHE_MILLIS) {
            return cached.task;
        }
        MesProTaskDO task = taskMapper.selectRunningTaskByWorkstationId(workstationId);
        cache.put(workstationId, new CachedTask(task, now));
        return task;
    }
    private record CachedTask(MesProTaskDO task, long time) {
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/spare/MesDvSpareService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,189 @@
package cn.iocoder.yudao.module.mes.service.dv.spare;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
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.dv.spare.vo.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.spare.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.repair.MesDvRepairDO;
import cn.iocoder.yudao.module.mes.dal.mysql.dv.spare.*;
import cn.iocoder.yudao.module.mes.dal.mysql.dv.repair.MesDvRepairMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryMapper;
import cn.iocoder.yudao.module.mes.enums.dv.MesDvRepairStatusEnum;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.*;
import java.math.BigDecimal;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
/** å¤‡ä»¶åº“存只通过留痕的流水变更,元数据编辑不允许覆盖库存。 */
@Service
public class MesDvSpareService {
    private static final ErrorCode INVALID = new ErrorCode(1_040_807_001, "备件操作失败:{}");
    private static final BigDecimal MAX_QUANTITY = new BigDecimal("999999999999999.999");
    @Resource private MesDvSparePartMapper partMapper;
    @Resource private MesDvSpareMovementMapper movementMapper;
    @Resource private MesDvRepairMapper repairMapper;
    @Resource private MesDvMachineryMapper machineryMapper;
    @Resource private AdminUserApi adminUserApi;
    @Transactional(rollbackFor=Exception.class, isolation=Isolation.READ_COMMITTED)
    public Long create(MesDvSparePartSaveReqVO req, Long operator) {
        validateMetadata(req);
        MesDvSparePartDO part = BeanUtils.toBean(req,MesDvSparePartDO.class);
        part.setStockQuantity(req.getInitialQuantity());
        partMapper.insert(part);
        if (req.getInitialQuantity().signum()>0) {
            writeMovement(part,1,req.getInitialQuantity(),BigDecimal.ZERO,null,null,null,operator,
                    "opening-"+UUID.randomUUID(),"登记初始库存");
        }
        return part.getId();
    }
    @Transactional(rollbackFor=Exception.class, isolation=Isolation.READ_COMMITTED)
    public void update(MesDvSparePartSaveReqVO req) {
        MesDvSparePartDO part = requirePart(req.getId());
        validateMetadata(req);
        MesDvSparePartDO changes = BeanUtils.toBean(req,MesDvSparePartDO.class);
        changes.setStockQuantity(part.getStockQuantity());
        partMapper.updateById(changes);
    }
    private void validateMetadata(MesDvSparePartSaveReqVO req) {
        req.setCode(req.getCode().trim());
        req.setName(req.getName().trim());
        req.setUnit(req.getUnit().trim());
        if (req.getCode().isEmpty() || req.getName().isEmpty() || req.getUnit().isEmpty()) fail("编码、名称和单位不能为空");
        var old=partMapper.selectOne(MesDvSparePartDO::getCode,req.getCode());
        if(old!=null&&!Objects.equals(old.getId(),req.getId())) fail("备件编码已存在");
        if(req.getId()!=null && movementMapper.selectCount(MesDvSpareMovementDO::getSparePartId,req.getId())>0) {
            var saved=partMapper.selectById(req.getId());
            if(!Objects.equals(saved.getUnit(),req.getUnit())) fail("已有库存流水的备件不能修改计量单位");
        }
    }
    @Transactional(rollbackFor=Exception.class, isolation=Isolation.READ_COMMITTED)
    public void delete(Long id) {
        var part=requirePart(id);
        if(part.getStockQuantity().signum()!=0 || movementMapper.selectCount(MesDvSpareMovementDO::getSparePartId,id)>0)
            fail("有库存或已有流水的备件不能删除,请停用");
        partMapper.deleteById(id);
    }
    public MesDvSparePartDO get(Long id) { return partMapper.selectById(id); }
    public PageResult<MesDvSparePartDO> page(MesDvSparePageReqVO req) { return partMapper.page(req); }
    public PageResult<MesDvSpareMovementRespVO> movements(MesDvSparePageReqVO req) {
        var page=movementMapper.page(req);
        var ids=page.getList().stream().filter(m->m.getType()==3).map(MesDvSpareMovementDO::getId).toList();
        Map<Long,BigDecimal> totals=new HashMap<>();
        movementMapper.returns(ids).forEach(m->totals.merge(m.getOriginalId(),m.getQuantity(),BigDecimal::add));
        return new PageResult<>(page.getList().stream().map(m-> {
            var vo=BeanUtils.toBean(m,MesDvSpareMovementRespVO.class);
            vo.setReturnedQuantity(totals.getOrDefault(m.getId(),BigDecimal.ZERO));
            vo.setReturnableQuantity(m.getType()==3?m.getQuantity().subtract(vo.getReturnedQuantity()):BigDecimal.ZERO);
            return vo;
        }).toList(),page.getTotal());
    }
    @Transactional(rollbackFor=Exception.class, isolation=Isolation.READ_COMMITTED)
    public Long receive(MesDvSpareActionReqVO req,Long operator) {
        validateAction(req);
        var part=requirePart(req.getSparePartId());
        Long prior=duplicate(req,2,operator,part.getId(),null,null,null);
        if(prior!=null)return prior;
        requireEnabled(part);
        var before=part.getStockQuantity();
        changeStock(part,req.getQuantity());
        return writeMovement(part,2,req.getQuantity(),before,null,null,null,operator,req.getRequestKey(),req.getRemark());
    }
    @Transactional(rollbackFor=Exception.class, isolation=Isolation.READ_COMMITTED)
    public Long issue(MesDvSpareActionReqVO req,Long operator) {
        validateAction(req);
        if(req.getRepairId()==null || req.getSparePartId()==null || req.getReceiverUserId()==null) fail("请选择维修单、备件和领用人");
        // ä¸Žç»´ä¿®çŠ¶æ€æµè½¬ä½¿ç”¨åŒä¸€è¡Œé”ï¼›å…ˆç»´ä¿®å•å†å¤‡ä»¶ï¼Œä¿æŒä¸€è‡´é”åºã€‚
        var repair=repairMapper.lockForSpare(req.getRepairId());
        if(repair==null)fail("维修单不存在");
        var part=requirePart(req.getSparePartId());
        Long prior=duplicate(req,3,operator,part.getId(),repair.getId(),null,req.getReceiverUserId());
        if(prior!=null)return prior;
        if(!Objects.equals(repair.getStatus(),MesDvRepairStatusEnum.PREPARE.getStatus()) &&
                !Objects.equals(repair.getStatus(),MesDvRepairStatusEnum.CONFIRMED.getStatus()))
            fail("仅草稿或维修中的维修单可以领用备件");
        requireEnabled(part);
        var receiver=adminUserApi.getUser(req.getReceiverUserId());
        if(receiver==null || !Objects.equals(receiver.getStatus(),0))fail("领用人不存在或已停用");
        var before=part.getStockQuantity();
        changeStock(part,req.getQuantity().negate());
        return writeMovement(part,3,req.getQuantity(),before,repair,null,req.getReceiverUserId(),
                operator,req.getRequestKey(),req.getRemark());
    }
    @Transactional(rollbackFor=Exception.class, isolation=Isolation.READ_COMMITTED)
    public Long returnPart(MesDvSpareActionReqVO req,Long operator) {
        validateAction(req);
        if(req.getOriginalId()==null)fail("请选择原领用记录");
        var original=movementMapper.lock(req.getOriginalId());
        if(original==null || original.getType()!=3)fail("原领用记录不存在");
        var part=requirePart(original.getSparePartId());
        Long prior=duplicate(req,4,operator,part.getId(),original.getRepairId(),original.getId(),original.getReceiverUserId());
        if(prior!=null)return prior;
        BigDecimal returned=movementMapper.returns(List.of(original.getId())).stream()
                .map(MesDvSpareMovementDO::getQuantity).reduce(BigDecimal.ZERO,BigDecimal::add);
        if(returned.add(req.getQuantity()).compareTo(original.getQuantity())>0)fail("退回数量超过尚未退回的领用数量");
        var before=part.getStockQuantity();
        changeStock(part,req.getQuantity()); // å·²åœç”¨å¤‡ä»¶ä»å…è®¸é€€å›žã€‚
        var record=snapshot(part,4,req.getQuantity(),before,operator,req.getRequestKey(),req.getRemark());
        record.setOriginalId(original.getId());
        record.setRepairId(original.getRepairId()); record.setRepairCode(original.getRepairCode());
        record.setMachineryId(original.getMachineryId()); record.setMachineryName(original.getMachineryName());
        record.setReceiverUserId(original.getReceiverUserId()); record.setReceiverName(original.getReceiverName());
        movementMapper.insert(record);
        return record.getId();
    }
    private Long duplicate(MesDvSpareActionReqVO req,int type,Long operator,Long partId,Long repairId,Long originalId,Long receiverId) {
        var old=movementMapper.byKey(req.getRequestKey());
        if(old==null)return null;
        if(old.getType()!=type || !Objects.equals(old.getOperatorUserId(),operator)
                || !Objects.equals(old.getSparePartId(),partId) || !Objects.equals(old.getRepairId(),repairId)
                || !Objects.equals(old.getOriginalId(),originalId) || !Objects.equals(old.getReceiverUserId(),receiverId)
                || old.getQuantity().compareTo(req.getQuantity())!=0 || !Objects.equals(old.getRemark(),req.getRemark()))
            fail("请求标识已用于其他操作,请刷新后重试");
        return old.getId();
    }
    private void validateAction(MesDvSpareActionReqVO req) {
        if(req.getQuantity()==null || req.getQuantity().signum()<=0 || req.getQuantity().scale()>3
                || req.getQuantity().compareTo(MAX_QUANTITY)>0)fail("数量必须大于0,最多三位小数");
        if(req.getRemark()==null || req.getRemark().trim().isEmpty())fail("请填写操作说明");
    }
    private MesDvSparePartDO requirePart(Long id) {
        if(id==null)fail("请选择备件");
        var part=partMapper.lock(id);
        if(part==null)fail("备件不存在");
        return part;
    }
    private void requireEnabled(MesDvSparePartDO part) { if(!Objects.equals(part.getStatus(),0))fail("备件已停用"); }
    private void changeStock(MesDvSparePartDO part,BigDecimal delta) {
        var after=part.getStockQuantity().add(delta);
        if(after.signum()<0)fail("库存不足,当前可用:"+part.getStockQuantity()+part.getUnit());
        if(after.compareTo(MAX_QUANTITY)>0)fail("库存数量超出系统允许范围");
        part.setStockQuantity(after);
        partMapper.updateById(new MesDvSparePartDO().setId(part.getId()).setStockQuantity(after));
    }
    private MesDvSpareMovementDO snapshot(MesDvSparePartDO part,int type,BigDecimal qty,BigDecimal before,
                                         Long operator,String key,String remark) {
        return new MesDvSpareMovementDO().setRequestKey(key).setSparePartId(part.getId())
                .setSparePartCode(part.getCode()).setSparePartName(part.getName()).setSpecification(part.getSpecification())
                .setUnit(part.getUnit()).setType(type).setQuantity(qty).setBeforeQuantity(before)
                .setAfterQuantity(part.getStockQuantity()).setOperatorUserId(operator).setRemark(remark);
    }
    private Long writeMovement(MesDvSparePartDO part,int type,BigDecimal qty,BigDecimal before,MesDvRepairDO repair,
                               Long original,Long receiver,Long operator,String key,String remark) {
        var record=snapshot(part,type,qty,before,operator,key,remark).setOriginalId(original).setReceiverUserId(receiver);
        if(receiver!=null)record.setReceiverName(adminUserApi.getUser(receiver).getNickname());
        if(repair!=null) {
            var machine=machineryMapper.selectById(repair.getMachineryId());
            if(machine==null)fail("维修设备不存在");
            record.setRepairId(repair.getId()).setRepairCode(repair.getCode()).setMachineryId(machine.getId()).setMachineryName(machine.getName());
        }
        movementMapper.insert(record);
        return record.getId();
    }
    private static void fail(String reason) { throw exception(INVALID,reason); }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/energy/EnergyAlertListener.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.mes.service.energy;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.*;
@Component @Slf4j
public class EnergyAlertListener {
    @Resource private EnergyAlertService energyAlertService;
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onRecordChanged(EnergyRecordChangedEvent event) {
        TenantUtils.execute(event.tenantId(), () -> {
            for (var date : event.dates()) {
                try { energyAlertService.check(date); }
                catch (Exception e) { log.error("能耗预警检查失败 tenant={} date={}", event.tenantId(), date, e); }
            }
        });
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/energy/EnergyAlertService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,123 @@
package cn.iocoder.yudao.module.mes.service.energy;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
import cn.iocoder.yudao.framework.common.pojo.*;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.energy.vo.EnergyAlertRuleSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.energy.*;
import cn.iocoder.yudao.module.mes.dal.mysql.energy.*;
import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi;
import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO;
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@Service @Slf4j
public class EnergyAlertService {
    @Resource private EnergyAlertRuleMapper energyAlertRuleMapper;
    @Resource private EnergyAlertLogMapper energyAlertLogMapper;
    @Resource private EnergyRecordMapper energyRecordMapper;
    @Resource private AdminUserService adminUserService;
    @Resource private NotifyMessageSendApi notifyMessageSendApi;
    public PageResult<EnergyAlertRuleDO> page(PageParam page) {
        return energyAlertRuleMapper.selectPage(page, new LambdaQueryWrapperX<EnergyAlertRuleDO>().orderByDesc(EnergyAlertRuleDO::getId));
    }
    public PageResult<EnergyAlertLogDO> logs(PageParam page, LocalDate date, String status) {
        return energyAlertLogMapper.selectPage(page, new LambdaQueryWrapperX<EnergyAlertLogDO>()
                .eqIfPresent(EnergyAlertLogDO::getStatus, status)
                .and(date != null, w -> w.and(day -> day.eq(EnergyAlertLogDO::getPeriodType, "DAY").eq(EnergyAlertLogDO::getRecordDate, date))
                        .or(month -> month.eq(EnergyAlertLogDO::getPeriodType, "MONTH").eq(EnergyAlertLogDO::getRecordDate, date.withDayOfMonth(1))))
                .orderByDesc(EnergyAlertLogDO::getId));
    }
    public Long save(EnergyAlertRuleSaveReqVO req) {
        if (req.getId() != null) get(req.getId());
        adminUserService.validateUserList(req.getRecipientIds());
        EnergyAlertRuleDO rule = BeanUtils.toBean(req, EnergyAlertRuleDO.class);
        rule.setName(req.getName().trim());
        rule.setUsageArea(req.getUsageArea() == null ? "" : req.getUsageArea().trim());
        rule.setProductCategory(req.getProductCategory() == null ? "" : req.getProductCategory().trim());
        rule.setRecipientIds(req.getRecipientIds().stream().distinct().map(String::valueOf).collect(Collectors.joining(",")));
        if (rule.getId() == null) energyAlertRuleMapper.insert(rule);
        else if (energyAlertRuleMapper.updateById(rule) == 0) throw missing();
        return rule.getId();
    }
    public void delete(Long id) {
        get(id);
        if (energyAlertRuleMapper.deleteById(id) == 0) throw missing();
    }
    private EnergyAlertRuleDO get(Long id) {
        var rule = energyAlertRuleMapper.selectById(id);
        if (rule == null) throw missing();
        return rule;
    }
    private RuntimeException missing() { return exception(new ErrorCode(1_040_951_000, "能耗预警规则不存在")); }
    /** Check the selected day and its calendar month; successful sends commit with their history. */
    @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
    public Map<String, Integer> check(LocalDate date) {
        int sent = 0, failed = 0;
        var rules = energyAlertRuleMapper.selectList(new LambdaQueryWrapperX<EnergyAlertRuleDO>()
                .eq(EnergyAlertRuleDO::getEnabled, true).orderByAsc(EnergyAlertRuleDO::getId));
        for (var candidate : rules) {
            var rule = energyAlertRuleMapper.lock(candidate.getId());
            if (rule == null || !Boolean.TRUE.equals(rule.getEnabled())) continue;
            boolean monthly = rule.getMetric().startsWith("MONTH_");
            LocalDate periodDate = monthly ? date.withDayOfMonth(1) : date;
            String periodType = monthly ? "MONTH" : "DAY";
            boolean amount = rule.getMetric().endsWith("AMOUNT");
            BigDecimal actual = energyRecordMapper.selectDailyValue(periodDate, rule);
            if (!exceeds(actual, rule.getThreshold())) continue;
            for (String id : rule.getRecipientIds().split(",")) {
                long recipient = Long.parseLong(id);
                var log = energyAlertLogMapper.selectOne(new LambdaQueryWrapperX<EnergyAlertLogDO>()
                        .eq(EnergyAlertLogDO::getRuleId, rule.getId()).eq(EnergyAlertLogDO::getRecordDate, periodDate)
                        .eq(EnergyAlertLogDO::getPeriodType, periodType)
                        .eq(EnergyAlertLogDO::getRecipientId, recipient));
                if (log != null && "SENT".equals(log.getStatus())) continue;
                if (log == null) log = new EnergyAlertLogDO();
                log.setRuleId(rule.getId()); log.setRuleName(rule.getName()); log.setRecordDate(periodDate); log.setPeriodType(periodType);
                log.setEnergyType(rule.getEnergyType()); log.setMetric(rule.getMetric());
                log.setScope((rule.getUsageArea().isEmpty() ? "全部区域" : rule.getUsageArea()) + " / "
                        + (rule.getProductCategory().isEmpty() ? "全部产品" : rule.getProductCategory()));
                log.setActualValue(actual); log.setThreshold(rule.getThreshold()); log.setRecipientId(recipient);
                log.setRecipientName(String.valueOf(recipient));
                try {
                    adminUserService.validateUserList(List.of(recipient));
                    log.setRecipientName(adminUserService.getUser(recipient).getNickname());
                    NotifySendSingleToUserReqDTO message = new NotifySendSingleToUserReqDTO();
                    message.setUserId(recipient); message.setTemplateCode(monthly ? "energy_monthly_warning" : "energy_daily_warning");
                    message.setTemplateParams(Map.of("ruleName", rule.getName(), "date", monthly ? java.time.YearMonth.from(date).toString() : date.toString(),
                            "energyName", energyName(rule.getEnergyType()), "scope", log.getScope(),
                            "metricName", amount ? "费用" : "用量",
                            "actual", actual.toPlainString(), "threshold", rule.getThreshold().toPlainString(),
                            "unit", amount ? "元" : unit(rule.getEnergyType())));
                    Long messageId = notifyMessageSendApi.sendSingleMessageToAdmin(message);
                    if (messageId == null) throw new IllegalStateException("能耗预警通知模板已停用");
                    log.setMessageId(messageId); log.setStatus("SENT"); log.setErrorMessage(""); sent++;
                } catch (Exception e) {
                    logFailure(rule.getId(), date, recipient, e);
                    log.setStatus("FAILED");
                    log.setErrorMessage("发送失败,请检查接收人是否启用以及通知模板 " + (monthly ? "energy_monthly_warning" : "energy_daily_warning") + " æ˜¯å¦å¯ç”¨ï¼Œç„¶åŽé‡æ–°æ£€æŸ¥è¯¥æ—¥æœŸã€‚");
                    failed++;
                }
                if (log.getId() == null) energyAlertLogMapper.insert(log); else energyAlertLogMapper.updateById(log);
            }
        }
        return Map.of("sent", sent, "failed", failed);
    }
    private void logFailure(Long ruleId, LocalDate date, long recipient, Exception cause) {
        log.warn("能耗预警站内通知发送失败 rule={} date={} recipient={}", ruleId, date, recipient, cause);
    }
    static boolean exceeds(BigDecimal actual, BigDecimal threshold) { return actual != null && actual.compareTo(threshold) > 0; }
    public static String energyName(String type) { return switch (type) { case "WATER" -> "æ°´"; case "ELECTRICITY" -> "电"; default -> "蒸汽"; }; }
    public static String unit(String type) { return switch (type) { case "WATER" -> "m³"; case "ELECTRICITY" -> "kWh"; default -> "t"; }; }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/energy/EnergyExportService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
package cn.iocoder.yudao.module.mes.service.energy;
import cn.idev.excel.FastExcelFactory;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.framework.excel.core.handler.ColumnWidthMatchStyleStrategy;
import cn.iocoder.yudao.module.mes.controller.admin.energy.vo.*;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@Service
public class EnergyExportService {
    @Resource private EnergyRecordService energyRecordService;
    public void records(EnergyRecordPageReqVO req, HttpServletResponse response) throws IOException {
        req.setPageNo(1); req.setPageSize(50_001);
        var page = energyRecordService.page(req);
        if (page.getTotal() > 50_000) throw exception(new ErrorCode(1_040_952_000, "单次最多导出50000条,请缩小日期范围"));
        List<List<Object>> rows = page.getList().stream().map(r -> Arrays.<Object>asList(r.getRecordDate().toString(),
                EnergyAlertService.energyName(r.getEnergyType()), r.getUsageArea(), r.getProductCategory(), r.getUsagePurpose(),
                EnergyAlertService.unit(r.getEnergyType()), r.getQuantity(), r.getUnitPrice(), r.getAmount(), r.getRecorder(), r.getRemark())).toList();
        write(response, "能耗台账.xlsx", List.of(new Sheet("能耗台账",
                List.of("日期", "能源", "使用区域", "产品类别", "用途", "用量单位", "用量", "单价(元/单位)", "费用(元)", "录入人", "备注"), rows)));
    }
    public void analysis(EnergyAnalysisReqVO req, HttpServletResponse response) throws IOException {
        var data = energyRecordService.analysis(req);
        var head = List.of("统计项", "能源", "用量单位", "用量", "费用(元)", "录入条数");
        List<List<Object>> summary = data.getSummary().stream().map(r -> row("合计", r)).toList();
        List<List<Object>> trend = data.getTrend().stream().map(r -> row(r.getLabel(), r)).toList();
        List<List<Object>> areas = data.getAreas().stream().map(r -> row(r.getLabel(), r)).toList();
        List<List<Object>> scope = List.of(Arrays.asList("开始日期", req.getStartDate().toString()),
                Arrays.asList("结束日期", req.getEndDate().toString()), Arrays.asList("汇总方式", "MONTH".equals(req.getGroupBy()) ? "按月" : "按日"),
                Arrays.asList("能源", req.getEnergyType() == null || req.getEnergyType().isEmpty() ? "全部" : EnergyAlertService.energyName(req.getEnergyType())),
                Arrays.asList("区域关键词", Objects.toString(req.getUsageArea(), "")), Arrays.asList("产品类别", Objects.toString(req.getProductCategory(), "")),
                Arrays.asList("总费用(元)", data.getTotalAmount()), Arrays.asList("录入条数", data.getRecordCount()));
        write(response, "能耗分析.xlsx", List.of(new Sheet("统计口径", List.of("项目", "内容"), scope),
                new Sheet("能源汇总", head, summary), new Sheet("日期趋势", head, trend), new Sheet("区域汇总", head, areas)));
    }
    private List<Object> row(String label, EnergyAnalysisRespVO.Summary r) {
        return Arrays.asList(label, EnergyAlertService.energyName(r.getEnergyType()), EnergyAlertService.unit(r.getEnergyType()), r.getQuantity(), r.getAmount(), r.getRecordCount());
    }
    private record Sheet(String name, List<String> headers, List<List<Object>> rows) {}
    private void write(HttpServletResponse response, String filename, List<Sheet> sheets) throws IOException {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
        try (var writer = FastExcelFactory.write(response.getOutputStream()).autoCloseStream(false)
                .registerWriteHandler(new ColumnWidthMatchStyleStrategy()).build()) {
            for (int i = 0; i < sheets.size(); i++) {
                var sheet = sheets.get(i);
                writer.write(sheet.rows(), FastExcelFactory.writerSheet(i, sheet.name())
                        .head(sheet.headers().stream().map(List::of).toList()).build());
            }
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/energy/EnergyRecordChangedEvent.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,6 @@
package cn.iocoder.yudao.module.mes.service.energy;
import java.time.LocalDate;
import java.util.Set;
public record EnergyRecordChangedEvent(Long tenantId, Set<LocalDate> dates) {}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/energy/EnergyRecordService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,127 @@
package cn.iocoder.yudao.module.mes.service.energy;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
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.energy.vo.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.energy.EnergyRecordDO;
import cn.iocoder.yudao.module.mes.dal.mysql.energy.EnergyRecordMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.annotation.Transactional;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import java.util.Set;
import java.math.RoundingMode;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@Service
public class EnergyRecordService {
    private static final ErrorCode NOT_EXISTS = new ErrorCode(1_040_950_000, "能耗记录不存在或已删除");
    private static final ErrorCode ID_REQUIRED = new ErrorCode(1_040_950_001, "修改能耗记录必须提供编号");
    @Resource
    private EnergyRecordMapper recordMapper;
    @Resource private ApplicationEventPublisher applicationEventPublisher;
    @Transactional(rollbackFor = Exception.class)
    public Long create(EnergyRecordSaveReqVO req) {
        EnergyRecordDO record = toRecord(req);
        record.setId(null);
        recordMapper.insert(record);
        applicationEventPublisher.publishEvent(new EnergyRecordChangedEvent(TenantContextHolder.getTenantId(), Set.of(record.getRecordDate())));
        return record.getId();
    }
    @Transactional(rollbackFor = Exception.class)
    public void update(EnergyRecordSaveReqVO req) {
        if (req.getId() == null) {
            throw exception(ID_REQUIRED);
        }
        EnergyRecordDO old = get(req.getId());
        if (recordMapper.updateById(toRecord(req)) == 0) {
            throw exception(NOT_EXISTS);
        }
        var dates = new java.util.HashSet<>(Set.of(old.getRecordDate()));
        dates.add(req.getRecordDate());
        applicationEventPublisher.publishEvent(new EnergyRecordChangedEvent(TenantContextHolder.getTenantId(), Set.copyOf(dates)));
    }
    @Transactional(rollbackFor = Exception.class)
    public void delete(Long id) {
        EnergyRecordDO old = get(id);
        if (recordMapper.deleteById(id) == 0) {
            throw exception(NOT_EXISTS);
        }
        applicationEventPublisher.publishEvent(new EnergyRecordChangedEvent(TenantContextHolder.getTenantId(), Set.of(old.getRecordDate())));
    }
    public EnergyRecordDO get(Long id) {
        EnergyRecordDO record = recordMapper.selectById(id);
        if (record == null) {
            throw exception(NOT_EXISTS);
        }
        return record;
    }
    public PageResult<EnergyRecordDO> page(EnergyRecordPageReqVO req) {
        return recordMapper.selectPage(req);
    }
    static EnergyRecordDO toRecord(EnergyRecordSaveReqVO req) {
        EnergyRecordDO record = BeanUtils.toBean(req, EnergyRecordDO.class);
        record.setUsageArea(req.getUsageArea().trim());
        record.setProductCategory(req.getProductCategory().trim());
        record.setUsagePurpose(req.getUsagePurpose().trim());
        record.setRecorder(req.getRecorder().trim());
        // ç©ºå¤‡æ³¨ä¹Ÿå¯åœ¨ä¿®æ”¹æ—¶æ¸…除。
        record.setRemark(req.getRemark() == null ? "" : req.getRemark().trim());
        record.setAmount(req.getQuantity().multiply(req.getUnitPrice()).setScale(2, RoundingMode.HALF_UP));
        return record;
    }
    public EnergyAnalysisRespVO analysis(EnergyAnalysisReqVO req) {
        return aggregate(recordMapper.selectAnalysis(req));
    }
    static EnergyAnalysisRespVO aggregate(List<EnergyRecordMapper.Aggregate> rows) {
        EnergyAnalysisRespVO result = new EnergyAnalysisRespVO();
        Map<String, EnergyAnalysisRespVO.Summary> summary = new LinkedHashMap<>();
        for (String type : List.of("WATER", "ELECTRICITY", "STEAM")) {
            EnergyAnalysisRespVO.Summary item = new EnergyAnalysisRespVO.Summary();
            item.setEnergyType(type);
            summary.put(type, item);
        }
        Map<List<String>, EnergyAnalysisRespVO.Bucket> trend = new LinkedHashMap<>();
        Map<List<String>, EnergyAnalysisRespVO.Bucket> areas = new LinkedHashMap<>();
        for (EnergyRecordMapper.Aggregate row : rows) {
            add(summary.get(row.getEnergyType()), row);
            add(bucket(trend, row.getPeriod(), row.getEnergyType()), row);
            add(bucket(areas, row.getUsageArea(), row.getEnergyType()), row);
            result.setTotalAmount(result.getTotalAmount().add(row.getAmount()));
            result.setRecordCount(result.getRecordCount() + row.getRecordCount());
        }
        result.setSummary(List.copyOf(summary.values()));
        result.setTrend(List.copyOf(trend.values()));
        result.setAreas(List.copyOf(areas.values()));
        return result;
    }
    private static EnergyAnalysisRespVO.Bucket bucket(Map<List<String>, EnergyAnalysisRespVO.Bucket> map,
                                                       String label, String type) {
        return map.computeIfAbsent(List.of(label, type), key -> {
            EnergyAnalysisRespVO.Bucket item = new EnergyAnalysisRespVO.Bucket();
            item.setLabel(label);
            item.setEnergyType(type);
            return item;
        });
    }
    private static void add(EnergyAnalysisRespVO.Summary item, EnergyRecordMapper.Aggregate row) {
        item.setQuantity(item.getQuantity().add(row.getQuantity()));
        item.setAmount(item.getAmount().add(row.getAmount()));
        item.setRecordCount(item.getRecordCount() + row.getRecordCount());
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ledger/MesQcInspectionLedgerService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.mes.service.qc.ledger;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ledger.vo.MesQcInspectionLedgerPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ledger.vo.MesQcInspectionLedgerRespVO;
/**
 * MES æ£€æµ‹ç»“果台账 Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface MesQcInspectionLedgerService {
    /**
     * èŽ·å¾—æ£€æµ‹ç»“æžœå°è´¦åˆ†é¡µï¼ˆå››ç±»æ£€éªŒå•ç»Ÿä¸€æŸ¥è¯¢ï¼Œä¸€è¡Œä¸€ä¸ªæ£€æµ‹é¡¹ç»“æžœï¼‰
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return å°è´¦åˆ†é¡µ
     */
    PageResult<MesQcInspectionLedgerRespVO> getInspectionLedgerPage(MesQcInspectionLedgerPageReqVO pageReqVO);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ledger/MesQcInspectionLedgerServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,75 @@
package cn.iocoder.yudao.module.mes.service.qc.ledger;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ledger.vo.MesQcInspectionLedgerPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ledger.vo.MesQcInspectionLedgerRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.vo.MesQcQualityExportVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.ledger.MesQcInspectionLedgerMapper;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import com.baomidou.mybatisplus.core.metadata.IPage;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
/**
 * MES æ£€æµ‹ç»“果台账 Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class MesQcInspectionLedgerServiceImpl implements MesQcInspectionLedgerService {
    @Resource
    private MesQcInspectionLedgerMapper ledgerMapper;
    @Resource
    private MesMdItemService itemService;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    @Resource
    private AdminUserApi adminUserApi;
    @Override
    public PageResult<MesQcInspectionLedgerRespVO> getInspectionLedgerPage(MesQcInspectionLedgerPageReqVO pageReqVO) {
        // åˆ†é¡µæŸ¥è¯¢ï¼ˆå››ç±»æ£€éªŒå• UNION ALL)
        IPage<MesQcInspectionLedgerRespVO> page = MyBatisUtils.buildPage(pageReqVO);
        ledgerMapper.selectInspectionLedgerPage(page, pageReqVO);
        List<MesQcInspectionLedgerRespVO> list = page.getRecords();
        if (CollUtil.isEmpty(list)) {
            return PageResult.empty(page.getTotal());
        }
        // æ‰¹é‡æŸ¥è¯¢ç‰©æ–™ã€è®¡é‡å•位、检测人员,并拼装判定依据
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(convertSet(list, MesQcInspectionLedgerRespVO::getItemId));
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesQcInspectionLedgerRespVO::getInspectorUserId));
        for (MesQcInspectionLedgerRespVO vo : list) {
            findAndThen(itemMap, vo.getItemId(), item -> {
                vo.setItemCode(item.getCode());
                vo.setItemName(item.getName());
                vo.setItemSpecification(item.getSpecification());
                findAndThen(unitMeasureMap, item.getUnitMeasureId(), unit -> vo.setUnitName(unit.getName()));
            });
            findAndThen(userMap, vo.getInspectorUserId(), user -> vo.setInspectorName(user.getNickname()));
            vo.setJudgementBasis(MesQcQualityExportVO.buildRangeText(
                    vo.getStandardValue(), vo.getMinThreshold(), vo.getMaxThreshold()));
        }
        return new PageResult<>(list, page.getTotal());
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyCertificateService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,234 @@
package cn.iocoder.yudao.module.mes.service.safety;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.hrm.api.employee.HrmEmployeeApi;
import cn.iocoder.yudao.module.hrm.api.employee.dto.HrmEmployeeSimpleDTO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.cert.SafetyCertificatePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.cert.SafetyCertificateRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.cert.SafetyCertificateSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyCertificateDO;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.SafetyCertificateMapper;
import cn.iocoder.yudao.module.mes.enums.safety.SafetyCertificateStatusEnum;
import cn.iocoder.yudao.module.mes.enums.safety.SafetyCertificateTypeEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.SafetyErrorCodeConstants.*;
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰¹ç§ä½œä¸šè¯å°è´¦ Service
 *
 * <p>核心能力:证书到期自动预警。状态由 {@link #resolveStatus} æ ¹æ®æœ‰æ•ˆæœŸå®žæ—¶æŽ¨å¯¼ï¼Œ
 * ä¸ä¾èµ–手工维护,避免过期未及时更新状态。</p>
 *
 * <p>持证人通过 employeeId å…³è”人力资源的员工档案:
 * æ–°å¢ž/修改时从档案带出姓名、工号、部门并落为快照;查询时实时回填最新档案信息,
 * å‘˜å·¥è°ƒå²—后证件列表也会跟着显示新部门。</p>
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class SafetyCertificateService {
    /** é»˜è®¤æå‰é¢„警天数 */
    public static final int DEFAULT_WARN_DAYS = 30;
    @Resource
    private SafetyCertificateMapper certificateMapper;
    @Resource
    private HrmEmployeeApi hrmEmployeeApi;
    @Resource
    private SafetyRefResolver refResolver;
    public Long createCertificate(SafetyCertificateSaveReqVO reqVO) {
        reqVO.setId(null);
        validateCertificateForCreate(reqVO);
        SafetyCertificateDO cert = buildCertificate(reqVO, null);
        certificateMapper.insert(cert);
        return cert.getId();
    }
    public void updateCertificate(SafetyCertificateSaveReqVO reqVO) {
        SafetyCertificateDO old = validateCertificateExists(reqVO.getId());
        validateCertificateForUpdate(reqVO, old);
        SafetyCertificateDO cert = buildCertificate(reqVO, old);
        certificateMapper.updateById(cert);
    }
    public void deleteCertificate(Long id) {
        validateCertificateExists(id);
        certificateMapper.deleteById(id);
    }
    public SafetyCertificateDO getCertificate(Long id) {
        return validateCertificateExists(id);
    }
    public PageResult<SafetyCertificateDO> getCertificatePage(SafetyCertificatePageReqVO reqVO) {
        return certificateMapper.selectPage(reqVO);
    }
    /**
     * æŸ¥è¯¢å³å°†åˆ°æœŸ / å·²è¿‡æœŸçš„证件列表
     *
     * @param days æå‰é¢„警天数,为空使用默认 30 å¤©
     */
    public List<SafetyCertificateDO> getExpiringCertificates(Integer days) {
        int warnDays = days == null || days <= 0 ? DEFAULT_WARN_DAYS : days;
        return certificateMapper.selectExpiringBefore(LocalDate.now().plusDays(warnDays));
    }
    /**
     * æ‰¹é‡è¡¥å…¨æŒè¯äººçš„æœ€æ–°å‘˜å·¥æ¡£æ¡ˆä¿¡æ¯ï¼ˆå§“名、工号、部门、手机、关联用户)
     *
     * <p>查询时用档案实时值覆盖快照值,保证员工调岗后台账同步;
     * å‘˜å·¥æ¡£æ¡ˆå·²è¢«åˆ é™¤æ—¶å›žé€€åˆ°å¿«ç…§å€¼ï¼Œä¿è¯åŽ†å²è¯ä»¶ä»å¯å±•ç¤ºã€‚</p>
     */
    public void fillEmployeeInfo(List<SafetyCertificateDO> certs) {
        List<Long> employeeIds = certs.stream()
                .map(SafetyCertificateDO::getEmployeeId)
                .filter(Objects::nonNull).distinct().toList();
        // ç”¨ new HashMap<>() è€Œéž Map.of():employeeId å…è®¸ä¸º null,
        // åŽé¢ employeeMap.get(cert.getEmployeeId()) åœ¨ç©ºé›†åˆåœºæ™¯ä¼š get(null),
        // Map.of() çš„ get(null) ä¼šæŠ› NPE å¯¼è‡´è¯ä»¶åˆ—表接口 500。
        Map<Long, HrmEmployeeSimpleDTO> employeeMap = employeeIds.isEmpty() ? new HashMap<>()
                : hrmEmployeeApi.getEmployeeList(employeeIds).stream()
                        .collect(Collectors.toMap(HrmEmployeeSimpleDTO::getId, e -> e, (a, b) -> a));
        certs.forEach(cert -> {
            HrmEmployeeSimpleDTO employee = employeeMap.get(cert.getEmployeeId());
            if (employee != null) {
                cert.setEmployeeName(employee.getName());
                cert.setEmployeeNo(employee.getEmployeeNo());
                cert.setDeptName(employee.getDeptName());
            }
        });
    }
    /**
     * æ‰¹é‡è¡¥å…¨å±•示字段,持证人档案一次性查好,避免逐条查询
     */
    public void fillDisplayFieldsBatch(List<SafetyCertificateDO> certs, List<SafetyCertificateRespVO> resps) {
        var employeeMap = refResolver.employeeMap(certs.stream().map(SafetyCertificateDO::getEmployeeId).toList());
        for (int i = 0; i < certs.size(); i++) {
            fillDisplayFields(certs.get(i), resps.get(i), employeeMap.get(certs.get(i).getEmployeeId()));
        }
    }
    /**
     * å•个证件的展示字段补全(类型名称、状态名称、剩余天数、持证人档案)
     */
    public void fillDisplayFields(SafetyCertificateDO cert, SafetyCertificateRespVO respVO) {
        fillDisplayFields(cert, respVO, cert.getEmployeeId() == null ? null
                : hrmEmployeeApi.getEmployee(cert.getEmployeeId()));
    }
    private void fillDisplayFields(SafetyCertificateDO cert, SafetyCertificateRespVO respVO,
                                   HrmEmployeeSimpleDTO employee) {
        respVO.setCertTypeName(SafetyCertificateTypeEnum.getNameByType(cert.getCertType()));
        String status = resolveStatus(cert.getExpireDate(), cert.getWarnDays(), cert.getStatus());
        respVO.setStatus(status);
        respVO.setStatusName(SafetyCertificateStatusEnum.getNameByStatus(status));
        if (cert.getExpireDate() != null) {
            respVO.setRemainingDays(ChronoUnit.DAYS.between(LocalDate.now(), cert.getExpireDate()));
        }
        if (employee != null) {
            respVO.setEmployeeName(employee.getName());
            respVO.setEmployeeNo(employee.getEmployeeNo());
            respVO.setDeptName(employee.getDeptName());
            respVO.setPhone(employee.getPhone());
            respVO.setUserId(employee.getUserId());
        }
    }
    /**
     * æ ¹æ®æœ‰æ•ˆæœŸæŽ¨å¯¼çŠ¶æ€ï¼šå·²æ³¨é”€ä¿æŒä¸å˜ï¼›è¿‡æœŸ â†’ EXPIRED;在预警期内 â†’ EXPIRING;否则 VALID
     *
     * @param expireDate æœ‰æ•ˆæœŸ
     * @param warnDays   æå‰é¢„警天数
     * @param current    å½“前状态,用于保护 REVOKED
     */
    public static String resolveStatus(LocalDate expireDate, Integer warnDays, String current) {
        if (SafetyCertificateStatusEnum.REVOKED.getStatus().equals(current)) {
            return SafetyCertificateStatusEnum.REVOKED.getStatus();
        }
        if (expireDate == null) {
            return SafetyCertificateStatusEnum.VALID.getStatus();
        }
        LocalDate today = LocalDate.now();
        if (expireDate.isBefore(today)) {
            return SafetyCertificateStatusEnum.EXPIRED.getStatus();
        }
        int days = warnDays == null || warnDays <= 0 ? DEFAULT_WARN_DAYS : warnDays;
        if (!expireDate.isAfter(today.plusDays(days))) {
            return SafetyCertificateStatusEnum.EXPIRING.getStatus();
        }
        return SafetyCertificateStatusEnum.VALID.getStatus();
    }
    /**
     * æž„建 DO:持证人姓名/工号/部门从员工档案带出并落为快照
     */
    private SafetyCertificateDO buildCertificate(SafetyCertificateSaveReqVO reqVO, SafetyCertificateDO old) {
        HrmEmployeeSimpleDTO employee = hrmEmployeeApi.getEmployee(reqVO.getEmployeeId());
        if (employee == null) {
            throw exception(SAFETY_CERT_EMPLOYEE_NOT_EXISTS);
        }
        SafetyCertificateDO cert = BeanUtils.toBean(reqVO, SafetyCertificateDO.class);
        cert.setEmployeeId(employee.getId());
        cert.setEmployeeName(employee.getName());
        cert.setEmployeeNo(employee.getEmployeeNo());
        cert.setDeptName(employee.getDeptName());
        cert.setCertNo(StrUtil.trim(reqVO.getCertNo()));
        cert.setWarnDays(reqVO.getWarnDays() == null ? DEFAULT_WARN_DAYS : reqVO.getWarnDays());
        // å·²æ³¨é”€çŠ¶æ€ä¸å…è®¸é€šè¿‡ç¼–è¾‘æ”¹å›ž
        String currentStatus = old == null ? reqVO.getStatus() : old.getStatus();
        cert.setStatus(resolveStatus(reqVO.getExpireDate(), cert.getWarnDays(), currentStatus));
        cert.setRemark(reqVO.getRemark() == null ? "" : reqVO.getRemark().trim());
        return cert;
    }
    private void validateCertificateForCreate(SafetyCertificateSaveReqVO reqVO) {
        validateExpireAfterIssue(reqVO);
        if (certificateMapper.selectByCertNo(StrUtil.trim(reqVO.getCertNo())) != null) {
            throw exception(SAFETY_CERT_NO_DUPLICATE);
        }
    }
    private void validateCertificateForUpdate(SafetyCertificateSaveReqVO reqVO, SafetyCertificateDO old) {
        validateExpireAfterIssue(reqVO);
        SafetyCertificateDO byCertNo = certificateMapper.selectByCertNo(StrUtil.trim(reqVO.getCertNo()));
        if (byCertNo != null && !byCertNo.getId().equals(old.getId())) {
            throw exception(SAFETY_CERT_NO_DUPLICATE);
        }
    }
    private void validateExpireAfterIssue(SafetyCertificateSaveReqVO reqVO) {
        if (reqVO.getIssueDate() != null && reqVO.getExpireDate() != null
                && !Objects.requireNonNull(reqVO.getExpireDate()).isAfter(reqVO.getIssueDate())) {
            throw exception(SAFETY_CERT_EXPIRE_BEFORE_ISSUE);
        }
    }
    private SafetyCertificateDO validateCertificateExists(Long id) {
        SafetyCertificateDO cert = id == null ? null : certificateMapper.selectById(id);
        if (cert == null) {
            throw exception(SAFETY_CERT_NOT_EXISTS);
        }
        return cert;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyFileService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,99 @@
package cn.iocoder.yudao.module.mes.service.safety;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.infra.api.file.FileApi;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
 * å®‰å…¨ç”Ÿäº§ - ç§»åŠ¨ç«¯é™„ä»¶ä¸Šä¼  Service
 *
 * <p>巡检、隐患、作业票、演练等环节的现场拍照统一走这里:
 * è½åˆ° infra æ–‡ä»¶å­˜å‚¨ï¼Œè¿”回可访问 URL,多个附件逗号拼接后存入业务表。</p>
 *
 * @author è¶…级管理员
 */
@Service
@Slf4j
public class SafetyFileService {
    /** å•个文件最大 20MB,手机原图裁切后足够 */
    private static final long MAX_SIZE = 20L * 1024 * 1024;
    /** å…è®¸çš„图片类型 */
    private static final List<String> ALLOWED_EXT = List.of(
            "jpg", "jpeg", "png", "webp", "heic", "gif", "bmp", "pdf", "doc", "docx", "xls", "xlsx");
    @Resource
    private FileApi fileApi;
    /**
     * ä¸Šä¼ å•个文件
     *
     * @param file     æ–‡ä»¶
     * @param bizType  ä¸šåŠ¡ç±»åž‹ï¼ˆsafety/inspection、safety/hazard ç­‰ï¼‰ï¼Œç”¨ä½œå­˜å‚¨ç›®å½•
     * @return æ–‡ä»¶è®¿é—® URL
     */
    public String upload(MultipartFile file, String bizType) {
        if (file == null || file.isEmpty()) {
            throw new IllegalArgumentException("上传文件不能为空");
        }
        if (file.getSize() > MAX_SIZE) {
            throw new IllegalArgumentException("单个文件不能超过 20MB");
        }
        String originalName = StrUtil.blankToDefault(file.getOriginalFilename(), "photo.jpg");
        String ext = StrUtil.subAfter(originalName, ".", true).toLowerCase();
        if (StrUtil.isNotBlank(ext) && !ALLOWED_EXT.contains(ext)) {
            throw new IllegalArgumentException("不支持的文件类型:" + ext);
        }
        byte[] content;
        try {
            content = IoUtil.readBytes(file.getInputStream());
        } catch (IOException e) {
            throw new IllegalStateException("读取上传文件失败", e);
        }
        String directory = StrUtil.blankToDefault(bizType, "safety") + "/" + java.time.LocalDate.now();
        return fileApi.createFile(content, originalName, directory, file.getContentType());
    }
    /**
     * æ‰¹é‡ä¸Šä¼ ï¼ˆç§»åŠ¨ç«¯ä¸€æ¬¡æ‹å¤šå¼ ï¼‰
     *
     * @return æ–‡ä»¶ URL åˆ—表
     */
    public List<String> uploadBatch(List<MultipartFile> files, String bizType) {
        if (files == null || files.isEmpty()) {
            return List.of();
        }
        if (files.size() > 9) {
            throw new IllegalArgumentException("单次最多上传 9 å¼ ");
        }
        List<String> urls = new ArrayList<>();
        for (MultipartFile file : files) {
            urls.add(upload(file, bizType));
        }
        return urls;
    }
    /**
     * åˆ é™¤é™„件(从逗号串里剔除某个 URL)
     */
    public String removeUrl(String urls, String target) {
        if (StrUtil.isBlank(urls) || StrUtil.isBlank(target)) {
            return urls;
        }
        return Arrays.stream(urls.split(","))
                .map(String::trim)
                .filter(url -> !url.equals(target.trim()))
                .reduce((a, b) -> a + "," + b)
                .orElse("");
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyHazardService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,220 @@
package cn.iocoder.yudao.module.mes.service.safety;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
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.hazard.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyHazardDO;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.SafetyHazardMapper;
import cn.iocoder.yudao.module.mes.enums.safety.SafetyHazardLevelEnum;
import cn.iocoder.yudao.module.mes.enums.safety.SafetyHazardSourceEnum;
import cn.iocoder.yudao.module.mes.enums.safety.SafetyHazardStatusEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.SafetyErrorCodeConstants.*;
/**
 * å®‰å…¨ç”Ÿäº§ - é𐿂£ç™»è®°ä¸Žé—­çޝ Service
 *
 * <p>完整闭环:上报 â†’ åˆ†æ´¾ â†’ æ•´æ”¹ â†’ å¤æŸ¥ï¼ˆé€šè¿‡åˆ™é—­çŽ¯ï¼Œä¸é€šè¿‡é€€å›žæ•´æ”¹ï¼‰ã€‚
 * æ¯ä¸ªçŽ¯èŠ‚éƒ½æ ¡éªŒå½“å‰çŠ¶æ€ï¼Œé˜²æ­¢è·³æ­¥æˆ–é‡å¤æ“ä½œã€‚</p>
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class SafetyHazardService {
    private static final DateTimeFormatter CODE_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
    @Resource
    private SafetyHazardMapper hazardMapper;
    @Resource
    private SafetyRefResolver refResolver;
    /**
     * é𐿂£ä¸ŠæŠ¥ï¼ˆå«ç§»åŠ¨ç«¯æ‹ç…§ä¸Šä¼ çš„ photoUrls)
     */
    @Transactional(rollbackFor = Exception.class)
    public Long createHazard(SafetyHazardSaveReqVO reqVO) {
        reqVO.setId(null);
        SafetyHazardDO hazard = BeanUtils.toBean(reqVO, SafetyHazardDO.class);
        hazard.setCode(hazardMapper.selectNextCode("HZ" + LocalDate.now().format(CODE_DATE)));
        hazard.setTitle(StrUtil.trim(reqVO.getTitle()));
        hazard.setReporterId(reqVO.getReporterId());
        hazard.setReporter(refResolver.requireEmployee(reqVO.getReporterId(), SAFETY_EMPLOYEE_REQUIRED).getName());
        hazard.setLevel(StrUtil.blankToDefault(reqVO.getLevel(), SafetyHazardLevelEnum.GENERAL.getLevel()));
        hazard.setSource(StrUtil.blankToDefault(reqVO.getSource(), SafetyHazardSourceEnum.SELF_CHECK.getSource()));
        hazard.setReportTime(reqVO.getReportTime() == null ? LocalDateTime.now() : reqVO.getReportTime());
        hazard.setStatus(SafetyHazardStatusEnum.REPORTED.getStatus());
        hazard.setRemark(reqVO.getRemark() == null ? "" : reqVO.getRemark().trim());
        hazardMapper.insert(hazard);
        return hazard.getId();
    }
    /**
     * ä¿®æ”¹é𐿂£åŸºç¡€ä¿¡æ¯ï¼ˆä»…未分派前允许改关键字段)
     */
    public void updateHazard(SafetyHazardSaveReqVO reqVO) {
        SafetyHazardDO old = validateHazardExists(reqVO.getId());
        if (!SafetyHazardStatusEnum.REPORTED.getStatus().equals(old.getStatus())
                && !SafetyHazardStatusEnum.ASSIGNED.getStatus().equals(old.getStatus())) {
            throw exception(SAFETY_HAZARD_STATUS_ERROR);
        }
        SafetyHazardDO update = BeanUtils.toBean(reqVO, SafetyHazardDO.class);
        // ç¼–号与状态不允许通过普通编辑修改
        update.setCode(null);
        update.setStatus(null);
        hazardMapper.updateById(update);
    }
    /**
     * åˆ†æ´¾æ•´æ”¹è´£ä»»äºº
     */
    @Transactional(rollbackFor = Exception.class)
    public void assignHazard(SafetyHazardAssignReqVO reqVO) {
        SafetyHazardDO hazard = validateHazardExists(reqVO.getId());
        if (!SafetyHazardStatusEnum.REPORTED.getStatus().equals(hazard.getStatus())
                && !SafetyHazardStatusEnum.REJECTED.getStatus().equals(hazard.getStatus())) {
            throw exception(SAFETY_HAZARD_STATUS_ERROR);
        }
        if (reqVO.getAssigneeId() == null) {
            throw exception(SAFETY_HAZARD_ASSIGNEE_REQUIRED);
        }
        var assignee = refResolver.requireEmployee(reqVO.getAssigneeId(), SAFETY_HAZARD_ASSIGNEE_REQUIRED);
        SafetyHazardDO update = new SafetyHazardDO();
        update.setId(hazard.getId());
        update.setAssigneeId(assignee.getId());
        update.setAssignee(assignee.getName());
        update.setAssignTime(LocalDateTime.now());
        update.setRectifyDeadline(reqVO.getRectifyDeadline());
        update.setStatus(SafetyHazardStatusEnum.RECTIFYING.getStatus());
        if (StrUtil.isNotBlank(reqVO.getRemark())) {
            update.setRemark(reqVO.getRemark().trim());
        }
        hazardMapper.updateById(update);
    }
    /**
     * æäº¤æ•´æ”¹ç»“æžœ
     */
    @Transactional(rollbackFor = Exception.class)
    public void rectifyHazard(SafetyHazardRectifyReqVO reqVO) {
        SafetyHazardDO hazard = validateHazardExists(reqVO.getId());
        if (!SafetyHazardStatusEnum.RECTIFYING.getStatus().equals(hazard.getStatus())
                && !SafetyHazardStatusEnum.REJECTED.getStatus().equals(hazard.getStatus())) {
            throw exception(SAFETY_HAZARD_STATUS_ERROR);
        }
        SafetyHazardDO update = new SafetyHazardDO();
        update.setId(hazard.getId());
        update.setRectifyDesc(reqVO.getRectifyDesc().trim());
        update.setRectifyPhotoUrls(reqVO.getRectifyPhotoUrls());
        update.setRectifyTime(LocalDateTime.now());
        update.setStatus(SafetyHazardStatusEnum.RECHECKING.getStatus());
        hazardMapper.updateById(update);
    }
    /**
     * å¤æŸ¥ï¼šé€šè¿‡åˆ™é—­çŽ¯ï¼Œä¸é€šè¿‡é€€å›žæ•´æ”¹ã€‚
     *
     * <p>校验整改人与复查人不能是同一人,保证闭环的两级分离。</p>
     */
    @Transactional(rollbackFor = Exception.class)
    public void recheckHazard(SafetyHazardRecheckReqVO reqVO) {
        SafetyHazardDO hazard = validateHazardExists(reqVO.getId());
        if (!SafetyHazardStatusEnum.RECHECKING.getStatus().equals(hazard.getStatus())) {
            throw exception(SAFETY_HAZARD_STATUS_ERROR);
        }
        var rechecker = refResolver.requireEmployee(reqVO.getRecheckerId(), SAFETY_EMPLOYEE_REQUIRED);
        // ç”¨å‘˜å·¥ç¼–号比较,比姓名比较可靠(同名不同人 / æ”¹åéƒ½ä¸ä¼šè¯¯åˆ¤ï¼‰
        if (hazard.getAssigneeId() != null && hazard.getAssigneeId().equals(rechecker.getId())) {
            throw exception(SAFETY_HAZARD_SAME_OPERATOR);
        }
        boolean passed = "PASSED".equals(reqVO.getRecheckResult());
        SafetyHazardDO update = new SafetyHazardDO();
        update.setId(hazard.getId());
        update.setRecheckerId(rechecker.getId());
        update.setRechecker(rechecker.getName());
        update.setRecheckTime(LocalDateTime.now());
        update.setRecheckResult(reqVO.getRecheckResult());
        update.setRecheckRemark(reqVO.getRecheckRemark());
        if (passed) {
            update.setStatus(SafetyHazardStatusEnum.CLOSED.getStatus());
            update.setCloseTime(LocalDateTime.now());
        } else {
            update.setStatus(SafetyHazardStatusEnum.REJECTED.getStatus());
        }
        hazardMapper.updateById(update);
    }
    public void deleteHazard(Long id) {
        validateHazardExists(id);
        hazardMapper.deleteById(id);
    }
    public SafetyHazardDO getHazard(Long id) {
        return validateHazardExists(id);
    }
    public PageResult<SafetyHazardDO> getHazardPage(SafetyHazardPageReqVO reqVO) {
        reqVO.normalizeTimeRange();
        return hazardMapper.selectPage(reqVO);
    }
    /**
     * æ‰¹é‡å›žå¡«ä¸ŠæŠ¥äºº / æ•´æ”¹è´£ä»»äºº / å¤æŸ¥äººçš„æœ€æ–°å§“名
     */
    public void fillPersonNames(List<SafetyHazardDO> hazards) {
        if (CollUtil.isEmpty(hazards)) {
            return;
        }
        var ids = new java.util.ArrayList<Long>(hazards.size() * 3);
        hazards.forEach(h -> { ids.add(h.getReporterId()); ids.add(h.getAssigneeId()); ids.add(h.getRecheckerId()); });
        var employeeMap = refResolver.employeeMap(ids);
        hazards.forEach(hazard -> {
            var reporter = employeeMap.get(hazard.getReporterId());
            if (reporter != null) hazard.setReporter(reporter.getName());
            var assignee = employeeMap.get(hazard.getAssigneeId());
            if (assignee != null) hazard.setAssignee(assignee.getName());
            var rechecker = employeeMap.get(hazard.getRecheckerId());
            if (rechecker != null) hazard.setRechecker(rechecker.getName());
        });
    }
    /**
     * å±•示字段补全(等级/来源/状态名称、超期标记)
     */
    public static void fillHazardDisplay(SafetyHazardDO hazard, SafetyHazardRespVO respVO) {
        respVO.setLevelName(SafetyHazardLevelEnum.getNameByLevel(hazard.getLevel()));
        respVO.setSourceName(SafetyHazardSourceEnum.getNameBySource(hazard.getSource()));
        respVO.setStatusName(SafetyHazardStatusEnum.getNameByStatus(hazard.getStatus()));
        respVO.setRecheckResultName(switch (StrUtil.blankToDefault(hazard.getRecheckResult(), "")) {
            case "PASSED" -> "通过";
            case "REJECTED" -> "不通过";
            default -> "";
        });
        boolean overdue = hazard.getRectifyDeadline() != null
                && hazard.getRectifyDeadline().isBefore(LocalDate.now())
                && !SafetyHazardStatusEnum.isFinished(hazard.getStatus());
        respVO.setOverdue(overdue);
    }
    private SafetyHazardDO validateHazardExists(Long id) {
        SafetyHazardDO hazard = id == null ? null : hazardMapper.selectById(id);
        if (hazard == null) {
            throw exception(SAFETY_HAZARD_NOT_EXISTS);
        }
        return hazard;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyInspectionService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,223 @@
package cn.iocoder.yudao.module.mes.service.safety;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
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.inspection.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyInspectionDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyInspectionPointDO;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.SafetyInspectionMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.SafetyInspectionPointMapper;
import cn.iocoder.yudao.module.mes.enums.safety.SafetyInspectCycleEnum;
import cn.iocoder.yudao.module.mes.enums.safety.SafetyInspectionPointTypeEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.SafetyErrorCodeConstants.*;
/**
 * å®‰å…¨ç”Ÿäº§ - å·¡æ£€ç‚¹ä½ä¸Žå·¡æ£€è®°å½• Service
 *
 * <p>覆盖高压配电室、机组区等日常安全巡检。巡检发现异常时可直接转隐患上报。</p>
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class SafetyInspectionService {
    @Resource
    private SafetyInspectionPointMapper pointMapper;
    @Resource
    private SafetyInspectionMapper inspectionMapper;
    @Resource
    private SafetyRefResolver refResolver;
    // ==================== å·¡æ£€ç‚¹ä½ ====================
    public Long createPoint(SafetyInspectionPointSaveReqVO reqVO) {
        reqVO.setId(null);
        String code = StrUtil.trim(reqVO.getCode());
        if (pointMapper.selectByCode(code) != null) {
            throw exception(SAFETY_POINT_CODE_DUPLICATE);
        }
        SafetyInspectionPointDO point = BeanUtils.toBean(reqVO, SafetyInspectionPointDO.class);
        point.setCode(code);
        point.setName(StrUtil.trim(reqVO.getName()));
        point.setInspectCycle(StrUtil.blankToDefault(reqVO.getInspectCycle(), SafetyInspectCycleEnum.DAILY.getCycle()));
        applyPointRefs(point, reqVO.getWorkshopId(), reqVO.getOwnerId());
        pointMapper.insert(point);
        return point.getId();
    }
    public void updatePoint(SafetyInspectionPointSaveReqVO reqVO) {
        SafetyInspectionPointDO old = validatePointExists(reqVO.getId());
        String code = StrUtil.trim(reqVO.getCode());
        SafetyInspectionPointDO byCode = pointMapper.selectByCode(code);
        if (byCode != null && !byCode.getId().equals(old.getId())) {
            throw exception(SAFETY_POINT_CODE_DUPLICATE);
        }
        SafetyInspectionPointDO point = BeanUtils.toBean(reqVO, SafetyInspectionPointDO.class);
        point.setCode(code);
        point.setName(StrUtil.trim(reqVO.getName()));
        applyPointRefs(point, reqVO.getWorkshopId(), reqVO.getOwnerId());
        pointMapper.updateById(point);
    }
    public void deletePoint(Long id) {
        validatePointExists(id);
        pointMapper.deleteById(id);
    }
    public SafetyInspectionPointDO getPoint(Long id) {
        return validatePointExists(id);
    }
    public PageResult<SafetyInspectionPointDO> getPointPage(SafetyInspectionPointPageReqVO reqVO) {
        return pointMapper.selectPage(reqVO);
    }
    /**
     * å¯ç”¨ä¸­çš„巡检点位列表(移动端巡检选择用)
     */
    public List<SafetyInspectionPointDO> getEnabledPointList() {
        return pointMapper.selectListByStatus(0);
    }
    /**
     * ç‚¹ä½çš„主数据引用:车间 + è´£ä»»äººï¼Œä»Žä¸»æ•°æ®å¸¦å‡ºåç§°è½å¿«ç…§
     */
    private void applyPointRefs(SafetyInspectionPointDO point, Long workshopId, Long ownerId) {
        point.setWorkshopId(workshopId);
        if (workshopId == null) {
            point.setWorkshopName("");
        } else {
            point.setWorkshopName(refResolver.requireWorkshop(workshopId).getName());
        }
        point.setOwnerId(ownerId);
        if (ownerId == null) {
            point.setOwnerName("");
        } else {
            point.setOwnerName(refResolver.requireEmployee(ownerId, SAFETY_EMPLOYEE_REQUIRED).getName());
        }
    }
    /**
     * æ‰¹é‡å›žå¡«ç‚¹ä½çš„车间名与责任人姓名(用主数据最新值覆盖快照)
     */
    public void fillPointRefs(List<SafetyInspectionPointDO> points) {
        if (CollUtil.isEmpty(points)) {
            return;
        }
        var workshopMap = refResolver.workshopMap(points.stream().map(SafetyInspectionPointDO::getWorkshopId).toList());
        var employeeMap = refResolver.employeeMap(points.stream().map(SafetyInspectionPointDO::getOwnerId).toList());
        points.forEach(point -> {
            var workshop = workshopMap.get(point.getWorkshopId());
            if (workshop != null) {
                point.setWorkshopName(workshop.getName());
            }
            var employee = employeeMap.get(point.getOwnerId());
            if (employee != null) {
                point.setOwnerName(employee.getName());
            }
        });
    }
    public static void fillPointDisplay(SafetyInspectionPointDO point, SafetyInspectionPointRespVO respVO) {
        respVO.setPointTypeName(SafetyInspectionPointTypeEnum.getNameByType(point.getPointType()));
        respVO.setInspectCycleName(SafetyInspectCycleEnum.getNameByCycle(point.getInspectCycle()));
    }
    // ==================== å·¡æ£€è®°å½• ====================
    public Long createInspection(SafetyInspectionSaveReqVO reqVO) {
        reqVO.setId(null);
        SafetyInspectionPointDO point = validatePointExists(reqVO.getPointId());
        if (Integer.valueOf(1).equals(point.getStatus())) {
            throw exception(SAFETY_POINT_DISABLED);
        }
        SafetyInspectionDO inspection = buildInspection(reqVO, point);
        inspectionMapper.insert(inspection);
        return inspection.getId();
    }
    public void updateInspection(SafetyInspectionSaveReqVO reqVO) {
        SafetyInspectionDO old = validateInspectionExists(reqVO.getId());
        SafetyInspectionPointDO point = validatePointExists(reqVO.getPointId());
        SafetyInspectionDO inspection = buildInspection(reqVO, point);
        inspection.setId(old.getId());
        inspectionMapper.updateById(inspection);
    }
    public void deleteInspection(Long id) {
        validateInspectionExists(id);
        inspectionMapper.deleteById(id);
    }
    public SafetyInspectionDO getInspection(Long id) {
        return validateInspectionExists(id);
    }
    public PageResult<SafetyInspectionDO> getInspectionPage(SafetyInspectionPageReqVO reqVO) {
        return inspectionMapper.selectPage(reqVO);
    }
    private SafetyInspectionDO buildInspection(SafetyInspectionSaveReqVO reqVO, SafetyInspectionPointDO point) {
        SafetyInspectionDO inspection = BeanUtils.toBean(reqVO, SafetyInspectionDO.class);
        inspection.setPointId(point.getId());
        inspection.setPointName(point.getName());
        inspection.setPointType(point.getPointType());
        inspection.setInspectorId(reqVO.getInspectorId());
        inspection.setInspector(refResolver.requireEmployee(reqVO.getInspectorId(), SAFETY_EMPLOYEE_REQUIRED).getName());
        inspection.setResult(StrUtil.blankToDefault(reqVO.getResult(), "NORMAL"));
        inspection.setInspectTime(reqVO.getInspectTime() == null ? LocalDateTime.now() : reqVO.getInspectTime());
        inspection.setHazardCount(reqVO.getHazardCount() == null ? 0 : reqVO.getHazardCount());
        inspection.setRemark(reqVO.getRemark() == null ? "" : reqVO.getRemark().trim());
        return inspection;
    }
    /**
     * æ‰¹é‡å›žå¡«å·¡æ£€äººæœ€æ–°å§“名
     */
    public void fillInspectorNames(List<SafetyInspectionDO> records) {
        if (CollUtil.isEmpty(records)) {
            return;
        }
        var employeeMap = refResolver.employeeMap(records.stream().map(SafetyInspectionDO::getInspectorId).toList());
        records.forEach(record -> {
            var employee = employeeMap.get(record.getInspectorId());
            if (employee != null) {
                record.setInspector(employee.getName());
            }
        });
    }
    /** æœ€è¿‘一次巡检时间(用于点位列表展示) */
    public LocalDateTime getLastInspectTime(Long pointId) {
        SafetyInspectionDO latest = inspectionMapper.selectLatestByPointId(pointId);
        return latest == null ? null : latest.getInspectTime();
    }
    private SafetyInspectionPointDO validatePointExists(Long id) {
        SafetyInspectionPointDO point = id == null ? null : pointMapper.selectById(id);
        if (point == null) {
            throw exception(SAFETY_POINT_NOT_EXISTS);
        }
        return point;
    }
    private SafetyInspectionDO validateInspectionExists(Long id) {
        SafetyInspectionDO inspection = id == null ? null : inspectionMapper.selectById(id);
        if (inspection == null) {
            throw exception(SAFETY_INSPECTION_NOT_EXISTS);
        }
        return inspection;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyLedgerExportService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,334 @@
package cn.iocoder.yudao.module.mes.service.safety;
import cn.idev.excel.FastExcelFactory;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ledger.SafetyLedgerExportReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.*;
import cn.iocoder.yudao.module.mes.enums.safety.*;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
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.SafetyErrorCodeConstants.SAFETY_LEDGER_DATE_RANGE_INVALID;
/**
 * å®‰å…¨ç”Ÿäº§ - ç”µåŠ›è¡Œä¸šæ ‡å‡†åŒ–å°è´¦å¯¼å‡º Service
 *
 * <p>按「全套」导出时一次生成多 Sheet å·¥ä½œç°¿ï¼Œè¦†ç›–安全标准化检查所需的全部台账:
 * ç»„织架构、特种作业证、巡检记录、隐患台账、作业票、监护记录、物资台账、
 * å‡ºå…¥åº“流水、培训记录、演练记录、事故登记。</p>
 *
 * @author è¶…级管理员
 */
@Service
public class SafetyLedgerExportService {
    @Resource
    private SafetyOrgService orgService;
    @Resource
    private SafetyCertificateService certificateService;
    @Resource
    private SafetyInspectionService inspectionService;
    @Resource
    private SafetyHazardService hazardService;
    @Resource
    private SafetyWorkPermitService permitService;
    @Resource
    private SafetyMaterialService materialService;
    @Resource
    private SafetyTrainingService trainingService;
    /**
     * å¯¼å‡ºæ ‡å‡†åŒ–台账
     */
    public void export(SafetyLedgerExportReqVO reqVO, HttpServletResponse response) throws IOException {
        LocalDate start = reqVO.getStartDate();
        LocalDate end = reqVO.getEndDate();
        if (start != null && end != null && start.isAfter(end)) {
            throw exception(SAFETY_LEDGER_DATE_RANGE_INVALID);
        }
        String type = reqVO.getType() == null ? "ALL" : reqVO.getType();
        List<Sheet> sheets = new ArrayList<>();
        if ("ALL".equals(type) || "CERT".equals(type)) {
            sheets.add(certSheet());
        }
        if ("ALL".equals(type) || "INSPECTION".equals(type)) {
            sheets.add(inspectionSheet(start, end));
        }
        if ("ALL".equals(type) || "HAZARD".equals(type)) {
            sheets.add(hazardSheet(start, end));
        }
        if ("ALL".equals(type) || "PERMIT".equals(type)) {
            sheets.add(permitSheet(start, end));
            sheets.add(monitorSheet());
        }
        if ("ALL".equals(type) || "MATERIAL".equals(type)) {
            sheets.add(materialSheet());
            sheets.add(stockSheet(start, end));
        }
        if ("ALL".equals(type) || "TRAINING".equals(type)) {
            sheets.add(trainingSheet(start, end));
        }
        if ("ALL".equals(type) || "DRILL".equals(type)) {
            sheets.add(drillSheet(start, end));
        }
        if ("ALL".equals(type) || "ACCIDENT".equals(type)) {
            sheets.add(accidentSheet(start, end));
        }
        String filename = switch (type) {
            case "CERT" -> "特种作业证台账.xlsx";
            case "INSPECTION" -> "安全巡检台账.xlsx";
            case "HAZARD" -> "隐患台账.xlsx";
            case "PERMIT" -> "高危作业票台账.xlsx";
            case "MATERIAL" -> "应急物资台账.xlsx";
            case "TRAINING" -> "安全培训台账.xlsx";
            case "DRILL" -> "消防演练台账.xlsx";
            case "ACCIDENT" -> "事故登记台账.xlsx";
            default -> "安全生产标准化台账.xlsx";
        };
        write(response, filename, sheets);
    }
    // ==================== å„ Sheet ====================
    private Sheet certSheet() {
        List<String> headers = List.of("持证人", "工号", "部门", "证件类型", "证件编号", "发证机关",
                "发证日期", "有效期至", "下次复审", "状态", "预警天数", "备注");
        var pageReqVO = new cn.iocoder.yudao.module.mes.controller.admin.safety.vo.cert.SafetyCertificatePageReqVO();
        pageReqVO.setPageNo(1);
        pageReqVO.setPageSize(50_000);
        var certs = certificateService.getCertificatePage(pageReqVO).getList();
        // å¯¼å‡ºæ—¶ç”¨å‘˜å·¥æ¡£æ¡ˆçš„æœ€æ–°å§“名/工号/部门
        certificateService.fillEmployeeInfo(certs);
        List<List<Object>> rows = certs.stream()
                .map(c -> Arrays.<Object>asList(
                        str(c.getEmployeeName()), str(c.getEmployeeNo()), str(c.getDeptName()),
                        SafetyCertificateTypeEnum.getNameByType(c.getCertType()), str(c.getCertNo()),
                        str(c.getIssuer()), date(c.getIssueDate()), date(c.getExpireDate()),
                        date(c.getReviewDate()),
                        SafetyCertificateStatusEnum.getNameByStatus(SafetyCertificateService.resolveStatus(
                                c.getExpireDate(), c.getWarnDays(), c.getStatus())),
                        Objects.toString(c.getWarnDays(), ""), str(c.getRemark())))
                .toList();
        return new Sheet("特种作业证台账", headers, rows);
    }
    private Sheet inspectionSheet(LocalDate start, LocalDate end) {
        List<String> headers = List.of("巡检时间", "点位名称", "点位类型", "巡检人", "结果", "巡检情况", "发现隐患数");
        var reqVO = new cn.iocoder.yudao.module.mes.controller.admin.safety.vo.inspection.SafetyInspectionPageReqVO();
        reqVO.setPageNo(1);
        reqVO.setPageSize(50_000);
        if (start != null && end != null) {
            reqVO.setInspectTime(new java.time.LocalDateTime[]{start.atStartOfDay(), end.atTime(23, 59, 59)});
        }
        var inspections = inspectionService.getInspectionPage(reqVO).getList();
        inspectionService.fillInspectorNames(inspections);
        List<List<Object>> rows = inspections.stream()
                .map(i -> Arrays.<Object>asList(
                        dateTime(i.getInspectTime()), str(i.getPointName()),
                        SafetyInspectionPointTypeEnum.getNameByType(i.getPointType()),
                        str(i.getInspector()),
                        "NORMAL".equals(i.getResult()) ? "正常" : "异常",
                        str(i.getContent()), Objects.toString(i.getHazardCount(), "0")))
                .toList();
        return new Sheet("安全巡检台账", headers, rows);
    }
    private Sheet hazardSheet(LocalDate start, LocalDate end) {
        List<String> headers = List.of("隐患编号", "标题", "等级", "来源", "位置", "上报人", "上报时间",
                "整改责任人", "整改期限", "整改措施", "整改时间", "复查人", "复查时间", "复查结论", "状态", "闭环时间");
        var reqVO = new cn.iocoder.yudao.module.mes.controller.admin.safety.vo.hazard.SafetyHazardPageReqVO();
        reqVO.setPageNo(1);
        reqVO.setPageSize(50_000);
        if (start != null && end != null) {
            reqVO.setReportTime(new java.time.LocalDateTime[]{start.atStartOfDay(), end.atTime(23, 59, 59)});
        }
        var hazards = hazardService.getHazardPage(reqVO).getList();
        hazardService.fillPersonNames(hazards);
        List<List<Object>> rows = hazards.stream()
                .map(h -> Arrays.<Object>asList(
                        str(h.getCode()), str(h.getTitle()),
                        SafetyHazardLevelEnum.getNameByLevel(h.getLevel()),
                        SafetyHazardSourceEnum.getNameBySource(h.getSource()),
                        str(h.getLocation()), str(h.getReporter()), dateTime(h.getReportTime()),
                        str(h.getAssignee()), date(h.getRectifyDeadline()), str(h.getRectifyDesc()),
                        dateTime(h.getRectifyTime()), str(h.getRechecker()), dateTime(h.getRecheckTime()),
                        "PASSED".equals(str(h.getRecheckResult())) ? "通过"
                                : "REJECTED".equals(str(h.getRecheckResult())) ? "不通过" : "",
                        SafetyHazardStatusEnum.getNameByStatus(h.getStatus()), dateTime(h.getCloseTime())))
                .toList();
        return new Sheet("隐患台账", headers, rows);
    }
    private Sheet permitSheet(LocalDate start, LocalDate end) {
        List<String> headers = List.of("作业票编号", "作业类型", "作业名称", "作业区域", "作业地点", "申请人",
                "申请时间", "计划开始", "计划结束", "实际开始", "实际结束", "风险等级", "监护人",
                "审批人", "审批时间", "审批意见", "状态");
        var reqVO = new cn.iocoder.yudao.module.mes.controller.admin.safety.vo.permit.SafetyWorkPermitPageReqVO();
        reqVO.setPageNo(1);
        reqVO.setPageSize(50_000);
        if (start != null && end != null) {
            reqVO.setPlanStartTime(new java.time.LocalDateTime[]{start.atStartOfDay(), end.atTime(23, 59, 59)});
        }
        var permits = permitService.getPermitPage(reqVO).getList();
        permitService.fillPermitRefs(permits);
        List<List<Object>> rows = permits.stream()
                .map(p -> Arrays.<Object>asList(
                        str(p.getCode()), SafetyPermitTypeEnum.getNameByType(p.getPermitType()),
                        str(p.getTitle()), str(p.getWorkshopName()), str(p.getLocation()),
                        str(p.getApplicant()), dateTime(p.getApplyTime()), dateTime(p.getPlanStartTime()),
                        dateTime(p.getPlanEndTime()), dateTime(p.getActualStartTime()), dateTime(p.getActualEndTime()),
                        SafetyRiskLevelEnum.getNameByLevel(p.getRiskLevel()), str(p.getGuardian()),
                        str(p.getApprover()), dateTime(p.getApproveTime()), str(p.getApproveComment()),
                        SafetyPermitStatusEnum.getNameByStatus(p.getStatus())))
                .toList();
        return new Sheet("高危作业票台账", headers, rows);
    }
    private Sheet monitorSheet() {
        List<String> headers = List.of("作业票编号", "监护时间", "监护人", "监护内容", "异常情况");
        var reqVO = new cn.iocoder.yudao.module.mes.controller.admin.safety.vo.permit.SafetyWorkPermitPageReqVO();
        reqVO.setPageNo(1);
        reqVO.setPageSize(5_000);
        List<List<Object>> rows = new ArrayList<>();
        for (SafetyWorkPermitDO permit : permitService.getPermitPage(reqVO).getList()) {
            var monitors = permitService.getMonitorList(permit.getId());
            permitService.fillMonitorGuardians(monitors);
            for (SafetyWorkPermitMonitorDO monitor : monitors) {
                rows.add(Arrays.asList(str(permit.getCode()), dateTime(monitor.getMonitorTime()),
                        str(monitor.getGuardian()), str(monitor.getMonitorContent()), str(monitor.getAbnormal())));
            }
        }
        return new Sheet("作业监护记录", headers, rows);
    }
    private Sheet materialSheet() {
        List<String> headers = List.of("物资编码", "物资名称", "类别", "规格型号", "单位", "库存数量",
                "安全库存", "存放位置", "有效期至", "保管人", "状态");
        var reqVO = new cn.iocoder.yudao.module.mes.controller.admin.safety.vo.material.SafetyMaterialPageReqVO();
        reqVO.setPageNo(1);
        reqVO.setPageSize(50_000);
        var materials = materialService.getMaterialPage(reqVO).getList();
        materialService.fillManagers(materials);
        List<List<Object>> rows = materials.stream()
                .map(m -> Arrays.<Object>asList(
                        str(m.getCode()), str(m.getName()),
                        SafetyMaterialCategoryEnum.getNameByCategory(m.getCategory()),
                        str(m.getSpec()), str(m.getUnit()), number(m.getStockQty()), number(m.getSafeStock()),
                        str(m.getStorageLocation()), date(m.getExpireDate()), str(m.getManager()),
                        Integer.valueOf(0).equals(m.getStatus()) ? "启用" : "停用"))
                .toList();
        return new Sheet("应急物资台账", headers, rows);
    }
    private Sheet stockSheet(LocalDate start, LocalDate end) {
        List<String> headers = List.of("业务日期", "物资名称", "类别", "业务类型", "数量",
                "操作前库存", "操作后库存", "经办人", "领用人", "用途/去向");
        var reqVO = new cn.iocoder.yudao.module.mes.controller.admin.safety.vo.material.SafetyMaterialStockPageReqVO();
        reqVO.setPageNo(1);
        reqVO.setPageSize(50_000);
        if (start != null && end != null) {
            reqVO.setBizDate(new LocalDate[]{start, end});
        }
        List<List<Object>> rows = materialService.getStockPage(reqVO).getList().stream()
                .map(s -> Arrays.<Object>asList(
                        date(s.getBizDate()), str(s.getMaterialName()),
                        SafetyMaterialCategoryEnum.getNameByCategory(s.getCategory()),
                        "IN".equals(s.getBizType()) ? "入库" : "出库",
                        number(s.getQuantity()), number(s.getStockBefore()), number(s.getStockAfter()),
                        str(s.getOperator()), str(s.getReceiver()), str(s.getPurpose())))
                .toList();
        return new Sheet("物资出入库流水", headers, rows);
    }
    private Sheet trainingSheet(LocalDate start, LocalDate end) {
        List<String> headers = List.of("培训日期", "培训主题", "培训类型", "学时", "讲师/组织人",
                "培训地点", "参加人数", "培训内容", "考核情况");
        var trainings = trainingService.listTrainingBetween(start, end);
        trainingService.fillTrainers(trainings);
        List<List<Object>> rows = trainings.stream()
                .map(t -> Arrays.<Object>asList(
                        date(t.getTrainDate()), str(t.getTitle()),
                        SafetyTrainingTypeEnum.getNameByType(t.getTrainingType()),
                        number(t.getDurationHours()), str(t.getTrainer()), str(t.getLocation()),
                        Objects.toString(t.getParticipantCount(), "0"),
                        str(t.getContent()), str(t.getExamResult())))
                .toList();
        return new Sheet("安全培训台账", headers, rows);
    }
    private Sheet drillSheet(LocalDate start, LocalDate end) {
        List<String> headers = List.of("演练日期", "演练名称", "演练类型", "地点", "组织人",
                "参加人数", "时长(分钟)", "演练过程", "总结与改进");
        var drills = trainingService.listDrillBetween(start, end);
        trainingService.fillOrganizers(drills);
        List<List<Object>> rows = drills.stream()
                .map(d -> Arrays.<Object>asList(
                        date(d.getDrillDate()), str(d.getTitle()),
                        SafetyDrillTypeEnum.getNameByType(d.getDrillType()), str(d.getLocation()),
                        str(d.getOrganizer()), Objects.toString(d.getParticipantCount(), "0"),
                        Objects.toString(d.getDurationMinutes(), ""),
                        str(d.getProcessDesc()), str(d.getSummary())))
                .toList();
        return new Sheet("消防演练台账", headers, rows);
    }
    private Sheet accidentSheet(LocalDate start, LocalDate end) {
        List<String> headers = List.of("事故编号", "事故名称", "事故类型", "等级", "发生时间", "发生地点",
                "伤亡情况", "经济损失(元)", "事故经过", "原因分析", "处理措施", "状态");
        var accidents = trainingService.listAccidentBetween(start, end);
        trainingService.fillAccidentRefs(accidents);
        List<List<Object>> rows = accidents.stream()
                .map(a -> Arrays.<Object>asList(
                        str(a.getCode()), str(a.getTitle()),
                        SafetyAccidentTypeEnum.getNameByType(a.getAccidentType()),
                        SafetyAccidentLevelEnum.getNameByLevel(a.getLevel()),
                        dateTime(a.getOccurTime()), str(a.getLocation()), str(a.getCasualtyDesc()),
                        number(a.getEconomicLoss()), str(a.getDescription()), str(a.getCauseAnalysis()),
                        str(a.getMeasures()), SafetyAccidentStatusEnum.getNameByStatus(a.getStatus())))
                .toList();
        return new Sheet("事故登记台账", headers, rows);
    }
    // ==================== å·¥å…·æ–¹æ³• ====================
    private static String str(String value) {
        return value == null ? "" : value;
    }
    private static String date(LocalDate value) {
        return value == null ? "" : value.toString();
    }
    private static String dateTime(java.time.LocalDateTime value) {
        return value == null ? "" : value.toString().replace('T', ' ');
    }
    private static String number(java.math.BigDecimal value) {
        return value == null ? "" : value.stripTrailingZeros().toPlainString();
    }
    private record Sheet(String name, List<String> headers, List<List<Object>> rows) {
    }
    private void write(HttpServletResponse response, String filename, List<Sheet> sheets) throws IOException {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
        try (var writer = FastExcelFactory.write(response.getOutputStream()).autoCloseStream(false).build()) {
            for (int i = 0; i < sheets.size(); i++) {
                Sheet sheet = sheets.get(i);
                writer.write(sheet.rows(), FastExcelFactory.writerSheet(i, sheet.name())
                        .head(sheet.headers().stream().map(List::of).toList()).build());
            }
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyMaterialService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,192 @@
package cn.iocoder.yudao.module.mes.service.safety;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
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.material.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyMaterialDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyMaterialStockDO;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.SafetyMaterialMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.SafetyMaterialStockMapper;
import cn.iocoder.yudao.module.mes.enums.safety.SafetyMaterialCategoryEnum;
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.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.SafetyErrorCodeConstants.*;
/**
 * å®‰å…¨ç”Ÿäº§ - åº”急物资台账与出入库 Service
 *
 * <p>覆盖电力绝缘用具、危化品、消防物资。出库时行锁校验库存,避免并发扣减为负。</p>
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class SafetyMaterialService {
    @Resource
    private SafetyMaterialMapper materialMapper;
    @Resource
    private SafetyRefResolver refResolver;
    @Resource
    private SafetyMaterialStockMapper stockMapper;
    // ==================== ç‰©èµ„台账 ====================
    public Long createMaterial(SafetyMaterialSaveReqVO reqVO) {
        reqVO.setId(null);
        String code = StrUtil.trim(reqVO.getCode());
        if (materialMapper.selectByCode(code) != null) {
            throw exception(SAFETY_MATERIAL_CODE_DUPLICATE);
        }
        SafetyMaterialDO material = BeanUtils.toBean(reqVO, SafetyMaterialDO.class);
        material.setCode(code);
        material.setName(StrUtil.trim(reqVO.getName()));
        material.setUnit(StrUtil.blankToDefault(reqVO.getUnit(), "个"));
        applyManager(material, reqVO.getManagerId());
        material.setStockQty(reqVO.getStockQty() == null ? BigDecimal.ZERO : reqVO.getStockQty());
        material.setSafeStock(reqVO.getSafeStock() == null ? BigDecimal.ZERO : reqVO.getSafeStock());
        materialMapper.insert(material);
        return material.getId();
    }
    public void updateMaterial(SafetyMaterialSaveReqVO reqVO) {
        SafetyMaterialDO old = validateMaterialExists(reqVO.getId());
        String code = StrUtil.trim(reqVO.getCode());
        SafetyMaterialDO byCode = materialMapper.selectByCode(code);
        if (byCode != null && !byCode.getId().equals(old.getId())) {
            throw exception(SAFETY_MATERIAL_CODE_DUPLICATE);
        }
        SafetyMaterialDO material = BeanUtils.toBean(reqVO, SafetyMaterialDO.class);
        material.setCode(code);
        material.setName(StrUtil.trim(reqVO.getName()));
        applyManager(material, reqVO.getManagerId());
        // åº“存数量只能通过出入库单变动,禁止直接编辑
        material.setStockQty(null);
        materialMapper.updateById(material);
    }
    public void deleteMaterial(Long id) {
        validateMaterialExists(id);
        materialMapper.deleteById(id);
    }
    public SafetyMaterialDO getMaterial(Long id) {
        return validateMaterialExists(id);
    }
    public PageResult<SafetyMaterialDO> getMaterialPage(SafetyMaterialPageReqVO reqVO) {
        return materialMapper.selectPage(reqVO);
    }
    /**
     * ä¿ç®¡äººï¼šä»Žå‘˜å·¥æ¡£æ¡ˆå¸¦å‡ºå§“名落快照
     */
    private void applyManager(SafetyMaterialDO material, Long managerId) {
        material.setManagerId(managerId);
        material.setManager(managerId == null ? ""
                : refResolver.requireEmployee(managerId, SAFETY_EMPLOYEE_REQUIRED).getName());
    }
    /**
     * æ‰¹é‡å›žå¡«ä¿ç®¡äººæœ€æ–°å§“名
     */
    public void fillManagers(List<SafetyMaterialDO> materials) {
        if (CollUtil.isEmpty(materials)) {
            return;
        }
        var employeeMap = refResolver.employeeMap(materials.stream().map(SafetyMaterialDO::getManagerId).toList());
        materials.forEach(material -> {
            var employee = employeeMap.get(material.getManagerId());
            if (employee != null) material.setManager(employee.getName());
        });
    }
    public static void fillMaterialDisplay(SafetyMaterialDO material, SafetyMaterialRespVO respVO) {
        respVO.setCategoryName(SafetyMaterialCategoryEnum.getNameByCategory(material.getCategory()));
        BigDecimal stock = material.getStockQty() == null ? BigDecimal.ZERO : material.getStockQty();
        BigDecimal safe = material.getSafeStock() == null ? BigDecimal.ZERO : material.getSafeStock();
        respVO.setBelowSafeStock(safe.signum() > 0 && stock.compareTo(safe) < 0);
        if (material.getExpireDate() != null) {
            respVO.setExpireRemainingDays(ChronoUnit.DAYS.between(LocalDate.now(), material.getExpireDate()));
        }
    }
    // ==================== å‡ºå…¥åº“ ====================
    /**
     * å‡ºå…¥åº“登记:行锁读取库存 â†’ è®¡ç®—变动 â†’ å†™æµæ°´ â†’ å›žå†™åº“å­˜
     */
    @Transactional(rollbackFor = Exception.class)
    public Long createStock(SafetyMaterialStockSaveReqVO reqVO) {
        SafetyMaterialDO material = materialMapper.selectByIdForUpdate(reqVO.getMaterialId());
        if (material == null) {
            throw exception(SAFETY_MATERIAL_NOT_EXISTS);
        }
        BigDecimal before = material.getStockQty() == null ? BigDecimal.ZERO : material.getStockQty();
        boolean inbound = "IN".equals(reqVO.getBizType());
        BigDecimal after = inbound ? before.add(reqVO.getQuantity()) : before.subtract(reqVO.getQuantity());
        if (!inbound && after.signum() < 0) {
            throw exception(SAFETY_MATERIAL_STOCK_NOT_ENOUGH);
        }
        SafetyMaterialStockDO stock = BeanUtils.toBean(reqVO, SafetyMaterialStockDO.class);
        stock.setId(null);
        stock.setMaterialName(material.getName());
        stock.setCategory(material.getCategory());
        stock.setQuantity(reqVO.getQuantity());
        stock.setStockBefore(before);
        stock.setStockAfter(after);
        stock.setBizDate(reqVO.getBizDate() == null ? LocalDate.now() : reqVO.getBizDate());
        stock.setOperator(StrUtil.trim(reqVO.getOperator()));
        stockMapper.insert(stock);
        SafetyMaterialDO update = new SafetyMaterialDO();
        update.setId(material.getId());
        update.setStockQty(after);
        materialMapper.updateById(update);
        return stock.getId();
    }
    public PageResult<SafetyMaterialStockDO> getStockPage(SafetyMaterialStockPageReqVO reqVO) {
        return stockMapper.selectPage(reqVO);
    }
    public static void fillStockDisplay(SafetyMaterialStockDO stock, SafetyMaterialStockRespVO respVO) {
        respVO.setCategoryName(SafetyMaterialCategoryEnum.getNameByCategory(stock.getCategory()));
        respVO.setBizTypeName("IN".equals(stock.getBizType()) ? "入库" : "出库");
    }
    /**
     * é¢„警清单:低于安全库存的物资
     */
    public List<SafetyMaterialDO> getBelowSafeStockList() {
        return materialMapper.selectBelowSafeStock();
    }
    /**
     * é¢„警清单:有效期临近(默认 30 å¤©å†…)的物资
     */
    public List<SafetyMaterialDO> getExpiringList(Integer days) {
        int warnDays = days == null || days <= 0 ? 30 : days;
        return materialMapper.selectExpiringBefore(LocalDate.now().plusDays(warnDays));
    }
    private SafetyMaterialDO validateMaterialExists(Long id) {
        SafetyMaterialDO material = id == null ? null : materialMapper.selectById(id);
        if (material == null) {
            throw exception(SAFETY_MATERIAL_NOT_EXISTS);
        }
        return material;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyOrgService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,157 @@
package cn.iocoder.yudao.module.mes.service.safety;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.org.SafetyOrgRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.org.SafetyOrgSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyOrgDO;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.SafetyOrgMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.SafetyErrorCodeConstants.*;
/**
 * å®‰å…¨ç”Ÿäº§ - å®‰å…¨ç»„织架构 Service
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class SafetyOrgService {
    @Resource
    private SafetyOrgMapper safetyOrgMapper;
    @Resource
    private SafetyRefResolver refResolver;
    public Long createOrg(SafetyOrgSaveReqVO reqVO) {
        reqVO.setId(null);
        validateParent(reqVO.getParentId(), null);
        SafetyOrgDO org = BeanUtils.toBean(reqVO, SafetyOrgDO.class);
        org.setParentId(reqVO.getParentId() == null ? 0L : reqVO.getParentId());
        applyLeader(org, reqVO.getLeaderId());
        safetyOrgMapper.insert(org);
        return org.getId();
    }
    public void updateOrg(SafetyOrgSaveReqVO reqVO) {
        validateOrgExists(reqVO.getId());
        validateParent(reqVO.getParentId(), reqVO.getId());
        SafetyOrgDO org = BeanUtils.toBean(reqVO, SafetyOrgDO.class);
        org.setParentId(reqVO.getParentId() == null ? 0L : reqVO.getParentId());
        applyLeader(org, reqVO.getLeaderId());
        safetyOrgMapper.updateById(org);
    }
    /**
     * è´Ÿè´£äººï¼šä»Žå‘˜å·¥æ¡£æ¡ˆå¸¦å‡ºå§“名与电话并落快照
     */
    private void applyLeader(SafetyOrgDO org, Long leaderId) {
        org.setLeaderId(leaderId);
        if (leaderId == null) {
            org.setLeaderName("");
            return;
        }
        var employee = refResolver.requireEmployee(leaderId, SAFETY_EMPLOYEE_REQUIRED);
        org.setLeaderName(employee.getName());
        org.setLeaderPhone(employee.getPhone());
    }
    public void deleteOrg(Long id) {
        validateOrgExists(id);
        if (safetyOrgMapper.selectCountByParentId(id) > 0) {
            throw exception(SAFETY_ORG_HAS_CHILDREN);
        }
        safetyOrgMapper.deleteById(id);
    }
    public SafetyOrgDO getOrg(Long id) {
        return validateOrgExists(id);
    }
    /**
     * å–负责人最新档案(供单条查询回填用)
     */
    public cn.iocoder.yudao.module.hrm.api.employee.dto.HrmEmployeeSimpleDTO getLeader(Long leaderId) {
        return refResolver.findEmployee(leaderId);
    }
    /**
     * æŸ¥è¯¢ç»„织树
     */
    public List<SafetyOrgRespVO> getOrgTree() {
        List<SafetyOrgDO> all = safetyOrgMapper.selectListOrdered();
        List<SafetyOrgRespVO> nodes = BeanUtils.toBean(all, SafetyOrgRespVO.class);
        // è´Ÿè´£äººç”¨å‘˜å·¥æ¡£æ¡ˆæœ€æ–°å§“名覆盖快照,员工改名/调岗后树也同步
        var employeeMap = refResolver.employeeMap(
                all.stream().map(SafetyOrgDO::getLeaderId).toList());
        nodes.forEach(node -> {
            var employee = employeeMap.get(node.getLeaderId());
            if (employee != null) {
                node.setLeaderName(employee.getName());
            }
        });
        Map<Long, List<SafetyOrgRespVO>> grouped = nodes.stream()
                .collect(Collectors.groupingBy(SafetyOrgRespVO::getParentId, Collectors.toList()));
        nodes.forEach(node -> node.setChildren(grouped.getOrDefault(node.getId(), new ArrayList<>())));
        List<SafetyOrgRespVO> roots = grouped.getOrDefault(0L, new ArrayList<>());
        if (CollUtil.isEmpty(roots)) {
            // å…¼å®¹ parentId ä¸ºç©ºçš„脏数据
            roots = nodes.stream().filter(n -> n.getParentId() == null || n.getParentId() == 0L).toList();
        }
        return roots;
    }
    private void validateParent(Long parentId, Long selfId) {
        if (parentId == null || parentId == 0L) {
            return;
        }
        if (parentId.equals(selfId)) {
            throw exception(SAFETY_ORG_PARENT_IS_SELF);
        }
        SafetyOrgDO parent = safetyOrgMapper.selectById(parentId);
        if (parent == null) {
            throw exception(SAFETY_ORG_PARENT_NOT_EXISTS);
        }
        // é˜²æ­¢æŠŠçˆ¶èŠ‚ç‚¹æŒ‚åˆ°è‡ªå·±çš„å­å­™ä¸‹ï¼Œå½¢æˆçŽ¯
        if (selfId != null && isDescendant(parentId, selfId)) {
            throw exception(SAFETY_ORG_PARENT_IS_CHILD);
        }
    }
    private boolean isDescendant(Long candidateId, Long ancestorId) {
        Long current = candidateId;
        int guard = 0;
        while (current != null && current != 0L && guard++ < 100) {
            if (current.equals(ancestorId)) {
                return true;
            }
            SafetyOrgDO node = safetyOrgMapper.selectById(current);
            if (node == null) {
                return false;
            }
            current = node.getParentId();
        }
        return false;
    }
    private SafetyOrgDO validateOrgExists(Long id) {
        if (id == null) {
            throw exception(SAFETY_ORG_NOT_EXISTS);
        }
        SafetyOrgDO org = safetyOrgMapper.selectById(id);
        if (org == null) {
            throw exception(SAFETY_ORG_NOT_EXISTS);
        }
        return org;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyOverviewService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,138 @@
package cn.iocoder.yudao.module.mes.service.safety;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.*;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.*;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.ledger.SafetyOverviewRespVO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
 * å®‰å…¨ç”Ÿäº§ - æ€»è§ˆç»Ÿè®¡ Service
 *
 * <p>提供首页 KPI ä¸Žè¶‹åŠ¿æ•°æ®ï¼Œå…¨éƒ¨åŸºäºŽçŽ°æœ‰å°è´¦å®žæ—¶è®¡ç®—ï¼Œä¸åšå†—ä½™ç»Ÿè®¡è¡¨ã€‚</p>
 *
 * @author è¶…级管理员
 */
@Service
public class SafetyOverviewService {
    private static final DateTimeFormatter MONTH_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM");
    /** è¶‹åŠ¿å±•ç¤ºæœˆæ•° */
    private static final int TREND_MONTHS = 6;
    @Resource
    private SafetyCertificateMapper certificateMapper;
    @Resource
    private SafetyHazardMapper hazardMapper;
    @Resource
    private SafetyWorkPermitMapper permitMapper;
    @Resource
    private SafetyMaterialMapper materialMapper;
    @Resource
    private SafetyTrainingMapper trainingMapper;
    @Resource
    private SafetyDrillMapper drillMapper;
    @Resource
    private SafetyAccidentMapper accidentMapper;
    public SafetyOverviewRespVO getOverview() {
        SafetyOverviewRespVO overview = new SafetyOverviewRespVO();
        LocalDate today = LocalDate.now();
        // ---------- ç‰¹ç§ä½œä¸šè¯ ----------
        List<SafetyCertificateDO> certs = certificateMapper.selectList();
        long expiring = 0;
        long expired = 0;
        for (SafetyCertificateDO cert : certs) {
            String status = SafetyCertificateService.resolveStatus(
                    cert.getExpireDate(), cert.getWarnDays(), cert.getStatus());
            if ("EXPIRED".equals(status)) {
                expired++;
            } else if ("EXPIRING".equals(status)) {
                expiring++;
            }
        }
        overview.setCertTotal((long) certs.size());
        overview.setCertExpiring(expiring);
        overview.setCertExpired(expired);
        // ---------- é𐿂£ ----------
        List<SafetyHazardDO> hazards = hazardMapper.selectList();
        long hazardOpen = hazards.stream()
                .filter(h -> !"CLOSED".equals(h.getStatus())).count();
        long hazardOverdue = hazardMapper.selectOverdue(today).size();
        long hazardClosed = hazards.size() - hazardOpen;
        overview.setHazardTotal((long) hazards.size());
        overview.setHazardOpen(hazardOpen);
        overview.setHazardOverdue(hazardOverdue);
        overview.setHazardCloseRate(hazards.isEmpty() ? 0
                : (int) Math.round(hazardClosed * 100.0 / hazards.size()));
        // ---------- ä½œä¸šç¥¨ ----------
        List<SafetyWorkPermitDO> permits = permitMapper.selectList();
        overview.setPermitPending(permits.stream()
                .filter(p -> "PENDING".equals(p.getStatus())).count());
        overview.setPermitInProgress(permits.stream()
                .filter(p -> "IN_PROGRESS".equals(p.getStatus())).count());
        // ---------- ç‰©èµ„ ----------
        overview.setMaterialBelowSafeStock((long) materialMapper.selectBelowSafeStock().size());
        overview.setMaterialExpiring((long) materialMapper.selectExpiringBefore(today.plusDays(30)).size());
        // ---------- æœ¬æœˆæ´»åЍ ----------
        YearMonth currentMonth = YearMonth.from(today);
        LocalDate monthStart = currentMonth.atDay(1);
        LocalDate monthEnd = currentMonth.atEndOfMonth();
        overview.setTrainingThisMonth((long) trainingMapper.selectListBetween(monthStart, monthEnd).size());
        overview.setDrillThisMonth((long) drillMapper.selectListBetween(monthStart, monthEnd).size());
        overview.setAccidentThisMonth((long) accidentMapper.selectListBetween(
                monthStart.atStartOfDay(), monthEnd.atTime(23, 59, 59)).size());
        // ---------- è¶‹åŠ¿ï¼ˆè¿‘ 6 ä¸ªæœˆï¼‰ ----------
        overview.setHazardTrend(buildTrend(hazards.stream().map(SafetyHazardDO::getReportTime).toList()));
        overview.setAccidentTrend(buildTrend(hazards.isEmpty() ? List.of()
                : accidentMapper.selectList().stream().map(SafetyAccidentDO::getOccurTime).toList()));
        return overview;
    }
    /**
     * æŒ‰æœˆèšåˆæ—¶é—´ç‚¹ï¼Œè¾“出近 N ä¸ªæœˆçš„连续序列(无数据的月份补 0)
     */
    private List<SafetyOverviewRespVO.MonthlyCount> buildTrend(List<LocalDateTime> times) {
        Map<String, Long> counts = new LinkedHashMap<>();
        YearMonth current = YearMonth.from(LocalDate.now());
        List<YearMonth> months = new ArrayList<>();
        for (int i = TREND_MONTHS - 1; i >= 0; i--) {
            YearMonth month = current.minusMonths(i);
            months.add(month);
            counts.put(month.format(MONTH_FORMAT), 0L);
        }
        for (LocalDateTime time : times) {
            if (time == null) {
                continue;
            }
            String key = YearMonth.from(time).format(MONTH_FORMAT);
            if (counts.containsKey(key)) {
                counts.put(key, counts.get(key) + 1);
            }
        }
        return months.stream().map(month -> {
            SafetyOverviewRespVO.MonthlyCount item = new SafetyOverviewRespVO.MonthlyCount();
            item.setMonth(month.format(MONTH_FORMAT));
            item.setCount(counts.get(month.format(MONTH_FORMAT)));
            return item;
        }).toList();
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyRefResolver.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,122 @@
package cn.iocoder.yudao.module.mes.service.safety;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.hrm.api.employee.HrmEmployeeApi;
import cn.iocoder.yudao.module.hrm.api.employee.dto.HrmEmployeeSimpleDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkshopDO;
import cn.iocoder.yudao.module.mes.dal.mysql.md.workstation.MesMdWorkshopMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.SafetyErrorCodeConstants.*;
/**
 * å®‰å…¨ç”Ÿäº§ - ä¸»æ•°æ®å¼•用解析器
 *
 * <p>模块内所有「人员」与「车间」字段统一走这里解析,避免每个 Service é‡å¤å†™ä¸€éã€‚</p>
 *
 * <p>统一约定:业务表存 {@code xxxId} å…³è”主数据,同时保留 {@code xxxName} ä½œä¸ºå¿«ç…§ã€‚
 * ä¿å­˜æ—¶ä»Žä¸»æ•°æ®æŠŠåç§°å¸¦å‡ºå†™å¿«ç…§ï¼›æŸ¥è¯¢æ—¶ç”¨ä¸»æ•°æ®æœ€æ–°å€¼è¦†ç›–快照,
 * è¿™æ ·å‘˜å·¥è°ƒå²—、车间改名后台账会自动跟着变,而主数据被删时老记录仍能靠快照展示。</p>
 *
 * @author è¶…级管理员
 */
@Component
public class SafetyRefResolver {
    @Resource
    private HrmEmployeeApi hrmEmployeeApi;
    @Resource
    private MesMdWorkshopMapper workshopMapper;
    // ==================== å‘˜å·¥ ====================
    /**
     * æŒ‰å‘˜å·¥ç¼–号取员工,必须存在
     *
     * @throws cn.iocoder.yudao.framework.common.exception.ServiceException å‘˜å·¥ä¸å­˜åœ¨æ—¶æŠ›å‡º
     */
    public HrmEmployeeSimpleDTO requireEmployee(Long employeeId, cn.iocoder.yudao.framework.common.exception.ErrorCode errorCode) {
        if (employeeId == null) {
            throw exception(errorCode);
        }
        HrmEmployeeSimpleDTO employee = hrmEmployeeApi.getEmployee(employeeId);
        if (employee == null) {
            throw exception(errorCode);
        }
        return employee;
    }
    /**
     * å¯ç©ºçš„员工解析:id ä¸ºç©ºè¿”回 null,不校验存在性(用于非必填字段)
     */
    public HrmEmployeeSimpleDTO findEmployee(Long employeeId) {
        return employeeId == null ? null : hrmEmployeeApi.getEmployee(employeeId);
    }
    /**
     * æ‰¹é‡å–员工,返回 id -> å‘˜å·¥ çš„ Map
     */
    public Map<Long, HrmEmployeeSimpleDTO> employeeMap(Collection<Long> employeeIds) {
        // æ³¨æ„ï¼šè¿™é‡Œå¿…须返回可接受 null key çš„空 Map。
        // è°ƒç”¨æ–¹ä¼šç›´æŽ¥ map.get(记录里的 xxxId),而 xxxId å…è®¸ä¸º null(例如组织未设负责人、
        // è¯ä»¶æœªå…³è”持证人),Map.of() çš„ get(null) ä¼šæŠ› NPE,导致列表接口 500。
        if (CollUtil.isEmpty(employeeIds)) {
            return new HashMap<>();
        }
        List<Long> ids = employeeIds.stream().filter(Objects::nonNull).distinct().toList();
        if (ids.isEmpty()) {
            return new HashMap<>();
        }
        return hrmEmployeeApi.getEmployeeList(ids).stream()
                .collect(Collectors.toMap(HrmEmployeeSimpleDTO::getId, e -> e, (a, b) -> a));
    }
    // ==================== è½¦é—´ ====================
    /**
     * æŒ‰è½¦é—´ç¼–号取车间,必须存在
     */
    public MesMdWorkshopDO requireWorkshop(Long workshopId) {
        if (workshopId == null) {
            throw exception(SAFETY_WORKSHOP_NOT_EXISTS);
        }
        MesMdWorkshopDO workshop = workshopMapper.selectById(workshopId);
        if (workshop == null) {
            throw exception(SAFETY_WORKSHOP_NOT_EXISTS);
        }
        return workshop;
    }
    /**
     * å¯ç©ºçš„车间解析
     */
    public MesMdWorkshopDO findWorkshop(Long workshopId) {
        return workshopId == null ? null : workshopMapper.selectById(workshopId);
    }
    /**
     * æ‰¹é‡å–车间,返回 id -> è½¦é—´ çš„ Map
     */
    public Map<Long, MesMdWorkshopDO> workshopMap(Collection<Long> workshopIds) {
        // åŒä¸Šï¼šè½¦é—´ç¼–号允许为 null,空集合必须返回可接受 null key çš„ Map
        if (CollUtil.isEmpty(workshopIds)) {
            return new HashMap<>();
        }
        List<Long> ids = workshopIds.stream().filter(Objects::nonNull).distinct().toList();
        if (ids.isEmpty()) {
            return new HashMap<>();
        }
        return workshopMapper.selectBatchIds(ids).stream()
                .collect(Collectors.toMap(MesMdWorkshopDO::getId, w -> w, (a, b) -> a));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyTrainingService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,273 @@
package cn.iocoder.yudao.module.mes.service.safety;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
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.train.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyAccidentDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyDrillDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyTrainingDO;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.SafetyAccidentMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.SafetyDrillMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.SafetyTrainingMapper;
import cn.iocoder.yudao.module.mes.enums.safety.*;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.SafetyErrorCodeConstants.*;
/**
 * å®‰å…¨ç”Ÿäº§ - åŸ¹è®­ã€æ¼”练、事故登记 Service
 *
 * <p>三类记录统一归档,供标准化台账导出使用。</p>
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class SafetyTrainingService {
    private static final DateTimeFormatter CODE_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
    @Resource
    private SafetyTrainingMapper trainingMapper;
    @Resource
    private SafetyRefResolver refResolver;
    @Resource
    private SafetyDrillMapper drillMapper;
    @Resource
    private SafetyAccidentMapper accidentMapper;
    // ==================== åŸ¹è®­ ====================
    public Long createTraining(SafetyTrainingSaveReqVO reqVO) {
        reqVO.setId(null);
        SafetyTrainingDO training = BeanUtils.toBean(reqVO, SafetyTrainingDO.class);
        training.setTitle(StrUtil.trim(reqVO.getTitle()));
        training.setTrainingType(StrUtil.blankToDefault(reqVO.getTrainingType(), SafetyTrainingTypeEnum.SAFETY.getType()));
        training.setTrainerId(reqVO.getTrainerId());
        training.setTrainer(reqVO.getTrainerId() == null ? ""
                : refResolver.requireEmployee(reqVO.getTrainerId(), SAFETY_EMPLOYEE_REQUIRED).getName());
        training.setParticipantCount(resolveCount(reqVO.getParticipantCount(), reqVO.getParticipants()));
        training.setRemark(reqVO.getRemark() == null ? "" : reqVO.getRemark().trim());
        trainingMapper.insert(training);
        return training.getId();
    }
    public void updateTraining(SafetyTrainingSaveReqVO reqVO) {
        validateTrainingExists(reqVO.getId());
        SafetyTrainingDO training = BeanUtils.toBean(reqVO, SafetyTrainingDO.class);
        training.setTitle(StrUtil.trim(reqVO.getTitle()));
        training.setParticipantCount(resolveCount(reqVO.getParticipantCount(), reqVO.getParticipants()));
        trainingMapper.updateById(training);
    }
    public void deleteTraining(Long id) {
        validateTrainingExists(id);
        trainingMapper.deleteById(id);
    }
    public SafetyTrainingDO getTraining(Long id) {
        return validateTrainingExists(id);
    }
    public PageResult<SafetyTrainingDO> getTrainingPage(SafetyTrainingPageReqVO reqVO) {
        return trainingMapper.selectPage(reqVO);
    }
    /**
     * äº‹æ•…的主数据引用:所属车间 + ç™»è®°äºº
     */
    private void applyAccidentRefs(SafetyAccidentDO accident, SafetyAccidentSaveReqVO reqVO) {
        accident.setWorkshopId(reqVO.getWorkshopId());
        accident.setWorkshopName(reqVO.getWorkshopId() == null ? ""
                : refResolver.requireWorkshop(reqVO.getWorkshopId()).getName());
        accident.setReporterId(reqVO.getReporterId());
        accident.setReporter(reqVO.getReporterId() == null ? ""
                : refResolver.requireEmployee(reqVO.getReporterId(), SAFETY_EMPLOYEE_REQUIRED).getName());
    }
    /** æ‰¹é‡å›žå¡«åŸ¹è®­è®²å¸ˆæœ€æ–°å§“名 */
    public void fillTrainers(List<SafetyTrainingDO> trainings) {
        if (CollUtil.isEmpty(trainings)) return;
        var employeeMap = refResolver.employeeMap(trainings.stream().map(SafetyTrainingDO::getTrainerId).toList());
        trainings.forEach(t -> {
            var e = employeeMap.get(t.getTrainerId());
            if (e != null) t.setTrainer(e.getName());
        });
    }
    /** æ‰¹é‡å›žå¡«æ¼”练组织人最新姓名 */
    public void fillOrganizers(List<SafetyDrillDO> drills) {
        if (CollUtil.isEmpty(drills)) return;
        var employeeMap = refResolver.employeeMap(drills.stream().map(SafetyDrillDO::getOrganizerId).toList());
        drills.forEach(d -> {
            var e = employeeMap.get(d.getOrganizerId());
            if (e != null) d.setOrganizer(e.getName());
        });
    }
    /** æ‰¹é‡å›žå¡«äº‹æ•…的车间名与登记人姓名 */
    public void fillAccidentRefs(List<SafetyAccidentDO> accidents) {
        if (CollUtil.isEmpty(accidents)) return;
        var workshopMap = refResolver.workshopMap(accidents.stream().map(SafetyAccidentDO::getWorkshopId).toList());
        var employeeMap = refResolver.employeeMap(accidents.stream().map(SafetyAccidentDO::getReporterId).toList());
        accidents.forEach(a -> {
            var w = workshopMap.get(a.getWorkshopId());
            if (w != null) a.setWorkshopName(w.getName());
            var e = employeeMap.get(a.getReporterId());
            if (e != null) a.setReporter(e.getName());
        });
    }
    public static void fillTrainingDisplay(SafetyTrainingDO training, SafetyTrainingRespVO respVO) {
        respVO.setTrainingTypeName(SafetyTrainingTypeEnum.getNameByType(training.getTrainingType()));
    }
    // ==================== æ¼”练 ====================
    public Long createDrill(SafetyDrillSaveReqVO reqVO) {
        reqVO.setId(null);
        SafetyDrillDO drill = BeanUtils.toBean(reqVO, SafetyDrillDO.class);
        drill.setTitle(StrUtil.trim(reqVO.getTitle()));
        drill.setDrillType(StrUtil.blankToDefault(reqVO.getDrillType(), SafetyDrillTypeEnum.FIRE.getType()));
        drill.setOrganizerId(reqVO.getOrganizerId());
        drill.setOrganizer(reqVO.getOrganizerId() == null ? ""
                : refResolver.requireEmployee(reqVO.getOrganizerId(), SAFETY_EMPLOYEE_REQUIRED).getName());
        drill.setParticipantCount(resolveCount(reqVO.getParticipantCount(), reqVO.getParticipants()));
        drill.setRemark(reqVO.getRemark() == null ? "" : reqVO.getRemark().trim());
        drillMapper.insert(drill);
        return drill.getId();
    }
    public void updateDrill(SafetyDrillSaveReqVO reqVO) {
        validateDrillExists(reqVO.getId());
        SafetyDrillDO drill = BeanUtils.toBean(reqVO, SafetyDrillDO.class);
        drill.setTitle(StrUtil.trim(reqVO.getTitle()));
        drill.setParticipantCount(resolveCount(reqVO.getParticipantCount(), reqVO.getParticipants()));
        drillMapper.updateById(drill);
    }
    public void deleteDrill(Long id) {
        validateDrillExists(id);
        drillMapper.deleteById(id);
    }
    public SafetyDrillDO getDrill(Long id) {
        return validateDrillExists(id);
    }
    public PageResult<SafetyDrillDO> getDrillPage(SafetyDrillPageReqVO reqVO) {
        return drillMapper.selectPage(reqVO);
    }
    public static void fillDrillDisplay(SafetyDrillDO drill, SafetyDrillRespVO respVO) {
        respVO.setDrillTypeName(SafetyDrillTypeEnum.getNameByType(drill.getDrillType()));
    }
    // ==================== äº‹æ•… ====================
    public Long createAccident(SafetyAccidentSaveReqVO reqVO) {
        reqVO.setId(null);
        SafetyAccidentDO accident = BeanUtils.toBean(reqVO, SafetyAccidentDO.class);
        accident.setCode(accidentMapper.selectNextCode("AC" + LocalDate.now().format(CODE_DATE)));
        accident.setTitle(StrUtil.trim(reqVO.getTitle()));
        accident.setLevel(StrUtil.blankToDefault(reqVO.getLevel(), SafetyAccidentLevelEnum.GENERAL.getLevel()));
        accident.setStatus(StrUtil.blankToDefault(reqVO.getStatus(), SafetyAccidentStatusEnum.REPORTED.getStatus()));
        applyAccidentRefs(accident, reqVO);
        accident.setReportTime(reqVO.getReportTime() == null ? LocalDateTime.now() : reqVO.getReportTime());
        accident.setRemark(reqVO.getRemark() == null ? "" : reqVO.getRemark().trim());
        accidentMapper.insert(accident);
        return accident.getId();
    }
    public void updateAccident(SafetyAccidentSaveReqVO reqVO) {
        validateAccidentExists(reqVO.getId());
        SafetyAccidentDO accident = BeanUtils.toBean(reqVO, SafetyAccidentDO.class);
        accident.setCode(null);
        applyAccidentRefs(accident, reqVO);
        accidentMapper.updateById(accident);
    }
    public void deleteAccident(Long id) {
        validateAccidentExists(id);
        accidentMapper.deleteById(id);
    }
    public SafetyAccidentDO getAccident(Long id) {
        return validateAccidentExists(id);
    }
    public PageResult<SafetyAccidentDO> getAccidentPage(SafetyAccidentPageReqVO reqVO) {
        return accidentMapper.selectPage(reqVO);
    }
    public static void fillAccidentDisplay(SafetyAccidentDO accident, SafetyAccidentRespVO respVO) {
        respVO.setAccidentTypeName(SafetyAccidentTypeEnum.getNameByType(accident.getAccidentType()));
        respVO.setLevelName(SafetyAccidentLevelEnum.getNameByLevel(accident.getLevel()));
        respVO.setStatusName(SafetyAccidentStatusEnum.getNameByStatus(accident.getStatus()));
    }
    // ==================== å°è´¦å¯¼å‡ºç”¨æŸ¥è¯¢ ====================
    public List<SafetyTrainingDO> listTrainingBetween(LocalDate begin, LocalDate end) {
        return trainingMapper.selectListBetween(begin, end);
    }
    public List<SafetyDrillDO> listDrillBetween(LocalDate begin, LocalDate end) {
        return drillMapper.selectListBetween(begin, end);
    }
    public List<SafetyAccidentDO> listAccidentBetween(LocalDate begin, LocalDate end) {
        return accidentMapper.selectListBetween(
                begin == null ? null : begin.atStartOfDay(),
                end == null ? null : end.atTime(23, 59, 59));
    }
    // ==================== ç§æœ‰ ====================
    /** å‚加人数未填时,按姓名逗号分隔数量推断 */
    private static int resolveCount(Integer count, String participants) {
        if (count != null && count > 0) {
            return count;
        }
        if (StrUtil.isBlank(participants)) {
            return 0;
        }
        return (int) java.util.Arrays.stream(participants.split("[,,、]"))
                .map(String::trim).filter(StrUtil::isNotBlank).count();
    }
    private SafetyTrainingDO validateTrainingExists(Long id) {
        SafetyTrainingDO training = id == null ? null : trainingMapper.selectById(id);
        if (training == null) {
            throw exception(SAFETY_TRAINING_NOT_EXISTS);
        }
        return training;
    }
    private SafetyDrillDO validateDrillExists(Long id) {
        SafetyDrillDO drill = id == null ? null : drillMapper.selectById(id);
        if (drill == null) {
            throw exception(SAFETY_DRILL_NOT_EXISTS);
        }
        return drill;
    }
    private SafetyAccidentDO validateAccidentExists(Long id) {
        SafetyAccidentDO accident = id == null ? null : accidentMapper.selectById(id);
        if (accident == null) {
            throw exception(SAFETY_ACCIDENT_NOT_EXISTS);
        }
        return accident;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyWorkPermitService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,286 @@
package cn.iocoder.yudao.module.mes.service.safety;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
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.permit.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyWorkPermitDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyWorkPermitMonitorDO;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.SafetyWorkPermitMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.SafetyWorkPermitMonitorMapper;
import cn.iocoder.yudao.module.mes.enums.safety.SafetyPermitStatusEnum;
import cn.iocoder.yudao.module.mes.enums.safety.SafetyPermitTypeEnum;
import cn.iocoder.yudao.module.mes.enums.safety.SafetyRiskLevelEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.SafetyErrorCodeConstants.*;
/**
 * å®‰å…¨ç”Ÿäº§ - é«˜å±ä½œä¸šç¥¨ Service
 *
 * <p>动火、高压带电、有限空间等作业的线上审批与电子留存。
 * çŠ¶æ€æµè½¬ï¼šè‰ç¨¿ â†’ å¾…审批 â†’ å·²æ‰¹å‡† â†’ ä½œä¸šä¸­ â†’ å·²å®Œç»“;驳回/作废为终止态。</p>
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class SafetyWorkPermitService {
    private static final DateTimeFormatter CODE_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
    @Resource
    private SafetyWorkPermitMapper permitMapper;
    @Resource
    private SafetyRefResolver refResolver;
    @Resource
    private SafetyWorkPermitMonitorMapper monitorMapper;
    /**
     * æäº¤ä½œä¸šç”³è¯·ï¼ˆè‰ç¨¿æˆ–待审批)
     */
    @Transactional(rollbackFor = Exception.class)
    public Long createPermit(SafetyWorkPermitSaveReqVO reqVO) {
        reqVO.setId(null);
        validateTimeRange(reqVO);
        SafetyWorkPermitDO permit = BeanUtils.toBean(reqVO, SafetyWorkPermitDO.class);
        permit.setCode(permitMapper.selectNextCode("WP" + LocalDate.now().format(CODE_DATE)));
        permit.setTitle(StrUtil.trim(reqVO.getTitle()));
        applyPermitRefs(permit, reqVO);
        permit.setApplyTime(reqVO.getApplyTime() == null ? LocalDateTime.now() : reqVO.getApplyTime());
        permit.setRiskLevel(StrUtil.blankToDefault(reqVO.getRiskLevel(), SafetyRiskLevelEnum.NORMAL.getLevel()));
        permit.setStatus(SafetyPermitStatusEnum.PENDING.getStatus());
        permit.setRemark(reqVO.getRemark() == null ? "" : reqVO.getRemark().trim());
        permitMapper.insert(permit);
        return permit.getId();
    }
    /**
     * ä¿®æ”¹ä½œä¸šç¥¨ï¼ˆä»…草稿/待审批可改)
     */
    public void updatePermit(SafetyWorkPermitSaveReqVO reqVO) {
        SafetyWorkPermitDO old = validatePermitExists(reqVO.getId());
        ensureStatus(old, SafetyPermitStatusEnum.DRAFT, SafetyPermitStatusEnum.PENDING);
        validateTimeRange(reqVO);
        SafetyWorkPermitDO update = BeanUtils.toBean(reqVO, SafetyWorkPermitDO.class);
        update.setCode(null);
        update.setStatus(null);
        applyPermitRefs(update, reqVO);
        permitMapper.updateById(update);
    }
    /**
     * ä½œä¸šç¥¨çš„主数据引用:作业车间 + ç”³è¯·äºº + ç›‘护人,从主数据带出名称落快照
     */
    private void applyPermitRefs(SafetyWorkPermitDO permit, SafetyWorkPermitSaveReqVO reqVO) {
        permit.setWorkshopId(reqVO.getWorkshopId());
        permit.setWorkshopName(reqVO.getWorkshopId() == null ? ""
                : refResolver.requireWorkshop(reqVO.getWorkshopId()).getName());
        var applicant = refResolver.requireEmployee(reqVO.getApplicantId(), SAFETY_EMPLOYEE_REQUIRED);
        permit.setApplicantId(applicant.getId());
        permit.setApplicant(applicant.getName());
        permit.setGuardianId(reqVO.getGuardianId());
        permit.setGuardian(reqVO.getGuardianId() == null ? ""
                : refResolver.requireEmployee(reqVO.getGuardianId(), SAFETY_EMPLOYEE_REQUIRED).getName());
    }
    /**
     * å®¡æ‰¹ä½œä¸šç¥¨
     *
     * <p>批准时校验高危作业(动火/高压带电/有限空间)必须指定监护人,
     * ä¿è¯ä½œä¸šç¥¨ç”µå­ç•™å­˜çš„完整性。</p>
     */
    @Transactional(rollbackFor = Exception.class)
    public void approvePermit(SafetyWorkPermitApproveReqVO reqVO) {
        SafetyWorkPermitDO permit = validatePermitExists(reqVO.getId());
        ensureStatus(permit, SafetyPermitStatusEnum.PENDING);
        var approver = refResolver.requireEmployee(reqVO.getApproverId(), SAFETY_EMPLOYEE_REQUIRED);
        // ç”³è¯·äººä¸èƒ½å®¡æ‰¹è‡ªå·±çš„作业票(按员工编号比较,避免同名误判)
        if (permit.getApplicantId() != null && permit.getApplicantId().equals(approver.getId())) {
            throw exception(SAFETY_PERMIT_SELF_APPROVE);
        }
        boolean approved = "APPROVED".equals(reqVO.getDecision());
        if (approved && SafetyPermitTypeEnum.requiresGuardian(permit.getPermitType())
                && permit.getGuardianId() == null) {
            throw exception(SAFETY_PERMIT_GUARDIAN_REQUIRED);
        }
        SafetyWorkPermitDO update = new SafetyWorkPermitDO();
        update.setId(permit.getId());
        update.setApproverId(approver.getId());
        update.setApprover(approver.getName());
        update.setApproveTime(LocalDateTime.now());
        update.setApproveComment(reqVO.getApproveComment());
        update.setStatus(approved ? SafetyPermitStatusEnum.APPROVED.getStatus()
                : SafetyPermitStatusEnum.REJECTED.getStatus());
        permitMapper.updateById(update);
    }
    /**
     * å¼€å§‹ä½œä¸šï¼ˆé¡»å·²æ‰¹å‡†ï¼‰
     */
    public void startPermit(Long id) {
        SafetyWorkPermitDO permit = validatePermitExists(id);
        if (!SafetyPermitStatusEnum.APPROVED.getStatus().equals(permit.getStatus())) {
            throw exception(SAFETY_PERMIT_NOT_APPROVED);
        }
        SafetyWorkPermitDO update = new SafetyWorkPermitDO();
        update.setId(id);
        update.setActualStartTime(LocalDateTime.now());
        update.setStatus(SafetyPermitStatusEnum.IN_PROGRESS.getStatus());
        permitMapper.updateById(update);
    }
    /**
     * ç»“束作业并归档
     */
    public void closePermit(Long id) {
        SafetyWorkPermitDO permit = validatePermitExists(id);
        ensureStatus(permit, SafetyPermitStatusEnum.IN_PROGRESS);
        SafetyWorkPermitDO update = new SafetyWorkPermitDO();
        update.setId(id);
        update.setActualEndTime(LocalDateTime.now());
        update.setStatus(SafetyPermitStatusEnum.CLOSED.getStatus());
        permitMapper.updateById(update);
    }
    /**
     * ä½œåºŸä½œä¸šç¥¨ï¼ˆæœªå®Œç»“前均可作废)
     */
    public void cancelPermit(Long id) {
        SafetyWorkPermitDO permit = validatePermitExists(id);
        if (SafetyPermitStatusEnum.CLOSED.getStatus().equals(permit.getStatus())
                || SafetyPermitStatusEnum.CANCELLED.getStatus().equals(permit.getStatus())) {
            throw exception(SAFETY_PERMIT_STATUS_ERROR);
        }
        SafetyWorkPermitDO update = new SafetyWorkPermitDO();
        update.setId(id);
        update.setStatus(SafetyPermitStatusEnum.CANCELLED.getStatus());
        permitMapper.updateById(update);
    }
    @Transactional(rollbackFor = Exception.class)
    public void deletePermit(Long id) {
        validatePermitExists(id);
        monitorMapper.deleteByPermitId(id);
        permitMapper.deleteById(id);
    }
    public SafetyWorkPermitDO getPermit(Long id) {
        return validatePermitExists(id);
    }
    public PageResult<SafetyWorkPermitDO> getPermitPage(SafetyWorkPermitPageReqVO reqVO) {
        return permitMapper.selectPage(reqVO);
    }
    // ==================== ç›‘护记录 ====================
    public Long createMonitor(SafetyWorkPermitMonitorSaveReqVO reqVO) {
        SafetyWorkPermitDO permit = validatePermitExists(reqVO.getPermitId());
        if (!SafetyPermitStatusEnum.IN_PROGRESS.getStatus().equals(permit.getStatus())
                && !SafetyPermitStatusEnum.APPROVED.getStatus().equals(permit.getStatus())) {
            throw exception(SAFETY_PERMIT_STATUS_ERROR);
        }
        SafetyWorkPermitMonitorDO monitor = BeanUtils.toBean(reqVO, SafetyWorkPermitMonitorDO.class);
        monitor.setId(null);
        var guardian = refResolver.requireEmployee(reqVO.getGuardianId(), SAFETY_EMPLOYEE_REQUIRED);
        monitor.setGuardianId(guardian.getId());
        monitor.setGuardian(guardian.getName());
        monitor.setMonitorTime(reqVO.getMonitorTime() == null ? LocalDateTime.now() : reqVO.getMonitorTime());
        monitorMapper.insert(monitor);
        return monitor.getId();
    }
    public void deleteMonitor(Long id) {
        if (id == null || monitorMapper.selectById(id) == null) {
            throw exception(SAFETY_PERMIT_MONITOR_NOT_EXISTS);
        }
        monitorMapper.deleteById(id);
    }
    public List<SafetyWorkPermitMonitorDO> getMonitorList(Long permitId) {
        return monitorMapper.selectListByPermitId(permitId);
    }
    // ==================== å±•示字段 ====================
    /**
     * æ‰¹é‡å›žå¡«ä½œä¸šç¥¨çš„车间名与三个人员姓名(申请人/审批人/监护人)
     */
    public void fillPermitRefs(List<SafetyWorkPermitDO> permits) {
        if (CollUtil.isEmpty(permits)) {
            return;
        }
        var workshopMap = refResolver.workshopMap(permits.stream().map(SafetyWorkPermitDO::getWorkshopId).toList());
        var ids = new java.util.ArrayList<Long>(permits.size() * 3);
        permits.forEach(p -> { ids.add(p.getApplicantId()); ids.add(p.getApproverId()); ids.add(p.getGuardianId()); });
        var employeeMap = refResolver.employeeMap(ids);
        permits.forEach(permit -> {
            var workshop = workshopMap.get(permit.getWorkshopId());
            if (workshop != null) permit.setWorkshopName(workshop.getName());
            var applicant = employeeMap.get(permit.getApplicantId());
            if (applicant != null) permit.setApplicant(applicant.getName());
            var approver = employeeMap.get(permit.getApproverId());
            if (approver != null) permit.setApprover(approver.getName());
            var guardian = employeeMap.get(permit.getGuardianId());
            if (guardian != null) permit.setGuardian(guardian.getName());
        });
    }
    /**
     * æ‰¹é‡å›žå¡«ç›‘护记录的监护人最新姓名
     */
    public void fillMonitorGuardians(List<SafetyWorkPermitMonitorDO> monitors) {
        if (CollUtil.isEmpty(monitors)) {
            return;
        }
        var employeeMap = refResolver.employeeMap(
                monitors.stream().map(SafetyWorkPermitMonitorDO::getGuardianId).toList());
        monitors.forEach(monitor -> {
            var employee = employeeMap.get(monitor.getGuardianId());
            if (employee != null) monitor.setGuardian(employee.getName());
        });
    }
    public static void fillPermitDisplay(SafetyWorkPermitDO permit, SafetyWorkPermitRespVO respVO) {
        respVO.setPermitTypeName(SafetyPermitTypeEnum.getNameByType(permit.getPermitType()));
        respVO.setRiskLevelName(SafetyRiskLevelEnum.getNameByLevel(permit.getRiskLevel()));
        respVO.setStatusName(SafetyPermitStatusEnum.getNameByStatus(permit.getStatus()));
        respVO.setGuardianRequired(SafetyPermitTypeEnum.requiresGuardian(permit.getPermitType()));
    }
    private void validateTimeRange(SafetyWorkPermitSaveReqVO reqVO) {
        if (reqVO.getPlanStartTime() != null && reqVO.getPlanEndTime() != null
                && !reqVO.getPlanEndTime().isAfter(reqVO.getPlanStartTime())) {
            throw exception(SAFETY_PERMIT_TIME_INVALID);
        }
    }
    private void ensureStatus(SafetyWorkPermitDO permit, SafetyPermitStatusEnum... allowed) {
        for (SafetyPermitStatusEnum status : allowed) {
            if (status.getStatus().equals(permit.getStatus())) {
                return;
            }
        }
        throw exception(SAFETY_PERMIT_STATUS_ERROR);
    }
    private SafetyWorkPermitDO validatePermitExists(Long id) {
        SafetyWorkPermitDO permit = id == null ? null : permitMapper.selectById(id);
        if (permit == null) {
            throw exception(SAFETY_PERMIT_NOT_EXISTS);
        }
        return permit;
    }
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/companyconfig/SystemCompanyConfigController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
package cn.iocoder.yudao.module.system.controller.admin.companyconfig;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.system.controller.admin.companyconfig.vo.SystemCompanyConfigRespVO;
import cn.iocoder.yudao.module.system.controller.admin.companyconfig.vo.SystemCompanyConfigSaveReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.companyconfig.SystemCompanyConfigDO;
import cn.iocoder.yudao.module.system.service.companyconfig.SystemCompanyConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - å…¬å¸å“ç‰Œä¿¡æ¯")
@RestController
@RequestMapping("/system/company-config")
@Validated
public class SystemCompanyConfigController {
    @Resource
    private SystemCompanyConfigService companyConfigService;
    /**
     * èŽ·å¾—å…¬å¸å“ç‰Œä¿¡æ¯
     *
     * å…ç™»å½•公开接口,供前端登录页 / åº”用启动时读取公司名称、logo、登录页标题。
     */
    @GetMapping("/get")
    @Operation(summary = "获得公司品牌信息(免登录公开)")
    @PermitAll
    public CommonResult<SystemCompanyConfigRespVO> getCompanyConfig() {
        SystemCompanyConfigDO config = companyConfigService.getCompanyConfig();
        return success(BeanUtils.toBean(config, SystemCompanyConfigRespVO.class));
    }
    /**
     * ä¿å­˜å…¬å¸å“ç‰Œä¿¡æ¯
     *
     * å•行 upsert,需登录且拥有 system:company-config:update æƒé™ã€‚
     * æƒé™é€šè¿‡èœå•「公司信息配置 - ä¿®æ”¹ã€æŽˆæƒï¼ˆè§ sql/mysql/system_company_config_menu.sql)。
     */
    @PostMapping("/save")
    @Operation(summary = "保存公司品牌信息")
    @PreAuthorize("@ss.hasPermission('system:company-config:update')")
    public CommonResult<Long> saveCompanyConfig(@Valid @RequestBody SystemCompanyConfigSaveReqVO reqVO) {
        return success(companyConfigService.saveCompanyConfig(reqVO));
    }
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/companyconfig/vo/SystemCompanyConfigRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.system.controller.admin.companyconfig.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - å…¬å¸å“ç‰Œä¿¡æ¯ Response VO")
@Data
public class SystemCompanyConfigRespVO {
    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "公司名称", example = "宁夏九泓化工")
    private String companyName;
    @Schema(description = "logo æ°¸ä¹…公开链接(previewURL)", example = "http://domain/admin-api/system/storage-blob/preview/xxx?publicKey=xxx")
    private String logoUrl;
    @Schema(description = "logo å¯¹åº”çš„ blob è®°å½• ID", example = "86")
    private Long logoBlobId;
    @Schema(description = "登录页标题", example = "制造运营管理平台")
    private String loginTitle;
    @Schema(description = "登录页描述", example = "工程化、高性能、跨组件库的前端模版")
    private String loginDescription;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDateTime createTime;
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/companyconfig/vo/SystemCompanyConfigSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.system.controller.admin.companyconfig.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - å…¬å¸å“ç‰Œä¿¡æ¯ä¿å­˜ Request VO")
@Data
public class SystemCompanyConfigSaveReqVO {
    @Schema(description = "主键(单行配置,可不传)", example = "1")
    private Long id;
    @Schema(description = "公司名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "宁夏九泓化工")
    @NotBlank(message = "公司名称不能为空")
    @Size(max = 128, message = "公司名称不能超过128个字符")
    private String companyName;
    @Schema(description = "logo æ°¸ä¹…公开链接(previewURL)", example = "http://domain/admin-api/system/storage-blob/preview/xxx?publicKey=xxx")
    @Size(max = 512, message = "logo åœ°å€ä¸èƒ½è¶…过512个字符")
    private String logoUrl;
    @Schema(description = "logo å¯¹åº”çš„ blob è®°å½• ID", example = "86")
    private Long logoBlobId;
    @Schema(description = "登录页标题", example = "制造运营管理平台")
    @Size(max = 128, message = "登录页标题不能超过128个字符")
    private String loginTitle;
    @Schema(description = "登录页描述", example = "工程化、高性能、跨组件库的前端模版")
    @Size(max = 128, message = "登录页描述不能超过128个字符")
    private String loginDescription;
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/database/DatabaseBackupController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.system.controller.admin.database;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.system.service.database.DatabaseBackupService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
@Tag(name = "管理后台 - æ•°æ®åº“备份")
@RestController
@RequestMapping("/system/database")
public class DatabaseBackupController {
    @Resource
    private DatabaseBackupService databaseBackupService;
    @GetMapping("/backup")
    @Operation(summary = "下载数据库备份")
    @PreAuthorize("@ss.hasPermission('system:database:backup')")
    public void backupDatabase(HttpServletResponse response) throws Exception {
        databaseBackupService.backupDatabase(response);
    }
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/companyconfig/SystemCompanyConfigDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.system.dal.dataobject.companyconfig;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * å…¬å¸å“ç‰Œä¿¡æ¯é…ç½® DO
 *
 * å•行配置:保存公司名称/logo/登录页标题,前端启动时读取并联动登录页、菜单栏、网页标题。
 *
 * @author è¶…级管理员
 */
@TableName("system_company_config")
@KeySequence("system_company_config_seq") // ç”¨äºŽ Oracle、PostgreSQL、Kingbase、DB2、H2 æ•°æ®åº“的主键自增。如果是 MySQL ç­‰æ•°æ®åº“,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
public class SystemCompanyConfigDO extends BaseDO {
    /**
     * ä¸»é”®
     */
    private Long id;
    /**
     * å…¬å¸åç§°
     */
    private String companyName;
    /**
     * logo æ°¸ä¹…公开链接(public-upload è¿”回的 previewURL)
     */
    private String logoUrl;
    /**
     * logo å¯¹åº”çš„ blob è®°å½• ID
     */
    private Long logoBlobId;
    /**
     * ç™»å½•页标题
     */
    private String loginTitle;
    /**
     * ç™»å½•页描述
     */
    private String loginDescription;
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/companyconfig/SystemCompanyConfigMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.system.dal.mysql.companyconfig;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.system.dal.dataobject.companyconfig.SystemCompanyConfigDO;
import org.apache.ibatis.annotations.Mapper;
/**
 * å…¬å¸å“ç‰Œä¿¡æ¯é…ç½® Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface SystemCompanyConfigMapper extends BaseMapperX<SystemCompanyConfigDO> {
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/companyconfig/SystemCompanyConfigService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.system.service.companyconfig;
import cn.iocoder.yudao.module.system.controller.admin.companyconfig.vo.SystemCompanyConfigSaveReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.companyconfig.SystemCompanyConfigDO;
/**
 * å…¬å¸å“ç‰Œä¿¡æ¯é…ç½® Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface SystemCompanyConfigService {
    /**
     * èŽ·å¾—å…¬å¸å“ç‰Œä¿¡æ¯ï¼ˆå•è¡Œé…ç½®ï¼‰
     *
     * @return å…¬å¸å“ç‰Œä¿¡æ¯ï¼Œæœªé…ç½®æ—¶è¿”回 null
     */
    SystemCompanyConfigDO getCompanyConfig();
    /**
     * ä¿å­˜å…¬å¸å“ç‰Œä¿¡æ¯ï¼ˆå•行 upsert)
     *
     * @param reqVO ä¿å­˜è¯·æ±‚
     * @return ä¸»é”®
     */
    Long saveCompanyConfig(SystemCompanyConfigSaveReqVO reqVO);
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/companyconfig/SystemCompanyConfigServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.system.service.companyconfig;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.system.controller.admin.companyconfig.vo.SystemCompanyConfigSaveReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.companyconfig.SystemCompanyConfigDO;
import cn.iocoder.yudao.module.system.dal.mysql.companyconfig.SystemCompanyConfigMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.List;
/**
 * å…¬å¸å“ç‰Œä¿¡æ¯é…ç½® Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
public class SystemCompanyConfigServiceImpl implements SystemCompanyConfigService {
    @Resource
    private SystemCompanyConfigMapper companyConfigMapper;
    @Override
    public SystemCompanyConfigDO getCompanyConfig() {
        // å•行配置:按主键升序取第一条
        List<SystemCompanyConfigDO> list = companyConfigMapper.selectList(new LambdaQueryWrapperX<SystemCompanyConfigDO>()
                .orderByAsc(SystemCompanyConfigDO::getId)
                .last("LIMIT 1"));
        return CollUtil.getFirst(list);
    }
    @Override
    public Long saveCompanyConfig(SystemCompanyConfigSaveReqVO reqVO) {
        SystemCompanyConfigDO config = getCompanyConfig();
        if (config == null) {
            // é¦–次保存:插入
            SystemCompanyConfigDO insertObj = BeanUtils.toBean(reqVO, SystemCompanyConfigDO.class);
            companyConfigMapper.insert(insertObj);
            return insertObj.getId();
        }
        // å·²å­˜åœ¨ï¼šæ›´æ–°å•行
        SystemCompanyConfigDO updateObj = BeanUtils.toBean(reqVO, SystemCompanyConfigDO.class);
        updateObj.setId(config.getId());
        companyConfigMapper.updateById(updateObj);
        return config.getId();
    }
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/database/DatabaseBackupService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package cn.iocoder.yudao.module.system.service.database;
import jakarta.servlet.http.HttpServletResponse;
public interface DatabaseBackupService {
    /**
     * å¤‡ä»½æ•°æ®åº“,并将 SQL æ–‡ä»¶å†™å…¥åˆ° HTTP å“åº”中
     *
     * @param response HTTP å“åº”
     */
    void backupDatabase(HttpServletResponse response) throws Exception;
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/database/DatabaseBackupServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,460 @@
package cn.iocoder.yudao.module.system.service.database;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.sql.DataSource;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
@Slf4j
public class DatabaseBackupServiceImpl implements DatabaseBackupService {
    /** å¤‡ä»½è¶…时时间(分钟) */
    private static final long BACKUP_TIMEOUT_MINUTES = 10;
    /** MySQL å®¹å™¨å†…部端口 */
    private static final String DOCKER_MYSQL_PORT = "3306";
    @Value("${spring.datasource.dynamic.datasource.master.url:}")
    private String dbUrl;
    @Value("${spring.datasource.dynamic.datasource.master.username:root}")
    private String dbUsername;
    @Value("${spring.datasource.dynamic.datasource.master.password:}")
    private String dbPassword;
    /** å¤‡ä»½å‘½ä»¤ï¼Œå¯æ˜¾å¼æŒ‡å®šï¼ˆå¦‚ docker exec mysql8 mysqldump);留空则自动检测本地 mysqldump æˆ– Docker MySQL å®¹å™¨ */
    @Value("${yudao.database-backup.command:}")
    private String backupCommand;
    /** å¤‡ä»½ç›®æ ‡æ•°æ®åº“ host,留空则从数据源 JDBC URL è§£æž */
    @Value("${yudao.database-backup.host:}")
    private String backupHost;
    /** å¤‡ä»½ç›®æ ‡æ•°æ®åº“ port,留空则从数据源 JDBC URL è§£æž */
    @Value("${yudao.database-backup.port:}")
    private String backupPort;
    /** å¤‡ä»½ç›®æ ‡æ•°æ®åº“名,留空则从数据源 JDBC URL è§£æž */
    @Value("${yudao.database-backup.database:}")
    private String backupDatabase;
    /** å¤‡ä»½è´¦å·ï¼Œç•™ç©ºåˆ™ä½¿ç”¨æ•°æ®æºè´¦å· */
    @Value("${yudao.database-backup.username:}")
    private String backupUsername;
    /** å¤‡ä»½å¯†ç ï¼Œç•™ç©ºåˆ™ä½¿ç”¨æ•°æ®æºå¯†ç  */
    @Value("${yudao.database-backup.password:}")
    private String backupPassword;
    /** ä¸»æ•°æ®æºï¼Œç”¨äºŽ JDBC å¯¼å‡ºå…œåº• */
    @Resource
    private DataSource dataSource;
    @Override
    public void backupDatabase(HttpServletResponse response) throws Exception {
        if (StrUtil.isEmpty(dbUrl)) {
            throw new ServiceException(500, "未找到数据库连接配置");
        }
        // å¤‡ä»½è¿žæŽ¥çš„æ•°æ®åº“信息:优先使用 yudao.database-backup é…ç½®ï¼Œæœªé…ç½®çš„部分从数据源 JDBC URL è§£æžå…œåº•
        String dbHost = backupHost;
        String dbPort = backupPort;
        String dbName = backupDatabase;
        if (StrUtil.isEmpty(dbHost) || StrUtil.isEmpty(dbPort) || StrUtil.isEmpty(dbName)) {
            Matcher matcher = Pattern.compile("jdbc:mysql://([^:]+):(\\d+)/([^?]+)").matcher(dbUrl);
            if (matcher.find()) {
                if (StrUtil.isEmpty(dbHost)) {
                    dbHost = matcher.group(1);
                }
                if (StrUtil.isEmpty(dbPort)) {
                    dbPort = matcher.group(2);
                }
                if (StrUtil.isEmpty(dbName)) {
                    dbName = matcher.group(3);
                }
            }
        }
        if (StrUtil.isEmpty(dbHost) || StrUtil.isEmpty(dbPort) || StrUtil.isEmpty(dbName)) {
            log.warn("无法从配置或 JDBC URL è§£æžå®Œæ•´çš„æ•°æ®åº“信息: host={}, port={}, db={}", dbHost, dbPort, dbName);
            throw new ServiceException(500, "无法解析数据库备份连接信息,请在 yudao.database-backup ä¸­é…ç½® host/port/database");
        }
        // localhost ç»Ÿä¸€èµ° TCP 127.0.0.1:mysqldump å¯¹ localhost å¯èƒ½èµ° socket/loopback,
        // è€Œä¸åŒçŽ¯å¢ƒ root@localhost çš„认证要求差异大,强制 TCP æ›´ç¨³å®š
        if ("localhost".equalsIgnoreCase(dbHost)) {
            dbHost = "127.0.0.1";
        }
        String username = StrUtil.isEmpty(backupUsername) ? dbUsername : backupUsername;
        String password = StrUtil.isEmpty(backupPassword) ? dbPassword : backupPassword;
        // ç»„装候选备份命令,按优先级依次尝试:手动配置 > æœ¬æœº mysqldump > Docker å®¹å™¨ mysqldump
        List<List<String>> candidates = new ArrayList<>();
        List<String> labels = new ArrayList<>();
        if (StrUtil.isNotEmpty(backupCommand)) {
            candidates.add(splitCommand(backupCommand));
            labels.add("手动配置命令[" + backupCommand + "]");
        }
        if (isLocalMysqldumpAvailable()) {
            candidates.add(buildLocalMysqldumpCommand(dbHost, dbPort, username, password, dbName));
            labels.add("本机 mysqldump");
        }
        String container = findMysqlDockerContainer(dbPort);
        if (StrUtil.isNotEmpty(container)) {
            candidates.add(buildDockerExecCommand(container, username, password, dbName));
            labels.add("Docker å®¹å™¨[" + container + "] mysqldump");
        }
        log.info("数据库备份候选方式:{}", labels);
        // ä¾æ¬¡å°è¯•所有外部命令方式
        List<String> failures = new ArrayList<>();
        String timestamp = DateUtil.format(new Date(), "yyyyMMdd_HHmmss");
        String fileName = "backup_" + timestamp + ".sql";
        for (int i = 0; i < candidates.size(); i++) {
            File tempFile = File.createTempFile("backup_", ".sql");
            try {
                String error = runMysqldump(candidates.get(i), password, tempFile);
                if (error == null) {
                    log.info("数据库备份成功,使用方式:{}", labels.get(i));
                    downloadAttachment(response, tempFile, fileName);
                    return;
                }
                log.warn("备份方式 [{}] å¤±è´¥ï¼š{}", labels.get(i), error);
                failures.add(labels.get(i) + ":" + error);
            } finally {
                FileUtil.del(tempFile);
            }
        }
        // å¤–部命令全部失败或不可用时,使用 JDBC å¯¼å‡ºå…œåº•,保证任何部署环境(如应用容器内无 mysqldump/docker)都能备份
        File tempFile = File.createTempFile("backup_", ".sql");
        try {
            String error = exportByJdbc(tempFile);
            if (error == null) {
                log.info("数据库备份成功,使用方式:JDBC å¯¼å‡ºå…œåº•");
                downloadAttachment(response, tempFile, fileName);
                return;
            }
            log.warn("JDBC å¯¼å‡ºå…œåº•失败:{}", error);
            failures.add("JDBC å¯¼å‡ºå…œåº•:" + error);
        } finally {
            FileUtil.del(tempFile);
        }
        throw new ServiceException(500, "数据库备份失败:" + String.join(" ï½œ ", failures));
    }
    /**
     * é€šè¿‡ JDBC ç›´è¿žæ•°æ®åº“导出建表语句与全量数据,作为外部命令不可用时的兜底备份方式
     *
     * @return æ‰§è¡ŒæˆåŠŸè¿”å›ž null;失败返回错误描述
     */
    private String exportByJdbc(File outputFile) {
        try (Connection conn = dataSource.getConnection();
             BufferedWriter writer = new BufferedWriter(
                     new OutputStreamWriter(new FileOutputStream(outputFile), StandardCharsets.UTF_8))) {
            writer.write("/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n");
            writer.write("/*!40101 SET NAMES utf8mb4 */;\n");
            writer.write("SET TIME_ZONE='+00:00';\n");
            writer.write("SET FOREIGN_KEY_CHECKS=0;\n");
            writer.write("SET UNIQUE_CHECKS=0;\n");
            writer.write("SET SQL_MODE='NO_AUTO_VALUE_ON_ZERO';\n\n");
            try (Statement st = conn.createStatement();
                 ResultSet rs = st.executeQuery("SHOW TABLES")) {
                List<String> tables = new ArrayList<>();
                while (rs.next()) {
                    tables.add(rs.getString(1));
                }
                for (String table : tables) {
                    exportTable(conn, writer, table);
                }
            }
            writer.write("\nSET FOREIGN_KEY_CHECKS=1;\n");
            writer.write("SET UNIQUE_CHECKS=1;\n");
            writer.write("/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n");
            return null;
        } catch (Exception e) {
            log.error("JDBC å¯¼å‡ºå…œåº•失败", e);
            return "JDBC å¯¼å‡ºå¼‚常: " + e.getMessage();
        }
    }
    /**
     * å¯¼å‡ºå•张表的建表语句与全部数据
     */
    private void exportTable(Connection conn, BufferedWriter writer, String table) throws Exception {
        String quoted = "`" + table.replace("`", "``") + "`";
        // å»ºè¡¨è¯­å¥
        try (Statement st = conn.createStatement();
             ResultSet rs = st.executeQuery("SHOW CREATE TABLE " + quoted)) {
            if (rs.next()) {
                writer.write(rs.getString(2));
                writer.write(";\n\n");
            }
        }
        // å…¨é‡æ•°æ®ï¼Œæµå¼è¯»å–避免大表占用过多内存
        try (Statement st = conn.createStatement()) {
            st.setFetchSize(Integer.MIN_VALUE);
            try (ResultSet rs = st.executeQuery("SELECT * FROM " + quoted)) {
                ResultSetMetaData meta = rs.getMetaData();
                int columnCount = meta.getColumnCount();
                int[] types = new int[columnCount];
                for (int i = 0; i < columnCount; i++) {
                    types[i] = meta.getColumnType(i + 1);
                }
                while (rs.next()) {
                    StringBuilder sb = new StringBuilder("INSERT INTO ").append(quoted).append(" VALUES (");
                    for (int i = 0; i < columnCount; i++) {
                        if (i > 0) {
                            sb.append(',');
                        }
                        if (rs.getObject(i + 1) == null) {
                            sb.append("NULL");
                            continue;
                        }
                        int type = types[i];
                        if (type == Types.BIT) {
                            sb.append(rs.getBoolean(i + 1) ? '1' : '0');
                        } else if (isBinaryType(type)) {
                            sb.append("X'").append(HexUtil.encodeHexStr(rs.getBytes(i + 1))).append("'");
                        } else {
                            sb.append('\'').append(escapeSql(rs.getString(i + 1))).append('\'');
                        }
                    }
                    sb.append(");\n");
                    writer.write(sb.toString());
                }
            }
        }
        writer.write('\n');
    }
    /**
     * æ˜¯å¦äºŒè¿›åˆ¶åˆ—类型,二进制数据以十六进制字面量导出避免乱码
     */
    private boolean isBinaryType(int type) {
        return type == Types.BINARY || type == Types.VARBINARY
                || type == Types.LONGVARBINARY || type == Types.BLOB;
    }
    /**
     * SQL å­—符串字面量转义
     */
    private String escapeSql(String value) {
        return value.replace("\\", "\\\\").replace("'", "''").replace("\0", "\\0");
    }
    /**
     * æž„建本机 mysqldump å¤‡ä»½å‘½ä»¤
     */
    private List<String> buildLocalMysqldumpCommand(String host, String port, String username, String password, String dbName) {
        List<String> command = new ArrayList<>();
        command.add("mysqldump");
        // MySQL 8 caching_sha2_password è®¤è¯é»˜è®¤ä¸è¯·æ±‚服务端公钥,会报 Access denied (using password: YES)
        command.add("--get-server-public-key");
        command.add("-h" + host);
        command.add("-P" + port);
        command.add("-u" + username);
        if (StrUtil.isEmpty(password)) {
            command.add("--skip-password");
        }
        command.add(dbName);
        return command;
    }
    /**
     * æž„建通过 docker exec è°ƒç”¨å®¹å™¨å†… mysqldump çš„备份命令
     */
    private List<String> buildDockerExecCommand(String container, String username, String password, String dbName) {
        List<String> command = new ArrayList<>();
        command.add("docker");
        command.add("exec");
        // docker exec ä¸ä¼šè½¬å‘宿主机环境变量,密码需通过 -e æ˜¾å¼ä¼ å…¥å®¹å™¨
        if (StrUtil.isNotEmpty(password)) {
            command.add("-e");
            command.add("MYSQL_PWD=" + password);
        }
        command.add(container);
        command.add("mysqldump");
        command.add("--get-server-public-key");
        // å®¹å™¨å†…固定走 127.0.0.1:3306 çš„ TCP è¿žæŽ¥è‡ªèº«ï¼Œç»•å¼€ socket/loopback è®¤è¯å·®å¼‚
        command.add("-h127.0.0.1");
        command.add("-P" + DOCKER_MYSQL_PORT);
        command.add("-u" + username);
        if (StrUtil.isEmpty(password)) {
            command.add("--skip-password");
        }
        command.add(dbName);
        return command;
    }
    /**
     * æ‰§è¡Œ mysqldump å‘½ä»¤ï¼Œå°† stdout å†™å…¥ä¸´æ—¶æ–‡ä»¶
     *
     * @return æ‰§è¡ŒæˆåŠŸè¿”å›ž null;失败返回错误描述
     */
    private String runMysqldump(List<String> command, String password, File outputFile) {
        try {
            ProcessBuilder pb = new ProcessBuilder(command);
            // å¯†ç é€šè¿‡çŽ¯å¢ƒå˜é‡ä¼ é€’ï¼Œé¿å…å‡ºçŽ°åœ¨å‘½ä»¤è¡ŒåŠè¿›ç¨‹åˆ—è¡¨
            if (StrUtil.isNotEmpty(password)) {
                pb.environment().put("MYSQL_PWD", password);
            }
            Process process = pb.start();
            // åŽå°è¯»å– stderr,防止管道缓冲区写满导致进程阻塞
            StringBuilder stderr = new StringBuilder();
            Thread stderrThread = new Thread(() -> {
                try (BufferedReader reader = new BufferedReader(
                        new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
                    String line;
                    while ((line = reader.readLine()) != null) {
                        stderr.append(line).append(System.lineSeparator());
                    }
                } catch (IOException ignored) {
                }
            });
            stderrThread.start();
            // mysqldump è¾“出写入本地临时文件(docker exec åœºæ™¯è¾“出不会落在容器内文件系统)
            try (InputStream in = process.getInputStream();
                 OutputStream out = new FileOutputStream(outputFile)) {
                IoUtil.copy(in, out);
            }
            boolean finished = process.waitFor(BACKUP_TIMEOUT_MINUTES, TimeUnit.MINUTES);
            stderrThread.join();
            if (!finished) {
                process.destroyForcibly();
                return "执行超时(超过 " + BACKUP_TIMEOUT_MINUTES + " åˆ†é’Ÿï¼‰";
            }
            if (process.exitValue() != 0) {
                return "exitCode=" + process.exitValue() + "," + stderr.toString().trim();
            }
            return null;
        } catch (IOException e) {
            return "启动命令失败: " + e.getMessage();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return "执行被中断: " + e.getMessage();
        }
    }
    /**
     * å°†å¤‡ä»½æ–‡ä»¶ä½œä¸ºé™„件输出到响应
     */
    private void downloadAttachment(HttpServletResponse response, File file, String fileName) throws IOException {
        response.setContentType("application/octet-stream; charset=UTF-8");
        response.setHeader("Content-Disposition", "attachment; filename=\"" + URLEncoder.encode(fileName, "UTF-8") + "\"");
        response.setContentLengthLong(file.length());
        try (FileInputStream fis = new FileInputStream(file);
             OutputStream os = response.getOutputStream()) {
            IoUtil.copy(fis, os);
        }
    }
    /**
     * å°†é…ç½®çš„命令字符串按空白切分为参数列表
     */
    private List<String> splitCommand(String command) {
        List<String> parts = new ArrayList<>();
        for (String token : command.trim().split("\\s+")) {
            parts.add(token);
        }
        return parts;
    }
    /**
     * æ£€æµ‹æœ¬æœº mysqldump æ˜¯å¦å¯ç”¨
     */
    private boolean isLocalMysqldumpAvailable() {
        try {
            Process process = new ProcessBuilder("mysqldump", "--version").redirectErrorStream(true).start();
            boolean finished = process.waitFor(5, TimeUnit.SECONDS);
            if (!finished) {
                process.destroyForcibly();
                return false;
            }
            return process.exitValue() == 0;
        } catch (IOException e) {
            return false;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
    /**
     * æŸ¥æ‰¾è¿è¡Œä¸­çš„ MySQL Docker å®¹å™¨ï¼Œä¼˜å…ˆé€‰æ‹©ç«¯å£æ˜ å°„与数据源端口一致的容器
     *
     * @return å®¹å™¨åï¼›æœªæ‰¾åˆ°è¿”回 null
     */
    private String findMysqlDockerContainer(String dbPort) {
        try {
            Process ps = new ProcessBuilder("docker", "ps", "--format", "{{.ID}}|{{.Names}}|{{.Image}}")
                    .redirectErrorStream(true).start();
            String output = IoUtil.read(ps.getInputStream(), StandardCharsets.UTF_8);
            if (!ps.waitFor(10, TimeUnit.SECONDS) || ps.exitValue() != 0) {
                return null;
            }
            List<String[]> containers = new ArrayList<>();
            for (String line : output.split("\n")) {
                String[] parts = line.trim().split("\\|");
                if (parts.length == 3 && parts[2].startsWith("mysql")) {
                    containers.add(parts);
                }
            }
            if (containers.isEmpty()) {
                return null;
            }
            // ä¼˜å…ˆé€‰æ‹©ç«¯å£æ˜ å°„与数据源端口一致的容器
            for (String[] container : containers) {
                Process portProcess = new ProcessBuilder("docker", "port", container[0], "3306/tcp")
                        .redirectErrorStream(true).start();
                String portOutput = IoUtil.read(portProcess.getInputStream(), StandardCharsets.UTF_8);
                if (portProcess.waitFor(10, TimeUnit.SECONDS) && portProcess.exitValue() == 0
                        && portOutput.contains(dbPort)) {
                    return container[1];
                }
            }
            log.warn("未匹配到端口映射的 MySQL å®¹å™¨ï¼Œä½¿ç”¨ç¬¬ä¸€ä¸ª: {}", containers.get(0)[1]);
            return containers.get(0)[1];
        } catch (IOException e) {
            log.warn("检测 Docker çŽ¯å¢ƒå¤±è´¥: {}", e.getMessage());
            return null;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        }
    }
}