Compare commits

...

5 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
13 changed files with 237 additions and 143 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

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

@@ -96,7 +96,7 @@ public class TencentDocConfigController extends BaseController {
config.put("appId", tencentDocConfig.getAppId());
config.put("apiBaseUrl", tencentDocConfig.getApiBaseUrl());
// 从接口获取 rowCount(匹配 sheetId 的表格实际有数据的行数),替代 Redis 上次记录的行数
// 从接口获取 rowCount 用于展示无任何进度缓存与填充逻辑一致每次取最后200行
if (fileId != null && !fileId.isEmpty() && sheetId != null && !sheetId.isEmpty()) {
try {
String accessToken = tencentDocTokenService.getValidAccessToken();
@@ -104,14 +104,13 @@ public class TencentDocConfigController extends BaseController {
int rowCount = tencentDocService.getSheetRowTotal(accessToken, fileId, sheetId);
if (rowCount > 0) {
config.put("currentProgress", rowCount);
int effectiveStart = startRow != null ? startRow : 3;
int nextStartRow = Math.max(effectiveStart, rowCount - 199); // 与填充逻辑一致含最后一行共200行
int nextStartRow = Math.max(3, rowCount - 199); // 与填充逻辑一致取最后200行
config.put("nextStartRow", nextStartRow);
config.put("progressHint", String.format("表格当前 %d 行数据(从接口获取),下次将从第 %d 行开始", rowCount, 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", String.format("未获取到 rowCount将从第 %d 行开始", startRow));
config.put("progressHint", "未获取到行数");
}
} else {
config.put("currentProgress", null);

View File

@@ -941,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;
@@ -966,91 +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 存储进度,改为每次从接口获取 rowCount匹配 sheetId 的表格实际有数据的行数)
int effectiveStartRow = configStartRow != null ? configStartRow : (headerRow + 1);
int startRow;
int endRow;
// 从接口获取 rowCount匹配 sheetId 的表格实际有数据的行数),替代 Redis 上次记录的行数
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 = rowCount - 199使 endRow 能取到 rowCount不漏最后一行共 200 行含最后一行)
startRow = Math.max(MIN_START_ROW_WHEN_USE_ROW_TOTAL,
Math.max(effectiveStartRow, rowTotal - (READ_ROWS_WHEN_USE_ROW_TOTAL - 1)));
endRow = Math.min(rowTotal, startRow + READ_ROWS_WHEN_USE_ROW_TOTAL - 1);
}
log.info("使用接口 rowCount={},本次范围: 第 {} ~ {} 行(共 {} 行,单次最多 {} 行)",
rowTotal, startRow, endRow, endRow - startRow + 1, READ_ROWS_WHEN_USE_ROW_TOTAL);
} else {
// 未取到 rowCount 时使用配置起始行(不再从 Redis 读取进度)
if (forceStartRow != null) {
startRow = forceStartRow;
endRow = startRow + API_MAX_ROWS_PER_REQUEST - 1;
log.info("使用强制指定的起始行: {}, 结束行: {}(未取到 rowCount", startRow, endRow);
} else {
startRow = effectiveStartRow;
endRow = startRow + API_MAX_ROWS_PER_REQUEST - 1;
log.info("从配置起始行开始: {} ~ {}(未取到 rowCount", 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);
}
// 若之前未取到 rowTotal 或来自 fallback再次尝试并用 sheet 最大行硬性限制,避免读到 348 超出 324 行
if (rowTotal <= 0) {
rowTotal = tencentDocService.getSheetRowTotal(accessToken, fileId, sheetId);
}
if (rowTotal > 0) {
if (startRow > rowTotal) {
// 按 rowCount 回溯,且保证 endRow=rowCount 不漏最后一行rowCount - 199 起共 200 行)
startRow = Math.max(MIN_START_ROW_WHEN_USE_ROW_TOTAL, rowTotal - (READ_ROWS_WHEN_USE_ROW_TOTAL - 1));
endRow = Math.min(rowTotal, startRow + READ_ROWS_WHEN_USE_ROW_TOTAL - 1);
log.info("按 rowCount={} 修正范围(含最后一行): 第 {} ~ {} 行", rowTotal, startRow, endRow);
} else if (endRow > rowTotal) {
endRow = rowTotal;
log.info("按 rowTotal={} 截断结束行: endRow={}", rowTotal, endRow);
}
} else {
// 未取到 rowTotal 时:若 startRow 过高(如 350 超出实际 324 行)可能是配置/Redis 过时,回溯至配置最小值
final int START_ROW_SAFE_THRESHOLD = 350;
if (startRow > START_ROW_SAFE_THRESHOLD) {
int oldStart = startRow;
startRow = Math.max(MIN_START_ROW_WHEN_USE_ROW_TOTAL, headerRow != null ? headerRow + 1 : 3);
endRow = startRow + READ_ROWS_WHEN_USE_ROW_TOTAL - 1;
log.warn("rowTotal 未获取且 startRow={} 过高,保守回溯至表头下一行 {},范围: {} ~ {}", oldStart, startRow, startRow, endRow);
}
}
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格式
@@ -1247,7 +1175,7 @@ public class TencentDocController extends BaseController {
// 无数据时计算下次起始行(不再写入 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));
log.info("本批无数据,下次起始行: {}(不再保存进度,下次从接口获取 rowCount", emptyNextStart);
@@ -1256,7 +1184,7 @@ public class TencentDocController extends BaseController {
result.put("endRow", effectiveEndRow);
result.put("lastMaxRow", null); // 不再使用 Redis 存储,改为从接口获取 rowCount
result.put("nextStartRow", emptyNextStart);
result.put("rowTotal", rowTotal > 0 ? rowTotal : null);
result.put("rowTotal", rowCount > 0 ? rowCount : null);
result.put("currentMaxRow", emptyCurrentMax);
result.put("filledCount", 0);
result.put("skippedCount", 0);
@@ -1747,7 +1675,7 @@ public class TencentDocController extends BaseController {
int currentMaxRow = (maxSuccessRow > 0) ? maxSuccessRow : effectiveEndRow;
// 整批都跳过时用较小回溯,尽快扫到后续行;有写入时用完整回溯覆盖物流变更
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));
String nextSyncHint = String.format(
@@ -1761,7 +1689,7 @@ public class TencentDocController extends BaseController {
result.put("currentMaxRow", currentMaxRow);
result.put("lastMaxRow", null); // 不再使用 Redis 存储,改为从接口获取 rowCount
result.put("nextStartRow", nextStartRow);
result.put("rowTotal", rowTotal > 0 ? rowTotal : null);
result.put("rowTotal", rowCount > 0 ? rowCount : null);
result.put("filledCount", filledCount);
result.put("skippedCount", skippedCount);
result.put("errorCount", errorCount);

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

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

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

@@ -274,46 +274,39 @@ 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 = MIN_START_ROW_WHEN_USE_ROW_TOTAL;
}
int batchStartRow = startRow;
int batchEndRow = startRow + READ_ROWS_WHEN_USE_ROW_TOTAL - 1;
// 先获取 rowTotal确保 batch 记录使用有效范围(避免 350 超出 324 行)
try {
String accessToken = tokenService.getValidAccessToken();
if (accessToken != null) {
int rowTotal = tencentDocService.getSheetRowTotal(accessToken, fileId, sheetId);
if (rowTotal > 0) {
if (startRow > rowTotal) {
batchStartRow = Math.max(MIN_START_ROW_WHEN_USE_ROW_TOTAL, rowTotal - (READ_ROWS_WHEN_USE_ROW_TOTAL - 1));
batchEndRow = Math.min(rowTotal, batchStartRow + READ_ROWS_WHEN_USE_ROW_TOTAL - 1);
log.info("配置起始行 {} 超出表尾 rowTotal={},修正为第 {} ~ {} 行", startRow, rowTotal, batchStartRow, batchEndRow);
} else {
batchEndRow = Math.min(rowTotal, startRow + READ_ROWS_WHEN_USE_ROW_TOTAL - 1);
}
}
}
} catch (Exception e) {
log.warn("获取 rowTotal 失败,使用配置起始行创建 batch: {}", e.getMessage());
}
log.info("读取配置 - fileId: {}, sheetId: {}, 创建 batch 范围: {} ~ {}", fileId, sheetId, batchStartRow, batchEndRow);
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,