Merge remote-tracking branch 'origin/master' into feat/iot-data-storage
This commit is contained in:
@@ -2,9 +2,10 @@ package com.viewsh.module.iot.api.device;
|
||||
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.module.iot.api.device.dto.IotDeviceSimpleRespDTO;
|
||||
import com.viewsh.module.iot.controller.admin.device.vo.device.IotDeviceRespVO;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.product.IotProductDO;
|
||||
import com.viewsh.module.iot.service.device.IotDeviceService;
|
||||
import com.viewsh.module.iot.service.product.IotProductService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -14,8 +15,8 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
import static com.viewsh.module.iot.api.device.IotDeviceQueryApi.PREFIX;
|
||||
@@ -36,55 +37,74 @@ public class IotDeviceQueryApiImpl implements IotDeviceQueryApi {
|
||||
@Resource
|
||||
private IotDeviceService deviceService;
|
||||
|
||||
@Resource
|
||||
private IotProductService productService;
|
||||
|
||||
@Override
|
||||
@GetMapping(PREFIX + "/simple-list")
|
||||
@Operation(summary = "获取设备精简列表(按类型/产品筛选)")
|
||||
public CommonResult<List<IotDeviceSimpleRespDTO>> getDeviceSimpleList(
|
||||
@RequestParam(value = "deviceType", required = false) Integer deviceType,
|
||||
@RequestParam(value = "productId", required = false) Long productId) {
|
||||
try {
|
||||
List<IotDeviceDO> list = deviceService.getDeviceListByCondition(deviceType, productId);
|
||||
List<IotDeviceSimpleRespDTO> result = list.stream()
|
||||
.map(device -> {
|
||||
IotDeviceSimpleRespDTO dto = new IotDeviceSimpleRespDTO();
|
||||
dto.setId(device.getId());
|
||||
dto.setDeviceName(device.getDeviceName());
|
||||
dto.setProductId(device.getProductId());
|
||||
dto.setProductKey(device.getProductKey());
|
||||
// TODO: 从产品服务获取产品名称
|
||||
dto.setProductName("产品_" + device.getProductKey());
|
||||
return dto;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
return success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("[getDeviceSimpleList] 查询设备列表失败: deviceType={}, productId={}", deviceType, productId, e);
|
||||
return success(List.of());
|
||||
}
|
||||
List<IotDeviceDO> list = deviceService.getDeviceListByCondition(deviceType, productId);
|
||||
List<IotDeviceSimpleRespDTO> result = list.stream()
|
||||
.map(this::toSimpleDTO)
|
||||
.toList();
|
||||
return success(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
@GetMapping(PREFIX + "/get")
|
||||
@Operation(summary = "获取设备详情")
|
||||
public CommonResult<IotDeviceSimpleRespDTO> getDevice(@RequestParam("id") Long id) {
|
||||
try {
|
||||
IotDeviceDO device = deviceService.getDevice(id);
|
||||
if (device == null) {
|
||||
return success(null);
|
||||
}
|
||||
|
||||
IotDeviceSimpleRespDTO dto = new IotDeviceSimpleRespDTO();
|
||||
dto.setId(device.getId());
|
||||
dto.setDeviceName(device.getDeviceName());
|
||||
dto.setProductId(device.getProductId());
|
||||
dto.setProductKey(device.getProductKey());
|
||||
dto.setProductName("产品_" + device.getProductKey());
|
||||
|
||||
return success(dto);
|
||||
} catch (Exception e) {
|
||||
log.error("[getDevice] 查询设备详情失败: id={}", id, e);
|
||||
IotDeviceDO device = deviceService.getDevice(id);
|
||||
if (device == null) {
|
||||
return success(null);
|
||||
}
|
||||
return success(toSimpleDTO(device));
|
||||
}
|
||||
|
||||
@Override
|
||||
@GetMapping(PREFIX + "/batch-get")
|
||||
@Operation(summary = "批量获取设备详情")
|
||||
public CommonResult<List<IotDeviceSimpleRespDTO>> batchGetDevices(
|
||||
@RequestParam("ids") Collection<Long> ids) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return success(List.of());
|
||||
}
|
||||
List<IotDeviceDO> devices = deviceService.getDeviceList(ids);
|
||||
List<IotDeviceSimpleRespDTO> result = devices.stream()
|
||||
.map(this::toSimpleDTO)
|
||||
.toList();
|
||||
return success(result);
|
||||
}
|
||||
|
||||
private IotDeviceSimpleRespDTO toSimpleDTO(IotDeviceDO device) {
|
||||
IotDeviceSimpleRespDTO dto = new IotDeviceSimpleRespDTO();
|
||||
dto.setId(device.getId());
|
||||
dto.setDeviceName(device.getDeviceName());
|
||||
dto.setProductId(device.getProductId());
|
||||
dto.setProductKey(device.getProductKey());
|
||||
dto.setNickname(device.getNickname());
|
||||
dto.setSerialNumber(device.getSerialNumber());
|
||||
dto.setState(device.getState());
|
||||
dto.setDeviceType(device.getDeviceType());
|
||||
// 从产品缓存获取产品名称
|
||||
dto.setProductName(resolveProductName(device.getProductId()));
|
||||
return dto;
|
||||
}
|
||||
|
||||
private String resolveProductName(Long productId) {
|
||||
if (productId == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
IotProductDO product = productService.getProductFromCache(productId);
|
||||
return product != null ? product.getName() : null;
|
||||
} catch (Exception e) {
|
||||
log.warn("[resolveProductName] 获取产品名称失败: productId={}, error={}", productId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.viewsh.module.iot.controller.rpc;
|
||||
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.module.iot.api.trajectory.DeviceLocationDTO;
|
||||
import com.viewsh.module.iot.api.trajectory.TrajectoryStateApi;
|
||||
import com.viewsh.module.iot.dal.redis.clean.TrajectoryStateRedisDAO;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 轨迹实时状态 RPC Controller
|
||||
* <p>
|
||||
* 实现 {@link TrajectoryStateApi},从 Redis 读取设备当前位置
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@RestController
|
||||
@Validated
|
||||
public class TrajectoryStateController implements TrajectoryStateApi {
|
||||
|
||||
@Resource
|
||||
private TrajectoryStateRedisDAO trajectoryStateRedisDAO;
|
||||
|
||||
@Override
|
||||
public CommonResult<DeviceLocationDTO> getCurrentLocation(Long deviceId) {
|
||||
TrajectoryStateRedisDAO.CurrentAreaInfo currentArea = trajectoryStateRedisDAO.getCurrentArea(deviceId);
|
||||
|
||||
if (currentArea == null) {
|
||||
return success(DeviceLocationDTO.builder()
|
||||
.deviceId(deviceId)
|
||||
.inArea(false)
|
||||
.build());
|
||||
}
|
||||
|
||||
return success(DeviceLocationDTO.builder()
|
||||
.deviceId(deviceId)
|
||||
.areaId(currentArea.getAreaId())
|
||||
.enterTime(currentArea.getEnterTime())
|
||||
.beaconMac(currentArea.getBeaconMac())
|
||||
.inArea(true)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,31 @@ public class BeaconPresenceConfig {
|
||||
private String beaconMac;
|
||||
|
||||
/**
|
||||
* <EFBFBD><EFBFBD>动窗口配置
|
||||
* iBeacon UUID
|
||||
* <p>
|
||||
* iBeacon 协议的 UUID 标识,用于区分不同的信标组
|
||||
* 例如:FDA50693-A4E2-4FB1-AFCF-C6EB07647825
|
||||
*/
|
||||
private String beaconUuid;
|
||||
|
||||
/**
|
||||
* iBeacon Major 值
|
||||
* <p>
|
||||
* 用于区分同一 UUID 下的不同区域或楼层
|
||||
* 范围:0-65535
|
||||
*/
|
||||
private Integer major;
|
||||
|
||||
/**
|
||||
* iBeacon Minor 值
|
||||
* <p>
|
||||
* 用于区分同一 Major 下的不同信标
|
||||
* 范围:0-65535
|
||||
*/
|
||||
private Integer minor;
|
||||
|
||||
/**
|
||||
* 滑动窗口配置
|
||||
*/
|
||||
private WindowConfig window;
|
||||
|
||||
|
||||
@@ -12,6 +12,27 @@ import lombok.Data;
|
||||
@Data
|
||||
public class TrafficThresholdConfig {
|
||||
|
||||
/**
|
||||
* 上报模式:INCREMENTAL / CUMULATIVE
|
||||
*/
|
||||
public static final String REPORT_MODE_INCREMENTAL = "INCREMENTAL";
|
||||
public static final String REPORT_MODE_CUMULATIVE = "CUMULATIVE";
|
||||
|
||||
/**
|
||||
* 设备上报模式
|
||||
* <p>
|
||||
* INCREMENTAL(默认):设备上报增量值,直接使用
|
||||
* CUMULATIVE:设备上报累计值,需计算差值得到增量
|
||||
*/
|
||||
private String reportMode;
|
||||
|
||||
/**
|
||||
* 是否累计值模式
|
||||
*/
|
||||
public boolean isCumulative() {
|
||||
return REPORT_MODE_CUMULATIVE.equalsIgnoreCase(reportMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发阈值
|
||||
* <p>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.viewsh.module.iot.dal.dataobject.integration.clean;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 轨迹记录功能配置
|
||||
* <p>
|
||||
* 存储在 iot_device.config JSON 字段中,与 ButtonEventConfig 同级
|
||||
* <pre>
|
||||
* {
|
||||
* "buttonEvent": { ... },
|
||||
* "trajectoryTracking": { "enabled": true }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Data
|
||||
public class TrajectoryTrackingConfig {
|
||||
|
||||
/**
|
||||
* 是否启用轨迹记录
|
||||
*/
|
||||
private Boolean enabled;
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.viewsh.module.iot.dal.redis.clean;
|
||||
|
||||
import com.viewsh.framework.tenant.core.context.TenantContextHolder;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.data.redis.core.Cursor;
|
||||
import org.springframework.data.redis.core.ScanOptions;
|
||||
@@ -54,6 +55,7 @@ public class TrafficCounterRedisDAO {
|
||||
private static final String FIELD_TOTAL_OUT = "totalOut";
|
||||
private static final String FIELD_LAST_PERSISTED_IN = "lastPersistedIn";
|
||||
private static final String FIELD_LAST_PERSISTED_OUT = "lastPersistedOut";
|
||||
private static final String FIELD_TENANT_ID = "tenantId";
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
|
||||
|
||||
@@ -124,6 +126,8 @@ public class TrafficCounterRedisDAO {
|
||||
|
||||
/**
|
||||
* 原子递增当日累积统计
|
||||
* <p>
|
||||
* 同时记录当前租户ID,供持久化任务使用
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @param date 日期
|
||||
@@ -138,6 +142,11 @@ public class TrafficCounterRedisDAO {
|
||||
if (peopleOut > 0) {
|
||||
stringRedisTemplate.opsForHash().increment(key, FIELD_TOTAL_OUT, peopleOut);
|
||||
}
|
||||
// 记录租户ID(幂等写入,同一设备始终属于同一租户)
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (tenantId != null) {
|
||||
stringRedisTemplate.opsForHash().putIfAbsent(key, FIELD_TENANT_ID, String.valueOf(tenantId));
|
||||
}
|
||||
// 设置 TTL(幂等,每次都设置保证不过期)
|
||||
stringRedisTemplate.expire(key, DAILY_TTL_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
@@ -235,6 +244,58 @@ public class TrafficCounterRedisDAO {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==================== 累计值设备上次值存取 ====================
|
||||
|
||||
/**
|
||||
* 累计值设备上次上报值 Key 模式
|
||||
* <p>
|
||||
* 格式:iot:clean:traffic:lastvalue:{deviceId}
|
||||
* Hash Field:people_in / people_out
|
||||
*/
|
||||
private static final String LAST_VALUE_KEY_PATTERN = "iot:clean:traffic:lastvalue:%s";
|
||||
|
||||
/**
|
||||
* 累计值 TTL(秒)- 7 天,跨天不丢失,设备离线数天后仍能衔接
|
||||
*/
|
||||
private static final int LAST_VALUE_TTL_SECONDS = 604800;
|
||||
|
||||
/**
|
||||
* 获取设备上次上报的累计值
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @param identifier 属性标识符(people_in / people_out)
|
||||
* @return 上次累计值,首次上报时返回 null
|
||||
*/
|
||||
public Long getLastCumulativeValue(Long deviceId, String identifier) {
|
||||
String key = String.format(LAST_VALUE_KEY_PATTERN, deviceId);
|
||||
Object value = stringRedisTemplate.opsForHash().get(key, identifier);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设备上次上报的累计值
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @param identifier 属性标识符(people_in / people_out)
|
||||
* @param value 当前累计值
|
||||
*/
|
||||
public void setLastCumulativeValue(Long deviceId, String identifier, long value) {
|
||||
String key = String.format(LAST_VALUE_KEY_PATTERN, deviceId);
|
||||
stringRedisTemplate.opsForHash().put(key, identifier, String.valueOf(value));
|
||||
// 仅在 key 无 TTL 时设置,避免高频上报场景下每次都执行 EXPIRE
|
||||
Long ttl = stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
|
||||
if (ttl == null || ttl == -1) {
|
||||
stringRedisTemplate.expire(key, LAST_VALUE_TTL_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
private static String formatThresholdKey(Long deviceId, Long areaId) {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.viewsh.module.iot.dal.redis.clean;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 轨迹检测 RSSI 滑动窗口 Redis DAO
|
||||
* <p>
|
||||
* 与 BeaconRssiWindowRedisDAO 功能相同,使用独立的 Key 前缀,
|
||||
* 避免与保洁到岗检测的窗口数据互相干扰
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Repository
|
||||
public class TrajectoryRssiWindowRedisDAO {
|
||||
|
||||
private static final String WINDOW_KEY_PATTERN = "iot:trajectory:rssi:window:%s:%s";
|
||||
private static final int WINDOW_TTL_SECONDS = 3600;
|
||||
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
/**
|
||||
* 更新滑动窗口(保留最近 N 个样本)
|
||||
*/
|
||||
public void updateWindow(Long deviceId, Long areaId, Integer rssi, Integer maxSize) {
|
||||
String key = formatKey(deviceId, areaId);
|
||||
List<Integer> window = getWindow(deviceId, areaId);
|
||||
window.add(rssi);
|
||||
if (window.size() > maxSize) {
|
||||
window = window.subList(window.size() - maxSize, window.size());
|
||||
}
|
||||
String windowStr = window.stream()
|
||||
.map(String::valueOf)
|
||||
.collect(Collectors.joining(","));
|
||||
stringRedisTemplate.opsForValue().set(key, windowStr, WINDOW_TTL_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取窗口样本
|
||||
*/
|
||||
public List<Integer> getWindow(Long deviceId, Long areaId) {
|
||||
String key = formatKey(deviceId, areaId);
|
||||
String windowStr = stringRedisTemplate.opsForValue().get(key);
|
||||
if (windowStr == null || windowStr.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return List.of(windowStr.split(","))
|
||||
.stream()
|
||||
.map(Integer::parseInt)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除窗口
|
||||
*/
|
||||
public void clearWindow(Long deviceId, Long areaId) {
|
||||
String key = formatKey(deviceId, areaId);
|
||||
stringRedisTemplate.delete(key);
|
||||
}
|
||||
|
||||
private static String formatKey(Long deviceId, Long areaId) {
|
||||
return String.format(WINDOW_KEY_PATTERN, deviceId, areaId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.viewsh.module.iot.dal.redis.clean;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 轨迹实时状态 Redis DAO
|
||||
* <p>
|
||||
* 维护每个设备当前所在区域的实时状态
|
||||
* <p>
|
||||
* Key: iot:trajectory:current:{deviceId}
|
||||
* Type: Hash
|
||||
* Fields: areaId, enterTime, beaconMac
|
||||
* TTL: 24小时
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Slf4j
|
||||
@Repository
|
||||
public class TrajectoryStateRedisDAO {
|
||||
|
||||
private static final String CURRENT_KEY_PATTERN = "iot:trajectory:current:%s";
|
||||
private static final int CURRENT_TTL_SECONDS = 86400; // 24小时
|
||||
|
||||
private static final String FIELD_AREA_ID = "areaId";
|
||||
private static final String FIELD_ENTER_TIME = "enterTime";
|
||||
private static final String FIELD_BEACON_MAC = "beaconMac";
|
||||
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
/**
|
||||
* 设置当前所在区域
|
||||
*/
|
||||
public void setCurrentArea(Long deviceId, Long areaId, long enterTime, String beaconMac) {
|
||||
String key = formatKey(deviceId);
|
||||
// 先删后写,清除旧字段(如上次的 beaconMac),保证一致性
|
||||
stringRedisTemplate.delete(key);
|
||||
Map<String, String> fields = new HashMap<>();
|
||||
fields.put(FIELD_AREA_ID, String.valueOf(areaId));
|
||||
fields.put(FIELD_ENTER_TIME, String.valueOf(enterTime));
|
||||
if (beaconMac != null) {
|
||||
fields.put(FIELD_BEACON_MAC, beaconMac);
|
||||
}
|
||||
stringRedisTemplate.opsForHash().putAll(key, fields);
|
||||
stringRedisTemplate.expire(key, CURRENT_TTL_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前所在区域信息
|
||||
*
|
||||
* @return 当前区域信息,如果不在任何区域则返回 null
|
||||
*/
|
||||
public CurrentAreaInfo getCurrentArea(Long deviceId) {
|
||||
String key = formatKey(deviceId);
|
||||
Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(key);
|
||||
if (entries == null || entries.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object areaIdObj = entries.get(FIELD_AREA_ID);
|
||||
Object enterTimeObj = entries.get(FIELD_ENTER_TIME);
|
||||
if (areaIdObj == null || enterTimeObj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CurrentAreaInfo info = new CurrentAreaInfo();
|
||||
info.setAreaId(Long.parseLong(areaIdObj.toString()));
|
||||
info.setEnterTime(Long.parseLong(enterTimeObj.toString()));
|
||||
Object beaconMacObj = entries.get(FIELD_BEACON_MAC);
|
||||
if (beaconMacObj != null) {
|
||||
info.setBeaconMac(beaconMacObj.toString());
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除当前区域状态
|
||||
*/
|
||||
public void clearCurrentArea(Long deviceId) {
|
||||
String key = formatKey(deviceId);
|
||||
stringRedisTemplate.delete(key);
|
||||
}
|
||||
|
||||
private static String formatKey(Long deviceId) {
|
||||
return String.format(CURRENT_KEY_PATTERN, deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前区域信息
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class CurrentAreaInfo {
|
||||
private Long areaId;
|
||||
private Long enterTime;
|
||||
private String beaconMac;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.viewsh.module.iot.service.integration.clean;
|
||||
|
||||
import com.viewsh.module.iot.dal.dataobject.integration.clean.BeaconPresenceConfig;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Beacon 全量注册表服务
|
||||
* <p>
|
||||
* 为轨迹检测功能提供 beaconMac → {areaId, config} 的全量映射
|
||||
* <p>
|
||||
* 数据来源:Ops 模块的 ops_area_device_relation 表(relationType=BEACON)
|
||||
* 缓存:Redis Hash,1小时 TTL,启动预热 + 定时刷新
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
public interface BeaconRegistryService {
|
||||
|
||||
/**
|
||||
* 获取所有已注册的 Beacon 配置
|
||||
*
|
||||
* @return beaconMac(大写) → BeaconAreaInfo 的映射
|
||||
*/
|
||||
Map<String, BeaconAreaInfo> getAllBeaconConfigs();
|
||||
|
||||
/**
|
||||
* 刷新 Beacon 注册表缓存
|
||||
*/
|
||||
void refreshRegistry();
|
||||
|
||||
/**
|
||||
* Beacon 与区域的关联信息
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
class BeaconAreaInfo {
|
||||
/**
|
||||
* 区域ID
|
||||
*/
|
||||
private Long areaId;
|
||||
/**
|
||||
* Beacon 检测配置
|
||||
*/
|
||||
private BeaconPresenceConfig config;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package com.viewsh.module.iot.service.integration.clean;
|
||||
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.framework.common.util.json.JsonUtils;
|
||||
import com.viewsh.module.iot.dal.dataobject.integration.clean.BeaconPresenceConfig;
|
||||
import com.viewsh.module.iot.dal.dataobject.integration.clean.CleanOrderIntegrationConfig;
|
||||
import com.viewsh.module.ops.api.area.AreaDeviceApi;
|
||||
import com.viewsh.module.ops.api.area.AreaDeviceDTO;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Beacon 全量注册表服务实现
|
||||
* <p>
|
||||
* 缓存策略:
|
||||
* - Redis Hash: iot:trajectory:beacon:registry,field=beaconMac,value=JSON{areaId,config}
|
||||
* - TTL: 1小时
|
||||
* - 启动时预热,每30分钟定时刷新
|
||||
* - 如果 Redis 缓存失效,降级为实时调用 Feign API
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class BeaconRegistryServiceImpl implements BeaconRegistryService {
|
||||
|
||||
private static final String REGISTRY_KEY = "iot:trajectory:beacon:registry";
|
||||
private static final int REGISTRY_TTL_SECONDS = 3600; // 1小时
|
||||
|
||||
@Resource
|
||||
private AreaDeviceApi areaDeviceApi;
|
||||
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@Resource
|
||||
private CleanOrderIntegrationConfigService configService;
|
||||
|
||||
/**
|
||||
* 启动时预热缓存
|
||||
*/
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
try {
|
||||
refreshRegistry();
|
||||
} catch (Exception e) {
|
||||
log.warn("[BeaconRegistry] 启动预热失败,将在下次查询时加载", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 每30分钟刷新一次注册表
|
||||
*/
|
||||
@Scheduled(fixedDelay = 30 * 60 * 1000, initialDelay = 30 * 60 * 1000)
|
||||
public void scheduledRefresh() {
|
||||
try {
|
||||
refreshRegistry();
|
||||
} catch (Exception e) {
|
||||
log.error("[BeaconRegistry] 定时刷新失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, BeaconAreaInfo> getAllBeaconConfigs() {
|
||||
// 1. 尝试从 Redis 读取
|
||||
try {
|
||||
Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(REGISTRY_KEY);
|
||||
if (entries != null && !entries.isEmpty()) {
|
||||
Map<String, BeaconAreaInfo> result = new HashMap<>(entries.size());
|
||||
for (Map.Entry<Object, Object> entry : entries.entrySet()) {
|
||||
String mac = entry.getKey().toString();
|
||||
BeaconAreaInfo info = JsonUtils.parseObject(entry.getValue().toString(), BeaconAreaInfo.class);
|
||||
if (info != null) {
|
||||
result.put(mac, info);
|
||||
}
|
||||
}
|
||||
log.debug("[BeaconRegistry] 从缓存加载 {} 个 Beacon 配置", result.size());
|
||||
return result;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[BeaconRegistry] 读取缓存失败,降级为实时查询", e);
|
||||
}
|
||||
|
||||
// 2. 缓存未命中,实时查询并写入缓存
|
||||
return loadAndCacheRegistry();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshRegistry() {
|
||||
loadAndCacheRegistry();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Ops 加载全量 Beacon 注册表并写入 Redis 缓存
|
||||
*/
|
||||
private Map<String, BeaconAreaInfo> loadAndCacheRegistry() {
|
||||
Map<String, BeaconAreaInfo> registry = new HashMap<>();
|
||||
|
||||
try {
|
||||
CommonResult<List<AreaDeviceDTO>> result = areaDeviceApi.getAllEnabledBeacons();
|
||||
if (result == null || !result.isSuccess() || result.getData() == null) {
|
||||
log.warn("[BeaconRegistry] 调用 Ops 获取 Beacon 列表失败");
|
||||
return registry;
|
||||
}
|
||||
|
||||
Map<String, String> redisEntries = new HashMap<>();
|
||||
|
||||
for (AreaDeviceDTO dto : result.getData()) {
|
||||
if (dto.getConfigData() == null || dto.getAreaId() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 反序列化配置
|
||||
CleanOrderIntegrationConfig integrationConfig = configService.convertConfig(dto.getConfigData());
|
||||
if (integrationConfig == null || integrationConfig.getBeaconPresence() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
BeaconPresenceConfig beaconConfig = integrationConfig.getBeaconPresence();
|
||||
if (beaconConfig.getEnabled() == null || !beaconConfig.getEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String mac = beaconConfig.getBeaconMac();
|
||||
if (mac == null || mac.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 统一转大写
|
||||
mac = mac.toUpperCase();
|
||||
|
||||
BeaconAreaInfo info = new BeaconAreaInfo(dto.getAreaId(), beaconConfig);
|
||||
registry.put(mac, info);
|
||||
redisEntries.put(mac, JsonUtils.toJsonString(info));
|
||||
}
|
||||
|
||||
// 写入 Redis(使用临时 Key + rename 保证原子性,避免竞态空窗期)
|
||||
if (!redisEntries.isEmpty()) {
|
||||
String tempKey = REGISTRY_KEY + ":tmp:" + System.currentTimeMillis();
|
||||
stringRedisTemplate.opsForHash().putAll(tempKey, redisEntries);
|
||||
stringRedisTemplate.expire(tempKey, REGISTRY_TTL_SECONDS, TimeUnit.SECONDS);
|
||||
stringRedisTemplate.rename(tempKey, REGISTRY_KEY);
|
||||
} else {
|
||||
// 所有 Beacon 都被禁用时,清除旧缓存
|
||||
stringRedisTemplate.delete(REGISTRY_KEY);
|
||||
}
|
||||
|
||||
log.info("[BeaconRegistry] 加载并缓存 {} 个 Beacon 配置", registry.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[BeaconRegistry] 加载 Beacon 注册表失败", e);
|
||||
}
|
||||
|
||||
return registry;
|
||||
}
|
||||
}
|
||||
@@ -112,17 +112,43 @@ public class CleanOrderIntegrationConfigServiceImpl implements CleanOrderIntegra
|
||||
// 2. 缓存未命中,调用 Ops 模块
|
||||
CommonResult<List<AreaDeviceDTO>> result = areaDeviceApi.getDevicesByAreaAndType(areaId, relationType);
|
||||
|
||||
if (result == null || !result.isSuccess() || result.getData() == null) {
|
||||
log.warn("[CleanOrderConfig] 调用 Ops 模块获取区域配置失败:areaId={}, relationType={}", areaId, relationType);
|
||||
if (result == null || !result.isSuccess() || result.getData() == null || result.getData().isEmpty()) {
|
||||
log.warn("[CleanOrderConfig] 调用 Ops 模块获取区域配置为空或失败:areaId={}, relationType={}", areaId, relationType);
|
||||
// 写入防缓存穿透标记 (1分钟)
|
||||
try {
|
||||
stringRedisTemplate.opsForValue().set(cacheKey, NULL_CACHE, 1, TimeUnit.MINUTES);
|
||||
} catch (Exception e) {
|
||||
log.warn("[CleanOrderConfig] 写入防击穿缓存失败:areaId={}, relationType={}", areaId, relationType, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 返回第一个启用的配置
|
||||
return result.getData().stream()
|
||||
// 3. 返回第一个启用的配置,并主动写入缓存 (模拟 Ops 端的缓存策略,24小时)
|
||||
AreaDeviceDTO validDto = result.getData().stream()
|
||||
.filter(dto -> dto.getEnabled() != null && dto.getEnabled())
|
||||
.findFirst()
|
||||
.map(this::wrapDto)
|
||||
.orElse(null);
|
||||
|
||||
if (validDto != null) {
|
||||
try {
|
||||
stringRedisTemplate.opsForValue().set(
|
||||
cacheKey,
|
||||
JsonUtils.toJsonString(validDto),
|
||||
24,
|
||||
TimeUnit.HOURS);
|
||||
} catch (Exception e) {
|
||||
log.warn("[CleanOrderConfig] 写入区域配置缓存失败:areaId={}, relationType={}", areaId, relationType, e);
|
||||
}
|
||||
return wrapDto(validDto);
|
||||
} else {
|
||||
// 没有找到启用的配置,写入 NULL_CACHE
|
||||
try {
|
||||
stringRedisTemplate.opsForValue().set(cacheKey, NULL_CACHE, 1, TimeUnit.MINUTES);
|
||||
} catch (Exception e) {
|
||||
log.warn("[CleanOrderConfig] 写入防击穿缓存失败:areaId={}, relationType={}", areaId, relationType, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.viewsh.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import com.viewsh.module.iot.service.rule.clean.processor.BeaconDetectionRuleProcessor;
|
||||
import com.viewsh.module.iot.service.rule.clean.processor.ButtonEventRuleProcessor;
|
||||
import com.viewsh.module.iot.service.rule.clean.processor.TrafficThresholdRuleProcessor;
|
||||
import com.viewsh.module.iot.service.rule.clean.processor.TrajectoryDetectionProcessor;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -33,6 +34,9 @@ public class CleanRuleProcessorManager {
|
||||
@Resource
|
||||
private ButtonEventRuleProcessor buttonEventRuleProcessor;
|
||||
|
||||
@Resource
|
||||
private TrajectoryDetectionProcessor trajectoryDetectionProcessor;
|
||||
|
||||
/**
|
||||
* 处理设备消息
|
||||
* <p>
|
||||
@@ -70,6 +74,15 @@ public class CleanRuleProcessorManager {
|
||||
// 属性上报:直接遍历 key-value
|
||||
data.forEach((identifier, value) ->
|
||||
processDataSafely(deviceId, identifier, value));
|
||||
|
||||
// 4. 蓝牙信号缺失补偿:当设备上报了属性但不含 bluetoothDevices 时,
|
||||
// 主动注入一次 null 调用,使 BeaconDetectionRuleProcessor 能写入 -999(信号缺失),
|
||||
// 避免退出检测窗口因无数据而停滞
|
||||
if (!data.containsKey("bluetoothDevices")) {
|
||||
beaconDetectionRuleProcessor.processPropertyChange(deviceId, "bluetoothDevices", null);
|
||||
// 轨迹检测同样需要信号丢失补偿,注入 null 使窗口写入 -999
|
||||
trajectoryDetectionProcessor.processPropertyChange(deviceId, "bluetoothDevices", null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,8 +132,11 @@ public class CleanRuleProcessorManager {
|
||||
switch (identifier) {
|
||||
case "people_in", "people_out" ->
|
||||
trafficThresholdRuleProcessor.processPropertyChange(deviceId, identifier, value);
|
||||
case "bluetoothDevices" ->
|
||||
beaconDetectionRuleProcessor.processPropertyChange(deviceId, identifier, value);
|
||||
case "bluetoothDevices" -> {
|
||||
beaconDetectionRuleProcessor.processPropertyChange(deviceId, identifier, value);
|
||||
// 轨迹检测:独立于保洁到岗检测,匹配所有已知 Beacon
|
||||
trajectoryDetectionProcessor.processPropertyChange(deviceId, identifier, value);
|
||||
}
|
||||
default -> {
|
||||
// 其他属性/事件忽略
|
||||
}
|
||||
|
||||
@@ -145,13 +145,16 @@ public class RssiSlidingWindowDetector {
|
||||
|
||||
/**
|
||||
* 从蓝牙设备列表中提取目标信标的 RSSI
|
||||
* <p>
|
||||
* 匹配策略:以设备上报数据为准,优先使用 MAC 地址匹配;
|
||||
* 若 MAC 未配置,降级为 iBeacon 三元组(uuid+major+minor)匹配
|
||||
*
|
||||
* @param bluetoothDevices 蓝牙设备列表
|
||||
* 格式:[{"rssi":-52,"type":243,"mac":"F0:C8:60:1D:10:BB"},...]
|
||||
* @param targetMac 目标信标 MAC 地址
|
||||
* @param beaconConfig 信标配置
|
||||
* @return RSSI 值,如果未找到返回 -999(缺失值)
|
||||
*/
|
||||
public Integer extractTargetRssi(Object bluetoothDevices, String targetMac) {
|
||||
public Integer extractTargetRssi(Object bluetoothDevices, BeaconPresenceConfig beaconConfig) {
|
||||
if (bluetoothDevices == null) {
|
||||
return -999;
|
||||
}
|
||||
@@ -160,17 +163,59 @@ public class RssiSlidingWindowDetector {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<java.util.Map<String, Object>> deviceList = (List<java.util.Map<String, Object>>) bluetoothDevices;
|
||||
|
||||
return deviceList.stream()
|
||||
.filter(device -> targetMac.equals(device.get("mac")))
|
||||
.map(device -> (Integer) device.get("rssi"))
|
||||
.findFirst()
|
||||
.orElse(-999);
|
||||
// 优先使用 MAC 地址匹配(工牌上报的是 MAC)
|
||||
String targetMac = beaconConfig.getBeaconMac();
|
||||
if (targetMac != null) {
|
||||
return deviceList.stream()
|
||||
.filter(device -> targetMac.equalsIgnoreCase((String) device.get("mac")))
|
||||
.map(device -> toInt(device.get("rssi")))
|
||||
.findFirst()
|
||||
.orElse(-999);
|
||||
}
|
||||
|
||||
// 降级为 iBeacon 三元组匹配(适配上报 uuid+major+minor 的设备)
|
||||
if (beaconConfig.getBeaconUuid() != null && beaconConfig.getMajor() != null
|
||||
&& beaconConfig.getMinor() != null) {
|
||||
return deviceList.stream()
|
||||
.filter(device -> matchIBeacon(device, beaconConfig))
|
||||
.map(device -> toInt(device.get("rssi")))
|
||||
.findFirst()
|
||||
.orElse(-999);
|
||||
}
|
||||
|
||||
log.warn("[RssiDetector] 信标配置缺少 beaconMac 和 iBeacon 三元组,无法匹配");
|
||||
return -999;
|
||||
} catch (Exception e) {
|
||||
log.error("[RssiDetector] 解析蓝牙数据失败:targetMac={}", targetMac, e);
|
||||
log.error("[RssiDetector] 解析蓝牙数据失败:beaconMac={}", beaconConfig.getBeaconMac(), e);
|
||||
return -999;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配 iBeacon 三元组(uuid + major + minor)
|
||||
*/
|
||||
private boolean matchIBeacon(java.util.Map<String, Object> device, BeaconPresenceConfig config) {
|
||||
String uuid = (String) device.get("uuid");
|
||||
Integer major = toInt(device.get("major"));
|
||||
Integer minor = toInt(device.get("minor"));
|
||||
return config.getBeaconUuid().equalsIgnoreCase(uuid)
|
||||
&& config.getMajor().equals(major)
|
||||
&& config.getMinor().equals(minor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全转换为 Integer
|
||||
*/
|
||||
private Integer toInt(Object value) {
|
||||
if (value instanceof Integer) {
|
||||
return (Integer) value;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).intValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断窗口是否有效
|
||||
* <p>
|
||||
|
||||
@@ -120,7 +120,7 @@ public class BeaconDetectionRuleProcessor {
|
||||
}
|
||||
|
||||
// 4. 解析蓝牙数据,提取目标信标的 RSSI
|
||||
Integer targetRssi = detector.extractTargetRssi(propertyValue, beaconConfig.getBeaconMac());
|
||||
Integer targetRssi = detector.extractTargetRssi(propertyValue, beaconConfig);
|
||||
|
||||
log.debug("[BeaconDetection] 提取RSSI:deviceId={}, areaId={}, beaconMac={}, rssi={}",
|
||||
deviceId, areaId, beaconConfig.getBeaconMac(), targetRssi);
|
||||
@@ -226,12 +226,7 @@ public class BeaconDetectionRuleProcessor {
|
||||
// 首次丢失
|
||||
signalLossRedisDAO.recordFirstLoss(deviceId, areaId, System.currentTimeMillis());
|
||||
|
||||
// 2. 发送警告
|
||||
publishTtsEvent(deviceId, "你已离开当前区域," +
|
||||
(exitConfig.getLossTimeoutMinutes() > 0 ? exitConfig.getLossTimeoutMinutes() + "分钟内工单将自动结算"
|
||||
: "工单将自动结算"));
|
||||
|
||||
// 3. 发布审<E5B883><E5AEA1><EFBFBD>日志
|
||||
// 2. 发布审计日志
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("firstLossTime", System.currentTimeMillis());
|
||||
data.put("rssi", window.isEmpty() ? -999 : window.get(window.size() - 1));
|
||||
|
||||
@@ -74,74 +74,62 @@ public class ButtonEventRuleProcessor {
|
||||
|
||||
log.debug("[ButtonEvent] 按键解析成功:deviceId={}, buttonId={}", deviceId, buttonId);
|
||||
|
||||
// 4. 匹配按键类型并处理
|
||||
if (buttonId.equals(buttonConfig.getConfirmKeyId())) {
|
||||
// 确认键
|
||||
handleConfirmButton(deviceId, buttonId);
|
||||
} else if (buttonId.equals(buttonConfig.getQueryKeyId())) {
|
||||
// 查询键
|
||||
handleQueryButton(deviceId, buttonId);
|
||||
// 4. 匹配按键类型并处理(确认键和查询键统一路由到同一逻辑)
|
||||
if (buttonId.equals(buttonConfig.getConfirmKeyId())
|
||||
|| buttonId.equals(buttonConfig.getQueryKeyId())) {
|
||||
// 所有已知按键统一走绿色按键逻辑(根据工单状态智能判断行为)
|
||||
handleGreenButton(deviceId, buttonId);
|
||||
} else {
|
||||
log.debug("[ButtonEvent] 未配置的按键:deviceId={}, buttonId={}", deviceId, buttonId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理确认按键
|
||||
* 处理绿色按键(统一按键逻辑)
|
||||
* <p>
|
||||
* 保洁员按下确认键,确认接收工单
|
||||
* 根据当前工单状态智能判断行为:
|
||||
* - 无工单:发布查询事件(Ops 端播报"没有工单")
|
||||
* - DISPATCHED:发布确认事件(触发确认状态转换 + 停止循环 + 播报地点)
|
||||
* - CONFIRMED/ARRIVED:发布查询事件(播报地点)
|
||||
* - 其他状态:发布查询事件(兜底处理)
|
||||
*/
|
||||
private void handleConfirmButton(Long deviceId, Integer buttonId) {
|
||||
|
||||
log.info("[ButtonEvent] 确认键按下:deviceId={}, buttonId={}", deviceId, buttonId);
|
||||
|
||||
// 1. 查询设备当前工单
|
||||
BadgeDeviceStatusRedisDAO.OrderInfo currentOrder = badgeDeviceStatusRedisDAO.getCurrentOrder(deviceId);
|
||||
if (currentOrder == null) {
|
||||
log.warn("[ButtonEvent] 设备无当前工单,跳过确认:deviceId={}", deviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
Long orderId = currentOrder.getOrderId();
|
||||
|
||||
// 2. 防重复检查(短时间内同一工单的确认操作去重)
|
||||
String dedupKey = String.format("iot:clean:button:dedup:confirm:%s:%s", deviceId, orderId);
|
||||
Boolean firstTime = stringRedisTemplate.opsForValue()
|
||||
.setIfAbsent(dedupKey, "1", 10, java.util.concurrent.TimeUnit.SECONDS);
|
||||
|
||||
if (!firstTime) {
|
||||
log.info("[ButtonEvent] 确认操作重复,跳过:deviceId={}, orderId={}", deviceId, orderId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 发布工单确认事件
|
||||
publishConfirmEvent(deviceId, orderId, buttonId);
|
||||
|
||||
log.info("[ButtonEvent] 发布工单确认事件:deviceId={}, orderId={}", deviceId, orderId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理查询按键
|
||||
* <p>
|
||||
* 保洁员按下查询键,查询当前工单信息
|
||||
*/
|
||||
private void handleQueryButton(Long deviceId, Integer buttonId) {
|
||||
|
||||
log.info("[ButtonEvent] 查询键按下:deviceId={}, buttonId={}", deviceId, buttonId);
|
||||
private void handleGreenButton(Long deviceId, Integer buttonId) {
|
||||
log.info("[ButtonEvent] 绿色按键按下:deviceId={}, buttonId={}", deviceId, buttonId);
|
||||
|
||||
// 1. 查询设备当前工单
|
||||
BadgeDeviceStatusRedisDAO.OrderInfo currentOrder = badgeDeviceStatusRedisDAO.getCurrentOrder(deviceId);
|
||||
if (currentOrder == null) {
|
||||
// 无工单 → 发布查询事件(Ops 端播报"没有工单")
|
||||
log.info("[ButtonEvent] 设备无当前工单:deviceId={}", deviceId);
|
||||
// 发布查询结果事件(无工单)
|
||||
publishQueryEvent(deviceId, null, buttonId, "当前无工单");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 发布查询事件
|
||||
publishQueryEvent(deviceId, currentOrder.getOrderId(), buttonId, "查询当前工单");
|
||||
Long orderId = currentOrder.getOrderId();
|
||||
String orderStatus = currentOrder.getStatus();
|
||||
|
||||
log.info("[ButtonEvent] 发布工单查询事件:deviceId={}, orderId={}", deviceId, currentOrder.getOrderId());
|
||||
// 2. 根据工单状态智能分派
|
||||
if ("DISPATCHED".equals(orderStatus)) {
|
||||
// DISPATCHED → 发布确认事件(触发确认 + 停止循环 + 播报地点)
|
||||
// 防重复检查
|
||||
String dedupKey = String.format("iot:clean:button:dedup:confirm:%s:%s", deviceId, orderId);
|
||||
Boolean firstTime = stringRedisTemplate.opsForValue()
|
||||
.setIfAbsent(dedupKey, "1", 10, java.util.concurrent.TimeUnit.SECONDS);
|
||||
|
||||
if (!Boolean.TRUE.equals(firstTime)) {
|
||||
// 重复确认不再静默,改为发查询事件给保洁员反馈(播报地点)
|
||||
log.info("[ButtonEvent] 确认操作重复,转为查询:deviceId={}, orderId={}", deviceId, orderId);
|
||||
publishQueryEvent(deviceId, orderId, buttonId, "重复确认,查询当前工单");
|
||||
return;
|
||||
}
|
||||
|
||||
publishConfirmEvent(deviceId, orderId, buttonId);
|
||||
log.info("[ButtonEvent] DISPATCHED状态,发布确认事件:deviceId={}, orderId={}", deviceId, orderId);
|
||||
} else {
|
||||
// CONFIRMED / ARRIVED / 其他状态 → 发布查询事件(播报地点)
|
||||
publishQueryEvent(deviceId, orderId, buttonId, "查询当前工单");
|
||||
log.info("[ButtonEvent] {}状态,发布查询事件:deviceId={}, orderId={}", orderStatus, deviceId, orderId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -191,15 +191,16 @@ public class SignalLossRuleProcessor {
|
||||
long minValidWorkMillis = exitConfig.getMinValidWorkMinutes() * 60000L;
|
||||
|
||||
// 6. 分支处理:有效 vs 无效作业
|
||||
if (durationMs < minValidWorkMillis) {
|
||||
// 作业时长不足,抑制完成
|
||||
handleInvalidWork(deviceId, badgeDeviceKey, areaId,
|
||||
durationMs, minValidWorkMillis, exitConfig);
|
||||
} else {
|
||||
// 作业时长有效,触发完成
|
||||
handleTimeoutComplete(deviceId, badgeDeviceKey, areaId,
|
||||
durationMs, lastLossTime);
|
||||
}
|
||||
// TODO 暂时取消作业时长不足抑制自动完成的逻辑,所有情况均触发完成
|
||||
// if (durationMs < minValidWorkMillis) {
|
||||
// // 作业时长不足,抑制完成
|
||||
// handleInvalidWork(deviceId, badgeDeviceKey, areaId,
|
||||
// durationMs, minValidWorkMillis, exitConfig);
|
||||
// } else {
|
||||
// 作业时长有效,触发完成
|
||||
handleTimeoutComplete(deviceId, badgeDeviceKey, areaId,
|
||||
durationMs, lastLossTime);
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -47,12 +47,16 @@ public class TrafficThresholdRuleProcessor {
|
||||
* 处理客流属性上报
|
||||
* <p>
|
||||
* 支持 people_in 和 people_out 两个属性:
|
||||
* - people_in:累加到阈值计数器 + 当日统计
|
||||
* - people_out:仅累加到当日统计
|
||||
* - people_in:累加到当日统计 + 阈值计数器(需配置)
|
||||
* - people_out:累加到当日统计
|
||||
* <p>
|
||||
* 支持两种上报模式(通过 configData.trafficThreshold.reportMode 配置):
|
||||
* - INCREMENTAL(默认):上报值直接作为增量
|
||||
* - CUMULATIVE:上报值为累计值,自动计算差值得到增量
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @param identifier 属性标识符(people_in 或 people_out)
|
||||
* @param propertyValue 属性值(周期内增量)
|
||||
* @param propertyValue 属性值(增量或累计值,取决于 reportMode)
|
||||
*/
|
||||
public void processPropertyChange(Long deviceId, String identifier, Object propertyValue) {
|
||||
// 1. 校验属性类型
|
||||
@@ -63,56 +67,106 @@ public class TrafficThresholdRuleProcessor {
|
||||
log.debug("[TrafficThreshold] 收到客流属性:deviceId={}, identifier={}, value={}",
|
||||
deviceId, identifier, propertyValue);
|
||||
|
||||
// 2. 获取设备关联信息(包含 areaId)
|
||||
CleanOrderIntegrationConfigService.AreaDeviceConfigWrapper configWrapper =
|
||||
getConfigWrapper(deviceId);
|
||||
|
||||
if (configWrapper == null || configWrapper.getConfig() == null) {
|
||||
log.debug("[TrafficThreshold] 设备无配置:deviceId={}", deviceId);
|
||||
// 2. 解析原始值
|
||||
Long rawValue = parseTrafficCount(propertyValue);
|
||||
if (rawValue == null || rawValue <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
TrafficThresholdConfig thresholdConfig = configWrapper.getConfig().getTrafficThreshold();
|
||||
if (thresholdConfig == null || !thresholdConfig.getAutoCreateOrder()) {
|
||||
log.debug("[TrafficThreshold] 未启用客流阈值触发:deviceId={}", deviceId);
|
||||
// 3. 获取配置,判断上报模式
|
||||
CleanOrderIntegrationConfigService.AreaDeviceConfigWrapper configWrapper = getConfigWrapper(deviceId);
|
||||
TrafficThresholdConfig thresholdConfig = resolveThresholdConfig(configWrapper);
|
||||
|
||||
// 4. 根据上报模式计算增量
|
||||
long increment;
|
||||
if (thresholdConfig != null && thresholdConfig.isCumulative()) {
|
||||
increment = resolveIncrement(deviceId, identifier, rawValue);
|
||||
} else {
|
||||
increment = rawValue;
|
||||
}
|
||||
if (increment <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 解析增量值
|
||||
Long increment = parseTrafficCount(propertyValue);
|
||||
if (increment == null || increment <= 0) {
|
||||
log.debug("[TrafficThreshold] 增量值无效:deviceId={}, identifier={}, value={}",
|
||||
deviceId, identifier, propertyValue);
|
||||
// 5. 累加到当日统计(统计与工单触发解耦)
|
||||
LocalDate today = LocalDate.now();
|
||||
if ("people_in".equals(identifier)) {
|
||||
trafficCounterRedisDAO.incrementDaily(deviceId, today, increment, 0);
|
||||
} else {
|
||||
trafficCounterRedisDAO.incrementDaily(deviceId, today, 0, increment);
|
||||
}
|
||||
log.debug("[TrafficThreshold] 当日统计累加:deviceId={}, identifier={}, increment={}",
|
||||
deviceId, identifier, increment);
|
||||
|
||||
// 6. 以下为工单触发逻辑,仅 people_in 参与
|
||||
if (!"people_in".equals(identifier)) {
|
||||
return;
|
||||
}
|
||||
if (thresholdConfig == null || !Boolean.TRUE.equals(thresholdConfig.getAutoCreateOrder())) {
|
||||
return;
|
||||
}
|
||||
|
||||
Long areaId = configWrapper.getAreaId();
|
||||
LocalDate today = LocalDate.now();
|
||||
handlePeopleIn(deviceId, areaId, increment, today, thresholdConfig, configWrapper);
|
||||
}
|
||||
|
||||
// 4. 根据属性类型分别处理
|
||||
if ("people_in".equals(identifier)) {
|
||||
handlePeopleIn(deviceId, areaId, increment, today, thresholdConfig, configWrapper);
|
||||
} else {
|
||||
// people_out:仅累加到当日统计
|
||||
trafficCounterRedisDAO.incrementDaily(deviceId, today, 0, increment);
|
||||
log.debug("[TrafficThreshold] people_out 累加到当日统计:deviceId={}, areaId={}, increment={}",
|
||||
deviceId, areaId, increment);
|
||||
/**
|
||||
* 从配置包装器中提取客流阈值配置
|
||||
*
|
||||
* @return 阈值配置,无配置时返回 null
|
||||
*/
|
||||
private TrafficThresholdConfig resolveThresholdConfig(
|
||||
CleanOrderIntegrationConfigService.AreaDeviceConfigWrapper configWrapper) {
|
||||
if (configWrapper == null || configWrapper.getConfig() == null) {
|
||||
return null;
|
||||
}
|
||||
return configWrapper.getConfig().getTrafficThreshold();
|
||||
}
|
||||
|
||||
/**
|
||||
* 累计值转增量
|
||||
* <p>
|
||||
* 通过 Redis 存储上次上报的累计值,计算差值得到本次增量。
|
||||
* 处理三种场景:首次上报、正常递增、设备重启归零。
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @param identifier 属性标识符
|
||||
* @param currentValue 本次上报的累计值
|
||||
* @return 增量值;首次上报返回 0
|
||||
*/
|
||||
private long resolveIncrement(Long deviceId, String identifier, long currentValue) {
|
||||
Long lastValue = trafficCounterRedisDAO.getLastCumulativeValue(deviceId, identifier);
|
||||
|
||||
// 无论是否能算出增量,都记录当前值
|
||||
trafficCounterRedisDAO.setLastCumulativeValue(deviceId, identifier, currentValue);
|
||||
|
||||
if (lastValue == null) {
|
||||
// 首次上报:无历史基准,不计入统计
|
||||
log.info("[TrafficThreshold] 累计值设备首次上报,建立基准:deviceId={}, identifier={}, value={}",
|
||||
deviceId, identifier, currentValue);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (currentValue >= lastValue) {
|
||||
return currentValue - lastValue;
|
||||
}
|
||||
|
||||
// currentValue < lastValue → 设备重启归零
|
||||
log.info("[TrafficThreshold] 检测到设备重启:deviceId={}, identifier={}, last={}, current={}",
|
||||
deviceId, identifier, lastValue, currentValue);
|
||||
return currentValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 people_in 增量
|
||||
*/
|
||||
private void handlePeopleIn(Long deviceId, Long areaId, Long increment, LocalDate today,
|
||||
private void handlePeopleIn(Long deviceId, Long areaId, long increment, LocalDate today,
|
||||
TrafficThresholdConfig thresholdConfig,
|
||||
CleanOrderIntegrationConfigService.AreaDeviceConfigWrapper configWrapper) {
|
||||
// 1. 原子累加到阈值计数器,返回累积值
|
||||
// 1. 原子累加到阈值计数器,返回累积值(当日统计已在 processPropertyChange 中完成)
|
||||
Long accumulated = trafficCounterRedisDAO.incrementThreshold(deviceId, areaId, increment);
|
||||
|
||||
// 2. 原子累加到当日统计
|
||||
trafficCounterRedisDAO.incrementDaily(deviceId, today, increment, 0);
|
||||
|
||||
log.debug("[TrafficThreshold] people_in 累加:deviceId={}, areaId={}, increment={}, accumulated={}, threshold={}",
|
||||
log.debug("[TrafficThreshold] people_in 阈值累加:deviceId={}, areaId={}, increment={}, accumulated={}, threshold={}",
|
||||
deviceId, areaId, increment, accumulated, thresholdConfig.getThreshold());
|
||||
|
||||
// 3. 阈值判定
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
package com.viewsh.module.iot.service.rule.clean.processor;
|
||||
|
||||
import com.viewsh.framework.common.util.json.JsonUtils;
|
||||
import com.viewsh.module.iot.core.integration.constants.TrajectoryTopics;
|
||||
import com.viewsh.module.iot.core.integration.event.trajectory.TrajectoryEnterEvent;
|
||||
import com.viewsh.module.iot.core.integration.event.trajectory.TrajectoryLeaveEvent;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.integration.clean.BeaconPresenceConfig;
|
||||
import com.viewsh.module.iot.dal.dataobject.integration.clean.TrajectoryTrackingConfig;
|
||||
import com.viewsh.module.iot.dal.redis.clean.TrajectoryRssiWindowRedisDAO;
|
||||
import com.viewsh.module.iot.dal.redis.clean.TrajectoryStateRedisDAO;
|
||||
import com.viewsh.module.iot.service.device.IotDeviceService;
|
||||
import com.viewsh.module.iot.service.integration.clean.BeaconRegistryService;
|
||||
import com.viewsh.module.iot.service.integration.clean.BeaconRegistryService.BeaconAreaInfo;
|
||||
import com.viewsh.module.iot.service.rule.clean.detector.RssiSlidingWindowDetector;
|
||||
import com.viewsh.module.iot.service.rule.clean.detector.RssiSlidingWindowDetector.AreaState;
|
||||
import com.viewsh.module.iot.service.rule.clean.detector.RssiSlidingWindowDetector.DetectionResult;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.viewsh.framework.tenant.core.context.TenantContextHolder;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 轨迹检测处理器
|
||||
* <p>
|
||||
* 工牌每次上报蓝牙数据时,匹配系统中所有已知 Beacon,
|
||||
* 基于"强进弱出"双阈值算法判断进出区域,自动记录轨迹。
|
||||
* <p>
|
||||
* 同一时刻只认一个区域,复用现有 {@link RssiSlidingWindowDetector}。
|
||||
* <p>
|
||||
* 核心流程:
|
||||
* <ol>
|
||||
* <li>检查设备是否开启轨迹功能</li>
|
||||
* <li>获取全量 Beacon 注册表</li>
|
||||
* <li>提取蓝牙列表中所有已知 Beacon 的 RSSI</li>
|
||||
* <li>更新各区域的 RSSI 滑动窗口</li>
|
||||
* <li>检测当前区域退出 / 新区域进入</li>
|
||||
* <li>发布进入/离开事件到 RocketMQ</li>
|
||||
* </ol>
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class TrajectoryDetectionProcessor {
|
||||
|
||||
/**
|
||||
* 设备轨迹功能开关缓存 Key
|
||||
*/
|
||||
private static final String DEVICE_ENABLED_KEY_PATTERN = "iot:trajectory:device:enabled:%s";
|
||||
private static final int DEVICE_ENABLED_TTL_SECONDS = 3600; // 1小时
|
||||
|
||||
@Resource
|
||||
private BeaconRegistryService beaconRegistryService;
|
||||
|
||||
@Resource
|
||||
private TrajectoryRssiWindowRedisDAO windowRedisDAO;
|
||||
|
||||
@Resource
|
||||
private TrajectoryStateRedisDAO stateRedisDAO;
|
||||
|
||||
@Resource
|
||||
private RssiSlidingWindowDetector detector;
|
||||
|
||||
@Resource
|
||||
private IotDeviceService deviceService;
|
||||
|
||||
@Resource
|
||||
private RocketMQTemplate rocketMQTemplate;
|
||||
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
/**
|
||||
* 处理蓝牙属性上报
|
||||
*
|
||||
* @param deviceId 设备ID(工牌)
|
||||
* @param identifier 属性标识符
|
||||
* @param propertyValue 蓝牙设备列表
|
||||
*/
|
||||
public void processPropertyChange(Long deviceId, String identifier, Object propertyValue) {
|
||||
if (!"bluetoothDevices".equals(identifier)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 检查设备是否开启轨迹功能
|
||||
if (!isTrajectoryEnabled(deviceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 信号丢失补偿快速路径:propertyValue 为 null 且设备不在任何区域时,直接跳过
|
||||
// 避免每次无蓝牙数据的属性上报都查询 Beacon 注册表
|
||||
if (propertyValue == null && stateRedisDAO.getCurrentArea(deviceId) == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 获取 Beacon 注册表
|
||||
Map<String, BeaconAreaInfo> beaconRegistry = beaconRegistryService.getAllBeaconConfigs();
|
||||
if (beaconRegistry == null || beaconRegistry.isEmpty()) {
|
||||
log.debug("[Trajectory] Beacon 注册表为空,跳过检测:deviceId={}", deviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 构建 areaId → config 索引(避免后续 O(n) 扫描)
|
||||
Map<Long, BeaconPresenceConfig> areaConfigIndex = new HashMap<>();
|
||||
for (BeaconAreaInfo info : beaconRegistry.values()) {
|
||||
areaConfigIndex.putIfAbsent(info.getAreaId(), info.getConfig());
|
||||
}
|
||||
|
||||
// 5. 提取蓝牙列表中所有已知 Beacon 的 RSSI
|
||||
Map<Long, MatchedBeacon> matchedBeacons = extractMatchedBeacons(propertyValue, beaconRegistry);
|
||||
|
||||
// 6. 获取当前所在区域
|
||||
TrajectoryStateRedisDAO.CurrentAreaInfo currentArea = stateRedisDAO.getCurrentArea(deviceId);
|
||||
|
||||
// 7. 对所有匹配区域更新滑动窗口
|
||||
updateAllWindows(deviceId, matchedBeacons, areaConfigIndex, currentArea);
|
||||
|
||||
// 8. 处理区域状态变化
|
||||
if (currentArea != null) {
|
||||
processWithCurrentArea(deviceId, currentArea, matchedBeacons, areaConfigIndex);
|
||||
} else {
|
||||
processWithoutCurrentArea(deviceId, matchedBeacons, areaConfigIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 核心检测逻辑 ====================
|
||||
|
||||
/**
|
||||
* 当前在某区域时的处理逻辑
|
||||
*/
|
||||
private void processWithCurrentArea(Long deviceId,
|
||||
TrajectoryStateRedisDAO.CurrentAreaInfo currentArea,
|
||||
Map<Long, MatchedBeacon> matchedBeacons,
|
||||
Map<Long, BeaconPresenceConfig> areaConfigIndex) {
|
||||
Long currentAreaId = currentArea.getAreaId();
|
||||
|
||||
// 6a. 检查当前区域的退出条件
|
||||
BeaconPresenceConfig currentConfig = areaConfigIndex.get(currentAreaId);
|
||||
if (currentConfig != null) {
|
||||
List<Integer> window = windowRedisDAO.getWindow(deviceId, currentAreaId);
|
||||
DetectionResult exitResult = detector.detect(
|
||||
window,
|
||||
currentConfig.getEnter(),
|
||||
currentConfig.getExit(),
|
||||
AreaState.IN_AREA);
|
||||
|
||||
if (exitResult == DetectionResult.LEAVE_CONFIRMED) {
|
||||
// 确认离开当前区域
|
||||
publishLeaveEvent(deviceId, currentAreaId, currentArea.getBeaconMac(),
|
||||
"SIGNAL_LOSS", currentArea.getEnterTime());
|
||||
stateRedisDAO.clearCurrentArea(deviceId);
|
||||
windowRedisDAO.clearWindow(deviceId, currentAreaId);
|
||||
|
||||
log.info("[Trajectory] 离开区域:deviceId={}, areaId={}, reason=SIGNAL_LOSS", deviceId, currentAreaId);
|
||||
|
||||
// 离开后,尝试进入新区域
|
||||
processWithoutCurrentArea(deviceId, matchedBeacons, areaConfigIndex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 6b. 当前区域未退出,检查是否有更强区域触发切换
|
||||
MatchedBeacon bestCandidate = findBestEnterCandidate(deviceId, matchedBeacons, currentAreaId);
|
||||
if (bestCandidate != null && !bestCandidate.areaId.equals(currentAreaId)) {
|
||||
// 区域切换:先离开当前区域,再进入新区域
|
||||
publishLeaveEvent(deviceId, currentAreaId, currentArea.getBeaconMac(),
|
||||
"AREA_SWITCH", currentArea.getEnterTime());
|
||||
windowRedisDAO.clearWindow(deviceId, currentAreaId);
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
stateRedisDAO.setCurrentArea(deviceId, bestCandidate.areaId, now, bestCandidate.beaconMac);
|
||||
windowRedisDAO.clearWindow(deviceId, bestCandidate.areaId);
|
||||
publishEnterEvent(deviceId, bestCandidate.areaId, bestCandidate.beaconMac, bestCandidate.rssi);
|
||||
|
||||
log.info("[Trajectory] 区域切换:deviceId={}, from={}, to={}", deviceId, currentAreaId, bestCandidate.areaId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前不在任何区域时的处理逻辑
|
||||
*/
|
||||
private void processWithoutCurrentArea(Long deviceId,
|
||||
Map<Long, MatchedBeacon> matchedBeacons,
|
||||
Map<Long, BeaconPresenceConfig> areaConfigIndex) {
|
||||
MatchedBeacon bestCandidate = findBestEnterCandidate(deviceId, matchedBeacons, null);
|
||||
if (bestCandidate != null) {
|
||||
long now = System.currentTimeMillis();
|
||||
stateRedisDAO.setCurrentArea(deviceId, bestCandidate.areaId, now, bestCandidate.beaconMac);
|
||||
windowRedisDAO.clearWindow(deviceId, bestCandidate.areaId);
|
||||
publishEnterEvent(deviceId, bestCandidate.areaId, bestCandidate.beaconMac, bestCandidate.rssi);
|
||||
|
||||
log.info("[Trajectory] 进入区域:deviceId={}, areaId={}, rssi={}", deviceId, bestCandidate.areaId, bestCandidate.rssi);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 辅助方法 ====================
|
||||
|
||||
/**
|
||||
* 从蓝牙列表中提取所有已知 Beacon 的 RSSI
|
||||
* 同一区域如果有多个 Beacon 匹配,取 RSSI 最强的
|
||||
*/
|
||||
private Map<Long, MatchedBeacon> extractMatchedBeacons(Object bluetoothDevices,
|
||||
Map<String, BeaconAreaInfo> beaconRegistry) {
|
||||
Map<Long, MatchedBeacon> result = new HashMap<>();
|
||||
|
||||
if (bluetoothDevices == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!(bluetoothDevices instanceof List)) {
|
||||
log.warn("[Trajectory] bluetoothDevices 类型不正确: {}", bluetoothDevices.getClass().getName());
|
||||
return result;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> deviceList = (List<Map<String, Object>>) bluetoothDevices;
|
||||
|
||||
for (Map<String, Object> device : deviceList) {
|
||||
String mac = (String) device.get("mac");
|
||||
if (mac == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
mac = mac.toUpperCase();
|
||||
BeaconAreaInfo info = beaconRegistry.get(mac);
|
||||
if (info == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Integer rssi = toInt(device.get("rssi"));
|
||||
if (rssi == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Long areaId = info.getAreaId();
|
||||
MatchedBeacon existing = result.get(areaId);
|
||||
if (existing == null || rssi > existing.rssi) {
|
||||
result.put(areaId, new MatchedBeacon(areaId, mac, rssi, info.getConfig()));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[Trajectory] 解析蓝牙数据失败", e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新所有匹配区域的滑动窗口
|
||||
* 对于当前所在但未匹配到的区域,注入 -999(信号缺失)
|
||||
*/
|
||||
private void updateAllWindows(Long deviceId,
|
||||
Map<Long, MatchedBeacon> matchedBeacons,
|
||||
Map<Long, BeaconPresenceConfig> areaConfigIndex,
|
||||
TrajectoryStateRedisDAO.CurrentAreaInfo currentArea) {
|
||||
// 更新匹配到的区域
|
||||
for (Map.Entry<Long, MatchedBeacon> entry : matchedBeacons.entrySet()) {
|
||||
MatchedBeacon mb = entry.getValue();
|
||||
int maxWindowSize = Math.max(
|
||||
mb.config.getEnter().getWindowSize(),
|
||||
mb.config.getExit().getWindowSize());
|
||||
windowRedisDAO.updateWindow(deviceId, mb.areaId, mb.rssi, maxWindowSize);
|
||||
}
|
||||
|
||||
// 当前区域未匹配到 Beacon 时,注入 -999 补偿
|
||||
if (currentArea != null && !matchedBeacons.containsKey(currentArea.getAreaId())) {
|
||||
BeaconPresenceConfig currentConfig = areaConfigIndex.get(currentArea.getAreaId());
|
||||
if (currentConfig != null) {
|
||||
int maxWindowSize = Math.max(
|
||||
currentConfig.getEnter().getWindowSize(),
|
||||
currentConfig.getExit().getWindowSize());
|
||||
windowRedisDAO.updateWindow(deviceId, currentArea.getAreaId(), -999, maxWindowSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 找到信号最强且满足进入条件的候选区域
|
||||
*
|
||||
* @param excludeAreaId 排除的区域ID(当前所在区域),null 表示不排除
|
||||
*/
|
||||
private MatchedBeacon findBestEnterCandidate(Long deviceId,
|
||||
Map<Long, MatchedBeacon> matchedBeacons,
|
||||
Long excludeAreaId) {
|
||||
MatchedBeacon best = null;
|
||||
|
||||
for (Map.Entry<Long, MatchedBeacon> entry : matchedBeacons.entrySet()) {
|
||||
Long areaId = entry.getKey();
|
||||
if (areaId.equals(excludeAreaId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
MatchedBeacon mb = entry.getValue();
|
||||
List<Integer> window = windowRedisDAO.getWindow(deviceId, areaId);
|
||||
DetectionResult result = detector.detect(
|
||||
window,
|
||||
mb.config.getEnter(),
|
||||
mb.config.getExit(),
|
||||
AreaState.OUT_AREA);
|
||||
|
||||
if (result == DetectionResult.ARRIVE_CONFIRMED) {
|
||||
if (best == null || mb.rssi > best.rssi) {
|
||||
best = mb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
// ==================== 功能开关 ====================
|
||||
|
||||
/**
|
||||
* 检查设备是否开启轨迹功能
|
||||
* 结果缓存1小时
|
||||
*/
|
||||
private boolean isTrajectoryEnabled(Long deviceId) {
|
||||
String cacheKey = String.format(DEVICE_ENABLED_KEY_PATTERN, deviceId);
|
||||
|
||||
// 先读缓存
|
||||
try {
|
||||
String cached = stringRedisTemplate.opsForValue().get(cacheKey);
|
||||
if (cached != null) {
|
||||
return "true".equals(cached);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[Trajectory] 读取功能开关缓存失败:deviceId={}", deviceId, e);
|
||||
}
|
||||
|
||||
// 缓存未命中,从设备配置读取
|
||||
boolean enabled = false;
|
||||
try {
|
||||
IotDeviceDO device = deviceService.getDeviceFromCache(deviceId);
|
||||
if (device != null && device.getConfig() != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> configMap = JsonUtils.parseObject(device.getConfig(), Map.class);
|
||||
if (configMap != null && configMap.containsKey("trajectoryTracking")) {
|
||||
Object ttObj = configMap.get("trajectoryTracking");
|
||||
TrajectoryTrackingConfig ttConfig = JsonUtils.parseObject(
|
||||
JsonUtils.toJsonString(ttObj), TrajectoryTrackingConfig.class);
|
||||
enabled = ttConfig != null && Boolean.TRUE.equals(ttConfig.getEnabled());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[Trajectory] 获取轨迹配置失败:deviceId={}", deviceId, e);
|
||||
}
|
||||
|
||||
// 写入缓存
|
||||
try {
|
||||
stringRedisTemplate.opsForValue().set(cacheKey, String.valueOf(enabled),
|
||||
DEVICE_ENABLED_TTL_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
log.warn("[Trajectory] 写入功能开关缓存失败:deviceId={}", deviceId, e);
|
||||
}
|
||||
|
||||
return enabled;
|
||||
}
|
||||
|
||||
// ==================== 事件发布 ====================
|
||||
|
||||
private void publishEnterEvent(Long deviceId, Long areaId, String beaconMac, Integer enterRssi) {
|
||||
try {
|
||||
IotDeviceDO device = deviceService.getDeviceFromCache(deviceId);
|
||||
TrajectoryEnterEvent event = TrajectoryEnterEvent.builder()
|
||||
.deviceId(deviceId)
|
||||
.deviceName(device != null ? device.getDeviceName() : null)
|
||||
.nickname(device != null ? device.getNickname() : null)
|
||||
.areaId(areaId)
|
||||
.beaconMac(beaconMac)
|
||||
.enterRssi(enterRssi)
|
||||
.tenantId(TenantContextHolder.getTenantId())
|
||||
.build();
|
||||
|
||||
rocketMQTemplate.syncSend(TrajectoryTopics.TRAJECTORY_ENTER,
|
||||
MessageBuilder.withPayload(event).build());
|
||||
|
||||
log.info("[Trajectory] 发布进入事件:eventId={}, deviceId={}, areaId={}",
|
||||
event.getEventId(), deviceId, areaId);
|
||||
} catch (Exception e) {
|
||||
log.error("[Trajectory] 发布进入事件失败:deviceId={}, areaId={}", deviceId, areaId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void publishLeaveEvent(Long deviceId, Long areaId, String beaconMac,
|
||||
String leaveReason, Long enterTimestamp) {
|
||||
try {
|
||||
IotDeviceDO device = deviceService.getDeviceFromCache(deviceId);
|
||||
TrajectoryLeaveEvent event = TrajectoryLeaveEvent.builder()
|
||||
.deviceId(deviceId)
|
||||
.deviceName(device != null ? device.getDeviceName() : null)
|
||||
.nickname(device != null ? device.getNickname() : null)
|
||||
.areaId(areaId)
|
||||
.beaconMac(beaconMac)
|
||||
.leaveReason(leaveReason)
|
||||
.enterTimestamp(enterTimestamp)
|
||||
.tenantId(TenantContextHolder.getTenantId())
|
||||
.build();
|
||||
|
||||
rocketMQTemplate.syncSend(TrajectoryTopics.TRAJECTORY_LEAVE,
|
||||
MessageBuilder.withPayload(event).build());
|
||||
|
||||
log.info("[Trajectory] 发布离开事件:eventId={}, deviceId={}, areaId={}, reason={}",
|
||||
event.getEventId(), deviceId, areaId, leaveReason);
|
||||
} catch (Exception e) {
|
||||
log.error("[Trajectory] 发布离开事件失败:deviceId={}, areaId={}", deviceId, areaId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Integer toInt(Object value) {
|
||||
if (value instanceof Integer) {
|
||||
return (Integer) value;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).intValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配到的 Beacon 信息
|
||||
*/
|
||||
private record MatchedBeacon(Long areaId, String beaconMac, Integer rssi, BeaconPresenceConfig config) {
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
server-addr: 124.221.55.225:8848 # Nacos 服务器地址
|
||||
server-addr: 124.222.218.198:8848 # Nacos 服务器地址
|
||||
username: nacos # Nacos 账号
|
||||
password: 9oDxX~}e7DeP # Nacos 密码
|
||||
discovery: # 【配置中心】配置项
|
||||
@@ -62,17 +62,17 @@ spring:
|
||||
master:
|
||||
# 默认使用服务器公网 IP (方便本地开发直连)
|
||||
# CI/CD 部署时会自动注入环境变量 MYSQL_HOST=aiot-mysql 切换为内网连接
|
||||
url: jdbc:mysql://${MYSQL_HOST:124.221.55.225}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:aiot-platform}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||
url: jdbc:mysql://${MYSQL_HOST:124.222.218.198}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:aiot-platform}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||
username: ${MYSQL_USERNAME:root}
|
||||
password: ${MYSQL_PASSWORD:65p^VTPi9Qd+}
|
||||
slave: # 模拟从库,可根据自己需要修改
|
||||
lazy: true # 开启懒加载,保证启动速度
|
||||
url: jdbc:mysql://${MYSQL_HOST:124.221.55.225}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:aiot-platform}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
|
||||
url: jdbc:mysql://${MYSQL_HOST:124.222.218.198}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:aiot-platform}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
|
||||
username: ${MYSQL_USERNAME:root}
|
||||
password: ${MYSQL_PASSWORD:65p^VTPi9Qd+}
|
||||
tdengine: # IoT 数据库(需要 IoT 物联网再开启噢!)
|
||||
# 本地开发走公网 IP,部署环境走内网容器名
|
||||
url: jdbc:TAOS-WS://${TDENGINE_HOST:124.221.55.225}:${TDENGINE_PORT:6041}/${TDENGINE_DATABASE:aiot_platform}
|
||||
url: jdbc:TAOS-WS://${TDENGINE_HOST:124.222.218.198}:${TDENGINE_PORT:6041}/${TDENGINE_DATABASE:aiot_platform}
|
||||
driver-class-name: com.taosdata.jdbc.ws.WebSocketDriver
|
||||
username: ${TDENGINE_USERNAME:root}
|
||||
password: ${TDENGINE_PASSWORD:taosdata}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
server-addr: 124.221.55.225:8848 # Nacos 服务器地址
|
||||
server-addr: 124.222.218.198:8848 # Nacos 服务器地址
|
||||
username: nacos # Nacos 账号
|
||||
password: 9oDxX~}e7DeP # Nacos 密码
|
||||
discovery: # 【配置中心】配置项
|
||||
@@ -62,17 +62,17 @@ spring:
|
||||
master:
|
||||
# 默认使用服务器公网 IP (方便本地开发直连)
|
||||
# CI/CD 部署时会自动注入环境变量 MYSQL_HOST=aiot-mysql 切换为内网连接
|
||||
url: jdbc:mysql://${MYSQL_HOST:124.221.55.225}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:aiot-platform}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||
url: jdbc:mysql://${MYSQL_HOST:124.222.218.198}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:aiot-platform}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||
username: ${MYSQL_USERNAME:root}
|
||||
password: ${MYSQL_PASSWORD:65p^VTPi9Qd+}
|
||||
slave: # 模拟从库,可根据自己需要修改
|
||||
lazy: true # 开启懒加载,保证启动速度
|
||||
url: jdbc:mysql://${MYSQL_HOST:124.221.55.225}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:aiot-platform}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
|
||||
url: jdbc:mysql://${MYSQL_HOST:124.222.218.198}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:aiot-platform}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
|
||||
username: ${MYSQL_USERNAME:root}
|
||||
password: ${MYSQL_PASSWORD:65p^VTPi9Qd+}
|
||||
tdengine: # IoT 数据库(需要 IoT 物联网再开启噢!)
|
||||
# 本地开发走公网 IP,部署环境走内网容器名
|
||||
url: jdbc:TAOS-WS://${TDENGINE_HOST:124.221.55.225}:${TDENGINE_PORT:6041}/${TDENGINE_DATABASE:aiot_platform}
|
||||
url: jdbc:TAOS-WS://${TDENGINE_HOST:124.222.218.198}:${TDENGINE_PORT:6041}/${TDENGINE_DATABASE:aiot_platform}
|
||||
driver-class-name: com.taosdata.jdbc.ws.WebSocketDriver
|
||||
username: ${TDENGINE_USERNAME:root}
|
||||
password: ${TDENGINE_PASSWORD:taosdata}
|
||||
|
||||
@@ -1,156 +1,166 @@
|
||||
--- #################### 注册中心 + 配置中心相关配置 ####################
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
server-addr: ${NACOS_ADDR:127.0.0.1:8848}
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
discovery:
|
||||
namespace: ${NACOS_DISCOVERY_NAMESPACE:prod}
|
||||
group: DEFAULT_GROUP
|
||||
metadata:
|
||||
version: 1.0.0
|
||||
config:
|
||||
namespace: ${NACOS_CONFIG_NAMESPACE:prod}
|
||||
group: DEFAULT_GROUP
|
||||
file-extension: yaml
|
||||
refresh-enabled: true
|
||||
|
||||
--- #################### 数据库相关配置 ####################
|
||||
spring:
|
||||
datasource:
|
||||
druid:
|
||||
web-stat-filter:
|
||||
enabled: true
|
||||
stat-view-servlet:
|
||||
enabled: true
|
||||
url-pattern: /druid/*
|
||||
login-username: ${DRUID_USERNAME:admin}
|
||||
login-password: ${DRUID_PASSWORD:admin}
|
||||
filter:
|
||||
stat:
|
||||
enabled: true
|
||||
log-slow-sql: true
|
||||
slow-sql-millis: 100
|
||||
merge-sql: true
|
||||
wall:
|
||||
config:
|
||||
multi-statement-allow: true
|
||||
dynamic:
|
||||
druid:
|
||||
initial-size: 5
|
||||
min-idle: 10
|
||||
max-active: 20
|
||||
max-wait: 60000
|
||||
time-between-eviction-runs-millis: 60000
|
||||
min-evictable-idle-time-millis: 600000
|
||||
max-evictable-idle-time-millis: 1800000
|
||||
validation-query: SELECT 1 FROM DUAL
|
||||
test-while-idle: true
|
||||
test-on-borrow: false
|
||||
test-on-return: false
|
||||
pool-prepared-statements: true
|
||||
max-pool-prepared-statement-per-connection-size: 20
|
||||
primary: master
|
||||
datasource:
|
||||
master:
|
||||
url: jdbc:mysql://${MYSQL_HOST:127.0.0.1}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:aiot_platform}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||
username: ${MYSQL_USER:root}
|
||||
password: ${MYSQL_PASSWORD:}
|
||||
slave:
|
||||
lazy: true
|
||||
url: jdbc:mysql://${MYSQL_SLAVE_HOST:${MYSQL_HOST:127.0.0.1}}:${MYSQL_SLAVE_PORT:${MYSQL_PORT:3306}}/${MYSQL_DATABASE:aiot_platform}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||
username: ${MYSQL_SLAVE_USER:${MYSQL_USER:root}}
|
||||
password: ${MYSQL_SLAVE_PASSWORD:${MYSQL_PASSWORD:}}
|
||||
tdengine:
|
||||
url: jdbc:TAOS-RS://${TDENGINE_HOST:172.17.16.14}:${TDENGINE_PORT:6041}/aiot_platform
|
||||
driver-class-name: com.taosdata.jdbc.rs.RestfulDriver
|
||||
username: ${TDENGINE_USERNAME:root}
|
||||
password: ${TDENGINE_PASSWORD:taosdata}
|
||||
druid:
|
||||
validation-query: SELECT SERVER_STATUS()
|
||||
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:127.0.0.1}
|
||||
port: ${REDIS_PORT:6379}
|
||||
database: ${REDIS_DATABASE:0}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
timeout: 5000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-wait: -1ms
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
|
||||
--- #################### MQ 消息队列相关配置 ####################
|
||||
|
||||
rocketmq:
|
||||
name-server: ${ROCKETMQ_NAMESRV_ADDR:127.0.0.1:9876}
|
||||
|
||||
spring:
|
||||
# 禁用 RabbitMQ 自动配置(如果不需要 RabbitMQ,避免启动时连接失败)
|
||||
autoconfigure:
|
||||
exclude:
|
||||
- org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
|
||||
# RabbitMQ 配置(已禁用自动配置,仅保留配置项供业务代码使用)
|
||||
# rabbitmq:
|
||||
# host: ${RABBITMQ_HOST:127.0.0.1}
|
||||
# port: ${RABBITMQ_PORT:5672}
|
||||
# username: ${RABBITMQ_USERNAME:guest}
|
||||
# password: ${RABBITMQ_PASSWORD:guest}
|
||||
kafka:
|
||||
bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:127.0.0.1:9092}
|
||||
|
||||
--- #################### 定时任务相关配置 ####################
|
||||
xxl:
|
||||
job:
|
||||
admin:
|
||||
addresses: ${XXL_JOB_ADMIN_ADDRESSES:http://172.17.16.14:19090/xxl-job-admin}
|
||||
|
||||
--- #################### 服务保障相关配置 ####################
|
||||
|
||||
lock4j:
|
||||
acquire-timeout: 3000
|
||||
expire: 30000
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
base-path: /actuator
|
||||
exposure:
|
||||
include: '*'
|
||||
|
||||
spring:
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
instance:
|
||||
service-host-type: IP
|
||||
username: ${SPRING_BOOT_ADMIN_USERNAME:admin}
|
||||
password: ${SPRING_BOOT_ADMIN_PASSWORD:admin}
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
com.viewsh: ${LOG_LEVEL:INFO}
|
||||
com.viewsh.module.iot.dal.mysql: debug
|
||||
com.viewsh.module.iot.dal.mysql.sms.SmsChannelMapper: INFO
|
||||
org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR
|
||||
file:
|
||||
name: ${LOG_FILE_PATH:/app/logs}/${spring.application.name}.log
|
||||
|
||||
--- #################### 芋道相关配置 ####################
|
||||
|
||||
viewsh:
|
||||
demo: false
|
||||
env:
|
||||
tag: ${HOSTNAME:prod}
|
||||
captcha:
|
||||
enable: true
|
||||
security:
|
||||
mock-enable: false
|
||||
--- #################### 注册中心 + 配置中心相关配置 ####################
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
server-addr: ${NACOS_ADDR:172.17.16.7:8848}
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
discovery:
|
||||
namespace: ${NACOS_DISCOVERY_NAMESPACE:prod}
|
||||
group: DEFAULT_GROUP
|
||||
metadata:
|
||||
version: 1.0.0
|
||||
config:
|
||||
namespace: ${NACOS_CONFIG_NAMESPACE:prod}
|
||||
group: DEFAULT_GROUP
|
||||
file-extension: yaml
|
||||
refresh-enabled: true
|
||||
|
||||
--- #################### 数据库相关配置 ####################
|
||||
spring:
|
||||
datasource:
|
||||
druid:
|
||||
web-stat-filter:
|
||||
enabled: true
|
||||
stat-view-servlet:
|
||||
enabled: true
|
||||
url-pattern: /druid/*
|
||||
login-username: ${DRUID_USERNAME:admin}
|
||||
login-password: ${DRUID_PASSWORD:admin}
|
||||
filter:
|
||||
stat:
|
||||
enabled: true
|
||||
log-slow-sql: true
|
||||
slow-sql-millis: 100
|
||||
merge-sql: true
|
||||
wall:
|
||||
config:
|
||||
multi-statement-allow: true
|
||||
dynamic:
|
||||
druid:
|
||||
initial-size: 5
|
||||
min-idle: 10
|
||||
max-active: 20
|
||||
max-wait: 60000
|
||||
time-between-eviction-runs-millis: 60000
|
||||
min-evictable-idle-time-millis: 600000
|
||||
max-evictable-idle-time-millis: 1800000
|
||||
validation-query: SELECT 1 FROM DUAL
|
||||
test-while-idle: true
|
||||
test-on-borrow: false
|
||||
test-on-return: false
|
||||
pool-prepared-statements: true
|
||||
max-pool-prepared-statement-per-connection-size: 20
|
||||
primary: master
|
||||
datasource:
|
||||
master:
|
||||
url: jdbc:mysql://${MYSQL_HOST:172.17.16.8}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:aiot-platform-test}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||
username: ${MYSQL_USER:root}
|
||||
password: ${MYSQL_PASSWORD:}
|
||||
slave:
|
||||
lazy: true
|
||||
url: jdbc:mysql://${MYSQL_SLAVE_HOST:${MYSQL_HOST:172.17.16.8}}:${MYSQL_SLAVE_PORT:${MYSQL_PORT:3306}}/${MYSQL_DATABASE:aiot-platform-test}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||
username: ${MYSQL_SLAVE_USER:${MYSQL_USER:root}}
|
||||
password: ${MYSQL_SLAVE_PASSWORD:${MYSQL_PASSWORD:}}
|
||||
tdengine:
|
||||
url: jdbc:TAOS-RS://${TDENGINE_HOST:172.17.16.7}:${TDENGINE_PORT:6041}/aiot_platform
|
||||
driver-class-name: com.taosdata.jdbc.rs.RestfulDriver
|
||||
username: ${TDENGINE_USERNAME:root}
|
||||
password: ${TDENGINE_PASSWORD:taosdata}
|
||||
druid:
|
||||
validation-query: SELECT SERVER_STATUS()
|
||||
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:172.17.16.13}
|
||||
port: ${REDIS_PORT:6379}
|
||||
database: ${REDIS_DATABASE:0}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
timeout: 5000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-wait: -1ms
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
|
||||
--- #################### MQ 消息队列相关配置 ####################
|
||||
|
||||
rocketmq:
|
||||
name-server: ${ROCKETMQ_NAMESRV_ADDR:rmq-4wd73bxpv.rocketmq.sh.qcloud.tencenttdmq.com:8080}
|
||||
producer:
|
||||
group: ${spring.application.name}_PRODUCER
|
||||
access-key: ${ROCKETMQ_ACCESS_KEY:}
|
||||
secret-key: ${ROCKETMQ_SECRET_KEY:}
|
||||
consumer:
|
||||
access-key: ${ROCKETMQ_ACCESS_KEY:}
|
||||
secret-key: ${ROCKETMQ_SECRET_KEY:}
|
||||
|
||||
spring:
|
||||
# 禁用 RabbitMQ 自动配置(如果不需要 RabbitMQ,避免启动时连接失败)
|
||||
autoconfigure:
|
||||
exclude:
|
||||
- org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
|
||||
# RabbitMQ 配置(已禁用自动配置,仅保留配置项供业务代码使用)
|
||||
# rabbitmq:
|
||||
# host: ${RABBITMQ_HOST:127.0.0.1}
|
||||
# port: ${RABBITMQ_PORT:5672}
|
||||
# username: ${RABBITMQ_USERNAME:guest}
|
||||
# password: ${RABBITMQ_PASSWORD:guest}
|
||||
kafka:
|
||||
bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:127.0.0.1:9092}
|
||||
|
||||
--- #################### 定时任务相关配置 ####################
|
||||
xxl:
|
||||
job:
|
||||
admin:
|
||||
addresses: ${XXL_JOB_ADMIN_ADDRESSES:http://172.17.16.7:19090/xxl-job-admin}
|
||||
executor:
|
||||
ip: ${XXL_JOB_EXECUTOR_IP:}
|
||||
port: ${XXL_JOB_EXECUTOR_PORT:9999}
|
||||
|
||||
--- #################### 服务保障相关配置 ####################
|
||||
|
||||
lock4j:
|
||||
acquire-timeout: 3000
|
||||
expire: 30000
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
base-path: /actuator
|
||||
exposure:
|
||||
include: '*'
|
||||
|
||||
spring:
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
instance:
|
||||
service-host-type: IP
|
||||
username: ${SPRING_BOOT_ADMIN_USERNAME:admin}
|
||||
password: ${SPRING_BOOT_ADMIN_PASSWORD:admin}
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
com.viewsh: ${LOG_LEVEL:INFO}
|
||||
com.viewsh.module.iot.dal.mysql: debug
|
||||
com.viewsh.module.iot.dal.mysql.sms.SmsChannelMapper: INFO
|
||||
org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR
|
||||
file:
|
||||
name: ${LOG_FILE_PATH:/app/logs}/${spring.application.name}.log
|
||||
|
||||
--- #################### 芋道相关配置 ####################
|
||||
|
||||
viewsh:
|
||||
demo: false
|
||||
env:
|
||||
tag: ${HOSTNAME:prod}
|
||||
captcha:
|
||||
enable: true
|
||||
security:
|
||||
mock-enable: false
|
||||
|
||||
Reference in New Issue
Block a user