Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
@ -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
|
||||
|
||||
@ -18,6 +18,14 @@
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- Java WebSocket 标准API -->
|
||||
<dependency>
|
||||
<groupId>javax.websocket</groupId>
|
||||
<artifactId>javax.websocket-api</artifactId>
|
||||
<version>1.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.drewnoakes</groupId>-->
|
||||
<!-- <artifactId>metadata-extractor</artifactId>-->
|
||||
|
||||
@ -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<DroProjectDroneMappe
|
||||
List<DroProjectDrone> list = this.lambdaQuery()
|
||||
.eq(DroProjectDrone::getProjectId, projectId)
|
||||
.list();
|
||||
if (CollUtil.isNotEmpty(list)) {
|
||||
droneManager.addAirportInfoByProject(list.stream().map(DroProjectDrone::getDroneSn).toList(), projectId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
* <p>
|
||||
* 考勤设备消息发送工具类(对应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);
|
||||
}
|
||||
}
|
||||
@ -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<String, KqjEntity.DeviceInfo> connectedDevices = new ConcurrentHashMap<>();
|
||||
// 2. 存储UUID对应的响应通道(key: UUID,value: 响应结果容器)
|
||||
private static final Map<String, ResponseHolder<KqjEntity.CommonResponse>> 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<KqjEntity.CommonResponse> 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<String> firstMsgFuture = new java.util.concurrent.CompletableFuture<>();
|
||||
// 使用AtomicReference包装临时处理器,解决未初始化问题
|
||||
final java.util.concurrent.atomic.AtomicReference<MessageHandler.Whole<String>> tempHandlerRef = new java.util.concurrent.atomic.AtomicReference<>();
|
||||
|
||||
// 定义临时消息处理器
|
||||
MessageHandler.Whole<String> 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<String> firstMsgFuture = new java.util.concurrent.CompletableFuture<>();
|
||||
//
|
||||
// // 临时注册消息处理器,读取第一条消息后移除
|
||||
// MessageHandler.Whole<String> 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<KqjEntity.CommonResponse> 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<T> {
|
||||
private final String sn; // 关联的设备SN
|
||||
private final java.util.concurrent.CompletableFuture<T> 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<T> getResultFuture() {
|
||||
return resultFuture;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -90,6 +90,16 @@ public class PgsProgressCategoryController extends BaseController {
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询进度类别的甘特图结构列表
|
||||
*/
|
||||
@SaCheckPermission("progress:progressCategory:query")
|
||||
@GetMapping("/list/gantt/{progressCategoryId}")
|
||||
public R<List<PgsProgressCategoryGanttSubProjectVo>> listGanttStructure(@NotNull(message = "类别主键不能为空")
|
||||
@PathVariable Long progressCategoryId) {
|
||||
return R.ok(pgsProgressCategoryService.getGanttStructureList(progressCategoryId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出进度类别列表
|
||||
*/
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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<PgsProgressCategoryGanttMatrixVo> children;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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<PgsProgressCategor
|
||||
*/
|
||||
BigDecimal getCompletedPercentage(List<PgsProgressCategory> categoryList);
|
||||
|
||||
/**
|
||||
* 获取项目进度百分比
|
||||
*
|
||||
* @param categoryList 项目进度列表
|
||||
* @return 项目进度百分比
|
||||
*/
|
||||
PgsProgressCategoryProgressDto getProgressPercentage(List<PgsProgressCategory> categoryList, List<PgsProgressPlan> planList);
|
||||
|
||||
/**
|
||||
* 获取项目进度类别未完成数量
|
||||
*
|
||||
@ -229,4 +238,12 @@ public interface IPgsProgressCategoryService extends IService<PgsProgressCategor
|
||||
* @return 进度类别甘特图结构
|
||||
*/
|
||||
List<PgsProgressCategoryGanttVo> listGanttByProject(Long projectId);
|
||||
|
||||
/**
|
||||
* 根据进度类别,获取进度类别甘特图结构
|
||||
*
|
||||
* @param progressCategoryId 进度类别id
|
||||
* @return 进度类别甘特图结构
|
||||
*/
|
||||
List<PgsProgressCategoryGanttSubProjectVo> getGanttStructureList(Long progressCategoryId);
|
||||
}
|
||||
|
||||
@ -166,13 +166,19 @@ public class PgsProgressCategoryServiceImpl extends ServiceImpl<PgsProgressCateg
|
||||
throw new ServiceException("进度类别信息不存在", HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// 获取当前进度数量和完成数量总和
|
||||
BigDecimal allNumber = progressPlanService.getCurrentPlanAndFinishedNumber(progressCategory);
|
||||
BigDecimal total = progressCategory.getTotal();
|
||||
PgsProgressCategoryLastTimeVo lastTimeVo = new PgsProgressCategoryLastTimeVo();
|
||||
BigDecimal total = PgsProgressUnitTypeEnum.PERCENTAGE.getValue().equals(progressCategory.getUnitType()) ?
|
||||
new BigDecimal(100) : progressCategory.getTotal();
|
||||
;
|
||||
if (total.compareTo(BigDecimal.ZERO) > 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<PgsProgressCateg
|
||||
List<PgsProgressCategory> 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<PgsProgressCateg
|
||||
return BigDecimalUtil.toPercentage(totalCompleted, totalWork);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目进度百分比
|
||||
*
|
||||
* @param categoryList 项目进度列表
|
||||
* @return 项目进度百分比
|
||||
*/
|
||||
@Override
|
||||
public PgsProgressCategoryProgressDto getProgressPercentage(List<PgsProgressCategory> categoryList,
|
||||
List<PgsProgressPlan> 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<PgsProgressCateg
|
||||
// 获取当前项目所有进度类别
|
||||
List<PgsProgressCategory> progressCategoryList = this.lambdaQuery()
|
||||
.in(PgsProgressCategory::getProjectId, projectIds)
|
||||
.eq(PgsProgressCategory::getMatrixId, 0)
|
||||
.list();
|
||||
if (CollUtil.isEmpty(progressCategoryList)) {
|
||||
return ganttList;
|
||||
}
|
||||
// 获取当前项目所有计划
|
||||
List<PgsProgressPlan> planList = progressPlanService.lambdaQuery()
|
||||
.in(PgsProgressPlan::getProjectId, projectIds)
|
||||
.list();
|
||||
// 获取关联项目的类别和计划
|
||||
List<PgsProgressCategory> projectCategoryList = progressCategoryList.stream()
|
||||
.filter(p -> p.getRelevancyStructure().equals(PgsRelevancyStructureEnum.PROJECT.getValue()))
|
||||
.toList();
|
||||
|
||||
if (CollUtil.isNotEmpty(projectCategoryList)) {
|
||||
// 封装进度类别数据
|
||||
List<PgsProgressCategoryGanttVo> list = progressCategoryList.stream().map(p -> {
|
||||
List<PgsProgressCategoryGanttVo> list = projectCategoryList.stream().map(p -> {
|
||||
// 获取所有子节点
|
||||
List<PgsProgressCategory> 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));
|
||||
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<PgsProgressCategory> finalChildren = children;
|
||||
List<PgsProgressPlan> 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<PgsProgressPlan> planList = progressPlanService.lambdaQuery()
|
||||
.eq(PgsProgressPlan::getProjectId, projectId)
|
||||
.list();
|
||||
List<PgsProgressCategoryGanttVo> 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<PgsProgressCategory> subProjectCategoryList = progressCategoryList.stream()
|
||||
.filter(p -> p.getRelevancyStructure().equals(PgsRelevancyStructureEnum.SUB_PROJECT.getValue()))
|
||||
.toList();
|
||||
if (CollUtil.isNotEmpty(subProjectCategoryList)) {
|
||||
// 获取顶层的类别
|
||||
List<PgsProgressCategory> topCategoryList = subProjectCategoryList.stream()
|
||||
.filter(p -> p.getParentId().equals(0L))
|
||||
.toList();
|
||||
Map<String, List<PgsProgressCategory>> topMap = topCategoryList.stream()
|
||||
.collect(Collectors.groupingBy(PgsProgressCategory::getName));
|
||||
// 封装进度类别数据
|
||||
List<PgsProgressCategoryGanttVo> list = new ArrayList<>();
|
||||
for (Map.Entry<String, List<PgsProgressCategory>> entry : topMap.entrySet()) {
|
||||
List<PgsProgressCategory> value = entry.getValue();
|
||||
// 递归封装本身及子节点数据
|
||||
this.getCategoryGanttAndChildrenVo(0L, value, list, progressCategoryList, planList);
|
||||
}
|
||||
ganttList.addAll(list);
|
||||
}
|
||||
// 获取关联方阵项目的类别和计划
|
||||
List<PgsProgressCategory> matrixCategoryList = progressCategoryList.stream()
|
||||
.filter(p -> p.getRelevancyStructure().equals(PgsRelevancyStructureEnum.MATRIX.getValue()))
|
||||
.toList();
|
||||
if (CollUtil.isNotEmpty(matrixCategoryList)) {
|
||||
// 获取顶层的类别
|
||||
List<PgsProgressCategory> topCategoryList = matrixCategoryList.stream()
|
||||
.filter(p -> p.getParentId().equals(0L))
|
||||
.toList();
|
||||
Map<String, List<PgsProgressCategory>> topMap = topCategoryList.stream()
|
||||
.collect(Collectors.groupingBy(PgsProgressCategory::getName));
|
||||
// 封装进度类别数据
|
||||
List<PgsProgressCategoryGanttVo> list = new ArrayList<>();
|
||||
for (Map.Entry<String, List<PgsProgressCategory>> entry : topMap.entrySet()) {
|
||||
List<PgsProgressCategory> 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<PgsProgressCategoryGanttSubProjectVo> 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<BusProject> projects = projectService.lambdaQuery()
|
||||
.eq(BusProject::getPId, project.getPId())
|
||||
.list();
|
||||
List<Long> projectIds = projects.stream().map(BusProject::getId).toList();
|
||||
String name = category.getName();
|
||||
String relevancyStructure = category.getRelevancyStructure();
|
||||
// 获取对应的进度类别列表
|
||||
List<PgsProgressCategory> 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<Long, List<PgsProgressCategory>> map = progressCategoryList.stream()
|
||||
.collect(Collectors.groupingBy(PgsProgressCategory::getProjectId));
|
||||
return map.entrySet().stream().map(entry -> {
|
||||
Long key = entry.getKey();
|
||||
List<PgsProgressCategory> value = entry.getValue();
|
||||
PgsProgressCategoryGanttSubProjectVo vo = new PgsProgressCategoryGanttSubProjectVo();
|
||||
vo.setRelevancyStructure(PgsRelevancyStructureEnum.MATRIX.getValue());
|
||||
vo.setSubProjectId(key);
|
||||
vo.setProgressCategoryName(name);
|
||||
// 封装关联方阵结构
|
||||
List<PgsProgressCategoryGanttMatrixVo> 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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算进度类别(含子类别)的开始和结束时间。
|
||||
* <p>
|
||||
* 逻辑说明:
|
||||
* 1. 若该进度类别为叶子节点,则直接根据其关联的计划表(Plan)的开始/结束时间计算。
|
||||
* 2. 若该进度类别有子节点,则递归计算所有子节点的开始/结束时间,
|
||||
* 以最早的开始时间和最晚的结束时间作为该类别的时间区间。
|
||||
* </p>
|
||||
* <p>
|
||||
*
|
||||
* @param progressCategoryId 当前进度类别ID
|
||||
* @param allChildren 所有进度类别(树形结构的扁平化列表)
|
||||
* @param allPlanList 所有进度计划列表
|
||||
* @return 当前进度类别的开始和结束时间(可能为空)
|
||||
*/
|
||||
private PgsProgressCategoryGanttVo getCategoryStartDateAndEndDate(Long progressCategoryId,
|
||||
List<PgsProgressCategory> allChildren,
|
||||
List<PgsProgressPlan> allPlanList) {
|
||||
PgsProgressCategoryGanttVo ganttVo = new PgsProgressCategoryGanttVo();
|
||||
|
||||
// 获取该类别下的所有叶子节点(无子节点的类别)
|
||||
List<PgsProgressCategory> children = this.getLeafNodesByTopId(progressCategoryId, allChildren);
|
||||
|
||||
if (CollUtil.isEmpty(children)) {
|
||||
// --- 叶子节点:直接根据计划计算 ---
|
||||
List<PgsProgressPlan> 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<PgsProgressCategoryGanttVo> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据名称,递归获取进度类别(含子类别)的甘特图数据。
|
||||
* <p>
|
||||
* 逻辑说明:
|
||||
* 1. 获取该进度类别下的所有叶子节点(无子节点的类别)。
|
||||
* 2. 递归获取所有叶子节点的甘特图数据。
|
||||
* 3. 获取所有叶子节点的甘特图数据,并添加到结果列表中。
|
||||
* </p>
|
||||
*
|
||||
* @param parentId 父级ID
|
||||
* @param value 当前进度类别列表
|
||||
* @param list 结果列表
|
||||
* @param progressCategoryList 所有进度类别列表
|
||||
* @param planList 所有进度计划列表
|
||||
*/
|
||||
private void getCategoryGanttAndChildrenVo(Long parentId,
|
||||
List<PgsProgressCategory> value,
|
||||
List<PgsProgressCategoryGanttVo> list,
|
||||
List<PgsProgressCategory> progressCategoryList,
|
||||
List<PgsProgressPlan> 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<PgsProgressCategory> children = value.stream()
|
||||
.map(category -> getLeafNodesByTopId(category.getId(), progressCategoryList))
|
||||
.filter(CollUtil::isNotEmpty)
|
||||
.flatMap(Collection::stream)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (CollUtil.isEmpty(children)) {
|
||||
children = value;
|
||||
}
|
||||
// 获取子节点的进度计划
|
||||
List<PgsProgressCategory> finalChildren = children;
|
||||
List<PgsProgressPlan> 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<PgsProgressCategoryGanttVo> 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<PgsProgressCategory> nextLevelChildren = children.stream()
|
||||
.filter(child -> child.getAncestors().split(",").length == level)
|
||||
.distinct()
|
||||
.toList();
|
||||
Map<String, List<PgsProgressCategory>> nextMap = nextLevelChildren.stream()
|
||||
.collect(Collectors.groupingBy(PgsProgressCategory::getName));
|
||||
for (Map.Entry<String, List<PgsProgressCategory>> entry : nextMap.entrySet()) {
|
||||
getCategoryGanttAndChildrenVo(first.getId(), entry.getValue(), list, progressCategoryList, planList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -127,16 +127,6 @@ public class PgsProgressPlanServiceImpl extends ServiceImpl<PgsProgressPlanMappe
|
||||
if (overlap) {
|
||||
throw new ServiceException("该进度类型下,时间区间已存在重叠的数据");
|
||||
}
|
||||
Long projectId = progressPlan.getProjectId();
|
||||
if (projectService.getById(projectId) == null) {
|
||||
throw new ServiceException("对应项目不存在", HttpStatus.NOT_FOUND);
|
||||
}
|
||||
Long matrixId = progressPlan.getMatrixId();
|
||||
FacMatrix matrix = matrixService.getById(matrixId);
|
||||
String matrixName = null;
|
||||
if (matrix != null) {
|
||||
matrixName = matrix.getMatrixName();
|
||||
}
|
||||
Long progressCategoryId = progressPlan.getProgressCategoryId();
|
||||
// 校验日期是否合法
|
||||
/* PgsProgressPlan lastProgressPlan = this.lambdaQuery()
|
||||
@ -148,6 +138,18 @@ public class PgsProgressPlanServiceImpl extends ServiceImpl<PgsProgressPlanMappe
|
||||
throw new ServiceException("开始日期不能早于上一条进度计划结束日期", HttpStatus.BAD_REQUEST);
|
||||
}*/
|
||||
PgsProgressCategory progressCategory = progressCategoryService.getById(progressCategoryId);
|
||||
progressPlan.setProjectId(progressCategory.getProjectId());
|
||||
progressPlan.setMatrixId(progressCategory.getMatrixId());
|
||||
Long projectId = progressPlan.getProjectId();
|
||||
if (projectService.getById(projectId) == null) {
|
||||
throw new ServiceException("对应项目不存在", HttpStatus.NOT_FOUND);
|
||||
}
|
||||
Long matrixId = progressPlan.getMatrixId();
|
||||
FacMatrix matrix = matrixService.getById(matrixId);
|
||||
String matrixName = null;
|
||||
if (matrix != null) {
|
||||
matrixName = matrix.getMatrixName();
|
||||
}
|
||||
this.validPlanNumber(req.getPlanNumber(), progressCategory);
|
||||
progressPlan.setProgressCategoryName(progressCategory.getName());
|
||||
progressPlan.setMatrixName(matrixName);
|
||||
@ -172,6 +174,8 @@ public class PgsProgressPlanServiceImpl extends ServiceImpl<PgsProgressPlanMappe
|
||||
if (!result) {
|
||||
throw new ServiceException("新增进度计划详情失败,数据库操作失败", HttpStatus.ERROR);
|
||||
}
|
||||
} else {
|
||||
throw new ServiceException("新增进度计划详情失败,请进行均分", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
// 更新进度分类计划总数量
|
||||
boolean update = progressCategoryService.lambdaUpdate()
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package org.dromara.project.domain.dto.leave;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@ -26,12 +27,14 @@ public class BusLeaveManagerReviewReq implements Serializable {
|
||||
* 管理员意见(1未读 2同意 3拒绝)
|
||||
*/
|
||||
@NotNull(message = "管理员意见不能为空")
|
||||
private String managerOpinion;
|
||||
private String gangerOpinion;
|
||||
|
||||
/**
|
||||
* 管理员说明
|
||||
* 班组长说明
|
||||
*/
|
||||
private String managerExplain;
|
||||
@ExcelProperty(value = "班组长说明")
|
||||
private String gangerExplain;
|
||||
|
||||
|
||||
/**
|
||||
* 备注
|
||||
|
||||
@ -6,8 +6,11 @@ import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import org.dromara.common.excel.annotation.ExcelDictFormat;
|
||||
import org.dromara.common.excel.convert.ExcelDictConvert;
|
||||
import org.dromara.common.translation.annotation.Translation;
|
||||
import org.dromara.common.translation.constant.TransConstant;
|
||||
import org.dromara.project.domain.BusLeave;
|
||||
import org.dromara.project.domain.vo.reissuecard.AuditUserVo;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
@ -65,6 +68,9 @@ public class BusLeaveVo implements Serializable {
|
||||
@ExcelDictFormat(dictType = "user_leave_type")
|
||||
private String leaveType;
|
||||
|
||||
@Translation(type = TransConstant.DICT_TYPE_TO_LABEL, mapper = "leaveType",other = "user_leave_type")
|
||||
private String leaveTypeName;
|
||||
|
||||
/**
|
||||
* 请假开始时间
|
||||
*/
|
||||
|
||||
@ -760,6 +760,7 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
|
||||
.eq(BusAttendance::getUserId, userId)
|
||||
.eq(b, BusAttendance::getProjectId, projectId)
|
||||
.in(BusAttendance::getClockStatus, abnormalList)
|
||||
.ge(BusAttendance::getClockDate, LocalDate.now().minusDays(29))
|
||||
.orderByDesc(BusAttendance::getClockDate)
|
||||
.orderByDesc(BusAttendance::getRuleTime)
|
||||
);
|
||||
|
||||
@ -59,6 +59,8 @@ import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.dromara.common.core.constant.TenantConstants.SUPER_ADMIN_ID;
|
||||
|
||||
/**
|
||||
* 施工人员请假申请Service业务层处理
|
||||
*
|
||||
@ -236,73 +238,79 @@ public class BusLeaveServiceImpl extends ServiceImpl<BusLeaveMapper, BusLeave>
|
||||
@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<BusAttendance> 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());
|
||||
}
|
||||
// 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);
|
||||
if(!list.isEmpty()){
|
||||
attendanceService.updateBatchById(list);
|
||||
}
|
||||
// 填充默认值,更新数据
|
||||
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);
|
||||
}else if(busLeave.getTimeType().equals("2")){
|
||||
if(busLeave.getPeriodType().equals("1")){
|
||||
LocalDateTime startTime = busLeave.getStartTime();
|
||||
LocalDate localDate = startTime.toLocalDate();
|
||||
List<BusAttendance> 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());
|
||||
}
|
||||
// 更新考勤表记录
|
||||
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<BusAttendance> 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);
|
||||
if(!list.isEmpty()){
|
||||
attendanceService.updateBatchById(list);
|
||||
}
|
||||
return true;
|
||||
|
||||
}else if(busLeave.getPeriodType().equals("2")){
|
||||
LocalDateTime endTime = busLeave.getEndTime();
|
||||
LocalDate localDate = endTime.toLocalDate();
|
||||
List<BusAttendance> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return i>0;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -375,7 +383,7 @@ public class BusLeaveServiceImpl extends ServiceImpl<BusLeaveMapper, BusLeave>
|
||||
// 精确查询
|
||||
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<BusLeaveMapper, BusLeave>
|
||||
// 添加审核状态
|
||||
String status = BusReviewStatusEnum.getEnumByOpinionStatus(leave.getGangerOpinion(), leave.getManagerOpinion());
|
||||
leaveVo.setAuditStatus(status);
|
||||
|
||||
//添加审核人
|
||||
if(StrUtil.isBlank(leaveVo.getGangerName())){
|
||||
String userType = leaveVo.getUserType();
|
||||
List<SysUser> 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<BusLeaveMapper, BusLeave>
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
@ -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<BusProjectMapper, BusProj
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
//获取项目列表
|
||||
List<BusProjectVo> projectVos = baseMapper.selectVoList(new LambdaQueryWrapper<BusProject>().eq(BusProject::getPId,0).eq(BusProject::getIsDelete,0));
|
||||
List<BusProjectVo> projectVos = baseMapper.selectVoList(new LambdaQueryWrapper<BusProject>().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<String, Map<String, Map<String, String>>> map = new HashMap<>();
|
||||
for (SysDictDataVo projectType : projectTypes) {
|
||||
Map<String, Map<String, String>> map1 = new HashMap<>();
|
||||
@ -1147,15 +1182,15 @@ public class BusProjectServiceImpl extends ServiceImpl<BusProjectMapper, BusProj
|
||||
BusProjectVo project = iterator.next();
|
||||
if (projectType.getDictValue().equals(project.getProjectType())) {
|
||||
Map<String, String> 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<BusProjectMapper, BusProj
|
||||
List<BusProjectVo> projectVos = baseMapper.getProjectDiYv();
|
||||
Map<String, Long> 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<BusProjectMapper, BusProj
|
||||
@Override
|
||||
public Map<String, Map<String, String>> getProjectCapacity() {
|
||||
//获取项目列表
|
||||
List<BusProjectVo> projectVos = baseMapper.selectVoList(new LambdaQueryWrapper<BusProject>().eq(BusProject::getPId,0).eq(BusProject::getIsDelete,0));
|
||||
List<BusProjectVo> projectVos = baseMapper.selectVoList(new LambdaQueryWrapper<BusProject>().eq(BusProject::getPId, 0).eq(BusProject::getIsDelete, 0));
|
||||
if (projectVos == null || projectVos.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, Map<String, String>> map = new HashMap<>();
|
||||
for (BusProjectVo project : projectVos) {
|
||||
Map<String, String> 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<BusProjectMapper, BusProj
|
||||
Map<String, Map<String, Object>> map = new HashMap<>();
|
||||
SubConstructionUser byUserId = constructionUserService.getByUserId(req.getId());
|
||||
Map<String, Object> 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<BusProjectMapper, BusProj
|
||||
.one();
|
||||
return project != null ? project.getProjectName() : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建项目信息
|
||||
*
|
||||
* @param name 项目名称
|
||||
* @param lat 纬度
|
||||
* @param lng 经度
|
||||
* @param projectType 项目类型
|
||||
* @return 项目信息
|
||||
*/
|
||||
private static BusProjectVo create(String province, String name, double lat, double lng, Integer projectType) {
|
||||
BusProjectVo vo = new BusProjectVo();
|
||||
vo.setId(IdWorker.getID());
|
||||
vo.setProvince(province);
|
||||
vo.setProjectName(name);
|
||||
vo.setLat(String.valueOf(lat));
|
||||
vo.setLng(String.valueOf(lng));
|
||||
vo.setProjectType(projectType.toString());
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
|
||||
@ -359,6 +359,28 @@ public class BusReissueCardServiceImpl extends ServiceImpl<BusReissueCardMapper,
|
||||
// 添加审核状态
|
||||
String status = BusReviewStatusEnum.getEnumByOpinionStatus(reissueCard.getGangerOpinion(), reissueCard.getManagerOpinion());
|
||||
reissueCardVo.setStatus(status);
|
||||
|
||||
//添加审核人
|
||||
if(StrUtil.isBlank(reissueCardVo.getGangerName())){
|
||||
String userType = reissueCardVo.getUserType();
|
||||
List<SysUser> 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);
|
||||
|
||||
@ -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<List<HseFileFolderVo>> listAll(ListQueryDto dto) {
|
||||
return R.ok(hseFileFolderService.listAll(dto));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定目录的树形结构(一次性加载所有层级)
|
||||
* @return 树形结构列表
|
||||
*/
|
||||
@GetMapping("/tree-all")
|
||||
public R<List<FileFolderTreeVO>> treeAll(ListQueryDto dto) {
|
||||
|
||||
// 1. 查询所有子项(利用path前缀匹配,一次性加载所有层级)
|
||||
List<HseFileFolder> allItems = hseFileFolderService.list(new LambdaQueryWrapper<HseFileFolder>()
|
||||
.like(HseFileFolder::getPath, "," + dto.getParentId() + ",") // 包含父ID的所有子项
|
||||
.eq(dto.getType()!=null,HseFileFolder::getType, dto.getType())
|
||||
.orderByDesc(HseFileFolder::getId)
|
||||
.orderByAsc(HseFileFolder::getSort)
|
||||
);
|
||||
|
||||
// 2. 构建树形结构
|
||||
List<FileFolderTreeVO> treeVOS = buildTree(allItems, dto.getParentId());
|
||||
return R.ok(treeVOS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归构建树形结构
|
||||
*/
|
||||
private List<FileFolderTreeVO> buildTree(List<HseFileFolder> 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<HseFileFolder> create(@RequestBody FileFolderCreateDTO dto) {
|
||||
return R.ok(hseFileFolderService.createFileOrFolder(dto));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件或文件夹(级联删除子项)
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@Transactional
|
||||
public R<Boolean> delete(@PathVariable Long id) {
|
||||
return R.ok(hseFileFolderService.deleteFileOrFolder(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动文件或文件夹到指定目录
|
||||
*/
|
||||
@PostMapping("/move")
|
||||
@Transactional
|
||||
public R<Boolean> move(@RequestBody FileFolderMoveDTO dto) {
|
||||
return R.ok(hseFileFolderService.moveFileOrFolder(dto.getId(), dto.getTargetParentId()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -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;
|
||||
|
||||
|
||||
}
|
||||
@ -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;
|
||||
|
||||
|
||||
}
|
||||
@ -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;
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
|
||||
}
|
||||
@ -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;
|
||||
|
||||
|
||||
}
|
||||
@ -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<FileFolderTreeVO> children;
|
||||
}
|
||||
@ -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<HseFileFolder, HseFileFolderVo> {
|
||||
|
||||
/**
|
||||
* 批量更新子项的路径和层级
|
||||
*/
|
||||
void batchUpdateChildPaths(
|
||||
@Param("oldPath") String oldPath,
|
||||
@Param("newPath") String newPath,
|
||||
@Param("levelDiff") int levelDiff,
|
||||
@Param("parentId") Long parentId);
|
||||
}
|
||||
@ -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<HseFileFolder>{
|
||||
|
||||
/**
|
||||
* 查询会议纪要
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 会议纪要
|
||||
*/
|
||||
HseFileFolderVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 分页查询会议纪要列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 会议纪要分页列表
|
||||
*/
|
||||
TableDataInfo<HseFileFolderVo> queryPageList(HseFileFolderBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询符合条件的会议纪要列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 会议纪要列表
|
||||
*/
|
||||
List<HseFileFolderVo> 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<Long> ids, Boolean isValid);
|
||||
|
||||
|
||||
List<HseFileFolderVo> listAll(ListQueryDto dto);
|
||||
|
||||
/**
|
||||
* 创建文件或文件夹
|
||||
*/
|
||||
HseFileFolder createFileOrFolder(FileFolderCreateDTO dto);
|
||||
|
||||
/**
|
||||
* 删除文件或文件夹(级联删除子项)
|
||||
*/
|
||||
boolean deleteFileOrFolder(Long id);
|
||||
|
||||
/**
|
||||
* 移动文件或文件夹到指定目录
|
||||
*/
|
||||
boolean moveFileOrFolder(Long id, Long targetParentId);
|
||||
}
|
||||
@ -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<HseFileFolderMapper, HseFileFolder> 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<HseFileFolderVo> queryPageList(HseFileFolderBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<HseFileFolder> lqw = buildQueryWrapper(bo);
|
||||
Page<HseFileFolderVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询符合条件的会议纪要列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 会议纪要列表
|
||||
*/
|
||||
@Override
|
||||
public List<HseFileFolderVo> queryList(HseFileFolderBo bo) {
|
||||
LambdaQueryWrapper<HseFileFolder> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<HseFileFolder> buildQueryWrapper(HseFileFolderBo bo) {
|
||||
Map<String, Object> params = bo.getParams();
|
||||
LambdaQueryWrapper<HseFileFolder> 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<Long> ids, Boolean isValid) {
|
||||
if(isValid){
|
||||
//TODO 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
return baseMapper.deleteByIds(ids) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HseFileFolderVo> listAll(ListQueryDto dto) {
|
||||
|
||||
LambdaQueryWrapper<HseFileFolder> 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<HseFileFolder> queryWrapper = Wrappers.<HseFileFolder>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;
|
||||
}
|
||||
}
|
||||
@ -383,6 +383,7 @@ public class SysUserController extends BaseController {
|
||||
public TableDataInfo<SysUserVo> fbList(FbUserListDto dto, PageQuery pageQuery) {
|
||||
return userService.selectPageFbUserList(dto, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分包单位列表
|
||||
*/
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.dromara.safety.mapper.HseFileFolderMapper">
|
||||
|
||||
<!-- 批量更新子项的路径和层级 -->
|
||||
<update id="batchUpdateChildPaths">
|
||||
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}
|
||||
</update>
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user