Compare commits

...

7 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
14 changed files with 255 additions and 155 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

@@ -66,7 +66,8 @@ public class OrderRowsController extends BaseController
TableDataInfo dataTable = getDataTable(list);
Date beginTime = getDateFromParams(orderRows.getParams(), "beginTime");
Date endTime = getDateFromParams(orderRows.getParams(), "endTime");
dataTable.setStatistics(buildStatistics(orderRows, beginTime, endTime));
// 与列表同数据源,不排除 isCount=0保证总订单数与分页 total 一致
dataTable.setStatistics(buildStatistics(orderRows, beginTime, endTime, true));
return dataTable;
}
@@ -172,21 +173,24 @@ public class OrderRowsController extends BaseController
*/
@GetMapping("/statistics")
public AjaxResult getStatistics(OrderRows orderRows, Date beginTime, Date endTime) {
return AjaxResult.success(buildStatistics(orderRows, beginTime, endTime));
return AjaxResult.success(buildStatistics(orderRows, beginTime, endTime, false));
}
/**
* 按列表相同条件构建统计数据(与 selectOrderRowsList 同条件日期、unionId 等,排除 isCount=0
* 构建统计数据。
* @param forList true=与列表同数据源(不排除 isCount=0保证总订单数与分页一致false=独立统计(排除 isCount=0
*/
private Map<String, Object> buildStatistics(OrderRows orderRows, Date beginTime, Date endTime) {
private Map<String, Object> buildStatistics(OrderRows orderRows, Date beginTime, Date endTime, boolean forList) {
Map<String, Object> result = new HashMap<>();
List<Long> excludeUnionIds = new ArrayList<>();
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) {
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) {
}
}
}
}

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 - 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 = 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("使用接口 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) {
// 配置/缓存的起始行超出表尾时,必须忽略 effectiveStartRow按 rowTotal 回溯
startRow = Math.max(MIN_START_ROW_WHEN_USE_ROW_TOTAL, rowTotal - READ_ROWS_WHEN_USE_ROW_TOTAL);
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格式
@@ -1164,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 };
@@ -1177,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);
@@ -1207,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) {
@@ -1245,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);
@@ -1254,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);
@@ -1745,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(
@@ -1759,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;
@@ -27,6 +31,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()
@@ -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,45 +274,38 @@ 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);
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(