package com.battery.business.service.impl; import java.util.ArrayList; import java.util.Date; import java.util.HexFormat; import java.util.List; import com.battery.business.domain.TDeviceIngestRecord; import com.battery.business.domain.vo.TDeviceIngestChannel; import com.battery.business.mapper.TDeviceIngestRecordMapper; /** * 落地表的内存替身:按来源行身份判重、心跳按来源覆盖、水位按 (设备时间, 协议序号) 取严格更晚的行—— * 与生产 Mapper 的 SQL 语义一致,使接收端点与适配器可以在没有数据库的情况下接起来测。 */ final class FakeIngestLanding implements TDeviceIngestRecordMapper { private final List rows = new ArrayList<>(); @Override public int insertIngestRecord(TDeviceIngestRecord record) { if (rows.stream().anyMatch(existing -> same(existing, record))) return 0; rows.add(record); return 1; } @Override public int upsertHeartbeat(TDeviceIngestRecord record) { rows.removeIf(existing -> same(existing, record)); rows.add(record); return 1; } /** 拓扑是声明:撞键即刷新声明内容,不追加新行(与生产 SQL 的 upsert 语义一致)。 */ @Override public int upsertTopology(TDeviceIngestRecord record) { int index = -1; for (int i = 0; i < rows.size(); i++) if (same(rows.get(i), record)) index = i; if (index < 0) { rows.add(record); return 1; } rows.set(index, record); return 1; } @Override public String selectIngestRecordHashByKey(String adapterCode, String payloadKind, String recordKey) { return rows.stream().filter(row -> same(row, adapterCode, payloadKind, recordKey)).findFirst() .map(row -> HexFormat.of().formatHex(row.getContentSha256())).orElse(null); } @Override public Date selectHeartbeatAt(String adapterCode) { return rows.stream() .filter(row -> adapterCode.equals(row.getAdapterCode()) && TDeviceIngestRecord.KIND_HEARTBEAT.equals(row.getPayloadKind())) .map(TDeviceIngestRecord::getReceivedAtUtc).max(Date::compareTo).orElse(null); } @Override public List selectTopologyChannels(String adapterCode) { List channels = new ArrayList<>(); for (TDeviceIngestRecord row : rows) { if (!adapterCode.equals(row.getAdapterCode()) || !TDeviceIngestRecord.KIND_TOPOLOGY.equals(row.getPayloadKind())) continue; TDeviceIngestChannel channel = new TDeviceIngestChannel(); channel.setDevtype(row.getDevtype()); channel.setDevid(row.getDevid()); channel.setSubdevid(row.getSubdevid()); channel.setChlid(row.getChlid()); channel.setVendorChannelNo(row.getVendorChannelNo()); channel.setLastDataAt(rows.stream().filter(sample -> isSample(sample, adapterCode, row)) .map(TDeviceIngestRecord::getDeviceTimeUtc).filter(java.util.Objects::nonNull) .max(Date::compareTo).orElse(null)); channels.add(channel); } channels.sort(java.util.Comparator.comparing(TDeviceIngestChannel::getDevid) .thenComparing(TDeviceIngestChannel::getSubdevid).thenComparing(TDeviceIngestChannel::getChlid)); return channels; } @Override public TDeviceIngestRecord selectLatestSample(String adapterCode, int devtype, int devid, int subdevid, int chlid) { return samples(adapterCode, devtype, devid, subdevid, chlid).stream() .max(java.util.Comparator.comparing(TDeviceIngestRecord::getDeviceTimeUtc) .thenComparing(TDeviceIngestRecord::getRecordSequence)) .orElse(null); } @Override public List selectSamplesOfSource(String adapterCode, int devtype, int devid, int subdevid, int chlid, String sourceRef) { return samples(adapterCode, devtype, devid, subdevid, chlid).stream() .filter(row -> sourceRef.equals(row.getSourceRef())) .sorted(java.util.Comparator.comparing(TDeviceIngestRecord::getDeviceTimeUtc) .thenComparing(TDeviceIngestRecord::getRecordSequence)) .toList(); } @Override public List selectSamplesSince(String adapterCode, int devtype, int devid, int subdevid, int chlid, Date watermarkTime, long watermarkSequence, int limit) { List later = samples(adapterCode, devtype, devid, subdevid, chlid).stream() .filter(row -> after(row, watermarkTime, watermarkSequence)) .sorted(java.util.Comparator.comparing(TDeviceIngestRecord::getDeviceTimeUtc) .thenComparing(TDeviceIngestRecord::getRecordSequence)) .toList(); return later.size() > limit ? later.subList(0, limit) : later; } /** 全部落地行,供断言"拒绝时没有落任何数据"使用。 */ List rows() { return List.copyOf(rows); } List samples(String adapterCode, int devtype, int devid, int subdevid, int chlid) { return rows.stream().filter(row -> adapterCode.equals(row.getAdapterCode()) && TDeviceIngestRecord.KIND_SAMPLE.equals(row.getPayloadKind()) && row.getDevtype() == devtype && row.getDevid() == devid && row.getSubdevid() == subdevid && row.getChlid() == chlid).toList(); } private static boolean isSample(TDeviceIngestRecord row, String adapterCode, TDeviceIngestRecord channel) { return adapterCode.equals(row.getAdapterCode()) && TDeviceIngestRecord.KIND_SAMPLE.equals(row.getPayloadKind()) && row.getDevtype().equals(channel.getDevtype()) && row.getDevid().equals(channel.getDevid()) && row.getSubdevid().equals(channel.getSubdevid()) && row.getChlid().equals(channel.getChlid()); } private static boolean after(TDeviceIngestRecord row, Date watermarkTime, long watermarkSequence) { if (row.getDeviceTimeUtc() == null) return false; if (watermarkTime == null) return true; int byTime = row.getDeviceTimeUtc().compareTo(watermarkTime); return byTime > 0 || (byTime == 0 && row.getRecordSequence() > watermarkSequence); } private static boolean same(TDeviceIngestRecord row, TDeviceIngestRecord other) { return same(row, other.getAdapterCode(), other.getPayloadKind(), other.getRecordKey()); } private static boolean same(TDeviceIngestRecord row, String adapterCode, String payloadKind, String recordKey) { return adapterCode.equals(row.getAdapterCode()) && payloadKind.equals(row.getPayloadKind()) && recordKey.equals(row.getRecordKey()); } }