4 Commits
Author SHA1 Message Date
bot_dev2 1870171e45 feat(wm-production): #67 报警规则引擎与报警管理中心
- DDL: V2__alert_engine.sql (prod_alert_rule, prod_alert_record, prod_alert_notification, prod_alert_rule_device)
- Entity: AlertRule(规则定义), AlertRecord(全生命周期增强), AlertNotification(通知记录)
- Mapper: AlertRuleMapper, AlertRecordMapper(自定义关联SQL), AlertNotificationMapper
- Service: AlertRuleService(规则CRUD + AND/OR组合条件评估引擎 + 阈值触发 + 多级别报警)
- Service: AlertCenterService(报警全生命周期: 确认/派单/处理/归档 + 统计看板)
- Controller: AlertController 23个RESTful端点 (/api/production/alert/*)
- 单元测试: AlertRuleServiceTest(12个) + AlertCenterServiceTest(5个) = 17个测试
2026-06-14 15:31:14 +08:00
bot_dev2 6c6db59ba9 feat(wm-production): #69 调度指令管理完整实现
- 实体: DispatchCommand/DispatchExecution/DispatchTracking
- Mapper: MyBatis-Plus + XML (含台账分页/详情/统计)
- Service: 完整状态机 (draft→issued→received→executing→completed/rejected)
- Controller: /api/production/dispatch-command (全生命周期API)
- SQL DDL: 三表+索引
- 前端: CommandList/CommandDetail/CommandCreate (Vue3+TS+Element Plus)
- 单元测试: DispatchCommandServiceTest + DispatchTrackingServiceTest
2026-06-14 15:30:52 +08:00
bot_dev2 21fa7cffd2 feat(wm-dispatch): #69 调度指令全生命周期管理
- 增强 DispatchCommand 实体,支持全生命周期状态流转
- 新增 CommandExecutionRecord(执行记录)、CommandTracking(过程追踪)实体
- 新增 DTO: CommandCreateRequest, CommandQueryRequest, ExecutionRequest, CommandStatVO
- 新增 Mapper: CommandExecutionRecordMapper, CommandTrackingMapper, 增强 DispatchCommandMapper(分页+统计)
- 新增 Service:
  - CommandLifecycleService: DRAFT→ISSUED→RECEIVED→EXECUTING→COMPLETED/REJECTED/CANCELLED 全流程
  - CommandLedgerService: 指令台账(多维度查询/统计/导出)
  - CommandTrackingService: 全过程追踪时间线
- 新增 DispatchCommandController: 18个 RESTful 端点 (/api/dispatch/command/*)
- DDL: V2__command_lifecycle.sql(3张表 + 索引)
- 单元测试: CommandLifecycleServiceTest(13个), CommandTrackingServiceTest(4个)
- 同步修复 DispatchBizService/DispatchController 适配新实体字段
2026-06-14 15:24:37 +08:00
bot_dev2 51c292f25b feat(wm-system): #74 系统管理(角色/用户/部门/日志/字典)
- 增强 SysRole: 5级角色(level 1-5), 数据权限(dataScope), 权限配置(permissions JSON)
- 增强 SysUser: 角色关联(roleId), 最后登录记录, 分页查询+多维度筛选
- 增强 SysDepartment: 树结构(ancestors), 子部门查询, 树形构建
- 增强 SysLog: 状态/错误信息字段, 分页+多条件筛选(类型/用户/模块/时间/状态)
- 新增 DataDictionary + DataDictionaryItem: 字典CRUD+字典项CRUD, 按编码查询
- 5个专用Controller: RoleController/UserController/DepartmentController/LogController/DictionaryController
- 28个RESTful API端点于 /api/system/*
- 5个专用Service接口+实现: RoleService/UserService/DepartmentService/LogService/DictionaryService
- DDL: V2__system_enhance.sql — 新增表+索引+初始数据
- 单元测试: 5个Service测试类, 覆盖核心业务逻辑
- DTO: 请求参数校验(jakarta.validation)
- 保持SysService向后兼容
2026-06-14 15:23:34 +08:00
92 changed files with 7097 additions and 130 deletions
+122
View File
@@ -0,0 +1,122 @@
-- =============================================
-- 智慧水务管理系统 - 报警规则引擎 + 报警管理中心 DDL
-- 版本: V2
-- =============================================
-- ==================== 报警规则定义 ====================
CREATE TABLE IF NOT EXISTS prod_alert_rule (
id BIGSERIAL PRIMARY KEY,
rule_name VARCHAR(100) NOT NULL,
rule_code VARCHAR(50) UNIQUE,
description TEXT,
device_type VARCHAR(30),
metric_key VARCHAR(50) NOT NULL,
alert_level VARCHAR(10) NOT NULL DEFAULT 'general', -- general/important/urgent
condition_expr TEXT NOT NULL, -- JSON: {"op":"AND","conditions":[{"metric":"pressure","operator":">","threshold":0.8},...]}
threshold_value DECIMAL(12,4), -- 简单阈值(向后兼容)
debounce_sec INT DEFAULT 300,
notify_channels VARCHAR(200), -- 逗号分隔: sms,wechat,app,email
notify_template VARCHAR(500), -- 通知模板
enabled SMALLINT DEFAULT 1,
priority INT DEFAULT 0, -- 规则优先级
effective_start TIME, -- 生效开始时间
effective_end TIME, -- 生效结束时间
created_by BIGINT,
updated_by BIGINT,
deleted SMALLINT DEFAULT 0,
created_time TIMESTAMP DEFAULT NOW(),
updated_time TIMESTAMP DEFAULT NOW()
);
COMMENT ON TABLE prod_alert_rule IS '报警规则定义表';
COMMENT ON COLUMN prod_alert_rule.alert_level IS '报警等级: general(一般)/important(重要)/urgent(紧急)';
COMMENT ON COLUMN prod_alert_rule.condition_expr IS '条件表达式JSON: 支持AND/OR组合条件';
-- ==================== 报警记录(全生命周期) ====================
CREATE TABLE IF NOT EXISTS prod_alert_record (
id BIGSERIAL PRIMARY KEY,
rule_id BIGINT REFERENCES prod_alert_rule(id),
rule_name VARCHAR(100),
device_id BIGINT,
device_sn VARCHAR(100),
device_name VARCHAR(200),
area VARCHAR(50),
metric_key VARCHAR(50) NOT NULL,
metric_value DECIMAL(12,4),
threshold_value VARCHAR(50),
alert_level VARCHAR(10) NOT NULL DEFAULT 'general',
title VARCHAR(200),
message TEXT,
-- 生命周期状态: 0=活跃 1=已确认 2=已派单 3=处理中 4=已处理 5=已归档
status INT DEFAULT 0,
confirmed_by BIGINT,
confirmed_time TIMESTAMP,
dispatch_time TIMESTAMP,
assignee_id BIGINT,
assignee_name VARCHAR(50),
handler_id BIGINT,
handler_name VARCHAR(50),
handle_result TEXT,
handle_time TIMESTAMP,
archive_time TIMESTAMP,
archive_reason VARCHAR(500),
resolved_at TIMESTAMP,
created_time TIMESTAMP DEFAULT NOW(),
updated_time TIMESTAMP DEFAULT NOW(),
deleted SMALLINT DEFAULT 0
);
COMMENT ON TABLE prod_alert_record IS '报警记录表(全生命周期)';
CREATE INDEX IF NOT EXISTS idx_alert_record_time ON prod_alert_record(created_time DESC);
CREATE INDEX IF NOT EXISTS idx_alert_record_device ON prod_alert_record(device_sn, created_time DESC);
CREATE INDEX IF NOT EXISTS idx_alert_record_status ON prod_alert_record(status);
CREATE INDEX IF NOT EXISTS idx_alert_record_level ON prod_alert_record(alert_level);
CREATE INDEX IF NOT EXISTS idx_alert_record_area ON prod_alert_record(area);
-- ==================== 报警通知记录 ====================
CREATE TABLE IF NOT EXISTS prod_alert_notification (
id BIGSERIAL PRIMARY KEY,
alert_record_id BIGINT REFERENCES prod_alert_record(id),
rule_id BIGINT,
channel VARCHAR(30) NOT NULL, -- sms/wechat/app/email
recipient VARCHAR(100) NOT NULL, -- 接收人标识
recipient_name VARCHAR(50),
title VARCHAR(200),
content TEXT,
status INT DEFAULT 0, -- 0=待发送 1=已发送 2=发送失败 3=已读
send_time TIMESTAMP,
read_time TIMESTAMP,
retry_count INT DEFAULT 0,
error_msg VARCHAR(500),
created_time TIMESTAMP DEFAULT NOW()
);
COMMENT ON TABLE prod_alert_notification IS '报警通知记录表';
CREATE INDEX IF NOT EXISTS idx_alert_notif_record ON prod_alert_notification(alert_record_id);
CREATE INDEX IF NOT EXISTS idx_alert_notif_status ON prod_alert_notification(status);
-- ==================== 报警规则-设备关联(可选) ====================
CREATE TABLE IF NOT EXISTS prod_alert_rule_device (
id BIGSERIAL PRIMARY KEY,
rule_id BIGINT REFERENCES prod_alert_rule(id),
device_id BIGINT,
device_sn VARCHAR(100),
area VARCHAR(50),
created_time TIMESTAMP DEFAULT NOW()
);
COMMENT ON TABLE prod_alert_rule_device IS '报警规则-设备/区域关联表';
-- ==================== 初始规则数据 ====================
INSERT INTO prod_alert_rule (rule_name, rule_code, metric_key, alert_level, condition_expr, threshold_value, debounce_sec, description, enabled) VALUES
('管网压力过高报警', 'RULE_PRESSURE_HIGH', 'pressure', 'urgent',
'{"op":"AND","conditions":[{"metric":"pressure","operator":">","threshold":0.8}]}',
0.8000, 300, '管网压力超过0.8MPa时触发紧急报警', 1),
('管网压力过低报警', 'RULE_PRESSURE_LOW', 'pressure', 'important',
'{"op":"OR","conditions":[{"metric":"pressure","operator":"<","threshold":0.2}]}',
0.2000, 300, '管网压力低于0.2MPa时触发重要报警', 1),
('水质浊度超标', 'RULE_TURBIDITY_HIGH', 'turbidity', 'urgent',
'{"op":"AND","conditions":[{"metric":"turbidity","operator":">","threshold":1.0}]}',
1.0000, 600, '水质浊度超过1.0NTU触发紧急报警', 1),
('余氯偏低报警', 'RULE_CHLORINE_LOW', 'residual_chlorine', 'general',
'{"op":"AND","conditions":[{"metric":"residual_chlorine","operator":"<","threshold":0.1}]}',
0.1000, 600, '余氯低于0.1mg/L触发一般报警', 1),
('流量异常波动', 'RULE_FLOW_ANOMALY', 'flow', 'important',
'{"op":"OR","conditions":[{"metric":"flow","operator":">","threshold":100},{"metric":"flow","operator":"<","threshold":5}]}',
NULL, 120, '流量异常偏高或偏低时触发报警', 1);
+68
View File
@@ -0,0 +1,68 @@
import request from './request'
const BASE = '/api/production/dispatch-command'
// 创建指令
export function createCommand(data: any) {
return request.post(BASE, data)
}
// 下发指令
export function issueCommand(id: number, issuedBy: number, operatorName?: string) {
return request.post(`${BASE}/${id}/issue`, null, {
params: { issuedBy, operatorName: operatorName || 'system' }
})
}
// 指令台账
export function listCommands(params: {
page?: number; size?: number; status?: string;
commandType?: string; keyword?: string; startDate?: string; endDate?: string
}) {
return request.get(BASE, { params })
}
// 指令详情
export function getCommandDetail(id: number) {
return request.get(`${BASE}/${id}`)
}
// 状态统计
export function getCommandStats() {
return request.get(`${BASE}/stats`)
}
// 接收确认
export function receiveCommand(id: number, userId: number, userName?: string) {
return request.post(`${BASE}/${id}/receive`, null, {
params: { userId, userName: userName || '' }
})
}
// 开始执行
export function startExecute(id: number, userId: number, userName?: string) {
return request.post(`${BASE}/${id}/start-execute`, null, {
params: { userId, userName: userName || '' }
})
}
// 完成执行
export function completeExecution(id: number, userId: number, data: {
userName?: string; feedback?: string; feedbackImages?: string
}) {
return request.post(`${BASE}/${id}/complete`, null, {
params: { userId, ...data }
})
}
// 驳回
export function rejectExecution(id: number, userId: number, reason: string, userName?: string) {
return request.post(`${BASE}/${id}/reject`, null, {
params: { userId, userName: userName || '', reason }
})
}
// 追踪日志
export function getTrackingLogs(id: number) {
return request.get(`${BASE}/${id}/tracking`)
}
+2
View File
@@ -11,6 +11,8 @@ const routes = [
{ path: 'system/role', name: 'role', component: () => import('@/views/system/role/RoleList.vue') },
{ path: 'system/menu', name: 'menu', component: () => import('@/views/system/menu/MenuList.vue') },
{ path: 'system/dept', name: 'dept', component: () => import('@/views/system/dept/DeptList.vue') },
{ path: 'dispatch-command', name: 'dispatchCommandList', component: () => import('@/views/dispatch-command/CommandList.vue') },
{ path: 'dispatch-command/:id', name: 'dispatchCommandDetail', component: () => import('@/views/dispatch-command/CommandDetail.vue') },
]
},
{ path: '/:pathMatch(.*)*', redirect: '/dashboard' }
@@ -0,0 +1,135 @@
<template>
<el-dialog
:model-value="visible"
@update:model-value="$emit('update:visible', $event)"
title="创建调度指令"
width="600"
:close-on-click-modal="false">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="指令标题" prop="commandTitle">
<el-input v-model="form.commandTitle" placeholder="请输入指令标题" maxlength="200" show-word-limit />
</el-form-item>
<el-form-item label="指令类型" prop="commandType">
<el-select v-model="form.commandType" placeholder="请选择">
<el-option label="常规" value="normal" />
<el-option label="应急" value="emergency" />
<el-option label="维护" value="maintenance" />
<el-option label="巡检" value="inspection" />
</el-select>
</el-form-item>
<el-form-item label="优先级" prop="priority">
<el-radio-group v-model="form.priority">
<el-radio value="low">低</el-radio>
<el-radio value="normal">普通</el-radio>
<el-radio value="high">高</el-radio>
<el-radio value="urgent">紧急</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="来源" prop="source">
<el-input v-model="form.source" placeholder="手动/系统/报警联动" />
</el-form-item>
<el-form-item label="指令内容" prop="commandContent">
<el-input v-model="form.commandContent" type="textarea" :rows="5"
placeholder="请输入指令详细内容" maxlength="2000" show-word-limit />
</el-form-item>
<el-form-item label="目标类型" prop="targetType">
<el-select v-model="form.targetType" placeholder="请选择">
<el-option label="指定人员" value="user" />
<el-option label="部门" value="dept" />
<el-option label="角色" value="role" />
</el-select>
</el-form-item>
<el-form-item label="目标人员" prop="targetIds">
<el-input v-model="form.targetIds" placeholder="目标ID列表,多个用逗号分隔,如: 1,2,3" />
<div class="form-tip">输入用户ID,多个用逗号分隔</div>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注信息(可选)" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="$emit('update:visible', false)">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">
创建指令
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { createCommand } from '@/api/dispatchCommand'
const props = defineProps<{ visible: boolean }>()
const emit = defineEmits<{
'update:visible': [value: boolean]
'created': []
}>()
const formRef = ref<FormInstance>()
const submitting = ref(false)
const defaultForm = () => ({
commandTitle: '',
commandType: 'normal',
priority: 'normal',
source: '手动',
commandContent: '',
targetType: 'user',
targetIds: '',
remark: ''
})
const form = reactive(defaultForm())
const rules: FormRules = {
commandTitle: [{ required: true, message: '请输入指令标题', trigger: 'blur' }],
commandType: [{ required: true, message: '请选择指令类型', trigger: 'change' }],
priority: [{ required: true, message: '请选择优先级', trigger: 'change' }],
commandContent: [{ required: true, message: '请输入指令内容', trigger: 'blur' }],
targetType: [{ required: true, message: '请选择目标类型', trigger: 'change' }],
targetIds: [{ required: true, message: '请输入目标ID', trigger: 'blur' }]
}
watch(() => props.visible, (val) => {
if (val) {
Object.assign(form, defaultForm())
formRef.value?.resetFields()
}
})
async function handleSubmit() {
if (!formRef.value) return
await formRef.value.validate()
submitting.value = true
try {
// 将 targetIds 转为 JSON 数组格式
const targetIdsArr = form.targetIds.split(',').map((s: string) => s.trim()).filter(Boolean)
await createCommand({
...form,
targetIds: JSON.stringify(targetIdsArr.map(Number))
})
ElMessage.success('指令创建成功')
emit('update:visible', false)
emit('created')
} catch (e: any) {
ElMessage.error(e.message || '创建失败')
} finally {
submitting.value = false
}
}
</script>
<style scoped>
.form-tip { font-size: 12px; color: #909399; margin-top: 4px; }
</style>
@@ -0,0 +1,304 @@
<template>
<div class="command-detail" v-loading="loading">
<!-- 返回按钮 -->
<el-page-header @back="router.back()" :title="'返回'" style="margin-bottom: 16px">
<template #content>
<span class="page-title">指令详情</span>
<el-tag :type="statusTag(detail.status)" style="margin-left: 12px">{{ statusLabel(detail.status) }}</el-tag>
</template>
</el-page-header>
<el-row :gutter="16">
<!-- 左侧:基本信息 + 状态流转图 -->
<el-col :span="14">
<el-card>
<template #header>
<span>{{ detail.command_title }}</span>
<el-tag size="small" style="margin-left: 8px">{{ detail.command_no }}</el-tag>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="指令编号">{{ detail.command_no }}</el-descriptions-item>
<el-descriptions-item label="类型">
<el-tag size="small">{{ typeLabel(detail.command_type) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="优先级">
<el-tag :type="priorityTag(detail.priority)" size="small">{{ priorityLabel(detail.priority) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="来源">{{ detail.source || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detail.created_at }}</el-descriptions-item>
<el-descriptions-item label="下发时间">{{ detail.issued_at || '-' }}</el-descriptions-item>
<el-descriptions-item label="完成时间">{{ detail.completed_at || '-' }}</el-descriptions-item>
<el-descriptions-item label="目标类型">{{ detail.target_type || '-' }}</el-descriptions-item>
</el-descriptions>
<div style="margin-top: 16px">
<h4>指令内容</h4>
<div class="content-block">{{ detail.command_content }}</div>
</div>
<!-- 状态流转图 -->
<div style="margin-top: 24px">
<h4>状态流转</h4>
<el-steps :active="statusStep(detail.status)" finish-status="success" align-center>
<el-step title="草稿" description="创建指令" />
<el-step title="已下发" description="下发给执行人" />
<el-step title="已接收" description="执行人确认" />
<el-step title="执行中" description="正在执行" />
<el-step title="完成/驳回" description="归档" />
</el-steps>
</div>
</el-card>
</el-col>
<!-- 右侧:执行记录列表 -->
<el-col :span="10">
<el-card>
<template #header>执行记录</template>
<el-timeline v-if="executions.length">
<el-timeline-item
v-for="exec in executions" :key="exec.id"
:type="executionTimelineType(exec.execute_status)"
:timestamp="exec.received_at || exec.created_at"
placement="top">
<div class="exec-card">
<div class="exec-header">
<span class="exec-user">{{ exec.user_name || `用户${exec.user_id}` }}</span>
<el-tag :type="executionStatusTag(exec.execute_status)" size="small">
{{ executionStatusLabel(exec.execute_status) }}
</el-tag>
</div>
<div v-if="exec.feedback" class="exec-feedback">
反馈: {{ exec.feedback }}
</div>
<div v-if="exec.rejected_reason" class="exec-reject">
驳回原因: {{ exec.rejected_reason }}
</div>
<div class="exec-actions" v-if="canOperate(exec)">
<el-button size="small" type="primary"
v-if="exec.execute_status === 'pending'"
@click="handleReceive(exec)">接收</el-button>
<el-button size="small" type="success"
v-if="exec.execute_status === 'received'"
@click="handleStartExecute(exec)">开始执行</el-button>
<el-button size="small" type="success"
v-if="exec.execute_status === 'executing'"
@click="showCompleteDialog(exec)">完成</el-button>
<el-button size="small" type="danger"
v-if="exec.execute_status !== 'completed' && exec.execute_status !== 'rejected'"
@click="handleReject(exec)">驳回</el-button>
</div>
</div>
</el-timeline-item>
</el-timeline>
<el-empty v-else description="暂无执行记录" />
</el-card>
</el-col>
</el-row>
<!-- 追踪日志 -->
<el-card style="margin-top: 16px">
<template #header>全过程追踪日志</template>
<el-timeline>
<el-timeline-item
v-for="log in trackingLogs" :key="log.id"
:timestamp="log.created_at" placement="top"
:type="trackingType(log.action)">
<div>
<el-tag size="small" :type="trackingType(log.action)">{{ trackingActionLabel(log.action) }}</el-tag>
<span style="margin-left: 8px">{{ log.operator_name || '' }}</span>
<span v-if="log.from_status" style="margin-left: 8px; color: #909399">
{{ log.from_status }} → {{ log.to_status }}
</span>
<div v-if="log.remark" style="color: #606266; margin-top: 4px">{{ log.remark }}</div>
</div>
</el-timeline-item>
</el-timeline>
<el-empty v-if="!trackingLogs.length" description="暂无追踪日志" />
</el-card>
<!-- 完成弹窗 -->
<el-dialog v-model="completeVisible" title="完成执行" width="500">
<el-form label-width="80px">
<el-form-item label="反馈说明">
<el-input v-model="completeForm.feedback" type="textarea" :rows="3" placeholder="请输入执行反馈" />
</el-form-item>
<el-form-item label="反馈图片">
<el-input v-model="completeForm.feedbackImages" placeholder="图片URL,多个用逗号分隔" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="completeVisible = false">取消</el-button>
<el-button type="primary" @click="handleComplete">确认完成</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getCommandDetail, receiveCommand, startExecute, completeExecution, rejectExecution } from '@/api/dispatchCommand'
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const detail = ref<any>({})
const executions = ref<any[]>([])
const trackingLogs = ref<any[]>([])
const completeVisible = ref(false)
const currentExec = ref<any>(null)
const completeForm = reactive({ feedback: '', feedbackImages: '' })
const commandId = Number(route.params.id)
// 模拟当前用户ID(实际应从登录态获取)
const currentUserId = 1
const currentUserName = 'admin'
const statusMap: Record<string, { label: string; type: string; step: number }> = {
draft: { label: '草稿', type: 'info', step: 0 },
issued: { label: '已下发', type: 'warning', step: 1 },
received: { label: '已接收', type: '', step: 2 },
executing: { label: '执行中', type: 'primary', step: 3 },
completed: { label: '已完成', type: 'success', step: 4 },
rejected: { label: '已驳回', type: 'danger', step: 4 }
}
const statusLabel = (s: string) => statusMap[s]?.label || s
const statusTag = (s: string) => (statusMap[s]?.type || 'info') as any
const statusStep = (s: string) => statusMap[s]?.step || 0
const typeMap: Record<string, string> = { normal: '常规', emergency: '应急', maintenance: '维护', inspection: '巡检' }
const typeLabel = (t: string) => typeMap[t] || t
const priorityMap: Record<string, { label: string; type: string }> = {
low: { label: '低', type: 'info' }, normal: { label: '普通', type: '' },
high: { label: '高', type: 'warning' }, urgent: { label: '紧急', type: 'danger' }
}
const priorityLabel = (p: string) => priorityMap[p]?.label || p
const priorityTag = (p: string) => (priorityMap[p]?.type || 'info') as any
const executionStatusLabel = (s: string) => {
const map: Record<string, string> = {
pending: '待接收', received: '已接收', executing: '执行中', completed: '已完成', rejected: '已驳回'
}
return map[s] || s
}
const executionStatusTag = (s: string) => {
const map: Record<string, string> = {
pending: 'info', received: '', executing: 'primary', completed: 'success', rejected: 'danger'
}
return (map[s] || 'info') as any
}
const executionTimelineType = (s: string) => {
const map: Record<string, string> = {
pending: 'info', received: 'primary', executing: 'primary', completed: 'success', rejected: 'danger'
}
return (map[s] || 'info') as any
}
const trackingActionLabel = (a: string) => {
const map: Record<string, string> = {
create: '创建', issue: '下发', receive: '接收', start_execute: '开始执行',
complete: '完成', reject: '驳回', cancel: '取消'
}
return map[a] || a
}
const trackingType = (a: string) => {
const map: Record<string, string> = {
create: 'info', issue: 'warning', receive: 'primary', start_execute: 'primary',
complete: 'success', reject: 'danger', cancel: 'danger'
}
return (map[a] || 'info') as any
}
function canOperate(exec: any) {
return exec.user_id === currentUserId || true // 简化:所有人可操作
}
async function fetchDetail() {
loading.value = true
try {
const res = await getCommandDetail(commandId)
detail.value = res.data || {}
executions.value = res.data?.executions || []
trackingLogs.value = res.data?.tracking_logs || res.data?.trackingLogs || []
} finally {
loading.value = false
}
}
async function handleReceive(exec: any) {
try {
await receiveCommand(commandId, exec.user_id, exec.user_name)
ElMessage.success('接收成功')
fetchDetail()
} catch (e: any) {
ElMessage.error(e.message || '操作失败')
}
}
async function handleStartExecute(exec: any) {
try {
await startExecute(commandId, exec.user_id, exec.user_name)
ElMessage.success('已开始执行')
fetchDetail()
} catch (e: any) {
ElMessage.error(e.message || '操作失败')
}
}
function showCompleteDialog(exec: any) {
currentExec.value = exec
completeForm.feedback = ''
completeForm.feedbackImages = ''
completeVisible.value = true
}
async function handleComplete() {
try {
await completeExecution(commandId, currentExec.value.user_id, {
userName: currentExec.value.user_name,
feedback: completeForm.feedback,
feedbackImages: completeForm.feedbackImages
})
ElMessage.success('执行完成')
completeVisible.value = false
fetchDetail()
} catch (e: any) {
ElMessage.error(e.message || '操作失败')
}
}
async function handleReject(exec: any) {
try {
const { value } = await ElMessageBox.prompt('请输入驳回原因', '驳回', {
confirmButtonText: '确认驳回',
cancelButtonText: '取消',
inputPattern: /.+/,
inputErrorMessage: '驳回原因不能为空'
})
await rejectExecution(commandId, exec.user_id, value, exec.user_name)
ElMessage.success('已驳回')
fetchDetail()
} catch { /* cancel */ }
}
onMounted(fetchDetail)
</script>
<style scoped>
.page-title { font-size: 16px; font-weight: 600; }
.content-block {
padding: 12px; background: #f5f7fa; border-radius: 4px;
white-space: pre-wrap; line-height: 1.6;
}
.exec-card { padding: 4px 0; }
.exec-header { display: flex; justify-content: space-between; align-items: center; }
.exec-user { font-weight: 600; }
.exec-feedback { margin-top: 6px; color: #606266; font-size: 13px; }
.exec-reject { margin-top: 6px; color: #f56c6c; font-size: 13px; }
.exec-actions { margin-top: 8px; }
</style>
@@ -0,0 +1,193 @@
<template>
<div class="command-list">
<el-card shadow="never" class="filter-card">
<el-form :inline="true" :model="filterForm">
<el-form-item label="关键词">
<el-input v-model="filterForm.keyword" placeholder="编号/标题" clearable @clear="handleSearch" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="filterForm.status" placeholder="全部" clearable @change="handleSearch">
<el-option label="草稿" value="draft" />
<el-option label="已下发" value="issued" />
<el-option label="已接收" value="received" />
<el-option label="执行中" value="executing" />
<el-option label="已完成" value="completed" />
<el-option label="已驳回" value="rejected" />
</el-select>
</el-form-item>
<el-form-item label="类型">
<el-select v-model="filterForm.commandType" placeholder="全部" clearable @change="handleSearch">
<el-option label="常规" value="normal" />
<el-option label="应急" value="emergency" />
<el-option label="维护" value="maintenance" />
<el-option label="巡检" value="inspection" />
</el-select>
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker v-model="dateRange" type="daterange" range-separator="至"
start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD"
@change="handleSearch" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch"><el-icon><Search /></el-icon> 查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-row :gutter="12" style="margin-top: 12px">
<el-col :span="4" v-for="stat in stats" :key="stat.status">
<el-card shadow="hover" class="stat-card" @click="filterByStatus(stat.status)">
<div class="stat-value">{{ stat.count }}</div>
<div class="stat-label">{{ statusLabel(stat.status) }}</div>
</el-card>
</el-col>
</el-row>
<div style="margin-top: 16px; display: flex; justify-content: space-between; align-items: center">
<el-button type="primary" @click="showCreateDialog = true"><el-icon><Plus /></el-icon> 创建指令</el-button>
<el-button @click="handleBatchIssue" :disabled="!selectedIds.length">批量下发</el-button>
</div>
<el-table :data="tableData" border style="margin-top: 10px"
@selection-change="handleSelectionChange" v-loading="loading">
<el-table-column type="selection" width="50" />
<el-table-column prop="command_no" label="指令编号" width="220" />
<el-table-column prop="command_title" label="标题" min-width="200" show-overflow-tooltip />
<el-table-column prop="command_type" label="类型" width="80">
<template #default="{ row }">
<el-tag :type="typeTag(row.command_type)" size="small">{{ typeLabel(row.command_type) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="priority" label="优先级" width="80">
<template #default="{ row }">
<el-tag :type="priorityTag(row.priority)" size="small">{{ priorityLabel(row.priority) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="90">
<template #default="{ row }">
<el-tag :type="statusTag(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="执行进度" width="120">
<template #default="{ row }">
<span>{{ row.completed_count || 0 }}/{{ row.total_executions || 0 }}</span>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="170" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="viewDetail(row)">详情</el-button>
<el-button link type="success" v-if="row.status === 'draft'" @click="handleIssue(row)">下发</el-button>
<el-button link type="danger" v-if="row.status === 'draft'" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination style="margin-top: 16px; justify-content: flex-end"
v-model:current-page="pagination.page" v-model:page-size="pagination.size"
:total="pagination.total" :page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next" @change="fetchData" />
<CommandCreate v-model:visible="showCreateDialog" @created="handleCreated" />
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Plus } from '@element-plus/icons-vue'
import { listCommands, issueCommand, getCommandStats } from '@/api/dispatchCommand'
import CommandCreate from './CommandCreate.vue'
const router = useRouter()
const loading = ref(false)
const tableData = ref<any[]>([])
const stats = ref<any[]>([])
const selectedIds = ref<number[]>([])
const showCreateDialog = ref(false)
const dateRange = ref<[string, string] | null>(null)
const filterForm = reactive({ keyword: '', status: '', commandType: '' })
const pagination = reactive({ page: 1, size: 10, total: 0 })
const statusMap: Record<string, { label: string; type: string }> = {
draft: { label: '草稿', type: 'info' },
issued: { label: '已下发', type: 'warning' },
received: { label: '已接收', type: '' },
executing: { label: '执行中', type: 'primary' },
completed: { label: '已完成', type: 'success' },
rejected: { label: '已驳回', type: 'danger' }
}
const typeMap: Record<string, string> = { normal: '常规', emergency: '应急', maintenance: '维护', inspection: '巡检' }
const priorityMap: Record<string, { label: string; type: string }> = {
low: { label: '低', type: 'info' }, normal: { label: '普通', type: '' },
high: { label: '高', type: 'warning' }, urgent: { label: '紧急', type: 'danger' }
}
function statusLabel(s: string) { return statusMap[s]?.label || s }
function statusTag(s: string) { return (statusMap[s]?.type || 'info') as any }
function typeLabel(t: string) { return typeMap[t] || t }
function typeTag(t: string) { return t === 'emergency' ? 'danger' : t === 'maintenance' ? 'warning' : '' }
function priorityLabel(p: string) { return priorityMap[p]?.label || p }
function priorityTag(p: string) { return (priorityMap[p]?.type || 'info') as any }
async function fetchData() {
loading.value = true
try {
const res = await listCommands({
page: pagination.page, size: pagination.size,
status: filterForm.status || undefined,
commandType: filterForm.commandType || undefined,
keyword: filterForm.keyword || undefined,
startDate: dateRange.value?.[0], endDate: dateRange.value?.[1]
})
tableData.value = res.data?.records || []
pagination.total = res.data?.total || 0
} finally { loading.value = false }
}
async function fetchStats() {
try { const res = await getCommandStats(); stats.value = res.data || [] } catch { /* ignore */ }
}
function handleSearch() { pagination.page = 1; fetchData() }
function handleReset() {
filterForm.keyword = ''; filterForm.status = ''; filterForm.commandType = ''; dateRange.value = null; handleSearch()
}
function filterByStatus(status: string) { filterForm.status = status; handleSearch() }
function handleSelectionChange(rows: any[]) { selectedIds.value = rows.map((r: any) => r.id) }
function viewDetail(row: any) { router.push({ path: `/dispatch-command/${row.id}` }) }
async function handleIssue(row: any) {
try {
await ElMessageBox.confirm(`确认下发指令 "${row.command_title}" ?`, '下发确认')
await issueCommand(row.id, 1, 'admin'); ElMessage.success('指令已下发'); fetchData(); fetchStats()
} catch { /* cancel */ }
}
async function handleBatchIssue() {
try {
await ElMessageBox.confirm(`确认批量下发 ${selectedIds.value.length} 条指令?`, '批量下发')
for (const id of selectedIds.value) { await issueCommand(id, 1, 'admin') }
ElMessage.success('批量下发完成'); fetchData(); fetchStats()
} catch { /* cancel */ }
}
function handleDelete(row: any) {
ElMessageBox.confirm(`确认删除指令 "${row.command_title}" ?`, '删除确认', { type: 'warning' })
.then(() => { ElMessage.info('删除功能待实现(逻辑删除)') }).catch(() => { /* cancel */ })
}
function handleCreated() { showCreateDialog.value = false; fetchData(); fetchStats() }
onMounted(() => { fetchData(); fetchStats() })
</script>
<style scoped>
.filter-card :deep(.el-form-item) { margin-bottom: 0; }
.stat-card { cursor: pointer; text-align: center; }
.stat-value { font-size: 28px; font-weight: bold; color: #409eff; }
.stat-label { font-size: 13px; color: #909399; margin-top: 4px; }
</style>
@@ -0,0 +1,159 @@
package com.water.dispatch.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.water.common.core.result.R;
import com.water.dispatch.entity.CommandExecutionRecord;
import com.water.dispatch.entity.CommandTracking;
import com.water.dispatch.entity.DispatchCommand;
import com.water.dispatch.entity.dto.*;
import com.water.dispatch.service.CommandLedgerService;
import com.water.dispatch.service.CommandLifecycleService;
import com.water.dispatch.service.CommandTrackingService;
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/dispatch/command")
@RequiredArgsConstructor
public class DispatchCommandController {
private final CommandLifecycleService lifecycleService;
private final CommandLedgerService ledgerService;
private final CommandTrackingService trackingService;
// ==================== 生命周期操作 ====================
@Operation(summary = "1. 创建指令")
@PostMapping
public R<DispatchCommand> create(@RequestBody CommandCreateRequest req) {
return R.ok(lifecycleService.create(req));
}
@Operation(summary = "2. 下发指令")
@PostMapping("/{id}/issue")
public R<DispatchCommand> issue(@PathVariable Long id) {
return R.ok(lifecycleService.issue(id));
}
@Operation(summary = "3. 接收确认")
@PostMapping("/{id}/receive")
public R<DispatchCommand> receive(@PathVariable Long id,
@RequestParam(required = false) Long receiverId,
@RequestParam(required = false) String receiverName) {
return R.ok(lifecycleService.receive(id, receiverId, receiverName));
}
@Operation(summary = "4. 开始执行")
@PostMapping("/{id}/start")
public R<DispatchCommand> startExecution(@PathVariable Long id,
@RequestParam(required = false) Long executorId,
@RequestParam(required = false) String executorName) {
return R.ok(lifecycleService.startExecution(id, executorId, executorName));
}
@Operation(summary = "5. 执行进度反馈")
@PostMapping("/{id}/progress")
public R<DispatchCommand> reportProgress(@PathVariable Long id,
@RequestBody ExecutionRequest req) {
return R.ok(lifecycleService.reportProgress(id, req.getExecutorId(), req.getExecutorName(),
req.getDescription(), req.getProgress() != null ? req.getProgress() : 0));
}
@Operation(summary = "6. 完成指令")
@PostMapping("/{id}/complete")
public R<DispatchCommand> complete(@PathVariable Long id,
@RequestBody ExecutionRequest req) {
return R.ok(lifecycleService.complete(id, req.getExecutorId(), req.getExecutorName(), req.getExecuteResult()));
}
@Operation(summary = "7. 驳回指令")
@PostMapping("/{id}/reject")
public R<DispatchCommand> reject(@PathVariable Long id,
@RequestBody ExecutionRequest req) {
return R.ok(lifecycleService.reject(id, req.getExecutorId(), req.getExecutorName(), req.getRejectReason()));
}
@Operation(summary = "8. 取消指令")
@PostMapping("/{id}/cancel")
public R<DispatchCommand> cancel(@PathVariable Long id,
@RequestParam(required = false) Long operatorId,
@RequestParam(required = false) String operatorName,
@RequestParam(required = false) String reason) {
return R.ok(lifecycleService.cancel(id, operatorId, operatorName, reason));
}
// ==================== 查询 ====================
@Operation(summary = "9. 获取指令详情(按ID)")
@GetMapping("/{id}")
public R<DispatchCommand> getById(@PathVariable Long id) {
return R.ok(lifecycleService.getById(id));
}
@Operation(summary = "10. 根据指令编号获取详情")
@GetMapping("/no/{commandNo}")
public R<DispatchCommand> getByCommandNo(@PathVariable String commandNo) {
return R.ok(lifecycleService.getByCommandNo(commandNo));
}
// ==================== 台账 ====================
@Operation(summary = "11. 指令台账分页查询")
@GetMapping("/page")
public R<IPage<DispatchCommand>> queryPage(CommandQueryRequest req) {
return R.ok(ledgerService.queryPage(req));
}
@Operation(summary = "12. 指令台账列表查询")
@GetMapping("/list")
public R<List<DispatchCommand>> queryList(CommandQueryRequest req) {
return R.ok(ledgerService.queryList(req));
}
@Operation(summary = "13. 指令统计概览")
@GetMapping("/statistics")
public R<CommandStatVO> statistics() {
return R.ok(ledgerService.statistics());
}
@Operation(summary = "14. 指令数据导出")
@GetMapping("/export")
public R<List<DispatchCommand>> exportData(CommandQueryRequest req) {
return R.ok(ledgerService.exportData(req));
}
// ==================== 追踪 ====================
@Operation(summary = "15. 获取指令追踪时间线")
@GetMapping("/{id}/timeline")
public R<List<CommandTracking>> getTimeline(@PathVariable Long id) {
return R.ok(trackingService.getTimeline(id));
}
@Operation(summary = "16. 获取执行记录")
@GetMapping("/{id}/executions")
public R<List<CommandExecutionRecord>> getExecutions(@PathVariable Long id) {
return R.ok(trackingService.getExecutionRecords(id));
}
@Operation(summary = "17. 获取完整时间线(追踪+执行记录合并)")
@GetMapping("/{id}/full-timeline")
public R<List<Map<String, Object>>> getFullTimeline(@PathVariable Long id) {
return R.ok(trackingService.getFullTimeline(id));
}
@Operation(summary = "18. 按指令编号获取追踪时间线")
@GetMapping("/no/{commandNo}/timeline")
public R<List<CommandTracking>> getTimelineByCommandNo(@PathVariable String commandNo) {
return R.ok(trackingService.getTimelineByCommandNo(commandNo));
}
}
@@ -1,4 +1,5 @@
package com.water.dispatch.controller;
import com.water.common.core.result.R;
import com.water.dispatch.entity.*;
import com.water.dispatch.service.DispatchBizService;
@@ -6,19 +7,71 @@ 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("/dispatch") @RequiredArgsConstructor
@Tag(name = "调度工作台")
@RestController
@RequestMapping("/dispatch")
@RequiredArgsConstructor
public class DispatchController {
private final DispatchBizService svc;
@GetMapping("/duty/today") public R<List<DutySchedule>> todayDuty() { return R.ok(svc.getTodayDuty()); }
@PostMapping("/command") public R<Map<String,Object>> createCommand(@RequestBody Map<String,Object> req) { return R.ok(svc.createCommand(req)); }
@PostMapping("/command/{cmdNo}/issue") public R<Map<String,Object>> issue(@PathVariable String cmdNo) { return R.ok(svc.issueCommand(cmdNo)); }
@GetMapping("/command/list") public R<List<DispatchCommand>> listCommands(@RequestParam(required=false) Integer status) { return R.ok(svc.listCommands(status)); }
@PostMapping("/work-order") public R<Map<String,Object>> createWO(@RequestBody Map<String,Object> req) { return R.ok(svc.createWorkOrder(req)); }
@PutMapping("/work-order/{id}/status") public R<Map<String,Object>> updateWOStatus(@PathVariable Long id, @RequestParam int status) { return R.ok(svc.updateWorkOrderStatus(id, status)); }
@PostMapping("/duty-log") public R<String> addLog(@RequestParam Long scheduleId, @RequestParam Long userId, @RequestParam String type, @RequestParam String content) { svc.addDutyLog(scheduleId,userId,type,content); return R.ok("OK"); }
@GetMapping("/duty-log/{scheduleId}") public R<List<DutyLog>> getLogs(@PathVariable Long scheduleId) { return R.ok(svc.getDutyLogs(scheduleId)); }
@GetMapping("/strategy") public R<List<DispatchStrategy>> listStrategies(@RequestParam(required=false) String type) { return R.ok(svc.listStrategies(type)); }
@GetMapping("/emergency-plan") public R<List<EmergencyPlan>> listPlans(@RequestParam(required=false) String type) { return R.ok(svc.listPlans(type)); }
@PostMapping("/emergency/simulate") public R<Map<String,Object>> simulate(@RequestParam String type, @RequestParam double lng, @RequestParam double lat) { return R.ok(svc.simulateEmergency(type,lng,lat)); }
@GetMapping("/duty/today")
public R<List<DutySchedule>> todayDuty() {
return R.ok(svc.getTodayDuty());
}
@PostMapping("/command")
public R<Map<String, Object>> createCommand(@RequestBody Map<String, Object> req) {
return R.ok(svc.createCommand(req));
}
@PostMapping("/command/{cmdNo}/issue")
public R<Map<String, Object>> issue(@PathVariable String cmdNo) {
return R.ok(svc.issueCommand(cmdNo));
}
@GetMapping("/command/list")
public R<List<DispatchCommand>> listCommands(@RequestParam(required = false) String status) {
return R.ok(svc.listCommands(status));
}
@PostMapping("/work-order")
public R<Map<String, Object>> createWO(@RequestBody Map<String, Object> req) {
return R.ok(svc.createWorkOrder(req));
}
@PutMapping("/work-order/{id}/status")
public R<Map<String, Object>> updateWOStatus(@PathVariable Long id, @RequestParam int status) {
return R.ok(svc.updateWorkOrderStatus(id, status));
}
@PostMapping("/duty-log")
public R<String> addLog(@RequestParam Long scheduleId, @RequestParam Long userId,
@RequestParam String type, @RequestParam String content) {
svc.addDutyLog(scheduleId, userId, type, content);
return R.ok("OK");
}
@GetMapping("/duty-log/{scheduleId}")
public R<List<DutyLog>> getLogs(@PathVariable Long scheduleId) {
return R.ok(svc.getDutyLogs(scheduleId));
}
@GetMapping("/strategy")
public R<List<DispatchStrategy>> listStrategies(@RequestParam(required = false) String type) {
return R.ok(svc.listStrategies(type));
}
@GetMapping("/emergency-plan")
public R<List<EmergencyPlan>> listPlans(@RequestParam(required = false) String type) {
return R.ok(svc.listPlans(type));
}
@PostMapping("/emergency/simulate")
public R<Map<String, Object>> simulate(@RequestParam String type,
@RequestParam double lng, @RequestParam double lat) {
return R.ok(svc.simulateEmergency(type, lng, lat));
}
}
@@ -0,0 +1,43 @@
package com.water.dispatch.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 指令执行记录
*/
@Data
@TableName("disp_command_execution_record")
public class CommandExecutionRecord {
@TableId(type = IdType.AUTO)
private Long id;
/** 关联指令ID */
private Long commandId;
/** 关联指令编号 */
private String commandNo;
/** 执行人ID */
private Long executorId;
/** 执行人姓名 */
private String executorName;
/** 动作: RECEIVE / START / PROGRESS / COMPLETE / REJECT */
private String action;
/** 描述 */
private String description;
/** 附件(JSON array) */
private String attachments;
/** 进度 0-100 */
private Integer progress;
private LocalDateTime createdAt;
}
@@ -0,0 +1,43 @@
package com.water.dispatch.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 指令过程追踪(时间线)
*/
@Data
@TableName("disp_command_tracking")
public class CommandTracking {
@TableId(type = IdType.AUTO)
private Long id;
/** 关联指令ID */
private Long commandId;
/** 关联指令编号 */
private String commandNo;
/** 阶段: CREATED / ISSUED / RECEIVED / EXECUTING / COMPLETED / REJECTED / CANCELLED */
private String stage;
/** 操作人ID */
private Long operatorId;
/** 操作人姓名 */
private String operatorName;
/** 动作描述 */
private String actionDesc;
/** 来源状态 */
private String fromStatus;
/** 目标状态 */
private String toStatus;
private LocalDateTime createdAt;
}
@@ -6,7 +6,7 @@ import lombok.Data;
import java.time.LocalDateTime;
/**
* 调度指令 - 全生命周期: 下发→接收→执行→完成→驳回
* 调度指令 - 全生命周期: DRAFT→ISSUED→RECEIVED→EXECUTING→COMPLETED/REJECTED/CANCELLED
*/
@Data
@TableName("disp_dispatch_command")
@@ -30,6 +30,9 @@ public class DispatchCommand {
/** 优先级: LOW-低 MEDIUM-中 HIGH-高 URGENT-紧急 */
private String priority;
/** 状态: DRAFT/ISSUED/RECEIVED/EXECUTING/COMPLETED/REJECTED/CANCELLED */
private String status;
/** 下发人ID */
private Long issuerId;
@@ -45,8 +48,8 @@ public class DispatchCommand {
/** 关联设施ID */
private Long facilityId;
/** 状态: ISSUED-下发 RECEIVED-接收 EXECUTING-执行 COMPLETED-完成 REJECTED-驳回 CANCELLED-取消 */
private String status;
/** 截止时间 */
private LocalDateTime deadline;
/** 下发时间 */
private LocalDateTime issuedAt;
@@ -60,19 +63,23 @@ public class DispatchCommand {
/** 完成时间 */
private LocalDateTime completedAt;
/** 驳回时间 */
private LocalDateTime rejectedAt;
/** 驳回原因 */
private String rejectReason;
/** 执行结果 */
private String executeResult;
/** 截止时间 */
private LocalDateTime deadline;
/** 备注 */
private String remark;
@TableLogic
private Integer deleted;
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}
@@ -0,0 +1,22 @@
package com.water.dispatch.entity.dto;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 指令创建请求
*/
@Data
public class CommandCreateRequest {
private String title;
private String content;
private String commandType; // NORMAL / EMERGENCY / MAINTENANCE
private String priority; // LOW / MEDIUM / HIGH / URGENT
private Long issuerId;
private String issuerName;
private Long receiverId;
private String receiverName;
private Long facilityId;
private LocalDateTime deadline;
private String remark;
}
@@ -0,0 +1,22 @@
package com.water.dispatch.entity.dto;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 指令台账查询请求
*/
@Data
public class CommandQueryRequest {
private String commandNo;
private String title;
private String commandType;
private String priority;
private String status;
private Long issuerId;
private Long receiverId;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Integer pageNum = 1;
private Integer pageSize = 20;
}
@@ -0,0 +1,18 @@
package com.water.dispatch.entity.dto;
import lombok.Data;
import java.util.Map;
/**
* 指令统计VO
*/
@Data
public class CommandStatVO {
private long total;
private Map<String, Long> byStatus;
private Map<String, Long> byType;
private Map<String, Long> byPriority;
private long overdueCount;
private long todayCreated;
private long todayCompleted;
}
@@ -0,0 +1,19 @@
package com.water.dispatch.entity.dto;
import lombok.Data;
/**
* 执行反馈请求
*/
@Data
public class ExecutionRequest {
private Long commandId;
private Long executorId;
private String executorName;
private String action; // RECEIVE / START / PROGRESS / COMPLETE / REJECT
private String description;
private String attachments; // JSON array
private Integer progress;
private String rejectReason; // 驳回时填写
private String executeResult; // 完成时填写
}
@@ -0,0 +1,9 @@
package com.water.dispatch.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.dispatch.entity.CommandExecutionRecord;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CommandExecutionRecordMapper extends BaseMapper<CommandExecutionRecord> {
}
@@ -0,0 +1,9 @@
package com.water.dispatch.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.dispatch.entity.CommandTracking;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CommandTrackingMapper extends BaseMapper<CommandTracking> {
}
@@ -1,5 +1,98 @@
package com.water.dispatch.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.dispatch.entity.DispatchCommand;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface DispatchCommandMapper extends BaseMapper<DispatchCommand> {}
import org.apache.ibatis.annotations.*;
import java.util.List;
import java.util.Map;
@Mapper
public interface DispatchCommandMapper extends BaseMapper<DispatchCommand> {
/**
* 分页 + 多维度查询指令台账
*/
@SelectProvider(type = CommandSqlProvider.class, method = "buildLedgerQuery")
IPage<DispatchCommand> selectLedgerPage(
Page<?> page,
@Param("commandNo") String commandNo,
@Param("title") String title,
@Param("commandType") String commandType,
@Param("priority") String priority,
@Param("status") String status,
@Param("issuerId") Long issuerId,
@Param("receiverId") Long receiverId,
@Param("startTime") String startTime,
@Param("endTime") String endTime);
/**
* 按状态统计
*/
@Select("SELECT status, COUNT(*) as cnt FROM disp_dispatch_command WHERE deleted=0 GROUP BY status")
List<Map<String, Object>> countByStatus();
/**
* 按类型统计
*/
@Select("SELECT command_type, COUNT(*) as cnt FROM disp_dispatch_command WHERE deleted=0 GROUP BY command_type")
List<Map<String, Object>> countByType();
/**
* 按优先级统计
*/
@Select("SELECT priority, COUNT(*) as cnt FROM disp_dispatch_command WHERE deleted=0 GROUP BY priority")
List<Map<String, Object>> countByPriority();
/**
* 逾期指令数
*/
@Select("SELECT COUNT(*) FROM disp_dispatch_command WHERE deleted=0 AND status NOT IN ('COMPLETED','CANCELLED','REJECTED') AND deadline < NOW()")
long countOverdue();
class CommandSqlProvider {
public String buildLedgerQuery(
@Param("commandNo") String commandNo,
@Param("title") String title,
@Param("commandType") String commandType,
@Param("priority") String priority,
@Param("status") String status,
@Param("issuerId") Long issuerId,
@Param("receiverId") Long receiverId,
@Param("startTime") String startTime,
@Param("endTime") String endTime) {
StringBuilder sb = new StringBuilder("SELECT * FROM disp_dispatch_command WHERE deleted=0");
if (commandNo != null && !commandNo.isEmpty()) {
sb.append(" AND command_no LIKE '%").append(commandNo).append("%'");
}
if (title != null && !title.isEmpty()) {
sb.append(" AND title LIKE '%").append(title).append("%'");
}
if (commandType != null && !commandType.isEmpty()) {
sb.append(" AND command_type = '").append(commandType).append("'");
}
if (priority != null && !priority.isEmpty()) {
sb.append(" AND priority = '").append(priority).append("'");
}
if (status != null && !status.isEmpty()) {
sb.append(" AND status = '").append(status).append("'");
}
if (issuerId != null) {
sb.append(" AND issuer_id = ").append(issuerId);
}
if (receiverId != null) {
sb.append(" AND receiver_id = ").append(receiverId);
}
if (startTime != null && !startTime.isEmpty()) {
sb.append(" AND created_at >= '").append(startTime).append("'");
}
if (endTime != null && !endTime.isEmpty()) {
sb.append(" AND created_at <= '").append(endTime).append("'");
}
sb.append(" ORDER BY created_at DESC");
return sb.toString();
}
}
}
@@ -0,0 +1,125 @@
package com.water.dispatch.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.dispatch.entity.DispatchCommand;
import com.water.dispatch.entity.dto.CommandQueryRequest;
import com.water.dispatch.entity.dto.CommandStatVO;
import com.water.dispatch.mapper.DispatchCommandMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 指令台账服务 - 多维度查询/统计/导出
*/
@Service
@RequiredArgsConstructor
public class CommandLedgerService {
private final DispatchCommandMapper commandMapper;
/**
* 分页多维度查询
*/
public IPage<DispatchCommand> queryPage(CommandQueryRequest req) {
Page<DispatchCommand> page = new Page<>(req.getPageNum(), req.getPageSize());
String startTime = req.getStartTime() != null ? req.getStartTime().toString() : null;
String endTime = req.getEndTime() != null ? req.getEndTime().toString() : null;
return commandMapper.selectLedgerPage(page,
req.getCommandNo(), req.getTitle(), req.getCommandType(),
req.getPriority(), req.getStatus(),
req.getIssuerId(), req.getReceiverId(),
startTime, endTime);
}
/**
* 列表查询(不分页)
*/
public List<DispatchCommand> queryList(CommandQueryRequest req) {
LambdaQueryWrapper<DispatchCommand> wrapper = new LambdaQueryWrapper<>();
if (req.getCommandNo() != null && !req.getCommandNo().isEmpty()) {
wrapper.like(DispatchCommand::getCommandNo, req.getCommandNo());
}
if (req.getTitle() != null && !req.getTitle().isEmpty()) {
wrapper.like(DispatchCommand::getTitle, req.getTitle());
}
if (req.getCommandType() != null && !req.getCommandType().isEmpty()) {
wrapper.eq(DispatchCommand::getCommandType, req.getCommandType());
}
if (req.getPriority() != null && !req.getPriority().isEmpty()) {
wrapper.eq(DispatchCommand::getPriority, req.getPriority());
}
if (req.getStatus() != null && !req.getStatus().isEmpty()) {
wrapper.eq(DispatchCommand::getStatus, req.getStatus());
}
if (req.getIssuerId() != null) {
wrapper.eq(DispatchCommand::getIssuerId, req.getIssuerId());
}
if (req.getReceiverId() != null) {
wrapper.eq(DispatchCommand::getReceiverId, req.getReceiverId());
}
if (req.getStartTime() != null) {
wrapper.ge(DispatchCommand::getCreatedAt, req.getStartTime());
}
if (req.getEndTime() != null) {
wrapper.le(DispatchCommand::getCreatedAt, req.getEndTime());
}
wrapper.orderByDesc(DispatchCommand::getCreatedAt);
return commandMapper.selectList(wrapper);
}
/**
* 统计概览
*/
public CommandStatVO statistics() {
CommandStatVO vo = new CommandStatVO();
vo.setTotal(commandMapper.selectCount(new LambdaQueryWrapper<>()));
Map<String, Long> byStatus = new HashMap<>();
commandMapper.countByStatus().forEach(m ->
byStatus.put(String.valueOf(m.get("status")), ((Number) m.get("cnt")).longValue()));
vo.setByStatus(byStatus);
Map<String, Long> byType = new HashMap<>();
commandMapper.countByType().forEach(m ->
byType.put(String.valueOf(m.get("command_type")), ((Number) m.get("cnt")).longValue()));
vo.setByType(byType);
Map<String, Long> byPriority = new HashMap<>();
commandMapper.countByPriority().forEach(m ->
byPriority.put(String.valueOf(m.get("priority")), ((Number) m.get("cnt")).longValue()));
vo.setByPriority(byPriority);
vo.setOverdueCount(commandMapper.countOverdue());
LocalDateTime todayStart = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);
LocalDateTime todayEnd = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);
vo.setTodayCreated(commandMapper.selectCount(
new LambdaQueryWrapper<DispatchCommand>()
.ge(DispatchCommand::getCreatedAt, todayStart)
.le(DispatchCommand::getCreatedAt, todayEnd)));
vo.setTodayCompleted(commandMapper.selectCount(
new LambdaQueryWrapper<DispatchCommand>()
.eq(DispatchCommand::getStatus, "COMPLETED")
.ge(DispatchCommand::getCompletedAt, todayStart)
.le(DispatchCommand::getCompletedAt, todayEnd)));
return vo;
}
/**
* 导出用数据(全量列表)
*/
public List<DispatchCommand> exportData(CommandQueryRequest req) {
return queryList(req);
}
}
@@ -0,0 +1,250 @@
package com.water.dispatch.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.common.core.exception.BusinessException;
import com.water.dispatch.entity.CommandExecutionRecord;
import com.water.dispatch.entity.CommandTracking;
import com.water.dispatch.entity.DispatchCommand;
import com.water.dispatch.entity.dto.CommandCreateRequest;
import com.water.dispatch.entity.dto.ExecutionRequest;
import com.water.dispatch.mapper.CommandExecutionRecordMapper;
import com.water.dispatch.mapper.CommandTrackingMapper;
import com.water.dispatch.mapper.DispatchCommandMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 调度指令全生命周期服务
* DRAFT → ISSUED → RECEIVED → EXECUTING → COMPLETED / REJECTED / CANCELLED
*/
@Service
@RequiredArgsConstructor
public class CommandLifecycleService {
private final DispatchCommandMapper commandMapper;
private final CommandExecutionRecordMapper executionMapper;
private final CommandTrackingMapper trackingMapper;
/**
* 创建指令(DRAFT)
*/
@Transactional
public DispatchCommand create(CommandCreateRequest req) {
DispatchCommand cmd = new DispatchCommand();
cmd.setCommandNo(generateCommandNo());
cmd.setTitle(req.getTitle());
cmd.setContent(req.getContent());
cmd.setCommandType(req.getCommandType() != null ? req.getCommandType() : "NORMAL");
cmd.setPriority(req.getPriority() != null ? req.getPriority() : "MEDIUM");
cmd.setStatus("DRAFT");
cmd.setIssuerId(req.getIssuerId());
cmd.setIssuerName(req.getIssuerName());
cmd.setReceiverId(req.getReceiverId());
cmd.setReceiverName(req.getReceiverName());
cmd.setFacilityId(req.getFacilityId());
cmd.setDeadline(req.getDeadline());
cmd.setRemark(req.getRemark());
commandMapper.insert(cmd);
addTracking(cmd, "CREATED", null, "DRAFT", req.getIssuerId(), req.getIssuerName(), "创建指令");
return cmd;
}
/**
* 下发指令(DRAFT → ISSUED)
*/
@Transactional
public DispatchCommand issue(Long commandId) {
DispatchCommand cmd = getCommandOrThrow(commandId);
assertStatus(cmd, "DRAFT");
cmd.setStatus("ISSUED");
cmd.setIssuedAt(LocalDateTime.now());
commandMapper.updateById(cmd);
addTracking(cmd, "ISSUED", "DRAFT", "ISSUED", cmd.getIssuerId(), cmd.getIssuerName(), "下发指令");
return cmd;
}
/**
* 接收确认(ISSUED → RECEIVED)
*/
@Transactional
public DispatchCommand receive(Long commandId, Long receiverId, String receiverName) {
DispatchCommand cmd = getCommandOrThrow(commandId);
assertStatus(cmd, "ISSUED");
cmd.setStatus("RECEIVED");
cmd.setReceivedAt(LocalDateTime.now());
if (receiverId != null) {
cmd.setReceiverId(receiverId);
cmd.setReceiverName(receiverName);
}
commandMapper.updateById(cmd);
addExecution(cmd, receiverId, receiverName, "RECEIVE", "确认接收指令", null, 0);
addTracking(cmd, "RECEIVED", "ISSUED", "RECEIVED", receiverId, receiverName, "接收确认");
return cmd;
}
/**
* 开始执行(RECEIVED → EXECUTING)
*/
@Transactional
public DispatchCommand startExecution(Long commandId, Long executorId, String executorName) {
DispatchCommand cmd = getCommandOrThrow(commandId);
assertStatus(cmd, "RECEIVED");
cmd.setStatus("EXECUTING");
cmd.setExecutedAt(LocalDateTime.now());
commandMapper.updateById(cmd);
addExecution(cmd, executorId, executorName, "START", "开始执行", null, 10);
addTracking(cmd, "EXECUTING", "RECEIVED", "EXECUTING", executorId, executorName, "开始执行");
return cmd;
}
/**
* 执行进度反馈(EXECUTING stays)
*/
@Transactional
public DispatchCommand reportProgress(Long commandId, Long executorId, String executorName,
String description, int progress) {
DispatchCommand cmd = getCommandOrThrow(commandId);
assertStatus(cmd, "EXECUTING");
addExecution(cmd, executorId, executorName, "PROGRESS", description, null, progress);
return cmd;
}
/**
* 完成指令(EXECUTING → COMPLETED)
*/
@Transactional
public DispatchCommand complete(Long commandId, Long executorId, String executorName, String result) {
DispatchCommand cmd = getCommandOrThrow(commandId);
assertStatus(cmd, "EXECUTING");
cmd.setStatus("COMPLETED");
cmd.setCompletedAt(LocalDateTime.now());
cmd.setExecuteResult(result);
commandMapper.updateById(cmd);
addExecution(cmd, executorId, executorName, "COMPLETE", result, null, 100);
addTracking(cmd, "COMPLETED", "EXECUTING", "COMPLETED", executorId, executorName, "执行完成: " + result);
return cmd;
}
/**
* 驳回指令(任意活跃状态 → REJECTED)
*/
@Transactional
public DispatchCommand reject(Long commandId, Long operatorId, String operatorName, String reason) {
DispatchCommand cmd = getCommandOrThrow(commandId);
String fromStatus = cmd.getStatus();
if ("COMPLETED".equals(fromStatus) || "CANCELLED".equals(fromStatus) || "REJECTED".equals(fromStatus)) {
throw new BusinessException("当前状态不允许驳回: " + fromStatus);
}
cmd.setStatus("REJECTED");
cmd.setRejectedAt(LocalDateTime.now());
cmd.setRejectReason(reason);
commandMapper.updateById(cmd);
addExecution(cmd, operatorId, operatorName, "REJECT", reason, null, 0);
addTracking(cmd, "REJECTED", fromStatus, "REJECTED", operatorId, operatorName, "驳回: " + reason);
return cmd;
}
/**
* 取消指令
*/
@Transactional
public DispatchCommand cancel(Long commandId, Long operatorId, String operatorName, String reason) {
DispatchCommand cmd = getCommandOrThrow(commandId);
String fromStatus = cmd.getStatus();
if ("COMPLETED".equals(fromStatus) || "CANCELLED".equals(fromStatus)) {
throw new BusinessException("当前状态不允许取消: " + fromStatus);
}
cmd.setStatus("CANCELLED");
commandMapper.updateById(cmd);
addTracking(cmd, "CANCELLED", fromStatus, "CANCELLED", operatorId, operatorName,
"取消指令" + (reason != null ? ": " + reason : ""));
return cmd;
}
/**
* 获取指令详情
*/
public DispatchCommand getById(Long id) {
return getCommandOrThrow(id);
}
/**
* 根据编号获取
*/
public DispatchCommand getByCommandNo(String commandNo) {
DispatchCommand cmd = commandMapper.selectOne(
new LambdaQueryWrapper<DispatchCommand>()
.eq(DispatchCommand::getCommandNo, commandNo));
if (cmd == null) {
throw new BusinessException("指令不存在: " + commandNo);
}
return cmd;
}
// ========== Internal ==========
private DispatchCommand getCommandOrThrow(Long id) {
DispatchCommand cmd = commandMapper.selectById(id);
if (cmd == null) {
throw new BusinessException("指令不存在: " + id);
}
return cmd;
}
private void assertStatus(DispatchCommand cmd, String expected) {
if (!expected.equals(cmd.getStatus())) {
throw new BusinessException("指令状态不正确,期望: " + expected + ",实际: " + cmd.getStatus());
}
}
private String generateCommandNo() {
return "CMD-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
+ "-" + (int) (Math.random() * 9000 + 1000);
}
private void addTracking(DispatchCommand cmd, String stage, String fromStatus, String toStatus,
Long operatorId, String operatorName, String desc) {
CommandTracking t = new CommandTracking();
t.setCommandId(cmd.getId());
t.setCommandNo(cmd.getCommandNo());
t.setStage(stage);
t.setFromStatus(fromStatus);
t.setToStatus(toStatus);
t.setOperatorId(operatorId);
t.setOperatorName(operatorName);
t.setActionDesc(desc);
trackingMapper.insert(t);
}
private void addExecution(DispatchCommand cmd, Long executorId, String executorName,
String action, String description, String attachments, int progress) {
CommandExecutionRecord r = new CommandExecutionRecord();
r.setCommandId(cmd.getId());
r.setCommandNo(cmd.getCommandNo());
r.setExecutorId(executorId);
r.setExecutorName(executorName);
r.setAction(action);
r.setDescription(description);
r.setAttachments(attachments);
r.setProgress(progress);
executionMapper.insert(r);
}
}
@@ -0,0 +1,94 @@
package com.water.dispatch.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.dispatch.entity.CommandExecutionRecord;
import com.water.dispatch.entity.CommandTracking;
import com.water.dispatch.mapper.CommandExecutionRecordMapper;
import com.water.dispatch.mapper.CommandTrackingMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.*;
/**
* 指令全过程追踪服务 - 时间线
*/
@Service
@RequiredArgsConstructor
public class CommandTrackingService {
private final CommandTrackingMapper trackingMapper;
private final CommandExecutionRecordMapper executionMapper;
/**
* 获取指令追踪时间线
*/
public List<CommandTracking> getTimeline(Long commandId) {
return trackingMapper.selectList(
new LambdaQueryWrapper<CommandTracking>()
.eq(CommandTracking::getCommandId, commandId)
.orderByAsc(CommandTracking::getCreatedAt));
}
/**
* 根据指令编号获取追踪时间线
*/
public List<CommandTracking> getTimelineByCommandNo(String commandNo) {
return trackingMapper.selectList(
new LambdaQueryWrapper<CommandTracking>()
.eq(CommandTracking::getCommandNo, commandNo)
.orderByAsc(CommandTracking::getCreatedAt));
}
/**
* 获取执行记录列表
*/
public List<CommandExecutionRecord> getExecutionRecords(Long commandId) {
return executionMapper.selectList(
new LambdaQueryWrapper<CommandExecutionRecord>()
.eq(CommandExecutionRecord::getCommandId, commandId)
.orderByAsc(CommandExecutionRecord::getCreatedAt));
}
/**
* 获取完整时间线(tracking + execution records 合并)
*/
public List<Map<String, Object>> getFullTimeline(Long commandId) {
List<CommandTracking> trackings = getTimeline(commandId);
List<CommandExecutionRecord> records = getExecutionRecords(commandId);
List<Map<String, Object>> timeline = new ArrayList<>();
for (CommandTracking t : trackings) {
Map<String, Object> item = new LinkedHashMap<>();
item.put("type", "TRACKING");
item.put("id", t.getId());
item.put("time", t.getCreatedAt());
item.put("stage", t.getStage());
item.put("operatorId", t.getOperatorId());
item.put("operatorName", t.getOperatorName());
item.put("description", t.getActionDesc());
item.put("fromStatus", t.getFromStatus());
item.put("toStatus", t.getToStatus());
timeline.add(item);
}
for (CommandExecutionRecord r : records) {
Map<String, Object> item = new LinkedHashMap<>();
item.put("type", "EXECUTION");
item.put("id", r.getId());
item.put("time", r.getCreatedAt());
item.put("action", r.getAction());
item.put("executorId", r.getExecutorId());
item.put("executorName", r.getExecutorName());
item.put("description", r.getDescription());
item.put("progress", r.getProgress());
item.put("attachments", r.getAttachments());
timeline.add(item);
}
// Sort by time
timeline.sort(Comparator.comparing(m -> (java.time.LocalDateTime) m.get("time")));
return timeline;
}
}
@@ -1,11 +1,19 @@
package com.water.dispatch.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.dispatch.entity.*;
import com.water.dispatch.mapper.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.*; import java.util.*;
@Service @RequiredArgsConstructor
import java.time.*;
import java.util.*;
/**
* 调度工作台 - 原有业务(值班/工单/策略/预案)
*/
@Service
@RequiredArgsConstructor
public class DispatchBizService {
private final DutyScheduleMapper dutyMapper;
private final DispatchCommandMapper cmdMapper;
@@ -16,66 +24,84 @@ public class DispatchBizService {
public List<DutySchedule> getTodayDuty() {
return dutyMapper.selectList(new LambdaQueryWrapper<DutySchedule>()
.eq(DutySchedule::getDutyDate, LocalDate.now()));
.eq(DutySchedule::getDutyDate, LocalDate.now()));
}
public Map<String,Object> createCommand(Map<String,Object> req) {
public Map<String, Object> createCommand(Map<String, Object> req) {
DispatchCommand c = new DispatchCommand();
c.setCmdNo("CMD-" + System.currentTimeMillis());
c.setTitle((String)req.get("title")); c.setContent((String)req.get("content"));
c.setType((String)req.getOrDefault("type","常规")); c.setStatus(0);
c.setCommandNo("CMD-" + System.currentTimeMillis());
c.setTitle((String) req.get("title"));
c.setContent((String) req.get("content"));
c.setCommandType((String) req.getOrDefault("commandType", "NORMAL"));
c.setPriority((String) req.getOrDefault("priority", "MEDIUM"));
c.setStatus("DRAFT");
cmdMapper.insert(c);
return Map.of("id",c.getId(),"cmdNo",c.getCmdNo());
return Map.of("id", c.getId(), "commandNo", c.getCommandNo());
}
public Map<String,Object> issueCommand(String cmdNo) {
public Map<String, Object> issueCommand(String commandNo) {
DispatchCommand c = cmdMapper.selectOne(new LambdaQueryWrapper<DispatchCommand>()
.eq(DispatchCommand::getCmdNo, cmdNo));
if(c==null) throw new RuntimeException("指令不存在");
c.setStatus(1); c.setIssuedTime(LocalDateTime.now());
.eq(DispatchCommand::getCommandNo, commandNo));
if (c == null) throw new RuntimeException("指令不存在");
c.setStatus("ISSUED");
c.setIssuedAt(LocalDateTime.now());
cmdMapper.updateById(c);
return Map.of("cmdNo",cmdNo,"status",1);
return Map.of("commandNo", commandNo, "status", c.getStatus());
}
public List<DispatchCommand> listCommands(Integer status) {
public List<DispatchCommand> listCommands(String status) {
return cmdMapper.selectList(new LambdaQueryWrapper<DispatchCommand>()
.eq(status!=null, DispatchCommand::getStatus, status));
.eq(status != null, DispatchCommand::getStatus, status));
}
public Map<String,Object> createWorkOrder(Map<String,Object> req) {
public Map<String, Object> createWorkOrder(Map<String, Object> req) {
WorkOrder w = new WorkOrder();
w.setOrderNo("WO-" + System.currentTimeMillis());
w.setTitle((String)req.get("title")); w.setDescription((String)req.get("description"));
w.setType((String)req.getOrDefault("type","维修")); w.setStatus(0);
w.setPriority((String)req.getOrDefault("priority","中"));
w.setTitle((String) req.get("title"));
w.setDescription((String) req.get("description"));
w.setType((String) req.getOrDefault("type", "维修"));
w.setStatus(0);
w.setPriority((String) req.getOrDefault("priority", "中"));
woMapper.insert(w);
return Map.of("id",w.getId(),"orderNo",w.getOrderNo());
return Map.of("id", w.getId(), "orderNo", w.getOrderNo());
}
public Map<String,Object> updateWorkOrderStatus(Long id, int status) {
public Map<String, Object> updateWorkOrderStatus(Long id, int status) {
WorkOrder w = woMapper.selectById(id);
if(w==null) throw new RuntimeException("工单不存在");
if (w == null) throw new RuntimeException("工单不存在");
w.setStatus(status);
if(status==2) w.setCompletedAt(LocalDateTime.now());
if (status == 2) w.setCompletedAt(LocalDateTime.now());
woMapper.updateById(w);
return Map.of("id",id,"status",status);
return Map.of("id", id, "status", status);
}
public void addDutyLog(Long scheduleId, Long userId, String type, String content) {
DutyLog l = new DutyLog();
l.setScheduleId(scheduleId); l.setUserId(userId);
l.setLogType(type); l.setContent(content);
l.setScheduleId(scheduleId);
l.setUserId(userId);
l.setLogType(type);
l.setContent(content);
logMapper.insert(l);
}
public List<DutyLog> getDutyLogs(Long scheduleId) {
return logMapper.selectList(new LambdaQueryWrapper<DutyLog>()
.eq(DutyLog::getScheduleId, scheduleId));
.eq(DutyLog::getScheduleId, scheduleId));
}
public List<DispatchStrategy> listStrategies(String type) {
return stratMapper.selectList(new LambdaQueryWrapper<DispatchStrategy>()
.eq(type!=null, DispatchStrategy::getType, type));
.eq(type != null, DispatchStrategy::getType, type));
}
public List<EmergencyPlan> listPlans(String type) {
return planMapper.selectList(new LambdaQueryWrapper<EmergencyPlan>()
.eq(type!=null, EmergencyPlan::getType, type));
.eq(type != null, EmergencyPlan::getType, type));
}
public Map<String,Object> simulateEmergency(String type, double lng, double lat) {
return Map.of("type",type,"lng",lng,"lat",lat,
"affectedArea","半径500米","affectedUsers",120,
"estimatedDuration","4小时");
public Map<String, Object> simulateEmergency(String type, double lng, double lat) {
return Map.of("type", type, "lng", lng, "lat", lat,
"affectedArea", "半径500米", "affectedUsers", 120,
"estimatedDuration", "4小时");
}
}
@@ -0,0 +1,73 @@
-- V2: 调度指令全生命周期管理
-- 新增/重建表,支持 创建→下发→接收确认→执行反馈→完成归档 全流程
-- 1. 调度指令主表(增强)
CREATE TABLE IF NOT EXISTS disp_dispatch_command (
id BIGSERIAL PRIMARY KEY,
command_no VARCHAR(64) NOT NULL UNIQUE,
title VARCHAR(200) NOT NULL,
content TEXT,
command_type VARCHAR(30) NOT NULL DEFAULT 'NORMAL', -- NORMAL / EMERGENCY / MAINTENANCE
priority VARCHAR(20) NOT NULL DEFAULT 'MEDIUM', -- LOW / MEDIUM / HIGH / URGENT
status VARCHAR(30) NOT NULL DEFAULT 'DRAFT', -- DRAFT / ISSUED / RECEIVED / EXECUTING / COMPLETED / REJECTED / CANCELLED
issuer_id BIGINT,
issuer_name VARCHAR(64),
receiver_id BIGINT,
receiver_name VARCHAR(64),
facility_id BIGINT,
deadline TIMESTAMP,
issued_at TIMESTAMP,
received_at TIMESTAMP,
executed_at TIMESTAMP,
completed_at TIMESTAMP,
rejected_at TIMESTAMP,
reject_reason TEXT,
execute_result TEXT,
remark TEXT,
deleted INT DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_cmd_status ON disp_dispatch_command(status);
CREATE INDEX idx_cmd_type ON disp_dispatch_command(command_type);
CREATE INDEX idx_cmd_priority ON disp_dispatch_command(priority);
CREATE INDEX idx_cmd_issuer ON disp_dispatch_command(issuer_id);
CREATE INDEX idx_cmd_receiver ON disp_dispatch_command(receiver_id);
CREATE INDEX idx_cmd_deadline ON disp_dispatch_command(deadline);
CREATE INDEX idx_cmd_created_at ON disp_dispatch_command(created_at);
-- 2. 执行记录表
CREATE TABLE IF NOT EXISTS disp_command_execution_record (
id BIGSERIAL PRIMARY KEY,
command_id BIGINT NOT NULL,
command_no VARCHAR(64),
executor_id BIGINT NOT NULL,
executor_name VARCHAR(64),
action VARCHAR(30) NOT NULL, -- RECEIVE / START / PROGRESS / COMPLETE / REJECT
description TEXT,
attachments TEXT, -- JSON array of file URLs
progress INT DEFAULT 0, -- 0-100
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_exec_cmd_id ON disp_command_execution_record(command_id);
CREATE INDEX idx_exec_cmd_no ON disp_command_execution_record(command_no);
-- 3. 过程追踪表(时间线)
CREATE TABLE IF NOT EXISTS disp_command_tracking (
id BIGSERIAL PRIMARY KEY,
command_id BIGINT NOT NULL,
command_no VARCHAR(64),
stage VARCHAR(30) NOT NULL, -- CREATED / ISSUED / RECEIVED / EXECUTING / COMPLETED / REJECTED / CANCELLED
operator_id BIGINT,
operator_name VARCHAR(64),
action_desc VARCHAR(500),
from_status VARCHAR(30),
to_status VARCHAR(30),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_track_cmd_id ON disp_command_tracking(command_id);
CREATE INDEX idx_track_cmd_no ON disp_command_tracking(command_no);
CREATE INDEX idx_track_stage ON disp_command_tracking(stage);
@@ -0,0 +1,248 @@
package com.water.dispatch.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.common.core.exception.BusinessException;
import com.water.dispatch.entity.CommandExecutionRecord;
import com.water.dispatch.entity.CommandTracking;
import com.water.dispatch.entity.DispatchCommand;
import com.water.dispatch.entity.dto.CommandCreateRequest;
import com.water.dispatch.mapper.CommandExecutionRecordMapper;
import com.water.dispatch.mapper.CommandTrackingMapper;
import com.water.dispatch.mapper.DispatchCommandMapper;
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 static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class CommandLifecycleServiceTest {
@Mock
private DispatchCommandMapper commandMapper;
@Mock
private CommandExecutionRecordMapper executionMapper;
@Mock
private CommandTrackingMapper trackingMapper;
@InjectMocks
private CommandLifecycleService lifecycleService;
@Test
void testCreateCommand() {
when(commandMapper.insert(any())).thenReturn(1);
when(trackingMapper.insert(any())).thenReturn(1);
CommandCreateRequest req = new CommandCreateRequest();
req.setTitle("测试指令");
req.setContent("测试内容");
req.setCommandType("NORMAL");
req.setPriority("HIGH");
req.setIssuerId(1L);
req.setIssuerName("张三");
DispatchCommand result = lifecycleService.create(req);
assertNotNull(result.getCommandNo());
assertEquals("DRAFT", result.getStatus());
assertEquals("测试指令", result.getTitle());
assertEquals("NORMAL", result.getCommandType());
assertEquals("HIGH", result.getPriority());
verify(commandMapper).insert(any());
verify(trackingMapper).insert(any());
}
@Test
void testIssueCommand() {
DispatchCommand cmd = buildCommand(1L, "DRAFT");
when(commandMapper.selectById(1L)).thenReturn(cmd);
when(commandMapper.updateById(any())).thenReturn(1);
when(trackingMapper.insert(any())).thenReturn(1);
DispatchCommand result = lifecycleService.issue(1L);
assertEquals("ISSUED", result.getStatus());
assertNotNull(result.getIssuedAt());
verify(commandMapper).updateById(any());
verify(trackingMapper).insert(any());
}
@Test
void testIssueCommandWrongStatus() {
DispatchCommand cmd = buildCommand(1L, "ISSUED");
when(commandMapper.selectById(1L)).thenReturn(cmd);
assertThrows(BusinessException.class, () -> lifecycleService.issue(1L));
}
@Test
void testReceiveCommand() {
DispatchCommand cmd = buildCommand(1L, "ISSUED");
when(commandMapper.selectById(1L)).thenReturn(cmd);
when(commandMapper.updateById(any())).thenReturn(1);
when(executionMapper.insert(any())).thenReturn(1);
when(trackingMapper.insert(any())).thenReturn(1);
DispatchCommand result = lifecycleService.receive(1L, 2L, "李四");
assertEquals("RECEIVED", result.getStatus());
assertNotNull(result.getReceivedAt());
assertEquals(2L, result.getReceiverId());
ArgumentCaptor<CommandExecutionRecord> execCaptor = ArgumentCaptor.forClass(CommandExecutionRecord.class);
verify(executionMapper).insert(execCaptor.capture());
assertEquals("RECEIVE", execCaptor.getValue().getAction());
}
@Test
void testStartExecution() {
DispatchCommand cmd = buildCommand(1L, "RECEIVED");
when(commandMapper.selectById(1L)).thenReturn(cmd);
when(commandMapper.updateById(any())).thenReturn(1);
when(executionMapper.insert(any())).thenReturn(1);
when(trackingMapper.insert(any())).thenReturn(1);
DispatchCommand result = lifecycleService.startExecution(1L, 2L, "李四");
assertEquals("EXECUTING", result.getStatus());
assertNotNull(result.getExecutedAt());
}
@Test
void testCompleteCommand() {
DispatchCommand cmd = buildCommand(1L, "EXECUTING");
when(commandMapper.selectById(1L)).thenReturn(cmd);
when(commandMapper.updateById(any())).thenReturn(1);
when(executionMapper.insert(any())).thenReturn(1);
when(trackingMapper.insert(any())).thenReturn(1);
DispatchCommand result = lifecycleService.complete(1L, 2L, "李四", "已完成管道修复");
assertEquals("COMPLETED", result.getStatus());
assertNotNull(result.getCompletedAt());
assertEquals("已完成管道修复", result.getExecuteResult());
ArgumentCaptor<CommandExecutionRecord> execCaptor = ArgumentCaptor.forClass(CommandExecutionRecord.class);
verify(executionMapper).insert(execCaptor.capture());
assertEquals(100, execCaptor.getValue().getProgress());
}
@Test
void testRejectCommand() {
DispatchCommand cmd = buildCommand(1L, "ISSUED");
when(commandMapper.selectById(1L)).thenReturn(cmd);
when(commandMapper.updateById(any())).thenReturn(1);
when(executionMapper.insert(any())).thenReturn(1);
when(trackingMapper.insert(any())).thenReturn(1);
DispatchCommand result = lifecycleService.reject(1L, 2L, "李四", "信息不足,无法执行");
assertEquals("REJECTED", result.getStatus());
assertEquals("信息不足,无法执行", result.getRejectReason());
assertNotNull(result.getRejectedAt());
}
@Test
void testRejectCompletedCommand() {
DispatchCommand cmd = buildCommand(1L, "COMPLETED");
when(commandMapper.selectById(1L)).thenReturn(cmd);
assertThrows(BusinessException.class,
() -> lifecycleService.reject(1L, 2L, "李四", "原因"));
}
@Test
void testCancelCommand() {
DispatchCommand cmd = buildCommand(1L, "DRAFT");
when(commandMapper.selectById(1L)).thenReturn(cmd);
when(commandMapper.updateById(any())).thenReturn(1);
when(trackingMapper.insert(any())).thenReturn(1);
DispatchCommand result = lifecycleService.cancel(1L, 1L, "张三", "计划变更");
assertEquals("CANCELLED", result.getStatus());
}
@Test
void testCancelAlreadyCancelledCommand() {
DispatchCommand cmd = buildCommand(1L, "CANCELLED");
when(commandMapper.selectById(1L)).thenReturn(cmd);
assertThrows(BusinessException.class,
() -> lifecycleService.cancel(1L, 1L, "张三", "reason"));
}
@Test
void testCommandNotFound() {
when(commandMapper.selectById(999L)).thenReturn(null);
assertThrows(BusinessException.class, () -> lifecycleService.issue(999L));
}
@Test
void testReportProgress() {
DispatchCommand cmd = buildCommand(1L, "EXECUTING");
when(commandMapper.selectById(1L)).thenReturn(cmd);
when(executionMapper.insert(any())).thenReturn(1);
DispatchCommand result = lifecycleService.reportProgress(1L, 2L, "李四", "完成50%", 50);
assertEquals("EXECUTING", result.getStatus()); // status unchanged
ArgumentCaptor<CommandExecutionRecord> captor = ArgumentCaptor.forClass(CommandExecutionRecord.class);
verify(executionMapper).insert(captor.capture());
assertEquals("PROGRESS", captor.getValue().getAction());
assertEquals(50, captor.getValue().getProgress());
}
@Test
void testFullLifecycle() {
// Create
when(commandMapper.insert(any())).thenReturn(1);
when(trackingMapper.insert(any())).thenReturn(1);
when(executionMapper.insert(any())).thenReturn(1);
CommandCreateRequest req = new CommandCreateRequest();
req.setTitle("全流程测试");
req.setCommandType("EMERGENCY");
req.setPriority("URGENT");
req.setIssuerId(1L);
req.setIssuerName("张三");
DispatchCommand cmd = lifecycleService.create(req);
cmd.setId(100L);
assertEquals("DRAFT", cmd.getStatus());
// Issue
when(commandMapper.selectById(100L)).thenReturn(cmd);
when(commandMapper.updateById(any())).thenReturn(1);
lifecycleService.issue(100L);
assertEquals("ISSUED", cmd.getStatus());
// Receive
lifecycleService.receive(100L, 2L, "李四");
assertEquals("RECEIVED", cmd.getStatus());
// Start
lifecycleService.startExecution(100L, 2L, "李四");
assertEquals("EXECUTING", cmd.getStatus());
// Complete
lifecycleService.complete(100L, 2L, "李四", "抢修完成");
assertEquals("COMPLETED", cmd.getStatus());
}
private DispatchCommand buildCommand(Long id, String status) {
DispatchCommand cmd = new DispatchCommand();
cmd.setId(id);
cmd.setCommandNo("CMD-TEST-001");
cmd.setTitle("测试指令");
cmd.setStatus(status);
cmd.setIssuerId(1L);
cmd.setIssuerName("张三");
return cmd;
}
}
@@ -0,0 +1,115 @@
package com.water.dispatch.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.dispatch.entity.CommandExecutionRecord;
import com.water.dispatch.entity.CommandTracking;
import com.water.dispatch.mapper.CommandExecutionRecordMapper;
import com.water.dispatch.mapper.CommandTrackingMapper;
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 java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CommandTrackingServiceTest {
@Mock
private CommandTrackingMapper trackingMapper;
@Mock
private CommandExecutionRecordMapper executionMapper;
@InjectMocks
private CommandTrackingService trackingService;
@Test
void testGetTimeline() {
CommandTracking t1 = new CommandTracking();
t1.setId(1L);
t1.setCommandId(100L);
t1.setStage("CREATED");
t1.setCreatedAt(LocalDateTime.of(2026, 6, 14, 10, 0));
CommandTracking t2 = new CommandTracking();
t2.setId(2L);
t2.setCommandId(100L);
t2.setStage("ISSUED");
t2.setCreatedAt(LocalDateTime.of(2026, 6, 14, 10, 5));
when(trackingMapper.selectList(any(LambdaQueryWrapper.class)))
.thenReturn(List.of(t1, t2));
List<CommandTracking> result = trackingService.getTimeline(100L);
assertEquals(2, result.size());
assertEquals("CREATED", result.get(0).getStage());
}
@Test
void testGetExecutionRecords() {
CommandExecutionRecord r1 = new CommandExecutionRecord();
r1.setId(1L);
r1.setCommandId(100L);
r1.setAction("RECEIVE");
r1.setProgress(0);
when(executionMapper.selectList(any(LambdaQueryWrapper.class)))
.thenReturn(List.of(r1));
List<CommandExecutionRecord> result = trackingService.getExecutionRecords(100L);
assertEquals(1, result.size());
assertEquals("RECEIVE", result.get(0).getAction());
}
@Test
void testGetFullTimeline() {
CommandTracking t = new CommandTracking();
t.setId(1L);
t.setCommandId(100L);
t.setStage("CREATED");
t.setOperatorName("张三");
t.setActionDesc("创建指令");
t.setCreatedAt(LocalDateTime.of(2026, 6, 14, 10, 0));
CommandExecutionRecord r = new CommandExecutionRecord();
r.setId(1L);
r.setCommandId(100L);
r.setAction("RECEIVE");
r.setExecutorName("李四");
r.setDescription("确认接收");
r.setProgress(0);
r.setCreatedAt(LocalDateTime.of(2026, 6, 14, 10, 5));
when(trackingMapper.selectList(any(LambdaQueryWrapper.class)))
.thenReturn(List.of(t));
when(executionMapper.selectList(any(LambdaQueryWrapper.class)))
.thenReturn(List.of(r));
List<Map<String, Object>> timeline = trackingService.getFullTimeline(100L);
assertEquals(2, timeline.size());
assertEquals("TRACKING", timeline.get(0).get("type"));
assertEquals("EXECUTION", timeline.get(1).get("type"));
}
@Test
void testGetTimelineByCommandNo() {
CommandTracking t = new CommandTracking();
t.setCommandNo("CMD-20260614-001");
t.setStage("ISSUED");
t.setCreatedAt(LocalDateTime.now());
when(trackingMapper.selectList(any(LambdaQueryWrapper.class)))
.thenReturn(List.of(t));
List<CommandTracking> result = trackingService.getTimelineByCommandNo("CMD-20260614-001");
assertEquals(1, result.size());
assertEquals("ISSUED", result.get(0).getStage());
}
}
@@ -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());
}
}
@@ -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));
}
}
@@ -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;
}
@@ -1,13 +1,106 @@
package com.water.production.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data; import java.time.LocalDateTime;
@Data @TableName("prod_alert_record")
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;
private String deviceId, area, level; // 一般/重要/紧急
private String alertType, description;
private Integer status; // 0活跃 1已确认 2已处理
private Long confirmedBy, handledBy;
@TableField(fill=FieldFill.INSERT) private LocalDateTime createdTime;
private LocalDateTime confirmedTime, handledTime;
@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;
}
@@ -0,0 +1,80 @@
package com.water.production.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
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 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,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,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);
}
@@ -1,5 +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;
@Mapper public interface AlertRecordMapper extends BaseMapper<AlertRecord> {}
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);
}
@@ -0,0 +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 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,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,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,366 @@
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 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.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();
}
// ==================== 规则评估引擎 ====================
/**
* 评估指标值是否触发报警 (核心引擎)
* 支持AND/OR组合条件的JSON表达式解析
*
* @param deviceSn 设备编号
* @param metricKey 指标键
* @param value 指标值
* @param area 区域
* @return 触发的报警记录列表
*/
@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;
// 去重检查(debounce)
String dedupKey = deviceSn + ":" + metricKey + ":" + rule.getAlertLevel();
long now = Instant.now().getEpochSecond();
Long lastTime = lastTriggerTime.get(dedupKey);
if (lastTime != null && (now - lastTime) < rule.getDebounceSec()) {
log.debug("Alert debounced: {} (last={}s ago, debounce={}s)", dedupKey, 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;
}
/**
* 解析并评估条件表达式 (支持AND/OR组合)
* 表达式格式:
* {"op":"AND","conditions":[
* {"metric":"pressure","operator":">","threshold":0.8},
* {"metric":"temperature","operator":"<","threshold":50}
* ]}
*
* 或简单格式: {"metric":"pressure","operator":">","threshold":0.8}
*/
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;
}
}
/**
* 评估组合条件(AND/OR)
*/
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;
// 如果条件指定了不同的metric, 则跳过(当前值不匹配)
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 {
// 跨天场景: 如 22:00 - 06:00
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());
// 实际调用: notifyService.send(channel, notification.getTitle(), notification.getContent());
}
private String levelLabel(String level) {
return switch (level) {
case "urgent" -> "紧急";
case "important" -> "重要";
case "general" -> "一般";
default -> level;
};
}
}
@@ -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,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,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 &gt;= #{startDate}::timestamp</if>
<if test="endDate != null and endDate != ''">AND c.created_at &lt;= #{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));
}
}
@@ -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;
}
}
@@ -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());
}
}
@@ -0,0 +1,59 @@
package com.water.system.controller;
import com.water.common.core.result.R;
import com.water.system.entity.SysDepartment;
import com.water.system.entity.dto.DeptRequest;
import com.water.system.service.DepartmentService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "部门管理")
@RestController
@RequestMapping("/api/system/dept")
@RequiredArgsConstructor
public class DepartmentController {
private final DepartmentService departmentService;
@Operation(summary = "获取部门树")
@GetMapping("/tree")
public R<List<SysDepartment>> tree() {
return R.ok(departmentService.getDeptTree());
}
@Operation(summary = "获取部门详情")
@GetMapping("/{id}")
public R<SysDepartment> getById(@PathVariable Long id) {
return R.ok(departmentService.getDeptById(id));
}
@Operation(summary = "创建部门")
@PostMapping
public R<Long> create(@Valid @RequestBody DeptRequest request) {
return R.ok(departmentService.createDept(request));
}
@Operation(summary = "更新部门")
@PutMapping("/{id}")
public R<String> update(@PathVariable Long id, @RequestBody DeptRequest request) {
departmentService.updateDept(id, request);
return R.ok("OK");
}
@Operation(summary = "删除部门")
@DeleteMapping("/{id}")
public R<String> delete(@PathVariable Long id) {
departmentService.deleteDept(id);
return R.ok("OK");
}
@Operation(summary = "获取子部门列表")
@GetMapping("/{id}/children")
public R<List<SysDepartment>> getChildren(@PathVariable Long id) {
return R.ok(departmentService.getChildDepts(id));
}
}
@@ -0,0 +1,87 @@
package com.water.system.controller;
import com.water.common.core.result.R;
import com.water.system.entity.DataDictionary;
import com.water.system.entity.DataDictionaryItem;
import com.water.system.entity.dto.DictItemRequest;
import com.water.system.entity.dto.DictRequest;
import com.water.system.service.DictionaryService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "数据字典管理")
@RestController
@RequestMapping("/api/system/dict")
@RequiredArgsConstructor
public class DictionaryController {
private final DictionaryService dictionaryService;
@Operation(summary = "获取字典列表")
@GetMapping("/list")
public R<List<DataDictionary>> list(@RequestParam(required = false) String keyword) {
return R.ok(dictionaryService.listDictionaries(keyword));
}
@Operation(summary = "获取字典详情")
@GetMapping("/{id}")
public R<DataDictionary> getById(@PathVariable Long id) {
return R.ok(dictionaryService.getDictionaryById(id));
}
@Operation(summary = "创建字典")
@PostMapping
public R<Long> create(@Valid @RequestBody DictRequest request) {
return R.ok(dictionaryService.createDictionary(request));
}
@Operation(summary = "更新字典")
@PutMapping("/{id}")
public R<String> update(@PathVariable Long id, @RequestBody DictRequest request) {
dictionaryService.updateDictionary(id, request);
return R.ok("OK");
}
@Operation(summary = "删除字典")
@DeleteMapping("/{id}")
public R<String> delete(@PathVariable Long id) {
dictionaryService.deleteDictionary(id);
return R.ok("OK");
}
@Operation(summary = "获取字典项列表(按字典ID)")
@GetMapping("/{dictId}/items")
public R<List<DataDictionaryItem>> listItems(@PathVariable Long dictId) {
return R.ok(dictionaryService.listItemsByDictId(dictId));
}
@Operation(summary = "根据字典编码获取字典项")
@GetMapping("/code/{dictCode}/items")
public R<List<DataDictionaryItem>> listItemsByCode(@PathVariable String dictCode) {
return R.ok(dictionaryService.listItemsByDictCode(dictCode));
}
@Operation(summary = "创建字典项")
@PostMapping("/item")
public R<Long> createItem(@Valid @RequestBody DictItemRequest request) {
return R.ok(dictionaryService.createItem(request));
}
@Operation(summary = "更新字典项")
@PutMapping("/item/{id}")
public R<String> updateItem(@PathVariable Long id, @RequestBody DictItemRequest request) {
dictionaryService.updateItem(id, request);
return R.ok("OK");
}
@Operation(summary = "删除字典项")
@DeleteMapping("/item/{id}")
public R<String> deleteItem(@PathVariable Long id) {
dictionaryService.deleteItem(id);
return R.ok("OK");
}
}
@@ -0,0 +1,32 @@
package com.water.system.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.water.common.core.result.R;
import com.water.system.entity.SysLog;
import com.water.system.entity.dto.LogQueryRequest;
import com.water.system.service.LogService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@Tag(name = "日志管理")
@RestController
@RequestMapping("/api/system/log")
@RequiredArgsConstructor
public class LogController {
private final LogService logService;
@Operation(summary = "分页查询日志")
@GetMapping("/page")
public R<IPage<SysLog>> page(LogQueryRequest request) {
return R.ok(logService.pageLogs(request));
}
@Operation(summary = "获取日志详情")
@GetMapping("/{id}")
public R<SysLog> getById(@PathVariable Long id) {
return R.ok(logService.getLogById(id));
}
}
@@ -0,0 +1,66 @@
package com.water.system.controller;
import com.water.common.core.result.R;
import com.water.system.entity.SysRole;
import com.water.system.entity.dto.RoleRequest;
import com.water.system.service.RoleService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "角色管理")
@RestController
@RequestMapping("/api/system/role")
@RequiredArgsConstructor
public class RoleController {
private final RoleService roleService;
@Operation(summary = "获取角色列表")
@GetMapping("/list")
public R<List<SysRole>> list() {
return R.ok(roleService.listRoles());
}
@Operation(summary = "获取角色详情")
@GetMapping("/{id}")
public R<SysRole> getById(@PathVariable Long id) {
return R.ok(roleService.getRoleById(id));
}
@Operation(summary = "创建角色")
@PostMapping
public R<Long> create(@Valid @RequestBody RoleRequest request) {
return R.ok(roleService.createRole(request));
}
@Operation(summary = "更新角色")
@PutMapping("/{id}")
public R<String> update(@PathVariable Long id, @Valid @RequestBody RoleRequest request) {
roleService.updateRole(id, request);
return R.ok("OK");
}
@Operation(summary = "删除角色")
@DeleteMapping("/{id}")
public R<String> delete(@PathVariable Long id) {
roleService.deleteRole(id);
return R.ok("OK");
}
@Operation(summary = "分配角色权限")
@PutMapping("/{id}/permissions")
public R<String> assignPermissions(@PathVariable Long id, @RequestBody List<String> permissions) {
roleService.assignPermissions(id, permissions);
return R.ok("OK");
}
@Operation(summary = "获取用户角色")
@GetMapping("/user/{userId}")
public R<List<SysRole>> getByUserId(@PathVariable Long userId) {
return R.ok(roleService.getRolesByUserId(userId));
}
}
@@ -0,0 +1,74 @@
package com.water.system.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.water.common.core.result.R;
import com.water.system.entity.SysUser;
import com.water.system.entity.dto.PasswordResetRequest;
import com.water.system.entity.dto.UserCreateRequest;
import com.water.system.entity.dto.UserUpdateRequest;
import com.water.system.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@Tag(name = "用户管理")
@RestController
@RequestMapping("/api/system/user")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@Operation(summary = "分页查询用户列表")
@GetMapping("/page")
public R<IPage<SysUser>> page(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) Long deptId) {
return R.ok(userService.pageUsers(pageNum, pageSize, keyword, status, deptId));
}
@Operation(summary = "获取用户详情")
@GetMapping("/{id}")
public R<SysUser> getById(@PathVariable Long id) {
return R.ok(userService.getUserById(id));
}
@Operation(summary = "创建用户")
@PostMapping
public R<Long> create(@Valid @RequestBody UserCreateRequest request) {
return R.ok(userService.createUser(request));
}
@Operation(summary = "更新用户信息")
@PutMapping("/{id}")
public R<String> update(@PathVariable Long id, @RequestBody UserUpdateRequest request) {
userService.updateUser(id, request);
return R.ok("OK");
}
@Operation(summary = "删除用户")
@DeleteMapping("/{id}")
public R<String> delete(@PathVariable Long id) {
userService.deleteUser(id);
return R.ok("OK");
}
@Operation(summary = "停用/启用用户")
@PutMapping("/{id}/status")
public R<String> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
userService.updateStatus(id, status);
return R.ok("OK");
}
@Operation(summary = "重置密码")
@PutMapping("/{id}/reset-password")
public R<String> resetPassword(@PathVariable Long id, @Valid @RequestBody PasswordResetRequest request) {
userService.resetPassword(id, request.getNewPassword());
return R.ok("OK");
}
}
@@ -0,0 +1,33 @@
package com.water.system.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 数据字典实体
*/
@Data
@TableName("sys_data_dictionary")
public class DataDictionary {
@TableId(type = IdType.AUTO)
private Long id;
/** 字典编码(唯一) */
private String dictCode;
/** 字典名称 */
private String dictName;
/** 字典描述 */
private String description;
/** 状态: 0-停用 1-正常 */
private Integer status;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
}
@@ -0,0 +1,42 @@
package com.water.system.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 数据字典项实体
*/
@Data
@TableName("sys_data_dictionary_item")
public class DataDictionaryItem {
@TableId(type = IdType.AUTO)
private Long id;
/** 字典ID */
private Long dictId;
/** 字典项编码 */
private String itemCode;
/** 字典项名称 */
private String itemName;
/** 字典项值 */
private String itemValue;
/** 排序号 */
private Integer sort;
/** 状态: 0-停用 1-正常 */
private Integer status;
/** 备注 */
private String remark;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
}
@@ -1,9 +1,47 @@
package com.water.system.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
@Data @TableName("sys_department")
import java.time.LocalDateTime;
import java.util.List;
/**
* 部门实体(树结构)
*/
@Data
@TableName("sys_department")
public class SysDepartment {
@TableId(type = IdType.AUTO) private Long id;
private Long parentId; private String deptName, leader, phone;
private Integer sort, status;
@TableId(type = IdType.AUTO)
private Long id;
/** 父部门ID, 0为根节点 */
private Long parentId;
/** 部门名称 */
private String deptName;
/** 负责人 */
private String leader;
/** 联系电话 */
private String phone;
/** 排序号 */
private Integer sort;
/** 状态: 0-停用 1-正常 */
private Integer status;
/** 层级路径(如: 0,1,5) */
private String ancestors;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
/** 子部门列表(非数据库字段) */
@TableField(exist = false)
private List<SysDepartment> children;
}
@@ -1,11 +1,58 @@
package com.water.system.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data; import java.time.LocalDateTime;
@Data @TableName("sys_log")
import lombok.Data;
import java.time.LocalDateTime;
/**
* 系统日志实体
* 支持: 登录日志(LOGIN) / 操作日志(OPERATE) / 异常日志(EXCEPTION)
*/
@Data
@TableName("sys_log")
public class SysLog {
@TableId(type = IdType.AUTO) private Long id;
private Long userId; private String username, logType, module, action;
private String requestUrl, requestMethod, requestParams, responseResult;
private String ip; private Long duration;
@TableField(fill=FieldFill.INSERT) private LocalDateTime createdTime;
@TableId(type = IdType.AUTO)
private Long id;
/** 操作用户ID */
private Long userId;
/** 操作用户名 */
private String username;
/** 日志类型: LOGIN-登录 OPERATE-操作 EXCEPTION-异常 */
private String logType;
/** 操作模块 */
private String module;
/** 操作描述 */
private String action;
/** 请求URL */
private String requestUrl;
/** 请求方法 */
private String requestMethod;
/** 请求参数 */
private String requestParams;
/** 响应结果 */
private String responseResult;
/** 操作IP */
private String ip;
/** 耗时(毫秒) */
private Long duration;
/** 状态: 0-失败 1-成功 */
private Integer status;
/** 错误信息 */
private String errorMsg;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdTime;
}
@@ -1,10 +1,46 @@
package com.water.system.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data; import java.time.LocalDateTime;
@Data @TableName("sys_role")
import lombok.Data;
import java.time.LocalDateTime;
/**
* 系统角色实体
* 支持5级角色: 1-超级管理员 2-系统管理员 3-部门经理 4-普通员工 5-访客
*/
@Data
@TableName("sys_role")
public class SysRole {
@TableId(type = IdType.AUTO) private Long id;
private String roleCode, roleName, description;
private Integer status, sort;
@TableField(fill=FieldFill.INSERT) private LocalDateTime createdTime;
@TableId(type = IdType.AUTO)
private Long id;
/** 角色编码 */
private String roleCode;
/** 角色名称 */
private String roleName;
/** 角色描述 */
private String description;
/** 角色级别: 1-超级管理员 2-系统管理员 3-部门经理 4-普通员工 5-访客 */
private Integer level;
/** 数据权限范围: ALL-全部 DEPT-本部门 DEPT_BELOW-本部门及以下 SELF-仅本人 */
private String dataScope;
/** 权限配置(JSON数组) */
private String permissions;
/** 状态: 0-停用 1-正常 */
private Integer status;
/** 排序号 */
private Integer sort;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
}
@@ -1,12 +1,54 @@
package com.water.system.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data; import java.time.LocalDateTime;
@Data @TableName("sys_user")
import lombok.Data;
import java.time.LocalDateTime;
/**
* 系统用户实体
*/
@Data
@TableName("sys_user")
public class SysUser {
@TableId(type = IdType.AUTO) private Long id;
private String username, password, realName, phone, email;
private Long deptId; private String avatar;
private Integer status; // 0停用 1正常
@TableField(fill=FieldFill.INSERT) private LocalDateTime createdTime;
@TableField(fill=FieldFill.INSERT_UPDATE) private LocalDateTime updatedTime;
@TableId(type = IdType.AUTO)
private Long id;
/** 用户名 */
private String username;
/** 密码(加密) */
private String password;
/** 真实姓名 */
private String realName;
/** 手机号 */
private String phone;
/** 邮箱 */
private String email;
/** 部门ID */
private Long deptId;
/** 角色ID */
private Long roleId;
/** 头像URL */
private String avatar;
/** 状态: 0-停用 1-正常 */
private Integer status;
/** 最后登录时间 */
private LocalDateTime lastLoginTime;
/** 最后登录IP */
private String lastLoginIp;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
}
@@ -0,0 +1,20 @@
package com.water.system.entity.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 部门创建/更新请求
*/
@Data
public class DeptRequest {
private Long parentId;
@NotBlank(message = "部门名称不能为空")
private String deptName;
private String leader;
private String phone;
private Integer sort;
private Integer status;
}
@@ -0,0 +1,25 @@
package com.water.system.entity.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 字典项创建/更新请求
*/
@Data
public class DictItemRequest {
@NotNull(message = "字典ID不能为空")
private Long dictId;
@NotBlank(message = "字典项编码不能为空")
private String itemCode;
@NotBlank(message = "字典项名称不能为空")
private String itemName;
private String itemValue;
private Integer sort;
private Integer status;
private String remark;
}
@@ -0,0 +1,19 @@
package com.water.system.entity.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 字典创建/更新请求
*/
@Data
public class DictRequest {
@NotBlank(message = "字典编码不能为空")
private String dictCode;
@NotBlank(message = "字典名称不能为空")
private String dictName;
private String description;
private Integer status;
}
@@ -0,0 +1,22 @@
package com.water.system.entity.dto;
import lombok.Data;
/**
* 日志查询请求
*/
@Data
public class LogQueryRequest extends PageQuery {
/** 日志类型: LOGIN/OPERATE/EXCEPTION */
private String logType;
/** 用户名(模糊) */
private String username;
/** 操作模块 */
private String module;
/** 开始时间(yyyy-MM-dd) */
private String startTime;
/** 结束时间(yyyy-MM-dd) */
private String endTime;
/** 状态: 0-失败 1-成功 */
private Integer status;
}
@@ -0,0 +1,14 @@
package com.water.system.entity.dto;
import lombok.Data;
/**
* 分页查询基础参数
*/
@Data
public class PageQuery {
/** 当前页码, 默认1 */
private Integer pageNum = 1;
/** 每页条数, 默认10 */
private Integer pageSize = 10;
}
@@ -0,0 +1,13 @@
package com.water.system.entity.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 密码重置请求
*/
@Data
public class PasswordResetRequest {
@NotBlank(message = "新密码不能为空")
private String newPassword;
}
@@ -0,0 +1,27 @@
package com.water.system.entity.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
* 角色创建/更新请求
*/
@Data
public class RoleRequest {
@NotBlank(message = "角色编码不能为空")
private String roleCode;
@NotBlank(message = "角色名称不能为空")
private String roleName;
private String description;
@NotNull(message = "角色级别不能为空")
private Integer level;
private String dataScope;
private List<String> permissions;
private Integer sort;
}
@@ -0,0 +1,22 @@
package com.water.system.entity.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 用户创建请求
*/
@Data
public class UserCreateRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
private String realName;
private String phone;
private String email;
private Long deptId;
private Long roleId;
}
@@ -0,0 +1,15 @@
package com.water.system.entity.dto;
import lombok.Data;
/**
* 用户更新请求
*/
@Data
public class UserUpdateRequest {
private String realName;
private String phone;
private String email;
private Long deptId;
private Long roleId;
}
@@ -0,0 +1,15 @@
package com.water.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.system.entity.DataDictionaryItem;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface DataDictionaryItemMapper extends BaseMapper<DataDictionaryItem> {
@Select("SELECT i.* FROM sys_data_dictionary_item i INNER JOIN sys_data_dictionary d ON d.id = i.dict_id WHERE d.dict_code = #{dictCode} AND d.deleted = 0 AND i.deleted = 0 ORDER BY i.sort")
List<DataDictionaryItem> selectByDictCode(@Param("dictCode") String dictCode);
}
@@ -0,0 +1,14 @@
package com.water.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.system.entity.DataDictionary;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface DataDictionaryMapper extends BaseMapper<DataDictionary> {
@Select("SELECT * FROM sys_data_dictionary WHERE dict_code = #{dictCode} AND deleted = 0")
DataDictionary selectByDictCode(@Param("dictCode") String dictCode);
}
@@ -1,5 +1,15 @@
package com.water.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.system.entity.SysDepartment;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface SysDepartmentMapper extends BaseMapper<SysDepartment> {}
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface SysDepartmentMapper extends BaseMapper<SysDepartment> {
@Select("SELECT * FROM sys_department WHERE ancestors LIKE CONCAT(#{ancestorPath}, '%') AND deleted = 0 ORDER BY sort")
List<SysDepartment> selectByAncestorPath(@Param("ancestorPath") String ancestorPath);
}
@@ -1,5 +1,9 @@
package com.water.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.system.entity.SysLog;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface SysLogMapper extends BaseMapper<SysLog> {}
@Mapper
public interface SysLogMapper extends BaseMapper<SysLog> {
}
@@ -1,5 +1,19 @@
package com.water.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.system.entity.SysRole;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface SysRoleMapper extends BaseMapper<SysRole> {}
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
@Mapper
public interface SysRoleMapper extends BaseMapper<SysRole> {
@Select("SELECT r.* FROM sys_role r INNER JOIN sys_user u ON u.role_id = r.id WHERE u.id = #{userId} AND r.deleted = 0")
List<SysRole> selectRolesByUserId(@Param("userId") Long userId);
@Select("SELECT * FROM sys_role WHERE level <= #{level} AND deleted = 0 ORDER BY level, sort")
List<SysRole> selectRolesByMaxLevel(@Param("level") Integer level);
}
@@ -1,5 +1,14 @@
package com.water.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.system.entity.SysUser;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface SysUserMapper extends BaseMapper<SysUser> {}
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {
@Select("SELECT * FROM sys_user WHERE username = #{username} AND deleted = 0")
SysUser selectByUsername(@Param("username") String username);
}
@@ -0,0 +1,29 @@
package com.water.system.service;
import com.water.system.entity.SysDepartment;
import com.water.system.entity.dto.DeptRequest;
import java.util.List;
/**
* 部门管理服务接口
*/
public interface DepartmentService {
/** 获取部门树 */
List<SysDepartment> getDeptTree();
/** 获取部门详情 */
SysDepartment getDeptById(Long id);
/** 创建部门 */
Long createDept(DeptRequest request);
/** 更新部门 */
void updateDept(Long id, DeptRequest request);
/** 删除部门 */
void deleteDept(Long id);
/** 获取所有子部门 */
List<SysDepartment> getChildDepts(Long parentId);
}
@@ -0,0 +1,43 @@
package com.water.system.service;
import com.water.system.entity.DataDictionary;
import com.water.system.entity.DataDictionaryItem;
import com.water.system.entity.dto.DictItemRequest;
import com.water.system.entity.dto.DictRequest;
import java.util.List;
/**
* 数据字典服务接口
*/
public interface DictionaryService {
/** 查询所有字典 */
List<DataDictionary> listDictionaries(String keyword);
/** 获取字典详情 */
DataDictionary getDictionaryById(Long id);
/** 创建字典 */
Long createDictionary(DictRequest request);
/** 更新字典 */
void updateDictionary(Long id, DictRequest request);
/** 删除字典 */
void deleteDictionary(Long id);
/** 根据字典ID查询字典项 */
List<DataDictionaryItem> listItemsByDictId(Long dictId);
/** 根据字典编码查询字典项 */
List<DataDictionaryItem> listItemsByDictCode(String dictCode);
/** 创建字典项 */
Long createItem(DictItemRequest request);
/** 更新字典项 */
void updateItem(Long id, DictItemRequest request);
/** 删除字典项 */
void deleteItem(Long id);
}
@@ -0,0 +1,20 @@
package com.water.system.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.water.system.entity.SysLog;
import com.water.system.entity.dto.LogQueryRequest;
/**
* 日志查询服务接口
*/
public interface LogService {
/** 分页查询日志 */
IPage<SysLog> pageLogs(LogQueryRequest request);
/** 获取日志详情 */
SysLog getLogById(Long id);
/** 记录日志 */
void saveLog(SysLog log);
}
@@ -0,0 +1,35 @@
package com.water.system.service;
import com.water.system.entity.SysRole;
import com.water.system.entity.dto.RoleRequest;
import java.util.List;
/**
* 角色管理服务接口
*/
public interface RoleService {
/** 查询所有角色 */
List<SysRole> listRoles();
/** 根据ID获取角色详情 */
SysRole getRoleById(Long id);
/** 创建角色 */
Long createRole(RoleRequest request);
/** 更新角色 */
void updateRole(Long id, RoleRequest request);
/** 删除角色 */
void deleteRole(Long id);
/** 分配权限 */
void assignPermissions(Long id, List<String> permissions);
/** 根据用户ID查询角色 */
List<SysRole> getRolesByUserId(Long userId);
/** 根据最大级别查询角色 */
List<SysRole> getRolesByMaxLevel(Integer level);
}
@@ -1,67 +1,76 @@
package com.water.system.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.system.entity.*;
import com.water.system.mapper.*;
import com.water.system.entity.dto.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.*;
@Service @RequiredArgsConstructor
/**
* 系统管理兼容服务 - 保持向后兼容,内部委托给新的专用服务
*/
@Service
@RequiredArgsConstructor
public class SysService {
private final SysRoleMapper roleMapper;
private final SysUserMapper userMapper;
private final RoleService roleService;
private final UserService userService;
private final DepartmentService departmentService;
private final LogService logService;
private final SysMenuMapper menuMapper;
private final SysDepartmentMapper deptMapper;
private final SysLogMapper logMapper;
// Roles
public List<SysRole> listRoles() { return roleMapper.selectList(null); }
public List<SysRole> listRoles() { return roleService.listRoles(); }
public Long createRole(Map<String,Object> req) {
SysRole r = new SysRole();
r.setRoleCode((String)req.get("roleCode"));
r.setRoleName((String)req.get("roleName"));
r.setDescription((String)req.get("description"));
r.setStatus(1); r.setSort(0);
roleMapper.insert(r);
return r.getId();
RoleRequest r = new RoleRequest();
r.setRoleCode((String) req.get("roleCode"));
r.setRoleName((String) req.get("roleName"));
r.setDescription((String) req.get("description"));
r.setLevel(req.get("level") != null ? ((Number) req.get("level")).intValue() : 4);
r.setDataScope((String) req.get("dataScope"));
return roleService.createRole(r);
}
// Users
public List<SysUser> listUsers(Integer status) {
return userMapper.selectList(new LambdaQueryWrapper<SysUser>()
.eq(status != null, SysUser::getStatus, status));
return userService.pageUsers(1, 1000, null, status, null).getRecords();
}
public Long createUser(Map<String,Object> req) {
SysUser u = new SysUser();
u.setUsername((String)req.get("username"));
u.setPassword((String)req.get("password"));
u.setRealName((String)req.get("realName"));
u.setPhone((String)req.get("phone"));
u.setStatus(1);
userMapper.insert(u);
return u.getId();
UserCreateRequest r = new UserCreateRequest();
r.setUsername((String) req.get("username"));
r.setPassword((String) req.get("password"));
r.setRealName((String) req.get("realName"));
r.setPhone((String) req.get("phone"));
r.setEmail((String) req.get("email"));
if (req.get("deptId") != null) r.setDeptId(((Number) req.get("deptId")).longValue());
if (req.get("roleId") != null) r.setRoleId(((Number) req.get("roleId")).longValue());
return userService.createUser(r);
}
public void updateUserStatus(Long id, int status) {
SysUser u = userMapper.selectById(id);
if (u == null) throw new RuntimeException("用户不存在");
u.setStatus(status);
userMapper.updateById(u);
userService.updateStatus(id, status);
}
// Menus
public List<SysMenu> listMenus() { return menuMapper.selectList(null); }
// Departments
public List<SysDepartment> listDepts() { return deptMapper.selectList(null); }
public List<SysDepartment> listDepts() { return departmentService.getDeptTree(); }
public Long createDept(Map<String,Object> req) {
SysDepartment d = new SysDepartment();
d.setDeptName((String)req.get("deptName"));
d.setLeader((String)req.get("leader"));
d.setSort(0); d.setStatus(1);
deptMapper.insert(d);
return d.getId();
DeptRequest r = new DeptRequest();
r.setDeptName((String) req.get("deptName"));
r.setLeader((String) req.get("leader"));
r.setPhone((String) req.get("phone"));
if (req.get("parentId") != null) r.setParentId(((Number) req.get("parentId")).longValue());
return departmentService.createDept(r);
}
// Logs
public List<SysLog> listLogs(String logType, String username) {
LambdaQueryWrapper<SysLog> w = new LambdaQueryWrapper<>();
if (logType != null) w.eq(SysLog::getLogType, logType);
if (username != null) w.like(SysLog::getUsername, username);
return logMapper.selectList(w.orderByDesc(SysLog::getCreatedTime).last("LIMIT 100"));
LogQueryRequest r = new LogQueryRequest();
r.setLogType(logType);
r.setUsername(username);
r.setPageNum(1);
r.setPageSize(100);
return logService.pageLogs(r).getRecords();
}
}
@@ -0,0 +1,37 @@
package com.water.system.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.water.system.entity.SysUser;
import com.water.system.entity.dto.UserCreateRequest;
import com.water.system.entity.dto.UserUpdateRequest;
import java.util.List;
/**
* 用户管理服务接口
*/
public interface UserService {
/** 分页查询用户列表 */
IPage<SysUser> pageUsers(Integer pageNum, Integer pageSize, String keyword, Integer status, Long deptId);
/** 获取用户详情 */
SysUser getUserById(Long id);
/** 创建用户 */
Long createUser(UserCreateRequest request);
/** 更新用户 */
void updateUser(Long id, UserUpdateRequest request);
/** 删除用户 */
void deleteUser(Long id);
/** 停用/启用用户 */
void updateStatus(Long id, Integer status);
/** 重置密码 */
void resetPassword(Long id, String newPassword);
/** 根据用户名查询 */
SysUser getByUsername(String username);
}
@@ -0,0 +1,102 @@
package com.water.system.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.common.core.exception.BusinessException;
import com.water.system.entity.SysDepartment;
import com.water.system.entity.dto.DeptRequest;
import com.water.system.mapper.SysDepartmentMapper;
import com.water.system.service.DepartmentService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class DepartmentServiceImpl implements DepartmentService {
private final SysDepartmentMapper deptMapper;
@Override
public List<SysDepartment> getDeptTree() {
List<SysDepartment> allDepts = deptMapper.selectList(
new LambdaQueryWrapper<SysDepartment>().orderByAsc(SysDepartment::getSort)
);
return buildTree(allDepts, 0L);
}
@Override
public SysDepartment getDeptById(Long id) {
SysDepartment dept = deptMapper.selectById(id);
if (dept == null) throw new BusinessException("部门不存在");
return dept;
}
@Override
public Long createDept(DeptRequest request) {
SysDepartment dept = new SysDepartment();
dept.setDeptName(request.getDeptName());
dept.setLeader(request.getLeader());
dept.setPhone(request.getPhone());
dept.setSort(request.getSort() != null ? request.getSort() : 0);
dept.setStatus(request.getStatus() != null ? request.getStatus() : 1);
Long parentId = request.getParentId() != null ? request.getParentId() : 0L;
dept.setParentId(parentId);
// 设置层级路径
if (parentId > 0) {
SysDepartment parent = getDeptById(parentId);
String ancestors = parent.getAncestors() != null ? parent.getAncestors() + "," + parentId : String.valueOf(parentId);
dept.setAncestors(ancestors);
} else {
dept.setAncestors("0");
}
deptMapper.insert(dept);
return dept.getId();
}
@Override
public void updateDept(Long id, DeptRequest request) {
SysDepartment dept = getDeptById(id);
if (request.getDeptName() != null) dept.setDeptName(request.getDeptName());
if (request.getLeader() != null) dept.setLeader(request.getLeader());
if (request.getPhone() != null) dept.setPhone(request.getPhone());
if (request.getSort() != null) dept.setSort(request.getSort());
if (request.getStatus() != null) dept.setStatus(request.getStatus());
deptMapper.updateById(dept);
}
@Override
public void deleteDept(Long id) {
// 检查是否有子部门
long childCount = deptMapper.selectCount(
new LambdaQueryWrapper<SysDepartment>().eq(SysDepartment::getParentId, id)
);
if (childCount > 0) {
throw new BusinessException("该部门下有子部门,不允许删除");
}
deptMapper.deleteById(id);
}
@Override
public List<SysDepartment> getChildDepts(Long parentId) {
SysDepartment parent = getDeptById(parentId);
String ancestorPath = parent.getAncestors() != null ? parent.getAncestors() + "," + parentId : String.valueOf(parentId);
return deptMapper.selectByAncestorPath(ancestorPath);
}
private List<SysDepartment> buildTree(List<SysDepartment> depts, Long parentId) {
Map<Long, List<SysDepartment>> grouped = depts.stream()
.collect(Collectors.groupingBy(SysDepartment::getParentId));
List<SysDepartment> roots = grouped.getOrDefault(parentId, new ArrayList<>());
for (SysDepartment root : roots) {
root.setChildren(buildTree(depts, root.getId()));
}
return roots;
}
}
@@ -0,0 +1,116 @@
package com.water.system.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.common.core.exception.BusinessException;
import com.water.system.entity.DataDictionary;
import com.water.system.entity.DataDictionaryItem;
import com.water.system.entity.dto.DictItemRequest;
import com.water.system.entity.dto.DictRequest;
import com.water.system.mapper.DataDictionaryMapper;
import com.water.system.mapper.DataDictionaryItemMapper;
import com.water.system.service.DictionaryService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class DictionaryServiceImpl implements DictionaryService {
private final DataDictionaryMapper dictMapper;
private final DataDictionaryItemMapper itemMapper;
@Override
public List<DataDictionary> listDictionaries(String keyword) {
LambdaQueryWrapper<DataDictionary> wrapper = new LambdaQueryWrapper<>();
if (keyword != null && !keyword.isBlank()) {
wrapper.like(DataDictionary::getDictCode, keyword)
.or().like(DataDictionary::getDictName, keyword);
}
wrapper.orderByAsc(DataDictionary::getDictCode);
return dictMapper.selectList(wrapper);
}
@Override
public DataDictionary getDictionaryById(Long id) {
DataDictionary dict = dictMapper.selectById(id);
if (dict == null) throw new BusinessException("字典不存在");
return dict;
}
@Override
public Long createDictionary(DictRequest request) {
DataDictionary existing = dictMapper.selectByDictCode(request.getDictCode());
if (existing != null) throw new BusinessException("字典编码已存在");
DataDictionary dict = new DataDictionary();
dict.setDictCode(request.getDictCode());
dict.setDictName(request.getDictName());
dict.setDescription(request.getDescription());
dict.setStatus(request.getStatus() != null ? request.getStatus() : 1);
dictMapper.insert(dict);
return dict.getId();
}
@Override
public void updateDictionary(Long id, DictRequest request) {
DataDictionary dict = getDictionaryById(id);
if (request.getDictName() != null) dict.setDictName(request.getDictName());
if (request.getDescription() != null) dict.setDescription(request.getDescription());
if (request.getStatus() != null) dict.setStatus(request.getStatus());
dictMapper.updateById(dict);
}
@Override
public void deleteDictionary(Long id) {
// 同时删除字典项
itemMapper.delete(new LambdaQueryWrapper<DataDictionaryItem>()
.eq(DataDictionaryItem::getDictId, id));
dictMapper.deleteById(id);
}
@Override
public List<DataDictionaryItem> listItemsByDictId(Long dictId) {
return itemMapper.selectList(
new LambdaQueryWrapper<DataDictionaryItem>()
.eq(DataDictionaryItem::getDictId, dictId)
.orderByAsc(DataDictionaryItem::getSort)
);
}
@Override
public List<DataDictionaryItem> listItemsByDictCode(String dictCode) {
return itemMapper.selectByDictCode(dictCode);
}
@Override
public Long createItem(DictItemRequest request) {
DataDictionaryItem item = new DataDictionaryItem();
item.setDictId(request.getDictId());
item.setItemCode(request.getItemCode());
item.setItemName(request.getItemName());
item.setItemValue(request.getItemValue());
item.setSort(request.getSort() != null ? request.getSort() : 0);
item.setStatus(request.getStatus() != null ? request.getStatus() : 1);
item.setRemark(request.getRemark());
itemMapper.insert(item);
return item.getId();
}
@Override
public void updateItem(Long id, DictItemRequest request) {
DataDictionaryItem item = itemMapper.selectById(id);
if (item == null) throw new BusinessException("字典项不存在");
if (request.getItemName() != null) item.setItemName(request.getItemName());
if (request.getItemValue() != null) item.setItemValue(request.getItemValue());
if (request.getSort() != null) item.setSort(request.getSort());
if (request.getStatus() != null) item.setStatus(request.getStatus());
if (request.getRemark() != null) item.setRemark(request.getRemark());
itemMapper.updateById(item);
}
@Override
public void deleteItem(Long id) {
itemMapper.deleteById(id);
}
}
@@ -0,0 +1,62 @@
package com.water.system.service.impl;
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.system.entity.SysLog;
import com.water.system.entity.dto.LogQueryRequest;
import com.water.system.mapper.SysLogMapper;
import com.water.system.service.LogService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Service
@RequiredArgsConstructor
public class LogServiceImpl implements LogService {
private final SysLogMapper logMapper;
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Override
public IPage<SysLog> pageLogs(LogQueryRequest request) {
Page<SysLog> page = new Page<>(request.getPageNum(), request.getPageSize());
LambdaQueryWrapper<SysLog> wrapper = new LambdaQueryWrapper<>();
if (request.getLogType() != null && !request.getLogType().isBlank()) {
wrapper.eq(SysLog::getLogType, request.getLogType());
}
if (request.getUsername() != null && !request.getUsername().isBlank()) {
wrapper.like(SysLog::getUsername, request.getUsername());
}
if (request.getModule() != null && !request.getModule().isBlank()) {
wrapper.eq(SysLog::getModule, request.getModule());
}
if (request.getStatus() != null) {
wrapper.eq(SysLog::getStatus, request.getStatus());
}
if (request.getStartTime() != null && !request.getStartTime().isBlank()) {
LocalDateTime start = LocalDate.parse(request.getStartTime(), DATE_FMT).atStartOfDay();
wrapper.ge(SysLog::getCreatedTime, start);
}
if (request.getEndTime() != null && !request.getEndTime().isBlank()) {
LocalDateTime end = LocalDate.parse(request.getEndTime(), DATE_FMT).plusDays(1).atStartOfDay();
wrapper.lt(SysLog::getCreatedTime, end);
}
wrapper.orderByDesc(SysLog::getCreatedTime);
return logMapper.selectPage(page, wrapper);
}
@Override
public SysLog getLogById(Long id) {
return logMapper.selectById(id);
}
@Override
public void saveLog(SysLog log) {
logMapper.insert(log);
}
}
@@ -0,0 +1,103 @@
package com.water.system.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.common.core.exception.BusinessException;
import com.water.system.entity.SysRole;
import com.water.system.entity.dto.RoleRequest;
import com.water.system.mapper.SysRoleMapper;
import com.water.system.service.RoleService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class RoleServiceImpl implements RoleService {
private final SysRoleMapper roleMapper;
private final ObjectMapper objectMapper;
@Override
public List<SysRole> listRoles() {
return roleMapper.selectList(
new LambdaQueryWrapper<SysRole>().orderByAsc(SysRole::getLevel, SysRole::getSort)
);
}
@Override
public SysRole getRoleById(Long id) {
SysRole role = roleMapper.selectById(id);
if (role == null) throw new BusinessException("角色不存在");
return role;
}
@Override
public Long createRole(RoleRequest request) {
// 检查编码唯一
long count = roleMapper.selectCount(
new LambdaQueryWrapper<SysRole>().eq(SysRole::getRoleCode, request.getRoleCode())
);
if (count > 0) throw new BusinessException("角色编码已存在");
SysRole role = new SysRole();
role.setRoleCode(request.getRoleCode());
role.setRoleName(request.getRoleName());
role.setDescription(request.getDescription());
role.setLevel(request.getLevel());
role.setDataScope(request.getDataScope() != null ? request.getDataScope() : "SELF");
role.setPermissions(toJson(request.getPermissions()));
role.setStatus(1);
role.setSort(request.getSort() != null ? request.getSort() : 0);
roleMapper.insert(role);
return role.getId();
}
@Override
public void updateRole(Long id, RoleRequest request) {
SysRole role = getRoleById(id);
role.setRoleName(request.getRoleName());
role.setDescription(request.getDescription());
role.setLevel(request.getLevel());
if (request.getDataScope() != null) role.setDataScope(request.getDataScope());
if (request.getPermissions() != null) role.setPermissions(toJson(request.getPermissions()));
if (request.getSort() != null) role.setSort(request.getSort());
roleMapper.updateById(role);
}
@Override
public void deleteRole(Long id) {
SysRole role = getRoleById(id);
if (role.getLevel() != null && role.getLevel() <= 2) {
throw new BusinessException("超级管理员和系统管理员角色不允许删除");
}
roleMapper.deleteById(id);
}
@Override
public void assignPermissions(Long id, List<String> permissions) {
SysRole role = getRoleById(id);
role.setPermissions(toJson(permissions));
roleMapper.updateById(role);
}
@Override
public List<SysRole> getRolesByUserId(Long userId) {
return roleMapper.selectRolesByUserId(userId);
}
@Override
public List<SysRole> getRolesByMaxLevel(Integer level) {
return roleMapper.selectRolesByMaxLevel(level);
}
private String toJson(Object obj) {
if (obj == null) return null;
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new BusinessException("JSON序列化失败: " + e.getMessage());
}
}
}
@@ -0,0 +1,115 @@
package com.water.system.service.impl;
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.system.entity.SysUser;
import com.water.system.entity.dto.UserCreateRequest;
import com.water.system.entity.dto.UserUpdateRequest;
import com.water.system.mapper.SysUserMapper;
import com.water.system.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import java.nio.charset.StandardCharsets;
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final SysUserMapper userMapper;
@Override
public IPage<SysUser> pageUsers(Integer pageNum, Integer pageSize, String keyword, Integer status, Long deptId) {
Page<SysUser> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
if (keyword != null && !keyword.isBlank()) {
wrapper.and(w -> w.like(SysUser::getUsername, keyword)
.or().like(SysUser::getRealName, keyword)
.or().like(SysUser::getPhone, keyword));
}
if (status != null) wrapper.eq(SysUser::getStatus, status);
if (deptId != null) wrapper.eq(SysUser::getDeptId, deptId);
wrapper.orderByDesc(SysUser::getCreatedTime);
IPage<SysUser> result = userMapper.selectPage(page, wrapper);
// 清除密码字段
result.getRecords().forEach(u -> u.setPassword(null));
return result;
}
@Override
public SysUser getUserById(Long id) {
SysUser user = userMapper.selectById(id);
if (user == null) throw new BusinessException("用户不存在");
user.setPassword(null);
return user;
}
@Override
public Long createUser(UserCreateRequest request) {
// 检查用户名唯一
SysUser existing = userMapper.selectByUsername(request.getUsername());
if (existing != null) throw new BusinessException("用户名已存在");
SysUser user = new SysUser();
user.setUsername(request.getUsername());
user.setPassword(encryptPassword(request.getPassword()));
user.setRealName(request.getRealName());
user.setPhone(request.getPhone());
user.setEmail(request.getEmail());
user.setDeptId(request.getDeptId());
user.setRoleId(request.getRoleId());
user.setStatus(1);
userMapper.insert(user);
return user.getId();
}
@Override
public void updateUser(Long id, UserUpdateRequest request) {
SysUser user = userMapper.selectById(id);
if (user == null) throw new BusinessException("用户不存在");
if (request.getRealName() != null) user.setRealName(request.getRealName());
if (request.getPhone() != null) user.setPhone(request.getPhone());
if (request.getEmail() != null) user.setEmail(request.getEmail());
if (request.getDeptId() != null) user.setDeptId(request.getDeptId());
if (request.getRoleId() != null) user.setRoleId(request.getRoleId());
userMapper.updateById(user);
}
@Override
public void deleteUser(Long id) {
SysUser user = userMapper.selectById(id);
if (user == null) throw new BusinessException("用户不存在");
if ("admin".equals(user.getUsername())) {
throw new BusinessException("系统管理员账号不允许删除");
}
userMapper.deleteById(id);
}
@Override
public void updateStatus(Long id, Integer status) {
SysUser user = userMapper.selectById(id);
if (user == null) throw new BusinessException("用户不存在");
user.setStatus(status);
userMapper.updateById(user);
}
@Override
public void resetPassword(Long id, String newPassword) {
SysUser user = userMapper.selectById(id);
if (user == null) throw new BusinessException("用户不存在");
user.setPassword(encryptPassword(newPassword));
userMapper.updateById(user);
}
@Override
public SysUser getByUsername(String username) {
return userMapper.selectByUsername(username);
}
private String encryptPassword(String password) {
return DigestUtils.md5DigestAsHex(password.getBytes(StandardCharsets.UTF_8));
}
}
@@ -0,0 +1,136 @@
-- V2__system_enhance.sql
-- 系统管理增强: 角色/用户/部门/日志/字典
-- 1. 增强 sys_role 表: 新增 level, data_scope, permissions, updated_time 字段
ALTER TABLE sys_role ADD COLUMN IF NOT EXISTS level INT DEFAULT 4;
ALTER TABLE sys_role ADD COLUMN IF NOT EXISTS data_scope VARCHAR(20) DEFAULT 'SELF';
ALTER TABLE sys_role ADD COLUMN IF NOT EXISTS permissions TEXT;
ALTER TABLE sys_role ADD COLUMN IF NOT EXISTS updated_time TIMESTAMP DEFAULT NOW();
ALTER TABLE sys_role ADD COLUMN IF NOT EXISTS deleted INT DEFAULT 0;
COMMENT ON COLUMN sys_role.level IS '角色级别: 1-超级管理员 2-系统管理员 3-部门经理 4-普通员工 5-访客';
COMMENT ON COLUMN sys_role.data_scope IS '数据权限: ALL/DEPT/DEPT_BELOW/SELF';
COMMENT ON COLUMN sys_role.permissions IS '权限配置(JSON数组)';
-- 2. 增强 sys_user 表: 新增 role_id, last_login_time, last_login_ip, deleted
ALTER TABLE sys_user ADD COLUMN IF NOT EXISTS role_id BIGINT;
ALTER TABLE sys_user ADD COLUMN IF NOT EXISTS last_login_time TIMESTAMP;
ALTER TABLE sys_user ADD COLUMN IF NOT EXISTS last_login_ip VARCHAR(50);
ALTER TABLE sys_user ADD COLUMN IF NOT EXISTS deleted INT DEFAULT 0;
-- 3. 增强 sys_department 表: 新增 ancestors, created_time, updated_time, deleted
ALTER TABLE sys_department ADD COLUMN IF NOT EXISTS ancestors VARCHAR(500) DEFAULT '0';
ALTER TABLE sys_department ADD COLUMN IF NOT EXISTS created_time TIMESTAMP DEFAULT NOW();
ALTER TABLE sys_department ADD COLUMN IF NOT EXISTS updated_time TIMESTAMP DEFAULT NOW();
ALTER TABLE sys_department ADD COLUMN IF NOT EXISTS deleted INT DEFAULT 0;
COMMENT ON COLUMN sys_department.ancestors IS '层级路径(如: 0,1,5)';
-- 4. 增强 sys_log 表: 新增 status, error_msg
ALTER TABLE sys_log ADD COLUMN IF NOT EXISTS status INT DEFAULT 1;
ALTER TABLE sys_log ADD COLUMN IF NOT EXISTS error_msg TEXT;
COMMENT ON COLUMN sys_log.log_type IS '日志类型: LOGIN/OPERATE/EXCEPTION';
COMMENT ON COLUMN sys_log.status IS '状态: 0-失败 1-成功';
-- 5. 新增数据字典表
CREATE TABLE IF NOT EXISTS sys_data_dictionary (
id BIGSERIAL PRIMARY KEY,
dict_code VARCHAR(100) UNIQUE NOT NULL,
dict_name VARCHAR(200) NOT NULL,
description VARCHAR(500),
status INT DEFAULT 1,
deleted INT DEFAULT 0,
created_time TIMESTAMP DEFAULT NOW(),
updated_time TIMESTAMP DEFAULT NOW()
);
COMMENT ON TABLE sys_data_dictionary IS '数据字典表';
COMMENT ON COLUMN sys_data_dictionary.dict_code IS '字典编码(唯一)';
COMMENT ON COLUMN sys_data_dictionary.dict_name IS '字典名称';
-- 6. 新增数据字典项表
CREATE TABLE IF NOT EXISTS sys_data_dictionary_item (
id BIGSERIAL PRIMARY KEY,
dict_id BIGINT NOT NULL,
item_code VARCHAR(100) NOT NULL,
item_name VARCHAR(200) NOT NULL,
item_value VARCHAR(500),
sort INT DEFAULT 0,
status INT DEFAULT 1,
remark VARCHAR(500),
deleted INT DEFAULT 0,
created_time TIMESTAMP DEFAULT NOW(),
updated_time TIMESTAMP DEFAULT NOW()
);
COMMENT ON TABLE sys_data_dictionary_item IS '数据字典项表';
COMMENT ON COLUMN sys_data_dictionary_item.dict_id IS '所属字典ID';
COMMENT ON COLUMN sys_data_dictionary_item.item_code IS '字典项编码';
COMMENT ON COLUMN sys_data_dictionary_item.item_name IS '字典项名称';
COMMENT ON COLUMN sys_data_dictionary_item.item_value IS '字典项值';
-- 索引
CREATE INDEX IF NOT EXISTS idx_dict_item_dict_id ON sys_data_dictionary_item(dict_id);
CREATE INDEX IF NOT EXISTS idx_dict_item_code ON sys_data_dictionary_item(item_code);
CREATE INDEX IF NOT EXISTS idx_sys_user_dept_id ON sys_user(dept_id);
CREATE INDEX IF NOT EXISTS idx_sys_user_role_id ON sys_user(role_id);
CREATE INDEX IF NOT EXISTS idx_sys_user_status ON sys_user(status);
CREATE INDEX IF NOT EXISTS idx_sys_role_level ON sys_role(level);
CREATE INDEX IF NOT EXISTS idx_sys_dept_parent_id ON sys_department(parent_id);
CREATE INDEX IF NOT EXISTS idx_sys_dept_ancestors ON sys_department(ancestors);
CREATE INDEX IF NOT EXISTS idx_sys_log_type ON sys_log(log_type);
CREATE INDEX IF NOT EXISTS idx_sys_log_user ON sys_log(username);
CREATE INDEX IF NOT EXISTS idx_sys_log_created ON sys_log(created_time);
CREATE INDEX IF NOT EXISTS idx_sys_log_module ON sys_log(module);
CREATE INDEX IF NOT EXISTS idx_sys_log_status ON sys_log(status);
-- 初始化5级角色数据
INSERT INTO sys_role (role_code, role_name, description, level, data_scope, sort, created_time) VALUES
('SUPER_ADMIN', '超级管理员', '拥有系统所有权限', 1, 'ALL', 1, NOW()),
('SYS_ADMIN', '系统管理员', '系统管理权限', 2, 'ALL', 2, NOW()),
('DEPT_MANAGER', '部门经理', '本部门管理权限', 3, 'DEPT', 3, NOW()),
('EMPLOYEE', '普通员工', '基础操作权限', 4, 'SELF', 4, NOW()),
('GUEST', '访客', '仅查看权限', 5, 'SELF', 5, NOW())
ON CONFLICT (role_code) DO NOTHING;
-- 初始化数据字典示例
INSERT INTO sys_data_dictionary (dict_code, dict_name, description, status, created_time) VALUES
('sys_user_status', '用户状态', '用户账号状态枚举', 1, NOW()),
('sys_log_type', '日志类型', '系统日志类型枚举', 1, NOW()),
('sys_role_level', '角色级别', '系统角色级别枚举', 1, NOW())
ON CONFLICT (dict_code) DO NOTHING;
INSERT INTO sys_data_dictionary_item (dict_id, item_code, item_name, item_value, sort, status, created_time)
SELECT d.id, item_code, item_name, item_value, sort, 1, NOW()
FROM sys_data_dictionary d
CROSS JOIN (VALUES
('sys_user_status', 'DISABLED', '停用', '0', 1),
('sys_user_status', 'ENABLED', '正常', '1', 2)
) AS v(dict_code, item_code, item_name, item_value, sort)
WHERE d.dict_code = v.dict_code
AND NOT EXISTS (SELECT 1 FROM sys_data_dictionary_item i WHERE i.dict_id = d.id AND i.item_code = v.item_code);
INSERT INTO sys_data_dictionary_item (dict_id, item_code, item_name, item_value, sort, status, created_time)
SELECT d.id, item_code, item_name, item_value, sort, 1, NOW()
FROM sys_data_dictionary d
CROSS JOIN (VALUES
('sys_log_type', 'LOGIN', '登录日志', 'LOGIN', 1),
('sys_log_type', 'OPERATE', '操作日志', 'OPERATE', 2),
('sys_log_type', 'EXCEPTION', '异常日志', 'EXCEPTION', 3)
) AS v(dict_code, item_code, item_name, item_value, sort)
WHERE d.dict_code = v.dict_code
AND NOT EXISTS (SELECT 1 FROM sys_data_dictionary_item i WHERE i.dict_id = d.id AND i.item_code = v.item_code);
INSERT INTO sys_data_dictionary_item (dict_id, item_code, item_name, item_value, sort, status, created_time)
SELECT d.id, item_code, item_name, item_value, sort, 1, NOW()
FROM sys_data_dictionary d
CROSS JOIN (VALUES
('sys_role_level', 'LEVEL_1', '超级管理员', '1', 1),
('sys_role_level', 'LEVEL_2', '系统管理员', '2', 2),
('sys_role_level', 'LEVEL_3', '部门经理', '3', 3),
('sys_role_level', 'LEVEL_4', '普通员工', '4', 4),
('sys_role_level', 'LEVEL_5', '访客', '5', 5)
) AS v(dict_code, item_code, item_name, item_value, sort)
WHERE d.dict_code = v.dict_code
AND NOT EXISTS (SELECT 1 FROM sys_data_dictionary_item i WHERE i.dict_id = d.id AND i.item_code = v.item_code);
@@ -0,0 +1,93 @@
package com.water.system.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.common.core.exception.BusinessException;
import com.water.system.entity.SysDepartment;
import com.water.system.entity.dto.DeptRequest;
import com.water.system.mapper.SysDepartmentMapper;
import com.water.system.service.impl.DepartmentServiceImpl;
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 java.util.Arrays;
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 DepartmentServiceImplTest {
@Mock
private SysDepartmentMapper deptMapper;
@InjectMocks
private DepartmentServiceImpl departmentService;
@Test
void getDeptTree_shouldBuildTree() {
SysDepartment root = new SysDepartment();
root.setId(1L);
root.setParentId(0L);
root.setDeptName("总公司");
SysDepartment child1 = new SysDepartment();
child1.setId(2L);
child1.setParentId(1L);
child1.setDeptName("技术部");
SysDepartment child2 = new SysDepartment();
child2.setId(3L);
child2.setParentId(1L);
child2.setDeptName("市场部");
when(deptMapper.selectList(any(LambdaQueryWrapper.class)))
.thenReturn(Arrays.asList(root, child1, child2));
List<SysDepartment> tree = departmentService.getDeptTree();
assertEquals(1, tree.size());
assertEquals("总公司", tree.get(0).getDeptName());
assertEquals(2, tree.get(0).getChildren().size());
}
@Test
void createDept_shouldSetAncestors() {
SysDepartment parent = new SysDepartment();
parent.setId(1L);
parent.setAncestors("0");
parent.setDeptName("总公司");
when(deptMapper.selectById(1L)).thenReturn(parent);
when(deptMapper.insert(any(SysDepartment.class))).thenAnswer(inv -> {
SysDepartment d = inv.getArgument(0);
d.setId(10L);
assertEquals("0,1", d.getAncestors());
return 1;
});
DeptRequest request = new DeptRequest();
request.setDeptName("研发部");
request.setParentId(1L);
Long id = departmentService.createDept(request);
assertEquals(10L, id);
}
@Test
void deleteDept_shouldFailWithChildren() {
when(deptMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(2L);
assertThrows(BusinessException.class, () -> departmentService.deleteDept(1L));
}
@Test
void deleteDept_shouldSucceedWithoutChildren() {
when(deptMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(0L);
assertDoesNotThrow(() -> departmentService.deleteDept(5L));
verify(deptMapper).deleteById(5L);
}
}
@@ -0,0 +1,104 @@
package com.water.system.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.common.core.exception.BusinessException;
import com.water.system.entity.DataDictionary;
import com.water.system.entity.DataDictionaryItem;
import com.water.system.entity.dto.DictItemRequest;
import com.water.system.entity.dto.DictRequest;
import com.water.system.mapper.DataDictionaryMapper;
import com.water.system.mapper.DataDictionaryItemMapper;
import com.water.system.service.impl.DictionaryServiceImpl;
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 java.util.Arrays;
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 DictionaryServiceImplTest {
@Mock
private DataDictionaryMapper dictMapper;
@Mock
private DataDictionaryItemMapper itemMapper;
@InjectMocks
private DictionaryServiceImpl dictionaryService;
@Test
void createDictionary_shouldSucceed() {
DictRequest request = new DictRequest();
request.setDictCode("water_quality_level");
request.setDictName("水质等级");
when(dictMapper.selectByDictCode("water_quality_level")).thenReturn(null);
when(dictMapper.insert(any(DataDictionary.class))).thenAnswer(inv -> {
DataDictionary d = inv.getArgument(0);
d.setId(10L);
return 1;
});
Long id = dictionaryService.createDictionary(request);
assertEquals(10L, id);
}
@Test
void createDictionary_shouldFailOnDuplicate() {
DictRequest request = new DictRequest();
request.setDictCode("sys_user_status");
DataDictionary existing = new DataDictionary();
existing.setDictCode("sys_user_status");
when(dictMapper.selectByDictCode("sys_user_status")).thenReturn(existing);
assertThrows(BusinessException.class, () -> dictionaryService.createDictionary(request));
}
@Test
void listItemsByDictCode_shouldReturnItems() {
DataDictionaryItem item1 = new DataDictionaryItem();
item1.setItemCode("ENABLED");
item1.setItemName("正常");
DataDictionaryItem item2 = new DataDictionaryItem();
item2.setItemCode("DISABLED");
item2.setItemName("停用");
when(itemMapper.selectByDictCode("sys_user_status")).thenReturn(Arrays.asList(item1, item2));
List<DataDictionaryItem> items = dictionaryService.listItemsByDictCode("sys_user_status");
assertEquals(2, items.size());
}
@Test
void deleteDictionary_shouldDeleteItemsToo() {
dictionaryService.deleteDictionary(1L);
verify(itemMapper).delete(any(LambdaQueryWrapper.class));
verify(dictMapper).deleteById(1L);
}
@Test
void createItem_shouldSucceed() {
DictItemRequest request = new DictItemRequest();
request.setDictId(1L);
request.setItemCode("NEW_ITEM");
request.setItemName("新项");
request.setItemValue("value");
when(itemMapper.insert(any(DataDictionaryItem.class))).thenAnswer(inv -> {
DataDictionaryItem i = inv.getArgument(0);
i.setId(20L);
return 1;
});
Long id = dictionaryService.createItem(request);
assertEquals(20L, id);
}
}
@@ -0,0 +1,93 @@
package com.water.system.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.system.entity.SysLog;
import com.water.system.entity.dto.LogQueryRequest;
import com.water.system.mapper.SysLogMapper;
import com.water.system.service.impl.LogServiceImpl;
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 java.time.LocalDateTime;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class LogServiceImplTest {
@Mock
private SysLogMapper logMapper;
@InjectMocks
private LogServiceImpl logService;
@Test
void pageLogs_shouldReturnPagedResult() {
Page<SysLog> page = new Page<>(1, 10);
SysLog log1 = new SysLog();
log1.setId(1L);
log1.setLogType("LOGIN");
log1.setUsername("admin");
log1.setCreatedTime(LocalDateTime.now());
SysLog log2 = new SysLog();
log2.setId(2L);
log2.setLogType("OPERATE");
log2.setUsername("user1");
log2.setModule("用户管理");
page.setRecords(Arrays.asList(log1, log2));
page.setTotal(2);
when(logMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class))).thenReturn(page);
LogQueryRequest request = new LogQueryRequest();
request.setPageNum(1);
request.setPageSize(10);
IPage<SysLog> result = logService.pageLogs(request);
assertEquals(2, result.getTotal());
assertEquals("LOGIN", result.getRecords().get(0).getLogType());
}
@Test
void pageLogs_withFilters_shouldApplyAllConditions() {
Page<SysLog> page = new Page<>(1, 10);
page.setRecords(Arrays.asList());
page.setTotal(0);
when(logMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class))).thenReturn(page);
LogQueryRequest request = new LogQueryRequest();
request.setLogType("OPERATE");
request.setUsername("admin");
request.setModule("系统管理");
request.setStatus(1);
request.setStartTime("2024-01-01");
request.setEndTime("2024-12-31");
IPage<SysLog> result = logService.pageLogs(request);
assertNotNull(result);
verify(logMapper).selectPage(any(Page.class), any(LambdaQueryWrapper.class));
}
@Test
void saveLog_shouldInsert() {
SysLog log = new SysLog();
log.setLogType("LOGIN");
log.setUsername("testuser");
log.setIp("127.0.0.1");
logService.saveLog(log);
verify(logMapper).insert(log);
}
}
@@ -0,0 +1,115 @@
package com.water.system.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.common.core.exception.BusinessException;
import com.water.system.entity.SysRole;
import com.water.system.entity.dto.RoleRequest;
import com.water.system.mapper.SysRoleMapper;
import com.water.system.service.impl.RoleServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
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 java.util.Arrays;
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 RoleServiceImplTest {
@Mock
private SysRoleMapper roleMapper;
@InjectMocks
private RoleServiceImpl roleService;
private final ObjectMapper objectMapper = new ObjectMapper();
@BeforeEach
void setUp() {
// ObjectMapper is final, inject manually via reflection or just let it be used
}
@Test
void listRoles_shouldReturnAllRoles() {
SysRole admin = new SysRole();
admin.setId(1L);
admin.setRoleCode("SUPER_ADMIN");
admin.setLevel(1);
SysRole user = new SysRole();
user.setId(4L);
user.setRoleCode("EMPLOYEE");
user.setLevel(4);
when(roleMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Arrays.asList(admin, user));
List<SysRole> roles = roleService.listRoles();
assertEquals(2, roles.size());
assertEquals("SUPER_ADMIN", roles.get(0).getRoleCode());
}
@Test
void createRole_shouldSucceed() {
RoleRequest request = new RoleRequest();
request.setRoleCode("TEST_ROLE");
request.setRoleName("测试角色");
request.setLevel(3);
request.setPermissions(Arrays.asList("user:read", "user:write"));
when(roleMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(0L);
when(roleMapper.insert(any(SysRole.class))).thenAnswer(inv -> {
SysRole r = inv.getArgument(0);
r.setId(10L);
return 1;
});
Long id = roleService.createRole(request);
assertEquals(10L, id);
verify(roleMapper).insert(any(SysRole.class));
}
@Test
void createRole_shouldFailOnDuplicateCode() {
RoleRequest request = new RoleRequest();
request.setRoleCode("SUPER_ADMIN");
request.setRoleName("重复角色");
request.setLevel(1);
when(roleMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(1L);
assertThrows(BusinessException.class, () -> roleService.createRole(request));
}
@Test
void deleteRole_shouldFailForAdminRoles() {
SysRole admin = new SysRole();
admin.setId(1L);
admin.setLevel(1);
admin.setRoleCode("SUPER_ADMIN");
when(roleMapper.selectById(1L)).thenReturn(admin);
assertThrows(BusinessException.class, () -> roleService.deleteRole(1L));
}
@Test
void deleteRole_shouldSucceedForNormalRoles() {
SysRole role = new SysRole();
role.setId(5L);
role.setLevel(4);
role.setRoleCode("EMPLOYEE");
when(roleMapper.selectById(5L)).thenReturn(role);
assertDoesNotThrow(() -> roleService.deleteRole(5L));
verify(roleMapper).deleteById(5L);
}
}
@@ -0,0 +1,120 @@
package com.water.system.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.system.entity.SysUser;
import com.water.system.entity.dto.UserCreateRequest;
import com.water.system.entity.dto.UserUpdateRequest;
import com.water.system.mapper.SysUserMapper;
import com.water.system.service.impl.UserServiceImpl;
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 java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {
@Mock
private SysUserMapper userMapper;
@InjectMocks
private UserServiceImpl userService;
@Test
void createUser_shouldSucceed() {
UserCreateRequest request = new UserCreateRequest();
request.setUsername("testuser");
request.setPassword("test123");
request.setRealName("测试用户");
request.setPhone("13800138000");
when(userMapper.selectByUsername("testuser")).thenReturn(null);
when(userMapper.insert(any(SysUser.class))).thenAnswer(inv -> {
SysUser u = inv.getArgument(0);
u.setId(100L);
return 1;
});
Long id = userService.createUser(request);
assertEquals(100L, id);
verify(userMapper).insert(any(SysUser.class));
}
@Test
void createUser_shouldFailOnDuplicateUsername() {
UserCreateRequest request = new UserCreateRequest();
request.setUsername("admin");
request.setPassword("pass");
SysUser existing = new SysUser();
existing.setUsername("admin");
when(userMapper.selectByUsername("admin")).thenReturn(existing);
assertThrows(BusinessException.class, () -> userService.createUser(request));
}
@Test
void updateStatus_shouldSucceed() {
SysUser user = new SysUser();
user.setId(1L);
user.setStatus(1);
when(userMapper.selectById(1L)).thenReturn(user);
userService.updateStatus(1L, 0);
verify(userMapper).updateById(argThat(u -> u.getStatus() == 0));
}
@Test
void deleteUser_shouldFailForAdmin() {
SysUser admin = new SysUser();
admin.setId(1L);
admin.setUsername("admin");
when(userMapper.selectById(1L)).thenReturn(admin);
assertThrows(BusinessException.class, () -> userService.deleteUser(1L));
}
@Test
void resetPassword_shouldSucceed() {
SysUser user = new SysUser();
user.setId(5L);
user.setUsername("testuser");
when(userMapper.selectById(5L)).thenReturn(user);
assertDoesNotThrow(() -> userService.resetPassword(5L, "newPass123"));
verify(userMapper).updateById(argThat(u -> u.getPassword() != null));
}
@Test
void pageUsers_shouldReturnPagedResult() {
Page<SysUser> page = new Page<>(1, 10);
SysUser user1 = new SysUser();
user1.setId(1L);
user1.setUsername("user1");
user1.setPassword("hashed");
SysUser user2 = new SysUser();
user2.setId(2L);
user2.setUsername("user2");
user2.setPassword("hashed");
page.setRecords(Arrays.asList(user1, user2));
page.setTotal(2);
when(userMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class))).thenReturn(page);
IPage<SysUser> result = userService.pageUsers(1, 10, null, null, null);
assertEquals(2, result.getTotal());
// 密码应被清除
assertNull(result.getRecords().get(0).getPassword());
}
}