diff --git a/start/src/main/java/com/zcloud/primeport/Application.java b/start/src/main/java/com/zcloud/primeport/Application.java index 446e6af..ca6287a 100644 --- a/start/src/main/java/com/zcloud/primeport/Application.java +++ b/start/src/main/java/com/zcloud/primeport/Application.java @@ -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 diff --git a/start/src/main/resources/application.yml b/start/src/main/resources/application.yml index ccad15a..2fb5cb0 100644 --- a/start/src/main/resources/application.yml +++ b/start/src/main/resources/application.yml @@ -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 diff --git a/start/src/main/resources/bootstrap.yml b/start/src/main/resources/bootstrap.yml index c0bc004..62d008f 100644 --- a/start/src/main/resources/bootstrap.yml +++ b/start/src/main/resources/bootstrap.yml @@ -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 diff --git a/start/src/main/resources/sdk.yml b/start/src/main/resources/sdk.yml index 605d431..bef0ea4 100644 --- a/start/src/main/resources/sdk.yml +++ b/start/src/main/resources/sdk.yml @@ -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 \ No newline at end of file + port: 4443 diff --git a/web-adapter/src/main/java/com/zcloud/primeport/web/DaHuaController.java b/web-adapter/src/main/java/com/zcloud/primeport/web/DaHuaController.java index b9376ff..a006301 100644 --- a/web-adapter/src/main/java/com/zcloud/primeport/web/DaHuaController.java +++ b/web-adapter/src/main/java/com/zcloud/primeport/web/DaHuaController.java @@ -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 syncAccessRecords( + @Valid @RequestBody DaHuaAccessRecordSyncCmd cmd) { + if (daHuaService == null) { + throw new RuntimeException("大华服务未配置"); + } + return daHuaService.syncAccessRecords(cmd); + } + @ApiOperation("删除人员") @PostMapping("/person/delete") public SingleResponse deletePerson(@Validated @RequestBody DaHuaPersonDeleteCmd cmd) { diff --git a/web-app/src/main/java/com/zcloud/primeport/service/DaHuaAccessRecordSyncApplicationService.java b/web-app/src/main/java/com/zcloud/primeport/service/DaHuaAccessRecordSyncApplicationService.java new file mode 100644 index 0000000..eaeb3ed --- /dev/null +++ b/web-app/src/main/java/com/zcloud/primeport/service/DaHuaAccessRecordSyncApplicationService.java @@ -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 synchronizedRecords = new HashMap<>(); + DaHuaAccessRecordSyncResultE result = new DaHuaAccessRecordSyncResultE(); + int pageNum = 1; + while (true) { + List> 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> 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 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 data = toMap(response.get("data")); + return toRecordList(data.get("pageData")); + } + + protected void persistPage(List> pageData, + SyncIndex index, + Map synchronizedRecords, + DaHuaAccessRecordSyncResultE result) { + Set recordIds = new LinkedHashSet<>(); + for (Map 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 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 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 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> mappingsByName, + String departmentName) { + List 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 source, String... fields) { + String value = firstString(source, fields); + if (isBlank(value)) { + return null; + } + return parseRequiredDateTime(value, fields[0]); + } + + private Object firstValue(Map source, String... fields) { + for (String field : fields) { + Object value = source.get(field); + if (value != null) { + return value; + } + } + return null; + } + + private String firstString(Map source, String... fields) { + return stringValue(firstValue(source, fields)); + } + + private Long firstLong(Map 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 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 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 source, String... fields) { + Object value = firstValue(source, fields); + return value == null ? null : new BigDecimal(value.toString()); + } + + @SuppressWarnings("unchecked") + private Map toMap(Object value) { + if (value == null) { + return new LinkedHashMap<>(); + } + if (value instanceof Map) { + return new LinkedHashMap<>((Map) value); + } + return JSONUtil.parseObj(JSONUtil.toJsonStr(value)); + } + + @SuppressWarnings("unchecked") + private List> toRecordList(Object value) { + List> result = new ArrayList<>(); + if (!(value instanceof Iterable)) { + return result; + } + for (Object item : (Iterable) value) { + if (item instanceof Map) { + result.add(new LinkedHashMap<>((Map) item)); + } else if (item != null) { + result.add(JSONUtil.parseObj(JSONUtil.toJsonStr(item))); + } + } + return result; + } + + private Collection nullSafe(Collection 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 details, String detail) { + if (details.size() < MAX_EXCEPTION_DETAILS) { + details.add(detail); + } + } + + private static class SyncIndex { + private final Map corpsById = new HashMap<>(); + private final Map channelsByCode = new HashMap<>(); + private final Map mappingsByDepartmentId = new HashMap<>(); + private final Map mappingsByOrgCode = new HashMap<>(); + private final Map> mappingsByDepartmentName = + new HashMap<>(); + } +} diff --git a/web-app/src/main/java/com/zcloud/primeport/service/DaHuaResourceSyncApplicationService.java b/web-app/src/main/java/com/zcloud/primeport/service/DaHuaResourceSyncApplicationService.java index 577d557..133e365 100644 --- a/web-app/src/main/java/com/zcloud/primeport/service/DaHuaResourceSyncApplicationService.java +++ b/web-app/src/main/java/com/zcloud/primeport/service/DaHuaResourceSyncApplicationService.java @@ -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> departments = fetchAll(actualPageSize, subsystem, true); List> devices = fetchAll(actualPageSize, subsystem, false); + List> 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> fetchAll(int pageSize, String subsystem, boolean organization) { + private List> fetchAll(int pageSize, String subsystem, Boolean organization) { List> 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> 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 seen = new LinkedHashSet<>(); for (Map 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> channels, DaHuaResourceSyncResultE result) { + channels = expandChannelRecords(channels); + MappingIndex mappingIndex = new MappingIndex(repositoryGateway.listMappings()); + ChannelIndex channelIndex = new ChannelIndex(repositoryGateway.listDeviceChannels()); + LocalDateTime now = LocalDateTime.now(); + Set seen = new LinkedHashSet<>(); + for (Map 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> extractNestedChannels(List> devices) { + List> result = new ArrayList<>(); + for (Map device : devices) { + Object nested = firstValue(device, "channels", "channelList"); + if (!(nested instanceof Iterable)) { + continue; + } + for (Object value : (Iterable) nested) { + if (value instanceof Map) { + Map channel = new HashMap<>((Map) 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> expandChannelRecords(List> records) { + List> result = new ArrayList<>(); + for (Map 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 channel = new HashMap<>((Map) value); + if (channel.get("deviceCode") == null) { + channel.put("deviceCode", firstString(record, "deviceCode", "code")); + } + result.add(channel); + } + } + } + return result; } private Map> groupCorpsByNormalizedName(List 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 uniqueList(List values) { @@ -377,5 +517,51 @@ public class DaHuaResourceSyncApplicationService { } add(device); } + + private List values() { + return new ArrayList<>(byDeviceCode.values()); + } + } + + private Integer channelUnitType(Map 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 byChannelCode = new HashMap<>(); + + private ChannelIndex(List 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 values() { + return new ArrayList<>(byChannelCode.values()); + } } } diff --git a/web-app/src/main/java/com/zcloud/primeport/service/DaHuaServiceImpl.java b/web-app/src/main/java/com/zcloud/primeport/service/DaHuaServiceImpl.java index 19adda4..a071fc0 100644 --- a/web-app/src/main/java/com/zcloud/primeport/service/DaHuaServiceImpl.java +++ b/web-app/src/main/java/com/zcloud/primeport/service/DaHuaServiceImpl.java @@ -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 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 saveTempVehicle(DaHuaTempVehicleSaveCmd cmd) { cmd.setDepartmentId(resolveDahuaDeptId(cmd.getDepartmentId())); diff --git a/web-app/src/test/java/com/zcloud/primeport/service/DaHuaAccessRecordSyncApplicationServiceTest.java b/web-app/src/test/java/com/zcloud/primeport/service/DaHuaAccessRecordSyncApplicationServiceTest.java new file mode 100644 index 0000000..b5d96e9 --- /dev/null +++ b/web-app/src/test/java/com/zcloud/primeport/service/DaHuaAccessRecordSyncApplicationServiceTest.java @@ -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 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 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 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 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 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 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 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 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 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 record(String id) { + Map 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 response(Collection> records) { + Map data = new HashMap<>(); + data.put("pageData", new ArrayList<>(records)); + Map response = new HashMap<>(); + response.put("passed", true); + response.put("success", true); + response.put("code", "0"); + response.put("data", data); + return response; + } +} diff --git a/web-app/src/test/java/com/zcloud/primeport/service/DaHuaResourceSyncApplicationServiceTest.java b/web-app/src/test/java/com/zcloud/primeport/service/DaHuaResourceSyncApplicationServiceTest.java index 3b5693d..e17ab4d 100644 --- a/web-app/src/test/java/com/zcloud/primeport/service/DaHuaResourceSyncApplicationServiceTest.java +++ b/web-app/src/test/java/com/zcloud/primeport/service/DaHuaResourceSyncApplicationServiceTest.java @@ -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 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> records, Long total) { DaHuaResourcePageE page = new DaHuaResourcePageE(); page.setRecords(records); @@ -114,4 +236,14 @@ class DaHuaResourceSyncApplicationServiceTest { value.put("ownerCode", orgCode); return value; } + + private Map channel(String code, String deviceCode, int unitType) { + Map 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; + } } diff --git a/web-client/src/main/java/com/zcloud/primeport/api/DaHuaServiceI.java b/web-client/src/main/java/com/zcloud/primeport/api/DaHuaServiceI.java index b1b6833..d987ddc 100644 --- a/web-client/src/main/java/com/zcloud/primeport/api/DaHuaServiceI.java +++ b/web-client/src/main/java/com/zcloud/primeport/api/DaHuaServiceI.java @@ -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> queryAccessRecord(DaHuaAccessRecordCmd cmd); + SingleResponse syncAccessRecords(DaHuaAccessRecordSyncCmd cmd); + SingleResponse saveTempVehicle(DaHuaTempVehicleSaveCmd cmd); SingleResponse syncResources(DaHuaResourceSyncCmd cmd); diff --git a/web-client/src/main/java/com/zcloud/primeport/dto/DaHuaAccessRecordSyncCmd.java b/web-client/src/main/java/com/zcloud/primeport/dto/DaHuaAccessRecordSyncCmd.java new file mode 100644 index 0000000..77c3624 --- /dev/null +++ b/web-client/src/main/java/com/zcloud/primeport/dto/DaHuaAccessRecordSyncCmd.java @@ -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; +} diff --git a/web-client/src/main/java/com/zcloud/primeport/dto/clientobject/DaHuaAccessRecordSyncResultCO.java b/web-client/src/main/java/com/zcloud/primeport/dto/clientobject/DaHuaAccessRecordSyncResultCO.java new file mode 100644 index 0000000..cc23157 --- /dev/null +++ b/web-client/src/main/java/com/zcloud/primeport/dto/clientobject/DaHuaAccessRecordSyncResultCO.java @@ -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 exceptionDetails = new ArrayList<>(); +} diff --git a/web-client/src/main/java/com/zcloud/primeport/dto/clientobject/DaHuaResourceSyncResultCO.java b/web-client/src/main/java/com/zcloud/primeport/dto/clientobject/DaHuaResourceSyncResultCO.java index e14d52b..61620ab 100644 --- a/web-client/src/main/java/com/zcloud/primeport/dto/clientobject/DaHuaResourceSyncResultCO.java +++ b/web-client/src/main/java/com/zcloud/primeport/dto/clientobject/DaHuaResourceSyncResultCO.java @@ -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 unmatchedDepartmentNames = new ArrayList<>(); @ApiModelProperty("未匹配企业的大华设备编码") private List unmatchedDeviceCodes = new ArrayList<>(); + @ApiModelProperty("同步异常明细") + private List exceptionDetails = new ArrayList<>(); } diff --git a/web-domain/src/main/java/com/zcloud/primeport/domain/gateway/DaHuaAccessRecordRepositoryGateway.java b/web-domain/src/main/java/com/zcloud/primeport/domain/gateway/DaHuaAccessRecordRepositoryGateway.java new file mode 100644 index 0000000..0c03fbf --- /dev/null +++ b/web-domain/src/main/java/com/zcloud/primeport/domain/gateway/DaHuaAccessRecordRepositoryGateway.java @@ -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 listByDahuaRecordIds(Collection dahuaRecordIds); + + LocalDateTime findLatestDahuaCreateTime(); + + void add(DaHuaAccessRecordE record); + + void update(DaHuaAccessRecordE record); +} diff --git a/web-domain/src/main/java/com/zcloud/primeport/domain/gateway/DaHuaAccessRecordSyncGateway.java b/web-domain/src/main/java/com/zcloud/primeport/domain/gateway/DaHuaAccessRecordSyncGateway.java new file mode 100644 index 0000000..cade5fc --- /dev/null +++ b/web-domain/src/main/java/com/zcloud/primeport/domain/gateway/DaHuaAccessRecordSyncGateway.java @@ -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); +} diff --git a/web-domain/src/main/java/com/zcloud/primeport/domain/gateway/DaHuaResourceGateway.java b/web-domain/src/main/java/com/zcloud/primeport/domain/gateway/DaHuaResourceGateway.java index c725142..63608ac 100644 --- a/web-domain/src/main/java/com/zcloud/primeport/domain/gateway/DaHuaResourceGateway.java +++ b/web-domain/src/main/java/com/zcloud/primeport/domain/gateway/DaHuaResourceGateway.java @@ -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); } diff --git a/web-domain/src/main/java/com/zcloud/primeport/domain/gateway/DaHuaResourceRepositoryGateway.java b/web-domain/src/main/java/com/zcloud/primeport/domain/gateway/DaHuaResourceRepositoryGateway.java index 12592a7..cb47c3e 100644 --- a/web-domain/src/main/java/com/zcloud/primeport/domain/gateway/DaHuaResourceRepositoryGateway.java +++ b/web-domain/src/main/java/com/zcloud/primeport/domain/gateway/DaHuaResourceRepositoryGateway.java @@ -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 listDeviceChannels(); + + void addDeviceChannel(DaHuaDeviceChannelE channel); + + void updateDeviceChannel(DaHuaDeviceChannelE channel); } diff --git a/web-domain/src/main/java/com/zcloud/primeport/domain/gateway/DaHuaResourceSyncGateway.java b/web-domain/src/main/java/com/zcloud/primeport/domain/gateway/DaHuaResourceSyncGateway.java new file mode 100644 index 0000000..5e8d8c7 --- /dev/null +++ b/web-domain/src/main/java/com/zcloud/primeport/domain/gateway/DaHuaResourceSyncGateway.java @@ -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); +} diff --git a/web-domain/src/main/java/com/zcloud/primeport/domain/model/CorpInfoSnapshotE.java b/web-domain/src/main/java/com/zcloud/primeport/domain/model/CorpInfoSnapshotE.java index febd50d..bfe6530 100644 --- a/web-domain/src/main/java/com/zcloud/primeport/domain/model/CorpInfoSnapshotE.java +++ b/web-domain/src/main/java/com/zcloud/primeport/domain/model/CorpInfoSnapshotE.java @@ -7,4 +7,5 @@ public class CorpInfoSnapshotE { private Long id; private String corpName; + private Integer portArea; } diff --git a/web-domain/src/main/java/com/zcloud/primeport/domain/model/DaHuaAccessRecordE.java b/web-domain/src/main/java/com/zcloud/primeport/domain/model/DaHuaAccessRecordE.java new file mode 100644 index 0000000..8871ce4 --- /dev/null +++ b/web-domain/src/main/java/com/zcloud/primeport/domain/model/DaHuaAccessRecordE.java @@ -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; +} diff --git a/web-domain/src/main/java/com/zcloud/primeport/domain/model/DaHuaAccessRecordSyncResultE.java b/web-domain/src/main/java/com/zcloud/primeport/domain/model/DaHuaAccessRecordSyncResultE.java new file mode 100644 index 0000000..17801b5 --- /dev/null +++ b/web-domain/src/main/java/com/zcloud/primeport/domain/model/DaHuaAccessRecordSyncResultE.java @@ -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 exceptionDetails = new ArrayList<>(); +} diff --git a/web-domain/src/main/java/com/zcloud/primeport/domain/model/DaHuaDeviceChannelE.java b/web-domain/src/main/java/com/zcloud/primeport/domain/model/DaHuaDeviceChannelE.java new file mode 100644 index 0000000..e8aa447 --- /dev/null +++ b/web-domain/src/main/java/com/zcloud/primeport/domain/model/DaHuaDeviceChannelE.java @@ -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; +} diff --git a/web-domain/src/main/java/com/zcloud/primeport/domain/model/DaHuaResourcePageE.java b/web-domain/src/main/java/com/zcloud/primeport/domain/model/DaHuaResourcePageE.java index fe5bd22..c4c1864 100644 --- a/web-domain/src/main/java/com/zcloud/primeport/domain/model/DaHuaResourcePageE.java +++ b/web-domain/src/main/java/com/zcloud/primeport/domain/model/DaHuaResourcePageE.java @@ -11,4 +11,6 @@ public class DaHuaResourcePageE { private List> records = new ArrayList<>(); private Long total; + /** Number of records returned by the remote page before nested channels are expanded. */ + private Integer sourceRecordCount; } diff --git a/web-domain/src/main/java/com/zcloud/primeport/domain/model/DaHuaResourceSyncResultE.java b/web-domain/src/main/java/com/zcloud/primeport/domain/model/DaHuaResourceSyncResultE.java index 8e002a0..62c2aca 100644 --- a/web-domain/src/main/java/com/zcloud/primeport/domain/model/DaHuaResourceSyncResultE.java +++ b/web-domain/src/main/java/com/zcloud/primeport/domain/model/DaHuaResourceSyncResultE.java @@ -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 unmatchedDepartmentNames = new ArrayList<>(); private List unmatchedDeviceCodes = new ArrayList<>(); + private List exceptionDetails = new ArrayList<>(); } diff --git a/web-infrastructure/src/main/java/com/zcloud/primeport/dahua/api/DaHuaResourceApi.java b/web-infrastructure/src/main/java/com/zcloud/primeport/dahua/api/DaHuaResourceApi.java index 0f1ee6f..568ff88 100644 --- a/web-infrastructure/src/main/java/com/zcloud/primeport/dahua/api/DaHuaResourceApi.java +++ b/web-infrastructure/src/main/java/com/zcloud/primeport/dahua/api/DaHuaResourceApi.java @@ -31,6 +31,10 @@ public class DaHuaResourceApi { return postPage(properties.getDevicePagePath(), body); } + public GeneralResponse pageChannels(Map body) throws ClientException { + return postPage(properties.getChannelPagePath(), body); + } + private GeneralResponse postPage(String path, Map body) throws ClientException { if (daHuaApiClient == null) { return null; diff --git a/web-infrastructure/src/main/java/com/zcloud/primeport/dahua/config/DaHuaAccessRecordProperties.java b/web-infrastructure/src/main/java/com/zcloud/primeport/dahua/config/DaHuaAccessRecordProperties.java new file mode 100644 index 0000000..b9f80eb --- /dev/null +++ b/web-infrastructure/src/main/java/com/zcloud/primeport/dahua/config/DaHuaAccessRecordProperties.java @@ -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; +} diff --git a/web-infrastructure/src/main/java/com/zcloud/primeport/dahua/config/DaHuaAutoConfig.java b/web-infrastructure/src/main/java/com/zcloud/primeport/dahua/config/DaHuaAutoConfig.java index 7d8a537..ada9518 100644 --- a/web-infrastructure/src/main/java/com/zcloud/primeport/dahua/config/DaHuaAutoConfig.java +++ b/web-infrastructure/src/main/java/com/zcloud/primeport/dahua/config/DaHuaAutoConfig.java @@ -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") diff --git a/web-infrastructure/src/main/java/com/zcloud/primeport/dahua/config/DaHuaResourceProperties.java b/web-infrastructure/src/main/java/com/zcloud/primeport/dahua/config/DaHuaResourceProperties.java index 252c0aa..0c8fce7 100644 --- a/web-infrastructure/src/main/java/com/zcloud/primeport/dahua/config/DaHuaResourceProperties.java +++ b/web-infrastructure/src/main/java/com/zcloud/primeport/dahua/config/DaHuaResourceProperties.java @@ -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; } diff --git a/web-infrastructure/src/main/java/com/zcloud/primeport/gatewayimpl/DaHuaAccessRecordRepositoryGatewayImpl.java b/web-infrastructure/src/main/java/com/zcloud/primeport/gatewayimpl/DaHuaAccessRecordRepositoryGatewayImpl.java new file mode 100644 index 0000000..6bdac00 --- /dev/null +++ b/web-infrastructure/src/main/java/com/zcloud/primeport/gatewayimpl/DaHuaAccessRecordRepositoryGatewayImpl.java @@ -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 listByDahuaRecordIds(Collection dahuaRecordIds) { + if (dahuaRecordIds == null || dahuaRecordIds.isEmpty()) { + return Collections.emptyList(); + } + LambdaQueryWrapper 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 convertList(List source) { + List result = new ArrayList<>(); + for (DaHuaAccessRecordDO item : source) { + result.add(copy(item, DaHuaAccessRecordE.class)); + } + return result; + } + + private T copy(Object source, Class 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); + } + } +} diff --git a/web-infrastructure/src/main/java/com/zcloud/primeport/gatewayimpl/DaHuaResourceGatewayImpl.java b/web-infrastructure/src/main/java/com/zcloud/primeport/gatewayimpl/DaHuaResourceGatewayImpl.java index c0c3955..81c90a6 100644 --- a/web-infrastructure/src/main/java/com/zcloud/primeport/gatewayimpl/DaHuaResourceGatewayImpl.java +++ b/web-infrastructure/src/main/java/com/zcloud/primeport/gatewayimpl/DaHuaResourceGatewayImpl.java @@ -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 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> sourceRecords = toRecordList(value); + page.setSourceRecordCount(sourceRecords.size()); + if (object != null) { + page.setTotal(firstLong(object, "totalRows", "total", "totalCount", "recordCount")); + } + List> channels = new ArrayList<>(); + for (Map record : sourceRecords) { + Object nested = firstValue(record, "channels", "channelList", "children"); + List> nestedChannels = toRecordList(nested); + if (nestedChannels.isEmpty() && firstValue(record, "channelCode") != null) { + channels.add(record); + } else { + for (Map channel : nestedChannels) { + Map merged = new HashMap<>(channel); + copyIfAbsent(merged, record, "deviceCode", "deviceName", "orgCode", "orgName", "departmentId"); + channels.add(merged); + } + } + } + page.setRecords(channels); + return page; + } + + private void copyIfAbsent(Map target, Map 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; } diff --git a/web-infrastructure/src/main/java/com/zcloud/primeport/gatewayimpl/DaHuaResourceRepositoryGatewayImpl.java b/web-infrastructure/src/main/java/com/zcloud/primeport/gatewayimpl/DaHuaResourceRepositoryGatewayImpl.java index 3d5f6a1..c9de675 100644 --- a/web-infrastructure/src/main/java/com/zcloud/primeport/gatewayimpl/DaHuaResourceRepositoryGatewayImpl.java +++ b/web-infrastructure/src/main/java/com/zcloud/primeport/gatewayimpl/DaHuaResourceRepositoryGatewayImpl.java @@ -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 listActiveCorps() { LambdaQueryWrapper 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 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()); diff --git a/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/dataobject/CorpInfoSnapshotDO.java b/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/dataobject/CorpInfoSnapshotDO.java index 5bd65ed..4d9ec22 100644 --- a/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/dataobject/CorpInfoSnapshotDO.java +++ b/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/dataobject/CorpInfoSnapshotDO.java @@ -11,4 +11,5 @@ import lombok.EqualsAndHashCode; public class CorpInfoSnapshotDO extends BaseDO { private String corpName; + private Integer portArea; } diff --git a/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/dataobject/DaHuaAccessRecordDO.java b/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/dataobject/DaHuaAccessRecordDO.java new file mode 100644 index 0000000..197892b --- /dev/null +++ b/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/dataobject/DaHuaAccessRecordDO.java @@ -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; +} diff --git a/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/dataobject/DaHuaDeviceChannelDO.java b/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/dataobject/DaHuaDeviceChannelDO.java new file mode 100644 index 0000000..e0b871f --- /dev/null +++ b/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/dataobject/DaHuaDeviceChannelDO.java @@ -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; +} diff --git a/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/mapper/DaHuaAccessRecordMapper.java b/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/mapper/DaHuaAccessRecordMapper.java new file mode 100644 index 0000000..2f406c7 --- /dev/null +++ b/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/mapper/DaHuaAccessRecordMapper.java @@ -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 { + + LocalDateTime findLatestDahuaCreateTime(); +} diff --git a/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/mapper/DaHuaDeviceChannelMapper.java b/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/mapper/DaHuaDeviceChannelMapper.java new file mode 100644 index 0000000..fe805c8 --- /dev/null +++ b/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/mapper/DaHuaDeviceChannelMapper.java @@ -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 { +} diff --git a/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/repository/DaHuaAccessRecordRepository.java b/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/repository/DaHuaAccessRecordRepository.java new file mode 100644 index 0000000..c0b2ee1 --- /dev/null +++ b/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/repository/DaHuaAccessRecordRepository.java @@ -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 { + + LocalDateTime findLatestDahuaCreateTime(); +} diff --git a/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/repository/DaHuaDeviceChannelRepository.java b/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/repository/DaHuaDeviceChannelRepository.java new file mode 100644 index 0000000..bfbd8f7 --- /dev/null +++ b/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/repository/DaHuaDeviceChannelRepository.java @@ -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 { +} diff --git a/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/repository/impl/DaHuaAccessRecordRepositoryImpl.java b/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/repository/impl/DaHuaAccessRecordRepositoryImpl.java new file mode 100644 index 0000000..860a88a --- /dev/null +++ b/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/repository/impl/DaHuaAccessRecordRepositoryImpl.java @@ -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 + implements DaHuaAccessRecordRepository { + + @Override + public LocalDateTime findLatestDahuaCreateTime() { + return baseMapper.findLatestDahuaCreateTime(); + } +} diff --git a/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/repository/impl/DaHuaDeviceChannelRepositoryImpl.java b/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/repository/impl/DaHuaDeviceChannelRepositoryImpl.java new file mode 100644 index 0000000..a4464d5 --- /dev/null +++ b/web-infrastructure/src/main/java/com/zcloud/primeport/persistence/repository/impl/DaHuaDeviceChannelRepositoryImpl.java @@ -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 + implements DaHuaDeviceChannelRepository { +} diff --git a/web-infrastructure/src/main/java/com/zcloud/primeport/plan/DaHuaAccessRecordSyncXxlJob.java b/web-infrastructure/src/main/java/com/zcloud/primeport/plan/DaHuaAccessRecordSyncXxlJob.java new file mode 100644 index 0000000..00c1c97 --- /dev/null +++ b/web-infrastructure/src/main/java/com/zcloud/primeport/plan/DaHuaAccessRecordSyncXxlJob.java @@ -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 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; + } + } +} diff --git a/web-infrastructure/src/main/java/com/zcloud/primeport/plan/DaHuaResourceSyncXxlJob.java b/web-infrastructure/src/main/java/com/zcloud/primeport/plan/DaHuaResourceSyncXxlJob.java new file mode 100644 index 0000000..5006ca6 --- /dev/null +++ b/web-infrastructure/src/main/java/com/zcloud/primeport/plan/DaHuaResourceSyncXxlJob.java @@ -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 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; + } + } +} diff --git a/web-infrastructure/src/main/resources/TableCreationDDL.sql b/web-infrastructure/src/main/resources/TableCreationDDL.sql index c394c57..69c7b98 100644 --- a/web-infrastructure/src/main/resources/TableCreationDDL.sql +++ b/web-infrastructure/src/main/resources/TableCreationDDL.sql @@ -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='大华门禁通行记录表'; diff --git a/web-infrastructure/src/main/resources/db/migration/V20260803_01__create_dahua_device_channel.sql b/web-infrastructure/src/main/resources/db/migration/V20260803_01__create_dahua_device_channel.sql new file mode 100644 index 0000000..6c6292e --- /dev/null +++ b/web-infrastructure/src/main/resources/db/migration/V20260803_01__create_dahua_device_channel.sql @@ -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; diff --git a/web-infrastructure/src/main/resources/db/migration/V20260803_02__add_corp_id_to_dahua_device_channel.sql b/web-infrastructure/src/main/resources/db/migration/V20260803_02__add_corp_id_to_dahua_device_channel.sql new file mode 100644 index 0000000..e0c143d --- /dev/null +++ b/web-infrastructure/src/main/resources/db/migration/V20260803_02__add_corp_id_to_dahua_device_channel.sql @@ -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`); diff --git a/web-infrastructure/src/main/resources/db/migration/V20260804_01__create_dahua_access_record.sql b/web-infrastructure/src/main/resources/db/migration/V20260804_01__create_dahua_access_record.sql new file mode 100644 index 0000000..61229c2 --- /dev/null +++ b/web-infrastructure/src/main/resources/db/migration/V20260804_01__create_dahua_access_record.sql @@ -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='大华门禁通行记录表'; diff --git a/web-infrastructure/src/main/resources/mapper/DaHuaAccessRecordMapper.xml b/web-infrastructure/src/main/resources/mapper/DaHuaAccessRecordMapper.xml new file mode 100644 index 0000000..2380057 --- /dev/null +++ b/web-infrastructure/src/main/resources/mapper/DaHuaAccessRecordMapper.xml @@ -0,0 +1,15 @@ + + + + + + + +