|
|
|
|
@ -0,0 +1,363 @@
|
|
|
|
|
package org.dromara.ems.report.service.impl.support;
|
|
|
|
|
|
|
|
|
|
import cn.hutool.core.collection.CollUtil;
|
|
|
|
|
import org.dromara.ems.record.domain.RecordIotenvInstant;
|
|
|
|
|
import org.dromara.ems.report.domain.vo.vibrationboard.VibrationAnomalyPageVo;
|
|
|
|
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
|
|
|
import java.math.RoundingMode;
|
|
|
|
|
import java.time.ZoneId;
|
|
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
|
import java.util.Comparator;
|
|
|
|
|
import java.util.LinkedHashMap;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.function.Function;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 振动异常分析器。
|
|
|
|
|
*
|
|
|
|
|
* <p>将振动采样数据按四种最常见异常场景检测:
|
|
|
|
|
* 1) 单点高风险 2) 相邻点突变 3) 连续超标 4) 小时桶内抖动。</p>
|
|
|
|
|
* <p>无状态工具类,可安全作为单例或 static 常量使用。</p>
|
|
|
|
|
*/
|
|
|
|
|
public class VibrationAnomalyAnalyzer {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 高风险事件:单点值 >= highThreshold 的记录。
|
|
|
|
|
* 按值倒序取 TOP200——前端只展示“最严重”的部分,避免列表过长影响体验。
|
|
|
|
|
*/
|
|
|
|
|
public List<VibrationAnomalyPageVo.HighEventItem> buildHighEvents(List<RecordIotenvInstant> rows,
|
|
|
|
|
Function<RecordIotenvInstant, BigDecimal> metricExtractor,
|
|
|
|
|
BigDecimal highThreshold) {
|
|
|
|
|
// 对所有采样行进行流式处理:过滤→排序→截取→映射→收集
|
|
|
|
|
return rows.stream()
|
|
|
|
|
// 过滤出指标值不为空且 >= 高风险阈值的记录
|
|
|
|
|
.filter(row -> {
|
|
|
|
|
// 通过提取函数获取当前行的指标值
|
|
|
|
|
BigDecimal value = metricExtractor.apply(row);
|
|
|
|
|
// 保留指标值非空且达到或超过高阈值的记录
|
|
|
|
|
return value != null && value.compareTo(highThreshold) >= 0;
|
|
|
|
|
})
|
|
|
|
|
// 按指标值倒序排列,值相同时按时间倒序(最严重、最近的排前面)
|
|
|
|
|
.sorted(Comparator.comparing((RecordIotenvInstant row) -> metricExtractor.apply(row), Comparator.nullsLast(Comparator.reverseOrder()))
|
|
|
|
|
.thenComparing(RecordIotenvInstant::getRecodeTime, Comparator.nullsLast(Comparator.reverseOrder())))
|
|
|
|
|
// 取 TOP200 条最严重的记录,避免前端列表过长影响体验
|
|
|
|
|
.limit(200)
|
|
|
|
|
// 将每条记录映射为高风险事件项 VO
|
|
|
|
|
.map(row -> {
|
|
|
|
|
// 创建高风险事件项实例
|
|
|
|
|
VibrationAnomalyPageVo.HighEventItem item = new VibrationAnomalyPageVo.HighEventItem();
|
|
|
|
|
// 设置设备ID
|
|
|
|
|
item.setMonitorId(row.getMonitorId());
|
|
|
|
|
// 设置设备名称
|
|
|
|
|
item.setMonitorName(row.getMonitorName());
|
|
|
|
|
// 设置指标值,保留两位小数
|
|
|
|
|
item.setValue(metricExtractor.apply(row).setScale(2, RoundingMode.HALF_UP));
|
|
|
|
|
// 设置记录时间
|
|
|
|
|
item.setRecodeTime(row.getRecodeTime());
|
|
|
|
|
// 返回构建好的事件项
|
|
|
|
|
return item;
|
|
|
|
|
})
|
|
|
|
|
// 收集为不可变列表返回
|
|
|
|
|
.toList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 变化过快事件:相邻采样点差值 >= rapidRiseThreshold。
|
|
|
|
|
* 先按设备分组再按时间排序,确保只比较同一台设备的前后采样点,
|
|
|
|
|
* 而不是不同设备的数据交叉对比——否则会产生大量误报。
|
|
|
|
|
*/
|
|
|
|
|
public List<VibrationAnomalyPageVo.RapidRiseEventItem> buildRapidRiseEvents(List<RecordIotenvInstant> rows,
|
|
|
|
|
Function<RecordIotenvInstant, BigDecimal> metricExtractor,
|
|
|
|
|
BigDecimal rapidRiseThreshold) {
|
|
|
|
|
// 创建可变结果列表,用于收集所有变化过快事件
|
|
|
|
|
List<VibrationAnomalyPageVo.RapidRiseEventItem> result = new ArrayList<>();
|
|
|
|
|
// 按设备分组并按时间排序,确保只比较同一台设备的前后采样点
|
|
|
|
|
Map<String, List<RecordIotenvInstant>> grouped = groupByMonitorAndSort(rows);
|
|
|
|
|
// 遍历每台设备的时间有序采样数据
|
|
|
|
|
grouped.forEach((monitorId, monitorRows) -> {
|
|
|
|
|
// 从第二个采样点开始,逐一与前一个采样点比较差值
|
|
|
|
|
for (int i = 1; i < monitorRows.size(); i++) {
|
|
|
|
|
// 提取当前采样点的指标值
|
|
|
|
|
BigDecimal currentValue = metricExtractor.apply(monitorRows.get(i));
|
|
|
|
|
// 提取前一个采样点的指标值
|
|
|
|
|
BigDecimal previousValue = metricExtractor.apply(monitorRows.get(i - 1));
|
|
|
|
|
// 任一值为空则跳过,无法计算差值
|
|
|
|
|
if (currentValue == null || previousValue == null) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
// 计算当前点与前一点的差值;正值表示上升,负值表示下降
|
|
|
|
|
BigDecimal diff = currentValue.subtract(previousValue);
|
|
|
|
|
// 急升和急降都属于突变信号,所以按绝对值判断是否达到变化过快阈值
|
|
|
|
|
if (diff.abs().compareTo(rapidRiseThreshold) >= 0) {
|
|
|
|
|
// 创建变化过快事件项实例
|
|
|
|
|
VibrationAnomalyPageVo.RapidRiseEventItem item = new VibrationAnomalyPageVo.RapidRiseEventItem();
|
|
|
|
|
// 设置设备ID
|
|
|
|
|
item.setMonitorId(monitorId);
|
|
|
|
|
// 设置设备名称
|
|
|
|
|
item.setMonitorName(monitorRows.get(i).getMonitorName());
|
|
|
|
|
// 设置差值,保留两位小数
|
|
|
|
|
item.setDiff(diff.setScale(2, RoundingMode.HALF_UP));
|
|
|
|
|
// 设置发生时间(取当前点的时间)
|
|
|
|
|
item.setRecodeTime(monitorRows.get(i).getRecodeTime());
|
|
|
|
|
// 将事件添加到结果列表
|
|
|
|
|
result.add(item);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
// 按变化幅度绝对值倒序排列并取 TOP200 返回,确保急降不会因为负号被排到后面
|
|
|
|
|
return result.stream()
|
|
|
|
|
.sorted(Comparator.comparing((VibrationAnomalyPageVo.RapidRiseEventItem item) -> item.getDiff().abs(), Comparator.reverseOrder()))
|
|
|
|
|
.limit(200)
|
|
|
|
|
.toList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 连续超标事件:连续 >= minSamples 个采样点超过 warningThreshold。
|
|
|
|
|
* 这类异常对应“慢性劣化”场景(如轴承磨损),单点可能不触发高阈值,
|
|
|
|
|
* 但持续偏高本身就是预警信号。
|
|
|
|
|
* 算法:状态机扫描,维护 startIndex + maxValue,中断时判定是否达到最小样本数。
|
|
|
|
|
*/
|
|
|
|
|
public List<VibrationAnomalyPageVo.ContinuousEventItem> buildContinuousEvents(List<RecordIotenvInstant> rows,
|
|
|
|
|
Function<RecordIotenvInstant, BigDecimal> metricExtractor,
|
|
|
|
|
BigDecimal warningThreshold,
|
|
|
|
|
Integer minSamples) {
|
|
|
|
|
// 创建可变结果列表,用于收集所有连续超标事件
|
|
|
|
|
List<VibrationAnomalyPageVo.ContinuousEventItem> result = new ArrayList<>();
|
|
|
|
|
// 按设备分组并按时间排序,确保同一设备的采样点按时间顺序可追溯
|
|
|
|
|
Map<String, List<RecordIotenvInstant>> grouped = groupByMonitorAndSort(rows);
|
|
|
|
|
// 遍历每台设备的时间有序采样数据,使用状态机检测连续超标段
|
|
|
|
|
grouped.forEach((monitorId, monitorRows) -> {
|
|
|
|
|
// 状态机变量:连续超标段的起始索引,-1 表示当前不在超标段内
|
|
|
|
|
int startIndex = -1;
|
|
|
|
|
// 状态机变量:当前连续超标段内出现的最大指标值
|
|
|
|
|
BigDecimal maxValue = BigDecimal.ZERO;
|
|
|
|
|
// 逐一扫描每个采样点
|
|
|
|
|
for (int i = 0; i < monitorRows.size(); i++) {
|
|
|
|
|
// 提取当前采样点的指标值
|
|
|
|
|
BigDecimal value = metricExtractor.apply(monitorRows.get(i));
|
|
|
|
|
// 判断当前点是否超过预警阈值(flagged=true 表示超标)
|
|
|
|
|
boolean flagged = value != null && value.compareTo(warningThreshold) >= 0;
|
|
|
|
|
// 当前点超标且尚未进入超标段时,标记为超标段起点
|
|
|
|
|
if (flagged && startIndex < 0) {
|
|
|
|
|
// 记录超标段的起始索引
|
|
|
|
|
startIndex = i;
|
|
|
|
|
// 初始化超标段内最大值
|
|
|
|
|
maxValue = value;
|
|
|
|
|
}
|
|
|
|
|
// 当前点超标时,持续更新超标段内最大值
|
|
|
|
|
if (flagged && value != null) {
|
|
|
|
|
// 取当前值与已记录最大值中的较大者
|
|
|
|
|
maxValue = maxValue.max(value);
|
|
|
|
|
}
|
|
|
|
|
// 超标段中断(当前点未超标)或到达数据末尾时,判定并结算超标段
|
|
|
|
|
if ((!flagged || i == monitorRows.size() - 1) && startIndex >= 0) {
|
|
|
|
|
// 确定超标段的结束索引:如果当前点仍超标(末尾情况)则包含当前点
|
|
|
|
|
int endIndex = flagged ? i : i - 1;
|
|
|
|
|
// 计算超标段包含的采样点数
|
|
|
|
|
int sampleCount = endIndex - startIndex + 1;
|
|
|
|
|
// 仅当连续超标点数 >= 最小要求时才记录为事件
|
|
|
|
|
if (sampleCount >= minSamples) {
|
|
|
|
|
// 创建连续超标事件项实例
|
|
|
|
|
VibrationAnomalyPageVo.ContinuousEventItem item = new VibrationAnomalyPageVo.ContinuousEventItem();
|
|
|
|
|
// 设置设备ID
|
|
|
|
|
item.setMonitorId(monitorId);
|
|
|
|
|
// 设置设备名称(取超标段起始点的设备名)
|
|
|
|
|
item.setMonitorName(monitorRows.get(startIndex).getMonitorName());
|
|
|
|
|
// 设置超标段开始时间
|
|
|
|
|
item.setStartTime(monitorRows.get(startIndex).getRecodeTime());
|
|
|
|
|
// 设置超标段结束时间
|
|
|
|
|
item.setEndTime(monitorRows.get(endIndex).getRecodeTime());
|
|
|
|
|
// 设置超标段内的峰值,保留两位小数
|
|
|
|
|
item.setMaxValue(maxValue.setScale(2, RoundingMode.HALF_UP));
|
|
|
|
|
// 设置超标段内的采样点数
|
|
|
|
|
item.setSampleCount(sampleCount);
|
|
|
|
|
// 将事件添加到结果列表
|
|
|
|
|
result.add(item);
|
|
|
|
|
}
|
|
|
|
|
// 重置状态机,准备检测下一个超标段
|
|
|
|
|
startIndex = -1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
// 按连续超标采样点数倒序排列并取 TOP100,优先展示持续时间最长的事件
|
|
|
|
|
return result.stream()
|
|
|
|
|
.sorted(Comparator.comparing(VibrationAnomalyPageVo.ContinuousEventItem::getSampleCount, Comparator.reverseOrder()))
|
|
|
|
|
.limit(100)
|
|
|
|
|
.toList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 抖动异常:按“设备+小时”分桶,统计桶内标准差 >= stddevThreshold 的桶。
|
|
|
|
|
* 这类异常对应“传感器报数不稳定”或“设备产生间歇性异常振动”场景。
|
|
|
|
|
* 单点 < 2 的桶无法计算标准差,直接跳过。
|
|
|
|
|
*/
|
|
|
|
|
public List<VibrationAnomalyPageVo.JitterEventItem> buildJitterEvents(List<RecordIotenvInstant> rows,
|
|
|
|
|
Function<RecordIotenvInstant, BigDecimal> metricExtractor,
|
|
|
|
|
BigDecimal stddevThreshold) {
|
|
|
|
|
// 使用 LinkedHashMap 按"设备ID_小时桶"分组,保持插入顺序
|
|
|
|
|
Map<String, List<BigDecimal>> bucketMap = new LinkedHashMap<>();
|
|
|
|
|
// 用于记录每个桶 key 对应的设备名称,供后续构建事件项时使用
|
|
|
|
|
Map<String, String> monitorNameMap = new LinkedHashMap<>();
|
|
|
|
|
// 遍历所有采样记录,按设备和小时维度分桶收集指标值
|
|
|
|
|
for (RecordIotenvInstant row : rows) {
|
|
|
|
|
// 通过提取函数获取当前行的指标值
|
|
|
|
|
BigDecimal value = metricExtractor.apply(row);
|
|
|
|
|
// 跳过指标值为空或记录时间为空的行
|
|
|
|
|
if (value == null || row.getRecodeTime() == null) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
// 将记录时间格式化为小时级别的时间桶(如 "2026-03-26 14:00:00")
|
|
|
|
|
String hourBucket = formatDate(row.getRecodeTime(), "yyyy-MM-dd HH") + ":00:00";
|
|
|
|
|
// 拼接"设备ID_小时桶"作为分桶 key,确保不同设备同一小时不会混淆
|
|
|
|
|
String key = row.getMonitorId() + "_" + hourBucket;
|
|
|
|
|
// 将指标值追加到对应桶中,桶不存在时自动创建
|
|
|
|
|
bucketMap.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
|
|
|
|
|
// 记录该桶对应的设备名称(同一桶内设备名相同,覆盖写入即可)
|
|
|
|
|
monitorNameMap.put(key, row.getMonitorName());
|
|
|
|
|
}
|
|
|
|
|
// 创建可变结果列表,用于收集所有抖动异常事件
|
|
|
|
|
List<VibrationAnomalyPageVo.JitterEventItem> result = new ArrayList<>();
|
|
|
|
|
// 遍历每个"设备_小时"桶,计算桶内标准差并判断是否超过抖动阈值
|
|
|
|
|
bucketMap.forEach((key, values) -> {
|
|
|
|
|
// 桶内样本数 < 2 时无法计算标准差,直接跳过
|
|
|
|
|
if (values.size() < 2) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 计算桶内所有指标值的总体标准差
|
|
|
|
|
BigDecimal stddev = stddev(values);
|
|
|
|
|
// 标准差达到或超过抖动阈值时,记录为抖动异常事件
|
|
|
|
|
if (stddev.compareTo(stddevThreshold) >= 0) {
|
|
|
|
|
// 按第一个下划线拆分 key,parts[0]=设备ID,parts[1]=小时桶时间
|
|
|
|
|
String[] parts = key.split("_", 2);
|
|
|
|
|
// 创建抖动异常事件项实例
|
|
|
|
|
VibrationAnomalyPageVo.JitterEventItem item = new VibrationAnomalyPageVo.JitterEventItem();
|
|
|
|
|
// 设置设备ID
|
|
|
|
|
item.setMonitorId(parts[0]);
|
|
|
|
|
// 设置设备名称(从名称映射表中获取)
|
|
|
|
|
item.setMonitorName(monitorNameMap.get(key));
|
|
|
|
|
// 设置小时桶时间标识
|
|
|
|
|
item.setHourBucket(parts[1]);
|
|
|
|
|
// 设置该桶的标准差值
|
|
|
|
|
item.setStddev(stddev);
|
|
|
|
|
// 设置该桶内的采样点数
|
|
|
|
|
item.setSampleCount(values.size());
|
|
|
|
|
// 将事件添加到结果列表
|
|
|
|
|
result.add(item);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
// 按标准差倒序排列并取 TOP100,优先展示抖动最剧烈的时段
|
|
|
|
|
return result.stream()
|
|
|
|
|
.sorted(Comparator.comparing(VibrationAnomalyPageVo.JitterEventItem::getStddev, Comparator.reverseOrder()))
|
|
|
|
|
.limit(100)
|
|
|
|
|
.toList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 按设备分组并按时间排序。
|
|
|
|
|
* 分组后再排序——确保同一设备的采样点按时间顺序可追溯,
|
|
|
|
|
* 这是“变化过快”和“连续超标”算法的前置条件。
|
|
|
|
|
*/
|
|
|
|
|
private Map<String, List<RecordIotenvInstant>> groupByMonitorAndSort(List<RecordIotenvInstant> rows) {
|
|
|
|
|
// 使用 LinkedHashMap 按设备ID分组,保持插入顺序
|
|
|
|
|
Map<String, List<RecordIotenvInstant>> grouped = new LinkedHashMap<>();
|
|
|
|
|
// 遍历所有行,按设备ID分组到各自列表中
|
|
|
|
|
for (RecordIotenvInstant row : rows) {
|
|
|
|
|
// 将当前行追加到对应设备的列表中,列表不存在时自动创建
|
|
|
|
|
grouped.computeIfAbsent(row.getMonitorId(), key -> new ArrayList<>()).add(row);
|
|
|
|
|
}
|
|
|
|
|
// 对每台设备的采样数据按时间升序排列,时间相同时按主键 objid 排序保证稳定性
|
|
|
|
|
grouped.values().forEach(monitorRows -> monitorRows.sort(
|
|
|
|
|
Comparator.comparing(RecordIotenvInstant::getRecodeTime, Comparator.nullsFirst(Comparator.naturalOrder()))
|
|
|
|
|
.thenComparing(RecordIotenvInstant::getObjid, Comparator.nullsFirst(Comparator.naturalOrder()))
|
|
|
|
|
));
|
|
|
|
|
// 返回分组并排序后的 Map
|
|
|
|
|
return grouped;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 标准差计算:使用总体统计标准差(除以 N 而非 N-1)。
|
|
|
|
|
* 报表场景只关心“一小时内波动是否可接受”,总体方差足够。
|
|
|
|
|
* BigDecimal 开方采用牛顿迭代,避免 double 往返的精度损失。
|
|
|
|
|
*/
|
|
|
|
|
private BigDecimal stddev(List<BigDecimal> values) {
|
|
|
|
|
// 空列表或仅一个元素时无法计算标准差,返回零值(保留4位小数)
|
|
|
|
|
if (CollUtil.isEmpty(values) || values.size() < 2) {
|
|
|
|
|
// 返回 0.0000 作为默认标准差
|
|
|
|
|
return BigDecimal.ZERO.setScale(4, RoundingMode.HALF_UP);
|
|
|
|
|
}
|
|
|
|
|
// 计算列表所有值的均值,作为方差计算的基准
|
|
|
|
|
BigDecimal avgValue = avg(values);
|
|
|
|
|
// 计算总体方差:各值与均值差的平方和 / N,保留8位小数减少中间截断误差
|
|
|
|
|
BigDecimal variance = values.stream()
|
|
|
|
|
// 计算每个值与均值的差
|
|
|
|
|
.map(value -> value.subtract(avgValue))
|
|
|
|
|
// 将差值平方
|
|
|
|
|
.map(value -> value.multiply(value))
|
|
|
|
|
// 累加所有平方差
|
|
|
|
|
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
|
|
|
|
// 除以样本总数 N(总体方差),保留8位小数
|
|
|
|
|
.divide(BigDecimal.valueOf(values.size()), 8, RoundingMode.HALF_UP);
|
|
|
|
|
// 采用 BigDecimal 牛顿迭代开方,避免 double 往返带来的精度抖动。
|
|
|
|
|
return sqrt(variance, 4);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 均值计算:保留 6 位小数以减少后续方差计算的累积截断误差。
|
|
|
|
|
* 比 Service 层的 avg(2 位)精度更高,因为此处作为标准差计算的中间步骤。
|
|
|
|
|
*/
|
|
|
|
|
private BigDecimal avg(List<BigDecimal> values) {
|
|
|
|
|
// 空列表返回零值(保留两位小数),避免上层做空判断
|
|
|
|
|
if (CollUtil.isEmpty(values)) {
|
|
|
|
|
// 返回 0.00 作为默认均值
|
|
|
|
|
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
|
|
|
|
}
|
|
|
|
|
// 对列表所有元素求和,初始值为零
|
|
|
|
|
BigDecimal sum = values.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
|
|
|
// 求和除以元素个数得到均值,保留6位小数(比展示层多,减少标准差计算的累积截断误差)
|
|
|
|
|
return sum.divide(BigDecimal.valueOf(values.size()), 6, RoundingMode.HALF_UP);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* BigDecimal 牛顿迭代开方。
|
|
|
|
|
* 先用 Math.sqrt(double) 获得初始猜测值(足够接近),再迭代收敛到指定精度。
|
|
|
|
|
* 收敛条件很紧(scale+4 位),通常 2~3 次迭代即可结束。
|
|
|
|
|
*/
|
|
|
|
|
private BigDecimal sqrt(BigDecimal value, int scale) {
|
|
|
|
|
// 空值或非正数直接返回零(无法对负数开方)
|
|
|
|
|
if (value == null || value.signum() <= 0) {
|
|
|
|
|
// 返回指定精度的零值
|
|
|
|
|
return BigDecimal.ZERO.setScale(scale, RoundingMode.HALF_UP);
|
|
|
|
|
}
|
|
|
|
|
// 定义常量2,用于牛顿迭代公式 x_new = (x + value/x) / 2
|
|
|
|
|
BigDecimal two = BigDecimal.valueOf(2);
|
|
|
|
|
// 设定收敛精度阈值,比目标精度多4位以确保最终结果足够精确
|
|
|
|
|
BigDecimal precision = BigDecimal.ONE.movePointLeft(scale + 4);
|
|
|
|
|
// 用 Math.sqrt(double) 获得初始猜测值(足够接近真实值,加速迭代收敛)
|
|
|
|
|
BigDecimal current = BigDecimal.valueOf(Math.sqrt(value.doubleValue()));
|
|
|
|
|
// 上一轮的迭代结果,用于判断是否收敛
|
|
|
|
|
BigDecimal previous;
|
|
|
|
|
// 牛顿迭代循环:x_new = (x + value/x) / 2,直到相邻两次迭代差 < 精度阈值
|
|
|
|
|
do {
|
|
|
|
|
// 保存当前值作为上一轮结果
|
|
|
|
|
previous = current;
|
|
|
|
|
// 执行一步牛顿迭代:(current + value/current) / 2,中间计算保留 scale+8 位小数
|
|
|
|
|
current = current.add(value.divide(current, scale + 8, RoundingMode.HALF_UP))
|
|
|
|
|
.divide(two, scale + 8, RoundingMode.HALF_UP);
|
|
|
|
|
// 当相邻两次迭代结果之差的绝对值 <= 精度阈值时退出循环
|
|
|
|
|
} while (current.subtract(previous).abs().compareTo(precision) > 0);
|
|
|
|
|
// 将最终结果截断到指定精度并返回
|
|
|
|
|
return current.setScale(scale, RoundingMode.HALF_UP);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 日期格式化工具:统一使用系统时区,确保与数据库存储时区一致。
|
|
|
|
|
*/
|
|
|
|
|
private String formatDate(java.util.Date date, String pattern) {
|
|
|
|
|
// 将 Date 转为系统时区的 LocalDateTime,再按指定模式格式化为字符串返回
|
|
|
|
|
return DateTimeFormatter.ofPattern(pattern)
|
|
|
|
|
.format(date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
|
|
|
|
|
}
|
|
|
|
|
}
|