1
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
|
||||||
IWxSendService.HealthCheckResult wxSendHealth = wxSendService.checkHealth();
|
|
||||||
Map<String, Object> wxSendMap = new HashMap<>();
|
Map<String, Object> wxSendMap = new HashMap<>();
|
||||||
wxSendMap.put("healthy", wxSendHealth.isHealthy());
|
wxSendMap.put("manualOnly", true);
|
||||||
wxSendMap.put("status", wxSendHealth.getStatus());
|
wxSendMap.put("healthy", null);
|
||||||
wxSendMap.put("message", wxSendHealth.getMessage());
|
wxSendMap.put("status", "未检测");
|
||||||
wxSendMap.put("serviceUrl", wxSendHealth.getServiceUrl());
|
wxSendMap.put("message", "点击「测试」将发送一条健康检查消息(会真实推送到微信)");
|
||||||
|
wxSendMap.put("serviceUrl", wxSendService.getHealthCheckServiceUrl());
|
||||||
healthMap.put("wxSend", wxSendMap);
|
healthMap.put("wxSend", wxSendMap);
|
||||||
} catch (Exception e) {
|
|
||||||
Map<String, Object> wxSendMap = new HashMap<>();
|
// 企微闲鱼通知:仅展示接口地址;真实检测见 POST /monitor/server/health/goofish-notify-test
|
||||||
wxSendMap.put("healthy", false);
|
Map<String, Object> goofishMap = new HashMap<>();
|
||||||
wxSendMap.put("status", "异常");
|
goofishMap.put("manualOnly", true);
|
||||||
wxSendMap.put("message", "健康检测异常: " + e.getMessage());
|
goofishMap.put("healthy", null);
|
||||||
wxSendMap.put("serviceUrl", "");
|
goofishMap.put("status", "未检测");
|
||||||
healthMap.put("wxSend", wxSendMap);
|
goofishMap.put("message", "点击「测试」将经 wxSend 向企微闲鱼应用发送一条测试文本");
|
||||||
}
|
goofishMap.put("serviceUrl", wxSendGoofishNotifyClient.getGoofishPushEndpointDisplay());
|
||||||
|
healthMap.put("goofishNotify", goofishMap);
|
||||||
|
|
||||||
// Ollama 服务健康检测(调试用)
|
// Ollama 服务健康检测(调试用)
|
||||||
try {
|
try {
|
||||||
@@ -116,6 +121,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<>();
|
||||||
ollamaMap.put("healthy", false);
|
ollamaMap.put("healthy", false);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
/** 结果消息 */
|
/** 结果消息 */
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,5 +41,10 @@ public interface ITencentDocBatchPushService {
|
|||||||
* 获取推送状态和倒计时信息
|
* 获取推送状态和倒计时信息
|
||||||
*/
|
*/
|
||||||
Map<String, Object> getPushStatusAndCountdown();
|
Map<String, Object> getPushStatusAndCountdown();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将长时间仍处于 RUNNING 的批次归档为 INTERRUPTED(并可选发企微告警,见实现类配置)
|
||||||
|
*/
|
||||||
|
void reconcileStaleRunningRecords(String fileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,16 @@ package com.ruoyi.jarvis.service;
|
|||||||
*/
|
*/
|
||||||
public interface IWxSendService {
|
public interface IWxSendService {
|
||||||
/**
|
/**
|
||||||
* 检查微信推送服务健康状态
|
* 检查微信推送服务健康状态(会真实下发一条测试消息,仅用于服务监控页「手动测试」)
|
||||||
* @return 健康状态信息,包含是否健康、状态描述等
|
* @return 健康状态信息,包含是否健康、状态描述等
|
||||||
*/
|
*/
|
||||||
HealthCheckResult checkHealth();
|
HealthCheckResult checkHealth();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已配置的微信推送健康检查 URL(展示用,不发起请求)
|
||||||
|
*/
|
||||||
|
String getHealthCheckServiceUrl();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 健康检测结果
|
* 健康检测结果
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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");
|
||||||
|
// 仅待发货等需判物流/发货的状态拉详情;其它状态由推送回调更新,减轻开放平台与本地压力
|
||||||
|
List<Integer> awaitingShip = resolveAutoShipOrderStatuses();
|
||||||
|
if (row.getOrderStatus() != null && awaitingShip.contains(row.getOrderStatus())) {
|
||||||
refreshDetail(row);
|
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++;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ public class WxSendServiceImpl implements IWxSendService {
|
|||||||
logger.info("微信推送服务健康检查地址已初始化: {}", healthCheckUrl);
|
logger.info("微信推送服务健康检查地址已初始化: {}", healthCheckUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHealthCheckServiceUrl() {
|
||||||
|
return healthCheckUrl;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IWxSendService.HealthCheckResult checkHealth() {
|
public IWxSendService.HealthCheckResult checkHealth() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 < #{beforeTime}
|
||||||
|
ORDER BY start_time ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user