1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
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<TDeviceIngestRecord> 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<TDeviceIngestChannel> selectTopologyChannels(String adapterCode)
    {
        List<TDeviceIngestChannel> 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<TDeviceIngestRecord> 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<TDeviceIngestRecord> selectSamplesSince(String adapterCode, int devtype, int devid, int subdevid,
            int chlid, Date watermarkTime, long watermarkSequence, int limit)
    {
        List<TDeviceIngestRecord> 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<TDeviceIngestRecord> rows() { return List.copyOf(rows); }
 
    List<TDeviceIngestRecord> 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());
    }
}