Compare commits
15 Commits
7ed5a76d2f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4407487fbf | ||
|
|
465e0993d6 | ||
|
|
0a20825831 | ||
|
|
b4749f3516 | ||
|
|
dc8b0b2fcf | ||
|
|
f321e40876 | ||
|
|
a58891ef04 | ||
|
|
26f6f6e058 | ||
|
|
0b0b431e95 | ||
|
|
4c07dda3d7 | ||
|
|
549224a83f | ||
|
|
c1484ecbfd | ||
|
|
e63ff7522e | ||
|
|
72dee7fc16 | ||
|
|
7367e28133 |
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,31 +169,33 @@ 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<>();
|
||||
public AjaxResult getStatistics(OrderRows orderRows, Date beginTime, Date endTime) {
|
||||
return AjaxResult.success(buildStatistics(orderRows, beginTime, endTime, false));
|
||||
}
|
||||
|
||||
// 获取superAdminList,筛选出isCount = 0的unionId
|
||||
/**
|
||||
* 构建统计数据。
|
||||
* @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);
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
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"));
|
||||
@@ -180,7 +205,6 @@ public AjaxResult getStatistics(OrderRows orderRows, Date beginTime, Date endTim
|
||||
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"));
|
||||
@@ -190,44 +214,40 @@ public AjaxResult getStatistics(OrderRows orderRows, Date beginTime, Date endTim
|
||||
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 isCancel = "3".equals(validCode);
|
||||
boolean isIllegal = "25".equals(validCode) || "26".equals(validCode)
|
||||
|| "27".equals(validCode) || "28".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; // 违规订单的实际费用等于计算的佣金
|
||||
actualFeeAmount = commissionAmount;
|
||||
} else if (row.getEstimateFee() != null) {
|
||||
commissionAmount = row.getEstimateFee();
|
||||
actualFeeAmount = commissionAmount;
|
||||
}
|
||||
}
|
||||
// 取消订单:如果actualFee为空或0,则使用公式计算
|
||||
else if (isCancel) {
|
||||
} else if (isCancel) {
|
||||
if (row.getActualFee() != null && row.getActualFee() > 0) {
|
||||
actualFeeAmount = row.getActualFee();
|
||||
commissionAmount = row.getEstimateFee() != null ? row.getEstimateFee() : 0;
|
||||
@@ -238,9 +258,7 @@ public AjaxResult getStatistics(OrderRows orderRows, Date beginTime, Date endTim
|
||||
commissionAmount = row.getEstimateFee() != null ? row.getEstimateFee() : 0;
|
||||
actualFeeAmount = row.getActualFee() != null ? row.getActualFee() : 0;
|
||||
}
|
||||
}
|
||||
// 其他订单:使用原有的字段值
|
||||
else {
|
||||
} else {
|
||||
commissionAmount = row.getEstimateFee() != null ? row.getEstimateFee() : 0;
|
||||
actualFeeAmount = row.getActualFee() != null ? row.getActualFee() : 0;
|
||||
}
|
||||
@@ -248,11 +266,9 @@ public AjaxResult getStatistics(OrderRows orderRows, Date beginTime, Date endTim
|
||||
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)) {
|
||||
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);
|
||||
@@ -260,8 +276,6 @@ public AjaxResult getStatistics(OrderRows orderRows, Date beginTime, Date endTim
|
||||
if (row.getSkuNum() != null) {
|
||||
stat.put("skuNum", (Long) stat.get("skuNum") + row.getSkuNum());
|
||||
}
|
||||
|
||||
// 统计违规订单
|
||||
if ("illegal".equals(group.getKey())) {
|
||||
violationOrders++;
|
||||
violationCommission += commissionAmount;
|
||||
@@ -273,15 +287,15 @@ public AjaxResult getStatistics(OrderRows orderRows, Date beginTime, Date endTim
|
||||
}
|
||||
|
||||
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 AjaxResult.success(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
} else {
|
||||
// 进度在阈值范围内,下次从配置起始行开始
|
||||
nextStartRow = startRow;
|
||||
progressHint = String.format("已读取到第 %d 行,下次将从第 %d 行重新开始",
|
||||
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", progressHint);
|
||||
config.put("progressHint", String.format("表格当前 %d 行(接口获取),每次同步取最后200行:第 %d ~ %d 行", rowCount, nextStartRow, rowCount));
|
||||
} else {
|
||||
config.put("currentProgress", null);
|
||||
config.put("nextStartRow", startRow);
|
||||
config.put("progressHint", String.format("尚未开始同步,将从第 %d 行开始", startRow));
|
||||
config.put("progressHint", "未获取到行数");
|
||||
}
|
||||
} else {
|
||||
config.put("currentProgress", null);
|
||||
config.put("nextStartRow", startRow);
|
||||
config.put("progressHint", "未授权,无法获取表格行数");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("获取 rowCount 失败: {}", e.getMessage());
|
||||
config.put("currentProgress", null);
|
||||
config.put("nextStartRow", 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());
|
||||
|
||||
@@ -54,11 +54,12 @@ 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,一次拉 500 行) */
|
||||
private static final int API_MAX_ROWS_PER_REQUEST = 500;
|
||||
/** 单次请求最大行数(腾讯文档 API:行数≤1000) */
|
||||
private static final int API_MAX_ROWS_PER_REQUEST = 200;
|
||||
/** 用 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 - 回溯,用于覆盖物流变更或延后补填 */
|
||||
@@ -940,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;
|
||||
|
||||
@@ -965,85 +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 - 199,endRow = 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 计算本次同步的起始行和结束行(完美推送回溯)
|
||||
// 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);
|
||||
} else {
|
||||
startRow = effectiveStartRow;
|
||||
endRow = startRow + API_MAX_ROWS_PER_REQUEST - 1;
|
||||
log.info("首次同步或配置已重置,从第 {} 行开始,到第 {} 行(单批最多 {} 行)", startRow, endRow, API_MAX_ROWS_PER_REQUEST);
|
||||
}
|
||||
|
||||
// 严格限制单次 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);
|
||||
}
|
||||
|
||||
log.info("开始填充物流链接 - 文件ID: {}, 工作表ID: {}, 起始行: {}, 结束行: {}, 上次最大行: {}",
|
||||
fileId, sheetId, startRow, endRow, lastMaxRow);
|
||||
log.info("同步物流 - fileId: {}, sheetId: {}, 接口行数: {}, 本次范围: 第 {} ~ {} 行(共 {} 行)",
|
||||
fileId, sheetId, rowCount, startRow, endRow, endRow - startRow + 1);
|
||||
|
||||
// 读取表格数据(先读取表头行用于识别列位置)
|
||||
// 根据官方文档,使用 A1 表示法(Excel格式)
|
||||
@@ -1156,21 +1091,23 @@ 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 行,严格限制单次行数,失败时逐步缩小范围重试
|
||||
// 腾讯文档 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 };
|
||||
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;
|
||||
}
|
||||
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);
|
||||
@@ -1200,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) {
|
||||
@@ -1235,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);
|
||||
@@ -1734,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);
|
||||
@@ -2124,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;
|
||||
@@ -2423,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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,17 @@ public interface ITencentDocService {
|
||||
*/
|
||||
JSONObject getSheetList(String accessToken, String fileId);
|
||||
|
||||
/**
|
||||
* 获取指定工作表的行数(优先返回 rowCount-实际有数据的行数,其次 rowTotal)
|
||||
* 用于替代 Redis 上次记录的行数,决定批量填充的操作范围
|
||||
*
|
||||
* @param accessToken 访问令牌
|
||||
* @param fileId 文件ID
|
||||
* @param sheetId 工作表ID
|
||||
* @return rowCount/rowTotal,未找到或解析失败返回 0
|
||||
*/
|
||||
int getSheetRowTotal(String accessToken, String fileId, String sheetId);
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*
|
||||
|
||||
@@ -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 中的统计值安全转为 long,null 或不存在时返回 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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, "");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -562,6 +562,67 @@ 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) {
|
||||
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++) {
|
||||
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 (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) {
|
||||
log.warn("获取工作表 rowTotal 失败 - fileId: {}, sheetId: {}", fileId, sheetId, e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject getUserInfo(String accessToken) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user