1
This commit is contained in:
@@ -43,6 +43,9 @@ public class TencentDocConfigController extends BaseController {
|
||||
// Redis key前缀(用于存储文档配置)
|
||||
private static final String REDIS_KEY_PREFIX = "tencent:doc:auto:config:";
|
||||
|
||||
/** Redis key前缀,用于存储上次处理的最大行数(与 TencentDocController 一致) */
|
||||
private static final String LAST_PROCESSED_ROW_KEY_PREFIX = "tendoc:last_row:";
|
||||
|
||||
/**
|
||||
* 获取当前配置
|
||||
* 注意:accessToken 由系统自动管理(通过授权登录),此接口只返回状态
|
||||
@@ -98,7 +101,7 @@ public class TencentDocConfigController extends BaseController {
|
||||
// 获取当前同步进度(如果有配置)
|
||||
// 注意:使用与 TencentDocController 相同的 Redis key 前缀
|
||||
if (fileId != null && !fileId.isEmpty() && sheetId != null && !sheetId.isEmpty()) {
|
||||
String syncProgressKey = "tendoc:last_row:" + fileId + ":" + sheetId;
|
||||
String syncProgressKey = LAST_PROCESSED_ROW_KEY_PREFIX + fileId + ":" + sheetId;
|
||||
Integer currentProgress = redisCache.getCacheObject(syncProgressKey);
|
||||
log.debug("读取同步进度 - key: {}, value: {}", syncProgressKey, currentProgress);
|
||||
if (currentProgress != null) {
|
||||
@@ -211,7 +214,7 @@ public class TencentDocConfigController extends BaseController {
|
||||
|
||||
// 清除该文档的同步进度(配置更新时重置进度,从新的startRow重新开始)
|
||||
// 注意:使用与 TencentDocController 相同的 Redis key 前缀
|
||||
String syncProgressKey = "tendoc:last_row:" + fileId.trim() + ":" + sheetId.trim();
|
||||
String syncProgressKey = LAST_PROCESSED_ROW_KEY_PREFIX + fileId.trim() + ":" + sheetId.trim();
|
||||
String configVersionKey = "tencent:doc:sync:config_version:" + fileId.trim() + ":" + sheetId.trim();
|
||||
redisCache.deleteObject(syncProgressKey);
|
||||
redisCache.deleteObject(configVersionKey);
|
||||
|
||||
@@ -1150,30 +1150,74 @@ public class TencentDocController extends BaseController {
|
||||
log.info("列位置识别完成 - 单号: {}, 物流单号: {}, 备注: {}, 是否安排: {}, 标记: {}, 下单电话: {}",
|
||||
orderNoColumn, logisticsLinkColumn, remarkColumn, arrangedColumn, markColumn, phoneColumn);
|
||||
|
||||
// 读取数据行
|
||||
// 使用 A1 表示法(Excel格式)
|
||||
String range = String.format("A%d:Z%d", startRow, endRow); // 例如:A3:Z203
|
||||
log.info("开始读取数据行 - 行号: {} ~ {}, range: {}", startRow, endRow, range);
|
||||
|
||||
// 读取数据行(空数据行可能导致接口异常,对 endRow 尝试 -1、-10 重试以保证业务执行成功)
|
||||
int effectiveEndRow = endRow;
|
||||
JSONObject sheetData = null;
|
||||
int[] retryDecrements = new int[] { 0, 1, 10 };
|
||||
for (int decrement : retryDecrements) {
|
||||
int tryEndRow = Math.max(startRow, endRow - decrement);
|
||||
if (tryEndRow < startRow) {
|
||||
continue;
|
||||
}
|
||||
String range = String.format("A%d:Z%d", startRow, tryEndRow);
|
||||
log.info("开始读取数据行 - 行号: {} ~ {}, range: {} (尝试 decrement={})", startRow, tryEndRow, range, decrement);
|
||||
try {
|
||||
sheetData = tencentDocService.readSheetData(accessToken, fileId, sheetId, range);
|
||||
log.info("数据行读取成功,响应: {}", sheetData != null ? "有数据" : "null");
|
||||
if (sheetData != null) {
|
||||
effectiveEndRow = tryEndRow;
|
||||
log.info("数据行读取成功,响应: 有数据 (使用 endRow={})", effectiveEndRow);
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("读取数据行失败", e);
|
||||
log.warn("读取数据行失败 (endRow={}, decrement={}),将重试更小范围: {}", tryEndRow, decrement, e.getMessage());
|
||||
if (decrement == retryDecrements[retryDecrements.length - 1]) {
|
||||
log.error("读取数据行多次重试后仍失败", e);
|
||||
return AjaxResult.error("读取数据行失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sheetData == null) {
|
||||
return AjaxResult.error("读取数据行返回null");
|
||||
}
|
||||
|
||||
JSONArray values = sheetData.getJSONArray("values");
|
||||
JSONArray values = null;
|
||||
try {
|
||||
values = sheetData.getJSONArray("values");
|
||||
} catch (Exception e) {
|
||||
log.warn("解析 values 异常,尝试将 endRow 缩小后重新读取: {}", e.getMessage());
|
||||
for (int decrement : new int[] { 1, 10 }) {
|
||||
int tryEndRow = Math.max(startRow, effectiveEndRow - decrement);
|
||||
if (tryEndRow >= startRow) {
|
||||
String retryRange = String.format("A%d:Z%d", startRow, tryEndRow);
|
||||
try {
|
||||
sheetData = tencentDocService.readSheetData(accessToken, fileId, sheetId, retryRange);
|
||||
if (sheetData != null) {
|
||||
values = sheetData.getJSONArray("values");
|
||||
effectiveEndRow = tryEndRow;
|
||||
break;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.warn("重新读取 (endRow={}) 仍失败: {}", tryEndRow, ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (values == null && sheetData != null) {
|
||||
try {
|
||||
values = sheetData.getJSONArray("values");
|
||||
} catch (Exception e) {
|
||||
log.error("解析 values 失败", e);
|
||||
return AjaxResult.error("解析数据行失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
endRow = effectiveEndRow; // 后续统一使用实际读取成功的 endRow
|
||||
String range = String.format("A%d:Z%d", startRow, endRow);
|
||||
log.info("解析后的数据行数: {}", values != null ? values.size() : "null");
|
||||
|
||||
if (values == null || values.isEmpty()) {
|
||||
log.warn("指定范围内没有数据,可能已处理完毕。range={}, sheetData keys={}",
|
||||
range, sheetData.keySet());
|
||||
range, sheetData != null ? sheetData.keySet() : null);
|
||||
|
||||
// 打印前10个键值对用于调试
|
||||
if (sheetData != null && !sheetData.isEmpty()) {
|
||||
@@ -1215,7 +1259,18 @@ public class TencentDocController extends BaseController {
|
||||
JSONArray updates = new JSONArray(); // 存储需要更新的行和值
|
||||
|
||||
for (int i = 0; i < values.size(); i++) {
|
||||
JSONArray row = values.getJSONArray(i);
|
||||
JSONArray row;
|
||||
try {
|
||||
Object rowObj = values.get(i);
|
||||
if (rowObj == null || !(rowObj instanceof JSONArray)) {
|
||||
continue; // 非数组(空数据行等)跳过
|
||||
}
|
||||
row = (JSONArray) rowObj;
|
||||
} catch (Exception e) {
|
||||
log.warn("解析第 {} 行数据异常,跳过(index={}): {}", startRow + i, i, e.getMessage());
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
if (row == null || row.size() <= Math.max(orderNoColumn, logisticsLinkColumn)) {
|
||||
continue; // 跳过空行或列数不足的行
|
||||
}
|
||||
@@ -1223,8 +1278,15 @@ public class TencentDocController extends BaseController {
|
||||
// 计算实际的行号(Excel行号,从1开始)
|
||||
int excelRow = startRow + i;
|
||||
|
||||
// 获取单号
|
||||
String orderNo = row.getString(orderNoColumn);
|
||||
// 获取单号(空数据行可能导致 getString 异常,单行 try 保证整批继续执行)
|
||||
String orderNo;
|
||||
try {
|
||||
orderNo = row.getString(orderNoColumn);
|
||||
} catch (Exception e) {
|
||||
log.warn("读取单号列异常,跳过行 {}: {}", excelRow, e.getMessage());
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
if (orderNo == null || orderNo.trim().isEmpty()) {
|
||||
skippedCount++;
|
||||
continue; // 跳过空单号的行
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.ruoyi.web.controller.monitor;
|
||||
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 日志文件查看(HTTPS 轮询读取最新内容,无需 WebSocket)
|
||||
*
|
||||
* @author system
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/monitor/logfile")
|
||||
public class LogfileController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LogfileController.class);
|
||||
|
||||
/** 允许读取的日志文件名(不含路径),与 logback 及实际日志目录一致 */
|
||||
private static final List<String> ALLOWED_FILES = Collections.unmodifiableList(
|
||||
Arrays.asList("all.log", "sys-info.log", "sys-error.log", "sys-user.log"));
|
||||
|
||||
/** 默认读取行数 */
|
||||
private static final int DEFAULT_LINES = 500;
|
||||
|
||||
/** 最大读取行数 */
|
||||
private static final int MAX_LINES = 5000;
|
||||
|
||||
@Value("${ruoyi.logPath:/home/van/project/ruoyi-java/logs}")
|
||||
private String logPath;
|
||||
|
||||
/**
|
||||
* 获取可选的日志文件列表
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('monitor:server:list')")
|
||||
@GetMapping("/list")
|
||||
public AjaxResult list() {
|
||||
List<String> files = new ArrayList<>(ALLOWED_FILES);
|
||||
return AjaxResult.success(files);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取日志文件末尾 N 行(tail 语义),通过 HTTPS 返回最新内容
|
||||
*
|
||||
* @param file 文件名,如 sys-info.log(必须在白名单内)
|
||||
* @param lines 行数,默认 500,最大 5000
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('monitor:server:list')")
|
||||
@GetMapping("/tail")
|
||||
public AjaxResult tail(
|
||||
@RequestParam(value = "file", required = true) String file,
|
||||
@RequestParam(value = "lines", required = false) Integer lines) {
|
||||
if (file == null || file.trim().isEmpty()) {
|
||||
return AjaxResult.error("参数 file 不能为空");
|
||||
}
|
||||
// 只允许白名单内的文件名,防止路径穿越
|
||||
String fileName = file.trim();
|
||||
if (!ALLOWED_FILES.contains(fileName)) {
|
||||
log.warn("拒绝非法日志文件请求: {}", fileName);
|
||||
return AjaxResult.error("不允许读取该文件");
|
||||
}
|
||||
int lineCount = lines != null && lines > 0 ? Math.min(lines, MAX_LINES) : DEFAULT_LINES;
|
||||
|
||||
Path baseDir = Paths.get(logPath).normalize().toAbsolutePath();
|
||||
Path target = baseDir.resolve(fileName).normalize();
|
||||
if (!target.startsWith(baseDir)) {
|
||||
log.warn("路径穿越被拒绝: {}", target);
|
||||
return AjaxResult.error("非法路径");
|
||||
}
|
||||
if (!Files.isRegularFile(target)) {
|
||||
return AjaxResult.error("文件不存在或不可读: " + fileName);
|
||||
}
|
||||
|
||||
try {
|
||||
List<String> allLines = Files.readAllLines(target, StandardCharsets.UTF_8);
|
||||
int total = allLines.size();
|
||||
int from = Math.max(0, total - lineCount);
|
||||
String content = allLines.subList(from, total).stream()
|
||||
.collect(Collectors.joining("\n"));
|
||||
return AjaxResult.success(new TailResult(fileName, content, total, from + 1, total));
|
||||
} catch (IOException e) {
|
||||
log.error("读取日志文件失败: {}", target, e);
|
||||
return AjaxResult.error("读取失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回体中的 data 结构 */
|
||||
public static class TailResult {
|
||||
private final String file;
|
||||
private final String content;
|
||||
private final int totalLines;
|
||||
private final int fromLine;
|
||||
private final int toLine;
|
||||
|
||||
public TailResult(String file, String content, int totalLines, int fromLine, int toLine) {
|
||||
this.file = file;
|
||||
this.content = content;
|
||||
this.totalLines = totalLines;
|
||||
this.fromLine = fromLine;
|
||||
this.toLine = toLine;
|
||||
}
|
||||
|
||||
public String getFile() { return file; }
|
||||
public String getContent() { return content; }
|
||||
public int getTotalLines() { return totalLines; }
|
||||
public int getFromLine() { return fromLine; }
|
||||
public int getToLine() { return toLine; }
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ ruoyi:
|
||||
copyrightYear: 2025
|
||||
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
|
||||
profile: D:/ruoyi-java/uploadPath
|
||||
# 日志目录(与 logback 中 log.path 一致,用于前端「日志查看」读取文件)
|
||||
logPath: D:/ruoyi-java/logs
|
||||
# 获取ip地址开关
|
||||
addressEnabled: false
|
||||
# 验证码类型 math 数字计算 char 字符验证
|
||||
|
||||
@@ -8,6 +8,8 @@ ruoyi:
|
||||
copyrightYear: 2025
|
||||
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
|
||||
profile: /home/van/project/ruoyi-java/uploadPath
|
||||
# 日志目录(与 logback 中 log.path 一致,用于前端「日志查看」读取文件)
|
||||
logPath: /home/van/project/ruoyi-java/logs
|
||||
# 获取ip地址开关
|
||||
addressEnabled: false
|
||||
# 验证码类型 math 数字计算 char 字符验证
|
||||
|
||||
3
sql/menu_logfile.sql
Normal file
3
sql/menu_logfile.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- 日志文件查看菜单(与「服务监控」同级,使用 monitor:server:list 权限)
|
||||
-- 执行后可在「系统监控」下看到「日志文件」菜单,通过 HTTPS 轮询查看最新日志,无需 SSH
|
||||
insert into sys_menu values('118', '日志文件', '2', '7', 'logfile', 'monitor/logfile/index', '', '', 1, 0, 'C', '0', '0', 'monitor:server:list', 'documentation', 'admin', sysdate(), '', null, '日志文件查看菜单');
|
||||
Reference in New Issue
Block a user