feat: 初始化腾讯IM分发工具骨架,含回调分发链路与历史消息补拉
- 应用骨架: Solon + MyBatis-Plus + PostgreSQL + Redis + Sa-Token - 核心链路: 回调网关 → 消息落库(幂等) → 分发队列(FOR UPDATE SKIP LOCKED) → worker消费/重试/死信 - 多租户隔离: 前缀法账号映射, 所有业务表带 tenant_id - 管理后台: FreeMarker 渲染, 租户/授权/队列/用量管理页面 - 历史消息补拉: 水位线驱动, getRoamMsg/getGroupMsg 增量拉取, 兜底回调丢失 - msg_key 统一算法(回调/补拉共享, 跨路径去重) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
package com.imutil.task;
|
||||
|
||||
import com.imutil.service.DispatchService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.noear.solon.annotation.Component;
|
||||
import org.noear.solon.annotation.Inject;
|
||||
import org.noear.solon.scheduling.annotation.Scheduled;
|
||||
|
||||
/**
|
||||
* 分发卡死巡检任务
|
||||
* <p>
|
||||
* 定时重置超时未回执的 processing 记录回 pending,避免工作线程宕机导致记录永久卡在 processing。
|
||||
* 对应 app.yml 的 solon.scheduling.job.dispatchRecoverJob。
|
||||
*
|
||||
* @author imutil
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DispatchRecoverTask {
|
||||
|
||||
@Inject
|
||||
private DispatchService dispatchService;
|
||||
|
||||
/**
|
||||
* 由 app.yml dispatchRecoverJob 驱动(默认 fixedDelay=60s)
|
||||
*/
|
||||
@Scheduled(name = "dispatchRecoverJob")
|
||||
public void run() {
|
||||
try {
|
||||
int n = dispatchService.recoverStuck();
|
||||
if (n > 0) {
|
||||
log.info("分发卡死巡检:重置 {} 条 processing 记录为 pending", n);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
log.error("分发卡死巡检异常", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.imutil.task;
|
||||
|
||||
import com.imutil.service.PullService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.noear.solon.annotation.Component;
|
||||
import org.noear.solon.annotation.Inject;
|
||||
import org.noear.solon.scheduling.annotation.Scheduled;
|
||||
|
||||
/**
|
||||
* 历史消息补拉巡检任务
|
||||
* <p>
|
||||
* 定时触发补拉服务,兜底腾讯回调丢失 / 本工具落库失败的消息,
|
||||
* 保证 im_message 不丢、业务系统不漏收。
|
||||
* 对应 app.yml 的 solon.scheduling.job.pullCheckJob。
|
||||
*
|
||||
* @author imutil
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class PullCheckTask {
|
||||
|
||||
@Inject
|
||||
private PullService pullService;
|
||||
|
||||
/**
|
||||
* 由 app.yml pullCheckJob 驱动(默认 fixedDelay=5分钟)
|
||||
*/
|
||||
@Scheduled(name = "pullCheckJob")
|
||||
public void run() {
|
||||
try {
|
||||
pullService.pullRound();
|
||||
} catch (Throwable e) {
|
||||
log.error("历史消息补拉巡检异常", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.imutil.task;
|
||||
|
||||
import com.imutil.service.UsageStatService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.noear.solon.annotation.Component;
|
||||
import org.noear.solon.annotation.Inject;
|
||||
import org.noear.solon.scheduling.annotation.Scheduled;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
/**
|
||||
* 用量统计定时聚合任务
|
||||
* <p>
|
||||
* 每小时聚合「上一整点小时」窗口的用量到 usage_stat(stat_level=1)。
|
||||
* 取上一小时(而非当前小时)确保窗口数据已全部落库完整。
|
||||
* 对应 app.yml 的 solon.scheduling.job.usageStatJob。
|
||||
*
|
||||
* @author imutil
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class UsageStatTask {
|
||||
|
||||
@Inject
|
||||
private UsageStatService usageStatService;
|
||||
|
||||
/**
|
||||
* 由 app.yml usageStatJob 驱动(默认每小时 03 分,避开整点边界)
|
||||
*/
|
||||
@Scheduled(name = "usageStatJob")
|
||||
public void run() {
|
||||
OffsetDateTime now = OffsetDateTime.now();
|
||||
// 当前整点 = 窗口终点,上一整点 = 窗口起点
|
||||
OffsetDateTime end = now.truncatedTo(ChronoUnit.HOURS);
|
||||
OffsetDateTime start = end.minusHours(1);
|
||||
try {
|
||||
usageStatService.aggregateHour(start, end);
|
||||
} catch (Throwable e) {
|
||||
log.error("用量小时聚合异常 window=[{}, {})", start, end, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user