调整实时定位推送为直接转换WebSocket消息
parent
1c630fa960
commit
7bfab8159f
|
|
@ -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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
*
|
||||
* <p>The cache contains identity and terminal snapshots only. Location coordinates are deliberately
|
||||
* not persisted here and continue to come from FindS at query time.</p>
|
||||
*/
|
||||
@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<StaffSnapshot> staffRows = loadAllStaff();
|
||||
Set<String> staffNos = new LinkedHashSet<>();
|
||||
for (StaffSnapshot row : staffRows) {
|
||||
staffNos.add(row.staffNo);
|
||||
}
|
||||
Map<String, PositionPersonLocalLookupService.LocalPerson> 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<StaffSnapshot> loadAllStaff() {
|
||||
List<StaffSnapshot> 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<Map<String, Object>> 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<String, Object> pageRequest(int pageNo) {
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("pageNo", pageNo);
|
||||
request.put("pageSize", PAGE_SIZE);
|
||||
return request;
|
||||
}
|
||||
|
||||
private int appendRows(List<StaffSnapshot> 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<StaffSnapshot> rows,
|
||||
Map<String, PositionPersonLocalLookupService.LocalPerson> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<PersonMapping> 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<Object> 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<String, LocalPerson> findUniquePeopleByPhones(Set<String> phones) {
|
||||
Map<String, List<LocalPerson>> grouped = new LinkedHashMap<>();
|
||||
for (List<String> batch : batches(phones)) {
|
||||
|
|
@ -110,6 +196,34 @@ class PositionPersonLocalLookupService {
|
|||
return unique;
|
||||
}
|
||||
|
||||
Map<String, String> findTerminalNosByIdCardNos(Set<String> idCardNos) {
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
for (List<String> 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<String, TerminalSnapshot> findTerminals(Set<String> terminalNos) {
|
||||
Map<String, TerminalSnapshot> result = new LinkedHashMap<>();
|
||||
for (List<String> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<PositionPersonCO> list(PositionPersonPageQry qry) {
|
||||
PositionPersonPageQry query = qry == null ? new PositionPersonPageQry() : qry;
|
||||
List<FindsStaff> findsStaff = loadAllFindsStaff();
|
||||
Set<String> phones = new LinkedHashSet<>();
|
||||
for (FindsStaff staff : findsStaff) {
|
||||
if (StringUtils.hasText(staff.staffNo)) {
|
||||
phones.add(staff.staffNo);
|
||||
List<MatchedPerson> matched = loadMatchedPeople(query);
|
||||
Set<String> idCardNos = new LinkedHashSet<>();
|
||||
for (MatchedPerson person : matched) {
|
||||
if (StringUtils.hasText(person.local.getIdCardNo())) {
|
||||
idCardNos.add(person.local.getIdCardNo());
|
||||
}
|
||||
}
|
||||
Map<String, PositionPersonLocalLookupService.LocalPerson> localPeople =
|
||||
localLookupService.findUniquePeopleByPhones(phones);
|
||||
|
||||
List<MatchedPerson> matched = new ArrayList<>();
|
||||
Map<String, String> localTerminalNos = localLookupService.findTerminalNosByIdCardNos(idCardNos);
|
||||
if (localTerminalNos == null) {
|
||||
localTerminalNos = Collections.emptyMap();
|
||||
}
|
||||
Set<String> terminalNos = new LinkedHashSet<>();
|
||||
Set<Long> 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<String, JsonNode> currentLocations = locateTerminals(terminalNos);
|
||||
|
|
@ -102,6 +94,54 @@ public class PositionPersonQueryExe {
|
|||
return PageResponse.of(rows.subList(fromIndex, toIndex), rows.size(), pageSize, pageIndex);
|
||||
}
|
||||
|
||||
private List<MatchedPerson> loadMatchedPeople(PositionPersonPageQry query) {
|
||||
if (localLookupService.hasPersonnelMappings()) {
|
||||
return loadPersistedMatches(query);
|
||||
}
|
||||
List<FindsStaff> findsStaff = loadAllFindsStaff();
|
||||
Set<String> phones = new LinkedHashSet<>();
|
||||
for (FindsStaff staff : findsStaff) {
|
||||
if (StringUtils.hasText(staff.staffNo)) {
|
||||
phones.add(staff.staffNo);
|
||||
}
|
||||
}
|
||||
Map<String, PositionPersonLocalLookupService.LocalPerson> localPeople =
|
||||
localLookupService.findUniquePeopleByPhones(phones);
|
||||
|
||||
List<MatchedPerson> matched = new ArrayList<>();
|
||||
Set<Long> 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<MatchedPerson> loadPersistedMatches(PositionPersonPageQry query) {
|
||||
List<MatchedPerson> result = new ArrayList<>();
|
||||
Set<Long> 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<FindsStaff> loadAllFindsStaff() {
|
||||
List<FindsStaff> result = new ArrayList<>();
|
||||
long total = Long.MAX_VALUE;
|
||||
int loaded = 0;
|
||||
for (int pageNo = 1; pageNo <= MAX_FINDS_PAGE_COUNT && loaded < total; pageNo++) {
|
||||
Map<String, Object> 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<Map<String, Object>> requests = new ArrayList<>(pageCount - 1);
|
||||
for (int pageNo = 2; pageNo <= pageCount; pageNo++) {
|
||||
requests.add(staffPageRequest(pageNo));
|
||||
}
|
||||
List<JsonNode> 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<String, Object> staffPageRequest(int pageNo) {
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("pageNo", pageNo);
|
||||
request.put("pageSize", FINDS_PAGE_SIZE);
|
||||
return request;
|
||||
}
|
||||
|
||||
private int appendFindsStaff(List<FindsStaff> 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<String, Object> 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<String, JsonNode> locateTerminals(Set<String> terminalNos) {
|
||||
if (terminalNos == null || terminalNos.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
|
|
@ -202,7 +248,7 @@ public class PositionPersonQueryExe {
|
|||
|
||||
private void locateTerminalBatch(List<String> terminalNos, Map<String, JsonNode> result) {
|
||||
Map<String, Object> 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()) {
|
||||
|
|
|
|||
|
|
@ -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<BiPersonLocationCO> match(byte[] payload) {
|
||||
JsonNode root = parse(payload);
|
||||
List<LocationCandidate> candidates = new ArrayList<>();
|
||||
Set<String> 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<String, PositionPersonLocalLookupService.LocalPerson> localPeople =
|
||||
localLookupService.findUniquePeopleByPhones(staffNos);
|
||||
if (localPeople.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Set<String> 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<String, PositionPersonLocalLookupService.TerminalSnapshot> terminals =
|
||||
localLookupService.findTerminals(terminalNos);
|
||||
|
||||
List<BiPersonLocationCO> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, Object> request) {
|
||||
OpenapiConfigDO config = loadEnabledConfig();
|
||||
return post(config, apiName, request,
|
||||
failFastConnectTimeoutMs, failFastReadTimeoutMs, 1);
|
||||
}
|
||||
|
||||
public List<JsonNode> postFailFastBatch(String apiName, List<Map<String, Object>> 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<JsonNode> result = new ArrayList<>(requests.size());
|
||||
for (Map<String, Object> request : requests) {
|
||||
result.add(post(config, apiName, request,
|
||||
failFastConnectTimeoutMs, failFastReadTimeoutMs, 1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ExecutorService executor = Executors.newFixedThreadPool(concurrency);
|
||||
try {
|
||||
List<Future<JsonNode>> futures = new ArrayList<>(requests.size());
|
||||
for (Map<String, Object> request : requests) {
|
||||
futures.add(executor.submit(() -> post(config, apiName, request,
|
||||
failFastConnectTimeoutMs, failFastReadTimeoutMs, 1)));
|
||||
}
|
||||
List<JsonNode> result = new ArrayList<>(requests.size());
|
||||
for (Future<JsonNode> 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<String, Object> request) {
|
||||
return post(config, apiName, request, connectTimeoutMs, readTimeoutMs, maxRequestAttempts);
|
||||
}
|
||||
|
||||
private JsonNode post(OpenapiConfigDO config, String apiName, Map<String, Object> 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);
|
||||
|
|
|
|||
|
|
@ -72,18 +72,18 @@ public class LocationKafkaProbeListener {
|
|||
);
|
||||
|
||||
try {
|
||||
List<BiPersonLocationCO> matchedLocations = locationMatcher.match(record.value());
|
||||
List<BiPersonLocationCO> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 '围栏编码',
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
Loading…
Reference in New Issue