diff --git a/db/20260729_terminal_log_menu_permissions.sql b/db/20260729_terminal_log_menu_permissions.sql new file mode 100644 index 000000000..cafa5fa22 --- /dev/null +++ b/db/20260729_terminal_log_menu_permissions.sql @@ -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' +); diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/manager/HardTerminalLogFileQueryDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/manager/HardTerminalLogFileQueryDto.java new file mode 100644 index 000000000..fd347365c --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/manager/HardTerminalLogFileQueryDto.java @@ -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; +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/manager/HardTerminalLogPreflightDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/manager/HardTerminalLogPreflightDto.java new file mode 100644 index 000000000..a2c6ecaf8 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/manager/HardTerminalLogPreflightDto.java @@ -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; +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/manager/HardTerminalLogUploadDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/manager/HardTerminalLogUploadDto.java new file mode 100644 index 000000000..8d8868cf1 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/manager/HardTerminalLogUploadDto.java @@ -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; +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/manager/HardTerminalLogFileEntity.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/manager/HardTerminalLogFileEntity.java new file mode 100755 index 000000000..859371fcf --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/manager/HardTerminalLogFileEntity.java @@ -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 { + + /** + * 编号 + */ + @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; +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardTerminalLogDeviceVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardTerminalLogDeviceVo.java new file mode 100644 index 000000000..e56f60518 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardTerminalLogDeviceVo.java @@ -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; +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardTerminalLogFileTreeVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardTerminalLogFileTreeVo.java new file mode 100644 index 000000000..7869baa73 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardTerminalLogFileTreeVo.java @@ -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 children = new ArrayList<>(); +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardTerminalLogPreflightVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardTerminalLogPreflightVo.java new file mode 100644 index 000000000..b13456e4f --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardTerminalLogPreflightVo.java @@ -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; +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardTerminalLogPreviewVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardTerminalLogPreviewVo.java new file mode 100644 index 000000000..563fc4e80 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardTerminalLogPreviewVo.java @@ -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; +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardTerminalLogUploadResultVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardTerminalLogUploadResultVo.java new file mode 100644 index 000000000..5b7dde157 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardTerminalLogUploadResultVo.java @@ -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; +} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardTerminalLogFileController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardTerminalLogFileController.java new file mode 100755 index 000000000..87dc4748b --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardTerminalLogFileController.java @@ -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 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 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(@ParameterObject Page page, + @ParameterObject HardTerminalLogFileQueryDto dto) { + return R.ok(hardTerminalLogFileService.scopePage(page, dto)); + } + + @Operation(summary = "按设备聚合终端日志") + @GetMapping("/device-page") + @PreAuthorize("@pms.hasPermission('hard_hardTerminalLogFile_view')") + public R> devicePage(@ParameterObject Page page, + @ParameterObject HardTerminalLogFileQueryDto dto) { + return R.ok(hardTerminalLogFileService.devicePage(page, dto)); + } + + @Operation(summary = "查询指定设备的日志文件树") + @GetMapping("/file-tree") + @PreAuthorize("@pms.hasPermission('hard_hardTerminalLogFile_view')") + public R> 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 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 pull(@PathVariable Long hardId) { + return R.ok(hardTerminalLogFileService.pull(hardId)); + } +} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/manager/HardTerminalLogFileMapper.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/manager/HardTerminalLogFileMapper.java new file mode 100755 index 000000000..152bb2995 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/manager/HardTerminalLogFileMapper.java @@ -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 { + + default LambdaQueryWrapper queryWrapper(HardTerminalLogFileQueryDto dto) { + HardTerminalLogFileQueryDto query = dto == null ? new HardTerminalLogFileQueryDto() : dto; + return new LambdaQueryWrapper() + .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 scopePage(Page page, HardTerminalLogFileQueryDto dto) { + return selectPageByScope(page, queryWrapper(dto), DataScope.of()); + } + + default List scopeList(HardTerminalLogFileQueryDto dto) { + return selectListByScope(queryWrapper(dto), DataScope.of()); + } + + List deviceGroups(@Param("dto") HardTerminalLogFileQueryDto dto, + @Param("dataScope") DataScope dataScope); +} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardTerminalLogFileService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardTerminalLogFileService.java new file mode 100755 index 000000000..7c6124680 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardTerminalLogFileService.java @@ -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 { + + HardTerminalLogPreflightVo preflight(Long hardId, HardTerminalLogPreflightDto dto); + + HardTerminalLogUploadResultVo upload(Long hardId, HardTerminalLogUploadDto dto, MultipartFile file); + + Page scopePage(Page page, HardTerminalLogFileQueryDto dto); + + Page devicePage(Page page, HardTerminalLogFileQueryDto dto); + + List fileTree(Long hardId, HardTerminalLogFileQueryDto dto); + + HardTerminalLogPreviewVo preview(Long id); + + void download(Long id, HttpServletResponse response); + + Boolean pull(Long hardId); +} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/manager/HardTerminalLogFileServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/manager/HardTerminalLogFileServiceImpl.java new file mode 100755 index 000000000..1153719ca --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/manager/HardTerminalLogFileServiceImpl.java @@ -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 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 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 scopePage(Page page, HardTerminalLogFileQueryDto dto) { + return baseMapper.scopePage(page, dto); + } + + @Override + public Page devicePage(Page page, HardTerminalLogFileQueryDto dto) { + HardTerminalLogFileQueryDto query = dto == null ? new HardTerminalLogFileQueryDto() : dto; + List scopedGroups = baseMapper.deviceGroups(query, DataScope.of()); + Map 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 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 fileTree(Long hardId, HardTerminalLogFileQueryDto dto) { + HardTerminalLogFileQueryDto query = dto == null ? new HardTerminalLogFileQueryDto() : dto; + query.setHardId(hardId); + List files = baseMapper.scopeList(query); + List 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 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 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 list = hardManagerMapper.selectListByScope( + Wrappers.lambdaQuery().eq(HardManagerEntity::getId, hardId), DataScope.of()); + if (list.isEmpty()) { + throw new RuntimeException("设备不存在或无数据权限"); + } + return list.get(0); + } + + private HardTerminalLogFileEntity requireScopedFile(Long id) { + List files = baseMapper.selectListByScope( + Wrappers.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.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 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 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) 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 root, HardTerminalLogFileEntity file) { + String[] segments = file.getRelativePath().split("/"); + List 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) { + } +} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/manager/HardTerminalLogFileMapper.xml b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/manager/HardTerminalLogFileMapper.xml new file mode 100755 index 000000000..179d93d9b --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/manager/HardTerminalLogFileMapper.xml @@ -0,0 +1,51 @@ + + + + + + + + diff --git a/pjcloud-mqtt/pjcloud-mqtt-api/src/main/java/com/bsd/pjcloud/mqtt/api/feign/RemoteMqttEventService.java b/pjcloud-mqtt/pjcloud-mqtt-api/src/main/java/com/bsd/pjcloud/mqtt/api/feign/RemoteMqttEventService.java index 52ec87606..3438b0af5 100644 --- a/pjcloud-mqtt/pjcloud-mqtt-api/src/main/java/com/bsd/pjcloud/mqtt/api/feign/RemoteMqttEventService.java +++ b/pjcloud-mqtt/pjcloud-mqtt-api/src/main/java/com/bsd/pjcloud/mqtt/api/feign/RemoteMqttEventService.java @@ -18,7 +18,7 @@ import java.util.List; public interface RemoteMqttEventService { @NoToken - @GetMapping("/mqtt/send-default-msg") + @PostMapping("/mqtt/send-default-msg") R sendDefaultMsg(@RequestBody MqttBaseDto dto); /** diff --git a/pjcloud-mqtt/pjcloud-mqtt-api/src/main/java/com/bsd/pjcloud/mqtt/constant/MqttActionEnum.java b/pjcloud-mqtt/pjcloud-mqtt-api/src/main/java/com/bsd/pjcloud/mqtt/constant/MqttActionEnum.java index 3c9af1d98..e93d37aa6 100644 --- a/pjcloud-mqtt/pjcloud-mqtt-api/src/main/java/com/bsd/pjcloud/mqtt/constant/MqttActionEnum.java +++ b/pjcloud-mqtt/pjcloud-mqtt-api/src/main/java/com/bsd/pjcloud/mqtt/constant/MqttActionEnum.java @@ -18,6 +18,7 @@ public enum MqttActionEnum { PRINT("打印变化"), FACE("人脸变化"), TAKE_FOOD_BROADCAST("定餐广播"), + PULL_TERMINAL_LOG("拉取终端当前日志"), ; private final String description; -} \ No newline at end of file +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysFileServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysFileServiceImpl.java index cc023e513..97d7b1405 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysFileServiceImpl.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysFileServiceImpl.java @@ -109,14 +109,16 @@ public class SysFileServiceImpl extends ServiceImpl impl @Override public R innerUpload(MultipartFile file, String dir, Long groupId, String type) { String fileName = IdUtil.simpleUUID() + StrUtil.DOT + FileUtil.extName(file.getOriginalFilename()); - Map resultMap = new HashMap<>(4); + Map 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 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 impl sysFile.setUpdateBy("SYSTEM"); // 调用save方法保存SysFile对象 this.save(sysFile); + return sysFile; } }