feat:设备日志模块
This commit is contained in:
1 parent
41fa28775b
commit
06330260e5
18 files changed
+1217
-5
No files matched your search
@@ -0,0 +1,77 @@
|
||||
-- 终端日志文件管理端权限补充。
|
||||
-- 当前 sys_menu.permission 长度为 32,下载权限使用 down,避免 33 位的 download 标识被数据库截断。
|
||||
-- 本脚本只维护权限元数据,不写 sys_role_menu;新按钮由管理员按角色显式授权,避免扩大现有角色权限。
|
||||
|
||||
SET @terminal_log_menu_seed := (SELECT COALESCE(MAX(menu_id), 0) FROM sys_menu);
|
||||
|
||||
INSERT INTO sys_menu
|
||||
(menu_id, name, permission, path, component, parent_id, icon, visible, sort_order, keep_alive, embedded,
|
||||
menu_type, create_by, create_time, update_by, update_time, del_flag, tenant_id)
|
||||
SELECT (@terminal_log_menu_seed := @terminal_log_menu_seed + 1), '日志预览',
|
||||
'hard_hardTerminalLogFile_preview', NULL, NULL, view_menu.parent_id, NULL, '0', 1, '0', '0', '1',
|
||||
'admin', NOW(), 'admin', NOW(), '0', view_menu.tenant_id
|
||||
FROM sys_menu view_menu
|
||||
WHERE view_menu.permission = 'hard_hardTerminalLogFile_view'
|
||||
AND view_menu.del_flag = '0'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_menu existing
|
||||
WHERE existing.tenant_id <=> view_menu.tenant_id
|
||||
AND existing.permission = 'hard_hardTerminalLogFile_preview'
|
||||
);
|
||||
|
||||
INSERT INTO sys_menu
|
||||
(menu_id, name, permission, path, component, parent_id, icon, visible, sort_order, keep_alive, embedded,
|
||||
menu_type, create_by, create_time, update_by, update_time, del_flag, tenant_id)
|
||||
SELECT (@terminal_log_menu_seed := @terminal_log_menu_seed + 1), '日志下载',
|
||||
'hard_hardTerminalLogFile_down', NULL, NULL, view_menu.parent_id, NULL, '0', 2, '0', '0', '1',
|
||||
'admin', NOW(), 'admin', NOW(), '0', view_menu.tenant_id
|
||||
FROM sys_menu view_menu
|
||||
WHERE view_menu.permission = 'hard_hardTerminalLogFile_view'
|
||||
AND view_menu.del_flag = '0'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_menu existing
|
||||
WHERE existing.tenant_id <=> view_menu.tenant_id
|
||||
AND existing.permission = 'hard_hardTerminalLogFile_down'
|
||||
);
|
||||
|
||||
INSERT INTO sys_menu
|
||||
(menu_id, name, permission, path, component, parent_id, icon, visible, sort_order, keep_alive, embedded,
|
||||
menu_type, create_by, create_time, update_by, update_time, del_flag, tenant_id)
|
||||
SELECT (@terminal_log_menu_seed := @terminal_log_menu_seed + 1), '立即拉取日志',
|
||||
'hard_hardTerminalLogFile_pull', NULL, NULL, view_menu.parent_id, NULL, '0', 3, '0', '0', '1',
|
||||
'admin', NOW(), 'admin', NOW(), '0', view_menu.tenant_id
|
||||
FROM sys_menu view_menu
|
||||
WHERE view_menu.permission = 'hard_hardTerminalLogFile_view'
|
||||
AND view_menu.del_flag = '0'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_menu existing
|
||||
WHERE existing.tenant_id <=> view_menu.tenant_id
|
||||
AND existing.permission = 'hard_hardTerminalLogFile_pull'
|
||||
);
|
||||
|
||||
UPDATE sys_menu
|
||||
SET del_flag = '0',
|
||||
visible = '0',
|
||||
update_by = 'admin',
|
||||
update_time = NOW()
|
||||
WHERE permission IN (
|
||||
'hard_hardTerminalLogFile_preview',
|
||||
'hard_hardTerminalLogFile_down',
|
||||
'hard_hardTerminalLogFile_pull'
|
||||
);
|
||||
|
||||
-- 生成器默认的写操作和导出按钮不再对管理端开放,保留记录便于审计和重复执行。
|
||||
UPDATE sys_menu
|
||||
SET del_flag = '1',
|
||||
visible = '0',
|
||||
update_by = 'admin',
|
||||
update_time = NOW()
|
||||
WHERE permission IN (
|
||||
'hard_hardTerminalLogFile_add',
|
||||
'hard_hardTerminalLogFile_edit',
|
||||
'hard_hardTerminalLogFile_del',
|
||||
'hard_hardTerminalLogFile_export'
|
||||
);
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.bsd.pjcloud.hard.api.dto.manager;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 终端日志管理端查询条件。
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "终端日志文件查询条件")
|
||||
public class HardTerminalLogFileQueryDto {
|
||||
|
||||
@Schema(description = "设备ID")
|
||||
private Long hardId;
|
||||
|
||||
@Schema(description = "设备名称")
|
||||
private String hardName;
|
||||
|
||||
@Schema(description = "设备唯一编号")
|
||||
private String uniqueCode;
|
||||
|
||||
@Schema(description = "商户ID")
|
||||
private Long merchantId;
|
||||
|
||||
@Schema(description = "部门ID")
|
||||
private Long deptId;
|
||||
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||
@Schema(description = "日志日期开始")
|
||||
private LocalDate logDateStart;
|
||||
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||
@Schema(description = "日志日期结束")
|
||||
private LocalDate logDateEnd;
|
||||
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
||||
@Schema(description = "上传时间开始")
|
||||
private LocalDateTime uploadTimeStart;
|
||||
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
||||
@Schema(description = "上传时间结束")
|
||||
private LocalDateTime uploadTimeEnd;
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.bsd.pjcloud.hard.api.dto.manager;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 终端日志上传预检参数。
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "终端日志上传预检参数")
|
||||
public class HardTerminalLogPreflightDto {
|
||||
|
||||
@NotBlank(message = "日志相对路径不能为空")
|
||||
@Size(max = 255, message = "日志相对路径不能超过255个字符")
|
||||
@Schema(description = "终端日志根目录下的相对路径")
|
||||
private String relativePath;
|
||||
|
||||
@NotNull(message = "文件大小不能为空")
|
||||
@Min(value = 0, message = "文件大小不能小于0")
|
||||
@Schema(description = "脱敏文件大小")
|
||||
private Long fileSize;
|
||||
|
||||
@NotBlank(message = "文件哈希不能为空")
|
||||
@Pattern(regexp = "(?i)^[0-9a-f]{32}$", message = "文件哈希必须为32位MD5")
|
||||
@Schema(description = "脱敏文件MD5")
|
||||
private String fileHash;
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package com.bsd.pjcloud.hard.api.dto.manager;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 终端日志上传元数据。设备快照由服务端根据请求头中的设备ID填充。
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "终端日志上传元数据")
|
||||
public class HardTerminalLogUploadDto {
|
||||
|
||||
@NotBlank(message = "日志相对路径不能为空")
|
||||
@Size(max = 255, message = "日志相对路径不能超过255个字符")
|
||||
@Schema(description = "终端日志根目录下的相对路径")
|
||||
private String relativePath;
|
||||
|
||||
@NotNull(message = "日志日期不能为空")
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||
@Schema(description = "日志所属日期")
|
||||
private LocalDate logDate;
|
||||
|
||||
@NotNull(message = "本地修改时间不能为空")
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
||||
@Schema(description = "终端本地文件最后修改时间")
|
||||
private LocalDateTime fileModifiedTime;
|
||||
|
||||
@Size(max = 64, message = "应用版本不能超过64个字符")
|
||||
@Schema(description = "应用版本")
|
||||
private String appVersion;
|
||||
|
||||
@NotBlank(message = "脱敏规则版本不能为空")
|
||||
@Size(max = 32, message = "脱敏规则版本不能超过32个字符")
|
||||
@Schema(description = "脱敏规则版本")
|
||||
private String sanitizeVersion;
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
package com.bsd.pjcloud.hard.api.entity.manager;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.baomidou.mybatisplus.extension.activerecord.Model;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import com.bsd.pjcloud.common.core.util.TenantTable;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 终端日志文件
|
||||
*/
|
||||
@Data
|
||||
@TenantTable
|
||||
@TableName("hard_terminal_log_file")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "终端日志文件")
|
||||
public class HardTerminalLogFileEntity extends Model<HardTerminalLogFileEntity> {
|
||||
|
||||
/**
|
||||
* 编号
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
@Schema(description = "编号")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 设备ID(hard_manager.id)
|
||||
*/
|
||||
@Schema(description = "设备ID(hard_manager.id)")
|
||||
private Long hardId;
|
||||
|
||||
/**
|
||||
* 上传时设备名称快照
|
||||
*/
|
||||
@Schema(description = "上传时设备名称快照")
|
||||
private String hardName;
|
||||
|
||||
/**
|
||||
* 上传时设备唯一编号快照
|
||||
*/
|
||||
@Schema(description = "上传时设备唯一编号快照")
|
||||
private String uniqueCode;
|
||||
|
||||
/**
|
||||
* 上传时设备类型快照
|
||||
*/
|
||||
@Schema(description = "上传时设备类型快照")
|
||||
private String hardType;
|
||||
|
||||
/**
|
||||
* 上传时商户ID快照
|
||||
*/
|
||||
@Schema(description = "上传时商户ID快照")
|
||||
private Long merchantId;
|
||||
|
||||
/**
|
||||
* 上传时部门ID快照
|
||||
*/
|
||||
@Schema(description = "上传时部门ID快照")
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 日志所属日期
|
||||
*/
|
||||
@Schema(description = "日志所属日期")
|
||||
private LocalDate logDate;
|
||||
|
||||
/**
|
||||
* 终端日志根目录下的相对路径
|
||||
*/
|
||||
@Schema(description = "终端日志根目录下的相对路径")
|
||||
private String relativePath;
|
||||
|
||||
/**
|
||||
* 原日志文件名
|
||||
*/
|
||||
@Schema(description = "原日志文件名")
|
||||
private String originalName;
|
||||
|
||||
/**
|
||||
* 文件大小(字节)
|
||||
*/
|
||||
@Schema(description = "文件大小(字节)")
|
||||
private Long fileSize;
|
||||
|
||||
/**
|
||||
* 文件内容哈希(当前使用MD5)
|
||||
*/
|
||||
@Schema(description = "文件内容哈希(当前使用MD5)")
|
||||
private String fileHash;
|
||||
|
||||
/**
|
||||
* 终端本地文件最后修改时间
|
||||
*/
|
||||
@Schema(description = "终端本地文件最后修改时间")
|
||||
private LocalDateTime fileModifiedTime;
|
||||
|
||||
/**
|
||||
* 文件中心sys_file记录ID
|
||||
*/
|
||||
@Schema(description = "文件中心sys_file记录ID")
|
||||
private Long sysFileId;
|
||||
|
||||
/**
|
||||
* OSS对象文件名
|
||||
*/
|
||||
@Schema(description = "OSS对象文件名")
|
||||
private String storageFileName;
|
||||
|
||||
/**
|
||||
* 文件访问相对地址
|
||||
*/
|
||||
@Schema(description = "文件访问相对地址")
|
||||
private String storageUrl;
|
||||
|
||||
/**
|
||||
* 上传时应用版本
|
||||
*/
|
||||
@Schema(description = "上传时应用版本")
|
||||
private String appVersion;
|
||||
|
||||
/**
|
||||
* 日志脱敏规则版本
|
||||
*/
|
||||
@Schema(description = "日志脱敏规则版本")
|
||||
private String sanitizeVersion;
|
||||
|
||||
/**
|
||||
* 最近成功上传时间
|
||||
*/
|
||||
@Schema(description = "最近成功上传时间")
|
||||
private LocalDateTime uploadTime;
|
||||
|
||||
/**
|
||||
* 成功上传次数
|
||||
*/
|
||||
@Schema(description = "成功上传次数")
|
||||
private Integer uploadCount;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description = "创建人")
|
||||
private String createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 修改人
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@Schema(description = "修改人")
|
||||
private String updateBy;
|
||||
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@Schema(description = "修改时间")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 删除标记
|
||||
*/
|
||||
@TableLogic
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description = "删除标记")
|
||||
private String delFlag;
|
||||
|
||||
/**
|
||||
* 租户ID
|
||||
*/
|
||||
@Schema(description = "租户ID")
|
||||
private Long tenantId;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.bsd.pjcloud.hard.api.vo.manager;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 终端日志设备聚合摘要。
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "终端日志设备摘要")
|
||||
public class HardTerminalLogDeviceVo {
|
||||
|
||||
private Long hardId;
|
||||
private String hardName;
|
||||
private String uniqueCode;
|
||||
private String hardType;
|
||||
private Long merchantId;
|
||||
private Long deptId;
|
||||
private Long fileCount;
|
||||
private LocalDateTime latestUploadTime;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.bsd.pjcloud.hard.api.vo.manager;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 终端日志文件树节点。
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "终端日志文件树节点")
|
||||
public class HardTerminalLogFileTreeVo {
|
||||
|
||||
private String key;
|
||||
private String label;
|
||||
private String relativePath;
|
||||
private Boolean directory;
|
||||
private Long fileId;
|
||||
private Long fileSize;
|
||||
private LocalDate logDate;
|
||||
private LocalDateTime uploadTime;
|
||||
private List<HardTerminalLogFileTreeVo> children = new ArrayList<>();
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.bsd.pjcloud.hard.api.vo.manager;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 终端日志上传预检结果。
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@Schema(description = "终端日志上传预检结果")
|
||||
public class HardTerminalLogPreflightVo {
|
||||
|
||||
private Long id;
|
||||
private Boolean needUpload;
|
||||
private String normalizedPath;
|
||||
private String message;
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.bsd.pjcloud.hard.api.vo.manager;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 终端日志尾部预览。
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@Schema(description = "终端日志预览")
|
||||
public class HardTerminalLogPreviewVo {
|
||||
|
||||
private Long id;
|
||||
private String originalName;
|
||||
private String relativePath;
|
||||
private String content;
|
||||
private Integer lineCount;
|
||||
private Long fileSize;
|
||||
private Boolean truncated;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.bsd.pjcloud.hard.api.vo.manager;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 终端日志上传结果。
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@Schema(description = "终端日志上传结果")
|
||||
public class HardTerminalLogUploadResultVo {
|
||||
|
||||
private Long id;
|
||||
private Boolean uploaded;
|
||||
private Boolean changed;
|
||||
private String relativePath;
|
||||
private Integer uploadCount;
|
||||
private String message;
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package com.bsd.pjcloud.hard.controller.hard;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.log.annotation.SysLog;
|
||||
import com.bsd.pjcloud.hard.api.dto.manager.HardTerminalLogFileQueryDto;
|
||||
import com.bsd.pjcloud.hard.api.dto.manager.HardTerminalLogPreflightDto;
|
||||
import com.bsd.pjcloud.hard.api.dto.manager.HardTerminalLogUploadDto;
|
||||
import com.bsd.pjcloud.hard.api.entity.manager.HardTerminalLogFileEntity;
|
||||
import com.bsd.pjcloud.hard.api.vo.manager.HardTerminalLogDeviceVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.manager.HardTerminalLogFileTreeVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.manager.HardTerminalLogPreflightVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.manager.HardTerminalLogPreviewVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.manager.HardTerminalLogUploadResultVo;
|
||||
import com.bsd.pjcloud.hard.service.HardTerminalLogFileService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 终端日志文件。
|
||||
*/
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/hardTerminalLogFile")
|
||||
@Tag(description = "hardTerminalLogFile", name = "终端日志文件管理")
|
||||
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
|
||||
public class HardTerminalLogFileController {
|
||||
|
||||
private final HardTerminalLogFileService hardTerminalLogFileService;
|
||||
|
||||
@Operation(summary = "终端日志上传预检")
|
||||
@PostMapping("/ad/preflight")
|
||||
public R<HardTerminalLogPreflightVo> preflight(@RequestHeader("DEVICE-ID") Long hardId,
|
||||
@Valid @RequestBody HardTerminalLogPreflightDto dto) {
|
||||
return R.ok(hardTerminalLogFileService.preflight(hardId, dto));
|
||||
}
|
||||
|
||||
@Operation(summary = "上传脱敏后的终端日志")
|
||||
@PostMapping(value = "/ad/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public R<HardTerminalLogUploadResultVo> upload(@RequestHeader("DEVICE-ID") Long hardId,
|
||||
@Valid @ModelAttribute HardTerminalLogUploadDto dto,
|
||||
@RequestPart("file") MultipartFile file) {
|
||||
return R.ok(hardTerminalLogFileService.upload(hardId, dto, file));
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询终端日志文件")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@pms.hasPermission('hard_hardTerminalLogFile_view')")
|
||||
public R<Page<HardTerminalLogFileEntity>> page(@ParameterObject Page<HardTerminalLogFileEntity> page,
|
||||
@ParameterObject HardTerminalLogFileQueryDto dto) {
|
||||
return R.ok(hardTerminalLogFileService.scopePage(page, dto));
|
||||
}
|
||||
|
||||
@Operation(summary = "按设备聚合终端日志")
|
||||
@GetMapping("/device-page")
|
||||
@PreAuthorize("@pms.hasPermission('hard_hardTerminalLogFile_view')")
|
||||
public R<Page<HardTerminalLogDeviceVo>> devicePage(@ParameterObject Page<HardTerminalLogDeviceVo> page,
|
||||
@ParameterObject HardTerminalLogFileQueryDto dto) {
|
||||
return R.ok(hardTerminalLogFileService.devicePage(page, dto));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询指定设备的日志文件树")
|
||||
@GetMapping("/file-tree")
|
||||
@PreAuthorize("@pms.hasPermission('hard_hardTerminalLogFile_view')")
|
||||
public R<List<HardTerminalLogFileTreeVo>> fileTree(@RequestParam Long hardId,
|
||||
@ParameterObject HardTerminalLogFileQueryDto dto) {
|
||||
return R.ok(hardTerminalLogFileService.fileTree(hardId, dto));
|
||||
}
|
||||
|
||||
@Operation(summary = "预览终端日志尾部")
|
||||
@GetMapping("/{id}/preview")
|
||||
@PreAuthorize("@pms.hasPermission('hard_hardTerminalLogFile_preview')")
|
||||
public R<HardTerminalLogPreviewVo> preview(@PathVariable Long id) {
|
||||
return R.ok(hardTerminalLogFileService.preview(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "下载完整终端日志")
|
||||
@GetMapping("/{id}/download")
|
||||
@PreAuthorize("@pms.hasPermission('hard_hardTerminalLogFile_down')")
|
||||
public void download(@PathVariable Long id, HttpServletResponse response) {
|
||||
hardTerminalLogFileService.download(id, response);
|
||||
}
|
||||
|
||||
@Operation(summary = "立即拉取指定终端当前日志")
|
||||
@SysLog("立即拉取终端当前日志")
|
||||
@PostMapping("/pull/{hardId}")
|
||||
@PreAuthorize("@pms.hasPermission('hard_hardTerminalLogFile_pull')")
|
||||
public R<Boolean> pull(@PathVariable Long hardId) {
|
||||
return R.ok(hardTerminalLogFileService.pull(hardId));
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package com.bsd.pjcloud.hard.mapper.manager;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
|
||||
import com.bsd.pjcloud.hard.api.dto.manager.HardTerminalLogFileQueryDto;
|
||||
import com.bsd.pjcloud.hard.api.entity.manager.HardTerminalLogFileEntity;
|
||||
import com.bsd.pjcloud.hard.api.vo.manager.HardTerminalLogDeviceVo;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface HardTerminalLogFileMapper extends PjcloudBaseMapper<HardTerminalLogFileEntity> {
|
||||
|
||||
default LambdaQueryWrapper<HardTerminalLogFileEntity> queryWrapper(HardTerminalLogFileQueryDto dto) {
|
||||
HardTerminalLogFileQueryDto query = dto == null ? new HardTerminalLogFileQueryDto() : dto;
|
||||
return new LambdaQueryWrapper<HardTerminalLogFileEntity>()
|
||||
.eq(ObjectUtil.isNotEmpty(query.getHardId()), HardTerminalLogFileEntity::getHardId, query.getHardId())
|
||||
.like(StrUtil.isNotBlank(query.getHardName()), HardTerminalLogFileEntity::getHardName, query.getHardName())
|
||||
.like(StrUtil.isNotBlank(query.getUniqueCode()), HardTerminalLogFileEntity::getUniqueCode, query.getUniqueCode())
|
||||
.eq(ObjectUtil.isNotEmpty(query.getMerchantId()), HardTerminalLogFileEntity::getMerchantId, query.getMerchantId())
|
||||
.eq(ObjectUtil.isNotEmpty(query.getDeptId()), HardTerminalLogFileEntity::getDeptId, query.getDeptId())
|
||||
.ge(ObjectUtil.isNotEmpty(query.getLogDateStart()), HardTerminalLogFileEntity::getLogDate, query.getLogDateStart())
|
||||
.le(ObjectUtil.isNotEmpty(query.getLogDateEnd()), HardTerminalLogFileEntity::getLogDate, query.getLogDateEnd())
|
||||
.ge(ObjectUtil.isNotEmpty(query.getUploadTimeStart()), HardTerminalLogFileEntity::getUploadTime, query.getUploadTimeStart())
|
||||
.le(ObjectUtil.isNotEmpty(query.getUploadTimeEnd()), HardTerminalLogFileEntity::getUploadTime, query.getUploadTimeEnd())
|
||||
.orderByDesc(HardTerminalLogFileEntity::getUploadTime)
|
||||
.orderByDesc(HardTerminalLogFileEntity::getId);
|
||||
}
|
||||
|
||||
default Page<HardTerminalLogFileEntity> scopePage(Page<HardTerminalLogFileEntity> page, HardTerminalLogFileQueryDto dto) {
|
||||
return selectPageByScope(page, queryWrapper(dto), DataScope.of());
|
||||
}
|
||||
|
||||
default List<HardTerminalLogFileEntity> scopeList(HardTerminalLogFileQueryDto dto) {
|
||||
return selectListByScope(queryWrapper(dto), DataScope.of());
|
||||
}
|
||||
|
||||
List<HardTerminalLogDeviceVo> deviceGroups(@Param("dto") HardTerminalLogFileQueryDto dto,
|
||||
@Param("dataScope") DataScope dataScope);
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.bsd.pjcloud.hard.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.bsd.pjcloud.hard.api.dto.manager.HardTerminalLogFileQueryDto;
|
||||
import com.bsd.pjcloud.hard.api.dto.manager.HardTerminalLogPreflightDto;
|
||||
import com.bsd.pjcloud.hard.api.dto.manager.HardTerminalLogUploadDto;
|
||||
import com.bsd.pjcloud.hard.api.entity.manager.HardTerminalLogFileEntity;
|
||||
import com.bsd.pjcloud.hard.api.vo.manager.HardTerminalLogDeviceVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.manager.HardTerminalLogFileTreeVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.manager.HardTerminalLogPreflightVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.manager.HardTerminalLogPreviewVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.manager.HardTerminalLogUploadResultVo;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface HardTerminalLogFileService extends IService<HardTerminalLogFileEntity> {
|
||||
|
||||
HardTerminalLogPreflightVo preflight(Long hardId, HardTerminalLogPreflightDto dto);
|
||||
|
||||
HardTerminalLogUploadResultVo upload(Long hardId, HardTerminalLogUploadDto dto, MultipartFile file);
|
||||
|
||||
Page<HardTerminalLogFileEntity> scopePage(Page<HardTerminalLogFileEntity> page, HardTerminalLogFileQueryDto dto);
|
||||
|
||||
Page<HardTerminalLogDeviceVo> devicePage(Page<HardTerminalLogDeviceVo> page, HardTerminalLogFileQueryDto dto);
|
||||
|
||||
List<HardTerminalLogFileTreeVo> fileTree(Long hardId, HardTerminalLogFileQueryDto dto);
|
||||
|
||||
HardTerminalLogPreviewVo preview(Long id);
|
||||
|
||||
void download(Long id, HttpServletResponse response);
|
||||
|
||||
Boolean pull(Long hardId);
|
||||
}
|
||||
+461
@@ -0,0 +1,461 @@
|
||||
package com.bsd.pjcloud.hard.service.impl.manager;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.SecureUtil;
|
||||
import com.baomidou.lock.LockInfo;
|
||||
import com.baomidou.lock.LockTemplate;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bsd.pjcloud.admin.api.feign.RemoteFileService;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
import com.bsd.pjcloud.common.data.tenant.TenantContextHolder;
|
||||
import com.bsd.pjcloud.hard.api.dto.manager.HardTerminalLogFileQueryDto;
|
||||
import com.bsd.pjcloud.hard.api.dto.manager.HardTerminalLogPreflightDto;
|
||||
import com.bsd.pjcloud.hard.api.dto.manager.HardTerminalLogUploadDto;
|
||||
import com.bsd.pjcloud.hard.api.entity.manager.HardManagerEntity;
|
||||
import com.bsd.pjcloud.hard.api.entity.manager.HardTerminalLogFileEntity;
|
||||
import com.bsd.pjcloud.hard.api.vo.manager.HardManagerVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.manager.HardTerminalLogDeviceVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.manager.HardTerminalLogFileTreeVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.manager.HardTerminalLogPreflightVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.manager.HardTerminalLogPreviewVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.manager.HardTerminalLogUploadResultVo;
|
||||
import com.bsd.pjcloud.hard.mapper.manager.HardManagerMapper;
|
||||
import com.bsd.pjcloud.hard.mapper.manager.HardTerminalLogFileMapper;
|
||||
import com.bsd.pjcloud.hard.service.HardManagerService;
|
||||
import com.bsd.pjcloud.hard.service.HardTerminalLogFileService;
|
||||
import com.bsd.pjcloud.mqtt.api.feign.RemoteMqttEventService;
|
||||
import com.bsd.pjcloud.mqtt.constant.MqttActionEnum;
|
||||
import com.bsd.pjcloud.mqtt.dto.MqttBaseDto;
|
||||
import feign.Response;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 终端日志文件服务。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class HardTerminalLogFileServiceImpl extends ServiceImpl<HardTerminalLogFileMapper, HardTerminalLogFileEntity> implements HardTerminalLogFileService {
|
||||
|
||||
private static final String TAG = "HardTerminalLogFileService";
|
||||
private static final int PREVIEW_MAX_BYTES = 1024 * 1024;
|
||||
private static final int PREVIEW_MAX_LINES = 5000;
|
||||
private static final long LOCK_EXPIRE_MILLIS = 180_000L;
|
||||
private static final long LOCK_WAIT_MILLIS = 5_000L;
|
||||
|
||||
private final HardManagerMapper hardManagerMapper;
|
||||
private final HardManagerService hardManagerService;
|
||||
private final RemoteFileService remoteFileService;
|
||||
private final RemoteMqttEventService remoteMqttEventService;
|
||||
private final LockTemplate lockTemplate;
|
||||
|
||||
@Override
|
||||
public HardTerminalLogPreflightVo preflight(Long hardId, HardTerminalLogPreflightDto dto) {
|
||||
requireTerminal(hardId);
|
||||
String relativePath = normalizeRelativePath(dto.getRelativePath());
|
||||
HardTerminalLogFileEntity existing = findByPath(hardId, relativePath);
|
||||
boolean unchanged = existing != null
|
||||
&& ObjectUtil.equals(existing.getFileSize(), dto.getFileSize())
|
||||
&& StrUtil.equalsIgnoreCase(existing.getFileHash(), dto.getFileHash());
|
||||
return HardTerminalLogPreflightVo.builder()
|
||||
.id(existing == null ? null : existing.getId())
|
||||
.needUpload(!unchanged)
|
||||
.normalizedPath(relativePath)
|
||||
.message(unchanged ? "日志未变化,无需重复上传" : "日志需要上传")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public HardTerminalLogUploadResultVo upload(Long hardId, HardTerminalLogUploadDto dto, MultipartFile file) {
|
||||
HardManagerEntity hard = requireTerminal(hardId);
|
||||
String relativePath = normalizeRelativePath(dto.getRelativePath());
|
||||
validateUploadFile(file);
|
||||
|
||||
String fileHash = calculateMd5(file);
|
||||
long fileSize = file.getSize();
|
||||
String lockKey = String.format("hard:terminal-log:%s:%s:%s", TenantContextHolder.getTenantId(), hardId,
|
||||
SecureUtil.md5(relativePath));
|
||||
LockInfo lock = lockTemplate.lock(lockKey, LOCK_EXPIRE_MILLIS, LOCK_WAIT_MILLIS);
|
||||
if (lock == null) {
|
||||
throw new RuntimeException("该日志正在上传,请稍后重试");
|
||||
}
|
||||
|
||||
try {
|
||||
HardTerminalLogFileEntity existing = findByPath(hardId, relativePath);
|
||||
if (existing != null && ObjectUtil.equals(existing.getFileSize(), fileSize)
|
||||
&& StrUtil.equalsIgnoreCase(existing.getFileHash(), fileHash)) {
|
||||
return uploadResult(existing, false, false, "日志未变化,无需重复上传");
|
||||
}
|
||||
|
||||
Map<String, Object> storage = uploadToFileCenter(file);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
boolean changed = existing != null;
|
||||
HardTerminalLogFileEntity entity = existing == null ? new HardTerminalLogFileEntity() : existing;
|
||||
fillTerminalSnapshot(entity, hard);
|
||||
entity.setLogDate(dto.getLogDate());
|
||||
entity.setRelativePath(relativePath);
|
||||
entity.setOriginalName(fileName(relativePath));
|
||||
entity.setFileSize(fileSize);
|
||||
entity.setFileHash(fileHash);
|
||||
entity.setFileModifiedTime(dto.getFileModifiedTime());
|
||||
entity.setSysFileId(numberValue(storage.get("id")));
|
||||
entity.setStorageFileName(stringValue(storage.get("fileName"), "文件中心未返回存储文件名"));
|
||||
entity.setStorageUrl(stringValue(storage.get("url"), "文件中心未返回文件地址"));
|
||||
entity.setAppVersion(dto.getAppVersion());
|
||||
entity.setSanitizeVersion(dto.getSanitizeVersion());
|
||||
entity.setUploadTime(now);
|
||||
entity.setUploadCount(existing == null ? 1 : Math.max(0, existing.getUploadCount() == null ? 0 : existing.getUploadCount()) + 1);
|
||||
entity.setTenantId(TenantContextHolder.getTenantId());
|
||||
if (existing == null) {
|
||||
save(entity);
|
||||
} else {
|
||||
updateById(entity);
|
||||
}
|
||||
return uploadResult(entity, true, changed, changed ? "日志已更新" : "日志上传成功");
|
||||
} finally {
|
||||
if (!lockTemplate.releaseLock(lock)) {
|
||||
log.warn("|{}|终端日志锁释放失败,lockKey:|{}|", TAG, lockKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<HardTerminalLogFileEntity> scopePage(Page<HardTerminalLogFileEntity> page, HardTerminalLogFileQueryDto dto) {
|
||||
return baseMapper.scopePage(page, dto);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<HardTerminalLogDeviceVo> devicePage(Page<HardTerminalLogDeviceVo> page, HardTerminalLogFileQueryDto dto) {
|
||||
HardTerminalLogFileQueryDto query = dto == null ? new HardTerminalLogFileQueryDto() : dto;
|
||||
List<HardTerminalLogDeviceVo> scopedGroups = baseMapper.deviceGroups(query, DataScope.of());
|
||||
Map<Long, HardTerminalLogDeviceVo> deviceMap = new LinkedHashMap<>();
|
||||
// SQL 先按部门和创建人分组,让数据权限拦截器过滤授权组;此处再合并同一设备,避免加载全部日志明细。
|
||||
for (HardTerminalLogDeviceVo group : scopedGroups) {
|
||||
HardTerminalLogDeviceVo device = deviceMap.get(group.getHardId());
|
||||
if (device == null) {
|
||||
group.setFileCount(group.getFileCount() == null ? 0L : group.getFileCount());
|
||||
deviceMap.put(group.getHardId(), group);
|
||||
continue;
|
||||
}
|
||||
device.setFileCount(device.getFileCount() + (group.getFileCount() == null ? 0L : group.getFileCount()));
|
||||
if (device.getLatestUploadTime() == null || group.getLatestUploadTime() != null
|
||||
&& group.getLatestUploadTime().isAfter(device.getLatestUploadTime())) {
|
||||
device.setHardName(group.getHardName());
|
||||
device.setUniqueCode(group.getUniqueCode());
|
||||
device.setHardType(group.getHardType());
|
||||
device.setMerchantId(group.getMerchantId());
|
||||
device.setDeptId(group.getDeptId());
|
||||
device.setLatestUploadTime(group.getLatestUploadTime());
|
||||
}
|
||||
}
|
||||
|
||||
List<HardTerminalLogDeviceVo> devices = new ArrayList<>(deviceMap.values());
|
||||
devices.sort(Comparator.comparing(HardTerminalLogDeviceVo::getLatestUploadTime,
|
||||
Comparator.nullsLast(Comparator.reverseOrder()))
|
||||
.thenComparing(HardTerminalLogDeviceVo::getHardId, Comparator.nullsLast(Comparator.reverseOrder())));
|
||||
page.setTotal(devices.size());
|
||||
long size = page.getSize();
|
||||
if (size <= 0) {
|
||||
page.setRecords(devices);
|
||||
return page;
|
||||
}
|
||||
long offset = Math.max(0, page.getCurrent() - 1) * size;
|
||||
int fromIndex = (int) Math.min(offset, devices.size());
|
||||
int toIndex = (int) Math.min(offset + size, devices.size());
|
||||
page.setRecords(devices.subList(fromIndex, toIndex));
|
||||
return page;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HardTerminalLogFileTreeVo> fileTree(Long hardId, HardTerminalLogFileQueryDto dto) {
|
||||
HardTerminalLogFileQueryDto query = dto == null ? new HardTerminalLogFileQueryDto() : dto;
|
||||
query.setHardId(hardId);
|
||||
List<HardTerminalLogFileEntity> files = baseMapper.scopeList(query);
|
||||
List<HardTerminalLogFileTreeVo> root = new ArrayList<>();
|
||||
for (HardTerminalLogFileEntity file : files) {
|
||||
appendTreeNode(root, file);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HardTerminalLogPreviewVo preview(Long id) {
|
||||
HardTerminalLogFileEntity file = requireScopedFile(id);
|
||||
try (Response response = remoteFileService.getFile(file.getStorageFileName())) {
|
||||
requireFileResponse(response);
|
||||
TailBytes tail = readTail(response.body().asInputStream(), PREVIEW_MAX_BYTES);
|
||||
String content = new String(tail.bytes(), StandardCharsets.UTF_8);
|
||||
if (tail.truncated()) {
|
||||
int lineBreak = content.indexOf('\n');
|
||||
content = lineBreak >= 0 ? content.substring(lineBreak + 1) : "";
|
||||
}
|
||||
List<String> lines = new ArrayList<>(Arrays.asList(content.split("\\R", -1)));
|
||||
boolean truncated = tail.truncated();
|
||||
if (lines.size() > PREVIEW_MAX_LINES) {
|
||||
lines = lines.subList(lines.size() - PREVIEW_MAX_LINES, lines.size());
|
||||
truncated = true;
|
||||
}
|
||||
if (!lines.isEmpty() && lines.get(lines.size() - 1).isEmpty()) {
|
||||
lines = lines.subList(0, lines.size() - 1);
|
||||
}
|
||||
return HardTerminalLogPreviewVo.builder()
|
||||
.id(file.getId())
|
||||
.originalName(file.getOriginalName())
|
||||
.relativePath(file.getRelativePath())
|
||||
.content(String.join("\n", lines))
|
||||
.lineCount(lines.size())
|
||||
.fileSize(file.getFileSize())
|
||||
.truncated(truncated)
|
||||
.build();
|
||||
} catch (IOException exception) {
|
||||
throw new RuntimeException("读取日志文件失败", exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(Long id, HttpServletResponse servletResponse) {
|
||||
HardTerminalLogFileEntity file = requireScopedFile(id);
|
||||
try (Response response = remoteFileService.getFile(file.getStorageFileName())) {
|
||||
requireFileResponse(response);
|
||||
servletResponse.setContentType("text/plain; charset=UTF-8");
|
||||
servletResponse.setHeader("Content-Disposition", "attachment; filename*=UTF-8''"
|
||||
+ URLEncoder.encode(file.getOriginalName(), StandardCharsets.UTF_8).replace("+", "%20"));
|
||||
if (file.getFileSize() != null) {
|
||||
servletResponse.setContentLengthLong(file.getFileSize());
|
||||
}
|
||||
IoUtil.copy(response.body().asInputStream(), servletResponse.getOutputStream());
|
||||
} catch (IOException exception) {
|
||||
throw new RuntimeException("下载日志文件失败", exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean pull(Long hardId) {
|
||||
HardManagerEntity scopedHard = requireScopedHard(hardId);
|
||||
HardManagerVo hard = hardManagerService.getByHardId(scopedHard.getId());
|
||||
if (hard == null || hard.getHardGroupId() == null || hard.getMerchantDeptId() == null) {
|
||||
throw new RuntimeException("设备消息主题配置不完整,无法下发日志拉取指令");
|
||||
}
|
||||
MqttBaseDto<?> dto = new MqttBaseDto<>();
|
||||
dto.setHardGroupId(hard.getHardGroupId());
|
||||
dto.setMerchantDeptId(hard.getMerchantDeptId());
|
||||
dto.setHardwareId(hard.getId());
|
||||
dto.setAction(MqttActionEnum.PULL_TERMINAL_LOG);
|
||||
R<Boolean> result = remoteMqttEventService.sendDefaultMsg(dto);
|
||||
if (result == null || !result.isOk() || !Boolean.TRUE.equals(result.getData())) {
|
||||
throw new RuntimeException(result == null || StrUtil.isBlank(result.getMsg()) ? "日志拉取指令下发失败" : result.getMsg());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private HardManagerEntity requireTerminal(Long hardId) {
|
||||
if (hardId == null) {
|
||||
throw new RuntimeException("DEVICE-ID不能为空");
|
||||
}
|
||||
HardManagerEntity hard = hardManagerMapper.selectById(hardId);
|
||||
if (hard == null) {
|
||||
throw new RuntimeException("当前租户下设备不存在");
|
||||
}
|
||||
return hard;
|
||||
}
|
||||
|
||||
private HardManagerEntity requireScopedHard(Long hardId) {
|
||||
List<HardManagerEntity> list = hardManagerMapper.selectListByScope(
|
||||
Wrappers.<HardManagerEntity>lambdaQuery().eq(HardManagerEntity::getId, hardId), DataScope.of());
|
||||
if (list.isEmpty()) {
|
||||
throw new RuntimeException("设备不存在或无数据权限");
|
||||
}
|
||||
return list.get(0);
|
||||
}
|
||||
|
||||
private HardTerminalLogFileEntity requireScopedFile(Long id) {
|
||||
List<HardTerminalLogFileEntity> files = baseMapper.selectListByScope(
|
||||
Wrappers.<HardTerminalLogFileEntity>lambdaQuery().eq(HardTerminalLogFileEntity::getId, id), DataScope.of());
|
||||
if (files.isEmpty()) {
|
||||
throw new RuntimeException("日志文件不存在或无数据权限");
|
||||
}
|
||||
return files.get(0);
|
||||
}
|
||||
|
||||
private HardTerminalLogFileEntity findByPath(Long hardId, String relativePath) {
|
||||
return getOne(Wrappers.<HardTerminalLogFileEntity>lambdaQuery()
|
||||
.eq(HardTerminalLogFileEntity::getHardId, hardId)
|
||||
.eq(HardTerminalLogFileEntity::getRelativePath, relativePath), false);
|
||||
}
|
||||
|
||||
private String normalizeRelativePath(String source) {
|
||||
String path = StrUtil.trim(source).replace('\\', '/');
|
||||
if (StrUtil.isBlank(path) || path.startsWith("/") || path.indexOf('\0') >= 0 || path.matches("^[A-Za-z]:.*")) {
|
||||
throw new RuntimeException("日志相对路径不合法");
|
||||
}
|
||||
List<String> segments = Arrays.stream(path.split("/"))
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.toList();
|
||||
if (segments.isEmpty() || segments.stream().anyMatch(segment -> ".".equals(segment) || "..".equals(segment))) {
|
||||
throw new RuntimeException("日志相对路径不合法");
|
||||
}
|
||||
String normalized = String.join("/", segments);
|
||||
if (normalized.length() > 255 || !normalized.toLowerCase().endsWith(".log") || fileName(normalized).length() > 128) {
|
||||
throw new RuntimeException("仅支持名称长度合规的.log日志文件");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private void validateUploadFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new RuntimeException("上传日志文件不能为空");
|
||||
}
|
||||
String originalName = file.getOriginalFilename();
|
||||
if (StrUtil.isBlank(originalName) || !originalName.toLowerCase().endsWith(".log")) {
|
||||
throw new RuntimeException("仅支持上传.log日志文件");
|
||||
}
|
||||
}
|
||||
|
||||
private String calculateMd5(MultipartFile file) {
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
return SecureUtil.md5(inputStream);
|
||||
} catch (IOException exception) {
|
||||
throw new RuntimeException("计算日志文件哈希失败", exception);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> uploadToFileCenter(MultipartFile file) {
|
||||
R<?> result = remoteFileService.innerUpload(file, "terminal-log", null, "terminal-log");
|
||||
if (result == null || !result.isOk() || !(result.getData() instanceof Map<?, ?> map)) {
|
||||
throw new RuntimeException(result == null || StrUtil.isBlank(result.getMsg()) ? "文件中心上传失败" : result.getMsg());
|
||||
}
|
||||
return (Map<String, Object>) map;
|
||||
}
|
||||
|
||||
private void fillTerminalSnapshot(HardTerminalLogFileEntity entity, HardManagerEntity hard) {
|
||||
entity.setHardId(hard.getId());
|
||||
entity.setHardName(hard.getName());
|
||||
entity.setUniqueCode(hard.getUniqueCode());
|
||||
entity.setHardType(hard.getType());
|
||||
entity.setMerchantId(hard.getMerchantId());
|
||||
entity.setDeptId(hard.getDeptId());
|
||||
}
|
||||
|
||||
private HardTerminalLogUploadResultVo uploadResult(HardTerminalLogFileEntity entity, boolean uploaded, boolean changed,
|
||||
String message) {
|
||||
return HardTerminalLogUploadResultVo.builder()
|
||||
.id(entity.getId())
|
||||
.uploaded(uploaded)
|
||||
.changed(changed)
|
||||
.relativePath(entity.getRelativePath())
|
||||
.uploadCount(entity.getUploadCount())
|
||||
.message(message)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void appendTreeNode(List<HardTerminalLogFileTreeVo> root, HardTerminalLogFileEntity file) {
|
||||
String[] segments = file.getRelativePath().split("/");
|
||||
List<HardTerminalLogFileTreeVo> level = root;
|
||||
StringBuilder currentPath = new StringBuilder();
|
||||
for (int index = 0; index < segments.length; index++) {
|
||||
String segment = segments[index];
|
||||
if (currentPath.length() > 0) {
|
||||
currentPath.append('/');
|
||||
}
|
||||
currentPath.append(segment);
|
||||
boolean leaf = index == segments.length - 1;
|
||||
String nodeKey = currentPath.toString();
|
||||
HardTerminalLogFileTreeVo node = level.stream()
|
||||
.filter(child -> StrUtil.equals(child.getLabel(), segment))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (node == null) {
|
||||
HardTerminalLogFileTreeVo value = new HardTerminalLogFileTreeVo();
|
||||
value.setKey(nodeKey);
|
||||
value.setLabel(segment);
|
||||
value.setRelativePath(nodeKey);
|
||||
value.setDirectory(!leaf);
|
||||
level.add(value);
|
||||
node = value;
|
||||
}
|
||||
if (leaf) {
|
||||
node.setFileId(file.getId());
|
||||
node.setFileSize(file.getFileSize());
|
||||
node.setLogDate(file.getLogDate());
|
||||
node.setUploadTime(file.getUploadTime());
|
||||
} else {
|
||||
level = node.getChildren();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void requireFileResponse(Response response) {
|
||||
if (response == null || response.status() < 200 || response.status() >= 300 || response.body() == null) {
|
||||
throw new RuntimeException("文件中心读取日志失败");
|
||||
}
|
||||
}
|
||||
|
||||
private TailBytes readTail(InputStream inputStream, int maxBytes) throws IOException {
|
||||
byte[] ring = new byte[maxBytes];
|
||||
byte[] buffer = new byte[8192];
|
||||
long total = 0;
|
||||
int read;
|
||||
while ((read = inputStream.read(buffer)) != -1) {
|
||||
for (int index = 0; index < read; index++) {
|
||||
ring[(int) (total % maxBytes)] = buffer[index];
|
||||
total++;
|
||||
}
|
||||
}
|
||||
int length = (int) Math.min(total, maxBytes);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream(length);
|
||||
long start = Math.max(0, total - maxBytes);
|
||||
for (long index = start; index < total; index++) {
|
||||
output.write(ring[(int) (index % maxBytes)]);
|
||||
}
|
||||
return new TailBytes(output.toByteArray(), total > maxBytes);
|
||||
}
|
||||
|
||||
private String fileName(String path) {
|
||||
return path.substring(path.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
private String stringValue(Object value, String message) {
|
||||
if (value == null || StrUtil.isBlank(value.toString())) {
|
||||
throw new RuntimeException(message);
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
private Long numberValue(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
if (value != null && StrUtil.isNotBlank(value.toString())) {
|
||||
return Long.valueOf(value.toString());
|
||||
}
|
||||
throw new RuntimeException("文件中心未返回文件记录ID");
|
||||
}
|
||||
|
||||
private record TailBytes(byte[] bytes, boolean truncated) {
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<?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="com.bsd.pjcloud.hard.mapper.manager.HardTerminalLogFileMapper">
|
||||
|
||||
<select id="deviceGroups" resultType="com.bsd.pjcloud.hard.api.vo.manager.HardTerminalLogDeviceVo">
|
||||
select hard_id,
|
||||
hard_name,
|
||||
unique_code,
|
||||
hard_type,
|
||||
merchant_id,
|
||||
dept_id,
|
||||
create_by,
|
||||
count(1) as file_count,
|
||||
max(upload_time) as latest_upload_time
|
||||
from hard_terminal_log_file
|
||||
<where>
|
||||
del_flag = '0'
|
||||
<if test="dto != null and dto.hardId != null">
|
||||
and hard_id = #{dto.hardId}
|
||||
</if>
|
||||
<if test="dto != null and dto.hardName != null and dto.hardName != ''">
|
||||
and hard_name like concat('%', #{dto.hardName}, '%')
|
||||
</if>
|
||||
<if test="dto != null and dto.uniqueCode != null and dto.uniqueCode != ''">
|
||||
and unique_code like concat('%', #{dto.uniqueCode}, '%')
|
||||
</if>
|
||||
<if test="dto != null and dto.merchantId != null">
|
||||
and merchant_id = #{dto.merchantId}
|
||||
</if>
|
||||
<if test="dto != null and dto.deptId != null">
|
||||
and dept_id = #{dto.deptId}
|
||||
</if>
|
||||
<if test="dto != null and dto.logDateStart != null">
|
||||
and log_date <![CDATA[ >= ]]> #{dto.logDateStart}
|
||||
</if>
|
||||
<if test="dto != null and dto.logDateEnd != null">
|
||||
and log_date <![CDATA[ <= ]]> #{dto.logDateEnd}
|
||||
</if>
|
||||
<if test="dto != null and dto.uploadTimeStart != null">
|
||||
and upload_time <![CDATA[ >= ]]> #{dto.uploadTimeStart}
|
||||
</if>
|
||||
<if test="dto != null and dto.uploadTimeEnd != null">
|
||||
and upload_time <![CDATA[ <= ]]> #{dto.uploadTimeEnd}
|
||||
</if>
|
||||
</where>
|
||||
group by hard_id, hard_name, unique_code, hard_type, merchant_id, dept_id, create_by
|
||||
order by latest_upload_time desc, hard_id desc
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
+1
-1
@@ -18,7 +18,7 @@ import java.util.List;
|
||||
public interface RemoteMqttEventService {
|
||||
|
||||
@NoToken
|
||||
@GetMapping("/mqtt/send-default-msg")
|
||||
@PostMapping("/mqtt/send-default-msg")
|
||||
R<Boolean> sendDefaultMsg(@RequestBody MqttBaseDto dto);
|
||||
|
||||
/**
|
||||
|
||||
+2
-1
@@ -18,6 +18,7 @@ public enum MqttActionEnum {
|
||||
PRINT("打印变化"),
|
||||
FACE("人脸变化"),
|
||||
TAKE_FOOD_BROADCAST("定餐广播"),
|
||||
PULL_TERMINAL_LOG("拉取终端当前日志"),
|
||||
;
|
||||
private final String description;
|
||||
}
|
||||
}
|
||||
+6
-3
@@ -109,14 +109,16 @@ public class SysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFile> impl
|
||||
@Override
|
||||
public R innerUpload(MultipartFile file, String dir, Long groupId, String type) {
|
||||
String fileName = IdUtil.simpleUUID() + StrUtil.DOT + FileUtil.extName(file.getOriginalFilename());
|
||||
Map<String, String> resultMap = new HashMap<>(4);
|
||||
Map<String, Object> resultMap = new HashMap<>(5);
|
||||
resultMap.put("bucketName", properties.getBucketName());
|
||||
resultMap.put("fileName", fileName);
|
||||
resultMap.put("url", String.format("/admin/sys-file/oss/file?fileName=%s", fileName));
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
fileTemplate.putObject(properties.getBucketName(), dir, fileName, inputStream, file.getContentType());
|
||||
// 文件管理数据记录,收集管理追踪文件
|
||||
innerFileLog(file, dir, fileName, groupId, type);
|
||||
SysFile sysFile = innerFileLog(file, dir, fileName, groupId, type);
|
||||
// 内部上传调用方需要文件中心主键执行后续替换、清理和保留周期管理。
|
||||
resultMap.put("id", sysFile.getId());
|
||||
} catch (Exception e) {
|
||||
return R.failed(e.getLocalizedMessage());
|
||||
}
|
||||
@@ -283,7 +285,7 @@ public class SysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFile> impl
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private void innerFileLog(MultipartFile file, String dir, String fileName, Long groupId, String type) {
|
||||
private SysFile innerFileLog(MultipartFile file, String dir, String fileName, Long groupId, String type) {
|
||||
// 创建SysFile对象并设置相关属性
|
||||
SysFile sysFile = new SysFile();
|
||||
sysFile.setFileName(fileName);
|
||||
@@ -304,6 +306,7 @@ public class SysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFile> impl
|
||||
sysFile.setUpdateBy("SYSTEM");
|
||||
// 调用save方法保存SysFile对象
|
||||
this.save(sysFile);
|
||||
return sysFile;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user