This commit is contained in:
Leo
2026-01-30 19:43:11 +08:00
parent 72ff30567b
commit 2b34ba6cdb
6 changed files with 215 additions and 18 deletions

View File

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

View File

@@ -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; // 跳过空单号的行

View File

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

View File

@@ -8,6 +8,8 @@ ruoyi:
copyrightYear: 2025
# 文件路径 示例( Windows配置D:/ruoyi/uploadPathLinux配置 /home/ruoyi/uploadPath
profile: D:/ruoyi-java/uploadPath
# 日志目录(与 logback 中 log.path 一致,用于前端「日志查看」读取文件)
logPath: D:/ruoyi-java/logs
# 获取ip地址开关
addressEnabled: false
# 验证码类型 math 数字计算 char 字符验证

View File

@@ -8,6 +8,8 @@ ruoyi:
copyrightYear: 2025
# 文件路径 示例( Windows配置D:/ruoyi/uploadPathLinux配置 /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
View 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, '日志文件查看菜单');