Compare commits
4
Commits
520b4084eb
...
1870171e45
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1870171e45 | ||
|
|
6c6db59ba9 | ||
|
|
21fa7cffd2 | ||
|
|
51c292f25b |
@@ -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);
|
||||
@@ -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`)
|
||||
}
|
||||
@@ -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>
|
||||
+159
@@ -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);
|
||||
+248
@@ -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());
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
package com.water.production.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.water.common.core.result.R;
|
||||
import com.water.production.entity.DispatchCommand;
|
||||
import com.water.production.entity.DispatchExecution;
|
||||
import com.water.production.entity.DispatchTracking;
|
||||
import com.water.production.service.DispatchCommandService;
|
||||
import com.water.production.service.DispatchTrackingService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "调度指令管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/production/dispatch-command")
|
||||
@RequiredArgsConstructor
|
||||
public class DispatchCommandController {
|
||||
|
||||
private final DispatchCommandService commandService;
|
||||
private final DispatchTrackingService trackingService;
|
||||
|
||||
@Operation(summary = "创建指令")
|
||||
@PostMapping
|
||||
public R<DispatchCommand> create(@RequestBody DispatchCommand command) {
|
||||
return R.ok(commandService.createCommand(command));
|
||||
}
|
||||
|
||||
@Operation(summary = "下发指令")
|
||||
@PostMapping("/{id}/issue")
|
||||
public R<DispatchCommand> issue(@PathVariable Long id,
|
||||
@RequestParam Long issuedBy,
|
||||
@RequestParam(required = false, defaultValue = "system") String operatorName) {
|
||||
return R.ok(commandService.issueCommand(id, issuedBy, operatorName));
|
||||
}
|
||||
|
||||
@Operation(summary = "指令台账(分页)")
|
||||
@GetMapping
|
||||
public R<IPage<Map<String, Object>>> list(
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) String commandType,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate) {
|
||||
return R.ok(commandService.listCommands(page, size, status, commandType, keyword, startDate, endDate));
|
||||
}
|
||||
|
||||
@Operation(summary = "指令详情")
|
||||
@GetMapping("/{id}")
|
||||
public R<Map<String, Object>> detail(@PathVariable Long id) {
|
||||
return R.ok(commandService.getCommandDetail(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "各状态统计")
|
||||
@GetMapping("/stats")
|
||||
public R<List<Map<String, Object>>> stats() {
|
||||
return R.ok(commandService.getStatusStats());
|
||||
}
|
||||
|
||||
@Operation(summary = "接收确认")
|
||||
@PostMapping("/{id}/receive")
|
||||
public R<DispatchExecution> receive(@PathVariable Long id,
|
||||
@RequestParam Long userId,
|
||||
@RequestParam(required = false, defaultValue = "") String userName) {
|
||||
return R.ok(commandService.receiveCommand(id, userId, userName));
|
||||
}
|
||||
|
||||
@Operation(summary = "开始执行")
|
||||
@PostMapping("/{id}/start-execute")
|
||||
public R<DispatchExecution> startExecute(@PathVariable Long id,
|
||||
@RequestParam Long userId,
|
||||
@RequestParam(required = false, defaultValue = "") String userName) {
|
||||
return R.ok(commandService.startExecution(id, userId, userName));
|
||||
}
|
||||
|
||||
@Operation(summary = "完成执行")
|
||||
@PostMapping("/{id}/complete")
|
||||
public R<DispatchExecution> complete(@PathVariable Long id,
|
||||
@RequestParam Long userId,
|
||||
@RequestParam(required = false, defaultValue = "") String userName,
|
||||
@RequestParam(required = false) String feedback,
|
||||
@RequestParam(required = false) String feedbackImages) {
|
||||
return R.ok(commandService.completeExecution(id, userId, userName, feedback, feedbackImages));
|
||||
}
|
||||
|
||||
@Operation(summary = "驳回")
|
||||
@PostMapping("/{id}/reject")
|
||||
public R<DispatchExecution> reject(@PathVariable Long id,
|
||||
@RequestParam Long userId,
|
||||
@RequestParam(required = false, defaultValue = "") String userName,
|
||||
@RequestParam String reason) {
|
||||
return R.ok(commandService.rejectExecution(id, userId, userName, reason));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询追踪日志")
|
||||
@GetMapping("/{id}/tracking")
|
||||
public R<List<DispatchTracking>> trackingLogs(@PathVariable Long id) {
|
||||
return R.ok(trackingService.getTrackingLogs(id));
|
||||
}
|
||||
}
|
||||
@@ -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 >= #{startDate}::timestamp</if>
|
||||
<if test="endDate != null and endDate != ''">AND c.created_at <= #{endDate}::timestamp</if>
|
||||
GROUP BY c.id
|
||||
ORDER BY c.created_at DESC
|
||||
</select>
|
||||
|
||||
<select id="selectCommandDetail" resultType="java.util.Map">
|
||||
SELECT c.*,
|
||||
(SELECT json_agg(json_build_object(
|
||||
'id', e.id, 'userId', e.user_id, 'userName', e.user_name,
|
||||
'executeStatus', e.execute_status, 'receivedAt', e.received_at,
|
||||
'feedback', e.feedback, 'completedAt', e.completed_at,
|
||||
'rejectedReason', e.rejected_reason
|
||||
)) FROM prod_dispatch_execution e WHERE e.command_id = c.id AND e.deleted = 0) AS executions
|
||||
FROM prod_dispatch_command c
|
||||
WHERE c.id = #{commandId} AND c.deleted = 0
|
||||
</select>
|
||||
|
||||
<select id="selectStatusStats" resultType="java.util.Map">
|
||||
SELECT status, COUNT(*) AS count
|
||||
FROM prod_dispatch_command WHERE deleted = 0
|
||||
GROUP BY status
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,159 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import com.water.production.entity.AlertRecord;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* AlertCenterService 报警管理中心单元测试
|
||||
* 测试状态流转、标签映射等核心逻辑
|
||||
*/
|
||||
class AlertCenterServiceTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("测试报警状态标签映射")
|
||||
void testStatusLabelMapping() {
|
||||
// 通过反射或构造测试状态标签的正确性
|
||||
AlertRecord record = new AlertRecord();
|
||||
|
||||
// 测试所有状态值
|
||||
record.setStatus(0);
|
||||
assertEquals(0, record.getStatus());
|
||||
|
||||
record.setStatus(1);
|
||||
assertEquals(1, record.getStatus());
|
||||
|
||||
record.setStatus(2);
|
||||
assertEquals(2, record.getStatus());
|
||||
|
||||
record.setStatus(3);
|
||||
assertEquals(3, record.getStatus());
|
||||
|
||||
record.setStatus(4);
|
||||
assertEquals(4, record.getStatus());
|
||||
|
||||
record.setStatus(5);
|
||||
assertEquals(5, record.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试报警等级标签")
|
||||
void testAlertLevelValues() {
|
||||
AlertRecord record = new AlertRecord();
|
||||
|
||||
record.setAlertLevel("general");
|
||||
assertEquals("general", record.getAlertLevel());
|
||||
|
||||
record.setAlertLevel("important");
|
||||
assertEquals("important", record.getAlertLevel());
|
||||
|
||||
record.setAlertLevel("urgent");
|
||||
assertEquals("urgent", record.getAlertLevel());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试报警记录生命周期字段完整性")
|
||||
void testAlertRecordLifecycleFields() {
|
||||
AlertRecord record = new AlertRecord();
|
||||
record.setId(1L);
|
||||
record.setRuleId(100L);
|
||||
record.setRuleName("管网压力过高报警");
|
||||
record.setDeviceSn("DEV-001");
|
||||
record.setArea("城东片区");
|
||||
record.setMetricKey("pressure");
|
||||
record.setAlertLevel("urgent");
|
||||
record.setTitle("[紧急] 管网压力过高报警 - DEV-001");
|
||||
record.setMessage("设备 DEV-001 指标 pressure 当前值 0.95");
|
||||
record.setStatus(0);
|
||||
record.setConfirmedBy(null);
|
||||
record.setConfirmedTime(null);
|
||||
|
||||
// 验证初始状态
|
||||
assertEquals(0, record.getStatus());
|
||||
assertNull(record.getConfirmedBy());
|
||||
assertNull(record.getAssigneeId());
|
||||
assertNull(record.getHandlerId());
|
||||
assertNull(record.getHandleResult());
|
||||
assertNull(record.getArchiveReason());
|
||||
|
||||
// 模拟确认
|
||||
record.setStatus(1);
|
||||
record.setConfirmedBy(10L);
|
||||
assertEquals(1, record.getStatus());
|
||||
assertEquals(10L, record.getConfirmedBy());
|
||||
|
||||
// 模拟派单
|
||||
record.setStatus(2);
|
||||
record.setAssigneeId(20L);
|
||||
record.setAssigneeName("张三");
|
||||
assertEquals(2, record.getStatus());
|
||||
assertEquals("张三", record.getAssigneeName());
|
||||
|
||||
// 模拟处理中
|
||||
record.setStatus(3);
|
||||
record.setHandlerId(20L);
|
||||
record.setHandlerName("张三");
|
||||
assertEquals(3, record.getStatus());
|
||||
|
||||
// 模拟完成处理
|
||||
record.setStatus(4);
|
||||
record.setHandleResult("已更换压力传感器,恢复正常");
|
||||
assertEquals(4, record.getStatus());
|
||||
assertNotNull(record.getHandleResult());
|
||||
|
||||
// 模拟归档
|
||||
record.setStatus(5);
|
||||
record.setArchiveReason("处理完毕,确认无误");
|
||||
assertEquals(5, record.getStatus());
|
||||
assertNotNull(record.getArchiveReason());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试条件表达式JSON结构验证")
|
||||
void testConditionExpressionStructure() {
|
||||
// 验证AND条件JSON
|
||||
String andExpr = "{\"op\":\"AND\",\"conditions\":[" +
|
||||
"{\"metric\":\"pressure\",\"operator\":\">\",\"threshold\":0.8}," +
|
||||
"{\"metric\":\"pressure\",\"operator\":\"<\",\"threshold\":1.5}" +
|
||||
"]}";
|
||||
assertTrue(andExpr.contains("\"op\":\"AND\""));
|
||||
assertTrue(andExpr.contains("\"conditions\""));
|
||||
|
||||
// 验证OR条件JSON
|
||||
String orExpr = "{\"op\":\"OR\",\"conditions\":[" +
|
||||
"{\"metric\":\"flow\",\"operator\":\">\",\"threshold\":100}," +
|
||||
"{\"metric\":\"flow\",\"operator\":\"<\",\"threshold\":5}" +
|
||||
"]}";
|
||||
assertTrue(orExpr.contains("\"op\":\"OR\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试报警标题格式化")
|
||||
void testAlertTitleFormat() {
|
||||
String level = "urgent";
|
||||
String ruleName = "管网压力过高报警";
|
||||
String deviceSn = "DEV-001";
|
||||
|
||||
String levelLabel = switch (level) {
|
||||
case "urgent" -> "紧急";
|
||||
case "important" -> "重要";
|
||||
case "general" -> "一般";
|
||||
default -> level;
|
||||
};
|
||||
|
||||
String title = String.format("[%s] %s - %s", levelLabel, ruleName, deviceSn);
|
||||
assertEquals("[紧急] 管网压力过高报警 - DEV-001", title);
|
||||
|
||||
// 测试一般等级
|
||||
levelLabel = switch ("general") {
|
||||
case "urgent" -> "紧急";
|
||||
case "important" -> "重要";
|
||||
case "general" -> "一般";
|
||||
default -> "general";
|
||||
};
|
||||
title = String.format("[%s] %s - %s", levelLabel, "余氯偏低报警", "DEV-002");
|
||||
assertEquals("[一般] 余氯偏低报警 - DEV-002", title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* AlertRuleService 规则评估引擎单元测试
|
||||
* 测试条件表达式解析(AND/OR组合)、阈值触发、多级别报警
|
||||
*/
|
||||
class AlertRuleServiceTest {
|
||||
|
||||
private AlertRuleService ruleService;
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
objectMapper = new ObjectMapper();
|
||||
ruleService = new AlertRuleService(null, null, null, objectMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试简单大于条件 - 触发报警")
|
||||
void testSimpleGreaterThan_triggered() {
|
||||
String expr = "{\"metric\":\"pressure\",\"operator\":\">\",\"threshold\":0.8}";
|
||||
assertTrue(ruleService.evaluateCondition(expr, "pressure", 0.9));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试简单大于条件 - 未触发")
|
||||
void testSimpleGreaterThan_notTriggered() {
|
||||
String expr = "{\"metric\":\"pressure\",\"operator\":\">\",\"threshold\":0.8}";
|
||||
assertFalse(ruleService.evaluateCondition(expr, "pressure", 0.7));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试简单小于条件 - 触发报警")
|
||||
void testSimpleLessThan_triggered() {
|
||||
String expr = "{\"metric\":\"residual_chlorine\",\"operator\":\"<\",\"threshold\":0.1}";
|
||||
assertTrue(ruleService.evaluateCondition(expr, "residual_chlorine", 0.05));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试AND组合条件 - 全部满足时触发")
|
||||
void testAndCondition_allMet() {
|
||||
String expr = "{\"op\":\"AND\",\"conditions\":[" +
|
||||
"{\"metric\":\"pressure\",\"operator\":\">\",\"threshold\":0.8}," +
|
||||
"{\"metric\":\"pressure\",\"operator\":\"<\",\"threshold\":1.5}" +
|
||||
"]}";
|
||||
assertTrue(ruleService.evaluateCondition(expr, "pressure", 1.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试AND组合条件 - 部分不满足时不触发")
|
||||
void testAndCondition_partialMet() {
|
||||
String expr = "{\"op\":\"AND\",\"conditions\":[" +
|
||||
"{\"metric\":\"pressure\",\"operator\":\">\",\"threshold\":0.8}," +
|
||||
"{\"metric\":\"pressure\",\"operator\":\"<\",\"threshold\":0.9}" +
|
||||
"]}";
|
||||
assertFalse(ruleService.evaluateCondition(expr, "pressure", 1.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试OR组合条件 - 任一满足即触发")
|
||||
void testOrCondition_oneMet() {
|
||||
String expr = "{\"op\":\"OR\",\"conditions\":[" +
|
||||
"{\"metric\":\"flow\",\"operator\":\">\",\"threshold\":100}," +
|
||||
"{\"metric\":\"flow\",\"operator\":\"<\",\"threshold\":5}" +
|
||||
"]}";
|
||||
// flow=3 < 5, 满足第二个条件
|
||||
assertTrue(ruleService.evaluateCondition(expr, "flow", 3.0));
|
||||
// flow=150 > 100, 满足第一个条件
|
||||
assertTrue(ruleService.evaluateCondition(expr, "flow", 150.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试OR组合条件 - 全不满足时不触发")
|
||||
void testOrCondition_noneMet() {
|
||||
String expr = "{\"op\":\"OR\",\"conditions\":[" +
|
||||
"{\"metric\":\"flow\",\"operator\":\">\",\"threshold\":100}," +
|
||||
"{\"metric\":\"flow\",\"operator\":\"<\",\"threshold\":5}" +
|
||||
"]}";
|
||||
// flow=50, 不满足任何条件
|
||||
assertFalse(ruleService.evaluateCondition(expr, "flow", 50.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试不同指标键不匹配 - 不触发")
|
||||
void testMetricKeyMismatch() {
|
||||
String expr = "{\"metric\":\"pressure\",\"operator\":\">\",\"threshold\":0.8}";
|
||||
// 条件检查pressure, 但实际metric是temperature
|
||||
assertFalse(ruleService.evaluateCondition(expr, "temperature", 50.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试等于条件")
|
||||
void testEqualsCondition() {
|
||||
String expr = "{\"metric\":\"ph\",\"operator\":\"==\",\"threshold\":7.0}";
|
||||
assertTrue(ruleService.evaluateCondition(expr, "ph", 7.0));
|
||||
assertFalse(ruleService.evaluateCondition(expr, "ph", 7.1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试大于等于和小于等于条件")
|
||||
void testGreaterEqualAndLessEqual() {
|
||||
String exprGte = "{\"metric\":\"pressure\",\"operator\":\">=\",\"threshold\":0.8}";
|
||||
assertTrue(ruleService.evaluateCondition(exprGte, "pressure", 0.8));
|
||||
assertTrue(ruleService.evaluateCondition(exprGte, "pressure", 0.9));
|
||||
assertFalse(ruleService.evaluateCondition(exprGte, "pressure", 0.79));
|
||||
|
||||
String exprLte = "{\"metric\":\"pressure\",\"operator\":\"<=\",\"threshold\":0.2}";
|
||||
assertTrue(ruleService.evaluateCondition(exprLte, "pressure", 0.2));
|
||||
assertTrue(ruleService.evaluateCondition(exprLte, "pressure", 0.1));
|
||||
assertFalse(ruleService.evaluateCondition(exprLte, "pressure", 0.21));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试空表达式和无效表达式")
|
||||
void testNullAndInvalidExpression() {
|
||||
assertFalse(ruleService.evaluateCondition(null, "pressure", 0.9));
|
||||
assertFalse(ruleService.evaluateCondition("", "pressure", 0.9));
|
||||
assertFalse(ruleService.evaluateCondition("not-json", "pressure", 0.9));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试多级别报警 - general/important/urgent 条件分别触发")
|
||||
void testMultiLevelAlerts() {
|
||||
// 一般: 余氯 < 0.1
|
||||
String generalExpr = "{\"metric\":\"residual_chlorine\",\"operator\":\"<\",\"threshold\":0.1}";
|
||||
assertTrue(ruleService.evaluateCondition(generalExpr, "residual_chlorine", 0.05));
|
||||
|
||||
// 重要: 压力 < 0.2
|
||||
String importantExpr = "{\"metric\":\"pressure\",\"operator\":\"<\",\"threshold\":0.2}";
|
||||
assertTrue(ruleService.evaluateCondition(importantExpr, "pressure", 0.15));
|
||||
|
||||
// 紧急: 浊度 > 1.0
|
||||
String urgentExpr = "{\"metric\":\"turbidity\",\"operator\":\">\",\"threshold\":1.0}";
|
||||
assertTrue(ruleService.evaluateCondition(urgentExpr, "turbidity", 2.5));
|
||||
}
|
||||
}
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.water.common.core.exception.BusinessException;
|
||||
import com.water.production.entity.DispatchCommand;
|
||||
import com.water.production.entity.DispatchExecution;
|
||||
import com.water.production.mapper.DispatchCommandMapper;
|
||||
import com.water.production.mapper.DispatchExecutionMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DispatchCommandServiceTest {
|
||||
|
||||
@Mock
|
||||
private DispatchCommandMapper commandMapper;
|
||||
@Mock
|
||||
private DispatchExecutionMapper executionMapper;
|
||||
@Mock
|
||||
private DispatchTrackingService trackingService;
|
||||
|
||||
@InjectMocks
|
||||
private DispatchCommandService commandService;
|
||||
|
||||
@Test
|
||||
void testCreateCommand() {
|
||||
when(commandMapper.insert(any())).thenReturn(1);
|
||||
|
||||
DispatchCommand cmd = new DispatchCommand();
|
||||
cmd.setCommandTitle("测试调度指令");
|
||||
cmd.setCommandContent("请检查A区域管网压力");
|
||||
cmd.setCommandType("normal");
|
||||
cmd.setPriority("high");
|
||||
cmd.setSource("手动");
|
||||
cmd.setTargetType("user");
|
||||
cmd.setTargetIds("[1,2]");
|
||||
|
||||
DispatchCommand result = commandService.createCommand(cmd);
|
||||
|
||||
assertNotNull(result.getCommandNo());
|
||||
assertTrue(result.getCommandNo().startsWith("CMD-"));
|
||||
assertEquals("draft", result.getStatus());
|
||||
assertEquals("测试调度指令", result.getCommandTitle());
|
||||
verify(commandMapper).insert(any());
|
||||
verify(trackingService).log(any(), isNull(), eq("create"), isNull(), isNull(), eq("draft"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIssueCommand() {
|
||||
DispatchCommand cmd = buildCommand("draft");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
when(commandMapper.updateById(any())).thenReturn(1);
|
||||
when(executionMapper.insert(any())).thenReturn(1);
|
||||
|
||||
DispatchCommand result = commandService.issueCommand(1L, 100L, "admin");
|
||||
|
||||
assertEquals("issued", result.getStatus());
|
||||
assertNotNull(result.getIssuedAt());
|
||||
assertEquals(100L, result.getIssuedBy());
|
||||
verify(executionMapper, times(2)).insert(any()); // 2 target users
|
||||
verify(trackingService).log(eq(1L), isNull(), eq("issue"), eq(100L), eq("admin"),
|
||||
eq("draft"), eq("issued"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIssueCommand_invalidTransition() {
|
||||
DispatchCommand cmd = buildCommand("issued");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
|
||||
assertThrows(BusinessException.class, () -> {
|
||||
commandService.issueCommand(1L, 100L, "admin");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReceiveCommand() {
|
||||
DispatchCommand cmd = buildCommand("issued");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
|
||||
DispatchExecution exec = buildExecution("pending");
|
||||
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
|
||||
when(executionMapper.updateById(any())).thenReturn(1);
|
||||
when(executionMapper.selectCount(any(LambdaQueryWrapper.class)))
|
||||
.thenReturn(2L) // total
|
||||
.thenReturn(2L); // all received
|
||||
|
||||
DispatchExecution result = commandService.receiveCommand(1L, 1L, "张三");
|
||||
|
||||
assertEquals("received", result.getExecuteStatus());
|
||||
assertNotNull(result.getReceivedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStartExecution() {
|
||||
DispatchCommand cmd = buildCommand("received");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
when(commandMapper.updateById(any())).thenReturn(1);
|
||||
|
||||
DispatchExecution exec = buildExecution("received");
|
||||
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
|
||||
when(executionMapper.updateById(any())).thenReturn(1);
|
||||
|
||||
DispatchExecution result = commandService.startExecution(1L, 1L, "张三");
|
||||
|
||||
assertEquals("executing", result.getExecuteStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStartExecution_wrongStatus() {
|
||||
DispatchCommand cmd = buildCommand("received");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
|
||||
DispatchExecution exec = buildExecution("pending");
|
||||
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
|
||||
|
||||
assertThrows(BusinessException.class, () -> {
|
||||
commandService.startExecution(1L, 1L, "张三");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCompleteExecution() {
|
||||
DispatchCommand cmd = buildCommand("executing");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
|
||||
DispatchExecution exec = buildExecution("executing");
|
||||
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
|
||||
when(executionMapper.updateById(any())).thenReturn(1);
|
||||
when(executionMapper.selectCount(any(LambdaQueryWrapper.class)))
|
||||
.thenReturn(1L) // total
|
||||
.thenReturn(1L); // all final
|
||||
|
||||
DispatchExecution result = commandService.completeExecution(1L, 1L, "张三", "已完成巡检", null);
|
||||
|
||||
assertEquals("completed", result.getExecuteStatus());
|
||||
assertEquals("已完成巡检", result.getFeedback());
|
||||
assertNotNull(result.getCompletedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCompleteExecution_wrongStatus() {
|
||||
DispatchCommand cmd = buildCommand("executing");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
|
||||
DispatchExecution exec = buildExecution("received");
|
||||
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
|
||||
|
||||
assertThrows(BusinessException.class, () -> {
|
||||
commandService.completeExecution(1L, 1L, "张三", "反馈", null);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRejectExecution() {
|
||||
DispatchCommand cmd = buildCommand("issued");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
|
||||
DispatchExecution exec = buildExecution("pending");
|
||||
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
|
||||
when(executionMapper.updateById(any())).thenReturn(1);
|
||||
when(executionMapper.selectCount(any(LambdaQueryWrapper.class)))
|
||||
.thenReturn(1L) // total
|
||||
.thenReturn(1L); // all final
|
||||
|
||||
DispatchExecution result = commandService.rejectExecution(1L, 1L, "张三", "人手不足");
|
||||
|
||||
assertEquals("rejected", result.getExecuteStatus());
|
||||
assertEquals("人手不足", result.getRejectedReason());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRejectExecution_alreadyCompleted() {
|
||||
DispatchCommand cmd = buildCommand("executing");
|
||||
when(commandMapper.selectById(1L)).thenReturn(cmd);
|
||||
|
||||
DispatchExecution exec = buildExecution("completed");
|
||||
when(executionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(exec);
|
||||
|
||||
assertThrows(BusinessException.class, () -> {
|
||||
commandService.rejectExecution(1L, 1L, "张三", "原因");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCommandNotFound() {
|
||||
when(commandMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThrows(BusinessException.class, () -> {
|
||||
commandService.issueCommand(999L, 1L, "admin");
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Helper ====================
|
||||
|
||||
private DispatchCommand buildCommand(String status) {
|
||||
DispatchCommand cmd = new DispatchCommand();
|
||||
cmd.setId(1L);
|
||||
cmd.setCommandNo("CMD-20260614150000-0001");
|
||||
cmd.setCommandTitle("测试指令");
|
||||
cmd.setCommandContent("测试内容");
|
||||
cmd.setCommandType("normal");
|
||||
cmd.setStatus(status);
|
||||
cmd.setTargetIds("[1,2]");
|
||||
return cmd;
|
||||
}
|
||||
|
||||
private DispatchExecution buildExecution(String status) {
|
||||
DispatchExecution exec = new DispatchExecution();
|
||||
exec.setId(1L);
|
||||
exec.setCommandId(1L);
|
||||
exec.setUserId(1L);
|
||||
exec.setUserName("张三");
|
||||
exec.setExecuteStatus(status);
|
||||
return exec;
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.water.production.entity.DispatchTracking;
|
||||
import com.water.production.mapper.DispatchTrackingMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DispatchTrackingServiceTest {
|
||||
|
||||
@Mock
|
||||
private DispatchTrackingMapper trackingMapper;
|
||||
|
||||
@InjectMocks
|
||||
private DispatchTrackingService trackingService;
|
||||
|
||||
@Test
|
||||
void testLog() {
|
||||
when(trackingMapper.insert(any())).thenReturn(1);
|
||||
|
||||
trackingService.log(1L, null, "create", null, null, "draft", "创建指令");
|
||||
|
||||
ArgumentCaptor<DispatchTracking> captor = ArgumentCaptor.forClass(DispatchTracking.class);
|
||||
verify(trackingMapper).insert(captor.capture());
|
||||
|
||||
DispatchTracking saved = captor.getValue();
|
||||
assertEquals(1L, saved.getCommandId());
|
||||
assertNull(saved.getExecutionId());
|
||||
assertEquals("create", saved.getAction());
|
||||
assertEquals("draft", saved.getToStatus());
|
||||
assertEquals("创建指令", saved.getRemark());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogWithExecution() {
|
||||
when(trackingMapper.insert(any())).thenReturn(1);
|
||||
|
||||
trackingService.log(1L, 5L, "receive", 10L, "张三", "pending", "received", "接收确认");
|
||||
|
||||
ArgumentCaptor<DispatchTracking> captor = ArgumentCaptor.forClass(DispatchTracking.class);
|
||||
verify(trackingMapper).insert(captor.capture());
|
||||
|
||||
DispatchTracking saved = captor.getValue();
|
||||
assertEquals(5L, saved.getExecutionId());
|
||||
assertEquals(10L, saved.getOperatorId());
|
||||
assertEquals("张三", saved.getOperatorName());
|
||||
assertEquals("pending", saved.getFromStatus());
|
||||
assertEquals("received", saved.getToStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetTrackingLogs() {
|
||||
DispatchTracking t1 = new DispatchTracking();
|
||||
t1.setId(1L);
|
||||
t1.setCommandId(1L);
|
||||
t1.setAction("create");
|
||||
|
||||
DispatchTracking t2 = new DispatchTracking();
|
||||
t2.setId(2L);
|
||||
t2.setCommandId(1L);
|
||||
t2.setAction("issue");
|
||||
|
||||
when(trackingMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(t1, t2));
|
||||
|
||||
List<DispatchTracking> logs = trackingService.getTrackingLogs(1L);
|
||||
|
||||
assertEquals(2, logs.size());
|
||||
assertEquals("create", logs.get(0).getAction());
|
||||
assertEquals("issue", logs.get(1).getAction());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetExecutionTrackingLogs() {
|
||||
DispatchTracking t1 = new DispatchTracking();
|
||||
t1.setId(3L);
|
||||
t1.setExecutionId(5L);
|
||||
t1.setAction("receive");
|
||||
|
||||
when(trackingMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(t1));
|
||||
|
||||
List<DispatchTracking> logs = trackingService.getExecutionTrackingLogs(5L);
|
||||
|
||||
assertEquals(1, logs.size());
|
||||
assertEquals(5L, logs.get(0).getExecutionId());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user