diff --git a/xinnengyuan/ruoyi-admin/src/main/resources/application-prod.yml b/xinnengyuan/ruoyi-admin/src/main/resources/application-prod.yml index 41f67e9e..fba67693 100644 --- a/xinnengyuan/ruoyi-admin/src/main/resources/application-prod.yml +++ b/xinnengyuan/ruoyi-admin/src/main/resources/application-prod.yml @@ -326,7 +326,7 @@ ys7: app-key: 3acf9f1a43dc4209841e0893003db0a2 app-secret: 4bbf3e9394f55d3af6e3af27b2d3db36 job: - capture-enabled: true # 控制是否启用萤石抓拍任务 + capture-enabled: false # 控制是否启用萤石抓拍任务 # 斯巴达算法 sparta: url: http://119.3.204.120:8040 diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/pom.xml b/xinnengyuan/ruoyi-modules/ruoyi-system/pom.xml index 5f707192..ce758e5f 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/pom.xml +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/pom.xml @@ -18,6 +18,14 @@ + + + javax.websocket + javax.websocket-api + 1.1 + provided + + diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/design/domain/vo/BusBillofquantitiesVersionsVo.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/design/domain/vo/BusBillofquantitiesVersionsVo.java index f9221e09..745914fd 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/design/domain/vo/BusBillofquantitiesVersionsVo.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/design/domain/vo/BusBillofquantitiesVersionsVo.java @@ -17,7 +17,7 @@ import java.util.Date; /** - * 工程量清单版本视图对象 bus_billofquantities_versions + * 工程量清单版本视图对象 bus_billofquantities_versions * * @author Lion Li * @date 2025-08-11 diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/drone/service/impl/DroProjectDroneServiceImpl.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/drone/service/impl/DroProjectDroneServiceImpl.java index 0c1bf8d5..01dd4f8a 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/drone/service/impl/DroProjectDroneServiceImpl.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/drone/service/impl/DroProjectDroneServiceImpl.java @@ -1,5 +1,6 @@ package org.dromara.drone.service.impl; +import cn.hutool.core.collection.CollUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; @@ -151,7 +152,9 @@ public class DroProjectDroneServiceImpl extends ServiceImpl list = this.lambdaQuery() .eq(DroProjectDrone::getProjectId, projectId) .list(); - droneManager.addAirportInfoByProject(list.stream().map(DroProjectDrone::getDroneSn).toList(), projectId); + if (CollUtil.isNotEmpty(list)) { + droneManager.addAirportInfoByProject(list.stream().map(DroProjectDrone::getDroneSn).toList(), projectId); + } return true; } diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/DeviceMessageSender.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/DeviceMessageSender.java new file mode 100644 index 00000000..acff4231 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/DeviceMessageSender.java @@ -0,0 +1,166 @@ +package org.dromara.mobileAttendanceMachine; + +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; + +/** + * @Author 铁憨憨 + * @Date 2025/10/14 15:48 + * @Version 1.0 + *

+ * 考勤设备消息发送工具类(对应Golang的ws包工具方法) + */ + +@Service +@Log4j2 +public class DeviceMessageSender { + /** + * 组装下发人员信息(对应Golang的TheSenderInformationOfTheAssemblyPersonnel) + * + * @param sn 设备编号 + * @param userId 用户ID + * @param name 用户名 + * @param face 人脸模板地址(HTTP链接) + * @return 异常信息(无异常则返回null) + */ + public static Exception sendPersonnelInformation(String sn, String userId, String name, String face) { + try { + // 生成UUID + String sUuid = DeviceWebSocketServer.generateUUIDWithSixRandomDigits(); + + // 构建人员信息消息体 + KqjEntity.PeopleInformation people = new KqjEntity().new PeopleInformation(); + people.setCmd("to_device"); + people.setFrom(sUuid); + people.setTo(sn); + + // 构建消息数据 + KqjEntity.PeopleInData data = new KqjEntity().new PeopleInData(); + data.setCmd("addUser"); + data.setUserId(userId); + data.setName(name); + data.setFaceTemplate(face); + data.setIdValid(""); // 永久有效 + people.setData(data); + + // 发送请求并等待响应(忽略响应结果,只关注是否成功) + DeviceWebSocketServer.sendRequestAndWaitResponse(sn, sUuid, people); + return null; + } catch (Exception e) { + log.error("下发人员信息失败,SN: {}, UserID: {}", sn, userId, e); + return e; + } + } + + /** + * 获取打卡设备所有人员(对应Golang的SelectUserAll) + * + * @param sn 设备编号 + * @return 响应结果(包含人员信息) + * @throws Exception 发送或接收过程中的异常 + */ + public static KqjEntity.CommonResponse getAllUsers(String sn) throws Exception { + // 生成UUID + String sUuid = DeviceWebSocketServer.generateUUIDWithSixRandomDigits(); + + // 构建获取人员信息请求 + KqjEntity.PersonnelInformationAcquisition request = new KqjEntity().new PersonnelInformationAcquisition(); + request.setCmd("to_device"); + request.setFrom(sUuid); + request.setTo(sn); + + // 构建请求数据 + KqjEntity.PersonnelInformationAcquisitionTwo data = new KqjEntity().new PersonnelInformationAcquisitionTwo(); + data.setCmd("getUserInfo"); + data.setValue(1); // 1表示获取所有人员 + request.setData(data); + + // 发送请求并返回响应 + return DeviceWebSocketServer.sendRequestAndWaitResponse(sn, sUuid, request); + } + + /** + * 删除指定人员(对应Golang的DelByUserId) + * + * @param sn 设备编号 + * @param userId 要删除的用户ID + * @return 响应结果 + * @throws Exception 发送或接收过程中的异常 + */ + public static KqjEntity.CommonResponse deleteUser(String sn, String userId) throws Exception { + // 生成UUID + String sUuid = DeviceWebSocketServer.generateUUIDWithSixRandomDigits(); + + // 构建删除人员请求 + KqjEntity.DeletionOfPersonnel request = new KqjEntity().new DeletionOfPersonnel(); + request.setCmd("to_device"); + request.setFrom(sUuid); + request.setTo(sn); + + // 构建请求数据 + KqjEntity.DeletionOfPersonnelData data = new KqjEntity().new DeletionOfPersonnelData(); + data.setCmd("delUser"); + data.setUserId(userId); + data.setUserType(0); // 0表示人脸接口下发的数据 + request.setData(data); + + // 发送请求并返回响应 + return DeviceWebSocketServer.sendRequestAndWaitResponse(sn, sUuid, request); + } + + /** + * 批量删除指定人员(对应Golang的BatchDelete) + * + * @param sn 设备编号 + * @param userIds 要删除的用户ID列表 + * @return 响应结果 + * @throws Exception 发送或接收过程中的异常 + */ + public static KqjEntity.CommonResponse batchDeleteUsers(String sn, String[] userIds) throws Exception { + // 生成UUID + String sUuid = DeviceWebSocketServer.generateUUIDWithSixRandomDigits(); + + // 构建批量删除请求 + KqjEntity.BatchDeletion request = new KqjEntity().new BatchDeletion(); + request.setCmd("to_device"); + request.setFrom(sUuid); + request.setTo(sn); + + // 构建请求数据 + KqjEntity.BatchDeletionData data = new KqjEntity().new BatchDeletionData(); + data.setCmd("delMultiUser"); + data.setUserIds(userIds); + data.setUserType(0); // 0表示人脸接口下发的数据 + request.setData(data); + + // 发送请求并返回响应 + return DeviceWebSocketServer.sendRequestAndWaitResponse(sn, sUuid, request); + } + + /** + * 删除指定考勤机全部人员(对应Golang的DelAll) + * + * @param sn 设备编号 + * @return 响应结果 + * @throws Exception 发送或接收过程中的异常 + */ + public static KqjEntity.CommonResponse deleteAllUsers(String sn) throws Exception { + // 生成UUID + String sUuid = DeviceWebSocketServer.generateUUIDWithSixRandomDigits(); + + // 构建删除全部人员请求 + KqjEntity.DeletionALlOfPersonnel request = new KqjEntity().new DeletionALlOfPersonnel(); + request.setCmd("to_device"); + request.setFrom(sUuid); + request.setTo(sn); + + // 构建请求数据 + KqjEntity.DeletionALlOfPersonnelData data = new KqjEntity().new DeletionALlOfPersonnelData(); + data.setCmd("delAllUser"); + data.setUserType(0); // 0表示人脸接口下发的数据 + request.setData(data); + + // 发送请求并返回响应 + return DeviceWebSocketServer.sendRequestAndWaitResponse(sn, sUuid, request); + } +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/DeviceWebSocketServer.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/DeviceWebSocketServer.java new file mode 100644 index 00000000..33532311 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/DeviceWebSocketServer.java @@ -0,0 +1,470 @@ +package org.dromara.mobileAttendanceMachine; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.log4j.Log4j2; + +import javax.websocket.*; +import javax.websocket.server.ServerEndpoint; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.http.WebSocket; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +/** + * WebSocket服务端实现(对应Golang的HandleWebSocket逻辑) + * ServerEndpoint注解指定WebSocket连接路径 + */ +@ServerEndpoint("/ws/device") +@Log4j2 +public class DeviceWebSocketServer { + + // ------------------------------ 常量定义(对应Golang的const) ------------------------------ + public static class Constants { + public static final String DECLARE = "declare"; // 设备初上线 + public static final String PING = "ping"; // 心跳 + public static final String TO_CLIENT = "to_client"; // 服务器消息下发到客户端的响应 + } + + // JSON序列化/反序列化工具(单例) + private static final ObjectMapper objectMapper = new ObjectMapper(); + + // 1. 存储所有连接的设备信息(key: 设备SN,value: 设备信息) + private static final Map connectedDevices = new ConcurrentHashMap<>(); + // 2. 存储UUID对应的响应通道(key: UUID,value: 响应结果容器) + private static final Map> responseChannels = new ConcurrentHashMap<>(); + + // 当前连接的WebSocket会话 + private Session session; + // 当前连接的设备SN(连接建立后从DECLARE消息中提取) + private String currentDeviceSn; + + + /** + * 连接建立时触发(对应Golang中upgrader.Upgrade后的初始化逻辑) + */ + @OnOpen + public void onOpen(Session session) { + this.session = session; + log.info("新的WebSocket连接建立,会话ID: {}", session.getId()); + + try { + // 读取设备第一条消息(DECLARE消息),完成设备注册 + KqjEntity.DeclareMessage declareMsg = registerDevice(); + this.currentDeviceSn = declareMsg.getSn(); + log.info("设备注册成功,SN: {}, 设备信息: {}", currentDeviceSn, declareMsg); + + } catch (Exception e) { + log.error("设备注册失败,关闭连接", e); + try { + session.close(new CloseReason(CloseReason.CloseCodes.PROTOCOL_ERROR, "设备注册失败")); + } catch (IOException ex) { + log.error("关闭异常连接失败", ex); + } + } + } + + + /** + * 接收客户端消息时触发(对应Golang的for循环读取消息逻辑) + */ + @OnMessage + public void onMessage(String message, Session session) { + if (message == null || message.isEmpty()) { + log.warn("收到空消息,忽略处理"); + return; + } + + try { + // 先解析通用消息的CMD字段(对应Golang的GenericMessage) + KqjEntity.GenericMessage genericMsg = objectMapper.readValue(message, KqjEntity.GenericMessage.class); + String cmd = genericMsg.getCmd(); + if (cmd == null) { + log.warn("收到无CMD字段的消息,忽略: {}", message); + return; + } + + // 根据CMD类型处理不同逻辑(对应Golang的switch case) + switch (cmd) { + case Constants.DECLARE: + log.info("设备在线心跳(DECLARE),SN: {}", currentDeviceSn); + break; + + case Constants.PING: + handlePing(message); + break; + + case Constants.TO_CLIENT: + handleToClientResponse(message); + break; + + default: + log.warn("收到未知CMD消息,类型: {}, 内容: {}", cmd, message); + } + + } catch (Exception e) { + log.error("处理消息失败,消息内容: {}", message, e); + } + } + + + /** + * 连接关闭时触发(对应Golang的defer conn.Close()和资源清理逻辑) + */ + @OnClose + public void onClose(Session session, CloseReason closeReason) { + log.info("WebSocket连接关闭,会话ID: {}, 原因: {}", session.getId(), closeReason); + + // 1. 移除设备连接信息 + if (currentDeviceSn != null) { + connectedDevices.remove(currentDeviceSn); + // 更新设备状态为离线(对应Golang的service.BusAttendanceMachine().Change) + updateDeviceStatus(currentDeviceSn, "0"); + } + + // 2. 清理当前设备对应的响应通道(避免内存泄漏) + responseChannels.entrySet().removeIf(entry -> { + if (entry.getValue().getSn().equals(currentDeviceSn)) { + entry.getValue().getResultFuture().completeExceptionally( + new Exception("设备连接已关闭,响应通道清理") + ); + return true; + } + return false; + }); + + this.session = null; + this.currentDeviceSn = null; + } + + + /** + * 连接异常时触发 + */ + @OnError + public void onError(Session session, Throwable throwable) { + log.error("WebSocket连接异常,会话ID: {}", session.getId(), throwable); + // 异常时主动关闭连接 + try { + if (session.isOpen()) { + session.close(new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, "连接异常")); + } + } catch (IOException e) { + log.error("关闭异常连接失败", e); + } + } + + + // ------------------------------ 核心业务方法 ------------------------------ + + /** + * 设备注册(对应Golang的addDevice函数) + * 读取DECLARE消息,解析设备信息并存储 + */ + private KqjEntity.DeclareMessage registerDevice() throws Exception { + // 阻塞读取第一条消息(DECLARE消息) + String firstMessage = readFirstMessage(); + KqjEntity.DeclareMessage declareMsg = objectMapper.readValue(firstMessage, KqjEntity.DeclareMessage.class); + + // 校验SN合法性 + String sn = declareMsg.getSn(); + if (sn == null || sn.isEmpty()) { + throw new IllegalArgumentException("设备SN为空,注册失败"); + } + + // 解析客户端IP和端口(对应Golang的parseRemoteAddr) + InetSocketAddress remoteAddr = (InetSocketAddress) session.getUserProperties().get("javax.websocket.endpoint.remoteAddress"); + String ip = remoteAddr.getAddress().getHostAddress(); + String port = String.valueOf(remoteAddr.getPort()); + + // 存储设备信息 + KqjEntity.DeviceInfo deviceInfo = new KqjEntity.DeviceInfo(); + deviceInfo.setIp(ip); + deviceInfo.setPort(port); + + // ======================== 添加类型检查 ======================== + // 检查session是否能转换为WebSocket,避免强转失败 + if (!(session instanceof WebSocket)) { + throw new IllegalStateException("WebSocket容器不支持Session转WebSocket,无法注册设备,SN: " + sn); + } + // 强转并赋值 + deviceInfo.setConn((WebSocket) session); // 用Session替换Golang的*websocket.Conn + // ============================================================ + + + connectedDevices.put(sn, deviceInfo); + + // 调用业务服务注册设备(对应Golang的service.BusAttendanceMachine().Register) + registerDeviceToService(sn); + + return declareMsg; + } + + + /** + * 处理PING消息(对应Golang的handlePing函数) + */ + private void handlePing(String message) throws Exception { + KqjEntity.DeclareMessage pingMsg = objectMapper.readValue(message, KqjEntity.DeclareMessage.class); + String sn = pingMsg.getSn(); + + // 1. 回复PONG消息 + KqjEntity.PongMessage pongMsg = new KqjEntity.PongMessage(); + pongMsg.setCmd(Constants.PING); + pongMsg.setFrom("server"); + pongMsg.setTo(sn); + + KqjEntity.PongMessageData pongData = new KqjEntity.PongMessageData(); + pongData.setCmd("pong"); + pongMsg.setData(pongData); + + // 发送PONG消息 + session.getBasicRemote().sendText(objectMapper.writeValueAsString(pongMsg)); + log.info("发送PONG消息给设备,SN: {}", sn); + + // 2. 更新设备连接状态(对应Golang的connectedDevices[sn]重设) + if (connectedDevices.containsKey(sn)) { + KqjEntity.DeviceInfo deviceInfo = connectedDevices.get(sn); + deviceInfo.setConn((WebSocket) session); + connectedDevices.put(sn, deviceInfo); + } + + // 3. 调用业务服务更新设备状态(对应Golang的service.BusAttendanceMachine().Register) + registerDeviceToService(sn); + } + + + /** + * 处理TO_CLIENT响应消息(对应Golang的requestResponse函数) + */ + private void handleToClientResponse(String message) throws Exception { + log.info("收到TO_CLIENT响应消息: {}", message); + KqjEntity.CommonResponse commonResp = objectMapper.readValue(message, KqjEntity.CommonResponse.class); + + // 根据UUID查找响应通道,传递响应结果 + String uuid = commonResp.getTo(); + ResponseHolder holder = responseChannels.get(uuid); + if (holder != null) { + holder.getResultFuture().complete(commonResp); + responseChannels.remove(uuid); // 移除已完成的通道 + log.info("响应已分发到UUID: {}, 响应内容: {}", uuid, commonResp); + } else { + log.warn("未找到UUID: {}对应的响应通道,响应丢弃", uuid); + } + } + + + // ------------------------------ 工具方法 ------------------------------ + + /** + * 读取设备第一条消息(阻塞直到收到消息) + */ + private String readFirstMessage() throws Exception { + // 使用CompletableFuture等待消息 + final java.util.concurrent.CompletableFuture firstMsgFuture = new java.util.concurrent.CompletableFuture<>(); + // 使用AtomicReference包装临时处理器,解决未初始化问题 + final java.util.concurrent.atomic.AtomicReference> tempHandlerRef = new java.util.concurrent.atomic.AtomicReference<>(); + + // 定义临时消息处理器 + MessageHandler.Whole tempHandler = msg -> { + if (!firstMsgFuture.isDone()) { + firstMsgFuture.complete(msg); + // 从引用中获取处理器并移除 + session.removeMessageHandler(tempHandlerRef.get()); + } + }; + // 将处理器存入引用 + tempHandlerRef.set(tempHandler); + // 注册处理器 + session.addMessageHandler(tempHandler); + + // 等待消息,超时10秒 + return firstMsgFuture.get(10, TimeUnit.SECONDS); +// // 使用Java并发工具等待消息(模拟Golang的阻塞读取) +// final java.util.concurrent.CompletableFuture firstMsgFuture = new java.util.concurrent.CompletableFuture<>(); +// +// // 临时注册消息处理器,读取第一条消息后移除 +// MessageHandler.Whole tempHandler = msg -> { +// if (!firstMsgFuture.isDone()) { +// firstMsgFuture.complete(msg); +// // 移除临时处理器(避免重复处理) +// session.removeMessageHandler(tempHandler); +// } +// }; +// session.addMessageHandler(tempHandler); +// +// // 等待消息,超时10秒(防止设备一直不发消息) +// return firstMsgFuture.get(10, TimeUnit.SECONDS); + } + + + /** + * 发送消息给指定设备(对应Golang的sendMessageToDevice函数) + */ + public static boolean sendMessageToDevice(String sn, String uuid, Object message) { + // 1. 检查设备是否在线 + KqjEntity.DeviceInfo deviceInfo = connectedDevices.get(sn); + if (deviceInfo == null) { + log.warn("设备不存在,SN: {}", sn); + responseChannels.remove(uuid); + return false; + } + + // 2. 将WebSocket转回Session(因为状态由Session管理) + WebSocket webSocket = deviceInfo.getConn(); + if (!(webSocket instanceof Session)) { + log.warn("设备连接类型错误,无法判断状态,SN: {}", sn); + responseChannels.remove(uuid); + return false; + } + Session session = (Session) webSocket; + + // 3. 检查连接是否打开 + if (!session.isOpen()) { + log.warn("设备连接已关闭,SN: {}", sn); + connectedDevices.remove(sn); // 移除已关闭的设备 + responseChannels.remove(uuid); + return false; + } + + try { + // 4. 序列化消息并发送 + String msgJson = objectMapper.writeValueAsString(message); + session.getBasicRemote().sendText(msgJson); // 通过Session发送消息 + log.info("发送消息给设备,SN: {}, UUID: {}, 消息: {}", sn, uuid, msgJson); + return true; + + } catch (Exception e) { + log.error("发送消息失败,SN: {}, UUID: {}", sn, uuid, e); + // 发送失败时移除设备(可能连接已异常) + connectedDevices.remove(sn); + responseChannels.remove(uuid); + return false; + } +// // 1. 检查设备是否在线 +// KqjEntity.DeviceInfo deviceInfo = connectedDevices.get(sn); +// if (deviceInfo == null || !deviceInfo.getSession().isOpen()) { +// log.warn("设备不在线,SN: {}", sn); +// // 清理无效的响应通道 +// responseChannels.remove(uuid); +// return false; +// } +// +// try { +// // 2. 序列化消息并发送 +// String msgJson = objectMapper.writeValueAsString(message); +// deviceInfo.getSession().getBasicRemote().sendText(msgJson); +// log.info("发送消息给设备,SN: {}, UUID: {}, 消息: {}", sn, uuid, msgJson); +// return true; +// +// } catch (Exception e) { +// log.error("发送消息失败,SN: {}, UUID: {}", sn, uuid, e); +// // 清理异常的响应通道 +// responseChannels.remove(uuid); +// return false; +// } + } + + + /** + * 发送请求并等待响应(对应Golang的SendRequestAndWaitResponse函数) + */ + public static KqjEntity.CommonResponse sendRequestAndWaitResponse(String sn, String uuid, Object payload) throws Exception { + // 1. 创建响应结果容器 + ResponseHolder responseHolder = new ResponseHolder<>(sn); + responseChannels.put(uuid, responseHolder); + + try { + // 2. 发送请求 + boolean sendSuccess = sendMessageToDevice(sn, uuid, payload); + if (!sendSuccess) { + throw new Exception("发送请求失败,设备不在线或发送异常,SN: " + sn); + } + + // 3. 等待响应,超时10秒 + return responseHolder.getResultFuture().get(10, TimeUnit.SECONDS); + + } catch (java.util.concurrent.TimeoutException e) { + log.error("等待响应超时,SN: {}, UUID: {}", sn, uuid); + responseChannels.remove(uuid); + throw new Exception("等待响应超时(10秒)", e); + + } finally { + // 清理响应通道(防止内存泄漏) + responseChannels.remove(uuid); + } + } + + + /** + * 生成带6位随机数的UUID(对应Golang的GenerateUUIDWithSixRandomDigits函数) + */ + public static String generateUUIDWithSixRandomDigits() { + // 生成标准UUID + String uuidStr = UUID.randomUUID().toString().replace("-", ""); + // 生成6位随机数(100000-999999) + Random random = new Random(); + int randomNum = random.nextInt(900000) + 100000; + // 拼接返回 + return uuidStr + "-" + randomNum; + } + + + // ------------------------------ 业务服务调用(模拟Golang的service层) ------------------------------ + + /** + * 注册设备到业务服务(对应Golang的service.BusAttendanceMachine().Register) + * 实际项目中替换为真实的Service调用 + */ + private void registerDeviceToService(String sn) { + try { + // 模拟业务服务调用(如更新数据库设备状态为在线) + log.info("调用业务服务注册设备,SN: {}", sn); + // TODO: 替换为真实的Service代码(如Spring Bean调用) + } catch (Exception e) { + log.error("业务服务注册设备失败,SN: {}", sn, e); + } + } + + + /** + * 更新设备状态(对应Golang的service.BusAttendanceMachine().Change) + * 实际项目中替换为真实的Service调用 + */ + private void updateDeviceStatus(String sn, String status) { + try { + log.info("调用业务服务更新设备状态,SN: {}, 状态: {}", sn, status); + // TODO: 替换为真实的Service代码(如Spring Bean调用) + } catch (Exception e) { + log.error("业务服务更新设备状态失败,SN: {}", sn, e); + } + } + + + // ------------------------------ 内部辅助类 ------------------------------ + + /** + * 响应结果容器(对应Golang的chan CommonResponse) + * 用CompletableFuture实现异步结果等待 + */ + private static class ResponseHolder { + private final String sn; // 关联的设备SN + private final java.util.concurrent.CompletableFuture resultFuture; + + public ResponseHolder(String sn) { + this.sn = sn; + this.resultFuture = new java.util.concurrent.CompletableFuture<>(); + } + + public String getSn() { + return sn; + } + + public java.util.concurrent.CompletableFuture getResultFuture() { + return resultFuture; + } + } +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/KqjEntity.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/KqjEntity.java new file mode 100644 index 00000000..e505dd7a --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/KqjEntity.java @@ -0,0 +1,297 @@ +package org.dromara.mobileAttendanceMachine; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.net.http.WebSocket; + +/** + * @Author 铁憨憨 + * @Date 2025/10/14 14:47 + * @Version 1.0 + */ + +@Data +@AllArgsConstructor +public class KqjEntity { + + @Data + public static class DeviceInfo { + @JsonProperty("ip") + private String ip; + + @JsonProperty("port") + private String port; + + @JsonProperty("conn") + private WebSocket conn; + } + + /** + * 通用消息结构,用于解析初始的 cmd 字段 + */ + @Data + public class GenericMessage { + @JsonProperty("cmd") + private String cmd; + } + + /** + * 公共响应结构 + */ + @Data + public class CommonResponse { + @JsonProperty("cmd") + private String cmd; + + @JsonProperty("from") + private String from; + + @JsonProperty("to") + private String to; + + @JsonProperty("data") + private CommonResponseData data; + } + + @Data + public class CommonResponseData { + @JsonProperty("cmd") + private String cmd; + + @JsonProperty("userIds") + private String[] userIds; // 用户IDS + + @JsonProperty("user_id") + private String userId; + + @JsonProperty("code") + private int code; + + @JsonProperty("msg") + private String msg; + + @JsonProperty("delFailed") + private DelMultiUserData[] delFailed; + } + + @Data + public class DelMultiUserData { + @JsonProperty("user_id") + private String userId; + + @JsonProperty("code") + private int code; + + @JsonProperty("msg") + private String msg; + } + + + /** + * 设备上线消息 + */ + @Data + public class DeclareMessage { + @JsonProperty("cmd") + private String cmd; + + @JsonProperty("type") + private String type; + + @JsonProperty("sn") + private String sn; + + @JsonProperty("version_code") + private String versionCode; + + @JsonProperty("version_name") + private String versionName; + + @JsonProperty("token") + private String token; + + @JsonProperty("ip") + private String ip; + + @JsonProperty("timestamp") + private double timestamp; + } + + @Data + public static class PongMessage { + @JsonProperty("cmd") + private String cmd; + + @JsonProperty("from") + private String from; + + @JsonProperty("to") + private String to; + + @JsonProperty("data") + private PongMessageData data; + } + + + /** + * 心跳回复消息结构 + */ + @Data + public static class PongMessageData { + @JsonProperty("cmd") + private String cmd; + } + + /** + * 人员信息下发 + */ + @Data + public class PeopleInformation { + @JsonProperty("cmd") + private String cmd; // 该接口固定为to_device + + @JsonProperty("from") + private String from; // 可不填写,填写uuid来做为发送请求或响应的标识 + + @JsonProperty("to") + private String to; // 设备号(请查看公共设置中的设备号) + + @JsonProperty("data") + private PeopleInData data; + } + + @Data + public class PeopleInData { + @JsonProperty("cmd") + private String cmd; + + @JsonProperty("user_id") + private String userId; + + @JsonProperty("name") + private String name; + + @JsonProperty("face_template") + private String faceTemplate; // http 链接图 + + @JsonProperty("id_valid") + private String idValid; // 人员有效期(人员在这个时间点后,无法通行)格式:yyyy-MM-dd 或者 yyyy-MM-dd HH:mm,为 "" 则为永久 + } + + /** + * 删除人员 + */ + @Data + public class DeletionOfPersonnel { + @JsonProperty("cmd") + private String cmd; + + @JsonProperty("from") + private String from; + + @JsonProperty("to") + private String to; + + @JsonProperty("data") + private DeletionOfPersonnelData data; + } + + @Data + public class DeletionOfPersonnelData { + @JsonProperty("cmd") + private String cmd; + + @JsonProperty("user_id") + private String userId; + + @JsonProperty("user_type") + private int userType; // 删除的用户类型:0-人脸接口下发的数据 1-人证比对接口下发的数据 + } + + /** + * 批量删除 + */ + @Data + public class BatchDeletion { + @JsonProperty("cmd") + private String cmd; + + @JsonProperty("from") + private String from; + + @JsonProperty("to") + private String to; + + @JsonProperty("data") + private BatchDeletionData data; + } + + @Data + public class BatchDeletionData { + @JsonProperty("cmd") + private String cmd; + + @JsonProperty("user_ids") + private String[] userIds; + + @JsonProperty("user_type") + private int userType; // 删除的用户类型:0-人脸接口下发的数据 1-人证比对接口下发的数据 + } + + /** + * 删除全部人员 + */ + @Data + public class DeletionALlOfPersonnel { + @JsonProperty("cmd") + private String cmd; + + @JsonProperty("from") + private String from; + + @JsonProperty("to") + private String to; + + @JsonProperty("data") + private DeletionALlOfPersonnelData data; + } + + @Data + public class DeletionALlOfPersonnelData { + @JsonProperty("cmd") + private String cmd; + + @JsonProperty("user_type") + private int userType; // 删除的用户类型:0-人脸接口下发的数据 1-人证比对接口下发的数据 + } + + /** + * 人员信息获取 + */ + @Data + public class PersonnelInformationAcquisition { + @JsonProperty("cmd") + private String cmd; // 该接口固定为to_device + + @JsonProperty("from") + private String from; // 可不填写,填写uuid来做为发送请求或响应的标识 + + @JsonProperty("to") + private String to; // 设备号(请查看公共设置中的设备号) + + @JsonProperty("data") + private PersonnelInformationAcquisitionTwo data; + } + + @Data + public class PersonnelInformationAcquisitionTwo { + @JsonProperty("cmd") + private String cmd; + + @JsonProperty("value") + private int value; + } + +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/WebSocketConfig.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/WebSocketConfig.java new file mode 100644 index 00000000..7f2f5d36 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/WebSocketConfig.java @@ -0,0 +1,24 @@ +package org.dromara.mobileAttendanceMachine; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +/** + * @Author 铁憨憨 + * @Date 2025/10/14 16:11 + * @Version 1.0 + * + * 系统启动就会开启ws + */ +@Configuration +public class WebSocketConfig { + + /** + * 自动注册所有标注了@ServerEndpoint的WebSocket端点 + */ + @Bean + public ServerEndpointExporter serverEndpointExporter() { + return new ServerEndpointExporter(); + } +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/controller/PgsProgressCategoryController.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/controller/PgsProgressCategoryController.java index 2e811382..1f78cb59 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/controller/PgsProgressCategoryController.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/controller/PgsProgressCategoryController.java @@ -90,6 +90,16 @@ public class PgsProgressCategoryController extends BaseController { return R.ok(list); } + /** + * 查询进度类别的甘特图结构列表 + */ + @SaCheckPermission("progress:progressCategory:query") + @GetMapping("/list/gantt/{progressCategoryId}") + public R> listGanttStructure(@NotNull(message = "类别主键不能为空") + @PathVariable Long progressCategoryId) { + return R.ok(pgsProgressCategoryService.getGanttStructureList(progressCategoryId)); + } + /** * 导出进度类别列表 */ @@ -353,7 +363,7 @@ public class PgsProgressCategoryController extends BaseController { @SaCheckPermission("progress:progressCategory:query") @GetMapping("/gantt/{projectId}") public R> listGantt(@NotNull(message = "主键不能为空") - @PathVariable Long projectId) { + @PathVariable Long projectId) { return R.ok(pgsProgressCategoryService.listGanttByProject(projectId)); } diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/domain/dto/progresscategory/PgsProgressCategoryProgressDto.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/domain/dto/progresscategory/PgsProgressCategoryProgressDto.java new file mode 100644 index 00000000..cbddb447 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/domain/dto/progresscategory/PgsProgressCategoryProgressDto.java @@ -0,0 +1,37 @@ +package org.dromara.progress.domain.dto.progresscategory; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +/** + * @author lilemy + * @date 2025-10-13 17:50 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PgsProgressCategoryProgressDto { + + /** + * 已完成百分比 + */ + private BigDecimal finishProgress; + + /** + * 已计划百分比 + */ + private BigDecimal planProgress; + + /** + * 未计划百分比 + */ + private BigDecimal unPlanProgress; + + /** + * 延期百分比 + */ + private BigDecimal delayProgress; +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/domain/vo/progresscategory/PgsProgressCategoryGanttMatrixVo.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/domain/vo/progresscategory/PgsProgressCategoryGanttMatrixVo.java new file mode 100644 index 00000000..1509f64b --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/domain/vo/progresscategory/PgsProgressCategoryGanttMatrixVo.java @@ -0,0 +1,37 @@ +package org.dromara.progress.domain.vo.progresscategory; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * @author lilemy + * @date 2025-10-14 16:24 + */ +@Data +public class PgsProgressCategoryGanttMatrixVo implements Serializable { + + @Serial + private static final long serialVersionUID = 2999870755203852970L; + + /** + * 主键id + */ + private Long matrixId; + + /** + * 名称 + */ + private String matrixName; + + /** + * 关联类别id + */ + private Long progressCategoryId; + + /** + * 关联类别名称 + */ + private String progressCategoryName; +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/domain/vo/progresscategory/PgsProgressCategoryGanttSubProjectVo.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/domain/vo/progresscategory/PgsProgressCategoryGanttSubProjectVo.java new file mode 100644 index 00000000..e10c26b8 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/domain/vo/progresscategory/PgsProgressCategoryGanttSubProjectVo.java @@ -0,0 +1,51 @@ +package org.dromara.progress.domain.vo.progresscategory; + +import lombok.Data; +import org.dromara.common.translation.annotation.Translation; +import org.dromara.common.translation.constant.TransConstant; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * @author lilemy + * @date 2025-10-14 16:22 + */ +@Data +public class PgsProgressCategoryGanttSubProjectVo implements Serializable { + + @Serial + private static final long serialVersionUID = -8517387026258889743L; + + /** + * 关联结构(1子项目 2方阵 3项目) + */ + private String relevancyStructure; + + /** + * 主键id + */ + private Long subProjectId; + + /** + * 名称 + */ + @Translation(type = TransConstant.PROJECT_ID_TO_NAME,mapper = "subProjectId") + private String subProjectName; + + /** + * 关联类别id + */ + private Long progressCategoryId; + + /** + * 关联类别名称 + */ + private String progressCategoryName; + + /** + * 关联方阵结构列表 + */ + private List children; +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/domain/vo/progresscategory/PgsProgressCategoryGanttVo.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/domain/vo/progresscategory/PgsProgressCategoryGanttVo.java index c4a0d7b1..bab051d6 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/domain/vo/progresscategory/PgsProgressCategoryGanttVo.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/domain/vo/progresscategory/PgsProgressCategoryGanttVo.java @@ -43,7 +43,37 @@ public class PgsProgressCategoryGanttVo implements Serializable { private LocalDate endDate; /** - * 进度 + * 已完成百分比 */ - private BigDecimal progress; + private BigDecimal finishProgress; + + /** + * 已计划百分比 + */ + private BigDecimal planProgress; + + /** + * 未计划百分比 + */ + private BigDecimal unPlanProgress; + + /** + * 延期百分比 + */ + private BigDecimal delayProgress; + + /** + * 关联结构(1子项目 2方阵 3项目) + */ + private String relevancyStructure; + + /** + * 完成状态(0未开始 1进行中 2已完成) + */ + private String status; + + /** + * 排序 + */ + private Long sort; } diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/service/IPgsProgressCategoryService.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/service/IPgsProgressCategoryService.java index 0ca22b2a..6d446563 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/service/IPgsProgressCategoryService.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/service/IPgsProgressCategoryService.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.IService; import org.dromara.facility.domain.FacMatrix; import org.dromara.progress.domain.PgsProgressCategory; +import org.dromara.progress.domain.PgsProgressPlan; import org.dromara.progress.domain.dto.progresscategory.*; import org.dromara.progress.domain.vo.progresscategory.*; @@ -198,6 +199,14 @@ public interface IPgsProgressCategoryService extends IService categoryList); + /** + * 获取项目进度百分比 + * + * @param categoryList 项目进度列表 + * @return 项目进度百分比 + */ + PgsProgressCategoryProgressDto getProgressPercentage(List categoryList, List planList); + /** * 获取项目进度类别未完成数量 * @@ -229,4 +238,12 @@ public interface IPgsProgressCategoryService extends IService listGanttByProject(Long projectId); + + /** + * 根据进度类别,获取进度类别甘特图结构 + * + * @param progressCategoryId 进度类别id + * @return 进度类别甘特图结构 + */ + List getGanttStructureList(Long progressCategoryId); } diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/service/impl/PgsProgressCategoryServiceImpl.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/service/impl/PgsProgressCategoryServiceImpl.java index 2945bf20..f83ddab2 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/service/impl/PgsProgressCategoryServiceImpl.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/service/impl/PgsProgressCategoryServiceImpl.java @@ -166,13 +166,19 @@ public class PgsProgressCategoryServiceImpl extends ServiceImpl 0) { + BigDecimal allNumber = progressPlanService.getCurrentPlanAndFinishedNumber(progressCategory); + total = PgsProgressUnitTypeEnum.PERCENTAGE.getValue().equals(progressCategory.getUnitType()) ? + new BigDecimal(100) : total; + // 剩余数量 + BigDecimal planTotal = total.subtract(allNumber); + lastTimeVo.setLeftNum(planTotal); + } else { + lastTimeVo.setLeftNum(BigDecimal.ZERO); + } PgsProgressPlan progressPlan = progressPlanService.lambdaQuery() .eq(PgsProgressPlan::getProgressCategoryId, id) .orderByDesc(PgsProgressPlan::getEndDate) @@ -1417,7 +1423,7 @@ public class PgsProgressCategoryServiceImpl extends ServiceImpl allChildren = allCategory.stream() .filter(item -> { String ancestors = item.getAncestors(); - return ancestors != null && ancestors.contains("," + topId + ","); + return ancestors != null && (ancestors.contains("," + topId + ",") || ancestors.contains("," + topId)); }) .toList(); @@ -1512,6 +1518,84 @@ public class PgsProgressCategoryServiceImpl extends ServiceImpl categoryList, + List planList) { + PgsProgressCategoryProgressDto dto = new PgsProgressCategoryProgressDto(); + // 如果没有数据,则返回0 + if (CollUtil.isEmpty(categoryList)) { + return dto; + } + // 总完成数量 + BigDecimal totalCompleted = BigDecimal.ZERO; + // 总计划数量 + BigDecimal totalPlan = BigDecimal.ZERO; + // 总延期数量 + BigDecimal totalDelay = BigDecimal.ZERO; + // 总数量 + BigDecimal totalWork = BigDecimal.ZERO; + // 遍历所有项目进度,计算总完成数和总数 + for (PgsProgressCategory category : categoryList) { + BigDecimal completed = category.getCompleted(); + BigDecimal total = category.getTotal(); + if (PgsProgressUnitTypeEnum.PERCENTAGE.getValue().equals(category.getUnitType())) { + completed = completed.divide(BigDecimal.valueOf(100L), 4, RoundingMode.HALF_UP).multiply(total); + } + totalCompleted = totalCompleted.add(completed); + totalWork = totalWork.add(total); + // 如果当前类型已完成,则直接返回 + if (completed.compareTo(total) == 0) { + continue; + } + // 获取当前类别的计划数量 + BigDecimal plan = planList.stream() + .filter(item -> item.getProgressCategoryId().equals(category.getId())) + // 1. 获取延期数量为0的计划 + .filter(item -> item.getDelayNumber().compareTo(BigDecimal.ZERO) == 0) + // 2. 过滤掉已完成数量 >= 计划数量的计划 + .filter(item -> item.getFinishedNumber().compareTo(item.getPlanNumber()) < 0) + // 3. 对每个计划取「计划数量 - 完成数量」 + .map(item -> { + BigDecimal planNumber = item.getPlanNumber(); + if (PgsProgressUnitTypeEnum.PERCENTAGE.getValue().equals(category.getUnitType())) { + planNumber = planNumber.divide(BigDecimal.valueOf(100L), 4, RoundingMode.HALF_UP).multiply(total); + } + return planNumber.subtract(item.getFinishedNumber()); + }) + // 4. 累加 + .reduce(BigDecimal.ZERO, BigDecimal::add); + totalPlan = totalPlan.add(plan); + // 获取当前类别的延期数量 + BigDecimal delay = planList.stream() + .filter(item -> item.getProgressCategoryId().equals(category.getId())) + .filter(item -> item.getDelayNumber().compareTo(BigDecimal.ZERO) > 0) + .map(item -> { + BigDecimal delayNum = item.getDelayNumber(); + if (PgsProgressUnitTypeEnum.PERCENTAGE.getValue().equals(category.getUnitType())) { + delayNum = delayNum.divide(BigDecimal.valueOf(100L), 4, RoundingMode.HALF_UP).multiply(total); + } + return delayNum; + }) + .reduce(BigDecimal.ZERO, BigDecimal::add); + totalDelay = totalDelay.add(delay); + } + dto.setFinishProgress(BigDecimalUtil.toPercentage(totalCompleted, totalWork)); + dto.setPlanProgress(BigDecimalUtil.toPercentage(totalPlan, totalWork)); + // 计算延期量 -> 历史延迟量 - 未完成计划数量 + totalDelay = totalDelay.subtract(totalPlan).max(BigDecimal.ZERO); + dto.setDelayProgress(BigDecimalUtil.toPercentage(totalDelay, totalWork)); + // 计算未计划量 + BigDecimal totalUnPlan = totalWork.subtract(totalCompleted).subtract(totalPlan).subtract(totalDelay); + dto.setUnPlanProgress(BigDecimalUtil.toPercentage(totalUnPlan, totalWork)); + return dto; + } + /** * 获取项目进度类别未完成数量 * @@ -1886,43 +1970,318 @@ public class PgsProgressCategoryServiceImpl extends ServiceImpl progressCategoryList = this.lambdaQuery() .in(PgsProgressCategory::getProjectId, projectIds) - .eq(PgsProgressCategory::getMatrixId, 0) .list(); if (CollUtil.isEmpty(progressCategoryList)) { return ganttList; } - // 封装进度类别数据 - List list = progressCategoryList.stream().map(p -> { - // 获取所有子节点 - List children = this.getLeafNodesByTopId(p.getId(), progressCategoryList); - PgsProgressCategoryGanttVo vo = new PgsProgressCategoryGanttVo(); - vo.setId(p.getId()); - vo.setParentId(p.getParentId()); - vo.setText(p.getName()); - vo.setStartDate(null); - vo.setEndDate(null); - vo.setProgress(this.getCompletedPercentage(children) - .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)); - return vo; - }).toList(); - ganttList.addAll(list); // 获取当前项目所有计划 List planList = progressPlanService.lambdaQuery() - .eq(PgsProgressPlan::getProjectId, projectId) + .in(PgsProgressPlan::getProjectId, projectIds) .list(); - List list1 = planList.stream().map(p -> { - PgsProgressCategoryGanttVo vo = new PgsProgressCategoryGanttVo(); - vo.setId(p.getId()); - vo.setParentId(p.getProgressCategoryId()); - vo.setText(p.getProgressCategoryName() + "-" + "计划"); - vo.setStartDate(p.getStartDate()); - vo.setEndDate(p.getEndDate()); - vo.setProgress(BigDecimalUtil.toPercentage(p.getFinishedNumber(), p.getPlanNumber()) - .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)); - return vo; - }).toList(); - ganttList.addAll(list1); + // 获取关联项目的类别和计划 + List projectCategoryList = progressCategoryList.stream() + .filter(p -> p.getRelevancyStructure().equals(PgsRelevancyStructureEnum.PROJECT.getValue())) + .toList(); + + if (CollUtil.isNotEmpty(projectCategoryList)) { + // 封装进度类别数据 + List list = projectCategoryList.stream().map(p -> { + // 获取所有子节点 + List children = this.getLeafNodesByTopId(p.getId(), progressCategoryList); + PgsProgressCategoryGanttVo vo = new PgsProgressCategoryGanttVo(); + vo.setId(p.getId()); + vo.setParentId(p.getParentId()); + vo.setText(p.getName()); + PgsProgressCategoryGanttVo ganttVo = this.getCategoryStartDateAndEndDate(p.getId(), progressCategoryList, planList); + vo.setStartDate(ganttVo.getStartDate()); + vo.setEndDate(ganttVo.getEndDate()); + if (CollUtil.isEmpty(children)) { + children = List.of(p); + } + // 获取当前子节点的所有计划 + List finalChildren = children; + List childrenPlanList = planList.stream() + .filter(plan -> finalChildren.stream().anyMatch(category -> category.getId().equals(plan.getProgressCategoryId()))) + .toList(); + PgsProgressCategoryProgressDto percentage = this.getProgressPercentage(finalChildren, childrenPlanList); + vo.setFinishProgress(percentage.getFinishProgress()); + vo.setPlanProgress(percentage.getPlanProgress()); + vo.setUnPlanProgress(percentage.getUnPlanProgress()); + vo.setDelayProgress(percentage.getDelayProgress()); + vo.setRelevancyStructure(PgsRelevancyStructureEnum.PROJECT.getValue()); + vo.setStatus(CollUtil.isNotEmpty(children) ? this.getParentStatus(children) : p.getStatus()); + vo.setSort(p.getSort()); + return vo; + }).toList(); + ganttList.addAll(list); + } + // 获取关联子项目的类别和计划 + List subProjectCategoryList = progressCategoryList.stream() + .filter(p -> p.getRelevancyStructure().equals(PgsRelevancyStructureEnum.SUB_PROJECT.getValue())) + .toList(); + if (CollUtil.isNotEmpty(subProjectCategoryList)) { + // 获取顶层的类别 + List topCategoryList = subProjectCategoryList.stream() + .filter(p -> p.getParentId().equals(0L)) + .toList(); + Map> topMap = topCategoryList.stream() + .collect(Collectors.groupingBy(PgsProgressCategory::getName)); + // 封装进度类别数据 + List list = new ArrayList<>(); + for (Map.Entry> entry : topMap.entrySet()) { + List value = entry.getValue(); + // 递归封装本身及子节点数据 + this.getCategoryGanttAndChildrenVo(0L, value, list, progressCategoryList, planList); + } + ganttList.addAll(list); + } + // 获取关联方阵项目的类别和计划 + List matrixCategoryList = progressCategoryList.stream() + .filter(p -> p.getRelevancyStructure().equals(PgsRelevancyStructureEnum.MATRIX.getValue())) + .toList(); + if (CollUtil.isNotEmpty(matrixCategoryList)) { + // 获取顶层的类别 + List topCategoryList = matrixCategoryList.stream() + .filter(p -> p.getParentId().equals(0L)) + .toList(); + Map> topMap = topCategoryList.stream() + .collect(Collectors.groupingBy(PgsProgressCategory::getName)); + // 封装进度类别数据 + List list = new ArrayList<>(); + for (Map.Entry> entry : topMap.entrySet()) { + List value = entry.getValue(); + // 递归封装本身及子节点数据 + this.getCategoryGanttAndChildrenVo(0L, value, list, progressCategoryList, planList); + } + ganttList.addAll(list); + } + // 按 sort 字段升序排序 + ganttList.sort(Comparator.comparing(PgsProgressCategoryGanttVo::getSort)); return ganttList; } + /** + * 根据进度类别,获取进度类别甘特图结构 + * + * @param progressCategoryId 进度类别id + * @return 进度类别甘特图结构 + */ + @Override + public List getGanttStructureList(Long progressCategoryId) { + // 获取类别信息 + PgsProgressCategory category = this.getById(progressCategoryId); + if (category == null) { + throw new ServiceException("进度类别不存在", HttpStatus.NOT_FOUND); + } + // 获取项目信息 + BusProject project = projectService.getById(category.getProjectId()); + // 整合所有项目id + List projects = projectService.lambdaQuery() + .eq(BusProject::getPId, project.getPId()) + .list(); + List projectIds = projects.stream().map(BusProject::getId).toList(); + String name = category.getName(); + String relevancyStructure = category.getRelevancyStructure(); + // 获取对应的进度类别列表 + List progressCategoryList = this.lambdaQuery() + .in(PgsProgressCategory::getProjectId, projectIds) + .eq(PgsProgressCategory::getName, name) + .eq(PgsProgressCategory::getRelevancyStructure, relevancyStructure) + .list(); + if (PgsRelevancyStructureEnum.SUB_PROJECT.getValue().equals(relevancyStructure)) { + // 构建关联子项目的结构 + return progressCategoryList.stream().map(p -> { + PgsProgressCategoryGanttSubProjectVo vo = new PgsProgressCategoryGanttSubProjectVo(); + vo.setRelevancyStructure(PgsRelevancyStructureEnum.SUB_PROJECT.getValue()); + vo.setSubProjectId(p.getProjectId()); + vo.setProgressCategoryId(p.getId()); + vo.setProgressCategoryName(p.getName()); + return vo; + }).toList(); + } else if (PgsRelevancyStructureEnum.MATRIX.getValue().equals(relevancyStructure)) { + // 构建关联方阵的结构 + Map> map = progressCategoryList.stream() + .collect(Collectors.groupingBy(PgsProgressCategory::getProjectId)); + return map.entrySet().stream().map(entry -> { + Long key = entry.getKey(); + List value = entry.getValue(); + PgsProgressCategoryGanttSubProjectVo vo = new PgsProgressCategoryGanttSubProjectVo(); + vo.setRelevancyStructure(PgsRelevancyStructureEnum.MATRIX.getValue()); + vo.setSubProjectId(key); + vo.setProgressCategoryName(name); + // 封装关联方阵结构 + List children = value.stream().map(v -> { + PgsProgressCategoryGanttMatrixVo matrixVo = new PgsProgressCategoryGanttMatrixVo(); + matrixVo.setMatrixId(v.getMatrixId()); + matrixVo.setMatrixName(v.getMatrixName()); + matrixVo.setProgressCategoryId(v.getId()); + matrixVo.setProgressCategoryName(v.getName()); + return matrixVo; + }).toList(); + vo.setChildren(children); + return vo; + }).toList(); + } else { + return List.of(); + } + } + + /** + * 计算进度类别(含子类别)的开始和结束时间。 + *

+ * 逻辑说明: + * 1. 若该进度类别为叶子节点,则直接根据其关联的计划表(Plan)的开始/结束时间计算。 + * 2. 若该进度类别有子节点,则递归计算所有子节点的开始/结束时间, + * 以最早的开始时间和最晚的结束时间作为该类别的时间区间。 + *

+ *

+ * + * @param progressCategoryId 当前进度类别ID + * @param allChildren 所有进度类别(树形结构的扁平化列表) + * @param allPlanList 所有进度计划列表 + * @return 当前进度类别的开始和结束时间(可能为空) + */ + private PgsProgressCategoryGanttVo getCategoryStartDateAndEndDate(Long progressCategoryId, + List allChildren, + List allPlanList) { + PgsProgressCategoryGanttVo ganttVo = new PgsProgressCategoryGanttVo(); + + // 获取该类别下的所有叶子节点(无子节点的类别) + List children = this.getLeafNodesByTopId(progressCategoryId, allChildren); + + if (CollUtil.isEmpty(children)) { + // --- 叶子节点:直接根据计划计算 --- + List planList = allPlanList.stream() + .filter(p -> p.getProgressCategoryId().equals(progressCategoryId)) + .toList(); + + if (CollUtil.isNotEmpty(planList)) { + LocalDate startDate = planList.stream() + .map(PgsProgressPlan::getStartDate) + .filter(Objects::nonNull) + .min(LocalDate::compareTo) + .orElse(null); + + LocalDate endDate = planList.stream() + .map(PgsProgressPlan::getEndDate) + .filter(Objects::nonNull) + .max(LocalDate::compareTo) + .orElse(null); + + ganttVo.setStartDate(startDate); + ganttVo.setEndDate(endDate); + } + + } else { + // --- 非叶子节点:递归计算子节点 --- + List childVoList = children.stream() + .map(child -> getCategoryStartDateAndEndDate(child.getId(), allChildren, allPlanList)) + .toList(); + + LocalDate minStart = childVoList.stream() + .map(PgsProgressCategoryGanttVo::getStartDate) + .filter(Objects::nonNull) + .min(LocalDate::compareTo) + .orElse(null); + + LocalDate maxEnd = childVoList.stream() + .map(PgsProgressCategoryGanttVo::getEndDate) + .filter(Objects::nonNull) + .max(LocalDate::compareTo) + .orElse(null); + + ganttVo.setStartDate(minStart); + ganttVo.setEndDate(maxEnd); + } + return ganttVo; + } + + /** + * 根据名称,递归获取进度类别(含子类别)的甘特图数据。 + *

+ * 逻辑说明: + * 1. 获取该进度类别下的所有叶子节点(无子节点的类别)。 + * 2. 递归获取所有叶子节点的甘特图数据。 + * 3. 获取所有叶子节点的甘特图数据,并添加到结果列表中。 + *

+ * + * @param parentId 父级ID + * @param value 当前进度类别列表 + * @param list 结果列表 + * @param progressCategoryList 所有进度类别列表 + * @param planList 所有进度计划列表 + */ + private void getCategoryGanttAndChildrenVo(Long parentId, + List value, + List list, + List progressCategoryList, + List planList) { + if (CollUtil.isEmpty(value)) { + return; + } + // 封装数据 + PgsProgressCategoryGanttVo vo = new PgsProgressCategoryGanttVo(); + PgsProgressCategory first = value.getFirst(); + vo.setId(first.getId()); + vo.setParentId(parentId); + vo.setText(first.getName()); + // 获取子节点 + List children = value.stream() + .map(category -> getLeafNodesByTopId(category.getId(), progressCategoryList)) + .filter(CollUtil::isNotEmpty) + .flatMap(Collection::stream) + .distinct() + .toList(); + if (CollUtil.isEmpty(children)) { + children = value; + } + // 获取子节点的进度计划 + List finalChildren = children; + List childrenPlanList = planList.stream() + .filter(plan -> finalChildren.stream().anyMatch(category -> category.getId().equals(plan.getProgressCategoryId()))) + .toList(); + // 获取进度情况 + PgsProgressCategoryProgressDto percentage = this.getProgressPercentage(finalChildren, childrenPlanList); + vo.setFinishProgress(percentage.getFinishProgress()); + vo.setPlanProgress(percentage.getPlanProgress()); + vo.setUnPlanProgress(percentage.getUnPlanProgress()); + vo.setDelayProgress(percentage.getDelayProgress()); + // 计算所有类别的开始时间和结束时间 + List ganttVoList = value.stream() + .map(category -> getCategoryStartDateAndEndDate(category.getId(), progressCategoryList, planList)) + .toList(); + // 获取最小开始时间 + LocalDate minStart = ganttVoList.stream() + .map(PgsProgressCategoryGanttVo::getStartDate) + .filter(Objects::nonNull) + .min(LocalDate::compareTo) + .orElse(null); + // 获取最大结束时间 + LocalDate maxEnd = ganttVoList.stream() + .map(PgsProgressCategoryGanttVo::getEndDate) + .filter(Objects::nonNull) + .max(LocalDate::compareTo) + .orElse(null); + vo.setStartDate(minStart); + vo.setEndDate(maxEnd); + vo.setRelevancyStructure(first.getRelevancyStructure()); + vo.setStatus(CollUtil.isNotEmpty(children) ? this.getParentStatus(children) : first.getStatus()); + vo.setSort(first.getSort()); + list.add(vo); + // 递归,获取子节点的数据 + if (CollUtil.isNotEmpty(children)) { + int level = first.getAncestors().split(",").length + 1; + // 仅获取下一层级的子节点数据 + List nextLevelChildren = children.stream() + .filter(child -> child.getAncestors().split(",").length == level) + .distinct() + .toList(); + Map> nextMap = nextLevelChildren.stream() + .collect(Collectors.groupingBy(PgsProgressCategory::getName)); + for (Map.Entry> entry : nextMap.entrySet()) { + getCategoryGanttAndChildrenVo(first.getId(), entry.getValue(), list, progressCategoryList, planList); + } + } + } + } diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/service/impl/PgsProgressPlanServiceImpl.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/service/impl/PgsProgressPlanServiceImpl.java index 075885c8..e1cce7b7 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/service/impl/PgsProgressPlanServiceImpl.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/progress/service/impl/PgsProgressPlanServiceImpl.java @@ -127,16 +127,6 @@ public class PgsProgressPlanServiceImpl extends ServiceImpl @Override @Transactional(rollbackFor = Exception.class) public Boolean managerReview(BusLeaveManagerReviewReq req) { - Long id = req.getId(); - String managerOpinion = req.getManagerOpinion(); - // 判断该请假记录是否存在 - BusLeave oldLeave = this.getById(id); - if (oldLeave == null) { - throw new ServiceException("施工人员请假申请不存在", HttpStatus.NOT_FOUND); + + BusLeave busLeave = baseMapper.selectById(req.getId()); + if (busLeave == null) { + throw new ServiceException("未找到该申请"); } - // 如果已经审核过,则返回 - if (!BusOpinionStatusEnum.UNREAD.getValue().equals(oldLeave.getManagerOpinion())) { - throw new ServiceException("该请假已审核,请勿重复操作", HttpStatus.BAD_REQUEST); + String gangerOpinion = req.getGangerOpinion(); + busLeave.setGangerOpinion(gangerOpinion); + busLeave.setGangerExplain(req.getGangerExplain()); + if(busLeave.getGangerId() == null){ + LoginUser loginUser = LoginHelper.getLoginUser(); + busLeave.setGangerName(loginUser.getNickname()); + busLeave.setGangerId(loginUser.getUserId()); } - // 判断班组长是否审核通过 - String gangerOpinion = oldLeave.getGangerOpinion(); - if (!BusOpinionStatusEnum.PASS.getValue().equals(gangerOpinion)) { - throw new ServiceException("请等待班组长审核通过后再进行操作", HttpStatus.BAD_REQUEST); + + busLeave.setGangerTime(LocalDateTime.now()); + + int i = baseMapper.updateById(busLeave); + + if(gangerOpinion.equals("2")){ + if(busLeave.getTimeType().equals("1")){ + LocalDateTime startTime = busLeave.getStartTime(); + LocalDateTime endTime = busLeave.getEndTime(); + LocalDate startDate = startTime.toLocalDate(); + LocalDate endDate = endTime.toLocalDate(); + + List list = attendanceService.list(Wrappers.lambdaQuery(BusAttendance.class) + .eq(BusAttendance::getProjectId, busLeave.getProjectId()) + .le(BusAttendance::getClockDate, endDate) + .ge(BusAttendance::getClockDate, startDate) + .eq(BusAttendance::getUserId, busLeave.getUserId()) + ); + for (BusAttendance attendance : list) { + attendance.setClockStatus(BusAttendanceClockStatusEnum.LATE.getValue()); + } + if(!list.isEmpty()){ + attendanceService.updateBatchById(list); + } + }else if(busLeave.getTimeType().equals("2")){ + if(busLeave.getPeriodType().equals("1")){ + LocalDateTime startTime = busLeave.getStartTime(); + LocalDate localDate = startTime.toLocalDate(); + List list = attendanceService.list(Wrappers.lambdaQuery(BusAttendance.class) + .eq(BusAttendance::getProjectId, busLeave.getProjectId()) + .eq(BusAttendance::getClockDate, localDate) + .eq(BusAttendance::getClockType, "1") + .eq(BusAttendance::getUserId, busLeave.getUserId()) + ); + for (BusAttendance attendance : list) { + attendance.setClockStatus(BusAttendanceClockStatusEnum.LATE.getValue()); + } + if(!list.isEmpty()){ + attendanceService.updateBatchById(list); + } + + }else if(busLeave.getPeriodType().equals("2")){ + LocalDateTime endTime = busLeave.getEndTime(); + LocalDate localDate = endTime.toLocalDate(); + List list = attendanceService.list(Wrappers.lambdaQuery(BusAttendance.class) + .eq(BusAttendance::getProjectId, busLeave.getProjectId()) + .eq(BusAttendance::getClockDate, localDate) + .eq(BusAttendance::getClockType, "2") + .eq(BusAttendance::getUserId, busLeave.getUserId()) + ); + for (BusAttendance attendance : list) { + attendance.setClockStatus(BusAttendanceClockStatusEnum.LATE.getValue()); + } + if(!list.isEmpty()){ + attendanceService.updateBatchById(list); + } + } + } } - // todo 判断当前用户是否为项目管理员 - // 判断是否为分包公司管理员 - Long contractorId = oldLeave.getContractorId(); - SubConstructionUser constructionUser = constructionUserService.lambdaQuery() - .eq(SubConstructionUser::getSysUserId, LoginHelper.getUserId()) - .eq(SubConstructionUser::getContractorId, contractorId) - .eq(SubConstructionUser::getUserRole, SubConstructionUserRoleEnum.ADMIN.getValue()) - .one(); - if (constructionUser == null) { - throw new ServiceException("您无权审核该请假申请", HttpStatus.FORBIDDEN); - } - // 填充默认值,更新数据 - BusLeave leave = new BusLeave(); - leave.setId(id); - leave.setManagerId(constructionUser.getId()); - leave.setManagerOpinion(managerOpinion); - leave.setManagerExplain(req.getManagerExplain()); - leave.setManagerTime(LocalDateTime.now()); - leave.setRemark(req.getRemark()); - boolean result = this.updateById(leave); - if (!result) { - throw new ServiceException("更新管理员审核操作失败", HttpStatus.ERROR); - } - // 更新考勤表记录 - LocalDateTime startTime = oldLeave.getStartTime(); - LocalDateTime endTime = oldLeave.getEndTime(); - // 计算相差的时间 -// long diffInMillis = endTime.getTime() - startTime.getTime(); -// long day = TimeUnit.MILLISECONDS.toDays(diffInMillis) + 1; -// Long userId = oldLeave.getUserId(); -// String userName = oldLeave.getUserName(); -// Long projectId = oldLeave.getProjectId(); - // 遍历每一天 - List attendanceList = new ArrayList<>(); -// for (long i = 0; i < day; i++) { -// BusAttendance attendance = new BusAttendance(); -// attendance.setUserId(userId); -// attendance.setUserName(userName); -// attendance.setProjectId(projectId); -// Date date = DateUtils.addDays(startTime, (int) i); -// attendance.setLeaveId(id); -// attendance.setClockDate(date); -// attendance.setClockStatus(BusAttendanceClockStatusEnum.LEAVE.getValue()); -// attendance.setCommuter(BusAttendanceCommuterEnum.ALLDAY.getValue()); -// attendanceList.add(attendance); -// } - boolean saveBatch = attendanceService.saveBatch(attendanceList); - if (!saveBatch) { - throw new ServiceException("更新考勤记录失败", HttpStatus.ERROR); - } - return true; + return i>0; } /** @@ -375,7 +383,7 @@ public class BusLeaveServiceImpl extends ServiceImpl // 精确查询 lqw.eq(ObjectUtils.isNotEmpty(id), BusLeave::getId, id); lqw.eq(ObjectUtils.isNotEmpty(userId), BusLeave::getUserId, userId); - lqw.eq(ObjectUtils.isNotEmpty(gangerId), BusLeave::getGangerId, gangerId); + lqw.eq(ObjectUtils.isNotEmpty(gangerId) && !Objects.equals(gangerId, SUPER_ADMIN_ID), BusLeave::getGangerId, gangerId); lqw.eq(StringUtils.isNotBlank(gangerOpinion), BusLeave::getGangerOpinion, gangerOpinion); lqw.eq(StringUtils.isNotBlank(managerOpinion), BusLeave::getManagerOpinion, managerOpinion); lqw.eq(ObjectUtils.isNotEmpty(projectId), BusLeave::getProjectId, projectId); @@ -418,6 +426,28 @@ public class BusLeaveServiceImpl extends ServiceImpl // 添加审核状态 String status = BusReviewStatusEnum.getEnumByOpinionStatus(leave.getGangerOpinion(), leave.getManagerOpinion()); leaveVo.setAuditStatus(status); + + //添加审核人 + if(StrUtil.isBlank(leaveVo.getGangerName())){ + String userType = leaveVo.getUserType(); + List sysUsers = new ArrayList<>(); + if(leaveVo.getGangerId()==null){ + if("1".equals(userType)){ + sysUsers = userService.selectUserByRoleIdAndProjectId(6L, leaveVo.getProjectId()); + } else if ("2".equals(userType)) { + sysUsers = userService.selectUserByRoleIdAndProjectId(7L, leaveVo.getProjectId()); + } + }else { + SysUserVo sysUserVo = userService.selectUserById(leaveVo.getGangerId()); + if(sysUserVo != null){ + leaveVo.setGangerName(sysUserVo.getNickName()); + } + } + if(CollectionUtil.isNotEmpty(sysUsers)){ + String collect = sysUsers.stream().map(SysUser::getNickName).collect(Collectors.joining()); + leaveVo.setGangerName(collect); + } + } return leaveVo; }).toList(); leaveVoPage.setRecords(leaveVoList); @@ -462,6 +492,12 @@ public class BusLeaveServiceImpl extends ServiceImpl // 5. 保存新记录(需补充完整字段赋值) leave.setUserTime(LocalDateTime.now()); + //插入班组 + SubConstructionUser bySysUserId = constructionUserService.getBySysUserId(userId); + if (bySysUserId != null) { + leave.setTeamId(bySysUserId.getTeamId()); + leave.setContractorId(bySysUserId.getContractorId()); + } this.save(leave); return leave.getId(); } diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/project/service/impl/BusProjectServiceImpl.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/project/service/impl/BusProjectServiceImpl.java index fd5d13e4..14c2f80f 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/project/service/impl/BusProjectServiceImpl.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/project/service/impl/BusProjectServiceImpl.java @@ -5,7 +5,6 @@ import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.PhoneUtil; import cn.hutool.core.util.RandomUtil; -import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; @@ -41,8 +40,6 @@ import org.dromara.facility.service.IFacMatrixService; import org.dromara.manager.weathermanager.WeatherConstant; import org.dromara.manager.weathermanager.WeatherManager; import org.dromara.manager.weathermanager.vo.WeatherVo; -import org.dromara.other.domain.OthYs7Device; -import org.dromara.other.service.IOthYs7DeviceService; import org.dromara.progress.domain.PgsProgressCategory; import org.dromara.progress.domain.enums.PgsRelevancyStructureEnum; import org.dromara.progress.domain.vo.progresscategory.PgsProgressCategoryStructureVo; @@ -60,6 +57,7 @@ import org.dromara.safety.service.IHseKnowledgeDocumentService; import org.dromara.system.domain.vo.SysDictDataVo; import org.dromara.system.service.ISysDictDataService; import org.dromara.workflow.service.IFlwDefinitionService; +import org.dromara.xzd.utilS.IdWorker; import org.springframework.beans.BeanUtils; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.annotation.Lazy; @@ -1133,10 +1131,47 @@ public class BusProjectServiceImpl extends ServiceImpl projectVos = baseMapper.selectVoList(new LambdaQueryWrapper().eq(BusProject::getPId,0).eq(BusProject::getIsDelete,0)); + List projectVos = baseMapper.selectVoList(new LambdaQueryWrapper().eq(BusProject::getPId, 0).eq(BusProject::getIsDelete, 0)); if (projectVos == null || projectVos.isEmpty()) { return Collections.emptyMap(); } + projectVos.add(create("北京市", "北京", 39.9042, 116.4074, 1)); + projectVos.add(create("天津市", "天津", 39.3434, 117.36162, 2)); + projectVos.add(create("河北省", "石家庄", 38.0428, 114.5149, 3)); + projectVos.add(create("山西省", "太原", 37.8706, 112.5493, 1)); + projectVos.add(create("内蒙古自治区", "呼和浩特", 40.8426, 111.7492, 2)); + projectVos.add(create("辽宁省", "沈阳", 41.8057, 123.4315, 2)); + projectVos.add(create("吉林省", "长春", 43.8171, 125.3235, 3)); + projectVos.add(create("黑龙江省", "哈尔滨", 45.8038, 126.5349, 1)); + projectVos.add(create("上海市", "上海", 31.2304, 121.4737, 3)); + projectVos.add(create("江苏省", "南京", 32.0603, 118.7969, 1)); + projectVos.add(create("浙江省", "杭州", 30.2741, 120.1551, 1)); + projectVos.add(create("安徽省", "合肥", 31.8206, 117.2272, 2)); + projectVos.add(create("福建省", "福州", 26.0745, 119.2965, 2)); + projectVos.add(create("江西省", "南昌", 28.6820, 115.8579, 2)); + projectVos.add(create("山东省", "济南", 36.6512, 117.1201, 1)); + projectVos.add(create("河南省", "郑州", 34.7473, 113.6249, 1)); + projectVos.add(create("湖北省", "武汉", 30.5928, 114.3055, 1)); + projectVos.add(create("湖南省", "长沙", 28.2278, 112.9389, 2)); + projectVos.add(create("广东省", "广州", 23.1291, 113.2644, 3)); + projectVos.add(create("广西壮族自治区", "南宁", 22.8170, 108.3669, 1)); + projectVos.add(create("海南省", "海口", 20.0440, 110.1999, 1)); + projectVos.add(create("四川省", "成都", 30.6595, 104.0657, 2)); + projectVos.add(create("贵州省", "贵阳", 26.6470, 106.6302, 1)); + projectVos.add(create("云南省", "昆明", 25.0406, 102.7123, 3)); + projectVos.add(create("西藏自治区", "拉萨", 29.6469, 91.1172, 1)); + projectVos.add(create("陕西省", "西安", 34.3416, 108.9398, 3)); + projectVos.add(create("甘肃省", "兰州", 36.0614, 103.8343, 1)); + projectVos.add(create("青海省", "西宁", 36.6232, 101.7782, 2)); + projectVos.add(create("宁夏回族自治区", "银川", 38.4872, 106.2309, 2)); + projectVos.add(create("新疆维吾尔自治区", "乌鲁木齐", 43.8266, 87.6168, 3)); + projectVos.add(create("香港特别行政区", "香港", 22.3193, 114.1694, 1)); + projectVos.add(create("澳门特别行政区", "澳门", 22.1987, 113.5439, 1)); + projectVos.add(create("台湾省", "台北", 25.0330, 121.5654, 1)); + // 老挝 - 万象 + projectVos.add(create("老挝", "万象", 17.9757, 102.6331, 1)); + // 老挝 - 琅勃拉邦 + projectVos.add(create("老挝", "琅勃拉邦", 19.8856, 102.1350, 2)); Map>> map = new HashMap<>(); for (SysDictDataVo projectType : projectTypes) { Map> map1 = new HashMap<>(); @@ -1147,15 +1182,15 @@ public class BusProjectServiceImpl extends ServiceImpl map2 = new HashMap<>(); - map2.put("lng",project.getLng()); - map2.put("lat",project.getLat()); - map2.put("projectId",project.getId().toString()); - map1.put(project.getProjectName(),map2); + map2.put("lng", project.getLng()); + map2.put("lat", project.getLat()); + map2.put("projectId", project.getId().toString()); + map1.put(project.getProjectName(), map2); //当满足条件时删除该元素 iterator.remove(); } } - map.put(projectType.getDictLabel(),map1); + map.put(projectType.getDictLabel(), map1); } return map; } @@ -1165,7 +1200,7 @@ public class BusProjectServiceImpl extends ServiceImpl projectVos = baseMapper.getProjectDiYv(); Map map = new HashMap<>(); for (BusProjectVo vo : projectVos) { - map.put(StringUtils.isNotBlank(vo.getProvince())?vo.getProvince():"未知地区",vo.getCount()); + map.put(StringUtils.isNotBlank(vo.getProvince()) ? vo.getProvince() : "未知地区", vo.getCount()); } return map; } @@ -1173,16 +1208,16 @@ public class BusProjectServiceImpl extends ServiceImpl> getProjectCapacity() { //获取项目列表 - List projectVos = baseMapper.selectVoList(new LambdaQueryWrapper().eq(BusProject::getPId,0).eq(BusProject::getIsDelete,0)); + List projectVos = baseMapper.selectVoList(new LambdaQueryWrapper().eq(BusProject::getPId, 0).eq(BusProject::getIsDelete, 0)); if (projectVos == null || projectVos.isEmpty()) { return Collections.emptyMap(); } Map> map = new HashMap<>(); for (BusProjectVo project : projectVos) { Map map2 = new HashMap<>(); - map2.put("actual",project.getActual()); - map2.put("plan",project.getPlan()); - map.put(project.getProjectName(),map2); + map2.put("actual", project.getActual()); + map2.put("plan", project.getPlan()); + map.put(project.getProjectName(), map2); } return map; } @@ -1192,14 +1227,14 @@ public class BusProjectServiceImpl extends ServiceImpl> map = new HashMap<>(); SubConstructionUser byUserId = constructionUserService.getByUserId(req.getId()); Map renyuan = new HashMap<>(); - if (byUserId != null){ - renyuan.put("name", byUserId.getUserName() != null ? byUserId.getUserName():""); - renyuan.put("teamName", byUserId.getTeamName() != null ? byUserId.getTeamName():""); - renyuan.put("phone", byUserId.getPhone() != null ? byUserId.getPhone():""); - if (byUserId.getTypeOfWork() !=null){ + if (byUserId != null) { + renyuan.put("name", byUserId.getUserName() != null ? byUserId.getUserName() : ""); + renyuan.put("teamName", byUserId.getTeamName() != null ? byUserId.getTeamName() : ""); + renyuan.put("phone", byUserId.getPhone() != null ? byUserId.getPhone() : ""); + if (byUserId.getTypeOfWork() != null) { renyuan.put("typeOfWork", dictDataService.selectDictLabel("type_of_work", byUserId.getTypeOfWork())); } - renyuan.put("contractorNmae",byUserId.getContractorId() != null ? contractorService.getById(byUserId.getContractorId()).getName():""); + renyuan.put("contractorNmae", byUserId.getContractorId() != null ? contractorService.getById(byUserId.getContractorId()).getName() : ""); } map.put("renyuan", renyuan); return map; @@ -1250,4 +1285,25 @@ public class BusProjectServiceImpl extends ServiceImpl sysUsers = new ArrayList<>(); + if(reissueCardVo.getGangerId()==null){ + if("1".equals(userType)){ + sysUsers = userService.selectUserByRoleIdAndProjectId(6L, reissueCardVo.getProjectId()); + } else if ("2".equals(userType)) { + sysUsers = userService.selectUserByRoleIdAndProjectId(7L, reissueCardVo.getProjectId()); + } + }else { + SysUserVo sysUserVo = userService.selectUserById(reissueCardVo.getGangerId()); + if(sysUserVo != null){ + reissueCardVo.setGangerName(sysUserVo.getNickName()); + } + } + if(CollectionUtil.isNotEmpty(sysUsers)){ + String collect = sysUsers.stream().map(SysUser::getNickName).collect(Collectors.joining()); + reissueCardVo.setGangerName(collect); + } + } return reissueCardVo; }).toList(); reissueCardVoPage.setRecords(reissueCardVoList); diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/controller/HseFileFolderController.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/controller/HseFileFolderController.java new file mode 100644 index 00000000..f1ca736c --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/controller/HseFileFolderController.java @@ -0,0 +1,140 @@ +package org.dromara.safety.controller; + +import java.util.List; +import java.util.stream.Collectors; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.RequiredArgsConstructor; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.*; +import cn.dev33.satoken.annotation.SaCheckPermission; +import org.dromara.safety.domain.HseFileFolder; +import org.dromara.safety.domain.dto.fileFolder.FileFolderCreateDTO; +import org.dromara.safety.domain.dto.fileFolder.FileFolderMoveDTO; +import org.dromara.safety.domain.dto.fileFolder.ListQueryDto; +import org.dromara.safety.domain.vo.fileFolder.FileFolderTreeVO; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import org.dromara.common.idempotent.annotation.RepeatSubmit; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.web.core.BaseController; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.core.domain.R; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import org.dromara.common.log.enums.BusinessType; +import org.dromara.common.excel.utils.ExcelUtil; +import org.dromara.safety.domain.vo.HseFileFolderVo; +import org.dromara.safety.domain.bo.HseFileFolderBo; +import org.dromara.safety.service.IHseFileFolderService; +import org.dromara.common.mybatis.core.page.TableDataInfo; + +/** + * 会议纪要 + * + * @author Lion Li + * @date 2025-10-14 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/safety/fileFolder") +public class HseFileFolderController extends BaseController { + + private final IHseFileFolderService hseFileFolderService; + + /** + * 查询会议纪要列表 + */ + @GetMapping("/list-all") + public R> listAll(ListQueryDto dto) { + return R.ok(hseFileFolderService.listAll(dto)); + } + + /** + * 查询指定目录的树形结构(一次性加载所有层级) + * @return 树形结构列表 + */ + @GetMapping("/tree-all") + public R> treeAll(ListQueryDto dto) { + + // 1. 查询所有子项(利用path前缀匹配,一次性加载所有层级) + List allItems = hseFileFolderService.list(new LambdaQueryWrapper() + .like(HseFileFolder::getPath, "," + dto.getParentId() + ",") // 包含父ID的所有子项 + .eq(dto.getType()!=null,HseFileFolder::getType, dto.getType()) + .orderByDesc(HseFileFolder::getId) + .orderByAsc(HseFileFolder::getSort) + ); + + // 2. 构建树形结构 + List treeVOS = buildTree(allItems, dto.getParentId()); + return R.ok(treeVOS); + } + + /** + * 递归构建树形结构 + */ + private List buildTree(List allItems, Long parentId) { + return allItems.stream() + .filter(item -> parentId.equals(item.getParentId())) + .map(item -> { + FileFolderTreeVO vo = new FileFolderTreeVO(); + // 复制基本属性(可使用BeanUtils.copyProperties) + vo.setId(item.getId()); + vo.setName(item.getName()); + vo.setParentId(item.getParentId()); + vo.setType(item.getType()); + vo.setLevel(item.getLevel()); + vo.setSort(item.getSort()); + vo.setFileSuffix(item.getFileSuffix()); + + // 递归查询子节点 + vo.setChildren(buildTree(allItems, item.getId())); + return vo; + }) + .collect(Collectors.toList()); + } + + /** + * 创建文件或文件夹 + */ + @PostMapping("/create") + public R create(@RequestBody FileFolderCreateDTO dto) { + return R.ok(hseFileFolderService.createFileOrFolder(dto)); + } + + /** + * 删除文件或文件夹(级联删除子项) + */ + @DeleteMapping("/{id}") + @Transactional + public R delete(@PathVariable Long id) { + return R.ok(hseFileFolderService.deleteFileOrFolder(id)); + } + + /** + * 移动文件或文件夹到指定目录 + */ + @PostMapping("/move") + @Transactional + public R move(@RequestBody FileFolderMoveDTO dto) { + return R.ok(hseFileFolderService.moveFileOrFolder(dto.getId(), dto.getTargetParentId())); + } + + + + + + + + + + + + + + + + +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/HseFileFolder.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/HseFileFolder.java new file mode 100644 index 00000000..3e05ce9f --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/HseFileFolder.java @@ -0,0 +1,92 @@ +package org.dromara.safety.domain; + +import org.dromara.common.mybatis.core.domain.BaseEntity; +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serial; + +/** + * 会议纪要对象 hse_file_folder + * + * @author Lion Li + * @date 2025-10-14 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("hse_file_folder") +public class HseFileFolder extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @TableId(value = "id") + private Long id; + + /** + * 项目id + */ + private Long projectId; + + /** + * 名称(文件名或文件夹名) + */ + private String name; + + /** + * 父级ID(0表示根目录) + */ + private Long parentId; + + /** + * 类型(1-文件夹,2-文件) + */ + private Integer type; + + /** + * 层级(根目录为1,子级+1) + */ + private Integer level; + + /** + * 同层级排序号 + */ + private Integer sort; + + /** + * 层级路径(如:1,2,3 表示ID为1→2→3的层级) + */ + private String path; + + /** + * 文件id + */ + private Long fileId; + + /** + * 文件后缀 + */ + private String fileSuffix; + + /** + * 存储路径 + */ + private String filePath; + + /** + * + */ + private String remark; + + /** + * 删除标志(0代表存在 1代表删除) + */ +// @TableLogic + private String delFlag; + + +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/bo/HseFileFolderBo.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/bo/HseFileFolderBo.java new file mode 100644 index 00000000..f30eee92 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/bo/HseFileFolderBo.java @@ -0,0 +1,90 @@ +package org.dromara.safety.domain.bo; + +import org.dromara.safety.domain.HseFileFolder; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import lombok.EqualsAndHashCode; +import jakarta.validation.constraints.*; + +/** + * 会议纪要业务对象 hse_file_folder + * + * @author Lion Li + * @date 2025-10-14 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@AutoMapper(target = HseFileFolder.class, reverseConvertGenerate = false) +public class HseFileFolderBo extends BaseEntity { + + /** + * 主键ID + */ + @NotNull(message = "主键ID不能为空", groups = { EditGroup.class }) + private Long id; + + /** + * 项目id + */ + @NotNull(message = "项目id不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long projectId; + + /** + * 名称(文件名或文件夹名) + */ + @NotBlank(message = "名称(文件名或文件夹名)不能为空", groups = { AddGroup.class, EditGroup.class }) + private String name; + + /** + * 父级ID(0表示根目录) + */ + private Long parentId; + + /** + * 类型(1-文件夹,2-文件) + */ + @NotNull(message = "类型(1-文件夹,2-文件)不能为空", groups = { AddGroup.class, EditGroup.class }) + private Integer type; + + /** + * 层级(根目录为1,子级+1) + */ + @NotNull(message = "层级(根目录为1,子级+1)不能为空", groups = { AddGroup.class, EditGroup.class }) + private Integer level; + + /** + * 同层级排序号 + */ + private Integer sort; + + /** + * 层级路径(如:1,2,3 表示ID为1→2→3的层级) + */ + @NotBlank(message = "层级路径(如:1,2,3 表示ID为1→2→3的层级)不能为空", groups = { AddGroup.class, EditGroup.class }) + private String path; + + /** + * 文件id + */ + private Long fileId; + + /** + * 文件后缀 + */ + private String fileSuffix; + + /** + * 存储路径 + */ + private String filePath; + + /** + * + */ + private String remark; + + +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/dto/fileFolder/FileFolderCreateDTO.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/dto/fileFolder/FileFolderCreateDTO.java new file mode 100644 index 00000000..ed883521 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/dto/fileFolder/FileFolderCreateDTO.java @@ -0,0 +1,51 @@ +package org.dromara.safety.domain.dto.fileFolder; + + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + + + +@Data +public class FileFolderCreateDTO { + + /** + * 名称 + */ + @NotBlank(message = "名称不能为空") + private String name; + + /** + * 父级ID(0表示根目录) + */ + private Long parentId = 0L; + + /** + * 文件类型 + */ + @NotNull(message = "类型不能为空") + private Integer type; + + /** + * 排序号 + */ + private Integer sort=0; + + /** + * 文件后缀 + */ + private String fileSuffix; + + /** + * 文件存储路径(仅文件有效) + */ + private String filePath; + + /** + * 备注 + */ + private String remark; + +} + diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/dto/fileFolder/FileFolderMoveDTO.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/dto/fileFolder/FileFolderMoveDTO.java new file mode 100644 index 00000000..2d50c157 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/dto/fileFolder/FileFolderMoveDTO.java @@ -0,0 +1,14 @@ +package org.dromara.safety.domain.dto.fileFolder; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class FileFolderMoveDTO { + + @NotNull(message = "文件/文件夹ID不能为空") + private Long id; + + @NotNull(message = "目标父目录ID不能为空") + private Long targetParentId; +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/dto/fileFolder/ListQueryDto.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/dto/fileFolder/ListQueryDto.java new file mode 100644 index 00000000..b0f9a631 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/dto/fileFolder/ListQueryDto.java @@ -0,0 +1,22 @@ +package org.dromara.safety.domain.dto.fileFolder; + + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class ListQueryDto { + /** + * 父目录ID,默认0(根目录) + */ + @NotNull(message = "父目录ID不能为空") + private Long parentId; + + + /** + * 类型:1-文件夹,2-文件,null-全部 + */ + private Integer type; + + +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/vo/HseFileFolderVo.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/vo/HseFileFolderVo.java new file mode 100644 index 00000000..b95f0eb5 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/vo/HseFileFolderVo.java @@ -0,0 +1,109 @@ +package org.dromara.safety.domain.vo; + +import org.dromara.safety.domain.HseFileFolder; +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import org.dromara.common.excel.annotation.ExcelDictFormat; +import org.dromara.common.excel.convert.ExcelDictConvert; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + + + +/** + * 会议纪要视图对象 hse_file_folder + * + * @author Lion Li + * @date 2025-10-14 + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = HseFileFolder.class) +public class HseFileFolderVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @ExcelProperty(value = "主键ID") + private Long id; + + /** + * 项目id + */ + @ExcelProperty(value = "项目id") + private Long projectId; + + /** + * 名称(文件名或文件夹名) + */ + @ExcelProperty(value = "名称", converter = ExcelDictConvert.class) + @ExcelDictFormat(readConverterExp = "文=件名或文件夹名") + private String name; + + /** + * 父级ID(0表示根目录) + */ + @ExcelProperty(value = "父级ID", converter = ExcelDictConvert.class) + @ExcelDictFormat(readConverterExp = "0=表示根目录") + private Long parentId; + + /** + * 类型(1-文件夹,2-文件) + */ + @ExcelProperty(value = "类型", converter = ExcelDictConvert.class) + @ExcelDictFormat(readConverterExp = "1=-文件夹,2-文件") + private Integer type; + + /** + * 层级(根目录为1,子级+1) + */ + @ExcelProperty(value = "层级", converter = ExcelDictConvert.class) + @ExcelDictFormat(readConverterExp = "根=目录为1,子级+1") + private Integer level; + + /** + * 同层级排序号 + */ + @ExcelProperty(value = "同层级排序号") + private Integer sort; + + /** + * 层级路径(如:1,2,3 表示ID为1→2→3的层级) + */ + @ExcelProperty(value = "层级路径", converter = ExcelDictConvert.class) + @ExcelDictFormat(readConverterExp = "如=:1,2,3,表=示ID为1→2→3的层级") + private String path; + + /** + * 文件id + */ + @ExcelProperty(value = "文件id") + private Long fileId; + + /** + * 文件后缀 + */ + @ExcelProperty(value = "文件后缀") + private String fileSuffix; + + /** + * 存储路径 + */ + @ExcelProperty(value = "存储路径") + private String filePath; + + /** + * + */ + @ExcelProperty(value = "") + private String remark; + + +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/vo/fileFolder/FileFolderTreeVO.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/vo/fileFolder/FileFolderTreeVO.java new file mode 100644 index 00000000..235798c0 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/domain/vo/fileFolder/FileFolderTreeVO.java @@ -0,0 +1,53 @@ +package org.dromara.safety.domain.vo.fileFolder; + +import lombok.Data; + +import java.util.List; + +@Data +public class FileFolderTreeVO { + + + private Long id; + + /** + * 名称(文件名或文件夹名) + */ + private String name; + + + /** + * 父级ID(0表示根目录) + */ + private Long parentId; + + /** + * 1-文件夹,2-文件 + */ + private Integer type; + + /** + * 层级(根目录为1,子级+1) + */ + private Integer level; + + /** + * 同层级排序号 + */ + private Integer sort; + + /** + * 文件id + */ + private Long fileId; + + /** + * 文件后缀 + */ + private String fileSuffix; + + /** + * 子级 + */ + private List children; +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/mapper/HseFileFolderMapper.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/mapper/HseFileFolderMapper.java new file mode 100644 index 00000000..110ddee5 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/mapper/HseFileFolderMapper.java @@ -0,0 +1,25 @@ +package org.dromara.safety.mapper; + + +import org.apache.ibatis.annotations.Param; +import org.dromara.safety.domain.HseFileFolder; +import org.dromara.safety.domain.vo.HseFileFolderVo; +import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; + +/** + * 会议纪要Mapper接口 + * + * @author Lion Li + * @date 2025-10-14 + */ +public interface HseFileFolderMapper extends BaseMapperPlus { + + /** + * 批量更新子项的路径和层级 + */ + void batchUpdateChildPaths( + @Param("oldPath") String oldPath, + @Param("newPath") String newPath, + @Param("levelDiff") int levelDiff, + @Param("parentId") Long parentId); +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/service/IHseFileFolderService.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/service/IHseFileFolderService.java new file mode 100644 index 00000000..178923bf --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/service/IHseFileFolderService.java @@ -0,0 +1,90 @@ +package org.dromara.safety.service; + +import org.dromara.safety.domain.dto.fileFolder.FileFolderCreateDTO; +import org.dromara.safety.domain.dto.fileFolder.ListQueryDto; +import org.dromara.safety.domain.vo.HseFileFolderVo; +import org.dromara.safety.domain.bo.HseFileFolderBo; +import org.dromara.safety.domain.HseFileFolder; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.mybatis.core.page.PageQuery; + +import com.baomidou.mybatisplus.extension.service.IService; +import java.util.Collection; +import java.util.List; + +/** + * 会议纪要Service接口 + * + * @author Lion Li + * @date 2025-10-14 + */ +public interface IHseFileFolderService extends IService{ + + /** + * 查询会议纪要 + * + * @param id 主键 + * @return 会议纪要 + */ + HseFileFolderVo queryById(Long id); + + /** + * 分页查询会议纪要列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 会议纪要分页列表 + */ + TableDataInfo queryPageList(HseFileFolderBo bo, PageQuery pageQuery); + + /** + * 查询符合条件的会议纪要列表 + * + * @param bo 查询条件 + * @return 会议纪要列表 + */ + List queryList(HseFileFolderBo bo); + + /** + * 新增会议纪要 + * + * @param bo 会议纪要 + * @return 是否新增成功 + */ + Boolean insertByBo(HseFileFolderBo bo); + + /** + * 修改会议纪要 + * + * @param bo 会议纪要 + * @return 是否修改成功 + */ + Boolean updateByBo(HseFileFolderBo bo); + + /** + * 校验并批量删除会议纪要信息 + * + * @param ids 待删除的主键集合 + * @param isValid 是否进行有效性校验 + * @return 是否删除成功 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + + List listAll(ListQueryDto dto); + + /** + * 创建文件或文件夹 + */ + HseFileFolder createFileOrFolder(FileFolderCreateDTO dto); + + /** + * 删除文件或文件夹(级联删除子项) + */ + boolean deleteFileOrFolder(Long id); + + /** + * 移动文件或文件夹到指定目录 + */ + boolean moveFileOrFolder(Long id, Long targetParentId); +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/service/impl/HseFileFolderServiceImpl.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/service/impl/HseFileFolderServiceImpl.java new file mode 100644 index 00000000..5f3cb302 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/safety/service/impl/HseFileFolderServiceImpl.java @@ -0,0 +1,271 @@ +package org.dromara.safety.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.dromara.common.core.exception.ServiceException; +import org.dromara.common.core.utils.MapstructUtils; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.mybatis.core.page.PageQuery; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.dromara.safety.domain.dto.fileFolder.FileFolderCreateDTO; +import org.dromara.safety.domain.dto.fileFolder.ListQueryDto; +import org.springframework.stereotype.Service; +import org.dromara.safety.domain.bo.HseFileFolderBo; +import org.dromara.safety.domain.vo.HseFileFolderVo; +import org.dromara.safety.domain.HseFileFolder; +import org.dromara.safety.mapper.HseFileFolderMapper; +import org.dromara.safety.service.IHseFileFolderService; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Collection; + +/** + * 会议纪要Service业务层处理 + * + * @author Lion Li + * @date 2025-10-14 + */ +@RequiredArgsConstructor +@Service +public class HseFileFolderServiceImpl extends ServiceImpl implements IHseFileFolderService { + + private final HseFileFolderMapper baseMapper; + + /** + * 查询会议纪要 + * + * @param id 主键 + * @return 会议纪要 + */ + @Override + public HseFileFolderVo queryById(Long id){ + return baseMapper.selectVoById(id); + } + + /** + * 分页查询会议纪要列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 会议纪要分页列表 + */ + @Override + public TableDataInfo queryPageList(HseFileFolderBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询符合条件的会议纪要列表 + * + * @param bo 查询条件 + * @return 会议纪要列表 + */ + @Override + public List queryList(HseFileFolderBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(HseFileFolderBo bo) { + Map params = bo.getParams(); + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.orderByDesc(HseFileFolder::getId); + lqw.orderByAsc(HseFileFolder::getSort); + lqw.eq(bo.getProjectId() != null, HseFileFolder::getProjectId, bo.getProjectId()); + lqw.like(StringUtils.isNotBlank(bo.getName()), HseFileFolder::getName, bo.getName()); + lqw.eq(bo.getParentId() != null, HseFileFolder::getParentId, bo.getParentId()); + lqw.eq(bo.getType() != null, HseFileFolder::getType, bo.getType()); + lqw.eq(bo.getLevel() != null, HseFileFolder::getLevel, bo.getLevel()); + lqw.eq(bo.getSort() != null, HseFileFolder::getSort, bo.getSort()); + lqw.eq(StringUtils.isNotBlank(bo.getPath()), HseFileFolder::getPath, bo.getPath()); + lqw.eq(bo.getFileId() != null, HseFileFolder::getFileId, bo.getFileId()); + lqw.eq(StringUtils.isNotBlank(bo.getFileSuffix()), HseFileFolder::getFileSuffix, bo.getFileSuffix()); + lqw.eq(StringUtils.isNotBlank(bo.getFilePath()), HseFileFolder::getFilePath, bo.getFilePath()); + return lqw; + } + + /** + * 新增会议纪要 + * + * @param bo 会议纪要 + * @return 是否新增成功 + */ + @Override + public Boolean insertByBo(HseFileFolderBo bo) { + HseFileFolder add = MapstructUtils.convert(bo, HseFileFolder.class); + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setId(add.getId()); + } + return flag; + } + + /** + * 修改会议纪要 + * + * @param bo 会议纪要 + * @return 是否修改成功 + */ + @Override + public Boolean updateByBo(HseFileFolderBo bo) { + HseFileFolder update = MapstructUtils.convert(bo, HseFileFolder.class); + validEntityBeforeSave(update); + return baseMapper.updateById(update) > 0; + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(HseFileFolder entity){ + //TODO 做一些数据校验,如唯一约束 + } + + /** + * 校验并批量删除会议纪要信息 + * + * @param ids 待删除的主键集合 + * @param isValid 是否进行有效性校验 + * @return 是否删除成功 + */ + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if(isValid){ + //TODO 做一些业务上的校验,判断是否需要校验 + } + return baseMapper.deleteByIds(ids) > 0; + } + + @Override + public List listAll(ListQueryDto dto) { + + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.orderByDesc(HseFileFolder::getId); + lqw.orderByAsc(HseFileFolder::getSort); + + lqw.eq(HseFileFolder::getParentId, dto.getParentId()); + lqw.eq(dto.getType()!=null,HseFileFolder::getType, dto.getType()); + + return baseMapper.selectVoList(lqw); + } + + public HseFileFolder createFileOrFolder(FileFolderCreateDTO dto) { + // 1. 验证父目录是否存在 + if (dto.getParentId() != 0) { + HseFileFolder parent = getById(dto.getParentId()); + if (parent == null || parent.getType() != 1) { // 父级必须是文件夹 + throw new ServiceException("父目录不存在或不是文件夹"); + } + } + + // 2. 构建新文件/文件夹对象 + HseFileFolder entity = new HseFileFolder(); + entity.setName(dto.getName()); + entity.setParentId(dto.getParentId()); + entity.setType(dto.getType()); + entity.setSort(dto.getSort() != null ? dto.getSort() : 0); + entity.setRemark(dto.getRemark()); + + + // 3. 设置层级和路径 + if (dto.getParentId() == 0) { // 根目录 + entity.setLevel(1); + entity.setPath("," + entity.getId() + ","); // ID会在插入后自动生成,这里先占位 + } else { // 子目录/文件 + HseFileFolder parent = getById(dto.getParentId()); + entity.setLevel(parent.getLevel() + 1); + entity.setPath(parent.getPath() + entity.getId() + ","); // ID会在插入后更新 + } + + // 4. 处理文件特有属性 + if (dto.getType() == 2) { // 如果是文件 + entity.setFileSuffix(dto.getFileSuffix()); + entity.setFilePath(dto.getFilePath()); + } + + // 5. 保存并更新路径(因为ID是自增的,需要先保存再更新路径) + save(entity); + + // 6. 修正路径中的ID + if (dto.getParentId() == 0) { + entity.setPath("," + entity.getId() + ","); + } else { + HseFileFolder parent = getById(dto.getParentId()); + entity.setPath(parent.getPath() + entity.getId() + ","); + } + updateById(entity); + + return entity; + } + + @Override + @Transactional + public boolean deleteFileOrFolder(Long id) { + HseFileFolder entity = getById(id); + if (entity == null) { + throw new ServiceException("文件/文件夹不存在"); + } + + // 1. 删除自身及所有子项(通过path匹配) + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .like(HseFileFolder::getPath, "," + id + ","); + + return remove(queryWrapper); + } + + @Override + @Transactional + public boolean moveFileOrFolder(Long id, Long targetParentId) { + // 1. 验证源文件和目标目录是否存在 + HseFileFolder source = getById(id); + if (source == null) { + throw new ServiceException("源文件/文件夹不存在"); + } + + HseFileFolder targetParent = getById(targetParentId); + if (targetParent == null || targetParent.getType() != 1) { + throw new ServiceException("目标目录不存在或不是文件夹"); + } + + // 2. 防止循环移动(不能移动到自身或子目录下) + if (source.getPath().contains("," + targetParentId + ",")) { + throw new ServiceException("不能将文件夹移动到其子目录下"); + } + + // 3. 获取原路径和新路径的前缀 + String oldPath = source.getPath(); + String oldParentPath = source.getParentId() == 0 ? ",0," : getById(source.getParentId()).getPath(); + String newParentPath = targetParent.getPath(); + + // 4. 计算新路径和新层级 + String newPath = newParentPath + id + ","; + int newLevel = targetParent.getLevel() + 1; + + // 5. 更新自身信息 + source.setParentId(targetParentId); + source.setLevel(newLevel); + source.setPath(newPath); + updateById(source); + + // 6. 更新所有子项的路径和层级 + if (source.getType() == 1) { // 如果是文件夹,需要更新其子项 + // 计算路径替换的前后缀 + String pathReplaceFrom = oldPath; + String pathReplaceTo = newPath; + int levelDiff = newLevel - source.getLevel(); // 层级变化量 + + // 批量更新子项 + baseMapper.batchUpdateChildPaths(pathReplaceFrom, pathReplaceTo, levelDiff, id); + } + + return true; + } +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysUserController.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysUserController.java index 61ee644f..db8cd249 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysUserController.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysUserController.java @@ -383,6 +383,7 @@ public class SysUserController extends BaseController { public TableDataInfo fbList(FbUserListDto dto, PageQuery pageQuery) { return userService.selectPageFbUserList(dto, pageQuery); } + /** * 获取分包单位列表 */ diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/transferData/controller/TransferDataController.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/transferData/controller/TransferDataController.java index 81fa4dc9..9983c6be 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/transferData/controller/TransferDataController.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/transferData/controller/TransferDataController.java @@ -3,6 +3,7 @@ package org.dromara.transferData.controller; import cn.dev33.satoken.annotation.SaIgnore; import cn.hutool.core.collection.CollectionUtil; import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.github.xiaoymin.knife4j.annotations.Ignore; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.dromara.contractor.domain.SubConstructionUser; diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/transferData/service/TransferDataService.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/transferData/service/TransferDataService.java index 9a4155d7..e3c6581e 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/transferData/service/TransferDataService.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/transferData/service/TransferDataService.java @@ -1,5 +1,6 @@ package org.dromara.transferData.service; +import cn.dev33.satoken.secure.BCrypt; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.StrUtil; @@ -566,6 +567,7 @@ public class TransferDataService { sysUser.setNickName(user.getUserName()); sysUser.setPhonenumber(phone); sysUser.setSex(user.getSex()); + sysUser.setPassword(BCrypt.hashpw("123456")); sysUser.setUserType(UserType.APP_USER.getUserType()); sysUserId = userService.save(sysUser); } else { diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/resources/mapper/safety/HseFileFolderMapper.xml b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/resources/mapper/safety/HseFileFolderMapper.xml new file mode 100644 index 00000000..2745bb26 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/resources/mapper/safety/HseFileFolderMapper.xml @@ -0,0 +1,18 @@ + + + + + + + UPDATE hse_file_folder + SET + path = REPLACE(path, #{oldPath}, #{newPath}), + level = level + #{levelDiff}, + update_time = NOW() + WHERE + path LIKE CONCAT(#{oldPath}, '%') + AND parent_id != #{parentId} + +