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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
package com.ruoyi.wrzs.access.service;
 
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.wrzs.access.mapper.AccessGateMapper;
import com.ruoyi.wrzs.access.mapper.AccessPersonMapper;
import com.ruoyi.wrzs.access.mapper.AccessRecordMapper;
import com.ruoyi.wrzs.access.mapper.AccessVehicleMapper;
import com.ruoyi.wrzs.access.pojo.AccessGate;
import com.ruoyi.wrzs.access.pojo.AccessPerson;
import com.ruoyi.wrzs.access.pojo.AccessRecord;
import com.ruoyi.wrzs.access.pojo.AccessVehicle;
import com.ruoyi.wrzs.weighbridge.mapper.WeighbridgeRecordMapper;
import com.ruoyi.wrzs.weighbridge.pojo.WeighbridgeRecord;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
 
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
/**
 * 门禁管理业务
 * <p>
 * 车辆:车牌识别 + RFID 标签;人员:人脸识别 + 工卡刷卡;
 * 车辆放行与地磅联动:未完成过磅的车辆不允许出场,同时记录进出时间、车牌号与承运商
 */
@Slf4j
@AllArgsConstructor
@Service
public class AccessService {
 
    private final AccessGateMapper gateMapper;
    private final AccessVehicleMapper vehicleMapper;
    private final AccessPersonMapper personMapper;
    private final AccessRecordMapper recordMapper;
    private final WeighbridgeRecordMapper weighbridgeRecordMapper;
 
    // ==================== 门禁点 ====================
 
    public IPage<AccessGate> gatePage(Page<AccessGate> page, AccessGate query) {
        return gateMapper.selectPage(page, Wrappers.<AccessGate>lambdaQuery()
                .like(StringUtils.hasText(query.getGateName()), AccessGate::getGateName, query.getGateName())
                .eq(StringUtils.hasText(query.getGateType()), AccessGate::getGateType, query.getGateType())
                .eq(StringUtils.hasText(query.getAreaName()), AccessGate::getAreaName, query.getAreaName())
                .eq(StringUtils.hasText(query.getStatus()), AccessGate::getStatus, query.getStatus())
                .orderByDesc(AccessGate::getId));
    }
 
    public List<AccessGate> gateList() {
        // 门禁点下拉:只排除"停用"的(本系统 status='1' 表示正常/启用,'0' 表示停用,
        // 与 RuoYi 惯例相反。原先写死 status='0' 导致下拉框查不到任何门禁点)
        return gateMapper.selectList(Wrappers.<AccessGate>lambdaQuery()
                .ne(AccessGate::getStatus, "0")
                .orderByAsc(AccessGate::getGateCode));
    }
 
    public boolean gateSave(AccessGate gate) {
        if (!StringUtils.hasText(gate.getStatus())) {
            gate.setStatus("0");
        }
        if (!StringUtils.hasText(gate.getLinkageEnabled())) {
            gate.setLinkageEnabled("1");
        }
        return gateMapper.insert(gate) > 0;
    }
 
    public boolean gateUpdate(AccessGate gate) {
        return gateMapper.updateById(gate) > 0;
    }
 
    public boolean gateRemove(List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return false;
        }
        return gateMapper.deleteBatchIds(ids) > 0;
    }
 
    // ==================== 车辆授权 ====================
 
    public IPage<AccessVehicle> vehiclePage(Page<AccessVehicle> page, AccessVehicle query) {
        return vehicleMapper.selectPage(page, Wrappers.<AccessVehicle>lambdaQuery()
                .like(StringUtils.hasText(query.getPlateNumber()), AccessVehicle::getPlateNumber, query.getPlateNumber())
                .like(StringUtils.hasText(query.getCarrierName()), AccessVehicle::getCarrierName, query.getCarrierName())
                .like(StringUtils.hasText(query.getRfidTag()), AccessVehicle::getRfidTag, query.getRfidTag())
                .eq(StringUtils.hasText(query.getStatus()), AccessVehicle::getStatus, query.getStatus())
                .orderByDesc(AccessVehicle::getId));
    }
 
    public boolean vehicleSave(AccessVehicle vehicle) {
        if (!StringUtils.hasText(vehicle.getStatus())) {
            vehicle.setStatus("0");
        }
        return vehicleMapper.insert(vehicle) > 0;
    }
 
    public boolean vehicleUpdate(AccessVehicle vehicle) {
        return vehicleMapper.updateById(vehicle) > 0;
    }
 
    public boolean vehicleRemove(List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return false;
        }
        return vehicleMapper.deleteBatchIds(ids) > 0;
    }
 
    // ==================== 人员授权 ====================
 
    public IPage<AccessPerson> personPage(Page<AccessPerson> page, AccessPerson query) {
        return personMapper.selectPage(page, Wrappers.<AccessPerson>lambdaQuery()
                .like(StringUtils.hasText(query.getPersonName()), AccessPerson::getPersonName, query.getPersonName())
                .like(StringUtils.hasText(query.getCardNo()), AccessPerson::getCardNo, query.getCardNo())
                .eq(StringUtils.hasText(query.getDeptName()), AccessPerson::getDeptName, query.getDeptName())
                .eq(StringUtils.hasText(query.getPersonType()), AccessPerson::getPersonType, query.getPersonType())
                .eq(StringUtils.hasText(query.getStatus()), AccessPerson::getStatus, query.getStatus())
                .orderByDesc(AccessPerson::getId));
    }
 
    public boolean personSave(AccessPerson person) {
        if (!StringUtils.hasText(person.getStatus())) {
            person.setStatus("0");
        }
        return personMapper.insert(person) > 0;
    }
 
    public boolean personUpdate(AccessPerson person) {
        return personMapper.updateById(person) > 0;
    }
 
    public boolean personRemove(List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return false;
        }
        return personMapper.deleteBatchIds(ids) > 0;
    }
 
    // ==================== 识别放行 ====================
 
    /**
     * 车辆识别放行(车牌识别 + RFID 标签 + 地磅联动)
     *
     * @param gateId      门禁点ID
     * @param plateNumber 识别车牌
     * @param rfidTag     RFID 标签(可空,车牌识别失败时用标签匹配)
     * @param direction   0进场 1出场
     * @param photoUrl    抓拍图片
     */
    public Map<String, Object> vehicleRecognize(Long gateId, String plateNumber, String rfidTag,
                                                String direction, String photoUrl) {
        AccessGate gate = gateMapper.selectById(gateId);
        if (gate == null) {
            throw new IllegalArgumentException("门禁点不存在");
        }
        if (!"0".equals(gate.getStatus())) {
            throw new IllegalArgumentException("门禁点已停用");
        }
        if (!StringUtils.hasText(plateNumber) && !StringUtils.hasText(rfidTag)) {
            throw new IllegalArgumentException("请至少提供车牌号或RFID标签");
        }
 
        AccessRecord record = new AccessRecord();
        record.setGateId(gate.getId());
        record.setGateName(gate.getGateName());
        record.setRecordType("0");
        record.setPlateNumber(plateNumber);
        record.setDirection(StringUtils.hasText(direction) ? direction : "0");
        record.setPassTime(LocalDateTime.now());
        record.setPhotoUrl(photoUrl);
        record.setWeighbridgeCode(gate.getWeighbridgeCode());
 
        StringBuilder reasons = new StringBuilder();
        AccessVehicle vehicle = matchVehicle(plateNumber, rfidTag);
        String identifyMode = StringUtils.hasText(plateNumber) ? "车牌识别" : "RFID标签";
        if (vehicle != null) {
            // 车牌识别成功后由 RFID 复核,两种方式同时可用
            if (StringUtils.hasText(plateNumber) && StringUtils.hasText(rfidTag)
                    && StringUtils.hasText(vehicle.getRfidTag())) {
                identifyMode = vehicle.getRfidTag().equals(rfidTag) ? "车牌识别+RFID标签" : "车牌识别";
                if (!vehicle.getRfidTag().equals(rfidTag)) {
                    reasons.append("RFID标签与车牌不匹配;");
                }
            }
            record.setCarrierName(vehicle.getCarrierName());
        } else {
            reasons.append("车辆未登记授权;");
        }
        record.setIdentifyMode(identifyMode);
 
        // 授权有效性校验
        if (vehicle != null) {
            if (!"0".equals(vehicle.getStatus())) {
                reasons.append("车辆授权已禁用;");
            }
            LocalDate today = LocalDate.now();
            if (vehicle.getValidEnd() != null && vehicle.getValidEnd().isBefore(today)) {
                reasons.append("车辆授权已过期(").append(vehicle.getValidEnd()).append(");");
            }
            if (vehicle.getValidStart() != null && vehicle.getValidStart().isAfter(today)) {
                reasons.append("车辆授权未生效(").append(vehicle.getValidStart()).append(");");
            }
        }
 
        // 地磅联动:出场必须完成过磅
        if ("1".equals(gate.getLinkageEnabled()) && StringUtils.hasText(gate.getWeighbridgeCode())
                && StringUtils.hasText(plateNumber)) {
            Map<String, Object> linkage = checkWeighbridgeLinkage(plateNumber, gate.getWeighbridgeCode(), record.getDirection());
            record.setLinkageStatus(String.valueOf(linkage.get("message")));
            if (Boolean.FALSE.equals(linkage.get("pass"))) {
                reasons.append(linkage.get("message")).append(";");
            }
            if (linkage.get("recordNo") != null) {
                record.setWeightRecordNo(String.valueOf(linkage.get("recordNo")));
            }
        } else if ("1".equals(gate.getLinkageEnabled())) {
            record.setLinkageStatus("门禁未配置联动地磅,跳过地磅校验");
        }
 
        boolean allow = reasons.length() == 0;
        record.setAllowFlag(allow ? "0" : "1");
        record.setRejectReason(allow ? null : trimTail(reasons.toString()));
        recordMapper.insert(record);
 
        Map<String, Object> result = new HashMap<>();
        result.put("allow", allow);
        result.put("record", record);
        result.put("carrierName", record.getCarrierName());
        result.put("message", allow ? "身份核验通过,道闸已放行" : "核验未通过:" + record.getRejectReason());
        if (vehicle != null) {
            result.put("driverName", vehicle.getDriverName());
            result.put("driverPhone", vehicle.getDriverPhone());
        }
        return result;
    }
 
    /** 人员识别放行(人脸识别 + 工卡刷卡) */
    public Map<String, Object> personRecognize(Long gateId, String cardNo, String personName,
                                               String direction, String photoUrl) {
        AccessGate gate = gateMapper.selectById(gateId);
        if (gate == null) {
            throw new IllegalArgumentException("门禁点不存在");
        }
        if (!"0".equals(gate.getStatus())) {
            throw new IllegalArgumentException("门禁点已停用");
        }
        if (!StringUtils.hasText(cardNo) && !StringUtils.hasText(personName)) {
            throw new IllegalArgumentException("请至少提供工卡号或人员姓名(人脸识别)");
        }
        AccessRecord record = new AccessRecord();
        record.setGateId(gate.getId());
        record.setGateName(gate.getGateName());
        record.setRecordType("1");
        record.setDirection(StringUtils.hasText(direction) ? direction : "0");
        record.setPassTime(LocalDateTime.now());
        record.setPhotoUrl(photoUrl);
        record.setIdentifyMode(StringUtils.hasText(cardNo) ? "工卡刷卡" : "人脸识别");
 
        AccessPerson person = null;
        if (StringUtils.hasText(cardNo)) {
            person = personMapper.selectOne(Wrappers.<AccessPerson>lambdaQuery()
                    .eq(AccessPerson::getCardNo, cardNo).last("limit 1"));
        }
        if (person == null && StringUtils.hasText(personName)) {
            person = personMapper.selectOne(Wrappers.<AccessPerson>lambdaQuery()
                    .eq(AccessPerson::getPersonName, personName).last("limit 1"));
        }
        StringBuilder reasons = new StringBuilder();
        if (person == null) {
            reasons.append("人员未登记授权;");
            record.setPersonName(personName);
        } else {
            record.setPersonName(person.getPersonName());
            if (StringUtils.hasText(cardNo) && StringUtils.hasText(personName)) {
                record.setIdentifyMode("人脸识别+工卡刷卡");
                if (!person.getPersonName().equals(personName)) {
                    reasons.append("人脸识别结果与工卡持有人不一致;");
                }
            }
            if (!"0".equals(person.getStatus())) {
                reasons.append("人员授权已禁用;");
            }
            LocalDate today = LocalDate.now();
            if (person.getValidEnd() != null && person.getValidEnd().isBefore(today)) {
                reasons.append("人员授权已过期(").append(person.getValidEnd()).append(");");
            }
            if (person.getValidStart() != null && person.getValidStart().isAfter(today)) {
                reasons.append("人员授权未生效(").append(person.getValidStart()).append(");");
            }
        }
        boolean allow = reasons.length() == 0;
        record.setAllowFlag(allow ? "0" : "1");
        record.setRejectReason(allow ? null : trimTail(reasons.toString()));
        recordMapper.insert(record);
 
        Map<String, Object> result = new HashMap<>();
        result.put("allow", allow);
        result.put("record", record);
        result.put("message", allow ? "身份核验通过,闸机已放行" : "核验未通过:" + record.getRejectReason());
        return result;
    }
 
    /** 车牌/RFID 匹配车辆授权 */
    private AccessVehicle matchVehicle(String plateNumber, String rfidTag) {
        if (StringUtils.hasText(plateNumber)) {
            AccessVehicle byPlate = vehicleMapper.selectOne(Wrappers.<AccessVehicle>lambdaQuery()
                    .eq(AccessVehicle::getPlateNumber, plateNumber).last("limit 1"));
            if (byPlate != null) {
                return byPlate;
            }
        }
        if (StringUtils.hasText(rfidTag)) {
            return vehicleMapper.selectOne(Wrappers.<AccessVehicle>lambdaQuery()
                    .eq(AccessVehicle::getRfidTag, rfidTag).last("limit 1"));
        }
        return null;
    }
 
    /**
     * 地磅联动校验
     * 进场:校验是否有有效订单或在途过磅任务,不做强制拦截
     * 出场:必须存在已完成过磅记录,否则不允许放行(防止绕磅逃单)
     */
    private Map<String, Object> checkWeighbridgeLinkage(String plateNumber, String weighbridgeCode, String direction) {
        Map<String, Object> result = new HashMap<>();
        List<WeighbridgeRecord> records = weighbridgeRecordMapper.selectList(Wrappers.<WeighbridgeRecord>lambdaQuery()
                .eq(WeighbridgeRecord::getPlateNumber, plateNumber)
                .eq(StringUtils.hasText(weighbridgeCode), WeighbridgeRecord::getWeighbridgeCode, weighbridgeCode)
                .orderByDesc(WeighbridgeRecord::getId));
        if (CollectionUtils.isEmpty(records)) {
            if ("1".equals(direction)) {
                result.put("pass", false);
                result.put("message", "地磅联动校验:未查询到过磅记录,禁止出场");
            } else {
                result.put("pass", true);
                result.put("message", "地磅联动校验:进场登记成功,等待过磅");
            }
            return result;
        }
        WeighbridgeRecord latest = records.get(0);
        result.put("recordNo", latest.getRecordNo());
        if ("1".equals(direction)) {
            if ("2".equals(latest.getWeighStatus())) {
                result.put("pass", true);
                result.put("message", "地磅联动校验:过磅完成,允许出场");
            } else {
                result.put("pass", false);
                result.put("message", "地磅联动校验:车辆尚未完成称重,禁止出场");
            }
        } else {
            result.put("pass", true);
            result.put("message", "地磅联动校验:进场登记成功");
        }
        return result;
    }
 
    private String trimTail(String text) {
        String value = text.trim();
        while (value.endsWith(";") || value.endsWith(";")) {
            value = value.substring(0, value.length() - 1);
        }
        return value;
    }
 
    // ==================== 通行记录 ====================
 
    public IPage<AccessRecord> recordPage(Page<AccessRecord> page, AccessRecord query) {
        return recordMapper.selectPage(page, Wrappers.<AccessRecord>lambdaQuery()
                .like(StringUtils.hasText(query.getPlateNumber()), AccessRecord::getPlateNumber, query.getPlateNumber())
                .like(StringUtils.hasText(query.getPersonName()), AccessRecord::getPersonName, query.getPersonName())
                .like(StringUtils.hasText(query.getCarrierName()), AccessRecord::getCarrierName, query.getCarrierName())
                .eq(StringUtils.hasText(query.getGateName()), AccessRecord::getGateName, query.getGateName())
                .eq(StringUtils.hasText(query.getRecordType()), AccessRecord::getRecordType, query.getRecordType())
                .eq(StringUtils.hasText(query.getDirection()), AccessRecord::getDirection, query.getDirection())
                .eq(StringUtils.hasText(query.getAllowFlag()), AccessRecord::getAllowFlag, query.getAllowFlag())
                .ge(query.getPassTime() != null, AccessRecord::getPassTime, query.getPassTime())
                .orderByDesc(AccessRecord::getId));
    }
 
    public boolean recordRemove(List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return false;
        }
        return recordMapper.deleteBatchIds(ids) > 0;
    }
 
    /** 今日通行统计 */
    public Map<String, Object> recordOverview() {
        LocalDateTime start = LocalDate.now().atStartOfDay();
        Map<String, Object> result = new HashMap<>();
        result.put("vehicleIn", recordMapper.selectCount(Wrappers.<AccessRecord>lambdaQuery()
                .eq(AccessRecord::getRecordType, "0").eq(AccessRecord::getDirection, "0")
                .ge(AccessRecord::getPassTime, start)));
        result.put("vehicleOut", recordMapper.selectCount(Wrappers.<AccessRecord>lambdaQuery()
                .eq(AccessRecord::getRecordType, "0").eq(AccessRecord::getDirection, "1")
                .ge(AccessRecord::getPassTime, start)));
        result.put("personIn", recordMapper.selectCount(Wrappers.<AccessRecord>lambdaQuery()
                .eq(AccessRecord::getRecordType, "1").eq(AccessRecord::getDirection, "0")
                .ge(AccessRecord::getPassTime, start)));
        result.put("personOut", recordMapper.selectCount(Wrappers.<AccessRecord>lambdaQuery()
                .eq(AccessRecord::getRecordType, "1").eq(AccessRecord::getDirection, "1")
                .ge(AccessRecord::getPassTime, start)));
        result.put("reject", recordMapper.selectCount(Wrappers.<AccessRecord>lambdaQuery()
                .eq(AccessRecord::getAllowFlag, "1").ge(AccessRecord::getPassTime, start)));
        return result;
    }
}