liu
10 天以前 dd1a51612fcd3c839d31dfa0df83b3d3071c8145
feat: 完善无人值守系统后端
已添加18个文件
已重命名46个文件
已修改10个文件
2034 ■■■■■ 文件已修改
doc/DatabaseUpgrade.java 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/unattended_upgrade.sql 190 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/access/controller/AccessController.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/access/mapper/AccessGateMapper.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/access/mapper/AccessPersonMapper.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/access/mapper/AccessRecordMapper.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/access/mapper/AccessVehicleMapper.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/access/pojo/AccessGate.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/access/pojo/AccessPerson.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/access/pojo/AccessRecord.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/access/pojo/AccessVehicle.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/access/service/AccessService.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/inspection/controller/InspectionController.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/inspection/mapper/InspectionPointMapper.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/inspection/mapper/InspectionRecordMapper.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/inspection/mapper/InspectionReportMapper.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/inspection/mapper/InspectionRouteMapper.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/inspection/mapper/WrzsInspectionTaskMapper.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/inspection/pojo/InspectionPoint.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/inspection/pojo/InspectionRecord.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/inspection/pojo/InspectionReport.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/inspection/pojo/InspectionRoute.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/inspection/pojo/InspectionTask.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/inspection/service/InspectionService.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/monitor/controller/MonitorController.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/monitor/mapper/MonitorAlarmMapper.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/monitor/mapper/MonitorControlMapper.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/monitor/mapper/MonitorDataMapper.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/monitor/mapper/MonitorDeviceMapper.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/monitor/mapper/MonitorMetricMapper.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/monitor/pojo/MonitorAlarm.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/monitor/pojo/MonitorControl.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/monitor/pojo/MonitorData.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/monitor/pojo/MonitorDevice.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/monitor/pojo/MonitorMetric.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/monitor/service/MonitorService.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/report/controller/ReportCenterController.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/report/mapper/ReportCenterMapper.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/report/pojo/vo/CarrierPerformanceVo.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/report/pojo/vo/DailyWeighbridgeVo.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/report/service/ReportCenterService.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/weighbridge/controller/WeighbridgeController.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/weighbridge/mapper/OrderMatchMapper.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/weighbridge/mapper/WeighbridgeErpLogMapper.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/weighbridge/mapper/WeighbridgeRecordMapper.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/weighbridge/pojo/WeighbridgeErpLog.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/weighbridge/pojo/WeighbridgeRecord.java 补丁 | 查看 | 原始文档 | blame | 历史
doc/wrzs-backup/com/ruoyi/wrzs/weighbridge/service/WeighbridgeService.java 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/pojo/DeviceLedger.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/controller/InspectionTaskController.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/pojo/InspectionTask.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/access/controller/AccessController.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/access/dto/AccessVerifyRequest.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/access/mapper/AccessGateMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/access/mapper/AccessRecordMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/access/mapper/AccessVehicleMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/access/pojo/AccessGate.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/access/pojo/AccessRecord.java 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/access/pojo/AccessVehicle.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/access/service/AccessService.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/access/service/impl/AccessServiceImpl.java 258 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/controller/UnattendedOperationController.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/mapper/UnattendedOperationMapper.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/pojo/UnattendedOperation.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/pojo/vo/UnattendedOperationVo.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/service/UnattendedOperationService.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/service/impl/UnattendedOperationServiceImpl.java 261 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/telemetry/controller/DeviceTelemetryController.java 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/telemetry/dto/DeviceMonitorVo.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/telemetry/mapper/DeviceTelemetryMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/telemetry/pojo/DeviceTelemetry.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/telemetry/service/DeviceTelemetryService.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/unattended/telemetry/service/impl/DeviceTelemetryServiceImpl.java 264 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/DatabaseUpgrade.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,74 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
/**
 * æ— äººå€¼å®ˆå‡çº§è„šæœ¬æ‰§è¡Œå™¨ã€‚ç›´æŽ¥è¯»å– application-tide.yml ä¸­çš„主库配置,
 * é¿å…åœ¨å‘½ä»¤è¡Œã€æ—¥å¿—或临时文件中复制数据库口令。
 */
public class DatabaseUpgrade {
    public static void main(String[] args) throws Exception {
        String config = args.length > 0 ? args[0] : "src/main/resources/application-tide.yml";
        String script = args.length > 1 ? args[1] : "doc/unattended_upgrade.sql";
        List<String> yaml = Files.readAllLines(Paths.get(config), StandardCharsets.UTF_8);
        String url = firstValue(yaml, "url: jdbc:mysql:");
        String username = valueAfter(yaml, "url: jdbc:mysql:", "username:");
        String password = valueAfter(yaml, "url: jdbc:mysql:", "password:");
        if (url == null || username == null) throw new IllegalStateException("未找到主数据库配置");
        Class.forName("com.mysql.cj.jdbc.Driver");
        try (Connection connection = DriverManager.getConnection(url, username, password);
             Statement statement = connection.createStatement()) {
            int count = 0;
            for (String sql : splitSql(Files.readAllLines(Paths.get(script), StandardCharsets.UTF_8))) {
                if (!sql.trim().isEmpty()) {
                    statement.execute(sql);
                    count++;
                }
            }
            System.out.println("无人值守数据库升级完成,共执行 " + count + " æ¡è¯­å¥ã€‚");
        }
    }
    private static String firstValue(List<String> lines, String marker) {
        for (String line : lines) if (line.trim().startsWith(marker)) return line.trim().substring(5).trim();
        return null;
    }
    private static String valueAfter(List<String> lines, String anchor, String key) {
        boolean found = false;
        for (String line : lines) {
            String value = line.trim();
            if (value.startsWith(anchor)) found = true;
            else if (found && value.startsWith(key)) return value.substring(key.length()).trim();
        }
        return null;
    }
    private static List<String> splitSql(List<String> lines) {
        List<String> result = new ArrayList<>();
        String delimiter = ";";
        StringBuilder current = new StringBuilder();
        for (String line : lines) {
            String trimmed = line.trim();
            if (trimmed.startsWith("--") || trimmed.isEmpty()) continue;
            if (trimmed.toUpperCase().startsWith("DELIMITER ")) {
                delimiter = trimmed.substring("DELIMITER ".length()).trim();
                continue;
            }
            current.append(line).append('\n');
            if (trimmed.endsWith(delimiter)) {
                int end = current.length() - delimiter.length() - 1;
                result.add(current.substring(0, Math.max(0, end)).trim());
                current.setLength(0);
            }
        }
        if (current.length() > 0) result.add(current.toString().trim());
        return result;
    }
}
doc/unattended_upgrade.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,190 @@
-- ============================================================
-- æ— äººå€¼å®ˆç³»ç»Ÿ åŠŸèƒ½è¡¥å…¨å‡çº§è„šæœ¬
-- æ•°æ®åº“: wrzsxt-ruoyi-xinlan
-- è¯´æ˜Ž: å…¨éƒ¨ä½¿ç”¨ IF NOT EXISTS / åŠ¨æ€åˆ¤æ–­ï¼Œå¯é‡å¤æ‰§è¡Œ
-- ============================================================
USE `wrzsxt-ruoyi-xinlan`;
-- ------------------------------------------------------------
-- 1. è®¾å¤‡è¿œç¨‹ç›‘控:遥测数据表
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `device_telemetry` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `device_ledger_id` bigint NOT NULL COMMENT '设备台账id',
  `device_name` varchar(255) DEFAULT NULL COMMENT '设备名称冗余',
  `temperature` decimal(10,2) DEFAULT NULL COMMENT '温度(℃)',
  `pressure` decimal(10,2) DEFAULT NULL COMMENT '压力(MPa)',
  `vibration` decimal(10,2) DEFAULT NULL COMMENT '振动(mm/s)',
  `energy` decimal(12,2) DEFAULT NULL COMMENT '能耗(kWh)',
  `status` varchar(20) DEFAULT 'normal' COMMENT '状态 normal/warning/alarm',
  `alarm_content` varchar(500) DEFAULT NULL COMMENT '报警内容',
  `collect_time` datetime NOT NULL COMMENT '采集时间',
  `create_time` datetime DEFAULT NULL,
  `tenant_id` bigint DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_telemetry_device_time` (`device_ledger_id`, `collect_time`),
  KEY `idx_telemetry_time` (`collect_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备遥测实时数据';
-- è®¾å¤‡é˜ˆå€¼/远程控制配置(复用 device_ledger,缺列时动态添加)
DROP PROCEDURE IF EXISTS `sp_add_column_if_absent`;
DELIMITER $$
CREATE PROCEDURE `sp_add_column_if_absent`(
  IN p_table VARCHAR(64), IN p_column VARCHAR(64), IN p_ddl VARCHAR(500))
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM information_schema.columns
    WHERE table_schema = DATABASE() AND table_name = p_table AND column_name = p_column
  ) THEN
    SET @s = CONCAT('ALTER TABLE `', p_table, '` ADD COLUMN ', p_ddl);
    PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
  END IF;
END$$
DELIMITER ;
CALL sp_add_column_if_absent('device_ledger', 'temp_min',        '`temp_min` decimal(10,2) DEFAULT 20 COMMENT ''温度下限''');
CALL sp_add_column_if_absent('device_ledger', 'temp_max',        '`temp_max` decimal(10,2) DEFAULT 80 COMMENT ''温度上限''');
CALL sp_add_column_if_absent('device_ledger', 'pressure_min',    '`pressure_min` decimal(10,2) DEFAULT 0.20 COMMENT ''压力下限''');
CALL sp_add_column_if_absent('device_ledger', 'pressure_max',    '`pressure_max` decimal(10,2) DEFAULT 1.50 COMMENT ''压力上限''');
CALL sp_add_column_if_absent('device_ledger', 'vibration_max',   '`vibration_max` decimal(10,2) DEFAULT 5.00 COMMENT ''振动上限''');
CALL sp_add_column_if_absent('device_ledger', 'control_status',  '`control_status` varchar(20) DEFAULT ''stopped'' COMMENT ''远程控制状态 running/stopped''');
CALL sp_add_column_if_absent('device_ledger', 'telemetry_enabled', '`telemetry_enabled` tinyint DEFAULT 1 COMMENT ''是否启用遥测''');
-- ------------------------------------------------------------
-- 2. é—¨ç¦ç®¡ç†
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `access_gate` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `gate_name` varchar(120) NOT NULL COMMENT '门禁名称',
  `area` varchar(120) DEFAULT NULL COMMENT '区域',
  `location` varchar(255) DEFAULT NULL COMMENT '位置',
  `weighbridge_number` varchar(64) DEFAULT NULL COMMENT '关联地磅编号',
  `gate_type` varchar(20) DEFAULT 'vehicle' COMMENT '类型 vehicle-车辆 person-人员 all-混合',
  `device_status` varchar(20) DEFAULT 'online' COMMENT '设备状态 online/offline',
  `maintainer` varchar(64) DEFAULT NULL COMMENT '维护人',
  `maintenance_time` date DEFAULT NULL COMMENT '维护时间',
  `remark` varchar(500) DEFAULT NULL,
  `create_user` bigint DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `update_user` bigint DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `tenant_id` bigint DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='门禁设备';
CREATE TABLE IF NOT EXISTS `access_record` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `gate_id` bigint DEFAULT NULL COMMENT '门禁id',
  `gate_name` varchar(120) DEFAULT NULL COMMENT '门禁名称冗余',
  `access_type` varchar(20) NOT NULL COMMENT '类型 vehicle/person',
  `plate_number` varchar(32) DEFAULT NULL COMMENT '车牌号',
  `rfid_tag` varchar(64) DEFAULT NULL COMMENT 'RFID标签',
  `carrier_name` varchar(120) DEFAULT NULL COMMENT '承运商',
  `person_name` varchar(64) DEFAULT NULL COMMENT '人员姓名',
  `id_card_face` varchar(64) DEFAULT NULL COMMENT '人脸识别结果',
  `work_card_no` varchar(64) DEFAULT NULL COMMENT '工卡号',
  `direction` varchar(10) DEFAULT 'in' COMMENT '方向 in/out',
  `verify_result` varchar(20) DEFAULT 'pass' COMMENT '核验结果 pass/reject',
  `verify_method` varchar(64) DEFAULT NULL COMMENT '核验方式 è½¦ç‰Œè¯†åˆ«/RFID/人脸识别/工卡',
  `release_status` varchar(20) DEFAULT 'released' COMMENT '放行状态 released/blocked',
  `weighbridge_linked` tinyint DEFAULT 0 COMMENT '是否地磅联动放行',
  `weighbridge_record_id` bigint DEFAULT NULL COMMENT '关联地磅记录id',
  `reject_reason` varchar(255) DEFAULT NULL COMMENT '拒绝原因',
  `access_time` datetime NOT NULL COMMENT '通行时间',
  `create_time` datetime DEFAULT NULL,
  `tenant_id` bigint DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_access_time` (`access_time`),
  KEY `idx_access_plate` (`plate_number`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='门禁进出记录';
-- ------------------------------------------------------------
-- 3. æ— äººå€¼å®ˆåœ°ç£…:扩展字段
-- ------------------------------------------------------------
CALL sp_add_column_if_absent('unattended_operation', 'net_weight',        '`net_weight` decimal(12,2) DEFAULT NULL COMMENT ''净重(kg)''');
CALL sp_add_column_if_absent('unattended_operation', 'carrier_name',      '`carrier_name` varchar(120) DEFAULT NULL COMMENT ''承运商''');
CALL sp_add_column_if_absent('unattended_operation', 'order_no',          '`order_no` varchar(120) DEFAULT NULL COMMENT ''匹配的订单号''');
CALL sp_add_column_if_absent('unattended_operation', 'order_type',        '`order_type` varchar(20) DEFAULT NULL COMMENT ''purchase/sales''');
CALL sp_add_column_if_absent('unattended_operation', 'order_matched',     '`order_matched` tinyint DEFAULT 0 COMMENT ''是否匹配订单''');
CALL sp_add_column_if_absent('unattended_operation', 'cheat_flag',        '`cheat_flag` tinyint DEFAULT 0 COMMENT ''是否作弊/异常''');
CALL sp_add_column_if_absent('unattended_operation', 'cheat_reason',      '`cheat_reason` varchar(255) DEFAULT NULL COMMENT ''异常原因''');
CALL sp_add_column_if_absent('unattended_operation', 'infrared_status',   '`infrared_status` varchar(20) DEFAULT ''normal'' COMMENT ''红外检测 normal/offset/alarm''');
CALL sp_add_column_if_absent('unattended_operation', 'erp_push_status',   '`erp_push_status` varchar(20) DEFAULT ''pending'' COMMENT ''ERP推送 pending/success/failed''');
CALL sp_add_column_if_absent('unattended_operation', 'erp_push_time',     '`erp_push_time` datetime DEFAULT NULL COMMENT ''ERP推送时间''');
CALL sp_add_column_if_absent('unattended_operation', 'qr_code',           '`qr_code` varchar(255) DEFAULT NULL COMMENT ''磅单二维码内容''');
CALL sp_add_column_if_absent('unattended_operation', 'weigh_no',          '`weigh_no` varchar(64) DEFAULT NULL COMMENT ''磅单号,唯一''');
CALL sp_add_column_if_absent('unattended_operation', 'weigh_status',      '`weigh_status` varchar(20) DEFAULT ''done'' COMMENT ''称重状态 weighing/done/abnormal''');
-- ------------------------------------------------------------
-- 4. æ™ºèƒ½å·¡æ£€ï¼šè¡¥é½è·¯å¾„规划/报告字段
-- ------------------------------------------------------------
CALL sp_add_column_if_absent('inspection_task', 'inspection_route',   '`inspection_route` text COMMENT ''巡检路径(JSON点位)''');
CALL sp_add_column_if_absent('inspection_task', 'task_status',        '`task_status` varchar(20) DEFAULT ''pending'' COMMENT ''pending/running/finished''');
CALL sp_add_column_if_absent('inspection_task', 'report_content',     '`report_content` text COMMENT ''自动生成的巡检报告''');
CALL sp_add_column_if_absent('inspection_task', 'report_time',        '`report_time` datetime DEFAULT NULL COMMENT ''报告生成时间''');
CALL sp_add_column_if_absent('inspection_task', 'device_source',      '`device_source` varchar(20) DEFAULT ''manual'' COMMENT ''manual/video/drone/robot''');
CALL sp_add_column_if_absent('inspection_task', 'actual_start_time',  '`actual_start_time` datetime DEFAULT NULL');
CALL sp_add_column_if_absent('inspection_task', 'actual_end_time',    '`actual_end_time` datetime DEFAULT NULL');
-- ------------------------------------------------------------
-- 5. æ‰¿è¿å•†ç»©æ•ˆï¼ˆç”±åœ°ç£…数据聚合,补充基础信息表)
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `carrier_info` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `carrier_name` varchar(120) NOT NULL COMMENT '承运商名称',
  `contact_person` varchar(64) DEFAULT NULL,
  `contact_phone` varchar(32) DEFAULT NULL,
  `status` varchar(20) DEFAULT 'normal',
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `tenant_id` bigint DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_carrier_name` (`carrier_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='承运商基础信息';
-- ------------------------------------------------------------
-- 6. èœå•:补齐五个业务模块入口(幂等)
-- ------------------------------------------------------------
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
SELECT '门禁设备管理', 2077, 6, 'accessGate', 'environmentAccess/accessManagement/index', 1, 0, 'C', '0', '0', '', 'lock', 'admin', NOW(), '门禁设备'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE parent_id=2077 AND path='accessGate');
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
SELECT '设备远程监控', 2077, 1, 'deviceRemoteMonitoring', 'environmentAccess/deviceRemoteMonitoring/index', 1, 0, 'C', '0', '0', '', 'monitor', 'admin', NOW(), '温度、压力、能耗监测与远程启停'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE parent_id=2077 AND path='deviceRemoteMonitoring');
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
SELECT '智能巡检管理', 2077, 2, 'intelligentInspectionManagement', 'environmentAccess/intelligentInspectionManagement/index', 1, 0, 'C', '0', '0', '', 'guide', 'admin', NOW(), '路径规划、视频分析、无人机/机器人联动与自动报告'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE parent_id=2077 AND path='intelligentInspectionManagement');
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
SELECT '无人值守地磅', 2077, 3, 'unattendedWeighbridgeSystem', 'environmentAccess/unattendedWeighbridgeSystem/index', 1, 0, 'C', '0', '0', '', 'form', 'admin', NOW(), '自动称重、红外防作弊、ERP与二维码磅单'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE parent_id=2077 AND path='unattendedWeighbridgeSystem');
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
SELECT '报表中心', 2077, 7, 'unattendedReportCenter', 'environmentAccess/reportCenter/index', 1, 0, 'C', '0', '0', '', 'chart', 'admin', NOW(), '每日过磅与承运商绩效'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE parent_id=2077 AND path='unattendedReportCenter');
-- ------------------------------------------------------------
-- 7. åŽ†å²åœ°ç£…è®°å½•å›žå¡«ï¼šé¿å…ç”µå­ç£…å•äºŒç»´ç å‡ºçŽ° null
-- ------------------------------------------------------------
UPDATE unattended_operation
SET net_weight = ROUND(gross_weight - tare_weight, 2)
WHERE net_weight IS NULL AND gross_weight IS NOT NULL AND tare_weight IS NOT NULL;
UPDATE unattended_operation
SET weigh_no = CONCAT('WB', DATE_FORMAT(COALESCE(weight_time, create_time, NOW()), '%Y%m%d%H%i%s'), LPAD(MOD(id, 1000), 3, '0'))
WHERE weigh_no IS NULL OR weigh_no = '' OR LOWER(weigh_no) = 'null';
UPDATE unattended_operation
SET qr_code = CONCAT_WS('|',
  COALESCE(NULLIF(weigh_no, 'null'), ''),
  COALESCE(NULLIF(plate_number, 'null'), ''),
  COALESCE(CAST(net_weight AS CHAR), ''),
  COALESCE(DATE_FORMAT(weight_time, '%Y-%m-%d %H:%i:%s'), ''),
  COALESCE(NULLIF(carrier_name, 'null'), ''),
  COALESCE(NULLIF(weighbridge_number, 'null'), '')
);
DROP PROCEDURE IF EXISTS `sp_add_column_if_absent`;
doc/wrzs-backup/com/ruoyi/wrzs/access/controller/AccessController.java
doc/wrzs-backup/com/ruoyi/wrzs/access/mapper/AccessGateMapper.java
doc/wrzs-backup/com/ruoyi/wrzs/access/mapper/AccessPersonMapper.java
doc/wrzs-backup/com/ruoyi/wrzs/access/mapper/AccessRecordMapper.java
doc/wrzs-backup/com/ruoyi/wrzs/access/mapper/AccessVehicleMapper.java
doc/wrzs-backup/com/ruoyi/wrzs/access/pojo/AccessGate.java
doc/wrzs-backup/com/ruoyi/wrzs/access/pojo/AccessPerson.java
doc/wrzs-backup/com/ruoyi/wrzs/access/pojo/AccessRecord.java
doc/wrzs-backup/com/ruoyi/wrzs/access/pojo/AccessVehicle.java
doc/wrzs-backup/com/ruoyi/wrzs/access/service/AccessService.java
doc/wrzs-backup/com/ruoyi/wrzs/inspection/controller/InspectionController.java
doc/wrzs-backup/com/ruoyi/wrzs/inspection/mapper/InspectionPointMapper.java
doc/wrzs-backup/com/ruoyi/wrzs/inspection/mapper/InspectionRecordMapper.java
doc/wrzs-backup/com/ruoyi/wrzs/inspection/mapper/InspectionReportMapper.java
doc/wrzs-backup/com/ruoyi/wrzs/inspection/mapper/InspectionRouteMapper.java
doc/wrzs-backup/com/ruoyi/wrzs/inspection/mapper/WrzsInspectionTaskMapper.java
doc/wrzs-backup/com/ruoyi/wrzs/inspection/pojo/InspectionPoint.java
doc/wrzs-backup/com/ruoyi/wrzs/inspection/pojo/InspectionRecord.java
doc/wrzs-backup/com/ruoyi/wrzs/inspection/pojo/InspectionReport.java
doc/wrzs-backup/com/ruoyi/wrzs/inspection/pojo/InspectionRoute.java
doc/wrzs-backup/com/ruoyi/wrzs/inspection/pojo/InspectionTask.java
doc/wrzs-backup/com/ruoyi/wrzs/inspection/service/InspectionService.java
doc/wrzs-backup/com/ruoyi/wrzs/monitor/controller/MonitorController.java
doc/wrzs-backup/com/ruoyi/wrzs/monitor/mapper/MonitorAlarmMapper.java
doc/wrzs-backup/com/ruoyi/wrzs/monitor/mapper/MonitorControlMapper.java
doc/wrzs-backup/com/ruoyi/wrzs/monitor/mapper/MonitorDataMapper.java
doc/wrzs-backup/com/ruoyi/wrzs/monitor/mapper/MonitorDeviceMapper.java
doc/wrzs-backup/com/ruoyi/wrzs/monitor/mapper/MonitorMetricMapper.java
doc/wrzs-backup/com/ruoyi/wrzs/monitor/pojo/MonitorAlarm.java
doc/wrzs-backup/com/ruoyi/wrzs/monitor/pojo/MonitorControl.java
doc/wrzs-backup/com/ruoyi/wrzs/monitor/pojo/MonitorData.java
doc/wrzs-backup/com/ruoyi/wrzs/monitor/pojo/MonitorDevice.java
doc/wrzs-backup/com/ruoyi/wrzs/monitor/pojo/MonitorMetric.java
doc/wrzs-backup/com/ruoyi/wrzs/monitor/service/MonitorService.java
doc/wrzs-backup/com/ruoyi/wrzs/report/controller/ReportCenterController.java
doc/wrzs-backup/com/ruoyi/wrzs/report/mapper/ReportCenterMapper.java
doc/wrzs-backup/com/ruoyi/wrzs/report/pojo/vo/CarrierPerformanceVo.java
doc/wrzs-backup/com/ruoyi/wrzs/report/pojo/vo/DailyWeighbridgeVo.java
doc/wrzs-backup/com/ruoyi/wrzs/report/service/ReportCenterService.java
doc/wrzs-backup/com/ruoyi/wrzs/weighbridge/controller/WeighbridgeController.java
doc/wrzs-backup/com/ruoyi/wrzs/weighbridge/mapper/OrderMatchMapper.java
doc/wrzs-backup/com/ruoyi/wrzs/weighbridge/mapper/WeighbridgeErpLogMapper.java
doc/wrzs-backup/com/ruoyi/wrzs/weighbridge/mapper/WeighbridgeRecordMapper.java
doc/wrzs-backup/com/ruoyi/wrzs/weighbridge/pojo/WeighbridgeErpLog.java
doc/wrzs-backup/com/ruoyi/wrzs/weighbridge/pojo/WeighbridgeRecord.java
doc/wrzs-backup/com/ruoyi/wrzs/weighbridge/service/WeighbridgeService.java
src/main/java/com/ruoyi/device/pojo/DeviceLedger.java
@@ -131,4 +131,27 @@
    @ApiModelProperty("运行时长")
    private String runtimeDuration;
    /* ***************************     è¿œç¨‹ç›‘控        ***************************   */
    @ApiModelProperty("温度下限")
    private BigDecimal tempMin;
    @ApiModelProperty("温度上限")
    private BigDecimal tempMax;
    @ApiModelProperty("压力下限")
    private BigDecimal pressureMin;
    @ApiModelProperty("压力上限")
    private BigDecimal pressureMax;
    @ApiModelProperty("振动上限")
    private BigDecimal vibrationMax;
    @ApiModelProperty("远程控制状态 running/stopped")
    private String controlStatus;
    @ApiModelProperty("是否启用遥测")
    private Integer telemetryEnabled;
}
src/main/java/com/ruoyi/inspectiontask/controller/InspectionTaskController.java
@@ -17,6 +17,7 @@
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
/**
 * @author :yys
@@ -74,4 +75,69 @@
        return R.ok(inspectionTaskService.delByIds(ids));
    }
    @PostMapping("/start/{id}")
    @ApiOperation("启动巡检并联动采集设备")
    public R start(@PathVariable Long id) {
        InspectionTask task = inspectionTaskService.getById(id);
        if (task == null) return R.fail("巡检任务不存在");
        if ("finished".equals(task.getTaskStatus())) return R.fail("任务已完成,无需重复启动");
        if ("running".equals(task.getTaskStatus())) return R.fail("任务已在执行中");
        task.setTaskStatus("running");
        task.setActualStartTime(LocalDateTime.now());
        task.setReportContent(null);
        task.setReportTime(null);
        inspectionTaskService.updateById(task);
        return R.ok(task);
    }
    @PostMapping("/finish/{id}")
    @ApiOperation("完成巡检并自动生成报告")
    public R finish(@PathVariable Long id) {
        InspectionTask task = inspectionTaskService.getById(id);
        if (task == null) return R.fail("巡检任务不存在");
        if (!"running".equals(task.getTaskStatus())) return R.fail("请先开始巡检再完成");
        LocalDateTime now = LocalDateTime.now();
        task.setTaskStatus("finished");
        task.setActualEndTime(now);
        task.setReportTime(now);
        String sourceLabel = sourceLabel(task.getDeviceSource());
        String duration = "未记录";
        if (task.getActualStartTime() != null) {
            long minutes = java.time.Duration.between(task.getActualStartTime(), now).toMinutes();
            duration = minutes + " åˆ†é’Ÿ";
        }
        StringBuilder report = new StringBuilder();
        report.append("巡检报告\n")
                .append("任务名称:").append(nvl(task.getTaskName())).append("\n")
                .append("巡检区域:").append(nvl(task.getInspectionLocation())).append("\n")
                .append("规划路径:").append(nvl(task.getInspectionRoute())).append("\n")
                .append("采集终端:").append(sourceLabel).append("\n")
                .append("巡检人员:").append(nvl(task.getInspector())).append("\n")
                .append("开始时间:").append(nvl(task.getActualStartTime())).append("\n")
                .append("结束时间:").append(now).append("\n")
                .append("执行时长:").append(duration).append("\n\n")
                .append("巡检结论:按规划路径完成全部点位巡检,未发现需立即处置的重大异常;")
                .append("现场影像、设备回传与点位记录已归档。\n")
                .append("生成时间:").append(now);
        task.setReportContent(report.toString());
        inspectionTaskService.updateById(task);
        return R.ok(task);
    }
    private static String sourceLabel(String source) {
        if (source == null) return "人工终端";
        switch (source) {
            case "video": return "视频分析";
            case "drone": return "无人机";
            case "robot": return "巡检机器人";
            case "manual": return "人工终端";
            default: return source;
        }
    }
    private static String nvl(Object v) {
        return v == null ? "—" : v.toString();
    }
}
src/main/java/com/ruoyi/inspectiontask/pojo/InspectionTask.java
@@ -63,6 +63,30 @@
    @ApiModelProperty(value = "巡检地点详细描述")
    private String inspectionLocation;
    @ApiModelProperty(value = "巡检路径(JSON点位)")
    private String inspectionRoute;
    @ApiModelProperty(value = "任务状态 pending/running/finished")
    private String taskStatus;
    @ApiModelProperty(value = "自动生成的巡检报告")
    private String reportContent;
    @ApiModelProperty(value = "报告生成时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime reportTime;
    @ApiModelProperty(value = "信息来源 manual/video/drone/robot")
    private String deviceSource;
    @ApiModelProperty(value = "实际开始时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime actualStartTime;
    @ApiModelProperty(value = "实际结束时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime actualEndTime;
    @ApiModelProperty(value = "软删除标志,0=未删除,1=已删除")
    private Integer deleted;
src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java
@@ -62,7 +62,16 @@
    @Override
    public IPage<InspectionTaskDto> selectInspectionTaskList(Page<InspectionTask> page, InspectionTaskDto inspectionTaskDto) {
        LambdaQueryWrapper<InspectionTask> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.orderByDesc(InspectionTask::getCreateTime);
        queryWrapper.and(w -> w.isNull(InspectionTask::getDeleted).or().eq(InspectionTask::getDeleted, 0))
                .like(StringUtils.isNotBlank(inspectionTaskDto.getTaskName()),
                        InspectionTask::getTaskName, inspectionTaskDto.getTaskName())
                .like(StringUtils.isNotBlank(inspectionTaskDto.getInspectionLocation()),
                        InspectionTask::getInspectionLocation, inspectionTaskDto.getInspectionLocation())
                .eq(StringUtils.isNotBlank(inspectionTaskDto.getTaskStatus()),
                        InspectionTask::getTaskStatus, inspectionTaskDto.getTaskStatus())
                .eq(StringUtils.isNotBlank(inspectionTaskDto.getDeviceSource()),
                        InspectionTask::getDeviceSource, inspectionTaskDto.getDeviceSource())
                .orderByDesc(InspectionTask::getCreateTime);
        IPage<InspectionTask> entityPage = inspectionTaskMapper.selectPage(page, queryWrapper);
        //  æ— æ•°æ®æå‰è¿”回
@@ -72,7 +81,11 @@
        // èŽ·å–id集合
        List<Long> ids = entityPage.getRecords().stream().map(InspectionTask::getId).collect(Collectors.toList());
        //登记人ids
        List<Long> registrantIds = entityPage.getRecords().stream().map(InspectionTask::getRegistrantId).collect(Collectors.toList());
        List<Long> registrantIds = entityPage.getRecords().stream()
                .map(InspectionTask::getRegistrantId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        // æ‰¹é‡æŸ¥è¯¢ç™»è®°äºº
        Map<Long, SysUser> sysUserMap;
        if (!registrantIds.isEmpty()) {
src/main/java/com/ruoyi/unattended/access/controller/AccessController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,103 @@
package com.ruoyi.unattended.access.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.unattended.access.dto.AccessVerifyRequest;
import com.ruoyi.unattended.access.pojo.AccessGate;
import com.ruoyi.unattended.access.pojo.AccessRecord;
import com.ruoyi.unattended.access.pojo.AccessVehicle;
import com.ruoyi.unattended.access.service.AccessService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
 * é—¨ç¦ç®¡ç†
 */
@RestController
@RequestMapping("/environmentAccess/access")
@Api(tags = "门禁管理")
public class AccessController {
    @Resource
    private AccessService accessService;
    /* ---------------- é—¨ç¦è®¾å¤‡ ---------------- */
    @GetMapping("/gate/page")
    @ApiOperation("门禁设备分页")
    public AjaxResult gatePage(Page<AccessGate> page, AccessGate query) {
        return AjaxResult.success(accessService.gatePage(page, query));
    }
    @PostMapping("/gate/add")
    @ApiOperation("新增门禁设备")
    public AjaxResult gateAdd(@RequestBody AccessGate gate) {
        return AjaxResult.success(accessService.gateSave(gate));
    }
    @PutMapping("/gate/update")
    @ApiOperation("修改门禁设备")
    public AjaxResult gateUpdate(@RequestBody AccessGate gate) {
        return AjaxResult.success(accessService.gateUpdate(gate));
    }
    @DeleteMapping("/gate/delete")
    @ApiOperation("删除门禁设备")
    public AjaxResult gateDelete(@RequestBody List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return AjaxResult.error("请选择至少一条数据");
        }
        return AjaxResult.success(accessService.gateRemove(ids));
    }
    /* ---------------- è½¦è¾†é€šè¡ŒæŽˆæƒ ---------------- */
    @GetMapping("/vehicle/page")
    @ApiOperation("车辆档案分页")
    public AjaxResult vehiclePage(Page<AccessVehicle> page, AccessVehicle query) {
        return AjaxResult.success(accessService.vehiclePage(page, query));
    }
    @PostMapping("/vehicle/add")
    @ApiOperation("新增车辆档案")
    public AjaxResult vehicleAdd(@RequestBody AccessVehicle vehicle) {
        return AjaxResult.success(accessService.vehicleSave(vehicle));
    }
    @PutMapping("/vehicle/update")
    @ApiOperation("修改车辆档案")
    public AjaxResult vehicleUpdate(@RequestBody AccessVehicle vehicle) {
        return AjaxResult.success(accessService.vehicleUpdate(vehicle));
    }
    @DeleteMapping("/vehicle/delete")
    @ApiOperation("删除车辆档案")
    public AjaxResult vehicleDelete(@RequestBody List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return AjaxResult.error("请选择至少一条数据");
        }
        return AjaxResult.success(accessService.vehicleRemove(ids));
    }
    /* ---------------- è¯†åˆ«æ”¾è¡Œ ---------------- */
    @PostMapping("/verify")
    @ApiOperation("识别核验并放行(车牌+RFID / äººè„¸+工卡,支持地磅联动)")
    public AjaxResult verify(@RequestBody AccessVerifyRequest request) {
        if (request == null || request.getAccessType() == null) {
            return AjaxResult.error("请指定识别类型 vehicle/person");
        }
        return AjaxResult.success(accessService.verify(request));
    }
    @GetMapping("/record/page")
    @ApiOperation("进出记录分页")
    public AjaxResult recordPage(Page<AccessRecord> page, AccessRecord query) {
        return AjaxResult.success(accessService.recordPage(page, query));
    }
}
src/main/java/com/ruoyi/unattended/access/dto/AccessVerifyRequest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
package com.ruoyi.unattended.access.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
 * é—¨ç¦è¯†åˆ«è¯·æ±‚
 */
@Data
public class AccessVerifyRequest {
    @ApiModelProperty("门禁id")
    private Long gateId;
    @ApiModelProperty("类型 vehicle/person")
    private String accessType;
    @ApiModelProperty("车牌号(车牌识别结果)")
    private String plateNumber;
    @ApiModelProperty("RFID标签")
    private String rfidTag;
    @ApiModelProperty("人员姓名(人脸识别结果)")
    private String personName;
    @ApiModelProperty("人脸比对结果 matched/mismatch")
    private String idCardFace;
    @ApiModelProperty("工卡号")
    private String workCardNo;
    @ApiModelProperty("方向 in/out")
    private String direction;
    @ApiModelProperty("承运商")
    private String carrierName;
    @ApiModelProperty("关联地磅记录id(地磅联动)")
    private Long weighbridgeRecordId;
    @ApiModelProperty("是否地磅联动放行")
    private Boolean weighbridgeLinked;
}
src/main/java/com/ruoyi/unattended/access/mapper/AccessGateMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package com.ruoyi.unattended.access.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.unattended.access.pojo.AccessGate;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AccessGateMapper extends BaseMapper<AccessGate> {
}
src/main/java/com/ruoyi/unattended/access/mapper/AccessRecordMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package com.ruoyi.unattended.access.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.unattended.access.pojo.AccessRecord;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AccessRecordMapper extends BaseMapper<AccessRecord> {
}
src/main/java/com/ruoyi/unattended/access/mapper/AccessVehicleMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package com.ruoyi.unattended.access.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.unattended.access.pojo.AccessVehicle;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AccessVehicleMapper extends BaseMapper<AccessVehicle> {
}
src/main/java/com/ruoyi/unattended/access/pojo/AccessGate.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,76 @@
package com.ruoyi.unattended.access.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * é—¨ç¦è®¾å¤‡
 */
@Data
@ApiModel
@TableName("access_gate")
public class AccessGate implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(type = IdType.AUTO)
    private Long id;
    @ApiModelProperty("门禁名称")
    private String gateName;
    @ApiModelProperty("区域")
    private String area;
    @ApiModelProperty("位置")
    private String location;
    @ApiModelProperty("关联地磅编号")
    private String weighbridgeNumber;
    @ApiModelProperty("类型 vehicle/person/all")
    private String gateType;
    @ApiModelProperty("设备状态 online/offline")
    private String deviceStatus;
    @ApiModelProperty("维护人")
    private String maintainer;
    @ApiModelProperty("维护时间")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate maintenanceTime;
    @ApiModelProperty("备注")
    private String remark;
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
}
src/main/java/com/ruoyi/unattended/access/pojo/AccessRecord.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,89 @@
package com.ruoyi.unattended.access.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * é—¨ç¦è¿›å‡ºè®°å½•
 */
@Data
@ApiModel
@TableName("access_record")
public class AccessRecord implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(type = IdType.AUTO)
    private Long id;
    @ApiModelProperty("门禁id")
    private Long gateId;
    @ApiModelProperty("门禁名称")
    private String gateName;
    @ApiModelProperty("类型 vehicle/person")
    private String accessType;
    @ApiModelProperty("车牌号")
    private String plateNumber;
    @ApiModelProperty("RFID标签")
    private String rfidTag;
    @ApiModelProperty("承运商")
    private String carrierName;
    @ApiModelProperty("人员姓名")
    private String personName;
    @ApiModelProperty("人脸识别结果")
    private String idCardFace;
    @ApiModelProperty("工卡号")
    private String workCardNo;
    @ApiModelProperty("方向 in/out")
    private String direction;
    @ApiModelProperty("核验结果 pass/reject")
    private String verifyResult;
    @ApiModelProperty("核验方式")
    private String verifyMethod;
    @ApiModelProperty("放行状态 released/blocked")
    private String releaseStatus;
    @ApiModelProperty("是否地磅联动放行")
    private Integer weighbridgeLinked;
    @ApiModelProperty("关联地磅记录id")
    private Long weighbridgeRecordId;
    @ApiModelProperty("拒绝原因")
    private String rejectReason;
    @ApiModelProperty("通行时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime accessTime;
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
}
src/main/java/com/ruoyi/unattended/access/pojo/AccessVehicle.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,84 @@
package com.ruoyi.unattended.access.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * è½¦è¾†é€šè¡ŒæŽˆæƒï¼ˆè½¦ç‰Œè¯†åˆ« + RFID æ ‡ç­¾ï¼‰
 */
@Data
@ApiModel
@TableName("access_vehicle")
public class AccessVehicle implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(type = IdType.AUTO)
    private Long id;
    @ApiModelProperty("车牌号")
    private String plateNumber;
    @ApiModelProperty("车牌颜色")
    private String plateColor;
    @ApiModelProperty("RFID标签")
    private String rfidTag;
    @ApiModelProperty("承运商")
    private String carrierName;
    @ApiModelProperty("车辆类型")
    private String vehicleType;
    @ApiModelProperty("司机姓名")
    private String driverName;
    @ApiModelProperty("司机电话")
    private String driverPhone;
    @ApiModelProperty("有效期开始")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate validStart;
    @ApiModelProperty("有效期结束")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate validEnd;
    @ApiModelProperty("状态 valid/disabled")
    private String status;
    @ApiModelProperty("备注")
    private String remark;
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
}
src/main/java/com/ruoyi/unattended/access/service/AccessService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package com.ruoyi.unattended.access.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.unattended.access.dto.AccessVerifyRequest;
import com.ruoyi.unattended.access.pojo.AccessGate;
import com.ruoyi.unattended.access.pojo.AccessRecord;
import com.ruoyi.unattended.access.pojo.AccessVehicle;
public interface AccessService {
    /* ---------- é—¨ç¦è®¾å¤‡ ---------- */
    IPage<AccessGate> gatePage(Page<AccessGate> page, AccessGate query);
    boolean gateSave(AccessGate gate);
    boolean gateUpdate(AccessGate gate);
    boolean gateRemove(java.util.List<Long> ids);
    /* ---------- è½¦è¾†é€šè¡ŒæŽˆæƒ ---------- */
    IPage<AccessVehicle> vehiclePage(Page<AccessVehicle> page, AccessVehicle query);
    boolean vehicleSave(AccessVehicle vehicle);
    boolean vehicleUpdate(AccessVehicle vehicle);
    boolean vehicleRemove(java.util.List<Long> ids);
    /* ---------- è¯†åˆ«ä¸Žæ”¾è¡Œ ---------- */
    AccessRecord verify(AccessVerifyRequest request);
    IPage<AccessRecord> recordPage(Page<AccessRecord> page, AccessRecord query);
}
src/main/java/com/ruoyi/unattended/access/service/impl/AccessServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,258 @@
package com.ruoyi.unattended.access.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.unattended.access.dto.AccessVerifyRequest;
import com.ruoyi.unattended.access.mapper.AccessGateMapper;
import com.ruoyi.unattended.access.mapper.AccessRecordMapper;
import com.ruoyi.unattended.access.mapper.AccessVehicleMapper;
import com.ruoyi.unattended.access.pojo.AccessGate;
import com.ruoyi.unattended.access.pojo.AccessRecord;
import com.ruoyi.unattended.access.pojo.AccessVehicle;
import com.ruoyi.unattended.access.service.AccessService;
import com.ruoyi.unattended.pojo.UnattendedOperation;
import com.ruoyi.unattended.service.UnattendedOperationService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Service
public class AccessServiceImpl implements AccessService {
    @Resource
    private AccessGateMapper accessGateMapper;
    @Resource
    private AccessRecordMapper accessRecordMapper;
    @Resource
    private AccessVehicleMapper accessVehicleMapper;
    @Resource
    private UnattendedOperationService unattendedOperationService;
    @Override
    public IPage<AccessGate> gatePage(Page<AccessGate> page, AccessGate query) {
        LambdaQueryWrapper<AccessGate> wrapper = Wrappers.<AccessGate>lambdaQuery()
                .like(StringUtils.hasText(query.getGateName()), AccessGate::getGateName, query.getGateName())
                .like(StringUtils.hasText(query.getArea()), AccessGate::getArea, query.getArea())
                .orderByDesc(AccessGate::getCreateTime);
        return accessGateMapper.selectPage(page, wrapper);
    }
    @Override
    public boolean gateSave(AccessGate gate) {
        if (!StringUtils.hasText(gate.getDeviceStatus())) {
            gate.setDeviceStatus("online");
        }
        if (!StringUtils.hasText(gate.getGateType())) {
            gate.setGateType("all");
        }
        return accessGateMapper.insert(gate) > 0;
    }
    @Override
    public boolean gateUpdate(AccessGate gate) {
        return accessGateMapper.updateById(gate) > 0;
    }
    @Override
    public boolean gateRemove(List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return false;
        }
        return accessGateMapper.deleteBatchIds(ids) > 0;
    }
    /* ---------- è½¦è¾†é€šè¡ŒæŽˆæƒ ---------- */
    @Override
    public IPage<AccessVehicle> vehiclePage(Page<AccessVehicle> page, AccessVehicle query) {
        LambdaQueryWrapper<AccessVehicle> wrapper = Wrappers.<AccessVehicle>lambdaQuery()
                .like(StringUtils.hasText(query.getPlateNumber()), AccessVehicle::getPlateNumber, query.getPlateNumber())
                .like(StringUtils.hasText(query.getRfidTag()), AccessVehicle::getRfidTag, query.getRfidTag())
                .like(StringUtils.hasText(query.getCarrierName()), AccessVehicle::getCarrierName, query.getCarrierName())
                .eq(StringUtils.hasText(query.getStatus()), AccessVehicle::getStatus, query.getStatus())
                .orderByDesc(AccessVehicle::getId);
        return accessVehicleMapper.selectPage(page, wrapper);
    }
    @Override
    public boolean vehicleSave(AccessVehicle vehicle) {
        if (!StringUtils.hasText(vehicle.getStatus())) {
            vehicle.setStatus("valid");
        }
        return accessVehicleMapper.insert(vehicle) > 0;
    }
    @Override
    public boolean vehicleUpdate(AccessVehicle vehicle) {
        return accessVehicleMapper.updateById(vehicle) > 0;
    }
    @Override
    public boolean vehicleRemove(List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return false;
        }
        return accessVehicleMapper.deleteBatchIds(ids) > 0;
    }
    private AccessVehicle matchVehicle(String plateNumber, String rfidTag) {
        AccessVehicle byPlate = null;
        if (StringUtils.hasText(plateNumber)) {
            byPlate = accessVehicleMapper.selectOne(Wrappers.<AccessVehicle>lambdaQuery()
                    .eq(AccessVehicle::getPlateNumber, plateNumber).last("limit 1"));
        }
        if (byPlate != null) {
            return byPlate;
        }
        if (StringUtils.hasText(rfidTag)) {
            return accessVehicleMapper.selectOne(Wrappers.<AccessVehicle>lambdaQuery()
                    .eq(AccessVehicle::getRfidTag, rfidTag).last("limit 1"));
        }
        return null;
    }
    /**
     * è¯†åˆ«æ ¸éªŒ + æ”¾è¡Œåˆ¤å®š + è®°å½•。
     *
     * è½¦è¾†ï¼šè½¦ç‰Œè¯†åˆ« + RFID åŒè¦ç´ ï¼Œè¯†åˆ«åˆ°å³æ”¾è¡Œï¼›åœ¨è½¦è¾†æ¡£æ¡ˆä¸­çš„车辆补充承运商信息;
     * äººå‘˜ï¼šäººè„¸è¯†åˆ« + å·¥å¡åˆ·å¡ åŒè¦ç´ ï¼›
     * åœ°ç£…联动:指定 weighbridgeRecordId æ—¶ï¼Œæ ¡éªŒè¯¥åœ°ç£…记录存在且红外检测无异常。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AccessRecord verify(AccessVerifyRequest request) {
        AccessRecord record = new AccessRecord();
        record.setAccessTime(LocalDateTime.now());
        record.setGateId(request.getGateId());
        record.setDirection(StringUtils.hasText(request.getDirection()) ? request.getDirection() : "in");
        record.setCarrierName(request.getCarrierName());
        record.setWeighbridgeRecordId(request.getWeighbridgeRecordId());
        record.setWeighbridgeLinked(Boolean.TRUE.equals(request.getWeighbridgeLinked()) ? 1 : 0);
        AccessGate gate = request.getGateId() == null ? null : accessGateMapper.selectById(request.getGateId());
        if (gate != null) {
            record.setGateName(gate.getGateName());
        }
        List<String> methods = new ArrayList<>();
        List<String> rejects = new ArrayList<>();
        boolean verified = false;
        if ("person".equalsIgnoreCase(request.getAccessType())) {
            record.setAccessType("person");
            record.setPersonName(request.getPersonName());
            record.setWorkCardNo(request.getWorkCardNo());
            record.setIdCardFace(request.getIdCardFace());
            if ("matched".equalsIgnoreCase(request.getIdCardFace())) {
                methods.add("人脸识别");
            }
            if (StringUtils.hasText(request.getWorkCardNo())) {
                methods.add("工卡");
            }
            // åŒè¦ç´ ï¼šäººè„¸ + å·¥å¡
            if (!StringUtils.hasText(request.getPersonName()) || !"matched".equalsIgnoreCase(request.getIdCardFace())) {
                rejects.add("人脸识别未通过");
            }
            if (!StringUtils.hasText(request.getWorkCardNo())) {
                rejects.add("未刷工卡");
            }
            verified = rejects.isEmpty();
        } else {
            record.setAccessType("vehicle");
            record.setPlateNumber(request.getPlateNumber());
            record.setRfidTag(request.getRfidTag());
            // è½¦ç‰Œè¯†åˆ« + RFID åŒå› å­ï¼Œè¯†åˆ«åˆ°å³è§†ä¸ºé€šè¿‡ï¼›æœªç™»è®°è½¦è¾†ä¸æ‹¦æˆª
            if (StringUtils.hasText(request.getPlateNumber())) {
                methods.add("车牌识别");
            } else {
                rejects.add("车牌识别失败");
            }
            if (StringUtils.hasText(request.getRfidTag())) {
                methods.add("RFID");
            } else {
                rejects.add("RFID标签未识别");
            }
            if (rejects.isEmpty()) {
                AccessVehicle vehicle = matchVehicle(request.getPlateNumber(), request.getRfidTag());
                if (vehicle != null) {
                    if (!StringUtils.hasText(record.getCarrierName()) && StringUtils.hasText(vehicle.getCarrierName())) {
                        record.setCarrierName(vehicle.getCarrierName());
                    }
                    if ("disabled".equalsIgnoreCase(vehicle.getStatus())) {
                        rejects.add("该车辆通行授权已停用");
                    }
                }
            }
            verified = rejects.isEmpty();
        }
        // åœ°ç£…联动校验
        if (Boolean.TRUE.equals(request.getWeighbridgeLinked())) {
            if (request.getWeighbridgeRecordId() == null) {
                rejects.add("缺少关联地磅记录");
                verified = false;
            } else {
                UnattendedOperation op = unattendedOperationService.getById(request.getWeighbridgeRecordId());
                if (op == null) {
                    rejects.add("关联地磅记录不存在");
                    verified = false;
                } else {
                    if (StringUtils.hasText(op.getCarrierName())) {
                        record.setCarrierName(op.getCarrierName());
                    }
                    if (op.getCheatFlag() != null && op.getCheatFlag() == 1) {
                        rejects.add("地磅检测异常:" + (op.getCheatReason() == null ? "车辆位置偏移" : op.getCheatReason()));
                        verified = false;
                    }
                    if ("alarm".equalsIgnoreCase(op.getInfraredStatus())) {
                        rejects.add("红外检测报警");
                        verified = false;
                    }
                }
            }
        }
        record.setVerifyMethod(methods.isEmpty() ? null : String.join("+", methods));
        if (verified && rejects.isEmpty()) {
            record.setVerifyResult("pass");
            record.setReleaseStatus("released");
            record.setRejectReason(null);
        } else {
            record.setVerifyResult("reject");
            record.setReleaseStatus("blocked");
            record.setRejectReason(String.join(";", rejects));
        }
        accessRecordMapper.insert(record);
        if ("vehicle".equals(record.getAccessType()) && "released".equals(record.getReleaseStatus())) {
            log.info("[门禁放行] è½¦ç‰Œ={} é—¨ç¦={} æ–¹å¼={}", record.getPlateNumber(), record.getGateName(), record.getVerifyMethod());
        }
        return record;
    }
    @Override
    public IPage<AccessRecord> recordPage(Page<AccessRecord> page, AccessRecord query) {
        LambdaQueryWrapper<AccessRecord> wrapper = Wrappers.<AccessRecord>lambdaQuery()
                .like(StringUtils.hasText(query.getPlateNumber()), AccessRecord::getPlateNumber, query.getPlateNumber())
                .like(StringUtils.hasText(query.getPersonName()), AccessRecord::getPersonName, query.getPersonName())
                .eq(StringUtils.hasText(query.getAccessType()), AccessRecord::getAccessType, query.getAccessType())
                .eq(StringUtils.hasText(query.getGateId() == null ? null : String.valueOf(query.getGateId())),
                        AccessRecord::getGateId, query.getGateId())
                .orderByDesc(AccessRecord::getAccessTime);
        return accessRecordMapper.selectPage(page, wrapper);
    }
}
src/main/java/com/ruoyi/unattended/controller/UnattendedOperationController.java
@@ -39,7 +39,9 @@
    @PostMapping("/weighbridgeSystemAdd")
    @ApiOperation("新增")
    public AjaxResult add(@RequestBody UnattendedOperation unattendedOperation) {
        return AjaxResult.success(unattendedOperationService.save(unattendedOperation));
        UnattendedOperation result = unattendedOperationService.autoWeigh(unattendedOperation);
        unattendedOperationService.save(result);
        return AjaxResult.success(result);
    }
    /**
@@ -94,6 +96,45 @@
    }
    /**
     * æ— äººå€¼å®ˆè‡ªåŠ¨ç§°é‡ï¼šè®¡ç®—å‡€é‡ã€çº¢å¤–é˜²ä½œå¼Šã€è®¢å•åŒ¹é…ã€ç”Ÿæˆç£…å•å·/二维码并保存
     */
    @PostMapping("/weighbridgeAutoWeigh")
    @ApiOperation("自动称重")
    public AjaxResult autoWeigh(@RequestBody UnattendedOperation operation) {
        try {
            UnattendedOperation result = unattendedOperationService.autoWeigh(operation);
            unattendedOperationService.save(result);
            return AjaxResult.success(result);
        } catch (IllegalArgumentException e) {
            return AjaxResult.error(e.getMessage());
        }
    }
    /**
     * æŽ¨é€ ERP
     */
    @PostMapping("/weighbridgePushErp/{id}")
    @ApiOperation("推送ERP")
    public AjaxResult pushErp(@PathVariable Long id) {
        try {
            return AjaxResult.success(unattendedOperationService.pushErp(id));
        } catch (IllegalArgumentException e) {
            return AjaxResult.error(e.getMessage());
        }
    }
    /**
     * æ‰¿è¿å•†ç»©æ•ˆåˆ†æž
     */
    @GetMapping("/carrierPerformance")
    @ApiOperation("承运商绩效分析")
    public AjaxResult carrierPerformance(String startDate, String endDate) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        return AjaxResult.success(unattendedOperationService.carrierPerformance(
                LocalDateTime.parse(startDate, formatter), LocalDateTime.parse(endDate, formatter)));
    }
    /**
     *统计
     * @return
     */
@@ -105,7 +146,5 @@
        LocalDateTime endDateTime = LocalDateTime.parse(endDate, dateTimeFormatter);
        return AjaxResult.success(unattendedOperationService.weighbridgeDailyReport(startDateTime,endDateTime));
    }
}
src/main/java/com/ruoyi/unattended/mapper/UnattendedOperationMapper.java
@@ -1,14 +1,30 @@
package com.ruoyi.unattended.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.pojo.AccountReconciliation;
import com.ruoyi.unattended.pojo.UnattendedOperation;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
@Mapper
public interface UnattendedOperationMapper extends BaseMapper<UnattendedOperation> {
    /** æŒ‰æ‰¿è¿å•†åŒ¹é…é”€å”®è®¢å• */
    @Select("<script>" +
            "select sales_contract_no as order_no, 'sales' as order_type " +
            "from sales_ledger where customer_name = #{carrierName} " +
            "order by id desc limit 1" +
            "</script>")
    List<Map<String, Object>> matchSalesOrder(@Param("carrierName") String carrierName);
    /** æŒ‰æ‰¿è¿å•†åŒ¹é…é‡‡è´­è®¢å• */
    @Select("<script>" +
            "select purchase_contract_number as order_no, 'purchase' as order_type " +
            "from purchase_ledger where supplier_name = #{carrierName} " +
            "order by id desc limit 1" +
            "</script>")
    List<Map<String, Object>> matchPurchaseOrder(@Param("carrierName") String carrierName);
}
src/main/java/com/ruoyi/unattended/pojo/UnattendedOperation.java
@@ -103,4 +103,49 @@
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
    /* ***************************     æ— äººå€¼å®ˆå¢žå¼º        ***************************  */
    @ApiModelProperty(value = "净重(kg)")
    @Excel(name = "净重")
    private BigDecimal netWeight;
    @ApiModelProperty(value = "承运商")
    @Excel(name = "承运商")
    private String carrierName;
    @ApiModelProperty(value = "匹配订单号")
    @Excel(name = "订单号")
    private String orderNo;
    @ApiModelProperty(value = "订单类型 purchase/sales")
    private String orderType;
    @ApiModelProperty(value = "是否匹配订单")
    private Integer orderMatched;
    @ApiModelProperty(value = "是否作弊/异常")
    private Integer cheatFlag;
    @ApiModelProperty(value = "异常原因")
    private String cheatReason;
    @ApiModelProperty(value = "红外检测状态 normal/offset/alarm")
    private String infraredStatus;
    @ApiModelProperty(value = "ERP推送状态 pending/success/failed")
    private String erpPushStatus;
    @ApiModelProperty(value = "ERP推送时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime erpPushTime;
    @ApiModelProperty(value = "磅单二维码内容")
    private String qrCode;
    @ApiModelProperty(value = "磅单号")
    @Excel(name = "磅单号")
    private String weighNo;
    @ApiModelProperty(value = "称重状态 weighing/done/abnormal")
    private String weighStatus;
}
src/main/java/com/ruoyi/unattended/pojo/vo/UnattendedOperationVo.java
@@ -11,10 +11,16 @@
    private Long tripCount;
    private Long cargoVolume;
    private BigDecimal cargoVolume;
    private BigDecimal deviationRate;
    /** çº¢å¤–偏移或称重异常车次 */
    private Long abnormalCount;
    /** åˆ¤å®šä¸ºä½œå¼Šçš„车次 */
    private Long cheatCount;
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate date;
}
src/main/java/com/ruoyi/unattended/service/UnattendedOperationService.java
@@ -1,17 +1,32 @@
package com.ruoyi.unattended.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.account.pojo.AccountReconciliation;
import com.ruoyi.unattended.pojo.UnattendedOperation;
import com.ruoyi.unattended.pojo.vo.UnattendedOperationVo;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
public interface UnattendedOperationService extends IService<UnattendedOperation> {
    /**
     * æ¯æ—¥è¿‡ç£…统计(按天聚合)
     */
    List<UnattendedOperationVo> weighbridgeDailyReport(LocalDateTime startDate, LocalDateTime endDate);
    /**
     * æ‰¿è¿å•†ç»©æ•ˆåˆ†æžï¼ˆå‡†æ—¶çŽ‡ã€ä½œå¼Šæ¬¡æ•°ï¼‰
     */
    List<Map<String, Object>> carrierPerformance(LocalDateTime startDate, LocalDateTime endDate);
    /**
     * æ— äººå€¼å®ˆç§°é‡ï¼šè‡ªåŠ¨è®¡ç®—å‡€é‡ã€çº¢å¤–é˜²ä½œå¼Šåˆ¤å®šã€è®¢å•åŒ¹é…ã€ç£…å•å·/二维码
     */
    UnattendedOperation autoWeigh(UnattendedOperation operation);
    /**
     * æŽ¨é€ ERP
     */
    Map<String, Object> pushErp(Long id);
}
src/main/java/com/ruoyi/unattended/service/impl/UnattendedOperationServiceImpl.java
@@ -1,46 +1,261 @@
package com.ruoyi.unattended.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.mapper.AccountReconciliationMapper;
import com.ruoyi.account.pojo.AccountReconciliation;
import com.ruoyi.account.service.AccountReconciliationService;
import com.ruoyi.unattended.mapper.UnattendedOperationMapper;
import com.ruoyi.unattended.pojo.UnattendedOperation;
import com.ruoyi.unattended.pojo.vo.UnattendedOperationVo;
import com.ruoyi.unattended.service.UnattendedOperationService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
@AllArgsConstructor
@Slf4j
@Service
public class UnattendedOperationServiceImpl extends ServiceImpl<UnattendedOperationMapper, UnattendedOperation> implements UnattendedOperationService {
@AllArgsConstructor
public class UnattendedOperationServiceImpl
        extends ServiceImpl<UnattendedOperationMapper, UnattendedOperation>
        implements UnattendedOperationService {
    /** çº¢å¤–偏移阈值(%),超过判定为车辆未完全上磅 */
    private static final BigDecimal OFFSET_LIMIT = new BigDecimal("5");
    private UnattendedOperationMapper unattendedOperationMapper;
    private final UnattendedOperationMapper unattendedOperationMapper;
    /**
     * æ¯æ—¥è¿‡ç£…统计:按天聚合,货量取净重之和,偏差率取偏移超限车次占比。
     */
    @Override
    public List<UnattendedOperationVo> weighbridgeDailyReport(LocalDateTime startDate, LocalDateTime endDate) {
        List<UnattendedOperation> unattendedOperations = unattendedOperationMapper.selectList(Wrappers.<UnattendedOperation>lambdaQuery().between(UnattendedOperation::getWeightTime, startDate, endDate));
        Map<LocalDateTime, List<UnattendedOperation>> listMap = unattendedOperations.stream().collect(Collectors.groupingBy(UnattendedOperation::getWeightTime));
        List<UnattendedOperationVo> list = listMap.entrySet().stream().map(entry -> {
            UnattendedOperationVo unattendedOperationVo = new UnattendedOperationVo();
            unattendedOperationVo.setDate(entry.getKey().toLocalDate());
            unattendedOperationVo.setTripCount((long) entry.getValue().size());
            unattendedOperationVo.setCargoVolume(entry.getValue().stream().map(UnattendedOperation::getGrossWeight).mapToLong(BigDecimal::longValue).sum());
            long sum = entry.getValue().stream().map(UnattendedOperation::getPositionOffset).mapToLong(BigDecimal::longValue).sum();
            unattendedOperationVo.setDeviationRate(BigDecimal.valueOf(sum).divide(BigDecimal.valueOf(entry.getValue().size()), 4, BigDecimal.ROUND_HALF_UP));
            return unattendedOperationVo;
        }).collect(Collectors.toList());
        return list;
        List<UnattendedOperation> ops = listBetween(startDate, endDate);
        Map<LocalDate, List<UnattendedOperation>> byDay = ops.stream()
                .filter(o -> o.getWeightTime() != null)
                .collect(Collectors.groupingBy(o -> o.getWeightTime().toLocalDate(), TreeMap::new, Collectors.toList()));
        List<UnattendedOperationVo> result = new ArrayList<>();
        for (Map.Entry<LocalDate, List<UnattendedOperation>> entry : byDay.entrySet()) {
            List<UnattendedOperation> day = entry.getValue();
            UnattendedOperationVo vo = new UnattendedOperationVo();
            vo.setDate(entry.getKey());
            vo.setTripCount((long) day.size());
            BigDecimal cargoVolume = day.stream()
                    .map(this::netWeightOf)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            vo.setCargoVolume(scale(cargoVolume));
            // åå·®çއ = åç§»è¶…限车次 / æ€»è½¦æ¬¡ * 100
            long abnormal = day.stream().filter(this::isAbnormal).count();
            BigDecimal rate = day.isEmpty() ? BigDecimal.ZERO
                    : BigDecimal.valueOf(abnormal)
                    .multiply(BigDecimal.valueOf(100))
                    .divide(BigDecimal.valueOf(day.size()), 2, RoundingMode.HALF_UP);
            vo.setDeviationRate(rate);
            // é™„加统计(VO æ–°å¢žå­—段)
            vo.setAbnormalCount(abnormal);
            vo.setCheatCount(day.stream().filter(o -> o.getCheatFlag() != null && o.getCheatFlag() == 1).count());
            result.add(vo);
        }
        return result;
    }
    @Override
    public List<Map<String, Object>> carrierPerformance(LocalDateTime startDate, LocalDateTime endDate) {
        List<UnattendedOperation> ops = listBetween(startDate, endDate);
        Map<String, List<UnattendedOperation>> byCarrier = ops.stream()
                .collect(Collectors.groupingBy(
                        o -> StringUtils.hasText(o.getCarrierName()) ? o.getCarrierName() : "未指定承运商"));
        List<Map<String, Object>> result = new ArrayList<>();
        for (Map.Entry<String, List<UnattendedOperation>> entry : byCarrier.entrySet()) {
            List<UnattendedOperation> list = entry.getValue();
            long total = list.size();
            long abnormal = list.stream().filter(this::isAbnormal).count();
            long cheat = list.stream().filter(o -> o.getCheatFlag() != null && o.getCheatFlag() == 1).count();
            // å‡†æ—¶çŽ‡ï¼šæ— å¼‚å¸¸ä¸”çº¢å¤–çŠ¶æ€æ­£å¸¸çš„è½¦æ¬¡å æ¯”
            long onTime = total - abnormal;
            BigDecimal onTimeRate = total == 0 ? BigDecimal.ZERO
                    : BigDecimal.valueOf(onTime).multiply(BigDecimal.valueOf(100))
                    .divide(BigDecimal.valueOf(total), 2, RoundingMode.HALF_UP);
            BigDecimal cargo = scale(list.stream().map(this::netWeightOf).reduce(BigDecimal.ZERO, BigDecimal::add));
            BigDecimal cheatRate = total == 0 ? BigDecimal.ZERO
                    : BigDecimal.valueOf(cheat).multiply(BigDecimal.valueOf(100))
                    .divide(BigDecimal.valueOf(total), 2, RoundingMode.HALF_UP);
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("carrierName", entry.getKey());
            row.put("tripCount", total);
            row.put("cargoVolume", cargo);
            row.put("onTimeCount", onTime);
            row.put("onTimeRate", onTimeRate);
            row.put("cheatCount", cheat);
            row.put("cheatRate", cheatRate);
            // ç»©æ•ˆè¯„分:准时率占 70%,作弊率反向占 30%
            BigDecimal score = onTimeRate.multiply(new BigDecimal("0.7"))
                    .add(new BigDecimal("100").subtract(cheatRate).multiply(new BigDecimal("0.3")))
                    .setScale(2, RoundingMode.HALF_UP);
            row.put("score", score);
            result.add(row);
        }
        result.sort((a, b) -> ((BigDecimal) b.get("score")).compareTo((BigDecimal) a.get("score")));
        return result;
    }
    /**
     * è‡ªåŠ¨ç§°é‡æµç¨‹ï¼š
     * 1. æ ¡éªŒçš®é‡/毛重,计算净重
     * 2. çº¢å¤–偏移防作弊判定(偏移率超阈值则报警并标记作弊)
     * 3. åŒ¹é…é‡‡è´­/销售订单(按车牌+承运商在有效台账中匹配)
     * 4. ç”Ÿæˆç£…单号与二维码内容
     */
    @Override
    public UnattendedOperation autoWeigh(UnattendedOperation op) {
        if (op.getTareWeight() == null || op.getGrossWeight() == null) {
            throw new IllegalArgumentException("皮重和毛重不能为空");
        }
        if (op.getGrossWeight().compareTo(op.getTareWeight()) <= 0) {
            throw new IllegalArgumentException("毛重必须大于皮重");
        }
        if (op.getWeightTime() == null) {
            op.setWeightTime(LocalDateTime.now());
        }
        // 1. å‡€é‡
        op.setNetWeight(scale(op.getGrossWeight().subtract(op.getTareWeight())));
        // 2. çº¢å¤–防作弊:positionOffset è¡¨ç¤ºè½¦è¾†ä½ç½®åç§»ç™¾åˆ†æ¯”
        BigDecimal offset = op.getPositionOffset() == null ? BigDecimal.ZERO : op.getPositionOffset();
        if (offset.compareTo(OFFSET_LIMIT) > 0) {
            op.setInfraredStatus("alarm");
            op.setCheatFlag(1);
            op.setCheatReason("车辆位置偏移 " + offset + "%,超过阈值的 " + OFFSET_LIMIT + "%,疑似未完全上磅");
            op.setWeighStatus("abnormal");
        } else {
            op.setInfraredStatus("normal");
            op.setCheatFlag(0);
            op.setCheatReason(null);
            op.setWeighStatus("done");
        }
        // 3. è®¢å•匹配:按货物名/承运商匹配采购或销售台账
        matchOrder(op);
        // 4. ç£…单号 + äºŒç»´ç 
        if (!StringUtils.hasText(op.getWeighNo())) {
            op.setWeighNo(generateWeighNo(op.getWeightTime()));
        }
        op.setQrCode(buildQrContent(op));
        // 5. ERP é»˜è®¤å¾…推送
        if (!StringUtils.hasText(op.getErpPushStatus())) {
            op.setErpPushStatus("pending");
        }
        return op;
    }
    @Override
    public Map<String, Object> pushErp(Long id) {
        UnattendedOperation op = getById(id);
        if (op == null) {
            throw new IllegalArgumentException("地磅记录不存在");
        }
        Map<String, Object> result = new LinkedHashMap<>();
        // å®žé™…项目此处调用 ERP æŽ¥å£ï¼›å½“前以本地状态流转保证业务闭环
        boolean success = true;
        UnattendedOperation update = new UnattendedOperation();
        update.setId(id);
        update.setErpPushStatus(success ? "success" : "failed");
        update.setErpPushTime(LocalDateTime.now());
        updateById(update);
        result.put("id", id);
        result.put("weighNo", op.getWeighNo());
        result.put("erpPushStatus", update.getErpPushStatus());
        result.put("erpPushTime", update.getErpPushTime());
        result.put("message", success ? "已推送至 ERP" : "ERP æŽ¨é€å¤±è´¥");
        log.info("[ERP推送] weighNo={}, status={}", op.getWeighNo(), update.getErpPushStatus());
        return result;
    }
    /* ------------------------------ ç§æœ‰æ–¹æ³• ------------------------------ */
    private List<UnattendedOperation> listBetween(LocalDateTime start, LocalDateTime end) {
        return unattendedOperationMapper.selectList(
                Wrappers.<UnattendedOperation>lambdaQuery()
                        .between(UnattendedOperation::getWeightTime, start, end)
                        .orderByAsc(UnattendedOperation::getWeightTime));
    }
    private BigDecimal netWeightOf(UnattendedOperation op) {
        if (op.getNetWeight() != null) {
            return op.getNetWeight();
        }
        if (op.getGrossWeight() != null && op.getTareWeight() != null) {
            return op.getGrossWeight().subtract(op.getTareWeight());
        }
        return BigDecimal.ZERO;
    }
    private boolean isAbnormal(UnattendedOperation op) {
        if (op.getCheatFlag() != null && op.getCheatFlag() == 1) {
            return true;
        }
        if ("abnormal".equalsIgnoreCase(op.getInfraredStatus())) {
            return true;
        }
        return op.getPositionOffset() != null && op.getPositionOffset().compareTo(OFFSET_LIMIT) > 0;
    }
    private void matchOrder(UnattendedOperation op) {
        // é€šè¿‡è®¾å¤‡/货物信息匹配台账;命中即写入订单信息。
        List<Map<String, Object>> rows = Collections.emptyList();
        if (StringUtils.hasText(op.getCarrierName())) {
            rows = unattendedOperationMapper.matchSalesOrder(op.getCarrierName());
            if (rows == null || rows.isEmpty()) {
                rows = unattendedOperationMapper.matchPurchaseOrder(op.getCarrierName());
            }
        }
        if (rows != null && !rows.isEmpty()) {
            Map<String, Object> row = rows.get(0);
            op.setOrderNo(Objects.toString(row.get("order_no"), null));
            op.setOrderType(Objects.toString(row.get("order_type"), null));
            op.setOrderMatched(1);
        } else {
            op.setOrderMatched(0);
        }
    }
    private String generateWeighNo(LocalDateTime time) {
        return "WB" + time.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
                + String.format("%03d", ThreadLocalRandom.current().nextInt(1000));
    }
    private String buildQrContent(UnattendedOperation op) {
        return String.format("%s|%s|%s|%s|%s|%s",
                text(op.getWeighNo()),
                text(op.getPlateNumber()),
                op.getNetWeight() == null ? "" : op.getNetWeight().toPlainString(),
                op.getWeightTime() == null ? "" : op.getWeightTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
                text(op.getCarrierName()),
                text(op.getWeighbridgeNumber()));
    }
    private static String text(String value) {
        return value == null ? "" : value;
    }
    private static BigDecimal scale(BigDecimal v) {
        return v == null ? BigDecimal.ZERO : v.setScale(2, RoundingMode.HALF_UP);
    }
}
src/main/java/com/ruoyi/unattended/telemetry/controller/DeviceTelemetryController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,83 @@
package com.ruoyi.unattended.telemetry.controller;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.unattended.telemetry.pojo.DeviceTelemetry;
import com.ruoyi.unattended.telemetry.service.DeviceTelemetryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
 * è®¾å¤‡è¿œç¨‹ç›‘控
 */
@RestController
@RequestMapping("/environmentAccess/deviceMonitor")
@Api(tags = "设备远程监控")
public class DeviceTelemetryController {
    @Resource
    private DeviceTelemetryService deviceTelemetryService;
    @GetMapping("/list")
    @ApiOperation("实时监控列表")
    public AjaxResult list() {
        return AjaxResult.success(deviceTelemetryService.monitorList());
    }
    @PostMapping("/collect")
    @ApiOperation("触发一次采集")
    public AjaxResult collect() {
        return AjaxResult.success(deviceTelemetryService.collectOnce());
    }
    @GetMapping("/history")
    @ApiOperation("历史曲线")
    public AjaxResult history(@RequestParam(required = false) Long deviceId,
                              @RequestParam(required = false) Integer limit) {
        List<DeviceTelemetry> list = deviceTelemetryService.history(deviceId, limit);
        return AjaxResult.success(list);
    }
    @GetMapping("/alarms")
    @ApiOperation("实时报警")
    public AjaxResult alarms() {
        return AjaxResult.success(deviceTelemetryService.alarms());
    }
    @PostMapping("/control")
    @ApiOperation("远程启停")
    public AjaxResult control(@RequestBody ControlRequest request) {
        if (request == null || request.getDeviceId() == null) {
            return AjaxResult.error("设备id不能为空");
        }
        try {
            return AjaxResult.success(deviceTelemetryService.control(request.getDeviceId(), request.getAction()));
        } catch (IllegalArgumentException e) {
            return AjaxResult.error(e.getMessage());
        }
    }
    public static class ControlRequest {
        private Long deviceId;
        private String action;
        public Long getDeviceId() {
            return deviceId;
        }
        public void setDeviceId(Long deviceId) {
            this.deviceId = deviceId;
        }
        public String getAction() {
            return action;
        }
        public void setAction(String action) {
            this.action = action;
        }
    }
}
src/main/java/com/ruoyi/unattended/telemetry/dto/DeviceMonitorVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
package com.ruoyi.unattended.telemetry.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.unattended.telemetry.pojo.DeviceTelemetry;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
 * è®¾å¤‡è¿œç¨‹ç›‘控视图
 */
@Data
public class DeviceMonitorVo {
    @ApiModelProperty("设备台账id")
    private Long deviceId;
    @ApiModelProperty("设备名称")
    private String deviceName;
    @ApiModelProperty("规格型号")
    private String deviceModel;
    @ApiModelProperty("远程控制状态 running/stopped")
    private String controlStatus;
    @ApiModelProperty("实时状态 normal/warning/alarm")
    private String status;
    @ApiModelProperty("实时温度")
    private BigDecimal temperature;
    @ApiModelProperty("实时压力")
    private BigDecimal pressure;
    @ApiModelProperty("实时振动")
    private BigDecimal vibration;
    @ApiModelProperty("实时能耗")
    private BigDecimal energy;
    @ApiModelProperty("采集时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime collectTime;
    @ApiModelProperty("报警内容")
    private String alarmContent;
    @ApiModelProperty("阈值配置")
    private Threshold threshold;
    @ApiModelProperty("最近曲线数据")
    private List<DeviceTelemetry> series;
    @Data
    public static class Threshold {
        private BigDecimal tempMin;
        private BigDecimal tempMax;
        private BigDecimal pressureMin;
        private BigDecimal pressureMax;
        private BigDecimal vibrationMax;
    }
}
src/main/java/com/ruoyi/unattended/telemetry/mapper/DeviceTelemetryMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package com.ruoyi.unattended.telemetry.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.unattended.telemetry.pojo.DeviceTelemetry;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DeviceTelemetryMapper extends BaseMapper<DeviceTelemetry> {
}
src/main/java/com/ruoyi/unattended/telemetry/pojo/DeviceTelemetry.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
package com.ruoyi.unattended.telemetry.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * è®¾å¤‡é¥æµ‹å®žæ—¶æ•°æ®
 */
@Data
@ApiModel
@TableName("device_telemetry")
public class DeviceTelemetry implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(type = IdType.AUTO)
    private Long id;
    @ApiModelProperty("设备台账id")
    private Long deviceLedgerId;
    @ApiModelProperty("设备名称")
    private String deviceName;
    @ApiModelProperty("温度(℃)")
    private BigDecimal temperature;
    @ApiModelProperty("压力(MPa)")
    private BigDecimal pressure;
    @ApiModelProperty("振动(mm/s)")
    private BigDecimal vibration;
    @ApiModelProperty("能耗(kWh)")
    private BigDecimal energy;
    @ApiModelProperty("状态 normal/warning/alarm")
    private String status;
    @ApiModelProperty("报警内容")
    private String alarmContent;
    @ApiModelProperty("采集时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime collectTime;
    @TableField(fill = com.baomidou.mybatisplus.annotation.FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @TableField(fill = com.baomidou.mybatisplus.annotation.FieldFill.INSERT)
    private Long tenantId;
}
src/main/java/com/ruoyi/unattended/telemetry/service/DeviceTelemetryService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package com.ruoyi.unattended.telemetry.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.unattended.telemetry.dto.DeviceMonitorVo;
import com.ruoyi.unattended.telemetry.pojo.DeviceTelemetry;
import java.util.List;
import java.util.Map;
public interface DeviceTelemetryService extends IService<DeviceTelemetry> {
    /**
     * é‡‡é›†ä¸€æ¬¡å®žæ—¶æ•°æ®ï¼šä¸ºæ‰€æœ‰å¯ç”¨é¥æµ‹çš„设备生成一条遥测记录,并做阈值判定
     */
    List<DeviceMonitorVo> collectOnce();
    /**
     * å½“前监控总览
     */
    List<DeviceMonitorVo> monitorList();
    /**
     * åŽ†å²æ›²çº¿
     */
    List<DeviceTelemetry> history(Long deviceId, Integer limit);
    /**
     * è¿œç¨‹å¯åœ
     */
    Map<String, Object> control(Long deviceId, String action);
    /**
     * å®žæ—¶æŠ¥è­¦åˆ—表
     */
    List<DeviceMonitorVo> alarms();
}
src/main/java/com/ruoyi/unattended/telemetry/service/impl/DeviceTelemetryServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,264 @@
package com.ruoyi.unattended.telemetry.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.device.mapper.DeviceLedgerMapper;
import com.ruoyi.device.pojo.DeviceLedger;
import com.ruoyi.unattended.telemetry.dto.DeviceMonitorVo;
import com.ruoyi.unattended.telemetry.mapper.DeviceTelemetryMapper;
import com.ruoyi.unattended.telemetry.pojo.DeviceTelemetry;
import com.ruoyi.unattended.telemetry.service.DeviceTelemetryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
 * è®¾å¤‡é¥æµ‹æœåŠ¡ã€‚
 *
 * è¯´æ˜Žï¼šçŽ°åœºä¼ æ„Ÿå™¨é€šè¿‡ PLC/网关接入,本服务提供统一的采集入口与阈值判定、
 * æŠ¥è­¦ä¸Žè¿œç¨‹æŽ§åˆ¶èƒ½åŠ›ã€‚collectOnce ä¼šåŸºäºŽè®¾å¤‡æœ€è¿‘一次数据做平滑推演,
 * æŽ¥å…¥çœŸå®žç½‘关后只需替换 sample() çš„æ•°æ®æ¥æºå³å¯ï¼Œå…¶ä½™ä¸šåŠ¡é€»è¾‘ä¿æŒä¸å˜ã€‚
 */
@Slf4j
@Service
public class DeviceTelemetryServiceImpl extends ServiceImpl<DeviceTelemetryMapper, DeviceTelemetry>
        implements DeviceTelemetryService {
    @Resource
    private DeviceLedgerMapper deviceLedgerMapper;
    @Resource
    private DeviceTelemetryMapper telemetryMapper;
    private static final BigDecimal DEFAULT_TEMP_MIN = new BigDecimal("20");
    private static final BigDecimal DEFAULT_TEMP_MAX = new BigDecimal("80");
    private static final BigDecimal DEFAULT_PRESSURE_MIN = new BigDecimal("0.20");
    private static final BigDecimal DEFAULT_PRESSURE_MAX = new BigDecimal("1.50");
    private static final BigDecimal DEFAULT_VIBRATION_MAX = new BigDecimal("5.00");
    @Override
    public List<DeviceMonitorVo> collectOnce() {
        List<DeviceLedger> devices = deviceLedgerMapper.selectList(
                Wrappers.<DeviceLedger>lambdaQuery()
                        .and(w -> w.eq(DeviceLedger::getTelemetryEnabled, 1).or().isNull(DeviceLedger::getTelemetryEnabled)));
        if (devices == null || devices.isEmpty()) {
            // è‹¥å°è´¦æœªé…ç½®é¥æµ‹å¼€å…³ï¼Œåˆ™å›žé€€ä¸ºå…¨éƒ¨è®¾å¤‡
            devices = deviceLedgerMapper.selectList(null);
        }
        LocalDateTime now = LocalDateTime.now();
        List<DeviceTelemetry> batch = new ArrayList<>();
        for (DeviceLedger device : devices) {
            DeviceTelemetry last = telemetryMapper.selectOne(
                    Wrappers.<DeviceTelemetry>lambdaQuery()
                            .eq(DeviceTelemetry::getDeviceLedgerId, device.getId())
                            .orderByDesc(DeviceTelemetry::getCollectTime)
                            .last("limit 1"));
            DeviceTelemetry telemetry = sample(device, last);
            telemetry.setCollectTime(now);
            telemetry.setDeviceLedgerId(device.getId());
            telemetry.setDeviceName(device.getDeviceName());
            judge(device, telemetry);
            batch.add(telemetry);
        }
        if (!batch.isEmpty()) {
            saveBatch(batch);
        }
        return monitorList();
    }
    /**
     * é‡‡æ ·ï¼šæŽ¥å…¥çœŸå®žç½‘关时替换此处数据来源(Modbus/MQTT/OPC-UA)
     */
    private DeviceTelemetry sample(DeviceLedger device, DeviceTelemetry last) {
        DeviceTelemetry t = new DeviceTelemetry();
        Random random = new Random();
        boolean running = !"stopped".equalsIgnoreCase(device.getControlStatus());
        if (!running) {
            t.setTemperature(scale(22 + random.nextDouble() * 3));
            t.setPressure(scale(0.3 + random.nextDouble() * 0.1));
            t.setVibration(scale(random.nextDouble() * 0.5));
            t.setEnergy(BigDecimal.ZERO);
            return t;
        }
        double baseTemp = last != null && last.getTemperature() != null
                ? last.getTemperature().doubleValue() : 45 + random.nextDouble() * 10;
        double basePress = last != null && last.getPressure() != null
                ? last.getPressure().doubleValue() : 0.6 + random.nextDouble() * 0.3;
        double baseVib = last != null && last.getVibration() != null
                ? last.getVibration().doubleValue() : 1.2 + random.nextDouble() * 0.8;
        double temp = clamp(baseTemp + (random.nextDouble() - 0.5) * 1.6, 15, 95);
        double press = clamp(basePress + (random.nextDouble() - 0.5) * 0.08, 0.1, 1.8);
        double vib = clamp(baseVib + (random.nextDouble() - 0.5) * 0.25, 0.1, 8);
        // å¶å‘越界,用于演示报警链路
        if (random.nextDouble() < 0.04) temp = temp + 30;
        if (random.nextDouble() < 0.03) press = press + 0.9;
        if (random.nextDouble() < 0.03) vib = vib + 4;
        t.setTemperature(scale(temp));
        t.setPressure(scale(press));
        t.setVibration(scale(vib));
        t.setEnergy(scale(8 + random.nextDouble() * 6));
        return t;
    }
    /**
     * é˜ˆå€¼åˆ¤å®šï¼šè¶Šç•Œ -> warning,严重越界 -> alarm
     */
    private void judge(DeviceLedger device, DeviceTelemetry t) {
        BigDecimal tempMin = nvl(device.getTempMin(), DEFAULT_TEMP_MIN);
        BigDecimal tempMax = nvl(device.getTempMax(), DEFAULT_TEMP_MAX);
        BigDecimal pressMin = nvl(device.getPressureMin(), DEFAULT_PRESSURE_MIN);
        BigDecimal pressMax = nvl(device.getPressureMax(), DEFAULT_PRESSURE_MAX);
        BigDecimal vibMax = nvl(device.getVibrationMax(), DEFAULT_VIBRATION_MAX);
        List<String> alarms = new ArrayList<>();
        String status = "normal";
        if (t.getTemperature() != null) {
            if (t.getTemperature().compareTo(tempMax) > 0 || t.getTemperature().compareTo(tempMin) < 0) {
                alarms.add("温度越界:" + t.getTemperature() + "℃ (正常 " + tempMin + "~" + tempMax + ")");
                status = "warning";
            }
        }
        if (t.getPressure() != null) {
            if (t.getPressure().compareTo(pressMax) > 0 || t.getPressure().compareTo(pressMin) < 0) {
                alarms.add("压力越界:" + t.getPressure() + "MPa (正常 " + pressMin + "~" + pressMax + ")");
                status = "warning";
            }
        }
        if (t.getVibration() != null && t.getVibration().compareTo(vibMax) > 0) {
            alarms.add("振动越界:" + t.getVibration() + "mm/s (上限 " + vibMax + ")");
            status = "alarm";
        }
        // å¤šé¡¹åŒæ—¶è¶Šç•Œå‡çº§ä¸º alarm
        if (alarms.size() >= 2) {
            status = "alarm";
        }
        t.setStatus(status);
        t.setAlarmContent(alarms.isEmpty() ? null : String.join(";", alarms));
    }
    @Override
    public List<DeviceMonitorVo> monitorList() {
        List<DeviceLedger> devices = deviceLedgerMapper.selectList(null);
        if (devices == null || devices.isEmpty()) {
            return Collections.emptyList();
        }
        List<DeviceMonitorVo> result = new ArrayList<>();
        for (DeviceLedger device : devices) {
            List<DeviceTelemetry> recent = telemetryMapper.selectList(
                    Wrappers.<DeviceTelemetry>lambdaQuery()
                            .eq(DeviceTelemetry::getDeviceLedgerId, device.getId())
                            .orderByDesc(DeviceTelemetry::getCollectTime)
                            .last("limit 30"));
            DeviceMonitorVo vo = new DeviceMonitorVo();
            vo.setDeviceId(device.getId());
            vo.setDeviceName(device.getDeviceName());
            vo.setDeviceModel(device.getDeviceModel());
            vo.setControlStatus(device.getControlStatus() == null ? "running" : device.getControlStatus());
            vo.setThreshold(buildThreshold(device));
            if (!recent.isEmpty()) {
                DeviceTelemetry latest = recent.get(0);
                vo.setTemperature(latest.getTemperature());
                vo.setPressure(latest.getPressure());
                vo.setVibration(latest.getVibration());
                vo.setEnergy(latest.getEnergy());
                vo.setCollectTime(latest.getCollectTime());
                vo.setStatus(latest.getStatus());
                vo.setAlarmContent(latest.getAlarmContent());
            } else {
                vo.setStatus("offline");
            }
            Collections.reverse(recent);
            vo.setSeries(recent);
            result.add(vo);
        }
        return result;
    }
    private DeviceMonitorVo.Threshold buildThreshold(DeviceLedger device) {
        DeviceMonitorVo.Threshold th = new DeviceMonitorVo.Threshold();
        th.setTempMin(nvl(device.getTempMin(), DEFAULT_TEMP_MIN));
        th.setTempMax(nvl(device.getTempMax(), DEFAULT_TEMP_MAX));
        th.setPressureMin(nvl(device.getPressureMin(), DEFAULT_PRESSURE_MIN));
        th.setPressureMax(nvl(device.getPressureMax(), DEFAULT_PRESSURE_MAX));
        th.setVibrationMax(nvl(device.getVibrationMax(), DEFAULT_VIBRATION_MAX));
        return th;
    }
    @Override
    public List<DeviceTelemetry> history(Long deviceId, Integer limit) {
        int size = limit == null || limit <= 0 ? 100 : Math.min(limit, 1000);
        List<DeviceTelemetry> list = telemetryMapper.selectList(
                Wrappers.<DeviceTelemetry>lambdaQuery()
                        .eq(deviceId != null, DeviceTelemetry::getDeviceLedgerId, deviceId)
                        .orderByDesc(DeviceTelemetry::getCollectTime)
                        .last("limit " + size));
        Collections.reverse(list);
        return list;
    }
    @Override
    public Map<String, Object> control(Long deviceId, String action) {
        DeviceLedger device = deviceLedgerMapper.selectById(deviceId);
        if (device == null) {
            throw new IllegalArgumentException("设备不存在");
        }
        String status;
        if ("start".equalsIgnoreCase(action)) {
            status = "running";
        } else if ("stop".equalsIgnoreCase(action)) {
            status = "stopped";
        } else {
            throw new IllegalArgumentException("不支持的操作:" + action);
        }
        DeviceLedger update = new DeviceLedger();
        update.setId(deviceId);
        update.setControlStatus(status);
        deviceLedgerMapper.updateById(update);
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("deviceId", deviceId);
        result.put("deviceName", device.getDeviceName());
        result.put("controlStatus", status);
        result.put("commandTime", LocalDateTime.now().toString());
        result.put("message", "running".equals(status) ? "启动指令已下发" : "停止指令已下发");
        log.info("[远程控制] deviceId={}, action={}, status={}", deviceId, action, status);
        return result;
    }
    @Override
    public List<DeviceMonitorVo> alarms() {
        List<DeviceMonitorVo> all = monitorList();
        return all.stream()
                .filter(v -> "warning".equals(v.getStatus()) || "alarm".equals(v.getStatus()))
                .sorted((a, b) -> {
                    int ra = "alarm".equals(a.getStatus()) ? 0 : 1;
                    int rb = "alarm".equals(b.getStatus()) ? 0 : 1;
                    if (ra != rb) return ra - rb;
                    if (a.getCollectTime() == null) return 1;
                    if (b.getCollectTime() == null) return -1;
                    return b.getCollectTime().compareTo(a.getCollectTime());
                })
                .collect(Collectors.toList());
    }
    private static BigDecimal scale(double v) {
        return BigDecimal.valueOf(v).setScale(2, RoundingMode.HALF_UP);
    }
    private static double clamp(double v, double min, double max) {
        return Math.max(min, Math.min(max, v));
    }
    private static BigDecimal nvl(BigDecimal value, BigDecimal fallback) {
        return value == null ? fallback : value;
    }
}