1
This commit is contained in:
@@ -57,8 +57,12 @@ public class TencentDocController extends BaseController {
|
|||||||
/** Redis key前缀,用于存储上次处理的最大行数 */
|
/** Redis key前缀,用于存储上次处理的最大行数 */
|
||||||
private static final String LAST_PROCESSED_ROW_KEY_PREFIX = "tendoc:last_row:";
|
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;
|
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) */
|
/** 数据区 range 列尾(30 列 = A 到 AD,API 列数≤200) */
|
||||||
private static final String DATA_RANGE_COL_END = "AD";
|
private static final String DATA_RANGE_COL_END = "AD";
|
||||||
/** 回溯行数:下次起始行 = 本次扫描终点 + 1 - 回溯,用于覆盖物流变更或延后补填 */
|
/** 回溯行数:下次起始行 = 本次扫描终点 + 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 effectiveStartRow = configStartRow != null ? configStartRow : (headerRow + 1);
|
||||||
|
|
||||||
int startRow;
|
int startRow;
|
||||||
int endRow;
|
int endRow;
|
||||||
|
|
||||||
|
// 直接拉取文档信息得到 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 {
|
||||||
|
// 未取到 rowTotal 时退回原逻辑
|
||||||
if (forceStartRow != null) {
|
if (forceStartRow != null) {
|
||||||
startRow = forceStartRow;
|
startRow = forceStartRow;
|
||||||
endRow = startRow + API_MAX_ROWS_PER_REQUEST - 1;
|
endRow = startRow + API_MAX_ROWS_PER_REQUEST - 1;
|
||||||
log.info("使用强制指定的起始行: {}, 结束行: {}(单批最多 {} 行)", startRow, endRow, API_MAX_ROWS_PER_REQUEST);
|
log.info("使用强制指定的起始行: {}, 结束行: {}(未取到 rowTotal)", startRow, endRow);
|
||||||
} else if (lastMaxRow != null && lastMaxRow >= effectiveStartRow) {
|
} else if (lastMaxRow != null && lastMaxRow >= effectiveStartRow) {
|
||||||
// 带回溯的起始:从 (上次最大行 + 1 - 回溯行数) 开始,保证覆盖物流变更与延后补填
|
|
||||||
startRow = Math.max(effectiveStartRow, lastMaxRow + 1 - BACKTRACK_ROWS);
|
startRow = Math.max(effectiveStartRow, lastMaxRow + 1 - BACKTRACK_ROWS);
|
||||||
endRow = startRow + API_MAX_ROWS_PER_REQUEST - 1;
|
endRow = startRow + API_MAX_ROWS_PER_REQUEST - 1;
|
||||||
log.info("上次最大行: {},回溯 {} 行后从第 {} 行开始,到第 {} 行(本批最多 {} 行)",
|
log.info("上次最大行: {},回溯后从第 {} 行到第 {} 行(未取到 rowTotal)", lastMaxRow, startRow, endRow);
|
||||||
lastMaxRow, BACKTRACK_ROWS, startRow, endRow, API_MAX_ROWS_PER_REQUEST);
|
|
||||||
} else {
|
} else {
|
||||||
startRow = effectiveStartRow;
|
startRow = effectiveStartRow;
|
||||||
endRow = startRow + API_MAX_ROWS_PER_REQUEST - 1;
|
endRow = startRow + API_MAX_ROWS_PER_REQUEST - 1;
|
||||||
log.info("首次同步或配置已重置,从第 {} 行开始,到第 {} 行(单批最多 {} 行)", startRow, endRow, API_MAX_ROWS_PER_REQUEST);
|
log.info("首次同步,从第 {} 行到第 {} 行(未取到 rowTotal)", startRow, endRow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 严格限制单次 range 行数,防止 API 报 range invalid
|
// 严格限制单次 range 行数(接口实际只能读 200 行)
|
||||||
int rowCount = endRow - startRow + 1;
|
if (endRow - startRow + 1 > READ_ROWS_WHEN_USE_ROW_TOTAL) {
|
||||||
if (rowCount > API_MAX_ROWS_PER_REQUEST) {
|
endRow = startRow + READ_ROWS_WHEN_USE_ROW_TOTAL - 1;
|
||||||
endRow = startRow + API_MAX_ROWS_PER_REQUEST - 1;
|
log.info("已截断结束行以符合 API 限制: endRow={}, 行数={}", endRow, READ_ROWS_WHEN_USE_ROW_TOTAL);
|
||||||
log.info("已截断结束行以符合 API 限制: endRow={}, 行数={}", endRow, API_MAX_ROWS_PER_REQUEST);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("开始填充物流链接 - 文件ID: {}, 工作表ID: {}, 起始行: {}, 结束行: {}, 上次最大行: {}",
|
log.info("开始填充物流链接 - 文件ID: {}, 工作表ID: {}, 起始行: {}, 结束行: {}, rowTotal: {}",
|
||||||
fileId, sheetId, startRow, endRow, lastMaxRow);
|
fileId, sheetId, startRow, endRow, rowTotal > 0 ? rowTotal : "未获取");
|
||||||
|
|
||||||
// 读取表格数据(先读取表头行用于识别列位置)
|
// 读取表格数据(先读取表头行用于识别列位置)
|
||||||
// 根据官方文档,使用 A1 表示法(Excel格式)
|
// 根据官方文档,使用 A1 表示法(Excel格式)
|
||||||
@@ -1156,16 +1169,16 @@ public class TencentDocController extends BaseController {
|
|||||||
log.info("列位置识别完成 - 单号: {}, 物流单号: {}, 备注: {}, 是否安排: {}, 标记: {}, 下单电话: {}",
|
log.info("列位置识别完成 - 单号: {}, 物流单号: {}, 备注: {}, 是否安排: {}, 标记: {}, 下单电话: {}",
|
||||||
orderNoColumn, logisticsLinkColumn, remarkColumn, arrangedColumn, markColumn, phoneColumn);
|
orderNoColumn, logisticsLinkColumn, remarkColumn, arrangedColumn, markColumn, phoneColumn);
|
||||||
|
|
||||||
// 读取数据行:严格限制单次行数(避免 range invalid),失败时逐步缩小范围重试
|
// 读取数据行:接口实际只能读 200 行,严格限制单次行数,失败时逐步缩小范围重试
|
||||||
int effectiveEndRow = Math.min(endRow, startRow + API_MAX_ROWS_PER_REQUEST - 1);
|
int effectiveEndRow = Math.min(endRow, startRow + READ_ROWS_WHEN_USE_ROW_TOTAL - 1);
|
||||||
JSONObject sheetData = null;
|
JSONObject sheetData = null;
|
||||||
int[] retryDecrements = new int[] { 0, 20, 50, 100 };
|
int[] retryDecrements = new int[] { 0, 20, 50, 100 };
|
||||||
for (int decrement : retryDecrements) {
|
for (int decrement : retryDecrements) {
|
||||||
int tryEndRow = Math.max(startRow, effectiveEndRow - decrement);
|
int tryEndRow = Math.max(startRow, effectiveEndRow - decrement);
|
||||||
int tryRowCount = tryEndRow - startRow + 1;
|
int tryRowCount = tryEndRow - startRow + 1;
|
||||||
if (tryRowCount > API_MAX_ROWS_PER_REQUEST) {
|
if (tryRowCount > READ_ROWS_WHEN_USE_ROW_TOTAL) {
|
||||||
tryEndRow = startRow + API_MAX_ROWS_PER_REQUEST - 1;
|
tryEndRow = startRow + READ_ROWS_WHEN_USE_ROW_TOTAL - 1;
|
||||||
tryRowCount = API_MAX_ROWS_PER_REQUEST;
|
tryRowCount = READ_ROWS_WHEN_USE_ROW_TOTAL;
|
||||||
}
|
}
|
||||||
if (tryEndRow < startRow) {
|
if (tryEndRow < startRow) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -98,6 +98,16 @@ public interface ITencentDocService {
|
|||||||
*/
|
*/
|
||||||
JSONObject getSheetList(String accessToken, String fileId);
|
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);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户信息
|
* 获取用户信息
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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
|
@Override
|
||||||
public JSONObject getUserInfo(String accessToken) {
|
public JSONObject getUserInfo(String accessToken) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user