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);
|
||||
|
||||
Reference in New Issue
Block a user