1
This commit is contained in:
@@ -58,6 +58,13 @@ public class JarvisGoofishProperties {
|
||||
*/
|
||||
private String autoShipOrderStatuses = "12";
|
||||
|
||||
/**
|
||||
* 定时/增量「订单列表」拉单是否仅请求 {@link #autoShipOrderStatuses} 中的状态(通常即待发货)。
|
||||
* 为 true 时可显著减少列表与后续详情拉取次数;其余状态依赖开放平台推送回调刷新。
|
||||
* 全量历史回补若需全状态,可临时设为 false。
|
||||
*/
|
||||
private boolean pullListOnlyAutoShipStatuses = true;
|
||||
|
||||
/**
|
||||
* 未在 erp_open_config 配置 express_code 时,自动发货使用的默认快递公司编码(官方列表中日日顺多为 rrs)。
|
||||
*/
|
||||
|
||||
@@ -59,7 +59,7 @@ public class TencentDocBatchPushRecord extends BaseEntity {
|
||||
/** 错误数量 */
|
||||
private Integer errorCount;
|
||||
|
||||
/** 状态:RUNNING-执行中,SUCCESS-成功,PARTIAL-部分成功,FAILED-失败 */
|
||||
/** 状态:RUNNING-执行中,SUCCESS-成功,PARTIAL-部分成功,FAILED-失败,INTERRUPTED-已中断(超时/未正常结束) */
|
||||
private String status;
|
||||
|
||||
/** 结果消息 */
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.ruoyi.jarvis.mapper;
|
||||
import com.ruoyi.jarvis.domain.TencentDocBatchPushRecord;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -41,5 +42,11 @@ public interface TencentDocBatchPushRecordMapper {
|
||||
*/
|
||||
TencentDocBatchPushRecord selectLastSuccessRecord(@Param("fileId") String fileId,
|
||||
@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();
|
||||
|
||||
/**
|
||||
* 将长时间仍处于 RUNNING 的批次归档为 INTERRUPTED(并可选发企微告警,见实现类配置)
|
||||
*/
|
||||
void reconcileStaleRunningRecords(String fileId);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,15 @@ package com.ruoyi.jarvis.service;
|
||||
*/
|
||||
public interface IWxSendService {
|
||||
/**
|
||||
* 检查微信推送服务健康状态
|
||||
* 检查微信推送服务健康状态(会真实下发一条测试消息,仅用于服务监控页「手动测试」)
|
||||
* @return 健康状态信息,包含是否健康、状态描述等
|
||||
*/
|
||||
HealthCheckResult checkHealth();
|
||||
|
||||
/**
|
||||
* 已配置的微信推送健康检查 URL(展示用,不发起请求)
|
||||
*/
|
||||
String getHealthCheckServiceUrl();
|
||||
|
||||
/**
|
||||
* 健康检测结果
|
||||
|
||||
@@ -73,7 +73,11 @@ public class GoofishOrderPipeline {
|
||||
ErpGoofishOrder row = upsertFromNotify(appKey, item, lastNotifyJson, "LIST_UPSERT");
|
||||
tryLinkJdOrder(row);
|
||||
mergeSummaryFromOrderDetailShape(row, item, "LIST");
|
||||
refreshDetail(row);
|
||||
// 仅待发货等需判物流/发货的状态拉详情;其它状态由推送回调更新,减轻开放平台与本地压力
|
||||
List<Integer> awaitingShip = resolveAutoShipOrderStatuses();
|
||||
if (row.getOrderStatus() != null && awaitingShip.contains(row.getOrderStatus())) {
|
||||
refreshDetail(row);
|
||||
}
|
||||
syncWaybillFromRedis(row);
|
||||
tryAutoShip(row);
|
||||
}
|
||||
@@ -903,6 +907,36 @@ public class GoofishOrderPipeline {
|
||||
log.warn("闲管家拉单: pull-max-pages 与 pull-page-size 乘积超过 10000,已收敛 maxPages={}", maxPages);
|
||||
}
|
||||
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) {
|
||||
int page = 1;
|
||||
while (page <= maxPages) {
|
||||
@@ -910,6 +944,9 @@ public class GoofishOrderPipeline {
|
||||
q.setAuthorizeId(aid);
|
||||
q.setUpdateTime(updateTimeStartSec, updateTimeEndSec);
|
||||
q.setPage(page, pageSize);
|
||||
if (orderStatusFilter != null) {
|
||||
q.setOrderStatus(orderStatusFilter);
|
||||
}
|
||||
String resp;
|
||||
try {
|
||||
resp = q.getResponseBody();
|
||||
@@ -936,8 +973,8 @@ public class GoofishOrderPipeline {
|
||||
break;
|
||||
}
|
||||
if (page == maxPages) {
|
||||
log.warn("闲管家拉单已达最大页数 appKey={} aid={} 区间[{},{}];若订单更多请缩小 pull-time-chunk-seconds",
|
||||
appKey, aid, updateTimeStartSec, updateTimeEndSec);
|
||||
log.warn("闲管家拉单已达最大页数 appKey={} aid={} 区间[{},{}] orderStatus={};若订单更多请缩小 pull-time-chunk-seconds",
|
||||
appKey, aid, updateTimeStartSec, updateTimeEndSec, orderStatusFilter);
|
||||
break;
|
||||
}
|
||||
page++;
|
||||
|
||||
@@ -6,6 +6,10 @@ import com.ruoyi.jarvis.domain.TencentDocOperationLog;
|
||||
import com.ruoyi.jarvis.mapper.TencentDocBatchPushRecordMapper;
|
||||
import com.ruoyi.jarvis.mapper.TencentDocOperationLogMapper;
|
||||
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 javax.annotation.Resource;
|
||||
@@ -18,6 +22,10 @@ import java.util.concurrent.TimeUnit;
|
||||
@Service
|
||||
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
|
||||
private TencentDocBatchPushRecordMapper batchPushRecordMapper;
|
||||
|
||||
@@ -27,6 +35,13 @@ public class TencentDocBatchPushServiceImpl implements ITencentDocBatchPushServi
|
||||
@Resource
|
||||
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_SCHEDULE_TIME_KEY = "tendoc:delayed_push:next_time";
|
||||
|
||||
@@ -81,6 +96,10 @@ public class TencentDocBatchPushServiceImpl implements ITencentDocBatchPushServi
|
||||
@Override
|
||||
public TencentDocBatchPushRecord getBatchPushRecord(String 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) {
|
||||
// 加载关联的操作日志
|
||||
List<TencentDocOperationLog> logs = operationLogMapper.selectLogsByBatchId(batchId);
|
||||
@@ -91,6 +110,8 @@ public class TencentDocBatchPushServiceImpl implements ITencentDocBatchPushServi
|
||||
|
||||
@Override
|
||||
public List<TencentDocBatchPushRecord> getBatchPushRecordListWithLogs(String fileId, String sheetId, Integer limit) {
|
||||
reconcileStaleRunningRecords(fileId);
|
||||
|
||||
TencentDocBatchPushRecord query = new TencentDocBatchPushRecord();
|
||||
query.setFileId(fileId);
|
||||
query.setSheetId(sheetId);
|
||||
@@ -156,5 +177,47 @@ public class TencentDocBatchPushServiceImpl implements ITencentDocBatchPushServi
|
||||
|
||||
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;
|
||||
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.jarvis.config.TencentDocConfig;
|
||||
import com.ruoyi.jarvis.wecom.WxSendGoofishNotifyClient;
|
||||
import com.ruoyi.jarvis.service.ITencentDocBatchPushService;
|
||||
import com.ruoyi.jarvis.service.ITencentDocDelayedPushService;
|
||||
import com.ruoyi.jarvis.service.ITencentDocTokenService;
|
||||
@@ -45,6 +47,9 @@ public class TencentDocDelayedPushServiceImpl implements ITencentDocDelayedPushS
|
||||
@Autowired
|
||||
private ITencentDocBatchPushService batchPushService;
|
||||
|
||||
@Autowired
|
||||
private WxSendGoofishNotifyClient wxSendGoofishNotifyClient;
|
||||
|
||||
@Autowired
|
||||
private TencentDocConfig tencentDocConfig;
|
||||
|
||||
@@ -342,13 +347,25 @@ public class TencentDocDelayedPushServiceImpl implements ITencentDocDelayedPushS
|
||||
Object result = method.invoke(controller, params);
|
||||
|
||||
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 决定范围
|
||||
|
||||
} catch (Exception ex) {
|
||||
log.error("批量同步调用失败", ex);
|
||||
if (batchId != null) {
|
||||
batchPushService.updateBatchPushRecord(batchId, "FAILED", 0, 0, 0,
|
||||
null, "批量同步调用失败: " + ex.getMessage());
|
||||
String msg = "批量同步调用失败: " + (ex.getMessage() != null ? ex.getMessage() : ex.getClass().getSimpleName());
|
||||
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) {
|
||||
try {
|
||||
batchPushService.updateBatchPushRecord(batchId, "FAILED", 0, 0, 0,
|
||||
null, "执行批量同步失败: " + e.getMessage());
|
||||
String msg = "执行批量同步失败: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName());
|
||||
batchPushService.updateBatchPushRecord(batchId, "FAILED", 0, 0, 0, null, msg);
|
||||
wxSendGoofishNotifyClient.pushGoofishAgentText(null,
|
||||
"【腾讯文档推送】定时批量同步异常\n批次: " + batchId + "\n" + msg);
|
||||
} catch (Exception ex) {
|
||||
log.error("更新批量推送记录失败", ex);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,11 @@ public class WxSendServiceImpl implements IWxSendService {
|
||||
healthCheckUrl = wxSendBaseUrl + wxSendHealthPath;
|
||||
logger.info("微信推送服务健康检查地址已初始化: {}", healthCheckUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHealthCheckServiceUrl() {
|
||||
return healthCheckUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IWxSendService.HealthCheckResult checkHealth() {
|
||||
|
||||
@@ -150,6 +150,39 @@ public class WxSendGoofishNotifyClient {
|
||||
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 {
|
||||
postJsonReturnsOk(url, toUser, content);
|
||||
}
|
||||
|
||||
@@ -91,10 +91,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<include refid="selectBatchPushRecordVo"/>
|
||||
WHERE file_id = #{fileId}
|
||||
AND sheet_id = #{sheetId}
|
||||
AND status IN ('SUCCESS', 'PARTIAL')
|
||||
AND status IN ('SUCCESS', 'PARTIAL', 'PARTIAL_SUCCESS')
|
||||
ORDER BY end_time DESC
|
||||
LIMIT 1
|
||||
</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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user