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,60 @@
package com.imutil.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.imutil.entity.UsageStat;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import java.time.OffsetDateTime;
/**
* 用量统计 Mapper
* <p>
* 提供按小时窗口的聚合 upsertPG ON CONFLICT 幂等,可重复跑)。
* 两个数据源各自 upsert 指定列,避免互相覆盖:
* - im_message → im_msg_count / im_dau
* - api_call_log → api_call_count
*
* @author imutil
*/
public interface UsageStatMapper extends BaseMapper<UsageStat> {
/**
* 从 im_message 聚合某小时窗口的消息量与活跃用户数,upsert 到 usage_statstat_level=1 小时)
* <p>
* 仅更新 im_msg_count / im_dau,不触碰已有 api_call_count。
*
* @param start 窗口起点(含)
* @param end 窗口终点(不含)
* @param statTime 统计时刻(=窗口起点整点,作为 usage_stat.stat_time
* @return 受影响租户数
*/
@Insert("INSERT INTO usage_stat (tenant_id, stat_time, stat_level, im_msg_count, im_dau, " +
"api_call_count, trtc_duration_sec, trtc_max_concurrent_room, created_at) " +
"SELECT tenant_id, #{statTime}, 1, count(*), count(DISTINCT from_account), 0, 0, 0, now() " +
"FROM im_message WHERE created_at >= #{start} AND created_at < #{end} " +
"GROUP BY tenant_id " +
"ON CONFLICT (tenant_id, stat_time, stat_level) DO UPDATE SET " +
"im_msg_count = EXCLUDED.im_msg_count, im_dau = EXCLUDED.im_dau")
int aggregateMsgHour(@Param("start") OffsetDateTime start,
@Param("end") OffsetDateTime end,
@Param("statTime") OffsetDateTime statTime);
/**
* 从 api_call_log 聚合某小时窗口的腾讯 API 调用次数,upsert 到 usage_statstat_level=1 小时)
* <p>
* 仅更新 api_call_count,不触碰已有 im_msg_count / im_dau。
*
* @return 受影响租户数
*/
@Insert("INSERT INTO usage_stat (tenant_id, stat_time, stat_level, im_msg_count, im_dau, " +
"api_call_count, trtc_duration_sec, trtc_max_concurrent_room, created_at) " +
"SELECT tenant_id, #{statTime}, 1, 0, 0, count(*), 0, 0, now() " +
"FROM api_call_log WHERE called_at >= #{start} AND called_at < #{end} " +
"GROUP BY tenant_id " +
"ON CONFLICT (tenant_id, stat_time, stat_level) DO UPDATE SET " +
"api_call_count = EXCLUDED.api_call_count")
int aggregateApiCallHour(@Param("start") OffsetDateTime start,
@Param("end") OffsetDateTime end,
@Param("statTime") OffsetDateTime statTime);
}