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:
@@ -15,12 +15,20 @@ import com.imutil.mapper.UsageStatMapper;
|
||||
import com.imutil.service.AdminUserService;
|
||||
import com.imutil.service.SourceAppService;
|
||||
import com.imutil.tencent.TencentImClient;
|
||||
import com.imutil.entity.ImMessage;
|
||||
import com.imutil.entity.MigrateTask;
|
||||
import com.imutil.service.MigrateService;
|
||||
import com.imutil.mapper.ImMessageMapper;
|
||||
import org.noear.solon.core.handle.UploadedFile;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.noear.solon.annotation.Controller;
|
||||
import org.noear.solon.annotation.Get;
|
||||
@@ -59,6 +67,9 @@ public class AdminController {
|
||||
@Inject
|
||||
private DistQueueMapper distQueueMapper;
|
||||
|
||||
@Inject
|
||||
private ImMessageMapper imMessageMapper;
|
||||
|
||||
@Inject
|
||||
private UsageStatMapper usageStatMapper;
|
||||
|
||||
@@ -520,6 +531,150 @@ public class AdminController {
|
||||
* @param title 页面标题
|
||||
* @param active 侧边栏高亮项(home/tenant/grant/queue/usage)
|
||||
*/
|
||||
// ==================== 消息记录 ====================
|
||||
|
||||
/**
|
||||
* 消息记录列表(已分发消息查询)
|
||||
* <p>
|
||||
* 默认最近 7 天 + 已分发(dist_status=1);带 msg_time 范围走分区裁剪。
|
||||
*/
|
||||
@Get
|
||||
@Mapping("/messages")
|
||||
public Object messagesPage(@Param(defaultValue = "") String tenantId,
|
||||
@Param(required = false) Integer convType,
|
||||
@Param(defaultValue = "1") Integer distStatus,
|
||||
@Param(defaultValue = "") String dateFrom,
|
||||
@Param(defaultValue = "") String dateTo,
|
||||
@Param(defaultValue = "") String keyword,
|
||||
@Param(defaultValue = "1") int page,
|
||||
@Param(defaultValue = "20") int size) {
|
||||
// 默认时间窗:最近 7 天(带 msg_time 范围触发分区裁剪,避免全分区扫描)
|
||||
OffsetDateTime now = OffsetDateTime.now();
|
||||
OffsetDateTime from = parseDateStart(dateFrom, now.minusDays(7));
|
||||
OffsetDateTime to = parseDateEnd(dateTo, now);
|
||||
int safePage = Math.max(1, page);
|
||||
int safeSize = size <= 0 ? 20 : Math.min(size, 200);
|
||||
int offset = (safePage - 1) * safeSize;
|
||||
|
||||
String tid = (tenantId == null || tenantId.isEmpty()) ? null : tenantId;
|
||||
String kw = (keyword == null || keyword.isEmpty()) ? null : keyword;
|
||||
List<ImMessage> rawRows = imMessageMapper.selectMessagePage(tid, convType, distStatus, from, to, kw, offset, safeSize);
|
||||
long total = imMessageMapper.countMessages(tid, convType, distStatus, from, to, kw);
|
||||
int totalPages = (int) ((total + safeSize - 1) / Math.max(1, safeSize));
|
||||
|
||||
// 转成 Map 并预格式化时间(FreeMarker 对 java.time OffsetDateTime 格式化支持不稳,Controller 端处理)
|
||||
DateTimeFormatter fmtDisp = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
DateTimeFormatter fmtIso = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSSxxx");
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
if (rawRows != null) {
|
||||
for (ImMessage r : rawRows) {
|
||||
Map<String, Object> v = new LinkedHashMap<>();
|
||||
v.put("msgKey", r.getMsgKey());
|
||||
v.put("msgKeyEnc", r.getMsgKey() == null ? "" : java.net.URLEncoder.encode(r.getMsgKey(), StandardCharsets.UTF_8));
|
||||
v.put("msgTimeStr", r.getMsgTime() == null ? "" : r.getMsgTime().format(fmtDisp));
|
||||
v.put("msgTimeIso", r.getMsgTime() == null ? "" : java.net.URLEncoder.encode(r.getMsgTime().format(fmtIso), StandardCharsets.UTF_8));
|
||||
v.put("tenantId", r.getTenantId());
|
||||
v.put("convType", r.getConvType());
|
||||
v.put("convId", r.getConvId());
|
||||
v.put("fromAccount", r.getFromAccount());
|
||||
v.put("toAccount", r.getToAccount());
|
||||
v.put("groupId", r.getGroupId());
|
||||
v.put("msgType", r.getMsgType());
|
||||
v.put("source", r.getSource());
|
||||
v.put("isCrossTenant", r.getIsCrossTenant());
|
||||
v.put("distStatus", r.getDistStatus());
|
||||
rows.add(v);
|
||||
}
|
||||
}
|
||||
|
||||
ModelAndView mv = view("messages.ftl", "消息记录", "messages");
|
||||
mv.put("rows", rows);
|
||||
mv.put("total", total);
|
||||
mv.put("page", safePage);
|
||||
mv.put("size", safeSize);
|
||||
mv.put("totalPages", totalPages);
|
||||
mv.put("tenantId", tenantId == null ? "" : tenantId);
|
||||
mv.put("convType", convType);
|
||||
mv.put("distStatus", distStatus);
|
||||
mv.put("dateFrom", dateFrom == null ? "" : dateFrom);
|
||||
mv.put("dateTo", dateTo == null ? "" : dateTo);
|
||||
mv.put("keyword", keyword == null ? "" : keyword);
|
||||
mv.put("tenants", tenantMapper.selectList(null));
|
||||
return mv;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息详情:完整字段 + msg_body + 关联 dist_queue 分发链路
|
||||
*/
|
||||
@Get
|
||||
@Mapping("/messages/detail")
|
||||
public Object messageDetail(@Param String msgKey, @Param String msgTime) {
|
||||
ModelAndView mv = view("messages_detail.ftl", "消息详情", "messages");
|
||||
if (msgKey == null || msgKey.isEmpty() || msgTime == null || msgTime.isEmpty()) {
|
||||
mv.put("msg", null);
|
||||
mv.put("dists", java.util.Collections.emptyList());
|
||||
return mv;
|
||||
}
|
||||
OffsetDateTime t;
|
||||
try {
|
||||
t = OffsetDateTime.parse(msgTime);
|
||||
} catch (Exception e) {
|
||||
mv.put("msg", null);
|
||||
mv.put("dists", java.util.Collections.emptyList());
|
||||
return mv;
|
||||
}
|
||||
ImMessage m = imMessageMapper.selectOne(Wrappers.<ImMessage>lambdaQuery()
|
||||
.eq(ImMessage::getMsgKey, msgKey).eq(ImMessage::getMsgTime, t));
|
||||
mv.put("msg", m);
|
||||
mv.put("msgTimeStr", (m != null && m.getMsgTime() != null)
|
||||
? m.getMsgTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")) : "");
|
||||
mv.put("msgCreatedStr", (m != null && m.getCreatedAt() != null)
|
||||
? m.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) : "");
|
||||
// dist_queue 分发链路,时间预格式化(同列表原因)
|
||||
DateTimeFormatter fmtDisp = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
List<DistQueue> rawDists = distQueueMapper.selectByMsgKey(msgKey);
|
||||
List<Map<String, Object>> dists = new ArrayList<>();
|
||||
if (rawDists != null) {
|
||||
for (DistQueue d : rawDists) {
|
||||
Map<String, Object> v = new LinkedHashMap<>();
|
||||
v.put("id", d.getId());
|
||||
v.put("targetUrl", d.getTargetUrl());
|
||||
v.put("tenantId", d.getTenantId());
|
||||
v.put("status", d.getStatus());
|
||||
v.put("retryCount", d.getRetryCount());
|
||||
v.put("createdStr", d.getCreatedAt() == null ? "" : d.getCreatedAt().format(fmtDisp));
|
||||
v.put("updatedStr", d.getUpdatedAt() == null ? "" : d.getUpdatedAt().format(fmtDisp));
|
||||
dists.add(v);
|
||||
}
|
||||
}
|
||||
mv.put("dists", dists);
|
||||
return mv;
|
||||
}
|
||||
|
||||
/** 解析 yyyy-MM-dd 为当天 0 点(+8),空或非法用默认值 */
|
||||
private OffsetDateTime parseDateStart(String s, OffsetDateTime def) {
|
||||
if (s == null || s.isEmpty()) {
|
||||
return def;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(s).atStartOfDay(ZoneOffset.ofHours(8)).toOffsetDateTime();
|
||||
} catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
/** 解析 yyyy-MM-dd 为次日 0 点(+8,开区间覆盖整天),空或非法用默认值 */
|
||||
private OffsetDateTime parseDateEnd(String s, OffsetDateTime def) {
|
||||
if (s == null || s.isEmpty()) {
|
||||
return def;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(s).plusDays(1).atStartOfDay(ZoneOffset.ofHours(8)).toOffsetDateTime();
|
||||
} catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
private ModelAndView view(String tpl, String title, String active) {
|
||||
ModelAndView mv = new ModelAndView(tpl);
|
||||
mv.put("basePath", basePath);
|
||||
|
||||
@@ -86,4 +86,17 @@ public interface DistQueueMapper extends BaseMapper<DistQueue> {
|
||||
*/
|
||||
@org.apache.ibatis.annotations.Select("SELECT status, count(*) AS cnt FROM dist_queue WHERE status IN (0, 1, 3) GROUP BY status")
|
||||
java.util.List<java.util.Map<String, Object>> countByStatus();
|
||||
|
||||
/**
|
||||
* 按消息键查分发记录(消息详情关联分发链路)
|
||||
* <p>
|
||||
* msg_key 无独立索引,LIMIT 100 控制扫描范围;大数据量建议加 idx_dq_msgkey。
|
||||
*
|
||||
* @param msgKey 消息键
|
||||
* @return 该消息的分发任务记录(按 id 倒序,最多 100 条)
|
||||
*/
|
||||
@org.apache.ibatis.annotations.Select("SELECT id, msg_key, tenant_id, conv_id, target_url, payload, status, retry_count, "
|
||||
+ "next_retry_at, locked_by, locked_at, created_at, updated_at "
|
||||
+ "FROM dist_queue WHERE msg_key = #{msgKey} ORDER BY id DESC LIMIT 100")
|
||||
List<DistQueue> selectByMsgKey(@Param("msgKey") String msgKey);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ package com.imutil.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.imutil.entity.ImMessage;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 消息主表 Mapper
|
||||
@@ -29,4 +31,62 @@ public interface ImMessageMapper extends BaseMapper<ImMessage> {
|
||||
@Param("msgTime") OffsetDateTime msgTime,
|
||||
@Param("fromStatus") int fromStatus,
|
||||
@Param("toStatus") int toStatus);
|
||||
|
||||
/**
|
||||
* 分页查询消息(管理后台消息记录列表)
|
||||
* <p>
|
||||
* 动态条件 + ORDER BY msg_time DESC + LIMIT/OFFSET。
|
||||
* 必须带 msg_time 范围(dateFrom/dateTo)触发分区裁剪,避免全分区扫描。
|
||||
*
|
||||
* @param tenantId 租户(null/空=全部)
|
||||
* @param convType 会话类型(null=全部,1=C2C 2=GROUP)
|
||||
* @param distStatus 分发状态(null=全部,0待发/1已发/2失败)
|
||||
* @param dateFrom 起始时间(含)
|
||||
* @param dateTo 结束时间(开区间,传次日 0 点以覆盖整天)
|
||||
* @param keyword 模糊关键字(null/空=无,匹配 from/to/conv_id)
|
||||
* @param offset 偏移
|
||||
* @param limit 条数
|
||||
*/
|
||||
@Select("<script>"
|
||||
+ "SELECT msg_key, tenant_id, msg_time, conv_type, conv_id, from_account, to_account, group_id, "
|
||||
+ "msg_type, source, is_cross_tenant, dist_status, created_at "
|
||||
+ "FROM im_message "
|
||||
+ "<where>"
|
||||
+ "<if test='tenantId != null and tenantId != \"\"'>AND tenant_id = #{tenantId} </if>"
|
||||
+ "<if test='convType != null'>AND conv_type = #{convType} </if>"
|
||||
+ "<if test='distStatus != null'>AND dist_status = #{distStatus} </if>"
|
||||
+ "<if test='dateFrom != null'>AND msg_time >= #{dateFrom} </if>"
|
||||
+ "<if test='dateTo != null'>AND msg_time < #{dateTo} </if>"
|
||||
+ "<if test='keyword != null and keyword != \"\"'>AND (from_account ILIKE CONCAT('%', #{keyword}, '%') OR to_account ILIKE CONCAT('%', #{keyword}, '%') OR conv_id ILIKE CONCAT('%', #{keyword}, '%')) </if>"
|
||||
+ "</where>"
|
||||
+ "ORDER BY msg_time DESC, msg_key DESC "
|
||||
+ "LIMIT #{limit} OFFSET #{offset}</script>")
|
||||
List<ImMessage> selectMessagePage(@Param("tenantId") String tenantId,
|
||||
@Param("convType") Integer convType,
|
||||
@Param("distStatus") Integer distStatus,
|
||||
@Param("dateFrom") OffsetDateTime dateFrom,
|
||||
@Param("dateTo") OffsetDateTime dateTo,
|
||||
@Param("keyword") String keyword,
|
||||
@Param("offset") int offset,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 计数(与 selectMessagePage 同条件,用于分页总数)
|
||||
*/
|
||||
@Select("<script>"
|
||||
+ "SELECT COUNT(*) FROM im_message "
|
||||
+ "<where>"
|
||||
+ "<if test='tenantId != null and tenantId != \"\"'>AND tenant_id = #{tenantId} </if>"
|
||||
+ "<if test='convType != null'>AND conv_type = #{convType} </if>"
|
||||
+ "<if test='distStatus != null'>AND dist_status = #{distStatus} </if>"
|
||||
+ "<if test='dateFrom != null'>AND msg_time >= #{dateFrom} </if>"
|
||||
+ "<if test='dateTo != null'>AND msg_time < #{dateTo} </if>"
|
||||
+ "<if test='keyword != null and keyword != \"\"'>AND (from_account ILIKE CONCAT('%', #{keyword}, '%') OR to_account ILIKE CONCAT('%', #{keyword}, '%') OR conv_id ILIKE CONCAT('%', #{keyword}, '%')) </if>"
|
||||
+ "</where></script>")
|
||||
long countMessages(@Param("tenantId") String tenantId,
|
||||
@Param("convType") Integer convType,
|
||||
@Param("distStatus") Integer distStatus,
|
||||
@Param("dateFrom") OffsetDateTime dateFrom,
|
||||
@Param("dateTo") OffsetDateTime dateTo,
|
||||
@Param("keyword") String keyword);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user