Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
2025-10-14 21:10:20 +08:00
37 changed files with 2756 additions and 140 deletions

View File

@ -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

View File

@ -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>-->

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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: 设备SNvalue: 设备信息)
private static final Map<String, KqjEntity.DeviceInfo> connectedDevices = new ConcurrentHashMap<>();
// 2. 存储UUID对应的响应通道key: UUIDvalue: 响应结果容器)
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("设备在线心跳DECLARESN: {}", 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;
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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));
}
/**
* 导出进度类别列表
*/

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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);
}
}
}
}

View File

@ -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()

View File

@ -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;
/**
* 备注

View File

@ -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;
/**
* 请假开始时间
*/

View File

@ -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)
);

View File

@ -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();
}

View File

@ -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;
@ -1137,6 +1135,43 @@ public class BusProjectServiceImpl extends ServiceImpl<BusProjectMapper, BusProj
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<>();
@ -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;
}
}

View File

@ -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);

View File

@ -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()));
}
}

View File

@ -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;
/**
* 父级ID0表示根目录
*/
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;
}

View File

@ -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;
/**
* 父级ID0表示根目录
*/
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;
}

View File

@ -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;
/**
* 父级ID0表示根目录
*/
private Long parentId = 0L;
/**
* 文件类型
*/
@NotNull(message = "类型不能为空")
private Integer type;
/**
* 排序号
*/
private Integer sort=0;
/**
* 文件后缀
*/
private String fileSuffix;
/**
* 文件存储路径(仅文件有效)
*/
private String filePath;
/**
* 备注
*/
private String remark;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
/**
* 父级ID0表示根目录
*/
@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;
}

View File

@ -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;
/**
* 父级ID0表示根目录
*/
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;
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -383,6 +383,7 @@ public class SysUserController extends BaseController {
public TableDataInfo<SysUserVo> fbList(FbUserListDto dto, PageQuery pageQuery) {
return userService.selectPageFbUserList(dto, pageQuery);
}
/**
* 获取分包单位列表
*/

View File

@ -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;

View File

@ -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 {

View File

@ -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>