diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/TencentDocController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/TencentDocController.java index 5dfce13..5a1e5f3 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/TencentDocController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/TencentDocController.java @@ -57,8 +57,12 @@ public class TencentDocController extends BaseController { /** Redis key前缀,用于存储上次处理的最大行数 */ private static final String LAST_PROCESSED_ROW_KEY_PREFIX = "tendoc:last_row:"; - /** 单次请求最大行数(腾讯文档 API:行数≤1000,一次拉 500 行) */ + /** 单次请求最大行数(腾讯文档 API:行数≤1000) */ private static final int API_MAX_ROWS_PER_REQUEST = 500; + /** 用 rowTotal 时接口实际单次只能读 200 行 */ + private static final int READ_ROWS_WHEN_USE_ROW_TOTAL = 200; + /** 用 rowTotal 时 startRow = rowTotal - 200 必须大于此值(保证至少为 3,避免表头) */ + private static final int MIN_START_ROW_WHEN_USE_ROW_TOTAL = 3; /** 数据区 range 列尾(30 列 = A 到 AD,API 列数≤200) */ private static final String DATA_RANGE_COL_END = "AD"; /** 回溯行数:下次起始行 = 本次扫描终点 + 1 - 回溯,用于覆盖物流变更或延后补填 */ @@ -1010,40 +1014,49 @@ public class TencentDocController extends BaseController { } } - // 计算本次同步的起始行和结束行(完美推送回溯) - // 1. 单次请求不超过 API_MAX_ROWS_PER_REQUEST,避免 range invalid - // 2. 下次起始 = 本次扫描终点 + 1 - BACKTRACK_ROWS,兼顾物流变更与补填 - // 3. 至少前进行数 MIN_ADVANCE_ROWS,避免文档中间大量空行时原地打转 int effectiveStartRow = configStartRow != null ? configStartRow : (headerRow + 1); - int startRow; int endRow; - if (forceStartRow != null) { - startRow = forceStartRow; - endRow = startRow + API_MAX_ROWS_PER_REQUEST - 1; - log.info("使用强制指定的起始行: {}, 结束行: {}(单批最多 {} 行)", startRow, endRow, API_MAX_ROWS_PER_REQUEST); - } else if (lastMaxRow != null && lastMaxRow >= effectiveStartRow) { - // 带回溯的起始:从 (上次最大行 + 1 - 回溯行数) 开始,保证覆盖物流变更与延后补填 - startRow = Math.max(effectiveStartRow, lastMaxRow + 1 - BACKTRACK_ROWS); - endRow = startRow + API_MAX_ROWS_PER_REQUEST - 1; - log.info("上次最大行: {},回溯 {} 行后从第 {} 行开始,到第 {} 行(本批最多 {} 行)", - lastMaxRow, BACKTRACK_ROWS, startRow, endRow, API_MAX_ROWS_PER_REQUEST); + // 直接拉取文档信息得到 rowTotal,用「最大行 - 200」作为起始(接口实际只能读 200 行),且保证 -200 之后 > 2 + int rowTotal = tencentDocService.getSheetRowTotal(accessToken, fileId, sheetId); + if (rowTotal > 0) { + if (forceStartRow != null) { + startRow = forceStartRow; + endRow = Math.min(rowTotal, startRow + READ_ROWS_WHEN_USE_ROW_TOTAL - 1); + } else { + // startRow = rowTotal - 200,且必须大于 2(>= MIN_START_ROW_WHEN_USE_ROW_TOTAL) + startRow = Math.max(MIN_START_ROW_WHEN_USE_ROW_TOTAL, + Math.max(effectiveStartRow, rowTotal - READ_ROWS_WHEN_USE_ROW_TOTAL)); + endRow = Math.min(rowTotal, startRow + READ_ROWS_WHEN_USE_ROW_TOTAL - 1); + } + log.info("使用文档 rowTotal={},本次范围: 第 {} ~ {} 行(共 {} 行,单次最多 {} 行)", + rowTotal, startRow, endRow, endRow - startRow + 1, READ_ROWS_WHEN_USE_ROW_TOTAL); } else { - startRow = effectiveStartRow; - endRow = startRow + API_MAX_ROWS_PER_REQUEST - 1; - log.info("首次同步或配置已重置,从第 {} 行开始,到第 {} 行(单批最多 {} 行)", startRow, endRow, API_MAX_ROWS_PER_REQUEST); + // 未取到 rowTotal 时退回原逻辑 + if (forceStartRow != null) { + startRow = forceStartRow; + endRow = startRow + API_MAX_ROWS_PER_REQUEST - 1; + log.info("使用强制指定的起始行: {}, 结束行: {}(未取到 rowTotal)", startRow, endRow); + } else if (lastMaxRow != null && lastMaxRow >= effectiveStartRow) { + startRow = Math.max(effectiveStartRow, lastMaxRow + 1 - BACKTRACK_ROWS); + endRow = startRow + API_MAX_ROWS_PER_REQUEST - 1; + log.info("上次最大行: {},回溯后从第 {} 行到第 {} 行(未取到 rowTotal)", lastMaxRow, startRow, endRow); + } else { + startRow = effectiveStartRow; + endRow = startRow + API_MAX_ROWS_PER_REQUEST - 1; + log.info("首次同步,从第 {} 行到第 {} 行(未取到 rowTotal)", startRow, endRow); + } } - // 严格限制单次 range 行数,防止 API 报 range invalid - int rowCount = endRow - startRow + 1; - if (rowCount > API_MAX_ROWS_PER_REQUEST) { - endRow = startRow + API_MAX_ROWS_PER_REQUEST - 1; - log.info("已截断结束行以符合 API 限制: endRow={}, 行数={}", endRow, API_MAX_ROWS_PER_REQUEST); + // 严格限制单次 range 行数(接口实际只能读 200 行) + if (endRow - startRow + 1 > READ_ROWS_WHEN_USE_ROW_TOTAL) { + endRow = startRow + READ_ROWS_WHEN_USE_ROW_TOTAL - 1; + log.info("已截断结束行以符合 API 限制: endRow={}, 行数={}", endRow, READ_ROWS_WHEN_USE_ROW_TOTAL); } - log.info("开始填充物流链接 - 文件ID: {}, 工作表ID: {}, 起始行: {}, 结束行: {}, 上次最大行: {}", - fileId, sheetId, startRow, endRow, lastMaxRow); + log.info("开始填充物流链接 - 文件ID: {}, 工作表ID: {}, 起始行: {}, 结束行: {}, rowTotal: {}", + fileId, sheetId, startRow, endRow, rowTotal > 0 ? rowTotal : "未获取"); // 读取表格数据(先读取表头行用于识别列位置) // 根据官方文档,使用 A1 表示法(Excel格式) @@ -1156,16 +1169,16 @@ public class TencentDocController extends BaseController { log.info("列位置识别完成 - 单号: {}, 物流单号: {}, 备注: {}, 是否安排: {}, 标记: {}, 下单电话: {}", orderNoColumn, logisticsLinkColumn, remarkColumn, arrangedColumn, markColumn, phoneColumn); - // 读取数据行:严格限制单次行数(避免 range invalid),失败时逐步缩小范围重试 - int effectiveEndRow = Math.min(endRow, startRow + API_MAX_ROWS_PER_REQUEST - 1); + // 读取数据行:接口实际只能读 200 行,严格限制单次行数,失败时逐步缩小范围重试 + int effectiveEndRow = Math.min(endRow, startRow + READ_ROWS_WHEN_USE_ROW_TOTAL - 1); JSONObject sheetData = null; int[] retryDecrements = new int[] { 0, 20, 50, 100 }; for (int decrement : retryDecrements) { int tryEndRow = Math.max(startRow, effectiveEndRow - decrement); int tryRowCount = tryEndRow - startRow + 1; - if (tryRowCount > API_MAX_ROWS_PER_REQUEST) { - tryEndRow = startRow + API_MAX_ROWS_PER_REQUEST - 1; - tryRowCount = API_MAX_ROWS_PER_REQUEST; + if (tryRowCount > READ_ROWS_WHEN_USE_ROW_TOTAL) { + tryEndRow = startRow + READ_ROWS_WHEN_USE_ROW_TOTAL - 1; + tryRowCount = READ_ROWS_WHEN_USE_ROW_TOTAL; } if (tryEndRow < startRow) { continue; diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/ITencentDocService.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/ITencentDocService.java index 0b528ef..bfa65c2 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/ITencentDocService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/ITencentDocService.java @@ -97,6 +97,16 @@ public interface ITencentDocService { * @return 工作表列表 */ JSONObject getSheetList(String accessToken, String fileId); + + /** + * 获取指定工作表的 rowTotal(文档信息接口返回,直接用于 range 上限) + * + * @param accessToken 访问令牌 + * @param fileId 文件ID + * @param sheetId 工作表ID + * @return rowTotal,未找到或解析失败返回 0 + */ + int getSheetRowTotal(String accessToken, String fileId, String sheetId); /** * 获取用户信息 diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/TencentDocServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/TencentDocServiceImpl.java index d865dd9..37de8c7 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/TencentDocServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/TencentDocServiceImpl.java @@ -562,6 +562,53 @@ public class TencentDocServiceImpl implements ITencentDocService { } } + @Override + public int getSheetRowTotal(String accessToken, String fileId, String sheetId) { + if (accessToken == null || fileId == null || sheetId == null) { + return 0; + } + try { + JSONObject result = getFileInfo(accessToken, fileId); + if (result == null) { + return 0; + } + // 兼容多种返回结构:data.properties[]、properties[]、sheets[].properties + com.alibaba.fastjson2.JSONArray list = null; + if (result.containsKey("data")) { + com.alibaba.fastjson2.JSONObject data = result.getJSONObject("data"); + if (data != null && data.containsKey("properties")) { + list = data.getJSONArray("properties"); + } else if (data != null && data.containsKey("sheets")) { + list = data.getJSONArray("sheets"); + } + } + if (list == null && result.containsKey("properties")) { + list = result.getJSONArray("properties"); + } + if (list == null && result.containsKey("sheets")) { + list = result.getJSONArray("sheets"); + } + if (list == null) { + return 0; + } + for (int i = 0; i < list.size(); i++) { + com.alibaba.fastjson2.JSONObject item = list.getJSONObject(i); + com.alibaba.fastjson2.JSONObject props = item.containsKey("properties") ? item.getJSONObject("properties") : item; + if (props == null) { + continue; + } + String sid = props.getString("sheetId"); + if (sheetId.equals(sid) && props.containsKey("rowTotal")) { + return Math.max(0, props.getIntValue("rowTotal")); + } + } + return 0; + } catch (Exception e) { + log.warn("获取工作表 rowTotal 失败 - fileId: {}, sheetId: {}", fileId, sheetId, e); + return 0; + } + } + @Override public JSONObject getUserInfo(String accessToken) { try {