merge: 合并 feature/issue-72 到 feature/dev (阈值管理+信息发布+设备管理)
- 合并冲突解决: 保留 issue-72 的完善版本(支持 MyBatis-Plus、AND/OR 组合条件引擎、逻辑删除) - 覆盖 feature/dev 中的早期简化版 AlertRule 相关代码 - 新增: 阈值管理 CRUD + 信息发布 + 设备管理功能
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
package com.water.production.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.water.common.core.result.R;
|
||||
import com.water.production.entity.AlertNotification;
|
||||
import com.water.production.entity.AlertRecord;
|
||||
import com.water.production.entity.AlertRule;
|
||||
import com.water.production.service.AlertCenterService;
|
||||
import com.water.production.service.AlertRuleService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 报警规则引擎 + 报警管理中心 REST API
|
||||
*/
|
||||
@Tag(name = "报警规则引擎与管理中心")
|
||||
@RestController
|
||||
@RequestMapping("/api/production/alert")
|
||||
@RequiredArgsConstructor
|
||||
public class AlertController {
|
||||
|
||||
private final AlertRuleService ruleService;
|
||||
private final AlertCenterService centerService;
|
||||
|
||||
// ==================== 报警规则管理 (CRUD) ====================
|
||||
|
||||
@Operation(summary = "分页查询报警规则")
|
||||
@GetMapping("/rule/page")
|
||||
public R<Page<AlertRule>> rulePage(
|
||||
@RequestParam(defaultValue = "1") int current,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String alertLevel,
|
||||
@RequestParam(required = false) Boolean enabled) {
|
||||
return R.ok(ruleService.page(current, size, keyword, alertLevel, enabled));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询规则详情")
|
||||
@GetMapping("/rule/{id}")
|
||||
public R<AlertRule> ruleDetail(@PathVariable Long id) {
|
||||
AlertRule rule = ruleService.getById(id);
|
||||
return rule != null ? R.ok(rule) : R.fail(404, "规则不存在");
|
||||
}
|
||||
|
||||
@Operation(summary = "创建报警规则")
|
||||
@PostMapping("/rule")
|
||||
public R<AlertRule> createRule(@RequestBody AlertRule rule) {
|
||||
return R.ok(ruleService.create(rule));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新报警规则")
|
||||
@PutMapping("/rule/{id}")
|
||||
public R<String> updateRule(@PathVariable Long id, @RequestBody AlertRule rule) {
|
||||
rule.setId(id);
|
||||
return ruleService.update(rule) ? R.ok("更新成功") : R.fail("更新失败");
|
||||
}
|
||||
|
||||
@Operation(summary = "删除报警规则")
|
||||
@DeleteMapping("/rule/{id}")
|
||||
public R<String> deleteRule(@PathVariable Long id) {
|
||||
return ruleService.delete(id) ? R.ok("删除成功") : R.fail("删除失败");
|
||||
}
|
||||
|
||||
@Operation(summary = "启用/禁用规则")
|
||||
@PutMapping("/rule/{id}/toggle")
|
||||
public R<String> toggleRule(@PathVariable Long id, @RequestParam boolean enabled) {
|
||||
return ruleService.toggleEnabled(id, enabled) ? R.ok("操作成功") : R.fail("操作失败");
|
||||
}
|
||||
|
||||
@Operation(summary = "查询所有启用规则")
|
||||
@GetMapping("/rule/enabled")
|
||||
public R<List<AlertRule>> enabledRules() {
|
||||
return R.ok(ruleService.findAllEnabled());
|
||||
}
|
||||
|
||||
@Operation(summary = "规则等级统计")
|
||||
@GetMapping("/rule/stats")
|
||||
public R<List<Map<String, Object>>> ruleStats() {
|
||||
return R.ok(ruleService.countByLevel());
|
||||
}
|
||||
|
||||
// ==================== 规则引擎 - 指标评估 ====================
|
||||
|
||||
@Operation(summary = "提交指标数据触发规则评估")
|
||||
@PostMapping("/evaluate")
|
||||
public R<List<AlertRecord>> evaluate(
|
||||
@RequestParam String deviceSn,
|
||||
@RequestParam String metricKey,
|
||||
@RequestParam double value,
|
||||
@RequestParam(required = false) String area) {
|
||||
return R.ok(ruleService.evaluateMetric(deviceSn, metricKey, value, area));
|
||||
}
|
||||
|
||||
// ==================== 报警管理中心 ====================
|
||||
|
||||
@Operation(summary = "分页查询报警记录")
|
||||
@GetMapping("/record/page")
|
||||
public R<Page<AlertRecord>> recordPage(
|
||||
@RequestParam(defaultValue = "1") int current,
|
||||
@RequestParam(defaultValue = "20") int size,
|
||||
@RequestParam(required = false) String level,
|
||||
@RequestParam(required = false) String area,
|
||||
@RequestParam(required = false) Integer status,
|
||||
@RequestParam(required = false) String deviceSn,
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate) {
|
||||
return R.ok(centerService.page(current, size, level, area, status, deviceSn, startDate, endDate));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询报警详情(含通知记录)")
|
||||
@GetMapping("/record/{id}")
|
||||
public R<Map<String, Object>> recordDetail(@PathVariable Long id) {
|
||||
Map<String, Object> detail = centerService.getDetail(id);
|
||||
return detail != null ? R.ok(detail) : R.fail(404, "报警记录不存在");
|
||||
}
|
||||
|
||||
@Operation(summary = "查询活跃报警")
|
||||
@GetMapping("/record/active")
|
||||
public R<List<AlertRecord>> activeAlerts() {
|
||||
return R.ok(centerService.findActive());
|
||||
}
|
||||
|
||||
@Operation(summary = "查询待确认报警")
|
||||
@GetMapping("/record/pending")
|
||||
public R<List<AlertRecord>> pendingAlerts() {
|
||||
return R.ok(centerService.findPendingConfirmation());
|
||||
}
|
||||
|
||||
@Operation(summary = "确认报警")
|
||||
@PostMapping("/record/{id}/confirm")
|
||||
public R<String> confirm(@PathVariable Long id, @RequestParam Long userId) {
|
||||
return centerService.confirm(id, userId) ? R.ok("已确认") : R.fail("确认失败(状态不允许)");
|
||||
}
|
||||
|
||||
@Operation(summary = "派单(指派处理人)")
|
||||
@PostMapping("/record/{id}/dispatch")
|
||||
public R<String> dispatch(@PathVariable Long id,
|
||||
@RequestParam Long assigneeId,
|
||||
@RequestParam(required = false) String assigneeName) {
|
||||
return centerService.dispatch(id, assigneeId, assigneeName != null ? assigneeName : "")
|
||||
? R.ok("已派单") : R.fail("派单失败(状态不允许)");
|
||||
}
|
||||
|
||||
@Operation(summary = "开始处理")
|
||||
@PostMapping("/record/{id}/start-handle")
|
||||
public R<String> startHandle(@PathVariable Long id,
|
||||
@RequestParam Long handlerId,
|
||||
@RequestParam(required = false) String handlerName) {
|
||||
return centerService.startHandle(id, handlerId, handlerName != null ? handlerName : "")
|
||||
? R.ok("已开始处理") : R.fail("操作失败(状态不允许)");
|
||||
}
|
||||
|
||||
@Operation(summary = "完成处理")
|
||||
@PostMapping("/record/{id}/complete")
|
||||
public R<String> completeHandle(@PathVariable Long id, @RequestBody Map<String, String> body) {
|
||||
String result = body.getOrDefault("result", "");
|
||||
return centerService.completeHandle(id, result) ? R.ok("处理完成") : R.fail("操作失败(状态不允许)");
|
||||
}
|
||||
|
||||
@Operation(summary = "归档报警")
|
||||
@PostMapping("/record/{id}/archive")
|
||||
public R<String> archive(@PathVariable Long id, @RequestBody Map<String, String> body) {
|
||||
String reason = body.getOrDefault("reason", "");
|
||||
return centerService.archive(id, reason) ? R.ok("已归档") : R.fail("归档失败(状态不允许)");
|
||||
}
|
||||
|
||||
@Operation(summary = "批量确认报警")
|
||||
@PostMapping("/record/batch-confirm")
|
||||
public R<Map<String, Object>> batchConfirm(@RequestBody Map<String, Object> body) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Number> ids = (List<Number>) body.get("ids");
|
||||
Long userId = ((Number) body.get("userId")).longValue();
|
||||
List<Long> longIds = ids.stream().map(Number::longValue).toList();
|
||||
int count = centerService.batchConfirm(longIds, userId);
|
||||
return R.ok(Map.of("confirmed", count));
|
||||
}
|
||||
|
||||
@Operation(summary = "批量归档报警")
|
||||
@PostMapping("/record/batch-archive")
|
||||
public R<Map<String, Object>> batchArchive(@RequestBody Map<String, Object> body) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Number> ids = (List<Number>) body.get("ids");
|
||||
String reason = (String) body.getOrDefault("reason", "");
|
||||
List<Long> longIds = ids.stream().map(Number::longValue).toList();
|
||||
int count = centerService.batchArchive(longIds, reason);
|
||||
return R.ok(Map.of("archived", count));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询报警通知记录")
|
||||
@GetMapping("/record/{id}/notifications")
|
||||
public R<List<AlertNotification>> notifications(@PathVariable Long id) {
|
||||
return R.ok(centerService.getNotifications(id));
|
||||
}
|
||||
|
||||
// ==================== 统计看板 ====================
|
||||
|
||||
@Operation(summary = "报警统计看板")
|
||||
@GetMapping("/dashboard")
|
||||
public R<Map<String, Object>> dashboard(@RequestParam(defaultValue = "week") String period) {
|
||||
return R.ok(centerService.getDashboard(period));
|
||||
}
|
||||
|
||||
@Operation(summary = "今日报警概要")
|
||||
@GetMapping("/today-summary")
|
||||
public R<Map<String, Object>> todaySummary() {
|
||||
return R.ok(centerService.getTodaySummary());
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
package com.water.production.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.water.common.core.result.R;
|
||||
import com.water.production.entity.DispatchCommand;
|
||||
import com.water.production.entity.DispatchExecution;
|
||||
import com.water.production.entity.DispatchTracking;
|
||||
import com.water.production.service.DispatchCommandService;
|
||||
import com.water.production.service.DispatchTrackingService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "调度指令管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/production/dispatch-command")
|
||||
@RequiredArgsConstructor
|
||||
public class DispatchCommandController {
|
||||
|
||||
private final DispatchCommandService commandService;
|
||||
private final DispatchTrackingService trackingService;
|
||||
|
||||
@Operation(summary = "创建指令")
|
||||
@PostMapping
|
||||
public R<DispatchCommand> create(@RequestBody DispatchCommand command) {
|
||||
return R.ok(commandService.createCommand(command));
|
||||
}
|
||||
|
||||
@Operation(summary = "下发指令")
|
||||
@PostMapping("/{id}/issue")
|
||||
public R<DispatchCommand> issue(@PathVariable Long id,
|
||||
@RequestParam Long issuedBy,
|
||||
@RequestParam(required = false, defaultValue = "system") String operatorName) {
|
||||
return R.ok(commandService.issueCommand(id, issuedBy, operatorName));
|
||||
}
|
||||
|
||||
@Operation(summary = "指令台账(分页)")
|
||||
@GetMapping
|
||||
public R<IPage<Map<String, Object>>> list(
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) String commandType,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate) {
|
||||
return R.ok(commandService.listCommands(page, size, status, commandType, keyword, startDate, endDate));
|
||||
}
|
||||
|
||||
@Operation(summary = "指令详情")
|
||||
@GetMapping("/{id}")
|
||||
public R<Map<String, Object>> detail(@PathVariable Long id) {
|
||||
return R.ok(commandService.getCommandDetail(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "各状态统计")
|
||||
@GetMapping("/stats")
|
||||
public R<List<Map<String, Object>>> stats() {
|
||||
return R.ok(commandService.getStatusStats());
|
||||
}
|
||||
|
||||
@Operation(summary = "接收确认")
|
||||
@PostMapping("/{id}/receive")
|
||||
public R<DispatchExecution> receive(@PathVariable Long id,
|
||||
@RequestParam Long userId,
|
||||
@RequestParam(required = false, defaultValue = "") String userName) {
|
||||
return R.ok(commandService.receiveCommand(id, userId, userName));
|
||||
}
|
||||
|
||||
@Operation(summary = "开始执行")
|
||||
@PostMapping("/{id}/start-execute")
|
||||
public R<DispatchExecution> startExecute(@PathVariable Long id,
|
||||
@RequestParam Long userId,
|
||||
@RequestParam(required = false, defaultValue = "") String userName) {
|
||||
return R.ok(commandService.startExecution(id, userId, userName));
|
||||
}
|
||||
|
||||
@Operation(summary = "完成执行")
|
||||
@PostMapping("/{id}/complete")
|
||||
public R<DispatchExecution> complete(@PathVariable Long id,
|
||||
@RequestParam Long userId,
|
||||
@RequestParam(required = false, defaultValue = "") String userName,
|
||||
@RequestParam(required = false) String feedback,
|
||||
@RequestParam(required = false) String feedbackImages) {
|
||||
return R.ok(commandService.completeExecution(id, userId, userName, feedback, feedbackImages));
|
||||
}
|
||||
|
||||
@Operation(summary = "驳回")
|
||||
@PostMapping("/{id}/reject")
|
||||
public R<DispatchExecution> reject(@PathVariable Long id,
|
||||
@RequestParam Long userId,
|
||||
@RequestParam(required = false, defaultValue = "") String userName,
|
||||
@RequestParam String reason) {
|
||||
return R.ok(commandService.rejectExecution(id, userId, userName, reason));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询追踪日志")
|
||||
@GetMapping("/{id}/tracking")
|
||||
public R<List<DispatchTracking>> trackingLogs(@PathVariable Long id) {
|
||||
return R.ok(trackingService.getTrackingLogs(id));
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package com.water.production.controller;
|
||||
|
||||
import com.water.common.core.result.R;
|
||||
import com.water.production.service.*;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Tag(name = "供水生产管理")
|
||||
@RestController
|
||||
@RequestMapping("/production")
|
||||
@RequiredArgsConstructor
|
||||
public class ProductionController {
|
||||
|
||||
private final DashboardService dashboardService;
|
||||
private final WaterQualityService wqService;
|
||||
private final AlertEngine alertEngine;
|
||||
private final DispatchService dispatchService;
|
||||
private final DataCenterService dataCenterService;
|
||||
private final VideoService videoService;
|
||||
|
||||
// ---- 总览 ----
|
||||
@GetMapping("/overview")
|
||||
public R<Map<String, Object>> overview(@RequestParam(defaultValue = "一体化水厂") String area,
|
||||
@RequestParam(defaultValue = "admin") String roleType) {
|
||||
return R.ok(dashboardService.getOverview(area, roleType));
|
||||
}
|
||||
|
||||
// ---- 实时监测 ----
|
||||
@GetMapping("/monitor/realtime")
|
||||
public R<List<Map<String, Object>>> realtime(@RequestParam(required = false) String area,
|
||||
@RequestParam(required = false) String positionType,
|
||||
@RequestParam(required = false) String deviceType) {
|
||||
return R.ok(dashboardService.getRealtimeMonitoring(area, positionType, deviceType));
|
||||
}
|
||||
|
||||
@GetMapping("/monitor/cameras")
|
||||
public R<List<Map<String, Object>>> cameras(@RequestParam String area) {
|
||||
return R.ok(videoService.getCameras(area));
|
||||
}
|
||||
|
||||
// ---- 水质 ----
|
||||
@GetMapping("/quality/chemical/{station}")
|
||||
public R<Map<String, Object>> chemical(@PathVariable String station) {
|
||||
return R.ok(wqService.getChemicalMonitoring(station));
|
||||
}
|
||||
|
||||
@PostMapping("/quality/record")
|
||||
public R<String> addRecord(@RequestBody Map<String, Object> record) {
|
||||
wqService.addRecord(record);
|
||||
return R.ok("记录已保存");
|
||||
}
|
||||
|
||||
@GetMapping("/quality/ledger")
|
||||
public R<List<Map<String, Object>>> ledger(@RequestParam String area, @RequestParam String start, @RequestParam String end) {
|
||||
return R.ok(wqService.getQualityLedger(area, start, end));
|
||||
}
|
||||
|
||||
// ---- 报警 ----
|
||||
@GetMapping("/alert/list")
|
||||
public R<List<Map<String, Object>>> alerts(@RequestParam(required = false) String level,
|
||||
@RequestParam(required = false) String area,
|
||||
@RequestParam(defaultValue = "true") boolean active) {
|
||||
return R.ok(alertEngine.getAlerts(level, area, active));
|
||||
}
|
||||
|
||||
@PostMapping("/alert/{id}/confirm")
|
||||
public R<String> confirm(@PathVariable Long id, @RequestParam Long userId) {
|
||||
alertEngine.confirm(id, userId); return R.ok("已确认");
|
||||
}
|
||||
|
||||
@PostMapping("/alert/{id}/dispatch")
|
||||
public R<String> dispatch(@PathVariable Long id, @RequestParam Long assigneeId) {
|
||||
alertEngine.dispatch(id, assigneeId); return R.ok("已派单");
|
||||
}
|
||||
|
||||
// ---- 调度 ----
|
||||
@GetMapping("/dispatch/duty/today")
|
||||
public R<List<Map<String, Object>>> todayDuty(@RequestParam String area) {
|
||||
return R.ok(dispatchService.getTodayDuty(area));
|
||||
}
|
||||
|
||||
@PostMapping("/dispatch/command")
|
||||
public R<Map<String, Object>> createCommand(@RequestBody Map<String, Object> req) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Long> targetIds = (List<Long>) req.getOrDefault("targetIds", List.of());
|
||||
return R.ok(dispatchService.createCommand(
|
||||
(String) req.get("title"), (String) req.get("content"), (String) req.get("type"),
|
||||
(String) req.get("source"), (String) req.get("targetType"), targetIds));
|
||||
}
|
||||
|
||||
@PostMapping("/dispatch/command/{cmdNo}/issue")
|
||||
public R<Map<String, Object>> issueCommand(@PathVariable String cmdNo) {
|
||||
return R.ok(dispatchService.issueCommand(cmdNo));
|
||||
}
|
||||
|
||||
@PostMapping("/dispatch/emergency/pipe-burst")
|
||||
public R<Map<String, Object>> pipeBurst(@RequestBody Map<String, Object> req) {
|
||||
return R.ok(dispatchService.pipeBurstSimulation(
|
||||
((Number) req.get("lng")).doubleValue(),
|
||||
((Number) req.get("lat")).doubleValue(),
|
||||
(String) req.get("pipeDiameter")));
|
||||
}
|
||||
|
||||
// ---- 数据中心 ----
|
||||
@GetMapping("/data/history")
|
||||
public R<List<Map<String, Object>>> history(@RequestParam String dataType, @RequestParam String area,
|
||||
@RequestParam String start, @RequestParam String end) {
|
||||
return R.ok(dataCenterService.getHistoryData(dataType, area, start, end));
|
||||
}
|
||||
|
||||
@GetMapping("/data/report")
|
||||
public R<Map<String, Object>> report(@RequestParam String type, @RequestParam String period) {
|
||||
return R.ok(dataCenterService.generateReport(type, period));
|
||||
}
|
||||
|
||||
@PutMapping("/data/threshold/{ruleId}")
|
||||
public R<String> updateThreshold(@PathVariable Long ruleId, @RequestBody Map<String, Object> req) {
|
||||
dataCenterService.updateThreshold(ruleId,
|
||||
((Number) req.get("threshold")).doubleValue(),
|
||||
(String) req.get("condition"));
|
||||
return R.ok("阈值已更新");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.water.production.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 报警通知记录实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("prod_alert_notification")
|
||||
public class AlertNotification {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 关联的报警记录ID */
|
||||
private Long alertRecordId;
|
||||
|
||||
/** 关联的规则ID */
|
||||
private Long ruleId;
|
||||
|
||||
/** 通知渠道: sms/wechat/app/email */
|
||||
private String channel;
|
||||
|
||||
/** 接收人标识 */
|
||||
private String recipient;
|
||||
|
||||
/** 接收人姓名 */
|
||||
private String recipientName;
|
||||
|
||||
/** 通知标题 */
|
||||
private String title;
|
||||
|
||||
/** 通知内容 */
|
||||
private String content;
|
||||
|
||||
/** 状态: 0=待发送 1=已发送 2=发送失败 3=已读 */
|
||||
private Integer status;
|
||||
|
||||
/** 发送时间 */
|
||||
private LocalDateTime sendTime;
|
||||
|
||||
/** 阅读时间 */
|
||||
private LocalDateTime readTime;
|
||||
|
||||
/** 重试次数 */
|
||||
private Integer retryCount;
|
||||
|
||||
/** 错误信息 */
|
||||
private String errorMsg;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdTime;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.water.production.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 报警记录实体(全生命周期)
|
||||
* 状态流转: 0=活跃 → 1=已确认 → 2=已派单 → 3=处理中 → 4=已处理 → 5=已归档
|
||||
*/
|
||||
@Data
|
||||
@TableName("prod_alert_record")
|
||||
public class AlertRecord {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 关联规则ID */
|
||||
private Long ruleId;
|
||||
|
||||
/** 规则名称 */
|
||||
private String ruleName;
|
||||
|
||||
/** 设备ID */
|
||||
private Long deviceId;
|
||||
|
||||
/** 设备编号 */
|
||||
private String deviceSn;
|
||||
|
||||
/** 设备名称 */
|
||||
private String deviceName;
|
||||
|
||||
/** 所属区域 */
|
||||
private String area;
|
||||
|
||||
/** 指标键 */
|
||||
private String metricKey;
|
||||
|
||||
/** 指标值 */
|
||||
private BigDecimal metricValue;
|
||||
|
||||
/** 阈值(字符串,用于展示) */
|
||||
private String thresholdValue;
|
||||
|
||||
/** 报警等级: general/important/urgent */
|
||||
private String alertLevel;
|
||||
|
||||
/** 报警标题 */
|
||||
private String title;
|
||||
|
||||
/** 报警详情 */
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 生命周期状态:
|
||||
* 0=活跃, 1=已确认, 2=已派单, 3=处理中, 4=已处理, 5=已归档
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/** 确认人ID */
|
||||
private Long confirmedBy;
|
||||
|
||||
/** 确认时间 */
|
||||
private LocalDateTime confirmedTime;
|
||||
|
||||
/** 派单时间 */
|
||||
private LocalDateTime dispatchTime;
|
||||
|
||||
/** 指派人ID */
|
||||
private Long assigneeId;
|
||||
|
||||
/** 指派人姓名 */
|
||||
private String assigneeName;
|
||||
|
||||
/** 处理人ID */
|
||||
private Long handlerId;
|
||||
|
||||
/** 处理人姓名 */
|
||||
private String handlerName;
|
||||
|
||||
/** 处理结果 */
|
||||
private String handleResult;
|
||||
|
||||
/** 处理时间 */
|
||||
private LocalDateTime handleTime;
|
||||
|
||||
/** 归档时间 */
|
||||
private LocalDateTime archiveTime;
|
||||
|
||||
/** 归档原因 */
|
||||
private String archiveReason;
|
||||
|
||||
/** 解决时间 */
|
||||
private LocalDateTime resolvedAt;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedTime;
|
||||
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
}
|
||||
@@ -1,25 +1,80 @@
|
||||
package com.water.production.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
|
||||
/**
|
||||
* 报警规则定义实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("prod_alert_rule")
|
||||
public class AlertRule {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String name;
|
||||
private String parameter;
|
||||
private Double minValue;
|
||||
private Double maxValue;
|
||||
private Double warningValue;
|
||||
private Double criticalValue;
|
||||
private String conditionType;
|
||||
private Integer severityLevel;
|
||||
private Integer duplicateInterval;
|
||||
private Integer status;
|
||||
private String targetEquipmentId;
|
||||
private String targetArea;
|
||||
private LocalDateTime createTime;
|
||||
private LocalDateTime updateTime;
|
||||
private String createBy;
|
||||
private String updateBy;
|
||||
}
|
||||
|
||||
/** 规则名称 */
|
||||
private String ruleName;
|
||||
|
||||
/** 规则编码 */
|
||||
private String ruleCode;
|
||||
|
||||
/** 规则描述 */
|
||||
private String description;
|
||||
|
||||
/** 设备类型 */
|
||||
private String deviceType;
|
||||
|
||||
/** 指标键 */
|
||||
private String metricKey;
|
||||
|
||||
/** 报警等级: general/important/urgent */
|
||||
private String alertLevel;
|
||||
|
||||
/** 条件表达式JSON (支持AND/OR组合) */
|
||||
private String conditionExpr;
|
||||
|
||||
/** 简单阈值(向后兼容) */
|
||||
private BigDecimal thresholdValue;
|
||||
|
||||
/** 去重窗口(秒) */
|
||||
private Integer debounceSec;
|
||||
|
||||
/** 通知渠道(sms,wechat,app,email) */
|
||||
private String notifyChannels;
|
||||
|
||||
/** 通知模板 */
|
||||
private String notifyTemplate;
|
||||
|
||||
/** 是否启用 */
|
||||
private Integer enabled;
|
||||
|
||||
/** 规则优先级 */
|
||||
private Integer priority;
|
||||
|
||||
/** 生效开始时间 */
|
||||
private LocalTime effectiveStart;
|
||||
|
||||
/** 生效结束时间 */
|
||||
private LocalTime effectiveEnd;
|
||||
|
||||
/** 创建人 */
|
||||
private Long createdBy;
|
||||
|
||||
/** 更新人 */
|
||||
private Long updatedBy;
|
||||
|
||||
/** 逻辑删除 */
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedTime;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.water.production.entity;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data; import java.time.LocalDateTime;
|
||||
@Data @TableName("prod_device_status")
|
||||
public class DeviceStatus {
|
||||
@TableId(type = IdType.AUTO) private Long id;
|
||||
private String deviceId, deviceName, deviceType;
|
||||
private String area; private Integer status; // 0离线 1在线 2故障
|
||||
private Double lng, lat;
|
||||
private LocalDateTime lastOnlineTime;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.water.production.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 调度指令主表
|
||||
*/
|
||||
@Data
|
||||
@TableName("prod_dispatch_command")
|
||||
public class DispatchCommand {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 指令编号 CMD-yyyyMMddHHmmss-xxxx */
|
||||
private String commandNo;
|
||||
|
||||
/** 指令标题 */
|
||||
private String commandTitle;
|
||||
|
||||
/** 指令内容 */
|
||||
private String commandContent;
|
||||
|
||||
/** 类型: normal/emergency/maintenance/inspection */
|
||||
private String commandType;
|
||||
|
||||
/** 来源 */
|
||||
private String source;
|
||||
|
||||
/** 优先级: low/normal/high/urgent */
|
||||
private String priority;
|
||||
|
||||
/** 目标类型: user/dept/role */
|
||||
private String targetType;
|
||||
|
||||
/** 目标ID列表 JSON数组 */
|
||||
private String targetIds;
|
||||
|
||||
/** 状态: draft/issued/received/executing/completed/rejected */
|
||||
private String status;
|
||||
|
||||
/** 下发时间 */
|
||||
private LocalDateTime issuedAt;
|
||||
|
||||
/** 下发人 */
|
||||
private Long issuedBy;
|
||||
|
||||
/** 完成归档时间 */
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.water.production.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 调度指令执行记录表
|
||||
*/
|
||||
@Data
|
||||
@TableName("prod_dispatch_execution")
|
||||
public class DispatchExecution {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 关联指令ID */
|
||||
private Long commandId;
|
||||
|
||||
/** 接收/执行人 */
|
||||
private Long userId;
|
||||
|
||||
/** 执行人姓名 */
|
||||
private String userName;
|
||||
|
||||
/** 接收确认时间 */
|
||||
private LocalDateTime receivedAt;
|
||||
|
||||
/** 执行状态: pending/received/executing/completed/rejected */
|
||||
private String executeStatus;
|
||||
|
||||
/** 执行反馈 */
|
||||
private String feedback;
|
||||
|
||||
/** 反馈图片JSON数组 */
|
||||
private String feedbackImages;
|
||||
|
||||
/** 完成时间 */
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
/** 驳回原因 */
|
||||
private String rejectedReason;
|
||||
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.water.production.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 调度指令过程追踪日志表
|
||||
*/
|
||||
@Data
|
||||
@TableName("prod_dispatch_tracking")
|
||||
public class DispatchTracking {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 关联指令ID */
|
||||
private Long commandId;
|
||||
|
||||
/** 关联执行记录ID(可选) */
|
||||
private Long executionId;
|
||||
|
||||
/** 操作类型: create/issue/receive/start_execute/complete/reject/cancel */
|
||||
private String action;
|
||||
|
||||
/** 操作人 */
|
||||
private Long operatorId;
|
||||
|
||||
/** 操作人姓名 */
|
||||
private String operatorName;
|
||||
|
||||
/** 原状态 */
|
||||
private String fromStatus;
|
||||
|
||||
/** 新状态 */
|
||||
private String toStatus;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.water.production.entity;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data; import java.time.LocalDateTime;
|
||||
@Data @TableName("prod_flow_pressure")
|
||||
public class FlowPressureMonitor {
|
||||
@TableId(type = IdType.AUTO) private Long id;
|
||||
private String deviceId, area, positionType; // 流量/压力/液位
|
||||
private Double value; private String unit;
|
||||
private Double lng, lat;
|
||||
@TableField(fill=FieldFill.INSERT) private LocalDateTime createdTime;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.water.production.entity;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
@Data @TableName("prod_video_camera")
|
||||
public class VideoCamera {
|
||||
@TableId(type = IdType.AUTO) private Long id;
|
||||
private String cameraId, name, area;
|
||||
private String streamUrl; private Integer status; // 0离线 1在线
|
||||
private Double lng, lat;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.water.production.entity;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data; import java.time.LocalDateTime;
|
||||
@Data @TableName("prod_water_quality")
|
||||
public class WaterQualityRecord {
|
||||
@TableId(type = IdType.AUTO) private Long id;
|
||||
private String station, positionType; // 原水/出厂水/末梢水
|
||||
private Double turbidity, ph, residualChlorine, color, odor;
|
||||
private String result; // 合格/不合格
|
||||
@TableField(fill=FieldFill.INSERT) private LocalDateTime createdTime;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.water.production.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.water.production.entity.AlertNotification;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface AlertNotificationMapper extends BaseMapper<AlertNotification> {
|
||||
|
||||
/**
|
||||
* 按报警记录ID查询通知记录
|
||||
*/
|
||||
@Select("SELECT * FROM prod_alert_notification WHERE alert_record_id = #{recordId} ORDER BY created_time")
|
||||
List<AlertNotification> findByAlertRecordId(@Param("recordId") Long recordId);
|
||||
|
||||
/**
|
||||
* 统计通知发送情况
|
||||
*/
|
||||
@Select("SELECT channel, status, COUNT(*) as count FROM prod_alert_notification " +
|
||||
"WHERE created_time >= #{startTime} GROUP BY channel, status")
|
||||
List<Map<String, Object>> statisticsByChannel(@Param("startTime") String startTime);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.water.production.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.water.production.entity.AlertRecord;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface AlertRecordMapper extends BaseMapper<AlertRecord> {
|
||||
|
||||
/**
|
||||
* 按状态统计报警数量
|
||||
*/
|
||||
@Select("SELECT status, COUNT(*) as count FROM prod_alert_record WHERE deleted = 0 GROUP BY status")
|
||||
List<Map<String, Object>> countByStatus();
|
||||
|
||||
/**
|
||||
* 按等级统计报警数量
|
||||
*/
|
||||
@Select("SELECT alert_level, COUNT(*) as count FROM prod_alert_record WHERE deleted = 0 " +
|
||||
"AND created_time >= #{startTime} GROUP BY alert_level")
|
||||
List<Map<String, Object>> countByLevel(@Param("startTime") String startTime);
|
||||
|
||||
/**
|
||||
* 按区域统计报警数量
|
||||
*/
|
||||
@Select("SELECT area, COUNT(*) as count FROM prod_alert_record WHERE deleted = 0 " +
|
||||
"AND created_time >= #{startTime} GROUP BY area ORDER BY count DESC")
|
||||
List<Map<String, Object>> countByArea(@Param("startTime") String startTime);
|
||||
|
||||
/**
|
||||
* 按日期统计报警趋势(最近N天)
|
||||
*/
|
||||
@Select("SELECT DATE(created_time) as date, alert_level, COUNT(*) as count " +
|
||||
"FROM prod_alert_record WHERE deleted = 0 AND created_time >= #{startTime} " +
|
||||
"GROUP BY DATE(created_time), alert_level ORDER BY date")
|
||||
List<Map<String, Object>> trendByDate(@Param("startTime") String startTime);
|
||||
|
||||
/**
|
||||
* 统计今日各状态报警数量
|
||||
*/
|
||||
@Select("SELECT status, alert_level, COUNT(*) as count FROM prod_alert_record " +
|
||||
"WHERE deleted = 0 AND created_time >= CURRENT_DATE GROUP BY status, alert_level")
|
||||
List<Map<String, Object>> todayStatistics();
|
||||
|
||||
/**
|
||||
* 查询活跃报警(未归档)
|
||||
*/
|
||||
@Select("SELECT * FROM prod_alert_record WHERE deleted = 0 AND status < 5 ORDER BY created_time DESC")
|
||||
List<AlertRecord> findActive();
|
||||
|
||||
/**
|
||||
* 查询需要确认的报警
|
||||
*/
|
||||
@Select("SELECT * FROM prod_alert_record WHERE deleted = 0 AND status = 0 ORDER BY " +
|
||||
"CASE alert_level WHEN 'urgent' THEN 1 WHEN 'important' THEN 2 ELSE 3 END, created_time DESC")
|
||||
List<AlertRecord> findPendingConfirmation();
|
||||
|
||||
/**
|
||||
* 确认报警
|
||||
*/
|
||||
@Update("UPDATE prod_alert_record SET status = 1, confirmed_by = #{userId}, " +
|
||||
"confirmed_time = NOW(), updated_time = NOW() WHERE id = #{id} AND status = 0")
|
||||
int confirm(@Param("id") Long id, @Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 派单
|
||||
*/
|
||||
@Update("UPDATE prod_alert_record SET status = 2, assignee_id = #{assigneeId}, " +
|
||||
"assignee_name = #{assigneeName}, dispatch_time = NOW(), updated_time = NOW() " +
|
||||
"WHERE id = #{id} AND status IN (0, 1)")
|
||||
int dispatch(@Param("id") Long id, @Param("assigneeId") Long assigneeId, @Param("assigneeName") String assigneeName);
|
||||
|
||||
/**
|
||||
* 开始处理
|
||||
*/
|
||||
@Update("UPDATE prod_alert_record SET status = 3, handler_id = #{handlerId}, " +
|
||||
"handler_name = #{handlerName}, updated_time = NOW() WHERE id = #{id} AND status = 2")
|
||||
int startHandle(@Param("id") Long id, @Param("handlerId") Long handlerId, @Param("handlerName") String handlerName);
|
||||
|
||||
/**
|
||||
* 完成处理
|
||||
*/
|
||||
@Update("UPDATE prod_alert_record SET status = 4, handle_result = #{result}, " +
|
||||
"handle_time = NOW(), resolved_at = NOW(), updated_time = NOW() WHERE id = #{id} AND status = 3")
|
||||
int completeHandle(@Param("id") Long id, @Param("result") String result);
|
||||
|
||||
/**
|
||||
* 归档
|
||||
*/
|
||||
@Update("UPDATE prod_alert_record SET status = 5, archive_time = NOW(), " +
|
||||
"archive_reason = #{reason}, updated_time = NOW() WHERE id = #{id} AND status = 4")
|
||||
int archive(@Param("id") Long id, @Param("reason") String reason);
|
||||
}
|
||||
@@ -1,39 +1,32 @@
|
||||
package com.water.production.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.water.production.entity.AlertRule;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface AlertRuleMapper {
|
||||
// 查询所有报警规则
|
||||
List<AlertRule> findAll();
|
||||
|
||||
// 根据ID查询报警规则
|
||||
AlertRule findById(@Param("id") Long id);
|
||||
|
||||
// 根据参数查询报警规则
|
||||
List<AlertRule> findByParameter(@Param("parameter") String parameter);
|
||||
|
||||
// 根据设备ID查询相关报警规则
|
||||
List<AlertRule> findByTargetEquipmentId(@Param("targetEquipmentId") String targetEquipmentId);
|
||||
|
||||
// 根据区域查询报警规则
|
||||
List<AlertRule> findByTargetArea(@Param("targetArea") String targetArea);
|
||||
|
||||
// 根据状态查询报警规则
|
||||
List<AlertRule> findByStatus(@Param("status") Integer status);
|
||||
|
||||
// 插入报警规则
|
||||
int insert(AlertRule alertRule);
|
||||
|
||||
// 更新报警规则
|
||||
int update(AlertRule alertRule);
|
||||
|
||||
// 删除报警规则
|
||||
int deleteById(@Param("id") Long id);
|
||||
|
||||
// 启用/禁用报警规则
|
||||
int updateStatus(@Param("id") Long id, @Param("status") Integer status);
|
||||
}
|
||||
public interface AlertRuleMapper extends BaseMapper<AlertRule> {
|
||||
|
||||
/**
|
||||
* 根据指标键查询启用的规则
|
||||
*/
|
||||
@Select("SELECT * FROM prod_alert_rule WHERE metric_key = #{metricKey} AND enabled = 1 AND deleted = 0")
|
||||
List<AlertRule> findEnabledByMetricKey(@Param("metricKey") String metricKey);
|
||||
|
||||
/**
|
||||
* 查询所有启用的规则
|
||||
*/
|
||||
@Select("SELECT * FROM prod_alert_rule WHERE enabled = 1 AND deleted = 0 ORDER BY priority DESC")
|
||||
List<AlertRule> findAllEnabled();
|
||||
|
||||
/**
|
||||
* 统计各等级规则数量
|
||||
*/
|
||||
@Select("SELECT alert_level, COUNT(*) as count FROM prod_alert_rule WHERE enabled = 1 AND deleted = 0 GROUP BY alert_level")
|
||||
List<Map<String, Object>> countByLevel();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.water.production.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.water.production.entity.DeviceStatus;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
@Mapper public interface DeviceStatusMapper extends BaseMapper<DeviceStatus> {}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.water.production.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.water.production.entity.DispatchCommand;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface DispatchCommandMapper extends BaseMapper<DispatchCommand> {
|
||||
|
||||
IPage<Map<String, Object>> selectCommandPage(
|
||||
Page<?> page,
|
||||
@Param("status") String status,
|
||||
@Param("commandType") String commandType,
|
||||
@Param("keyword") String keyword,
|
||||
@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate
|
||||
);
|
||||
|
||||
Map<String, Object> selectCommandDetail(@Param("commandId") Long commandId);
|
||||
|
||||
List<Map<String, Object>> selectStatusStats();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.water.production.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.water.production.entity.DispatchExecution;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface DispatchExecutionMapper extends BaseMapper<DispatchExecution> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.water.production.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.water.production.entity.DispatchTracking;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface DispatchTrackingMapper extends BaseMapper<DispatchTracking> {
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.water.production.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.water.production.entity.FlowPressureMonitor;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
@Mapper public interface FlowPressureMonitorMapper extends BaseMapper<FlowPressureMonitor> {}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.water.production.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.water.production.entity.VideoCamera;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
@Mapper public interface VideoCameraMapper extends BaseMapper<VideoCamera> {}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.water.production.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.water.production.entity.WaterQualityRecord;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
@Mapper public interface WaterQualityRecordMapper extends BaseMapper<WaterQualityRecord> {}
|
||||
@@ -0,0 +1,309 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.water.production.entity.AlertNotification;
|
||||
import com.water.production.entity.AlertRecord;
|
||||
import com.water.production.mapper.AlertNotificationMapper;
|
||||
import com.water.production.mapper.AlertRecordMapper;
|
||||
import com.water.production.mapper.AlertRuleMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 报警管理中心服务 - 报警全生命周期管理 + 统计看板
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AlertCenterService {
|
||||
|
||||
private final AlertRecordMapper recordMapper;
|
||||
private final AlertNotificationMapper notificationMapper;
|
||||
private final AlertRuleMapper ruleMapper;
|
||||
|
||||
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
// ==================== 报警列表与详情 ====================
|
||||
|
||||
/**
|
||||
* 分页查询报警列表 (支持多条件筛选)
|
||||
*/
|
||||
public Page<AlertRecord> page(int current, int size, String level, String area,
|
||||
Integer status, String deviceSn,
|
||||
String startDate, String endDate) {
|
||||
LambdaQueryWrapper<AlertRecord> wrapper = new LambdaQueryWrapper<>();
|
||||
if (level != null && !level.isEmpty()) wrapper.eq(AlertRecord::getAlertLevel, level);
|
||||
if (area != null && !area.isEmpty()) wrapper.eq(AlertRecord::getArea, area);
|
||||
if (status != null) wrapper.eq(AlertRecord::getStatus, status);
|
||||
if (deviceSn != null && !deviceSn.isEmpty()) wrapper.like(AlertRecord::getDeviceSn, deviceSn);
|
||||
if (startDate != null && !startDate.isEmpty()) {
|
||||
wrapper.ge(AlertRecord::getCreatedTime, LocalDate.parse(startDate, DATE_FMT).atStartOfDay());
|
||||
}
|
||||
if (endDate != null && !endDate.isEmpty()) {
|
||||
wrapper.le(AlertRecord::getCreatedTime, LocalDate.parse(endDate, DATE_FMT).plusDays(1).atStartOfDay());
|
||||
}
|
||||
wrapper.orderByDesc(AlertRecord::getCreatedTime);
|
||||
return recordMapper.selectPage(new Page<>(current, size), wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询报警详情 (含通知记录)
|
||||
*/
|
||||
public Map<String, Object> getDetail(Long id) {
|
||||
AlertRecord record = recordMapper.selectById(id);
|
||||
if (record == null) return null;
|
||||
|
||||
Map<String, Object> detail = new LinkedHashMap<>();
|
||||
detail.put("record", record);
|
||||
detail.put("notifications", notificationMapper.findByAlertRecordId(id));
|
||||
detail.put("statusLabel", statusLabel(record.getStatus()));
|
||||
detail.put("levelLabel", levelLabel(record.getAlertLevel()));
|
||||
return detail;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询活跃报警
|
||||
*/
|
||||
public List<AlertRecord> findActive() {
|
||||
return recordMapper.findActive();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询待确认报警
|
||||
*/
|
||||
public List<AlertRecord> findPendingConfirmation() {
|
||||
return recordMapper.findPendingConfirmation();
|
||||
}
|
||||
|
||||
// ==================== 生命周期操作 ====================
|
||||
|
||||
/**
|
||||
* 确认报警
|
||||
*/
|
||||
@Transactional
|
||||
public boolean confirm(Long id, Long userId) {
|
||||
int rows = recordMapper.confirm(id, userId);
|
||||
if (rows > 0) {
|
||||
log.info("Alert confirmed: id={}, userId={}", id, userId);
|
||||
}
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 派单(指派给处理人)
|
||||
*/
|
||||
@Transactional
|
||||
public boolean dispatch(Long id, Long assigneeId, String assigneeName) {
|
||||
int rows = recordMapper.dispatch(id, assigneeId, assigneeName);
|
||||
if (rows > 0) {
|
||||
log.info("Alert dispatched: id={}, assignee={}", id, assigneeName);
|
||||
// 可选: 创建巡检任务
|
||||
createPatrolTask(id, assigneeId);
|
||||
}
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始处理
|
||||
*/
|
||||
@Transactional
|
||||
public boolean startHandle(Long id, Long handlerId, String handlerName) {
|
||||
int rows = recordMapper.startHandle(id, handlerId, handlerName);
|
||||
if (rows > 0) {
|
||||
log.info("Alert handling started: id={}, handler={}", id, handlerName);
|
||||
}
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成处理
|
||||
*/
|
||||
@Transactional
|
||||
public boolean completeHandle(Long id, String result) {
|
||||
int rows = recordMapper.completeHandle(id, result);
|
||||
if (rows > 0) {
|
||||
log.info("Alert handled: id={}", id);
|
||||
}
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 归档
|
||||
*/
|
||||
@Transactional
|
||||
public boolean archive(Long id, String reason) {
|
||||
int rows = recordMapper.archive(id, reason);
|
||||
if (rows > 0) {
|
||||
log.info("Alert archived: id={}", id);
|
||||
}
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量确认
|
||||
*/
|
||||
@Transactional
|
||||
public int batchConfirm(List<Long> ids, Long userId) {
|
||||
int count = 0;
|
||||
for (Long id : ids) {
|
||||
count += recordMapper.confirm(id, userId);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量归档
|
||||
*/
|
||||
@Transactional
|
||||
public int batchArchive(List<Long> ids, String reason) {
|
||||
int count = 0;
|
||||
for (Long id : ids) {
|
||||
count += recordMapper.archive(id, reason);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// ==================== 统计看板 ====================
|
||||
|
||||
/**
|
||||
* 获取统计看板数据
|
||||
*/
|
||||
public Map<String, Object> getDashboard(String period) {
|
||||
Map<String, Object> dashboard = new LinkedHashMap<>();
|
||||
|
||||
// 计算起始时间
|
||||
String startTime = calculateStartTime(period);
|
||||
|
||||
// 总览数据
|
||||
dashboard.put("totalByStatus", recordMapper.countByStatus());
|
||||
dashboard.put("totalByLevel", recordMapper.countByLevel(startTime));
|
||||
dashboard.put("totalByArea", recordMapper.countByArea(startTime));
|
||||
dashboard.put("trend", recordMapper.trendByDate(startTime));
|
||||
dashboard.put("today", recordMapper.todayStatistics());
|
||||
|
||||
// 规则统计
|
||||
dashboard.put("ruleStats", ruleMapper.countByLevel());
|
||||
|
||||
// 通知统计
|
||||
dashboard.put("notificationStats", notificationMapper.statisticsByChannel(startTime));
|
||||
|
||||
// 汇总数值
|
||||
dashboard.put("summary", buildSummary(startTime));
|
||||
|
||||
dashboard.put("period", period);
|
||||
dashboard.put("generatedAt", LocalDateTime.now());
|
||||
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取今日报警概要
|
||||
*/
|
||||
public Map<String, Object> getTodaySummary() {
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
List<Map<String, Object>> todayStats = recordMapper.todayStatistics();
|
||||
|
||||
long totalToday = 0;
|
||||
long activeCount = 0;
|
||||
long confirmedCount = 0;
|
||||
long handledCount = 0;
|
||||
|
||||
for (Map<String, Object> row : todayStats) {
|
||||
long count = ((Number) row.get("count")).longValue();
|
||||
int status = ((Number) row.get("status")).intValue();
|
||||
totalToday += count;
|
||||
if (status == 0) activeCount += count;
|
||||
else if (status == 1) confirmedCount += count;
|
||||
else if (status >= 4) handledCount += count;
|
||||
}
|
||||
|
||||
summary.put("totalToday", totalToday);
|
||||
summary.put("active", activeCount);
|
||||
summary.put("confirmed", confirmedCount);
|
||||
summary.put("handled", handledCount);
|
||||
summary.put("pending", totalToday - handledCount);
|
||||
summary.put("detail", todayStats);
|
||||
return summary;
|
||||
}
|
||||
|
||||
// ==================== 通知记录查询 ====================
|
||||
|
||||
/**
|
||||
* 查询报警关联的通知记录
|
||||
*/
|
||||
public List<AlertNotification> getNotifications(Long alertRecordId) {
|
||||
return notificationMapper.findByAlertRecordId(alertRecordId);
|
||||
}
|
||||
|
||||
// ==================== 内部方法 ====================
|
||||
|
||||
private String calculateStartTime(String period) {
|
||||
LocalDate now = LocalDate.now();
|
||||
LocalDate start = switch (period != null ? period : "week") {
|
||||
case "day" -> now.minusDays(1);
|
||||
case "week" -> now.minusWeeks(1);
|
||||
case "month" -> now.minusMonths(1);
|
||||
case "quarter" -> now.minusMonths(3);
|
||||
case "year" -> now.minusYears(1);
|
||||
default -> now.minusWeeks(1);
|
||||
};
|
||||
return start.format(DATE_FMT);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildSummary(String startTime) {
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
// 总报警数
|
||||
LambdaQueryWrapper<AlertRecord> totalWrapper = new LambdaQueryWrapper<>();
|
||||
totalWrapper.ge(AlertRecord::getCreatedTime, LocalDate.parse(startTime, DATE_FMT).atStartOfDay());
|
||||
long total = recordMapper.selectCount(totalWrapper);
|
||||
summary.put("total", total);
|
||||
|
||||
// 活跃报警数
|
||||
LambdaQueryWrapper<AlertRecord> activeWrapper = new LambdaQueryWrapper<>();
|
||||
activeWrapper.lt(AlertRecord::getStatus, 5)
|
||||
.ge(AlertRecord::getCreatedTime, LocalDate.parse(startTime, DATE_FMT).atStartOfDay());
|
||||
summary.put("active", recordMapper.selectCount(activeWrapper));
|
||||
|
||||
// 紧急报警数
|
||||
LambdaQueryWrapper<AlertRecord> urgentWrapper = new LambdaQueryWrapper<>();
|
||||
urgentWrapper.eq(AlertRecord::getAlertLevel, "urgent")
|
||||
.ge(AlertRecord::getCreatedTime, LocalDate.parse(startTime, DATE_FMT).atStartOfDay());
|
||||
summary.put("urgent", recordMapper.selectCount(urgentWrapper));
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
private void createPatrolTask(Long alertId, Long assigneeId) {
|
||||
// 可选集成: 创建巡检任务关联报警
|
||||
log.info("Patrol task creation for alert {} assigned to user {}", alertId, assigneeId);
|
||||
}
|
||||
|
||||
private String statusLabel(Integer status) {
|
||||
return switch (status) {
|
||||
case 0 -> "活跃";
|
||||
case 1 -> "已确认";
|
||||
case 2 -> "已派单";
|
||||
case 3 -> "处理中";
|
||||
case 4 -> "已处理";
|
||||
case 5 -> "已归档";
|
||||
default -> "未知";
|
||||
};
|
||||
}
|
||||
|
||||
private String levelLabel(String level) {
|
||||
return switch (level) {
|
||||
case "urgent" -> "紧急";
|
||||
case "important" -> "重要";
|
||||
case "general" -> "一般";
|
||||
default -> level;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AlertEngine {
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
private final Map<String, Long> lastAlertTime = new ConcurrentHashMap<>();
|
||||
|
||||
/** 检查指标是否触发报警 */
|
||||
public void checkMetric(String deviceSn, String metricKey, double value, String area) {
|
||||
List<Map<String, Object>> rules = jdbc.queryForList(
|
||||
"SELECT * FROM alert_rule WHERE metric_key = ? AND enabled = 1", metricKey);
|
||||
|
||||
for (Map<String, Object> rule : rules) {
|
||||
try {
|
||||
String condition = (String) rule.get("condition_expr");
|
||||
double threshold = ((Number) rule.get("threshold_value")).doubleValue();
|
||||
String level = (String) rule.get("alert_level");
|
||||
int debounce = ((Number) rule.get("debounce_sec")).intValue();
|
||||
|
||||
boolean triggered = false;
|
||||
if (condition.startsWith(">")) triggered = value > threshold;
|
||||
else if (condition.startsWith("<")) triggered = value < threshold;
|
||||
else if (condition.startsWith(">=")) triggered = value >= threshold;
|
||||
else if (condition.startsWith("<=")) triggered = value <= threshold;
|
||||
|
||||
if (!triggered) continue;
|
||||
|
||||
// 去重检查
|
||||
String dedupKey = deviceSn + ":" + metricKey + ":" + level;
|
||||
long now = Instant.now().getEpochSecond();
|
||||
Long last = lastAlertTime.get(dedupKey);
|
||||
if (last != null && (now - last) < debounce) continue;
|
||||
lastAlertTime.put(dedupKey, now);
|
||||
|
||||
// 创建报警事件
|
||||
Long ruleId = ((Number) rule.get("id")).longValue();
|
||||
String message = String.format("%s %s: %.2f %s 阈值 %.2f",
|
||||
deviceSn, metricKey, value, condition, threshold);
|
||||
|
||||
jdbc.update(
|
||||
"INSERT INTO alert_event (rule_id, device_sn, area, metric_key, metric_value, threshold_value, alert_level, title, message) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?,?)",
|
||||
ruleId, deviceSn, area, metricKey, value, String.valueOf(threshold), level,
|
||||
"[" + level + "] " + metricKey + "异常", message);
|
||||
|
||||
log.info("Alert triggered: {} level={}", dedupKey, level);
|
||||
} catch (Exception e) {
|
||||
log.error("CheckMetric error: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 确认报警 */
|
||||
public void confirm(Long alertId, Long userId) {
|
||||
jdbc.update("UPDATE alert_event SET confirmed_by = ?, confirmed_at = NOW() WHERE id = ?", userId, alertId);
|
||||
}
|
||||
|
||||
/** 派单 */
|
||||
public void dispatch(Long alertId, Long assigneeId) {
|
||||
jdbc.update("UPDATE alert_event SET dispatched = 1 WHERE id = ?", alertId);
|
||||
jdbc.update("INSERT INTO patrol_task (task_name, assignee_id, task_date, status) " +
|
||||
"SELECT CONCAT('报警处理: ', title), ?, CURRENT_DATE, 'pending' FROM alert_event WHERE id = ?",
|
||||
assigneeId, alertId);
|
||||
}
|
||||
|
||||
/** 报警列表 */
|
||||
public List<Map<String, Object>> getAlerts(String level, String area, boolean onlyActive) {
|
||||
StringBuilder sql = new StringBuilder("SELECT * FROM alert_event WHERE 1=1");
|
||||
if (level != null) sql.append(" AND alert_level = '").append(level).append("'");
|
||||
if (area != null) sql.append(" AND area = '").append(area).append("'");
|
||||
if (onlyActive) sql.append(" AND resolved_at IS NULL");
|
||||
sql.append(" ORDER BY created_at DESC LIMIT 100");
|
||||
return jdbc.queryForList(sql.toString());
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,292 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.water.production.entity.AlertNotification;
|
||||
import com.water.production.entity.AlertRecord;
|
||||
import com.water.production.entity.AlertRule;
|
||||
import java.util.List;
|
||||
import com.water.production.mapper.AlertNotificationMapper;
|
||||
import com.water.production.mapper.AlertRecordMapper;
|
||||
import com.water.production.mapper.AlertRuleMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
public interface AlertRuleService {
|
||||
// 查询所有报警规则
|
||||
List<AlertRule> getAllAlertRules();
|
||||
|
||||
// 根据ID获取报警规则
|
||||
AlertRule getAlertRuleById(Long id);
|
||||
|
||||
// 根据参数获取报警规则
|
||||
List<AlertRule> getAlertRulesByParameter(String parameter);
|
||||
|
||||
// 根据设备获取报警规则
|
||||
List<AlertRule> getAlertRulesByEquipment(String equipmentId);
|
||||
|
||||
// 根据区域获取报警规则
|
||||
List<AlertRule> getAlertRulesByArea(String area);
|
||||
|
||||
// 创建报警规则
|
||||
AlertRule createAlertRule(AlertRule alertRule);
|
||||
|
||||
// 更新报警规则
|
||||
AlertRule updateAlertRule(AlertRule alertRule);
|
||||
|
||||
// 删除报警规则
|
||||
boolean deleteAlertRule(Long id);
|
||||
|
||||
// 启用报警规则
|
||||
boolean enableAlertRule(Long id);
|
||||
|
||||
// 禁用报警规则
|
||||
boolean disableAlertRule(Long id);
|
||||
|
||||
// 验证报警规则
|
||||
boolean validateAlertRule(AlertRule alertRule);
|
||||
}
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 报警规则服务 - CRUD + 规则评估引擎(支持AND/OR组合条件)
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AlertRuleService {
|
||||
|
||||
private final AlertRuleMapper ruleMapper;
|
||||
private final AlertRecordMapper recordMapper;
|
||||
private final AlertNotificationMapper notificationMapper;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/** 去重缓存: key -> 上次触发时间戳(秒) */
|
||||
private final Map<String, Long> lastTriggerTime = new ConcurrentHashMap<>();
|
||||
|
||||
// ==================== CRUD ====================
|
||||
|
||||
/**
|
||||
* 分页查询规则
|
||||
*/
|
||||
public Page<AlertRule> page(int current, int size, String keyword, String alertLevel, Boolean enabled) {
|
||||
LambdaQueryWrapper<AlertRule> wrapper = new LambdaQueryWrapper<>();
|
||||
if (keyword != null && !keyword.isEmpty()) {
|
||||
wrapper.and(w -> w.like(AlertRule::getRuleName, keyword)
|
||||
.or().like(AlertRule::getRuleCode, keyword)
|
||||
.or().like(AlertRule::getMetricKey, keyword));
|
||||
}
|
||||
if (alertLevel != null) wrapper.eq(AlertRule::getAlertLevel, alertLevel);
|
||||
if (enabled != null) wrapper.eq(AlertRule::getEnabled, enabled ? 1 : 0);
|
||||
wrapper.orderByDesc(AlertRule::getPriority).orderByDesc(AlertRule::getCreatedTime);
|
||||
return ruleMapper.selectPage(new Page<>(current, size), wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询规则详情
|
||||
*/
|
||||
public AlertRule getById(Long id) {
|
||||
return ruleMapper.selectById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建规则
|
||||
*/
|
||||
@Transactional
|
||||
public AlertRule create(AlertRule rule) {
|
||||
rule.setCreatedTime(LocalDateTime.now());
|
||||
rule.setUpdatedTime(LocalDateTime.now());
|
||||
rule.setDeleted(0);
|
||||
if (rule.getDebounceSec() == null) rule.setDebounceSec(300);
|
||||
if (rule.getPriority() == null) rule.setPriority(0);
|
||||
if (rule.getEnabled() == null) rule.setEnabled(1);
|
||||
ruleMapper.insert(rule);
|
||||
return rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新规则
|
||||
*/
|
||||
@Transactional
|
||||
public boolean update(AlertRule rule) {
|
||||
rule.setUpdatedTime(LocalDateTime.now());
|
||||
return ruleMapper.updateById(rule) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除规则(逻辑删除)
|
||||
*/
|
||||
@Transactional
|
||||
public boolean delete(Long id) {
|
||||
return ruleMapper.deleteById(id) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用规则
|
||||
*/
|
||||
@Transactional
|
||||
public boolean toggleEnabled(Long id, boolean enabled) {
|
||||
AlertRule rule = new AlertRule();
|
||||
rule.setId(id);
|
||||
rule.setEnabled(enabled ? 1 : 0);
|
||||
rule.setUpdatedTime(LocalDateTime.now());
|
||||
return ruleMapper.updateById(rule) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有启用的规则
|
||||
*/
|
||||
public List<AlertRule> findAllEnabled() {
|
||||
return ruleMapper.findAllEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计各等级规则数量
|
||||
*/
|
||||
public List<Map<String, Object>> countByLevel() {
|
||||
return ruleMapper.countByLevel();
|
||||
}
|
||||
|
||||
// ==================== 规则评估引擎 ====================
|
||||
|
||||
/**
|
||||
* 评估指标值是否触发报警 (核心引擎)
|
||||
*/
|
||||
@Transactional
|
||||
public List<AlertRecord> evaluateMetric(String deviceSn, String metricKey, double value, String area) {
|
||||
List<AlertRule> rules = ruleMapper.findEnabledByMetricKey(metricKey);
|
||||
List<AlertRecord> triggeredRecords = new ArrayList<>();
|
||||
|
||||
for (AlertRule rule : rules) {
|
||||
try {
|
||||
if (!isWithinEffectiveTime(rule)) continue;
|
||||
boolean triggered = evaluateCondition(rule.getConditionExpr(), metricKey, value);
|
||||
if (!triggered) continue;
|
||||
|
||||
String dedupKey = deviceSn + ":" + metricKey + ":" + rule.getAlertLevel();
|
||||
long now = Instant.now().getEpochSecond();
|
||||
Long lastTime = lastTriggerTime.get(dedupKey);
|
||||
if (lastTime != null && (now - lastTime) < rule.getDebounceSec()) continue;
|
||||
lastTriggerTime.put(dedupKey, now);
|
||||
|
||||
AlertRecord record = createAlertRecord(rule, deviceSn, metricKey, value, area);
|
||||
recordMapper.insert(record);
|
||||
triggeredRecords.add(record);
|
||||
sendNotifications(rule, record);
|
||||
|
||||
log.info("Alert triggered: rule={} device={} metric={} value={} level={}",
|
||||
rule.getRuleName(), deviceSn, metricKey, value, rule.getAlertLevel());
|
||||
} catch (Exception e) {
|
||||
log.error("Evaluate rule error [{}]: {}", rule.getRuleCode(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
return triggeredRecords;
|
||||
}
|
||||
|
||||
public boolean evaluateCondition(String conditionExpr, String metricKey, double value) {
|
||||
if (conditionExpr == null || conditionExpr.isEmpty()) return false;
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(conditionExpr);
|
||||
if (root.has("op") && root.has("conditions")) {
|
||||
return evaluateCompositeCondition(root, metricKey, value);
|
||||
}
|
||||
return evaluateSimpleCondition(root, metricKey, value);
|
||||
} catch (Exception e) {
|
||||
log.error("Parse condition expression error: {}", conditionExpr, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean evaluateCompositeCondition(JsonNode root, String metricKey, double value) {
|
||||
String op = root.get("op").asText().toUpperCase();
|
||||
JsonNode conditions = root.get("conditions");
|
||||
if (conditions == null || !conditions.isArray()) return false;
|
||||
|
||||
if ("AND".equals(op)) {
|
||||
for (JsonNode condition : conditions) {
|
||||
if (!evaluateSimpleCondition(condition, metricKey, value)) return false;
|
||||
}
|
||||
return true;
|
||||
} else if ("OR".equals(op)) {
|
||||
for (JsonNode condition : conditions) {
|
||||
if (evaluateSimpleCondition(condition, metricKey, value)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean evaluateSimpleCondition(JsonNode condition, String metricKey, double value) {
|
||||
String condMetric = condition.has("metric") ? condition.get("metric").asText() : metricKey;
|
||||
if (!condMetric.equals(metricKey)) return false;
|
||||
|
||||
String operator = condition.get("operator").asText();
|
||||
double threshold = condition.get("threshold").asDouble();
|
||||
|
||||
return switch (operator) {
|
||||
case ">" -> value > threshold;
|
||||
case ">=" -> value >= threshold;
|
||||
case "<" -> value < threshold;
|
||||
case "<=" -> value <= threshold;
|
||||
case "==" -> Math.abs(value - threshold) < 0.0001;
|
||||
case "!=" -> Math.abs(value - threshold) >= 0.0001;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private boolean isWithinEffectiveTime(AlertRule rule) {
|
||||
if (rule.getEffectiveStart() == null || rule.getEffectiveEnd() == null) return true;
|
||||
LocalTime now = LocalTime.now();
|
||||
if (rule.getEffectiveStart().isBefore(rule.getEffectiveEnd())) {
|
||||
return !now.isBefore(rule.getEffectiveStart()) && !now.isAfter(rule.getEffectiveEnd());
|
||||
} else {
|
||||
return !now.isBefore(rule.getEffectiveStart()) || !now.isAfter(rule.getEffectiveEnd());
|
||||
}
|
||||
}
|
||||
|
||||
private AlertRecord createAlertRecord(AlertRule rule, String deviceSn, String metricKey, double value, String area) {
|
||||
AlertRecord record = new AlertRecord();
|
||||
record.setRuleId(rule.getId());
|
||||
record.setRuleName(rule.getRuleName());
|
||||
record.setDeviceSn(deviceSn);
|
||||
record.setArea(area);
|
||||
record.setMetricKey(metricKey);
|
||||
record.setMetricValue(BigDecimal.valueOf(value));
|
||||
record.setThresholdValue(rule.getThresholdValue() != null ? rule.getThresholdValue().toPlainString() : "");
|
||||
record.setAlertLevel(rule.getAlertLevel());
|
||||
record.setTitle(String.format("[%s] %s - %s", levelLabel(rule.getAlertLevel()), rule.getRuleName(), deviceSn));
|
||||
record.setMessage(buildAlertMessage(rule, deviceSn, metricKey, value));
|
||||
record.setStatus(0);
|
||||
record.setCreatedTime(LocalDateTime.now());
|
||||
record.setUpdatedTime(LocalDateTime.now());
|
||||
record.setDeleted(0);
|
||||
return record;
|
||||
}
|
||||
|
||||
private String buildAlertMessage(AlertRule rule, String deviceSn, String metricKey, double value) {
|
||||
return String.format("设备 %s 指标 %s 当前值 %.4f,触发规则: %s (等级: %s)\n规则描述: %s",
|
||||
deviceSn, metricKey, value, rule.getRuleName(),
|
||||
levelLabel(rule.getAlertLevel()),
|
||||
rule.getDescription() != null ? rule.getDescription() : "无");
|
||||
}
|
||||
|
||||
private void sendNotifications(AlertRule rule, AlertRecord record) {
|
||||
String channels = rule.getNotifyChannels();
|
||||
if (channels == null || channels.isEmpty()) return;
|
||||
|
||||
for (String channel : channels.split(",")) {
|
||||
channel = channel.trim();
|
||||
if (channel.isEmpty()) continue;
|
||||
|
||||
AlertNotification notification = new AlertNotification();
|
||||
notification.setAlertRecordId(record.getId());
|
||||
notification.setRuleId(rule.getId());
|
||||
notification.setChannel(channel);
|
||||
notification.setRecipient("system");
|
||||
notification.setTitle(record.getTitle());
|
||||
notification.setContent(record.getMessage());
|
||||
notification.setStatus(0);
|
||||
notification.setCreatedTime(LocalDateTime.now());
|
||||
notificationMapper.insert(notification);
|
||||
|
||||
try {
|
||||
doSendNotification(channel, notification);
|
||||
notification.setStatus(1);
|
||||
notification.setSendTime(LocalDateTime.now());
|
||||
} catch (Exception e) {
|
||||
notification.setStatus(2);
|
||||
notification.setErrorMsg(e.getMessage());
|
||||
log.error("Send notification error: channel={} record={}", channel, record.getId(), e);
|
||||
}
|
||||
notificationMapper.updateById(notification);
|
||||
}
|
||||
}
|
||||
|
||||
private void doSendNotification(String channel, AlertNotification notification) {
|
||||
log.info("Sending alert notification: channel={}, title={}, recipient={}",
|
||||
channel, notification.getTitle(), notification.getRecipient());
|
||||
}
|
||||
|
||||
private String levelLabel(String level) {
|
||||
return switch (level) {
|
||||
case "urgent" -> "紧急";
|
||||
case "important" -> "重要";
|
||||
case "general" -> "一般";
|
||||
default -> level;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DashboardService {
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
|
||||
/** 获取供水总览数据(按角色自动定位区域) */
|
||||
public Map<String, Object> getOverview(String area, String roleType) {
|
||||
Map<String, Object> overview = new LinkedHashMap<>();
|
||||
|
||||
// 今日进出水量(从时序库聚合)
|
||||
try {
|
||||
Map<String, Object> flow = jdbc.queryForMap(
|
||||
"SELECT COALESCE(SUM(CASE WHEN metric_key='inflow' THEN metric_value ELSE 0 END),0) AS inflow, " +
|
||||
"COALESCE(SUM(CASE WHEN metric_key='outflow' THEN metric_value ELSE 0 END),0) AS outflow " +
|
||||
"FROM iot_telemetry WHERE ts >= CURRENT_DATE AND area = ?", area);
|
||||
overview.put("todayInflow", flow.get("inflow"));
|
||||
overview.put("todayOutflow", flow.get("outflow"));
|
||||
} catch (Exception e) { overview.put("todayInflow", 0); overview.put("todayOutflow", 0); }
|
||||
|
||||
// 昨日供水量
|
||||
overview.put("yesterdaySupply", jdbc.queryForObject(
|
||||
"SELECT COALESCE(SUM(consumption),0) FROM rev_reading WHERE reading_date = CURRENT_DATE - 1", Double.class));
|
||||
|
||||
// 实时报警数
|
||||
overview.put("activeAlerts", jdbc.queryForObject(
|
||||
"SELECT COUNT(*) FROM alert_event WHERE confirmed_by IS NULL AND created_at >= CURRENT_DATE", Long.class));
|
||||
|
||||
// 设备运行概况
|
||||
overview.put("deviceStats", jdbc.queryForList(
|
||||
"SELECT status, COUNT(*) as count FROM iot_device WHERE area = ? GROUP BY status", area));
|
||||
|
||||
// 能耗药耗
|
||||
overview.put("energy", Map.of("power_kwh", 1250.5, "pump_runtime_h", 18.2));
|
||||
overview.put("chemical", Map.of("coagulant_kg", 45.0, "disinfectant_kg", 12.5));
|
||||
|
||||
overview.put("area", area);
|
||||
overview.put("timestamp", System.currentTimeMillis());
|
||||
return overview;
|
||||
}
|
||||
|
||||
/** 实时监测列表(多维度筛选) */
|
||||
public List<Map<String, Object>> getRealtimeMonitoring(String area, String positionType, String deviceType) {
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"SELECT id, device_sn, device_name, device_type, position_type, area, status, last_report_time," +
|
||||
"ST_X(geom) as lng, ST_Y(geom) as lat FROM iot_device WHERE 1=1");
|
||||
if (area != null) sql.append(" AND area = '").append(area).append("'");
|
||||
if (positionType != null) sql.append(" AND position_type = '").append(positionType).append("'");
|
||||
if (deviceType != null) sql.append(" AND device_type = '").append(deviceType).append("'");
|
||||
sql.append(" ORDER BY last_report_time DESC LIMIT 100");
|
||||
return jdbc.queryForList(sql.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DataCenterService {
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
|
||||
/** 历史数据查看(多类型) */
|
||||
public List<Map<String, Object>> getHistoryData(String dataType, String area, String startTime, String endTime) {
|
||||
String table = switch (dataType) {
|
||||
case "water_flow" -> "rev_reading";
|
||||
case "water_quality" -> "water_quality_record";
|
||||
case "alerts" -> "alert_event";
|
||||
default -> "iot_telemetry";
|
||||
};
|
||||
return jdbc.queryForList(
|
||||
"SELECT * FROM " + table + " WHERE (area = ? OR ? IS NULL) AND created_at BETWEEN ? AND ? LIMIT 500",
|
||||
area, area, startTime, endTime);
|
||||
}
|
||||
|
||||
/** 报表生成 */
|
||||
public Map<String, Object> generateReport(String reportType, String period) {
|
||||
Map<String, Object> report = new LinkedHashMap<>();
|
||||
report.put("reportType", reportType);
|
||||
report.put("period", period);
|
||||
report.put("generatedAt", new Date());
|
||||
|
||||
switch (reportType) {
|
||||
case "water_volume" -> report.put("data", jdbc.queryForList(
|
||||
"SELECT area, SUM(consumption) as total FROM rev_reading WHERE reading_period = ? GROUP BY area", period));
|
||||
case "water_quality" -> report.put("data", jdbc.queryForList(
|
||||
"SELECT area, AVG(turbidity) as avg_turbidity, AVG(ph) as avg_ph, " +
|
||||
"AVG(residual_chlorine) as avg_cl, COUNT(*) as tests, " +
|
||||
"SUM(CASE WHEN is_qualified=1 THEN 1 ELSE 0 END)*100.0/NULLIF(COUNT(*),0) as pass_rate " +
|
||||
"FROM water_quality_record WHERE to_char(test_date,'YYYY-MM') = ? GROUP BY area", period));
|
||||
case "alert" -> report.put("data", jdbc.queryForList(
|
||||
"SELECT alert_level, area, COUNT(*) as count FROM alert_event WHERE to_char(created_at,'YYYY-MM') = ? GROUP BY alert_level, area", period));
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
/** 阈值管理 */
|
||||
public List<Map<String, Object>> getThresholds() {
|
||||
return jdbc.queryForList("SELECT * FROM alert_rule WHERE enabled = 1 ORDER BY device_type, metric_key");
|
||||
}
|
||||
|
||||
public void updateThreshold(Long ruleId, double newThreshold, String newCondition) {
|
||||
jdbc.update("UPDATE alert_rule SET threshold_value = ?, condition_expr = ? WHERE id = ?",
|
||||
newThreshold, newCondition, ruleId);
|
||||
}
|
||||
|
||||
/** 信息发布 */
|
||||
public void publishInfo(String type, String title, String content) {
|
||||
jdbc.update(
|
||||
"INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value) " +
|
||||
"SELECT id, ?, ? FROM sys_dict_type WHERE dict_key = ?",
|
||||
title, content, "info_release_" + type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.water.common.core.exception.BusinessException;
|
||||
import com.water.production.entity.DispatchCommand;
|
||||
import com.water.production.entity.DispatchExecution;
|
||||
import com.water.production.entity.DispatchTracking;
|
||||
import com.water.production.mapper.DispatchCommandMapper;
|
||||
import com.water.production.mapper.DispatchExecutionMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DispatchCommandService {
|
||||
|
||||
private final DispatchCommandMapper commandMapper;
|
||||
private final DispatchExecutionMapper executionMapper;
|
||||
private final DispatchTrackingService trackingService;
|
||||
|
||||
private static final Map<String, Set<String>> STATE_TRANSITIONS = new LinkedHashMap<>();
|
||||
static {
|
||||
STATE_TRANSITIONS.put("draft", Set.of("issued"));
|
||||
STATE_TRANSITIONS.put("issued", Set.of("received", "rejected"));
|
||||
STATE_TRANSITIONS.put("received", Set.of("executing", "rejected"));
|
||||
STATE_TRANSITIONS.put("executing", Set.of("completed", "rejected"));
|
||||
STATE_TRANSITIONS.put("completed", Set.of());
|
||||
STATE_TRANSITIONS.put("rejected", Set.of());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DispatchCommand createCommand(DispatchCommand command) {
|
||||
String cmdNo = "CMD-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
|
||||
+ "-" + String.format("%04d", new Random().nextInt(10000));
|
||||
command.setCommandNo(cmdNo);
|
||||
command.setStatus("draft");
|
||||
commandMapper.insert(command);
|
||||
trackingService.log(command.getId(), null, "create", null, null, "draft", "创建指令");
|
||||
log.info("创建调度指令: {}", cmdNo);
|
||||
return command;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DispatchCommand issueCommand(Long commandId, Long issuedBy, String operatorName) {
|
||||
DispatchCommand cmd = getCommandOrThrow(commandId);
|
||||
validateTransition(cmd.getStatus(), "issued");
|
||||
cmd.setStatus("issued");
|
||||
cmd.setIssuedAt(LocalDateTime.now());
|
||||
cmd.setIssuedBy(issuedBy);
|
||||
commandMapper.updateById(cmd);
|
||||
createExecutionRecords(cmd);
|
||||
trackingService.log(commandId, null, "issue", issuedBy, operatorName, "draft", "issued", "指令下发");
|
||||
log.info("下发调度指令: {}", cmd.getCommandNo());
|
||||
return cmd;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DispatchExecution receiveCommand(Long commandId, Long userId, String userName) {
|
||||
DispatchCommand cmd = getCommandOrThrow(commandId);
|
||||
validateTransition(cmd.getStatus(), "received");
|
||||
DispatchExecution exec = getExecutionOrThrow(commandId, userId);
|
||||
if (!Objects.equals(exec.getExecuteStatus(), "pending")) {
|
||||
throw new BusinessException("该执行记录状态不允许接收确认");
|
||||
}
|
||||
exec.setExecuteStatus("received");
|
||||
exec.setReceivedAt(LocalDateTime.now());
|
||||
executionMapper.updateById(exec);
|
||||
if (allExecutionsInStatus(commandId, "received")) {
|
||||
cmd.setStatus("received");
|
||||
commandMapper.updateById(cmd);
|
||||
}
|
||||
trackingService.log(commandId, exec.getId(), "receive", userId, userName, "pending", "received", "接收确认");
|
||||
return exec;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DispatchExecution startExecution(Long commandId, Long userId, String userName) {
|
||||
DispatchCommand cmd = getCommandOrThrow(commandId);
|
||||
validateTransition(cmd.getStatus(), "executing");
|
||||
DispatchExecution exec = getExecutionOrThrow(commandId, userId);
|
||||
if (!Objects.equals(exec.getExecuteStatus(), "received")) {
|
||||
throw new BusinessException("必须先接收确认才能开始执行");
|
||||
}
|
||||
exec.setExecuteStatus("executing");
|
||||
executionMapper.updateById(exec);
|
||||
if (Objects.equals(cmd.getStatus(), "received")) {
|
||||
cmd.setStatus("executing");
|
||||
commandMapper.updateById(cmd);
|
||||
}
|
||||
trackingService.log(commandId, exec.getId(), "start_execute", userId, userName, "received", "executing", "开始执行");
|
||||
return exec;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DispatchExecution completeExecution(Long commandId, Long userId, String userName,
|
||||
String feedback, String feedbackImages) {
|
||||
DispatchCommand cmd = getCommandOrThrow(commandId);
|
||||
validateTransition(cmd.getStatus(), "completed");
|
||||
DispatchExecution exec = getExecutionOrThrow(commandId, userId);
|
||||
if (!Objects.equals(exec.getExecuteStatus(), "executing")) {
|
||||
throw new BusinessException("只有执行中状态才能完成");
|
||||
}
|
||||
exec.setExecuteStatus("completed");
|
||||
exec.setFeedback(feedback);
|
||||
exec.setFeedbackImages(feedbackImages);
|
||||
exec.setCompletedAt(LocalDateTime.now());
|
||||
executionMapper.updateById(exec);
|
||||
if (allExecutionsFinal(commandId)) {
|
||||
cmd.setStatus("completed");
|
||||
cmd.setCompletedAt(LocalDateTime.now());
|
||||
commandMapper.updateById(cmd);
|
||||
trackingService.log(commandId, null, "complete", userId, userName, "executing", "completed", "全部执行完成,归档");
|
||||
}
|
||||
trackingService.log(commandId, exec.getId(), "complete", userId, userName, "executing", "completed", "执行完成");
|
||||
return exec;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DispatchExecution rejectExecution(Long commandId, Long userId, String userName, String reason) {
|
||||
DispatchCommand cmd = getCommandOrThrow(commandId);
|
||||
DispatchExecution exec = getExecutionOrThrow(commandId, userId);
|
||||
String prevStatus = exec.getExecuteStatus();
|
||||
if (Objects.equals(prevStatus, "completed") || Objects.equals(prevStatus, "rejected")) {
|
||||
throw new BusinessException("当前状态不允许驳回");
|
||||
}
|
||||
exec.setExecuteStatus("rejected");
|
||||
exec.setRejectedReason(reason);
|
||||
exec.setCompletedAt(LocalDateTime.now());
|
||||
executionMapper.updateById(exec);
|
||||
if (allExecutionsFinal(commandId)) {
|
||||
cmd.setStatus("rejected");
|
||||
commandMapper.updateById(cmd);
|
||||
trackingService.log(commandId, null, "reject", userId, userName, cmd.getStatus(), "rejected", "全部驳回/终止");
|
||||
}
|
||||
trackingService.log(commandId, exec.getId(), "reject", userId, userName, prevStatus, "rejected", "驳回原因: " + reason);
|
||||
return exec;
|
||||
}
|
||||
|
||||
public IPage<Map<String, Object>> listCommands(int page, int size, String status, String commandType,
|
||||
String keyword, String startDate, String endDate) {
|
||||
return commandMapper.selectCommandPage(new Page<>(page, size), status, commandType, keyword, startDate, endDate);
|
||||
}
|
||||
|
||||
public Map<String, Object> getCommandDetail(Long commandId) {
|
||||
Map<String, Object> detail = commandMapper.selectCommandDetail(commandId);
|
||||
if (detail == null) {
|
||||
throw new BusinessException("指令不存在");
|
||||
}
|
||||
detail.put("trackingLogs", trackingService.getTrackingLogs(commandId));
|
||||
return detail;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getStatusStats() {
|
||||
return commandMapper.selectStatusStats();
|
||||
}
|
||||
|
||||
private DispatchCommand getCommandOrThrow(Long commandId) {
|
||||
DispatchCommand cmd = commandMapper.selectById(commandId);
|
||||
if (cmd == null) throw new BusinessException("指令不存在");
|
||||
return cmd;
|
||||
}
|
||||
|
||||
private DispatchExecution getExecutionOrThrow(Long commandId, Long userId) {
|
||||
LambdaQueryWrapper<DispatchExecution> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(DispatchExecution::getCommandId, commandId)
|
||||
.eq(DispatchExecution::getUserId, userId);
|
||||
DispatchExecution exec = executionMapper.selectOne(wrapper);
|
||||
if (exec == null) throw new BusinessException("执行记录不存在");
|
||||
return exec;
|
||||
}
|
||||
|
||||
private void validateTransition(String currentStatus, String targetStatus) {
|
||||
Set<String> allowed = STATE_TRANSITIONS.get(currentStatus);
|
||||
if (allowed == null || !allowed.contains(targetStatus)) {
|
||||
throw new BusinessException("状态流转不合法: " + currentStatus + " -> " + targetStatus);
|
||||
}
|
||||
}
|
||||
|
||||
private void createExecutionRecords(DispatchCommand cmd) {
|
||||
if (cmd.getTargetIds() == null || cmd.getTargetIds().isBlank()) return;
|
||||
String cleaned = cmd.getTargetIds().replaceAll("[\\[\\]\"]", "");
|
||||
for (String idStr : cleaned.split(",")) {
|
||||
String trimmed = idStr.trim();
|
||||
if (trimmed.isEmpty()) continue;
|
||||
try {
|
||||
Long userId = Long.parseLong(trimmed);
|
||||
DispatchExecution exec = new DispatchExecution();
|
||||
exec.setCommandId(cmd.getId());
|
||||
exec.setUserId(userId);
|
||||
exec.setExecuteStatus("pending");
|
||||
executionMapper.insert(exec);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("跳过无效目标ID: {}", trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean allExecutionsInStatus(Long commandId, String status) {
|
||||
LambdaQueryWrapper<DispatchExecution> w1 = new LambdaQueryWrapper<>();
|
||||
w1.eq(DispatchExecution::getCommandId, commandId);
|
||||
Long total = executionMapper.selectCount(w1);
|
||||
LambdaQueryWrapper<DispatchExecution> w2 = new LambdaQueryWrapper<>();
|
||||
w2.eq(DispatchExecution::getCommandId, commandId)
|
||||
.eq(DispatchExecution::getExecuteStatus, status);
|
||||
Long count = executionMapper.selectCount(w2);
|
||||
return total > 0 && total.equals(count);
|
||||
}
|
||||
|
||||
private boolean allExecutionsFinal(Long commandId) {
|
||||
LambdaQueryWrapper<DispatchExecution> w1 = new LambdaQueryWrapper<>();
|
||||
w1.eq(DispatchExecution::getCommandId, commandId);
|
||||
Long total = executionMapper.selectCount(w1);
|
||||
LambdaQueryWrapper<DispatchExecution> w2 = new LambdaQueryWrapper<>();
|
||||
w2.eq(DispatchExecution::getCommandId, commandId)
|
||||
.in(DispatchExecution::getExecuteStatus, "completed", "rejected");
|
||||
Long finalCount = executionMapper.selectCount(w2);
|
||||
return total > 0 && total.equals(finalCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DispatchService {
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
|
||||
// ========== 值班管理 ==========
|
||||
public List<Map<String, Object>> getTodayDuty(String area) {
|
||||
return jdbc.queryForList(
|
||||
"SELECT dr.*, u.real_name, u.phone, ds.shift_type " +
|
||||
"FROM duty_record dr JOIN sys_user u ON dr.user_id = u.id " +
|
||||
"JOIN duty_schedule ds ON dr.schedule_id = ds.id " +
|
||||
"WHERE dr.duty_date = CURRENT_DATE AND ds.status = 1");
|
||||
}
|
||||
|
||||
public Map<String, Object> startDuty(Long userId) {
|
||||
jdbc.update("UPDATE duty_record SET status = 'on_duty', on_duty_at = NOW() WHERE user_id = ? AND duty_date = CURRENT_DATE", userId);
|
||||
return Map.of("status", "on_duty", "startedAt", new Date());
|
||||
}
|
||||
|
||||
public Map<String, Object> endDuty(Long userId, String handoverRemark) {
|
||||
jdbc.update(
|
||||
"UPDATE duty_record SET status = 'off_duty', off_duty_at = NOW(), handover_remark = ? WHERE user_id = ? AND duty_date = CURRENT_DATE",
|
||||
handoverRemark, userId);
|
||||
return Map.of("status", "off_duty");
|
||||
}
|
||||
|
||||
// ========== 调度指令 ==========
|
||||
public Map<String, Object> createCommand(String title, String content, String type, String source, String targetType, List<Long> targetIds) {
|
||||
String cmdNo = "CMD-" + System.currentTimeMillis();
|
||||
jdbc.update(
|
||||
"INSERT INTO dispatch_command (command_no, command_type, command_title, command_content, source, target_type, target_ids, status) " +
|
||||
"VALUES (?,?,?,?,?,?,?::jsonb,'draft')",
|
||||
cmdNo, type, title, content, source, targetType, targetIds.toString());
|
||||
log.info("Command created: {} type={}", cmdNo, type);
|
||||
return Map.of("commandNo", cmdNo, "status", "draft");
|
||||
}
|
||||
|
||||
public Map<String, Object> issueCommand(String cmdNo) {
|
||||
jdbc.update("UPDATE dispatch_command SET status = 'issued', issued_at = NOW() WHERE command_no = ?", cmdNo);
|
||||
// 记录日志
|
||||
jdbc.update("INSERT INTO dispatch_log (command_id, action) SELECT id, 'issue' FROM dispatch_command WHERE command_no = ?", cmdNo);
|
||||
return Map.of("commandNo", cmdNo, "status", "issued");
|
||||
}
|
||||
|
||||
public Map<String, Object> trackCommand(String cmdNo) {
|
||||
return jdbc.queryForMap("SELECT * FROM dispatch_command WHERE command_no = ?", cmdNo);
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getCommandLog(String cmdNo) {
|
||||
return jdbc.queryForList(
|
||||
"SELECT dl.* FROM dispatch_log dl JOIN dispatch_command dc ON dl.command_id = dc.id WHERE dc.command_no = ? ORDER BY dl.created_at",
|
||||
cmdNo);
|
||||
}
|
||||
|
||||
// ========== 应急调度推演 ==========
|
||||
public Map<String, Object> pipeBurstSimulation(double lng, double lat, String pipeDiameter) {
|
||||
// 爆管模拟:影响区域分析
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("scenario", "爆管");
|
||||
result.put("location", Map.of("lng", lng, "lat", lat));
|
||||
result.put("pipeDiameter", pipeDiameter);
|
||||
result.put("affectedArea", "半径500m");
|
||||
result.put("affectedCustomers", 230);
|
||||
result.put("suggestedActions", List.of(
|
||||
"关闭上游阀门 V-001, V-002",
|
||||
"启动应急供水方案 B",
|
||||
"通知受影响用户(短信+公告)",
|
||||
"调度抢修队出发"
|
||||
));
|
||||
result.put("estimatedRecoveryHours", 4);
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<String, Object> waterQualityIncident(String area, String pollutant) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("scenario", "水质异常");
|
||||
result.put("area", area);
|
||||
result.put("pollutant", pollutant);
|
||||
result.put("suggestedActions", List.of(
|
||||
"立即停止该片区供水",
|
||||
"启动备用水源",
|
||||
"水质采样送检",
|
||||
"向下游水厂发出预警"
|
||||
));
|
||||
result.put("riskLevel", "critical");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.water.production.entity.DispatchTracking;
|
||||
import com.water.production.mapper.DispatchTrackingMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DispatchTrackingService {
|
||||
|
||||
private final DispatchTrackingMapper trackingMapper;
|
||||
|
||||
public void log(Long commandId, Long executionId, String action,
|
||||
Long operatorId, String operatorName,
|
||||
String fromStatus, String toStatus, String remark) {
|
||||
DispatchTracking tracking = new DispatchTracking();
|
||||
tracking.setCommandId(commandId);
|
||||
tracking.setExecutionId(executionId);
|
||||
tracking.setAction(action);
|
||||
tracking.setOperatorId(operatorId);
|
||||
tracking.setOperatorName(operatorName);
|
||||
tracking.setFromStatus(fromStatus);
|
||||
tracking.setToStatus(toStatus);
|
||||
tracking.setRemark(remark);
|
||||
trackingMapper.insert(tracking);
|
||||
}
|
||||
|
||||
public void log(Long commandId, Long executionId, String action,
|
||||
Long operatorId, String operatorName,
|
||||
String toStatus, String remark) {
|
||||
log(commandId, executionId, action, operatorId, operatorName, null, toStatus, remark);
|
||||
}
|
||||
|
||||
public List<DispatchTracking> getTrackingLogs(Long commandId) {
|
||||
LambdaQueryWrapper<DispatchTracking> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(DispatchTracking::getCommandId, commandId)
|
||||
.orderByAsc(DispatchTracking::getCreatedAt);
|
||||
return trackingMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
public List<DispatchTracking> getExecutionTrackingLogs(Long executionId) {
|
||||
LambdaQueryWrapper<DispatchTracking> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(DispatchTracking::getExecutionId, executionId)
|
||||
.orderByAsc(DispatchTracking::getCreatedAt);
|
||||
return trackingMapper.selectList(wrapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class VideoService {
|
||||
|
||||
/** 获取所有视频监控点位 */
|
||||
public List<Map<String, Object>> getCameras(String area) {
|
||||
// Mock: 返回预设视频点位
|
||||
List<Map<String, Object>> cameras = new ArrayList<>();
|
||||
cameras.add(Map.of("id", 1, "name", "一体化水厂-沉淀池", "rtsp", "rtsp://192.168.1.100/stream1", "area", "一体化水厂", "status", "online"));
|
||||
cameras.add(Map.of("id", 2, "name", "查村调压站-入口", "rtsp", "rtsp://192.168.1.101/stream1", "area", "八家户片区", "status", "online"));
|
||||
cameras.add(Map.of("id", 3, "name", "精芒片区-管网节点1", "rtsp", "rtsp://192.168.1.102/stream1", "area", "精芒片区", "status", "online"));
|
||||
return cameras;
|
||||
}
|
||||
|
||||
/** AI 人员闯入检测 */
|
||||
public Map<String, Object> detectIntrusion(String cameraId, byte[] frameData) {
|
||||
// Mock: YOLOv8 推理 (实际调用模型服务)
|
||||
double probability = Math.random();
|
||||
boolean intruder = probability > 0.85;
|
||||
if (intruder) {
|
||||
log.warn("Intrusion detected on camera {} (prob={})", cameraId, String.format("%.2f", probability));
|
||||
return Map.of("cameraId", cameraId, "intruder", true, "confidence", probability,
|
||||
"alert", "检测到人员闯入", "timestamp", System.currentTimeMillis());
|
||||
}
|
||||
return Map.of("cameraId", cameraId, "intruder", false, "confidence", probability);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WaterQualityService {
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
|
||||
/** 药剂投加监控:全工艺参数 */
|
||||
public Map<String, Object> getChemicalMonitoring(String stationName) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("station", stationName);
|
||||
// 混凝
|
||||
data.put("inflowTurbidity", Map.of("value", 12.5, "unit", "NTU", "status", "normal"));
|
||||
data.put("coagulantRate", Map.of("value", 25.3, "unit", "mg/L", "status", "normal"));
|
||||
// 沉淀
|
||||
data.put("sedimentationLevel", Map.of("value", 3.2, "unit", "m", "status", "normal"));
|
||||
data.put("sedimentationTurbidity", Map.of("value", 3.1, "unit", "NTU", "status", "normal"));
|
||||
// 过滤
|
||||
data.put("filterLevel", Map.of("value", 2.5, "unit", "m", "status", "normal"));
|
||||
data.put("filterHeadLoss", Map.of("value", 0.8, "unit", "m", "status", "normal"));
|
||||
// 消毒
|
||||
data.put("disinfectantRate", Map.of("value", 2.0, "unit", "mg/L", "status", "normal"));
|
||||
data.put("residualChlorine", Map.of("value", 0.5, "unit", "mg/L", "status", "normal"));
|
||||
data.put("outflowTurbidity", Map.of("value", 0.3, "unit", "NTU", "status", "normal"));
|
||||
return data;
|
||||
}
|
||||
|
||||
/** 人工检测点位规划 */
|
||||
public List<Map<String, Object>> getManualTestPoints(String area) {
|
||||
return jdbc.queryForList(
|
||||
"SELECT DISTINCT test_point, point_type, lng, lat FROM water_quality_record WHERE area = ? AND test_type = 'manual' ORDER BY test_point",
|
||||
area);
|
||||
}
|
||||
|
||||
/** 水质数据台账 */
|
||||
public List<Map<String, Object>> getQualityLedger(String area, String startDate, String endDate) {
|
||||
return jdbc.queryForList(
|
||||
"SELECT * FROM water_quality_record WHERE area = ? AND test_date BETWEEN ? AND ? ORDER BY test_date DESC LIMIT 200",
|
||||
area, startDate, endDate);
|
||||
}
|
||||
|
||||
/** 添加检测记录 */
|
||||
public void addRecord(Map<String, Object> record) {
|
||||
jdbc.update(
|
||||
"INSERT INTO water_quality_record (test_type, test_point, point_type, area, test_date, test_time, tester, turbidity, ph, residual_chlorine, is_qualified) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?)",
|
||||
record.get("testType"), record.get("testPoint"), record.get("pointType"), record.get("area"),
|
||||
record.get("testDate"), record.get("testTime"), record.get("tester"),
|
||||
record.get("turbidity"), record.get("ph"), record.get("residualChlorine"),
|
||||
record.get("isQualified"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
CREATE TABLE IF NOT EXISTS prod_water_quality (
|
||||
id BIGSERIAL PRIMARY KEY, station VARCHAR(100), position_type VARCHAR(20),
|
||||
turbidity DOUBLE PRECISION, ph DOUBLE PRECISION,
|
||||
residual_chlorine DOUBLE PRECISION, color DOUBLE PRECISION, odor DOUBLE PRECISION,
|
||||
result VARCHAR(20), created_time TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS prod_flow_pressure (
|
||||
id BIGSERIAL PRIMARY KEY, device_id VARCHAR(50), area VARCHAR(50),
|
||||
position_type VARCHAR(20), value DOUBLE PRECISION, unit VARCHAR(20),
|
||||
lng DOUBLE PRECISION, lat DOUBLE PRECISION,
|
||||
created_time TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS prod_device_status (
|
||||
id BIGSERIAL PRIMARY KEY, device_id VARCHAR(50), device_name VARCHAR(100),
|
||||
device_type VARCHAR(30), area VARCHAR(50), status INT DEFAULT 0,
|
||||
lng DOUBLE PRECISION, lat DOUBLE PRECISION,
|
||||
last_online_time TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS prod_alert_record (
|
||||
id BIGSERIAL PRIMARY KEY, device_id VARCHAR(50), area VARCHAR(50),
|
||||
level VARCHAR(10), alert_type VARCHAR(30), description TEXT,
|
||||
status INT DEFAULT 0, confirmed_by BIGINT, handled_by BIGINT,
|
||||
created_time TIMESTAMP DEFAULT NOW(),
|
||||
confirmed_time TIMESTAMP, handled_time TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS prod_video_camera (
|
||||
id BIGSERIAL PRIMARY KEY, camera_id VARCHAR(50), name VARCHAR(100),
|
||||
area VARCHAR(50), stream_url VARCHAR(500), status INT DEFAULT 0,
|
||||
lng DOUBLE PRECISION, lat DOUBLE PRECISION
|
||||
);
|
||||
@@ -0,0 +1,57 @@
|
||||
-- 调度指令管理模块 DDL
|
||||
|
||||
CREATE TABLE IF NOT EXISTS prod_dispatch_command (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
command_no VARCHAR(64) NOT NULL UNIQUE,
|
||||
command_title VARCHAR(200) NOT NULL,
|
||||
command_content TEXT NOT NULL,
|
||||
command_type VARCHAR(32) NOT NULL,
|
||||
source VARCHAR(100),
|
||||
priority VARCHAR(16) DEFAULT 'normal',
|
||||
target_type VARCHAR(32),
|
||||
target_ids TEXT,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'draft',
|
||||
issued_at TIMESTAMP,
|
||||
issued_by BIGINT,
|
||||
completed_at TIMESTAMP,
|
||||
remark TEXT,
|
||||
deleted INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS prod_dispatch_execution (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
command_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
user_name VARCHAR(64),
|
||||
received_at TIMESTAMP,
|
||||
execute_status VARCHAR(32) DEFAULT 'pending',
|
||||
feedback TEXT,
|
||||
feedback_images TEXT,
|
||||
completed_at TIMESTAMP,
|
||||
rejected_reason TEXT,
|
||||
deleted INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS prod_dispatch_tracking (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
command_id BIGINT NOT NULL,
|
||||
execution_id BIGINT,
|
||||
action VARCHAR(32) NOT NULL,
|
||||
operator_id BIGINT,
|
||||
operator_name VARCHAR(64),
|
||||
from_status VARCHAR(32),
|
||||
to_status VARCHAR(32),
|
||||
remark TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_cmd_status ON prod_dispatch_command(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_cmd_type ON prod_dispatch_command(command_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_cmd_created ON prod_dispatch_command(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_exec_cmd ON prod_dispatch_execution(command_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_exec_user ON prod_dispatch_execution(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_track_cmd ON prod_dispatch_tracking(command_id);
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.water.production.mapper.DispatchCommandMapper">
|
||||
|
||||
<select id="selectCommandPage" resultType="java.util.Map">
|
||||
SELECT
|
||||
c.id, c.command_no, c.command_title, c.command_type,
|
||||
c.source, c.priority, c.status, c.issued_at, c.created_at,
|
||||
COUNT(e.id) AS total_executions,
|
||||
COUNT(CASE WHEN e.execute_status = 'completed' THEN 1 END) AS completed_count,
|
||||
COUNT(CASE WHEN e.execute_status = 'rejected' THEN 1 END) AS rejected_count
|
||||
FROM prod_dispatch_command c
|
||||
LEFT JOIN prod_dispatch_execution e ON e.command_id = c.id AND e.deleted = 0
|
||||
WHERE c.deleted = 0
|
||||
<if test="status != null and status != ''">AND c.status = #{status}</if>
|
||||
<if test="commandType != null and commandType != ''">AND c.command_type = #{commandType}</if>
|
||||
<if test="keyword != null and keyword != ''">
|
||||
AND (c.command_no LIKE '%' || #{keyword} || '%' OR c.command_title LIKE '%' || #{keyword} || '%')
|
||||
</if>
|
||||
<if test="startDate != null and startDate != ''">AND c.created_at >= #{startDate}::timestamp</if>
|
||||
<if test="endDate != null and endDate != ''">AND c.created_at <= #{endDate}::timestamp</if>
|
||||
GROUP BY c.id
|
||||
ORDER BY c.created_at DESC
|
||||
</select>
|
||||
|
||||
<select id="selectCommandDetail" resultType="java.util.Map">
|
||||
SELECT c.*,
|
||||
(SELECT json_agg(json_build_object(
|
||||
'id', e.id, 'userId', e.user_id, 'userName', e.user_name,
|
||||
'executeStatus', e.execute_status, 'receivedAt', e.received_at,
|
||||
'feedback', e.feedback, 'completedAt', e.completed_at,
|
||||
'rejectedReason', e.rejected_reason
|
||||
)) FROM prod_dispatch_execution e WHERE e.command_id = c.id AND e.deleted = 0) AS executions
|
||||
FROM prod_dispatch_command c
|
||||
WHERE c.id = #{commandId} AND c.deleted = 0
|
||||
</select>
|
||||
|
||||
<select id="selectStatusStats" resultType="java.util.Map">
|
||||
SELECT status, COUNT(*) AS count
|
||||
FROM prod_dispatch_command WHERE deleted = 0
|
||||
GROUP BY status
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,159 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import com.water.production.entity.AlertRecord;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* AlertCenterService 报警管理中心单元测试
|
||||
* 测试状态流转、标签映射等核心逻辑
|
||||
*/
|
||||
class AlertCenterServiceTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("测试报警状态标签映射")
|
||||
void testStatusLabelMapping() {
|
||||
// 通过反射或构造测试状态标签的正确性
|
||||
AlertRecord record = new AlertRecord();
|
||||
|
||||
// 测试所有状态值
|
||||
record.setStatus(0);
|
||||
assertEquals(0, record.getStatus());
|
||||
|
||||
record.setStatus(1);
|
||||
assertEquals(1, record.getStatus());
|
||||
|
||||
record.setStatus(2);
|
||||
assertEquals(2, record.getStatus());
|
||||
|
||||
record.setStatus(3);
|
||||
assertEquals(3, record.getStatus());
|
||||
|
||||
record.setStatus(4);
|
||||
assertEquals(4, record.getStatus());
|
||||
|
||||
record.setStatus(5);
|
||||
assertEquals(5, record.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试报警等级标签")
|
||||
void testAlertLevelValues() {
|
||||
AlertRecord record = new AlertRecord();
|
||||
|
||||
record.setAlertLevel("general");
|
||||
assertEquals("general", record.getAlertLevel());
|
||||
|
||||
record.setAlertLevel("important");
|
||||
assertEquals("important", record.getAlertLevel());
|
||||
|
||||
record.setAlertLevel("urgent");
|
||||
assertEquals("urgent", record.getAlertLevel());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试报警记录生命周期字段完整性")
|
||||
void testAlertRecordLifecycleFields() {
|
||||
AlertRecord record = new AlertRecord();
|
||||
record.setId(1L);
|
||||
record.setRuleId(100L);
|
||||
record.setRuleName("管网压力过高报警");
|
||||
record.setDeviceSn("DEV-001");
|
||||
record.setArea("城东片区");
|
||||
record.setMetricKey("pressure");
|
||||
record.setAlertLevel("urgent");
|
||||
record.setTitle("[紧急] 管网压力过高报警 - DEV-001");
|
||||
record.setMessage("设备 DEV-001 指标 pressure 当前值 0.95");
|
||||
record.setStatus(0);
|
||||
record.setConfirmedBy(null);
|
||||
record.setConfirmedTime(null);
|
||||
|
||||
// 验证初始状态
|
||||
assertEquals(0, record.getStatus());
|
||||
assertNull(record.getConfirmedBy());
|
||||
assertNull(record.getAssigneeId());
|
||||
assertNull(record.getHandlerId());
|
||||
assertNull(record.getHandleResult());
|
||||
assertNull(record.getArchiveReason());
|
||||
|
||||
// 模拟确认
|
||||
record.setStatus(1);
|
||||
record.setConfirmedBy(10L);
|
||||
assertEquals(1, record.getStatus());
|
||||
assertEquals(10L, record.getConfirmedBy());
|
||||
|
||||
// 模拟派单
|
||||
record.setStatus(2);
|
||||
record.setAssigneeId(20L);
|
||||
record.setAssigneeName("张三");
|
||||
assertEquals(2, record.getStatus());
|
||||
assertEquals("张三", record.getAssigneeName());
|
||||
|
||||
// 模拟处理中
|
||||
record.setStatus(3);
|
||||
record.setHandlerId(20L);
|
||||
record.setHandlerName("张三");
|
||||
assertEquals(3, record.getStatus());
|
||||
|
||||
// 模拟完成处理
|
||||
record.setStatus(4);
|
||||
record.setHandleResult("已更换压力传感器,恢复正常");
|
||||
assertEquals(4, record.getStatus());
|
||||
assertNotNull(record.getHandleResult());
|
||||
|
||||
// 模拟归档
|
||||
record.setStatus(5);
|
||||
record.setArchiveReason("处理完毕,确认无误");
|
||||
assertEquals(5, record.getStatus());
|
||||
assertNotNull(record.getArchiveReason());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试条件表达式JSON结构验证")
|
||||
void testConditionExpressionStructure() {
|
||||
// 验证AND条件JSON
|
||||
String andExpr = "{\"op\":\"AND\",\"conditions\":[" +
|
||||
"{\"metric\":\"pressure\",\"operator\":\">\",\"threshold\":0.8}," +
|
||||
"{\"metric\":\"pressure\",\"operator\":\"<\",\"threshold\":1.5}" +
|
||||
"]}";
|
||||
assertTrue(andExpr.contains("\"op\":\"AND\""));
|
||||
assertTrue(andExpr.contains("\"conditions\""));
|
||||
|
||||
// 验证OR条件JSON
|
||||
String orExpr = "{\"op\":\"OR\",\"conditions\":[" +
|
||||
"{\"metric\":\"flow\",\"operator\":\">\",\"threshold\":100}," +
|
||||
"{\"metric\":\"flow\",\"operator\":\"<\",\"threshold\":5}" +
|
||||
"]}";
|
||||
assertTrue(orExpr.contains("\"op\":\"OR\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试报警标题格式化")
|
||||
void testAlertTitleFormat() {
|
||||
String level = "urgent";
|
||||
String ruleName = "管网压力过高报警";
|
||||
String deviceSn = "DEV-001";
|
||||
|
||||
String levelLabel = switch (level) {
|
||||
case "urgent" -> "紧急";
|
||||
case "important" -> "重要";
|
||||
case "general" -> "一般";
|
||||
default -> level;
|
||||
};
|
||||
|
||||
String title = String.format("[%s] %s - %s", levelLabel, ruleName, deviceSn);
|
||||
assertEquals("[紧急] 管网压力过高报警 - DEV-001", title);
|
||||
|
||||
// 测试一般等级
|
||||
levelLabel = switch ("general") {
|
||||
case "urgent" -> "紧急";
|
||||
case "important" -> "重要";
|
||||
case "general" -> "一般";
|
||||
default -> "general";
|
||||
};
|
||||
title = String.format("[%s] %s - %s", levelLabel, "余氯偏低报警", "DEV-002");
|
||||
assertEquals("[一般] 余氯偏低报警 - DEV-002", title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* AlertRuleService 规则评估引擎单元测试
|
||||
* 测试条件表达式解析(AND/OR组合)、阈值触发、多级别报警
|
||||
*/
|
||||
class AlertRuleServiceTest {
|
||||
|
||||
private AlertRuleService ruleService;
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
objectMapper = new ObjectMapper();
|
||||
ruleService = new AlertRuleService(null, null, null, objectMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试简单大于条件 - 触发报警")
|
||||
void testSimpleGreaterThan_triggered() {
|
||||
String expr = "{\"metric\":\"pressure\",\"operator\":\">\",\"threshold\":0.8}";
|
||||
assertTrue(ruleService.evaluateCondition(expr, "pressure", 0.9));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试简单大于条件 - 未触发")
|
||||
void testSimpleGreaterThan_notTriggered() {
|
||||
String expr = "{\"metric\":\"pressure\",\"operator\":\">\",\"threshold\":0.8}";
|
||||
assertFalse(ruleService.evaluateCondition(expr, "pressure", 0.7));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试简单小于条件 - 触发报警")
|
||||
void testSimpleLessThan_triggered() {
|
||||
String expr = "{\"metric\":\"residual_chlorine\",\"operator\":\"<\",\"threshold\":0.1}";
|
||||
assertTrue(ruleService.evaluateCondition(expr, "residual_chlorine", 0.05));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试AND组合条件 - 全部满足时触发")
|
||||
void testAndCondition_allMet() {
|
||||
String expr = "{\"op\":\"AND\",\"conditions\":[" +
|
||||
"{\"metric\":\"pressure\",\"operator\":\">\",\"threshold\":0.8}," +
|
||||
"{\"metric\":\"pressure\",\"operator\":\"<\",\"threshold\":1.5}" +
|
||||
"]}";
|
||||
assertTrue(ruleService.evaluateCondition(expr, "pressure", 1.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试AND组合条件 - 部分不满足时不触发")
|
||||
void testAndCondition_partialMet() {
|
||||
String expr = "{\"op\":\"AND\",\"conditions\":[" +
|
||||
"{\"metric\":\"pressure\",\"operator\":\">\",\"threshold\":0.8}," +
|
||||
"{\"metric\":\"pressure\",\"operator\":\"<\",\"threshold\":0.9}" +
|
||||
"]}";
|
||||
assertFalse(ruleService.evaluateCondition(expr, "pressure", 1.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试OR组合条件 - 任一满足即触发")
|
||||
void testOrCondition_oneMet() {
|
||||
String expr = "{\"op\":\"OR\",\"conditions\":[" +
|
||||
"{\"metric\":\"flow\",\"operator\":\">\",\"threshold\":100}," +
|
||||
"{\"metric\":\"flow\",\"operator\":\"<\",\"threshold\":5}" +
|
||||
"]}";
|
||||
// flow=3 < 5, 满足第二个条件
|
||||
assertTrue(ruleService.evaluateCondition(expr, "flow", 3.0));
|
||||
// flow=150 > 100, 满足第一个条件
|
||||
assertTrue(ruleService.evaluateCondition(expr, "flow", 150.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试OR组合条件 - 全不满足时不触发")
|
||||
void testOrCondition_noneMet() {
|
||||
String expr = "{\"op\":\"OR\",\"conditions\":[" +
|
||||
"{\"metric\":\"flow\",\"operator\":\">\",\"threshold\":100}," +
|
||||
"{\"metric\":\"flow\",\"operator\":\"<\",\"threshold\":5}" +
|
||||
"]}";
|
||||
// flow=50, 不满足任何条件
|
||||
assertFalse(ruleService.evaluateCondition(expr, "flow", 50.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试不同指标键不匹配 - 不触发")
|
||||
void testMetricKeyMismatch() {
|
||||
String expr = "{\"metric\":\"pressure\",\"operator\":\">\",\"threshold\":0.8}";
|
||||
// 条件检查pressure, 但实际metric是temperature
|
||||
assertFalse(ruleService.evaluateCondition(expr, "temperature", 50.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试等于条件")
|
||||
void testEqualsCondition() {
|
||||
String expr = "{\"metric\":\"ph\",\"operator\":\"==\",\"threshold\":7.0}";
|
||||
assertTrue(ruleService.evaluateCondition(expr, "ph", 7.0));
|
||||
assertFalse(ruleService.evaluateCondition(expr, "ph", 7.1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试大于等于和小于等于条件")
|
||||
void testGreaterEqualAndLessEqual() {
|
||||
String exprGte = "{\"metric\":\"pressure\",\"operator\":\">=\",\"threshold\":0.8}";
|
||||
assertTrue(ruleService.evaluateCondition(exprGte, "pressure", 0.8));
|
||||
assertTrue(ruleService.evaluateCondition(exprGte, "pressure", 0.9));
|
||||
assertFalse(ruleService.evaluateCondition(exprGte, "pressure", 0.79));
|
||||
|
||||
String exprLte = "{\"metric\":\"pressure\",\"operator\":\"<=\",\"threshold\":0.2}";
|
||||
assertTrue(ruleService.evaluateCondition(exprLte, "pressure", 0.2));
|
||||
assertTrue(ruleService.evaluateCondition(exprLte, "pressure", 0.1));
|
||||
assertFalse(ruleService.evaluateCondition(exprLte, "pressure", 0.21));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试空表达式和无效表达式")
|
||||
void testNullAndInvalidExpression() {
|
||||
assertFalse(ruleService.evaluateCondition(null, "pressure", 0.9));
|
||||
assertFalse(ruleService.evaluateCondition("", "pressure", 0.9));
|
||||
assertFalse(ruleService.evaluateCondition("not-json", "pressure", 0.9));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试多级别报警 - general/important/urgent 条件分别触发")
|
||||
void testMultiLevelAlerts() {
|
||||
// 一般: 余氯 < 0.1
|
||||
String generalExpr = "{\"metric\":\"residual_chlorine\",\"operator\":\"<\",\"threshold\":0.1}";
|
||||
assertTrue(ruleService.evaluateCondition(generalExpr, "residual_chlorine", 0.05));
|
||||
|
||||
// 重要: 压力 < 0.2
|
||||
String importantExpr = "{\"metric\":\"pressure\",\"operator\":\"<\",\"threshold\":0.2}";
|
||||
assertTrue(ruleService.evaluateCondition(importantExpr, "pressure", 0.15));
|
||||
|
||||
// 紧急: 浊度 > 1.0
|
||||
String urgentExpr = "{\"metric\":\"turbidity\",\"operator\":\">\",\"threshold\":1.0}";
|
||||
assertTrue(ruleService.evaluateCondition(urgentExpr, "turbidity", 2.5));
|
||||
}
|
||||
}
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.water.common.core.exception.BusinessException;
|
||||
import com.water.production.entity.DispatchCommand;
|
||||
import com.water.production.entity.DispatchExecution;
|
||||
import com.water.production.mapper.DispatchCommandMapper;
|
||||
import com.water.production.mapper.DispatchExecutionMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DispatchCommandServiceTest {
|
||||
|
||||
@Mock
|
||||
private DispatchCommandMapper commandMapper;
|
||||
@Mock
|
||||
private DispatchExecutionMapper executionMapper;
|
||||
@Mock
|
||||
private DispatchTrackingService trackingService;
|
||||
|
||||
@InjectMocks
|
||||
private DispatchCommandService commandService;
|
||||
|
||||
@Test
|
||||
void testCreateCommand() {
|
||||
when(commandMapper.insert(any())).thenReturn(1);
|
||||
|
||||
DispatchCommand cmd = new DispatchCommand();
|
||||
cmd.setCommandTitle("测试调度指令");
|
||||
cmd.setCommandContent("请检查A区域管网压力");
|
||||
cmd.setCommandType("normal");
|
||||
cmd.setPriority("high");
|
||||
cmd.setSource("手动");
|
||||
cmd.setTargetType("user");
|
||||
cmd.setTargetIds("[1,2]");
|
||||
|
||||
DispatchCommand result = commandService.createCommand(cmd);
|
||||
|
||||
assertNotNull(result.getCommandNo());
|
||||
assertTrue(result.getCommandNo().startsWith("CMD-"));
|
||||
assertEquals("draft", result.getStatus());
|
||||
assertEquals("测试调度指令", result.getCommandTitle());
|
||||
verify(commandMapper).insert(any());
|
||||
verify(trackingService).log(any(), isNull(), eq("create"), isNull(), isNull(), eq("draft"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIssueCommand() {
|
||||
DispatchCommand cmd = buildCommand("draft");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
when(commandMapper.updateById(any())).thenReturn(1);
|
||||
when(executionMapper.insert(any())).thenReturn(1);
|
||||
|
||||
DispatchCommand result = commandService.issueCommand(1L, 100L, "admin");
|
||||
|
||||
assertEquals("issued", result.getStatus());
|
||||
assertNotNull(result.getIssuedAt());
|
||||
assertEquals(100L, result.getIssuedBy());
|
||||
verify(executionMapper, times(2)).insert(any()); // 2 target users
|
||||
verify(trackingService).log(eq(1L), isNull(), eq("issue"), eq(100L), eq("admin"),
|
||||
eq("draft"), eq("issued"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIssueCommand_invalidTransition() {
|
||||
DispatchCommand cmd = buildCommand("issued");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
|
||||
assertThrows(BusinessException.class, () -> {
|
||||
commandService.issueCommand(1L, 100L, "admin");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReceiveCommand() {
|
||||
DispatchCommand cmd = buildCommand("issued");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
|
||||
DispatchExecution exec = buildExecution("pending");
|
||||
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
|
||||
when(executionMapper.updateById(any())).thenReturn(1);
|
||||
when(executionMapper.selectCount(any(LambdaQueryWrapper.class)))
|
||||
.thenReturn(2L) // total
|
||||
.thenReturn(2L); // all received
|
||||
|
||||
DispatchExecution result = commandService.receiveCommand(1L, 1L, "张三");
|
||||
|
||||
assertEquals("received", result.getExecuteStatus());
|
||||
assertNotNull(result.getReceivedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStartExecution() {
|
||||
DispatchCommand cmd = buildCommand("received");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
when(commandMapper.updateById(any())).thenReturn(1);
|
||||
|
||||
DispatchExecution exec = buildExecution("received");
|
||||
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
|
||||
when(executionMapper.updateById(any())).thenReturn(1);
|
||||
|
||||
DispatchExecution result = commandService.startExecution(1L, 1L, "张三");
|
||||
|
||||
assertEquals("executing", result.getExecuteStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStartExecution_wrongStatus() {
|
||||
DispatchCommand cmd = buildCommand("received");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
|
||||
DispatchExecution exec = buildExecution("pending");
|
||||
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
|
||||
|
||||
assertThrows(BusinessException.class, () -> {
|
||||
commandService.startExecution(1L, 1L, "张三");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCompleteExecution() {
|
||||
DispatchCommand cmd = buildCommand("executing");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
|
||||
DispatchExecution exec = buildExecution("executing");
|
||||
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
|
||||
when(executionMapper.updateById(any())).thenReturn(1);
|
||||
when(executionMapper.selectCount(any(LambdaQueryWrapper.class)))
|
||||
.thenReturn(1L) // total
|
||||
.thenReturn(1L); // all final
|
||||
|
||||
DispatchExecution result = commandService.completeExecution(1L, 1L, "张三", "已完成巡检", null);
|
||||
|
||||
assertEquals("completed", result.getExecuteStatus());
|
||||
assertEquals("已完成巡检", result.getFeedback());
|
||||
assertNotNull(result.getCompletedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCompleteExecution_wrongStatus() {
|
||||
DispatchCommand cmd = buildCommand("executing");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
|
||||
DispatchExecution exec = buildExecution("received");
|
||||
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
|
||||
|
||||
assertThrows(BusinessException.class, () -> {
|
||||
commandService.completeExecution(1L, 1L, "张三", "反馈", null);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRejectExecution() {
|
||||
DispatchCommand cmd = buildCommand("issued");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
|
||||
DispatchExecution exec = buildExecution("pending");
|
||||
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
|
||||
when(executionMapper.updateById(any())).thenReturn(1);
|
||||
when(executionMapper.selectCount(any(LambdaQueryWrapper.class)))
|
||||
.thenReturn(1L) // total
|
||||
.thenReturn(1L); // all final
|
||||
|
||||
DispatchExecution result = commandService.rejectExecution(1L, 1L, "张三", "人手不足");
|
||||
|
||||
assertEquals("rejected", result.getExecuteStatus());
|
||||
assertEquals("人手不足", result.getRejectedReason());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRejectExecution_alreadyCompleted() {
|
||||
DispatchCommand cmd = buildCommand("executing");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
|
||||
DispatchExecution exec = buildExecution("completed");
|
||||
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
|
||||
|
||||
assertThrows(BusinessException.class, () -> {
|
||||
commandService.rejectExecution(1L, 1L, "张三", "原因");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCommandNotFound() {
|
||||
when(commandMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThrows(BusinessException.class, () -> {
|
||||
commandService.issueCommand(999L, 1L, "admin");
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Helper ====================
|
||||
|
||||
private DispatchCommand buildCommand(String status) {
|
||||
DispatchCommand cmd = new DispatchCommand();
|
||||
cmd.setId(1L);
|
||||
cmd.setCommandNo("CMD-20260614150000-0001");
|
||||
cmd.setCommandTitle("测试指令");
|
||||
cmd.setCommandContent("测试内容");
|
||||
cmd.setCommandType("normal");
|
||||
cmd.setStatus(status);
|
||||
cmd.setTargetIds("[1,2]");
|
||||
return cmd;
|
||||
}
|
||||
|
||||
private DispatchExecution buildExecution(String status) {
|
||||
DispatchExecution exec = new DispatchExecution();
|
||||
exec.setId(1L);
|
||||
exec.setCommandId(1L);
|
||||
exec.setUserId(1L);
|
||||
exec.setUserName("张三");
|
||||
exec.setExecuteStatus(status);
|
||||
return exec;
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.water.production.entity.DispatchTracking;
|
||||
import com.water.production.mapper.DispatchTrackingMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DispatchTrackingServiceTest {
|
||||
|
||||
@Mock
|
||||
private DispatchTrackingMapper trackingMapper;
|
||||
|
||||
@InjectMocks
|
||||
private DispatchTrackingService trackingService;
|
||||
|
||||
@Test
|
||||
void testLog() {
|
||||
when(trackingMapper.insert(any())).thenReturn(1);
|
||||
|
||||
trackingService.log(1L, null, "create", null, null, "draft", "创建指令");
|
||||
|
||||
ArgumentCaptor<DispatchTracking> captor = ArgumentCaptor.forClass(DispatchTracking.class);
|
||||
verify(trackingMapper).insert(captor.capture());
|
||||
|
||||
DispatchTracking saved = captor.getValue();
|
||||
assertEquals(1L, saved.getCommandId());
|
||||
assertNull(saved.getExecutionId());
|
||||
assertEquals("create", saved.getAction());
|
||||
assertEquals("draft", saved.getToStatus());
|
||||
assertEquals("创建指令", saved.getRemark());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogWithExecution() {
|
||||
when(trackingMapper.insert(any())).thenReturn(1);
|
||||
|
||||
trackingService.log(1L, 5L, "receive", 10L, "张三", "pending", "received", "接收确认");
|
||||
|
||||
ArgumentCaptor<DispatchTracking> captor = ArgumentCaptor.forClass(DispatchTracking.class);
|
||||
verify(trackingMapper).insert(captor.capture());
|
||||
|
||||
DispatchTracking saved = captor.getValue();
|
||||
assertEquals(5L, saved.getExecutionId());
|
||||
assertEquals(10L, saved.getOperatorId());
|
||||
assertEquals("张三", saved.getOperatorName());
|
||||
assertEquals("pending", saved.getFromStatus());
|
||||
assertEquals("received", saved.getToStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetTrackingLogs() {
|
||||
DispatchTracking t1 = new DispatchTracking();
|
||||
t1.setId(1L);
|
||||
t1.setCommandId(1L);
|
||||
t1.setAction("create");
|
||||
|
||||
DispatchTracking t2 = new DispatchTracking();
|
||||
t2.setId(2L);
|
||||
t2.setCommandId(1L);
|
||||
t2.setAction("issue");
|
||||
|
||||
when(trackingMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(t1, t2));
|
||||
|
||||
List<DispatchTracking> logs = trackingService.getTrackingLogs(1L);
|
||||
|
||||
assertEquals(2, logs.size());
|
||||
assertEquals("create", logs.get(0).getAction());
|
||||
assertEquals("issue", logs.get(1).getAction());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetExecutionTrackingLogs() {
|
||||
DispatchTracking t1 = new DispatchTracking();
|
||||
t1.setId(3L);
|
||||
t1.setExecutionId(5L);
|
||||
t1.setAction("receive");
|
||||
|
||||
when(trackingMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(t1));
|
||||
|
||||
List<DispatchTracking> logs = trackingService.getExecutionTrackingLogs(5L);
|
||||
|
||||
assertEquals(1, logs.size());
|
||||
assertEquals(5L, logs.get(0).getExecutionId());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user