This commit is contained in:
2025-11-07 13:42:53 +08:00
parent e868566b2d
commit a61dac3c57
4 changed files with 534 additions and 5 deletions

View File

@@ -0,0 +1,201 @@
# 腾讯文档延迟推送配置说明
## 📋 功能说明
H-TF订单录单后**不立即推送**到腾讯文档,而是采用**智能延迟推送机制**
1. 录单完成 → 触发10分钟倒计时
2. 10分钟内有新录单 → 重置倒计时
3. 10分钟内无新录单 → 自动执行推送
4. 推送执行中有新录单 → 推送完成后重新倒计时
## ⚙️ 配置文件
`application.yml` 中添加配置:
```yaml
# 腾讯文档延迟推送配置
tencent:
doc:
delayed:
push:
# 延迟时间分钟默认10分钟
minutes: 10
```
## 🎯 工作原理
### 1. Redis存储
- **倒计时结束时间**: `tendoc:delayed_push:next_time`
- **推送执行锁**: `tendoc:delayed_push:lock`
- **新订单标记**: `tendoc:delayed_push:new_order_flag`
### 2. 定时任务
- 每30秒检查一次是否到期
- 到期后自动执行推送
### 3. 防并发机制
- 使用Redis分布式锁
- 确保同一时间只有一个推送任务在执行
### 4. 智能重试
- 推送执行期间有新录单 → 推送完成后自动重新开始倒计时
## 📊 API接口待实现
### 查询倒计时状态
```
GET /jarvis-api/jarvis/tendoc/delayedPushStatus
```
**响应示例:**
```json
{
"code": 200,
"data": {
"hasPending": true,
"remainingSeconds": 300,
"nextPushTime": "2025-11-06 23:10:00",
"isPushing": false
}
}
```
### 立即执行推送
```
POST /jarvis-api/jarvis/tendoc/executeDelayedPushNow
```
### 取消待推送任务
```
POST /jarvis-api/jarvis/tendoc/cancelDelayedPush
```
## 🔍 日志输出
### 触发延迟推送
```
✓ H-TF订单已触发延迟推送 - 单号: 2025110601, 第三方单号: JY202511061595
触发延迟推送10分钟后执行23:10:00
```
### 倒计时检查
```
距离下次推送还有 300 秒
```
### 执行推送
```
倒计时结束,开始执行推送
✓ 获取推送锁成功
开始执行批量同步...
批量同步调用完成,响应码: 200
✓ 推送执行完成
✓ 释放推送锁
```
### 推送期间有新录单
```
推送执行中,标记有新订单,推送完成后将重新开始倒计时
...
推送期间有新订单,重新开始倒计时
触发延迟推送10分钟后执行23:20:00
```
## 🎯 使用场景
### 场景1连续录单
```
23:00:00 - 录单1 → 触发倒计时23:10:00执行
23:02:00 - 录单2 → 重置倒计时23:12:00执行
23:05:00 - 录单3 → 重置倒计时23:15:00执行
23:15:00 - 10分钟无新录单→ 自动推送
```
### 场景2推送执行中有新录单
```
23:00:00 - 录单1 → 触发倒计时23:10:00执行
23:10:00 - 开始推送预计需要2分钟
23:11:00 - 录单2 → 标记有新订单
23:12:00 - 推送完成 → 检测到标记 → 重新触发倒计时23:22:00执行
```
### 场景3手动触发推送
```
23:00:00 - 录单1 → 触发倒计时23:10:00执行
23:05:00 - 手动点击"批量同步物流" → 立即执行推送
23:05:05 - 推送完成 → 清除倒计时
23:06:00 - 录单2 → 重新触发倒计时23:16:00执行
```
## ⚠️ 注意事项
1. **延迟时间建议**:
- 录单频率高设置5-10分钟
- 录单频率低设置10-15分钟
2. **服务器重启**:
- 倒计时存储在Redis中
- 服务器重启后倒计时会继续Redis数据保留
3. **推送失败**:
- 推送失败不会自动重试
- 需要手动点击"批量同步物流"
4. **并发安全**:
- 使用Redis分布式锁
- 多台服务器部署时也能正确工作
## 🔧 故障排查
### 问题1倒计时不触发
**检查步骤:**
1. 确认Service已正常启动
2. 查看日志中是否有"延迟推送服务已启动"
3. 检查Redis连接是否正常
**解决方法:**
```bash
# 查看Redis中的倒计时
redis-cli
> get "tendoc:delayed_push:next_time"
```
### 问题2推送不执行
**检查步骤:**
1. 查看日志中是否有"倒计时结束,开始执行推送"
2. 检查是否有错误日志
3. 查看Redis锁状态
**解决方法:**
```bash
# 查看锁状态
redis-cli
> get "tendoc:delayed_push:lock"
# 如果有锁但长时间未释放,手动删除
> del "tendoc:delayed_push:lock"
```
### 问题3倒计时一直重置
**原因:** 录单频率太高,倒计时不断被重置
**解决方法:**
- 减少延迟时间如改为5分钟
- 或手动触发推送
---
**最后更新**: 2025-11-06
**版本**: v1.0

View File

@@ -0,0 +1,33 @@
package com.ruoyi.jarvis.service;
/**
* 腾讯文档延迟推送服务接口
*
* @author system
*/
public interface ITencentDocDelayedPushService {
/**
* 触发延迟推送
* 录单时调用此方法会重置10分钟倒计时
*/
void triggerDelayedPush();
/**
* 立即执行推送(用于手动触发)
*/
void executePushNow();
/**
* 获取下次推送的剩余时间(秒)
*
* @return 剩余秒数,如果没有待推送返回-1
*/
long getRemainingSeconds();
/**
* 取消待推送任务
*/
void cancelPendingPush();
}

View File

@@ -48,6 +48,8 @@ public class InstructionServiceImpl implements IInstructionService {
private com.ruoyi.jarvis.config.TencentDocConfig tencentDocConfig;
@Resource
private com.ruoyi.common.core.redis.RedisCache redisCache;
@Resource(required = false)
private com.ruoyi.jarvis.service.ITencentDocDelayedPushService tencentDocDelayedPushService;
// 录单模板(与 jd/JDUtil 中 WENAN_D 保持一致)
private static final String WENAN_D = "单:\n" + "{单号} \n备注{单的备注}\n" + "分销标记:{分销标记}\n" + "第三方单号:{第三方单号}\n" + "型号:\n" + "{型号}\n" + "链接:\n" + "{链接}\n" + "下单付款:\n" + "\n" + "后返金额:\n" + "\n" + "地址:\n" + "{地址}\n" + "物流链接:\n" + "\n" + "订单号:\n" + "\n" + "下单人:\n" + "\n" + "京粉实际价格:\n" + "\n";
@@ -1233,11 +1235,20 @@ private String handleTF(String input) {
jdOrderService.insertJDOrder(order);
}
// 注意:H-TF订单不再自动写入腾讯文档,需通过订单列表手动触发
// 原因:防止并发写入和数据覆盖,需要人工确认
// if ("H-TF".equals(order.getDistributionMark())) {
// asyncWriteToTencentDoc(order);
// }
// H-TF订单触发延迟推送机制
// 录单后重置10分钟倒计时10分钟内无新录单则自动推送
if ("H-TF".equals(order.getDistributionMark())) {
try {
if (tencentDocDelayedPushService != null) {
tencentDocDelayedPushService.triggerDelayedPush();
System.out.println("✓ H-TF订单已触发延迟推送 - 单号: " + order.getRemark() +
", 第三方单号: " + order.getThirdPartyOrderNo());
}
} catch (Exception e) {
// 触发延迟推送失败不影响录单结果
System.err.println("✗ 触发延迟推送失败 - 单号: " + order.getRemark() + ", 错误: " + e.getMessage());
}
}
// 返回完整的表单格式,使用原始输入保留完整物流链接
return formatOrderForm(order, originalInput);

View File

@@ -0,0 +1,284 @@
package com.ruoyi.jarvis.service.impl;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.jarvis.service.ITencentDocDelayedPushService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 腾讯文档延迟推送服务实现
*
* 功能说明:
* 1. 录单时触发10分钟倒计时
* 2. 10分钟内有新录单重置倒计时
* 3. 10分钟到期后自动执行推送
* 4. 推送执行期间有录单,推送完成后重新开始倒计时
* 5. 使用分布式锁防止并发推送
*
* @author system
*/
@Service
public class TencentDocDelayedPushServiceImpl implements ITencentDocDelayedPushService {
private static final Logger log = LoggerFactory.getLogger(TencentDocDelayedPushServiceImpl.class);
@Autowired
private RedisCache redisCache;
/**
* 延迟时间(分钟),可通过配置文件修改
*/
@Value("${tencent.doc.delayed.push.minutes:10}")
private int delayMinutes;
/**
* Redis Key - 存储下次推送的时间戳
*/
private static final String REDIS_KEY_NEXT_PUSH_TIME = "tendoc:delayed_push:next_time";
/**
* Redis Key - 推送执行锁
*/
private static final String REDIS_KEY_PUSH_LOCK = "tendoc:delayed_push:lock";
/**
* Redis Key - 推送期间有新录单标记
*/
private static final String REDIS_KEY_NEW_ORDER_FLAG = "tendoc:delayed_push:new_order_flag";
/**
* 定时任务执行器
*/
private ScheduledExecutorService scheduler;
/**
* 初始化定时任务
*/
@PostConstruct
public void init() {
// 创建单线程的定时任务执行器
scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread thread = new Thread(r, "TencentDoc-DelayedPush-Thread");
thread.setDaemon(true);
return thread;
});
// 每30秒检查一次是否需要推送
scheduler.scheduleWithFixedDelay(this::checkAndExecutePush, 30, 30, TimeUnit.SECONDS);
log.info("腾讯文档延迟推送服务已启动,延迟时间: {} 分钟", delayMinutes);
}
/**
* 关闭定时任务
*/
@PreDestroy
public void destroy() {
if (scheduler != null && !scheduler.isShutdown()) {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
log.info("腾讯文档延迟推送服务已关闭");
}
@Override
public void triggerDelayedPush() {
try {
// 计算下次推送时间 = 当前时间 + 延迟分钟数
long nextPushTime = System.currentTimeMillis() + (delayMinutes * 60 * 1000L);
// 检查是否正在执行推送
String lockValue = redisCache.getCacheObject(REDIS_KEY_PUSH_LOCK);
if (lockValue != null && "locked".equals(lockValue)) {
// 正在推送中,标记有新订单,推送完成后会重新触发
redisCache.setCacheObject(REDIS_KEY_NEW_ORDER_FLAG, "true", 1, TimeUnit.HOURS);
log.info("推送执行中,标记有新订单,推送完成后将重新开始倒计时");
return;
}
// 更新下次推送时间
redisCache.setCacheObject(REDIS_KEY_NEXT_PUSH_TIME, nextPushTime, delayMinutes + 5, TimeUnit.MINUTES);
log.info("触发延迟推送,{}分钟后执行({}", delayMinutes,
new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date(nextPushTime)));
} catch (Exception e) {
log.error("触发延迟推送失败", e);
}
}
@Override
public void executePushNow() {
log.info("手动触发立即推送");
// 清除待推送标记
redisCache.deleteObject(REDIS_KEY_NEXT_PUSH_TIME);
// 执行推送
doExecutePush();
}
@Override
public long getRemainingSeconds() {
try {
Long nextPushTime = redisCache.getCacheObject(REDIS_KEY_NEXT_PUSH_TIME);
if (nextPushTime == null) {
return -1;
}
long remaining = (nextPushTime - System.currentTimeMillis()) / 1000;
return remaining > 0 ? remaining : 0;
} catch (Exception e) {
log.error("获取剩余时间失败", e);
return -1;
}
}
@Override
public void cancelPendingPush() {
redisCache.deleteObject(REDIS_KEY_NEXT_PUSH_TIME);
log.info("已取消待推送任务");
}
/**
* 定时检查并执行推送
*/
private void checkAndExecutePush() {
try {
// 获取下次推送时间
Long nextPushTime = redisCache.getCacheObject(REDIS_KEY_NEXT_PUSH_TIME);
if (nextPushTime == null) {
// 没有待推送任务
return;
}
long now = System.currentTimeMillis();
if (now < nextPushTime) {
// 还没到推送时间
long remainingSeconds = (nextPushTime - now) / 1000;
log.debug("距离下次推送还有 {} 秒", remainingSeconds);
return;
}
// 时间到了,执行推送
log.info("倒计时结束,开始执行推送");
doExecutePush();
} catch (Exception e) {
log.error("检查推送任务失败", e);
}
}
/**
* 执行推送
*/
private void doExecutePush() {
String lockValue = null;
try {
// 1. 尝试获取分布式锁
lockValue = redisCache.getCacheObject(REDIS_KEY_PUSH_LOCK);
if (lockValue != null && "locked".equals(lockValue)) {
log.warn("推送任务已在执行中,跳过本次推送");
return;
}
// 2. 加锁30分钟超时防止死锁
redisCache.setCacheObject(REDIS_KEY_PUSH_LOCK, "locked", 30, TimeUnit.MINUTES);
log.info("✓ 获取推送锁成功,开始执行推送");
// 3. 清除待推送标记
redisCache.deleteObject(REDIS_KEY_NEXT_PUSH_TIME);
// 4. 清除新订单标记
redisCache.deleteObject(REDIS_KEY_NEW_ORDER_FLAG);
// 5. 调用批量同步接口
// 注意这里需要通过HTTP调用Controller的接口或者注入Controller的方法
// 为了避免循环依赖这里使用Spring的ApplicationContext来获取Bean
executeBatchSync();
log.info("✓ 推送执行完成");
} catch (Exception e) {
log.error("❌ 推送执行失败", e);
} finally {
// 6. 释放锁
try {
redisCache.deleteObject(REDIS_KEY_PUSH_LOCK);
log.info("✓ 释放推送锁");
} catch (Exception e) {
log.error("释放推送锁失败", e);
}
// 7. 检查是否有新订单标记
String newOrderFlag = redisCache.getCacheObject(REDIS_KEY_NEW_ORDER_FLAG);
if (newOrderFlag != null && "true".equals(newOrderFlag)) {
log.info("推送期间有新订单,重新开始倒计时");
redisCache.deleteObject(REDIS_KEY_NEW_ORDER_FLAG);
triggerDelayedPush();
}
}
}
/**
* 执行批量同步
*
* 说明这里通过HTTP调用本地接口避免复杂的依赖注入
*/
private void executeBatchSync() {
try {
log.info("开始执行批量同步...");
// 使用RestTemplate或HttpClient调用本地接口
// 这里简化处理直接发送HTTP请求到本地
java.net.URL url = new java.net.URL("http://localhost:30313/jarvis-api/jarvis/tendoc/fillLogisticsByOrderNo");
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setDoOutput(true);
// 发送空JSON对象
try (java.io.OutputStream os = conn.getOutputStream()) {
byte[] input = "{}".getBytes("utf-8");
os.write(input, 0, input.length);
}
int responseCode = conn.getResponseCode();
log.info("批量同步调用完成,响应码: {}", responseCode);
if (responseCode == 200) {
// 读取响应
try (java.io.BufferedReader br = new java.io.BufferedReader(
new java.io.InputStreamReader(conn.getInputStream(), "utf-8"))) {
StringBuilder response = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
log.info("批量同步结果: {}", response.toString());
}
} else {
log.error("批量同步调用失败,响应码: {}", responseCode);
}
} catch (Exception e) {
log.error("执行批量同步失败", e);
throw new RuntimeException("执行批量同步失败", e);
}
}
}