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
+82
View File
@@ -0,0 +1,82 @@
<#--
管理后台公共布局宏
用法:<#import "_macros.ftl" as m><@m.layout active="home" title="仪表盘">页面内容</@m.layout>
对齐 yxtech:左侧边栏 + 右侧主区域,内嵌 CSS(无外部框架)。
-->
<#macro layout active title>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>${title} - IM分发工具管理后台</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:-apple-system,BlinkMacSystemFont,"Microsoft YaHei",sans-serif; display:flex; height:100vh; overflow:hidden; color:#333; }
.sidebar { width:200px; flex-shrink:0; background:#2c3e50; display:flex; flex-direction:column; }
.sidebar-brand { padding:18px; font-size:13px; font-weight:600; color:#fff; line-height:1.5; border-bottom:1px solid rgba(255,255,255,.1); }
.sidebar-nav { flex:1; padding:10px 0; }
.nav-item { display:block; color:rgba(255,255,255,.72); text-decoration:none; padding:11px 20px; font-size:14px; border-left:3px solid transparent; }
.nav-item:hover { background:rgba(255,255,255,.08); color:#fff; }
.nav-item.active { background:rgba(74,144,226,.18); color:#fff; border-left-color:#4a90e2; }
.sidebar-footer { border-top:1px solid rgba(255,255,255,.1); }
.logout-link { display:block; color:rgba(248,165,165,.85); text-decoration:none; padding:10px 20px; font-size:14px; }
.main-wrapper { flex:1; display:flex; flex-direction:column; min-width:0; overflow:hidden; }
.main-header { height:50px; background:#fff; border-bottom:1px solid #e8e8e8; padding:0 24px; display:flex; align-items:center; }
.page-title { font-size:15px; font-weight:600; color:#2c3e50; }
.content { flex:1; overflow-y:auto; padding:20px 24px; background:#f8f9fa; }
.alert { padding:11px 16px; border-radius:6px; margin-bottom:16px; font-size:14px; background:#d4edda; border:1px solid #c3e6cb; color:#155724; }
.alert-warn { background:#fff3cd; border-color:#ffeaa7; color:#856404; }
.stat-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:16px; margin-bottom:20px; }
.stat-card { background:#fff; border-radius:8px; padding:20px; box-shadow:0 1px 6px rgba(0,0,0,.07); }
.stat-card .label { font-size:13px; color:#888; }
.stat-card .value { font-size:28px; font-weight:600; color:#2c3e50; margin-top:6px; }
.data-table { width:100%; border-collapse:collapse; background:#fff; border-radius:8px; overflow:hidden; box-shadow:0 1px 6px rgba(0,0,0,.07); font-size:14px; }
.data-table th,.data-table td { padding:11px 14px; text-align:left; border-bottom:1px solid #f0f0f0; }
.data-table th { background:#f5f6fa; font-weight:600; color:#495057; font-size:13px; }
.data-table tr:hover td { background:#fafbff; }
.btn { display:inline-block; padding:7px 15px; font-size:14px; border-radius:6px; cursor:pointer; border:none; text-decoration:none; }
.btn-primary { background:#4a90e2; color:#fff; }
.btn-secondary { background:#6c757d; color:#fff; }
.btn-danger { background:#dc3545; color:#fff; }
.btn-success { background:#28a745; color:#fff; }
.btn-sm { padding:4px 9px; font-size:12px; }
.badge { display:inline-block; padding:2px 8px; border-radius:10px; font-size:12px; }
.badge-ok { background:#d1e7dd; color:#0f5132; }
.badge-no { background:#f8d7da; color:#842029; }
.badge-warn { background:#fff3cd; color:#856404; }
.modal { display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,.5); align-items:center; justify-content:center; z-index:1000; }
.modal.show { display:flex; }
.modal-content { background:#fff; border-radius:10px; padding:24px 30px; width:480px; max-height:90vh; overflow-y:auto; }
.modal-title { font-size:16px; font-weight:600; margin-bottom:18px; }
.modal-body .form-group { margin-bottom:14px; }
.modal-body label { display:block; font-size:13px; color:#666; margin-bottom:6px; }
.modal-body input,.modal-body select { width:100%; padding:9px 12px; border:1px solid #ddd; border-radius:6px; font-size:14px; }
.modal-footer { display:flex; gap:10px; justify-content:flex-end; margin-top:18px; }
.muted { color:#999; font-size:12px; }
.empty { text-align:center; padding:30px; color:#aaa; font-size:14px; }
</style>
</head>
<body>
<aside class="sidebar">
<div class="sidebar-brand">IM分发工具<br>管理后台</div>
<nav class="sidebar-nav">
<a href="${basePath}/admin/home" class="nav-item <#if active=='home'>active</#if>">仪表盘</a>
<a href="${basePath}/admin/tenant" class="nav-item <#if active=='tenant'>active</#if>">租户管理</a>
<a href="${basePath}/admin/grant" class="nav-item <#if active=='grant'>active</#if>">跨租户授权</a>
<a href="${basePath}/admin/queue" class="nav-item <#if active=='queue'>active</#if>">队列监控</a>
<a href="${basePath}/admin/usage" class="nav-item <#if active=='usage'>active</#if>">用量报表</a>
</nav>
<div class="sidebar-footer">
<a href="${basePath}/admin/logout" class="logout-link">退出登录</a>
</div>
</aside>
<div class="main-wrapper">
<header class="main-header"><span class="page-title">${title}</span></header>
<div class="content">
<#nested>
</div>
</div>
</body>
</html>
</#macro>