| src/main/java/com/zy/asrs/controller/NotifyReportController.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/asrs/domain/NotifySendResult.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/asrs/domain/param/NotifyResendParam.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/asrs/domain/vo/NotifyReportVo.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/asrs/service/NotifyAsyncService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/asrs/utils/NotifyUtils.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/resources/application.yml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/resources/sql/20260307_add_notify_report_menu.sql | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/webapp/views/notifyReport/notifyReport.html | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/main/java/com/zy/asrs/controller/NotifyReportController.java
New file @@ -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", "日志请求报文无法解析为通知对象"); 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; } } src/main/java/com/zy/asrs/domain/NotifySendResult.java
New file @@ -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; } src/main/java/com/zy/asrs/domain/param/NotifyResendParam.java
New file @@ -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; } src/main/java/com/zy/asrs/domain/vo/NotifyReportVo.java
New file @@ -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); } } 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) { // 通知成功 Integer code = jsonObject == null ? null : jsonObject.getInteger("code"); if (code != null && code == 200) { if (deleteOnSuccess && key != null) { redisUtil.del(key); success = true; } 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; } /** 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; } return key; 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 prefix + device; } private boolean append(String notifyType, Integer device, String taskNo, String superTaskNo, NotifyMsgType msgType, String data) { 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: src/main/resources/sql/20260307_add_notify_report_menu.sql
New file @@ -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'); src/main/webapp/views/notifyReport/notifyReport.html
New file @@ -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">查看当前待通知队列、接口发送日志,支持按任务/设备快速筛选,并对失败或待发送通知执行手动补发。</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">来源:notifyUri + 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="通用搜索:任务号、消息描述、报文关键字、Redis 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 实时数据,发送日志显示历史接口调用结果。</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>