diff --git a/db/postgresql/V2__alert_engine.sql b/db/postgresql/V2__alert_engine.sql new file mode 100644 index 00000000..eeeabb81 --- /dev/null +++ b/db/postgresql/V2__alert_engine.sql @@ -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); diff --git a/frontend/src/api/dispatchCommand.ts b/frontend/src/api/dispatchCommand.ts new file mode 100644 index 00000000..ca0717f9 --- /dev/null +++ b/frontend/src/api/dispatchCommand.ts @@ -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`) +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 996badad..8c76219f 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -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' } diff --git a/frontend/src/views/dispatch-command/CommandCreate.vue b/frontend/src/views/dispatch-command/CommandCreate.vue new file mode 100644 index 00000000..8a87315c --- /dev/null +++ b/frontend/src/views/dispatch-command/CommandCreate.vue @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + 低 + 普通 + 高 + 紧急 + + + + + + + + + + + + + + + + + + + + + + 输入用户ID,多个用逗号分隔 + + + + + + + + + 取消 + + 创建指令 + + + + + + + + diff --git a/frontend/src/views/dispatch-command/CommandDetail.vue b/frontend/src/views/dispatch-command/CommandDetail.vue new file mode 100644 index 00000000..56601ea0 --- /dev/null +++ b/frontend/src/views/dispatch-command/CommandDetail.vue @@ -0,0 +1,304 @@ + + + + + + 指令详情 + {{ statusLabel(detail.status) }} + + + + + + + + + {{ detail.command_title }} + {{ detail.command_no }} + + + + {{ detail.command_no }} + + {{ typeLabel(detail.command_type) }} + + + {{ priorityLabel(detail.priority) }} + + {{ detail.source || '-' }} + {{ detail.created_at }} + {{ detail.issued_at || '-' }} + {{ detail.completed_at || '-' }} + {{ detail.target_type || '-' }} + + + + 指令内容 + {{ detail.command_content }} + + + + + 状态流转 + + + + + + + + + + + + + + + 执行记录 + + + + + {{ exec.user_name || `用户${exec.user_id}` }} + + {{ executionStatusLabel(exec.execute_status) }} + + + + 反馈: {{ exec.feedback }} + + + 驳回原因: {{ exec.rejected_reason }} + + + 接收 + 开始执行 + 完成 + 驳回 + + + + + + + + + + + + 全过程追踪日志 + + + + {{ trackingActionLabel(log.action) }} + {{ log.operator_name || '' }} + + {{ log.from_status }} → {{ log.to_status }} + + {{ log.remark }} + + + + + + + + + + + + + + + + + + 取消 + 确认完成 + + + + + + + + diff --git a/frontend/src/views/dispatch-command/CommandList.vue b/frontend/src/views/dispatch-command/CommandList.vue new file mode 100644 index 00000000..a37e2118 --- /dev/null +++ b/frontend/src/views/dispatch-command/CommandList.vue @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 查询 + 重置 + + + + + + + + {{ stat.count }} + {{ statusLabel(stat.status) }} + + + + + + 创建指令 + 批量下发 + + + + + + + + + {{ typeLabel(row.command_type) }} + + + + + {{ priorityLabel(row.priority) }} + + + + + {{ statusLabel(row.status) }} + + + + + {{ row.completed_count || 0 }}/{{ row.total_executions || 0 }} + + + + + + 详情 + 下发 + 删除 + + + + + + + + + + + + + diff --git a/frontend/src/views/patrol/ProblemReportingView.vue b/frontend/src/views/patrol/ProblemReportingView.vue new file mode 100644 index 00000000..0f60f62b --- /dev/null +++ b/frontend/src/views/patrol/ProblemReportingView.vue @@ -0,0 +1,558 @@ + + + + + + 巡检问题上报 + {{ problemCount }} 个问题待处理 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 获取位置 + + + + + + + + + + + 最多上传5张照片,支持JPG、PNG格式 + + + + + {{ isEditing ? '更新问题' : '提交问题' }} + + 重置 + 取消编辑 + + + + + + + + + 问题列表 + + + + + + + + + + + + {{ getLevelText(row.problemLevel) }} + + + + + + + {{ getStatusText(row.status) }} + + + + + + {{ formatDate(row.reportTime) }} + + + + + + 查看 + + + 编辑 + + + 创建工单 + + + 查看工单 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index abebbd81..2cb55ca7 100644 --- a/pom.xml +++ b/pom.xml @@ -39,12 +39,17 @@ wm-iot wm-data-engine wm-bpm + wm-bpm-engine wm-production wm-revenue wm-patrol wm-bi wm-notify wm-job + wm-dispatch + wm-system + wm-mobile-app + wm-config diff --git a/sql/problem_reporting.sql b/sql/problem_reporting.sql new file mode 100644 index 00000000..f392aa8d --- /dev/null +++ b/sql/problem_reporting.sql @@ -0,0 +1,112 @@ +-- ============================================= +-- 智慧水务管理系统 - 巡检问题上报 + 工单管理 DDL +-- 版本: V1 +-- ============================================= + +-- 巡检问题上报表 +CREATE TABLE IF NOT EXISTS patrol_problem ( + id BIGSERIAL PRIMARY KEY, + problem_no VARCHAR(30) UNIQUE NOT NULL, -- 问题编号:WQ-2026-001 + task_id BIGINT REFERENCES patrol_task(id), + point_seq INT, + device_id BIGINT, + device_name VARCHAR(200), + problem_type VARCHAR(50) NOT NULL, -- 设备故障/水质异常/安全隐患/环境卫生/其他 + problem_level VARCHAR(20) DEFAULT 'normal', -- low/normal/high/critical + problem_title VARCHAR(200) NOT NULL, + problem_description TEXT, + location VARCHAR(300), + lng DOUBLE PRECISION, + lat DOUBLE PRECISION, + photo_urls JSONB, -- 现场照片URL数组 + reporter_id BIGINT REFERENCES sys_user(id), + reporter_name VARCHAR(50), + report_time TIMESTAMP DEFAULT NOW(), + status VARCHAR(20) DEFAULT 'reported', -- reported/processing/completed/closed + work_order_id BIGINT, -- 关联工单ID + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +COMMENT ON TABLE patrol_problem IS '巡检问题上报表'; +CREATE INDEX IF NOT EXISTS idx_problem_task ON patrol_problem(task_id); +CREATE INDEX IF NOT EXISTS idx_problem_status ON patrol_problem(status); +CREATE INDEX IF NOT EXISTS idx_problem_device ON patrol_problem(device_id); +CREATE INDEX IF NOT EXISTS idx_problem_type ON patrol_problem(problem_type); + +-- 工单表 +CREATE TABLE IF NOT EXISTS work_order ( + id BIGSERIAL PRIMARY KEY, + order_no VARCHAR(30) UNIQUE NOT NULL, -- 工单编号:WO-2026-001 + problem_id BIGINT REFERENCES patrol_problem(id), + order_type VARCHAR(50) NOT NULL, -- 设备维修/水质处理/安全隐患处理/清洁/其他 + priority VARCHAR(20) DEFAULT 'normal', -- low/normal/high/critical + title VARCHAR(200) NOT NULL, + description TEXT, + location VARCHAR(300), + contact_person VARCHAR(50), + contact_phone VARCHAR(20), + reporter_id BIGINT REFERENCES sys_user(id), + reporter_name VARCHAR(50), + assignee_id BIGINT REFERENCES sys_user(id), + assignee_name VARCHAR(50), + status VARCHAR(20) DEFAULT 'pending', -- pending/assigned/processing/completed/cancelled + process_status VARCHAR(20) DEFAULT 'created', -- created/accepted/in_progress/completed + estimated_duration INT, -- 预计工时(分钟) + actual_start_time TIMESTAMP, + actual_end_time TIMESTAMP, + completion_time TIMESTAMP, + photos_before JSONB, -- 处理前照片 + photos_after JSONB, -- 处理后照片 + solution_description TEXT, -- 处理方案描述 + solution_result TEXT, -- 处理结果 + customer_feedback TEXT, -- 客户反馈 + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +COMMENT ON TABLE work_order IS '工单表'; +CREATE INDEX IF NOT EXISTS idx_order_problem ON work_order(problem_id); +CREATE INDEX IF NOT EXISTS idx_order_status ON work_order(status, process_status); +CREATE INDEX IF NOT EXISTS idx_order_assignee ON work_order(assignee_id); + +-- 工单处理记录表 +CREATE TABLE IF NOT EXISTS work_order_process ( + id BIGSERIAL PRIMARY KEY, + work_order_id BIGINT REFERENCES work_order(id), + process_step VARCHAR(50) NOT NULL, -- created/accepted/in_progress/completed + processor_id BIGINT REFERENCES sys_user(id), + processor_name VARCHAR(50), + action VARCHAR(50) NOT NULL, -- create/assign/start/complete/cancel + comment TEXT, + photos JSONB, -- 处理过程照片 + created_at TIMESTAMP DEFAULT NOW() +); +COMMENT ON TABLE work_order_process IS '工单处理记录表'; +CREATE INDEX IF NOT EXISTS idx_process_order ON work_order_process(work_order_id); +CREATE INDEX IF NOT EXISTS idx_process_step ON work_order_process(process_step); + +-- 工单附件表 +CREATE TABLE IF NOT EXISTS work_order_attachment ( + id BIGSERIAL PRIMARY KEY, + work_order_id BIGINT REFERENCES work_order(id), + file_name VARCHAR(200) NOT NULL, + file_path VARCHAR(500) NOT NULL, + file_type VARCHAR(50), -- image/pdf/doc/other + file_size BIGINT, + uploaded_by BIGINT REFERENCES sys_user(id), + uploaded_at TIMESTAMP DEFAULT NOW() +); +COMMENT ON TABLE work_order_attachment IS '工单附件表'; +CREATE INDEX IF NOT EXISTS idx_attachment_order ON work_order_attachment(work_order_id); + +-- 巡检问题与工单关联触发记录 +CREATE TABLE IF NOT EXISTS patrol_work_order_trigger ( + id BIGSERIAL PRIMARY KEY, + patrol_problem_id BIGINT REFERENCES patrol_problem(id), + work_order_id BIGINT REFERENCES work_order(id), + trigger_type VARCHAR(20) NOT NULL, -- auto/manual + trigger_condition JSONB, -- 触发条件 + created_at TIMESTAMP DEFAULT NOW() +); +COMMENT ON TABLE patrol_work_order_trigger IS '巡检问题与工单关联触发记录'; +CREATE INDEX IF NOT EXISTS idx_trigger_problem ON patrol_work_order_trigger(patrol_problem_id); +CREATE INDEX IF NOT EXISTS idx_trigger_order ON patrol_work_order_trigger(work_order_id); \ No newline at end of file diff --git a/wm-bpm-engine/pom.xml b/wm-bpm-engine/pom.xml new file mode 100644 index 00000000..da031531 --- /dev/null +++ b/wm-bpm-engine/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + com.water + wm-parent + 1.0.0-SNAPSHOT + + wm-bpm-engine + wm-bpm-engine + BPM 业务流程引擎模块 + + + com.water + wm-common + + + org.springframework.boot + spring-boot-starter-web + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + com.baomidou + mybatis-plus-spring-boot3-starter + + + cn.dev33 + sa-token-spring-boot3-starter + + + org.postgresql + postgresql + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + com.h2database + h2 + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/wm-bpm-engine/src/main/java/com/water/bpmengine/BpmEngineApplication.java b/wm-bpm-engine/src/main/java/com/water/bpmengine/BpmEngineApplication.java new file mode 100644 index 00000000..13ca0d39 --- /dev/null +++ b/wm-bpm-engine/src/main/java/com/water/bpmengine/BpmEngineApplication.java @@ -0,0 +1,7 @@ +package com.water.bpmengine; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +@SpringBootApplication +public class BpmEngineApplication { + public static void main(String[] args) { SpringApplication.run(BpmEngineApplication.class, args); } +} diff --git a/wm-bpm-engine/src/main/java/com/water/bpmengine/controller/BpmEngineController.java b/wm-bpm-engine/src/main/java/com/water/bpmengine/controller/BpmEngineController.java new file mode 100644 index 00000000..e45ce5c4 --- /dev/null +++ b/wm-bpm-engine/src/main/java/com/water/bpmengine/controller/BpmEngineController.java @@ -0,0 +1,77 @@ +package com.water.bpmengine.controller; +import com.water.common.core.result.R; +import com.water.bpmengine.entity.*; +import com.water.bpmengine.service.BpmEngineService; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import java.util.*; + +@Tag(name = "业务流程引擎") +@RestController @RequestMapping("/bpm") @RequiredArgsConstructor +public class BpmEngineController { + private final BpmEngineService svc; + + @GetMapping("/definition/list") + public R> listDefs( + @RequestParam(required=false) String category, + @RequestParam(required=false) Integer status) { + return R.ok(svc.listDefinitions(category, status)); + } + @PostMapping("/definition") + public R createDef(@RequestBody Map req) { + return R.ok(svc.createDefinition(req)); + } + @PostMapping("/definition/{id}/publish") + public R publish(@PathVariable Long id) { + svc.publishDefinition(id); return R.ok("OK"); + } + @PostMapping("/process/start") + public R> start( + @RequestParam Long definitionId, + @RequestParam String title, + @RequestParam String initiator) { + return R.ok(svc.startProcess(definitionId, title, initiator)); + } + @GetMapping("/process/list") + public R> listInstances( + @RequestParam(required=false) String initiator, + @RequestParam(required=false) Integer status) { + return R.ok(svc.listInstances(initiator, status)); + } + @GetMapping("/task/todo") + public R> todo(@RequestParam String assignee) { + return R.ok(svc.getTodoTasks(assignee)); + } + @GetMapping("/task/done") + public R> done(@RequestParam String assignee) { + return R.ok(svc.getDoneTasks(assignee)); + } + @PostMapping("/task/{id}/approve") + public R> approve(@PathVariable Long id, + @RequestParam(required=false) String comment) { + return R.ok(svc.approveTask(id, comment)); + } + @PostMapping("/task/{id}/reject") + public R> reject(@PathVariable Long id, + @RequestParam(required=false) String comment) { + return R.ok(svc.rejectTask(id, comment)); + } + @PostMapping("/task/{id}/transfer") + public R> transfer(@PathVariable Long id, + @RequestParam String newAssignee) { + return R.ok(svc.transferTask(id, newAssignee)); + } + @GetMapping("/statistics") + public R> stats(@RequestParam(required=false) String processKey) { + return R.ok(svc.getStatistics(processKey)); + } + @GetMapping("/template/list") + public R> listTemplates() { + return R.ok(svc.listTemplates()); + } + @PostMapping("/template") + public R createTemplate(@RequestBody Map req) { + return R.ok(svc.createTemplate(req)); + } +} diff --git a/wm-bpm-engine/src/main/java/com/water/bpmengine/entity/ProcessDefinition.java b/wm-bpm-engine/src/main/java/com/water/bpmengine/entity/ProcessDefinition.java new file mode 100644 index 00000000..7e07256e --- /dev/null +++ b/wm-bpm-engine/src/main/java/com/water/bpmengine/entity/ProcessDefinition.java @@ -0,0 +1,13 @@ +package com.water.bpmengine.entity; +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; import java.time.LocalDateTime; +@Data @TableName("bpm_process_definition") +public class ProcessDefinition { + @TableId(type = IdType.AUTO) private Long id; + private String processKey, name, category; + private String bpmnXml; + private Integer version; private Integer status; // 0草稿 1已发布 2已停用 + private String description; + @TableField(fill=FieldFill.INSERT) private LocalDateTime createdTime; + @TableField(fill=FieldFill.INSERT_UPDATE) private LocalDateTime updatedTime; +} diff --git a/wm-bpm-engine/src/main/java/com/water/bpmengine/entity/ProcessInstance.java b/wm-bpm-engine/src/main/java/com/water/bpmengine/entity/ProcessInstance.java new file mode 100644 index 00000000..ae2325da --- /dev/null +++ b/wm-bpm-engine/src/main/java/com/water/bpmengine/entity/ProcessInstance.java @@ -0,0 +1,13 @@ +package com.water.bpmengine.entity; +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; import java.time.LocalDateTime; +@Data @TableName("bpm_process_instance") +public class ProcessInstance { + @TableId(type = IdType.AUTO) private Long id; + private Long definitionId; private String processKey; + private String title, initiator; + private Integer status; // 0运行中 1已完成 2已驳回 3已撤销 + private String currentNodeName; + @TableField(fill=FieldFill.INSERT) private LocalDateTime createdTime; + private LocalDateTime completedTime; +} diff --git a/wm-bpm-engine/src/main/java/com/water/bpmengine/entity/ProcessTemplate.java b/wm-bpm-engine/src/main/java/com/water/bpmengine/entity/ProcessTemplate.java new file mode 100644 index 00000000..d84bea0e --- /dev/null +++ b/wm-bpm-engine/src/main/java/com/water/bpmengine/entity/ProcessTemplate.java @@ -0,0 +1,11 @@ +package com.water.bpmengine.entity; +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; import java.time.LocalDateTime; +@Data @TableName("bpm_process_template") +public class ProcessTemplate { + @TableId(type = IdType.AUTO) private Long id; + private String name, category, description; + private String bpmnXml; + private Integer status; + @TableField(fill=FieldFill.INSERT) private LocalDateTime createdTime; +} diff --git a/wm-bpm-engine/src/main/java/com/water/bpmengine/entity/TaskItem.java b/wm-bpm-engine/src/main/java/com/water/bpmengine/entity/TaskItem.java new file mode 100644 index 00000000..fa6b719b --- /dev/null +++ b/wm-bpm-engine/src/main/java/com/water/bpmengine/entity/TaskItem.java @@ -0,0 +1,13 @@ +package com.water.bpmengine.entity; +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; import java.time.LocalDateTime; +@Data @TableName("bpm_task_item") +public class TaskItem { + @TableId(type = IdType.AUTO) private Long id; + private Long instanceId; private String taskName, taskType; + private String assignee; + private Integer status; // 0待办 1已办 2驳回 3转办 + private String comment; + @TableField(fill=FieldFill.INSERT) private LocalDateTime createdTime; + private LocalDateTime completedTime; +} diff --git a/wm-bpm-engine/src/main/java/com/water/bpmengine/mapper/ProcessDefinitionMapper.java b/wm-bpm-engine/src/main/java/com/water/bpmengine/mapper/ProcessDefinitionMapper.java new file mode 100644 index 00000000..a5761749 --- /dev/null +++ b/wm-bpm-engine/src/main/java/com/water/bpmengine/mapper/ProcessDefinitionMapper.java @@ -0,0 +1,5 @@ +package com.water.bpmengine.mapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.bpmengine.entity.ProcessDefinition; +import org.apache.ibatis.annotations.Mapper; +@Mapper public interface ProcessDefinitionMapper extends BaseMapper {} diff --git a/wm-bpm-engine/src/main/java/com/water/bpmengine/mapper/ProcessInstanceMapper.java b/wm-bpm-engine/src/main/java/com/water/bpmengine/mapper/ProcessInstanceMapper.java new file mode 100644 index 00000000..94d30c1b --- /dev/null +++ b/wm-bpm-engine/src/main/java/com/water/bpmengine/mapper/ProcessInstanceMapper.java @@ -0,0 +1,5 @@ +package com.water.bpmengine.mapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.bpmengine.entity.ProcessInstance; +import org.apache.ibatis.annotations.Mapper; +@Mapper public interface ProcessInstanceMapper extends BaseMapper {} diff --git a/wm-bpm-engine/src/main/java/com/water/bpmengine/mapper/ProcessTemplateMapper.java b/wm-bpm-engine/src/main/java/com/water/bpmengine/mapper/ProcessTemplateMapper.java new file mode 100644 index 00000000..e4a53c41 --- /dev/null +++ b/wm-bpm-engine/src/main/java/com/water/bpmengine/mapper/ProcessTemplateMapper.java @@ -0,0 +1,5 @@ +package com.water.bpmengine.mapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.bpmengine.entity.ProcessTemplate; +import org.apache.ibatis.annotations.Mapper; +@Mapper public interface ProcessTemplateMapper extends BaseMapper {} diff --git a/wm-bpm-engine/src/main/java/com/water/bpmengine/mapper/TaskItemMapper.java b/wm-bpm-engine/src/main/java/com/water/bpmengine/mapper/TaskItemMapper.java new file mode 100644 index 00000000..42b0aad0 --- /dev/null +++ b/wm-bpm-engine/src/main/java/com/water/bpmengine/mapper/TaskItemMapper.java @@ -0,0 +1,5 @@ +package com.water.bpmengine.mapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.bpmengine.entity.TaskItem; +import org.apache.ibatis.annotations.Mapper; +@Mapper public interface TaskItemMapper extends BaseMapper {} diff --git a/wm-bpm-engine/src/main/java/com/water/bpmengine/service/BpmEngineService.java b/wm-bpm-engine/src/main/java/com/water/bpmengine/service/BpmEngineService.java new file mode 100644 index 00000000..7d3b8940 --- /dev/null +++ b/wm-bpm-engine/src/main/java/com/water/bpmengine/service/BpmEngineService.java @@ -0,0 +1,122 @@ +package com.water.bpmengine.service; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.water.bpmengine.entity.*; +import com.water.bpmengine.mapper.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import java.time.LocalDateTime; import java.util.*; + +@Service @RequiredArgsConstructor +public class BpmEngineService { + private final ProcessDefinitionMapper defMapper; + private final ProcessInstanceMapper instMapper; + private final TaskItemMapper taskMapper; + private final ProcessTemplateMapper tplMapper; + + // === 流程定义 === + public List listDefinitions(String category, Integer status) { + return defMapper.selectList(new LambdaQueryWrapper() + .eq(category != null, ProcessDefinition::getCategory, category) + .eq(status != null, ProcessDefinition::getStatus, status)); + } + public Long createDefinition(Map req) { + ProcessDefinition d = new ProcessDefinition(); + d.setProcessKey((String)req.get("processKey")); + d.setName((String)req.get("name")); + d.setCategory((String)req.get("category")); + d.setBpmnXml((String)req.get("bpmnXml")); + d.setDescription((String)req.get("description")); + d.setVersion(1); d.setStatus(0); + defMapper.insert(d); + return d.getId(); + } + public void publishDefinition(Long id) { + ProcessDefinition d = defMapper.selectById(id); + if (d == null) throw new RuntimeException("流程定义不存在"); + d.setStatus(1); defMapper.updateById(d); + } + + // === 流程实例 === + public Map startProcess(Long definitionId, String title, String initiator) { + ProcessDefinition d = defMapper.selectById(definitionId); + if (d == null || d.getStatus() != 1) throw new RuntimeException("流程未发布"); + ProcessInstance inst = new ProcessInstance(); + inst.setDefinitionId(definitionId); inst.setProcessKey(d.getProcessKey()); + inst.setTitle(title); inst.setInitiator(initiator); + inst.setStatus(0); inst.setCurrentNodeName("开始"); + instMapper.insert(inst); + TaskItem t = new TaskItem(); + t.setInstanceId(inst.getId()); t.setTaskName("审批节点"); + t.setTaskType("审批"); t.setAssignee(initiator); t.setStatus(0); + taskMapper.insert(t); + return Map.of("instanceId", inst.getId(), "processKey", d.getProcessKey()); + } + public List listInstances(String initiator, Integer status) { + return instMapper.selectList(new LambdaQueryWrapper() + .eq(initiator != null, ProcessInstance::getInitiator, initiator) + .eq(status != null, ProcessInstance::getStatus, status)); + } + + // === 任务处理 === + public List getTodoTasks(String assignee) { + return taskMapper.selectList(new LambdaQueryWrapper() + .eq(TaskItem::getAssignee, assignee).eq(TaskItem::getStatus, 0)); + } + public List getDoneTasks(String assignee) { + return taskMapper.selectList(new LambdaQueryWrapper() + .eq(TaskItem::getAssignee, assignee).in(TaskItem::getStatus, 1, 2)); + } + public Map approveTask(Long taskId, String comment) { + TaskItem t = taskMapper.selectById(taskId); + if (t == null || t.getStatus() != 0) throw new RuntimeException("任务不可审批"); + t.setStatus(1); t.setComment(comment); t.setCompletedTime(LocalDateTime.now()); + taskMapper.updateById(t); + ProcessInstance inst = instMapper.selectById(t.getInstanceId()); + inst.setCurrentNodeName("已完成"); inst.setStatus(1); inst.setCompletedTime(LocalDateTime.now()); + instMapper.updateById(inst); + return Map.of("taskId", taskId, "status", "已审批"); + } + public Map rejectTask(Long taskId, String comment) { + TaskItem t = taskMapper.selectById(taskId); + if (t == null) throw new RuntimeException("任务不存在"); + t.setStatus(2); t.setComment(comment); t.setCompletedTime(LocalDateTime.now()); + taskMapper.updateById(t); + ProcessInstance inst = instMapper.selectById(t.getInstanceId()); + inst.setStatus(2); inst.setCompletedTime(LocalDateTime.now()); + instMapper.updateById(inst); + return Map.of("taskId", taskId, "status", "已驳回"); + } + public Map transferTask(Long taskId, String newAssignee) { + TaskItem t = taskMapper.selectById(taskId); + if (t == null) throw new RuntimeException("任务不存在"); + t.setStatus(3); t.setCompletedTime(LocalDateTime.now()); + taskMapper.updateById(t); + TaskItem nt = new TaskItem(); + nt.setInstanceId(t.getInstanceId()); nt.setTaskName(t.getTaskName()); + nt.setTaskType(t.getTaskType()); nt.setAssignee(newAssignee); nt.setStatus(0); + taskMapper.insert(nt); + return Map.of("oldTaskId", taskId, "newTaskId", nt.getId()); + } + + // === 统计 === + public Map getStatistics(String processKey) { + long total = instMapper.selectCount(new LambdaQueryWrapper() + .eq(processKey != null, ProcessInstance::getProcessKey, processKey)); + long completed = instMapper.selectCount(new LambdaQueryWrapper() + .eq(processKey != null, ProcessInstance::getProcessKey, processKey) + .eq(ProcessInstance::getStatus, 1)); + return Map.of("total", total, "completed", completed, + "running", total - completed, + "completionRate", total > 0 ? (double)completed / total : 0); + } + + // === 模板 === + public List listTemplates() { return tplMapper.selectList(null); } + public Long createTemplate(Map req) { + ProcessTemplate t = new ProcessTemplate(); + t.setName((String)req.get("name")); t.setCategory((String)req.get("category")); + t.setBpmnXml((String)req.get("bpmnXml")); t.setStatus(1); + tplMapper.insert(t); + return t.getId(); + } +} diff --git a/wm-bpm-engine/src/main/resources/application.yml b/wm-bpm-engine/src/main/resources/application.yml new file mode 100644 index 00000000..e93ad582 --- /dev/null +++ b/wm-bpm-engine/src/main/resources/application.yml @@ -0,0 +1,15 @@ +server: + port: 9040 +spring: + application: + name: wm-bpm-engine + datasource: + url: jdbc:postgresql://localhost:5432/water_bpm + username: water + password: water123 +mybatis-plus: + configuration: + map-underscore-to-camel-case: true + global-config: + db-config: + id-type: auto diff --git a/wm-bpm-engine/src/main/resources/db/V1__bpm_engine.sql b/wm-bpm-engine/src/main/resources/db/V1__bpm_engine.sql new file mode 100644 index 00000000..93b8b4de --- /dev/null +++ b/wm-bpm-engine/src/main/resources/db/V1__bpm_engine.sql @@ -0,0 +1,23 @@ +-- BPM Engine DDL +CREATE TABLE IF NOT EXISTS bpm_process_definition ( + id BIGSERIAL PRIMARY KEY, process_key VARCHAR(100), name VARCHAR(200), + category VARCHAR(50), bpmn_xml TEXT, version INT DEFAULT 1, + status INT DEFAULT 0, description TEXT, + created_time TIMESTAMP DEFAULT NOW(), updated_time TIMESTAMP DEFAULT NOW() +); +CREATE TABLE IF NOT EXISTS bpm_process_instance ( + id BIGSERIAL PRIMARY KEY, definition_id BIGINT, process_key VARCHAR(100), + title VARCHAR(200), initiator VARCHAR(50), status INT DEFAULT 0, + current_node_name VARCHAR(100), + created_time TIMESTAMP DEFAULT NOW(), completed_time TIMESTAMP +); +CREATE TABLE IF NOT EXISTS bpm_task_item ( + id BIGSERIAL PRIMARY KEY, instance_id BIGINT, task_name VARCHAR(100), + task_type VARCHAR(30), assignee VARCHAR(50), status INT DEFAULT 0, + comment TEXT, created_time TIMESTAMP DEFAULT NOW(), completed_time TIMESTAMP +); +CREATE TABLE IF NOT EXISTS bpm_process_template ( + id BIGSERIAL PRIMARY KEY, name VARCHAR(200), category VARCHAR(50), + bpmn_xml TEXT, status INT DEFAULT 1, + created_time TIMESTAMP DEFAULT NOW() +); diff --git a/wm-bpm/src/main/java/com/water/bpm/entity/BpmApprovalRecord.java b/wm-bpm/src/main/java/com/water/bpm/entity/BpmApprovalRecord.java index 65be9884..1b97a1f9 100644 --- a/wm-bpm/src/main/java/com/water/bpm/entity/BpmApprovalRecord.java +++ b/wm-bpm/src/main/java/com/water/bpm/entity/BpmApprovalRecord.java @@ -1,18 +1,63 @@ package com.water.bpm.entity; +import com.baomidou.mybatisplus.annotation.*; +import com.water.common.core.entity.BaseEntity; import lombok.Data; +import lombok.EqualsAndHashCode; + import java.time.LocalDateTime; +/** + * 审批记录实体 + * BPM-03: 流程处理 + */ @Data -public class BpmApprovalRecord { - private Long id; +@EqualsAndHashCode(callSuper = true) +@TableName("bpm_approval_record") +public class BpmApprovalRecord extends BaseEntity { + + /** 流程实例ID */ private Long instanceId; + + /** 流程实例UUID */ + private String instanceUuid; + + /** 节点标识 */ private String nodeId; + + /** 节点名称 */ private String nodeName; + + /** 审批人ID */ private Long approverId; + + /** 审批人姓名 */ private String approverName; - private String action; // approve/reject/transfer/delegate/back + + /** 审批动作: approve/reject/transfer/delegate/back/countersign */ + private String action; + + /** 审批意见 */ private String comment; - private String targetAssignee; // 转办/委派目标 + + /** 转办/委派目标人ID */ + private Long targetAssigneeId; + + /** 转办/委派目标人姓名 */ + private String targetAssigneeName; + + /** 会签结果: all/pass_one/veto */ + private String countersignResult; + + /** 会签通过数 */ + private Integer countersignApproved; + + /** 会签总数 */ + private Integer countersignTotal; + + /** 审批时间 */ private LocalDateTime approvedAt; + + /** 租户ID */ + private String tenantId; } diff --git a/wm-bpm/src/main/java/com/water/bpm/entity/BpmFormTemplate.java b/wm-bpm/src/main/java/com/water/bpm/entity/BpmFormTemplate.java new file mode 100644 index 00000000..7f9e083e --- /dev/null +++ b/wm-bpm/src/main/java/com/water/bpm/entity/BpmFormTemplate.java @@ -0,0 +1,49 @@ +package com.water.bpm.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.water.common.core.entity.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 流程表单模板实体 + * BPM-02: 模板化快速创建 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("bpm_form_template") +public class BpmFormTemplate extends BaseEntity { + + /** 模板名称 */ + private String templateName; + + /** 模板编码 */ + private String templateCode; + + /** 模板分类 */ + private String category; + + /** 表单 JSON Schema */ + private String formSchema; + + /** 流程 BPMN XML 模板 */ + private String bpmnTemplate; + + /** 模板描述 */ + private String description; + + /** 模板图标 */ + private String icon; + + /** 使用次数 */ + private Integer useCount; + + /** 状态: 0-草稿 1-启用 2-停用 */ + private Integer status; + + /** 创建人 */ + private String createdBy; + + /** 租户ID */ + private String tenantId; +} diff --git a/wm-bpm/src/main/java/com/water/bpm/entity/BpmOrchestration.java b/wm-bpm/src/main/java/com/water/bpm/entity/BpmOrchestration.java new file mode 100644 index 00000000..197724ac --- /dev/null +++ b/wm-bpm/src/main/java/com/water/bpm/entity/BpmOrchestration.java @@ -0,0 +1,52 @@ +package com.water.bpm.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.water.common.core.entity.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 流程编排实体 + * BPM-05: 跨系统流程编排 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("bpm_orchestration") +public class BpmOrchestration extends BaseEntity { + + /** 编排名称 */ + private String orchestrationName; + + /** 编排编码 */ + private String orchestrationCode; + + /** 编排描述 */ + private String description; + + /** 包含的流程定义ID列表 JSON */ + private String processDefinitionIds; + + /** 编排规则 JSON (流程间依赖关系、触发条件) */ + private String orchestrationRules; + + /** 触发方式: manual/scheduled/event */ + private String triggerType; + + /** 定时表达式 (cron) */ + private String cronExpression; + + /** 事件名称 (event触发时) */ + private String eventName; + + /** 状态: 0-草稿 1-启用 2-停用 */ + private Integer status; + + /** 创建人 */ + private String createdBy; + + /** 执行次数 */ + private Integer executionCount; + + /** 租户ID */ + private String tenantId; +} diff --git a/wm-bpm/src/main/java/com/water/bpm/entity/BpmProcessDefinition.java b/wm-bpm/src/main/java/com/water/bpm/entity/BpmProcessDefinition.java index 4a45e9e1..f3b1c96c 100644 --- a/wm-bpm/src/main/java/com/water/bpm/entity/BpmProcessDefinition.java +++ b/wm-bpm/src/main/java/com/water/bpm/entity/BpmProcessDefinition.java @@ -1,20 +1,58 @@ package com.water.bpm.entity; +import com.baomidou.mybatisplus.annotation.*; +import com.water.common.core.entity.BaseEntity; import lombok.Data; +import lombok.EqualsAndHashCode; + import java.time.LocalDateTime; +/** + * 流程定义实体 + * BPM-01: 流程定义管理 + */ @Data -public class BpmProcessDefinition { - private Long id; +@EqualsAndHashCode(callSuper = true) +@TableName("bpm_process_definition") +public class BpmProcessDefinition extends BaseEntity { + + /** 流程唯一标识 */ private String processKey; + + /** 流程名称 */ private String processName; + + /** 流程描述 */ private String description; - private String bpmnXml; // BPMN 2.0 XML - private String formSchema; // 表单 JSON Schema - private String category; // revenue/patrol/dispatch/maintenance + + /** BPMN 2.0 XML 定义 */ + private String bpmnXml; + + /** 表单 JSON Schema */ + private String formSchema; + + /** 流程分类: revenue/patrol/dispatch/maintenance/inspection */ + private String category; + + /** 版本号 */ + @TableField("version") private Integer version; - private Integer status; // 0:草稿 1:发布 2:停用 + + /** 状态: 0-草稿 1-已发布 2-已停用 */ + private Integer status; + + /** 创建人 */ private String createdBy; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; + + /** 流程图标 */ + private String icon; + + /** 排序号 */ + private Integer sortOrder; + + /** 发布时间 */ + private LocalDateTime publishedAt; + + /** 租户ID */ + private String tenantId; } diff --git a/wm-bpm/src/main/java/com/water/bpm/entity/BpmProcessInstance.java b/wm-bpm/src/main/java/com/water/bpm/entity/BpmProcessInstance.java index 40d907ff..a8e4a0b7 100644 --- a/wm-bpm/src/main/java/com/water/bpm/entity/BpmProcessInstance.java +++ b/wm-bpm/src/main/java/com/water/bpm/entity/BpmProcessInstance.java @@ -1,26 +1,84 @@ package com.water.bpm.entity; +import com.baomidou.mybatisplus.annotation.*; +import com.water.common.core.entity.BaseEntity; import lombok.Data; -import java.time.LocalDateTime; -import java.util.Map; +import lombok.EqualsAndHashCode; +import java.time.LocalDateTime; + +/** + * 流程实例实体 + * BPM-03: 流程处理 + */ @Data -public class BpmProcessInstance { - private Long id; +@EqualsAndHashCode(callSuper = true) +@TableName("bpm_process_instance") +public class BpmProcessInstance extends BaseEntity { + + /** 实例唯一ID */ private String instanceId; + + /** 关联流程定义ID */ private Long definitionId; + + /** 流程标识 */ private String processKey; - private String businessKey; // 关联业务ID - private String businessType; // 业务类型 + + /** 流程名称 */ + private String processName; + + /** 业务主键(关联业务表) */ + private String businessKey; + + /** 业务类型 */ + private String businessType; + + /** 流程标题 */ private String title; + + /** 发起人ID */ private Long initiatorId; + + /** 发起人姓名 */ private String initiatorName; - private String currentNode; // 当前审批节点 - private String currentAssignee; // 当前处理人 - private String status; // running/completed/terminated/rejected - private Map variables; - private Map formData; + + /** 当前节点标识 */ + private String currentNodeId; + + /** 当前节点名称 */ + private String currentNodeName; + + /** 当前处理人ID */ + private Long currentAssigneeId; + + /** 当前处理人姓名 */ + private String currentAssigneeName; + + /** 状态: running/completed/terminated/rejected/suspended */ + private String status; + + /** 优先级: 0-普通 1-紧急 2-特急 */ + private Integer priority; + + /** 流程变量 JSON */ + private String variables; + + /** 表单数据 JSON */ + private String formData; + + /** 开始时间 */ private LocalDateTime startedAt; + + /** 结束时间 */ private LocalDateTime completedAt; - private LocalDateTime createdAt; + + /** 预计完成时间 */ + private LocalDateTime expectedCompletionAt; + + /** 耗时(秒) */ + private Long durationSeconds; + + /** 租户ID */ + private String tenantId; } diff --git a/wm-bpm/src/main/java/com/water/bpm/entity/BpmProcessNode.java b/wm-bpm/src/main/java/com/water/bpm/entity/BpmProcessNode.java new file mode 100644 index 00000000..1717794d --- /dev/null +++ b/wm-bpm/src/main/java/com/water/bpm/entity/BpmProcessNode.java @@ -0,0 +1,61 @@ +package com.water.bpm.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.water.common.core.entity.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 流程节点定义实体 + * BPM-01: 流程定义 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("bpm_process_node") +public class BpmProcessNode extends BaseEntity { + + /** 关联流程定义ID */ + private Long definitionId; + + /** 节点标识 */ + private String nodeId; + + /** 节点名称 */ + private String nodeName; + + /** 节点类型: start/end/userTask/serviceTask/gateway/subprocess/timer */ + private String nodeType; + + /** 处理人类型: role/user/department/position/initiator */ + private String assigneeType; + + /** 处理人值(角色ID/用户ID/部门ID等) */ + private String assigneeValue; + + /** 处理人名称(冗余) */ + private String assigneeName; + + /** 多人审批方式: sequential(会签)/parallel(或签)/countersign(比例签) */ + private String multiInstanceType; + + /** 会签通过比例 */ + private Integer countersignRate; + + /** 超时时间(小时) */ + private Integer timeoutHours; + + /** 超时处理: remind/transfer/escalate/auto_approve */ + private String timeoutAction; + + /** 表单权限 JSON */ + private String formPermission; + + /** 节点条件表达式 */ + private String conditionExpression; + + /** 排序号 */ + private Integer sortOrder; + + /** 租户ID */ + private String tenantId; +} diff --git a/wm-bpm/src/main/java/com/water/bpm/entity/BpmProcessStat.java b/wm-bpm/src/main/java/com/water/bpm/entity/BpmProcessStat.java new file mode 100644 index 00000000..143e62c2 --- /dev/null +++ b/wm-bpm/src/main/java/com/water/bpm/entity/BpmProcessStat.java @@ -0,0 +1,63 @@ +package com.water.bpm.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.water.common.core.entity.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 流程统计实体 + * BPM-04: 统计评估 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("bpm_process_stat") +public class BpmProcessStat extends BaseEntity { + + /** 流程定义ID */ + private Long definitionId; + + /** 流程标识 */ + private String processKey; + + /** 统计周期: day/week/month/quarter/year */ + private String period; + + /** 统计日期 */ + private String statDate; + + /** 发起总数 */ + private Integer startCount; + + /** 完成总数 */ + private Integer completedCount; + + /** 驳回总数 */ + private Integer rejectedCount; + + /** 撤回总数 */ + private Integer terminatedCount; + + /** 平均耗时(秒) */ + private Long avgDurationSeconds; + + /** 最长耗时(秒) */ + private Long maxDurationSeconds; + + /** 最短耗时(秒) */ + private Long minDurationSeconds; + + /** 平均节点耗时(秒) */ + private Long avgNodeDurationSeconds; + + /** 超时任务数 */ + private Integer timeoutCount; + + /** 一次通过率(百分比) */ + private Double firstPassRate; + + /** 租户ID */ + private String tenantId; +} diff --git a/wm-bpm/src/main/java/com/water/bpm/entity/BpmTodoTask.java b/wm-bpm/src/main/java/com/water/bpm/entity/BpmTodoTask.java new file mode 100644 index 00000000..53a4f3ae --- /dev/null +++ b/wm-bpm/src/main/java/com/water/bpm/entity/BpmTodoTask.java @@ -0,0 +1,75 @@ +package com.water.bpm.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.water.common.core.entity.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 待办任务实体 + * BPM-06: 待办/已办中心 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("bpm_todo_task") +public class BpmTodoTask extends BaseEntity { + + /** 流程实例ID */ + private Long instanceId; + + /** 流程实例UUID */ + private String instanceUuid; + + /** 流程标题 */ + private String title; + + /** 流程标识 */ + private String processKey; + + /** 流程名称 */ + private String processName; + + /** 节点标识 */ + private String nodeId; + + /** 节点名称 */ + private String nodeName; + + /** 处理人ID */ + private Long assigneeId; + + /** 处理人姓名 */ + private String assigneeName; + + /** 发起人ID */ + private Long initiatorId; + + /** 发起人姓名 */ + private String initiatorName; + + /** 业务主键 */ + private String businessKey; + + /** 任务状态: pending/completed/transferred/delegated */ + private String status; + + /** 优先级: 0-普通 1-紧急 2-特急 */ + private Integer priority; + + /** 接收时间 */ + private LocalDateTime receivedAt; + + /** 完成时间 */ + private LocalDateTime completedAt; + + /** 超时时间 */ + private LocalDateTime deadlineAt; + + /** 是否已读 */ + private Boolean isRead; + + /** 租户ID */ + private String tenantId; +} diff --git a/wm-bpm/src/main/java/com/water/bpm/entity/dto/ApprovalRequest.java b/wm-bpm/src/main/java/com/water/bpm/entity/dto/ApprovalRequest.java new file mode 100644 index 00000000..2837a542 --- /dev/null +++ b/wm-bpm/src/main/java/com/water/bpm/entity/dto/ApprovalRequest.java @@ -0,0 +1,33 @@ +package com.water.bpm.entity.dto; + +import lombok.Data; + +/** + * 审批请求 DTO + */ +@Data +public class ApprovalRequest { + /** 流程实例ID */ + private Long instanceId; + + /** 流程实例UUID */ + private String instanceUuid; + + /** 节点标识 */ + private String nodeId; + + /** 节点名称 */ + private String nodeName; + + /** 审批动作: approve/reject/transfer/delegate/back/countersign */ + private String action; + + /** 审批意见 */ + private String comment; + + /** 转办/委派目标人ID */ + private Long targetAssigneeId; + + /** 转办/委派目标人姓名 */ + private String targetAssigneeName; +} diff --git a/wm-bpm/src/main/java/com/water/bpm/entity/dto/ProcessInstanceQuery.java b/wm-bpm/src/main/java/com/water/bpm/entity/dto/ProcessInstanceQuery.java new file mode 100644 index 00000000..7e699738 --- /dev/null +++ b/wm-bpm/src/main/java/com/water/bpm/entity/dto/ProcessInstanceQuery.java @@ -0,0 +1,41 @@ +package com.water.bpm.entity.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 流程实例查询 DTO + */ +@Data +public class ProcessInstanceQuery { + /** 流程标识 */ + private String processKey; + + /** 流程标题 */ + private String title; + + /** 状态 */ + private String status; + + /** 发起人ID */ + private Long initiatorId; + + /** 业务主键 */ + private String businessKey; + + /** 业务类型 */ + private String businessType; + + /** 开始时间(起) */ + private LocalDateTime startTimeFrom; + + /** 开始时间(止) */ + private LocalDateTime startTimeTo; + + /** 页码 */ + private Integer pageNum = 1; + + /** 每页条数 */ + private Integer pageSize = 10; +} diff --git a/wm-bpm/src/main/java/com/water/bpm/entity/dto/ProcessStartRequest.java b/wm-bpm/src/main/java/com/water/bpm/entity/dto/ProcessStartRequest.java new file mode 100644 index 00000000..93cf7503 --- /dev/null +++ b/wm-bpm/src/main/java/com/water/bpm/entity/dto/ProcessStartRequest.java @@ -0,0 +1,32 @@ +package com.water.bpm.entity.dto; + +import lombok.Data; + +import java.util.Map; + +/** + * 流程发起请求 DTO + */ +@Data +public class ProcessStartRequest { + /** 流程定义ID */ + private Long definitionId; + + /** 业务主键 */ + private String businessKey; + + /** 业务类型 */ + private String businessType; + + /** 流程标题 */ + private String title; + + /** 表单数据 */ + private Map formData; + + /** 流程变量 */ + private Map variables; + + /** 优先级 */ + private Integer priority; +} diff --git a/wm-bpm/src/main/java/com/water/bpm/entity/dto/ProcessStatVO.java b/wm-bpm/src/main/java/com/water/bpm/entity/dto/ProcessStatVO.java new file mode 100644 index 00000000..f9d72cf7 --- /dev/null +++ b/wm-bpm/src/main/java/com/water/bpm/entity/dto/ProcessStatVO.java @@ -0,0 +1,60 @@ +package com.water.bpm.entity.dto; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * 流程统计 VO + */ +@Data +public class ProcessStatVO { + /** 流程定义ID */ + private Long definitionId; + + /** 流程名称 */ + private String processName; + + /** 流程标识 */ + private String processKey; + + /** 总实例数 */ + private Integer totalInstances; + + /** 运行中数量 */ + private Integer runningCount; + + /** 已完成数量 */ + private Integer completedCount; + + /** 已驳回数量 */ + private Integer rejectedCount; + + /** 已撤回数量 */ + private Integer terminatedCount; + + /** 平均耗时(小时) */ + private Double avgDurationHours; + + /** 最长耗时(小时) */ + private Double maxDurationHours; + + /** 最短耗时(小时) */ + private Double minDurationHours; + + /** 一次通过率 */ + private Double firstPassRate; + + /** 超时率 */ + private Double timeoutRate; + + /** 各节点平均耗时 */ + private List> nodeAvgDurations; + + /** 瓶颈节点(耗时最长) */ + private String bottleneckNode; + + /** 趋势数据 */ + private List> trendData; +} diff --git a/wm-bpm/src/main/java/com/water/bpm/mapper/BpmApprovalRecordMapper.java b/wm-bpm/src/main/java/com/water/bpm/mapper/BpmApprovalRecordMapper.java new file mode 100644 index 00000000..0ca4c9b4 --- /dev/null +++ b/wm-bpm/src/main/java/com/water/bpm/mapper/BpmApprovalRecordMapper.java @@ -0,0 +1,39 @@ +package com.water.bpm.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.bpm.entity.BpmApprovalRecord; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 审批记录 Mapper + */ +@Mapper +public interface BpmApprovalRecordMapper extends BaseMapper { + + /** + * 根据流程实例查询审批记录 + */ + @Select("SELECT * FROM bpm_approval_record WHERE instance_id = #{instanceId} AND deleted = 0 ORDER BY approved_at") + List selectByInstanceId(@Param("instanceId") Long instanceId); + + /** + * 根据流程实例UUID查询审批记录 + */ + @Select("SELECT * FROM bpm_approval_record WHERE instance_uuid = #{instanceUuid} AND deleted = 0 ORDER BY approved_at") + List selectByInstanceUuid(@Param("instanceUuid") String instanceUuid); + + /** + * 查询某人的审批统计 + */ + @Select("SELECT approver_id, approver_name, COUNT(*) as total, " + + "SUM(CASE WHEN action = 'approve' THEN 1 ELSE 0 END) as approved, " + + "SUM(CASE WHEN action = 'reject' THEN 1 ELSE 0 END) as rejected, " + + "AVG(EXTRACT(EPOCH FROM (approved_at - created_at))) as avg_handle_seconds " + + "FROM bpm_approval_record WHERE deleted = 0 AND approved_at >= #{startDate} " + + "GROUP BY approver_id, approver_name") + List> statByApprover(@Param("startDate") java.time.LocalDateTime startDate); +} diff --git a/wm-bpm/src/main/java/com/water/bpm/mapper/BpmFormTemplateMapper.java b/wm-bpm/src/main/java/com/water/bpm/mapper/BpmFormTemplateMapper.java new file mode 100644 index 00000000..d0a90a47 --- /dev/null +++ b/wm-bpm/src/main/java/com/water/bpm/mapper/BpmFormTemplateMapper.java @@ -0,0 +1,28 @@ +package com.water.bpm.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.bpm.entity.BpmFormTemplate; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 表单模板 Mapper + */ +@Mapper +public interface BpmFormTemplateMapper extends BaseMapper { + + /** + * 根据分类查询模板 + */ + @Select("SELECT * FROM bpm_form_template WHERE category = #{category} AND status = 1 AND deleted = 0 ORDER BY use_count DESC") + List selectByCategory(@Param("category") String category); + + /** + * 查询热门模板 + */ + @Select("SELECT * FROM bpm_form_template WHERE status = 1 AND deleted = 0 ORDER BY use_count DESC LIMIT #{limit}") + List selectHotTemplates(@Param("limit") int limit); +} diff --git a/wm-bpm/src/main/java/com/water/bpm/mapper/BpmOrchestrationMapper.java b/wm-bpm/src/main/java/com/water/bpm/mapper/BpmOrchestrationMapper.java new file mode 100644 index 00000000..6e5a0770 --- /dev/null +++ b/wm-bpm/src/main/java/com/water/bpm/mapper/BpmOrchestrationMapper.java @@ -0,0 +1,28 @@ +package com.water.bpm.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.bpm.entity.BpmOrchestration; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 流程编排 Mapper + */ +@Mapper +public interface BpmOrchestrationMapper extends BaseMapper { + + /** + * 查询所有启用的编排 + */ + @Select("SELECT * FROM bpm_orchestration WHERE status = 1 AND deleted = 0 ORDER BY created_at DESC") + List selectEnabled(); + + /** + * 查询事件触发的编排 + */ + @Select("SELECT * FROM bpm_orchestration WHERE trigger_type = 'event' AND event_name = #{eventName} " + + "AND status = 1 AND deleted = 0") + List selectByEvent(String eventName); +} diff --git a/wm-bpm/src/main/java/com/water/bpm/mapper/BpmProcessDefinitionMapper.java b/wm-bpm/src/main/java/com/water/bpm/mapper/BpmProcessDefinitionMapper.java new file mode 100644 index 00000000..2758f6c7 --- /dev/null +++ b/wm-bpm/src/main/java/com/water/bpm/mapper/BpmProcessDefinitionMapper.java @@ -0,0 +1,34 @@ +package com.water.bpm.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.bpm.entity.BpmProcessDefinition; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 流程定义 Mapper + */ +@Mapper +public interface BpmProcessDefinitionMapper extends BaseMapper { + + /** + * 根据分类查询已发布的流程定义 + */ + @Select("SELECT * FROM bpm_process_definition WHERE category = #{category} AND status = 1 AND deleted = 0 ORDER BY sort_order") + List selectByCategory(@Param("category") String category); + + /** + * 根据 processKey 查询最新版本 + */ + @Select("SELECT * FROM bpm_process_definition WHERE process_key = #{processKey} AND deleted = 0 ORDER BY version DESC LIMIT 1") + BpmProcessDefinition selectByProcessKey(@Param("processKey") String processKey); + + /** + * 查询所有分类 + */ + @Select("SELECT DISTINCT category FROM bpm_process_definition WHERE deleted = 0 AND status = 1 ORDER BY category") + List selectAllCategories(); +} diff --git a/wm-bpm/src/main/java/com/water/bpm/mapper/BpmProcessInstanceMapper.java b/wm-bpm/src/main/java/com/water/bpm/mapper/BpmProcessInstanceMapper.java new file mode 100644 index 00000000..c00d8bc9 --- /dev/null +++ b/wm-bpm/src/main/java/com/water/bpm/mapper/BpmProcessInstanceMapper.java @@ -0,0 +1,43 @@ +package com.water.bpm.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.bpm.entity.BpmProcessInstance; +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 + */ +@Mapper +public interface BpmProcessInstanceMapper extends BaseMapper { + + /** + * 统计各状态数量 + */ + @Select("SELECT status, COUNT(*) as count FROM bpm_process_instance WHERE deleted = 0 GROUP BY status") + List> countByStatus(); + + /** + * 根据流程定义统计 + */ + @Select("SELECT definition_id, COUNT(*) as total, " + + "SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) as running, " + + "SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed, " + + "SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected, " + + "SUM(CASE WHEN status = 'terminated' THEN 1 ELSE 0 END) as terminated, " + + "AVG(duration_seconds) as avg_duration, " + + "MAX(duration_seconds) as max_duration, " + + "MIN(duration_seconds) as min_duration " + + "FROM bpm_process_instance WHERE deleted = 0 GROUP BY definition_id") + List> statByDefinition(); + + /** + * 查询我发起的流程 + */ + @Select("SELECT * FROM bpm_process_instance WHERE initiator_id = #{userId} AND deleted = 0 ORDER BY created_at DESC") + List selectMyInitiated(@Param("userId") Long userId); +} diff --git a/wm-bpm/src/main/java/com/water/bpm/mapper/BpmProcessNodeMapper.java b/wm-bpm/src/main/java/com/water/bpm/mapper/BpmProcessNodeMapper.java new file mode 100644 index 00000000..c9b94565 --- /dev/null +++ b/wm-bpm/src/main/java/com/water/bpm/mapper/BpmProcessNodeMapper.java @@ -0,0 +1,22 @@ +package com.water.bpm.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.bpm.entity.BpmProcessNode; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 流程节点 Mapper + */ +@Mapper +public interface BpmProcessNodeMapper extends BaseMapper { + + /** + * 查询流程定义的所有节点 + */ + @Select("SELECT * FROM bpm_process_node WHERE definition_id = #{definitionId} AND deleted = 0 ORDER BY sort_order") + List selectByDefinitionId(@Param("definitionId") Long definitionId); +} diff --git a/wm-bpm/src/main/java/com/water/bpm/mapper/BpmProcessStatMapper.java b/wm-bpm/src/main/java/com/water/bpm/mapper/BpmProcessStatMapper.java new file mode 100644 index 00000000..481a308c --- /dev/null +++ b/wm-bpm/src/main/java/com/water/bpm/mapper/BpmProcessStatMapper.java @@ -0,0 +1,25 @@ +package com.water.bpm.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.bpm.entity.BpmProcessStat; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 流程统计 Mapper + */ +@Mapper +public interface BpmProcessStatMapper extends BaseMapper { + + /** + * 查询某流程的统计趋势 + */ + @Select("SELECT * FROM bpm_process_stat WHERE definition_id = #{definitionId} AND period = #{period} " + + "AND deleted = 0 ORDER BY stat_date DESC LIMIT #{limit}") + List selectTrend(@Param("definitionId") Long definitionId, + @Param("period") String period, + @Param("limit") int limit); +} diff --git a/wm-bpm/src/main/java/com/water/bpm/mapper/BpmTodoTaskMapper.java b/wm-bpm/src/main/java/com/water/bpm/mapper/BpmTodoTaskMapper.java new file mode 100644 index 00000000..848542ff --- /dev/null +++ b/wm-bpm/src/main/java/com/water/bpm/mapper/BpmTodoTaskMapper.java @@ -0,0 +1,42 @@ +package com.water.bpm.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.bpm.entity.BpmTodoTask; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 待办任务 Mapper + */ +@Mapper +public interface BpmTodoTaskMapper extends BaseMapper { + + /** + * 查询待办列表 + */ + @Select("SELECT * FROM bpm_todo_task WHERE assignee_id = #{userId} AND status = 'pending' AND deleted = 0 " + + "ORDER BY priority DESC, received_at") + List selectPendingByUserId(@Param("userId") Long userId); + + /** + * 查询已办列表 + */ + @Select("SELECT * FROM bpm_todo_task WHERE assignee_id = #{userId} AND status != 'pending' AND deleted = 0 " + + "ORDER BY completed_at DESC") + List selectDoneByUserId(@Param("userId") Long userId); + + /** + * 统计待办数量 + */ + @Select("SELECT COUNT(*) FROM bpm_todo_task WHERE assignee_id = #{userId} AND status = 'pending' AND deleted = 0") + Integer countPendingByUserId(@Param("userId") Long userId); + + /** + * 查询超时的待办 + */ + @Select("SELECT * FROM bpm_todo_task WHERE status = 'pending' AND deadline_at < NOW() AND deleted = 0") + List selectTimeoutTasks(); +} diff --git a/wm-config/pom.xml b/wm-config/pom.xml new file mode 100644 index 00000000..db5f2c1f --- /dev/null +++ b/wm-config/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + com.waterwm-parent1.0.0-SNAPSHOT + wm-config + + com.waterwm-common + org.springframework.bootspring-boot-starter-web + com.alibaba.cloudspring-cloud-starter-alibaba-nacos-discovery + com.baomidoumybatis-plus-spring-boot3-starter + cn.dev33sa-token-spring-boot3-starter + org.postgresqlpostgresql + + diff --git a/wm-config/src/main/java/com/water/config/ConfigApplication.java b/wm-config/src/main/java/com/water/config/ConfigApplication.java new file mode 100644 index 00000000..26db7723 --- /dev/null +++ b/wm-config/src/main/java/com/water/config/ConfigApplication.java @@ -0,0 +1,15 @@ +package com.water.config; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +@SpringBootApplication +@EnableDiscoveryClient +@MapperScan("com.water.config.mapper") +public class ConfigApplication { + public static void main(String[] args) { + SpringApplication.run(ConfigApplication.class, args); + } +} diff --git a/wm-config/src/main/java/com/water/config/controller/AnnouncementController.java b/wm-config/src/main/java/com/water/config/controller/AnnouncementController.java new file mode 100644 index 00000000..9cbeaee4 --- /dev/null +++ b/wm-config/src/main/java/com/water/config/controller/AnnouncementController.java @@ -0,0 +1,68 @@ +package com.water.config.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.water.common.core.result.R; +import com.water.config.entity.Announcement; +import com.water.config.service.AnnouncementService; +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/config/announcement") +@RequiredArgsConstructor +public class AnnouncementController { + + private final AnnouncementService announcementService; + + @Operation(summary = "分页查询公告") + @GetMapping("/list") + public R> list(@RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false) Integer type, + @RequestParam(required = false) Integer publishStatus) { + return R.ok(announcementService.pageAnnouncements(page, size, type, publishStatus)); + } + + @Operation(summary = "获取公告详情") + @GetMapping("/{id}") + public R getById(@PathVariable Long id) { + return R.ok(announcementService.getById(id)); + } + + @Operation(summary = "创建公告(草稿)") + @PostMapping + public R create(@RequestBody Announcement announcement) { + return R.ok(announcementService.createAnnouncement(announcement)); + } + + @Operation(summary = "更新公告") + @PutMapping("/{id}") + public R update(@PathVariable Long id, @RequestBody Announcement announcement) { + announcementService.updateAnnouncement(id, announcement); + return R.ok("更新成功"); + } + + @Operation(summary = "发布公告") + @PostMapping("/{id}/publish") + public R publish(@PathVariable Long id) { + announcementService.publish(id); + return R.ok("发布成功"); + } + + @Operation(summary = "撤回公告") + @PostMapping("/{id}/withdraw") + public R withdraw(@PathVariable Long id) { + announcementService.withdraw(id); + return R.ok("撤回成功"); + } + + @Operation(summary = "删除公告") + @DeleteMapping("/{id}") + public R delete(@PathVariable Long id) { + announcementService.deleteAnnouncement(id); + return R.ok("删除成功"); + } +} diff --git a/wm-config/src/main/java/com/water/config/controller/DeviceManageController.java b/wm-config/src/main/java/com/water/config/controller/DeviceManageController.java new file mode 100644 index 00000000..707978d0 --- /dev/null +++ b/wm-config/src/main/java/com/water/config/controller/DeviceManageController.java @@ -0,0 +1,98 @@ +package com.water.config.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.water.common.core.result.R; +import com.water.config.entity.DeviceInfo; +import com.water.config.entity.DeviceMaintenance; +import com.water.config.service.DeviceManageService; +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; + +@Tag(name = "设备管理") +@RestController +@RequestMapping("/api/config/device") +@RequiredArgsConstructor +public class DeviceManageController { + + private final DeviceManageService deviceManageService; + + @Operation(summary = "分页查询设备") + @GetMapping("/list") + public R> list(@RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false) String deviceName, + @RequestParam(required = false) Integer category, + @RequestParam(required = false) Integer deviceStatus) { + return R.ok(deviceManageService.pageDevices(page, size, deviceName, category, deviceStatus)); + } + + @Operation(summary = "获取设备详情") + @GetMapping("/{id}") + public R getById(@PathVariable Long id) { + return R.ok(deviceManageService.getById(id)); + } + + @Operation(summary = "创建设备") + @PostMapping + public R create(@RequestBody DeviceInfo device) { + return R.ok(deviceManageService.createDevice(device)); + } + + @Operation(summary = "更新设备") + @PutMapping("/{id}") + public R update(@PathVariable Long id, @RequestBody DeviceInfo device) { + deviceManageService.updateDevice(id, device); + return R.ok("更新成功"); + } + + @Operation(summary = "删除设备") + @DeleteMapping("/{id}") + public R delete(@PathVariable Long id) { + deviceManageService.removeById(id); + return R.ok("删除成功"); + } + + @Operation(summary = "更新设备状态") + @PutMapping("/{id}/status") + public R updateStatus(@PathVariable Long id, @RequestParam Integer deviceStatus) { + deviceManageService.updateDeviceStatus(id, deviceStatus); + return R.ok("状态更新成功"); + } + + @Operation(summary = "按分类查询设备") + @GetMapping("/category/{category}") + public R> getByCategory(@PathVariable Integer category) { + return R.ok(deviceManageService.getDevicesByCategory(category)); + } + + @Operation(summary = "按状态查询设备") + @GetMapping("/status/{status}") + public R> getByStatus(@PathVariable Integer status) { + return R.ok(deviceManageService.getDevicesByStatus(status)); + } + + @Operation(summary = "添加维保记录") + @PostMapping("/maintenance") + public R addMaintenance(@RequestBody DeviceMaintenance maintenance) { + return R.ok(deviceManageService.addMaintenance(maintenance)); + } + + @Operation(summary = "查询维保记录") + @GetMapping("/maintenance") + public R> pageMaintenances(@RequestParam(required = false) Long deviceId, + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size) { + return R.ok(deviceManageService.pageMaintenances(deviceId, page, size)); + } + + @Operation(summary = "更新维保记录") + @PutMapping("/maintenance/{id}") + public R updateMaintenance(@PathVariable Long id, @RequestBody DeviceMaintenance maintenance) { + deviceManageService.updateMaintenance(id, maintenance); + return R.ok("更新成功"); + } +} diff --git a/wm-config/src/main/java/com/water/config/controller/ThresholdController.java b/wm-config/src/main/java/com/water/config/controller/ThresholdController.java new file mode 100644 index 00000000..a5507b29 --- /dev/null +++ b/wm-config/src/main/java/com/water/config/controller/ThresholdController.java @@ -0,0 +1,84 @@ +package com.water.config.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.water.common.core.result.R; +import com.water.config.entity.ThresholdChangeLog; +import com.water.config.entity.ThresholdConfig; +import com.water.config.service.ThresholdService; +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; + +@Tag(name = "阈值管理") +@RestController +@RequestMapping("/api/config/threshold") +@RequiredArgsConstructor +public class ThresholdController { + + private final ThresholdService thresholdService; + + @Operation(summary = "分页查询阈值配置") + @GetMapping("/list") + public R> list(@RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false) String metricCode, + @RequestParam(required = false) Integer level) { + return R.ok(thresholdService.pageThresholds(page, size, metricCode, level)); + } + + @Operation(summary = "获取阈值详情") + @GetMapping("/{id}") + public R getById(@PathVariable Long id) { + return R.ok(thresholdService.getById(id)); + } + + @Operation(summary = "创建阈值配置") + @PostMapping + public R create(@RequestBody ThresholdConfig config) { + return R.ok(thresholdService.createThreshold(config)); + } + + @Operation(summary = "更新阈值配置") + @PutMapping("/{id}") + public R update(@PathVariable Long id, @RequestBody ThresholdConfig config) { + thresholdService.updateThreshold(id, config); + return R.ok("更新成功"); + } + + @Operation(summary = "删除阈值配置") + @DeleteMapping("/{id}") + public R delete(@PathVariable Long id) { + thresholdService.deleteThreshold(id); + return R.ok("删除成功"); + } + + @Operation(summary = "启用/禁用阈值") + @PutMapping("/{id}/status") + public R toggleStatus(@PathVariable Long id, @RequestParam Integer status) { + thresholdService.toggleStatus(id, status); + return R.ok(status == 1 ? "已启用" : "已禁用"); + } + + @Operation(summary = "获取指标的全局阈值(多级)") + @GetMapping("/global/{metricCode}") + public R> getGlobalThresholds(@PathVariable String metricCode) { + return R.ok(thresholdService.getGlobalThresholds(metricCode)); + } + + @Operation(summary = "获取设备阈值配置") + @GetMapping("/device/{deviceId}") + public R> getDeviceThresholds(@PathVariable Long deviceId) { + return R.ok(thresholdService.getDeviceThresholds(deviceId)); + } + + @Operation(summary = "获取阈值变更历史") + @GetMapping("/history") + public R> getChangeHistory(@RequestParam(required = false) Long thresholdId, + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size) { + return R.ok(thresholdService.getChangeHistory(thresholdId, page, size)); + } +} diff --git a/wm-config/src/main/java/com/water/config/entity/Announcement.java b/wm-config/src/main/java/com/water/config/entity/Announcement.java new file mode 100644 index 00000000..4a1ce99d --- /dev/null +++ b/wm-config/src/main/java/com/water/config/entity/Announcement.java @@ -0,0 +1,35 @@ +package com.water.config.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +/** + * 公告通知 + */ +@Data +@TableName("config_announcement") +public class Announcement { + @TableId(type = IdType.AUTO) + private Long id; + /** 标题 */ + private String title; + /** 内容 */ + private String content; + /** 类型: 1-系统公告 2-维护通知 3-紧急通知 */ + private Integer type; + /** 发布状态: 0-草稿 1-已发布 2-已撤回 */ + private Integer publishStatus; + /** 发布渠道(JSON数组): ["sms","push","site"] */ + private String channels; + /** 发布人 */ + private String publisher; + /** 发布时间 */ + private LocalDateTime publishTime; + /** 撤回时间 */ + private LocalDateTime withdrawTime; + @TableLogic + private Integer deleted; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/wm-config/src/main/java/com/water/config/entity/DeviceInfo.java b/wm-config/src/main/java/com/water/config/entity/DeviceInfo.java new file mode 100644 index 00000000..76d84794 --- /dev/null +++ b/wm-config/src/main/java/com/water/config/entity/DeviceInfo.java @@ -0,0 +1,45 @@ +package com.water.config.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +/** + * 设备台账 + */ +@Data +@TableName("config_device_info") +public class DeviceInfo { + @TableId(type = IdType.AUTO) + private Long id; + /** 设备编码 */ + private String deviceCode; + /** 设备名称 */ + private String deviceName; + /** 设备分类: 1-水表 2-压力传感器 3-流量计 4-水质监测仪 5-阀门 9-其他 */ + private Integer category; + /** 品牌 */ + private String brand; + /** 型号 */ + private String model; + /** 安装位置 */ + private String location; + /** 经度 */ + private Double longitude; + /** 纬度 */ + private Double latitude; + /** 设备状态: 0-离线 1-在线 2-故障 3-维修中 */ + private Integer deviceStatus; + /** 安装日期 */ + private LocalDateTime installDate; + /** 最后维护时间 */ + private LocalDateTime lastMaintenanceTime; + /** 负责人 */ + private String responsiblePerson; + /** 备注 */ + private String remark; + @TableLogic + private Integer deleted; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/wm-config/src/main/java/com/water/config/entity/DeviceMaintenance.java b/wm-config/src/main/java/com/water/config/entity/DeviceMaintenance.java new file mode 100644 index 00000000..d566af13 --- /dev/null +++ b/wm-config/src/main/java/com/water/config/entity/DeviceMaintenance.java @@ -0,0 +1,37 @@ +package com.water.config.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +/** + * 设备维保记录 + */ +@Data +@TableName("config_device_maintenance") +public class DeviceMaintenance { + @TableId(type = IdType.AUTO) + private Long id; + /** 设备ID */ + private Long deviceId; + /** 维保类型: 1-日常巡检 2-定期保养 3-故障维修 4-更换配件 */ + private Integer maintenanceType; + /** 维保描述 */ + private String description; + /** 维保人 */ + private String operator; + /** 维保开始时间 */ + private LocalDateTime startTime; + /** 维保结束时间 */ + private LocalDateTime endTime; + /** 维保结果: 0-未完成 1-已完成 2-需要返修 */ + private Integer result; + /** 费用 */ + private Double cost; + /** 附件(JSON) */ + private String attachments; + @TableLogic + private Integer deleted; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/wm-config/src/main/java/com/water/config/entity/ThresholdChangeLog.java b/wm-config/src/main/java/com/water/config/entity/ThresholdChangeLog.java new file mode 100644 index 00000000..cadd348b --- /dev/null +++ b/wm-config/src/main/java/com/water/config/entity/ThresholdChangeLog.java @@ -0,0 +1,35 @@ +package com.water.config.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 阈值变更记录 + */ +@Data +@TableName("config_threshold_change_log") +public class ThresholdChangeLog { + @TableId(type = IdType.AUTO) + private Long id; + /** 关联阈值ID */ + private Long thresholdId; + /** 变更前最小值 */ + private BigDecimal oldMinValue; + /** 变更前最大值 */ + private BigDecimal oldMaxValue; + /** 变更后最小值 */ + private BigDecimal newMinValue; + /** 变更后最大值 */ + private BigDecimal newMaxValue; + /** 变更前级别 */ + private Integer oldLevel; + /** 变更后级别 */ + private Integer newLevel; + /** 变更人 */ + private String operator; + /** 变更原因 */ + private String reason; + private LocalDateTime createdAt; +} diff --git a/wm-config/src/main/java/com/water/config/entity/ThresholdConfig.java b/wm-config/src/main/java/com/water/config/entity/ThresholdConfig.java new file mode 100644 index 00000000..3eae99f7 --- /dev/null +++ b/wm-config/src/main/java/com/water/config/entity/ThresholdConfig.java @@ -0,0 +1,38 @@ +package com.water.config.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 阈值配置 + */ +@Data +@TableName("config_threshold") +public class ThresholdConfig { + @TableId(type = IdType.AUTO) + private Long id; + /** 指标编码 */ + private String metricCode; + /** 指标名称 */ + private String metricName; + /** 设备ID(可选,null表示全局) */ + private Long deviceId; + /** 阈值级别: 1-预警 2-报警 3-紧急 */ + private Integer level; + /** 最小值 */ + private BigDecimal minValue; + /** 最大值 */ + private BigDecimal maxValue; + /** 单位 */ + private String unit; + /** 启用状态: 0-禁用 1-启用 */ + private Integer status; + /** 备注 */ + private String remark; + @TableLogic + private Integer deleted; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/wm-config/src/main/java/com/water/config/mapper/AnnouncementMapper.java b/wm-config/src/main/java/com/water/config/mapper/AnnouncementMapper.java new file mode 100644 index 00000000..95d92801 --- /dev/null +++ b/wm-config/src/main/java/com/water/config/mapper/AnnouncementMapper.java @@ -0,0 +1,9 @@ +package com.water.config.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.config.entity.Announcement; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface AnnouncementMapper extends BaseMapper { +} diff --git a/wm-config/src/main/java/com/water/config/mapper/DeviceInfoMapper.java b/wm-config/src/main/java/com/water/config/mapper/DeviceInfoMapper.java new file mode 100644 index 00000000..63ad028c --- /dev/null +++ b/wm-config/src/main/java/com/water/config/mapper/DeviceInfoMapper.java @@ -0,0 +1,17 @@ +package com.water.config.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.config.entity.DeviceInfo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; +import java.util.List; + +@Mapper +public interface DeviceInfoMapper extends BaseMapper { + + @Select("SELECT * FROM config_device_info WHERE device_status = #{status} AND deleted = 0") + List selectByDeviceStatus(Integer status); + + @Select("SELECT * FROM config_device_info WHERE category = #{category} AND deleted = 0 ORDER BY device_code") + List selectByCategory(Integer category); +} diff --git a/wm-config/src/main/java/com/water/config/mapper/DeviceMaintenanceMapper.java b/wm-config/src/main/java/com/water/config/mapper/DeviceMaintenanceMapper.java new file mode 100644 index 00000000..45a39323 --- /dev/null +++ b/wm-config/src/main/java/com/water/config/mapper/DeviceMaintenanceMapper.java @@ -0,0 +1,14 @@ +package com.water.config.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.config.entity.DeviceMaintenance; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; +import java.util.List; + +@Mapper +public interface DeviceMaintenanceMapper extends BaseMapper { + + @Select("SELECT * FROM config_device_maintenance WHERE device_id = #{deviceId} AND deleted = 0 ORDER BY start_time DESC") + List selectByDeviceId(Long deviceId); +} diff --git a/wm-config/src/main/java/com/water/config/mapper/ThresholdChangeLogMapper.java b/wm-config/src/main/java/com/water/config/mapper/ThresholdChangeLogMapper.java new file mode 100644 index 00000000..9d37f034 --- /dev/null +++ b/wm-config/src/main/java/com/water/config/mapper/ThresholdChangeLogMapper.java @@ -0,0 +1,9 @@ +package com.water.config.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.config.entity.ThresholdChangeLog; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ThresholdChangeLogMapper extends BaseMapper { +} diff --git a/wm-config/src/main/java/com/water/config/mapper/ThresholdConfigMapper.java b/wm-config/src/main/java/com/water/config/mapper/ThresholdConfigMapper.java new file mode 100644 index 00000000..8e57c1f1 --- /dev/null +++ b/wm-config/src/main/java/com/water/config/mapper/ThresholdConfigMapper.java @@ -0,0 +1,17 @@ +package com.water.config.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.config.entity.ThresholdConfig; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; +import java.util.List; + +@Mapper +public interface ThresholdConfigMapper extends BaseMapper { + + @Select("SELECT * FROM config_threshold WHERE metric_code = #{metricCode} AND device_id IS NULL AND status = 1 AND deleted = 0 ORDER BY level") + List selectGlobalByMetricCode(String metricCode); + + @Select("SELECT * FROM config_threshold WHERE device_id = #{deviceId} AND status = 1 AND deleted = 0 ORDER BY metric_code, level") + List selectByDeviceId(Long deviceId); +} diff --git a/wm-config/src/main/java/com/water/config/service/AnnouncementService.java b/wm-config/src/main/java/com/water/config/service/AnnouncementService.java new file mode 100644 index 00000000..6091d726 --- /dev/null +++ b/wm-config/src/main/java/com/water/config/service/AnnouncementService.java @@ -0,0 +1,131 @@ +package com.water.config.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.water.common.core.exception.BusinessException; +import com.water.config.entity.Announcement; +import com.water.config.mapper.AnnouncementMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +/** + * 公告通知服务 + */ +@Slf4j +@Service +public class AnnouncementService extends ServiceImpl { + + /** + * 分页查询公告 + */ + public Page pageAnnouncements(int page, int size, Integer type, Integer publishStatus) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + if (type != null) { + qw.eq(Announcement::getType, type); + } + if (publishStatus != null) { + qw.eq(Announcement::getPublishStatus, publishStatus); + } + qw.orderByDesc(Announcement::getCreatedAt); + return this.page(new Page<>(page, size), qw); + } + + /** + * 创建公告(草稿) + */ + public Announcement createAnnouncement(Announcement announcement) { + announcement.setPublishStatus(0); + this.save(announcement); + return announcement; + } + + /** + * 更新公告(仅草稿可更新) + */ + public void updateAnnouncement(Long id, Announcement announcement) { + Announcement existing = this.getById(id); + if (existing == null) { + throw new BusinessException("公告不存在"); + } + if (existing.getPublishStatus() != 0) { + throw new BusinessException("已发布的公告不可修改"); + } + announcement.setId(id); + this.updateById(announcement); + } + + /** + * 发布公告 + */ + @Transactional + public void publish(Long id) { + Announcement announcement = this.getById(id); + if (announcement == null) { + throw new BusinessException("公告不存在"); + } + if (announcement.getPublishStatus() != 0) { + throw new BusinessException("只有草稿状态的公告可以发布"); + } + announcement.setPublishStatus(1); + announcement.setPublishTime(LocalDateTime.now()); + this.updateById(announcement); + // 多渠道发布 + dispatchChannels(announcement); + } + + /** + * 撤回公告 + */ + @Transactional + public void withdraw(Long id) { + Announcement announcement = this.getById(id); + if (announcement == null) { + throw new BusinessException("公告不存在"); + } + if (announcement.getPublishStatus() != 1) { + throw new BusinessException("只有已发布的公告可以撤回"); + } + announcement.setPublishStatus(2); + announcement.setWithdrawTime(LocalDateTime.now()); + this.updateById(announcement); + } + + /** + * 删除公告 + */ + public void deleteAnnouncement(Long id) { + Announcement existing = this.getById(id); + if (existing == null) { + throw new BusinessException("公告不存在"); + } + if (existing.getPublishStatus() == 1) { + throw new BusinessException("已发布的公告不可删除,请先撤回"); + } + this.removeById(id); + } + + /** + * 多渠道分发(模拟) + */ + private void dispatchChannels(Announcement announcement) { + String channels = announcement.getChannels(); + if (channels == null || channels.isEmpty()) { + log.info("公告 {} 无渠道配置,仅站内信发布", announcement.getId()); + return; + } + log.info("公告 {} 发布渠道: {}", announcement.getId(), channels); + if (channels.contains("sms")) { + log.info("→ 短信渠道已触发"); + } + if (channels.contains("push")) { + log.info("→ APP推送渠道已触发"); + } + if (channels.contains("site")) { + log.info("→ 站内信渠道已触发"); + } + } +} diff --git a/wm-config/src/main/java/com/water/config/service/DeviceManageService.java b/wm-config/src/main/java/com/water/config/service/DeviceManageService.java new file mode 100644 index 00000000..839f8733 --- /dev/null +++ b/wm-config/src/main/java/com/water/config/service/DeviceManageService.java @@ -0,0 +1,150 @@ +package com.water.config.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.water.common.core.exception.BusinessException; +import com.water.config.entity.DeviceInfo; +import com.water.config.entity.DeviceMaintenance; +import com.water.config.mapper.DeviceInfoMapper; +import com.water.config.mapper.DeviceMaintenanceMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 设备管理服务 + */ +@Service +@RequiredArgsConstructor +public class DeviceManageService extends ServiceImpl { + + private final DeviceMaintenanceMapper maintenanceMapper; + + /** + * 分页查询设备 + */ + public Page pageDevices(int page, int size, String deviceName, Integer category, Integer deviceStatus) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + if (deviceName != null && !deviceName.isEmpty()) { + qw.like(DeviceInfo::getDeviceName, deviceName); + } + if (category != null) { + qw.eq(DeviceInfo::getCategory, category); + } + if (deviceStatus != null) { + qw.eq(DeviceInfo::getDeviceStatus, deviceStatus); + } + qw.orderByDesc(DeviceInfo::getCreatedAt); + return this.page(new Page<>(page, size), qw); + } + + /** + * 创建设备 + */ + public DeviceInfo createDevice(DeviceInfo device) { + // 检查设备编码唯一性 + long count = this.count(new LambdaQueryWrapper() + .eq(DeviceInfo::getDeviceCode, device.getDeviceCode())); + if (count > 0) { + throw new BusinessException("设备编码已存在"); + } + this.save(device); + return device; + } + + /** + * 更新设备 + */ + public void updateDevice(Long id, DeviceInfo device) { + DeviceInfo existing = this.getById(id); + if (existing == null) { + throw new BusinessException("设备不存在"); + } + device.setId(id); + this.updateById(device); + } + + /** + * 更新设备状态 + */ + public void updateDeviceStatus(Long id, Integer deviceStatus) { + DeviceInfo device = this.getById(id); + if (device == null) { + throw new BusinessException("设备不存在"); + } + device.setDeviceStatus(deviceStatus); + this.updateById(device); + } + + /** + * 按分类查询设备 + */ + public List getDevicesByCategory(Integer category) { + return baseMapper.selectByCategory(category); + } + + /** + * 按状态查询设备 + */ + public List getDevicesByStatus(Integer status) { + return baseMapper.selectByDeviceStatus(status); + } + + /** + * 添加维保记录 + */ + @Transactional + public DeviceMaintenance addMaintenance(DeviceMaintenance maintenance) { + DeviceInfo device = this.getById(maintenance.getDeviceId()); + if (device == null) { + throw new BusinessException("设备不存在"); + } + maintenanceMapper.insert(maintenance); + // 如果维保完成,更新设备最后维护时间 + if (maintenance.getResult() != null && maintenance.getResult() == 1) { + device.setLastMaintenanceTime(LocalDateTime.now()); + if (device.getDeviceStatus() == 3) { + device.setDeviceStatus(1); // 维修中 -> 在线 + } + this.updateById(device); + } + return maintenance; + } + + /** + * 查询设备维保历史 + */ + public Page pageMaintenances(Long deviceId, int page, int size) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + if (deviceId != null) { + qw.eq(DeviceMaintenance::getDeviceId, deviceId); + } + qw.orderByDesc(DeviceMaintenance::getStartTime); + return maintenanceMapper.selectPage(new Page<>(page, size), qw); + } + + /** + * 更新维保记录 + */ + public void updateMaintenance(Long id, DeviceMaintenance maintenance) { + maintenance.setId(id); + maintenanceMapper.updateById(maintenance); + } + + /** + * 获取设备统计(按分类) + */ + public List getDeviceCountByCategory() { + // 简化版:返回各分类的设备数量 + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.select(DeviceInfo::getCategory); + qw.groupBy(DeviceInfo::getCategory); + return this.listMaps(qw).stream() + .map(m -> ((Number) m.get("category")).longValue()) + .toList(); + } +} diff --git a/wm-config/src/main/java/com/water/config/service/ThresholdService.java b/wm-config/src/main/java/com/water/config/service/ThresholdService.java new file mode 100644 index 00000000..550940b6 --- /dev/null +++ b/wm-config/src/main/java/com/water/config/service/ThresholdService.java @@ -0,0 +1,144 @@ +package com.water.config.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.water.config.entity.ThresholdChangeLog; +import com.water.config.entity.ThresholdConfig; +import com.water.config.mapper.ThresholdChangeLogMapper; +import com.water.config.mapper.ThresholdConfigMapper; +import com.water.common.core.exception.BusinessException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 阈值管理服务 + */ +@Service +@RequiredArgsConstructor +public class ThresholdService extends ServiceImpl { + + private final ThresholdChangeLogMapper changeLogMapper; + + /** + * 分页查询阈值配置 + */ + public Page pageThresholds(int page, int size, String metricCode, Integer level) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + if (metricCode != null && !metricCode.isEmpty()) { + qw.like(ThresholdConfig::getMetricCode, metricCode); + } + if (level != null) { + qw.eq(ThresholdConfig::getLevel, level); + } + qw.orderByAsc(ThresholdConfig::getMetricCode, ThresholdConfig::getLevel); + return this.page(new Page<>(page, size), qw); + } + + /** + * 创建阈值配置 + */ + @Transactional + public ThresholdConfig createThreshold(ThresholdConfig config) { + validateThreshold(config); + this.save(config); + recordChangeLog(config, null, "新建阈值配置"); + return config; + } + + /** + * 更新阈值配置(记录变更) + */ + @Transactional + public void updateThreshold(Long id, ThresholdConfig config) { + ThresholdConfig old = this.getById(id); + if (old == null) { + throw new BusinessException("阈值配置不存在"); + } + config.setId(id); + validateThreshold(config); + this.updateById(config); + recordChangeLog(config, old, "更新阈值配置"); + } + + /** + * 删除阈值配置 + */ + @Transactional + public void deleteThreshold(Long id) { + ThresholdConfig old = this.getById(id); + if (old == null) { + throw new BusinessException("阈值配置不存在"); + } + this.removeById(id); + recordChangeLog(old, old, "删除阈值配置"); + } + + /** + * 获取某指标的全局阈值(多级) + */ + public List getGlobalThresholds(String metricCode) { + return baseMapper.selectGlobalByMetricCode(metricCode); + } + + /** + * 获取某设备的阈值配置 + */ + public List getDeviceThresholds(Long deviceId) { + return baseMapper.selectByDeviceId(deviceId); + } + + /** + * 获取阈值变更历史 + */ + public Page getChangeHistory(Long thresholdId, int page, int size) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + if (thresholdId != null) { + qw.eq(ThresholdChangeLog::getThresholdId, thresholdId); + } + qw.orderByDesc(ThresholdChangeLog::getCreatedAt); + return changeLogMapper.selectPage(new Page<>(page, size), qw); + } + + /** + * 启用/禁用阈值 + */ + public void toggleStatus(Long id, Integer status) { + ThresholdConfig config = this.getById(id); + if (config == null) { + throw new BusinessException("阈值配置不存在"); + } + config.setStatus(status); + this.updateById(config); + } + + private void validateThreshold(ThresholdConfig config) { + if (config.getMinValue() != null && config.getMaxValue() != null) { + if (config.getMinValue().compareTo(config.getMaxValue()) > 0) { + throw new BusinessException("最小值不能大于最大值"); + } + } + if (config.getLevel() != null && (config.getLevel() < 1 || config.getLevel() > 3)) { + throw new BusinessException("阈值级别必须在1-3之间"); + } + } + + private void recordChangeLog(ThresholdConfig newConfig, ThresholdConfig oldConfig, String reason) { + ThresholdChangeLog log = new ThresholdChangeLog(); + log.setThresholdId(newConfig.getId()); + if (oldConfig != null) { + log.setOldMinValue(oldConfig.getMinValue()); + log.setOldMaxValue(oldConfig.getMaxValue()); + log.setOldLevel(oldConfig.getLevel()); + } + log.setNewMinValue(newConfig.getMinValue()); + log.setNewMaxValue(newConfig.getMaxValue()); + log.setNewLevel(newConfig.getLevel()); + log.setReason(reason); + log.setOperator("system"); + changeLogMapper.insert(log); + } +} diff --git a/wm-config/src/main/resources/application.yml b/wm-config/src/main/resources/application.yml new file mode 100644 index 00000000..f1cbf385 --- /dev/null +++ b/wm-config/src/main/resources/application.yml @@ -0,0 +1,30 @@ +server: + port: 8090 + +spring: + application: + name: wm-config + datasource: + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:water}?currentSchema=public + username: ${DB_USER:postgres} + password: ${DB_PASS:postgres} + cloud: + nacos: + discovery: + server-addr: ${NACOS_ADDR:localhost:8848} + +mybatis-plus: + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + global-config: + db-config: + logic-delete-field: deleted + logic-delete-value: 1 + logic-not-delete-value: 0 + +sa-token: + token-name: Authorization + timeout: 86400 + active-timeout: 1800 diff --git a/wm-config/src/main/resources/db/schema.sql b/wm-config/src/main/resources/db/schema.sql new file mode 100644 index 00000000..f58cf900 --- /dev/null +++ b/wm-config/src/main/resources/db/schema.sql @@ -0,0 +1,102 @@ +-- ============================================================ +-- wm-config DDL: 阈值管理 + 信息发布 + 设备管理 +-- ============================================================ + +-- 阈值配置表 +CREATE TABLE IF NOT EXISTS config_threshold ( + id BIGSERIAL PRIMARY KEY, + metric_code VARCHAR(64) NOT NULL, + metric_name VARCHAR(128) NOT NULL, + device_id BIGINT, + level SMALLINT NOT NULL DEFAULT 1, -- 1-预警 2-报警 3-紧急 + min_value NUMERIC(12,4), + max_value NUMERIC(12,4), + unit VARCHAR(32), + status SMALLINT NOT NULL DEFAULT 1, -- 0-禁用 1-启用 + remark VARCHAR(500), + deleted SMALLINT NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +COMMENT ON TABLE config_threshold IS '阈值配置表'; +CREATE INDEX idx_threshold_metric ON config_threshold(metric_code); +CREATE INDEX idx_threshold_device ON config_threshold(device_id); + +-- 阈值变更记录表 +CREATE TABLE IF NOT EXISTS config_threshold_change_log ( + id BIGSERIAL PRIMARY KEY, + threshold_id BIGINT NOT NULL, + old_min_value NUMERIC(12,4), + old_max_value NUMERIC(12,4), + new_min_value NUMERIC(12,4), + new_max_value NUMERIC(12,4), + old_level SMALLINT, + new_level SMALLINT, + operator VARCHAR(64), + reason VARCHAR(500), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +COMMENT ON TABLE config_threshold_change_log IS '阈值变更记录表'; +CREATE INDEX idx_change_log_threshold ON config_threshold_change_log(threshold_id); + +-- 公告通知表 +CREATE TABLE IF NOT EXISTS config_announcement ( + id BIGSERIAL PRIMARY KEY, + title VARCHAR(256) NOT NULL, + content TEXT, + type SMALLINT NOT NULL DEFAULT 1, -- 1-系统公告 2-维护通知 3-紧急通知 + publish_status SMALLINT NOT NULL DEFAULT 0, -- 0-草稿 1-已发布 2-已撤回 + channels VARCHAR(256), -- JSON: ["sms","push","site"] + publisher VARCHAR(64), + publish_time TIMESTAMP, + withdraw_time TIMESTAMP, + deleted SMALLINT NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +COMMENT ON TABLE config_announcement IS '公告通知表'; +CREATE INDEX idx_announcement_status ON config_announcement(publish_status); + +-- 设备台账表 +CREATE TABLE IF NOT EXISTS config_device_info ( + id BIGSERIAL PRIMARY KEY, + device_code VARCHAR(64) NOT NULL UNIQUE, + device_name VARCHAR(128) NOT NULL, + category SMALLINT NOT NULL DEFAULT 9, -- 1-水表 2-压力传感器 3-流量计 4-水质监测仪 5-阀门 9-其他 + brand VARCHAR(64), + model VARCHAR(64), + location VARCHAR(256), + longitude DOUBLE PRECISION, + latitude DOUBLE PRECISION, + device_status SMALLINT NOT NULL DEFAULT 0, -- 0-离线 1-在线 2-故障 3-维修中 + install_date TIMESTAMP, + last_maintenance_time TIMESTAMP, + responsible_person VARCHAR(64), + remark VARCHAR(500), + deleted SMALLINT NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +COMMENT ON TABLE config_device_info IS '设备台账表'; +CREATE INDEX idx_device_code ON config_device_info(device_code); +CREATE INDEX idx_device_category ON config_device_info(category); +CREATE INDEX idx_device_status ON config_device_info(device_status); + +-- 设备维保记录表 +CREATE TABLE IF NOT EXISTS config_device_maintenance ( + id BIGSERIAL PRIMARY KEY, + device_id BIGINT NOT NULL, + maintenance_type SMALLINT NOT NULL DEFAULT 1, -- 1-日常巡检 2-定期保养 3-故障维修 4-更换配件 + description TEXT, + operator VARCHAR(64), + start_time TIMESTAMP, + end_time TIMESTAMP, + result SMALLINT NOT NULL DEFAULT 0, -- 0-未完成 1-已完成 2-需要返修 + cost DOUBLE PRECISION, + attachments TEXT, -- JSON + deleted SMALLINT NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +COMMENT ON TABLE config_device_maintenance IS '设备维保记录表'; +CREATE INDEX idx_maintenance_device ON config_device_maintenance(device_id); diff --git a/wm-config/src/test/java/com/water/config/AnnouncementServiceTest.java b/wm-config/src/test/java/com/water/config/AnnouncementServiceTest.java new file mode 100644 index 00000000..85481eae --- /dev/null +++ b/wm-config/src/test/java/com/water/config/AnnouncementServiceTest.java @@ -0,0 +1,100 @@ +package com.water.config; + +import com.water.common.core.exception.BusinessException; +import com.water.config.entity.Announcement; +import com.water.config.mapper.AnnouncementMapper; +import com.water.config.service.AnnouncementService; +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 static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AnnouncementServiceTest { + + @Mock + private AnnouncementMapper announcementMapper; + @InjectMocks + private AnnouncementService announcementService; + + private Announcement draft; + + @BeforeEach + void setUp() { + draft = new Announcement(); + draft.setTitle("系统维护通知"); + draft.setContent("今晚22:00-次日06:00系统维护"); + draft.setType(2); + draft.setChannels("[\"site\",\"sms\"]"); + } + + @Test + void createAnnouncement_setsDraftStatus() { + when(announcementMapper.insert(any())).thenReturn(1); + + Announcement result = announcementService.createAnnouncement(draft); + + assertEquals(0, result.getPublishStatus()); + verify(announcementMapper).insert(any(Announcement.class)); + } + + @Test + void publish_draft_success() { + Announcement existing = new Announcement(); + existing.setId(1L); + existing.setPublishStatus(0); + existing.setChannels("[\"site\"]"); + + when(announcementMapper.selectById(1L)).thenReturn(existing); + when(announcementMapper.updateById(any())).thenReturn(1); + + assertDoesNotThrow(() -> announcementService.publish(1L)); + verify(announcementMapper).updateById(argThat(a -> + a.getPublishStatus() == 1 && a.getPublishTime() != null)); + } + + @Test + void publish_alreadyPublished_throws() { + Announcement existing = new Announcement(); + existing.setId(1L); + existing.setPublishStatus(1); + + when(announcementMapper.selectById(1L)).thenReturn(existing); + + BusinessException ex = assertThrows(BusinessException.class, + () -> announcementService.publish(1L)); + assertEquals("只有草稿状态的公告可以发布", ex.getMessage()); + } + + @Test + void withdraw_notPublished_throws() { + Announcement existing = new Announcement(); + existing.setId(1L); + existing.setPublishStatus(0); + + when(announcementMapper.selectById(1L)).thenReturn(existing); + + BusinessException ex = assertThrows(BusinessException.class, + () -> announcementService.withdraw(1L)); + assertEquals("只有已发布的公告可以撤回", ex.getMessage()); + } + + @Test + void delete_published_throws() { + Announcement existing = new Announcement(); + existing.setId(1L); + existing.setPublishStatus(1); + + when(announcementMapper.selectById(1L)).thenReturn(existing); + + BusinessException ex = assertThrows(BusinessException.class, + () -> announcementService.deleteAnnouncement(1L)); + assertEquals("已发布的公告不可删除,请先撤回", ex.getMessage()); + } +} diff --git a/wm-config/src/test/java/com/water/config/DeviceManageServiceTest.java b/wm-config/src/test/java/com/water/config/DeviceManageServiceTest.java new file mode 100644 index 00000000..d62c5340 --- /dev/null +++ b/wm-config/src/test/java/com/water/config/DeviceManageServiceTest.java @@ -0,0 +1,107 @@ +package com.water.config; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.water.common.core.exception.BusinessException; +import com.water.config.entity.DeviceInfo; +import com.water.config.entity.DeviceMaintenance; +import com.water.config.mapper.DeviceInfoMapper; +import com.water.config.mapper.DeviceMaintenanceMapper; +import com.water.config.service.DeviceManageService; +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 static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class DeviceManageServiceTest { + + @Mock + private DeviceInfoMapper deviceInfoMapper; + @Mock + private DeviceMaintenanceMapper maintenanceMapper; + @InjectMocks + private DeviceManageService deviceManageService; + + private DeviceInfo device; + + @BeforeEach + void setUp() { + device = new DeviceInfo(); + device.setDeviceCode("WM-001"); + device.setDeviceName("1号水表"); + device.setCategory(1); + device.setBrand("海天"); + device.setModel("HT-200"); + device.setDeviceStatus(0); + } + + @Test + void createDevice_success() { + when(deviceInfoMapper.selectCount(any())).thenReturn(0L); + when(deviceInfoMapper.insert(any())).thenReturn(1); + + DeviceInfo result = deviceManageService.createDevice(device); + + assertNotNull(result); + assertEquals("WM-001", result.getDeviceCode()); + verify(deviceInfoMapper).insert(any(DeviceInfo.class)); + } + + @Test + void createDevice_duplicateCode_throws() { + when(deviceInfoMapper.selectCount(any())).thenReturn(1L); + + BusinessException ex = assertThrows(BusinessException.class, + () -> deviceManageService.createDevice(device)); + assertEquals("设备编码已存在", ex.getMessage()); + } + + @Test + void updateDevice_notFound_throws() { + when(deviceInfoMapper.selectById(999L)).thenReturn(null); + + BusinessException ex = assertThrows(BusinessException.class, + () -> deviceManageService.updateDevice(999L, device)); + assertEquals("设备不存在", ex.getMessage()); + } + + @Test + void addMaintenance_deviceNotFound_throws() { + DeviceMaintenance maintenance = new DeviceMaintenance(); + maintenance.setDeviceId(999L); + + when(deviceInfoMapper.selectById(999L)).thenReturn(null); + + BusinessException ex = assertThrows(BusinessException.class, + () -> deviceManageService.addMaintenance(maintenance)); + assertEquals("设备不存在", ex.getMessage()); + } + + @Test + void addMaintenance_completed_updatesDeviceTime() { + DeviceInfo existingDevice = new DeviceInfo(); + existingDevice.setId(1L); + existingDevice.setDeviceStatus(3); // 维修中 + + DeviceMaintenance maintenance = new DeviceMaintenance(); + maintenance.setDeviceId(1L); + maintenance.setResult(1); // 已完成 + maintenance.setDescription("更换电池"); + + when(deviceInfoMapper.selectById(1L)).thenReturn(existingDevice); + when(maintenanceMapper.insert(any())).thenReturn(1); + when(deviceInfoMapper.updateById(any())).thenReturn(1); + + DeviceMaintenance result = deviceManageService.addMaintenance(maintenance); + + assertNotNull(result); + verify(deviceInfoMapper).updateById(argThat(d -> + d.getLastMaintenanceTime() != null && d.getDeviceStatus() == 1)); + } +} diff --git a/wm-config/src/test/java/com/water/config/ThresholdServiceTest.java b/wm-config/src/test/java/com/water/config/ThresholdServiceTest.java new file mode 100644 index 00000000..6042f259 --- /dev/null +++ b/wm-config/src/test/java/com/water/config/ThresholdServiceTest.java @@ -0,0 +1,96 @@ +package com.water.config; + +import com.water.common.core.exception.BusinessException; +import com.water.config.entity.ThresholdChangeLog; +import com.water.config.entity.ThresholdConfig; +import com.water.config.mapper.ThresholdChangeLogMapper; +import com.water.config.mapper.ThresholdConfigMapper; +import com.water.config.service.ThresholdService; +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.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ThresholdServiceTest { + + @Mock + private ThresholdConfigMapper thresholdConfigMapper; + @Mock + private ThresholdChangeLogMapper changeLogMapper; + @InjectMocks + private ThresholdService thresholdService; + + private ThresholdConfig validConfig; + + @BeforeEach + void setUp() { + validConfig = new ThresholdConfig(); + validConfig.setMetricCode("water_pressure"); + validConfig.setMetricName("水压"); + validConfig.setLevel(1); + validConfig.setMinValue(new BigDecimal("0.1")); + validConfig.setMaxValue(new BigDecimal("0.8")); + validConfig.setUnit("MPa"); + validConfig.setStatus(1); + } + + @Test + void createThreshold_success() { + when(thresholdConfigMapper.insert(any())).thenReturn(1); + when(changeLogMapper.insert(any())).thenReturn(1); + + ThresholdConfig result = thresholdService.createThreshold(validConfig); + + assertNotNull(result); + assertEquals("water_pressure", result.getMetricCode()); + assertEquals(1, result.getLevel()); + verify(thresholdConfigMapper).insert(any(ThresholdConfig.class)); + verify(changeLogMapper).insert(any(ThresholdChangeLog.class)); + } + + @Test + void createThreshold_minGreaterThanMax_throws() { + validConfig.setMinValue(new BigDecimal("1.0")); + validConfig.setMaxValue(new BigDecimal("0.5")); + + BusinessException ex = assertThrows(BusinessException.class, + () -> thresholdService.createThreshold(validConfig)); + assertEquals("最小值不能大于最大值", ex.getMessage()); + } + + @Test + void createThreshold_invalidLevel_throws() { + validConfig.setLevel(5); + + BusinessException ex = assertThrows(BusinessException.class, + () -> thresholdService.createThreshold(validConfig)); + assertEquals("阈值级别必须在1-3之间", ex.getMessage()); + } + + @Test + void updateThreshold_notFound_throws() { + when(thresholdConfigMapper.selectById(999L)).thenReturn(null); + + BusinessException ex = assertThrows(BusinessException.class, + () -> thresholdService.updateThreshold(999L, validConfig)); + assertEquals("阈值配置不存在", ex.getMessage()); + } + + @Test + void deleteThreshold_notFound_throws() { + when(thresholdConfigMapper.selectById(999L)).thenReturn(null); + + BusinessException ex = assertThrows(BusinessException.class, + () -> thresholdService.deleteThreshold(999L)); + assertEquals("阈值配置不存在", ex.getMessage()); + } +} diff --git a/wm-data-engine/pom.xml b/wm-data-engine/pom.xml index d56746cb..a722533e 100644 --- a/wm-data-engine/pom.xml +++ b/wm-data-engine/pom.xml @@ -3,15 +3,120 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - com.waterwm-parent1.0.0-SNAPSHOT + + com.water + wm-parent + 1.0.0-SNAPSHOT + wm-data-engine + wm-data-engine + 数据汇聚引擎模块 + - com.waterwm-common - org.springframework.bootspring-boot-starter-web - com.alibaba.cloudspring-cloud-starter-alibaba-nacos-discovery - org.springframework.kafkaspring-kafka - org.springframework.bootspring-boot-starter-data-redis - org.postgresqlpostgresql - net.postgispostgis-jdbc + + + com.water + wm-common + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-websocket + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.springframework.kafka + spring-kafka + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.postgresql + postgresql + + + + + net.postgis + postgis-jdbc + + + + + com.baomidou + mybatis-plus-spring-boot3-starter + + + + + io.minio + minio + + + + + cn.hutool + hutool-all + + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + + + + + com.alibaba + easyexcel + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.kafka + spring-kafka-test + test + + + + com.h2database + h2 + test + - \ No newline at end of file + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/wm-data-engine/src/main/java/com/water/data_engine/config/KafkaConfig.java b/wm-data-engine/src/main/java/com/water/data_engine/config/KafkaConfig.java new file mode 100644 index 00000000..c56e78bd --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/config/KafkaConfig.java @@ -0,0 +1,62 @@ +package com.water.data_engine.config; + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.core.*; + +import java.util.HashMap; +import java.util.Map; + +/** + * Kafka 配置 + * 用于实时数据流采集和传输 + */ +@Configuration +public class KafkaConfig { + + @Value("${spring.kafka.bootstrap-servers:${KAFKA_SERVERS:127.0.0.1}:9092}") + private String bootstrapServers; + + @Bean + public ProducerFactory producerFactory() { + Map props = new HashMap<>(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.ACKS_CONFIG, "1"); + props.put(ProducerConfig.RETRIES_CONFIG, 3); + return new DefaultKafkaProducerFactory<>(props); + } + + @Bean + public KafkaTemplate kafkaTemplate(ProducerFactory producerFactory) { + return new KafkaTemplate<>(producerFactory); + } + + @Bean + public ConsumerFactory consumerFactory() { + Map props = new HashMap<>(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(ConsumerConfig.GROUP_ID_CONFIG, "wm-data-engine"); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest"); + return new DefaultKafkaConsumerFactory<>(props); + } + + @Bean + public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory( + ConsumerFactory consumerFactory) { + ConcurrentKafkaListenerContainerFactory factory = + new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory); + factory.setConcurrency(3); + return factory; + } +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/config/MyBatisPlusConfig.java b/wm-data-engine/src/main/java/com/water/data_engine/config/MyBatisPlusConfig.java new file mode 100644 index 00000000..18cb8dcc --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/config/MyBatisPlusConfig.java @@ -0,0 +1,49 @@ +package com.water.data_engine.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.apache.ibatis.reflection.MetaObject; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.LocalDateTime; + +/** + * MyBatis-Plus 配置 + */ +@Configuration +@MapperScan("com.water.data_engine.mapper") +public class MyBatisPlusConfig { + + /** + * 分页插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL)); + return interceptor; + } + + /** + * 自动填充处理器 + */ + @Bean + public MetaObjectHandler metaObjectHandler() { + return new MetaObjectHandler() { + @Override + public void insertFill(MetaObject metaObject) { + this.strictInsertFill(metaObject, "createdAt", LocalDateTime.class, LocalDateTime.now()); + this.strictInsertFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now()); + } + + @Override + public void updateFill(MetaObject metaObject) { + this.strictUpdateFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now()); + } + }; + } +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/config/WebSocketConfig.java b/wm-data-engine/src/main/java/com/water/data_engine/config/WebSocketConfig.java new file mode 100644 index 00000000..e81045a0 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/config/WebSocketConfig.java @@ -0,0 +1,32 @@ +package com.water.data_engine.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +/** + * WebSocket 配置 + * 支持 STOMP 协议,用于实时数据推送 + */ +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端订阅前缀: /topic (广播), /queue (点对点) + registry.enableSimpleBroker("/topic", "/queue"); + // 客户端发送消息前缀 + registry.setApplicationDestinationPrefixes("/app"); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + // WebSocket 连接端点 + registry.addEndpoint("/ws/data-engine") + .setAllowedOriginPatterns("*") + .withSockJS(); + } +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/controller/DataCollectController.java b/wm-data-engine/src/main/java/com/water/data_engine/controller/DataCollectController.java new file mode 100644 index 00000000..83563c3f --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/controller/DataCollectController.java @@ -0,0 +1,85 @@ +package com.water.data_engine.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.water.common.core.result.R; +import com.water.data_engine.entity.CollectRecord; +import com.water.data_engine.entity.CollectTask; +import com.water.data_engine.service.DataCollectService; +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; + +/** + * 数据采集控制器 + * DE-01: 实时流(MQTT/Kafka) + 批量采集 + */ +@Tag(name = "数据采集") +@RestController +@RequestMapping("/api/data-engine/collect") +@RequiredArgsConstructor +public class DataCollectController { + + private final DataCollectService collectService; + + // ==================== 实时数据采集 ==================== + + @Operation(summary = "实时数据接入") + @PostMapping("/realtime") + public R ingestRealtime(@RequestBody Map request) { + String sourceType = (String) request.get("sourceType"); + String sourceId = (String) request.get("sourceId"); + @SuppressWarnings("unchecked") + Map data = (Map) request.get("data"); + + String topic = collectService.ingestRealtime(sourceType, sourceId, data); + return R.ok("数据已接入,topic: " + topic); + } + + @Operation(summary = "批量数据接入") + @PostMapping("/batch") + public R batchIngest(@RequestBody List> batchData) { + int count = collectService.batchIngest(batchData); + return R.ok("批量接入完成,成功: " + count + " 条"); + } + + // ==================== 采集任务管理 ==================== + + @Operation(summary = "创建批量采集任务") + @PostMapping("/task") + public R createTask(@RequestBody Map request) { + String taskName = (String) request.get("taskName"); + Long sourceId = Long.valueOf(request.get("sourceId").toString()); + String targetTable = (String) request.get("targetTable"); + + CollectTask task = collectService.createBatchTask(taskName, sourceId, targetTable); + return R.ok(task); + } + + @Operation(summary = "执行采集任务") + @PostMapping("/task/{taskId}/execute") + public R executeTask(@PathVariable Long taskId, + @RequestBody List> dataList) { + collectService.executeTask(taskId, dataList); + return R.ok("任务执行完成"); + } + + @Operation(summary = "查询采集任务列表") + @GetMapping("/task/list") + public R> listTasks(@RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false) String status) { + return R.ok(collectService.listTasks(page, size, status)); + } + + @Operation(summary = "查询采集记录") + @GetMapping("/record/list") + public R> listRecords(@RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false) Long taskId) { + return R.ok(collectService.listRecords(page, size, taskId)); + } +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/controller/DataController.java b/wm-data-engine/src/main/java/com/water/data_engine/controller/DataController.java new file mode 100644 index 00000000..c5d9ff32 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/controller/DataController.java @@ -0,0 +1,65 @@ +package com.water.data_engine.controller; + +import com.water.common.core.result.R; +import com.water.data_engine.service.DataCollectService; +import com.water.data_engine.service.DataGovernanceService; +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("/api/data-engine") +@RequiredArgsConstructor +public class DataController { + + private final DataCollectService collectService; + private final DataGovernanceService governanceService; + + @Operation(summary = "数据接入(兼容旧接口)") + @PostMapping("/ingest") + public R ingest(@RequestBody Map req) { + String sourceType = (String) req.get("sourceType"); + String sourceId = (String) req.get("sourceId"); + @SuppressWarnings("unchecked") + Map data = (Map) req.get("data"); + collectService.ingestRealtime(sourceType, sourceId, data); + return R.ok("数据已接入"); + } + + @Operation(summary = "批量接入(兼容旧接口)") + @PostMapping("/ingest/batch") + public R batchIngest(@RequestBody List> batch) { + collectService.batchIngest(batch); + return R.ok("批量接入完成"); + } + + @Operation(summary = "数据标准化+清洗+质控(管道演示)") + @PostMapping("/pipeline") + public R> pipeline(@RequestBody Map raw) { + Map result = governanceService.pipeline(raw); + return R.ok(result); + } + + @Operation(summary = "引擎状态") + @GetMapping("/status") + public R> status() { + Map status = new LinkedHashMap<>(); + status.put("module", "wm-data-engine"); + status.put("version", "1.0.0"); + status.put("status", "running"); + status.put("features", List.of( + "DE-01 数据采集(实时流/批量)", + "DE-02 数据接入(REST/WebSocket/数据库)", + "DE-03 数据存储(TDengine/PostgreSQL/MinIO)", + "DE-04 数据集成(多源异构整合)" + )); + return R.ok(status); + } +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/controller/DataGovernanceController.java b/wm-data-engine/src/main/java/com/water/data_engine/controller/DataGovernanceController.java new file mode 100644 index 00000000..5e3f1435 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/controller/DataGovernanceController.java @@ -0,0 +1,117 @@ +package com.water.data_engine.controller; + +import com.water.common.core.result.R; +import com.water.data_engine.entity.QualityRule; +import com.water.data_engine.service.DataGovernanceService; +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/data-engine/governance") +@RequiredArgsConstructor +public class DataGovernanceController { + + private final DataGovernanceService governanceService; + + // ==================== 数据标准化 ==================== + + @Operation(summary = "数据标准化") + @PostMapping("/standardize") + public R> standardize(@RequestBody Map raw) { + return R.ok(governanceService.standardize(raw)); + } + + @Operation(summary = "批量数据标准化") + @PostMapping("/standardize/batch") + public R>> batchStandardize(@RequestBody List> rawDataList) { + return R.ok(governanceService.batchStandardize(rawDataList)); + } + + // ==================== 数据清洗 ==================== + + @Operation(summary = "数据清洗") + @PostMapping("/clean") + public R> clean(@RequestBody Map data) { + return R.ok(governanceService.clean(data)); + } + + @Operation(summary = "批量数据清洗") + @PostMapping("/clean/batch") + public R>> batchClean(@RequestBody List> dataList) { + return R.ok(governanceService.batchClean(dataList)); + } + + // ==================== 数据质量 ==================== + + @Operation(summary = "数据质量检查") + @PostMapping("/quality/check") + public R> qualityCheck(@RequestBody Map data) { + return R.ok(governanceService.qualityCheck(data)); + } + + @Operation(summary = "批量数据质量检查") + @PostMapping("/quality/check/batch") + public R>> batchQualityCheck(@RequestBody List> dataList) { + return R.ok(governanceService.batchQualityCheck(dataList)); + } + + @Operation(summary = "执行质量规则检查") + @PostMapping("/quality/rules/execute") + public R> executeQualityRules(@RequestBody Map request) { + String tableName = (String) request.get("tableName"); + return R.ok(governanceService.executeQualityRules(tableName)); + } + + // ==================== 数据管道 ==================== + + @Operation(summary = "完整数据管道(标准化->清洗->质控)") + @PostMapping("/pipeline") + public R> pipeline(@RequestBody Map raw) { + return R.ok(governanceService.pipeline(raw)); + } + + @Operation(summary = "批量数据管道") + @PostMapping("/pipeline/batch") + public R>> batchPipeline(@RequestBody List> rawDataList) { + return R.ok(governanceService.batchPipeline(rawDataList)); + } + + // ==================== 质量规则管理 ==================== + + @Operation(summary = "创建质量规则") + @PostMapping("/quality/rule") + public R createQualityRule(@RequestBody QualityRule rule) { + return R.ok(governanceService.createQualityRule(rule)); + } + + @Operation(summary = "更新质量规则") + @PutMapping("/quality/rule/{id}") + public R updateQualityRule(@PathVariable Long id, @RequestBody QualityRule rule) { + return R.ok(governanceService.updateQualityRule(id, rule)); + } + + @Operation(summary = "删除质量规则") + @DeleteMapping("/quality/rule/{id}") + public R deleteQualityRule(@PathVariable Long id) { + governanceService.deleteQualityRule(id); + return R.ok("删除成功"); + } + + @Operation(summary = "查询质量规则列表") + @GetMapping("/quality/rule/list") + public R> listQualityRules( + @RequestParam(required = false) String tableName, + @RequestParam(required = false) String ruleType) { + return R.ok(governanceService.listQualityRules(tableName, ruleType)); + } +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/controller/DataIngestController.java b/wm-data-engine/src/main/java/com/water/data_engine/controller/DataIngestController.java new file mode 100644 index 00000000..f79875cd --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/controller/DataIngestController.java @@ -0,0 +1,118 @@ +package com.water.data_engine.controller; + +import com.water.common.core.result.R; +import com.water.data_engine.entity.DataSource; +import com.water.data_engine.service.DataIngestService; +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 org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.Map; + +/** + * 数据接入控制器 + * DE-02: RESTful API / WebSocket / 数据库直连 + */ +@Tag(name = "数据接入") +@RestController +@RequestMapping("/api/data-engine/ingest") +@RequiredArgsConstructor +public class DataIngestController { + + private final DataIngestService ingestService; + + // ==================== API 接入 ==================== + + @Operation(summary = "通过 API 接入单条数据") + @PostMapping("/api/{sourceCode}") + public R ingestViaApi(@PathVariable String sourceCode, + @RequestBody Map data) { + String topic = ingestService.ingestViaApi(sourceCode, data); + return R.ok("数据已接入,topic: " + topic); + } + + @Operation(summary = "通过 API 批量接入数据") + @PostMapping("/api/{sourceCode}/batch") + public R batchIngestViaApi(@PathVariable String sourceCode, + @RequestBody List> dataList) { + int count = ingestService.batchIngestViaApi(sourceCode, dataList); + return R.ok("批量接入完成,成功: " + count + " 条"); + } + + // ==================== 数据库接入 ==================== + + @Operation(summary = "从外部数据库拉取数据") + @PostMapping("/database/{sourceId}/pull") + public R pullFromDatabase(@PathVariable Long sourceId, + @RequestBody Map request) { + String sql = (String) request.get("sql"); + String targetTable = (String) request.get("targetTable"); + int count = ingestService.pullFromDatabase(sourceId, sql, targetTable); + return R.ok("数据拉取完成,成功: " + count + " 条"); + } + + @Operation(summary = "同步数据到本地表") + @PostMapping("/database/{sourceId}/sync") + public R syncToTable(@PathVariable Long sourceId, + @RequestBody Map request) { + String querySql = (String) request.get("querySql"); + String targetTable = (String) request.get("targetTable"); + @SuppressWarnings("unchecked") + List columns = (List) request.get("columns"); + int count = ingestService.syncToTable(sourceId, querySql, targetTable, columns); + return R.ok("同步完成,成功: " + count + " 条"); + } + + // ==================== 文件接入 ==================== + + @Operation(summary = "通过文件(CSV)接入数据") + @PostMapping("/file/{sourceCode}") + public R ingestFromFile(@PathVariable String sourceCode, + @RequestParam("file") MultipartFile file) throws Exception { + int count = ingestService.ingestFromFile(file, sourceCode); + return R.ok("文件数据接入完成,成功: " + count + " 条"); + } + + // ==================== 数据源管理 ==================== + + @Operation(summary = "创建数据源") + @PostMapping("/source") + public R createDataSource(@RequestBody DataSource dataSource) { + return R.ok(ingestService.createDataSource(dataSource)); + } + + @Operation(summary = "更新数据源") + @PutMapping("/source/{id}") + public R updateDataSource(@PathVariable Long id, + @RequestBody DataSource dataSource) { + return R.ok(ingestService.updateDataSource(id, dataSource)); + } + + @Operation(summary = "删除数据源") + @DeleteMapping("/source/{id}") + public R deleteDataSource(@PathVariable Long id) { + ingestService.deleteDataSource(id); + return R.ok("删除成功"); + } + + @Operation(summary = "查询数据源列表") + @GetMapping("/source/list") + public R> listDataSources(@RequestParam(required = false) String sourceType) { + return R.ok(ingestService.listDataSources(sourceType)); + } + + @Operation(summary = "获取数据源详情") + @GetMapping("/source/{id}") + public R getDataSource(@PathVariable Long id) { + return R.ok(ingestService.getDataSource(id)); + } + + @Operation(summary = "测试数据源连接") + @PostMapping("/source/{id}/test") + public R testConnection(@PathVariable Long id) { + return R.ok(ingestService.testConnection(id)); + } +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/controller/DataIntegrationController.java b/wm-data-engine/src/main/java/com/water/data_engine/controller/DataIntegrationController.java new file mode 100644 index 00000000..707ccac6 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/controller/DataIntegrationController.java @@ -0,0 +1,134 @@ +package com.water.data_engine.controller; + +import com.water.common.core.result.R; +import com.water.data_engine.entity.DataLineage; +import com.water.data_engine.entity.SyncTask; +import com.water.data_engine.service.DataIntegrationService; +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.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 数据集成控制器 + * DE-04: 多源异构数据整合 + */ +@Tag(name = "数据集成") +@RestController +@RequestMapping("/api/data-engine/integration") +@RequiredArgsConstructor +public class DataIntegrationController { + + private final DataIntegrationService integrationService; + + // ==================== 数据同步 ==================== + + @Operation(summary = "创建同步任务") + @PostMapping("/sync/task") + public R createSyncTask(@RequestBody SyncTask syncTask) { + return R.ok(integrationService.createSyncTask(syncTask)); + } + + @Operation(summary = "执行同步任务") + @PostMapping("/sync/task/{taskId}/execute") + public R executeSyncTask(@PathVariable Long taskId) { + int count = integrationService.executeSyncTask(taskId); + return R.ok("同步完成,处理: " + count + " 条"); + } + + @Operation(summary = "执行全量同步") + @PostMapping("/sync/full") + public R fullSync(@RequestBody Map request) { + Long sourceId = Long.valueOf(request.get("sourceId").toString()); + String sourceTable = (String) request.get("sourceTable"); + String targetTable = (String) request.get("targetTable"); + int count = integrationService.fullSync(sourceId, sourceTable, targetTable); + return R.ok("全量同步完成: " + count + " 条"); + } + + @Operation(summary = "执行增量同步") + @PostMapping("/sync/incremental") + public R incrementalSync(@RequestBody Map request) { + Long sourceId = Long.valueOf(request.get("sourceId").toString()); + String sourceTable = (String) request.get("sourceTable"); + String targetTable = (String) request.get("targetTable"); + String timestampColumn = (String) request.get("timestampColumn"); + LocalDateTime lastSyncTime = LocalDateTime.parse((String) request.get("lastSyncTime")); + int count = integrationService.incrementalSync(sourceId, sourceTable, targetTable, + timestampColumn, lastSyncTime); + return R.ok("增量同步完成: " + count + " 条"); + } + + @Operation(summary = "查询同步任务列表") + @GetMapping("/sync/task/list") + public R> listSyncTasks(@RequestParam(required = false) String status) { + return R.ok(integrationService.listSyncTasks(status)); + } + + @Operation(summary = "获取同步任务详情") + @GetMapping("/sync/task/{id}") + public R getSyncTask(@PathVariable Long id) { + return R.ok(integrationService.getSyncTask(id)); + } + + @Operation(summary = "删除同步任务") + @DeleteMapping("/sync/task/{id}") + public R deleteSyncTask(@PathVariable Long id) { + integrationService.deleteSyncTask(id); + return R.ok("删除成功"); + } + + // ==================== 数据合并与聚合 ==================== + + @Operation(summary = "数据合并(多源整合)") + @PostMapping("/merge") + public R>> mergeData(@RequestBody Map request) { + @SuppressWarnings("unchecked") + List sourceTables = (List) request.get("sourceTables"); + String joinColumn = (String) request.get("joinColumn"); + @SuppressWarnings("unchecked") + List selectColumns = (List) request.get("selectColumns"); + return R.ok(integrationService.mergeData(sourceTables, joinColumn, selectColumns)); + } + + @Operation(summary = "数据聚合(按维度汇总)") + @PostMapping("/aggregate") + public R>> aggregateData(@RequestBody Map request) { + String sourceTable = (String) request.get("sourceTable"); + @SuppressWarnings("unchecked") + List groupByColumns = (List) request.get("groupByColumns"); + @SuppressWarnings("unchecked") + Map aggregations = (Map) request.get("aggregations"); + return R.ok(integrationService.aggregateData(sourceTable, groupByColumns, aggregations)); + } + + // ==================== 数据血缘 ==================== + + @Operation(summary = "创建数据血缘关系") + @PostMapping("/lineage") + public R createLineage(@RequestBody DataLineage lineage) { + return R.ok(integrationService.createLineage(lineage)); + } + + @Operation(summary = "查询血缘关系(上游)") + @GetMapping("/lineage/upstream/{tableName}") + public R> getUpstreamLineage(@PathVariable String tableName) { + return R.ok(integrationService.getUpstreamLineage(tableName)); + } + + @Operation(summary = "查询血缘关系(下游)") + @GetMapping("/lineage/downstream/{tableName}") + public R> getDownstreamLineage(@PathVariable String tableName) { + return R.ok(integrationService.getDownstreamLineage(tableName)); + } + + @Operation(summary = "查询完整血缘链路") + @GetMapping("/lineage/full/{tableName}") + public R> getFullLineage(@PathVariable String tableName) { + return R.ok(integrationService.getFullLineage(tableName)); + } +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/controller/DataStorageController.java b/wm-data-engine/src/main/java/com/water/data_engine/controller/DataStorageController.java new file mode 100644 index 00000000..58cee624 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/controller/DataStorageController.java @@ -0,0 +1,154 @@ +package com.water.data_engine.controller; + +import com.water.common.core.result.R; +import com.water.data_engine.entity.StorageConfig; +import com.water.data_engine.service.DataStorageService; +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 org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 数据存储控制器 + * DE-03: TDengine + PostgreSQL + MinIO + */ +@Tag(name = "数据存储") +@RestController +@RequestMapping("/api/data-engine/storage") +@RequiredArgsConstructor +public class DataStorageController { + + private final DataStorageService storageService; + + // ==================== TDengine 时序存储 ==================== + + @Operation(summary = "写入遥测数据到 TDengine") + @PostMapping("/tdengine") + public R writeToTDengine(@RequestBody Map data) { + storageService.writeToTDengine( + (String) data.get("deviceSn"), + (String) data.get("deviceType"), + (String) data.get("area"), + (String) data.get("metricKey"), + ((Number) data.get("value")).doubleValue() + ); + return R.ok("写入成功"); + } + + @Operation(summary = "批量写入遥测数据") + @PostMapping("/tdengine/batch") + public R batchWriteToTDengine(@RequestBody List> dataList) { + int count = storageService.batchWriteToTDengine(dataList); + return R.ok("批量写入成功: " + count + " 条"); + } + + @Operation(summary = "查询遥测数据") + @GetMapping("/tdengine/query") + public R>> queryFromTDengine( + @RequestParam String deviceSn, + @RequestParam String metricKey, + @RequestParam String startTime, + @RequestParam String endTime) { + return R.ok(storageService.queryFromTDengine( + deviceSn, metricKey, + LocalDateTime.parse(startTime), + LocalDateTime.parse(endTime))); + } + + @Operation(summary = "查询聚合数据(小时级)") + @GetMapping("/tdengine/hourly") + public R>> queryHourlyAgg( + @RequestParam String deviceSn, + @RequestParam String metricKey, + @RequestParam String startTime, + @RequestParam String endTime) { + return R.ok(storageService.queryHourlyAgg( + deviceSn, metricKey, + LocalDateTime.parse(startTime), + LocalDateTime.parse(endTime))); + } + + // ==================== PostgreSQL 关系存储 ==================== + + @Operation(summary = "插入数据到 PostgreSQL") + @PostMapping("/postgres/{table}") + public R insertToPostgres(@PathVariable String table, + @RequestBody Map data) { + return R.ok(storageService.insertToPostgres(table, data)); + } + + @Operation(summary = "批量插入数据") + @PostMapping("/postgres/{table}/batch") + public R batchInsertToPostgres(@PathVariable String table, + @RequestBody List> dataList) { + int count = storageService.batchInsertToPostgres(table, dataList); + return R.ok("批量插入成功: " + count + " 条"); + } + + @Operation(summary = "更新数据") + @PutMapping("/postgres/{table}/{id}") + public R updateInPostgres(@PathVariable String table, + @PathVariable Long id, + @RequestBody Map data) { + int count = storageService.updateInPostgres(table, id, data); + return R.ok("更新成功: " + count + " 条"); + } + + @Operation(summary = "查询数据") + @GetMapping("/postgres/{table}") + public R>> queryFromPostgres( + @PathVariable String table, + @RequestParam Map conditions, + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size) { + return R.ok(storageService.queryFromPostgres(table, conditions, page, size)); + } + + // ==================== MinIO 对象存储 ==================== + + @Operation(summary = "上传文件到 MinIO") + @PostMapping("/minio/upload") + public R uploadToMinio(@RequestParam("file") MultipartFile file, + @RequestParam(defaultValue = "default") String module) throws Exception { + String objectName = storageService.uploadToMinio(file, module); + return R.ok(objectName); + } + + @Operation(summary = "列出 MinIO 文件") + @GetMapping("/minio/list") + public R> listMinioObjects(@RequestParam(defaultValue = "") String prefix) throws Exception { + return R.ok(storageService.listMinioObjects(prefix)); + } + + // ==================== 存储配置管理 ==================== + + @Operation(summary = "创建存储配置") + @PostMapping("/config") + public R createStorageConfig(@RequestBody StorageConfig config) { + return R.ok(storageService.createStorageConfig(config)); + } + + @Operation(summary = "更新存储配置") + @PutMapping("/config/{id}") + public R updateStorageConfig(@PathVariable Long id, + @RequestBody StorageConfig config) { + return R.ok(storageService.updateStorageConfig(id, config)); + } + + @Operation(summary = "查询存储配置列表") + @GetMapping("/config/list") + public R> listStorageConfigs(@RequestParam(required = false) String storageType) { + return R.ok(storageService.listStorageConfigs(storageType)); + } + + @Operation(summary = "测试存储连接") + @PostMapping("/config/{id}/test") + public R testStorageConnection(@PathVariable Long id) { + return R.ok(storageService.testStorageConnection(id)); + } +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/entity/CollectRecord.java b/wm-data-engine/src/main/java/com/water/data_engine/entity/CollectRecord.java new file mode 100644 index 00000000..c6e619d1 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/entity/CollectRecord.java @@ -0,0 +1,46 @@ +package com.water.data_engine.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 数据采集记录实体 + */ +@Data +@TableName("de_collect_record") +public class CollectRecord { + + @TableId(type = IdType.AUTO) + private Long id; + + /** 任务ID */ + private Long taskId; + + /** 数据源ID */ + private Long sourceId; + + /** 数据源类型 */ + private String sourceType; + + /** 数据源Key */ + private String sourceKey; + + /** 原始数据(JSON) */ + @TableField(typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class) + private Object rawData; + + /** 处理后数据(JSON) */ + @TableField(typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class) + private Object processedData; + + /** 状态: success/failed/skipped */ + private String status; + + /** 错误信息 */ + private String errorMsg; + + /** 采集时间 */ + private LocalDateTime collectTime; +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/entity/CollectTask.java b/wm-data-engine/src/main/java/com/water/data_engine/entity/CollectTask.java new file mode 100644 index 00000000..8ae6da78 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/entity/CollectTask.java @@ -0,0 +1,82 @@ +package com.water.data_engine.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 数据采集任务实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("de_collect_task") +public class CollectTask extends com.water.common.core.entity.BaseEntity { + + /** + * 任务名称 + */ + private String taskName; + + /** + * 数据源ID + */ + private Long sourceId; + + /** + * 采集类型: realtime/batch/manual + */ + private String collectType; + + /** + * Kafka/MQTT topic + */ + private String topic; + + /** + * 目标表名 + */ + private String targetTable; + + /** + * 转换规则(JSON) + */ + @TableField(typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class) + private Object transformRule; + + /** + * 状态: pending/running/paused/completed/failed + */ + private String status; + + /** + * 总记录数 + */ + private Long totalCount; + + /** + * 成功数 + */ + private Long successCount; + + /** + * 失败数 + */ + private Long failCount; + + /** + * 开始时间 + */ + private LocalDateTime startTime; + + /** + * 结束时间 + */ + private LocalDateTime endTime; + + /** + * 错误信息 + */ + private String errorMsg; +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/entity/DataLineage.java b/wm-data-engine/src/main/java/com/water/data_engine/entity/DataLineage.java new file mode 100644 index 00000000..91794677 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/entity/DataLineage.java @@ -0,0 +1,41 @@ +package com.water.data_engine.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 数据血缘关系实体 + */ +@Data +@TableName("de_data_lineage") +public class DataLineage { + + @TableId(type = IdType.AUTO) + private Long id; + + /** 源表 */ + private String sourceTable; + + /** 源列 */ + private String sourceColumn; + + /** 目标表 */ + private String targetTable; + + /** 目标列 */ + private String targetColumn; + + /** 转换类型: direct/mapping/aggregation/calculation */ + private String transformType; + + /** 转换规则 */ + private String transformRule; + + /** 描述 */ + private String description; + + /** 创建时间 */ + private LocalDateTime createdAt; +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/entity/DataSource.java b/wm-data-engine/src/main/java/com/water/data_engine/entity/DataSource.java new file mode 100644 index 00000000..96d250d2 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/entity/DataSource.java @@ -0,0 +1,67 @@ +package com.water.data_engine.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 数据源配置实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("de_data_source") +public class DataSource extends com.water.common.core.entity.BaseEntity { + + /** + * 数据源名称 + */ + private String sourceName; + + /** + * 数据源编码(唯一) + */ + private String sourceCode; + + /** + * 数据源类型: mqtt/kafka/rest/websocket/database/file + */ + private String sourceType; + + /** + * 数据分类: iot/manual/api/database + */ + private String category; + + /** + * 连接配置(JSON) + */ + @TableField(typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class) + private Object connectionConfig; + + /** + * 同步模式: realtime/batch/scheduled + */ + private String syncMode; + + /** + * 定时同步Cron表达式 + */ + private String syncCron; + + /** + * 状态: 0-禁用 1-启用 + */ + private Integer status; + + /** + * 描述 + */ + private String description; + + /** + * 最后同步时间 + */ + private LocalDateTime lastSyncAt; +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/entity/QualityRule.java b/wm-data-engine/src/main/java/com/water/data_engine/entity/QualityRule.java new file mode 100644 index 00000000..724f2084 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/entity/QualityRule.java @@ -0,0 +1,38 @@ +package com.water.data_engine.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 数据质量规则实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("de_quality_rule") +public class QualityRule extends com.water.common.core.entity.BaseEntity { + + /** 规则名称 */ + private String ruleName; + + /** 规则类型: completeness/validity/timeliness/consistency */ + private String ruleType; + + /** 表名 */ + private String tableName; + + /** 列名 */ + private String columnName; + + /** 规则表达式 */ + private String ruleExpr; + + /** 阈值 */ + private java.math.BigDecimal threshold; + + /** 严重级别: info/warning/error */ + private String severity; + + /** 是否启用 */ + private Integer enabled; +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/entity/StorageConfig.java b/wm-data-engine/src/main/java/com/water/data_engine/entity/StorageConfig.java new file mode 100644 index 00000000..870b6de6 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/entity/StorageConfig.java @@ -0,0 +1,42 @@ +package com.water.data_engine.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 存储配置实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("de_storage_config") +public class StorageConfig extends com.water.common.core.entity.BaseEntity { + + /** 存储名称 */ + private String storageName; + + /** 存储类型: tdengine/postgresql/minio */ + private String storageType; + + /** 连接URL */ + private String connectionUrl; + + /** 用户名 */ + private String username; + + /** 密码 */ + private String password; + + /** 数据库名 */ + private String databaseName; + + /** 桶名(MinIO) */ + private String bucketName; + + /** 扩展配置 */ + @TableField(typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class) + private Object extraConfig; + + /** 状态 */ + private Integer status; +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/entity/SyncTask.java b/wm-data-engine/src/main/java/com/water/data_engine/entity/SyncTask.java new file mode 100644 index 00000000..dcff7858 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/entity/SyncTask.java @@ -0,0 +1,43 @@ +package com.water.data_engine.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 数据同步任务实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("de_sync_task") +public class SyncTask extends com.water.common.core.entity.BaseEntity { + + /** 任务名称 */ + private String taskName; + + /** 数据源ID */ + private Long sourceId; + + /** 目标存储ID */ + private Long targetStorageId; + + /** 同步类型: full/incremental/cdc */ + private String syncType; + + /** 同步Cron表达式 */ + private String syncCron; + + /** 最后同步时间 */ + private LocalDateTime lastSyncAt; + + /** 最后同步记录数 */ + private Long lastSyncCount; + + /** 状态: pending/running/paused/completed/failed */ + private String status; + + /** 错误信息 */ + private String errorMsg; +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/mapper/CollectRecordMapper.java b/wm-data-engine/src/main/java/com/water/data_engine/mapper/CollectRecordMapper.java new file mode 100644 index 00000000..a1c39c1d --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/mapper/CollectRecordMapper.java @@ -0,0 +1,12 @@ +package com.water.data_engine.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.data_engine.entity.CollectRecord; +import org.apache.ibatis.annotations.Mapper; + +/** + * 采集记录Mapper + */ +@Mapper +public interface CollectRecordMapper extends BaseMapper { +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/mapper/CollectTaskMapper.java b/wm-data-engine/src/main/java/com/water/data_engine/mapper/CollectTaskMapper.java new file mode 100644 index 00000000..e95e4d37 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/mapper/CollectTaskMapper.java @@ -0,0 +1,12 @@ +package com.water.data_engine.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.data_engine.entity.CollectTask; +import org.apache.ibatis.annotations.Mapper; + +/** + * 采集任务Mapper + */ +@Mapper +public interface CollectTaskMapper extends BaseMapper { +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/mapper/DataLineageMapper.java b/wm-data-engine/src/main/java/com/water/data_engine/mapper/DataLineageMapper.java new file mode 100644 index 00000000..e2850f5e --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/mapper/DataLineageMapper.java @@ -0,0 +1,12 @@ +package com.water.data_engine.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.data_engine.entity.DataLineage; +import org.apache.ibatis.annotations.Mapper; + +/** + * 数据血缘Mapper + */ +@Mapper +public interface DataLineageMapper extends BaseMapper { +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/mapper/DataSourceMapper.java b/wm-data-engine/src/main/java/com/water/data_engine/mapper/DataSourceMapper.java new file mode 100644 index 00000000..94cf8c93 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/mapper/DataSourceMapper.java @@ -0,0 +1,12 @@ +package com.water.data_engine.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.data_engine.entity.DataSource; +import org.apache.ibatis.annotations.Mapper; + +/** + * 数据源Mapper + */ +@Mapper +public interface DataSourceMapper extends BaseMapper { +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/mapper/QualityRuleMapper.java b/wm-data-engine/src/main/java/com/water/data_engine/mapper/QualityRuleMapper.java new file mode 100644 index 00000000..ebf4beae --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/mapper/QualityRuleMapper.java @@ -0,0 +1,12 @@ +package com.water.data_engine.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.data_engine.entity.QualityRule; +import org.apache.ibatis.annotations.Mapper; + +/** + * 质量规则Mapper + */ +@Mapper +public interface QualityRuleMapper extends BaseMapper { +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/mapper/StorageConfigMapper.java b/wm-data-engine/src/main/java/com/water/data_engine/mapper/StorageConfigMapper.java new file mode 100644 index 00000000..7b7e6d92 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/mapper/StorageConfigMapper.java @@ -0,0 +1,12 @@ +package com.water.data_engine.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.data_engine.entity.StorageConfig; +import org.apache.ibatis.annotations.Mapper; + +/** + * 存储配置Mapper + */ +@Mapper +public interface StorageConfigMapper extends BaseMapper { +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/mapper/SyncTaskMapper.java b/wm-data-engine/src/main/java/com/water/data_engine/mapper/SyncTaskMapper.java new file mode 100644 index 00000000..4d34b9bb --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/mapper/SyncTaskMapper.java @@ -0,0 +1,12 @@ +package com.water.data_engine.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.data_engine.entity.SyncTask; +import org.apache.ibatis.annotations.Mapper; + +/** + * 同步任务Mapper + */ +@Mapper +public interface SyncTaskMapper extends BaseMapper { +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/service/DataCollectService.java b/wm-data-engine/src/main/java/com/water/data_engine/service/DataCollectService.java new file mode 100644 index 00000000..ef13c838 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/service/DataCollectService.java @@ -0,0 +1,281 @@ +package com.water.data_engine.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.water.data_engine.entity.CollectRecord; +import com.water.data_engine.entity.CollectTask; +import com.water.data_engine.entity.DataSource; +import com.water.data_engine.mapper.CollectRecordMapper; +import com.water.data_engine.mapper.CollectTaskMapper; +import com.water.data_engine.mapper.DataSourceMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.*; + +/** + * 数据采集服务 + * DE-01: 实时流(MQTT/Kafka) + 批量采集 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class DataCollectService { + + private final KafkaTemplate kafkaTemplate; + private final JdbcTemplate jdbcTemplate; + private final DataSourceMapper dataSourceMapper; + private final CollectTaskMapper collectTaskMapper; + private final CollectRecordMapper collectRecordMapper; + private final SimpMessagingTemplate wsMessagingTemplate; + private final ObjectMapper mapper = new ObjectMapper(); + + // ==================== 实时流采集 ==================== + + /** + * 实时数据接入:接收各来源数据,统一写入 Kafka + * 支持 MQTT/Kafka 来源的实时流 + */ + public String ingestRealtime(String sourceType, String sourceId, Map rawData) { + try { + Map envelope = buildEnvelope(sourceType, sourceId, rawData); + String json = mapper.writeValueAsString(envelope); + + // 根据来源路由到不同 topic + String topic = routeTopic(sourceType); + kafkaTemplate.send(topic, sourceId, json); + + // 保存采集记录 + saveCollectRecord(null, sourceType, sourceId, rawData, "success", null); + + // 通过 WebSocket 推送实时数据 + wsMessagingTemplate.convertAndSend("/topic/data/realtime/" + sourceType, envelope); + + log.debug("实时数据接入: {} -> {}, topic: {}", sourceType, sourceId, topic); + return topic; + } catch (Exception e) { + log.error("实时数据接入失败: {}", e.getMessage(), e); + saveCollectRecord(null, sourceType, sourceId, rawData, "failed", e.getMessage()); + throw new RuntimeException("数据接入失败: " + e.getMessage()); + } + } + + /** + * Kafka 消费者:处理 IoT 设备遥测数据 + */ + @KafkaListener(topics = "iot.raw.generic", groupId = "wm-data-engine") + public void consumeIotRaw(String message) { + try { + @SuppressWarnings("unchecked") + Map envelope = mapper.readValue(message, Map.class); + @SuppressWarnings("unchecked") + Map data = (Map