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,92 @@
package com.imutil.worker;
import com.imutil.entity.DistQueue;
import com.imutil.service.DispatchService;
import lombok.extern.slf4j.Slf4j;
import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Init;
import org.noear.solon.annotation.Inject;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 分发工作线程
* <p>
* 应用启动后({@code @Init})创建 daemon 线程池,每个线程循环:
* 抢占一批 pending → 逐条 HTTP 转发业务系统 → 回执更新状态。
* 空闲时按 pollIntervalMs 休眠,避免空转。
* <p>
* daemon 线程不阻止 JVM 退出;业务系统宕机时由回执失败+重试+死信兜底(见 DispatchService)。
*
* @author imutil
*/
@Slf4j
@Component
public class DispatchWorker {
@Inject
private DispatchService dispatchService;
@Inject("${imutil.dispatch.workerCount:4}")
private int workerCount;
@Inject("${imutil.dispatch.pollIntervalMs:1000}")
private long pollIntervalMs;
private final AtomicInteger seq = new AtomicInteger(0);
private ExecutorService pool;
/**
* 应用启动后启动工作线程
*/
@Init
public void start() {
pool = Executors.newFixedThreadPool(workerCount, r -> {
Thread t = new Thread(r, "dispatch-worker-" + seq.incrementAndGet());
t.setDaemon(true);
return t;
});
for (int i = 0; i < workerCount; i++) {
pool.submit(this::loop);
}
log.info("分发工作线程已启动 count={}", workerCount);
}
/**
* 工作循环
*/
private void loop() {
String workerName = "w-" + seq.get();
while (true) {
try {
List<DistQueue> tasks = dispatchService.fetchAndLock(workerName);
if (tasks.isEmpty()) {
Thread.sleep(pollIntervalMs);
continue;
}
for (DistQueue task : tasks) {
try {
dispatchService.dispatch(task, workerName);
} catch (Throwable e) {
log.error("分发单条异常 id={}", task.getId(), e);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
} catch (Throwable e) {
log.error("分发工作循环异常", e);
try {
Thread.sleep(pollIntervalMs);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
return;
}
}
}
}
}