Browse Source

断面交通量统计

develop
xiepufeng 9 months ago
parent
commit
1a6098dcff
  1. 144
      zc-business/src/main/java/com/zc/business/domain/DcTrafficSectionData.java
  2. 49
      zc-business/src/main/java/com/zc/business/enums/TrafficDataPeriodTypeEnum.java
  3. 22
      zc-business/src/main/java/com/zc/business/mapper/DcTrafficSectionDataMapper.java
  4. 47
      zc-business/src/main/java/com/zc/business/message/device/handler/DeviceMessageHandler.java
  5. 15
      zc-business/src/main/java/com/zc/business/service/DcTrafficSectionDataService.java
  6. 173
      zc-business/src/main/java/com/zc/business/service/impl/DcTrafficSectionDataServiceImpl.java
  7. 47
      zc-business/src/main/java/com/zc/business/statistics/cache/AbstractTrafficStatisticsCache.java
  8. 139
      zc-business/src/main/java/com/zc/business/statistics/cache/DailyTrafficStatisticsCache.java
  9. 139
      zc-business/src/main/java/com/zc/business/statistics/cache/MonthlyTrafficStatisticsCache.java
  10. 144
      zc-business/src/main/java/com/zc/business/statistics/cache/QuarterlyTrafficStatisticsCache.java
  11. 143
      zc-business/src/main/java/com/zc/business/statistics/cache/YearlyTrafficStatisticsCache.java
  12. 70
      zc-business/src/main/java/com/zc/business/statistics/handler/RealtimeTrafficStatistics.java
  13. 44
      zc-business/src/main/resources/mapper/business/DcTrafficSectionDataMapper.xml

144
zc-business/src/main/java/com/zc/business/domain/DcTrafficSectionData.java

@ -0,0 +1,144 @@
package com.zc.business.domain;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.annotation.TableId;
import com.ruoyi.common.core.domain.BaseEntity;
import com.zc.business.enums.TrafficDataPeriodTypeEnum;
import lombok.Data;
import org.apache.commons.codec.digest.DigestUtils;
import java.util.Date;
import java.util.Objects;
/**
* 交通断面数据统计定义
* @author xiepufeng
*/
@Data
public class DcTrafficSectionData extends BaseEntity {
/**
* 主键
*/
@TableId
private String id;
/**
* 车流量
*/
private Integer trafficVolume;
/**
* 平均速度
*/
private Integer averageSpeed;
/**
* 所属设备
*/
private Long deviceId;
/**
* 统计时间
*/
private Date statisticalDate;
/**
* 道路方向
*/
private Byte direction;
/**
* 时段类型
* 1- 2- 3- 4-
*/
private Byte periodType;
/**
* 所在桩号
*/
private Integer stakeMark;
/**
* 设置交通数据的统计周期类型
* @param periodType 统计周期类型的枚举值
*/
public void setPeriodType(TrafficDataPeriodTypeEnum periodType) {
this.periodType = periodType.getCode(); // 将枚举类型转换为代码值存储
}
/**
* 重写equals方法用于比较两个对象是否相等
* @param o 要与当前对象比较的对象
* @return 如果两个对象相等则返回true否则返回false
*/
@Override
public boolean equals(Object o) {
if (this == o) return true; // 同一对象比较,直接返回true
if (o == null || getClass() != o.getClass()) return false; // 对象为空或类型不同,返回false
DcTrafficSectionData that = (DcTrafficSectionData) o; // 类型转换
return Objects.equals(deviceId, that.deviceId) &&
Objects.equals(statisticalDate, that.statisticalDate) &&
Objects.equals(direction, that.direction) &&
Objects.equals(periodType, that.periodType) &&
Objects.equals(stakeMark, that.stakeMark); // 比较各属性值
}
/**
* 重写hashCode方法基于对象的属性生成哈希码
* @return 对象的哈希码值
*/
@Override
public int hashCode() {
return Objects.hash(deviceId, statisticalDate, direction, periodType, stakeMark);
}
/**
* 设置统计日期根据不同的统计周期类型来调整日期使其对应周期的起始日期
* @param statisticalDate 统计日期原始日期
*/
public void setStatisticalDate(Date statisticalDate) {
TrafficDataPeriodTypeEnum typeEnum = TrafficDataPeriodTypeEnum.valueOfCode(periodType);
setStatisticalDate(statisticalDate, typeEnum);
}
/**
* 根据给定的统计周期类型和日期设置统计日期为相应周期的起始日期
* @param statisticalDate 原始统计日期
* @param typeEnum 统计周期类型
*/
public void setStatisticalDate(Date statisticalDate, TrafficDataPeriodTypeEnum typeEnum) {
switch (typeEnum) {
case DAY:
// 设置为当天的起始时间
this.statisticalDate = DateUtil.beginOfDay(statisticalDate);
break;
case MONTH:
// 设置为当月的起始日期
this.statisticalDate = DateUtil.beginOfMonth(statisticalDate);
break;
case QUARTER:
// 设置为当季度的起始日期
this.statisticalDate = DateUtil.beginOfQuarter(statisticalDate);
break;
case YEAR:
// 设置为当年的起始日期
this.statisticalDate = DateUtil.beginOfYear(statisticalDate);
break;
default:
// 如果不是预定义的周期类型,则不做任何处理
this.statisticalDate = statisticalDate;
}
}
/**
* 根据设备ID统计时间方向时段类型桩号生成一个唯一ID
*/
public void generateUniqueId() {
String combinedAttributes = deviceId + "_" + DateUtil.format(statisticalDate, "yyyyMMdd_HHmmss") + "_" + direction + "_" + periodType + "_" + stakeMark;
this.id = DigestUtils.md5Hex(combinedAttributes);
}
}

49
zc-business/src/main/java/com/zc/business/enums/TrafficDataPeriodTypeEnum.java

@ -0,0 +1,49 @@
package com.zc.business.enums;
import lombok.Getter;
/**
* 定义一个枚举类型 TrafficDataPeriodTypeEnum用于表示周期类型
*/
@Getter
public enum TrafficDataPeriodTypeEnum {
// 枚举成员:年,关联字节型代码 1 和描述 "年"
YEAR((byte) 1, "年"),
// 枚举成员:月,关联字节型代码 2 和描述 "月"
MONTH((byte) 2, "月"),
// 枚举成员:季,关联字节型代码 3 和描述 "季"
QUARTER((byte) 3, "季"),
// 枚举成员:日,关联字节型代码 4 和描述 "日"
DAY((byte) 4, "日");
// 每个枚举成员的属性:代码,存储对应的字节型数值
private final Byte code;
// 每个枚举成员的属性:描述,存储该周期类型的中文描述
private final String description;
// 构造方法,初始化每个枚举成员的代码和描述信息
TrafficDataPeriodTypeEnum(Byte code, String description) {
this.code = code;
this.description = description;
}
// 反向查找方法,根据给定的字节型代码查找对应的枚举值
// 如果找到匹配项,则返回该枚举类型;否则抛出IllegalArgumentException异常
public static TrafficDataPeriodTypeEnum valueOfCode(Byte code) {
// 遍历所有PeriodTypeEnum枚举值
for (TrafficDataPeriodTypeEnum type : values()) {
// 如果当前枚举类型的code与输入的code相等
if (type.getCode().equals(code)) {
// 返回找到的枚举类型
return type;
}
}
// 如果循环结束都没有找到匹配的code,则抛出异常,说明提供的code非法
throw new IllegalArgumentException("无效的周期类型代码: " + code);
}
}

22
zc-business/src/main/java/com/zc/business/mapper/DcTrafficSectionDataMapper.java

@ -0,0 +1,22 @@
package com.zc.business.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zc.business.domain.DcTrafficSectionData;
import org.apache.ibatis.annotations.Mapper;
/**
* 交通断面数据统计Mapper接口
*
* @author xiepufeng
*/
@Mapper
public interface DcTrafficSectionDataMapper extends BaseMapper<DcTrafficSectionData> {
/**
* 插入或更新交通路段数据
*
* @param trafficSectionData 交通路段数据对象包含需要插入或更新的数据
* @return 返回一个布尔值表示操作是否成功true表示插入或更新成功false表示失败
*/
boolean insertOrUpdate(DcTrafficSectionData trafficSectionData);
}

47
zc-business/src/main/java/com/zc/business/message/device/handler/DeviceMessageHandler.java

@ -1,5 +1,6 @@
package com.zc.business.message.device.handler;
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
@ -11,19 +12,15 @@ import com.zc.business.enums.IotProductEnum;
import com.zc.business.enums.WarningSourceEnum;
import com.zc.business.enums.WarningStateEnum;
import com.zc.business.enums.WarningSubclassEnum;
import com.zc.business.service.DcTrafficSectionDataService;
import com.zc.business.service.IDcDeviceService;
import com.zc.business.service.IDcWarningService;
import com.zc.business.service.IMiddleDatabaseService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@ -49,8 +46,11 @@ public class DeviceMessageHandler {
@Resource
private RedisCache redisCache;
@Value("${iot.address}")
private String iotAddress;
@Resource
private DcTrafficSectionDataService dcTrafficSectionDataService;
/*@Value("${iot.address}")
private String iotAddress;*/
/**
* 更新设备状态
@ -94,7 +94,13 @@ public class DeviceMessageHandler {
// 摄像头检测事件
if (IotProductEnum.CAMERA_DETECTION_EVENT.value().equals(productId)) {
this.cameraDetectionEventHandle(data);
cameraDetectionEventHandle(data);
return;
}
// 一站式情况调查站
if (IotProductEnum.ONE_STOP_PRODUCT.value().equals(productId)) {
oneStopDeviceMessageHandle(data);
}
}
@ -134,7 +140,8 @@ public class DeviceMessageHandler {
// 标题
dcWarning.setWarningTitle(title);
dcWarning.setRemark(convertTimestampToString(captureTime) + " " + title);
dcWarning.setRemark(DateUtil.formatDateTime(DateUtil.date(captureTime)) + " " + title);
// 影响车道
dcWarning.setLane(String.valueOf(data.getInteger("relatedLaneNo")));
@ -166,24 +173,14 @@ public class DeviceMessageHandler {
}
/**
* 将毫秒级时间戳转换为"yyyy-MM-dd HH:mm:ss"格式的字符串
*
* @param timestampInMillis 毫秒级时间戳
* @return 格式化后的日期时间字符串
*/
public static String convertTimestampToString(long timestampInMillis) {
// 转换为Instant对象
Instant instant = Instant.ofEpochMilli(timestampInMillis);
// 转换为LocalDateTime(默认时区)
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
// 创建DateTimeFormatter实例并指定输出格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 格式化LocalDateTime对象为字符串
return localDateTime.format(formatter);
/**
* 一站式情况调查站设备消息处理入口
* @param msg 设备消息
*/
private void oneStopDeviceMessageHandle(JSONObject msg) {
dcTrafficSectionDataService.processRealtimeMessage(msg);
}
}

15
zc-business/src/main/java/com/zc/business/service/DcTrafficSectionDataService.java

@ -0,0 +1,15 @@
package com.zc.business.service;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.IService;
import com.zc.business.domain.DcTrafficSectionData;
public interface DcTrafficSectionDataService extends IService<DcTrafficSectionData> {
/**
* 处理实时接收到的设备消息并将其转换为交通断面统计数据对象并缓存
*
* @param msg 设备发送的JSON格式实时消息
*/
void processRealtimeMessage(JSONObject msg);
}

173
zc-business/src/main/java/com/zc/business/service/impl/DcTrafficSectionDataServiceImpl.java

@ -0,0 +1,173 @@
package com.zc.business.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.DateUtils;
import com.zc.business.domain.DcTrafficSectionData;
import com.zc.business.enums.TrafficDataPeriodTypeEnum;
import com.zc.business.statistics.cache.*;
import com.zc.business.mapper.DcTrafficSectionDataMapper;
import com.zc.business.service.DcTrafficSectionDataService;
import com.zc.business.statistics.handler.RealtimeTrafficStatistics;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Map;
import java.util.function.Consumer;
/**
* 通断面数据服务实现类负责处理实时设备消息缓存数据定时任务以及数据保存等功能
* @author xiepufeng
*/
@Service
public class DcTrafficSectionDataServiceImpl
extends ServiceImpl<DcTrafficSectionDataMapper, DcTrafficSectionData>
implements DcTrafficSectionDataService {
@Resource
private DcTrafficSectionDataMapper dcTrafficSectionDataMapper;
/**
* 处理实时接收到的设备消息并将其转换为交通断面统计数据对象并缓存
*
* @param msg 设备发送的JSON格式实时消息
*/
@Override
public void processRealtimeMessage(JSONObject msg) {
// 1. 将设备消息转换为交通断面数据统计定义对象
DcTrafficSectionData dcTrafficSectionData = convertToTrafficStatistics(msg);
// 2. 将转换后的数据添加到缓存中
DailyTrafficStatisticsCache.addCacheData(dcTrafficSectionData);
}
/**
* 将设备实时消息转换为交通断面数据统计定义对象
*
* @param msg JSON格式的设备实时消息
* @return 转换后的交通断面数据统计定义对象
*/
public DcTrafficSectionData convertToTrafficStatistics(JSONObject msg) {
DcTrafficSectionData dcTrafficSectionData = new DcTrafficSectionData();
// TODO
return dcTrafficSectionData;
}
/**
* 定义每小时第20分钟执行的任务用于清除过期缓存数据并将缓存中的数据整合后保存至数据库
*/
@Scheduled(cron = "0 20 * * * ?") // 每小时的20分整点执行该任务
public void performHourlyCleanupAndPersist() {
// 清除已过期的缓存数据
DailyTrafficStatisticsCache.clearExpiredData();
MonthlyTrafficStatisticsCache.clearExpiredData();
QuarterlyTrafficStatisticsCache.clearExpiredData();
YearlyTrafficStatisticsCache.clearExpiredData();
// 整合缓存数据并保存至数据库
// 将缓存中的数据按日统计后保存至数据库
// 添加月交通断面数据到缓存中
persistAggregatedData(DailyTrafficStatisticsCache.getCache(), TrafficDataPeriodTypeEnum.DAY, MonthlyTrafficStatisticsCache::addCacheData);
// 将缓存中的数据按月统计后保存至数据库
// 添加季度交通断面数据到缓存中
persistAggregatedData(MonthlyTrafficStatisticsCache.getCache(), TrafficDataPeriodTypeEnum.MONTH, QuarterlyTrafficStatisticsCache::addCacheData);
// 将缓存中的数据按季度统计后保存至数据库
// 添加年交通断面数据到缓存中
persistAggregatedData(QuarterlyTrafficStatisticsCache.getCache(), TrafficDataPeriodTypeEnum.QUARTER, YearlyTrafficStatisticsCache::addCacheData);
// 将缓存中的数据按年统计后保存至数据库
persistAggregatedData(YearlyTrafficStatisticsCache.getCache(), TrafficDataPeriodTypeEnum.YEAR, (a) -> {});
}
/**
* 将缓存中的数据统计后保存至数据库
*/
public void persistAggregatedData(
Map<String, ? extends AbstractTrafficStatisticsCache> cache,
TrafficDataPeriodTypeEnum periodType,
Consumer<DcTrafficSectionData> consumer
) {
for (AbstractTrafficStatisticsCache data : cache.values()) {
// 如果数据已经存储过,则跳过此次处理
if (data.isStored()) {
continue;
}
DcTrafficSectionData aggregatedData = RealtimeTrafficStatistics.trafficStatistics(data.getData(), periodType);
if (dcTrafficSectionDataMapper.insertOrUpdate(aggregatedData)) {
// 设置数据已存储状态
data.setStored(true);
// 调用回调函数
consumer.accept(aggregatedData);
}
}
}
/**
* 将计算给定列表中所有交通断面数据的统计结果保存至数据库
*/
public boolean persistData(DcTrafficSectionData aggregatedData) {
if (aggregatedData == null) {
return false;
}
// 创建更新条件封装器
LambdaUpdateWrapper<DcTrafficSectionData> updateWrapper = new LambdaUpdateWrapper<>();
// 设置更新条件:根据设备ID、方向、时段类型、桩号、统计时间查找记录
updateWrapper.eq(DcTrafficSectionData::getDeviceId, aggregatedData.getDeviceId());
// 方向
updateWrapper.eq(DcTrafficSectionData::getDirection, aggregatedData.getDirection());
// 时段类型
updateWrapper.eq(DcTrafficSectionData::getPeriodType, aggregatedData.getPeriodType());
// 桩号
updateWrapper.eq(DcTrafficSectionData::getStakeMark, aggregatedData.getStakeMark());
// 统计时间
updateWrapper.eq(DcTrafficSectionData::getStatisticalDate, aggregatedData.getStatisticalDate());
if (this.update(aggregatedData, updateWrapper)) {
return true;
} else {
// 更新失败则尝试插入
aggregatedData.setCreateTime(DateUtils.getNowDate());
return this.save(aggregatedData);
}
}
/* public boolean persistData(DcTrafficSectionData aggregatedData) {
if (aggregatedData == null) {
return false;
}
// 创建更新条件封装器
LambdaUpdateWrapper<DcTrafficSectionData> updateWrapper = new LambdaUpdateWrapper<>();
// 设置更新条件:根据设备ID、方向、时段类型、桩号、统计时间查找记录
updateWrapper.eq(DcTrafficSectionData::getDeviceId, aggregatedData.getDeviceId());
// 方向
updateWrapper.eq(DcTrafficSectionData::getDirection, aggregatedData.getDirection());
// 时段类型
updateWrapper.eq(DcTrafficSectionData::getPeriodType, aggregatedData.getPeriodType());
// 桩号
updateWrapper.eq(DcTrafficSectionData::getStakeMark, aggregatedData.getStakeMark());
// 统计时间
updateWrapper.eq(DcTrafficSectionData::getStatisticalDate, aggregatedData.getStatisticalDate());
if (this.update(aggregatedData, updateWrapper)) {
return true;
} else {
// 更新失败则尝试插入
aggregatedData.setCreateTime(DateUtils.getNowDate());
return this.save(aggregatedData);
}
}*/
}

47
zc-business/src/main/java/com/zc/business/statistics/cache/AbstractTrafficStatisticsCache.java

@ -0,0 +1,47 @@
package com.zc.business.statistics.cache;
import com.zc.business.domain.DcTrafficSectionData;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Date;
/**
* 交通断面数据缓存定义
* @author xiepufeng
*/
@Getter
@Setter
public abstract class AbstractTrafficStatisticsCache {
// 日志记录器
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 缓存对象的键由设备ID与统计日期组成
*/
private String cacheKey;
/**
* 设备上报日期字符串
*/
private String statisticalDateStr;
/**
* 数据最后添加到缓存中的时间
*/
private Date lastAddedTime;
/**
* 标记该缓存实例是否已存储过数据
*/
private boolean stored;
/**
* 存储具体交通断面数据的列表
*/
private Collection<DcTrafficSectionData> data;
}

139
zc-business/src/main/java/com/zc/business/statistics/cache/DailyTrafficStatisticsCache.java

@ -0,0 +1,139 @@
package com.zc.business.statistics.cache;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import com.zc.business.domain.DcTrafficSectionData;
import com.zc.business.enums.TrafficDataPeriodTypeEnum;
import lombok.Getter;
import lombok.Setter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
*
* 以天为单位的交通交通断面数据缓存类用于存储和管理设备上报的交通断面统计数据同时提供了数据缓存的有效性管理和清理功能
* @author xiepufeng
*/
@Getter
@Setter
public class DailyTrafficStatisticsCache extends AbstractTrafficStatisticsCache {
@Getter
// 静态缓存容器,使用ConcurrentHashMap保证线程安全
private static final Map<String, DailyTrafficStatisticsCache> cache = new ConcurrentHashMap<>();
// 最大缓存时间(单位:秒)
private static final long MAX_CACHE_TIME = 25 * 60 * 60; // 缓存数据最长保留25小时
// 最大容量限制(一个设备),防止内存溢出
private static final int MAX_CAPACITY = 60/5 * (24 + 1) + 1000; // 缓存的最大条目数
// 私有构造函数,确保只能通过静态方法获取实例
private DailyTrafficStatisticsCache() {
super();
this.setData(new ArrayList<>());
}
/**
* 添加交通断面数据到缓存中
*
* @param dcTrafficSectionData 待添加的交通断面统计数据
*/
public static void addCacheData(DcTrafficSectionData dcTrafficSectionData) {
// 获取或新建对应的缓存实例
DailyTrafficStatisticsCache instance = getInstance(dcTrafficSectionData);
// 检查缓存容量是否达到上限
if (instance.getData().size() >= MAX_CAPACITY) {
instance.getLogger().error("交通断面数据缓存出现异常,最大缓存量达到设定上线 {}, 当前设备是 {}", MAX_CAPACITY, dcTrafficSectionData.getDeviceId());
return;
}
// 更新最后添加时间
instance.setLastAddedTime(DateUtil.date());
// 更新状态
instance.setStored(false);
String formattedDate = formatDate(dcTrafficSectionData.getStatisticalDate());
String key = generateCacheKey(dcTrafficSectionData);
// 设置key和thatDay属性(仅在首次添加时)
if (instance.getCacheKey() == null) {
instance.setCacheKey(key);
instance.setStatisticalDateStr(formattedDate);
}
dcTrafficSectionData.setStatisticalDate(dcTrafficSectionData.getStatisticalDate(), TrafficDataPeriodTypeEnum.DAY);
// 将新数据添加到数据列表中
instance.getData().add(dcTrafficSectionData);
}
/**
* 获取或创建对应设备与日期的DcTrafficSectionDataCache实例
*
* @param dcTrafficSectionData 交通断面数据统计定义
* @return 对应的交通断面数据缓存实例
*/
private static DailyTrafficStatisticsCache getInstance(DcTrafficSectionData dcTrafficSectionData) {
// 使用toKey方法生成唯一键,并根据此键计算并返回缓存实例
return cache.computeIfAbsent(generateCacheKey(dcTrafficSectionData), k -> new DailyTrafficStatisticsCache());
}
/**
* 该方法用于生成一个基于DcTrafficSectionData对象属性的唯一键字符串
* 这个键由设备ID与统计日期两部分组成通常用于索引或标识特定设备在某一日期的交通流量数据
*
* @param dcTrafficSectionData DcTrafficSectionData类型的对象包含设备ID和统计日期等信息
* @return 格式化后的字符串键格式为 "deviceId|formattedDate"
*/
public static String generateCacheKey(DcTrafficSectionData dcTrafficSectionData) {
// 获取设备ID
Long deviceId = dcTrafficSectionData.getDeviceId();
// 获取并格式化统计日期
String formattedDate = formatDate(dcTrafficSectionData.getStatisticalDate());
// 使用"|"字符连接设备ID和日期,形成唯一键
return deviceId + "|" + formattedDate;
}
/**
* 清除所有过期的交通断面数据缓存项
*/
public static void clearExpiredData() {
// 使用stream API找出所有过期的数据缓存项键
Set<String> keysToRemove = cache.keySet().stream()
.filter(DailyTrafficStatisticsCache::isCacheItemExpire)
.collect(Collectors.toSet());
// 安全地从缓存中删除这些过期项
keysToRemove.forEach(cache::remove);
}
/**
* 检查给定缓存键所对应的缓存项是否已经过期
*
* @param key 缓存key
* @return 如果已过期则返回true否则返回false
*/
private static boolean isCacheItemExpire(String key) {
Date lastAddedTime = cache.get(key).getLastAddedTime();
Date currentTime = DateUtil.date();
long betweenSecond = DateUtil.between(lastAddedTime, currentTime, DateUnit.SECOND);
return betweenSecond > MAX_CACHE_TIME;
}
/**
* Date 类型的日期格式化为指定格式的字符串
*
* @param date 需要格式化的 Date 对象
* @return 格式化后的日期字符串
*/
private static String formatDate(Date date) {
// 使用 DateUtil 工具类将 date 格式化为指定格式的字符串
return DateUtil.formatDate(date);
}
}

139
zc-business/src/main/java/com/zc/business/statistics/cache/MonthlyTrafficStatisticsCache.java

@ -0,0 +1,139 @@
package com.zc.business.statistics.cache;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import com.zc.business.domain.DcTrafficSectionData;
import com.zc.business.enums.TrafficDataPeriodTypeEnum;
import lombok.Getter;
import lombok.Setter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* 以月为单位的交通交通断面数据缓存类用于存储和管理设备上报的交通断面统计数据同时提供了数据缓存的有效性管理和清理功能
* @author xiepufeng
*/
@Getter
@Setter
public class MonthlyTrafficStatisticsCache extends AbstractTrafficStatisticsCache {
// 静态缓存容器,使用ConcurrentHashMap保证线程安全
@Getter
private static final Map<String, MonthlyTrafficStatisticsCache> cache = new ConcurrentHashMap<>();
// 最大缓存时间(单位:秒)
private static final long MAX_CACHE_TIME = (60 * 60) * (31 * 24 + 1);
// 最大容量限制,防止内存溢出
private static final int MAX_CAPACITY = 31 + 1000; // 缓存的最大条目数
// 私有构造函数,确保只能通过静态方法获取实例
private MonthlyTrafficStatisticsCache() {
super();
this.setData(new HashSet<>());
}
/**
* 添加交通断面数据到缓存中
*
* @param dcTrafficSectionData 待添加的交通断面统计数据
*/
public static void addCacheData(DcTrafficSectionData dcTrafficSectionData) {
// 获取或新建对应的缓存实例
MonthlyTrafficStatisticsCache instance = getInstance(dcTrafficSectionData);
// 检查缓存容量是否达到上限
if (instance.getData().size() >= MAX_CAPACITY) {
instance.getLogger().error("交通断面数据缓存出现异常,最大缓存量达到设定上线 {}, 当前设备是 {}", MAX_CAPACITY, dcTrafficSectionData.getDeviceId());
return;
}
// 更新最后添加时间
instance.setLastAddedTime(DateUtil.date());
// 更新状态
instance.setStored(false);
String formattedDate = formatDate(dcTrafficSectionData.getStatisticalDate());
String key = generateCacheKey(dcTrafficSectionData);
// 设置key和thatDay属性(仅在首次添加时)
if (instance.getCacheKey() == null) {
instance.setCacheKey(key);
instance.setStatisticalDateStr(formattedDate);
}
dcTrafficSectionData.setStatisticalDate(dcTrafficSectionData.getStatisticalDate(), TrafficDataPeriodTypeEnum.MONTH);
// 将新数据添加到数据列表中
instance.getData().add(dcTrafficSectionData);
}
/**
* 获取或创建对应设备与日期的DcTrafficSectionDataCache实例
*
* @param dcTrafficSectionData 交通断面数据统计定义
* @return 对应的交通断面数据缓存实例
*/
private static MonthlyTrafficStatisticsCache getInstance(DcTrafficSectionData dcTrafficSectionData) {
// 使用toKey方法生成唯一键,并根据此键计算并返回缓存实例
return cache.computeIfAbsent(generateCacheKey(dcTrafficSectionData), k -> new MonthlyTrafficStatisticsCache());
}
/**
* 该方法用于生成一个基于DcTrafficSectionData对象属性的唯一键字符串
* 这个键由设备ID与统计日期两部分组成通常用于索引或标识特定设备在某一日期的交通流量数据
*
* @param dcTrafficSectionData DcTrafficSectionData类型的对象包含设备ID和统计日期等信息
* @return 格式化后的字符串键格式为 "deviceId|formattedDate"
*/
public static String generateCacheKey(DcTrafficSectionData dcTrafficSectionData) {
// 获取设备ID
Long deviceId = dcTrafficSectionData.getDeviceId();
// 获取并格式化统计日期
String formattedDate = formatDate(dcTrafficSectionData.getStatisticalDate());
// 使用"|"字符连接设备ID和日期,形成唯一键
return deviceId + "|" + formattedDate;
}
/**
* 清除所有过期的交通断面数据缓存项
*/
public static void clearExpiredData() {
// 使用stream API找出所有过期的数据缓存项键
Set<String> keysToRemove = cache.keySet().stream()
.filter(MonthlyTrafficStatisticsCache::isCacheItemExpire)
.collect(Collectors.toSet());
// 安全地从缓存中删除这些过期项
keysToRemove.forEach(cache::remove);
}
/**
* 检查给定缓存键所对应的缓存项是否已经过期
*
* @param key 缓存key
* @return 如果已过期则返回true否则返回false
*/
private static boolean isCacheItemExpire(String key) {
Date lastAddedTime = cache.get(key).getLastAddedTime();
Date currentTime = DateUtil.date();
long betweenSecond = DateUtil.between(lastAddedTime, currentTime, DateUnit.SECOND);
return betweenSecond > MAX_CACHE_TIME;
}
/**
* Date 类型的日期格式化为 "yyyy-MM-01" 格式的字符串
* @param date 需要格式化的日期对象
* @return 格式化后的日期字符串
*/
private static String formatDate(Date date) {
// 使用 DateUtil 工具类将 date 格式化为指定格式的字符串
return DateUtil.format(date, "yyyy-MM-01");
}
}

144
zc-business/src/main/java/com/zc/business/statistics/cache/QuarterlyTrafficStatisticsCache.java

@ -0,0 +1,144 @@
package com.zc.business.statistics.cache;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import com.zc.business.domain.DcTrafficSectionData;
import com.zc.business.enums.TrafficDataPeriodTypeEnum;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* 以季度为单位的交通交通断面数据缓存类用于存储和管理设备上报的交通断面统计数据同时提供了数据缓存的有效性管理和清理功能
* @author xiepufeng
*/
@Getter
@Setter
public class QuarterlyTrafficStatisticsCache extends AbstractTrafficStatisticsCache {
// 静态缓存容器,使用ConcurrentHashMap保证线程安全
@Getter
private static final Map<String, QuarterlyTrafficStatisticsCache> cache = new ConcurrentHashMap<>();
// 最大缓存时间(单位:秒)
private static final long MAX_CACHE_TIME = (60 * 60) * (3 * 31 * 24 + 1);
// 最大容量限制,防止内存溢出
private static final int MAX_CAPACITY = 3 + 1000; // 缓存的最大条目数
// 私有构造函数,确保只能通过静态方法获取实例
private QuarterlyTrafficStatisticsCache() {
super();
this.setData(new HashSet<>());
}
/**
* 添加交通断面数据到缓存中
*
* @param dcTrafficSectionData 待添加的交通断面统计数据
*/
public static void addCacheData(DcTrafficSectionData dcTrafficSectionData) {
// 获取或新建对应的缓存实例
QuarterlyTrafficStatisticsCache instance = getInstance(dcTrafficSectionData);
// 检查缓存容量是否达到上限
if (instance.getData().size() >= MAX_CAPACITY) {
instance.getLogger().error("交通断面数据缓存出现异常,最大缓存量达到设定上线 {}, 当前设备是 {}", MAX_CAPACITY, dcTrafficSectionData.getDeviceId());
return;
}
// 更新最后添加时间
instance.setLastAddedTime(DateUtil.date());
// 更新状态
instance.setStored(false);
String formattedDate = formatDate(dcTrafficSectionData.getStatisticalDate());
String key = generateCacheKey(dcTrafficSectionData);
// 设置key和thatDay属性(仅在首次添加时)
if (instance.getCacheKey() == null) {
instance.setCacheKey(key);
instance.setStatisticalDateStr(formattedDate);
}
dcTrafficSectionData.setStatisticalDate(dcTrafficSectionData.getStatisticalDate(), TrafficDataPeriodTypeEnum.QUARTER);
// 将新数据添加到数据列表中
instance.getData().add(dcTrafficSectionData);
}
/**
* 获取或创建对应设备与日期的DcTrafficSectionDataCache实例
*
* @param dcTrafficSectionData 交通断面数据统计定义
* @return 对应的交通断面数据缓存实例
*/
private static QuarterlyTrafficStatisticsCache getInstance(DcTrafficSectionData dcTrafficSectionData) {
// 使用toKey方法生成唯一键,并根据此键计算并返回缓存实例
return cache.computeIfAbsent(generateCacheKey(dcTrafficSectionData), k -> new QuarterlyTrafficStatisticsCache());
}
/**
* 该方法用于生成一个基于DcTrafficSectionData对象属性的唯一键字符串
* 这个键由设备ID与统计日期两部分组成通常用于索引或标识特定设备在某一日期的交通流量数据
*
* @param dcTrafficSectionData DcTrafficSectionData类型的对象包含设备ID和统计日期等信息
* @return 格式化后的字符串键格式为 "deviceId|formattedDate"
*/
public static String generateCacheKey(DcTrafficSectionData dcTrafficSectionData) {
// 获取设备ID
Long deviceId = dcTrafficSectionData.getDeviceId();
// 获取并格式化统计日期
String formattedDate = formatDate(dcTrafficSectionData.getStatisticalDate());
// 使用"|"字符连接设备ID和日期,形成唯一键
return deviceId + "|" + formattedDate;
}
/**
* 清除所有过期的交通断面数据缓存项
*/
public static void clearExpiredData() {
// 使用stream API找出所有过期的数据缓存项键
Set<String> keysToRemove = cache.keySet().stream()
.filter(QuarterlyTrafficStatisticsCache::isCacheItemExpire)
.collect(Collectors.toSet());
// 安全地从缓存中删除这些过期项
keysToRemove.forEach(cache::remove);
}
/**
* 检查给定缓存键所对应的缓存项是否已经过期
*
* @param key 缓存key
* @return 如果已过期则返回true否则返回false
*/
private static boolean isCacheItemExpire(String key) {
Date lastAddedTime = cache.get(key).getLastAddedTime();
Date currentTime = DateUtil.date();
long betweenSecond = DateUtil.between(lastAddedTime, currentTime, DateUnit.SECOND);
return betweenSecond > MAX_CACHE_TIME;
}
/**
* Date 类型的日期格式化为 "yyyy-MM-01" 格式字符串
*
* @param date 需要格式化的日期对象
* @return 格式化后的日期字符串格式为 "yyyy-MM-01"
*/
private static String formatDate(Date date) {
// 使用 DateUtil 工具类将 date 格式化为指定格式的字符串
return DateUtil.format(date, "yyyy-MM-01");
}
}

143
zc-business/src/main/java/com/zc/business/statistics/cache/YearlyTrafficStatisticsCache.java

@ -0,0 +1,143 @@
package com.zc.business.statistics.cache;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import com.zc.business.domain.DcTrafficSectionData;
import com.zc.business.enums.TrafficDataPeriodTypeEnum;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* 以年为单位的交通交通断面数据缓存类用于存储和管理设备上报的交通断面统计数据同时提供了数据缓存的有效性管理和清理功能
* @author xiepufeng
*/
@Getter
@Setter
public class YearlyTrafficStatisticsCache extends AbstractTrafficStatisticsCache {
// 静态缓存容器,使用ConcurrentHashMap保证线程安全
@Getter
private static final Map<String, YearlyTrafficStatisticsCache> cache = new ConcurrentHashMap<>();
// 最大缓存时间(单位:秒)
private static final long MAX_CACHE_TIME = (60 * 60) * (366 * 24 + 1);
// 最大容量限制,防止内存溢出
private static final int MAX_CAPACITY = 4 + 1000; // 缓存的最大条目数
// 私有构造函数,确保只能通过静态方法获取实例
private YearlyTrafficStatisticsCache() {
super();
this.setData(new HashSet<>());
}
/**
* 添加交通断面数据到缓存中
*
* @param dcTrafficSectionData 待添加的交通断面统计数据
*/
public static void addCacheData(DcTrafficSectionData dcTrafficSectionData) {
// 获取或新建对应的缓存实例
YearlyTrafficStatisticsCache instance = getInstance(dcTrafficSectionData);
// 检查缓存容量是否达到上限
if (instance.getData().size() >= MAX_CAPACITY) {
instance.getLogger().error("交通断面数据缓存出现异常,最大缓存量达到设定上线 {}, 当前设备是 {}", MAX_CAPACITY, dcTrafficSectionData.getDeviceId());
return;
}
// 更新最后添加时间
instance.setLastAddedTime(DateUtil.date());
// 更新状态
instance.setStored(false);
String formattedDate = formatDate(dcTrafficSectionData.getStatisticalDate());
String key = generateCacheKey(dcTrafficSectionData);
// 设置key和thatDay属性(仅在首次添加时)
if (instance.getCacheKey() == null) {
instance.setCacheKey(key);
instance.setStatisticalDateStr(formattedDate);
}
dcTrafficSectionData.setStatisticalDate(dcTrafficSectionData.getStatisticalDate(), TrafficDataPeriodTypeEnum.YEAR);
// 将新数据添加到数据列表中
instance.getData().add(dcTrafficSectionData);
}
/**
* 获取或创建对应设备与日期的DcTrafficSectionDataCache实例
*
* @param dcTrafficSectionData 交通断面数据统计定义
* @return 对应的交通断面数据缓存实例
*/
private static YearlyTrafficStatisticsCache getInstance(DcTrafficSectionData dcTrafficSectionData) {
// 使用toKey方法生成唯一键,并根据此键计算并返回缓存实例
return cache.computeIfAbsent(generateCacheKey(dcTrafficSectionData), k -> new YearlyTrafficStatisticsCache());
}
/**
* 该方法用于生成一个基于DcTrafficSectionData对象属性的唯一键字符串
* 这个键由设备ID与统计日期两部分组成通常用于索引或标识特定设备在某一日期的交通流量数据
*
* @param dcTrafficSectionData DcTrafficSectionData类型的对象包含设备ID和统计日期等信息
* @return 格式化后的字符串键格式为 "deviceId|formattedDate"
*/
public static String generateCacheKey(DcTrafficSectionData dcTrafficSectionData) {
// 获取设备ID
Long deviceId = dcTrafficSectionData.getDeviceId();
// 获取并格式化统计日期
String formattedDate = formatDate(dcTrafficSectionData.getStatisticalDate());
// 使用"|"字符连接设备ID和日期,形成唯一键
return deviceId + "|" + formattedDate;
}
/**
* 清除所有过期的交通断面数据缓存项
*/
public static void clearExpiredData() {
// 使用stream API找出所有过期的数据缓存项键
Set<String> keysToRemove = cache.keySet().stream()
.filter(YearlyTrafficStatisticsCache::isCacheItemExpire)
.collect(Collectors.toSet());
// 安全地从缓存中删除这些过期项
keysToRemove.forEach(cache::remove);
}
/**
* 检查给定缓存键所对应的缓存项是否已经过期
*
* @param key 缓存key
* @return 如果已过期则返回true否则返回false
*/
private static boolean isCacheItemExpire(String key) {
Date lastAddedTime = cache.get(key).getLastAddedTime();
Date currentTime = DateUtil.date();
long betweenSecond = DateUtil.between(lastAddedTime, currentTime, DateUnit.SECOND);
return betweenSecond > MAX_CACHE_TIME;
}
/**
* Date 类型的日期格式化为 "yyyy-01-01" 格式的字符串
*
* @param date 需要格式化的日期对象
* @return 格式化后的日期字符串
*/
private static String formatDate(Date date) {
// 使用 DateUtil 工具类将 date 格式化为 "yyyy-01-01" 格式的字符串
return DateUtil.format(date, "yyyy-01-01");
}
}

70
zc-business/src/main/java/com/zc/business/statistics/handler/RealtimeTrafficStatistics.java

@ -0,0 +1,70 @@
package com.zc.business.statistics.handler;
import com.ruoyi.common.utils.DateUtils;
import com.zc.business.domain.DcTrafficSectionData;
import com.zc.business.enums.TrafficDataPeriodTypeEnum;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
public class RealtimeTrafficStatistics {
/**
* 对给定的交通数据集合进行统计分析返回一个综合交通数据对象
*
* @param dataCollection 交通数据集合不可为null或空包含多个交通路段的详细数据
* @param trafficDataPeriodType 交通数据的时段类型例如小时周等
* @return 综合交通数据对象包含车流量总和平均车速等统计结果如果输入数据为空则返回null
*/
public static DcTrafficSectionData trafficStatistics(Collection<DcTrafficSectionData> dataCollection, TrafficDataPeriodTypeEnum trafficDataPeriodType) {
// 判断输入数据是否为空
if (CollectionUtils.isEmpty(dataCollection)) {
return null;
}
// 创建一个汇总统计用的对象
DcTrafficSectionData aggregatedData = new DcTrafficSectionData();
// 初始化车流量总和和计算平均车速所需的分子部分
int trafficVolume = 0;
double numerator = 0;
// 遍历原始数据列表,累加车流量并计算平均车速
for (DcTrafficSectionData data: dataCollection) {
// 累加车流量
trafficVolume += data.getTrafficVolume();
// 计算分子部分
numerator += data.getAverageSpeed() * data.getTrafficVolume();
}
// 使用第一个数据项的信息填充汇总统计对象的基本属性(设备ID、桩号、方向)
DcTrafficSectionData firstDcTrafficSectionData = dataCollection.iterator().next();
// 设备id
aggregatedData.setDeviceId(firstDcTrafficSectionData.getDeviceId());
// 桩号
aggregatedData.setStakeMark(firstDcTrafficSectionData.getStakeMark());
// 道路方向
aggregatedData.setDirection(firstDcTrafficSectionData.getDirection());
// 计算平均车速并设置到汇总统计对象中
if (trafficVolume != 0) {
aggregatedData.setAverageSpeed((int) Math.round(numerator / trafficVolume));
} else {
// 若车流量为0,则默认设置平均车速为0
aggregatedData.setAverageSpeed(0);
}
// 时段类型
aggregatedData.setPeriodType(trafficDataPeriodType);
// 设置统计时间
aggregatedData.setStatisticalDate(firstDcTrafficSectionData.getStatisticalDate(), trafficDataPeriodType);
// 车流量
aggregatedData.setTrafficVolume(trafficVolume);
// 更新或插入操作
aggregatedData.setUpdateTime(DateUtils.getNowDate());
// 生成主键
aggregatedData.generateUniqueId();
return aggregatedData;
}
}

44
zc-business/src/main/resources/mapper/business/DcTrafficSectionDataMapper.xml

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zc.business.mapper.DcTrafficSectionDataMapper">
<!-- 插入或更新交通路段数据 -->
<insert id="insertOrUpdate" parameterType="com.zc.business.domain.DcTrafficSectionData">
INSERT INTO
dc_traffic_section_data
(
id,
traffic_volume,
average_speed,
device_id,
statistical_date,
direction,
period_type,
stake_mark,
create_time
)
VALUES
(
#{id},
#{trafficVolume},
#{averageSpeed},
#{deviceId},
#{statisticalDate},
#{direction},
#{periodType},
#{stakeMark},
NOW())
ON DUPLICATE KEY UPDATE
traffic_volume = VALUES(traffic_volume),
average_speed = VALUES(average_speed),
device_id = VALUES(device_id),
statistical_date = VALUES(statistical_date),
direction = VALUES(direction),
period_type = VALUES(period_type),
stake_mark = VALUES(stake_mark),
update_time = VALUES(NOW())
</insert>
</mapper>
Loading…
Cancel
Save