From 7bfab8159f1dcd96bbd0bd25dad207f00332c4d9 Mon Sep 17 00:00:00 2001 From: shenzhidan Date: Thu, 23 Jul 2026 16:50:00 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=AE=9E=E6=97=B6=E5=AE=9A?= =?UTF-8?q?=E4=BD=8D=E6=8E=A8=E9=80=81=E4=B8=BA=E7=9B=B4=E6=8E=A5=E8=BD=AC?= =?UTF-8?q?=E6=8D=A2WebSocket=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan/FindsStaffMappingSyncJob.java | 32 ++ .../query/FindsStaffMappingSyncService.java | 287 ++++++++++++++++++ .../PositionPersonLocalLookupService.java | 137 +++++++++ .../command/query/PositionPersonQueryExe.java | 168 ++++++---- .../query/RealtimePersonLocationMatcher.java | 141 +++++---- .../integration/finds/FindsOpenApiClient.java | 112 ++++++- .../kafka/LocationKafkaProbeListener.java | 10 +- .../src/main/resources/TableCreationDDL.sql | 45 +++ .../V20260723_01__personnel_match_mapping.sql | 45 +++ 9 files changed, 844 insertions(+), 133 deletions(-) create mode 100644 web-adapter/src/main/java/com/zcloud/personnel/positioning/plan/FindsStaffMappingSyncJob.java create mode 100644 web-app/src/main/java/com/zcloud/personnel/positioning/command/query/FindsStaffMappingSyncService.java create mode 100644 web-infrastructure/src/main/resources/db/migration/mysql/V20260723_01__personnel_match_mapping.sql diff --git a/web-adapter/src/main/java/com/zcloud/personnel/positioning/plan/FindsStaffMappingSyncJob.java b/web-adapter/src/main/java/com/zcloud/personnel/positioning/plan/FindsStaffMappingSyncJob.java new file mode 100644 index 0000000..869fb37 --- /dev/null +++ b/web-adapter/src/main/java/com/zcloud/personnel/positioning/plan/FindsStaffMappingSyncJob.java @@ -0,0 +1,32 @@ +package com.zcloud.personnel.positioning.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.context.XxlJobHelper; +import com.xxl.job.core.handler.annotation.XxlJob; +import com.zcloud.personnel.positioning.command.query.FindsStaffMappingSyncService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class FindsStaffMappingSyncJob implements Job { + private final FindsStaffMappingSyncService syncService; + + @Override + @JobRegister(cron = "0 */5 * * * ?", jobDesc = "FindS人员映射同步", author = "系统", triggerStatus = 1) + @XxlJob("com.zcloud.personnel.positioning.plan.FindsStaffMappingSyncJob") + public ReturnT execute(String param) { + try { + int count = syncService.syncAll(); + String message = "FindS人员映射同步完成,本次处理 " + count + " 条"; + XxlJobHelper.log(message); + return new ReturnT<>(ReturnT.SUCCESS_CODE, message); + } catch (Exception e) { + String message = "FindS人员映射同步失败:" + e.getMessage(); + XxlJobHelper.log(message); + return new ReturnT<>(ReturnT.FAIL_CODE, message); + } + } +} diff --git a/web-app/src/main/java/com/zcloud/personnel/positioning/command/query/FindsStaffMappingSyncService.java b/web-app/src/main/java/com/zcloud/personnel/positioning/command/query/FindsStaffMappingSyncService.java new file mode 100644 index 0000000..fd48ded --- /dev/null +++ b/web-app/src/main/java/com/zcloud/personnel/positioning/command/query/FindsStaffMappingSyncService.java @@ -0,0 +1,287 @@ +package com.zcloud.personnel.positioning.command.query; + +import com.alibaba.cola.exception.BizException; +import com.fasterxml.jackson.databind.JsonNode; +import com.zcloud.personnel.positioning.integration.finds.FindsOpenApiClient; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Refreshes the FindS-to-local personnel mapping cache. + * + *

The cache contains identity and terminal snapshots only. Location coordinates are deliberately + * not persisted here and continue to come from FindS at query time.

+ */ +@Component +@RequiredArgsConstructor +public class FindsStaffMappingSyncService { + private static final Logger LOGGER = LoggerFactory.getLogger(FindsStaffMappingSyncService.class); + private static final String STAFF_LIST_API = "finds.staff.list"; + private static final String SOURCE_CONFIG_CODE = "FINDS"; + private static final int PAGE_SIZE = 2000; + private static final int MAX_PAGE_COUNT = 50; + private static final int BATCH_CONCURRENCY = 4; + + private final FindsOpenApiClient findsOpenApiClient; + private final PositionPersonLocalLookupService localLookupService; + private final JdbcTemplate jdbcTemplate; + + public int syncAll() { + // The schema uses second precision; keep the same precision for the stale-row cutoff. + LocalDateTime syncTime = LocalDateTime.now().withNano(0); + List staffRows = loadAllStaff(); + Set staffNos = new LinkedHashSet<>(); + for (StaffSnapshot row : staffRows) { + staffNos.add(row.staffNo); + } + Map localPeople = + localLookupService.findUniquePeopleByPhones(staffNos); + for (StaffSnapshot row : staffRows) { + upsert(row, localPeople.get(row.staffNo), syncTime); + } + disableMissingRows(syncTime); + LOGGER.info("FindS staff mapping synchronization completed, total={}, matched={}", + staffRows.size(), countMatched(staffRows, localPeople)); + return staffRows.size(); + } + + private List loadAllStaff() { + List result = new ArrayList<>(); + JsonNode firstData = successData( + findsOpenApiClient.postFailFast(STAFF_LIST_API, pageRequest(1))); + int firstPageSize = appendRows(result, extractRows(firstData)); + if (firstPageSize == 0) { + return result; + } + long total = totalValue(firstData, firstPageSize); + int pageCount = (int) Math.min((total + PAGE_SIZE - 1) / PAGE_SIZE, MAX_PAGE_COUNT); + if (pageCount <= 1) { + return result; + } + List> requests = new ArrayList<>(pageCount - 1); + for (int pageNo = 2; pageNo <= pageCount; pageNo++) { + requests.add(pageRequest(pageNo)); + } + for (JsonNode root : findsOpenApiClient.postFailFastBatch( + STAFF_LIST_API, requests, BATCH_CONCURRENCY)) { + appendRows(result, extractRows(successData(root))); + } + return result; + } + + private Map pageRequest(int pageNo) { + Map request = new LinkedHashMap<>(); + request.put("pageNo", pageNo); + request.put("pageSize", PAGE_SIZE); + return request; + } + + private int appendRows(List result, JsonNode rows) { + if (rows == null || !rows.isArray()) { + return 0; + } + for (JsonNode row : rows) { + StaffSnapshot snapshot = map(row); + if (snapshot != null) { + result.add(snapshot); + } + } + return rows.size(); + } + + private StaffSnapshot map(JsonNode row) { + if (row == null || row.isNull()) { + return null; + } + JsonNode terminalInfo = row.path("terminalInfo"); + StaffSnapshot snapshot = new StaffSnapshot(); + snapshot.staffNo = firstText( + text(firstPresent(row, "trackNo")), + text(firstPresent(terminalInfo, "trackNo")), + text(firstPresent(row, "workNo")), + text(firstPresent(row, "employeeNo")), + text(firstPresent(row, "staffNo")), + text(firstPresent(row, "code")), + text(firstPresent(terminalInfo, "staffNo"))); + if (!StringUtils.hasText(snapshot.staffNo)) { + return null; + } + snapshot.staffNoType = firstText( + text(firstPresent(row, "trackNo")), + text(firstPresent(terminalInfo, "trackNo"))) != null + ? "TRACK_NO" : "STAFF_NO"; + snapshot.findsTrackId = longValue(firstPresent(row, "trackId", "id")); + snapshot.idCardNo = firstText( + text(firstPresent(row, "idCardNo", "identityNo", "certificateNo", "cardNo")), + text(firstPresent(terminalInfo, "idCardNo"))); + snapshot.staffName = firstText(text(firstPresent(row, "staffName", "name", "realName", "userName"))); + snapshot.corpinfoId = firstText(text(firstPresent(row, + "corpinfoId", "companyId", "corpId", "enterpriseId"))); + snapshot.corpinfoName = firstText(text(firstPresent(row, + "companyName", "corpName", "enterpriseName", "orgName"))); + snapshot.departmentName = firstText(text(firstPresent(row, + "departmentName", "deptName", "orgDepartmentName"))); + snapshot.terminalNo = firstText( + text(firstPresent(row, "terminalNo")), + text(firstPresent(terminalInfo, "terminalNo")), + text(firstPresent(row.path("terminal"), "terminalNo")), + text(firstPresent(row.path("bindTerminal"), "terminalNo")), + terminalNoFromList(row.path("takenTerminalList"))); + return snapshot; + } + + private void upsert(StaffSnapshot row, + PositionPersonLocalLookupService.LocalPerson local, + LocalDateTime syncTime) { + String matchStatus = local == null ? "UNMATCHED" : "MATCHED"; + String matchType = local == null ? null : "MOBILE"; + if (local != null && StringUtils.hasText(row.idCardNo) && StringUtils.hasText(local.getIdCardNo()) + && !row.idCardNo.equals(local.getIdCardNo().trim())) { + matchStatus = "AMBIGUOUS"; + matchType = null; + local = null; + } + String sql = "INSERT INTO personnel_match (source_system, source_config_code, finds_staff_no, " + + "finds_staff_no_type, finds_track_id, finds_id_card_no, finds_staff_name, " + + "finds_corpinfo_id, finds_corpinfo_name, finds_department_name, finds_terminal_no, " + + "local_user_id, local_mobile_no, local_id_card_no, match_status, match_type, " + + "last_seen_time, last_sync_time, sync_status, sync_error, delete_enum) " + + "VALUES ('FINDS', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'SUCCESS', NULL, 'FALSE') " + + "ON DUPLICATE KEY UPDATE finds_staff_no_type = VALUES(finds_staff_no_type), " + + "finds_track_id = VALUES(finds_track_id), finds_id_card_no = VALUES(finds_id_card_no), " + + "finds_staff_name = VALUES(finds_staff_name), finds_corpinfo_id = VALUES(finds_corpinfo_id), " + + "finds_corpinfo_name = VALUES(finds_corpinfo_name), finds_department_name = VALUES(finds_department_name), " + + "finds_terminal_no = VALUES(finds_terminal_no), local_user_id = VALUES(local_user_id), " + + "local_mobile_no = VALUES(local_mobile_no), local_id_card_no = VALUES(local_id_card_no), " + + "match_status = VALUES(match_status), match_type = VALUES(match_type), " + + "last_seen_time = VALUES(last_seen_time), last_sync_time = VALUES(last_sync_time), " + + "sync_status = 'SUCCESS', sync_error = NULL, delete_enum = 'FALSE', version = version + 1"; + jdbcTemplate.update(sql, SOURCE_CONFIG_CODE, row.staffNo, row.staffNoType, row.findsTrackId, + row.idCardNo, row.staffName, row.corpinfoId, row.corpinfoName, row.departmentName, + row.terminalNo, local == null ? null : local.getUserId(), + local == null ? null : local.getMobileNo(), local == null ? null : local.getIdCardNo(), + matchStatus, matchType, syncTime, syncTime); + } + + private void disableMissingRows(LocalDateTime syncTime) { + jdbcTemplate.update("UPDATE personnel_match SET match_status = 'DISABLED', " + + "sync_status = 'SUCCESS', last_sync_time = ?, version = version + 1 " + + "WHERE source_config_code = ? AND (delete_enum IS NULL OR delete_enum = 'FALSE') " + + "AND (last_seen_time IS NULL OR last_seen_time < ?)", + syncTime, SOURCE_CONFIG_CODE, syncTime); + } + + private int countMatched(List rows, + Map localPeople) { + int count = 0; + for (StaffSnapshot row : rows) { + if (localPeople.containsKey(row.staffNo)) { + count++; + } + } + return count; + } + + private JsonNode successData(JsonNode root) { + int code = root == null ? -1 : root.path("code").asInt(-1); + if (code != 200 && code != 0) { + throw new BizException("FindS staff list synchronization failed, code=" + code); + } + return root.path("data"); + } + + private JsonNode extractRows(JsonNode data) { + if (data == null || data.isNull() || data.isMissingNode()) { + return null; + } + if (data.isArray()) { + return data; + } + for (String field : new String[]{"data", "rows", "list", "records"}) { + JsonNode value = data.get(field); + if (value != null && value.isArray()) { + return value; + } + } + return null; + } + + private long totalValue(JsonNode data, long defaultValue) { + Long total = longValue(firstPresent(data, "total", "totalCount", "count", "totalRecords")); + return total == null ? defaultValue : total; + } + + private JsonNode firstPresent(JsonNode node, String... fields) { + if (node == null || node.isNull() || node.isMissingNode()) { + return null; + } + for (String field : fields) { + JsonNode value = node.get(field); + if (value != null && !value.isNull() && !value.isMissingNode()) { + return value; + } + } + return null; + } + + private String terminalNoFromList(JsonNode terminals) { + if (terminals != null && terminals.isArray()) { + for (JsonNode terminal : terminals) { + String terminalNo = text(firstPresent(terminal, "terminalNo")); + if (StringUtils.hasText(terminalNo)) { + return terminalNo.trim(); + } + } + } + return null; + } + + private String text(JsonNode node) { + return node == null || node.isNull() || node.isMissingNode() || !node.isValueNode() + ? null : node.asText(); + } + + private String firstText(String... values) { + for (String value : values) { + if (StringUtils.hasText(value)) { + return value.trim(); + } + } + return null; + } + + private Long longValue(JsonNode node) { + if (node == null || node.isNull() || node.isMissingNode()) { + return null; + } + try { + return node.isNumber() ? node.asLong() : Long.valueOf(node.asText()); + } catch (NumberFormatException ignored) { + return null; + } + } + + private static class StaffSnapshot { + private String staffNo; + private String staffNoType; + private Long findsTrackId; + private String idCardNo; + private String staffName; + private String corpinfoId; + private String corpinfoName; + private String departmentName; + private String terminalNo; + } +} diff --git a/web-app/src/main/java/com/zcloud/personnel/positioning/command/query/PositionPersonLocalLookupService.java b/web-app/src/main/java/com/zcloud/personnel/positioning/command/query/PositionPersonLocalLookupService.java index 0a26c9d..4ca580d 100644 --- a/web-app/src/main/java/com/zcloud/personnel/positioning/command/query/PositionPersonLocalLookupService.java +++ b/web-app/src/main/java/com/zcloud/personnel/positioning/command/query/PositionPersonLocalLookupService.java @@ -1,7 +1,9 @@ package com.zcloud.personnel.positioning.command.query; +import com.zcloud.personnel.positioning.dto.PositionPersonPageQry; import lombok.Data; import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -22,6 +24,90 @@ class PositionPersonLocalLookupService { private final JdbcTemplate jdbcTemplate; + boolean hasPersonnelMappings() { + try { + Integer count = jdbcTemplate.queryForObject( + "SELECT COUNT(1) FROM personnel_match " + + "WHERE (delete_enum IS NULL OR delete_enum = 'FALSE')", + Integer.class); + return count != null && count > 0; + } catch (DataAccessException ignored) { + // Keep the pre-migration FindS fallback available during rolling deployment. + return false; + } + } + + List findMatchedPersonnel(PositionPersonPageQry query) { + StringBuilder sql = new StringBuilder( + "SELECT pm.finds_staff_no, pm.finds_staff_no_type, pm.finds_id_card_no, " + + "pm.finds_staff_name, pm.finds_corpinfo_id, pm.finds_corpinfo_name, " + + "pm.finds_department_name, pm.finds_terminal_no, " + + "u.id AS user_id, u.username AS local_staff_no, u.name AS staff_name, " + + "TRIM(u.phone) AS mobile_no, u.user_id_card AS id_card_no, u.corpinfo_id, " + + "ci.corp_name AS corpinfo_name, ci.code AS credit_code, ci.port_area, " + + "u.department_id, d.name AS department_name " + + "FROM personnel_match pm JOIN user_scope_v u ON u.id = pm.local_user_id " + + "LEFT JOIN corp_info ci ON CAST(ci.corpinfo_id AS CHAR) = CAST(u.corpinfo_id AS CHAR) " + + "LEFT JOIN department d ON d.id = u.department_id " + + "AND (d.delete_enum IS NULL OR d.delete_enum = 'FALSE') " + + "WHERE pm.match_status = 'MATCHED' " + + "AND (pm.delete_enum IS NULL OR pm.delete_enum = 'FALSE') " + + "AND (u.delete_enum IS NULL OR u.delete_enum = 'FALSE')"); + List args = new ArrayList<>(); + if (query != null && query.getUserId() != null) { + sql.append(" AND u.id = ?"); + args.add(query.getUserId()); + } + if (query != null && StringUtils.hasText(query.getStaffName())) { + sql.append(" AND u.name LIKE ?"); + args.add("%" + query.getStaffName().trim() + "%"); + } + if (query != null && query.getCorpinfoId() != null) { + sql.append(" AND CAST(u.corpinfo_id AS CHAR) = ?"); + args.add(String.valueOf(query.getCorpinfoId())); + } + if (query != null && StringUtils.hasText(query.getCorpinfoName())) { + sql.append(" AND ci.corp_name LIKE ?"); + args.add("%" + query.getCorpinfoName().trim() + "%"); + } + if (query != null && query.getDepartmentId() != null) { + sql.append(" AND u.department_id = ?"); + args.add(query.getDepartmentId()); + } + if (query != null && StringUtils.hasText(query.getDepartmentName())) { + sql.append(" AND d.name LIKE ?"); + args.add("%" + query.getDepartmentName().trim() + "%"); + } + sql.append(" ORDER BY u.id DESC, pm.finds_staff_no"); + return jdbcTemplate.query(sql.toString(), args.toArray(), (rs, rowNum) -> { + LocalPerson local = new LocalPerson(); + local.setUserId(nullableLong(rs.getLong("user_id"), rs.wasNull())); + local.setLocalStaffNo(rs.getString("local_staff_no")); + local.setStaffName(rs.getString("staff_name")); + local.setMobileNo(normalize(rs.getString("mobile_no"))); + local.setIdCardNo(normalize(rs.getString("id_card_no"))); + local.setCorpinfoId(rs.getString("corpinfo_id")); + local.setCorpinfoName(rs.getString("corpinfo_name")); + local.setCreditCode(rs.getString("credit_code")); + int portArea = rs.getInt("port_area"); + local.setPortArea(rs.wasNull() ? null : portArea); + local.setDepartmentId(nullableLong(rs.getLong("department_id"), rs.wasNull())); + local.setDepartmentName(rs.getString("department_name")); + + PersonMapping mapping = new PersonMapping(); + mapping.setFindsStaffNo(normalize(rs.getString("finds_staff_no"))); + mapping.setFindsStaffNoType(normalize(rs.getString("finds_staff_no_type"))); + mapping.setFindsIdCardNo(normalize(rs.getString("finds_id_card_no"))); + mapping.setFindsStaffName(rs.getString("finds_staff_name")); + mapping.setFindsCorpinfoId(rs.getString("finds_corpinfo_id")); + mapping.setFindsCorpinfoName(rs.getString("finds_corpinfo_name")); + mapping.setFindsDepartmentName(rs.getString("finds_department_name")); + mapping.setFindsTerminalNo(normalize(rs.getString("finds_terminal_no"))); + mapping.setLocal(local); + return mapping; + }); + } + Map findUniquePeopleByPhones(Set phones) { Map> grouped = new LinkedHashMap<>(); for (List batch : batches(phones)) { @@ -110,6 +196,34 @@ class PositionPersonLocalLookupService { return unique; } + Map findTerminalNosByIdCardNos(Set idCardNos) { + Map result = new LinkedHashMap<>(); + for (List batch : batches(idCardNos)) { + String placeholders = String.join(",", Collections.nCopies(batch.size(), "?")); + String sql = "SELECT COALESCE(NULLIF(TRIM(tb.id_card_no), ''), " + + "NULLIF(TRIM(t.bind_id_card_no), '')) AS id_card_no, t.terminal_no " + + "FROM terminal t LEFT JOIN terminal_bind tb ON tb.id = (" + + "SELECT tb2.id FROM terminal_bind tb2 WHERE tb2.terminal_no = t.terminal_no " + + "AND tb2.bind_status = 'BOUND' " + + "AND (tb2.delete_enum IS NULL OR tb2.delete_enum = 'FALSE') " + + "ORDER BY tb2.bind_time DESC, tb2.id DESC LIMIT 1) " + + "WHERE (t.delete_enum IS NULL OR t.delete_enum = 'FALSE') " + + "AND COALESCE(NULLIF(TRIM(tb.id_card_no), ''), " + + "NULLIF(TRIM(t.bind_id_card_no), '')) IN (" + placeholders + ") " + + "ORDER BY id_card_no, t.update_time DESC, t.id DESC"; + jdbcTemplate.query(sql, batch.toArray(), (rs, rowNum) -> { + String idCardNo = normalize(rs.getString("id_card_no")); + String terminalNo = normalize(rs.getString("terminal_no")); + return new TerminalBinding(idCardNo, terminalNo); + }).forEach(binding -> { + if (StringUtils.hasText(binding.idCardNo) && StringUtils.hasText(binding.terminalNo)) { + result.putIfAbsent(binding.idCardNo, binding.terminalNo); + } + }); + } + return result; + } + Map findTerminals(Set terminalNos) { Map result = new LinkedHashMap<>(); for (List batch : batches(terminalNos)) { @@ -178,6 +292,19 @@ class PositionPersonLocalLookupService { private String lastLocationName; } + @Data + static class PersonMapping { + private String findsStaffNo; + private String findsStaffNoType; + private String findsIdCardNo; + private String findsStaffName; + private String findsCorpinfoId; + private String findsCorpinfoName; + private String findsDepartmentName; + private String findsTerminalNo; + private LocalPerson local; + } + private static class TerminalPerson { private final String terminalNo; private final LocalPerson person; @@ -187,4 +314,14 @@ class PositionPersonLocalLookupService { this.person = person; } } + + private static class TerminalBinding { + private final String idCardNo; + private final String terminalNo; + + private TerminalBinding(String idCardNo, String terminalNo) { + this.idCardNo = idCardNo; + this.terminalNo = terminalNo; + } + } } diff --git a/web-app/src/main/java/com/zcloud/personnel/positioning/command/query/PositionPersonQueryExe.java b/web-app/src/main/java/com/zcloud/personnel/positioning/command/query/PositionPersonQueryExe.java index af27389..97b06d8 100644 --- a/web-app/src/main/java/com/zcloud/personnel/positioning/command/query/PositionPersonQueryExe.java +++ b/web-app/src/main/java/com/zcloud/personnel/positioning/command/query/PositionPersonQueryExe.java @@ -30,10 +30,10 @@ public class PositionPersonQueryExe { static final String STATUS_OFFLINE = "OFFLINE"; private static final String STAFF_LIST_API = "finds.staff.list"; - private static final String STAFF_TERMINAL_QUERY_API = "finds.staff.taken.terminal.query"; private static final String POINT_LOCATE_API = "finds.point.locate"; - private static final int FINDS_PAGE_SIZE = 200; + private static final int FINDS_PAGE_SIZE = 2000; private static final int MAX_FINDS_PAGE_COUNT = 50; + private static final int FINDS_BATCH_CONCURRENCY = 4; private static final int LOCATE_BATCH_SIZE = 100; private final FindsOpenApiClient findsOpenApiClient; @@ -41,34 +41,26 @@ public class PositionPersonQueryExe { public PageResponse list(PositionPersonPageQry qry) { PositionPersonPageQry query = qry == null ? new PositionPersonPageQry() : qry; - List findsStaff = loadAllFindsStaff(); - Set phones = new LinkedHashSet<>(); - for (FindsStaff staff : findsStaff) { - if (StringUtils.hasText(staff.staffNo)) { - phones.add(staff.staffNo); + List matched = loadMatchedPeople(query); + Set idCardNos = new LinkedHashSet<>(); + for (MatchedPerson person : matched) { + if (StringUtils.hasText(person.local.getIdCardNo())) { + idCardNos.add(person.local.getIdCardNo()); } } - Map localPeople = - localLookupService.findUniquePeopleByPhones(phones); - List matched = new ArrayList<>(); + Map localTerminalNos = localLookupService.findTerminalNosByIdCardNos(idCardNos); + if (localTerminalNos == null) { + localTerminalNos = Collections.emptyMap(); + } Set terminalNos = new LinkedHashSet<>(); - Set matchedUserIds = new LinkedHashSet<>(); - for (FindsStaff staff : findsStaff) { - PositionPersonLocalLookupService.LocalPerson local = localPeople.get(staff.staffNo); - if (local == null || !matchesLocalPerson(local, query)) { - continue; + for (MatchedPerson person : matched) { + if (!StringUtils.hasText(person.finds.terminalNo)) { + person.finds.terminalNo = localTerminalNos.get(normalize(person.local.getIdCardNo())); } - if (local.getUserId() != null && !matchedUserIds.add(local.getUserId())) { - continue; + if (StringUtils.hasText(person.finds.terminalNo)) { + terminalNos.add(person.finds.terminalNo); } - if (!StringUtils.hasText(staff.terminalNo)) { - staff.terminalNo = resolveTerminalNo(staff.staffNo); - } - if (StringUtils.hasText(staff.terminalNo)) { - terminalNos.add(staff.terminalNo); - } - matched.add(new MatchedPerson(staff, local)); } Map currentLocations = locateTerminals(terminalNos); @@ -102,6 +94,54 @@ public class PositionPersonQueryExe { return PageResponse.of(rows.subList(fromIndex, toIndex), rows.size(), pageSize, pageIndex); } + private List loadMatchedPeople(PositionPersonPageQry query) { + if (localLookupService.hasPersonnelMappings()) { + return loadPersistedMatches(query); + } + List findsStaff = loadAllFindsStaff(); + Set phones = new LinkedHashSet<>(); + for (FindsStaff staff : findsStaff) { + if (StringUtils.hasText(staff.staffNo)) { + phones.add(staff.staffNo); + } + } + Map localPeople = + localLookupService.findUniquePeopleByPhones(phones); + + List matched = new ArrayList<>(); + Set matchedUserIds = new LinkedHashSet<>(); + for (FindsStaff staff : findsStaff) { + PositionPersonLocalLookupService.LocalPerson local = localPeople.get(staff.staffNo); + if (local == null || !matchesLocalPerson(local, query)) { + continue; + } + if (local.getUserId() != null && !matchedUserIds.add(local.getUserId())) { + continue; + } + matched.add(new MatchedPerson(staff, local)); + } + return matched; + } + + private List loadPersistedMatches(PositionPersonPageQry query) { + List result = new ArrayList<>(); + Set matchedUserIds = new LinkedHashSet<>(); + for (PositionPersonLocalLookupService.PersonMapping mapping + : localLookupService.findMatchedPersonnel(query)) { + PositionPersonLocalLookupService.LocalPerson local = mapping.getLocal(); + if (local == null || (local.getUserId() != null && !matchedUserIds.add(local.getUserId()))) { + continue; + } + FindsStaff staff = new FindsStaff(); + staff.staffNo = mapping.getFindsStaffNo(); + staff.sourceCompanyName = mapping.getFindsCorpinfoName(); + staff.sourceDepartmentName = mapping.getFindsDepartmentName(); + staff.terminalNo = mapping.getFindsTerminalNo(); + result.add(new MatchedPerson(staff, local)); + } + return result; + } + public PositionPersonCO queryByUserId(Long userId) { if (userId == null) { throw new BizException("本平台用户ID不能为空"); @@ -119,30 +159,53 @@ public class PositionPersonQueryExe { private List loadAllFindsStaff() { List result = new ArrayList<>(); - long total = Long.MAX_VALUE; - int loaded = 0; - for (int pageNo = 1; pageNo <= MAX_FINDS_PAGE_COUNT && loaded < total; pageNo++) { - Map request = new LinkedHashMap<>(); - request.put("pageNo", pageNo); - request.put("pageSize", FINDS_PAGE_SIZE); - JsonNode root = findsOpenApiClient.post(STAFF_LIST_API, request); + JsonNode firstRoot = findsOpenApiClient.postFailFast(STAFF_LIST_API, staffPageRequest(1)); + JsonNode firstData = assertSuccess(firstRoot, STAFF_LIST_API).path("data"); + int firstPageSize = appendFindsStaff(result, extractRows(firstData)); + if (firstPageSize == 0) { + return result; + } + + long total = totalValue(firstData, firstPageSize); + int pageCount = (int) Math.min((total + FINDS_PAGE_SIZE - 1) / FINDS_PAGE_SIZE, + MAX_FINDS_PAGE_COUNT); + if (pageCount <= 1) { + return result; + } + + List> requests = new ArrayList<>(pageCount - 1); + for (int pageNo = 2; pageNo <= pageCount; pageNo++) { + requests.add(staffPageRequest(pageNo)); + } + List roots = findsOpenApiClient.postFailFastBatch( + STAFF_LIST_API, requests, FINDS_BATCH_CONCURRENCY); + for (JsonNode root : roots) { JsonNode data = assertSuccess(root, STAFF_LIST_API).path("data"); - JsonNode rows = extractRows(data); - if (rows == null || !rows.isArray() || rows.size() == 0) { - break; - } - for (JsonNode row : rows) { - FindsStaff staff = mapFindsStaff(row); - if (StringUtils.hasText(staff.staffNo)) { - result.add(staff); - } - } - loaded += rows.size(); - total = totalValue(data, loaded); + appendFindsStaff(result, extractRows(data)); } return result; } + private Map staffPageRequest(int pageNo) { + Map request = new LinkedHashMap<>(); + request.put("pageNo", pageNo); + request.put("pageSize", FINDS_PAGE_SIZE); + return request; + } + + private int appendFindsStaff(List result, JsonNode rows) { + if (rows == null || !rows.isArray() || rows.size() == 0) { + return 0; + } + for (JsonNode row : rows) { + FindsStaff staff = mapFindsStaff(row); + if (StringUtils.hasText(staff.staffNo)) { + result.add(staff); + } + } + return rows.size(); + } + private FindsStaff mapFindsStaff(JsonNode row) { FindsStaff staff = new FindsStaff(); JsonNode terminalInfo = row.path("terminalInfo"); @@ -164,23 +227,6 @@ public class PositionPersonQueryExe { return staff; } - private String resolveTerminalNo(String staffNo) { - if (!StringUtils.hasText(staffNo)) { - return null; - } - Map request = Collections.singletonMap("staffNo", staffNo); - JsonNode root = findsOpenApiClient.post(STAFF_TERMINAL_QUERY_API, request); - JsonNode data = assertSuccess(root, STAFF_TERMINAL_QUERY_API).path("data"); - JsonNode rows = extractRows(data); - if (rows == null || !rows.isArray() || rows.size() == 0) { - return null; - } - JsonNode first = rows.get(0); - return firstText(text(firstPresent(first, "terminalNo")), - text(firstPresent(first.path("bindTerminal"), "terminalNo")), - terminalNoFromList(first.path("takenTerminalList"))); - } - private Map locateTerminals(Set terminalNos) { if (terminalNos == null || terminalNos.isEmpty()) { return Collections.emptyMap(); @@ -202,7 +248,7 @@ public class PositionPersonQueryExe { private void locateTerminalBatch(List terminalNos, Map result) { Map request = Collections.singletonMap("terminalNoList", new ArrayList<>(terminalNos)); - JsonNode root = findsOpenApiClient.post(POINT_LOCATE_API, request); + JsonNode root = findsOpenApiClient.postFailFast(POINT_LOCATE_API, request); JsonNode data = assertSuccess(root, POINT_LOCATE_API).path("data"); JsonNode rows = extractRows(data); if (rows != null && rows.isArray()) { diff --git a/web-app/src/main/java/com/zcloud/personnel/positioning/command/query/RealtimePersonLocationMatcher.java b/web-app/src/main/java/com/zcloud/personnel/positioning/command/query/RealtimePersonLocationMatcher.java index 2f39328..2241239 100644 --- a/web-app/src/main/java/com/zcloud/personnel/positioning/command/query/RealtimePersonLocationMatcher.java +++ b/web-app/src/main/java/com/zcloud/personnel/positioning/command/query/RealtimePersonLocationMatcher.java @@ -11,10 +11,7 @@ import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; -import java.util.Set; @Component @RequiredArgsConstructor @@ -23,48 +20,21 @@ public class RealtimePersonLocationMatcher { private static final String[] ENVELOPE_FIELDS = {"data", "payload", "message", "body"}; private final ObjectMapper objectMapper; - private final PositionPersonLocalLookupService localLookupService; public List match(byte[] payload) { JsonNode root = parse(payload); List candidates = new ArrayList<>(); - Set staffNos = new LinkedHashSet<>(); for (JsonNode row : extractRows(root)) { String staffNo = staffNo(row); if (!StringUtils.hasText(staffNo)) { continue; } - candidates.add(new LocationCandidate(row, staffNo)); - staffNos.add(staffNo); + candidates.add(new LocationCandidate(row, staffNo, terminalNo(row))); } - if (staffNos.isEmpty()) { - return Collections.emptyList(); - } - - Map localPeople = - localLookupService.findUniquePeopleByPhones(staffNos); - if (localPeople.isEmpty()) { - return Collections.emptyList(); - } - Set terminalNos = new LinkedHashSet<>(); - for (LocationCandidate candidate : candidates) { - if (!localPeople.containsKey(candidate.staffNo)) { - continue; - } - candidate.terminalNo = terminalNo(candidate.row); - if (StringUtils.hasText(candidate.terminalNo)) { - terminalNos.add(candidate.terminalNo); - } - } - Map terminals = - localLookupService.findTerminals(terminalNos); List result = new ArrayList<>(); for (LocationCandidate candidate : candidates) { - PositionPersonLocalLookupService.LocalPerson local = localPeople.get(candidate.staffNo); - if (local != null) { - result.add(toClientObject(candidate, local, terminals.get(candidate.terminalNo))); - } + result.add(toClientObject(candidate)); } return result; } @@ -167,29 +137,49 @@ public class RealtimePersonLocationMatcher { directText(locationNode(row), "terminalNo")); } - private BiPersonLocationCO toClientObject(LocationCandidate candidate, - PositionPersonLocalLookupService.LocalPerson local, - PositionPersonLocalLookupService.TerminalSnapshot terminal) { + private BiPersonLocationCO toClientObject(LocationCandidate candidate) { JsonNode row = candidate.row; JsonNode location = locationNode(row); + JsonNode staff = firstObject(row, "staff", "person", "personInfo", "staffInfo"); + String orgName = firstText( + directText(row, "orgName", "organizationName", "companyName", "corpName", "enterpriseName"), + directText(staff, "orgName", "organizationName", "companyName", "corpName", "enterpriseName")); + String orgCode = firstText( + directText(row, "orgCode", "companyCode", "corpCode", "enterpriseCode"), + directText(staff, "orgCode", "companyCode", "corpCode", "enterpriseCode")); BiPersonLocationCO co = new BiPersonLocationCO(); - co.setUserId(local.getUserId()); - co.setStaffName(local.getStaffName()); - co.setLocalStaffNo(local.getLocalStaffNo()); + co.setStaffName(firstText( + directText(row, "staffName", "name", "realName", "userName"), + directText(staff, "staffName", "name", "realName", "userName"))); co.setStaffNo(candidate.staffNo); - co.setIdCardNo(local.getIdCardNo()); - co.setMobileNo(local.getMobileNo()); - co.setDepartmentId(local.getDepartmentId()); - co.setDepartmentName(local.getDepartmentName()); - co.setCompanyName(local.getCorpinfoName()); - co.setCorpinfoId(local.getCorpinfoId()); - co.setCorpinfoName(local.getCorpinfoName()); - co.setCreditCode(local.getCreditCode()); - co.setPortArea(local.getPortArea()); - co.setOrgName(directText(row, "orgName", "companyName", "corpName", "enterpriseName")); - co.setOrgCode(directText(row, "orgCode", "companyCode", "corpCode", "enterpriseCode")); + co.setIdCardNo(firstText( + directText(row, "idCardNo", "identityNo", "certificateNo", "cardNo"), + directText(staff, "idCardNo", "identityNo", "certificateNo", "cardNo"))); + co.setMobileNo(firstText( + directText(row, "mobileNo", "phone", "phoneNo", "tel"), + directText(staff, "mobileNo", "phone", "phoneNo", "tel"))); + co.setDepartmentName(firstText( + directText(row, "departmentName", "deptName"), + directText(child(row, "department"), "name", "departmentName"), + directText(child(staff, "department"), "name", "departmentName"))); + co.setCompanyName(firstText( + directText(row, "companyName", "corpName", "enterpriseName"), + directText(staff, "companyName", "corpName", "enterpriseName"), + orgName)); + co.setCorpinfoId(firstText( + directText(row, "corpinfoId", "companyId", "corpId", "enterpriseId"), + directText(staff, "corpinfoId", "companyId", "corpId", "enterpriseId"))); + co.setCorpinfoName(firstText(co.getCompanyName(), orgName)); + co.setOrgName(orgName); + co.setOrgCode(orgCode); + co.setCreditCode(firstText( + directText(row, "creditCode", "socialCreditCode", "unifiedSocialCreditCode", + "unifiedCreditCode", "creditNo"), + directText(staff, "creditCode", "socialCreditCode", "unifiedSocialCreditCode", + "unifiedCreditCode", "creditNo"))); + co.setPortArea(integerValue(firstValue(row, staff, "portArea", "portAreaCode"))); co.setTerminalNo(candidate.terminalNo); - co.setPositionMode(positionMode(terminal)); + co.setPositionMode(positionMode(row)); co.setLon(decimalValue(firstValue(location, row, "lon", "lng", "longitude"))); co.setLat(decimalValue(firstValue(location, row, "lat", "latitude"))); co.setAlt(decimalValue(firstValue(location, row, "alt", "altitude"))); @@ -200,8 +190,12 @@ public class RealtimePersonLocationMatcher { co.setCurrentLocation(firstText( directText(location, "fenceName", "areaName", "positionName", "location", "address"), directText(row, "fenceName", "areaName", "positionName", "locationName", "address"))); - co.setOnline(co.getLon() != null && co.getLat() != null); - co.setLocationRawJson(row.toString()); + Boolean online = booleanValue(firstValue(location, row, "online", "isOnline")); + co.setOnline(online == null ? co.getLon() != null && co.getLat() != null : online); + co.setAlarmStatus(booleanValue(firstValue(location, row, "alarmStatus", "isAlarm"))); + co.setAlarmCount(integerValue(firstValue(location, row, "alarmCount", "unclosedAlarmCount"))); + co.setStaffRawJson(row.toString()); + co.setLocationRawJson(location.toString()); return co; } @@ -272,8 +266,11 @@ public class RealtimePersonLocationMatcher { } } - private String positionMode(PositionPersonLocalLookupService.TerminalSnapshot terminal) { - String deviceType = terminal == null ? null : terminal.getDeviceType(); + private String positionMode(JsonNode row) { + JsonNode terminal = firstObject(row, "terminalInfo", "terminal", "bindTerminal"); + String deviceType = firstText( + directText(row, "positionMode", "deviceType", "terminalType"), + directText(terminal, "positionMode", "deviceType", "terminalType", "type")); if ("CARD".equalsIgnoreCase(deviceType)) { return "\u5b9a\u4f4d\u5361"; } @@ -286,6 +283,37 @@ public class RealtimePersonLocationMatcher { return StringUtils.hasText(deviceType) ? deviceType : "\u5b9a\u4f4d\u7ec8\u7aef"; } + private Integer integerValue(JsonNode node) { + if (node == null || node.isNull()) { + return null; + } + try { + return node.isNumber() ? node.asInt() : Integer.valueOf(node.asText()); + } catch (NumberFormatException ignored) { + return null; + } + } + + private Boolean booleanValue(JsonNode node) { + if (node == null || node.isNull()) { + return null; + } + if (node.isBoolean()) { + return node.asBoolean(); + } + String value = node.asText(); + if (!StringUtils.hasText(value)) { + return null; + } + if ("1".equals(value) || "true".equalsIgnoreCase(value) || "yes".equalsIgnoreCase(value)) { + return Boolean.TRUE; + } + if ("0".equals(value) || "false".equalsIgnoreCase(value) || "no".equalsIgnoreCase(value)) { + return Boolean.FALSE; + } + return null; + } + private JsonNode child(JsonNode node, String fieldName) { return node == null || !node.isObject() ? null : node.get(fieldName); } @@ -302,11 +330,12 @@ public class RealtimePersonLocationMatcher { private static class LocationCandidate { private final JsonNode row; private final String staffNo; - private String terminalNo; + private final String terminalNo; - private LocationCandidate(JsonNode row, String staffNo) { + private LocationCandidate(JsonNode row, String staffNo, String terminalNo) { this.row = row; this.staffNo = staffNo; + this.terminalNo = terminalNo; } } } diff --git a/web-app/src/main/java/com/zcloud/personnel/positioning/integration/finds/FindsOpenApiClient.java b/web-app/src/main/java/com/zcloud/personnel/positioning/integration/finds/FindsOpenApiClient.java index 39bb3fe..2204e9a 100644 --- a/web-app/src/main/java/com/zcloud/personnel/positioning/integration/finds/FindsOpenApiClient.java +++ b/web-app/src/main/java/com/zcloud/personnel/positioning/integration/finds/FindsOpenApiClient.java @@ -6,7 +6,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.jjb.saas.framework.auth.model.SSOUser; import com.jjb.saas.framework.auth.utils.AuthContext; import com.zcloud.personnel.positioning.persistence.dataobject.OpenapiConfigDO; -import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -20,8 +23,14 @@ import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; /** * FindS OpenAPI signed client. @@ -30,12 +39,25 @@ import java.util.Map; * @Date 2026-07-07 */ @Component -@AllArgsConstructor +@RequiredArgsConstructor public class FindsOpenApiClient { + private static final Logger LOGGER = LoggerFactory.getLogger(FindsOpenApiClient.class); private static final String SIK_PATH_SEGMENT = "sik"; - private static final int CONNECT_TIMEOUT_MS = 10000; - private static final int READ_TIMEOUT_MS = 60000; - private static final int MAX_REQUEST_ATTEMPTS = 3; + + @Value("${finds.openapi.connect-timeout-ms:10000}") + private int connectTimeoutMs = 10000; + + @Value("${finds.openapi.read-timeout-ms:60000}") + private int readTimeoutMs = 60000; + + @Value("${finds.openapi.max-request-attempts:3}") + private int maxRequestAttempts = 3; + + @Value("${finds.openapi.fail-fast-connect-timeout-ms:5000}") + private int failFastConnectTimeoutMs = 5000; + + @Value("${finds.openapi.fail-fast-read-timeout-ms:15000}") + private int failFastReadTimeoutMs = 15000; private final OpenapiConfigCache openapiConfigCache; private final ObjectMapper objectMapper; @@ -45,7 +67,60 @@ public class FindsOpenApiClient { return post(config, apiName, request); } + public JsonNode postFailFast(String apiName, Map request) { + OpenapiConfigDO config = loadEnabledConfig(); + return post(config, apiName, request, + failFastConnectTimeoutMs, failFastReadTimeoutMs, 1); + } + + public List postFailFastBatch(String apiName, List> requests, + int maxConcurrency) { + if (requests == null || requests.isEmpty()) { + return Collections.emptyList(); + } + OpenapiConfigDO config = loadEnabledConfig(); + int concurrency = Math.max(1, Math.min(Math.min(maxConcurrency, 8), requests.size())); + if (concurrency == 1) { + List result = new ArrayList<>(requests.size()); + for (Map request : requests) { + result.add(post(config, apiName, request, + failFastConnectTimeoutMs, failFastReadTimeoutMs, 1)); + } + return result; + } + + ExecutorService executor = Executors.newFixedThreadPool(concurrency); + try { + List> futures = new ArrayList<>(requests.size()); + for (Map request : requests) { + futures.add(executor.submit(() -> post(config, apiName, request, + failFastConnectTimeoutMs, failFastReadTimeoutMs, 1))); + } + List result = new ArrayList<>(requests.size()); + for (Future future : futures) { + result.add(future.get()); + } + return result; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new BizException("FindS OpenAPI批量请求被中断"); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + throw new BizException("FindS OpenAPI批量请求失败:" + cause.getMessage()); + } finally { + executor.shutdownNow(); + } + } + public JsonNode post(OpenapiConfigDO config, String apiName, Map request) { + return post(config, apiName, request, connectTimeoutMs, readTimeoutMs, maxRequestAttempts); + } + + private JsonNode post(OpenapiConfigDO config, String apiName, Map request, + int connectTimeout, int readTimeout, int requestAttempts) { validateConfig(config); String requestJson = writeJson(request == null ? Collections.emptyMap() : request); String timestamp = String.valueOf(System.currentTimeMillis()); @@ -55,7 +130,8 @@ public class FindsOpenApiClient { String requestPath = buildApiPath(internalPrefix, apiName, config.getSik()); String signature = sign(signPath, requestJson, config.getSisCipherText(), timestamp); String url = trimTrailingSlash(config.getHostUrl()) + requestPath + "?_sign=" + signature; - String response = doPost(url, requestJson, timestamp); + String response = doPost(url, requestJson, timestamp, apiName, + connectTimeout, readTimeout, requestAttempts); try { return objectMapper.readTree(response); } catch (IOException e) { @@ -105,16 +181,22 @@ public class FindsOpenApiClient { } } - private String doPost(String url, String requestJson, String timestamp) { + private String doPost(String url, String requestJson, String timestamp, String apiName, + int configuredConnectTimeout, int configuredReadTimeout, + int configuredRequestAttempts) { IOException lastException = null; - for (int attempt = 1; attempt <= MAX_REQUEST_ATTEMPTS; attempt++) { + int attempts = Math.max(configuredRequestAttempts, 1); + int connectTimeout = Math.max(configuredConnectTimeout, 1); + int readTimeout = Math.max(configuredReadTimeout, 1); + for (int attempt = 1; attempt <= attempts; attempt++) { HttpURLConnection connection = null; + long startedAt = System.nanoTime(); try { byte[] body = ("request=" + URLEncoder.encode(requestJson, "UTF-8")) .getBytes(StandardCharsets.UTF_8); connection = (HttpURLConnection) new URL(url).openConnection(); - connection.setConnectTimeout(CONNECT_TIMEOUT_MS); - connection.setReadTimeout(READ_TIMEOUT_MS); + connection.setConnectTimeout(connectTimeout); + connection.setReadTimeout(readTimeout); connection.setRequestMethod("POST"); connection.setDoOutput(true); connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); @@ -129,10 +211,14 @@ public class FindsOpenApiClient { if (status != HttpURLConnection.HTTP_OK) { throw new BizException("FindS OpenAPI HTTP调用失败,status=" + status + ",body=" + response); } + LOGGER.debug("FindS OpenAPI request succeeded, api={}, attempt={}, elapsedMs={}", + apiName, attempt, elapsedMillis(startedAt)); return response; } catch (IOException e) { lastException = e; - if (attempt < MAX_REQUEST_ATTEMPTS) { + LOGGER.warn("FindS OpenAPI request failed, api={}, attempt={}/{}, elapsedMs={}, message={}", + apiName, attempt, attempts, elapsedMillis(startedAt), e.getMessage()); + if (attempt < attempts) { sleepBeforeRetry(attempt); } } finally { @@ -145,6 +231,10 @@ public class FindsOpenApiClient { ? "unknown error" : lastException.getMessage())); } + private long elapsedMillis(long startedAt) { + return (System.nanoTime() - startedAt) / 1_000_000L; + } + private void sleepBeforeRetry(int attempt) { try { Thread.sleep(500L * attempt); diff --git a/web-app/src/main/java/com/zcloud/personnel/positioning/integration/kafka/LocationKafkaProbeListener.java b/web-app/src/main/java/com/zcloud/personnel/positioning/integration/kafka/LocationKafkaProbeListener.java index e015624..73e79f9 100644 --- a/web-app/src/main/java/com/zcloud/personnel/positioning/integration/kafka/LocationKafkaProbeListener.java +++ b/web-app/src/main/java/com/zcloud/personnel/positioning/integration/kafka/LocationKafkaProbeListener.java @@ -72,18 +72,18 @@ public class LocationKafkaProbeListener { ); try { - List matchedLocations = locationMatcher.match(record.value()); + List locations = locationMatcher.match(record.value()); int deliveries = 0; - for (BiPersonLocationCO location : matchedLocations) { + for (BiPersonLocationCO location : locations) { String payload = serialize(location); for (RealtimeLocationPublisher publisher : publishers) { deliveries += publisher.publish(payload); } } acknowledgment.acknowledge(); - LOGGER.info("Kafka location message processed, topic={}, partition={}, offset={}, matched={}, " + LOGGER.info("Kafka location message processed, topic={}, partition={}, offset={}, converted={}, " + "websocketDeliveries={}", - record.topic(), record.partition(), record.offset(), matchedLocations.size(), deliveries); + record.topic(), record.partition(), record.offset(), locations.size(), deliveries); } catch (RealtimeLocationPayloadException e) { acknowledgment.acknowledge(); LOGGER.warn("Ignored invalid Kafka location message, topic={}, partition={}, offset={}, reason={}", @@ -105,7 +105,7 @@ public class LocationKafkaProbeListener { payload.remove(nullFields); return objectMapper.writeValueAsString(payload); } catch (JsonProcessingException e) { - throw new IllegalStateException("Failed to serialize matched personnel location", e); + throw new IllegalStateException("Failed to serialize FindS personnel location", e); } } diff --git a/web-infrastructure/src/main/resources/TableCreationDDL.sql b/web-infrastructure/src/main/resources/TableCreationDDL.sql index 8fd4472..d7553d2 100644 --- a/web-infrastructure/src/main/resources/TableCreationDDL.sql +++ b/web-infrastructure/src/main/resources/TableCreationDDL.sql @@ -170,6 +170,51 @@ CREATE TABLE IF NOT EXISTS `terminal_bind` ( KEY `idx_tenant_org` (`tenant_id`, `org_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='人员定位终端绑定记录'; +-- FindS staff to local platform user mapping. This is a mapping cache, not a personnel master table. +CREATE TABLE IF NOT EXISTS `personnel_match` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'primary key', + `source_system` varchar(32) NOT NULL DEFAULT 'FINDS' COMMENT 'external source system', + `source_config_code` varchar(64) NOT NULL DEFAULT 'FINDS' COMMENT 'FindS config scope', + `finds_staff_no` varchar(64) NOT NULL COMMENT 'FindS business staff/track number', + `finds_staff_no_type` varchar(32) NOT NULL DEFAULT 'TRACK_NO' COMMENT 'TRACK_NO/WORK_NO/STAFF_NO', + `finds_track_id` bigint DEFAULT NULL COMMENT 'FindS track object id', + `finds_id_card_no` varchar(64) DEFAULT NULL COMMENT 'FindS id card number', + `finds_staff_name` varchar(128) DEFAULT NULL COMMENT 'FindS staff name snapshot', + `finds_corpinfo_id` varchar(64) DEFAULT NULL COMMENT 'FindS company id snapshot', + `finds_corpinfo_name` varchar(255) DEFAULT NULL COMMENT 'FindS company name snapshot', + `finds_department_name` varchar(255) DEFAULT NULL COMMENT 'FindS department name snapshot', + `finds_terminal_no` varchar(64) DEFAULT NULL COMMENT 'FindS terminal number snapshot', + `local_user_id` bigint DEFAULT NULL COMMENT 'matched local platform user id', + `local_mobile_no` varchar(64) DEFAULT NULL COMMENT 'local platform mobile snapshot used for matching', + `local_id_card_no` varchar(64) DEFAULT NULL COMMENT 'matched local id card snapshot', + `match_status` varchar(32) NOT NULL DEFAULT 'UNMATCHED' COMMENT 'MATCHED/UNMATCHED/AMBIGUOUS/DISABLED', + `match_type` varchar(32) DEFAULT NULL COMMENT 'MOBILE/ID_CARD/MOBILE_AND_ID_CARD', + `last_seen_time` datetime DEFAULT NULL COMMENT 'last time observed from FindS', + `last_sync_time` datetime DEFAULT NULL COMMENT 'last mapping synchronization time', + `sync_status` varchar(32) NOT NULL DEFAULT 'SUCCESS' COMMENT 'SUCCESS/FAIL', + `sync_error` varchar(1000) DEFAULT NULL COMMENT 'last synchronization error', + `delete_enum` varchar(32) NOT NULL DEFAULT 'FALSE' COMMENT 'soft delete flag', + `remarks` varchar(500) DEFAULT NULL COMMENT 'remarks', + `tenant_id` bigint DEFAULT NULL COMMENT 'tenant id', + `org_id` bigint DEFAULT NULL COMMENT 'organization id', + `env` varchar(32) DEFAULT NULL COMMENT 'environment', + `version` int NOT NULL DEFAULT 0 COMMENT 'version', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT 'created time', + `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'updated time', + `create_id` bigint DEFAULT NULL COMMENT 'creator id', + `update_id` bigint DEFAULT NULL COMMENT 'updater id', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_personnel_match_source_staff_delete` (`source_config_code`, `finds_staff_no`, `delete_enum`), + KEY `idx_personnel_match_local_user` (`local_user_id`, `match_status`), + KEY `idx_personnel_match_local_mobile` (`local_mobile_no`), + KEY `idx_personnel_match_find_id_card` (`finds_id_card_no`), + KEY `idx_personnel_match_terminal_no` (`finds_terminal_no`), + KEY `idx_personnel_match_local_id_card` (`local_id_card_no`), + KEY `idx_personnel_match_scope_status` (`finds_corpinfo_id`, `match_status`), + KEY `idx_personnel_match_last_seen` (`last_seen_time`), + KEY `idx_personnel_match_tenant_org` (`tenant_id`, `org_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='FindS to local personnel mapping'; + CREATE TABLE IF NOT EXISTS `fence` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `fence_code` varchar(64) NOT NULL COMMENT '围栏编码', diff --git a/web-infrastructure/src/main/resources/db/migration/mysql/V20260723_01__personnel_match_mapping.sql b/web-infrastructure/src/main/resources/db/migration/mysql/V20260723_01__personnel_match_mapping.sql new file mode 100644 index 0000000..41d9d4e --- /dev/null +++ b/web-infrastructure/src/main/resources/db/migration/mysql/V20260723_01__personnel_match_mapping.sql @@ -0,0 +1,45 @@ +-- Cache the cross-system personnel mapping used by the local-first personnel list. +-- The table stores identity/mapping snapshots only. Realtime coordinates remain in FindS/Redis. +CREATE TABLE IF NOT EXISTS `personnel_match` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'primary key', + `source_system` varchar(32) NOT NULL DEFAULT 'FINDS' COMMENT 'external source system', + `source_config_code` varchar(64) NOT NULL DEFAULT 'FINDS' COMMENT 'FindS config scope', + `finds_staff_no` varchar(64) NOT NULL COMMENT 'FindS business staff/track number', + `finds_staff_no_type` varchar(32) NOT NULL DEFAULT 'TRACK_NO' COMMENT 'TRACK_NO/WORK_NO/STAFF_NO', + `finds_track_id` bigint DEFAULT NULL COMMENT 'FindS track object id', + `finds_id_card_no` varchar(64) DEFAULT NULL COMMENT 'FindS id card number', + `finds_staff_name` varchar(128) DEFAULT NULL COMMENT 'FindS staff name snapshot', + `finds_corpinfo_id` varchar(64) DEFAULT NULL COMMENT 'FindS company id snapshot', + `finds_corpinfo_name` varchar(255) DEFAULT NULL COMMENT 'FindS company name snapshot', + `finds_department_name` varchar(255) DEFAULT NULL COMMENT 'FindS department name snapshot', + `finds_terminal_no` varchar(64) DEFAULT NULL COMMENT 'FindS terminal number snapshot', + `local_user_id` bigint DEFAULT NULL COMMENT 'matched local platform user id', + `local_mobile_no` varchar(64) DEFAULT NULL COMMENT 'local platform mobile snapshot used for matching', + `local_id_card_no` varchar(64) DEFAULT NULL COMMENT 'matched local id card snapshot', + `match_status` varchar(32) NOT NULL DEFAULT 'UNMATCHED' COMMENT 'MATCHED/UNMATCHED/AMBIGUOUS/DISABLED', + `match_type` varchar(32) DEFAULT NULL COMMENT 'MOBILE/ID_CARD/MOBILE_AND_ID_CARD', + `last_seen_time` datetime DEFAULT NULL COMMENT 'last time observed from FindS', + `last_sync_time` datetime DEFAULT NULL COMMENT 'last mapping synchronization time', + `sync_status` varchar(32) NOT NULL DEFAULT 'SUCCESS' COMMENT 'SUCCESS/FAIL', + `sync_error` varchar(1000) DEFAULT NULL COMMENT 'last synchronization error', + `delete_enum` varchar(32) NOT NULL DEFAULT 'FALSE' COMMENT 'soft delete flag', + `remarks` varchar(500) DEFAULT NULL COMMENT 'remarks', + `tenant_id` bigint DEFAULT NULL COMMENT 'tenant id', + `org_id` bigint DEFAULT NULL COMMENT 'organization id', + `env` varchar(32) DEFAULT NULL COMMENT 'environment', + `version` int NOT NULL DEFAULT 0 COMMENT 'version', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT 'created time', + `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'updated time', + `create_id` bigint DEFAULT NULL COMMENT 'creator id', + `update_id` bigint DEFAULT NULL COMMENT 'updater id', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_personnel_match_source_staff_delete` (`source_config_code`, `finds_staff_no`, `delete_enum`), + KEY `idx_personnel_match_local_user` (`local_user_id`, `match_status`), + KEY `idx_personnel_match_local_mobile` (`local_mobile_no`), + KEY `idx_personnel_match_find_id_card` (`finds_id_card_no`), + KEY `idx_personnel_match_terminal_no` (`finds_terminal_no`), + KEY `idx_personnel_match_local_id_card` (`local_id_card_no`), + KEY `idx_personnel_match_scope_status` (`finds_corpinfo_id`, `match_status`), + KEY `idx_personnel_match_last_seen` (`last_seen_time`), + KEY `idx_personnel_match_tenant_org` (`tenant_id`, `org_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='FindS to local personnel mapping';