集成大华通行记录增量同步与定时任务

dev
shenzhidan 2026-08-05 09:53:50 +08:00
parent 2934139574
commit d7c6c82bb8
48 changed files with 2008 additions and 16 deletions

View File

@ -2,7 +2,6 @@ package com.zcloud.primeport;
import com.jjb.saas.base.starter.bootstart.JJBSpringbootApplication;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
/**
* Spring Boot Starter

View File

@ -1,4 +1,9 @@
dahua:
resource:
sync-enabled: true
sync-page-size: 200
subsystem: evo-accesscontrol
auto-match-by-name: true
oauth:
host: 123.183.159.35
client-id: 123.183.159.35

View File

@ -1,8 +1,8 @@
spring:
config:
import:
# - classpath:nacos.yml
- classpath:prodnacos.yml
- classpath:nacos.yml
# - classpath:prodnacos.yml
- classpath:sdk.yml
- classpath:swagger.yml
# - classpath:ds.yml

View File

@ -85,6 +85,17 @@ message:
closedAreaPersonReceiveAuditReject: MS000161
# 大华平台配置
dahua:
access-record:
sync-enabled: true
sync-page-size: 1000
sync-delay-minutes: 5
sync-overlap-minutes: 10
initial-lookback-minutes: 1440
resource:
sync-enabled: true
sync-page-size: 200
subsystem: evo-accesscontrol
auto-match-by-name: true
oauth:
host: 123.183.159.35
client-id: 123.183.159.35
@ -92,4 +103,4 @@ dahua:
username: system
password: zcloud88888
http: false
port: 4443
port: 4443

View File

@ -7,9 +7,11 @@ import com.zcloud.primeport.domain.model.DaHuaCarAddCmd;
import com.zcloud.primeport.domain.model.DaHuaCarPageCmd;
import com.zcloud.primeport.domain.model.DaHuaPersonSyncCmd;
import com.zcloud.primeport.domain.model.DaHuaTempVehicleSaveCmd;
import com.zcloud.primeport.dto.DaHuaAccessRecordSyncCmd;
import com.zcloud.primeport.dto.DaHuaCarDeleteCmd;
import com.zcloud.primeport.dto.DaHuaPersonDeleteCmd;
import com.zcloud.primeport.dto.DaHuaResourceSyncCmd;
import com.zcloud.primeport.dto.clientobject.DaHuaAccessRecordSyncResultCO;
import com.zcloud.primeport.dto.clientobject.DaHuaResourceSyncResultCO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@ -18,6 +20,7 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import javax.validation.Valid;
@Api(tags = "大华平台对接")
@RequestMapping("/${application.gateway}/dahua")
@ -100,6 +103,16 @@ public class DaHuaController {
return daHuaService.queryAccessRecord(cmd);
}
@ApiOperation("同步大华门禁通行记录并落库")
@PostMapping("/access/record/sync")
public SingleResponse<DaHuaAccessRecordSyncResultCO> syncAccessRecords(
@Valid @RequestBody DaHuaAccessRecordSyncCmd cmd) {
if (daHuaService == null) {
throw new RuntimeException("大华服务未配置");
}
return daHuaService.syncAccessRecords(cmd);
}
@ApiOperation("删除人员")
@PostMapping("/person/delete")
public SingleResponse<Boolean> deletePerson(@Validated @RequestBody DaHuaPersonDeleteCmd cmd) {

View File

@ -0,0 +1,457 @@
package com.zcloud.primeport.service;
import cn.hutool.json.JSONUtil;
import com.zcloud.primeport.domain.gateway.DaHuaAccessRecordRepositoryGateway;
import com.zcloud.primeport.domain.gateway.DaHuaGateway;
import com.zcloud.primeport.domain.gateway.DaHuaResourceRepositoryGateway;
import com.zcloud.primeport.domain.gateway.DaHuaAccessRecordSyncGateway;
import com.zcloud.primeport.domain.model.CorpInfoSnapshotE;
import com.zcloud.primeport.domain.model.DaHuaAccessRecordCmd;
import com.zcloud.primeport.domain.model.DaHuaAccessRecordE;
import com.zcloud.primeport.domain.model.DaHuaAccessRecordSyncResultE;
import com.zcloud.primeport.domain.model.DaHuaDepartmentCorpMappingE;
import com.zcloud.primeport.domain.model.DaHuaDeviceChannelE;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.text.Normalizer;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@Service
@RequiredArgsConstructor
public class DaHuaAccessRecordSyncApplicationService implements DaHuaAccessRecordSyncGateway {
private static final DateTimeFormatter DATE_TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final int MAX_EXCEPTION_DETAILS = 100;
private final DaHuaGateway daHuaGateway;
private final DaHuaResourceRepositoryGateway resourceRepositoryGateway;
private final DaHuaAccessRecordRepositoryGateway accessRecordRepositoryGateway;
public DaHuaAccessRecordSyncResultE sync(String startSwingTime,
String endSwingTime,
String startCreateTime,
String endCreateTime,
int pageSize) {
validateTimeRange(startSwingTime, endSwingTime, "swing time");
validateOptionalTimeRange(startCreateTime, endCreateTime, "create time");
if (pageSize < 1 || pageSize > 10000) {
throw new IllegalArgumentException("pageSize must be between 1 and 10000");
}
SyncIndex index = buildIndex();
Map<String, DaHuaAccessRecordE> synchronizedRecords = new HashMap<>();
DaHuaAccessRecordSyncResultE result = new DaHuaAccessRecordSyncResultE();
int pageNum = 1;
while (true) {
List<Map<String, Object>> pageData = fetchPage(pageNum, pageSize, startSwingTime,
endSwingTime, startCreateTime, endCreateTime);
result.setPageCount(result.getPageCount() + 1);
if (pageData.isEmpty()) {
break;
}
persistPage(pageData, index, synchronizedRecords, result);
if (pageData.size() < pageSize) {
break;
}
pageNum++;
}
return result;
}
@Override
public DaHuaAccessRecordSyncResultE syncIncremental(int pageSize,
int delayMinutes,
int overlapMinutes,
int initialLookbackMinutes) {
LocalDateTime endCreateTime = LocalDateTime.now().minusMinutes(Math.max(delayMinutes, 0));
LocalDateTime latestCreateTime = accessRecordRepositoryGateway.findLatestDahuaCreateTime();
LocalDateTime startCreateTime = latestCreateTime == null
? endCreateTime.minusMinutes(Math.max(initialLookbackMinutes, 1))
: latestCreateTime.minusMinutes(Math.max(overlapMinutes, 0));
if (!startCreateTime.isBefore(endCreateTime)) {
return new DaHuaAccessRecordSyncResultE();
}
return sync(format(startCreateTime.minusDays(2)), format(endCreateTime.plusDays(1)),
format(startCreateTime), format(endCreateTime), pageSize);
}
private List<Map<String, Object>> fetchPage(int pageNum,
int pageSize,
String startSwingTime,
String endSwingTime,
String startCreateTime,
String endCreateTime) {
DaHuaAccessRecordCmd cmd = new DaHuaAccessRecordCmd();
cmd.setPageNum(pageNum);
cmd.setPageSize(pageSize);
cmd.setStartSwingTime(startSwingTime);
cmd.setEndSwingTime(endSwingTime);
cmd.setStartCreateTime(blankToNull(startCreateTime));
cmd.setEndCreateTime(blankToNull(endCreateTime));
Map<String, Object> response = daHuaGateway.queryAccessRecord(cmd);
if (response == null || !isSuccessful(response)) {
String code = response == null ? null : stringValue(response.get("code"));
String errMsg = response == null ? "empty response" : stringValue(response.get("errMsg"));
throw new IllegalStateException("Dahua access record query failed: code=" + code
+ ", errMsg=" + errMsg);
}
Map<String, Object> data = toMap(response.get("data"));
return toRecordList(data.get("pageData"));
}
protected void persistPage(List<Map<String, Object>> pageData,
SyncIndex index,
Map<String, DaHuaAccessRecordE> synchronizedRecords,
DaHuaAccessRecordSyncResultE result) {
Set<String> recordIds = new LinkedHashSet<>();
for (Map<String, Object> source : pageData) {
String recordId = blankToNull(firstString(source, "id", "recordId"));
if (recordId != null && !synchronizedRecords.containsKey(recordId)) {
recordIds.add(recordId);
}
}
for (DaHuaAccessRecordE existing : accessRecordRepositoryGateway.listByDahuaRecordIds(recordIds)) {
synchronizedRecords.put(existing.getDahuaRecordId(), existing);
}
for (Map<String, Object> source : pageData) {
result.setRecordTotal(result.getRecordTotal() + 1);
try {
String recordId = blankToNull(firstString(source, "id", "recordId"));
if (recordId == null) {
throw new IllegalArgumentException("record id is missing");
}
DaHuaAccessRecordE record = synchronizedRecords.get(recordId);
boolean created = record == null;
if (created) {
record = new DaHuaAccessRecordE();
}
mapRecord(record, source, recordId, index);
if (created) {
accessRecordRepositoryGateway.add(record);
result.setRecordCreated(result.getRecordCreated() + 1);
} else {
accessRecordRepositoryGateway.update(record);
result.setRecordUpdated(result.getRecordUpdated() + 1);
}
synchronizedRecords.put(recordId, record);
} catch (Exception e) {
result.setRecordInvalid(result.getRecordInvalid() + 1);
addDetail(result.getExceptionDetails(), "Access record sync failed: " + e.getMessage());
}
}
}
private void mapRecord(DaHuaAccessRecordE record,
Map<String, Object> source,
String recordId,
SyncIndex index) {
String channelCode = blankToNull(firstString(source, "channelCode", "acsChannelCode"));
String departmentName = blankToNull(firstString(source, "deptName", "departmentName"));
Long departmentId = firstLong(source, "deptId", "departmentId");
String orgCode = blankToNull(firstString(source, "orgCode", "departmentCode", "deptCode"));
DaHuaDeviceChannelE channel = channelCode == null ? null : index.channelsByCode.get(channelCode);
DaHuaDepartmentCorpMappingE personMapping = resolvePersonMapping(
index, departmentId, orgCode, departmentName);
CorpInfoSnapshotE personCorp = personMapping == null
? null : index.corpsById.get(personMapping.getCorpId());
CorpInfoSnapshotE deviceCorp = channel == null
? null : index.corpsById.get(channel.getCorpId());
record.setDahuaRecordId(recordId);
record.setCorpId(null);
record.setCorpName(null);
record.setPortArea(null);
record.setDahuaDepartmentId(departmentId);
if (personMapping != null) {
record.setCorpId(personMapping.getCorpId());
record.setCorpName(personCorp == null ? personMapping.getCorpName() : personCorp.getCorpName());
record.setPortArea(personCorp == null ? null : personCorp.getPortArea());
if (record.getDahuaDepartmentId() == null) {
record.setDahuaDepartmentId(personMapping.getDahuaDepartmentId());
}
}
record.setDahuaDepartmentName(departmentName != null
? departmentName : (personMapping == null ? null : personMapping.getDahuaDepartmentName()));
record.setDeviceCorpId(null);
record.setDeviceCorpName(null);
record.setDevicePortArea(null);
record.setMkmjId(null);
record.setPassageId(null);
record.setGateId(null);
if (channel != null) {
record.setDeviceCorpId(channel.getCorpId());
record.setDeviceCorpName(deviceCorp == null ? null : deviceCorp.getCorpName());
record.setDevicePortArea(deviceCorp == null ? null : deviceCorp.getPortArea());
record.setMkmjId(channel.getMkmjId());
record.setPassageId(channel.getPassageId());
record.setGateId(channel.getGateId());
}
record.setChannelCode(channelCode);
record.setChannelName(firstString(source, "channelName", "acsChannelName"));
record.setDeviceCode(firstString(source, "deviceCode"));
record.setDeviceName(firstString(source, "deviceName"));
record.setPersonId(firstLong(source, "personId"));
record.setPersonCode(firstString(source, "personCode"));
record.setPersonName(firstString(source, "personName"));
record.setPaperNumber(firstString(source, "paperNumber"));
record.setCardNumber(firstString(source, "cardNumber"));
record.setCardStatus(firstInteger(source, "cardStatus"));
record.setCardType(firstInteger(source, "cardType"));
record.setImageType(firstInteger(source, "imageType"));
record.setEnterOrExit(firstInteger(source, "enterOrExit"));
record.setOpenType(firstInteger(source, "openType"));
record.setOpenResult(firstInteger(source, "openResult"));
record.setRecordImageUrl(firstString(source, "recordImageUrl"));
record.setRemark(firstString(source, "remark"));
record.setSwingTime(firstDateTime(source, "swingTime"));
record.setDahuaCreateTime(firstDateTime(source, "createTime"));
record.setMaskState(firstInteger(source, "maskState"));
record.setOverTemp(firstBoolean(source, "overTemp"));
record.setCurrentTemperature(firstDecimal(source, "curTemp"));
record.setSourceType("PULL");
record.setRawData(JSONUtil.toJsonStr(source));
record.setLastSyncTime(LocalDateTime.now());
record.setDeleteEnum("FALSE");
}
private SyncIndex buildIndex() {
SyncIndex index = new SyncIndex();
for (CorpInfoSnapshotE corp : nullSafe(resourceRepositoryGateway.listActiveCorps())) {
index.corpsById.put(corp.getId(), corp);
}
for (DaHuaDeviceChannelE channel : nullSafe(resourceRepositoryGateway.listDeviceChannels())) {
if (channel.getChannelCode() != null && !"TRUE".equalsIgnoreCase(channel.getDeleteEnum())) {
index.channelsByCode.put(channel.getChannelCode(), channel);
}
}
for (DaHuaDepartmentCorpMappingE mapping : nullSafe(resourceRepositoryGateway.listMappings())) {
if (!isActive(mapping)) {
continue;
}
if (mapping.getDahuaDepartmentId() != null) {
index.mappingsByDepartmentId.put(mapping.getDahuaDepartmentId(), mapping);
}
String orgCode = blankToNull(mapping.getDahuaOrgCode());
if (orgCode != null) {
index.mappingsByOrgCode.put(orgCode, mapping);
}
String key = normalizeName(mapping.getDahuaDepartmentName());
if (key != null) {
index.mappingsByDepartmentName.computeIfAbsent(key, ignored -> new ArrayList<>()).add(mapping);
}
}
return index;
}
private DaHuaDepartmentCorpMappingE resolvePersonMapping(SyncIndex index,
Long departmentId,
String orgCode,
String departmentName) {
DaHuaDepartmentCorpMappingE mapping = departmentId == null
? null : index.mappingsByDepartmentId.get(departmentId);
if (mapping == null && orgCode != null) {
mapping = index.mappingsByOrgCode.get(orgCode);
}
return mapping != null
? mapping : uniqueMapping(index.mappingsByDepartmentName, departmentName);
}
private boolean isSuccessful(Map<String, Object> response) {
Object passed = response.get("passed");
if (passed instanceof Boolean) {
return (Boolean) passed;
}
Object success = response.get("success");
return Boolean.TRUE.equals(success) || "0".equals(stringValue(response.get("code")));
}
private boolean isActive(DaHuaDepartmentCorpMappingE mapping) {
return mapping != null
&& !"TRUE".equalsIgnoreCase(mapping.getDeleteEnum())
&& (mapping.getBindStatus() == null || mapping.getBindStatus().intValue() == 1);
}
private DaHuaDepartmentCorpMappingE uniqueMapping(
Map<String, List<DaHuaDepartmentCorpMappingE>> mappingsByName,
String departmentName) {
List<DaHuaDepartmentCorpMappingE> matches = mappingsByName.get(normalizeName(departmentName));
return matches != null && matches.size() == 1 ? matches.get(0) : null;
}
private void validateOptionalTimeRange(String start, String end, String label) {
if (isBlank(start) && isBlank(end)) {
return;
}
if (isBlank(start) || isBlank(end)) {
throw new IllegalArgumentException(label + " start and end must both be provided");
}
validateTimeRange(start, end, label);
}
private void validateTimeRange(String start, String end, String label) {
LocalDateTime startTime = parseRequiredDateTime(start, label + " start");
LocalDateTime endTime = parseRequiredDateTime(end, label + " end");
if (startTime.isAfter(endTime)) {
throw new IllegalArgumentException(label + " start cannot be after end");
}
}
private LocalDateTime parseRequiredDateTime(String value, String field) {
if (isBlank(value)) {
throw new IllegalArgumentException(field + " cannot be blank");
}
try {
return LocalDateTime.parse(value.trim(), DATE_TIME_FORMATTER);
} catch (DateTimeParseException e) {
throw new IllegalArgumentException(field + " must use yyyy-MM-dd HH:mm:ss", e);
}
}
private LocalDateTime firstDateTime(Map<String, Object> source, String... fields) {
String value = firstString(source, fields);
if (isBlank(value)) {
return null;
}
return parseRequiredDateTime(value, fields[0]);
}
private Object firstValue(Map<String, Object> source, String... fields) {
for (String field : fields) {
Object value = source.get(field);
if (value != null) {
return value;
}
}
return null;
}
private String firstString(Map<String, Object> source, String... fields) {
return stringValue(firstValue(source, fields));
}
private Long firstLong(Map<String, Object> source, String... fields) {
Object value = firstValue(source, fields);
if (value == null) {
return null;
}
if (value instanceof Number) {
return ((Number) value).longValue();
}
return Long.valueOf(value.toString());
}
private Integer firstInteger(Map<String, Object> source, String... fields) {
Object value = firstValue(source, fields);
if (value == null) {
return null;
}
if (value instanceof Number) {
return ((Number) value).intValue();
}
return Integer.valueOf(value.toString());
}
private Boolean firstBoolean(Map<String, Object> source, String... fields) {
Object value = firstValue(source, fields);
if (value == null) {
return null;
}
if (value instanceof Boolean) {
return (Boolean) value;
}
return Boolean.valueOf(value.toString());
}
private BigDecimal firstDecimal(Map<String, Object> source, String... fields) {
Object value = firstValue(source, fields);
return value == null ? null : new BigDecimal(value.toString());
}
@SuppressWarnings("unchecked")
private Map<String, Object> toMap(Object value) {
if (value == null) {
return new LinkedHashMap<>();
}
if (value instanceof Map) {
return new LinkedHashMap<>((Map<String, Object>) value);
}
return JSONUtil.parseObj(JSONUtil.toJsonStr(value));
}
@SuppressWarnings("unchecked")
private List<Map<String, Object>> toRecordList(Object value) {
List<Map<String, Object>> result = new ArrayList<>();
if (!(value instanceof Iterable)) {
return result;
}
for (Object item : (Iterable<?>) value) {
if (item instanceof Map) {
result.add(new LinkedHashMap<>((Map<String, Object>) item));
} else if (item != null) {
result.add(JSONUtil.parseObj(JSONUtil.toJsonStr(item)));
}
}
return result;
}
private <T> Collection<T> nullSafe(Collection<T> values) {
return values == null ? new ArrayList<>() : values;
}
private String normalizeName(String value) {
if (isBlank(value)) {
return null;
}
return Normalizer.normalize(value, Normalizer.Form.NFKC)
.replaceAll("\\s+", "")
.toLowerCase(Locale.ROOT);
}
private String stringValue(Object value) {
return value == null ? null : value.toString();
}
private String blankToNull(String value) {
return isBlank(value) ? null : value.trim();
}
private boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
private String format(LocalDateTime value) {
return value.format(DATE_TIME_FORMATTER);
}
private void addDetail(List<String> details, String detail) {
if (details.size() < MAX_EXCEPTION_DETAILS) {
details.add(detail);
}
}
private static class SyncIndex {
private final Map<Long, CorpInfoSnapshotE> corpsById = new HashMap<>();
private final Map<String, DaHuaDeviceChannelE> channelsByCode = new HashMap<>();
private final Map<Long, DaHuaDepartmentCorpMappingE> mappingsByDepartmentId = new HashMap<>();
private final Map<String, DaHuaDepartmentCorpMappingE> mappingsByOrgCode = new HashMap<>();
private final Map<String, List<DaHuaDepartmentCorpMappingE>> mappingsByDepartmentName =
new HashMap<>();
}
}

View File

@ -3,9 +3,11 @@ package com.zcloud.primeport.service;
import cn.hutool.json.JSONUtil;
import com.zcloud.primeport.domain.gateway.DaHuaResourceGateway;
import com.zcloud.primeport.domain.gateway.DaHuaResourceRepositoryGateway;
import com.zcloud.primeport.domain.gateway.DaHuaResourceSyncGateway;
import com.zcloud.primeport.domain.model.CorpInfoSnapshotE;
import com.zcloud.primeport.domain.model.DaHuaDepartmentCorpMappingE;
import com.zcloud.primeport.domain.model.DaHuaDeviceE;
import com.zcloud.primeport.domain.model.DaHuaDeviceChannelE;
import com.zcloud.primeport.domain.model.DaHuaResourcePageE;
import com.zcloud.primeport.domain.model.DaHuaResourceSyncResultE;
import lombok.RequiredArgsConstructor;
@ -24,7 +26,7 @@ import java.util.Set;
@Service
@RequiredArgsConstructor
public class DaHuaResourceSyncApplicationService {
public class DaHuaResourceSyncApplicationService implements DaHuaResourceSyncGateway {
private static final int MAX_PAGE_COUNT = 10000;
private static final int MAX_DETAIL_COUNT = 200;
@ -32,6 +34,7 @@ public class DaHuaResourceSyncApplicationService {
private final DaHuaResourceGateway resourceGateway;
private final DaHuaResourceRepositoryGateway repositoryGateway;
@Override
@Transactional(rollbackFor = Exception.class)
public DaHuaResourceSyncResultE sync(Integer pageSize, String subsystem, boolean autoMatchByName) {
int actualPageSize = pageSize == null ? 200 : pageSize;
@ -41,27 +44,40 @@ public class DaHuaResourceSyncApplicationService {
List<Map<String, Object>> departments = fetchAll(actualPageSize, subsystem, true);
List<Map<String, Object>> devices = fetchAll(actualPageSize, subsystem, false);
List<Map<String, Object>> channels = fetchAll(actualPageSize, subsystem, null);
if (channels.isEmpty()) {
channels = extractNestedChannels(devices);
}
DaHuaResourceSyncResultE result = new DaHuaResourceSyncResultE();
result.setDepartmentTotal(departments.size());
syncDepartmentMappings(departments, autoMatchByName, result);
result.setDeviceTotal(devices.size());
syncDevices(devices, result);
result.setChannelTotal(0);
syncDeviceChannels(channels, result);
deduplicateDetails(result);
return result;
}
private List<Map<String, Object>> fetchAll(int pageSize, String subsystem, boolean organization) {
private List<Map<String, Object>> fetchAll(int pageSize, String subsystem, Boolean organization) {
List<Map<String, Object>> records = new ArrayList<>();
long sourceRecordTotal = 0L;
for (int pageNum = 1; pageNum <= MAX_PAGE_COUNT; pageNum++) {
DaHuaResourcePageE page = organization
DaHuaResourcePageE page = organization == null
? resourceGateway.pageChannels(pageNum, pageSize, subsystem)
: organization
? resourceGateway.pageOrganizations(pageNum, pageSize, subsystem)
: resourceGateway.pageDevices(pageNum, pageSize, subsystem);
List<Map<String, Object>> pageRecords = page == null || page.getRecords() == null
? new ArrayList<>() : page.getRecords();
records.addAll(pageRecords);
if (pageRecords.isEmpty() || pageRecords.size() < pageSize
|| (page.getTotal() != null && records.size() >= page.getTotal())) {
int sourceRecordCount = page == null || page.getSourceRecordCount() == null
? pageRecords.size() : page.getSourceRecordCount();
sourceRecordTotal += sourceRecordCount;
Long remoteTotal = page == null ? null : page.getTotal();
if (sourceRecordCount == 0 || sourceRecordCount < pageSize
|| (remoteTotal != null && remoteTotal >= 0 && sourceRecordTotal >= remoteTotal)) {
return records;
}
}
@ -141,6 +157,7 @@ public class DaHuaResourceSyncApplicationService {
MappingIndex mappingIndex = new MappingIndex(repositoryGateway.listMappings());
DeviceIndex deviceIndex = new DeviceIndex(repositoryGateway.listDevices());
LocalDateTime now = LocalDateTime.now();
Set<String> seen = new LinkedHashSet<>();
for (Map<String, Object> source : devices) {
String deviceCode = blankToNull(firstString(source, "deviceCode", "code", "resourceCode"));
@ -148,6 +165,7 @@ public class DaHuaResourceSyncApplicationService {
result.setDeviceInvalid(result.getDeviceInvalid() + 1);
continue;
}
seen.add(deviceCode);
Long departmentId = firstLong(source, "departmentId", "deptId");
String orgCode = blankToNull(firstString(source,
@ -204,6 +222,127 @@ public class DaHuaResourceSyncApplicationService {
addDetail(result.getUnmatchedDeviceCodes(), deviceCode);
}
}
for (DaHuaDeviceE existing : deviceIndex.values()) {
if (!seen.contains(existing.getDeviceCode()) && !"TRUE".equalsIgnoreCase(existing.getDeleteEnum())) {
existing.setDeleteEnum("TRUE");
existing.setLastSyncTime(now);
repositoryGateway.updateDevice(existing);
result.setDeviceInvalid(result.getDeviceInvalid() + 1);
}
}
}
private void syncDeviceChannels(List<Map<String, Object>> channels, DaHuaResourceSyncResultE result) {
channels = expandChannelRecords(channels);
MappingIndex mappingIndex = new MappingIndex(repositoryGateway.listMappings());
ChannelIndex channelIndex = new ChannelIndex(repositoryGateway.listDeviceChannels());
LocalDateTime now = LocalDateTime.now();
Set<String> seen = new LinkedHashSet<>();
for (Map<String, Object> source : channels) {
try {
Integer unitType = channelUnitType(source);
if (unitType == null || unitType.intValue() != 7) {
continue;
}
result.setChannelTotal(result.getChannelTotal() + 1);
String channelCode = blankToNull(firstString(source, "channelCode", "code"));
String deviceCode = blankToNull(firstString(source, "deviceCode", "deviceCode"));
if (channelCode == null || deviceCode == null) {
result.setChannelInvalid(result.getChannelInvalid() + 1);
addDetail(result.getExceptionDetails(), "通道缺少channelCode或deviceCode: " + JSONUtil.toJsonStr(source));
continue;
}
seen.add(channelCode);
Long departmentId = firstLong(source, "dahuaDepartmentId", "departmentId", "deptId");
String orgCode = blankToNull(firstString(source, "dahuaOrgCode", "orgCode", "ownerCode"));
DaHuaDepartmentCorpMappingE mapping = departmentId == null ? null : mappingIndex.byDepartmentId.get(departmentId);
if (mapping == null && orgCode != null) {
mapping = mappingIndex.byOrgCode.get(orgCode);
}
DaHuaDeviceChannelE channel = channelIndex.byChannelCode.get(channelCode);
boolean created = channel == null;
if (created) {
channel = new DaHuaDeviceChannelE();
}
channel.setChannelCode(channelCode);
channel.setChannelName(firstString(source, "channelName", "name"));
channel.setDeviceCode(deviceCode);
channel.setCorpId(mapping == null ? null : mapping.getCorpId());
channel.setUnitType(unitType);
channel.setDahuaDepartmentId(departmentId != null ? departmentId : (mapping == null ? null : mapping.getDahuaDepartmentId()));
channel.setDahuaOrgCode(orgCode);
String orgName = firstString(source, "orgName", "departmentName", "deptName");
channel.setDahuaOrgName(isBlank(orgName) && mapping != null ? mapping.getDahuaDepartmentName() : orgName);
channel.setOnlineStatus(firstString(source, "onlineStatus", "isOnline", "status"));
channel.setRawData(JSONUtil.toJsonStr(source));
channel.setLastSyncTime(now);
channel.setDeleteEnum("FALSE");
if (created) {
repositoryGateway.addDeviceChannel(channel);
channelIndex.add(channel);
result.setChannelCreated(result.getChannelCreated() + 1);
} else {
repositoryGateway.updateDeviceChannel(channel);
result.setChannelUpdated(result.getChannelUpdated() + 1);
}
} catch (Exception e) {
result.setChannelInvalid(result.getChannelInvalid() + 1);
addDetail(result.getExceptionDetails(), "通道同步异常: " + e.getMessage());
}
}
for (DaHuaDeviceChannelE existing : channelIndex.values()) {
if (!seen.contains(existing.getChannelCode()) && !"TRUE".equalsIgnoreCase(existing.getDeleteEnum())) {
existing.setDeleteEnum("TRUE");
existing.setLastSyncTime(now);
repositoryGateway.updateDeviceChannel(existing);
result.setChannelInvalid(result.getChannelInvalid() + 1);
}
}
}
private List<Map<String, Object>> extractNestedChannels(List<Map<String, Object>> devices) {
List<Map<String, Object>> result = new ArrayList<>();
for (Map<String, Object> device : devices) {
Object nested = firstValue(device, "channels", "channelList");
if (!(nested instanceof Iterable)) {
continue;
}
for (Object value : (Iterable<?>) nested) {
if (value instanceof Map) {
Map<String, Object> channel = new HashMap<>((Map<String, Object>) value);
if (channel.get("deviceCode") == null) {
channel.put("deviceCode", firstString(device, "deviceCode", "code"));
}
if (channel.get("orgCode") == null) {
channel.put("orgCode", firstString(device, "orgCode", "ownerCode"));
}
result.add(channel);
}
}
}
return result;
}
@SuppressWarnings("unchecked")
private List<Map<String, Object>> expandChannelRecords(List<Map<String, Object>> records) {
List<Map<String, Object>> result = new ArrayList<>();
for (Map<String, Object> record : records) {
Object nested = firstValue(record, "channels", "channelList");
if (!(nested instanceof Iterable)) {
result.add(record);
continue;
}
for (Object value : (Iterable<?>) nested) {
if (value instanceof Map) {
Map<String, Object> channel = new HashMap<>((Map<String, Object>) value);
if (channel.get("deviceCode") == null) {
channel.put("deviceCode", firstString(record, "deviceCode", "code"));
}
result.add(channel);
}
}
}
return result;
}
private Map<String, List<CorpInfoSnapshotE>> groupCorpsByNormalizedName(List<CorpInfoSnapshotE> corps) {
@ -319,6 +458,7 @@ public class DaHuaResourceSyncApplicationService {
private void deduplicateDetails(DaHuaResourceSyncResultE result) {
result.setUnmatchedDepartmentNames(uniqueList(result.getUnmatchedDepartmentNames()));
result.setUnmatchedDeviceCodes(uniqueList(result.getUnmatchedDeviceCodes()));
result.setExceptionDetails(uniqueList(result.getExceptionDetails()));
}
private List<String> uniqueList(List<String> values) {
@ -377,5 +517,51 @@ public class DaHuaResourceSyncApplicationService {
}
add(device);
}
private List<DaHuaDeviceE> values() {
return new ArrayList<>(byDeviceCode.values());
}
}
private Integer channelUnitType(Map<String, Object> source) {
Integer unitType = firstInteger(source, "unitType", "unit_type");
if (unitType != null) {
return unitType;
}
String channelCode = firstString(source, "channelCode", "code");
if (!isBlank(channelCode)) {
String[] parts = channelCode.split("\\$");
if (parts.length > 1) {
try {
return Integer.valueOf(parts[1]);
} catch (NumberFormatException ignored) {
// Continue as an invalid channel below.
}
}
}
return null;
}
private static class ChannelIndex {
private final Map<String, DaHuaDeviceChannelE> byChannelCode = new HashMap<>();
private ChannelIndex(List<DaHuaDeviceChannelE> channels) {
if (channels == null) {
return;
}
for (DaHuaDeviceChannelE channel : channels) {
add(channel);
}
}
private void add(DaHuaDeviceChannelE channel) {
if (channel.getChannelCode() != null) {
byChannelCode.put(channel.getChannelCode(), channel);
}
}
private List<DaHuaDeviceChannelE> values() {
return new ArrayList<>(byChannelCode.values());
}
}
}

View File

@ -11,8 +11,11 @@ import com.zcloud.gbscommon.dahua.cmd.DaHuaPersonSyncCmd;
import com.zcloud.gbscommon.dahua.cmd.DaHuaTempVehicleSaveCmd;
import com.zcloud.gbscommon.dahua.facade.ZcloudDaHuaFacade;
import com.zcloud.primeport.domain.gateway.DaHuaGateway;
import com.zcloud.primeport.domain.model.DaHuaAccessRecordSyncResultE;
import com.zcloud.primeport.domain.model.DaHuaResourceSyncResultE;
import com.zcloud.primeport.dto.DaHuaAccessRecordSyncCmd;
import com.zcloud.primeport.dto.DaHuaResourceSyncCmd;
import com.zcloud.primeport.dto.clientobject.DaHuaAccessRecordSyncResultCO;
import com.zcloud.primeport.dto.clientobject.DaHuaResourceSyncResultCO;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.BeanUtils;
@ -31,6 +34,9 @@ public class DaHuaServiceImpl implements ZcloudDaHuaFacade {
@Autowired
private DaHuaResourceSyncApplicationService daHuaResourceSyncApplicationService;
@Autowired
private DaHuaAccessRecordSyncApplicationService daHuaAccessRecordSyncApplicationService;
private Long resolveDahuaDeptId(Long corpId) {
if (corpId == null) {
return null;
@ -236,6 +242,17 @@ public class DaHuaServiceImpl implements ZcloudDaHuaFacade {
return resp;
}
@Override
public SingleResponse<DaHuaAccessRecordSyncResultCO> syncAccessRecords(DaHuaAccessRecordSyncCmd cmd) {
int pageSize = cmd.getPageSize() == null ? 1000 : cmd.getPageSize();
DaHuaAccessRecordSyncResultE result = daHuaAccessRecordSyncApplicationService.sync(
cmd.getStartSwingTime(), cmd.getEndSwingTime(), cmd.getStartCreateTime(),
cmd.getEndCreateTime(), pageSize);
DaHuaAccessRecordSyncResultCO data = new DaHuaAccessRecordSyncResultCO();
BeanUtils.copyProperties(result, data);
return SingleResponse.of(data);
}
@Override
public SingleResponse<Object> saveTempVehicle(DaHuaTempVehicleSaveCmd cmd) {
cmd.setDepartmentId(resolveDahuaDeptId(cmd.getDepartmentId()));

View File

@ -0,0 +1,287 @@
package com.zcloud.primeport.service;
import com.zcloud.primeport.domain.gateway.DaHuaAccessRecordRepositoryGateway;
import com.zcloud.primeport.domain.gateway.DaHuaGateway;
import com.zcloud.primeport.domain.gateway.DaHuaResourceRepositoryGateway;
import com.zcloud.primeport.domain.model.CorpInfoSnapshotE;
import com.zcloud.primeport.domain.model.DaHuaAccessRecordCmd;
import com.zcloud.primeport.domain.model.DaHuaAccessRecordE;
import com.zcloud.primeport.domain.model.DaHuaAccessRecordSyncResultE;
import com.zcloud.primeport.domain.model.DaHuaDepartmentCorpMappingE;
import com.zcloud.primeport.domain.model.DaHuaDeviceChannelE;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class DaHuaAccessRecordSyncApplicationServiceTest {
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Mock
private DaHuaGateway daHuaGateway;
@Mock
private DaHuaResourceRepositoryGateway resourceRepositoryGateway;
@Mock
private DaHuaAccessRecordRepositoryGateway accessRecordRepositoryGateway;
private DaHuaAccessRecordSyncApplicationService applicationService;
@BeforeEach
void setUp() {
applicationService = new DaHuaAccessRecordSyncApplicationService(
daHuaGateway, resourceRepositoryGateway, accessRecordRepositoryGateway);
when(resourceRepositoryGateway.listActiveCorps()).thenReturn(Collections.emptyList());
when(resourceRepositoryGateway.listMappings()).thenReturn(Collections.emptyList());
when(resourceRepositoryGateway.listDeviceChannels()).thenReturn(Collections.emptyList());
}
@Test
void shouldFetchEveryPageAndStopOnShortPageWithoutTotals() {
when(accessRecordRepositoryGateway.listByDahuaRecordIds(anyCollection()))
.thenReturn(Collections.emptyList());
when(daHuaGateway.queryAccessRecord(any(DaHuaAccessRecordCmd.class)))
.thenAnswer(invocation -> {
DaHuaAccessRecordCmd cmd = invocation.getArgument(0);
if (cmd.getPageNum() == 1) {
return response(Arrays.asList(record("R-1"), record("R-2")));
}
if (cmd.getPageNum() == 2) {
return response(Collections.singletonList(record("R-3")));
}
return response(Collections.emptyList());
});
DaHuaAccessRecordSyncResultE result = applicationService.sync(
"2026-08-01 00:00:00", "2026-08-02 00:00:00", null, null, 2);
assertEquals(2, result.getPageCount());
assertEquals(3, result.getRecordTotal());
assertEquals(3, result.getRecordCreated());
ArgumentCaptor<DaHuaAccessRecordCmd> commandCaptor =
ArgumentCaptor.forClass(DaHuaAccessRecordCmd.class);
verify(daHuaGateway, org.mockito.Mockito.times(2)).queryAccessRecord(commandCaptor.capture());
assertEquals(Integer.valueOf(1), commandCaptor.getAllValues().get(0).getPageNum());
assertEquals(Integer.valueOf(2), commandCaptor.getAllValues().get(1).getPageNum());
}
@Test
void shouldUpdateIdempotentlyAndPersistPersonDeviceAndGateDimensions() {
CorpInfoSnapshotE personCorp = corp(100L, "人员企业", 1);
CorpInfoSnapshotE deviceCorp = corp(200L, "设备企业", 2);
when(resourceRepositoryGateway.listActiveCorps())
.thenReturn(Arrays.asList(personCorp, deviceCorp));
DaHuaDepartmentCorpMappingE mapping = new DaHuaDepartmentCorpMappingE();
mapping.setDahuaDepartmentId(10L);
mapping.setDahuaDepartmentName("映射表旧部门名称");
mapping.setCorpId(100L);
mapping.setCorpName("人员企业");
mapping.setBindStatus(1);
mapping.setDeleteEnum("FALSE");
when(resourceRepositoryGateway.listMappings()).thenReturn(Collections.singletonList(mapping));
DaHuaDeviceChannelE channel = new DaHuaDeviceChannelE();
channel.setChannelCode("CH-1");
channel.setCorpId(200L);
channel.setMkmjId(300L);
channel.setPassageId(301L);
channel.setGateId(302L);
channel.setDeleteEnum("FALSE");
when(resourceRepositoryGateway.listDeviceChannels()).thenReturn(Collections.singletonList(channel));
DaHuaAccessRecordE existing = new DaHuaAccessRecordE();
existing.setId(900L);
existing.setDahuaRecordId("R-1");
when(accessRecordRepositoryGateway.listByDahuaRecordIds(anyCollection()))
.thenReturn(Collections.singletonList(existing));
Map<String, Object> source = record("R-1");
source.put("deptId", 10L);
source.put("deptName", "大华最新部门名称");
source.put("channelCode", "CH-1");
source.put("enterOrExit", 1);
source.put("openResult", 1);
when(daHuaGateway.queryAccessRecord(any(DaHuaAccessRecordCmd.class)))
.thenReturn(response(Collections.singletonList(source)));
DaHuaAccessRecordSyncResultE result = applicationService.sync(
"2026-08-01 00:00:00", "2026-08-02 00:00:00", null, null, 100);
assertEquals(0, result.getRecordCreated());
assertEquals(1, result.getRecordUpdated());
verify(accessRecordRepositoryGateway, never()).add(any(DaHuaAccessRecordE.class));
ArgumentCaptor<DaHuaAccessRecordE> recordCaptor =
ArgumentCaptor.forClass(DaHuaAccessRecordE.class);
verify(accessRecordRepositoryGateway).update(recordCaptor.capture());
DaHuaAccessRecordE saved = recordCaptor.getValue();
assertEquals(Long.valueOf(100L), saved.getCorpId());
assertEquals("人员企业", saved.getCorpName());
assertEquals(Integer.valueOf(1), saved.getPortArea());
assertEquals(Long.valueOf(10L), saved.getDahuaDepartmentId());
assertEquals("大华最新部门名称", saved.getDahuaDepartmentName());
assertEquals(Long.valueOf(200L), saved.getDeviceCorpId());
assertEquals("设备企业", saved.getDeviceCorpName());
assertEquals(Integer.valueOf(2), saved.getDevicePortArea());
assertEquals(Long.valueOf(300L), saved.getMkmjId());
assertEquals(Long.valueOf(301L), saved.getPassageId());
assertEquals(Long.valueOf(302L), saved.getGateId());
assertEquals(Integer.valueOf(1), saved.getEnterOrExit());
assertEquals(Integer.valueOf(1), saved.getOpenResult());
}
@Test
void shouldMapEnterpriseByOrgCodeWhenDepartmentIdIsMissing() {
CorpInfoSnapshotE personCorp = corp(100L, "人员企业", 1);
when(resourceRepositoryGateway.listActiveCorps())
.thenReturn(Collections.singletonList(personCorp));
DaHuaDepartmentCorpMappingE mapping = new DaHuaDepartmentCorpMappingE();
mapping.setDahuaDepartmentId(10L);
mapping.setDahuaOrgCode("ORG-10");
mapping.setDahuaDepartmentName("映射表部门名称");
mapping.setCorpId(100L);
mapping.setCorpName("人员企业");
mapping.setBindStatus(1);
mapping.setDeleteEnum("FALSE");
when(resourceRepositoryGateway.listMappings()).thenReturn(Collections.singletonList(mapping));
when(accessRecordRepositoryGateway.listByDahuaRecordIds(anyCollection()))
.thenReturn(Collections.emptyList());
Map<String, Object> source = record("R-ORG");
source.put("orgCode", "ORG-10");
source.put("deptName", "另一个部门名称");
when(daHuaGateway.queryAccessRecord(any(DaHuaAccessRecordCmd.class)))
.thenReturn(response(Collections.singletonList(source)));
applicationService.sync(
"2026-08-01 00:00:00", "2026-08-02 00:00:00", null, null, 100);
ArgumentCaptor<DaHuaAccessRecordE> recordCaptor =
ArgumentCaptor.forClass(DaHuaAccessRecordE.class);
verify(accessRecordRepositoryGateway).add(recordCaptor.capture());
assertEquals(Long.valueOf(100L), recordCaptor.getValue().getCorpId());
assertEquals("人员企业", recordCaptor.getValue().getCorpName());
assertEquals(Long.valueOf(10L), recordCaptor.getValue().getDahuaDepartmentId());
}
@Test
void shouldKeepSourceDepartmentIdWhenEnterpriseIsNotMapped() {
when(accessRecordRepositoryGateway.listByDahuaRecordIds(anyCollection()))
.thenReturn(Collections.emptyList());
Map<String, Object> source = record("R-1");
source.put("deptId", 88L);
source.put("deptName", "未映射企业");
when(daHuaGateway.queryAccessRecord(any(DaHuaAccessRecordCmd.class)))
.thenReturn(response(Collections.singletonList(source)));
applicationService.sync(
"2026-08-01 00:00:00", "2026-08-02 00:00:00", null, null, 100);
ArgumentCaptor<DaHuaAccessRecordE> recordCaptor =
ArgumentCaptor.forClass(DaHuaAccessRecordE.class);
verify(accessRecordRepositoryGateway).add(recordCaptor.capture());
assertEquals(Long.valueOf(88L), recordCaptor.getValue().getDahuaDepartmentId());
assertEquals("未映射企业", recordCaptor.getValue().getDahuaDepartmentName());
assertEquals(null, recordCaptor.getValue().getCorpId());
}
@Test
void shouldCountMissingRecordIdAsInvalidAndContinue() {
Map<String, Object> invalid = record(null);
when(daHuaGateway.queryAccessRecord(any(DaHuaAccessRecordCmd.class)))
.thenReturn(response(Collections.singletonList(invalid)));
when(accessRecordRepositoryGateway.listByDahuaRecordIds(anyCollection()))
.thenReturn(Collections.emptyList());
DaHuaAccessRecordSyncResultE result = applicationService.sync(
"2026-08-01 00:00:00", "2026-08-02 00:00:00", null, null, 100);
assertEquals(1, result.getRecordTotal());
assertEquals(1, result.getRecordInvalid());
assertEquals(1, result.getExceptionDetails().size());
verify(accessRecordRepositoryGateway, never()).add(any(DaHuaAccessRecordE.class));
verify(accessRecordRepositoryGateway, never()).update(any(DaHuaAccessRecordE.class));
}
@Test
void shouldBuildIncrementalWindowFromLatestDahuaCreateTime() {
LocalDateTime latestCreateTime = LocalDateTime.now().minusHours(1).withNano(0);
when(accessRecordRepositoryGateway.findLatestDahuaCreateTime()).thenReturn(latestCreateTime);
when(daHuaGateway.queryAccessRecord(any(DaHuaAccessRecordCmd.class)))
.thenReturn(response(Collections.emptyList()));
LocalDateTime earliestExpectedEnd = LocalDateTime.now().minusMinutes(5).minusSeconds(2);
applicationService.syncIncremental(1000, 5, 10, 1440);
LocalDateTime latestExpectedEnd = LocalDateTime.now().minusMinutes(5).plusSeconds(2);
ArgumentCaptor<DaHuaAccessRecordCmd> commandCaptor =
ArgumentCaptor.forClass(DaHuaAccessRecordCmd.class);
verify(daHuaGateway).queryAccessRecord(commandCaptor.capture());
DaHuaAccessRecordCmd cmd = commandCaptor.getValue();
LocalDateTime startCreateTime = LocalDateTime.parse(cmd.getStartCreateTime(), FORMATTER);
LocalDateTime endCreateTime = LocalDateTime.parse(cmd.getEndCreateTime(), FORMATTER);
assertEquals(latestCreateTime.minusMinutes(10), startCreateTime);
assertFalse(endCreateTime.isBefore(earliestExpectedEnd));
assertFalse(endCreateTime.isAfter(latestExpectedEnd));
assertEquals(startCreateTime.minusDays(2),
LocalDateTime.parse(cmd.getStartSwingTime(), FORMATTER));
assertEquals(endCreateTime.plusDays(1),
LocalDateTime.parse(cmd.getEndSwingTime(), FORMATTER));
assertTrue(cmd.getPageSize() <= 10000);
}
private CorpInfoSnapshotE corp(Long id, String name, Integer portArea) {
CorpInfoSnapshotE corp = new CorpInfoSnapshotE();
corp.setId(id);
corp.setCorpName(name);
corp.setPortArea(portArea);
return corp;
}
private Map<String, Object> record(String id) {
Map<String, Object> value = new HashMap<>();
if (id != null) {
value.put("id", id);
}
value.put("personCode", "P-1");
value.put("personName", "测试人员");
value.put("swingTime", "2026-08-01 10:00:00");
value.put("createTime", "2026-08-01 10:00:05");
return value;
}
private Map<String, Object> response(Collection<Map<String, Object>> records) {
Map<String, Object> data = new HashMap<>();
data.put("pageData", new ArrayList<>(records));
Map<String, Object> response = new HashMap<>();
response.put("passed", true);
response.put("success", true);
response.put("code", "0");
response.put("data", data);
return response;
}
}

View File

@ -5,6 +5,7 @@ import com.zcloud.primeport.domain.gateway.DaHuaResourceRepositoryGateway;
import com.zcloud.primeport.domain.model.CorpInfoSnapshotE;
import com.zcloud.primeport.domain.model.DaHuaDepartmentCorpMappingE;
import com.zcloud.primeport.domain.model.DaHuaDeviceE;
import com.zcloud.primeport.domain.model.DaHuaDeviceChannelE;
import com.zcloud.primeport.domain.model.DaHuaResourcePageE;
import com.zcloud.primeport.domain.model.DaHuaResourceSyncResultE;
import org.junit.jupiter.api.BeforeEach;
@ -27,6 +28,7 @@ import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.times;
@ExtendWith(MockitoExtension.class)
class DaHuaResourceSyncApplicationServiceTest {
@ -91,6 +93,126 @@ class DaHuaResourceSyncApplicationServiceTest {
verify(resourceGateway, never()).pageOrganizations(any(), any(), any());
}
@Test
void shouldPageChannelsFilterNonAccessChannelsAndMarkMissingAsInvalid() {
when(resourceGateway.pageOrganizations(1, 2, "access")).thenReturn(page(Collections.emptyList(), 0L));
when(resourceGateway.pageDevices(1, 2, "access")).thenReturn(page(Collections.emptyList(), 0L));
when(resourceGateway.pageChannels(1, 2, "access")).thenReturn(page(
java.util.Arrays.asList(channel("CH-1", "DEV-1", 7), channel("CH-2", "DEV-1", 8)), 2L));
when(repositoryGateway.listActiveCorps()).thenReturn(Collections.emptyList());
when(repositoryGateway.listMappings()).thenReturn(Collections.emptyList());
when(repositoryGateway.listDevices()).thenReturn(Collections.emptyList());
DaHuaDeviceChannelE old = new DaHuaDeviceChannelE();
old.setId(9L);
old.setChannelCode("CH-OLD");
old.setDeleteEnum("FALSE");
when(repositoryGateway.listDeviceChannels()).thenReturn(Collections.singletonList(old));
DaHuaResourceSyncResultE result = applicationService.sync(2, "access", true);
assertEquals(1, result.getChannelTotal());
assertEquals(1, result.getChannelCreated());
assertEquals(1, result.getChannelInvalid());
verify(repositoryGateway).addDeviceChannel(any(DaHuaDeviceChannelE.class));
verify(repositoryGateway, times(1)).updateDeviceChannel(any(DaHuaDeviceChannelE.class));
}
@Test
void shouldUpdateExistingChannelIdempotentlyAndSupportMultipleChannelsPerDevice() {
when(resourceGateway.pageOrganizations(1, 100, "access")).thenReturn(page(Collections.emptyList(), 0L));
when(resourceGateway.pageDevices(1, 100, "access")).thenReturn(page(Collections.emptyList(), 0L));
when(resourceGateway.pageChannels(1, 100, "access")).thenReturn(page(
java.util.Arrays.asList(channel("CH-1", "DEV-1", 7), channel("CH-2", "DEV-1", 7)), 2L));
when(repositoryGateway.listActiveCorps()).thenReturn(Collections.emptyList());
when(repositoryGateway.listMappings()).thenReturn(Collections.emptyList());
when(repositoryGateway.listDevices()).thenReturn(Collections.emptyList());
DaHuaDeviceChannelE existing = new DaHuaDeviceChannelE();
existing.setId(10L);
existing.setChannelCode("CH-1");
existing.setDeleteEnum("TRUE");
when(repositoryGateway.listDeviceChannels()).thenReturn(Collections.singletonList(existing));
DaHuaResourceSyncResultE result = applicationService.sync(100, "access", true);
assertEquals(2, result.getChannelTotal());
assertEquals(1, result.getChannelCreated());
assertEquals(1, result.getChannelUpdated());
ArgumentCaptor<DaHuaDeviceChannelE> updated = ArgumentCaptor.forClass(DaHuaDeviceChannelE.class);
verify(repositoryGateway).updateDeviceChannel(updated.capture());
assertEquals("FALSE", updated.getValue().getDeleteEnum());
assertEquals("CH-1", updated.getValue().getChannelCode());
}
@Test
void shouldFetchAllChannelPages() {
when(resourceGateway.pageOrganizations(1, 1, "access")).thenReturn(page(Collections.emptyList(), 0L));
when(resourceGateway.pageDevices(1, 1, "access")).thenReturn(page(Collections.emptyList(), 0L));
when(resourceGateway.pageChannels(1, 1, "access")).thenReturn(page(
Collections.singletonList(channel("CH-1", "DEV-1", 7)), null));
when(resourceGateway.pageChannels(2, 1, "access")).thenReturn(page(
Collections.singletonList(channel("CH-2", "DEV-1", 7)), null));
when(resourceGateway.pageChannels(3, 1, "access")).thenReturn(page(Collections.emptyList(), null));
when(repositoryGateway.listActiveCorps()).thenReturn(Collections.emptyList());
when(repositoryGateway.listMappings()).thenReturn(Collections.emptyList());
when(repositoryGateway.listDevices()).thenReturn(Collections.emptyList());
when(repositoryGateway.listDeviceChannels()).thenReturn(Collections.emptyList());
DaHuaResourceSyncResultE result = applicationService.sync(1, "access", true);
assertEquals(2, result.getChannelTotal());
verify(resourceGateway).pageChannels(2, 1, "access");
}
@Test
void shouldIgnoreUnknownNegativeTotalAndContinuePaging() {
when(resourceGateway.pageOrganizations(1, 1, "access")).thenReturn(page(
Collections.singletonList(department(10L, "企业一", "ORG-10")), -1L));
when(resourceGateway.pageOrganizations(2, 1, "access")).thenReturn(page(
Collections.singletonList(department(11L, "企业二", "ORG-11")), -1L));
when(resourceGateway.pageOrganizations(3, 1, "access"))
.thenReturn(page(Collections.emptyList(), -1L));
when(resourceGateway.pageDevices(1, 1, "access"))
.thenReturn(page(Collections.emptyList(), 0L));
when(resourceGateway.pageChannels(1, 1, "access"))
.thenReturn(page(Collections.emptyList(), 0L));
when(repositoryGateway.listActiveCorps()).thenReturn(Collections.emptyList());
when(repositoryGateway.listMappings()).thenReturn(Collections.emptyList());
when(repositoryGateway.listDevices()).thenReturn(Collections.emptyList());
when(repositoryGateway.listDeviceChannels()).thenReturn(Collections.emptyList());
DaHuaResourceSyncResultE result = applicationService.sync(1, "access", false);
assertEquals(2, result.getDepartmentTotal());
verify(resourceGateway).pageOrganizations(3, 1, "access");
}
@Test
void shouldPageChannelsByRemoteDeviceCountInsteadOfExpandedChannelCount() {
when(resourceGateway.pageOrganizations(1, 1, "access"))
.thenReturn(page(Collections.emptyList(), 0L));
when(resourceGateway.pageDevices(1, 1, "access"))
.thenReturn(page(Collections.emptyList(), 0L));
DaHuaResourcePageE firstPage = page(Collections.emptyList(), null);
firstPage.setSourceRecordCount(1);
DaHuaResourcePageE secondPage = page(
Collections.singletonList(channel("CH-2", "DEV-2", 7)), null);
secondPage.setSourceRecordCount(1);
DaHuaResourcePageE lastPage = page(Collections.emptyList(), null);
lastPage.setSourceRecordCount(0);
when(resourceGateway.pageChannels(1, 1, "access")).thenReturn(firstPage);
when(resourceGateway.pageChannels(2, 1, "access")).thenReturn(secondPage);
when(resourceGateway.pageChannels(3, 1, "access")).thenReturn(lastPage);
when(repositoryGateway.listActiveCorps()).thenReturn(Collections.emptyList());
when(repositoryGateway.listMappings()).thenReturn(Collections.emptyList());
when(repositoryGateway.listDevices()).thenReturn(Collections.emptyList());
when(repositoryGateway.listDeviceChannels()).thenReturn(Collections.emptyList());
DaHuaResourceSyncResultE result = applicationService.sync(1, "access", false);
assertEquals(1, result.getChannelTotal());
verify(resourceGateway).pageChannels(3, 1, "access");
}
private DaHuaResourcePageE page(List<Map<String, Object>> records, Long total) {
DaHuaResourcePageE page = new DaHuaResourcePageE();
page.setRecords(records);
@ -114,4 +236,14 @@ class DaHuaResourceSyncApplicationServiceTest {
value.put("ownerCode", orgCode);
return value;
}
private Map<String, Object> channel(String code, String deviceCode, int unitType) {
Map<String, Object> value = new HashMap<>();
value.put("channelCode", code);
value.put("channelName", code);
value.put("deviceCode", deviceCode);
value.put("unitType", unitType);
value.put("onlineStatus", "ON");
return value;
}
}

View File

@ -7,9 +7,11 @@ import com.zcloud.primeport.domain.model.DaHuaCarAddCmd;
import com.zcloud.primeport.domain.model.DaHuaCarPageCmd;
import com.zcloud.primeport.domain.model.DaHuaPersonSyncCmd;
import com.zcloud.primeport.domain.model.DaHuaTempVehicleSaveCmd;
import com.zcloud.primeport.dto.DaHuaAccessRecordSyncCmd;
import com.zcloud.primeport.dto.DaHuaCarDeleteCmd;
import com.zcloud.primeport.dto.DaHuaPersonDeleteCmd;
import com.zcloud.primeport.dto.DaHuaResourceSyncCmd;
import com.zcloud.primeport.dto.clientobject.DaHuaAccessRecordSyncResultCO;
import com.zcloud.primeport.dto.clientobject.DaHuaResourceSyncResultCO;
import java.util.Map;
@ -34,6 +36,8 @@ public interface DaHuaServiceI {
SingleResponse<Map<String, Object>> queryAccessRecord(DaHuaAccessRecordCmd cmd);
SingleResponse<DaHuaAccessRecordSyncResultCO> syncAccessRecords(DaHuaAccessRecordSyncCmd cmd);
SingleResponse<Object> saveTempVehicle(DaHuaTempVehicleSaveCmd cmd);
SingleResponse<DaHuaResourceSyncResultCO> syncResources(DaHuaResourceSyncCmd cmd);

View File

@ -0,0 +1,32 @@
package com.zcloud.primeport.dto;
import com.alibaba.cola.dto.Command;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
@Data
public class DaHuaAccessRecordSyncCmd extends Command {
@ApiModelProperty(value = "Dahua event start time, yyyy-MM-dd HH:mm:ss", required = true)
@NotBlank(message = "startSwingTime cannot be blank")
private String startSwingTime;
@ApiModelProperty(value = "Dahua event end time, yyyy-MM-dd HH:mm:ss", required = true)
@NotBlank(message = "endSwingTime cannot be blank")
private String endSwingTime;
@ApiModelProperty("Dahua storage start time, yyyy-MM-dd HH:mm:ss")
private String startCreateTime;
@ApiModelProperty("Dahua storage end time, yyyy-MM-dd HH:mm:ss")
private String endCreateTime;
@ApiModelProperty("Page size, default 1000 and maximum 10000")
@Min(value = 1, message = "pageSize must be positive")
@Max(value = 10000, message = "pageSize cannot exceed 10000")
private Integer pageSize;
}

View File

@ -0,0 +1,18 @@
package com.zcloud.primeport.dto.clientobject;
import com.alibaba.cola.dto.ClientObject;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class DaHuaAccessRecordSyncResultCO extends ClientObject {
private int pageCount;
private int recordTotal;
private int recordCreated;
private int recordUpdated;
private int recordInvalid;
private List<String> exceptionDetails = new ArrayList<>();
}

View File

@ -30,8 +30,18 @@ public class DaHuaResourceSyncResultCO extends ClientObject {
private Integer deviceUnmatched = 0;
@ApiModelProperty("无效设备数据数")
private Integer deviceInvalid = 0;
@ApiModelProperty("门禁通道总数")
private Integer channelTotal = 0;
@ApiModelProperty("新增门禁通道数")
private Integer channelCreated = 0;
@ApiModelProperty("更新门禁通道数")
private Integer channelUpdated = 0;
@ApiModelProperty("失效门禁通道数")
private Integer channelInvalid = 0;
@ApiModelProperty("未匹配的大华部门名称")
private List<String> unmatchedDepartmentNames = new ArrayList<>();
@ApiModelProperty("未匹配企业的大华设备编码")
private List<String> unmatchedDeviceCodes = new ArrayList<>();
@ApiModelProperty("同步异常明细")
private List<String> exceptionDetails = new ArrayList<>();
}

View File

@ -0,0 +1,18 @@
package com.zcloud.primeport.domain.gateway;
import com.zcloud.primeport.domain.model.DaHuaAccessRecordE;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
public interface DaHuaAccessRecordRepositoryGateway {
List<DaHuaAccessRecordE> listByDahuaRecordIds(Collection<String> dahuaRecordIds);
LocalDateTime findLatestDahuaCreateTime();
void add(DaHuaAccessRecordE record);
void update(DaHuaAccessRecordE record);
}

View File

@ -0,0 +1,12 @@
package com.zcloud.primeport.domain.gateway;
import com.zcloud.primeport.domain.model.DaHuaAccessRecordSyncResultE;
/**
* Application port used by the scheduled access-record synchronization job.
*/
public interface DaHuaAccessRecordSyncGateway {
DaHuaAccessRecordSyncResultE syncIncremental(int pageSize, int delayMinutes,
int overlapMinutes, int initialLookbackMinutes);
}

View File

@ -7,4 +7,7 @@ public interface DaHuaResourceGateway {
DaHuaResourcePageE pageOrganizations(Integer pageNum, Integer pageSize, String subsystem);
DaHuaResourcePageE pageDevices(Integer pageNum, Integer pageSize, String subsystem);
/** Official V5.0.18 device/subsystem/page response contains nested channels. */
DaHuaResourcePageE pageChannels(Integer pageNum, Integer pageSize, String subsystem);
}

View File

@ -3,6 +3,7 @@ package com.zcloud.primeport.domain.gateway;
import com.zcloud.primeport.domain.model.CorpInfoSnapshotE;
import com.zcloud.primeport.domain.model.DaHuaDepartmentCorpMappingE;
import com.zcloud.primeport.domain.model.DaHuaDeviceE;
import com.zcloud.primeport.domain.model.DaHuaDeviceChannelE;
import java.util.List;
@ -21,4 +22,10 @@ public interface DaHuaResourceRepositoryGateway {
void addDevice(DaHuaDeviceE device);
void updateDevice(DaHuaDeviceE device);
List<DaHuaDeviceChannelE> listDeviceChannels();
void addDeviceChannel(DaHuaDeviceChannelE channel);
void updateDeviceChannel(DaHuaDeviceChannelE channel);
}

View File

@ -0,0 +1,11 @@
package com.zcloud.primeport.domain.gateway;
import com.zcloud.primeport.domain.model.DaHuaResourceSyncResultE;
/**
* Application port used by the scheduled resource synchronization job.
*/
public interface DaHuaResourceSyncGateway {
DaHuaResourceSyncResultE sync(Integer pageSize, String subsystem, boolean autoMatchByName);
}

View File

@ -7,4 +7,5 @@ public class CorpInfoSnapshotE {
private Long id;
private String corpName;
private Integer portArea;
}

View File

@ -0,0 +1,59 @@
package com.zcloud.primeport.domain.model;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
public class DaHuaAccessRecordE {
private Long id;
private String dahuaRecordId;
private Long corpId;
private String corpName;
private Integer portArea;
private Long deviceCorpId;
private String deviceCorpName;
private Integer devicePortArea;
private Long dahuaDepartmentId;
private String dahuaDepartmentName;
private Long mkmjId;
private Long passageId;
private Long gateId;
private String channelCode;
private String channelName;
private String deviceCode;
private String deviceName;
private Long personId;
private String personCode;
private String personName;
private String paperNumber;
private String cardNumber;
private Integer cardStatus;
private Integer cardType;
private Integer imageType;
private Integer enterOrExit;
private Integer openType;
private Integer openResult;
private String recordImageUrl;
private String remark;
private LocalDateTime swingTime;
private LocalDateTime dahuaCreateTime;
private Integer maskState;
private Boolean overTemp;
private BigDecimal currentTemperature;
private String sourceType;
private String rawData;
private LocalDateTime lastSyncTime;
private String deleteEnum;
}

View File

@ -0,0 +1,17 @@
package com.zcloud.primeport.domain.model;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class DaHuaAccessRecordSyncResultE {
private int pageCount;
private int recordTotal;
private int recordCreated;
private int recordUpdated;
private int recordInvalid;
private List<String> exceptionDetails = new ArrayList<>();
}

View File

@ -0,0 +1,26 @@
package com.zcloud.primeport.domain.model;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class DaHuaDeviceChannelE {
private Long id;
private String channelCode;
private String channelName;
private String deviceCode;
private Long corpId;
private Long mkmjId;
private Long passageId;
private Long gateId;
private Integer unitType;
private Long dahuaDepartmentId;
private String dahuaOrgCode;
private String dahuaOrgName;
private String onlineStatus;
private String rawData;
private LocalDateTime lastSyncTime;
private String deleteEnum;
}

View File

@ -11,4 +11,6 @@ public class DaHuaResourcePageE {
private List<Map<String, Object>> records = new ArrayList<>();
private Long total;
/** Number of records returned by the remote page before nested channels are expanded. */
private Integer sourceRecordCount;
}

View File

@ -18,6 +18,11 @@ public class DaHuaResourceSyncResultE {
private Integer deviceUpdated = 0;
private Integer deviceUnmatched = 0;
private Integer deviceInvalid = 0;
private Integer channelTotal = 0;
private Integer channelCreated = 0;
private Integer channelUpdated = 0;
private Integer channelInvalid = 0;
private List<String> unmatchedDepartmentNames = new ArrayList<>();
private List<String> unmatchedDeviceCodes = new ArrayList<>();
private List<String> exceptionDetails = new ArrayList<>();
}

View File

@ -31,6 +31,10 @@ public class DaHuaResourceApi {
return postPage(properties.getDevicePagePath(), body);
}
public GeneralResponse pageChannels(Map<String, Object> body) throws ClientException {
return postPage(properties.getChannelPagePath(), body);
}
private GeneralResponse postPage(String path, Map<String, Object> body) throws ClientException {
if (daHuaApiClient == null) {
return null;

View File

@ -0,0 +1,15 @@
package com.zcloud.primeport.dahua.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "dahua.access-record")
public class DaHuaAccessRecordProperties {
private boolean syncEnabled = true;
private Integer syncPageSize = 1000;
private Integer syncDelayMinutes = 5;
private Integer syncOverlapMinutes = 10;
private Integer initialLookbackMinutes = 1440;
}

View File

@ -8,7 +8,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties({DaHuaProperties.class, DaHuaResourceProperties.class})
@EnableConfigurationProperties({DaHuaProperties.class, DaHuaResourceProperties.class,
DaHuaAccessRecordProperties.class})
public class DaHuaAutoConfig {
@Bean("dahuaApiOauthConfig")

View File

@ -9,4 +9,13 @@ public class DaHuaResourceProperties {
private String organizationPagePath = "/evo-apigw/evo-brm/1.2.0/organization/subsystem/page";
private String devicePagePath = "/evo-apigw/evo-brm/1.2.0/device/subsystem/page";
/** V5.0.18 uses the same device pagination endpoint; channels are nested in pageData. */
private String channelPagePath = "/evo-apigw/evo-brm/1.2.0/device/subsystem/page";
private Integer channelCategory = 8;
/** Official access-control example type; override when the platform uses another 8_x subtype. */
private String channelType = "8_16";
private boolean syncEnabled = true;
private Integer syncPageSize = 200;
private String subsystem = "evo-accesscontrol";
private boolean autoMatchByName = true;
}

View File

@ -0,0 +1,84 @@
package com.zcloud.primeport.gatewayimpl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.jjb.saas.framework.repository.basedo.BaseDO;
import com.zcloud.primeport.domain.gateway.DaHuaAccessRecordRepositoryGateway;
import com.zcloud.primeport.domain.model.DaHuaAccessRecordE;
import com.zcloud.primeport.persistence.dataobject.DaHuaAccessRecordDO;
import com.zcloud.primeport.persistence.repository.DaHuaAccessRecordRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@Service
@RequiredArgsConstructor
public class DaHuaAccessRecordRepositoryGatewayImpl implements DaHuaAccessRecordRepositoryGateway {
private final DaHuaAccessRecordRepository repository;
private final DefaultIdentifierGenerator idGenerator = new DefaultIdentifierGenerator();
@Override
public List<DaHuaAccessRecordE> listByDahuaRecordIds(Collection<String> dahuaRecordIds) {
if (dahuaRecordIds == null || dahuaRecordIds.isEmpty()) {
return Collections.emptyList();
}
LambdaQueryWrapper<DaHuaAccessRecordDO> query = new LambdaQueryWrapper<>();
query.in(DaHuaAccessRecordDO::getDahuaRecordId, dahuaRecordIds);
return convertList(repository.list(query));
}
@Override
public LocalDateTime findLatestDahuaCreateTime() {
return repository.findLatestDahuaCreateTime();
}
@Override
public void add(DaHuaAccessRecordE record) {
DaHuaAccessRecordDO data = copy(record, DaHuaAccessRecordDO.class);
initializeBase(data);
repository.save(data);
record.setId(data.getId());
}
@Override
public void update(DaHuaAccessRecordE record) {
DaHuaAccessRecordDO data = copy(record, DaHuaAccessRecordDO.class);
data.setUpdateTime(LocalDateTime.now());
repository.updateById(data);
}
private void initializeBase(BaseDO target) {
LocalDateTime now = LocalDateTime.now();
target.setId(idGenerator.nextId(target).longValue());
target.setDeleteEnum("FALSE");
target.setEnv("PROD");
target.setVersion(0);
target.setCreateTime(now);
target.setUpdateTime(now);
}
private List<DaHuaAccessRecordE> convertList(List<DaHuaAccessRecordDO> source) {
List<DaHuaAccessRecordE> result = new ArrayList<>();
for (DaHuaAccessRecordDO item : source) {
result.add(copy(item, DaHuaAccessRecordE.class));
}
return result;
}
private <T> T copy(Object source, Class<T> targetType) {
try {
T target = targetType.newInstance();
BeanUtils.copyProperties(source, target);
return target;
} catch (InstantiationException | IllegalAccessException e) {
throw new IllegalStateException("Failed to convert Dahua access record persistence object", e);
}
}
}

View File

@ -6,12 +6,14 @@ import cn.hutool.json.JSONUtil;
import com.dahuatech.icc.exception.ClientException;
import com.dahuatech.icc.oauth.model.v202010.GeneralResponse;
import com.zcloud.primeport.dahua.api.DaHuaResourceApi;
import com.zcloud.primeport.dahua.config.DaHuaResourceProperties;
import com.zcloud.primeport.domain.gateway.DaHuaResourceGateway;
import com.zcloud.primeport.domain.model.DaHuaResourcePageE;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -21,39 +23,90 @@ import java.util.Map;
public class DaHuaResourceGatewayImpl implements DaHuaResourceGateway {
private final DaHuaResourceApi daHuaResourceApi;
private final DaHuaResourceProperties properties;
@Override
public DaHuaResourcePageE pageOrganizations(Integer pageNum, Integer pageSize, String subsystem) {
return requestPage(pageNum, pageSize, subsystem, true);
return requestPage(pageNum, pageSize, subsystem, true, false);
}
@Override
public DaHuaResourcePageE pageDevices(Integer pageNum, Integer pageSize, String subsystem) {
return requestPage(pageNum, pageSize, subsystem, false);
return requestPage(pageNum, pageSize, subsystem, false, false);
}
@Override
public DaHuaResourcePageE pageChannels(Integer pageNum, Integer pageSize, String subsystem) {
return requestPage(pageNum, pageSize, subsystem, false, true);
}
private DaHuaResourcePageE requestPage(Integer pageNum, Integer pageSize, String subsystem,
boolean organization) {
boolean organization, boolean channelsOnly) {
if (!daHuaResourceApi.isConfigured()) {
throw new RuntimeException("大华服务未配置");
}
Map<String, Object> body = new HashMap<>();
body.put("pageNum", pageNum);
body.put("pageSize", pageSize);
if (channelsOnly) {
body.put("categorys", Collections.singletonList(properties.getChannelCategory()));
if (!isBlank(properties.getChannelType())) {
body.put("types", Collections.singletonList(properties.getChannelType().trim()));
}
}
if (!isBlank(subsystem)) {
body.put("subsystem", subsystem.trim());
}
try {
GeneralResponse response = organization
? daHuaResourceApi.pageOrganizations(body)
: channelsOnly ? daHuaResourceApi.pageChannels(body)
: daHuaResourceApi.pageDevices(body);
Object data = unwrapData(response, organization ? "部门" : "设备");
return parsePage(data);
Object data = unwrapData(response, organization ? "部门" : (channelsOnly ? "设备通道" : "设备"));
return channelsOnly ? parseChannelPage(data) : parsePage(data);
} catch (ClientException e) {
throw new RuntimeException("调用大华" + (organization ? "部门" : "设备") + "分页接口失败", e);
}
}
private DaHuaResourcePageE parseChannelPage(Object data) {
DaHuaResourcePageE page = new DaHuaResourcePageE();
if (data == null) {
return page;
}
JSONObject object = toJsonObject(data);
Object value = object == null ? data : firstValue(object, "pageData", "records", "list", "dataList");
List<Map<String, Object>> sourceRecords = toRecordList(value);
page.setSourceRecordCount(sourceRecords.size());
if (object != null) {
page.setTotal(firstLong(object, "totalRows", "total", "totalCount", "recordCount"));
}
List<Map<String, Object>> channels = new ArrayList<>();
for (Map<String, Object> record : sourceRecords) {
Object nested = firstValue(record, "channels", "channelList", "children");
List<Map<String, Object>> nestedChannels = toRecordList(nested);
if (nestedChannels.isEmpty() && firstValue(record, "channelCode") != null) {
channels.add(record);
} else {
for (Map<String, Object> channel : nestedChannels) {
Map<String, Object> merged = new HashMap<>(channel);
copyIfAbsent(merged, record, "deviceCode", "deviceName", "orgCode", "orgName", "departmentId");
channels.add(merged);
}
}
}
page.setRecords(channels);
return page;
}
private void copyIfAbsent(Map<String, Object> target, Map<String, Object> source, String... fields) {
for (String field : fields) {
if (target.get(field) == null && source.get(field) != null) {
target.put(field, source.get(field));
}
}
}
private Object unwrapData(GeneralResponse response, String resourceName) {
if (response == null) {
throw new RuntimeException("大华" + resourceName + "分页接口返回为空");
@ -84,6 +137,7 @@ public class DaHuaResourceGatewayImpl implements DaHuaResourceGateway {
}
if (data instanceof JSONArray || data instanceof List) {
page.setRecords(toRecordList(data));
page.setSourceRecordCount(page.getRecords().size());
return page;
}
JSONObject object = toJsonObject(data);
@ -91,6 +145,7 @@ public class DaHuaResourceGatewayImpl implements DaHuaResourceGateway {
return page;
}
page.setRecords(toRecordList(firstValue(object, "pageData", "records", "list", "dataList")));
page.setSourceRecordCount(page.getRecords().size());
page.setTotal(firstLong(object, "totalRows", "total", "totalCount", "recordCount"));
return page;
}

View File

@ -7,12 +7,15 @@ import com.zcloud.primeport.domain.gateway.DaHuaResourceRepositoryGateway;
import com.zcloud.primeport.domain.model.CorpInfoSnapshotE;
import com.zcloud.primeport.domain.model.DaHuaDepartmentCorpMappingE;
import com.zcloud.primeport.domain.model.DaHuaDeviceE;
import com.zcloud.primeport.domain.model.DaHuaDeviceChannelE;
import com.zcloud.primeport.persistence.dataobject.CorpInfoSnapshotDO;
import com.zcloud.primeport.persistence.dataobject.DaHuaDepartmentCorpMappingDO;
import com.zcloud.primeport.persistence.dataobject.DaHuaDeviceDO;
import com.zcloud.primeport.persistence.dataobject.DaHuaDeviceChannelDO;
import com.zcloud.primeport.persistence.repository.CorpInfoSnapshotRepository;
import com.zcloud.primeport.persistence.repository.DaHuaDepartmentCorpMappingRepository;
import com.zcloud.primeport.persistence.repository.DaHuaDeviceRepository;
import com.zcloud.primeport.persistence.repository.DaHuaDeviceChannelRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
@ -28,13 +31,15 @@ public class DaHuaResourceRepositoryGatewayImpl implements DaHuaResourceReposito
private final CorpInfoSnapshotRepository corpInfoSnapshotRepository;
private final DaHuaDepartmentCorpMappingRepository mappingRepository;
private final DaHuaDeviceRepository deviceRepository;
private final DaHuaDeviceChannelRepository deviceChannelRepository;
private final DefaultIdentifierGenerator idGenerator = new DefaultIdentifierGenerator();
@Override
public List<CorpInfoSnapshotE> listActiveCorps() {
LambdaQueryWrapper<CorpInfoSnapshotDO> query = new LambdaQueryWrapper<>();
query.eq(CorpInfoSnapshotDO::getDeleteEnum, "FALSE")
.select(CorpInfoSnapshotDO::getId, CorpInfoSnapshotDO::getCorpName);
.select(CorpInfoSnapshotDO::getId, CorpInfoSnapshotDO::getCorpName,
CorpInfoSnapshotDO::getPortArea);
return convertList(corpInfoSnapshotRepository.list(query), CorpInfoSnapshotE.class);
}
@ -79,6 +84,26 @@ public class DaHuaResourceRepositoryGatewayImpl implements DaHuaResourceReposito
deviceRepository.updateById(data);
}
@Override
public List<DaHuaDeviceChannelE> listDeviceChannels() {
return convertList(deviceChannelRepository.list(new LambdaQueryWrapper<>()), DaHuaDeviceChannelE.class);
}
@Override
public void addDeviceChannel(DaHuaDeviceChannelE channel) {
DaHuaDeviceChannelDO data = copy(channel, DaHuaDeviceChannelDO.class);
initializeBase(data);
deviceChannelRepository.save(data);
channel.setId(data.getId());
}
@Override
public void updateDeviceChannel(DaHuaDeviceChannelE channel) {
DaHuaDeviceChannelDO data = copy(channel, DaHuaDeviceChannelDO.class);
data.setUpdateTime(LocalDateTime.now());
deviceChannelRepository.updateById(data);
}
private void initializeBase(BaseDO target) {
LocalDateTime now = LocalDateTime.now();
target.setId(idGenerator.nextId(target).longValue());

View File

@ -11,4 +11,5 @@ import lombok.EqualsAndHashCode;
public class CorpInfoSnapshotDO extends BaseDO {
private String corpName;
private Integer portArea;
}

View File

@ -0,0 +1,62 @@
package com.zcloud.primeport.persistence.dataobject;
import com.baomidou.mybatisplus.annotation.TableName;
import com.jjb.saas.framework.repository.basedo.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("dahua_access_record")
@EqualsAndHashCode(callSuper = true)
public class DaHuaAccessRecordDO extends BaseDO {
private String dahuaRecordId;
private Long corpId;
private String corpName;
private Integer portArea;
private Long deviceCorpId;
private String deviceCorpName;
private Integer devicePortArea;
private Long dahuaDepartmentId;
private String dahuaDepartmentName;
private Long mkmjId;
private Long passageId;
private Long gateId;
private String channelCode;
private String channelName;
private String deviceCode;
private String deviceName;
private Long personId;
private String personCode;
private String personName;
private String paperNumber;
private String cardNumber;
private Integer cardStatus;
private Integer cardType;
private Integer imageType;
private Integer enterOrExit;
private Integer openType;
private Integer openResult;
private String recordImageUrl;
private String remark;
private LocalDateTime swingTime;
private LocalDateTime dahuaCreateTime;
private Integer maskState;
private Boolean overTemp;
private BigDecimal currentTemperature;
private String sourceType;
private String rawData;
private LocalDateTime lastSyncTime;
}

View File

@ -0,0 +1,29 @@
package com.zcloud.primeport.persistence.dataobject;
import com.baomidou.mybatisplus.annotation.TableName;
import com.jjb.saas.framework.repository.basedo.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@TableName("dahua_device_channel")
@EqualsAndHashCode(callSuper = true)
public class DaHuaDeviceChannelDO extends BaseDO {
private String channelCode;
private String channelName;
private String deviceCode;
private Long corpId;
private Long mkmjId;
private Long passageId;
private Long gateId;
private Integer unitType;
private Long dahuaDepartmentId;
private String dahuaOrgCode;
private String dahuaOrgName;
private String onlineStatus;
private String rawData;
private LocalDateTime lastSyncTime;
}

View File

@ -0,0 +1,13 @@
package com.zcloud.primeport.persistence.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zcloud.primeport.persistence.dataobject.DaHuaAccessRecordDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
@Mapper
public interface DaHuaAccessRecordMapper extends BaseMapper<DaHuaAccessRecordDO> {
LocalDateTime findLatestDahuaCreateTime();
}

View File

@ -0,0 +1,9 @@
package com.zcloud.primeport.persistence.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zcloud.primeport.persistence.dataobject.DaHuaDeviceChannelDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DaHuaDeviceChannelMapper extends BaseMapper<DaHuaDeviceChannelDO> {
}

View File

@ -0,0 +1,11 @@
package com.zcloud.primeport.persistence.repository;
import com.jjb.saas.framework.repository.repo.BaseRepository;
import com.zcloud.primeport.persistence.dataobject.DaHuaAccessRecordDO;
import java.time.LocalDateTime;
public interface DaHuaAccessRecordRepository extends BaseRepository<DaHuaAccessRecordDO> {
LocalDateTime findLatestDahuaCreateTime();
}

View File

@ -0,0 +1,7 @@
package com.zcloud.primeport.persistence.repository;
import com.jjb.saas.framework.repository.repo.BaseRepository;
import com.zcloud.primeport.persistence.dataobject.DaHuaDeviceChannelDO;
public interface DaHuaDeviceChannelRepository extends BaseRepository<DaHuaDeviceChannelDO> {
}

View File

@ -0,0 +1,20 @@
package com.zcloud.primeport.persistence.repository.impl;
import com.jjb.saas.framework.repository.repo.impl.BaseRepositoryImpl;
import com.zcloud.primeport.persistence.dataobject.DaHuaAccessRecordDO;
import com.zcloud.primeport.persistence.mapper.DaHuaAccessRecordMapper;
import com.zcloud.primeport.persistence.repository.DaHuaAccessRecordRepository;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
@Repository
public class DaHuaAccessRecordRepositoryImpl
extends BaseRepositoryImpl<DaHuaAccessRecordMapper, DaHuaAccessRecordDO>
implements DaHuaAccessRecordRepository {
@Override
public LocalDateTime findLatestDahuaCreateTime() {
return baseMapper.findLatestDahuaCreateTime();
}
}

View File

@ -0,0 +1,13 @@
package com.zcloud.primeport.persistence.repository.impl;
import com.jjb.saas.framework.repository.repo.impl.BaseRepositoryImpl;
import com.zcloud.primeport.persistence.dataobject.DaHuaDeviceChannelDO;
import com.zcloud.primeport.persistence.mapper.DaHuaDeviceChannelMapper;
import com.zcloud.primeport.persistence.repository.DaHuaDeviceChannelRepository;
import org.springframework.stereotype.Service;
@Service
public class DaHuaDeviceChannelRepositoryImpl
extends BaseRepositoryImpl<DaHuaDeviceChannelMapper, DaHuaDeviceChannelDO>
implements DaHuaDeviceChannelRepository {
}

View File

@ -0,0 +1,43 @@
package com.zcloud.primeport.plan;
import com.jjb.saas.framework.job.Job;
import com.jjb.saas.framework.job.annotation.JobRegister;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.annotation.XxlJob;
import com.zcloud.primeport.dahua.config.DaHuaAccessRecordProperties;
import com.zcloud.primeport.domain.gateway.DaHuaAccessRecordSyncGateway;
import com.zcloud.primeport.domain.model.DaHuaAccessRecordSyncResultE;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
public class DaHuaAccessRecordSyncXxlJob implements Job {
private final DaHuaAccessRecordSyncGateway syncGateway;
private final DaHuaAccessRecordProperties properties;
@Override
@JobRegister(cron = "0 */5 * * * ?", jobDesc = "大华门禁通行记录增量同步", triggerStatus = 1)
@XxlJob("com.zcloud.plan.DaHuaAccessRecordSyncXxlJob")
public ReturnT<String> execute(String param) {
if (!properties.isSyncEnabled()) {
log.info("大华门禁通行记录增量同步已关闭");
return ReturnT.SUCCESS;
}
try {
DaHuaAccessRecordSyncResultE result = syncGateway.syncIncremental(
properties.getSyncPageSize(), properties.getSyncDelayMinutes(),
properties.getSyncOverlapMinutes(), properties.getInitialLookbackMinutes());
log.info("大华门禁通行记录增量同步完成: pages={}, total={}, created={}, updated={}, invalid={}",
result.getPageCount(), result.getRecordTotal(), result.getRecordCreated(),
result.getRecordUpdated(), result.getRecordInvalid());
return ReturnT.SUCCESS;
} catch (Exception e) {
log.error("大华门禁通行记录增量同步失败", e);
return ReturnT.FAIL;
}
}
}

View File

@ -0,0 +1,43 @@
package com.zcloud.primeport.plan;
import com.jjb.saas.framework.job.Job;
import com.jjb.saas.framework.job.annotation.JobRegister;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.annotation.XxlJob;
import com.zcloud.primeport.dahua.config.DaHuaResourceProperties;
import com.zcloud.primeport.domain.gateway.DaHuaResourceSyncGateway;
import com.zcloud.primeport.domain.model.DaHuaResourceSyncResultE;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
public class DaHuaResourceSyncXxlJob implements Job {
private final DaHuaResourceSyncGateway syncGateway;
private final DaHuaResourceProperties properties;
@Override
@JobRegister(cron = "0 */10 * * * ?", jobDesc = "大华部门、设备和门禁通道同步", triggerStatus = 1)
@XxlJob("com.zcloud.plan.DaHuaResourceSyncXxlJob")
public ReturnT<String> execute(String param) {
if (!properties.isSyncEnabled()) {
log.info("大华资源同步已关闭");
return ReturnT.SUCCESS;
}
try {
DaHuaResourceSyncResultE result = syncGateway.sync(
properties.getSyncPageSize(), properties.getSubsystem(),
properties.isAutoMatchByName());
log.info("大华资源同步完成: departments={}, devices={}, channels={}, invalidChannels={}",
result.getDepartmentTotal(), result.getDeviceTotal(), result.getChannelTotal(),
result.getChannelInvalid());
return ReturnT.SUCCESS;
} catch (Exception e) {
log.error("大华资源同步失败", e);
return ReturnT.FAIL;
}
}
}

View File

@ -398,3 +398,103 @@ CREATE TABLE `dahua_device` (
KEY `idx_dahua_device_corp` (`corp_id`),
KEY `idx_dahua_device_match_status` (`match_status`, `delete_enum`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='大华门口设备同步表';
CREATE TABLE `dahua_device_channel` (
`id` bigint NOT NULL COMMENT '主键ID',
`channel_code` varchar(128) NOT NULL COMMENT '大华门禁通道编码',
`channel_name` varchar(255) DEFAULT NULL COMMENT '大华门禁通道名称',
`device_code` varchar(128) NOT NULL COMMENT '所属大华设备编码',
`corp_id` bigint DEFAULT NULL COMMENT '平台企业ID',
`mkmj_id` bigint DEFAULT NULL COMMENT '本平台口门ID',
`passage_id` bigint DEFAULT NULL COMMENT '本平台通道ID',
`gate_id` bigint DEFAULT NULL COMMENT '本平台闸机ID',
`unit_type` int NOT NULL COMMENT '单元类型门禁通道为7',
`dahua_department_id` bigint DEFAULT NULL COMMENT '大华所属部门ID',
`dahua_org_code` varchar(64) DEFAULT NULL COMMENT '大华组织编码',
`dahua_org_name` varchar(255) DEFAULT NULL COMMENT '大华组织名称',
`online_status` varchar(32) DEFAULT NULL COMMENT '设备在线状态',
`raw_data` json DEFAULT NULL COMMENT '大华原始通道数据',
`last_sync_time` datetime DEFAULT NULL COMMENT '最近同步时间',
`delete_enum` varchar(32) NOT NULL DEFAULT 'FALSE' COMMENT '删除标识',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`create_id` bigint DEFAULT NULL COMMENT '创建人ID',
`update_id` bigint DEFAULT NULL COMMENT '更新人ID',
`env` varchar(32) NOT NULL DEFAULT 'PROD' COMMENT '环境标识',
`create_name` varchar(255) DEFAULT NULL COMMENT '创建人姓名',
`update_name` varchar(255) DEFAULT NULL COMMENT '更新人姓名',
`tenant_id` bigint DEFAULT NULL COMMENT '租户ID',
`org_id` bigint DEFAULT NULL COMMENT '组织ID',
`version` int NOT NULL DEFAULT '0' COMMENT '版本号',
`remarks` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_dahua_device_channel_code` (`channel_code`),
KEY `idx_dahua_device_channel_device_code` (`device_code`),
KEY `idx_dahua_device_channel_corp` (`corp_id`),
KEY `idx_dahua_device_channel_mkmj` (`mkmj_id`),
KEY `idx_dahua_device_channel_passage` (`passage_id`),
KEY `idx_dahua_device_channel_gate` (`gate_id`),
KEY `idx_dahua_device_channel_department` (`dahua_department_id`),
KEY `idx_dahua_device_channel_org_code` (`dahua_org_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='大华门禁通道同步表';
CREATE TABLE `dahua_access_record` (
`id` bigint NOT NULL COMMENT '主键ID',
`dahua_record_id` varchar(128) NOT NULL COMMENT '大华门禁通行记录ID幂等键',
`corp_id` bigint DEFAULT NULL COMMENT '通行人员所属本平台企业ID',
`corp_name` varchar(255) DEFAULT NULL COMMENT '通行人员所属本平台企业名称快照',
`port_area` int DEFAULT NULL COMMENT '通行人员所属企业港区',
`device_corp_id` bigint DEFAULT NULL COMMENT '门禁设备所属本平台企业ID',
`device_corp_name` varchar(255) DEFAULT NULL COMMENT '门禁设备所属本平台企业名称快照',
`device_port_area` int DEFAULT NULL COMMENT '门禁设备所属企业港区',
`dahua_department_id` bigint DEFAULT NULL COMMENT '大华人员部门ID',
`dahua_department_name` varchar(255) DEFAULT NULL COMMENT '大华人员部门名称快照',
`mkmj_id` bigint DEFAULT NULL COMMENT '本平台口门ID',
`passage_id` bigint DEFAULT NULL COMMENT '本平台通道ID',
`gate_id` bigint DEFAULT NULL COMMENT '本平台闸机ID',
`channel_code` varchar(128) DEFAULT NULL COMMENT '大华门禁通道编码',
`channel_name` varchar(255) DEFAULT NULL COMMENT '大华门禁通道名称',
`device_code` varchar(128) DEFAULT NULL COMMENT '大华门禁设备编码',
`device_name` varchar(255) DEFAULT NULL COMMENT '大华门禁设备名称',
`person_id` bigint DEFAULT NULL COMMENT '大华人员ID',
`person_code` varchar(128) DEFAULT NULL COMMENT '大华人员编号',
`person_name` varchar(255) DEFAULT NULL COMMENT '人员名称快照',
`paper_number` varchar(128) DEFAULT NULL COMMENT '证件号码',
`card_number` varchar(128) DEFAULT NULL COMMENT '卡号',
`card_status` int DEFAULT NULL COMMENT '卡状态',
`card_type` int DEFAULT NULL COMMENT '卡类型',
`image_type` int DEFAULT NULL COMMENT '图片类型',
`enter_or_exit` int DEFAULT NULL COMMENT '进出方向1-进2-出',
`open_type` int DEFAULT NULL COMMENT '开门类型',
`open_result` int DEFAULT NULL COMMENT '开门结果1-成功0-失败',
`record_image_url` varchar(1024) DEFAULT NULL COMMENT '通行抓拍相对地址',
`remark` varchar(1000) DEFAULT NULL COMMENT '大华记录备注',
`swing_time` datetime DEFAULT NULL COMMENT '通行发生时间',
`dahua_create_time` datetime DEFAULT NULL COMMENT '记录在大华平台的入库时间',
`mask_state` int DEFAULT NULL COMMENT '口罩状态',
`over_temp` tinyint(1) DEFAULT NULL COMMENT '是否超温',
`current_temperature` decimal(6,2) DEFAULT NULL COMMENT '当前体温',
`source_type` varchar(32) NOT NULL DEFAULT 'PULL' COMMENT '数据来源',
`raw_data` json DEFAULT NULL COMMENT '大华原始记录',
`last_sync_time` datetime DEFAULT NULL COMMENT '最近同步时间',
`delete_enum` varchar(32) NOT NULL DEFAULT 'FALSE' COMMENT '删除标识',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`create_id` bigint DEFAULT NULL COMMENT '创建人ID',
`update_id` bigint DEFAULT NULL COMMENT '更新人ID',
`env` varchar(32) NOT NULL DEFAULT 'PROD' COMMENT '环境标识',
`create_name` varchar(255) DEFAULT NULL COMMENT '创建人姓名',
`update_name` varchar(255) DEFAULT NULL COMMENT '更新人姓名',
`tenant_id` bigint DEFAULT NULL COMMENT '租户ID',
`org_id` bigint DEFAULT NULL COMMENT '组织ID',
`version` int NOT NULL DEFAULT '0' COMMENT '版本号',
`remarks` varchar(500) DEFAULT NULL COMMENT '基础备注',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_dahua_access_record_record_id` (`dahua_record_id`),
KEY `idx_dahua_access_record_corp_time` (`corp_id`, `swing_time`),
KEY `idx_dahua_access_record_device_corp_time` (`device_corp_id`, `swing_time`),
KEY `idx_dahua_access_record_mkmj_time` (`mkmj_id`, `swing_time`),
KEY `idx_dahua_access_record_channel_time` (`channel_code`, `swing_time`),
KEY `idx_dahua_access_record_person_time` (`person_code`, `swing_time`),
KEY `idx_dahua_access_record_create_time` (`dahua_create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='大华门禁通行记录表';

View File

@ -0,0 +1,30 @@
CREATE TABLE IF NOT EXISTS `dahua_device_channel` (
`id` bigint NOT NULL,
`channel_code` varchar(128) NOT NULL,
`channel_name` varchar(255) DEFAULT NULL,
`device_code` varchar(128) NOT NULL,
`unit_type` int NOT NULL,
`dahua_department_id` bigint DEFAULT NULL,
`dahua_org_code` varchar(64) DEFAULT NULL,
`dahua_org_name` varchar(255) DEFAULT NULL,
`online_status` varchar(32) DEFAULT NULL,
`raw_data` json DEFAULT NULL,
`last_sync_time` datetime DEFAULT NULL,
`delete_enum` varchar(32) NOT NULL DEFAULT 'FALSE',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`create_id` bigint DEFAULT NULL,
`update_id` bigint DEFAULT NULL,
`env` varchar(32) NOT NULL DEFAULT 'PROD',
`create_name` varchar(255) DEFAULT NULL,
`update_name` varchar(255) DEFAULT NULL,
`tenant_id` bigint DEFAULT NULL,
`org_id` bigint DEFAULT NULL,
`version` int NOT NULL DEFAULT '0',
`remarks` varchar(500) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_dahua_device_channel_code` (`channel_code`),
KEY `idx_dahua_device_channel_device_code` (`device_code`),
KEY `idx_dahua_device_channel_department` (`dahua_department_id`),
KEY `idx_dahua_device_channel_org_code` (`dahua_org_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

View File

@ -0,0 +1,3 @@
ALTER TABLE `dahua_device_channel`
ADD COLUMN `corp_id` bigint DEFAULT NULL COMMENT '平台企业ID' AFTER `device_code`,
ADD KEY `idx_dahua_device_channel_corp` (`corp_id`);

View File

@ -0,0 +1,68 @@
ALTER TABLE `dahua_device_channel`
ADD COLUMN `mkmj_id` bigint DEFAULT NULL COMMENT '本平台口门ID' AFTER `corp_id`,
ADD COLUMN `passage_id` bigint DEFAULT NULL COMMENT '本平台通道ID' AFTER `mkmj_id`,
ADD COLUMN `gate_id` bigint DEFAULT NULL COMMENT '本平台闸机ID' AFTER `passage_id`,
ADD KEY `idx_dahua_device_channel_mkmj` (`mkmj_id`),
ADD KEY `idx_dahua_device_channel_passage` (`passage_id`),
ADD KEY `idx_dahua_device_channel_gate` (`gate_id`);
CREATE TABLE IF NOT EXISTS `dahua_access_record` (
`id` bigint NOT NULL COMMENT '主键ID',
`dahua_record_id` varchar(128) NOT NULL COMMENT '大华门禁通行记录ID幂等键',
`corp_id` bigint DEFAULT NULL COMMENT '通行人员所属本平台企业ID',
`corp_name` varchar(255) DEFAULT NULL COMMENT '通行人员所属本平台企业名称快照',
`port_area` int DEFAULT NULL COMMENT '通行人员所属企业港区',
`device_corp_id` bigint DEFAULT NULL COMMENT '门禁设备所属本平台企业ID',
`device_corp_name` varchar(255) DEFAULT NULL COMMENT '门禁设备所属本平台企业名称快照',
`device_port_area` int DEFAULT NULL COMMENT '门禁设备所属企业港区',
`dahua_department_id` bigint DEFAULT NULL COMMENT '大华人员部门ID',
`dahua_department_name` varchar(255) DEFAULT NULL COMMENT '大华人员部门名称快照',
`mkmj_id` bigint DEFAULT NULL COMMENT '本平台口门ID',
`passage_id` bigint DEFAULT NULL COMMENT '本平台通道ID',
`gate_id` bigint DEFAULT NULL COMMENT '本平台闸机ID',
`channel_code` varchar(128) DEFAULT NULL COMMENT '大华门禁通道编码',
`channel_name` varchar(255) DEFAULT NULL COMMENT '大华门禁通道名称',
`device_code` varchar(128) DEFAULT NULL COMMENT '大华门禁设备编码',
`device_name` varchar(255) DEFAULT NULL COMMENT '大华门禁设备名称',
`person_id` bigint DEFAULT NULL COMMENT '大华人员ID',
`person_code` varchar(128) DEFAULT NULL COMMENT '大华人员编号',
`person_name` varchar(255) DEFAULT NULL COMMENT '人员名称快照',
`paper_number` varchar(128) DEFAULT NULL COMMENT '证件号码',
`card_number` varchar(128) DEFAULT NULL COMMENT '卡号',
`card_status` int DEFAULT NULL COMMENT '卡状态',
`card_type` int DEFAULT NULL COMMENT '卡类型',
`image_type` int DEFAULT NULL COMMENT '图片类型',
`enter_or_exit` int DEFAULT NULL COMMENT '进出方向1-进2-出',
`open_type` int DEFAULT NULL COMMENT '开门类型',
`open_result` int DEFAULT NULL COMMENT '开门结果1-成功0-失败',
`record_image_url` varchar(1024) DEFAULT NULL COMMENT '通行抓拍相对地址',
`remark` varchar(1000) DEFAULT NULL COMMENT '大华记录备注',
`swing_time` datetime DEFAULT NULL COMMENT '通行发生时间',
`dahua_create_time` datetime DEFAULT NULL COMMENT '记录在大华平台的入库时间',
`mask_state` int DEFAULT NULL COMMENT '口罩状态',
`over_temp` tinyint(1) DEFAULT NULL COMMENT '是否超温',
`current_temperature` decimal(6,2) DEFAULT NULL COMMENT '当前体温',
`source_type` varchar(32) NOT NULL DEFAULT 'PULL' COMMENT '数据来源',
`raw_data` json DEFAULT NULL COMMENT '大华原始记录',
`last_sync_time` datetime DEFAULT NULL COMMENT '最近同步时间',
`delete_enum` varchar(32) NOT NULL DEFAULT 'FALSE' COMMENT '删除标识',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`create_id` bigint DEFAULT NULL COMMENT '创建人ID',
`update_id` bigint DEFAULT NULL COMMENT '更新人ID',
`env` varchar(32) NOT NULL DEFAULT 'PROD' COMMENT '环境标识',
`create_name` varchar(255) DEFAULT NULL COMMENT '创建人姓名',
`update_name` varchar(255) DEFAULT NULL COMMENT '更新人姓名',
`tenant_id` bigint DEFAULT NULL COMMENT '租户ID',
`org_id` bigint DEFAULT NULL COMMENT '组织ID',
`version` int NOT NULL DEFAULT '0' COMMENT '版本号',
`remarks` varchar(500) DEFAULT NULL COMMENT '基础备注',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_dahua_access_record_record_id` (`dahua_record_id`),
KEY `idx_dahua_access_record_corp_time` (`corp_id`, `swing_time`),
KEY `idx_dahua_access_record_device_corp_time` (`device_corp_id`, `swing_time`),
KEY `idx_dahua_access_record_mkmj_time` (`mkmj_id`, `swing_time`),
KEY `idx_dahua_access_record_channel_time` (`channel_code`, `swing_time`),
KEY `idx_dahua_access_record_person_time` (`person_code`, `swing_time`),
KEY `idx_dahua_access_record_create_time` (`dahua_create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='大华门禁通行记录表';

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zcloud.primeport.persistence.mapper.DaHuaAccessRecordMapper">
<select id="findLatestDahuaCreateTime" resultType="java.time.LocalDateTime">
SELECT dahua_create_time
FROM dahua_access_record
WHERE dahua_create_time IS NOT NULL
ORDER BY dahua_create_time DESC
LIMIT 1
</select>
</mapper>