Compare commits

..

13 Commits

Author SHA1 Message Date
van
4407487fbf 1 2026-03-11 21:25:58 +08:00
van
465e0993d6 1 2026-03-10 16:27:24 +08:00
van
0a20825831 1 2026-03-10 00:31:54 +08:00
van
b4749f3516 1 2026-03-09 15:25:22 +08:00
van
dc8b0b2fcf 1 2026-03-05 23:22:49 +08:00
van
f321e40876 1 2026-03-03 20:36:26 +08:00
van
a58891ef04 1 2026-03-03 20:08:59 +08:00
van
26f6f6e058 1 2026-03-03 19:21:36 +08:00
van
0b0b431e95 从接口拿行数 2026-03-01 00:04:24 +08:00
Leo
4c07dda3d7 1 2026-02-08 11:35:38 +08:00
Leo
549224a83f 1 2026-02-08 11:20:30 +08:00
Leo
c1484ecbfd 1 2026-02-06 20:25:03 +08:00
Leo
e63ff7522e 1 2026-02-06 20:17:29 +08:00
17 changed files with 450 additions and 338 deletions

View File

@@ -34,15 +34,17 @@ public class InstructionController extends BaseController {
}
/**
* 获取历史消息记录
* 获取历史消息记录(支持关键词搜索,在全部历史数据中匹配)
* @param type 消息类型request(请求) 或 response(响应)
* @param limit 获取数量默认100条
* @param limit 获取数量默认100条;有 keyword 时为返回匹配条数上限默认200
* @param keyword 可选,搜索关键词;不为空时在全部数据中过滤后返回
* @return 历史消息列表
*/
@GetMapping("/history")
public AjaxResult getHistory(@RequestParam(required = false, defaultValue = "request") String type,
@RequestParam(required = false, defaultValue = "100") Integer limit) {
java.util.List<String> history = instructionService.getHistory(type, limit);
@RequestParam(required = false, defaultValue = "100") Integer limit,
@RequestParam(required = false) String keyword) {
java.util.List<String> history = instructionService.getHistory(type, limit, keyword);
return AjaxResult.success(history);
}
}

View File

@@ -1,6 +1,7 @@
package com.ruoyi.web.controller.jarvis;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.RequestParam;
@@ -62,7 +63,29 @@ public class OrderRowsController extends BaseController
startPage();
List<OrderRows> list = orderRowsService.selectOrderRowsList(orderRows);
return getDataTable(list);
TableDataInfo dataTable = getDataTable(list);
Date beginTime = getDateFromParams(orderRows.getParams(), "beginTime");
Date endTime = getDateFromParams(orderRows.getParams(), "endTime");
// 与列表同数据源,不排除 isCount=0保证总订单数与分页 total 一致
dataTable.setStatistics(buildStatistics(orderRows, beginTime, endTime, true));
return dataTable;
}
private static Date getDateFromParams(Map<String, Object> params, String key) {
if (params == null) return null;
Object v = params.get(key);
if (v == null) return null;
if (v instanceof Date) return (Date) v;
if (v instanceof String) {
String s = ((String) v).trim();
if (s.isEmpty()) return null;
try {
return new SimpleDateFormat("yyyy-MM-dd").parse(s);
} catch (Exception e) {
return null;
}
}
return null;
}
/**
@@ -146,143 +169,134 @@ public class OrderRowsController extends BaseController
return AjaxResult.success(options);
}
/**
* 根据联盟ID或日期范围统计订单数据按validCode分组
* 根据联盟ID或日期范围统计订单数据按validCode分组(独立接口,与列表同条件时建议用 list 返回的 statistics
*/
@GetMapping("/statistics")
public AjaxResult getStatistics(OrderRows orderRows, Date beginTime, Date endTime) {
Map<String, Object> result = new HashMap<>();
// 获取superAdminList筛选出isCount = 0的unionId
List<Long> excludeUnionIds = new ArrayList<>();
List<SuperAdmin> superAdminList = superAdminService.selectSuperAdminList(null);
logger.info("superAdminList.size: {}", superAdminList.size());
for (SuperAdmin superAdmin : superAdminList) {
if (superAdmin.getIsCount() != null && superAdmin.getIsCount() == 0 && superAdmin.getUnionId() != null) {
try {
excludeUnionIds.add(Long.parseLong(superAdmin.getUnionId()));
} catch (NumberFormatException e) {
// 忽略无法解析的unionId
}
}
@GetMapping("/statistics")
public AjaxResult getStatistics(OrderRows orderRows, Date beginTime, Date endTime) {
return AjaxResult.success(buildStatistics(orderRows, beginTime, endTime, false));
}
logger.info("excludeUnionIds: {}", excludeUnionIds.size());
// 使用新的查询方法直接在SQL中完成日期过滤和unionId排除
List<OrderRows> filteredList = orderRowsService.selectOrderRowsListWithFilter(orderRows, beginTime, endTime, excludeUnionIds);
// 定义分组
Map<String, List<String>> groups = new HashMap<>();
groups.put("cancel", Arrays.asList("3"));
groups.put("invalid", Arrays.asList("2","4","5","6","7","8","9","10","11","14","19","20","21","22","23","29","30","31","32","33","34"));
groups.put("pending", Arrays.asList("15"));
groups.put("paid", Arrays.asList("16"));
groups.put("finished", Arrays.asList("17"));
groups.put("deposit", Arrays.asList("24"));
groups.put("illegal", Arrays.asList("25","26","27","28"));
// 初始化统计结果
Map<String, Map<String, Object>> groupStats = new HashMap<>();
groupStats.put("cancel", createGroupStat("取消", "cancel"));
groupStats.put("invalid", createGroupStat("无效", "invalid"));
groupStats.put("pending", createGroupStat("待付款", "pending"));
groupStats.put("paid", createGroupStat("已付款", "paid"));
groupStats.put("finished", createGroupStat("已完成", "finished"));
groupStats.put("deposit", createGroupStat("已付定金", "deposit"));
groupStats.put("illegal", createGroupStat("违规", "illegal"));
// 总统计数据
int totalOrders = 0;
double totalCommission = 0;
double totalActualFee = 0;
long totalSkuNum = 0;
// 违规订单统计
long violationOrders = 0;
double violationCommission = 0.0;
// 按分组统计
for (OrderRows row : filteredList) {
totalOrders++;
if (row.getSkuNum() != null) {
totalSkuNum += row.getSkuNum();
}
// 计算佣金金额(对于违规和取消订单使用特殊计算)
String validCode = row.getValidCode() != null ? String.valueOf(row.getValidCode()) : null;
boolean isCancel = "3".equals(validCode); // 取消订单
boolean isIllegal = "25".equals(validCode) || "26".equals(validCode)
|| "27".equals(validCode) || "28".equals(validCode); // 违规订单
double commissionAmount = 0.0;
double actualFeeAmount = 0.0;
// 违规订单:始终使用 estimateCosPrice * commissionRate / 100 计算
if (isIllegal) {
if (row.getEstimateCosPrice() != null && row.getCommissionRate() != null) {
commissionAmount = row.getEstimateCosPrice() * row.getCommissionRate() * 0.01;
actualFeeAmount = commissionAmount; // 违规订单的实际费用等于计算的佣金
} else if (row.getEstimateFee() != null) {
commissionAmount = row.getEstimateFee();
actualFeeAmount = commissionAmount;
/**
* 构建统计数据。
* @param forList true=与列表同数据源(不排除 isCount=0保证总订单数与分页一致false=独立统计(排除 isCount=0
*/
private Map<String, Object> buildStatistics(OrderRows orderRows, Date beginTime, Date endTime, boolean forList) {
Map<String, Object> result = new HashMap<>();
List<Long> excludeUnionIds = new ArrayList<>();
if (!forList) {
List<SuperAdmin> superAdminList = superAdminService.selectSuperAdminList(null);
for (SuperAdmin superAdmin : superAdminList) {
if (superAdmin.getIsCount() != null && superAdmin.getIsCount() == 0 && superAdmin.getUnionId() != null) {
try {
excludeUnionIds.add(Long.parseLong(superAdmin.getUnionId()));
} catch (NumberFormatException e) {
}
}
}
}
// 取消订单如果actualFee为空或0则使用公式计算
else if (isCancel) {
if (row.getActualFee() != null && row.getActualFee() > 0) {
actualFeeAmount = row.getActualFee();
commissionAmount = row.getEstimateFee() != null ? row.getEstimateFee() : 0;
} else if (row.getEstimateCosPrice() != null && row.getCommissionRate() != null) {
commissionAmount = row.getEstimateCosPrice() * row.getCommissionRate() * 0.01;
actualFeeAmount = commissionAmount;
List<OrderRows> filteredList = orderRowsService.selectOrderRowsListWithFilter(orderRows, beginTime, endTime, excludeUnionIds);
Map<String, List<String>> groups = new HashMap<>();
groups.put("cancel", Arrays.asList("3"));
groups.put("invalid", Arrays.asList("2","4","5","6","7","8","9","10","11","14","19","20","21","22","23","29","30","31","32","33","34"));
groups.put("pending", Arrays.asList("15"));
groups.put("paid", Arrays.asList("16"));
groups.put("finished", Arrays.asList("17"));
groups.put("deposit", Arrays.asList("24"));
groups.put("illegal", Arrays.asList("25","26","27","28"));
Map<String, Map<String, Object>> groupStats = new HashMap<>();
groupStats.put("cancel", createGroupStat("取消", "cancel"));
groupStats.put("invalid", createGroupStat("无效", "invalid"));
groupStats.put("pending", createGroupStat("待付款", "pending"));
groupStats.put("paid", createGroupStat("已付款", "paid"));
groupStats.put("finished", createGroupStat("已完成", "finished"));
groupStats.put("deposit", createGroupStat("已付定金", "deposit"));
groupStats.put("illegal", createGroupStat("违规", "illegal"));
int totalOrders = 0;
double totalCosPrice = 0;
double totalCommission = 0;
double totalActualFee = 0;
long totalSkuNum = 0;
long violationOrders = 0;
double violationCommission = 0.0;
for (OrderRows row : filteredList) {
totalOrders++;
if (row.getEstimateCosPrice() != null) {
totalCosPrice += row.getEstimateCosPrice();
}
if (row.getSkuNum() != null) {
totalSkuNum += row.getSkuNum();
}
String validCode = row.getValidCode() != null ? String.valueOf(row.getValidCode()) : null;
boolean isCancel = "3".equals(validCode);
boolean isIllegal = "25".equals(validCode) || "26".equals(validCode)
|| "27".equals(validCode) || "28".equals(validCode);
double commissionAmount = 0.0;
double actualFeeAmount = 0.0;
if (isIllegal) {
if (row.getEstimateCosPrice() != null && row.getCommissionRate() != null) {
commissionAmount = row.getEstimateCosPrice() * row.getCommissionRate() * 0.01;
actualFeeAmount = commissionAmount;
} else if (row.getEstimateFee() != null) {
commissionAmount = row.getEstimateFee();
actualFeeAmount = commissionAmount;
}
} else if (isCancel) {
if (row.getActualFee() != null && row.getActualFee() > 0) {
actualFeeAmount = row.getActualFee();
commissionAmount = row.getEstimateFee() != null ? row.getEstimateFee() : 0;
} else if (row.getEstimateCosPrice() != null && row.getCommissionRate() != null) {
commissionAmount = row.getEstimateCosPrice() * row.getCommissionRate() * 0.01;
actualFeeAmount = commissionAmount;
} else {
commissionAmount = row.getEstimateFee() != null ? row.getEstimateFee() : 0;
actualFeeAmount = row.getActualFee() != null ? row.getActualFee() : 0;
}
} else {
commissionAmount = row.getEstimateFee() != null ? row.getEstimateFee() : 0;
actualFeeAmount = row.getActualFee() != null ? row.getActualFee() : 0;
}
}
// 其他订单:使用原有的字段值
else {
commissionAmount = row.getEstimateFee() != null ? row.getEstimateFee() : 0;
actualFeeAmount = row.getActualFee() != null ? row.getActualFee() : 0;
}
totalCommission += commissionAmount;
totalActualFee += actualFeeAmount;
totalCommission += commissionAmount;
totalActualFee += actualFeeAmount;
// 按validCode分组统计
if (validCode != null) {
for (Map.Entry<String, List<String>> group : groups.entrySet()) {
List<String> codes = group.getValue();
if (codes.contains(validCode)) {
Map<String, Object> stat = groupStats.get(group.getKey());
stat.put("count", (Integer) stat.get("count") + 1);
stat.put("commission", (Double) stat.get("commission") + commissionAmount);
stat.put("actualFee", (Double) stat.get("actualFee") + actualFeeAmount);
if (row.getSkuNum() != null) {
stat.put("skuNum", (Long) stat.get("skuNum") + row.getSkuNum());
if (validCode != null) {
for (Map.Entry<String, List<String>> group : groups.entrySet()) {
if (group.getValue().contains(validCode)) {
Map<String, Object> stat = groupStats.get(group.getKey());
stat.put("count", (Integer) stat.get("count") + 1);
stat.put("commission", (Double) stat.get("commission") + commissionAmount);
stat.put("actualFee", (Double) stat.get("actualFee") + actualFeeAmount);
if (row.getSkuNum() != null) {
stat.put("skuNum", (Long) stat.get("skuNum") + row.getSkuNum());
}
if ("illegal".equals(group.getKey())) {
violationOrders++;
violationCommission += commissionAmount;
}
break;
}
// 统计违规订单
if ("illegal".equals(group.getKey())) {
violationOrders++;
violationCommission += commissionAmount;
}
break;
}
}
}
result.put("totalOrders", totalOrders);
result.put("totalCosPrice", totalCosPrice);
result.put("totalCommission", totalCommission);
result.put("totalActualFee", totalActualFee);
result.put("totalSkuNum", totalSkuNum);
result.put("violationOrders", violationOrders);
result.put("violationCommission", violationCommission);
result.put("groupStats", groupStats);
return result;
}
result.put("totalOrders", totalOrders);
result.put("totalCommission", totalCommission);
result.put("totalActualFee", totalActualFee);
result.put("totalSkuNum", totalSkuNum);
result.put("violationOrders", violationOrders);
result.put("violationCommission", violationCommission);
result.put("groupStats", groupStats);
return AjaxResult.success(result);
}
/**
* 创建分组统计对象

View File

@@ -155,5 +155,26 @@ public class SocialMediaController extends BaseController
return AjaxResult.error("删除失败: " + e.getMessage());
}
}
/**
* 闲鱼文案(手动):根据标题+可选型号生成代下单、教你下单文案不依赖JD接口
*/
@Log(title = "闲鱼文案(手动)生成", businessType = BusinessType.OTHER)
@PostMapping("/xianyu-wenan/generate")
public AjaxResult generateXianyuWenan(@RequestBody Map<String, Object> request)
{
try {
String title = (String) request.get("title");
String remark = (String) request.get("remark");
Map<String, Object> result = socialMediaService.generateXianyuWenan(title, remark);
if (Boolean.TRUE.equals(result.get("success"))) {
return AjaxResult.success(result);
}
return AjaxResult.error((String) result.get("error"));
} catch (Exception e) {
logger.error("闲鱼文案生成失败", e);
return AjaxResult.error("生成失败: " + e.getMessage());
}
}
}

View File

@@ -43,8 +43,6 @@ 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:";
/**
* 获取当前配置
@@ -98,43 +96,32 @@ public class TencentDocConfigController extends BaseController {
config.put("appId", tencentDocConfig.getAppId());
config.put("apiBaseUrl", tencentDocConfig.getApiBaseUrl());
// 获取当前同步进度(如果有配置)
// 注意:使用与 TencentDocController 相同的 Redis key 前缀
// 仅从接口获取 rowCount 用于展示无任何进度缓存与填充逻辑一致每次取最后200行
if (fileId != null && !fileId.isEmpty() && sheetId != null && !sheetId.isEmpty()) {
String syncProgressKey = LAST_PROCESSED_ROW_KEY_PREFIX + fileId + ":" + sheetId;
Integer currentProgress = redisCache.getCacheObject(syncProgressKey);
log.debug("读取同步进度 - key: {}, value: {}", syncProgressKey, currentProgress);
if (currentProgress != null) {
config.put("currentProgress", currentProgress);
// 根据回溯机制计算下次起始
int threshold = startRow + 100;
int nextStartRow;
String progressHint;
if (currentProgress <= (startRow + 49)) {
// 进度较小,下次从配置起始行开始
nextStartRow = startRow;
progressHint = String.format("已读取到第 %d 行,下次将从第 %d 行重新开始(进度较小)",
currentProgress, nextStartRow);
} else if (currentProgress > threshold) {
// 进度较大下次回溯100行但不能小于起始行
nextStartRow = Math.max(startRow, currentProgress - 100);
progressHint = String.format("已读取到第 %d 行,下次将从第 %d 行开始回溯100行防止遗漏",
currentProgress, nextStartRow);
try {
String accessToken = tencentDocTokenService.getValidAccessToken();
if (accessToken != null && !accessToken.isEmpty()) {
int rowCount = tencentDocService.getSheetRowTotal(accessToken, fileId, sheetId);
if (rowCount > 0) {
config.put("currentProgress", rowCount);
int nextStartRow = Math.max(3, rowCount - 199); // 与填充逻辑一致取最后200
config.put("nextStartRow", nextStartRow);
config.put("progressHint", String.format("表格当前 %d 行接口获取每次同步取最后200行第 %d ~ %d 行", rowCount, nextStartRow, rowCount));
} else {
config.put("currentProgress", null);
config.put("nextStartRow", startRow);
config.put("progressHint", "未获取到行数");
}
} else {
// 进度在阈值范围内,下次从配置起始行开始
nextStartRow = startRow;
progressHint = String.format("已读取到第 %d 行,下次将从第 %d 行重新开始",
currentProgress, nextStartRow);
config.put("currentProgress", null);
config.put("nextStartRow", startRow);
config.put("progressHint", "未授权,无法获取表格行数");
}
config.put("nextStartRow", nextStartRow);
config.put("progressHint", progressHint);
} else {
} catch (Exception e) {
log.warn("获取 rowCount 失败: {}", e.getMessage());
config.put("currentProgress", null);
config.put("nextStartRow", startRow);
config.put("progressHint", String.format("尚未开始同步,将从第 %d 行开始", startRow));
config.put("progressHint", "获取表格行数失败: " + e.getMessage());
}
}
@@ -212,13 +199,8 @@ public class TencentDocConfigController extends BaseController {
redisCache.setCacheObject(REDIS_KEY_PREFIX + "headerRow", headerRow, 180, TimeUnit.DAYS);
redisCache.setCacheObject(REDIS_KEY_PREFIX + "startRow", startRow, 180, TimeUnit.DAYS);
// 清除该文档的同步进度配置更新时重置进度从新的startRow重新开始
// 注意:使用与 TencentDocController 相同的 Redis key 前缀
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);
log.info("配置已更新,已清除同步进度 - key: {}, 将从第 {} 行重新开始同步", syncProgressKey, startRow);
// 不再使用 Redis 存储进度配置更新后下次从接口获取 rowCount 决定范围
log.info("配置已更新,将从第 {} 行开始(下次从接口获取 rowCount", startRow);
// 同时更新TencentDocConfig对象内存中
tencentDocConfig.setFileId(fileId.trim());

View File

@@ -54,9 +54,6 @@ public class TencentDocController extends BaseController {
@Autowired
private com.ruoyi.jarvis.service.ITencentDocDelayedPushService delayedPushService;
/** Redis key前缀用于存储上次处理的最大行数 */
private static final String LAST_PROCESSED_ROW_KEY_PREFIX = "tendoc:last_row:";
/** 单次请求最大行数(腾讯文档 API行数≤1000 */
private static final int API_MAX_ROWS_PER_REQUEST = 200;
/** 用 rowTotal 时接口实际单次只能读 200 行 */
@@ -944,24 +941,13 @@ public class TencentDocController extends BaseController {
}
}
// 从配置中读取表头行和数据起始行
// 从配置中读取表头行(仅用于读表头,不参与范围计算)
Integer headerRow = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "headerRow");
Integer configStartRow = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "startRow");
if (headerRow == null) {
headerRow = tencentDocConfig.getHeaderRow();
}
if (configStartRow == null) {
configStartRow = tencentDocConfig.getStartRow();
}
// 可选参数是否强制从指定行开始如果为true则忽略Redis记录的最大行数
Boolean forceStart = params.get("forceStart") != null ?
Boolean.valueOf(params.get("forceStart").toString()) : false;
Integer forceStartRow = params.get("forceStartRow") != null ?
Integer.valueOf(params.get("forceStartRow").toString()) : configStartRow;
// 新增参数是否跳过已推送的订单默认true防止重复推送
// 是否跳过已推送的订单默认true防止重复推送
Boolean skipPushedOrders = params.get("skipPushedOrders") != null ?
Boolean.valueOf(params.get("skipPushedOrders").toString()) : true;
@@ -969,94 +955,30 @@ public class TencentDocController extends BaseController {
return AjaxResult.error("文档配置不完整,请先配置 fileId 和 sheetId");
}
// 如果batchId为空创建一个新的批次ID和批量推送记录用于日志记录
if (batchId == null || batchId.trim().isEmpty()) {
// 计算起始行和结束行(用于创建批量推送记录)
int estimatedStartRow = configStartRow != null ? configStartRow : (headerRow + 1);
int estimatedEndRow = estimatedStartRow + 199;
// 每次从接口获取当前行数,无任何缓存。取「最后 200 行」startRow = rowCount - 199endRow = rowCount
int rowCount = tencentDocService.getSheetRowTotal(accessToken, fileId, sheetId);
if (rowCount <= 0) {
log.warn("接口未返回有效行数,无法执行填充");
return AjaxResult.error("无法获取表格行数,请检查文档与授权");
}
int startRow = Math.max(MIN_START_ROW_WHEN_USE_ROW_TOTAL, rowCount - (READ_ROWS_WHEN_USE_ROW_TOTAL - 1));
int endRow = rowCount;
// 如果batchId为空创建批量推送记录用于日志
if (batchId == null || batchId.trim().isEmpty()) {
batchId = batchPushService.createBatchPushRecord(
fileId,
sheetId,
"MANUAL", // 手动触发
"MANUAL_TRIGGER", // 手动触发来源
estimatedStartRow,
estimatedEndRow
"MANUAL",
"MANUAL_TRIGGER",
startRow,
endRow
);
log.info("未提供batchId自动创建新的批次ID和批量推送记录: {}", batchId);
log.info("未提供batchId自动创建批量推送记录: {}", batchId);
}
log.info("同步物流配置 - fileId: {}, sheetId: {}, batchId: {}, 配置起始行: {}, 表头行: {}",
fileId, sheetId, batchId, configStartRow, headerRow);
// 生成Redis key用于存储该文件的工作表的动态处理进度
String redisKey = LAST_PROCESSED_ROW_KEY_PREFIX + fileId + ":" + sheetId;
// 生成配置版本key用于检测配置是否被重新设置
String configVersionKey = "tencent:doc:sync:config_version:" + fileId + ":" + sheetId;
String currentConfigVersion = configStartRow + "_" + headerRow; // 配置版本起始行_表头行
// 检查配置是否被更新(如果配置版本变化,则重置动态进度)
String savedConfigVersion = redisCache.getCacheObject(configVersionKey);
if (savedConfigVersion == null || !savedConfigVersion.equals(currentConfigVersion)) {
// 配置已更新,清除旧的处理进度
redisCache.deleteObject(redisKey);
redisCache.setCacheObject(configVersionKey, currentConfigVersion, 180, TimeUnit.DAYS);
log.info("检测到配置更新,重置同步进度 - 新配置版本: {}", currentConfigVersion);
}
// 获取动态处理进度(上次处理到的最大行数)
Integer lastMaxRow = null;
if (!forceStart && redisCache.hasKey(redisKey)) {
Object cacheObj = redisCache.getCacheObject(redisKey);
if (cacheObj != null) {
lastMaxRow = Integer.valueOf(cacheObj.toString());
log.info("读取到上次处理进度 - 最大行: {}", lastMaxRow);
}
}
int effectiveStartRow = configStartRow != null ? configStartRow : (headerRow + 1);
int startRow;
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) {
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 行数(接口实际只能读 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: {}, 起始行: {}, 结束行: {}, rowTotal: {}",
fileId, sheetId, startRow, endRow, rowTotal > 0 ? rowTotal : "未获取");
log.info("同步物流 - fileId: {}, sheetId: {}, 接口行数: {}, 本次范围: 第 {} ~ {} 行(共 {} 行)",
fileId, sheetId, rowCount, startRow, endRow, endRow - startRow + 1);
// 读取表格数据(先读取表头行用于识别列位置)
// 根据官方文档,使用 A1 表示法Excel格式
@@ -1170,6 +1092,7 @@ public class TencentDocController extends BaseController {
orderNoColumn, logisticsLinkColumn, remarkColumn, arrangedColumn, markColumn, phoneColumn);
// 读取数据行:接口实际只能读 200 行,严格限制单次行数,失败时逐步缩小范围重试
// 腾讯文档 get_range 的 range 为「结束行不包含」:要读到 endRow 含最后一行,须传 endRow+1
int effectiveEndRow = Math.min(endRow, startRow + READ_ROWS_WHEN_USE_ROW_TOTAL - 1);
JSONObject sheetData = null;
int[] retryDecrements = new int[] { 0, 20, 50, 100 };
@@ -1183,7 +1106,8 @@ public class TencentDocController extends BaseController {
if (tryEndRow < startRow) {
continue;
}
String range = String.format("A%d:%s%d", startRow, DATA_RANGE_COL_END, tryEndRow);
int rangeEndInclusive = tryEndRow + 1; // API 结束行不包含,+1 才能读到 tryEndRow
String range = String.format("A%d:%s%d", startRow, DATA_RANGE_COL_END, rangeEndInclusive);
log.info("开始读取数据行 - 行号: {} ~ {} (共 {} 行), range: {} (尝试 decrement={})", startRow, tryEndRow, tryRowCount, range, decrement);
try {
sheetData = tencentDocService.readSheetData(accessToken, fileId, sheetId, range);
@@ -1213,7 +1137,7 @@ public class TencentDocController extends BaseController {
for (int decrement : new int[] { 1, 10 }) {
int tryEndRow = Math.max(startRow, effectiveEndRow - decrement);
if (tryEndRow >= startRow) {
String retryRange = String.format("A%d:%s%d", startRow, DATA_RANGE_COL_END, tryEndRow);
String retryRange = String.format("A%d:%s%d", startRow, DATA_RANGE_COL_END, tryEndRow + 1); // API 结束行不包含
try {
sheetData = tencentDocService.readSheetData(accessToken, fileId, sheetId, retryRange);
if (sheetData != null) {
@@ -1248,20 +1172,19 @@ public class TencentDocController extends BaseController {
log.warn("sheetData内容预览: {}", sheetData.toJSONString().substring(0, Math.min(500, sheetData.toJSONString().length())));
}
// 无数据时也推进进度,使用较小回溯尽快扫到后续行
// 无数据时计算下次起始行(不再写入 Redis下次从接口获取 rowCount
int emptyCurrentMax = effectiveEndRow;
int emptyBacktrack = BACKTRACK_ROWS_WHEN_ALL_SKIPPED;
int emptyNextStart = Math.max(effectiveStartRow,
int emptyNextStart = Math.max(MIN_START_ROW_WHEN_USE_ROW_TOTAL,
Math.max(startRow + MIN_ADVANCE_ROWS, emptyCurrentMax + 1 - emptyBacktrack));
int emptyProgressToSave = Math.max(emptyCurrentMax, startRow + MIN_ADVANCE_ROWS + emptyBacktrack - 1);
redisCache.setCacheObject(redisKey, emptyProgressToSave, 30, TimeUnit.DAYS);
log.info("本批无数据,已推进进度: nextStartRow={}, Redis 进度={}", emptyNextStart, emptyProgressToSave);
log.info("本批无数据,下次起始行: {}(不再保存进度,下次从接口获取 rowCount", emptyNextStart);
JSONObject result = new JSONObject();
result.put("startRow", startRow);
result.put("endRow", effectiveEndRow);
result.put("lastMaxRow", lastMaxRow);
result.put("lastMaxRow", null); // 不再使用 Redis 存储,改为从接口获取 rowCount
result.put("nextStartRow", emptyNextStart);
result.put("rowTotal", rowCount > 0 ? rowCount : null);
result.put("currentMaxRow", emptyCurrentMax);
result.put("filledCount", 0);
result.put("skippedCount", 0);
@@ -1747,30 +1670,26 @@ public class TencentDocController extends BaseController {
successUpdates, skippedCount, errorCount, maxSuccessRow > 0 ? maxSuccessRow : "");
}
// 完美推送回溯:根据本次扫描/写入结果决定下次起始行 Redis 进度
// 根据本次扫描/写入结果计算下次起始行(不再写入 Redis,下次从接口获取 rowCount 决定范围)
// currentMaxRow = 本批实际涉及的最大行(有写入用最大成功行,否则用本批扫描终点)
int currentMaxRow = (maxSuccessRow > 0) ? maxSuccessRow : effectiveEndRow;
// 整批都跳过时用较小回溯,尽快扫到后续行(如 308;有写入时用完整回溯覆盖物流变更
// 整批都跳过时用较小回溯,尽快扫到后续行;有写入时用完整回溯覆盖物流变更
int backtrack = (successUpdates > 0) ? BACKTRACK_ROWS : BACKTRACK_ROWS_WHEN_ALL_SKIPPED;
int nextStartRow = Math.max(effectiveStartRow,
int nextStartRow = Math.max(MIN_START_ROW_WHEN_USE_ROW_TOTAL,
Math.max(startRow + MIN_ADVANCE_ROWS, currentMaxRow + 1 - backtrack));
// 进度写入 Redis至少推进到 (startRow + 最小前进 + 回溯 - 1),保证下次不会重复扫同一段
int progressToSave = Math.max(currentMaxRow, startRow + MIN_ADVANCE_ROWS + backtrack - 1);
redisCache.setCacheObject(redisKey, progressToSave, 30, TimeUnit.DAYS);
String nextSyncHint = String.format(
"下次将从第 %d 行开始(本批最大行: %d回溯 %d 行;已写入 %d 条,跳过 %d 条)",
"下次将从第 %d 行开始(本批最大行: %d回溯 %d 行;已写入 %d 条,跳过 %d 条;下次从接口获取 rowCount",
nextStartRow, currentMaxRow, backtrack, successUpdates, skippedCount);
log.info("========== 更新Redis进度 ==========");
log.info("本批范围: {}~{},实际最大行: {},下次起始: {}Redis 进度: {}", startRow, effectiveEndRow, currentMaxRow, nextStartRow, progressToSave);
log.info("Redis Key: {}", redisKey);
log.info("本批范围: {}~{},实际最大行: {},下次起始: {}(不再保存 Redis 进度", startRow, effectiveEndRow, currentMaxRow, nextStartRow);
JSONObject result = new JSONObject();
result.put("startRow", startRow);
result.put("endRow", effectiveEndRow);
result.put("currentMaxRow", currentMaxRow);
result.put("lastMaxRow", lastMaxRow);
result.put("lastMaxRow", null); // 不再使用 Redis 存储,改为从接口获取 rowCount
result.put("nextStartRow", nextStartRow);
result.put("rowTotal", rowCount > 0 ? rowCount : null);
result.put("filledCount", filledCount);
result.put("skippedCount", skippedCount);
result.put("errorCount", errorCount);
@@ -2137,8 +2056,8 @@ public class TencentDocController extends BaseController {
int skippedCount = 0;
int errorCount = 0;
// 分批读取数据,每批500行API 行数≤1000列数≤200
final int BATCH_SIZE = 500;
// 分批读取数据,接口单次只能读 200 行
final int BATCH_SIZE = 200;
int currentStartRow = startRow;
int totalBatches = (int) Math.ceil((double)(endRow - startRow + 1) / BATCH_SIZE);
int currentBatch = 0;
@@ -2436,8 +2355,8 @@ public class TencentDocController extends BaseController {
int errorCount = 0; // 错误数量
java.util.List<String> updatedOrderNos = new java.util.ArrayList<>(); // 更新的单号列表
// 分批读取数据,每批500行API 行数≤1000列数≤200
final int BATCH_SIZE = 500;
// 分批读取数据,接口单次只能读 200 行
final int BATCH_SIZE = 200;
int currentStartRow = startRow;
int totalBatches = (int) Math.ceil((double)(endRow - startRow + 1) / BATCH_SIZE);
int currentBatch = 0;

View File

@@ -1,10 +1,14 @@
package com.ruoyi.web.controller.monitor;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
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.RestController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.framework.web.domain.Server;
import com.ruoyi.jarvis.service.ILogisticsService;
import com.ruoyi.jarvis.service.IWxSendService;
@@ -28,6 +32,10 @@ public class ServerController
@Resource
private IWxSendService wxSendService;
/** Ollama 服务地址,用于健康检查 */
@Value("${jarvis.ollama.base-url:http://192.168.8.34:11434}")
private String ollamaBaseUrl;
@PreAuthorize("@ss.hasPermi('monitor:server:list')")
@GetMapping()
public AjaxResult getInfo() throws Exception
@@ -82,6 +90,38 @@ public class ServerController
healthMap.put("wxSend", wxSendMap);
}
// Ollama 服务健康检测(调试用)
try {
String url = ollamaBaseUrl.replaceAll("/$", "") + "/api/tags";
String result = HttpUtils.sendGet(url);
if (result != null && !result.trim().isEmpty()) {
JSONObject json = JSON.parseObject(result);
if (json != null && json.containsKey("models") && !json.containsKey("error")) {
Map<String, Object> ollamaMap = new HashMap<>();
ollamaMap.put("healthy", true);
ollamaMap.put("status", "正常");
ollamaMap.put("message", "Ollama 服务可用");
ollamaMap.put("serviceUrl", ollamaBaseUrl);
healthMap.put("ollama", ollamaMap);
} else {
putOllamaUnhealthy(healthMap, url, json != null && json.getString("error") != null ? json.getString("error") : "返回格式异常");
}
} else {
putOllamaUnhealthy(healthMap, url, "返回为空");
}
} catch (Exception e) {
putOllamaUnhealthy(healthMap, ollamaBaseUrl, "健康检测异常: " + e.getMessage());
}
return AjaxResult.success(healthMap);
}
private void putOllamaUnhealthy(Map<String, Object> healthMap, String url, String message) {
Map<String, Object> ollamaMap = new HashMap<>();
ollamaMap.put("healthy", false);
ollamaMap.put("status", "异常");
ollamaMap.put("message", message);
ollamaMap.put("serviceUrl", url);
healthMap.put("ollama", ollamaMap);
}
}

View File

@@ -205,6 +205,10 @@ jarvis:
# 获取评论接口服务地址(后端转发,避免前端跨域)
fetch-comments:
base-url: http://192.168.8.60:5008
# Ollama 大模型服务(监控健康度调试用)
ollama:
base-url: http://192.168.8.34:11434
model: qwen3.5:9b
# 腾讯文档开放平台配置
# 文档地址https://docs.qq.com/open/document/app/openapi/v3/sheet/model/spreadsheet.html
tencent:

View File

@@ -205,6 +205,10 @@ jarvis:
# 获取评论接口服务地址(后端转发)
fetch-comments:
base-url: http://192.168.8.60:5008
# Ollama 大模型服务(监控健康度调试用)
ollama:
base-url: http://192.168.8.34:11434
model: qwen3.5:9b
# 腾讯文档开放平台配置
# 文档地址https://docs.qq.com/open/document/app/openapi/v3/sheet/model/spreadsheet.html
tencent:

View File

@@ -24,6 +24,9 @@ public class TableDataInfo implements Serializable
/** 消息内容 */
private String msg;
/** 扩展数据(如统计信息等,可选) */
private Object statistics;
/**
* 表格数据对象
*/
@@ -82,4 +85,14 @@ public class TableDataInfo implements Serializable
{
this.msg = msg;
}
public Object getStatistics()
{
return statistics;
}
public void setStatistics(Object statistics)
{
this.statistics = statistics;
}
}

View File

@@ -35,6 +35,15 @@ public interface IInstructionService {
* @return 历史消息列表
*/
java.util.List<String> getHistory(String type, Integer limit);
/**
* 获取历史消息记录(支持关键词搜索,在全部数据中匹配)
* @param type 消息类型request(请求) 或 response(响应)
* @param limit 返回数量上限默认200条
* @param keyword 搜索关键词,为空则按 limit 取最近 N 条
* @return 历史消息列表
*/
java.util.List<String> getHistory(String type, Integer limit, String keyword);
}

View File

@@ -63,5 +63,14 @@ public interface ISocialMediaService
* 删除提示词模板(恢复默认)
*/
com.ruoyi.common.core.domain.AjaxResult deletePromptTemplate(String key);
/**
* 根据标题(+可选型号备注生成闲鱼文案代下单、教你下单不依赖JD接口
*
* @param title 商品标题(必填)
* @param remark 型号/备注(可选)
* @return 包含代下单、教你下单两种文案的 Map
*/
Map<String, Object> generateXianyuWenan(String title, String remark);
}

View File

@@ -99,12 +99,13 @@ public interface ITencentDocService {
JSONObject getSheetList(String accessToken, String fileId);
/**
* 获取指定工作表的 rowTotal文档信息接口返回直接用于 range 上限
* 获取指定工作表的行数(优先返回 rowCount-实际有数据的行数,其次 rowTotal
* 用于替代 Redis 上次记录的行数,决定批量填充的操作范围
*
* @param accessToken 访问令牌
* @param fileId 文件ID
* @param sheetId 工作表ID
* @return rowTotal未找到或解析失败返回 0
* @return rowCount/rowTotal未找到或解析失败返回 0
*/
int getSheetRowTotal(String accessToken, String fileId, String sheetId);

View File

@@ -131,9 +131,9 @@ public class CommentServiceImpl implements ICommentService {
}
if (statMap != null) {
stats.setTotalCount(((Number) statMap.get("totalCount")).longValue());
stats.setAvailableCount(((Number) statMap.get("availableCount")).longValue());
stats.setUsedCount(((Number) statMap.get("usedCount")).longValue());
stats.setTotalCount(toLong(statMap.get("totalCount")));
stats.setAvailableCount(toLong(statMap.get("availableCount")));
stats.setUsedCount(toLong(statMap.get("usedCount")));
// 设置最后一条评论的创建时间
Object lastUpdateTime = statMap.get("lastCommentUpdateTime");
if (lastUpdateTime != null) {
@@ -439,5 +439,22 @@ public class CommentServiceImpl implements ICommentService {
return statistics;
}
/**
* 将 Map 中的统计值安全转为 longnull 或不存在时返回 0
*/
private static long toLong(Object value) {
if (value == null) {
return 0L;
}
if (value instanceof Number) {
return ((Number) value).longValue();
}
try {
return Long.parseLong(value.toString());
} catch (NumberFormatException e) {
return 0L;
}
}
}

View File

@@ -142,12 +142,16 @@ public class InstructionServiceImpl implements IInstructionService {
@Override
public List<String> getHistory(String type, Integer limit) {
return getHistory(type, limit, null);
}
@Override
public List<String> getHistory(String type, Integer limit, String keyword) {
if (stringRedisTemplate == null) {
return Collections.emptyList();
}
try {
// 确定Redis键
String key;
if ("request".equalsIgnoreCase(type)) {
key = "instruction:request";
@@ -157,11 +161,23 @@ public class InstructionServiceImpl implements IInstructionService {
return Collections.emptyList();
}
// 确定获取数量默认100条
int count = (limit != null && limit > 0) ? Math.min(limit, 1000) : 1000;
int maxReturn = (limit != null && limit > 0) ? Math.min(limit, 1000) : 200;
boolean hasKeyword = keyword != null && !keyword.trim().isEmpty();
String kwLower = hasKeyword ? keyword.trim().toLowerCase() : null;
// 从Redis获取历史消息索引0到count-1
List<String> messages = stringRedisTemplate.opsForList().range(key, 0, count - 1);
List<String> messages;
if (hasKeyword) {
// 搜索模式:取全部数据后在内存中按关键词过滤
messages = stringRedisTemplate.opsForList().range(key, 0, -1);
if (messages == null) messages = Collections.emptyList();
messages = messages.stream()
.filter(msg -> msg != null && msg.toLowerCase().contains(kwLower))
.limit(maxReturn)
.collect(Collectors.toList());
} else {
// 普通模式:只取最近 N 条
messages = stringRedisTemplate.opsForList().range(key, 0, maxReturn - 1);
}
return messages != null ? messages : Collections.emptyList();
} catch (Exception e) {

View File

@@ -52,6 +52,23 @@ public class SocialMediaServiceImpl implements ISocialMediaService
put("content:both", "通用文案生成提示词模板\n占位符%s - 商品名称,%s - 价格信息,%s - 关键词信息");
}};
/** 闲鱼文案-代下单(一键代下)固定正文 */
private static final String WENAN_ZCXS =
"\n\n 购买后,两小时内出库,物流会电话联系您,同时生成京东官方安装单。送装一体,无需担心。\n\n\n"
+ " 1:全新正品,原包装未拆封(京东商城代购,就近直发)\n"
+ " 2:可提供下单运单号与电子发票(发票在收到货后找我要)。\n"
+ " 3:收货时查看是否有质量或运损问题。可拍照让京东免费申请换新。\n"
+ " 4:价格有浮动,不支持补差价,谢谢理解。\n"
+ " 5:全国联保,全国统一安装标准。支持官方 400服务号查询假一赔十。\n ";
/** 闲鱼文案-教你下单固定正文(含“信息更新日期:”占位,生成时替换为当前日期) */
private static final String WENAN_FANAN_BX = "本人提供免费指导下单服务,一台也是团购价,细心指导\n" + "\n"
+ "【质量】官旗下单,包正的\n" + "【物流】您自己账户可跟踪24小时发货\n" + "【售后】您自己账户直接联系,无忧售后\n"
+ "【安装】专业人员安装,全程无需您操心\n" + "【价格】标价就是到手价,骑共享单车去酒吧,该省省该花花\n"
+ "【服务】手把手教您下单,有问题随时咨询\n" + "【体验】所有服务都是官旗提供,价格有内部渠道优惠,同品质更优惠!\n" + "\n"
+ "信息更新日期:\n" + "\n" + "捡漏价格不定时有变动,优惠不等人,发「省份+型号」免费咨询当日最低价!";
/** 标题/型号清洗:去掉营销敏感词 */
private static final String TITLE_CLEAN_REGEX = "以旧|政府|换新|领取|国家|补贴|15%|20%|国补|立减|【|】";
/**
* 提取商品标题关键词
*/
@@ -418,5 +435,40 @@ public class SocialMediaServiceImpl implements ISocialMediaService
return null;
}
}
/**
* 根据标题(+可选型号备注生成闲鱼文案代下单、教你下单不依赖JD接口
*/
@Override
public Map<String, Object> generateXianyuWenan(String title, String remark) {
Map<String, Object> result = new HashMap<>();
if (StringUtils.isEmpty(title) || StringUtils.isEmpty(title.trim())) {
result.put("success", false);
result.put("error", "商品标题不能为空");
return result;
}
String cleanTitle = cleanTitleOrRemark(title.trim());
String cleanRemark = StringUtils.isNotEmpty(remark) ? cleanTitleOrRemark(remark.trim()) : "";
String displayTitle = cleanTitle + cleanRemark;
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy年MM月dd日HH时mm分ss秒");
String format = sdf.format(new java.util.Date());
String wenanJiaonixiadan = WENAN_FANAN_BX.replace("信息更新日期:", "信息更新日期:" + format);
result.put("success", true);
result.put("daixiadan", "(一键代下) " + displayTitle + "\n" + WENAN_ZCXS);
result.put("jiaonixiadan", "【教你下单】 " + displayTitle + "\n" + wenanJiaonixiadan);
return result;
}
/**
* 清洗标题/型号中的敏感词
*/
private static String cleanTitleOrRemark(String text) {
if (text == null) {
return "";
}
return text.replaceAll(TITLE_CLEAN_REGEX, "");
}
}

View File

@@ -5,6 +5,7 @@ import com.ruoyi.jarvis.config.TencentDocConfig;
import com.ruoyi.jarvis.service.ITencentDocBatchPushService;
import com.ruoyi.jarvis.service.ITencentDocDelayedPushService;
import com.ruoyi.jarvis.service.ITencentDocTokenService;
import com.ruoyi.jarvis.service.ITencentDocService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
@@ -50,8 +51,14 @@ public class TencentDocDelayedPushServiceImpl implements ITencentDocDelayedPushS
@Autowired
private ITencentDocTokenService tokenService;
@Autowired
private ITencentDocService tencentDocService;
private ApplicationContext applicationContext;
private static final int READ_ROWS_WHEN_USE_ROW_TOTAL = 200;
private static final int MIN_START_ROW_WHEN_USE_ROW_TOTAL = 3;
/**
* 延迟时间(分钟),可通过配置文件修改
*/
@@ -267,32 +274,47 @@ public class TencentDocDelayedPushServiceImpl implements ITencentDocDelayedPushS
try {
log.info("开始执行批量同步...");
// 从 Redis 读取配置信息(用户通过前端配置页面设置
// 注意:使用与 TencentDocConfigController 相同的 key 前缀
// 从 Redis 读取 fileId、sheetId仅文档标识无行数缓存
final String CONFIG_KEY_PREFIX = "tencent:doc:auto:config:";
String fileId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "fileId");
String sheetId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "sheetId");
Integer startRow = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "startRow");
if (startRow == null) {
startRow = 3; // 默认值
}
log.info("读取配置 - fileId: {}, sheetId: {}, startRow: {}", fileId, sheetId, startRow);
if (StringUtils.isEmpty(fileId) || StringUtils.isEmpty(sheetId)) {
log.error("腾讯文档配置不完整无法执行批量同步。请先在前端配置页面设置文件ID和工作表ID");
return;
}
// 创建批量推送记录
// 每次从接口获取当前行数,取最后 200 行,无任何缓存
int batchStartRow;
int batchEndRow;
try {
String accessToken = tokenService.getValidAccessToken();
if (accessToken == null) {
log.warn("无法获取 accessToken跳过本次批量同步");
return;
}
int rowCount = tencentDocService.getSheetRowTotal(accessToken, fileId, sheetId);
if (rowCount <= 0) {
log.warn("接口未返回有效行数,跳过本次批量同步");
return;
}
batchStartRow = Math.max(MIN_START_ROW_WHEN_USE_ROW_TOTAL, rowCount - (READ_ROWS_WHEN_USE_ROW_TOTAL - 1));
batchEndRow = rowCount;
} catch (Exception e) {
log.warn("获取表格行数失败,跳过本次批量同步: {}", e.getMessage());
return;
}
log.info("本次范围(接口 rowCount 取最后200行- fileId: {}, sheetId: {}, {} ~ {}", fileId, sheetId, batchStartRow, batchEndRow);
// 创建批量推送记录(使用有效范围)
batchId = batchPushService.createBatchPushRecord(
fileId,
sheetId,
"AUTO",
"DELAYED_TIMER",
startRow,
startRow + 499 // 与 Controller API_MAX_ROWS_PER_REQUEST=500 一致单批最多500行
batchStartRow,
batchEndRow
);
log.info("✓ 创建批量推送记录批次ID: {}", batchId);
@@ -320,34 +342,7 @@ public class TencentDocDelayedPushServiceImpl implements ITencentDocDelayedPushS
Object result = method.invoke(controller, params);
log.info("✓ 批量同步执行完成,结果: {}", result);
// 将接口返回的 nextStartRow 写回 Redis下次定时执行从新起始行开始否则会一直从配置的 99 开始)
if (result instanceof java.util.Map) {
@SuppressWarnings("unchecked")
java.util.Map<String, Object> resultMap = (java.util.Map<String, Object>) result;
Object data = resultMap.get("data");
if (data instanceof java.util.Map) {
@SuppressWarnings("unchecked")
java.util.Map<String, Object> dataMap = (java.util.Map<String, Object>) data;
Object nextStartRowObj = dataMap.get("nextStartRow");
if (nextStartRowObj != null) {
int nextStartRow;
if (nextStartRowObj instanceof Number) {
nextStartRow = ((Number) nextStartRowObj).intValue();
} else {
try {
nextStartRow = Integer.parseInt(nextStartRowObj.toString());
} catch (NumberFormatException e) {
nextStartRow = -1;
}
}
if (nextStartRow >= 1) {
redisCache.setCacheObject(CONFIG_KEY_PREFIX + "startRow", nextStartRow, 180, TimeUnit.DAYS);
log.info("✓ 已更新下次起始行到 Redis: startRow={}(下次定时同步从第 {} 行开始)", nextStartRow, nextStartRow);
}
}
}
}
// 不再将 nextStartRow 写入 Redis下次定时执行时从接口获取 rowCount 决定范围
} catch (Exception ex) {
log.error("批量同步调用失败", ex);

View File

@@ -589,6 +589,8 @@ public class TencentDocServiceImpl implements ITencentDocService {
list = result.getJSONArray("sheets");
}
if (list == null) {
log.debug("getSheetRowTotal 未找到 properties/sheets 列表 - fileId: {}, sheetId: {}, resultKeys: {}",
fileId, sheetId, result != null ? result.keySet() : "null");
return 0;
}
for (int i = 0; i < list.size(); i++) {
@@ -598,9 +600,21 @@ public class TencentDocServiceImpl implements ITencentDocService {
continue;
}
String sid = props.getString("sheetId");
if (sheetId.equals(sid) && props.containsKey("rowTotal")) {
if (sid == null || !sheetId.equals(sid)) {
continue;
}
// 优先使用 rowCount表格实际有数据的行数用于操作范围其次 rowTotal表格容量上限
if (props.containsKey("rowCount")) {
return Math.max(0, props.getIntValue("rowCount"));
}
if (props.containsKey("rowTotal")) {
return Math.max(0, props.getIntValue("rowTotal"));
}
if (props.containsKey("row_total")) {
return Math.max(0, props.getIntValue("row_total"));
}
log.debug("getSheetRowTotal 找到 sheetId 但无 rowCount/rowTotal - fileId: {}, sheetId: {}, propsKeys: {}",
fileId, sheetId, props.keySet());
}
return 0;
} catch (Exception e) {