This commit is contained in:
van
2026-04-11 22:35:39 +08:00
parent 5205d8c155
commit 94f319514e
14 changed files with 296 additions and 30 deletions

View File

@@ -55,6 +55,9 @@ public class TencentDocController extends BaseController {
@Autowired @Autowired
private com.ruoyi.jarvis.service.ITencentDocDelayedPushService delayedPushService; private com.ruoyi.jarvis.service.ITencentDocDelayedPushService delayedPushService;
@Autowired
private com.ruoyi.jarvis.wecom.WxSendGoofishNotifyClient wxSendGoofishNotifyClient;
/** 单次请求最大行数(腾讯文档 API行数≤1000 */ /** 单次请求最大行数(腾讯文档 API行数≤1000 */
private static final int API_MAX_ROWS_PER_REQUEST = 200; private static final int API_MAX_ROWS_PER_REQUEST = 200;
/** 用 rowTotal 时接口实际单次只能读 200 行 */ /** 用 rowTotal 时接口实际单次只能读 200 行 */
@@ -938,6 +941,7 @@ public class TencentDocController extends BaseController {
@Anonymous @Anonymous
@PostMapping("/fillLogisticsByOrderNo") @PostMapping("/fillLogisticsByOrderNo")
public AjaxResult fillLogisticsByOrderNo(@RequestBody Map<String, Object> params) { public AjaxResult fillLogisticsByOrderNo(@RequestBody Map<String, Object> params) {
String batchId = null;
try { try {
// 直接尝试刷新token如果失败说明需要首次授权 // 直接尝试刷新token如果失败说明需要首次授权
String accessToken; String accessToken;
@@ -955,7 +959,7 @@ public class TencentDocController extends BaseController {
} }
// 从参数获取批次ID如果是批量调用会传入 // 从参数获取批次ID如果是批量调用会传入
String batchId = params.get("batchId") != null ? String.valueOf(params.get("batchId")) : null; batchId = params.get("batchId") != null ? String.valueOf(params.get("batchId")) : null;
// 从参数或配置中获取文档信息 // 从参数或配置中获取文档信息
String fileId = (String) params.get("fileId"); String fileId = (String) params.get("fileId");
@@ -1920,7 +1924,7 @@ public class TencentDocController extends BaseController {
if (errorCount > 0 && successUpdates == 0) { if (errorCount > 0 && successUpdates == 0) {
status = "FAILED"; status = "FAILED";
} else if (errorCount > 0) { } else if (errorCount > 0) {
status = "PARTIAL_SUCCESS"; status = "PARTIAL";
} }
batchPushService.updateBatchPushRecord(batchId, status, successUpdates, skippedCount, errorCount, batchPushService.updateBatchPushRecord(batchId, status, successUpdates, skippedCount, errorCount,
message, null); message, null);
@@ -1948,7 +1952,21 @@ public class TencentDocController extends BaseController {
return AjaxResult.success("填充物流链接完成", result); return AjaxResult.success("填充物流链接完成", result);
} catch (Exception e) { } catch (Exception e) {
log.error("填充物流链接失败", e); log.error("填充物流链接失败", e);
return AjaxResult.error("填充物流链接失败: " + e.getMessage()); String errMsg = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
if (batchId != null && !batchId.trim().isEmpty()) {
try {
batchPushService.updateBatchPushRecord(batchId, "FAILED", 0, 0, 0, null, errMsg);
} catch (Exception ex) {
log.error("异常后更新批量推送记录失败 batchId={}", batchId, ex);
}
try {
wxSendGoofishNotifyClient.pushGoofishAgentText(null,
"【腾讯文档推送】批量同步异常\n批次: " + batchId + "\n" + errMsg);
} catch (Exception ex) {
log.warn("腾讯文档推送异常企微通知失败: {}", ex.toString());
}
}
return AjaxResult.error("填充物流链接失败: " + errMsg);
} }
} }
@@ -2093,6 +2111,10 @@ public class TencentDocController extends BaseController {
String fileId = tencentDocConfig.getFileId(); String fileId = tencentDocConfig.getFileId();
String sheetId = tencentDocConfig.getSheetId(); String sheetId = tencentDocConfig.getSheetId();
if (fileId != null && !fileId.trim().isEmpty()) {
batchPushService.reconcileStaleRunningRecords(fileId);
}
if (fileId != null && sheetId != null) { if (fileId != null && sheetId != null) {
com.ruoyi.jarvis.domain.TencentDocBatchPushRecord lastSuccess = com.ruoyi.jarvis.domain.TencentDocBatchPushRecord lastSuccess =
batchPushService.getLastSuccessRecord(fileId, sheetId); batchPushService.getLastSuccessRecord(fileId, sheetId);

View File

@@ -5,6 +5,7 @@ import com.alibaba.fastjson2.JSONObject;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
@@ -12,6 +13,7 @@ import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.framework.web.domain.Server; import com.ruoyi.framework.web.domain.Server;
import com.ruoyi.jarvis.service.ILogisticsService; import com.ruoyi.jarvis.service.ILogisticsService;
import com.ruoyi.jarvis.service.IWxSendService; import com.ruoyi.jarvis.service.IWxSendService;
import com.ruoyi.jarvis.wecom.WxSendGoofishNotifyClient;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.HashMap; import java.util.HashMap;
@@ -32,6 +34,9 @@ public class ServerController
@Resource @Resource
private IWxSendService wxSendService; private IWxSendService wxSendService;
@Resource
private WxSendGoofishNotifyClient wxSendGoofishNotifyClient;
/** Ollama 服务地址,用于健康检查 */ /** Ollama 服务地址,用于健康检查 */
@Value("${jarvis.ollama.base-url:http://192.168.8.34:11434}") @Value("${jarvis.ollama.base-url:http://192.168.8.34:11434}")
private String ollamaBaseUrl; private String ollamaBaseUrl;
@@ -72,23 +77,23 @@ public class ServerController
healthMap.put("logistics", logisticsMap); healthMap.put("logistics", logisticsMap);
} }
// 微信推送服务健康检测 // 微信推送:不在此自动下发消息,仅展示配置地址;真实检测见 POST /monitor/server/health/wx-send-test
try { Map<String, Object> wxSendMap = new HashMap<>();
IWxSendService.HealthCheckResult wxSendHealth = wxSendService.checkHealth(); wxSendMap.put("manualOnly", true);
Map<String, Object> wxSendMap = new HashMap<>(); wxSendMap.put("healthy", null);
wxSendMap.put("healthy", wxSendHealth.isHealthy()); wxSendMap.put("status", "未检测");
wxSendMap.put("status", wxSendHealth.getStatus()); wxSendMap.put("message", "点击「测试」将发送一条健康检查消息(会真实推送到微信)");
wxSendMap.put("message", wxSendHealth.getMessage()); wxSendMap.put("serviceUrl", wxSendService.getHealthCheckServiceUrl());
wxSendMap.put("serviceUrl", wxSendHealth.getServiceUrl()); healthMap.put("wxSend", wxSendMap);
healthMap.put("wxSend", wxSendMap);
} catch (Exception e) { // 企微闲鱼通知:仅展示接口地址;真实检测见 POST /monitor/server/health/goofish-notify-test
Map<String, Object> wxSendMap = new HashMap<>(); Map<String, Object> goofishMap = new HashMap<>();
wxSendMap.put("healthy", false); goofishMap.put("manualOnly", true);
wxSendMap.put("status", "异常"); goofishMap.put("healthy", null);
wxSendMap.put("message", "健康检测异常: " + e.getMessage()); goofishMap.put("status", "未检测");
wxSendMap.put("serviceUrl", ""); goofishMap.put("message", "点击「测试」将经 wxSend 向企微闲鱼应用发送一条测试文本");
healthMap.put("wxSend", wxSendMap); goofishMap.put("serviceUrl", wxSendGoofishNotifyClient.getGoofishPushEndpointDisplay());
} healthMap.put("goofishNotify", goofishMap);
// Ollama 服务健康检测(调试用) // Ollama 服务健康检测(调试用)
try { try {
@@ -115,6 +120,54 @@ public class ServerController
return AjaxResult.success(healthMap); return AjaxResult.success(healthMap);
} }
/**
* 手动测试微信推送(会真实下发一条消息)
*/
@PreAuthorize("@ss.hasPermi('monitor:server:list')")
@PostMapping("/health/wx-send-test")
public AjaxResult testWxSendHealth() {
try {
IWxSendService.HealthCheckResult r = wxSendService.checkHealth();
Map<String, Object> m = new HashMap<>();
m.put("manualOnly", true);
m.put("healthy", r.isHealthy());
m.put("status", r.getStatus());
m.put("message", r.getMessage());
m.put("serviceUrl", r.getServiceUrl());
return AjaxResult.success(m);
} catch (Exception e) {
Map<String, Object> m = new HashMap<>();
m.put("manualOnly", true);
m.put("healthy", false);
m.put("status", "异常");
m.put("message", "检测异常: " + e.getMessage());
m.put("serviceUrl", wxSendService.getHealthCheckServiceUrl());
return AjaxResult.success(m);
}
}
/**
* 手动测试企微闲鱼通知(经 wxSend goofish-active-push
*/
@PreAuthorize("@ss.hasPermi('monitor:server:list')")
@PostMapping("/health/goofish-notify-test")
public AjaxResult testGoofishNotify() {
String err = wxSendGoofishNotifyClient.testGoofishNotify();
Map<String, Object> m = new HashMap<>();
m.put("manualOnly", true);
m.put("serviceUrl", wxSendGoofishNotifyClient.getGoofishPushEndpointDisplay());
if (err == null) {
m.put("healthy", true);
m.put("status", "正常");
m.put("message", "闲鱼通知测试消息已发送");
} else {
m.put("healthy", false);
m.put("status", "异常");
m.put("message", err);
}
return AjaxResult.success(m);
}
private void putOllamaUnhealthy(Map<String, Object> healthMap, String url, String message) { private void putOllamaUnhealthy(Map<String, Object> healthMap, String url, String message) {
Map<String, Object> ollamaMap = new HashMap<>(); Map<String, Object> ollamaMap = new HashMap<>();

View File

@@ -288,6 +288,8 @@ jarvis:
pull-max-update-time-range-seconds: 15552000 pull-max-update-time-range-seconds: 15552000
# 全量拉单起点:距今多少天(默认约 3 年) # 全量拉单起点:距今多少天(默认约 3 年)
pull-full-history-days: 1095 pull-full-history-days: 1095
# 列表拉单是否仅拉 auto-ship-order-statuses默认待发货false 时恢复按时间窗全状态(全量回补可临时关闭)
pull-list-only-auto-ship-statuses: true
auto-ship-batch-size: 20 auto-ship-batch-size: 20

View File

@@ -58,6 +58,13 @@ public class JarvisGoofishProperties {
*/ */
private String autoShipOrderStatuses = "12"; private String autoShipOrderStatuses = "12";
/**
* 定时/增量「订单列表」拉单是否仅请求 {@link #autoShipOrderStatuses} 中的状态(通常即待发货)。
* 为 true 时可显著减少列表与后续详情拉取次数;其余状态依赖开放平台推送回调刷新。
* 全量历史回补若需全状态,可临时设为 false。
*/
private boolean pullListOnlyAutoShipStatuses = true;
/** /**
* 未在 erp_open_config 配置 express_code 时,自动发货使用的默认快递公司编码(官方列表中日日顺多为 rrs * 未在 erp_open_config 配置 express_code 时,自动发货使用的默认快递公司编码(官方列表中日日顺多为 rrs
*/ */

View File

@@ -59,7 +59,7 @@ public class TencentDocBatchPushRecord extends BaseEntity {
/** 错误数量 */ /** 错误数量 */
private Integer errorCount; private Integer errorCount;
/** 状态RUNNING-执行中SUCCESS-成功PARTIAL-部分成功FAILED-失败 */ /** 状态RUNNING-执行中SUCCESS-成功PARTIAL-部分成功FAILED-失败INTERRUPTED-已中断(超时/未正常结束) */
private String status; private String status;
/** 结果消息 */ /** 结果消息 */

View File

@@ -3,6 +3,7 @@ package com.ruoyi.jarvis.mapper;
import com.ruoyi.jarvis.domain.TencentDocBatchPushRecord; import com.ruoyi.jarvis.domain.TencentDocBatchPushRecord;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List; import java.util.List;
/** /**
@@ -41,5 +42,11 @@ public interface TencentDocBatchPushRecordMapper {
*/ */
TencentDocBatchPushRecord selectLastSuccessRecord(@Param("fileId") String fileId, TencentDocBatchPushRecord selectLastSuccessRecord(@Param("fileId") String fileId,
@Param("sheetId") String sheetId); @Param("sheetId") String sheetId);
/**
* 仍为 RUNNING 且开始时间早于指定时间的批次(用于超时归档)
*/
List<TencentDocBatchPushRecord> selectRunningRecordsBefore(@Param("fileId") String fileId,
@Param("beforeTime") Date beforeTime);
} }

View File

@@ -41,5 +41,10 @@ public interface ITencentDocBatchPushService {
* 获取推送状态和倒计时信息 * 获取推送状态和倒计时信息
*/ */
Map<String, Object> getPushStatusAndCountdown(); Map<String, Object> getPushStatusAndCountdown();
/**
* 将长时间仍处于 RUNNING 的批次归档为 INTERRUPTED并可选发企微告警见实现类配置
*/
void reconcileStaleRunningRecords(String fileId);
} }

View File

@@ -5,10 +5,15 @@ package com.ruoyi.jarvis.service;
*/ */
public interface IWxSendService { public interface IWxSendService {
/** /**
* 检查微信推送服务健康状态 * 检查微信推送服务健康状态(会真实下发一条测试消息,仅用于服务监控页「手动测试」)
* @return 健康状态信息,包含是否健康、状态描述等 * @return 健康状态信息,包含是否健康、状态描述等
*/ */
HealthCheckResult checkHealth(); HealthCheckResult checkHealth();
/**
* 已配置的微信推送健康检查 URL展示用不发起请求
*/
String getHealthCheckServiceUrl();
/** /**
* 健康检测结果 * 健康检测结果

View File

@@ -73,7 +73,11 @@ public class GoofishOrderPipeline {
ErpGoofishOrder row = upsertFromNotify(appKey, item, lastNotifyJson, "LIST_UPSERT"); ErpGoofishOrder row = upsertFromNotify(appKey, item, lastNotifyJson, "LIST_UPSERT");
tryLinkJdOrder(row); tryLinkJdOrder(row);
mergeSummaryFromOrderDetailShape(row, item, "LIST"); mergeSummaryFromOrderDetailShape(row, item, "LIST");
refreshDetail(row); // 仅待发货等需判物流/发货的状态拉详情;其它状态由推送回调更新,减轻开放平台与本地压力
List<Integer> awaitingShip = resolveAutoShipOrderStatuses();
if (row.getOrderStatus() != null && awaitingShip.contains(row.getOrderStatus())) {
refreshDetail(row);
}
syncWaybillFromRedis(row); syncWaybillFromRedis(row);
tryAutoShip(row); tryAutoShip(row);
} }
@@ -903,6 +907,36 @@ public class GoofishOrderPipeline {
log.warn("闲管家拉单: pull-max-pages 与 pull-page-size 乘积超过 10000已收敛 maxPages={}", maxPages); log.warn("闲管家拉单: pull-max-pages 与 pull-page-size 乘积超过 10000已收敛 maxPages={}", maxPages);
} }
int saved = 0; int saved = 0;
List<Integer> listStatusFilters = goofishProperties.isPullListOnlyAutoShipStatuses()
? resolveAutoShipOrderStatuses()
: null;
if (listStatusFilters != null && listStatusFilters.isEmpty()) {
listStatusFilters = null;
}
if (listStatusFilters == null) {
saved += pullForAppKeyUpdateTimeRangeOnceForStatuses(appKey, cred, authorizeIds, updateTimeStartSec,
updateTimeEndSec, pageSize, maxPages, null);
} else {
for (Integer st : listStatusFilters) {
saved += pullForAppKeyUpdateTimeRangeOnceForStatuses(appKey, cred, authorizeIds, updateTimeStartSec,
updateTimeEndSec, pageSize, maxPages, st);
try {
Thread.sleep(100);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
return saved;
}
/**
* @param orderStatusFilter null 表示列表请求不带 order_status拉全状态
*/
private int pullForAppKeyUpdateTimeRangeOnceForStatuses(String appKey, IERPAccount cred, List<Long> authorizeIds,
long updateTimeStartSec, long updateTimeEndSec, int pageSize, int maxPages, Integer orderStatusFilter) {
int saved = 0;
for (Long aid : authorizeIds) { for (Long aid : authorizeIds) {
int page = 1; int page = 1;
while (page <= maxPages) { while (page <= maxPages) {
@@ -910,6 +944,9 @@ public class GoofishOrderPipeline {
q.setAuthorizeId(aid); q.setAuthorizeId(aid);
q.setUpdateTime(updateTimeStartSec, updateTimeEndSec); q.setUpdateTime(updateTimeStartSec, updateTimeEndSec);
q.setPage(page, pageSize); q.setPage(page, pageSize);
if (orderStatusFilter != null) {
q.setOrderStatus(orderStatusFilter);
}
String resp; String resp;
try { try {
resp = q.getResponseBody(); resp = q.getResponseBody();
@@ -936,8 +973,8 @@ public class GoofishOrderPipeline {
break; break;
} }
if (page == maxPages) { if (page == maxPages) {
log.warn("闲管家拉单已达最大页数 appKey={} aid={} 区间[{},{}];若订单更多请缩小 pull-time-chunk-seconds", log.warn("闲管家拉单已达最大页数 appKey={} aid={} 区间[{},{}] orderStatus={};若订单更多请缩小 pull-time-chunk-seconds",
appKey, aid, updateTimeStartSec, updateTimeEndSec); appKey, aid, updateTimeStartSec, updateTimeEndSec, orderStatusFilter);
break; break;
} }
page++; page++;

View File

@@ -6,6 +6,10 @@ import com.ruoyi.jarvis.domain.TencentDocOperationLog;
import com.ruoyi.jarvis.mapper.TencentDocBatchPushRecordMapper; import com.ruoyi.jarvis.mapper.TencentDocBatchPushRecordMapper;
import com.ruoyi.jarvis.mapper.TencentDocOperationLogMapper; import com.ruoyi.jarvis.mapper.TencentDocOperationLogMapper;
import com.ruoyi.jarvis.service.ITencentDocBatchPushService; import com.ruoyi.jarvis.service.ITencentDocBatchPushService;
import com.ruoyi.jarvis.wecom.WxSendGoofishNotifyClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
@@ -18,6 +22,10 @@ import java.util.concurrent.TimeUnit;
@Service @Service
public class TencentDocBatchPushServiceImpl implements ITencentDocBatchPushService { public class TencentDocBatchPushServiceImpl implements ITencentDocBatchPushService {
private static final Logger log = LoggerFactory.getLogger(TencentDocBatchPushServiceImpl.class);
private static final String REDIS_STALE_BATCH_NOTIFY_KEY = "tendoc:batch:stale-notified:";
@Resource @Resource
private TencentDocBatchPushRecordMapper batchPushRecordMapper; private TencentDocBatchPushRecordMapper batchPushRecordMapper;
@@ -27,6 +35,13 @@ public class TencentDocBatchPushServiceImpl implements ITencentDocBatchPushServi
@Resource @Resource
private RedisCache redisCache; private RedisCache redisCache;
@Resource
private WxSendGoofishNotifyClient wxSendGoofishNotifyClient;
/** 仍为 RUNNING 超过该分钟数则归档为 INTERRUPTED可配置 */
@Value("${jarvis.tencent-doc.batch-push.stale-running-threshold-minutes:45}")
private int staleRunningThresholdMinutes;
private static final String DELAYED_PUSH_TASK_KEY = "tendoc:delayed_push:task_scheduled"; private static final String DELAYED_PUSH_TASK_KEY = "tendoc:delayed_push:task_scheduled";
private static final String DELAYED_PUSH_SCHEDULE_TIME_KEY = "tendoc:delayed_push:next_time"; private static final String DELAYED_PUSH_SCHEDULE_TIME_KEY = "tendoc:delayed_push:next_time";
@@ -81,6 +96,10 @@ public class TencentDocBatchPushServiceImpl implements ITencentDocBatchPushServi
@Override @Override
public TencentDocBatchPushRecord getBatchPushRecord(String batchId) { public TencentDocBatchPushRecord getBatchPushRecord(String batchId) {
TencentDocBatchPushRecord record = batchPushRecordMapper.selectByBatchId(batchId); TencentDocBatchPushRecord record = batchPushRecordMapper.selectByBatchId(batchId);
if (record != null && record.getFileId() != null && !record.getFileId().trim().isEmpty()) {
reconcileStaleRunningRecords(record.getFileId());
record = batchPushRecordMapper.selectByBatchId(batchId);
}
if (record != null) { if (record != null) {
// 加载关联的操作日志 // 加载关联的操作日志
List<TencentDocOperationLog> logs = operationLogMapper.selectLogsByBatchId(batchId); List<TencentDocOperationLog> logs = operationLogMapper.selectLogsByBatchId(batchId);
@@ -91,6 +110,8 @@ public class TencentDocBatchPushServiceImpl implements ITencentDocBatchPushServi
@Override @Override
public List<TencentDocBatchPushRecord> getBatchPushRecordListWithLogs(String fileId, String sheetId, Integer limit) { public List<TencentDocBatchPushRecord> getBatchPushRecordListWithLogs(String fileId, String sheetId, Integer limit) {
reconcileStaleRunningRecords(fileId);
TencentDocBatchPushRecord query = new TencentDocBatchPushRecord(); TencentDocBatchPushRecord query = new TencentDocBatchPushRecord();
query.setFileId(fileId); query.setFileId(fileId);
query.setSheetId(sheetId); query.setSheetId(sheetId);
@@ -156,5 +177,47 @@ public class TencentDocBatchPushServiceImpl implements ITencentDocBatchPushServi
return result; return result;
} }
@Override
public void reconcileStaleRunningRecords(String fileId) {
if (fileId == null || fileId.trim().isEmpty() || staleRunningThresholdMinutes <= 0) {
return;
}
Date before = new Date(System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(staleRunningThresholdMinutes));
List<TencentDocBatchPushRecord> stale = batchPushRecordMapper.selectRunningRecordsBefore(fileId, before);
if (stale == null || stale.isEmpty()) {
return;
}
for (TencentDocBatchPushRecord r : stale) {
String bid = r.getBatchId();
if (bid == null || bid.isEmpty()) {
continue;
}
TencentDocBatchPushRecord fresh = batchPushRecordMapper.selectByBatchId(bid);
if (fresh == null || !"RUNNING".equals(fresh.getStatus())) {
continue;
}
String resultMsg = String.format(
"任务已中断:超过 %d 分钟仍处于「执行中」(可能请求超时、进程退出或服务重启),系统已自动标记为结束。批次: %s",
staleRunningThresholdMinutes, bid);
String errMsg = "长时间未完成,自动归档为已中断";
try {
updateBatchPushRecord(bid, "INTERRUPTED", 0, 0, 0, resultMsg, errMsg);
log.warn("归档超时未结束的批量推送记录 batchId={} thresholdMinutes={}", bid, staleRunningThresholdMinutes);
} catch (Exception e) {
log.error("归档超时批量推送记录失败 batchId={}", bid, e);
continue;
}
String dedupeKey = REDIS_STALE_BATCH_NOTIFY_KEY + bid;
if (redisCache.getCacheObject(dedupeKey) != null) {
continue;
}
String pushText = "【腾讯文档推送】批次长时间未结束,已标记为「已中断」\n" + resultMsg;
boolean ok = wxSendGoofishNotifyClient.pushGoofishAgentText(null, pushText);
if (ok) {
redisCache.setCacheObject(dedupeKey, "1", 7, TimeUnit.DAYS);
}
}
}
} }

View File

@@ -1,7 +1,9 @@
package com.ruoyi.jarvis.service.impl; package com.ruoyi.jarvis.service.impl;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.jarvis.config.TencentDocConfig; import com.ruoyi.jarvis.config.TencentDocConfig;
import com.ruoyi.jarvis.wecom.WxSendGoofishNotifyClient;
import com.ruoyi.jarvis.service.ITencentDocBatchPushService; import com.ruoyi.jarvis.service.ITencentDocBatchPushService;
import com.ruoyi.jarvis.service.ITencentDocDelayedPushService; import com.ruoyi.jarvis.service.ITencentDocDelayedPushService;
import com.ruoyi.jarvis.service.ITencentDocTokenService; import com.ruoyi.jarvis.service.ITencentDocTokenService;
@@ -45,6 +47,9 @@ public class TencentDocDelayedPushServiceImpl implements ITencentDocDelayedPushS
@Autowired @Autowired
private ITencentDocBatchPushService batchPushService; private ITencentDocBatchPushService batchPushService;
@Autowired
private WxSendGoofishNotifyClient wxSendGoofishNotifyClient;
@Autowired @Autowired
private TencentDocConfig tencentDocConfig; private TencentDocConfig tencentDocConfig;
@@ -342,13 +347,25 @@ public class TencentDocDelayedPushServiceImpl implements ITencentDocDelayedPushS
Object result = method.invoke(controller, params); Object result = method.invoke(controller, params);
log.info("✓ 批量同步执行完成,结果: {}", result); log.info("✓ 批量同步执行完成,结果: {}", result);
if (result instanceof AjaxResult) {
AjaxResult ar = (AjaxResult) result;
if (!ar.isSuccess() && batchId != null) {
Object msgObj = ar.get(AjaxResult.MSG_TAG);
String msg = msgObj != null ? String.valueOf(msgObj) : "同步接口返回失败";
batchPushService.updateBatchPushRecord(batchId, "FAILED", 0, 0, 0, null, msg);
wxSendGoofishNotifyClient.pushGoofishAgentText(null,
"【腾讯文档推送】定时批量同步失败\n批次: " + batchId + "\n" + msg);
}
}
// 不再将 nextStartRow 写入 Redis下次定时执行时从接口获取 rowCount 决定范围 // 不再将 nextStartRow 写入 Redis下次定时执行时从接口获取 rowCount 决定范围
} catch (Exception ex) { } catch (Exception ex) {
log.error("批量同步调用失败", ex); log.error("批量同步调用失败", ex);
if (batchId != null) { if (batchId != null) {
batchPushService.updateBatchPushRecord(batchId, "FAILED", 0, 0, 0, String msg = "批量同步调用失败: " + (ex.getMessage() != null ? ex.getMessage() : ex.getClass().getSimpleName());
null, "批量同步调用失败: " + ex.getMessage()); batchPushService.updateBatchPushRecord(batchId, "FAILED", 0, 0, 0, null, msg);
wxSendGoofishNotifyClient.pushGoofishAgentText(null,
"【腾讯文档推送】定时批量同步异常\n批次: " + batchId + "\n" + msg);
} }
} }
@@ -357,8 +374,10 @@ public class TencentDocDelayedPushServiceImpl implements ITencentDocDelayedPushS
// 更新批量推送记录为失败状态 // 更新批量推送记录为失败状态
if (batchId != null) { if (batchId != null) {
try { try {
batchPushService.updateBatchPushRecord(batchId, "FAILED", 0, 0, 0, String msg = "执行批量同步失败: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName());
null, "执行批量同步失败: " + e.getMessage()); batchPushService.updateBatchPushRecord(batchId, "FAILED", 0, 0, 0, null, msg);
wxSendGoofishNotifyClient.pushGoofishAgentText(null,
"【腾讯文档推送】定时批量同步异常\n批次: " + batchId + "\n" + msg);
} catch (Exception ex) { } catch (Exception ex) {
log.error("更新批量推送记录失败", ex); log.error("更新批量推送记录失败", ex);
} }

View File

@@ -32,6 +32,11 @@ public class WxSendServiceImpl implements IWxSendService {
healthCheckUrl = wxSendBaseUrl + wxSendHealthPath; healthCheckUrl = wxSendBaseUrl + wxSendHealthPath;
logger.info("微信推送服务健康检查地址已初始化: {}", healthCheckUrl); logger.info("微信推送服务健康检查地址已初始化: {}", healthCheckUrl);
} }
@Override
public String getHealthCheckServiceUrl() {
return healthCheckUrl;
}
@Override @Override
public IWxSendService.HealthCheckResult checkHealth() { public IWxSendService.HealthCheckResult checkHealth() {

View File

@@ -150,6 +150,39 @@ public class WxSendGoofishNotifyClient {
return b; return b;
} }
/**
* 服务监控展示:闲鱼企微应用推送接口完整 URL不发起请求
*/
public String getGoofishPushEndpointDisplay() {
if (!StringUtils.hasText(wxsendBaseUrl)) {
return "";
}
return normalizeBase(wxsendBaseUrl) + "/wecom/goofish-active-push";
}
/**
* 服务监控手动测试:经 wxSend 向企微「闲鱼」应用发一条文本。
*
* @return null 表示 HTTP 2xx 且调用链未抛错;非空为可直接展示的失败原因
*/
public String testGoofishNotify() {
if (!StringUtils.hasText(wxsendBaseUrl)) {
return "未配置 jarvis.wecom.wxsend-base-url";
}
if (!StringUtils.hasText(goofishPushSecret)) {
return "未配置 jarvis.wecom.goofish-push-secret";
}
if (!StringUtils.hasText(goofishNotifyTouser)) {
return "未配置 jarvis.wecom.goofish-notify-touser接收人为空";
}
String content = "【服务监控·闲鱼通知测试】RuoYi 手动触发 " + new java.util.Date();
boolean ok = pushGoofishAgentText(null, content);
if (ok) {
return null;
}
return "推送未成功:请核对 wxSend 与企微应用、密钥及接收人,或查看服务端日志";
}
private void postJson(String url, String toUser, String content) throws Exception { private void postJson(String url, String toUser, String content) throws Exception {
postJsonReturnsOk(url, toUser, content); postJsonReturnsOk(url, toUser, content);
} }

View File

@@ -91,10 +91,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<include refid="selectBatchPushRecordVo"/> <include refid="selectBatchPushRecordVo"/>
WHERE file_id = #{fileId} WHERE file_id = #{fileId}
AND sheet_id = #{sheetId} AND sheet_id = #{sheetId}
AND status IN ('SUCCESS', 'PARTIAL') AND status IN ('SUCCESS', 'PARTIAL', 'PARTIAL_SUCCESS')
ORDER BY end_time DESC ORDER BY end_time DESC
LIMIT 1 LIMIT 1
</select> </select>
<select id="selectRunningRecordsBefore" resultMap="BatchPushRecordResult">
<include refid="selectBatchPushRecordVo"/>
WHERE status = 'RUNNING'
<if test="fileId != null and fileId != ''">AND file_id = #{fileId}</if>
AND start_time &lt; #{beforeTime}
ORDER BY start_time ASC
</select>
</mapper> </mapper>