Compare commits
5
Commits
778726f943
...
f808efdc80
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f808efdc80 | ||
|
|
8290b813f1 | ||
|
|
4268f8df6b | ||
|
|
919f75cf9e | ||
|
|
28dcea5fb6 |
@@ -0,0 +1,56 @@
|
||||
package com.water.bpm.controller;
|
||||
|
||||
import com.water.bpm.entity.BpmApprovalRecord;
|
||||
import com.water.bpm.entity.BpmProcessDefinition;
|
||||
import com.water.bpm.entity.BpmProcessInstance;
|
||||
import com.water.bpm.service.ProcessEngine;
|
||||
import com.water.common.core.result.R;
|
||||
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("/bpm")
|
||||
@RequiredArgsConstructor
|
||||
public class ProcessController {
|
||||
|
||||
private final ProcessEngine processEngine;
|
||||
|
||||
@Operation(summary = "发起流程")
|
||||
@PostMapping("/start")
|
||||
public R<BpmProcessInstance> start(@RequestBody Map<String, Object> req) {
|
||||
BpmProcessDefinition def = new BpmProcessDefinition();
|
||||
def.setId(Long.parseLong(String.valueOf(req.get("definitionId"))));
|
||||
def.setProcessKey((String) req.get("processKey"));
|
||||
def.setProcessName((String) req.get("processName"));
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> formData = (Map<String, Object>) req.getOrDefault("formData", new HashMap<>());
|
||||
return R.ok(processEngine.startProcess(def, 1L, "当前用户",
|
||||
(String) req.get("businessKey"), formData));
|
||||
}
|
||||
|
||||
@Operation(summary = "审批")
|
||||
@PostMapping("/approve")
|
||||
public R<BpmProcessInstance> approve(@RequestBody Map<String, Object> req) {
|
||||
return R.ok(processEngine.approve(
|
||||
(String) req.get("instanceId"), 1L, "当前用户",
|
||||
(String) req.get("nodeId"), (String) req.get("nodeName"),
|
||||
(String) req.get("action"), (String) req.get("comment")));
|
||||
}
|
||||
|
||||
@Operation(summary = "我的待办")
|
||||
@GetMapping("/todo")
|
||||
public R<List<BpmProcessInstance>> todo() {
|
||||
return R.ok(processEngine.getTodoList(1L));
|
||||
}
|
||||
|
||||
@Operation(summary = "流程详情")
|
||||
@GetMapping("/instance/{id}")
|
||||
public R<BpmProcessInstance> instance(@PathVariable String id) {
|
||||
return R.ok(processEngine.getInstance(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.water.bpm.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class BpmApprovalRecord {
|
||||
private Long id;
|
||||
private Long instanceId;
|
||||
private String nodeId;
|
||||
private String nodeName;
|
||||
private Long approverId;
|
||||
private String approverName;
|
||||
private String action; // approve/reject/transfer/delegate/back
|
||||
private String comment;
|
||||
private String targetAssignee; // 转办/委派目标
|
||||
private LocalDateTime approvedAt;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.water.bpm.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class BpmProcessDefinition {
|
||||
private Long id;
|
||||
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
|
||||
private Integer version;
|
||||
private Integer status; // 0:草稿 1:发布 2:停用
|
||||
private String createdBy;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.water.bpm.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class BpmProcessInstance {
|
||||
private Long id;
|
||||
private String instanceId;
|
||||
private Long definitionId;
|
||||
private String processKey;
|
||||
private String businessKey; // 关联业务ID
|
||||
private String businessType; // 业务类型
|
||||
private String title;
|
||||
private Long initiatorId;
|
||||
private String initiatorName;
|
||||
private String currentNode; // 当前审批节点
|
||||
private String currentAssignee; // 当前处理人
|
||||
private String status; // running/completed/terminated/rejected
|
||||
private Map<String, Object> variables;
|
||||
private Map<String, Object> formData;
|
||||
private LocalDateTime startedAt;
|
||||
private LocalDateTime completedAt;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.water.bpm.service;
|
||||
|
||||
import com.water.bpm.entity.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ProcessEngine {
|
||||
|
||||
// 简化流程引擎:模拟 Camunda/Flowable 核心功能
|
||||
private final Map<String, BpmProcessInstance> instances = new ConcurrentHashMap<>();
|
||||
private final List<BpmApprovalRecord> approvalRecords = new ArrayList<>();
|
||||
|
||||
/** 创建流程实例 */
|
||||
@Transactional
|
||||
public BpmProcessInstance startProcess(BpmProcessDefinition definition, Long initiatorId,
|
||||
String initiatorName, String businessKey,
|
||||
Map<String, Object> formData) {
|
||||
BpmProcessInstance instance = new BpmProcessInstance();
|
||||
instance.setId(System.currentTimeMillis());
|
||||
instance.setInstanceId(UUID.randomUUID().toString());
|
||||
instance.setDefinitionId(definition.getId());
|
||||
instance.setProcessKey(definition.getProcessKey());
|
||||
instance.setTitle(definition.getProcessName());
|
||||
instance.setBusinessKey(businessKey);
|
||||
instance.setInitiatorId(initiatorId);
|
||||
instance.setInitiatorName(initiatorName);
|
||||
instance.setStatus("running");
|
||||
instance.setCurrentNode("START");
|
||||
instance.setFormData(formData);
|
||||
instance.setStartedAt(java.time.LocalDateTime.now());
|
||||
instance.setCreatedAt(java.time.LocalDateTime.now());
|
||||
instances.put(instance.getInstanceId(), instance);
|
||||
log.info("Process started: {} - {}", instance.getProcessKey(), instance.getTitle());
|
||||
return instance;
|
||||
}
|
||||
|
||||
/** 审批节点 */
|
||||
@Transactional
|
||||
public BpmProcessInstance approve(String instanceId, Long approverId, String approverName,
|
||||
String nodeId, String nodeName,
|
||||
String action, String comment) {
|
||||
BpmProcessInstance instance = instances.get(instanceId);
|
||||
if (instance == null) throw new RuntimeException("流程实例不存在");
|
||||
|
||||
BpmApprovalRecord record = new BpmApprovalRecord();
|
||||
record.setInstanceId(instance.getId());
|
||||
record.setNodeId(nodeId);
|
||||
record.setNodeName(nodeName);
|
||||
record.setApproverId(approverId);
|
||||
record.setApproverName(approverName);
|
||||
record.setAction(action);
|
||||
record.setComment(comment);
|
||||
record.setApprovedAt(java.time.LocalDateTime.now());
|
||||
approvalRecords.add(record);
|
||||
|
||||
instance.setCurrentNode(nodeName);
|
||||
switch (action) {
|
||||
case "approve": instance.setStatus("running"); break;
|
||||
case "reject": instance.setStatus("rejected"); instance.setCompletedAt(java.time.LocalDateTime.now()); break;
|
||||
default: instance.setStatus("running");
|
||||
}
|
||||
log.info("Approval: {} - {}: {}", instanceId, action, comment);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/** 完成流程 */
|
||||
@Transactional
|
||||
public void completeProcess(String instanceId) {
|
||||
BpmProcessInstance instance = instances.get(instanceId);
|
||||
if (instance != null) {
|
||||
instance.setStatus("completed");
|
||||
instance.setCompletedAt(java.time.LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询待办 */
|
||||
public List<BpmProcessInstance> getTodoList(Long userId) {
|
||||
return instances.values().stream()
|
||||
.filter(i -> "running".equals(i.getStatus()) && i.getInitiatorId().equals(userId))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/** 查询流程实例 */
|
||||
public BpmProcessInstance getInstance(String instanceId) {
|
||||
return instances.get(instanceId);
|
||||
}
|
||||
}
|
||||
+114
-9
@@ -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">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent><groupId>com.water</groupId><artifactId>wm-parent</artifactId><version>1.0.0-SNAPSHOT</version></parent>
|
||||
<parent>
|
||||
<groupId>com.water</groupId>
|
||||
<artifactId>wm-parent</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>wm-data-engine</artifactId>
|
||||
<name>wm-data-engine</name>
|
||||
<description>数据汇聚引擎模块</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency><groupId>com.water</groupId><artifactId>wm-common</artifactId></dependency>
|
||||
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
|
||||
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>
|
||||
<dependency><groupId>org.springframework.kafka</groupId><artifactId>spring-kafka</artifactId></dependency>
|
||||
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
|
||||
<dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId></dependency>
|
||||
<dependency><groupId>net.postgis</groupId><artifactId>postgis-jdbc</artifactId></dependency>
|
||||
<!-- 公共模块 -->
|
||||
<dependency>
|
||||
<groupId>com.water</groupId>
|
||||
<artifactId>wm-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- WebSocket -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Nacos 服务发现 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Kafka -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.kafka</groupId>
|
||||
<artifactId>spring-kafka</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- PostgreSQL -->
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- PostGIS -->
|
||||
<dependency>
|
||||
<groupId>net.postgis</groupId>
|
||||
<artifactId>postgis-jdbc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis-Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MinIO -->
|
||||
<dependency>
|
||||
<groupId>io.minio</groupId>
|
||||
<artifactId>minio</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Hutool -->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Knife4j OpenAPI3 -->
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- EasyExcel -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>easyexcel</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.kafka</groupId>
|
||||
<artifactId>spring-kafka-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
@@ -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<String, String> producerFactory() {
|
||||
Map<String, Object> 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<String, String> kafkaTemplate(ProducerFactory<String, String> producerFactory) {
|
||||
return new KafkaTemplate<>(producerFactory);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ConsumerFactory<String, String> consumerFactory() {
|
||||
Map<String, Object> 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<String, String> kafkaListenerContainerFactory(
|
||||
ConsumerFactory<String, String> consumerFactory) {
|
||||
ConcurrentKafkaListenerContainerFactory<String, String> factory =
|
||||
new ConcurrentKafkaListenerContainerFactory<>();
|
||||
factory.setConsumerFactory(consumerFactory);
|
||||
factory.setConcurrency(3);
|
||||
return factory;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
+85
@@ -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<String> ingestRealtime(@RequestBody Map<String, Object> request) {
|
||||
String sourceType = (String) request.get("sourceType");
|
||||
String sourceId = (String) request.get("sourceId");
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> data = (Map<String, Object>) request.get("data");
|
||||
|
||||
String topic = collectService.ingestRealtime(sourceType, sourceId, data);
|
||||
return R.ok("数据已接入,topic: " + topic);
|
||||
}
|
||||
|
||||
@Operation(summary = "批量数据接入")
|
||||
@PostMapping("/batch")
|
||||
public R<String> batchIngest(@RequestBody List<Map<String, Object>> batchData) {
|
||||
int count = collectService.batchIngest(batchData);
|
||||
return R.ok("批量接入完成,成功: " + count + " 条");
|
||||
}
|
||||
|
||||
// ==================== 采集任务管理 ====================
|
||||
|
||||
@Operation(summary = "创建批量采集任务")
|
||||
@PostMapping("/task")
|
||||
public R<CollectTask> createTask(@RequestBody Map<String, Object> 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<String> executeTask(@PathVariable Long taskId,
|
||||
@RequestBody List<Map<String, Object>> dataList) {
|
||||
collectService.executeTask(taskId, dataList);
|
||||
return R.ok("任务执行完成");
|
||||
}
|
||||
|
||||
@Operation(summary = "查询采集任务列表")
|
||||
@GetMapping("/task/list")
|
||||
public R<Page<CollectTask>> 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<Page<CollectRecord>> listRecords(@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(required = false) Long taskId) {
|
||||
return R.ok(collectService.listRecords(page, size, taskId));
|
||||
}
|
||||
}
|
||||
@@ -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<String> ingest(@RequestBody Map<String, Object> req) {
|
||||
String sourceType = (String) req.get("sourceType");
|
||||
String sourceId = (String) req.get("sourceId");
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> data = (Map<String, Object>) req.get("data");
|
||||
collectService.ingestRealtime(sourceType, sourceId, data);
|
||||
return R.ok("数据已接入");
|
||||
}
|
||||
|
||||
@Operation(summary = "批量接入(兼容旧接口)")
|
||||
@PostMapping("/ingest/batch")
|
||||
public R<String> batchIngest(@RequestBody List<Map<String, Object>> batch) {
|
||||
collectService.batchIngest(batch);
|
||||
return R.ok("批量接入完成");
|
||||
}
|
||||
|
||||
@Operation(summary = "数据标准化+清洗+质控(管道演示)")
|
||||
@PostMapping("/pipeline")
|
||||
public R<Map<String, Object>> pipeline(@RequestBody Map<String, Object> raw) {
|
||||
Map<String, Object> result = governanceService.pipeline(raw);
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "引擎状态")
|
||||
@GetMapping("/status")
|
||||
public R<Map<String, Object>> status() {
|
||||
Map<String, Object> 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);
|
||||
}
|
||||
}
|
||||
+117
@@ -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<Map<String, Object>> standardize(@RequestBody Map<String, Object> raw) {
|
||||
return R.ok(governanceService.standardize(raw));
|
||||
}
|
||||
|
||||
@Operation(summary = "批量数据标准化")
|
||||
@PostMapping("/standardize/batch")
|
||||
public R<List<Map<String, Object>>> batchStandardize(@RequestBody List<Map<String, Object>> rawDataList) {
|
||||
return R.ok(governanceService.batchStandardize(rawDataList));
|
||||
}
|
||||
|
||||
// ==================== 数据清洗 ====================
|
||||
|
||||
@Operation(summary = "数据清洗")
|
||||
@PostMapping("/clean")
|
||||
public R<Map<String, Object>> clean(@RequestBody Map<String, Object> data) {
|
||||
return R.ok(governanceService.clean(data));
|
||||
}
|
||||
|
||||
@Operation(summary = "批量数据清洗")
|
||||
@PostMapping("/clean/batch")
|
||||
public R<List<Map<String, Object>>> batchClean(@RequestBody List<Map<String, Object>> dataList) {
|
||||
return R.ok(governanceService.batchClean(dataList));
|
||||
}
|
||||
|
||||
// ==================== 数据质量 ====================
|
||||
|
||||
@Operation(summary = "数据质量检查")
|
||||
@PostMapping("/quality/check")
|
||||
public R<Map<String, Object>> qualityCheck(@RequestBody Map<String, Object> data) {
|
||||
return R.ok(governanceService.qualityCheck(data));
|
||||
}
|
||||
|
||||
@Operation(summary = "批量数据质量检查")
|
||||
@PostMapping("/quality/check/batch")
|
||||
public R<List<Map<String, Object>>> batchQualityCheck(@RequestBody List<Map<String, Object>> dataList) {
|
||||
return R.ok(governanceService.batchQualityCheck(dataList));
|
||||
}
|
||||
|
||||
@Operation(summary = "执行质量规则检查")
|
||||
@PostMapping("/quality/rules/execute")
|
||||
public R<Map<String, Object>> executeQualityRules(@RequestBody Map<String, Object> request) {
|
||||
String tableName = (String) request.get("tableName");
|
||||
return R.ok(governanceService.executeQualityRules(tableName));
|
||||
}
|
||||
|
||||
// ==================== 数据管道 ====================
|
||||
|
||||
@Operation(summary = "完整数据管道(标准化->清洗->质控)")
|
||||
@PostMapping("/pipeline")
|
||||
public R<Map<String, Object>> pipeline(@RequestBody Map<String, Object> raw) {
|
||||
return R.ok(governanceService.pipeline(raw));
|
||||
}
|
||||
|
||||
@Operation(summary = "批量数据管道")
|
||||
@PostMapping("/pipeline/batch")
|
||||
public R<List<Map<String, Object>>> batchPipeline(@RequestBody List<Map<String, Object>> rawDataList) {
|
||||
return R.ok(governanceService.batchPipeline(rawDataList));
|
||||
}
|
||||
|
||||
// ==================== 质量规则管理 ====================
|
||||
|
||||
@Operation(summary = "创建质量规则")
|
||||
@PostMapping("/quality/rule")
|
||||
public R<QualityRule> createQualityRule(@RequestBody QualityRule rule) {
|
||||
return R.ok(governanceService.createQualityRule(rule));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新质量规则")
|
||||
@PutMapping("/quality/rule/{id}")
|
||||
public R<QualityRule> updateQualityRule(@PathVariable Long id, @RequestBody QualityRule rule) {
|
||||
return R.ok(governanceService.updateQualityRule(id, rule));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除质量规则")
|
||||
@DeleteMapping("/quality/rule/{id}")
|
||||
public R<String> deleteQualityRule(@PathVariable Long id) {
|
||||
governanceService.deleteQualityRule(id);
|
||||
return R.ok("删除成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "查询质量规则列表")
|
||||
@GetMapping("/quality/rule/list")
|
||||
public R<List<QualityRule>> listQualityRules(
|
||||
@RequestParam(required = false) String tableName,
|
||||
@RequestParam(required = false) String ruleType) {
|
||||
return R.ok(governanceService.listQualityRules(tableName, ruleType));
|
||||
}
|
||||
}
|
||||
+118
@@ -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<String> ingestViaApi(@PathVariable String sourceCode,
|
||||
@RequestBody Map<String, Object> data) {
|
||||
String topic = ingestService.ingestViaApi(sourceCode, data);
|
||||
return R.ok("数据已接入,topic: " + topic);
|
||||
}
|
||||
|
||||
@Operation(summary = "通过 API 批量接入数据")
|
||||
@PostMapping("/api/{sourceCode}/batch")
|
||||
public R<String> batchIngestViaApi(@PathVariable String sourceCode,
|
||||
@RequestBody List<Map<String, Object>> dataList) {
|
||||
int count = ingestService.batchIngestViaApi(sourceCode, dataList);
|
||||
return R.ok("批量接入完成,成功: " + count + " 条");
|
||||
}
|
||||
|
||||
// ==================== 数据库接入 ====================
|
||||
|
||||
@Operation(summary = "从外部数据库拉取数据")
|
||||
@PostMapping("/database/{sourceId}/pull")
|
||||
public R<String> pullFromDatabase(@PathVariable Long sourceId,
|
||||
@RequestBody Map<String, Object> 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<String> syncToTable(@PathVariable Long sourceId,
|
||||
@RequestBody Map<String, Object> request) {
|
||||
String querySql = (String) request.get("querySql");
|
||||
String targetTable = (String) request.get("targetTable");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> columns = (List<String>) request.get("columns");
|
||||
int count = ingestService.syncToTable(sourceId, querySql, targetTable, columns);
|
||||
return R.ok("同步完成,成功: " + count + " 条");
|
||||
}
|
||||
|
||||
// ==================== 文件接入 ====================
|
||||
|
||||
@Operation(summary = "通过文件(CSV)接入数据")
|
||||
@PostMapping("/file/{sourceCode}")
|
||||
public R<String> 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<DataSource> createDataSource(@RequestBody DataSource dataSource) {
|
||||
return R.ok(ingestService.createDataSource(dataSource));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新数据源")
|
||||
@PutMapping("/source/{id}")
|
||||
public R<DataSource> updateDataSource(@PathVariable Long id,
|
||||
@RequestBody DataSource dataSource) {
|
||||
return R.ok(ingestService.updateDataSource(id, dataSource));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除数据源")
|
||||
@DeleteMapping("/source/{id}")
|
||||
public R<String> deleteDataSource(@PathVariable Long id) {
|
||||
ingestService.deleteDataSource(id);
|
||||
return R.ok("删除成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "查询数据源列表")
|
||||
@GetMapping("/source/list")
|
||||
public R<List<DataSource>> listDataSources(@RequestParam(required = false) String sourceType) {
|
||||
return R.ok(ingestService.listDataSources(sourceType));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取数据源详情")
|
||||
@GetMapping("/source/{id}")
|
||||
public R<DataSource> getDataSource(@PathVariable Long id) {
|
||||
return R.ok(ingestService.getDataSource(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "测试数据源连接")
|
||||
@PostMapping("/source/{id}/test")
|
||||
public R<Boolean> testConnection(@PathVariable Long id) {
|
||||
return R.ok(ingestService.testConnection(id));
|
||||
}
|
||||
}
|
||||
+134
@@ -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<SyncTask> createSyncTask(@RequestBody SyncTask syncTask) {
|
||||
return R.ok(integrationService.createSyncTask(syncTask));
|
||||
}
|
||||
|
||||
@Operation(summary = "执行同步任务")
|
||||
@PostMapping("/sync/task/{taskId}/execute")
|
||||
public R<String> executeSyncTask(@PathVariable Long taskId) {
|
||||
int count = integrationService.executeSyncTask(taskId);
|
||||
return R.ok("同步完成,处理: " + count + " 条");
|
||||
}
|
||||
|
||||
@Operation(summary = "执行全量同步")
|
||||
@PostMapping("/sync/full")
|
||||
public R<String> fullSync(@RequestBody Map<String, Object> 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<String> incrementalSync(@RequestBody Map<String, Object> 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<List<SyncTask>> listSyncTasks(@RequestParam(required = false) String status) {
|
||||
return R.ok(integrationService.listSyncTasks(status));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取同步任务详情")
|
||||
@GetMapping("/sync/task/{id}")
|
||||
public R<SyncTask> getSyncTask(@PathVariable Long id) {
|
||||
return R.ok(integrationService.getSyncTask(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除同步任务")
|
||||
@DeleteMapping("/sync/task/{id}")
|
||||
public R<String> deleteSyncTask(@PathVariable Long id) {
|
||||
integrationService.deleteSyncTask(id);
|
||||
return R.ok("删除成功");
|
||||
}
|
||||
|
||||
// ==================== 数据合并与聚合 ====================
|
||||
|
||||
@Operation(summary = "数据合并(多源整合)")
|
||||
@PostMapping("/merge")
|
||||
public R<List<Map<String, Object>>> mergeData(@RequestBody Map<String, Object> request) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> sourceTables = (List<String>) request.get("sourceTables");
|
||||
String joinColumn = (String) request.get("joinColumn");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> selectColumns = (List<String>) request.get("selectColumns");
|
||||
return R.ok(integrationService.mergeData(sourceTables, joinColumn, selectColumns));
|
||||
}
|
||||
|
||||
@Operation(summary = "数据聚合(按维度汇总)")
|
||||
@PostMapping("/aggregate")
|
||||
public R<List<Map<String, Object>>> aggregateData(@RequestBody Map<String, Object> request) {
|
||||
String sourceTable = (String) request.get("sourceTable");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> groupByColumns = (List<String>) request.get("groupByColumns");
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> aggregations = (Map<String, String>) request.get("aggregations");
|
||||
return R.ok(integrationService.aggregateData(sourceTable, groupByColumns, aggregations));
|
||||
}
|
||||
|
||||
// ==================== 数据血缘 ====================
|
||||
|
||||
@Operation(summary = "创建数据血缘关系")
|
||||
@PostMapping("/lineage")
|
||||
public R<DataLineage> createLineage(@RequestBody DataLineage lineage) {
|
||||
return R.ok(integrationService.createLineage(lineage));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询血缘关系(上游)")
|
||||
@GetMapping("/lineage/upstream/{tableName}")
|
||||
public R<List<DataLineage>> getUpstreamLineage(@PathVariable String tableName) {
|
||||
return R.ok(integrationService.getUpstreamLineage(tableName));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询血缘关系(下游)")
|
||||
@GetMapping("/lineage/downstream/{tableName}")
|
||||
public R<List<DataLineage>> getDownstreamLineage(@PathVariable String tableName) {
|
||||
return R.ok(integrationService.getDownstreamLineage(tableName));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询完整血缘链路")
|
||||
@GetMapping("/lineage/full/{tableName}")
|
||||
public R<Map<String, Object>> getFullLineage(@PathVariable String tableName) {
|
||||
return R.ok(integrationService.getFullLineage(tableName));
|
||||
}
|
||||
}
|
||||
+154
@@ -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<String> writeToTDengine(@RequestBody Map<String, Object> 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<String> batchWriteToTDengine(@RequestBody List<Map<String, Object>> dataList) {
|
||||
int count = storageService.batchWriteToTDengine(dataList);
|
||||
return R.ok("批量写入成功: " + count + " 条");
|
||||
}
|
||||
|
||||
@Operation(summary = "查询遥测数据")
|
||||
@GetMapping("/tdengine/query")
|
||||
public R<List<Map<String, Object>>> 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<List<Map<String, Object>>> 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<Long> insertToPostgres(@PathVariable String table,
|
||||
@RequestBody Map<String, Object> data) {
|
||||
return R.ok(storageService.insertToPostgres(table, data));
|
||||
}
|
||||
|
||||
@Operation(summary = "批量插入数据")
|
||||
@PostMapping("/postgres/{table}/batch")
|
||||
public R<String> batchInsertToPostgres(@PathVariable String table,
|
||||
@RequestBody List<Map<String, Object>> dataList) {
|
||||
int count = storageService.batchInsertToPostgres(table, dataList);
|
||||
return R.ok("批量插入成功: " + count + " 条");
|
||||
}
|
||||
|
||||
@Operation(summary = "更新数据")
|
||||
@PutMapping("/postgres/{table}/{id}")
|
||||
public R<String> updateInPostgres(@PathVariable String table,
|
||||
@PathVariable Long id,
|
||||
@RequestBody Map<String, Object> data) {
|
||||
int count = storageService.updateInPostgres(table, id, data);
|
||||
return R.ok("更新成功: " + count + " 条");
|
||||
}
|
||||
|
||||
@Operation(summary = "查询数据")
|
||||
@GetMapping("/postgres/{table}")
|
||||
public R<List<Map<String, Object>>> queryFromPostgres(
|
||||
@PathVariable String table,
|
||||
@RequestParam Map<String, Object> 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<String> 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<List<String>> listMinioObjects(@RequestParam(defaultValue = "") String prefix) throws Exception {
|
||||
return R.ok(storageService.listMinioObjects(prefix));
|
||||
}
|
||||
|
||||
// ==================== 存储配置管理 ====================
|
||||
|
||||
@Operation(summary = "创建存储配置")
|
||||
@PostMapping("/config")
|
||||
public R<StorageConfig> createStorageConfig(@RequestBody StorageConfig config) {
|
||||
return R.ok(storageService.createStorageConfig(config));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新存储配置")
|
||||
@PutMapping("/config/{id}")
|
||||
public R<StorageConfig> updateStorageConfig(@PathVariable Long id,
|
||||
@RequestBody StorageConfig config) {
|
||||
return R.ok(storageService.updateStorageConfig(id, config));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询存储配置列表")
|
||||
@GetMapping("/config/list")
|
||||
public R<List<StorageConfig>> listStorageConfigs(@RequestParam(required = false) String storageType) {
|
||||
return R.ok(storageService.listStorageConfigs(storageType));
|
||||
}
|
||||
|
||||
@Operation(summary = "测试存储连接")
|
||||
@PostMapping("/config/{id}/test")
|
||||
public R<Boolean> testStorageConnection(@PathVariable Long id) {
|
||||
return R.ok(storageService.testStorageConnection(id));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<CollectRecord> {
|
||||
}
|
||||
@@ -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<CollectTask> {
|
||||
}
|
||||
@@ -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<DataLineage> {
|
||||
}
|
||||
@@ -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<DataSource> {
|
||||
}
|
||||
@@ -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<QualityRule> {
|
||||
}
|
||||
@@ -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<StorageConfig> {
|
||||
}
|
||||
@@ -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<SyncTask> {
|
||||
}
|
||||
@@ -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<String, String> 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<String, Object> rawData) {
|
||||
try {
|
||||
Map<String, Object> 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<String, Object> envelope = mapper.readValue(message, Map.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> data = (Map<String, Object>) envelope.get("data");
|
||||
|
||||
String deviceSn = (String) data.getOrDefault("deviceSn", "unknown");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> metrics = (List<Map<String, Object>>) data.getOrDefault("metrics", List.of());
|
||||
|
||||
for (Map<String, Object> metric : metrics) {
|
||||
String key = (String) metric.get("key");
|
||||
Object value = metric.get("value");
|
||||
// 写入 TDengine
|
||||
writeToTDengine(deviceSn, key, value);
|
||||
}
|
||||
|
||||
log.debug("消费 IoT 数据: device={}, metrics={}", deviceSn, metrics.size());
|
||||
} catch (Exception e) {
|
||||
log.error("消费 IoT 数据失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kafka 消费者:处理水质数据
|
||||
*/
|
||||
@KafkaListener(topics = "data.quality", groupId = "wm-data-engine")
|
||||
public void consumeQualityData(String message) {
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> envelope = mapper.readValue(message, Map.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> data = (Map<String, Object>) envelope.get("data");
|
||||
|
||||
// 写入 PostgreSQL
|
||||
String sql = """
|
||||
INSERT INTO water_quality_record (test_type, test_point, point_type, area,
|
||||
turbidity, ph, residual_chlorine, is_qualified, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())
|
||||
""";
|
||||
jdbcTemplate.update(sql,
|
||||
data.get("testType"),
|
||||
data.get("testPoint"),
|
||||
data.get("pointType"),
|
||||
data.get("area"),
|
||||
data.get("turbidity"),
|
||||
data.get("ph"),
|
||||
data.get("residualChlorine"),
|
||||
data.get("isQualified"));
|
||||
|
||||
log.debug("消费水质数据: point={}", data.get("testPoint"));
|
||||
} catch (Exception e) {
|
||||
log.error("消费水质数据失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 批量采集 ====================
|
||||
|
||||
/**
|
||||
* 批量数据采集
|
||||
*/
|
||||
@Transactional
|
||||
public int batchIngest(List<Map<String, Object>> batchData) {
|
||||
int successCount = 0;
|
||||
for (Map<String, Object> data : batchData) {
|
||||
try {
|
||||
String sourceType = (String) data.getOrDefault("sourceType", "batch");
|
||||
String sourceId = (String) data.getOrDefault("sourceId", UUID.randomUUID().toString());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> rawData = (Map<String, Object>) data.getOrDefault("data", new HashMap<>());
|
||||
ingestRealtime(sourceType, sourceId, rawData);
|
||||
successCount++;
|
||||
} catch (Exception e) {
|
||||
log.warn("批量采集单条失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
return successCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建批量采集任务
|
||||
*/
|
||||
@Transactional
|
||||
public CollectTask createBatchTask(String taskName, Long sourceId, String targetTable) {
|
||||
CollectTask task = new CollectTask();
|
||||
task.setTaskName(taskName);
|
||||
task.setSourceId(sourceId);
|
||||
task.setCollectType("batch");
|
||||
task.setTargetTable(targetTable);
|
||||
task.setStatus("pending");
|
||||
task.setTotalCount(0L);
|
||||
task.setSuccessCount(0L);
|
||||
task.setFailCount(0L);
|
||||
collectTaskMapper.insert(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行采集任务
|
||||
*/
|
||||
@Transactional
|
||||
public void executeTask(Long taskId, List<Map<String, Object>> dataList) {
|
||||
CollectTask task = collectTaskMapper.selectById(taskId);
|
||||
if (task == null) {
|
||||
throw new RuntimeException("任务不存在: " + taskId);
|
||||
}
|
||||
|
||||
task.setStatus("running");
|
||||
task.setStartTime(LocalDateTime.now());
|
||||
task.setTotalCount((long) dataList.size());
|
||||
collectTaskMapper.updateById(task);
|
||||
|
||||
long success = 0;
|
||||
long fail = 0;
|
||||
|
||||
for (Map<String, Object> data : dataList) {
|
||||
try {
|
||||
String sourceType = (String) data.getOrDefault("sourceType", "batch");
|
||||
String sourceId = (String) data.getOrDefault("sourceId", UUID.randomUUID().toString());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> rawData = (Map<String, Object>) data.getOrDefault("data", data);
|
||||
ingestRealtime(sourceType, sourceId, rawData);
|
||||
success++;
|
||||
} catch (Exception e) {
|
||||
fail++;
|
||||
log.warn("任务 {} 采集失败: {}", taskId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
task.setSuccessCount(success);
|
||||
task.setFailCount(fail);
|
||||
task.setStatus("completed");
|
||||
task.setEndTime(LocalDateTime.now());
|
||||
collectTaskMapper.updateById(task);
|
||||
}
|
||||
|
||||
// ==================== 查询方法 ====================
|
||||
|
||||
/**
|
||||
* 查询采集任务列表
|
||||
*/
|
||||
public Page<CollectTask> listTasks(int page, int size, String status) {
|
||||
LambdaQueryWrapper<CollectTask> wrapper = new LambdaQueryWrapper<>();
|
||||
if (status != null && !status.isEmpty()) {
|
||||
wrapper.eq(CollectTask::getStatus, status);
|
||||
}
|
||||
wrapper.orderByDesc(CollectTask::getCreatedAt);
|
||||
return collectTaskMapper.selectPage(new Page<>(page, size), wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询采集记录
|
||||
*/
|
||||
public Page<CollectRecord> listRecords(int page, int size, Long taskId) {
|
||||
LambdaQueryWrapper<CollectRecord> wrapper = new LambdaQueryWrapper<>();
|
||||
if (taskId != null) {
|
||||
wrapper.eq(CollectRecord::getTaskId, taskId);
|
||||
}
|
||||
wrapper.orderByDesc(CollectRecord::getCollectTime);
|
||||
return collectRecordMapper.selectPage(new Page<>(page, size), wrapper);
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
private Map<String, Object> buildEnvelope(String sourceType, String sourceId, Map<String, Object> rawData) {
|
||||
Map<String, Object> envelope = new LinkedHashMap<>();
|
||||
envelope.put("sourceType", sourceType);
|
||||
envelope.put("sourceId", sourceId);
|
||||
envelope.put("timestamp", Instant.now().toEpochMilli());
|
||||
envelope.put("data", rawData);
|
||||
return envelope;
|
||||
}
|
||||
|
||||
private String routeTopic(String sourceType) {
|
||||
return switch (sourceType) {
|
||||
case "iot", "mqtt" -> "iot.raw.generic";
|
||||
case "quality" -> "data.quality";
|
||||
case "manual" -> "data.manual";
|
||||
case "api" -> "data.api";
|
||||
default -> "data.raw";
|
||||
};
|
||||
}
|
||||
|
||||
private void writeToTDengine(String deviceSn, String metricKey, Object value) {
|
||||
String sql = "INSERT INTO water_iot.iot_telemetry (ts, device_sn, metric_key, metric_value, quality) VALUES (NOW, ?, ?, ?, 1)";
|
||||
jdbcTemplate.update(sql, deviceSn, metricKey, value);
|
||||
}
|
||||
|
||||
private void saveCollectRecord(Long taskId, String sourceType, String sourceKey,
|
||||
Map<String, Object> rawData, String status, String errorMsg) {
|
||||
try {
|
||||
CollectRecord record = new CollectRecord();
|
||||
record.setTaskId(taskId);
|
||||
record.setSourceType(sourceType);
|
||||
record.setSourceKey(sourceKey);
|
||||
record.setRawData(rawData);
|
||||
record.setStatus(status);
|
||||
record.setErrorMsg(errorMsg);
|
||||
record.setCollectTime(LocalDateTime.now());
|
||||
collectRecordMapper.insert(record);
|
||||
} catch (Exception e) {
|
||||
log.error("保存采集记录失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
+380
@@ -0,0 +1,380 @@
|
||||
package com.water.data_engine.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.water.data_engine.entity.QualityRule;
|
||||
import com.water.data_engine.mapper.QualityRuleMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 数据治理服务
|
||||
* 数据标准化、清洗、质量控制
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DataGovernanceService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final QualityRuleMapper qualityRuleMapper;
|
||||
|
||||
// 水利行业标准字段映射
|
||||
private static final Map<String, String> STANDARD_FIELD_MAP = Map.of(
|
||||
"flow", "LL", // 流量
|
||||
"pressure", "YL", // 压力
|
||||
"level", "SW", // 水位
|
||||
"turbidity", "ZD", // 浊度
|
||||
"ph", "PH", // pH值
|
||||
"residual_chlorine", "YLJL", // 余氯
|
||||
"temperature", "WD", // 温度
|
||||
"conductivity", "DD", // 电导率
|
||||
"dissolved_oxygen", "RJY", // 溶解氧
|
||||
"ammonia", "AD" // 氨氮
|
||||
);
|
||||
|
||||
// 数值型标准字段
|
||||
private static final List<String> NUMERIC_FIELDS = List.of(
|
||||
"LL", "YL", "SW", "ZD", "PH", "YLJL", "WD", "DD", "RJY", "AD"
|
||||
);
|
||||
|
||||
// ==================== 数据标准化 ====================
|
||||
|
||||
/**
|
||||
* 数据标准化:水利数据对象标准映射
|
||||
*/
|
||||
public Map<String, Object> standardize(Map<String, Object> raw) {
|
||||
Map<String, Object> std = new LinkedHashMap<>();
|
||||
|
||||
for (Map.Entry<String, Object> entry : raw.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
|
||||
// 字段名映射
|
||||
String standardKey = STANDARD_FIELD_MAP.getOrDefault(key, key);
|
||||
std.put(standardKey, value);
|
||||
}
|
||||
|
||||
// 添加标准化标记
|
||||
std.put("_standardized", true);
|
||||
std.put("_standardize_time", LocalDateTime.now().toString());
|
||||
|
||||
return std;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量标准化
|
||||
*/
|
||||
public List<Map<String, Object>> batchStandardize(List<Map<String, Object>> rawDataList) {
|
||||
return rawDataList.stream()
|
||||
.map(this::standardize)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ==================== 数据清洗 ====================
|
||||
|
||||
/**
|
||||
* 数据清洗:缺失值填充、异常值检测
|
||||
*/
|
||||
public Map<String, Object> clean(Map<String, Object> data) {
|
||||
Map<String, Object> cleaned = new LinkedHashMap<>(data);
|
||||
|
||||
// 1. 缺失值处理
|
||||
for (String field : NUMERIC_FIELDS) {
|
||||
Object value = cleaned.get(field);
|
||||
if (value == null || "".equals(value.toString().trim())) {
|
||||
cleaned.put(field, -9999.0);
|
||||
cleaned.put(field + "_flag", "MISSING");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 异常值检测
|
||||
detectAnomalies(cleaned);
|
||||
|
||||
// 3. 数据类型转换
|
||||
convertDataTypes(cleaned);
|
||||
|
||||
cleaned.put("_cleaned", true);
|
||||
cleaned.put("_clean_time", LocalDateTime.now().toString());
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量清洗
|
||||
*/
|
||||
public List<Map<String, Object>> batchClean(List<Map<String, Object>> dataList) {
|
||||
return dataList.stream()
|
||||
.map(this::clean)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ==================== 数据质量控制 ====================
|
||||
|
||||
/**
|
||||
* 数据质量检查
|
||||
*/
|
||||
public Map<String, Object> qualityCheck(Map<String, Object> data) {
|
||||
Map<String, Object> result = new LinkedHashMap<>(data);
|
||||
int score = 100;
|
||||
List<String> issues = new ArrayList<>();
|
||||
|
||||
// 1. 完整性检查
|
||||
for (String field : NUMERIC_FIELDS) {
|
||||
if (data.containsKey(field + "_flag") && "MISSING".equals(data.get(field + "_flag"))) {
|
||||
score -= 5;
|
||||
issues.add(field + "数据缺失");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 异常值检查
|
||||
if (data.containsKey("LL_flag") && "ABNORMAL".equals(data.get("LL_flag"))) {
|
||||
score -= 15;
|
||||
issues.add("流量数据异常(负值)");
|
||||
}
|
||||
if (data.containsKey("PH")) {
|
||||
double ph = ((Number) data.get("PH")).doubleValue();
|
||||
if (ph < 0 || ph > 14) {
|
||||
score -= 10;
|
||||
issues.add("pH值超出合理范围(0-14)");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 时效性检查
|
||||
if (data.containsKey("_standardize_time")) {
|
||||
// 检查数据是否过于陈旧
|
||||
// 简化处理:假设超过1小时为陈旧数据
|
||||
}
|
||||
|
||||
// 4. 一致性检查
|
||||
if (data.containsKey("SW") && data.containsKey("YL")) {
|
||||
// 水位和压力应该有一定的相关性
|
||||
// 简化处理
|
||||
}
|
||||
|
||||
result.put("_quality_score", Math.max(score, 0));
|
||||
result.put("_quality_issues", issues);
|
||||
result.put("_quality_checked", true);
|
||||
result.put("_quality_check_time", LocalDateTime.now().toString());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量质量检查
|
||||
*/
|
||||
public List<Map<String, Object>> batchQualityCheck(List<Map<String, Object>> dataList) {
|
||||
return dataList.stream()
|
||||
.map(this::qualityCheck)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行质量规则检查
|
||||
*/
|
||||
@Transactional
|
||||
public Map<String, Object> executeQualityRules(String tableName) {
|
||||
LambdaQueryWrapper<QualityRule> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(QualityRule::getTableName, tableName);
|
||||
wrapper.eq(QualityRule::getEnabled, 1);
|
||||
List<QualityRule> rules = qualityRuleMapper.selectList(wrapper);
|
||||
|
||||
Map<String, Object> results = new HashMap<>();
|
||||
int totalChecks = rules.size();
|
||||
int passedChecks = 0;
|
||||
|
||||
for (QualityRule rule : rules) {
|
||||
try {
|
||||
boolean passed = executeSingleRule(rule);
|
||||
if (passed) {
|
||||
passedChecks++;
|
||||
}
|
||||
results.put(rule.getRuleName(), passed ? "PASS" : "FAIL");
|
||||
} catch (Exception e) {
|
||||
results.put(rule.getRuleName(), "ERROR: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
BigDecimal passRate = totalChecks > 0
|
||||
? BigDecimal.valueOf(passedChecks).multiply(BigDecimal.valueOf(100))
|
||||
.divide(BigDecimal.valueOf(totalChecks), 2, RoundingMode.HALF_UP)
|
||||
: BigDecimal.ZERO;
|
||||
|
||||
Map<String, Object> summary = new HashMap<>();
|
||||
summary.put("table", tableName);
|
||||
summary.put("total_rules", totalChecks);
|
||||
summary.put("passed", passedChecks);
|
||||
summary.put("failed", totalChecks - passedChecks);
|
||||
summary.put("pass_rate", passRate);
|
||||
summary.put("details", results);
|
||||
summary.put("check_time", LocalDateTime.now());
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
// ==================== 数据血缘 ====================
|
||||
|
||||
/**
|
||||
* 建立数据血缘关系
|
||||
*/
|
||||
@Transactional
|
||||
public void buildLineage(Long sourceId, Long targetId, String relation) {
|
||||
String sql = """
|
||||
INSERT INTO de_data_lineage (source_table, source_column, target_table, target_column,
|
||||
transform_type, description, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, NOW())
|
||||
ON CONFLICT DO NOTHING
|
||||
""";
|
||||
jdbcTemplate.update(sql, "iot_telemetry", null, "iot_telemetry_hourly", null, relation, "自动聚合");
|
||||
}
|
||||
|
||||
// ==================== 数据管道 ====================
|
||||
|
||||
/**
|
||||
* 完整的数据处理管道:标准化 -> 清洗 -> 质控
|
||||
*/
|
||||
public Map<String, Object> pipeline(Map<String, Object> raw) {
|
||||
Map<String, Object> std = standardize(raw);
|
||||
Map<String, Object> cleaned = clean(std);
|
||||
Map<String, Object> result = qualityCheck(cleaned);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量数据管道
|
||||
*/
|
||||
public List<Map<String, Object>> batchPipeline(List<Map<String, Object>> rawDataList) {
|
||||
return rawDataList.stream()
|
||||
.map(this::pipeline)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ==================== 质量规则管理 ====================
|
||||
|
||||
/**
|
||||
* 创建质量规则
|
||||
*/
|
||||
@Transactional
|
||||
public QualityRule createQualityRule(QualityRule rule) {
|
||||
rule.setEnabled(1);
|
||||
qualityRuleMapper.insert(rule);
|
||||
return rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新质量规则
|
||||
*/
|
||||
@Transactional
|
||||
public QualityRule updateQualityRule(Long id, QualityRule rule) {
|
||||
rule.setId(id);
|
||||
qualityRuleMapper.updateById(rule);
|
||||
return qualityRuleMapper.selectById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除质量规则
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteQualityRule(Long id) {
|
||||
qualityRuleMapper.deleteById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询质量规则列表
|
||||
*/
|
||||
public List<QualityRule> listQualityRules(String tableName, String ruleType) {
|
||||
LambdaQueryWrapper<QualityRule> wrapper = new LambdaQueryWrapper<>();
|
||||
if (tableName != null && !tableName.isEmpty()) {
|
||||
wrapper.eq(QualityRule::getTableName, tableName);
|
||||
}
|
||||
if (ruleType != null && !ruleType.isEmpty()) {
|
||||
wrapper.eq(QualityRule::getRuleType, ruleType);
|
||||
}
|
||||
return qualityRuleMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
private void detectAnomalies(Map<String, Object> data) {
|
||||
// 流量异常检测(负值)
|
||||
if (data.containsKey("LL")) {
|
||||
double ll = ((Number) data.get("LL")).doubleValue();
|
||||
if (ll < 0) {
|
||||
data.put("LL_flag", "ABNORMAL");
|
||||
}
|
||||
}
|
||||
|
||||
// 压力异常检测(超范围)
|
||||
if (data.containsKey("YL")) {
|
||||
double yl = ((Number) data.get("YL")).doubleValue();
|
||||
if (yl < 0 || yl > 100) {
|
||||
data.put("YL_flag", "ABNORMAL");
|
||||
}
|
||||
}
|
||||
|
||||
// 水位异常检测
|
||||
if (data.containsKey("SW")) {
|
||||
double sw = ((Number) data.get("SW")).doubleValue();
|
||||
if (sw < -100 || sw > 1000) {
|
||||
data.put("SW_flag", "ABNORMAL");
|
||||
}
|
||||
}
|
||||
|
||||
// 浊度异常检测
|
||||
if (data.containsKey("ZD")) {
|
||||
double zd = ((Number) data.get("ZD")).doubleValue();
|
||||
if (zd < 0 || zd > 1000) {
|
||||
data.put("ZD_flag", "ABNORMAL");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void convertDataTypes(Map<String, Object> data) {
|
||||
for (String field : NUMERIC_FIELDS) {
|
||||
Object value = data.get(field);
|
||||
if (value instanceof String) {
|
||||
try {
|
||||
data.put(field, Double.parseDouble((String) value));
|
||||
} catch (NumberFormatException e) {
|
||||
data.put(field + "_flag", "INVALID_TYPE");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean executeSingleRule(QualityRule rule) {
|
||||
// 简化实现:根据规则类型执行不同检查
|
||||
return switch (rule.getRuleType()) {
|
||||
case "completeness" -> checkCompleteness(rule);
|
||||
case "validity" -> checkValidity(rule);
|
||||
case "timeliness" -> checkTimeliness(rule);
|
||||
default -> true;
|
||||
};
|
||||
}
|
||||
|
||||
private boolean checkCompleteness(QualityRule rule) {
|
||||
String sql = String.format(
|
||||
"SELECT COUNT(*) FROM %s WHERE %s IS NULL",
|
||||
rule.getTableName(), rule.getColumnName());
|
||||
Integer nullCount = jdbcTemplate.queryForObject(sql, Integer.class);
|
||||
return nullCount == null || nullCount == 0;
|
||||
}
|
||||
|
||||
private boolean checkValidity(QualityRule rule) {
|
||||
// 简化:检查是否有无效值
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean checkTimeliness(QualityRule rule) {
|
||||
// 简化:检查数据时效性
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package com.water.data_engine.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.water.data_engine.entity.DataSource;
|
||||
import com.water.data_engine.mapper.DataSourceMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 数据接入服务
|
||||
* DE-02: RESTful API / WebSocket / 数据库直连
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DataIngestService {
|
||||
|
||||
private final DataSourceMapper dataSourceMapper;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final DataCollectService collectService;
|
||||
|
||||
// ==================== RESTful API 接入 ====================
|
||||
|
||||
/**
|
||||
* 通过 API 接入单条数据
|
||||
*/
|
||||
@Transactional
|
||||
public String ingestViaApi(String sourceCode, Map<String, Object> data) {
|
||||
DataSource source = getSourceByCode(sourceCode);
|
||||
if (source == null) {
|
||||
throw new RuntimeException("数据源不存在: " + sourceCode);
|
||||
}
|
||||
if (source.getStatus() != 1) {
|
||||
throw new RuntimeException("数据源已禁用: " + sourceCode);
|
||||
}
|
||||
|
||||
// 更新最后同步时间
|
||||
source.setLastSyncAt(LocalDateTime.now());
|
||||
dataSourceMapper.updateById(source);
|
||||
|
||||
return collectService.ingestRealtime(source.getCategory(), sourceCode, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 API 批量接入数据
|
||||
*/
|
||||
@Transactional
|
||||
public int batchIngestViaApi(String sourceCode, List<Map<String, Object>> dataList) {
|
||||
DataSource source = getSourceByCode(sourceCode);
|
||||
if (source == null) {
|
||||
throw new RuntimeException("数据源不存在: " + sourceCode);
|
||||
}
|
||||
|
||||
List<Map<String, Object>> wrappedList = dataList.stream()
|
||||
.map(data -> {
|
||||
Map<String, Object> wrapped = new HashMap<>(data);
|
||||
wrapped.put("sourceType", source.getCategory());
|
||||
wrapped.put("sourceId", sourceCode);
|
||||
return wrapped;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return collectService.batchIngest(wrappedList);
|
||||
}
|
||||
|
||||
// ==================== 数据库直连接入 ====================
|
||||
|
||||
/**
|
||||
* 从外部数据库拉取数据
|
||||
*/
|
||||
@Transactional
|
||||
public int pullFromDatabase(Long sourceId, String sql, String targetTable) {
|
||||
DataSource source = dataSourceMapper.selectById(sourceId);
|
||||
if (source == null) {
|
||||
throw new RuntimeException("数据源不存在: " + sourceId);
|
||||
}
|
||||
|
||||
try {
|
||||
// 执行查询
|
||||
List<Map<String, Object>> results = jdbcTemplate.queryForList(sql);
|
||||
int count = 0;
|
||||
|
||||
for (Map<String, Object> row : results) {
|
||||
try {
|
||||
collectService.ingestRealtime("database", source.getSourceCode(), row);
|
||||
count++;
|
||||
} catch (Exception e) {
|
||||
log.warn("数据库数据接入失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 更新同步时间
|
||||
source.setLastSyncAt(LocalDateTime.now());
|
||||
dataSourceMapper.updateById(source);
|
||||
|
||||
return count;
|
||||
} catch (Exception e) {
|
||||
log.error("从数据库拉取数据失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("数据拉取失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从外部数据库同步到本地表
|
||||
*/
|
||||
@Transactional
|
||||
public int syncToTable(Long sourceId, String querySql, String targetTable, List<String> columns) {
|
||||
List<Map<String, Object>> results = jdbcTemplate.queryForList(querySql);
|
||||
int count = 0;
|
||||
|
||||
for (Map<String, Object> row : results) {
|
||||
try {
|
||||
String insertSql = buildInsertSql(targetTable, columns);
|
||||
Object[] params = columns.stream()
|
||||
.map(col -> row.get(col))
|
||||
.toArray();
|
||||
jdbcTemplate.update(insertSql, params);
|
||||
count++;
|
||||
} catch (Exception e) {
|
||||
log.warn("同步到表失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 更新数据源同步时间
|
||||
DataSource source = dataSourceMapper.selectById(sourceId);
|
||||
if (source != null) {
|
||||
source.setLastSyncAt(LocalDateTime.now());
|
||||
dataSourceMapper.updateById(source);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
// ==================== 文件接入 ====================
|
||||
|
||||
/**
|
||||
* 通过文件(CSV)接入数据
|
||||
*/
|
||||
@Transactional
|
||||
public int ingestFromFile(MultipartFile file, String sourceCode) throws Exception {
|
||||
DataSource source = getSourceByCode(sourceCode);
|
||||
if (source == null) {
|
||||
throw new RuntimeException("数据源不存在: " + sourceCode);
|
||||
}
|
||||
|
||||
List<Map<String, Object>> dataList = parseCsv(file);
|
||||
return batchIngestViaApi(sourceCode, dataList);
|
||||
}
|
||||
|
||||
// ==================== 数据源管理 ====================
|
||||
|
||||
/**
|
||||
* 创建数据源
|
||||
*/
|
||||
@Transactional
|
||||
public DataSource createDataSource(DataSource dataSource) {
|
||||
// 检查编码唯一性
|
||||
LambdaQueryWrapper<DataSource> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(DataSource::getSourceCode, dataSource.getSourceCode());
|
||||
if (dataSourceMapper.selectCount(wrapper) > 0) {
|
||||
throw new RuntimeException("数据源编码已存在: " + dataSource.getSourceCode());
|
||||
}
|
||||
|
||||
dataSource.setStatus(1);
|
||||
dataSourceMapper.insert(dataSource);
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新数据源
|
||||
*/
|
||||
@Transactional
|
||||
public DataSource updateDataSource(Long id, DataSource dataSource) {
|
||||
DataSource existing = dataSourceMapper.selectById(id);
|
||||
if (existing == null) {
|
||||
throw new RuntimeException("数据源不存在: " + id);
|
||||
}
|
||||
|
||||
dataSource.setId(id);
|
||||
dataSourceMapper.updateById(dataSource);
|
||||
return dataSourceMapper.selectById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据源
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteDataSource(Long id) {
|
||||
dataSourceMapper.deleteById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询数据源列表
|
||||
*/
|
||||
public List<DataSource> listDataSources(String sourceType) {
|
||||
LambdaQueryWrapper<DataSource> wrapper = new LambdaQueryWrapper<>();
|
||||
if (sourceType != null && !sourceType.isEmpty()) {
|
||||
wrapper.eq(DataSource::getSourceType, sourceType);
|
||||
}
|
||||
wrapper.orderByDesc(DataSource::getCreatedAt);
|
||||
return dataSourceMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据源详情
|
||||
*/
|
||||
public DataSource getDataSource(Long id) {
|
||||
return dataSourceMapper.selectById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试数据源连接
|
||||
*/
|
||||
public boolean testConnection(Long id) {
|
||||
DataSource source = dataSourceMapper.selectById(id);
|
||||
if (source == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 根据数据源类型测试连接
|
||||
return switch (source.getSourceType()) {
|
||||
case "database" -> testDatabaseConnection(source);
|
||||
case "kafka" -> testKafkaConnection(source);
|
||||
default -> true;
|
||||
};
|
||||
} catch (Exception e) {
|
||||
log.error("测试连接失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
private DataSource getSourceByCode(String sourceCode) {
|
||||
LambdaQueryWrapper<DataSource> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(DataSource::getSourceCode, sourceCode);
|
||||
return dataSourceMapper.selectOne(wrapper);
|
||||
}
|
||||
|
||||
private String buildInsertSql(String table, List<String> columns) {
|
||||
String cols = String.join(", ", columns);
|
||||
String placeholders = columns.stream()
|
||||
.map(c -> "?")
|
||||
.collect(Collectors.joining(", "));
|
||||
return String.format("INSERT INTO %s (%s) VALUES (%s)", table, cols, placeholders);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> parseCsv(MultipartFile file) throws Exception {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String headerLine = reader.readLine();
|
||||
if (headerLine == null) {
|
||||
return result;
|
||||
}
|
||||
String[] headers = headerLine.split(",");
|
||||
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
String[] values = line.split(",");
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
for (int i = 0; i < headers.length && i < values.length; i++) {
|
||||
row.put(headers[i].trim(), values[i].trim());
|
||||
}
|
||||
result.add(row);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean testDatabaseConnection(DataSource source) {
|
||||
// 简单的连接测试
|
||||
try {
|
||||
jdbcTemplate.queryForObject("SELECT 1", Integer.class);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean testKafkaConnection(DataSource source) {
|
||||
// Kafka 连接测试(简化)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
package com.water.data_engine.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.water.data_engine.entity.DataLineage;
|
||||
import com.water.data_engine.entity.SyncTask;
|
||||
import com.water.data_engine.mapper.DataLineageMapper;
|
||||
import com.water.data_engine.mapper.SyncTaskMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 数据集成服务
|
||||
* DE-04: 多源异构数据整合
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DataIntegrationService {
|
||||
|
||||
private final SyncTaskMapper syncTaskMapper;
|
||||
private final DataLineageMapper dataLineageMapper;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final DataStorageService storageService;
|
||||
|
||||
// ==================== 数据同步任务 ====================
|
||||
|
||||
/**
|
||||
* 创建同步任务
|
||||
*/
|
||||
@Transactional
|
||||
public SyncTask createSyncTask(SyncTask syncTask) {
|
||||
syncTask.setStatus("pending");
|
||||
syncTaskMapper.insert(syncTask);
|
||||
return syncTask;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行同步任务
|
||||
*/
|
||||
@Transactional
|
||||
public int executeSyncTask(Long taskId) {
|
||||
SyncTask task = syncTaskMapper.selectById(taskId);
|
||||
if (task == null) {
|
||||
throw new RuntimeException("同步任务不存在: " + taskId);
|
||||
}
|
||||
|
||||
task.setStatus("running");
|
||||
syncTaskMapper.updateById(task);
|
||||
|
||||
try {
|
||||
int count = performSync(task);
|
||||
|
||||
task.setStatus("completed");
|
||||
task.setLastSyncAt(LocalDateTime.now());
|
||||
task.setLastSyncCount((long) count);
|
||||
task.setErrorMsg(null);
|
||||
syncTaskMapper.updateById(task);
|
||||
|
||||
return count;
|
||||
} catch (Exception e) {
|
||||
task.setStatus("failed");
|
||||
task.setErrorMsg(e.getMessage());
|
||||
syncTaskMapper.updateById(task);
|
||||
throw new RuntimeException("同步任务执行失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行全量同步
|
||||
*/
|
||||
@Transactional
|
||||
public int fullSync(Long sourceId, String sourceTable, String targetTable) {
|
||||
try {
|
||||
// 查询源表所有数据
|
||||
String querySql = "SELECT * FROM " + sourceTable;
|
||||
List<Map<String, Object>> data = jdbcTemplate.queryForList(querySql);
|
||||
|
||||
// 批量写入目标表
|
||||
return storageService.batchInsertToPostgres(targetTable, data);
|
||||
} catch (Exception e) {
|
||||
log.error("全量同步失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("同步失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行增量同步(基于时间戳)
|
||||
*/
|
||||
@Transactional
|
||||
public int incrementalSync(Long sourceId, String sourceTable, String targetTable,
|
||||
String timestampColumn, LocalDateTime lastSyncTime) {
|
||||
try {
|
||||
String querySql = String.format(
|
||||
"SELECT * FROM %s WHERE %s > ? ORDER BY %s",
|
||||
sourceTable, timestampColumn, timestampColumn);
|
||||
List<Map<String, Object>> data = jdbcTemplate.queryForList(querySql, lastSyncTime);
|
||||
|
||||
return storageService.batchInsertToPostgres(targetTable, data);
|
||||
} catch (Exception e) {
|
||||
log.error("增量同步失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("增量同步失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据合并(多源整合)
|
||||
*/
|
||||
@Transactional
|
||||
public List<Map<String, Object>> mergeData(List<String> sourceTables,
|
||||
String joinColumn,
|
||||
List<String> selectColumns) {
|
||||
if (sourceTables.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
// 构建 UNION ALL 查询
|
||||
String columnList = String.join(", ", selectColumns);
|
||||
String unions = sourceTables.stream()
|
||||
.map(table -> String.format("SELECT %s FROM %s", columnList, table))
|
||||
.collect(Collectors.joining(" UNION ALL "));
|
||||
|
||||
String sql = String.format("SELECT %s FROM (%s) AS merged ORDER BY %s DESC",
|
||||
columnList, unions, joinColumn);
|
||||
|
||||
return jdbcTemplate.queryForList(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据聚合(按维度汇总)
|
||||
*/
|
||||
public List<Map<String, Object>> aggregateData(String sourceTable,
|
||||
List<String> groupByColumns,
|
||||
Map<String, String> aggregations) {
|
||||
String groupBy = String.join(", ", groupByColumns);
|
||||
String aggExpr = aggregations.entrySet().stream()
|
||||
.map(e -> String.format("%s(%s) AS %s_%s", e.getValue(), e.getKey(), e.getKey(), e.getValue().toLowerCase()))
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
String sql = String.format(
|
||||
"SELECT %s, %s FROM %s GROUP BY %s ORDER BY %s",
|
||||
groupBy, aggExpr, sourceTable, groupBy, groupBy);
|
||||
|
||||
return jdbcTemplate.queryForList(sql);
|
||||
}
|
||||
|
||||
// ==================== 数据血缘 ====================
|
||||
|
||||
/**
|
||||
* 创建数据血缘关系
|
||||
*/
|
||||
@Transactional
|
||||
public DataLineage createLineage(DataLineage lineage) {
|
||||
lineage.setCreatedAt(LocalDateTime.now());
|
||||
dataLineageMapper.insert(lineage);
|
||||
return lineage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询血缘关系(上游)
|
||||
*/
|
||||
public List<DataLineage> getUpstreamLineage(String tableName) {
|
||||
LambdaQueryWrapper<DataLineage> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(DataLineage::getTargetTable, tableName);
|
||||
return dataLineageMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询血缘关系(下游)
|
||||
*/
|
||||
public List<DataLineage> getDownstreamLineage(String tableName) {
|
||||
LambdaQueryWrapper<DataLineage> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(DataLineage::getSourceTable, tableName);
|
||||
return dataLineageMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询完整血缘链路
|
||||
*/
|
||||
public Map<String, Object> getFullLineage(String tableName) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("table", tableName);
|
||||
result.put("upstream", getUpstreamLineage(tableName));
|
||||
result.put("downstream", getDownstreamLineage(tableName));
|
||||
return result;
|
||||
}
|
||||
|
||||
// ==================== 查询方法 ====================
|
||||
|
||||
/**
|
||||
* 查询同步任务列表
|
||||
*/
|
||||
public List<SyncTask> listSyncTasks(String status) {
|
||||
LambdaQueryWrapper<SyncTask> wrapper = new LambdaQueryWrapper<>();
|
||||
if (status != null && !status.isEmpty()) {
|
||||
wrapper.eq(SyncTask::getStatus, status);
|
||||
}
|
||||
wrapper.orderByDesc(SyncTask::getCreatedAt);
|
||||
return syncTaskMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取同步任务详情
|
||||
*/
|
||||
public SyncTask getSyncTask(Long id) {
|
||||
return syncTaskMapper.selectById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除同步任务
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteSyncTask(Long id) {
|
||||
syncTaskMapper.deleteById(id);
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
private int performSync(SyncTask task) {
|
||||
// 根据同步类型执行不同策略
|
||||
return switch (task.getSyncType()) {
|
||||
case "full" -> performFullSync(task);
|
||||
case "incremental" -> performIncrementalSync(task);
|
||||
default -> 0;
|
||||
};
|
||||
}
|
||||
|
||||
private int performFullSync(SyncTask task) {
|
||||
// 简化实现:实际应根据 sourceId 查找数据源配置
|
||||
log.info("执行全量同步任务: {}", task.getTaskName());
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int performIncrementalSync(SyncTask task) {
|
||||
LocalDateTime lastSync = task.getLastSyncAt();
|
||||
if (lastSync == null) {
|
||||
// 首次同步,使用默认起始时间
|
||||
lastSync = LocalDateTime.now().minusDays(1);
|
||||
}
|
||||
log.info("执行增量同步任务: {}, lastSync: {}", task.getTaskName(), lastSync);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package com.water.data_engine.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.water.data_engine.entity.StorageConfig;
|
||||
import com.water.data_engine.mapper.StorageConfigMapper;
|
||||
import io.minio.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 数据存储管理服务
|
||||
* DE-03: TDengine + PostgreSQL + MinIO
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DataStorageService {
|
||||
|
||||
private final StorageConfigMapper storageConfigMapper;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
// ==================== TDengine 时序存储 ====================
|
||||
|
||||
/**
|
||||
* 写入遥测数据到 TDengine
|
||||
*/
|
||||
public void writeToTDengine(String deviceSn, String deviceType, String area,
|
||||
String metricKey, Double value) {
|
||||
try {
|
||||
// 使用子表方式写入(按设备分表)
|
||||
String childTable = "device_" + deviceSn.replaceAll("[^a-zA-Z0-9]", "_");
|
||||
String createTableSql = String.format(
|
||||
"CREATE TABLE IF NOT EXISTS water_iot.%s USING water_iot.iot_telemetry TAGS('%s', '%s', '%s')",
|
||||
childTable, deviceType, area, deviceSn);
|
||||
jdbcTemplate.update(createTableSql);
|
||||
|
||||
String insertSql = String.format(
|
||||
"INSERT INTO water_iot.%s (ts, device_sn, metric_key, metric_value, quality) VALUES (NOW, '%s', '%s', %f, 1)",
|
||||
childTable, deviceSn, metricKey, value);
|
||||
jdbcTemplate.update(insertSql);
|
||||
} catch (Exception e) {
|
||||
log.error("写入 TDengine 失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量写入遥测数据
|
||||
*/
|
||||
@Transactional
|
||||
public int batchWriteToTDengine(List<Map<String, Object>> dataList) {
|
||||
int count = 0;
|
||||
for (Map<String, Object> data : dataList) {
|
||||
try {
|
||||
writeToTDengine(
|
||||
(String) data.get("deviceSn"),
|
||||
(String) data.get("deviceType"),
|
||||
(String) data.get("area"),
|
||||
(String) data.get("metricKey"),
|
||||
((Number) data.get("value")).doubleValue()
|
||||
);
|
||||
count++;
|
||||
} catch (Exception e) {
|
||||
log.warn("批量写入单条失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 TDengine 查询遥测数据
|
||||
*/
|
||||
public List<Map<String, Object>> queryFromTDengine(String deviceSn, String metricKey,
|
||||
LocalDateTime startTime, LocalDateTime endTime) {
|
||||
try {
|
||||
String sql = """
|
||||
SELECT ts, device_sn, metric_key, metric_value, quality
|
||||
FROM water_iot.iot_telemetry
|
||||
WHERE device_sn = ? AND metric_key = ?
|
||||
AND ts >= ? AND ts <= ?
|
||||
ORDER BY ts DESC
|
||||
""";
|
||||
return jdbcTemplate.queryForList(sql, deviceSn, metricKey, startTime, endTime);
|
||||
} catch (Exception e) {
|
||||
log.error("查询 TDengine 失败: {}", e.getMessage());
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聚合数据(小时级)
|
||||
*/
|
||||
public List<Map<String, Object>> queryHourlyAgg(String deviceSn, String metricKey,
|
||||
LocalDateTime startTime, LocalDateTime endTime) {
|
||||
try {
|
||||
String sql = """
|
||||
SELECT _wstart as ts, device_sn, metric_key,
|
||||
MIN(metric_value) as min_val, MAX(metric_value) as max_val,
|
||||
AVG(metric_value) as avg_val, COUNT(*) as cnt
|
||||
FROM water_iot.iot_telemetry
|
||||
WHERE device_sn = ? AND metric_key = ?
|
||||
AND ts >= ? AND ts <= ?
|
||||
INTERVAL(1h)
|
||||
""";
|
||||
return jdbcTemplate.queryForList(sql, deviceSn, metricKey, startTime, endTime);
|
||||
} catch (Exception e) {
|
||||
log.error("查询聚合数据失败: {}", e.getMessage());
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PostgreSQL 关系存储 ====================
|
||||
|
||||
/**
|
||||
* 通用数据插入(PostgreSQL)
|
||||
*/
|
||||
@Transactional
|
||||
public Long insertToPostgres(String table, Map<String, Object> data) {
|
||||
List<String> columns = new ArrayList<>(data.keySet());
|
||||
String cols = String.join(", ", columns);
|
||||
String placeholders = columns.stream().map(c -> "?").collect(Collectors.joining(", "));
|
||||
String sql = String.format("INSERT INTO %s (%s) VALUES (%s) RETURNING id", table, cols, placeholders);
|
||||
|
||||
Object[] params = columns.stream().map(data::get).toArray();
|
||||
return jdbcTemplate.queryForObject(sql, Long.class, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入(PostgreSQL)
|
||||
*/
|
||||
@Transactional
|
||||
public int batchInsertToPostgres(String table, List<Map<String, Object>> dataList) {
|
||||
if (dataList.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (Map<String, Object> data : dataList) {
|
||||
try {
|
||||
insertToPostgres(table, data);
|
||||
count++;
|
||||
} catch (Exception e) {
|
||||
log.warn("批量插入单条失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新数据(PostgreSQL)
|
||||
*/
|
||||
@Transactional
|
||||
public int updateInPostgres(String table, Long id, Map<String, Object> data) {
|
||||
List<String> setClauses = new ArrayList<>();
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, Object> entry : data.entrySet()) {
|
||||
setClauses.add(entry.getKey() + " = ?");
|
||||
params.add(entry.getValue());
|
||||
}
|
||||
params.add(id);
|
||||
|
||||
String sql = String.format("UPDATE %s SET %s WHERE id = ?", table, String.join(", ", setClauses));
|
||||
return jdbcTemplate.update(sql, params.toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询数据(PostgreSQL)
|
||||
*/
|
||||
public List<Map<String, Object>> queryFromPostgres(String table, Map<String, Object> conditions,
|
||||
int page, int size) {
|
||||
StringBuilder sql = new StringBuilder("SELECT * FROM ").append(table).append(" WHERE 1=1");
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, Object> entry : conditions.entrySet()) {
|
||||
sql.append(" AND ").append(entry.getKey()).append(" = ?");
|
||||
params.add(entry.getValue());
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY id DESC LIMIT ? OFFSET ?");
|
||||
params.add(size);
|
||||
params.add((page - 1) * size);
|
||||
|
||||
return jdbcTemplate.queryForList(sql.toString(), params.toArray());
|
||||
}
|
||||
|
||||
// ==================== MinIO 对象存储 ====================
|
||||
|
||||
/**
|
||||
* 上传文件到 MinIO
|
||||
*/
|
||||
public String uploadToMinio(MultipartFile file, String module) throws Exception {
|
||||
MinioClient client = getMinioClient();
|
||||
String bucket = "water-management";
|
||||
String objectName = module + "/" + LocalDate.now() + "/" +
|
||||
UUID.randomUUID() + "_" + file.getOriginalFilename();
|
||||
|
||||
// 确保桶存在
|
||||
if (!client.bucketExists(BucketExistsArgs.builder().bucket(bucket).build())) {
|
||||
client.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
|
||||
}
|
||||
|
||||
client.putObject(PutObjectArgs.builder()
|
||||
.bucket(bucket)
|
||||
.object(objectName)
|
||||
.stream(file.getInputStream(), file.getSize(), -1)
|
||||
.contentType(file.getContentType())
|
||||
.build());
|
||||
|
||||
return objectName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 MinIO 下载文件
|
||||
*/
|
||||
public InputStream downloadFromMinio(String objectName) throws Exception {
|
||||
MinioClient client = getMinioClient();
|
||||
return client.getObject(GetObjectArgs.builder()
|
||||
.bucket("water-management")
|
||||
.object(objectName)
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出 MinIO 文件
|
||||
*/
|
||||
public List<String> listMinioObjects(String prefix) throws Exception {
|
||||
MinioClient client = getMinioClient();
|
||||
List<String> objects = new ArrayList<>();
|
||||
|
||||
Iterable<Result<Item>> results = client.listObjects(ListObjectsArgs.builder()
|
||||
.bucket("water-management")
|
||||
.prefix(prefix)
|
||||
.build());
|
||||
|
||||
for (Result<Item> result : results) {
|
||||
objects.add(result.get().objectName());
|
||||
}
|
||||
|
||||
return objects;
|
||||
}
|
||||
|
||||
// ==================== 存储配置管理 ====================
|
||||
|
||||
/**
|
||||
* 创建存储配置
|
||||
*/
|
||||
@Transactional
|
||||
public StorageConfig createStorageConfig(StorageConfig config) {
|
||||
config.setStatus(1);
|
||||
storageConfigMapper.insert(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新存储配置
|
||||
*/
|
||||
@Transactional
|
||||
public StorageConfig updateStorageConfig(Long id, StorageConfig config) {
|
||||
config.setId(id);
|
||||
storageConfigMapper.updateById(config);
|
||||
return storageConfigMapper.selectById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询存储配置列表
|
||||
*/
|
||||
public List<StorageConfig> listStorageConfigs(String storageType) {
|
||||
LambdaQueryWrapper<StorageConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
if (storageType != null && !storageType.isEmpty()) {
|
||||
wrapper.eq(StorageConfig::getStorageType, storageType);
|
||||
}
|
||||
return storageConfigMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试存储连接
|
||||
*/
|
||||
public boolean testStorageConnection(Long id) {
|
||||
StorageConfig config = storageConfigMapper.selectById(id);
|
||||
if (config == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return switch (config.getStorageType()) {
|
||||
case "postgresql" -> testPostgresConnection(config);
|
||||
case "tdengine" -> testTDengineConnection(config);
|
||||
case "minio" -> testMinioConnection(config);
|
||||
default -> false;
|
||||
};
|
||||
} catch (Exception e) {
|
||||
log.error("测试存储连接失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
private MinioClient getMinioClient() {
|
||||
return MinioClient.builder()
|
||||
.endpoint(System.getenv().getOrDefault("MINIO_ENDPOINT", "http://127.0.0.1:9000"))
|
||||
.credentials(
|
||||
System.getenv().getOrDefault("MINIO_ACCESS_KEY", "minioadmin"),
|
||||
System.getenv().getOrDefault("MINIO_SECRET_KEY", "minioadmin"))
|
||||
.build();
|
||||
}
|
||||
|
||||
private boolean testPostgresConnection(StorageConfig config) {
|
||||
try {
|
||||
jdbcTemplate.queryForObject("SELECT 1", Integer.class);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean testTDengineConnection(StorageConfig config) {
|
||||
try {
|
||||
jdbcTemplate.queryForObject("SELECT server_version()", String.class);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean testMinioConnection(StorageConfig config) {
|
||||
try {
|
||||
MinioClient client = getMinioClient();
|
||||
client.bucketExists(BucketExistsArgs.builder().bucket("water-management").build());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package com.water.data_engine.websocket;
|
||||
|
||||
import com.water.data_engine.service.DataCollectService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.messaging.handler.annotation.SendTo;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* WebSocket 数据推送控制器
|
||||
* 用于实时数据推送到前端
|
||||
*/
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class DataWebSocketController {
|
||||
|
||||
private final SimpMessagingTemplate messagingTemplate;
|
||||
private final DataCollectService collectService;
|
||||
|
||||
/**
|
||||
* 接收客户端订阅请求
|
||||
*/
|
||||
@MessageMapping("/subscribe/data")
|
||||
@SendTo("/topic/data/realtime")
|
||||
public Map<String, Object> subscribeRealtimeData(Map<String, Object> request) {
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("status", "subscribed");
|
||||
response.put("timestamp", LocalDateTime.now().toString());
|
||||
response.put("message", "已订阅实时数据推送");
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收客户端发送的控制指令
|
||||
*/
|
||||
@MessageMapping("/control/pause")
|
||||
@SendTo("/topic/data/control")
|
||||
public Map<String, Object> pauseDataPush(Map<String, Object> request) {
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("action", "pause");
|
||||
response.put("status", "success");
|
||||
response.put("timestamp", LocalDateTime.now().toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
@MessageMapping("/control/resume")
|
||||
@SendTo("/topic/data/control")
|
||||
public Map<String, Object> resumeDataPush(Map<String, Object> request) {
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("action", "resume");
|
||||
response.put("status", "success");
|
||||
response.put("timestamp", LocalDateTime.now().toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动推送数据到指定 topic
|
||||
*/
|
||||
public void pushRealtimeData(String sourceType, Map<String, Object> data) {
|
||||
messagingTemplate.convertAndSend("/topic/data/realtime/" + sourceType, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送告警数据
|
||||
*/
|
||||
public void pushAlertData(Map<String, Object> alert) {
|
||||
messagingTemplate.convertAndSend("/topic/data/alert", alert);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送统计数据
|
||||
*/
|
||||
public void pushStatistics(Map<String, Object> stats) {
|
||||
messagingTemplate.convertAndSend("/topic/data/statistics", stats);
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,44 @@ spring:
|
||||
url: jdbc:postgresql://${PG_HOST:127.0.0.1}:5432/water_management
|
||||
username: ${PG_USER:water}
|
||||
password: ${PG_PASS:water123}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_HOST:127.0.0.1}:8848
|
||||
kafka:
|
||||
bootstrap-servers: ${KAFKA_SERVERS:127.0.0.1}:9092
|
||||
consumer:
|
||||
group-id: wm-data-engine
|
||||
auto-offset-reset: latest
|
||||
producer:
|
||||
key-serializer: org.apache.kafka.common.serialization.StringSerializer
|
||||
value-serializer: org.apache.kafka.common.serialization.StringSerializer
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 100MB
|
||||
max-request-size: 100MB
|
||||
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath*:/mapper/**/*.xml
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
|
||||
global-config:
|
||||
db-config:
|
||||
logic-delete-field: deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
|
||||
# MinIO 配置
|
||||
minio:
|
||||
endpoint: ${MINIO_ENDPOINT:http://127.0.0.1:9000}
|
||||
access-key: ${MINIO_ACCESS_KEY:minioadmin}
|
||||
secret-key: ${MINIO_SECRET_KEY:minioadmin}
|
||||
bucket: water-management
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.water.data_engine: DEBUG
|
||||
com.baomidou.mybatisplus: DEBUG
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
-- =============================================
|
||||
-- 智慧水务管理系统 - 数据引擎 DDL
|
||||
-- 版本: V1
|
||||
-- 描述: 数据汇聚引擎相关表
|
||||
-- =============================================
|
||||
|
||||
-- ==================== 数据源管理 ====================
|
||||
|
||||
-- 数据源配置表
|
||||
CREATE TABLE IF NOT EXISTS de_data_source (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
source_name VARCHAR(100) NOT NULL,
|
||||
source_code VARCHAR(50) UNIQUE NOT NULL,
|
||||
source_type VARCHAR(30) NOT NULL, -- mqtt/kafka/rest/websocket/database/file
|
||||
category VARCHAR(30), -- iot/manual/api/database
|
||||
connection_config JSONB, -- 连接配置(JSON)
|
||||
sync_mode VARCHAR(20) DEFAULT 'realtime', -- realtime/batch/scheduled
|
||||
sync_cron VARCHAR(50), -- 定时同步Cron表达式
|
||||
status SMALLINT DEFAULT 1, -- 0:禁用 1:启用
|
||||
description VARCHAR(500),
|
||||
last_sync_at TIMESTAMP,
|
||||
deleted SMALLINT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE de_data_source IS '数据源配置表';
|
||||
COMMENT ON COLUMN de_data_source.source_type IS '数据源类型: mqtt/kafka/rest/websocket/database/file';
|
||||
COMMENT ON COLUMN de_data_source.sync_mode IS '同步模式: realtime/batch/scheduled';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_de_data_source_type ON de_data_source(source_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_de_data_source_status ON de_data_source(status);
|
||||
|
||||
-- ==================== 数据采集 ====================
|
||||
|
||||
-- 数据采集任务表
|
||||
CREATE TABLE IF NOT EXISTS de_collect_task (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
task_name VARCHAR(100) NOT NULL,
|
||||
source_id BIGINT REFERENCES de_data_source(id),
|
||||
collect_type VARCHAR(30) NOT NULL, -- realtime/batch/manual
|
||||
topic VARCHAR(100), -- Kafka/MQTT topic
|
||||
target_table VARCHAR(100), -- 目标表名
|
||||
transform_rule JSONB, -- 转换规则
|
||||
status VARCHAR(20) DEFAULT 'pending', -- pending/running/paused/completed/failed
|
||||
total_count BIGINT DEFAULT 0,
|
||||
success_count BIGINT DEFAULT 0,
|
||||
fail_count BIGINT DEFAULT 0,
|
||||
start_time TIMESTAMP,
|
||||
end_time TIMESTAMP,
|
||||
error_msg TEXT,
|
||||
deleted SMALLINT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE de_collect_task IS '数据采集任务表';
|
||||
CREATE INDEX IF NOT EXISTS idx_de_collect_task_status ON de_collect_task(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_de_collect_task_source ON de_collect_task(source_id);
|
||||
|
||||
-- 数据采集记录表
|
||||
CREATE TABLE IF NOT EXISTS de_collect_record (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
task_id BIGINT REFERENCES de_collect_task(id),
|
||||
source_id BIGINT REFERENCES de_data_source(id),
|
||||
source_type VARCHAR(30),
|
||||
source_key VARCHAR(100),
|
||||
raw_data JSONB,
|
||||
processed_data JSONB,
|
||||
status VARCHAR(20) DEFAULT 'success', -- success/failed/skipped
|
||||
error_msg VARCHAR(500),
|
||||
collect_time TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE de_collect_record IS '数据采集记录表';
|
||||
CREATE INDEX IF NOT EXISTS idx_de_collect_record_time ON de_collect_record(collect_time DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_de_collect_record_task ON de_collect_record(task_id);
|
||||
|
||||
-- ==================== 数据接入 ====================
|
||||
|
||||
-- API接入配置表
|
||||
CREATE TABLE IF NOT EXISTS de_api_config (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
api_name VARCHAR(100) NOT NULL,
|
||||
api_path VARCHAR(200) UNIQUE NOT NULL,
|
||||
method VARCHAR(10) DEFAULT 'POST', -- GET/POST/PUT
|
||||
source_id BIGINT REFERENCES de_data_source(id),
|
||||
request_schema JSONB, -- 请求Schema定义
|
||||
response_schema JSONB, -- 响应Schema定义
|
||||
auth_type VARCHAR(20) DEFAULT 'none', -- none/token/api_key/basic
|
||||
rate_limit INT DEFAULT 100, -- 限流(次/分钟)
|
||||
status SMALLINT DEFAULT 1,
|
||||
deleted SMALLINT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE de_api_config IS 'API接入配置表';
|
||||
|
||||
-- ==================== 数据存储 ====================
|
||||
|
||||
-- 存储配置表
|
||||
CREATE TABLE IF NOT EXISTS de_storage_config (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
storage_name VARCHAR(100) NOT NULL,
|
||||
storage_type VARCHAR(30) NOT NULL, -- tdengine/postgresql/minio
|
||||
connection_url VARCHAR(500),
|
||||
username VARCHAR(100),
|
||||
password VARCHAR(255),
|
||||
database_name VARCHAR(100),
|
||||
bucket_name VARCHAR(100),
|
||||
extra_config JSONB,
|
||||
status SMALLINT DEFAULT 1,
|
||||
deleted SMALLINT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE de_storage_config IS '存储配置表';
|
||||
|
||||
-- 存储路由规则表(哪类数据存到哪)
|
||||
CREATE TABLE IF NOT EXISTS de_storage_route (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
source_type VARCHAR(30) NOT NULL,
|
||||
data_category VARCHAR(50), -- telemetry/quality/billing/document
|
||||
storage_id BIGINT REFERENCES de_storage_config(id),
|
||||
target_table VARCHAR(100),
|
||||
partition_rule VARCHAR(200), -- 分区规则
|
||||
retention_days INT DEFAULT 365,
|
||||
status SMALLINT DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE de_storage_route IS '存储路由规则表';
|
||||
|
||||
-- ==================== 数据集成 ====================
|
||||
|
||||
-- 数据同步任务表
|
||||
CREATE TABLE IF NOT EXISTS de_sync_task (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
task_name VARCHAR(100) NOT NULL,
|
||||
source_id BIGINT REFERENCES de_data_source(id),
|
||||
target_storage_id BIGINT REFERENCES de_storage_config(id),
|
||||
sync_type VARCHAR(30) NOT NULL, -- full/incremental/cdc
|
||||
sync_cron VARCHAR(50),
|
||||
last_sync_at TIMESTAMP,
|
||||
last_sync_count BIGINT,
|
||||
status VARCHAR(20) DEFAULT 'pending', -- pending/running/paused/completed/failed
|
||||
error_msg TEXT,
|
||||
deleted SMALLINT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE de_sync_task IS '数据同步任务表';
|
||||
CREATE INDEX IF NOT EXISTS idx_de_sync_task_status ON de_sync_task(status);
|
||||
|
||||
-- ==================== 数据质量 ====================
|
||||
|
||||
-- 数据质量规则表
|
||||
CREATE TABLE IF NOT EXISTS de_quality_rule (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
rule_name VARCHAR(100) NOT NULL,
|
||||
rule_type VARCHAR(30) NOT NULL, -- completeness/validity/timeliness/consistency
|
||||
table_name VARCHAR(100),
|
||||
column_name VARCHAR(100),
|
||||
rule_expr VARCHAR(500), -- 规则表达式
|
||||
threshold DECIMAL(5,2), -- 阈值
|
||||
severity VARCHAR(20) DEFAULT 'warning', -- info/warning/error
|
||||
enabled SMALLINT DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE de_quality_rule IS '数据质量规则表';
|
||||
|
||||
-- 数据质量检查记录表
|
||||
CREATE TABLE IF NOT EXISTS de_quality_check (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
rule_id BIGINT REFERENCES de_quality_rule(id),
|
||||
check_time TIMESTAMP DEFAULT NOW(),
|
||||
total_count BIGINT,
|
||||
pass_count BIGINT,
|
||||
fail_count BIGINT,
|
||||
pass_rate DECIMAL(5,2),
|
||||
result_detail JSONB,
|
||||
status VARCHAR(20) DEFAULT 'success' -- success/failed
|
||||
);
|
||||
COMMENT ON TABLE de_quality_check IS '数据质量检查记录表';
|
||||
CREATE INDEX IF NOT EXISTS idx_de_quality_check_time ON de_quality_check(check_time DESC);
|
||||
|
||||
-- ==================== 数据血缘 ====================
|
||||
|
||||
-- 数据血缘关系表
|
||||
CREATE TABLE IF NOT EXISTS de_data_lineage (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
source_table VARCHAR(100) NOT NULL,
|
||||
source_column VARCHAR(100),
|
||||
target_table VARCHAR(100) NOT NULL,
|
||||
target_column VARCHAR(100),
|
||||
transform_type VARCHAR(30), -- direct/mapping/aggregation/calculation
|
||||
transform_rule TEXT,
|
||||
description VARCHAR(500),
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE de_data_lineage IS '数据血缘关系表';
|
||||
CREATE INDEX IF NOT EXISTS idx_de_lineage_source ON de_data_lineage(source_table);
|
||||
CREATE INDEX IF NOT EXISTS idx_de_lineage_target ON de_data_lineage(target_table);
|
||||
|
||||
-- ==================== 数据引擎统计 ====================
|
||||
|
||||
-- 数据统计仪表板
|
||||
CREATE TABLE IF NOT EXISTS de_stat_daily (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
stat_date DATE NOT NULL,
|
||||
source_id BIGINT,
|
||||
collect_count BIGINT DEFAULT 0,
|
||||
store_count BIGINT DEFAULT 0,
|
||||
quality_score DECIMAL(5,2),
|
||||
sync_count BIGINT DEFAULT 0,
|
||||
error_count BIGINT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
UNIQUE(stat_date, source_id)
|
||||
);
|
||||
COMMENT ON TABLE de_stat_daily IS '日统计数据表';
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
package com.water.data_engine.service;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 数据采集服务测试
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DataCollectServiceTest {
|
||||
|
||||
@Mock
|
||||
private KafkaTemplate<String, String> kafkaTemplate;
|
||||
|
||||
@Mock
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Mock
|
||||
private com.water.data_engine.mapper.DataSourceMapper dataSourceMapper;
|
||||
|
||||
@Mock
|
||||
private com.water.data_engine.mapper.CollectTaskMapper collectTaskMapper;
|
||||
|
||||
@Mock
|
||||
private com.water.data_engine.mapper.CollectRecordMapper collectRecordMapper;
|
||||
|
||||
@Mock
|
||||
private SimpMessagingTemplate wsMessagingTemplate;
|
||||
|
||||
private DataCollectService collectService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
collectService = new DataCollectService(
|
||||
kafkaTemplate, jdbcTemplate, dataSourceMapper,
|
||||
collectTaskMapper, collectRecordMapper, wsMessagingTemplate
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("实时数据接入-IoT设备数据")
|
||||
void testIngestRealtime_IoT() {
|
||||
// Given
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("deviceSn", "FM001");
|
||||
data.put("metrics", List.of(
|
||||
Map.of("key", "LL", "value", 12.5),
|
||||
Map.of("key", "YL", "value", 0.35)
|
||||
));
|
||||
|
||||
// When
|
||||
String topic = collectService.ingestRealtime("iot", "FM001", data);
|
||||
|
||||
// Then
|
||||
assertEquals("iot.raw.generic", topic);
|
||||
verify(kafkaTemplate).send(eq("iot.raw.generic"), eq("FM001"), anyString());
|
||||
verify(wsMessagingTemplate).convertAndSend(eq("/topic/data/realtime/iot"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("实时数据接入-水质数据")
|
||||
void testIngestRealtime_Quality() {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("testPoint", "水厂出口");
|
||||
data.put("turbidity", 0.5);
|
||||
data.put("ph", 7.2);
|
||||
|
||||
String topic = collectService.ingestRealtime("quality", "WQ001", data);
|
||||
|
||||
assertEquals("data.quality", topic);
|
||||
verify(kafkaTemplate).send(eq("data.quality"), eq("WQ001"), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("批量数据采集")
|
||||
void testBatchIngest() {
|
||||
List<Map<String, Object>> batchData = List.of(
|
||||
Map.of("sourceType", "iot", "sourceId", "FM001", "data", Map.of("LL", 12.5)),
|
||||
Map.of("sourceType", "iot", "sourceId", "FM002", "data", Map.of("LL", 15.3)),
|
||||
Map.of("sourceType", "manual", "sourceId", "MAN001", "data", Map.of("SW", 100.0))
|
||||
);
|
||||
|
||||
int count = collectService.batchIngest(batchData);
|
||||
|
||||
assertEquals(3, count);
|
||||
verify(kafkaTemplate, times(3)).send(anyString(), anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建批量采集任务")
|
||||
void testCreateBatchTask() {
|
||||
com.water.data_engine.entity.CollectTask task = collectService.createBatchTask(
|
||||
"测试批量任务", 1L, "iot_telemetry");
|
||||
|
||||
assertNotNull(task);
|
||||
assertEquals("测试批量任务", task.getTaskName());
|
||||
assertEquals("batch", task.getCollectType());
|
||||
assertEquals("pending", task.getStatus());
|
||||
verify(collectTaskMapper).insert(any(com.water.data_engine.entity.CollectTask.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Topic路由测试")
|
||||
void testRouteTopic() {
|
||||
// 测试不同数据源类型的topic路由
|
||||
assertDoesNotThrow(() -> collectService.ingestRealtime("iot", "test", Map.of()));
|
||||
assertDoesNotThrow(() -> collectService.ingestRealtime("mqtt", "test", Map.of()));
|
||||
assertDoesNotThrow(() -> collectService.ingestRealtime("quality", "test", Map.of()));
|
||||
assertDoesNotThrow(() -> collectService.ingestRealtime("manual", "test", Map.of()));
|
||||
assertDoesNotThrow(() -> collectService.ingestRealtime("api", "test", Map.of()));
|
||||
assertDoesNotThrow(() -> collectService.ingestRealtime("unknown", "test", Map.of()));
|
||||
}
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
package com.water.data_engine.service;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 数据治理服务测试
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DataGovernanceServiceTest {
|
||||
|
||||
@Mock
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Mock
|
||||
private com.water.data_engine.mapper.QualityRuleMapper qualityRuleMapper;
|
||||
|
||||
private DataGovernanceService governanceService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
governanceService = new DataGovernanceService(jdbcTemplate, qualityRuleMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("数据标准化-字段映射")
|
||||
void testStandardize() {
|
||||
Map<String, Object> raw = new HashMap<>();
|
||||
raw.put("flow", 12.5);
|
||||
raw.put("pressure", 0.35);
|
||||
raw.put("level", 100.0);
|
||||
raw.put("turbidity", 0.5);
|
||||
raw.put("ph", 7.2);
|
||||
raw.put("custom_field", "test");
|
||||
|
||||
Map<String, Object> result = governanceService.standardize(raw);
|
||||
|
||||
// 验证字段映射
|
||||
assertEquals(12.5, result.get("LL"));
|
||||
assertEquals(0.35, result.get("YL"));
|
||||
assertEquals(100.0, result.get("SW"));
|
||||
assertEquals(0.5, result.get("ZD"));
|
||||
assertEquals(7.2, result.get("PH"));
|
||||
assertEquals("test", result.get("custom_field"));
|
||||
assertEquals(true, result.get("_standardized"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("数据标准化-批量处理")
|
||||
void testBatchStandardize() {
|
||||
List<Map<String, Object>> rawDataList = List.of(
|
||||
Map.of("flow", 12.5, "pressure", 0.35),
|
||||
Map.of("level", 100.0, "turbidity", 0.5)
|
||||
);
|
||||
|
||||
List<Map<String, Object>> result = governanceService.batchStandardize(rawDataList);
|
||||
|
||||
assertEquals(2, result.size());
|
||||
assertEquals(12.5, result.get(0).get("LL"));
|
||||
assertEquals(100.0, result.get(1).get("SW"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("数据清洗-缺失值处理")
|
||||
void testClean_MissingValues() {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("LL", null);
|
||||
data.put("YL", "");
|
||||
data.put("SW", 100.0);
|
||||
|
||||
Map<String, Object> result = governanceService.clean(data);
|
||||
|
||||
assertEquals(-9999.0, result.get("LL"));
|
||||
assertEquals("MISSING", result.get("LL_flag"));
|
||||
assertEquals(-9999.0, result.get("YL"));
|
||||
assertEquals("MISSING", result.get("YL_flag"));
|
||||
assertEquals(100.0, result.get("SW"));
|
||||
assertEquals(true, result.get("_cleaned"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("数据清洗-异常值检测")
|
||||
void testClean_AnomalyDetection() {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("LL", -5.0); // 流量为负值
|
||||
data.put("YL", 150.0); // 压力超范围
|
||||
data.put("ZD", -10.0); // 浊度为负值
|
||||
|
||||
Map<String, Object> result = governanceService.clean(data);
|
||||
|
||||
assertEquals("ABNORMAL", result.get("LL_flag"));
|
||||
assertEquals("ABNORMAL", result.get("YL_flag"));
|
||||
assertEquals("ABNORMAL", result.get("ZD_flag"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("数据质量检查-完整性")
|
||||
void testQualityCheck_Completeness() {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("LL", 12.5);
|
||||
data.put("LL_flag", "MISSING");
|
||||
|
||||
Map<String, Object> result = governanceService.qualityCheck(data);
|
||||
|
||||
assertTrue((int) result.get("_quality_score") < 100);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> issues = (List<String>) result.get("_quality_issues");
|
||||
assertTrue(issues.stream().anyMatch(i -> i.contains("流量数据缺失")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("数据质量检查-异常值")
|
||||
void testQualityCheck_Anomaly() {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("LL", -5.0);
|
||||
data.put("LL_flag", "ABNORMAL");
|
||||
|
||||
Map<String, Object> result = governanceService.qualityCheck(data);
|
||||
|
||||
assertTrue((int) result.get("_quality_score") < 100);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> issues = (List<String>) result.get("_quality_issues");
|
||||
assertTrue(issues.stream().anyMatch(i -> i.contains("流量数据异常")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("完整数据管道")
|
||||
void testPipeline() {
|
||||
Map<String, Object> raw = new HashMap<>();
|
||||
raw.put("flow", 12.5);
|
||||
raw.put("pressure", 0.35);
|
||||
|
||||
Map<String, Object> result = governanceService.pipeline(raw);
|
||||
|
||||
// 验证经过标准化
|
||||
assertEquals(12.5, result.get("LL"));
|
||||
assertEquals(0.35, result.get("YL"));
|
||||
// 验证经过清洗
|
||||
assertEquals(true, result.get("_cleaned"));
|
||||
// 验证经过质控
|
||||
assertEquals(true, result.get("_quality_checked"));
|
||||
assertNotNull(result.get("_quality_score"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("批量数据管道")
|
||||
void testBatchPipeline() {
|
||||
List<Map<String, Object>> rawDataList = List.of(
|
||||
Map.of("flow", 12.5, "pressure", 0.35),
|
||||
Map.of("level", 100.0, "turbidity", 0.5)
|
||||
);
|
||||
|
||||
List<Map<String, Object>> result = governanceService.batchPipeline(rawDataList);
|
||||
|
||||
assertEquals(2, result.size());
|
||||
assertTrue(result.stream().allMatch(r -> Boolean.TRUE.equals(r.get("_quality_checked"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("pH值范围检查")
|
||||
void testQualityCheck_PHRange() {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("PH", 15.0); // 超出 0-14 范围
|
||||
|
||||
Map<String, Object> result = governanceService.qualityCheck(data);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> issues = (List<String>) result.get("_quality_issues");
|
||||
assertTrue(issues.stream().anyMatch(i -> i.contains("pH值")));
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package com.water.data_engine.service;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 数据接入服务测试
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DataIngestServiceTest {
|
||||
|
||||
@Mock
|
||||
private com.water.data_engine.mapper.DataSourceMapper dataSourceMapper;
|
||||
|
||||
@Mock
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Mock
|
||||
private DataCollectService collectService;
|
||||
|
||||
private DataIngestService ingestService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ingestService = new DataIngestService(dataSourceMapper, jdbcTemplate, collectService);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("通过API接入数据-数据源不存在")
|
||||
void testIngestViaApi_SourceNotFound() {
|
||||
when(dataSourceMapper.selectOne(any())).thenReturn(null);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> {
|
||||
ingestService.ingestViaApi("nonexistent", Map.of("key", "value"));
|
||||
});
|
||||
|
||||
assertTrue(ex.getMessage().contains("数据源不存在"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("通过API接入数据-数据源已禁用")
|
||||
void testIngestViaApi_SourceDisabled() {
|
||||
com.water.data_engine.entity.DataSource source = createMockSource();
|
||||
source.setStatus(0);
|
||||
when(dataSourceMapper.selectOne(any())).thenReturn(source);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> {
|
||||
ingestService.ingestViaApi("test_source", Map.of("key", "value"));
|
||||
});
|
||||
|
||||
assertTrue(ex.getMessage().contains("数据源已禁用"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("通过API接入数据-成功")
|
||||
void testIngestViaApi_Success() {
|
||||
com.water.data_engine.entity.DataSource source = createMockSource();
|
||||
when(dataSourceMapper.selectOne(any())).thenReturn(source);
|
||||
when(collectService.ingestRealtime(anyString(), anyString(), anyMap()))
|
||||
.thenReturn("iot.raw.generic");
|
||||
|
||||
String topic = ingestService.ingestViaApi("test_source", Map.of("LL", 12.5));
|
||||
|
||||
assertNotNull(topic);
|
||||
verify(dataSourceMapper).updateById(any(com.water.data_engine.entity.DataSource.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("批量API接入")
|
||||
void testBatchIngestViaApi() {
|
||||
com.water.data_engine.entity.DataSource source = createMockSource();
|
||||
when(dataSourceMapper.selectOne(any())).thenReturn(source);
|
||||
when(collectService.batchIngest(anyList())).thenReturn(3);
|
||||
|
||||
List<Map<String, Object>> dataList = List.of(
|
||||
Map.of("LL", 12.5),
|
||||
Map.of("LL", 15.3),
|
||||
Map.of("LL", 18.7)
|
||||
);
|
||||
|
||||
int count = ingestService.batchIngestViaApi("test_source", dataList);
|
||||
|
||||
assertEquals(3, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建数据源-编码重复")
|
||||
void testCreateDataSource_DuplicateCode() {
|
||||
when(dataSourceMapper.selectCount(any())).thenReturn(1L);
|
||||
|
||||
com.water.data_engine.entity.DataSource ds = createMockSource();
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> {
|
||||
ingestService.createDataSource(ds);
|
||||
});
|
||||
|
||||
assertTrue(ex.getMessage().contains("数据源编码已存在"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建数据源-成功")
|
||||
void testCreateDataSource_Success() {
|
||||
when(dataSourceMapper.selectCount(any())).thenReturn(0L);
|
||||
|
||||
com.water.data_engine.entity.DataSource ds = createMockSource();
|
||||
com.water.data_engine.entity.DataSource created = ingestService.createDataSource(ds);
|
||||
|
||||
assertNotNull(created);
|
||||
assertEquals(1, created.getStatus());
|
||||
verify(dataSourceMapper).insert(any(com.water.data_engine.entity.DataSource.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("查询数据源列表")
|
||||
void testListDataSources() {
|
||||
List<com.water.data_engine.entity.DataSource> mockList = List.of(
|
||||
createMockSource(),
|
||||
createMockSource()
|
||||
);
|
||||
when(dataSourceMapper.selectList(any())).thenReturn(mockList);
|
||||
|
||||
List<com.water.data_engine.entity.DataSource> result = ingestService.listDataSources(null);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(2, result.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("从数据库拉取数据-数据源不存在")
|
||||
void testPullFromDatabase_SourceNotFound() {
|
||||
when(dataSourceMapper.selectById(anyLong())).thenReturn(null);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> {
|
||||
ingestService.pullFromDatabase(999L, "SELECT 1", "target_table");
|
||||
});
|
||||
|
||||
assertTrue(ex.getMessage().contains("数据源不存在"));
|
||||
}
|
||||
|
||||
private com.water.data_engine.entity.DataSource createMockSource() {
|
||||
com.water.data_engine.entity.DataSource ds = new com.water.data_engine.entity.DataSource();
|
||||
ds.setId(1L);
|
||||
ds.setSourceCode("test_source");
|
||||
ds.setSourceName("测试数据源");
|
||||
ds.setSourceType("rest");
|
||||
ds.setCategory("iot");
|
||||
ds.setStatus(1);
|
||||
return ds;
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
package com.water.data_engine.service;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 数据集成服务测试
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DataIntegrationServiceTest {
|
||||
|
||||
@Mock
|
||||
private com.water.data_engine.mapper.SyncTaskMapper syncTaskMapper;
|
||||
|
||||
@Mock
|
||||
private com.water.data_engine.mapper.DataLineageMapper dataLineageMapper;
|
||||
|
||||
@Mock
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Mock
|
||||
private DataStorageService storageService;
|
||||
|
||||
private DataIntegrationService integrationService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
integrationService = new DataIntegrationService(
|
||||
syncTaskMapper, dataLineageMapper, jdbcTemplate, storageService
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建同步任务")
|
||||
void testCreateSyncTask() {
|
||||
com.water.data_engine.entity.SyncTask task = new com.water.data_engine.entity.SyncTask();
|
||||
task.setTaskName("测试同步任务");
|
||||
task.setSourceId(1L);
|
||||
task.setTargetStorageId(2L);
|
||||
task.setSyncType("full");
|
||||
|
||||
com.water.data_engine.entity.SyncTask created = integrationService.createSyncTask(task);
|
||||
|
||||
assertNotNull(created);
|
||||
assertEquals("pending", created.getStatus());
|
||||
verify(syncTaskMapper).insert(any(com.water.data_engine.entity.SyncTask.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("数据合并-多源整合")
|
||||
void testMergeData() {
|
||||
List<Map<String, Object>> mockResult = List.of(
|
||||
Map.of("id", 1, "device_sn", "FM001", "value", 12.5),
|
||||
Map.of("id", 2, "device_sn", "FM002", "value", 15.3)
|
||||
);
|
||||
|
||||
when(jdbcTemplate.queryForList(anyString())).thenReturn(mockResult);
|
||||
|
||||
List<Map<String, Object>> result = integrationService.mergeData(
|
||||
List.of("iot_telemetry", "manual_data"),
|
||||
"id",
|
||||
List.of("id", "device_sn", "value")
|
||||
);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(2, result.size());
|
||||
verify(jdbcTemplate).queryForList(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("数据聚合-按维度汇总")
|
||||
void testAggregateData() {
|
||||
List<Map<String, Object>> mockResult = List.of(
|
||||
Map.of("area", "精芒片区", "metric_key_avg", 12.5, "metric_key_count", 100),
|
||||
Map.of("area", "托里片区", "metric_key_avg", 15.3, "metric_key_count", 80)
|
||||
);
|
||||
|
||||
when(jdbcTemplate.queryForList(anyString())).thenReturn(mockResult);
|
||||
|
||||
List<Map<String, Object>> result = integrationService.aggregateData(
|
||||
"iot_telemetry",
|
||||
List.of("area"),
|
||||
Map.of("metric_key", "AVG", "metric_key", "COUNT")
|
||||
);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(2, result.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建数据血缘关系")
|
||||
void testCreateLineage() {
|
||||
com.water.data_engine.entity.DataLineage lineage = new com.water.data_engine.entity.DataLineage();
|
||||
lineage.setSourceTable("iot_telemetry");
|
||||
lineage.setTargetTable("iot_telemetry_hourly");
|
||||
lineage.setTransformType("aggregation");
|
||||
lineage.setDescription("小时聚合");
|
||||
|
||||
com.water.data_engine.entity.DataLineage created = integrationService.createLineage(lineage);
|
||||
|
||||
assertNotNull(created);
|
||||
assertNotNull(created.getCreatedAt());
|
||||
verify(dataLineageMapper).insert(any(com.water.data_engine.entity.DataLineage.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("查询血缘关系-上游")
|
||||
void testGetUpstreamLineage() {
|
||||
List<com.water.data_engine.entity.DataLineage> mockLineages = List.of(
|
||||
createMockLineage("raw_data", "iot_telemetry")
|
||||
);
|
||||
|
||||
when(dataLineageMapper.selectList(any())).thenReturn(mockLineages);
|
||||
|
||||
List<com.water.data_engine.entity.DataLineage> result =
|
||||
integrationService.getUpstreamLineage("iot_telemetry");
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("raw_data", result.get(0).getSourceTable());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("查询血缘关系-下游")
|
||||
void testGetDownstreamLineage() {
|
||||
List<com.water.data_engine.entity.DataLineage> mockLineages = List.of(
|
||||
createMockLineage("iot_telemetry", "iot_telemetry_hourly")
|
||||
);
|
||||
|
||||
when(dataLineageMapper.selectList(any())).thenReturn(mockLineages);
|
||||
|
||||
List<com.water.data_engine.entity.DataLineage> result =
|
||||
integrationService.getDownstreamLineage("iot_telemetry");
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("iot_telemetry_hourly", result.get(0).getTargetTable());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("查询完整血缘链路")
|
||||
void testGetFullLineage() {
|
||||
when(dataLineageMapper.selectList(any())).thenReturn(List.of());
|
||||
|
||||
Map<String, Object> result = integrationService.getFullLineage("iot_telemetry");
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("iot_telemetry", result.get("table"));
|
||||
assertNotNull(result.get("upstream"));
|
||||
assertNotNull(result.get("downstream"));
|
||||
}
|
||||
|
||||
private com.water.data_engine.entity.DataLineage createMockLineage(String source, String target) {
|
||||
com.water.data_engine.entity.DataLineage lineage = new com.water.data_engine.entity.DataLineage();
|
||||
lineage.setSourceTable(source);
|
||||
lineage.setTargetTable(target);
|
||||
lineage.setTransformType("direct");
|
||||
return lineage;
|
||||
}
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
package com.water.data_engine.service;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 数据存储服务测试
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DataStorageServiceTest {
|
||||
|
||||
@Mock
|
||||
private com.water.data_engine.mapper.StorageConfigMapper storageConfigMapper;
|
||||
|
||||
@Mock
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
private DataStorageService storageService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
storageService = new DataStorageService(storageConfigMapper, jdbcTemplate);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("写入TDengine-遥测数据")
|
||||
void testWriteToTDengine() {
|
||||
when(jdbcTemplate.update(anyString(), any(), any(), anyDouble())).thenReturn(1);
|
||||
|
||||
assertDoesNotThrow(() -> storageService.writeToTDengine(
|
||||
"FM001", "flow_meter", "精芒片区", "LL", 12.5
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("批量写入TDengine")
|
||||
void testBatchWriteToTDengine() {
|
||||
List<Map<String, Object>> dataList = List.of(
|
||||
Map.of("deviceSn", "FM001", "deviceType", "flow_meter", "area", "精芒片区",
|
||||
"metricKey", "LL", "value", 12.5),
|
||||
Map.of("deviceSn", "FM002", "deviceType", "flow_meter", "area", "托里片区",
|
||||
"metricKey", "LL", "value", 15.3)
|
||||
);
|
||||
|
||||
int count = storageService.batchWriteToTDengine(dataList);
|
||||
|
||||
// 由于内部异常被捕获,可能返回0
|
||||
assertTrue(count >= 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("插入PostgreSQL")
|
||||
void testInsertToPostgres() {
|
||||
when(jdbcTemplate.queryForObject(anyString(), eq(Long.class), any(), any()))
|
||||
.thenReturn(1L);
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("device_sn", "FM001");
|
||||
data.put("metric_key", "LL");
|
||||
|
||||
Long id = storageService.insertToPostgres("iot_telemetry", data);
|
||||
|
||||
assertNotNull(id);
|
||||
assertEquals(1L, id);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("批量插入PostgreSQL")
|
||||
void testBatchInsertToPostgres() {
|
||||
when(jdbcTemplate.queryForObject(anyString(), eq(Long.class), any(), any()))
|
||||
.thenReturn(1L);
|
||||
|
||||
List<Map<String, Object>> dataList = List.of(
|
||||
Map.of("device_sn", "FM001", "value", 12.5),
|
||||
Map.of("device_sn", "FM002", "value", 15.3)
|
||||
);
|
||||
|
||||
int count = storageService.batchInsertToPostgres("iot_telemetry", dataList);
|
||||
|
||||
assertEquals(2, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新PostgreSQL数据")
|
||||
void testUpdateInPostgres() {
|
||||
when(jdbcTemplate.update(anyString(), any(), anyLong())).thenReturn(1);
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("value", 20.0);
|
||||
data.put("status", "verified");
|
||||
|
||||
int count = storageService.updateInPostgres("iot_telemetry", 1L, data);
|
||||
|
||||
assertEquals(1, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("查询PostgreSQL数据")
|
||||
void testQueryFromPostgres() {
|
||||
List<Map<String, Object>> mockResult = List.of(
|
||||
Map.of("id", 1, "device_sn", "FM001", "value", 12.5)
|
||||
);
|
||||
|
||||
when(jdbcTemplate.queryForList(anyString(), any(), any(), anyInt(), anyInt()))
|
||||
.thenReturn(mockResult);
|
||||
|
||||
List<Map<String, Object>> result = storageService.queryFromPostgres(
|
||||
"iot_telemetry", Map.of("device_sn", "FM001"), 1, 10
|
||||
);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建存储配置")
|
||||
void testCreateStorageConfig() {
|
||||
com.water.data_engine.entity.StorageConfig config = new com.water.data_engine.entity.StorageConfig();
|
||||
config.setStorageName("主TDengine");
|
||||
config.setStorageType("tdengine");
|
||||
config.setConnectionUrl("jdbc:TAOS://localhost:6030");
|
||||
|
||||
com.water.data_engine.entity.StorageConfig created = storageService.createStorageConfig(config);
|
||||
|
||||
assertNotNull(created);
|
||||
assertEquals(1, created.getStatus());
|
||||
verify(storageConfigMapper).insert(any(com.water.data_engine.entity.StorageConfig.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("查询存储配置列表")
|
||||
void testListStorageConfigs() {
|
||||
List<com.water.data_engine.entity.StorageConfig> mockConfigs = List.of(
|
||||
createMockConfig("tdengine"),
|
||||
createMockConfig("postgresql")
|
||||
);
|
||||
|
||||
when(storageConfigMapper.selectList(any())).thenReturn(mockConfigs);
|
||||
|
||||
List<com.water.data_engine.entity.StorageConfig> result =
|
||||
storageService.listStorageConfigs(null);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(2, result.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("空列表插入测试")
|
||||
void testBatchInsertEmpty() {
|
||||
int count = storageService.batchInsertToPostgres("test_table", List.of());
|
||||
assertEquals(0, count);
|
||||
}
|
||||
|
||||
private com.water.data_engine.entity.StorageConfig createMockConfig(String type) {
|
||||
com.water.data_engine.entity.StorageConfig config = new com.water.data_engine.entity.StorageConfig();
|
||||
config.setStorageType(type);
|
||||
config.setStorageName("Test " + type);
|
||||
config.setStatus(1);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.water.iot.adapter;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
public class AdapterFactory {
|
||||
|
||||
private final Map<String, ProtocolAdapter> adapters = new ConcurrentHashMap<>();
|
||||
|
||||
public AdapterFactory(List<ProtocolAdapter> adapterList) {
|
||||
for (ProtocolAdapter a : adapterList) {
|
||||
adapters.put(a.protocol().toLowerCase(), a);
|
||||
}
|
||||
}
|
||||
|
||||
public ProtocolAdapter getAdapter(String protocol) {
|
||||
ProtocolAdapter adapter = adapters.get(protocol.toLowerCase());
|
||||
if (adapter == null) {
|
||||
throw new IllegalArgumentException("Unsupported protocol: " + protocol);
|
||||
}
|
||||
return adapter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.water.iot.adapter;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ModbusAdapter implements ProtocolAdapter {
|
||||
|
||||
@Override
|
||||
public String protocol() { return "Modbus"; }
|
||||
|
||||
@Override
|
||||
public Map<String, Object> parseTelemetry(String deviceSn, byte[] raw) {
|
||||
// Modbus RTU/TCP 数据帧解析
|
||||
Map<String, Object> telemetry = new HashMap<>();
|
||||
telemetry.put("deviceSn", deviceSn);
|
||||
telemetry.put("timestamp", System.currentTimeMillis());
|
||||
telemetry.put("raw_hex", bytesToHex(raw));
|
||||
// 简化: 按寄存器地址映射指标
|
||||
List<Map<String, Object>> metrics = new ArrayList<>();
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("key", "register_0");
|
||||
m.put("value", raw.length > 0 ? raw[0] & 0xFF : 0);
|
||||
metrics.add(m);
|
||||
telemetry.put("metrics", metrics);
|
||||
return telemetry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeCommand(Map<String, Object> command) {
|
||||
// Modbus 写寄存器指令
|
||||
return new byte[]{0x01, 0x06, 0x00, 0x00, 0x00, 0x01, 0x48, 0x0A};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean authenticate(String deviceSn, String credential) {
|
||||
return true;
|
||||
}
|
||||
|
||||
private String bytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) sb.append(String.format("%02X", b));
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.water.iot.adapter;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class MqttAdapter implements ProtocolAdapter {
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public String protocol() { return "MQTT"; }
|
||||
|
||||
@Override
|
||||
public Map<String, Object> parseTelemetry(String deviceSn, byte[] raw) {
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> data = mapper.readValue(raw, Map.class);
|
||||
Map<String, Object> telemetry = new HashMap<>();
|
||||
telemetry.put("deviceSn", deviceSn);
|
||||
telemetry.put("timestamp", System.currentTimeMillis());
|
||||
// 标准格式: {deviceSn, ts, metrics: [{key, value, unit}]}
|
||||
telemetry.put("metrics", data.getOrDefault("metrics", data));
|
||||
return telemetry;
|
||||
} catch (Exception e) {
|
||||
log.error("MQTT parse error: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeCommand(Map<String, Object> command) {
|
||||
try { return mapper.writeValueAsBytes(command); }
|
||||
catch (Exception e) { return null; }
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean authenticate(String deviceSn, String credential) {
|
||||
// TODO: 从设备表查询校验
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.water.iot.adapter;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 设备协议适配器接口 — 策略模式
|
||||
* 每种协议实现此接口,统一处理设备数据
|
||||
*/
|
||||
public interface ProtocolAdapter {
|
||||
|
||||
/** 支持的协议名 */
|
||||
String protocol();
|
||||
|
||||
/** 将原始数据转为标准遥测格式 */
|
||||
Map<String, Object> parseTelemetry(String deviceSn, byte[] raw);
|
||||
|
||||
/** 将指令转为协议特定的下发格式 */
|
||||
byte[] encodeCommand(Map<String, Object> command);
|
||||
|
||||
/** 设备鉴权 */
|
||||
boolean authenticate(String deviceSn, String credential);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.water.iot.service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DeviceShadowService {
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
private static final String SHADOW_PREFIX = "iot:shadow:";
|
||||
private static final long SHADOW_TTL_HOURS = 24;
|
||||
|
||||
/** 更新设备上报状态 */
|
||||
public void updateReported(String deviceSn, Map<String, Object> state) {
|
||||
try {
|
||||
String key = SHADOW_PREFIX + deviceSn;
|
||||
String json = mapper.writeValueAsString(state);
|
||||
redisTemplate.opsForHash().put(key, "reported", json);
|
||||
redisTemplate.expire(key, SHADOW_TTL_HOURS, TimeUnit.HOURS);
|
||||
// 同步更新数据库设备最后上报时间
|
||||
jdbcTemplate.update("UPDATE iot_device SET last_report_time = NOW() WHERE device_sn = ?", deviceSn);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Shadow update error: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取设备影子 */
|
||||
public Map<Object, Object> getShadow(String deviceSn) {
|
||||
return redisTemplate.opsForHash().entries(SHADOW_PREFIX + deviceSn);
|
||||
}
|
||||
|
||||
/** 更新期望状态(云端→设备) */
|
||||
public void updateDesired(String deviceSn, String desiredJson) {
|
||||
redisTemplate.opsForHash().put(SHADOW_PREFIX + deviceSn, "desired", desiredJson);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.water.iot.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OtaService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final DeviceShadowService shadowService;
|
||||
|
||||
/** 创建 OTA 升级任务 */
|
||||
public void createUpgrade(Long modelId, String firmwareVersion, String firmwareUrl, String checkMd5) {
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO iot_device_event (device_id, device_sn, event_type, event_data) " +
|
||||
"SELECT id, device_sn, 'ota', json_build_object('version',?, 'url',?, 'md5',?) " +
|
||||
"FROM iot_device WHERE model_id = ? AND status = 'online'",
|
||||
firmwareVersion, firmwareUrl, checkMd5, modelId);
|
||||
log.info("OTA task created for model {}: version={}", modelId, firmwareVersion);
|
||||
}
|
||||
|
||||
/** 设备查询是否有待升级固件 */
|
||||
public Map<String, Object> checkUpgrade(String deviceSn, String currentVersion) {
|
||||
return jdbcTemplate.queryForMap(
|
||||
"SELECT * FROM iot_device_event WHERE device_sn = ? AND event_type = 'ota' ORDER BY created_at DESC LIMIT 1",
|
||||
deviceSn);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.water.patrol.controller;
|
||||
|
||||
import com.water.common.core.result.R;
|
||||
import com.water.patrol.service.PatrolService;
|
||||
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.LocalDate;
|
||||
import java.util.*;
|
||||
|
||||
@Tag(name = "巡检管理")
|
||||
@RestController
|
||||
@RequestMapping("/patrol")
|
||||
@RequiredArgsConstructor
|
||||
public class PatrolController {
|
||||
|
||||
private final PatrolService patrolService;
|
||||
|
||||
// ---- 路线 ----
|
||||
@PostMapping("/route")
|
||||
public R<Map<String, Object>> createRoute(@RequestBody Map<String, Object> req) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> points = (List<Map<String, Object>>) req.getOrDefault("points", List.of());
|
||||
return R.ok(patrolService.createRoute(
|
||||
(String) req.get("routeName"), (String) req.get("area"),
|
||||
points, (int) req.getOrDefault("estimDuration", 60)));
|
||||
}
|
||||
|
||||
@GetMapping("/route/list")
|
||||
public R<List<Map<String, Object>>> routes(@RequestParam String area) {
|
||||
return R.ok(patrolService.getRoutes(area));
|
||||
}
|
||||
|
||||
// ---- 任务 ----
|
||||
@PostMapping("/task")
|
||||
public R<Map<String, Object>> createTask(@RequestBody Map<String, Object> req) {
|
||||
return R.ok(patrolService.createTask(
|
||||
Long.parseLong(String.valueOf(req.get("routeId"))),
|
||||
Long.parseLong(String.valueOf(req.get("assigneeId"))),
|
||||
(String) req.get("taskDate")));
|
||||
}
|
||||
|
||||
@GetMapping("/task/today")
|
||||
public R<List<Map<String, Object>>> todayTasks(@RequestParam Long userId) {
|
||||
return R.ok(patrolService.getTodayTasks(userId));
|
||||
}
|
||||
|
||||
@PutMapping("/task/{id}/start")
|
||||
public R<Map<String, Object>> startTask(@PathVariable Long id) {
|
||||
return R.ok(patrolService.startTask(id));
|
||||
}
|
||||
|
||||
@PutMapping("/task/{id}/complete")
|
||||
public R<Map<String, Object>> completeTask(@PathVariable Long id, @RequestParam double distance) {
|
||||
return R.ok(patrolService.completeTask(id, distance));
|
||||
}
|
||||
|
||||
// ---- 巡检记录 ----
|
||||
@PostMapping("/record")
|
||||
public R<Map<String, Object>> record(@RequestBody Map<String, Object> req) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> items = (List<Map<String, Object>>) req.getOrDefault("checkItems", List.of());
|
||||
return R.ok(patrolService.recordCheck(
|
||||
Long.parseLong(String.valueOf(req.get("taskId"))),
|
||||
(int) req.get("pointSeq"),
|
||||
req.get("deviceId") != null ? Long.parseLong(String.valueOf(req.get("deviceId"))) : null,
|
||||
items,
|
||||
((Number) req.get("lng")).doubleValue(),
|
||||
((Number) req.get("lat")).doubleValue()));
|
||||
}
|
||||
|
||||
@GetMapping("/record/list/{taskId}")
|
||||
public R<List<Map<String, Object>>> records(@PathVariable Long taskId) {
|
||||
return R.ok(patrolService.getTaskRecords(taskId));
|
||||
}
|
||||
|
||||
// ---- 问题上报 ----
|
||||
@PostMapping("/issue/report")
|
||||
public R<Map<String, Object>> reportIssue(@RequestBody Map<String, Object> req) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> photos = (List<String>) req.getOrDefault("photoUrls", List.of());
|
||||
return R.ok(patrolService.reportIssue(
|
||||
Long.parseLong(String.valueOf(req.get("taskId"))),
|
||||
req.get("deviceId") != null ? Long.parseLong(String.valueOf(req.get("deviceId"))) : null,
|
||||
(String) req.get("issueType"), (String) req.get("description"),
|
||||
photos,
|
||||
((Number) req.get("lng")).doubleValue(),
|
||||
((Number) req.get("lat")).doubleValue()));
|
||||
}
|
||||
|
||||
// ---- 统计 ----
|
||||
@GetMapping("/stats")
|
||||
public R<Map<String, Object>> stats(@RequestParam String area,
|
||||
@RequestParam String start,
|
||||
@RequestParam String end) {
|
||||
return R.ok(patrolService.getStats(area, LocalDate.parse(start), LocalDate.parse(end)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.water.patrol.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PatrolService {
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
|
||||
// ========== 路线管理 ==========
|
||||
public Map<String, Object> createRoute(String routeName, String area, List<Map<String, Object>> points, int estimDuration) {
|
||||
jdbc.update("INSERT INTO patrol_route (route_name, area, route_points, estim_duration) VALUES (?,?,?::jsonb,?)",
|
||||
routeName, area, points.toString(), estimDuration);
|
||||
return Map.of("routeName", routeName, "area", area, "points", points.size());
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getRoutes(String area) {
|
||||
return jdbc.queryForList("SELECT * FROM patrol_route WHERE area = ? AND status = 1", area);
|
||||
}
|
||||
|
||||
// ========== 任务管理 ==========
|
||||
public Map<String, Object> createTask(Long routeId, Long assigneeId, String taskDate) {
|
||||
jdbc.update(
|
||||
"INSERT INTO patrol_task (route_id, assignee_id, task_name, task_date, plan_start, plan_end, status) " +
|
||||
"SELECT ?, ?, route_name, ?, CAST(? AS TIMESTAMP), CAST(? AS TIMESTAMP) + (estim_duration || ' minutes')::INTERVAL, 'pending' " +
|
||||
"FROM patrol_route WHERE id = ?",
|
||||
routeId, assigneeId, taskDate, taskDate + " 09:00:00", taskDate + " 09:00:00", routeId);
|
||||
return Map.of("routeId", routeId, "assigneeId", assigneeId, "date", taskDate, "status", "created");
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getTodayTasks(Long userId) {
|
||||
return jdbc.queryForList(
|
||||
"SELECT pt.*, pr.route_name, pr.area FROM patrol_task pt " +
|
||||
"LEFT JOIN patrol_route pr ON pt.route_id = pr.id " +
|
||||
"WHERE pt.task_date = CURRENT_DATE AND pt.assignee_id = ? " +
|
||||
"ORDER BY pt.plan_start", userId);
|
||||
}
|
||||
|
||||
public Map<String, Object> startTask(Long taskId) {
|
||||
jdbc.update("UPDATE patrol_task SET status = 'in_progress', actual_start = NOW() WHERE id = ?", taskId);
|
||||
return Map.of("taskId", taskId, "status", "in_progress", "startedAt", new Date());
|
||||
}
|
||||
|
||||
public Map<String, Object> completeTask(Long taskId, double distance) {
|
||||
jdbc.update(
|
||||
"UPDATE patrol_task SET status = 'completed', actual_end = NOW(), distance = ? WHERE id = ?",
|
||||
distance, taskId);
|
||||
return Map.of("taskId", taskId, "status", "completed", "distance", distance);
|
||||
}
|
||||
|
||||
// ========== 巡检记录 ==========
|
||||
public Map<String, Object> recordCheck(Long taskId, int pointSeq, Long deviceId,
|
||||
List<Map<String, Object>> checkItems,
|
||||
double lng, double lat) {
|
||||
jdbc.update(
|
||||
"INSERT INTO patrol_record (task_id, point_seq, device_id, check_items, gps_lng, gps_lat, record_time) " +
|
||||
"VALUES (?,?,?,?::jsonb,?,?,NOW())",
|
||||
taskId, pointSeq, deviceId, checkItems.toString(), lng, lat);
|
||||
return Map.of("taskId", taskId, "pointSeq", pointSeq, "recorded", true);
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getTaskRecords(Long taskId) {
|
||||
return jdbc.queryForList(
|
||||
"SELECT * FROM patrol_record WHERE task_id = ? ORDER BY point_seq", taskId);
|
||||
}
|
||||
|
||||
// ========== 问题上报(巡检APP) ==========
|
||||
public Map<String, Object> reportIssue(Long taskId, Long deviceId, String issueType,
|
||||
String description, List<String> photoUrls,
|
||||
double lng, double lat) {
|
||||
// 自动创建工单
|
||||
jdbc.update(
|
||||
"INSERT INTO patrol_task (task_name, assignee_id, task_date, status) " +
|
||||
"SELECT CONCAT('问题处理: ', ?), assignee_id, CURRENT_DATE, 'pending' FROM patrol_task WHERE id = ?",
|
||||
issueType + ": " + description.substring(0, Math.min(description.length(), 50)), taskId);
|
||||
|
||||
log.info("Issue reported: type={} desc={}", issueType, description);
|
||||
return Map.of("reported", true, "issueType", issueType, "photos", photoUrls);
|
||||
}
|
||||
|
||||
// ========== 统计分析 ==========
|
||||
public Map<String, Object> getStats(String area, LocalDate start, LocalDate end) {
|
||||
Map<String, Object> stats = new LinkedHashMap<>();
|
||||
|
||||
// 任务执行率
|
||||
stats.put("completionRate", jdbc.queryForMap(
|
||||
"SELECT COUNT(*) as total, SUM(CASE WHEN status='completed' THEN 1 ELSE 0 END) as completed " +
|
||||
"FROM patrol_task WHERE task_date BETWEEN ? AND ?", start, end));
|
||||
|
||||
// 人员里程
|
||||
stats.put("personDistance", jdbc.queryForList(
|
||||
"SELECT u.real_name, SUM(pt.distance) as total_km " +
|
||||
"FROM patrol_task pt JOIN sys_user u ON pt.assignee_id = u.id " +
|
||||
"WHERE pt.task_date BETWEEN ? AND ? GROUP BY u.id, u.real_name", start, end));
|
||||
|
||||
// 巡检工作量
|
||||
stats.put("workload", jdbc.queryForList(
|
||||
"SELECT task_date, COUNT(*) as tasks, SUM(distance) as total_km " +
|
||||
"FROM patrol_task WHERE task_date BETWEEN ? AND ? GROUP BY task_date ORDER BY task_date",
|
||||
start, end));
|
||||
|
||||
// 问题分类统计
|
||||
stats.put("issueStats", jdbc.queryForList(
|
||||
"SELECT SUBSTRING(task_name FROM '^[^:]+') as issue_type, COUNT(*) as count " +
|
||||
"FROM patrol_task WHERE task_name LIKE '%问题处理:%' AND task_date BETWEEN ? AND ? GROUP BY 1",
|
||||
start, end));
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package com.water.production.controller;
|
||||
|
||||
import com.water.common.core.result.R;
|
||||
import com.water.production.service.*;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Tag(name = "供水生产管理")
|
||||
@RestController
|
||||
@RequestMapping("/production")
|
||||
@RequiredArgsConstructor
|
||||
public class ProductionController {
|
||||
|
||||
private final DashboardService dashboardService;
|
||||
private final WaterQualityService wqService;
|
||||
private final AlertEngine alertEngine;
|
||||
private final DispatchService dispatchService;
|
||||
private final DataCenterService dataCenterService;
|
||||
private final VideoService videoService;
|
||||
|
||||
// ---- 总览 ----
|
||||
@GetMapping("/overview")
|
||||
public R<Map<String, Object>> overview(@RequestParam(defaultValue = "一体化水厂") String area,
|
||||
@RequestParam(defaultValue = "admin") String roleType) {
|
||||
return R.ok(dashboardService.getOverview(area, roleType));
|
||||
}
|
||||
|
||||
// ---- 实时监测 ----
|
||||
@GetMapping("/monitor/realtime")
|
||||
public R<List<Map<String, Object>>> realtime(@RequestParam(required = false) String area,
|
||||
@RequestParam(required = false) String positionType,
|
||||
@RequestParam(required = false) String deviceType) {
|
||||
return R.ok(dashboardService.getRealtimeMonitoring(area, positionType, deviceType));
|
||||
}
|
||||
|
||||
@GetMapping("/monitor/cameras")
|
||||
public R<List<Map<String, Object>>> cameras(@RequestParam String area) {
|
||||
return R.ok(videoService.getCameras(area));
|
||||
}
|
||||
|
||||
// ---- 水质 ----
|
||||
@GetMapping("/quality/chemical/{station}")
|
||||
public R<Map<String, Object>> chemical(@PathVariable String station) {
|
||||
return R.ok(wqService.getChemicalMonitoring(station));
|
||||
}
|
||||
|
||||
@PostMapping("/quality/record")
|
||||
public R<String> addRecord(@RequestBody Map<String, Object> record) {
|
||||
wqService.addRecord(record);
|
||||
return R.ok("记录已保存");
|
||||
}
|
||||
|
||||
@GetMapping("/quality/ledger")
|
||||
public R<List<Map<String, Object>>> ledger(@RequestParam String area, @RequestParam String start, @RequestParam String end) {
|
||||
return R.ok(wqService.getQualityLedger(area, start, end));
|
||||
}
|
||||
|
||||
// ---- 报警 ----
|
||||
@GetMapping("/alert/list")
|
||||
public R<List<Map<String, Object>>> alerts(@RequestParam(required = false) String level,
|
||||
@RequestParam(required = false) String area,
|
||||
@RequestParam(defaultValue = "true") boolean active) {
|
||||
return R.ok(alertEngine.getAlerts(level, area, active));
|
||||
}
|
||||
|
||||
@PostMapping("/alert/{id}/confirm")
|
||||
public R<String> confirm(@PathVariable Long id, @RequestParam Long userId) {
|
||||
alertEngine.confirm(id, userId); return R.ok("已确认");
|
||||
}
|
||||
|
||||
@PostMapping("/alert/{id}/dispatch")
|
||||
public R<String> dispatch(@PathVariable Long id, @RequestParam Long assigneeId) {
|
||||
alertEngine.dispatch(id, assigneeId); return R.ok("已派单");
|
||||
}
|
||||
|
||||
// ---- 调度 ----
|
||||
@GetMapping("/dispatch/duty/today")
|
||||
public R<List<Map<String, Object>>> todayDuty(@RequestParam String area) {
|
||||
return R.ok(dispatchService.getTodayDuty(area));
|
||||
}
|
||||
|
||||
@PostMapping("/dispatch/command")
|
||||
public R<Map<String, Object>> createCommand(@RequestBody Map<String, Object> req) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Long> targetIds = (List<Long>) req.getOrDefault("targetIds", List.of());
|
||||
return R.ok(dispatchService.createCommand(
|
||||
(String) req.get("title"), (String) req.get("content"), (String) req.get("type"),
|
||||
(String) req.get("source"), (String) req.get("targetType"), targetIds));
|
||||
}
|
||||
|
||||
@PostMapping("/dispatch/command/{cmdNo}/issue")
|
||||
public R<Map<String, Object>> issueCommand(@PathVariable String cmdNo) {
|
||||
return R.ok(dispatchService.issueCommand(cmdNo));
|
||||
}
|
||||
|
||||
@PostMapping("/dispatch/emergency/pipe-burst")
|
||||
public R<Map<String, Object>> pipeBurst(@RequestBody Map<String, Object> req) {
|
||||
return R.ok(dispatchService.pipeBurstSimulation(
|
||||
((Number) req.get("lng")).doubleValue(),
|
||||
((Number) req.get("lat")).doubleValue(),
|
||||
(String) req.get("pipeDiameter")));
|
||||
}
|
||||
|
||||
// ---- 数据中心 ----
|
||||
@GetMapping("/data/history")
|
||||
public R<List<Map<String, Object>>> history(@RequestParam String dataType, @RequestParam String area,
|
||||
@RequestParam String start, @RequestParam String end) {
|
||||
return R.ok(dataCenterService.getHistoryData(dataType, area, start, end));
|
||||
}
|
||||
|
||||
@GetMapping("/data/report")
|
||||
public R<Map<String, Object>> report(@RequestParam String type, @RequestParam String period) {
|
||||
return R.ok(dataCenterService.generateReport(type, period));
|
||||
}
|
||||
|
||||
@PutMapping("/data/threshold/{ruleId}")
|
||||
public R<String> updateThreshold(@PathVariable Long ruleId, @RequestBody Map<String, Object> req) {
|
||||
dataCenterService.updateThreshold(ruleId,
|
||||
((Number) req.get("threshold")).doubleValue(),
|
||||
(String) req.get("condition"));
|
||||
return R.ok("阈值已更新");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AlertEngine {
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
private final Map<String, Long> lastAlertTime = new ConcurrentHashMap<>();
|
||||
|
||||
/** 检查指标是否触发报警 */
|
||||
public void checkMetric(String deviceSn, String metricKey, double value, String area) {
|
||||
List<Map<String, Object>> rules = jdbc.queryForList(
|
||||
"SELECT * FROM alert_rule WHERE metric_key = ? AND enabled = 1", metricKey);
|
||||
|
||||
for (Map<String, Object> rule : rules) {
|
||||
try {
|
||||
String condition = (String) rule.get("condition_expr");
|
||||
double threshold = ((Number) rule.get("threshold_value")).doubleValue();
|
||||
String level = (String) rule.get("alert_level");
|
||||
int debounce = ((Number) rule.get("debounce_sec")).intValue();
|
||||
|
||||
boolean triggered = false;
|
||||
if (condition.startsWith(">")) triggered = value > threshold;
|
||||
else if (condition.startsWith("<")) triggered = value < threshold;
|
||||
else if (condition.startsWith(">=")) triggered = value >= threshold;
|
||||
else if (condition.startsWith("<=")) triggered = value <= threshold;
|
||||
|
||||
if (!triggered) continue;
|
||||
|
||||
// 去重检查
|
||||
String dedupKey = deviceSn + ":" + metricKey + ":" + level;
|
||||
long now = Instant.now().getEpochSecond();
|
||||
Long last = lastAlertTime.get(dedupKey);
|
||||
if (last != null && (now - last) < debounce) continue;
|
||||
lastAlertTime.put(dedupKey, now);
|
||||
|
||||
// 创建报警事件
|
||||
Long ruleId = ((Number) rule.get("id")).longValue();
|
||||
String message = String.format("%s %s: %.2f %s 阈值 %.2f",
|
||||
deviceSn, metricKey, value, condition, threshold);
|
||||
|
||||
jdbc.update(
|
||||
"INSERT INTO alert_event (rule_id, device_sn, area, metric_key, metric_value, threshold_value, alert_level, title, message) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?,?)",
|
||||
ruleId, deviceSn, area, metricKey, value, String.valueOf(threshold), level,
|
||||
"[" + level + "] " + metricKey + "异常", message);
|
||||
|
||||
log.info("Alert triggered: {} level={}", dedupKey, level);
|
||||
} catch (Exception e) {
|
||||
log.error("CheckMetric error: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 确认报警 */
|
||||
public void confirm(Long alertId, Long userId) {
|
||||
jdbc.update("UPDATE alert_event SET confirmed_by = ?, confirmed_at = NOW() WHERE id = ?", userId, alertId);
|
||||
}
|
||||
|
||||
/** 派单 */
|
||||
public void dispatch(Long alertId, Long assigneeId) {
|
||||
jdbc.update("UPDATE alert_event SET dispatched = 1 WHERE id = ?", alertId);
|
||||
jdbc.update("INSERT INTO patrol_task (task_name, assignee_id, task_date, status) " +
|
||||
"SELECT CONCAT('报警处理: ', title), ?, CURRENT_DATE, 'pending' FROM alert_event WHERE id = ?",
|
||||
assigneeId, alertId);
|
||||
}
|
||||
|
||||
/** 报警列表 */
|
||||
public List<Map<String, Object>> getAlerts(String level, String area, boolean onlyActive) {
|
||||
StringBuilder sql = new StringBuilder("SELECT * FROM alert_event WHERE 1=1");
|
||||
if (level != null) sql.append(" AND alert_level = '").append(level).append("'");
|
||||
if (area != null) sql.append(" AND area = '").append(area).append("'");
|
||||
if (onlyActive) sql.append(" AND resolved_at IS NULL");
|
||||
sql.append(" ORDER BY created_at DESC LIMIT 100");
|
||||
return jdbc.queryForList(sql.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DashboardService {
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
|
||||
/** 获取供水总览数据(按角色自动定位区域) */
|
||||
public Map<String, Object> getOverview(String area, String roleType) {
|
||||
Map<String, Object> overview = new LinkedHashMap<>();
|
||||
|
||||
// 今日进出水量(从时序库聚合)
|
||||
try {
|
||||
Map<String, Object> flow = jdbc.queryForMap(
|
||||
"SELECT COALESCE(SUM(CASE WHEN metric_key='inflow' THEN metric_value ELSE 0 END),0) AS inflow, " +
|
||||
"COALESCE(SUM(CASE WHEN metric_key='outflow' THEN metric_value ELSE 0 END),0) AS outflow " +
|
||||
"FROM iot_telemetry WHERE ts >= CURRENT_DATE AND area = ?", area);
|
||||
overview.put("todayInflow", flow.get("inflow"));
|
||||
overview.put("todayOutflow", flow.get("outflow"));
|
||||
} catch (Exception e) { overview.put("todayInflow", 0); overview.put("todayOutflow", 0); }
|
||||
|
||||
// 昨日供水量
|
||||
overview.put("yesterdaySupply", jdbc.queryForObject(
|
||||
"SELECT COALESCE(SUM(consumption),0) FROM rev_reading WHERE reading_date = CURRENT_DATE - 1", Double.class));
|
||||
|
||||
// 实时报警数
|
||||
overview.put("activeAlerts", jdbc.queryForObject(
|
||||
"SELECT COUNT(*) FROM alert_event WHERE confirmed_by IS NULL AND created_at >= CURRENT_DATE", Long.class));
|
||||
|
||||
// 设备运行概况
|
||||
overview.put("deviceStats", jdbc.queryForList(
|
||||
"SELECT status, COUNT(*) as count FROM iot_device WHERE area = ? GROUP BY status", area));
|
||||
|
||||
// 能耗药耗
|
||||
overview.put("energy", Map.of("power_kwh", 1250.5, "pump_runtime_h", 18.2));
|
||||
overview.put("chemical", Map.of("coagulant_kg", 45.0, "disinfectant_kg", 12.5));
|
||||
|
||||
overview.put("area", area);
|
||||
overview.put("timestamp", System.currentTimeMillis());
|
||||
return overview;
|
||||
}
|
||||
|
||||
/** 实时监测列表(多维度筛选) */
|
||||
public List<Map<String, Object>> getRealtimeMonitoring(String area, String positionType, String deviceType) {
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"SELECT id, device_sn, device_name, device_type, position_type, area, status, last_report_time," +
|
||||
"ST_X(geom) as lng, ST_Y(geom) as lat FROM iot_device WHERE 1=1");
|
||||
if (area != null) sql.append(" AND area = '").append(area).append("'");
|
||||
if (positionType != null) sql.append(" AND position_type = '").append(positionType).append("'");
|
||||
if (deviceType != null) sql.append(" AND device_type = '").append(deviceType).append("'");
|
||||
sql.append(" ORDER BY last_report_time DESC LIMIT 100");
|
||||
return jdbc.queryForList(sql.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DataCenterService {
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
|
||||
/** 历史数据查看(多类型) */
|
||||
public List<Map<String, Object>> getHistoryData(String dataType, String area, String startTime, String endTime) {
|
||||
String table = switch (dataType) {
|
||||
case "water_flow" -> "rev_reading";
|
||||
case "water_quality" -> "water_quality_record";
|
||||
case "alerts" -> "alert_event";
|
||||
default -> "iot_telemetry";
|
||||
};
|
||||
return jdbc.queryForList(
|
||||
"SELECT * FROM " + table + " WHERE (area = ? OR ? IS NULL) AND created_at BETWEEN ? AND ? LIMIT 500",
|
||||
area, area, startTime, endTime);
|
||||
}
|
||||
|
||||
/** 报表生成 */
|
||||
public Map<String, Object> generateReport(String reportType, String period) {
|
||||
Map<String, Object> report = new LinkedHashMap<>();
|
||||
report.put("reportType", reportType);
|
||||
report.put("period", period);
|
||||
report.put("generatedAt", new Date());
|
||||
|
||||
switch (reportType) {
|
||||
case "water_volume" -> report.put("data", jdbc.queryForList(
|
||||
"SELECT area, SUM(consumption) as total FROM rev_reading WHERE reading_period = ? GROUP BY area", period));
|
||||
case "water_quality" -> report.put("data", jdbc.queryForList(
|
||||
"SELECT area, AVG(turbidity) as avg_turbidity, AVG(ph) as avg_ph, " +
|
||||
"AVG(residual_chlorine) as avg_cl, COUNT(*) as tests, " +
|
||||
"SUM(CASE WHEN is_qualified=1 THEN 1 ELSE 0 END)*100.0/NULLIF(COUNT(*),0) as pass_rate " +
|
||||
"FROM water_quality_record WHERE to_char(test_date,'YYYY-MM') = ? GROUP BY area", period));
|
||||
case "alert" -> report.put("data", jdbc.queryForList(
|
||||
"SELECT alert_level, area, COUNT(*) as count FROM alert_event WHERE to_char(created_at,'YYYY-MM') = ? GROUP BY alert_level, area", period));
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
/** 阈值管理 */
|
||||
public List<Map<String, Object>> getThresholds() {
|
||||
return jdbc.queryForList("SELECT * FROM alert_rule WHERE enabled = 1 ORDER BY device_type, metric_key");
|
||||
}
|
||||
|
||||
public void updateThreshold(Long ruleId, double newThreshold, String newCondition) {
|
||||
jdbc.update("UPDATE alert_rule SET threshold_value = ?, condition_expr = ? WHERE id = ?",
|
||||
newThreshold, newCondition, ruleId);
|
||||
}
|
||||
|
||||
/** 信息发布 */
|
||||
public void publishInfo(String type, String title, String content) {
|
||||
jdbc.update(
|
||||
"INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value) " +
|
||||
"SELECT id, ?, ? FROM sys_dict_type WHERE dict_key = ?",
|
||||
title, content, "info_release_" + type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DispatchService {
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
|
||||
// ========== 值班管理 ==========
|
||||
public List<Map<String, Object>> getTodayDuty(String area) {
|
||||
return jdbc.queryForList(
|
||||
"SELECT dr.*, u.real_name, u.phone, ds.shift_type " +
|
||||
"FROM duty_record dr JOIN sys_user u ON dr.user_id = u.id " +
|
||||
"JOIN duty_schedule ds ON dr.schedule_id = ds.id " +
|
||||
"WHERE dr.duty_date = CURRENT_DATE AND ds.status = 1");
|
||||
}
|
||||
|
||||
public Map<String, Object> startDuty(Long userId) {
|
||||
jdbc.update("UPDATE duty_record SET status = 'on_duty', on_duty_at = NOW() WHERE user_id = ? AND duty_date = CURRENT_DATE", userId);
|
||||
return Map.of("status", "on_duty", "startedAt", new Date());
|
||||
}
|
||||
|
||||
public Map<String, Object> endDuty(Long userId, String handoverRemark) {
|
||||
jdbc.update(
|
||||
"UPDATE duty_record SET status = 'off_duty', off_duty_at = NOW(), handover_remark = ? WHERE user_id = ? AND duty_date = CURRENT_DATE",
|
||||
handoverRemark, userId);
|
||||
return Map.of("status", "off_duty");
|
||||
}
|
||||
|
||||
// ========== 调度指令 ==========
|
||||
public Map<String, Object> createCommand(String title, String content, String type, String source, String targetType, List<Long> targetIds) {
|
||||
String cmdNo = "CMD-" + System.currentTimeMillis();
|
||||
jdbc.update(
|
||||
"INSERT INTO dispatch_command (command_no, command_type, command_title, command_content, source, target_type, target_ids, status) " +
|
||||
"VALUES (?,?,?,?,?,?,?::jsonb,'draft')",
|
||||
cmdNo, type, title, content, source, targetType, targetIds.toString());
|
||||
log.info("Command created: {} type={}", cmdNo, type);
|
||||
return Map.of("commandNo", cmdNo, "status", "draft");
|
||||
}
|
||||
|
||||
public Map<String, Object> issueCommand(String cmdNo) {
|
||||
jdbc.update("UPDATE dispatch_command SET status = 'issued', issued_at = NOW() WHERE command_no = ?", cmdNo);
|
||||
// 记录日志
|
||||
jdbc.update("INSERT INTO dispatch_log (command_id, action) SELECT id, 'issue' FROM dispatch_command WHERE command_no = ?", cmdNo);
|
||||
return Map.of("commandNo", cmdNo, "status", "issued");
|
||||
}
|
||||
|
||||
public Map<String, Object> trackCommand(String cmdNo) {
|
||||
return jdbc.queryForMap("SELECT * FROM dispatch_command WHERE command_no = ?", cmdNo);
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getCommandLog(String cmdNo) {
|
||||
return jdbc.queryForList(
|
||||
"SELECT dl.* FROM dispatch_log dl JOIN dispatch_command dc ON dl.command_id = dc.id WHERE dc.command_no = ? ORDER BY dl.created_at",
|
||||
cmdNo);
|
||||
}
|
||||
|
||||
// ========== 应急调度推演 ==========
|
||||
public Map<String, Object> pipeBurstSimulation(double lng, double lat, String pipeDiameter) {
|
||||
// 爆管模拟:影响区域分析
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("scenario", "爆管");
|
||||
result.put("location", Map.of("lng", lng, "lat", lat));
|
||||
result.put("pipeDiameter", pipeDiameter);
|
||||
result.put("affectedArea", "半径500m");
|
||||
result.put("affectedCustomers", 230);
|
||||
result.put("suggestedActions", List.of(
|
||||
"关闭上游阀门 V-001, V-002",
|
||||
"启动应急供水方案 B",
|
||||
"通知受影响用户(短信+公告)",
|
||||
"调度抢修队出发"
|
||||
));
|
||||
result.put("estimatedRecoveryHours", 4);
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<String, Object> waterQualityIncident(String area, String pollutant) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("scenario", "水质异常");
|
||||
result.put("area", area);
|
||||
result.put("pollutant", pollutant);
|
||||
result.put("suggestedActions", List.of(
|
||||
"立即停止该片区供水",
|
||||
"启动备用水源",
|
||||
"水质采样送检",
|
||||
"向下游水厂发出预警"
|
||||
));
|
||||
result.put("riskLevel", "critical");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class VideoService {
|
||||
|
||||
/** 获取所有视频监控点位 */
|
||||
public List<Map<String, Object>> getCameras(String area) {
|
||||
// Mock: 返回预设视频点位
|
||||
List<Map<String, Object>> cameras = new ArrayList<>();
|
||||
cameras.add(Map.of("id", 1, "name", "一体化水厂-沉淀池", "rtsp", "rtsp://192.168.1.100/stream1", "area", "一体化水厂", "status", "online"));
|
||||
cameras.add(Map.of("id", 2, "name", "查村调压站-入口", "rtsp", "rtsp://192.168.1.101/stream1", "area", "八家户片区", "status", "online"));
|
||||
cameras.add(Map.of("id", 3, "name", "精芒片区-管网节点1", "rtsp", "rtsp://192.168.1.102/stream1", "area", "精芒片区", "status", "online"));
|
||||
return cameras;
|
||||
}
|
||||
|
||||
/** AI 人员闯入检测 */
|
||||
public Map<String, Object> detectIntrusion(String cameraId, byte[] frameData) {
|
||||
// Mock: YOLOv8 推理 (实际调用模型服务)
|
||||
double probability = Math.random();
|
||||
boolean intruder = probability > 0.85;
|
||||
if (intruder) {
|
||||
log.warn("Intrusion detected on camera {} (prob={})", cameraId, String.format("%.2f", probability));
|
||||
return Map.of("cameraId", cameraId, "intruder", true, "confidence", probability,
|
||||
"alert", "检测到人员闯入", "timestamp", System.currentTimeMillis());
|
||||
}
|
||||
return Map.of("cameraId", cameraId, "intruder", false, "confidence", probability);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WaterQualityService {
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
|
||||
/** 药剂投加监控:全工艺参数 */
|
||||
public Map<String, Object> getChemicalMonitoring(String stationName) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("station", stationName);
|
||||
// 混凝
|
||||
data.put("inflowTurbidity", Map.of("value", 12.5, "unit", "NTU", "status", "normal"));
|
||||
data.put("coagulantRate", Map.of("value", 25.3, "unit", "mg/L", "status", "normal"));
|
||||
// 沉淀
|
||||
data.put("sedimentationLevel", Map.of("value", 3.2, "unit", "m", "status", "normal"));
|
||||
data.put("sedimentationTurbidity", Map.of("value", 3.1, "unit", "NTU", "status", "normal"));
|
||||
// 过滤
|
||||
data.put("filterLevel", Map.of("value", 2.5, "unit", "m", "status", "normal"));
|
||||
data.put("filterHeadLoss", Map.of("value", 0.8, "unit", "m", "status", "normal"));
|
||||
// 消毒
|
||||
data.put("disinfectantRate", Map.of("value", 2.0, "unit", "mg/L", "status", "normal"));
|
||||
data.put("residualChlorine", Map.of("value", 0.5, "unit", "mg/L", "status", "normal"));
|
||||
data.put("outflowTurbidity", Map.of("value", 0.3, "unit", "NTU", "status", "normal"));
|
||||
return data;
|
||||
}
|
||||
|
||||
/** 人工检测点位规划 */
|
||||
public List<Map<String, Object>> getManualTestPoints(String area) {
|
||||
return jdbc.queryForList(
|
||||
"SELECT DISTINCT test_point, point_type, lng, lat FROM water_quality_record WHERE area = ? AND test_type = 'manual' ORDER BY test_point",
|
||||
area);
|
||||
}
|
||||
|
||||
/** 水质数据台账 */
|
||||
public List<Map<String, Object>> getQualityLedger(String area, String startDate, String endDate) {
|
||||
return jdbc.queryForList(
|
||||
"SELECT * FROM water_quality_record WHERE area = ? AND test_date BETWEEN ? AND ? ORDER BY test_date DESC LIMIT 200",
|
||||
area, startDate, endDate);
|
||||
}
|
||||
|
||||
/** 添加检测记录 */
|
||||
public void addRecord(Map<String, Object> record) {
|
||||
jdbc.update(
|
||||
"INSERT INTO water_quality_record (test_type, test_point, point_type, area, test_date, test_time, tester, turbidity, ph, residual_chlorine, is_qualified) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?)",
|
||||
record.get("testType"), record.get("testPoint"), record.get("pointType"), record.get("area"),
|
||||
record.get("testDate"), record.get("testTime"), record.get("tester"),
|
||||
record.get("turbidity"), record.get("ph"), record.get("residualChlorine"),
|
||||
record.get("isQualified"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.water.revenue.controller;
|
||||
|
||||
import com.water.common.core.result.R;
|
||||
import com.water.revenue.service.RemoteReadingService;
|
||||
import com.water.revenue.service.WorkOrderService;
|
||||
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("/revenue/ops")
|
||||
@RequiredArgsConstructor
|
||||
public class MeterWorkController {
|
||||
|
||||
private final RemoteReadingService rrService;
|
||||
private final WorkOrderService woService;
|
||||
|
||||
// ---- 远传集抄 ----
|
||||
@PostMapping("/reading/batch/{area}")
|
||||
public R<Map<String, Object>> batchRead(@PathVariable String area) {
|
||||
return R.ok(rrService.batchRead(area));
|
||||
}
|
||||
|
||||
@GetMapping("/dma/analysis")
|
||||
public R<Map<String, Object>> dmaAnalysis(@RequestParam String area, @RequestParam String period) {
|
||||
return R.ok(rrService.dmaAnalysis(area, period));
|
||||
}
|
||||
|
||||
@GetMapping("/meter/large")
|
||||
public R<List<Map<String, Object>>> largeMeters() {
|
||||
return R.ok(rrService.largeMeterMonitor());
|
||||
}
|
||||
|
||||
// ---- 工单 ----
|
||||
@PostMapping("/work-order")
|
||||
public R<Map<String, Object>> createWO(@RequestBody Map<String, Object> req) {
|
||||
return R.ok(woService.create(
|
||||
(String) req.get("title"), (String) req.get("type"), (String) req.get("priority"),
|
||||
1L, "当前用户", (String) req.get("description"), (String) req.get("area")));
|
||||
}
|
||||
|
||||
@PutMapping("/work-order/assign")
|
||||
public R<Map<String, Object>> assign(@RequestBody Map<String, Object> req) {
|
||||
return R.ok(woService.assign(
|
||||
(String) req.get("woNo"), Long.parseLong(String.valueOf(req.get("assigneeId"))),
|
||||
(String) req.get("assigneeName")));
|
||||
}
|
||||
|
||||
@PutMapping("/work-order/complete")
|
||||
public R<Map<String, Object>> complete(@RequestBody Map<String, Object> req) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> photos = (List<String>) req.getOrDefault("photos", List.of());
|
||||
return R.ok(woService.complete((String) req.get("woNo"), (String) req.get("result"), photos));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.water.revenue.controller;
|
||||
|
||||
import com.water.common.core.result.R;
|
||||
import com.water.revenue.service.*;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
|
||||
@Tag(name = "营业收费")
|
||||
@RestController
|
||||
@RequestMapping("/revenue")
|
||||
@RequiredArgsConstructor
|
||||
public class RevenueController {
|
||||
|
||||
private final RevenueBaseService baseService;
|
||||
private final InstallService installService;
|
||||
private final BillingService billingService;
|
||||
private final MeterService meterService;
|
||||
|
||||
// ---- 营收平台 ----
|
||||
@PostMapping("/auth/sso")
|
||||
public R<Map<String, Object>> ssoLogin(@RequestBody Map<String, String> req) {
|
||||
return R.ok(baseService.ssoLogin(req.get("username"), req.get("password"), req.get("appType")));
|
||||
}
|
||||
|
||||
// ---- 报装管理 ----
|
||||
@PostMapping("/install/pre-apply")
|
||||
public R<Map<String, Object>> preApply(@RequestBody Map<String, String> req) {
|
||||
return R.ok(installService.preApply(req.get("name"), req.get("phone"),
|
||||
req.get("area"), req.get("address"), req.get("customerType"), req.get("caliber")));
|
||||
}
|
||||
|
||||
@GetMapping("/install/progress/{appNo}")
|
||||
public R<Map<String, Object>> progress(@PathVariable String appNo) {
|
||||
return R.ok(installService.getProgress(appNo));
|
||||
}
|
||||
|
||||
// ---- 营业收费 ----
|
||||
@PostMapping("/billing/generate")
|
||||
public R<Map<String, Object>> generateBill(@RequestParam Long readingId) {
|
||||
return R.ok(billingService.generateBill(readingId));
|
||||
}
|
||||
|
||||
@PostMapping("/billing/pay")
|
||||
public R<Map<String, Object>> pay(@RequestBody Map<String, Object> req) {
|
||||
return R.ok(billingService.pay(
|
||||
Long.parseLong(String.valueOf(req.get("billId"))),
|
||||
(String) req.get("payMethod"),
|
||||
(String) req.get("payChannel"),
|
||||
new BigDecimal(String.valueOf(req.get("amount")))));
|
||||
}
|
||||
|
||||
// ---- 表务管理 ----
|
||||
@PostMapping("/meter/stock-in")
|
||||
public R<String> stockIn(@RequestBody Map<String, String> req) {
|
||||
meterService.stockIn(req.get("meterNo"), req.get("caliber"),
|
||||
req.get("meterType"), req.get("manufacturer"), Integer.parseInt(req.get("quantity")));
|
||||
return R.ok("入库成功");
|
||||
}
|
||||
|
||||
@PostMapping("/meter/replace")
|
||||
public R<String> replace(@RequestBody Map<String, Object> req) {
|
||||
meterService.replace(
|
||||
Long.parseLong(String.valueOf(req.get("oldMeterId"))),
|
||||
Long.parseLong(String.valueOf(req.get("newMeterId"))),
|
||||
new BigDecimal(String.valueOf(req.get("oldReading"))),
|
||||
(String) req.get("remark"));
|
||||
return R.ok("换表成功");
|
||||
}
|
||||
|
||||
@GetMapping("/meter/lifecycle/{meterId}")
|
||||
public R<List<Map<String, Object>>> lifecycle(@PathVariable Long meterId) {
|
||||
return R.ok(meterService.getLifecycle(meterId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.water.revenue.controller;
|
||||
|
||||
import com.water.common.core.result.R;
|
||||
import com.water.revenue.service.CustomerServiceCenter;
|
||||
import com.water.revenue.service.WechatService;
|
||||
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.math.BigDecimal;
|
||||
import java.util.*;
|
||||
|
||||
@Tag(name = "客服热线 & 微信网厅")
|
||||
@RestController
|
||||
@RequestMapping("/revenue/customer")
|
||||
@RequiredArgsConstructor
|
||||
public class WechatController {
|
||||
|
||||
private final CustomerServiceCenter csc;
|
||||
private final WechatService wechatService;
|
||||
|
||||
// ---- 客服 ----
|
||||
@GetMapping("/bills/query")
|
||||
public R<List<Map<String, Object>>> queryBills(@RequestParam String phoneOrNo) {
|
||||
return R.ok(csc.queryBills(phoneOrNo));
|
||||
}
|
||||
|
||||
@GetMapping("/knowledge/search")
|
||||
public R<List<Map<String, Object>>> searchKnowledge(@RequestParam String keyword) {
|
||||
return R.ok(csc.searchKnowledge(keyword));
|
||||
}
|
||||
|
||||
@GetMapping("/notices/{type}")
|
||||
public R<List<Map<String, Object>>> notices(@PathVariable String type) {
|
||||
return R.ok(csc.getNotices(type));
|
||||
}
|
||||
|
||||
@GetMapping("/kpi")
|
||||
public R<Map<String, Object>> kpi() { return R.ok(csc.getKpi()); }
|
||||
|
||||
// ---- 微信网厅 ----
|
||||
@PostMapping("/wechat/bind")
|
||||
public R<Map<String, Object>> bind(@RequestBody Map<String, String> req) {
|
||||
return R.ok(wechatService.bindUser(req.get("openId"), req.get("customerNo"), req.get("phone")));
|
||||
}
|
||||
|
||||
@PostMapping("/wechat/pay/prepay")
|
||||
public R<Map<String, Object>> prepay(@RequestBody Map<String, String> req) {
|
||||
return R.ok(wechatService.wechatPayPrepay(
|
||||
req.get("customerNo"), req.get("billPeriod"),
|
||||
new BigDecimal(req.get("amount"))));
|
||||
}
|
||||
|
||||
@GetMapping("/wechat/ai-answer")
|
||||
public R<String> aiAnswer(@RequestParam String question) {
|
||||
return R.ok(wechatService.aiAnswer(question));
|
||||
}
|
||||
|
||||
@PostMapping("/wechat/notice/publish")
|
||||
public R<String> publishNotice(@RequestBody Map<String, String> req) {
|
||||
wechatService.publishNotice(req.get("type"), req.get("title"), req.get("content"));
|
||||
return R.ok("公告已发布");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.water.revenue.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.YearMonth;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BillingService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/** 根据抄表记录生成账单 */
|
||||
@Transactional
|
||||
public Map<String, Object> generateBill(Long readingId) {
|
||||
Map<String, Object> reading = jdbcTemplate.queryForMap(
|
||||
"SELECT r.*, m.customer_id, m.caliber, c.customer_type FROM rev_reading r " +
|
||||
"JOIN rev_meter m ON r.meter_id = m.id " +
|
||||
"JOIN rev_customer c ON m.customer_id = c.id " +
|
||||
"WHERE r.id = ?", readingId);
|
||||
|
||||
BigDecimal consumption = (BigDecimal) reading.get("consumption");
|
||||
String customerType = (String) reading.get("customer_type");
|
||||
String period = (String) reading.get("reading_period");
|
||||
|
||||
// 查询阶梯水价
|
||||
List<Map<String, Object>> prices = jdbcTemplate.queryForList(
|
||||
"SELECT * FROM rev_water_price WHERE customer_type = ? AND effective_date <= CURRENT_DATE ORDER BY tier_no",
|
||||
customerType);
|
||||
|
||||
BigDecimal waterFee = BigDecimal.ZERO;
|
||||
BigDecimal remaining = consumption;
|
||||
|
||||
for (Map<String, Object> p : prices) {
|
||||
BigDecimal rangeEnd = p.get("range_end") != null ? (BigDecimal) p.get("range_end") : BigDecimal.valueOf(99999);
|
||||
BigDecimal price = (BigDecimal) p.get("water_price");
|
||||
BigDecimal tierUsage = remaining.min(rangeEnd);
|
||||
waterFee = waterFee.add(tierUsage.multiply(price));
|
||||
remaining = remaining.subtract(tierUsage);
|
||||
if (remaining.compareTo(BigDecimal.ZERO) <= 0) break;
|
||||
}
|
||||
|
||||
// 污水处理费(用水量的80%)
|
||||
BigDecimal sewageFee = consumption.multiply(((BigDecimal) prices.get(0).getOrDefault("sewage_price", BigDecimal.ZERO)))
|
||||
.multiply(BigDecimal.valueOf(0.8));
|
||||
|
||||
BigDecimal totalFee = waterFee.add(sewageFee);
|
||||
String billNo = "BILL-" + System.currentTimeMillis();
|
||||
Date dueDate = java.sql.Date.valueOf(LocalDate.now().plusDays(30));
|
||||
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO rev_bill (bill_no, customer_id, meter_id, reading_id, bill_period, prev_reading, curr_reading, consumption, water_fee, sewage_fee, total_fee, status, due_date) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
billNo, reading.get("customer_id"), reading.get("meter_id"), readingId,
|
||||
period, reading.get("prev_reading"), reading.get("curr_reading"),
|
||||
consumption, waterFee, sewageFee, totalFee, "pending", dueDate);
|
||||
|
||||
log.info("Bill generated: {} total={}", billNo, totalFee);
|
||||
return Map.of("billNo", billNo, "totalFee", totalFee, "waterFee", waterFee, "sewageFee", sewageFee);
|
||||
}
|
||||
|
||||
/** 缴费 */
|
||||
@Transactional
|
||||
public Map<String, Object> pay(long billId, String payMethod, String payChannel, BigDecimal amount) {
|
||||
String paymentNo = "PAY-" + System.currentTimeMillis();
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO rev_payment (bill_id, customer_id, payment_no, amount, pay_method, pay_channel) " +
|
||||
"SELECT ?, customer_id, ?, ?, ?, ? FROM rev_bill WHERE id = ?",
|
||||
billId, paymentNo, amount, payMethod, payChannel, billId);
|
||||
|
||||
jdbcTemplate.update(
|
||||
"UPDATE rev_bill SET paid_fee = paid_fee + ?, status = CASE WHEN paid_fee >= total_fee THEN 'paid' ELSE 'partial' END, paid_at = NOW() WHERE id = ?",
|
||||
amount, billId);
|
||||
|
||||
return Map.of("paymentNo", paymentNo, "amount", amount, "status", "success");
|
||||
}
|
||||
|
||||
/** 欠费统计 */
|
||||
public List<Map<String, Object>> getOverdueBills(String area) {
|
||||
return jdbcTemplate.queryForList(
|
||||
"SELECT b.*, c.customer_name, c.phone FROM rev_bill b " +
|
||||
"JOIN rev_customer c ON b.customer_id = c.id " +
|
||||
"WHERE b.status IN ('pending','partial') AND c.area = ? AND b.due_date < CURRENT_DATE",
|
||||
area);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.water.revenue.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CustomerServiceCenter {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/** 水费查询(语音/在线) */
|
||||
public List<Map<String, Object>> queryBills(String phoneOrCustomerNo) {
|
||||
return jdbcTemplate.queryForList(
|
||||
"SELECT b.*, c.customer_name, c.phone FROM rev_bill b " +
|
||||
"JOIN rev_customer c ON b.customer_id = c.id " +
|
||||
"WHERE c.phone = ? OR c.customer_no = ? ORDER BY b.bill_period DESC LIMIT 12",
|
||||
phoneOrCustomerNo, phoneOrCustomerNo);
|
||||
}
|
||||
|
||||
/** 知识库管理 */
|
||||
public List<Map<String, Object>> searchKnowledge(String keyword) {
|
||||
return jdbcTemplate.queryForList(
|
||||
"SELECT dict_label, dict_value FROM sys_dict_data WHERE dict_type_id = " +
|
||||
"(SELECT id FROM sys_dict_type WHERE dict_key = 'knowledge_base') " +
|
||||
"AND dict_label LIKE ?", "%" + keyword + "%");
|
||||
}
|
||||
|
||||
/** 公告板 */
|
||||
public List<Map<String, Object>> getNotices(String noticeType) {
|
||||
return jdbcTemplate.queryForList(
|
||||
"SELECT dict_label, dict_value, created_at FROM sys_dict_data WHERE dict_type_id = " +
|
||||
"(SELECT id FROM sys_dict_type WHERE dict_key = ?) ORDER BY created_at DESC LIMIT 10",
|
||||
"notice_" + noticeType); // notice_water_stop, notice_water_quality, etc.
|
||||
}
|
||||
|
||||
/** KPI 指标 */
|
||||
public Map<String, Object> getKpi() {
|
||||
return jdbcTemplate.queryForMap("""
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM rev_bill WHERE status = 'pending' AND created_at > CURRENT_DATE - 30) AS pending_bills,
|
||||
(SELECT COUNT(*) FROM rev_install WHERE status IN ('pre_apply','engineering')) AS pending_installs,
|
||||
(SELECT ROUND(AVG(EXTRACT(EPOCH FROM (updated_at - created_at))/3600)::numeric, 2)
|
||||
FROM rev_install WHERE status = 'completed' AND created_at > CURRENT_DATE - 30) AS avg_install_hours
|
||||
""");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.water.revenue.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InstallService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/** 预受理申请 */
|
||||
public Map<String, Object> preApply(String name, String phone, String area, String address, String customerType, String caliber) {
|
||||
String appNo = "INS-" + System.currentTimeMillis();
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO rev_install (application_no, applicant_name, applicant_phone, area, address, customer_type, caliber, status) VALUES (?,?,?,?,?,?,?,?)",
|
||||
appNo, name, phone, area, address, customerType, caliber, "pre_apply");
|
||||
return Map.of("applicationNo", appNo, "status", "pre_apply");
|
||||
}
|
||||
|
||||
/** 工程申请 */
|
||||
public Map<String, Object> engineeringApply(String appNo, Map<String, Object> engData) {
|
||||
jdbcTemplate.update(
|
||||
"UPDATE rev_install SET status = 'engineering', updated_at = NOW() WHERE application_no = ?",
|
||||
appNo);
|
||||
return Map.of("applicationNo", appNo, "status", "engineering");
|
||||
}
|
||||
|
||||
/** 派单到施工 */
|
||||
public Map<String, Object> assignTask(String appNo, Long assigneeId) {
|
||||
jdbcTemplate.update(
|
||||
"UPDATE rev_install SET status = 'pending_review', updated_at = NOW() WHERE application_no = ?",
|
||||
appNo);
|
||||
return Map.of("applicationNo", appNo, "status", "pending_review", "assigneeId", assigneeId);
|
||||
}
|
||||
|
||||
/** 查询报装进度 */
|
||||
public Map<String, Object> getProgress(String appNo) {
|
||||
return jdbcTemplate.queryForMap(
|
||||
"SELECT application_no, applicant_name, applicant_phone, area, address, customer_type, caliber, status, created_at, updated_at FROM rev_install WHERE application_no = ?",
|
||||
appNo);
|
||||
}
|
||||
|
||||
/** 报装统计报表 */
|
||||
public List<Map<String, Object>> getStatsReport(String area, LocalDate start, LocalDate end) {
|
||||
return jdbcTemplate.queryForList(
|
||||
"SELECT area, customer_type, status, COUNT(*) as count FROM rev_install WHERE created_at BETWEEN ? AND ? GROUP BY area, customer_type, status",
|
||||
start, end);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.water.revenue.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MeterService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/** 水表入库 */
|
||||
public void stockIn(String meterNo, String caliber, String meterType, String manufacturer, int quantity) {
|
||||
for (int i = 0; i < quantity; i++) {
|
||||
String no = meterNo + "-" + (i + 1);
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO rev_meter (meter_no, caliber, meter_type, manufacturer, status) VALUES (?,?,?,?,?)",
|
||||
no, caliber, meterType, manufacturer, "warehouse");
|
||||
}
|
||||
log.info("Meter stock in: {} x{}", meterNo, quantity);
|
||||
}
|
||||
|
||||
/** 水表出库安装 */
|
||||
@Transactional
|
||||
public void install(Long meterId, Long customerId, String address, BigDecimal initialReading) {
|
||||
jdbcTemplate.update(
|
||||
"UPDATE rev_meter SET customer_id = ?, install_address = ?, initial_reading = ?, current_reading = ?, status = 'active', install_date = CURRENT_DATE WHERE id = ?",
|
||||
customerId, address, initialReading, initialReading, meterId);
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO rev_meter_log (meter_id, operation_type, old_reading, new_reading, remark) VALUES (?,?,?,?,?)",
|
||||
meterId, "install", BigDecimal.ZERO, initialReading, "新表安装");
|
||||
}
|
||||
|
||||
/** 故障换表 */
|
||||
@Transactional
|
||||
public void replace(Long oldMeterId, Long newMeterId, BigDecimal oldReading, String remark) {
|
||||
// 旧表拆除
|
||||
jdbcTemplate.update("UPDATE rev_meter SET status = 'dismantled', current_reading = ? WHERE id = ?", oldReading, oldMeterId);
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO rev_meter_log (meter_id, operation_type, old_reading, remark) VALUES (?,?,?,?)",
|
||||
oldMeterId, "dismantle", oldReading, remark);
|
||||
|
||||
// 新表安装(继承客户信息)
|
||||
Map<String, Object> oldMeter = jdbcTemplate.queryForMap("SELECT customer_id, install_address FROM rev_meter WHERE id = ?", oldMeterId);
|
||||
jdbcTemplate.update(
|
||||
"UPDATE rev_meter SET customer_id = ?, install_address = ?, status = 'active', install_date = CURRENT_DATE WHERE id = ?",
|
||||
oldMeter.get("customer_id"), oldMeter.get("install_address"), newMeterId);
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO rev_meter_log (meter_id, operation_type, new_meter_no, remark) VALUES (?,?,?,?)",
|
||||
newMeterId, "change", String.valueOf(newMeterId), "替换旧表 #" + oldMeterId);
|
||||
}
|
||||
|
||||
/** 水表报废 */
|
||||
public void scrap(Long meterId, String reason) {
|
||||
jdbcTemplate.update("UPDATE rev_meter SET status = 'scrapped' WHERE id = ?", meterId);
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO rev_meter_log (meter_id, operation_type, remark) VALUES (?,?,?)",
|
||||
meterId, "scrap", reason);
|
||||
}
|
||||
|
||||
/** 查询水表生命周期记录 */
|
||||
public List<Map<String, Object>> getLifecycle(Long meterId) {
|
||||
return jdbcTemplate.queryForList(
|
||||
"SELECT * FROM rev_meter_log WHERE meter_id = ? ORDER BY created_at",
|
||||
meterId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.water.revenue.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RemoteReadingService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/** 远传集抄:批量采集 */
|
||||
public Map<String, Object> batchRead(String area) {
|
||||
List<Map<String, Object>> meters = jdbcTemplate.queryForList(
|
||||
"SELECT rm.id, rm.meter_no, rm.current_reading, i.device_sn " +
|
||||
"FROM rev_meter rm LEFT JOIN iot_device i ON rm.device_id = i.id " +
|
||||
"JOIN rev_customer c ON rm.customer_id = c.id " +
|
||||
"WHERE rm.status = 'active' AND c.area = ?", area);
|
||||
|
||||
int success = 0, failed = 0;
|
||||
String period = java.time.YearMonth.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM"));
|
||||
|
||||
for (Map<String, Object> m : meters) {
|
||||
try {
|
||||
String deviceSn = (String) m.get("device_sn");
|
||||
// 从 IoT 平台获取实时读数(mock: 随机增量)
|
||||
BigDecimal prev = m.get("current_reading") != null ? (BigDecimal) m.get("current_reading") : BigDecimal.ZERO;
|
||||
BigDecimal curr = prev.add(BigDecimal.valueOf(new Random().nextDouble() * 50));
|
||||
BigDecimal consumption = curr.subtract(prev);
|
||||
if (consumption.compareTo(BigDecimal.ZERO) < 0) consumption = BigDecimal.ZERO;
|
||||
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO rev_reading (meter_id, reading_date, reading_period, prev_reading, curr_reading, consumption, read_type) " +
|
||||
"VALUES (?, CURRENT_DATE, ?, ?, ?, ?, 'remote')",
|
||||
m.get("id"), period, prev, curr, consumption);
|
||||
jdbcTemplate.update("UPDATE rev_meter SET current_reading = ? WHERE id = ?", curr, m.get("id"));
|
||||
success++;
|
||||
} catch (Exception e) {
|
||||
failed++;
|
||||
log.warn("Read failed for meter {}: {}", m.get("meter_no"), e.getMessage());
|
||||
}
|
||||
}
|
||||
log.info("Batch read: area={} success={} failed={}", area, success, failed);
|
||||
return Map.of("area", area, "success", success, "failed", failed, "period", period);
|
||||
}
|
||||
|
||||
/** DMA 分区漏损分析 */
|
||||
public Map<String, Object> dmaAnalysis(String area, String dateStr) {
|
||||
List<Map<String, Object>> result = jdbcTemplate.queryForList(
|
||||
"SELECT area, SUM(consumption) as total_consumption, COUNT(DISTINCT rm.id) as meter_count " +
|
||||
"FROM rev_reading rr JOIN rev_meter rm ON rr.meter_id = rm.id " +
|
||||
"JOIN rev_customer c ON rm.customer_id = c.id " +
|
||||
"WHERE c.area = ? AND rr.reading_period = ? GROUP BY area",
|
||||
area, dateStr);
|
||||
|
||||
// 漏损率 = 1 - (售水量/供水量)
|
||||
Map<String, Object> dma = new HashMap<>(result.isEmpty() ? Map.of() : result.get(0));
|
||||
dma.put("supplyEstimate", 1000); // TODO: 从水厂出水量获取
|
||||
dma.put("leakRate", "分析中");
|
||||
return dma;
|
||||
}
|
||||
|
||||
/** 大表监控 (DN80+) */
|
||||
public List<Map<String, Object>> largeMeterMonitor() {
|
||||
return jdbcTemplate.queryForList(
|
||||
"SELECT rm.*, c.customer_name, c.area, i.device_sn, i.status as device_status " +
|
||||
"FROM rev_meter rm JOIN rev_customer c ON rm.customer_id = c.id " +
|
||||
"LEFT JOIN iot_device i ON rm.device_id = i.id " +
|
||||
"WHERE rm.caliber IN ('DN80','DN100','DN150','DN200','DN300','DN400') AND rm.status = 'active'");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.water.revenue.service;
|
||||
|
||||
import cn.dev33.satoken.secure.BCrypt;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RevenueBaseService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
// ========== SSO 单点登录 ==========
|
||||
public Map<String, Object> ssoLogin(String username, String password, String appType) {
|
||||
// 从统一用户表验证
|
||||
Map<String, Object> user = jdbcTemplate.queryForMap(
|
||||
"SELECT id, username, real_name, phone, status FROM sys_user WHERE username = ? AND status = 1",
|
||||
username);
|
||||
if (user == null) throw new RuntimeException("用户不存在");
|
||||
// 生成 SSO Token
|
||||
String ssoToken = UUID.randomUUID().toString();
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO sys_oper_log (user_id, username, module, operation, request_url) VALUES (?,?,?,?,?)",
|
||||
user.get("id"), username, "revenue", "sso_login", "/revenue/auth/sso");
|
||||
user.put("ssoToken", ssoToken);
|
||||
user.put("appType", appType);
|
||||
return user;
|
||||
}
|
||||
|
||||
// ========== 应用接入管理 ==========
|
||||
public void registerApp(String appName, String appKey, String appSecret, String redirectUri) {
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value) " +
|
||||
"SELECT id, ?, ? FROM sys_dict_type WHERE dict_key = 'app_config'",
|
||||
appName, appKey + ":" + appSecret + ":" + redirectUri);
|
||||
}
|
||||
|
||||
// ========== 运维审计 ==========
|
||||
public void auditLog(Long userId, String action, String target, String detail) {
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO sys_oper_log (user_id, module, operation, request_url, request_params) VALUES (?,?,?,?,?)",
|
||||
userId, "revenue_audit", action, target, detail);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.water.revenue.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WechatService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/** 微信用户绑定 */
|
||||
public Map<String, Object> bindUser(String wechatOpenId, String customerNo, String phone) {
|
||||
jdbcTemplate.update(
|
||||
"UPDATE rev_customer SET phone = COALESCE(phone, ?) WHERE customer_no = ?",
|
||||
phone, customerNo);
|
||||
// 存储微信绑定关系
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value) " +
|
||||
"SELECT id, ?, ? FROM sys_dict_type WHERE dict_key = 'wechat_bind' " +
|
||||
"ON CONFLICT DO NOTHING",
|
||||
wechatOpenId, customerNo);
|
||||
return Map.of("openId", wechatOpenId, "customerNo", customerNo, "status", "bound");
|
||||
}
|
||||
|
||||
/** 在线缴费(微信支付预下单) */
|
||||
public Map<String, Object> wechatPayPrepay(String customerNo, String billPeriod, BigDecimal amount) {
|
||||
String orderNo = "WXPAY-" + System.currentTimeMillis();
|
||||
Map<String, Object> bill = jdbcTemplate.queryForMap(
|
||||
"SELECT id, total_fee FROM rev_bill WHERE customer_id = " +
|
||||
"(SELECT id FROM rev_customer WHERE customer_no = ?) AND bill_period = ? AND status IN ('pending','partial')",
|
||||
customerNo, billPeriod);
|
||||
// 微信支付统一下单(mock)
|
||||
log.info("WeChat Pay prepay: order={} bill={} amount={}", orderNo, bill.get("id"), amount);
|
||||
return Map.of("orderNo", orderNo, "prepayId", "wx" + orderNo, "amount", amount);
|
||||
}
|
||||
|
||||
/** AI 客服问答 */
|
||||
public String aiAnswer(String question) {
|
||||
// 简易关键词匹配
|
||||
Map<String, String> qa = new LinkedHashMap<>();
|
||||
qa.put("水费", "您可以发送户号查询水费账单,或通过在线缴费功能直接支付。");
|
||||
qa.put("停水", "请查看停水公告了解最新停水计划。如有紧急停水,请拨打客服热线。");
|
||||
qa.put("报装", "新装水表可通过网上营业厅-业务办理-报装申请提交,我们会安排现场踏勘。");
|
||||
qa.put("水质", "水质报告每月更新,详见水质公告。如发现水质异常请立即联系我们。");
|
||||
qa.put("发票", "缴费后可在电子发票中申请开具电子发票,发送到您的微信或邮箱。");
|
||||
qa.put("过户", "房屋买卖后请携带房产证和身份证前往营业厅办理过户手续。");
|
||||
|
||||
for (Map.Entry<String, String> e : qa.entrySet()) {
|
||||
if (question.contains(e.getKey())) return e.getValue();
|
||||
}
|
||||
return "您好!我是智慧水务AI客服,您可以问我关于水费、报装、停水、水质、发票等问题。如需人工服务请转接客服热线。";
|
||||
}
|
||||
|
||||
/** 后台公告管理 */
|
||||
public void publishNotice(String type, String title, String content) {
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value) " +
|
||||
"SELECT id, ?, ? FROM sys_dict_type WHERE dict_key = ?",
|
||||
title, content, "notice_" + type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.water.revenue.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WorkOrderService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/** 创建工单 */
|
||||
public Map<String, Object> create(String title, String type, String priority, Long reporterId,
|
||||
String reporterName, String description, String area) {
|
||||
String woNo = "WO-" + System.currentTimeMillis();
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO patrol_task (task_name, task_date, status) VALUES (?, CURRENT_DATE, 'pending')",
|
||||
title + "[" + woNo + "]");
|
||||
log.info("WorkOrder created: {} type={} priority={}", woNo, type, priority);
|
||||
return Map.of("woNo", woNo, "title", title, "status", "pending", "area", area);
|
||||
}
|
||||
|
||||
/** 工单分派 */
|
||||
public Map<String, Object> assign(String woNo, Long assigneeId, String assigneeName) {
|
||||
jdbcTemplate.update(
|
||||
"UPDATE patrol_task SET assignee_id = ?, status = 'in_progress' WHERE task_name LIKE ?",
|
||||
assigneeId, "%" + woNo + "%");
|
||||
return Map.of("woNo", woNo, "assigneeId", assigneeId, "status", "assigned");
|
||||
}
|
||||
|
||||
/** 工单处理完成 */
|
||||
public Map<String, Object> complete(String woNo, String result, List<String> photoUrls) {
|
||||
jdbcTemplate.update(
|
||||
"UPDATE patrol_task SET status = 'completed', actual_end = NOW() WHERE task_name LIKE ?",
|
||||
"%" + woNo + "%");
|
||||
return Map.of("woNo", woNo, "status", "completed", "photos", photoUrls);
|
||||
}
|
||||
|
||||
/** 工单统计 */
|
||||
public Map<String, Object> stats(String area) {
|
||||
return jdbcTemplate.queryForMap(
|
||||
"SELECT status, COUNT(*) as count FROM patrol_task WHERE task_date >= CURRENT_DATE - 30 GROUP BY status");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user