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:
yixiong
2026-07-08 20:28:32 +08:00
co-authored by Claude
parent 20bd78baa8
commit d22fdcfaad
80 changed files with 4955 additions and 0 deletions
@@ -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_statstat_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);
}
}
}