From 86e79681da9c98bd08bd1f2be0c6dbd3f3dd3159 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期六, 07 三月 2026 13:47:31 +0800
Subject: [PATCH] #

---
 src/main/java/com/zy/asrs/domain/NotifySendResult.java           |   13 
 src/main/java/com/zy/asrs/domain/param/NotifyResendParam.java    |   15 
 src/main/webapp/views/notifyReport/notifyReport.html             |  839 ++++++++++++++++++++++++++++++++
 src/main/java/com/zy/asrs/service/NotifyAsyncService.java        |   32 
 src/main/java/com/zy/asrs/utils/NotifyUtils.java                 |   52 +
 src/main/java/com/zy/asrs/domain/vo/NotifyReportVo.java          |  109 ++++
 src/main/resources/sql/20260307_add_notify_report_menu.sql       |   51 +
 src/main/java/com/zy/asrs/controller/NotifyReportController.java |  414 +++++++++++++++
 src/main/resources/application.yml                               |    2 
 9 files changed, 1,505 insertions(+), 22 deletions(-)

diff --git a/src/main/java/com/zy/asrs/controller/NotifyReportController.java b/src/main/java/com/zy/asrs/controller/NotifyReportController.java
new file mode 100644
index 0000000..017c759
--- /dev/null
+++ b/src/main/java/com/zy/asrs/controller/NotifyReportController.java
@@ -0,0 +1,414 @@
+package com.zy.asrs.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.mapper.EntityWrapper;
+import com.baomidou.mybatisplus.plugins.Page;
+import com.core.annotations.ManagerAuth;
+import com.core.common.Cools;
+import com.core.common.R;
+import com.zy.asrs.domain.NotifyDto;
+import com.zy.asrs.domain.NotifySendResult;
+import com.zy.asrs.domain.param.NotifyResendParam;
+import com.zy.asrs.domain.vo.NotifyReportVo;
+import com.zy.asrs.entity.HttpRequestLog;
+import com.zy.asrs.service.HttpRequestLogService;
+import com.zy.asrs.service.NotifyAsyncService;
+import com.zy.asrs.utils.NotifyUtils;
+import com.zy.common.utils.RedisUtil;
+import com.zy.common.web.BaseController;
+import com.zy.system.entity.Config;
+import com.zy.system.service.ConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@RestController
+public class NotifyReportController extends BaseController {
+
+    @Autowired
+    private RedisUtil redisUtil;
+    @Autowired
+    private NotifyUtils notifyUtils;
+    @Autowired
+    private NotifyAsyncService notifyAsyncService;
+    @Autowired
+    private HttpRequestLogService httpRequestLogService;
+    @Autowired
+    private ConfigService configService;
+
+    @RequestMapping(value = "/notifyReport/summary/auth")
+    @ManagerAuth
+    public R summary() {
+        Map<String, Object> result = new HashMap<>();
+        String notifyEnable = getConfigValue("notifyEnable");
+        String notifyUri = getConfigValue("notifyUri");
+        String notifyUriPath = getConfigValue("notifyUriPath");
+        String endpoint = buildNotifyEndpoint(notifyUri, notifyUriPath);
+
+        result.put("notifyEnable", notifyEnable);
+        result.put("notifyEnable$", "Y".equalsIgnoreCase(notifyEnable) ? "宸插紑鍚�" : "宸插叧闂�");
+        result.put("notifyUri", notifyUri);
+        result.put("notifyUriPath", notifyUriPath);
+        result.put("notifyEndpoint", endpoint);
+        result.put("queueCount", loadQueueRecords(null, null).size());
+        if (!Cools.isEmpty(endpoint)) {
+            result.put("logCount", httpRequestLogService.selectCount(new EntityWrapper<HttpRequestLog>().eq("name", endpoint)));
+        } else {
+            result.put("logCount", 0);
+        }
+        return R.ok(result);
+    }
+
+    @RequestMapping(value = "/notifyReport/queue/list/auth")
+    @ManagerAuth
+    public R queueList(@RequestParam(defaultValue = "1") Integer curr,
+                       @RequestParam(defaultValue = "15") Integer limit,
+                       @RequestParam(required = false) String notifyType,
+                       @RequestParam(required = false) Integer device,
+                       @RequestParam(required = false) String taskNo,
+                       @RequestParam(required = false) String superTaskNo,
+                       @RequestParam(required = false) String msgType,
+                       @RequestParam(required = false) String condition) {
+        List<NotifyReportVo> records = loadQueueRecords(notifyType, device);
+        List<NotifyReportVo> filtered = new ArrayList<>();
+        for (NotifyReportVo record : records) {
+            if (!matchesQueue(record, notifyType, device, taskNo, superTaskNo, msgType, condition)) {
+                continue;
+            }
+            filtered.add(record);
+        }
+        filtered.sort(Comparator.comparing(NotifyReportVo::getId, Comparator.nullsLast(Long::compareTo)).reversed());
+        return R.ok(buildPage(curr, limit, filtered));
+    }
+
+    @RequestMapping(value = "/notifyReport/log/list/auth")
+    @ManagerAuth
+    public R logList(@RequestParam(defaultValue = "1") Integer curr,
+                     @RequestParam(defaultValue = "15") Integer limit,
+                     @RequestParam(required = false) String notifyType,
+                     @RequestParam(required = false) Integer device,
+                     @RequestParam(required = false) String taskNo,
+                     @RequestParam(required = false) String superTaskNo,
+                     @RequestParam(required = false) String msgType,
+                     @RequestParam(required = false) Integer result,
+                     @RequestParam(required = false) String condition) {
+        String endpoint = buildNotifyEndpoint(getConfigValue("notifyUri"), getConfigValue("notifyUriPath"));
+        EntityWrapper<HttpRequestLog> wrapper = new EntityWrapper<>();
+        if (!Cools.isEmpty(endpoint)) {
+            wrapper.eq("name", endpoint);
+        } else {
+            wrapper.like("request", "\"notifyType\"");
+        }
+        if (!Cools.isEmpty(taskNo)) {
+            wrapper.like("request", taskNo);
+        }
+        if (!Cools.isEmpty(superTaskNo)) {
+            wrapper.like("request", superTaskNo);
+        }
+        if (!Cools.isEmpty(msgType)) {
+            wrapper.like("request", msgType);
+        }
+        if (!Cools.isEmpty(notifyType)) {
+            wrapper.like("request", "\"notifyType\":\"" + notifyType + "\"");
+        }
+        if (device != null) {
+            wrapper.like("request", "\"device\":" + device);
+        }
+        if (!Cools.isEmpty(condition)) {
+            wrapper.andNew().like("request", condition).or().like("response", condition);
+        }
+        if (result != null) {
+            wrapper.eq("result", result);
+        }
+        wrapper.orderBy("create_time", false);
+
+        Page<HttpRequestLog> logPage = httpRequestLogService.selectPage(new Page<>(curr, limit), wrapper);
+        Page<NotifyReportVo> resultPage = new Page<>(curr, limit);
+        resultPage.setTotal(logPage.getTotal());
+
+        List<NotifyReportVo> rows = new ArrayList<>();
+        for (HttpRequestLog log : logPage.getRecords()) {
+            rows.add(buildLogVo(log));
+        }
+        resultPage.setRecords(rows);
+        return R.ok(resultPage);
+    }
+
+    @RequestMapping(value = "/notifyReport/resend/auth")
+    @ManagerAuth
+    public R resend(@RequestBody NotifyResendParam param) {
+        if (param == null || Cools.isEmpty(param.getSourceType())) {
+            return R.error("琛ュ彂鍙傛暟涓嶈兘涓虹┖");
+        }
+
+        String notifyUri = getConfigValue("notifyUri");
+        String notifyUriPath = getConfigValue("notifyUriPath");
+        if (Cools.isEmpty(notifyUri) || Cools.isEmpty(notifyUriPath)) {
+            return R.error("璇峰厛閰嶇疆 notifyUri 鍜� notifyUriPath");
+        }
+
+        List<Map<String, Object>> details = new ArrayList<>();
+        int successCount = 0;
+        int failCount = 0;
+
+        if ("queue".equalsIgnoreCase(param.getSourceType())) {
+            if (Cools.isEmpty(param.getRedisKeys())) {
+                return R.error("璇烽�夋嫨瑕佽ˉ鍙戠殑闃熷垪閫氱煡");
+            }
+            for (String redisKey : param.getRedisKeys()) {
+                Map<String, Object> detail = resendQueue(redisKey, notifyUri, notifyUriPath);
+                details.add(detail);
+                if (Boolean.TRUE.equals(detail.get("success"))) {
+                    successCount++;
+                } else {
+                    failCount++;
+                }
+            }
+        } else if ("log".equalsIgnoreCase(param.getSourceType())) {
+            if (Cools.isEmpty(param.getLogIds())) {
+                return R.error("璇烽�夋嫨瑕佽ˉ鍙戠殑閫氱煡鏃ュ織");
+            }
+            for (Long logId : param.getLogIds()) {
+                Map<String, Object> detail = resendLog(logId, notifyUri, notifyUriPath);
+                details.add(detail);
+                if (Boolean.TRUE.equals(detail.get("success"))) {
+                    successCount++;
+                } else {
+                    failCount++;
+                }
+            }
+        } else {
+            return R.error("涓嶆敮鎸佺殑琛ュ彂鏉ユ簮");
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("successCount", successCount);
+        result.put("failCount", failCount);
+        result.put("allSuccess", failCount == 0);
+        result.put("details", details);
+        return R.ok(result);
+    }
+
+    private Page<NotifyReportVo> buildPage(Integer curr, Integer limit, List<NotifyReportVo> records) {
+        Page<NotifyReportVo> page = new Page<>(curr, limit);
+        page.setTotal(records.size());
+
+        int fromIndex = Math.max((curr - 1) * limit, 0);
+        if (fromIndex >= records.size()) {
+            page.setRecords(new ArrayList<>());
+            return page;
+        }
+        int toIndex = Math.min(fromIndex + limit, records.size());
+        page.setRecords(new ArrayList<>(records.subList(fromIndex, toIndex)));
+        return page;
+    }
+
+    private List<NotifyReportVo> loadQueueRecords(String notifyType, Integer device) {
+        Set<String> keys = new LinkedHashSet<>();
+        List<String> notifyTypes = new ArrayList<>();
+        if (!Cools.isEmpty(notifyType)) {
+            notifyTypes.add(notifyType);
+        } else {
+            notifyTypes.addAll(notifyUtils.getSupportedNotifyTypes());
+        }
+
+        for (String item : notifyTypes) {
+            String prefix = notifyUtils.getKeyPrefix(item);
+            if (Cools.isEmpty(prefix)) {
+                continue;
+            }
+
+            String pattern = device == null ? prefix + "*" : prefix + device + "_*";
+            Set matched = redisUtil.keys(pattern);
+            if (matched == null) {
+                continue;
+            }
+            for (Object key : matched) {
+                if (key != null) {
+                    keys.add(String.valueOf(key));
+                }
+            }
+        }
+
+        List<NotifyReportVo> rows = new ArrayList<>();
+        for (String key : keys) {
+            Object object = redisUtil.get(key);
+            if (!(object instanceof NotifyDto)) {
+                continue;
+            }
+            rows.add(buildQueueVo(key, (NotifyDto) object));
+        }
+        return rows;
+    }
+
+    private NotifyReportVo buildQueueVo(String redisKey, NotifyDto dto) {
+        NotifyReportVo vo = new NotifyReportVo();
+        vo.setSourceType("queue");
+        vo.setRedisKey(redisKey);
+        vo.setId(dto.getId());
+        vo.setNotifyType(dto.getNotifyType());
+        vo.setDevice(dto.getDevice());
+        vo.setTaskNo(dto.getTaskNo());
+        vo.setSuperTaskNo(dto.getSuperTaskNo());
+        vo.setMsgType(dto.getMsgType());
+        vo.setMsgDesc(dto.getMsgDesc());
+        vo.setData(dto.getData());
+        vo.setFailTimes(dto.getFailTimes());
+        vo.setRetryTimes(dto.getRetryTimes());
+        vo.setRetryTime(dto.getRetryTime());
+        vo.setLastRetryTime(dto.getLastRetryTime());
+        vo.setRequestPayload(JSON.toJSONString(dto));
+        return vo;
+    }
+
+    private NotifyReportVo buildLogVo(HttpRequestLog log) {
+        NotifyReportVo vo = new NotifyReportVo();
+        vo.setSourceType("log");
+        vo.setLogId(log.getId());
+        vo.setCreateTime(log.getCreateTime());
+        vo.setResult(log.getResult());
+        vo.setResponse(log.getResponse());
+        vo.setRequestPayload(log.getRequest());
+
+        NotifyDto dto = parseNotifyDto(log.getRequest());
+        if (dto != null) {
+            vo.setId(dto.getId());
+            vo.setNotifyType(dto.getNotifyType());
+            vo.setDevice(dto.getDevice());
+            vo.setTaskNo(dto.getTaskNo());
+            vo.setSuperTaskNo(dto.getSuperTaskNo());
+            vo.setMsgType(dto.getMsgType());
+            vo.setMsgDesc(dto.getMsgDesc());
+            vo.setData(dto.getData());
+            vo.setFailTimes(dto.getFailTimes());
+            vo.setRetryTimes(dto.getRetryTimes());
+            vo.setRetryTime(dto.getRetryTime());
+            vo.setLastRetryTime(dto.getLastRetryTime());
+        } else {
+            vo.setData(log.getRequest());
+        }
+        return vo;
+    }
+
+    private boolean matchesQueue(NotifyReportVo record, String notifyType, Integer device, String taskNo,
+                                 String superTaskNo, String msgType, String condition) {
+        if (!Cools.isEmpty(notifyType) && !notifyType.equals(record.getNotifyType())) {
+            return false;
+        }
+        if (device != null && !device.equals(record.getDevice())) {
+            return false;
+        }
+        if (!containsValue(record.getTaskNo(), taskNo)) {
+            return false;
+        }
+        if (!containsValue(record.getSuperTaskNo(), superTaskNo)) {
+            return false;
+        }
+        if (!containsValue(record.getMsgType(), msgType) && !containsValue(record.getMsgDesc(), msgType)) {
+            return false;
+        }
+        if (Cools.isEmpty(condition)) {
+            return true;
+        }
+        return containsValue(record.getTaskNo(), condition)
+                || containsValue(record.getSuperTaskNo(), condition)
+                || containsValue(record.getMsgType(), condition)
+                || containsValue(record.getMsgDesc(), condition)
+                || containsValue(record.getData(), condition)
+                || containsValue(record.getNotifyType$(), condition)
+                || containsValue(record.getRedisKey(), condition);
+    }
+
+    private boolean containsValue(String source, String target) {
+        if (Cools.isEmpty(target)) {
+            return true;
+        }
+        if (Cools.isEmpty(source)) {
+            return false;
+        }
+        return source.contains(target);
+    }
+
+    private Map<String, Object> resendQueue(String redisKey, String notifyUri, String notifyUriPath) {
+        Map<String, Object> detail = new HashMap<>();
+        detail.put("sourceType", "queue");
+        detail.put("redisKey", redisKey);
+
+        Object object = redisUtil.get(redisKey);
+        if (!(object instanceof NotifyDto)) {
+            detail.put("success", false);
+            detail.put("message", "闃熷垪閫氱煡涓嶅瓨鍦ㄦ垨宸茶娑堣垂");
+            return detail;
+        }
+
+        NotifyDto notifyDto = (NotifyDto) object;
+        NotifySendResult result = notifyAsyncService.sendNotifyNow(notifyUri, notifyUriPath, redisKey, notifyDto, true, false);
+        detail.put("success", result.isSuccess());
+        detail.put("message", result.getMessage());
+        detail.put("taskNo", notifyDto.getTaskNo());
+        detail.put("superTaskNo", notifyDto.getSuperTaskNo());
+        detail.put("response", result.getResponse());
+        return detail;
+    }
+
+    private Map<String, Object> resendLog(Long logId, String notifyUri, String notifyUriPath) {
+        Map<String, Object> detail = new HashMap<>();
+        detail.put("sourceType", "log");
+        detail.put("logId", logId);
+
+        HttpRequestLog log = httpRequestLogService.selectById(logId);
+        if (log == null) {
+            detail.put("success", false);
+            detail.put("message", "閫氱煡鏃ュ織涓嶅瓨鍦�");
+            return detail;
+        }
+
+        NotifyDto notifyDto = parseNotifyDto(log.getRequest());
+        if (notifyDto == null) {
+            detail.put("success", false);
+            detail.put("message", "鏃ュ織璇锋眰鎶ユ枃鏃犳硶瑙f瀽涓洪�氱煡瀵硅薄");
+            return detail;
+        }
+
+        NotifySendResult result = notifyAsyncService.sendNotifyNow(notifyUri, notifyUriPath, null, notifyDto, false, false);
+        detail.put("success", result.isSuccess());
+        detail.put("message", result.getMessage());
+        detail.put("taskNo", notifyDto.getTaskNo());
+        detail.put("superTaskNo", notifyDto.getSuperTaskNo());
+        detail.put("response", result.getResponse());
+        return detail;
+    }
+
+    private NotifyDto parseNotifyDto(String request) {
+        if (Cools.isEmpty(request)) {
+            return null;
+        }
+        try {
+            return JSON.parseObject(request, NotifyDto.class);
+        } catch (Exception ignored) {
+            return null;
+        }
+    }
+
+    private String getConfigValue(String code) {
+        Config config = configService.selectOne(new EntityWrapper<Config>().eq("code", code));
+        return config == null ? null : config.getValue();
+    }
+
+    private String buildNotifyEndpoint(String notifyUri, String notifyUriPath) {
+        if (Cools.isEmpty(notifyUri) || Cools.isEmpty(notifyUriPath)) {
+            return null;
+        }
+        return notifyUri + notifyUriPath;
+    }
+}
diff --git a/src/main/java/com/zy/asrs/domain/NotifySendResult.java b/src/main/java/com/zy/asrs/domain/NotifySendResult.java
new file mode 100644
index 0000000..c92adec
--- /dev/null
+++ b/src/main/java/com/zy/asrs/domain/NotifySendResult.java
@@ -0,0 +1,13 @@
+package com.zy.asrs.domain;
+
+import lombok.Data;
+
+@Data
+public class NotifySendResult {
+
+    private boolean success;
+
+    private String response;
+
+    private String message;
+}
diff --git a/src/main/java/com/zy/asrs/domain/param/NotifyResendParam.java b/src/main/java/com/zy/asrs/domain/param/NotifyResendParam.java
new file mode 100644
index 0000000..7c41c13
--- /dev/null
+++ b/src/main/java/com/zy/asrs/domain/param/NotifyResendParam.java
@@ -0,0 +1,15 @@
+package com.zy.asrs.domain.param;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class NotifyResendParam {
+
+    private String sourceType;
+
+    private List<String> redisKeys;
+
+    private List<Long> logIds;
+}
diff --git a/src/main/java/com/zy/asrs/domain/vo/NotifyReportVo.java b/src/main/java/com/zy/asrs/domain/vo/NotifyReportVo.java
new file mode 100644
index 0000000..1e62426
--- /dev/null
+++ b/src/main/java/com/zy/asrs/domain/vo/NotifyReportVo.java
@@ -0,0 +1,109 @@
+package com.zy.asrs.domain.vo;
+
+import com.core.common.Cools;
+import lombok.Data;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+@Data
+public class NotifyReportVo {
+
+    private String sourceType;
+
+    private String redisKey;
+
+    private Long logId;
+
+    private Long id;
+
+    private String notifyType;
+
+    private Integer device;
+
+    private String taskNo;
+
+    private String superTaskNo;
+
+    private String msgType;
+
+    private String msgDesc;
+
+    private String data;
+
+    private String requestPayload;
+
+    private Integer failTimes;
+
+    private Integer retryTimes;
+
+    private Integer retryTime;
+
+    private Long lastRetryTime;
+
+    private Date createTime;
+
+    private Integer result;
+
+    private String response;
+
+    public String getNotifyType$() {
+        if (Cools.isEmpty(this.notifyType)) {
+            return "";
+        }
+        if ("Crn".equals(this.notifyType)) {
+            return "鍫嗗灈鏈�";
+        }
+        if ("Devp".equals(this.notifyType)) {
+            return "杈撻�佺嚎";
+        }
+        if ("DualCrn".equals(this.notifyType)) {
+            return "鍙屼几浣嶅爢鍨涙満";
+        }
+        if ("Rgv".equals(this.notifyType)) {
+            return "RGV";
+        }
+        if ("task".equals(this.notifyType)) {
+            return "浠诲姟";
+        }
+        return this.notifyType;
+    }
+
+    public String getRetryProgress$() {
+        if (this.retryTimes == null && this.failTimes == null) {
+            return "";
+        }
+        return (this.retryTimes == null ? 0 : this.retryTimes) + "/" + (this.failTimes == null ? 0 : this.failTimes);
+    }
+
+    public String getQueueStatus$() {
+        if (!"queue".equals(this.sourceType)) {
+            return "";
+        }
+        if (this.retryTimes == null || this.retryTimes == 0) {
+            return "寰呭彂閫�";
+        }
+        return "閲嶈瘯涓�";
+    }
+
+    public String getResult$() {
+        if (this.result == null) {
+            return "";
+        }
+        return this.result == 1 ? "鎴愬姛" : "澶辫触";
+    }
+
+    public String getLastRetryTime$() {
+        if (this.lastRetryTime == null || this.lastRetryTime <= 0L) {
+            return "";
+        }
+        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(this.lastRetryTime));
+    }
+
+    public String getCreateTime$() {
+        if (this.createTime == null) {
+            return "";
+        }
+        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.createTime);
+    }
+}
diff --git a/src/main/java/com/zy/asrs/service/NotifyAsyncService.java b/src/main/java/com/zy/asrs/service/NotifyAsyncService.java
index e2ac52c..7bde6e2 100644
--- a/src/main/java/com/zy/asrs/service/NotifyAsyncService.java
+++ b/src/main/java/com/zy/asrs/service/NotifyAsyncService.java
@@ -3,6 +3,7 @@
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.zy.asrs.domain.NotifyDto;
+import com.zy.asrs.domain.NotifySendResult;
 import com.zy.asrs.entity.HttpRequestLog;
 import com.zy.common.utils.HttpHandler;
 import com.zy.common.utils.RedisUtil;
@@ -36,40 +37,51 @@
      */
     @Async
     public void sendNotifyAsync(String notifyUri, String notifyUriPath, String key, NotifyDto notifyDto) {
+        sendNotifyNow(notifyUri, notifyUriPath, key, notifyDto, true, true);
+    }
+
+    public NotifySendResult sendNotifyNow(String notifyUri, String notifyUriPath, String key, NotifyDto notifyDto,
+                                          boolean deleteOnSuccess, boolean updateRetryState) {
         HttpRequestLog httpRequestLog = new HttpRequestLog();
         httpRequestLog.setName(notifyUri + notifyUriPath);
         httpRequestLog.setRequest(JSON.toJSONString(notifyDto));
         httpRequestLog.setCreateTime(new Date());
 
-        boolean success = false;
+        NotifySendResult result = new NotifySendResult();
+        result.setSuccess(false);
         try {
-            // 瑙﹀彂閫氱煡
             String response = new HttpHandler.Builder()
                     .setUri(notifyUri)
                     .setPath(notifyUriPath)
                     .setJson(JSON.toJSONString(notifyDto))
                     .build()
                     .doPost();
+            result.setResponse(response);
             httpRequestLog.setResponse(response);
 
             JSONObject jsonObject = JSON.parseObject(response);
-            Integer code = jsonObject.getInteger("code");
-            if (code == 200) {
-                // 閫氱煡鎴愬姛
-                redisUtil.del(key);
-                success = true;
+            Integer code = jsonObject == null ? null : jsonObject.getInteger("code");
+            if (code != null && code == 200) {
+                if (deleteOnSuccess && key != null) {
+                    redisUtil.del(key);
+                }
+                result.setSuccess(true);
+                result.setMessage("閫氱煡鎴愬姛");
+            } else {
+                result.setMessage("閫氱煡鎺ュ彛杩斿洖澶辫触");
             }
         } catch (Exception e) {
             log.error("寮傛閫氱煡澶辫触, key={}", key, e);
+            result.setMessage("閫氱煡寮傚父: " + e.getMessage());
         } finally {
-            // 淇濆瓨璁板綍
+            httpRequestLog.setResult(result.isSuccess() ? 1 : 0);
             httpRequestLogService.insert(httpRequestLog);
         }
 
-        if (!success) {
-            // 閫氱煡澶辫触锛屾洿鏂伴噸璇曟鏁�
+        if (!result.isSuccess() && updateRetryState && key != null) {
             handleNotifyFailure(key, notifyDto);
         }
+        return result;
     }
 
     /**
diff --git a/src/main/java/com/zy/asrs/utils/NotifyUtils.java b/src/main/java/com/zy/asrs/utils/NotifyUtils.java
index c573ccf..4bda147 100644
--- a/src/main/java/com/zy/asrs/utils/NotifyUtils.java
+++ b/src/main/java/com/zy/asrs/utils/NotifyUtils.java
@@ -13,6 +13,7 @@
 import org.springframework.stereotype.Component;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
 
@@ -40,7 +41,7 @@
             return null;
         }
 
-        Set keys = redisUtil.keys(key + "*");
+        Set keys = redisUtil.keys(key + "_*");
         if (keys == null) {
             return null;
         }
@@ -52,23 +53,52 @@
         return list;
     }
 
-    public String getKey(String notifyType, Integer device) {
-        String key = null;
+    public List<String> getSupportedNotifyTypes() {
+        return Arrays.asList(
+                String.valueOf(SlaveType.Crn),
+                String.valueOf(SlaveType.Devp),
+                String.valueOf(SlaveType.DualCrn),
+                String.valueOf(SlaveType.Rgv),
+                "task"
+        );
+    }
+
+    public String getKeyPrefix(String notifyType) {
         if (notifyType.equals(String.valueOf(SlaveType.Crn))) {
-            key = RedisKeyType.QUEUE_CRN.key + device;
+            return RedisKeyType.QUEUE_CRN.key;
         } else if (notifyType.equals(String.valueOf(SlaveType.Devp))) {
-            key = RedisKeyType.QUEUE_DEVP.key + device;
+            return RedisKeyType.QUEUE_DEVP.key;
         } else if (notifyType.equals(String.valueOf(SlaveType.DualCrn))) {
-            key = RedisKeyType.QUEUE_DUAL_CRN.key + device;
+            return RedisKeyType.QUEUE_DUAL_CRN.key;
         } else if (notifyType.equals(String.valueOf(SlaveType.Rgv))) {
-            key = RedisKeyType.QUEUE_RGV.key + device;
+            return RedisKeyType.QUEUE_RGV.key;
         } else if (notifyType.equals("task")) {
-            key = RedisKeyType.QUEUE_TASK.key + device;
-        } else {
+            return RedisKeyType.QUEUE_TASK.key;
+        }
+        return null;
+    }
+
+    public String getNotifyTypeDesc(String notifyType) {
+        if (notifyType.equals(String.valueOf(SlaveType.Crn))) {
+            return "鍫嗗灈鏈�";
+        } else if (notifyType.equals(String.valueOf(SlaveType.Devp))) {
+            return "杈撻�佺嚎";
+        } else if (notifyType.equals(String.valueOf(SlaveType.DualCrn))) {
+            return "鍙屼几浣嶅爢鍨涙満";
+        } else if (notifyType.equals(String.valueOf(SlaveType.Rgv))) {
+            return "RGV";
+        } else if (notifyType.equals("task")) {
+            return "浠诲姟";
+        }
+        return notifyType;
+    }
+
+    public String getKey(String notifyType, Integer device) {
+        String prefix = getKeyPrefix(notifyType);
+        if (prefix == null) {
             return null;
         }
-
-        return key;
+        return prefix + device;
     }
 
     private boolean append(String notifyType, Integer device, String taskNo, String superTaskNo, NotifyMsgType msgType, String data) {
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index f809816..87e4b79 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -1,6 +1,6 @@
 # 绯荤粺鐗堟湰淇℃伅
 app:
-  version: 1.0.5.0
+  version: 1.0.5.1
   version-type: dev  # prd 鎴� dev
 
 server:
diff --git a/src/main/resources/sql/20260307_add_notify_report_menu.sql b/src/main/resources/sql/20260307_add_notify_report_menu.sql
new file mode 100644
index 0000000..a83ae50
--- /dev/null
+++ b/src/main/resources/sql/20260307_add_notify_report_menu.sql
@@ -0,0 +1,51 @@
+-- 灏� 閫氱煡涓婃姤 鑿滃崟鎸傝浇鍒帮細鏃ュ織鎶ヨ〃锛堜紭鍏堬級鎴栧紑鍙戜笓鐢�
+-- 璇存槑锛氭墽琛屾湰鑴氭湰鍚庯紝璇峰湪鈥滆鑹叉巿鏉冣�濋噷缁欏搴旇鑹插嬀閫夋柊鑿滃崟鍜屸�滄煡鐪嬧�濇潈闄愩��
+
+SET @notify_parent_id := COALESCE(
+  (
+    SELECT id
+    FROM sys_resource
+    WHERE code = 'logReport' AND level = 1
+    ORDER BY id
+    LIMIT 1
+  ),
+  (
+    SELECT id
+    FROM sys_resource
+    WHERE code = 'develop' AND level = 1
+    ORDER BY id
+    LIMIT 1
+  )
+);
+
+INSERT INTO sys_resource(code, name, resource_id, level, sort, status)
+SELECT 'notifyReport/notifyReport.html', '閫氱煡涓婃姤', @notify_parent_id, 2, 998, 1
+FROM dual
+WHERE @notify_parent_id IS NOT NULL
+  AND NOT EXISTS (
+    SELECT 1
+    FROM sys_resource
+    WHERE code = 'notifyReport/notifyReport.html' AND level = 2
+  );
+
+SET @notify_report_id := (
+  SELECT id
+  FROM sys_resource
+  WHERE code = 'notifyReport/notifyReport.html' AND level = 2
+  ORDER BY id
+  LIMIT 1
+);
+
+INSERT INTO sys_resource(code, name, resource_id, level, sort, status)
+SELECT 'notifyReport/notifyReport.html#view', '鏌ョ湅', @notify_report_id, 3, 1, 1
+FROM dual
+WHERE @notify_report_id IS NOT NULL
+  AND NOT EXISTS (
+    SELECT 1
+    FROM sys_resource
+    WHERE code = 'notifyReport/notifyReport.html#view' AND level = 3
+  );
+
+SELECT id, code, name, resource_id, level, sort, status
+FROM sys_resource
+WHERE code IN ('notifyReport/notifyReport.html', 'notifyReport/notifyReport.html#view');
diff --git a/src/main/webapp/views/notifyReport/notifyReport.html b/src/main/webapp/views/notifyReport/notifyReport.html
new file mode 100644
index 0000000..b5cbb67
--- /dev/null
+++ b/src/main/webapp/views/notifyReport/notifyReport.html
@@ -0,0 +1,839 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>閫氱煡涓婃姤</title>
+  <link rel="stylesheet" href="../../static/vue/element/element.css" />
+  <style>
+    [v-cloak] {
+      display: none;
+    }
+
+    html,
+    body {
+      margin: 0;
+      min-height: 100%;
+      font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif;
+      background:
+        radial-gradient(1200px 420px at 0% -5%, rgba(36, 91, 160, 0.14), transparent 55%),
+        radial-gradient(1000px 500px at 100% 0%, rgba(35, 147, 120, 0.11), transparent 60%),
+        linear-gradient(180deg, #f2f6fb 0%, #edf2f7 100%);
+      color: #243447;
+    }
+
+    .page-shell {
+      max-width: 1680px;
+      margin: 0 auto;
+      padding: 16px;
+      box-sizing: border-box;
+    }
+
+    .hero {
+      border-radius: 18px;
+      padding: 18px 20px 16px;
+      color: #fff;
+      background: linear-gradient(135deg, #114a7a 0%, #226ca1 44%, #1f9c8c 100%);
+      box-shadow: 0 18px 36px rgba(17, 74, 122, 0.22);
+      margin-bottom: 14px;
+      overflow: hidden;
+      position: relative;
+    }
+
+    .hero::after {
+      content: "";
+      position: absolute;
+      right: -60px;
+      top: -90px;
+      width: 280px;
+      height: 280px;
+      border-radius: 50%;
+      background: rgba(255, 255, 255, 0.08);
+      filter: blur(2px);
+    }
+
+    .hero-head {
+      position: relative;
+      z-index: 1;
+      display: flex;
+      align-items: flex-start;
+      justify-content: space-between;
+      gap: 12px;
+      flex-wrap: wrap;
+    }
+
+    .hero-title {
+      display: flex;
+      flex-direction: column;
+      gap: 6px;
+    }
+
+    .hero-title-main {
+      font-size: 22px;
+      font-weight: 700;
+      letter-spacing: 0.4px;
+    }
+
+    .hero-title-sub {
+      font-size: 13px;
+      opacity: 0.9;
+      max-width: 760px;
+      line-height: 1.6;
+    }
+
+    .hero-actions {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      flex-wrap: wrap;
+    }
+
+    .summary-grid {
+      position: relative;
+      z-index: 1;
+      margin-top: 16px;
+      display: grid;
+      grid-template-columns: repeat(4, minmax(0, 1fr));
+      gap: 10px;
+    }
+
+    .summary-card {
+      min-height: 78px;
+      border-radius: 14px;
+      padding: 12px 14px;
+      background: rgba(255, 255, 255, 0.15);
+      border: 1px solid rgba(255, 255, 255, 0.22);
+      backdrop-filter: blur(3px);
+    }
+
+    .summary-card .label {
+      font-size: 12px;
+      opacity: 0.86;
+    }
+
+    .summary-card .value {
+      margin-top: 7px;
+      font-size: 24px;
+      font-weight: 700;
+      line-height: 1.15;
+      word-break: break-all;
+    }
+
+    .summary-card .sub {
+      margin-top: 6px;
+      font-size: 12px;
+      opacity: 0.88;
+      word-break: break-all;
+    }
+
+    .panel {
+      border-radius: 18px;
+      border: 1px solid #dbe4ef;
+      background:
+        radial-gradient(700px 220px at -10% 0%, rgba(41, 112, 196, 0.05), transparent 55%),
+        radial-gradient(860px 260px at 110% 16%, rgba(31, 156, 140, 0.06), transparent 58%),
+        rgba(250, 252, 255, 0.88);
+      box-shadow: 0 16px 32px rgba(39, 63, 92, 0.08);
+      overflow: hidden;
+    }
+
+    .toolbar-card {
+      padding: 16px 16px 6px;
+      margin-bottom: 14px;
+    }
+
+    .toolbar-title {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      gap: 10px;
+      margin-bottom: 14px;
+      flex-wrap: wrap;
+    }
+
+    .toolbar-title .main {
+      font-size: 16px;
+      font-weight: 700;
+      color: #243447;
+    }
+
+    .toolbar-title .sub {
+      font-size: 12px;
+      color: #7b8ba1;
+    }
+
+    .filter-grid {
+      display: grid;
+      grid-template-columns: repeat(6, minmax(0, 1fr));
+      gap: 12px;
+    }
+
+    .filter-item.full {
+      grid-column: span 2;
+    }
+
+    .filter-actions {
+      margin-top: 14px;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      gap: 12px;
+      flex-wrap: wrap;
+    }
+
+    .filter-actions-left,
+    .filter-actions-right {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      flex-wrap: wrap;
+    }
+
+    .board {
+      padding: 14px;
+    }
+
+    .board-head {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      gap: 10px;
+      margin-bottom: 12px;
+      flex-wrap: wrap;
+    }
+
+    .board-head .title {
+      font-size: 15px;
+      font-weight: 700;
+      color: #26384d;
+    }
+
+    .board-head .desc {
+      font-size: 12px;
+      color: #8594a8;
+    }
+
+    .board .el-tabs__nav-wrap::after {
+      background: rgba(222, 230, 238, 0.86);
+    }
+
+    .table-shell {
+      border-radius: 16px;
+      overflow: hidden;
+      border: 1px solid #e0e8f2;
+      background: rgba(255, 255, 255, 0.9);
+    }
+
+    .table-toolbar {
+      padding: 12px 12px 0;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      gap: 10px;
+      flex-wrap: wrap;
+    }
+
+    .table-toolbar-left,
+    .table-toolbar-right {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      flex-wrap: wrap;
+    }
+
+    .table-note {
+      font-size: 12px;
+      color: #8090a4;
+    }
+
+    .code-text {
+      font-family: Menlo, Monaco, Consolas, "Liberation Mono", monospace;
+      font-size: 12px;
+    }
+
+    .payload-box {
+      max-height: 62vh;
+      overflow: auto;
+      border-radius: 12px;
+      border: 1px solid #d9e4f0;
+      background: linear-gradient(180deg, #fbfdff 0%, #f4f8fc 100%);
+      padding: 14px;
+      margin: 0;
+      white-space: pre-wrap;
+      word-break: break-word;
+      line-height: 1.65;
+      color: #28405a;
+      font-family: Menlo, Monaco, Consolas, "Liberation Mono", monospace;
+      font-size: 12px;
+    }
+
+    .pagination-wrap {
+      padding: 12px 14px 14px;
+      display: flex;
+      justify-content: flex-end;
+      background: rgba(255, 255, 255, 0.72);
+    }
+
+    .empty-hint {
+      padding: 34px 12px;
+      text-align: center;
+      color: #8c9aae;
+      font-size: 13px;
+    }
+
+    @media (max-width: 1360px) {
+      .summary-grid {
+        grid-template-columns: repeat(2, minmax(0, 1fr));
+      }
+
+      .filter-grid {
+        grid-template-columns: repeat(3, minmax(0, 1fr));
+      }
+
+      .filter-item.full {
+        grid-column: span 1;
+      }
+    }
+
+    @media (max-width: 900px) {
+      .page-shell {
+        padding: 10px;
+      }
+
+      .summary-grid,
+      .filter-grid {
+        grid-template-columns: 1fr;
+      }
+
+      .hero-title-main {
+        font-size: 18px;
+      }
+
+      .table-toolbar,
+      .filter-actions {
+        align-items: flex-start;
+      }
+
+      .pagination-wrap {
+        justify-content: center;
+      }
+    }
+  </style>
+</head>
+<body>
+<div id="app" class="page-shell" v-cloak>
+  <section class="hero">
+    <div class="hero-head">
+      <div class="hero-title">
+        <div class="hero-title-main">閫氱煡涓婃姤涓績</div>
+        <div class="hero-title-sub">鏌ョ湅褰撳墠寰呴�氱煡闃熷垪銆佹帴鍙e彂閫佹棩蹇楋紝鏀寔鎸変换鍔�/璁惧蹇�熺瓫閫夛紝骞跺澶辫触鎴栧緟鍙戦�侀�氱煡鎵ц鎵嬪姩琛ュ彂銆�</div>
+      </div>
+      <div class="hero-actions">
+        <el-button type="primary" icon="el-icon-refresh" :loading="summaryLoading" @click="refreshAll">鍒锋柊鍏ㄥ眬</el-button>
+      </div>
+    </div>
+    <div class="summary-grid">
+      <div class="summary-card">
+        <div class="label">涓婃姤寮�鍏�</div>
+        <div class="value">{{ summary['notifyEnable$'] || '-' }}</div>
+        <div class="sub">`notifyEnable` = {{ summary.notifyEnable || '-' }}</div>
+      </div>
+      <div class="summary-card">
+        <div class="label">褰撳墠闃熷垪鏁�</div>
+        <div class="value">{{ summary.queueCount || 0 }}</div>
+        <div class="sub">Redis 涓緟涓婃姤鎴栧緟閲嶈瘯閫氱煡</div>
+      </div>
+      <div class="summary-card">
+        <div class="label">閫氱煡鏃ュ織鏁�</div>
+        <div class="value">{{ summary.logCount || 0 }}</div>
+        <div class="sub">閫氱煡鎺ュ彛鍘嗗彶璋冪敤璁板綍</div>
+      </div>
+      <div class="summary-card">
+        <div class="label">閫氱煡鍦板潃</div>
+        <div class="value" style="font-size: 16px;">{{ summary.notifyEndpoint || '-' }}</div>
+        <div class="sub">鏉ユ簮锛歯otifyUri + notifyUriPath</div>
+      </div>
+    </div>
+  </section>
+
+  <section class="panel toolbar-card">
+    <div class="toolbar-title">
+      <div>
+        <div class="main">绛涢�夋潯浠�</div>
+        <div class="sub">闃熷垪涓庢棩蹇楀叡鐢ㄥ悓涓�缁勬煡璇㈡潯浠讹紝鍒囨崲椤电鏃朵繚鎸佷竴鑷淬��</div>
+      </div>
+    </div>
+
+    <div class="filter-grid">
+      <div class="filter-item">
+        <el-select v-model="filters.notifyType" clearable placeholder="閫氱煡绫诲瀷" size="medium">
+          <el-option label="鍫嗗灈鏈�" value="Crn"></el-option>
+          <el-option label="鍙屼几浣嶅爢鍨涙満" value="DualCrn"></el-option>
+          <el-option label="杈撻�佺嚎" value="Devp"></el-option>
+          <el-option label="RGV" value="Rgv"></el-option>
+          <el-option label="浠诲姟" value="task"></el-option>
+        </el-select>
+      </div>
+      <div class="filter-item">
+        <el-input v-model.trim="filters.device" placeholder="璁惧鍙�" clearable></el-input>
+      </div>
+      <div class="filter-item">
+        <el-input v-model.trim="filters.taskNo" placeholder="浠诲姟鍙�" clearable></el-input>
+      </div>
+      <div class="filter-item">
+        <el-input v-model.trim="filters.superTaskNo" placeholder="涓婄骇浠诲姟鍙�" clearable></el-input>
+      </div>
+      <div class="filter-item">
+        <el-input v-model.trim="filters.msgType" placeholder="娑堟伅绫诲瀷/鎻忚堪" clearable></el-input>
+      </div>
+      <div class="filter-item">
+        <el-select v-model="filters.result" clearable placeholder="鏃ュ織缁撴灉">
+          <el-option label="鎴愬姛" :value="1"></el-option>
+          <el-option label="澶辫触" :value="0"></el-option>
+        </el-select>
+      </div>
+      <div class="filter-item full">
+        <el-input v-model.trim="filters.condition" placeholder="閫氱敤鎼滅储锛氫换鍔″彿銆佹秷鎭弿杩般�佹姤鏂囧叧閿瓧銆丷edis Key" clearable></el-input>
+      </div>
+    </div>
+
+    <div class="filter-actions">
+      <div class="filter-actions-left">
+        <el-button type="primary" icon="el-icon-search" :loading="queue.loading || log.loading" @click="handleSearch">鏌ヨ</el-button>
+        <el-button icon="el-icon-refresh-left" @click="handleReset">閲嶇疆</el-button>
+      </div>
+      <div class="filter-actions-right">
+        <span class="table-note">褰撳墠椤电锛歿{ activeTab === 'queue' ? '閫氱煡闃熷垪' : '鍙戦�佹棩蹇�' }}</span>
+      </div>
+    </div>
+  </section>
+
+  <section class="panel board">
+    <div class="board-head">
+      <div>
+        <div class="title">閫氱煡鏌ョ湅涓庤ˉ鍙�</div>
+        <div class="desc">褰撳墠闃熷垪鏄剧ず Redis 瀹炴椂鏁版嵁锛屽彂閫佹棩蹇楁樉绀哄巻鍙叉帴鍙h皟鐢ㄧ粨鏋溿��</div>
+      </div>
+    </div>
+
+    <el-tabs v-model="activeTab" @tab-click="handleTabChange">
+      <el-tab-pane label="褰撳墠閫氱煡闃熷垪" name="queue">
+        <div class="table-shell">
+          <div class="table-toolbar">
+            <div class="table-toolbar-left">
+              <el-button type="primary" size="mini" icon="el-icon-position" :disabled="queue.selection.length === 0" :loading="resendLoading" @click="batchResendQueue">鎵归噺琛ュ彂</el-button>
+              <span class="table-note">宸查�� {{ queue.selection.length }} 鏉�</span>
+            </div>
+            <div class="table-toolbar-right">
+              <span class="table-note">灞曠ず寰呭彂閫佸拰寰呴噸璇曢�氱煡</span>
+            </div>
+          </div>
+          <el-table
+            :data="queue.records"
+            border
+            stripe
+            height="520"
+            v-loading="queue.loading"
+            @selection-change="queue.selection = $event"
+            :header-cell-style="headerCellStyle">
+            <el-table-column type="selection" width="48"></el-table-column>
+            <el-table-column prop="id" label="閫氱煡ID" min-width="180" show-overflow-tooltip>
+              <template slot-scope="scope">
+                <span class="code-text">{{ scope.row.id }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="notifyType$" label="閫氱煡绫诲瀷" width="120"></el-table-column>
+            <el-table-column prop="device" label="璁惧鍙�" width="88"></el-table-column>
+            <el-table-column prop="taskNo" label="浠诲姟鍙�" min-width="120" show-overflow-tooltip></el-table-column>
+            <el-table-column prop="superTaskNo" label="涓婄骇浠诲姟鍙�" min-width="130" show-overflow-tooltip></el-table-column>
+            <el-table-column prop="msgDesc" label="娑堟伅鎻忚堪" min-width="180" show-overflow-tooltip></el-table-column>
+            <el-table-column prop="retryProgress$" label="閲嶈瘯娆℃暟" width="88"></el-table-column>
+            <el-table-column prop="retryTime" label="闂撮殧(s)" width="84"></el-table-column>
+            <el-table-column prop="lastRetryTime$" label="涓婃閲嶈瘯鏃堕棿" width="168"></el-table-column>
+            <el-table-column label="闃熷垪鐘舵��" width="104">
+              <template slot-scope="scope">
+                <el-tag size="mini" :type="scope.row.queueStatus$ === '寰呭彂閫�' ? 'success' : 'warning'">{{ scope.row.queueStatus$ || '-' }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="鎿嶄綔" width="170" fixed="right">
+              <template slot-scope="scope">
+                <el-button type="text" size="mini" @click="openPayload('閫氱煡鎶ユ枃', scope.row.requestPayload)">鏌ョ湅鎶ユ枃</el-button>
+                <el-button type="text" size="mini" @click="resendQueueRow(scope.row)">琛ュ彂</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+          <div v-if="!queue.loading && queue.records.length === 0" class="empty-hint">褰撳墠娌℃湁寰呭彂閫侀�氱煡</div>
+          <div class="pagination-wrap">
+            <el-pagination
+              background
+              layout="total, sizes, prev, pager, next, jumper"
+              :current-page="queue.page.curr"
+              :page-sizes="[15, 30, 50, 100, 200]"
+              :page-size="queue.page.limit"
+              :total="queue.page.total"
+              @size-change="handleQueueSizeChange"
+              @current-change="handleQueuePageChange">
+            </el-pagination>
+          </div>
+        </div>
+      </el-tab-pane>
+
+      <el-tab-pane label="閫氱煡鍙戦�佹棩蹇�" name="log">
+        <div class="table-shell">
+          <div class="table-toolbar">
+            <div class="table-toolbar-left">
+              <el-button type="primary" size="mini" icon="el-icon-position" :disabled="log.selection.length === 0" :loading="resendLoading" @click="batchResendLog">鎵归噺琛ュ彂</el-button>
+              <span class="table-note">宸查�� {{ log.selection.length }} 鏉�</span>
+            </div>
+            <div class="table-toolbar-right">
+              <span class="table-note">鏀寔浠庡巻鍙叉棩蹇楅噸鏂板彂閫侀�氱煡</span>
+            </div>
+          </div>
+          <el-table
+            :data="log.records"
+            border
+            stripe
+            height="520"
+            v-loading="log.loading"
+            @selection-change="log.selection = $event"
+            :header-cell-style="headerCellStyle">
+            <el-table-column type="selection" width="48"></el-table-column>
+            <el-table-column prop="createTime$" label="鍙戦�佹椂闂�" width="166"></el-table-column>
+            <el-table-column label="缁撴灉" width="84">
+              <template slot-scope="scope">
+                <el-tag v-if="scope.row.result === 1" size="mini" type="success">鎴愬姛</el-tag>
+                <el-tag v-else-if="scope.row.result === 0" size="mini" type="danger">澶辫触</el-tag>
+                <span v-else>-</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="notifyType$" label="閫氱煡绫诲瀷" width="120"></el-table-column>
+            <el-table-column prop="device" label="璁惧鍙�" width="88"></el-table-column>
+            <el-table-column prop="taskNo" label="浠诲姟鍙�" min-width="120" show-overflow-tooltip></el-table-column>
+            <el-table-column prop="superTaskNo" label="涓婄骇浠诲姟鍙�" min-width="130" show-overflow-tooltip></el-table-column>
+            <el-table-column prop="msgDesc" label="娑堟伅鎻忚堪" min-width="180" show-overflow-tooltip></el-table-column>
+            <el-table-column label="鎿嶄綔" width="250" fixed="right">
+              <template slot-scope="scope">
+                <el-button type="text" size="mini" @click="openPayload('閫氱煡鎶ユ枃', scope.row.requestPayload)">鏌ョ湅鎶ユ枃</el-button>
+                <el-button type="text" size="mini" @click="openPayload('鎺ュ彛鍝嶅簲', scope.row.response)">鏌ョ湅鍝嶅簲</el-button>
+                <el-button type="text" size="mini" @click="resendLogRow(scope.row)">琛ュ彂</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+          <div v-if="!log.loading && log.records.length === 0" class="empty-hint">褰撳墠绛涢�夋潯浠朵笅娌℃湁閫氱煡鏃ュ織</div>
+          <div class="pagination-wrap">
+            <el-pagination
+              background
+              layout="total, sizes, prev, pager, next, jumper"
+              :current-page="log.page.curr"
+              :page-sizes="[15, 30, 50, 100, 200]"
+              :page-size="log.page.limit"
+              :total="log.page.total"
+              @size-change="handleLogSizeChange"
+              @current-change="handleLogPageChange">
+            </el-pagination>
+          </div>
+        </div>
+      </el-tab-pane>
+    </el-tabs>
+  </section>
+
+  <el-dialog :title="dialog.title" :visible.sync="dialog.visible" width="72%" :close-on-click-modal="false">
+    <pre class="payload-box">{{ dialog.content || '-' }}</pre>
+  </el-dialog>
+</div>
+
+<script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
+<script type="text/javascript" src="../../static/vue/element/element.js"></script>
+<script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script>
+<script>
+  new Vue({
+    el: '#app',
+    data: function () {
+      return {
+        activeTab: 'queue',
+        summaryLoading: false,
+        resendLoading: false,
+        summary: {},
+        filters: {
+          notifyType: '',
+          device: '',
+          taskNo: '',
+          superTaskNo: '',
+          msgType: '',
+          result: null,
+          condition: ''
+        },
+        queue: {
+          loading: false,
+          records: [],
+          selection: [],
+          page: {
+            curr: 1,
+            limit: 15,
+            total: 0
+          }
+        },
+        log: {
+          loading: false,
+          records: [],
+          selection: [],
+          page: {
+            curr: 1,
+            limit: 15,
+            total: 0
+          }
+        },
+        dialog: {
+          visible: false,
+          title: '',
+          content: ''
+        },
+        headerCellStyle: {
+          background: '#f7f9fc',
+          color: '#2d4058',
+          fontWeight: 600
+        }
+      };
+    },
+    created: function () {
+      this.refreshAll();
+    },
+    methods: {
+      buildFilterParams: function (includeResult) {
+        var params = {};
+        var filters = this.filters;
+        if (filters.notifyType) params.notifyType = filters.notifyType;
+        if (filters.device !== '' && filters.device !== null && filters.device !== undefined) {
+          params.device = filters.device;
+        }
+        if (filters.taskNo) params.taskNo = filters.taskNo;
+        if (filters.superTaskNo) params.superTaskNo = filters.superTaskNo;
+        if (filters.msgType) params.msgType = filters.msgType;
+        if (filters.condition) params.condition = filters.condition;
+        if (includeResult && filters.result !== '' && filters.result !== null && filters.result !== undefined) {
+          params.result = filters.result;
+        }
+        return params;
+      },
+      buildQueryString: function (params) {
+        var search = [];
+        Object.keys(params).forEach(function (key) {
+          if (params[key] === '' || params[key] === null || params[key] === undefined) {
+            return;
+          }
+          search.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key]));
+        });
+        return search.join('&');
+      },
+      fetchJson: function (url, options) {
+        var requestOptions = options || {};
+        requestOptions.headers = requestOptions.headers || {};
+        requestOptions.headers.token = localStorage.getItem('token');
+        if (requestOptions.body && !requestOptions.headers['Content-Type']) {
+          requestOptions.headers['Content-Type'] = 'application/json;charset=UTF-8';
+        }
+        return fetch(url, requestOptions).then(function (response) {
+          return response.json();
+        });
+      },
+      ensureSuccess: function (res, fallback) {
+        if (res && res.code === 403) {
+          top.location.href = baseUrl + '/';
+          return false;
+        }
+        if (!res || res.code !== 200) {
+          this.$message.error((res && res.msg) || fallback || '璇锋眰澶辫触');
+          return false;
+        }
+        return true;
+      },
+      refreshAll: function () {
+        this.loadSummary();
+        this.loadQueue();
+        this.loadLog();
+      },
+      loadSummary: function () {
+        var self = this;
+        self.summaryLoading = true;
+        self.fetchJson(baseUrl + '/notifyReport/summary/auth')
+          .then(function (res) {
+            if (!self.ensureSuccess(res, '鑾峰彇閫氱煡姒傝澶辫触')) {
+              return;
+            }
+            self.summary = res.data || {};
+          })
+          .finally(function () {
+            self.summaryLoading = false;
+          });
+      },
+      loadQueue: function () {
+        var self = this;
+        self.queue.loading = true;
+        var params = self.buildFilterParams(false);
+        params.curr = self.queue.page.curr;
+        params.limit = self.queue.page.limit;
+        self.fetchJson(baseUrl + '/notifyReport/queue/list/auth?' + self.buildQueryString(params))
+          .then(function (res) {
+            if (!self.ensureSuccess(res, '鑾峰彇閫氱煡闃熷垪澶辫触')) {
+              return;
+            }
+            self.queue.records = (res.data && res.data.records) || [];
+            self.queue.page.total = (res.data && res.data.total) || 0;
+            self.queue.selection = [];
+          })
+          .finally(function () {
+            self.queue.loading = false;
+          });
+      },
+      loadLog: function () {
+        var self = this;
+        self.log.loading = true;
+        var params = self.buildFilterParams(true);
+        params.curr = self.log.page.curr;
+        params.limit = self.log.page.limit;
+        self.fetchJson(baseUrl + '/notifyReport/log/list/auth?' + self.buildQueryString(params))
+          .then(function (res) {
+            if (!self.ensureSuccess(res, '鑾峰彇閫氱煡鏃ュ織澶辫触')) {
+              return;
+            }
+            self.log.records = (res.data && res.data.records) || [];
+            self.log.page.total = (res.data && res.data.total) || 0;
+            self.log.selection = [];
+          })
+          .finally(function () {
+            self.log.loading = false;
+          });
+      },
+      handleSearch: function () {
+        this.queue.page.curr = 1;
+        this.log.page.curr = 1;
+        this.refreshAll();
+      },
+      handleReset: function () {
+        this.filters = {
+          notifyType: '',
+          device: '',
+          taskNo: '',
+          superTaskNo: '',
+          msgType: '',
+          result: null,
+          condition: ''
+        };
+        this.queue.page.curr = 1;
+        this.log.page.curr = 1;
+        this.refreshAll();
+      },
+      handleTabChange: function () {
+        if (this.activeTab === 'queue' && this.queue.records.length === 0) {
+          this.loadQueue();
+          return;
+        }
+        if (this.activeTab === 'log' && this.log.records.length === 0) {
+          this.loadLog();
+        }
+      },
+      handleQueuePageChange: function (page) {
+        this.queue.page.curr = page;
+        this.loadQueue();
+      },
+      handleQueueSizeChange: function (size) {
+        this.queue.page.limit = size;
+        this.queue.page.curr = 1;
+        this.loadQueue();
+      },
+      handleLogPageChange: function (page) {
+        this.log.page.curr = page;
+        this.loadLog();
+      },
+      handleLogSizeChange: function (size) {
+        this.log.page.limit = size;
+        this.log.page.curr = 1;
+        this.loadLog();
+      },
+      openPayload: function (title, content) {
+        this.dialog.title = title;
+        this.dialog.content = content || '';
+        this.dialog.visible = true;
+      },
+      resendRequest: function (payload) {
+        var self = this;
+        self.resendLoading = true;
+        return self.fetchJson(baseUrl + '/notifyReport/resend/auth', {
+          method: 'POST',
+          body: JSON.stringify(payload)
+        }).then(function (res) {
+          if (!self.ensureSuccess(res, '琛ュ彂澶辫触')) {
+            return;
+          }
+          var data = res.data || {};
+          var message = '鎴愬姛 ' + (data.successCount || 0) + ' 鏉�';
+          if ((data.failCount || 0) > 0) {
+            message += '锛屽け璐� ' + data.failCount + ' 鏉�';
+          }
+          self.$message({
+            type: (data.failCount || 0) > 0 ? 'warning' : 'success',
+            message: message
+          });
+          self.loadSummary();
+          self.loadQueue();
+          self.loadLog();
+          if ((data.failCount || 0) > 0) {
+            self.openPayload('琛ュ彂缁撴灉', JSON.stringify(data.details || [], null, 2));
+          }
+        }).finally(function () {
+          self.resendLoading = false;
+        });
+      },
+      confirmResend: function (payload, title) {
+        var self = this;
+        self.$confirm(title || '纭畾鎵ц鎵嬪姩琛ュ彂鍚楋紵', '鎻愮ず', {
+          confirmButtonText: '纭畾',
+          cancelButtonText: '鍙栨秷',
+          type: 'warning'
+        }).then(function () {
+          self.resendRequest(payload);
+        }).catch(function () {});
+      },
+      resendQueueRow: function (row) {
+        this.confirmResend({
+          sourceType: 'queue',
+          redisKeys: [row.redisKey]
+        }, '纭畾琛ュ彂璇ラ槦鍒楅�氱煡鍚楋紵');
+      },
+      resendLogRow: function (row) {
+        this.confirmResend({
+          sourceType: 'log',
+          logIds: [row.logId]
+        }, '纭畾鎸夎鏃ュ織閲嶆柊琛ュ彂閫氱煡鍚楋紵');
+      },
+      batchResendQueue: function () {
+        var keys = this.queue.selection.map(function (item) {
+          return item.redisKey;
+        });
+        if (keys.length === 0) {
+          this.$message.warning('璇烽�夋嫨瑕佽ˉ鍙戠殑闃熷垪閫氱煡');
+          return;
+        }
+        this.confirmResend({
+          sourceType: 'queue',
+          redisKeys: keys
+        }, '纭畾鎵归噺琛ュ彂閫変腑鐨勯槦鍒楅�氱煡鍚楋紵');
+      },
+      batchResendLog: function () {
+        var ids = this.log.selection.map(function (item) {
+          return item.logId;
+        });
+        if (ids.length === 0) {
+          this.$message.warning('璇烽�夋嫨瑕佽ˉ鍙戠殑閫氱煡鏃ュ織');
+          return;
+        }
+        this.confirmResend({
+          sourceType: 'log',
+          logIds: ids
+        }, '纭畾鎵归噺琛ュ彂閫変腑鐨勯�氱煡鏃ュ織鍚楋紵');
+      }
+    }
+  });
+</script>
+</body>
+</html>

--
Gitblit v1.9.1