feat(t16): 消息记录查看(列表+筛选+分页+详情+分发链路)

- ImMessageMapper: +selectMessagePage/+countMessages(动态条件+LIMIT/OFFSET,带时间窗分区裁剪)
- DistQueueMapper: +selectByMsgKey(详情关联分发链路,LIMIT 100)
- AdminController: GET /admin/messages(列表+筛选+分页,默认近7天已分发)+ /admin/messages/detail
- messages.ftl/messages_detail.ftl: 列表+筛选+分页 / 详情+msg_body+dist_queue链路
- _macros.ftl: 侧边栏加消息记录导航
- 时间由Controller预格式化(FreeMarker对java.time支持不稳),msgTimeIso用纳秒精度匹配DB微秒

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
连龙刚
2026-07-09 17:22:31 +08:00
co-authored by Claude
parent 707f0b4824
commit 4ce030aa74
6 changed files with 413 additions and 0 deletions
+1
View File
@@ -66,6 +66,7 @@
<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>
<a href="${basePath}/admin/messages" class="nav-item <#if active=='messages'>active</#if>">消息记录</a>
<a href="${basePath}/admin/sourceapp" class="nav-item <#if active=='sourceapp'>active</#if>">老应用配置</a>
<a href="${basePath}/admin/migrate" class="nav-item <#if active=='migrate'>active</#if>">数据迁移</a>
</nav>
+107
View File
@@ -0,0 +1,107 @@
<#--
消息记录列表页
数据源 im_message(按月分区),默认最近 7 天 + 已分发(dist_status=1)。
带时间窗走分区裁剪;时间已由 Controller 预格式化(msgTimeStr/msgTimeIso)。
-->
<#import "_macros.ftl" as m>
<@m.layout active="messages" title="消息记录">
<form method="get" action="${basePath}/admin/messages" style="background:#fff;padding:16px;border-radius:8px;margin-bottom:16px;display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end">
<div style="margin:0">
<label style="display:block;font-size:12px;color:#666;margin-bottom:6px">租户</label>
<select name="tenantId" style="padding:7px;border:1px solid #ddd;border-radius:6px">
<option value="">全部租户</option>
<#list tenants as t>
<option value="${t.tenantId!}" <#if tenantId?? && tenantId==t.tenantId?string>selected</#if>>${t.tenantId!}${t.tenantName!}</option>
</#list>
</select>
</div>
<div style="margin:0">
<label style="display:block;font-size:12px;color:#666;margin-bottom:6px">会话类型</label>
<select name="convType" style="padding:7px;border:1px solid #ddd;border-radius:6px">
<option value="">全部</option>
<option value="1" <#if convType?? && convType==1>selected</#if>>C2C</option>
<option value="2" <#if convType?? && convType==2>selected</#if>>群</option>
</select>
</div>
<div style="margin:0">
<label style="display:block;font-size:12px;color:#666;margin-bottom:6px">分发状态</label>
<select name="distStatus" style="padding:7px;border:1px solid #ddd;border-radius:6px">
<option value="">全部</option>
<option value="1" <#if distStatus?? && distStatus==1>selected</#if>>已分发</option>
<option value="0" <#if distStatus?? && distStatus==0>selected</#if>>待分发</option>
<option value="2" <#if distStatus?? && distStatus==2>selected</#if>>失败</option>
</select>
</div>
<div style="margin:0">
<label style="display:block;font-size:12px;color:#666;margin-bottom:6px">起始日期</label>
<input type="date" name="dateFrom" value="${dateFrom!}" style="padding:7px;border:1px solid #ddd;border-radius:6px">
</div>
<div style="margin:0">
<label style="display:block;font-size:12px;color:#666;margin-bottom:6px">结束日期</label>
<input type="date" name="dateTo" value="${dateTo!}" style="padding:7px;border:1px solid #ddd;border-radius:6px">
</div>
<div style="margin:0">
<label style="display:block;font-size:12px;color:#666;margin-bottom:6px">关键字(账号/会话)</label>
<input type="text" name="keyword" value="${keyword!}" placeholder="from/to/conv_id" style="padding:7px;border:1px solid #ddd;border-radius:6px;width:180px">
</div>
<button type="submit" class="btn btn-primary">查询</button>
<a href="${basePath}/admin/messages" class="btn btn-secondary">重置</a>
</form>
<p class="muted" style="margin-bottom:10px">共 ${total!0} 条,第 ${page!1}/${totalPages!0} 页(默认最近 7 天 + 已分发)</p>
<#if rows?has_content>
<table class="data-table">
<thead>
<tr>
<th>时间</th><th>租户</th><th>类型</th><th>会话</th><th>发送方</th><th>接收方/群</th><th>消息类型</th><th>分发状态</th><th>来源</th><th>跨租户</th><th>操作</th>
</tr>
</thead>
<tbody>
<#list rows as r>
<tr>
<td>${r.msgTimeStr!}</td>
<td>${r.tenantId!}</td>
<td><#if r.convType?? && r.convType==1>C2C<#elseif r.convType?? && r.convType==2>群<#else>-</#if></td>
<td class="muted">${r.convId!}</td>
<td>${r.fromAccount!}</td>
<td><#if r.convType?? && r.convType==2>${r.groupId!}<#else>${r.toAccount!}</#if></td>
<td>${r.msgType!}</td>
<td>
<#if r.distStatus?? && r.distStatus==1><span class="badge badge-ok">已分发</span>
<#elseif r.distStatus?? && r.distStatus==0><span class="badge badge-warn">待分发</span>
<#elseif r.distStatus?? && r.distStatus==2><span class="badge badge-no">失败</span>
<#else>-</#if>
</td>
<td>${r.source!}</td>
<td><#if r.isCrossTenant?? && r.isCrossTenant>是<#else>-</#if></td>
<td>
<a class="btn btn-sm btn-primary" href="${basePath}/admin/messages/detail?msgKey=${r.msgKeyEnc!}&msgTime=${r.msgTimeIso!}">详情</a>
</td>
</tr>
</#list>
</tbody>
</table>
<form id="pageForm" method="get" action="${basePath}/admin/messages" style="display:inline">
<input type="hidden" name="tenantId" value="${tenantId!}">
<input type="hidden" name="convType" value="${convType!}">
<input type="hidden" name="distStatus" value="${distStatus!}">
<input type="hidden" name="dateFrom" value="${dateFrom!}">
<input type="hidden" name="dateTo" value="${dateTo!}">
<input type="hidden" name="keyword" value="${keyword!}">
<input type="hidden" name="page" id="pageInput" value="${page!1}">
</form>
<div style="margin-top:16px;display:flex;gap:8px;align-items:center">
<#if (page!1) gt 1>
<button type="button" class="btn btn-secondary btn-sm" onclick="document.getElementById('pageInput').value='${page-1}';document.getElementById('pageForm').submit()">上一页</button>
</#if>
<span class="muted">第 ${page!1} 页</span>
<#if (page!1) lt (totalPages!0)>
<button type="button" class="btn btn-secondary btn-sm" onclick="document.getElementById('pageInput').value='${page+1}';document.getElementById('pageForm').submit()">下一页</button>
</#if>
</div>
<#else>
<div class="empty">无符合条件的消息</div>
</#if>
</@m.layout>
@@ -0,0 +1,77 @@
<#--
消息详情页
基本信息(im_message 完整字段,时间由 Controller 预格式化)+ msg_body 原始 JSON + 分发链路(dist_queue
-->
<#import "_macros.ftl" as m>
<@m.layout active="messages" title="消息详情">
<div style="margin-bottom:16px">
<a href="${basePath}/admin/messages" class="btn btn-secondary btn-sm">← 返回列表</a>
</div>
<#if msg??>
<div style="background:#fff;border-radius:8px;padding:20px;box-shadow:0 1px 6px rgba(0,0,0,.07);margin-bottom:16px">
<h3 style="font-size:15px;margin-bottom:14px">基本信息</h3>
<table class="data-table" style="margin-bottom:0">
<tbody>
<tr><th style="width:140px">MsgKey</th><td>${msg.msgKey!}</td></tr>
<tr><th>消息时间</th><td>${msgTimeStr!}</td></tr>
<tr><th>租户</th><td>${msg.tenantId!}</td></tr>
<tr><th>会话类型</th><td><#if msg.convType?? && msg.convType==1>C2C<#elseif msg.convType?? && msg.convType==2>群<#else>-</#if></td></tr>
<tr><th>会话ID</th><td>${msg.convId!}</td></tr>
<tr><th>发送方</th><td>${msg.fromAccount!}</td></tr>
<tr><th>接收方</th><td>${msg.toAccount!}</td></tr>
<tr><th>群ID</th><td>${msg.groupId!}</td></tr>
<tr><th>消息类型</th><td>${msg.msgType!}</td></tr>
<tr><th>来源</th><td>${msg.source!}</td></tr>
<tr><th>跨租户</th><td><#if msg.isCrossTenant?? && msg.isCrossTenant>是<#else>否</#if></td></tr>
<tr><th>分发状态</th><td>
<#if msg.distStatus?? && msg.distStatus==1><span class="badge badge-ok">已分发</span>
<#elseif msg.distStatus?? && msg.distStatus==0><span class="badge badge-warn">待分发</span>
<#elseif msg.distStatus?? && msg.distStatus==2><span class="badge badge-no">失败</span>
<#else>-</#if>
</td></tr>
<tr><th>入库时间</th><td>${msgCreatedStr!}</td></tr>
</tbody>
</table>
</div>
<div style="background:#fff;border-radius:8px;padding:20px;box-shadow:0 1px 6px rgba(0,0,0,.07);margin-bottom:16px">
<h3 style="font-size:15px;margin-bottom:14px">消息体(msg_body</h3>
<pre style="background:#f6f8fa;padding:14px;border-radius:6px;overflow:auto;font-size:13px;max-height:400px;white-space:pre-wrap;word-break:break-all">${msg.msgBody!}</pre>
</div>
<div style="background:#fff;border-radius:8px;padding:20px;box-shadow:0 1px 6px rgba(0,0,0,.07)">
<h3 style="font-size:15px;margin-bottom:14px">分发链路(dist_queue,最多 100 条)</h3>
<#if dists?has_content>
<table class="data-table">
<thead>
<tr><th>ID</th><th>目标地址</th><th>租户</th><th>状态</th><th>重试</th><th>创建时间</th><th>更新时间</th></tr>
</thead>
<tbody>
<#list dists as d>
<tr>
<td>${d.id!}</td>
<td class="muted" style="max-width:320px;word-break:break-all">${d.targetUrl!}</td>
<td>${d.tenantId!}</td>
<td>
<#if d.status?? && d.status==0><span class="badge badge-warn">待分发</span>
<#elseif d.status?? && d.status==1><span class="badge badge-warn">处理中</span>
<#elseif d.status?? && d.status==2><span class="badge badge-ok">已分发</span>
<#elseif d.status?? && d.status==3><span class="badge badge-no">死信</span>
<#else>-</#if>
</td>
<td>${d.retryCount!}</td>
<td>${d.createdStr!}</td>
<td>${d.updatedStr!}</td>
</tr>
</#list>
</tbody>
</table>
<#else>
<div class="empty">无分发记录</div>
</#if>
</div>
<#else>
<div class="empty">消息不存在或参数缺失</div>
</#if>
</@m.layout>