diff --git a/start/src/main/java/com/zcloud/personnel/positioning/config/FlywayRecoveryConfiguration.java b/start/src/main/java/com/zcloud/personnel/positioning/config/FlywayRecoveryConfiguration.java index 6d55cf1..ca9ffbe 100644 --- a/start/src/main/java/com/zcloud/personnel/positioning/config/FlywayRecoveryConfiguration.java +++ b/start/src/main/java/com/zcloud/personnel/positioning/config/FlywayRecoveryConfiguration.java @@ -13,10 +13,11 @@ import java.util.List; import java.util.stream.Collectors; /** - * Recovers the known partial MySQL migration left by V20260710_02. + * Recovers known partial or removed MySQL migrations. * - *

The migration is idempotent and can be safely rerun after Flyway removes - * its failed history entry. Other failed migrations are never repaired + *

V20260710_02 is idempotent and can be safely rerun after Flyway removes + * its failed history entry. V20260722_01 was removed and only its failed + * history entry needs cleanup. Other failed migrations are never repaired * automatically so that new database problems still stop application startup.

*/ @Configuration @@ -24,6 +25,7 @@ public class FlywayRecoveryConfiguration { private static final Logger LOGGER = LoggerFactory.getLogger(FlywayRecoveryConfiguration.class); private static final MigrationVersion RECOVERABLE_VERSION = MigrationVersion.fromVersion("20260710.02"); + private static final MigrationVersion REMOVED_VERSION = MigrationVersion.fromVersion("20260722.01"); @Bean public FlywayMigrationStrategy flywayMigrationStrategy() { @@ -34,7 +36,7 @@ public class FlywayRecoveryConfiguration { if (!failedMigrations.isEmpty()) { boolean onlyKnownFailure = failedMigrations.size() == 1 - && RECOVERABLE_VERSION.equals(failedMigrations.get(0).getVersion()); + && isRecoverable(failedMigrations.get(0).getVersion()); if (!onlyKnownFailure) { String failedVersions = failedMigrations.stream() .map(migration -> migration.getVersion() == null @@ -45,11 +47,16 @@ public class FlywayRecoveryConfiguration { "Flyway contains unsupported failed migrations: " + failedVersions); } - LOGGER.warn("Repairing known failed Flyway migration {} before retry", RECOVERABLE_VERSION); + LOGGER.warn("Repairing known failed Flyway migration {} before retry", + failedMigrations.get(0).getVersion()); flyway.repair(); } flyway.migrate(); }; } + + private boolean isRecoverable(MigrationVersion version) { + return RECOVERABLE_VERSION.equals(version) || REMOVED_VERSION.equals(version); + } } diff --git a/start/src/main/resources/bootstrap.yml b/start/src/main/resources/bootstrap.yml index 272b3e7..dfff433 100644 --- a/start/src/main/resources/bootstrap.yml +++ b/start/src/main/resources/bootstrap.yml @@ -1,8 +1,8 @@ spring: config: import: - - classpath:nacos-prod.yml -# - classpath:nacos.yml +# - classpath:nacos-prod.yml + - classpath:nacos.yml - classpath:kafka.yml - classpath:sdk.yml - classpath:swagger.yml diff --git a/start/src/main/resources/kafka.yml b/start/src/main/resources/kafka.yml index fc17624..754983e 100644 --- a/start/src/main/resources/kafka.yml +++ b/start/src/main/resources/kafka.yml @@ -30,3 +30,8 @@ personnel-positioning: allowed-origins: ${PERSONNEL_POSITIONING_WEBSOCKET_LOCATION_ALLOWED_ORIGINS:*} send-time-limit-ms: ${PERSONNEL_POSITIONING_WEBSOCKET_LOCATION_SEND_TIME_LIMIT_MS:10000} buffer-size-limit-bytes: ${PERSONNEL_POSITIONING_WEBSOCKET_LOCATION_BUFFER_SIZE_LIMIT_BYTES:524288} + mock: + enabled: ${PERSONNEL_POSITIONING_WEBSOCKET_LOCATION_MOCK_ENABLED:true} + initial-delay-ms: ${PERSONNEL_POSITIONING_WEBSOCKET_LOCATION_MOCK_INITIAL_DELAY_MS:1000} + interval-ms: ${PERSONNEL_POSITIONING_WEBSOCKET_LOCATION_MOCK_INTERVAL_MS:1000} + new-person-delay-ms: ${PERSONNEL_POSITIONING_WEBSOCKET_LOCATION_MOCK_NEW_PERSON_DELAY_MS:8000} diff --git a/start/src/main/resources/sdk.yml b/start/src/main/resources/sdk.yml index 74a32b1..8b7abd3 100644 --- a/start/src/main/resources/sdk.yml +++ b/start/src/main/resources/sdk.yml @@ -1,5 +1,6 @@ sdk: server: +# app-key: 4e11af8a2b224f6fb1c680df53c72457 app-key: 9f32c2300c4444a6a2ab8f48a15802a0 client: gateway: diff --git a/web-adapter/src/main/java/com/zcloud/personnel/positioning/websocket/MockPersonLocationPublisher.java b/web-adapter/src/main/java/com/zcloud/personnel/positioning/websocket/MockPersonLocationPublisher.java new file mode 100644 index 0000000..8a21744 --- /dev/null +++ b/web-adapter/src/main/java/com/zcloud/personnel/positioning/websocket/MockPersonLocationPublisher.java @@ -0,0 +1,190 @@ +package com.zcloud.personnel.positioning.websocket; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.zcloud.personnel.positioning.dto.clientobject.BiPersonLocationCO; +import com.zcloud.personnel.positioning.integration.realtime.RealtimeLocationPublisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + +import javax.annotation.PostConstruct; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicLongArray; + +/** + * 前端联调用实时点位模拟器,仅在配置明确开启时生效。 + */ +@Configuration(proxyBeanMethods = false) +@EnableScheduling +@ConditionalOnProperty( + prefix = "personnel-positioning.websocket.location.mock", + name = "enabled", + havingValue = "true" +) +public class MockPersonLocationPublisher { + private static final Logger LOGGER = LoggerFactory.getLogger(MockPersonLocationPublisher.class); + private static final BigDecimal STEP = new BigDecimal("0.000030"); + private static final int TRACK_POINT_COUNT = 40; + + private final ObjectMapper objectMapper; + private final ObjectProvider publishers; + private final AtomicLong sequence = new AtomicLong(); + private final AtomicLongArray trackSequences = new AtomicLongArray(3); + private final AtomicBoolean thirdPersonActivated = new AtomicBoolean(); + private final long newPersonDelayMs; + private final long startedAtMs; + + public MockPersonLocationPublisher(ObjectMapper objectMapper, + ObjectProvider publishers, + @Value("${personnel-positioning.websocket.location.mock.new-person-delay-ms:8000}") + long newPersonDelayMs) { + this.objectMapper = objectMapper; + this.publishers = publishers; + this.newPersonDelayMs = Math.max(0, newPersonDelayMs); + this.startedAtMs = System.currentTimeMillis(); + } + + @PostConstruct + public void logEnabled() { + LOGGER.warn("人员实时点位WebSocket模拟数据已开启,仅用于前端联调,新增人员延迟={}ms", + newPersonDelayMs); + } + + @Scheduled( + initialDelayString = "${personnel-positioning.websocket.location.mock.initial-delay-ms:1000}", + fixedRateString = "${personnel-positioning.websocket.location.mock.interval-ms:1000}" + ) + public void publishNextLocation() { + int personIndex = nextPersonIndex(); + long trackIndex = trackSequences.getAndIncrement(personIndex) % TRACK_POINT_COUNT; + BiPersonLocationCO location = createPerson(personIndex, trackIndex); + + String payload = serialize(location); + int deliveries = 0; + for (RealtimeLocationPublisher publisher : publishers) { + deliveries += publisher.publish(payload); + } + LOGGER.debug("模拟人员点位已推送,staffNo={}, terminalNo={}, websocketDeliveries={}", + location.getStaffNo(), location.getTerminalNo(), deliveries); + } + + private int nextPersonIndex() { + if (!thirdPersonActivated.get() + && System.currentTimeMillis() - startedAtMs >= newPersonDelayMs + && thirdPersonActivated.compareAndSet(false, true)) { + LOGGER.info("模拟新增人员开始推送,staffNo=CODEX-STAFF-003, terminalNo=CODEX-TERMINAL-003"); + return 2; + } + int personCount = thirdPersonActivated.get() ? 3 : 2; + return (int) (sequence.getAndIncrement() % personCount); + } + + private BiPersonLocationCO createPerson(int personIndex, long trackIndex) { + if (personIndex == 0) { + return createEastPortPerson(trackIndex); + } + if (personIndex == 1) { + return createWestPortPerson(trackIndex); + } + return createHarborPerson(trackIndex); + } + + private BiPersonLocationCO createEastPortPerson(long trackIndex) { + BigDecimal offset = STEP.multiply(BigDecimal.valueOf(trackIndex)); + BiPersonLocationCO location = baseLocation( + "Codex测试人员A", "CODEX-STAFF-001", "CODEX-TERMINAL-001"); + location.setCompanyName("Codex测试东港公司"); + location.setCorpinfoId("CODEX-CORP-EAST"); + location.setCorpinfoName("Codex测试东港公司"); + location.setOrgCode("CODEX-ORG-EAST"); + location.setOrgName("Codex测试东港组织"); + location.setPortArea(2); + location.setCurrentLocation("东港区模拟路线"); + location.setLon(new BigDecimal("119.604000").add(offset)); + location.setLat(new BigDecimal("39.925000").add(offset.divide(BigDecimal.valueOf(2)))); + location.setDirection(new BigDecimal("45")); + return location; + } + + private BiPersonLocationCO createWestPortPerson(long trackIndex) { + BigDecimal offset = STEP.multiply(BigDecimal.valueOf(trackIndex)); + BiPersonLocationCO location = baseLocation( + "Codex测试人员B", "CODEX-STAFF-002", "CODEX-TERMINAL-002"); + location.setCompanyName("Codex测试西港公司"); + location.setCorpinfoId("CODEX-CORP-WEST"); + location.setCorpinfoName("Codex测试西港公司"); + location.setOrgCode("CODEX-ORG-WEST"); + location.setOrgName("Codex测试西港组织"); + location.setPortArea(3); + location.setCurrentLocation("西港区模拟路线"); + location.setLon(new BigDecimal("119.552000").subtract(offset)); + location.setLat(new BigDecimal("39.913000").add(offset.divide(BigDecimal.valueOf(3)))); + location.setDirection(new BigDecimal("315")); + return location; + } + + private BiPersonLocationCO createHarborPerson(long trackIndex) { + BigDecimal offset = STEP.multiply(BigDecimal.valueOf(trackIndex)); + BiPersonLocationCO location = baseLocation( + "Codex测试新增人员C", "CODEX-STAFF-003", "CODEX-TERMINAL-003"); + location.setCompanyName("Codex测试港区公司"); + location.setCorpinfoId("CODEX-CORP-HARBOR"); + location.setCorpinfoName("Codex测试港区公司"); + location.setOrgCode("CODEX-ORG-HARBOR"); + location.setOrgName("Codex测试港区组织"); + location.setPortArea(1); + location.setCurrentLocation("秦皇岛港区模拟路线"); + location.setLon(new BigDecimal("119.580000").add(offset)); + location.setLat(new BigDecimal("39.920000").subtract(offset.divide(BigDecimal.valueOf(2)))); + location.setDirection(new BigDecimal("135")); + return location; + } + + private BiPersonLocationCO baseLocation(String staffName, String staffNo, String terminalNo) { + BiPersonLocationCO location = new BiPersonLocationCO(); + location.setStaffName(staffName); + location.setLocalStaffNo(staffNo); + location.setStaffNo(staffNo); + location.setTerminalNo(terminalNo); + location.setPositionMode("模拟定位卡"); + location.setLastLocationTime(System.currentTimeMillis()); + location.setAlt(new BigDecimal("5.0")); + location.setSpeed(new BigDecimal("1.2")); + location.setOnline(true); + location.setAlarmStatus(false); + location.setAlarmCount(0); + return location; + } + + private String serialize(BiPersonLocationCO location) { + try { + ObjectNode payload = objectMapper.valueToTree(location); + List nullFields = new ArrayList<>(); + Iterator> fields = payload.fields(); + while (fields.hasNext()) { + Map.Entry field = fields.next(); + if (field.getValue().isNull()) { + nullFields.add(field.getKey()); + } + } + payload.remove(nullFields); + return objectMapper.writeValueAsString(payload); + } catch (JsonProcessingException e) { + throw new IllegalStateException("模拟人员点位序列化失败", e); + } + } +} diff --git a/web-app/src/main/java/com/zcloud/personnel/positioning/command/query/AlarmRuleQueryExe.java b/web-app/src/main/java/com/zcloud/personnel/positioning/command/query/AlarmRuleQueryExe.java index 4f48053..c525f02 100644 --- a/web-app/src/main/java/com/zcloud/personnel/positioning/command/query/AlarmRuleQueryExe.java +++ b/web-app/src/main/java/com/zcloud/personnel/positioning/command/query/AlarmRuleQueryExe.java @@ -44,6 +44,12 @@ import java.util.stream.Collectors; @Component @AllArgsConstructor public class AlarmRuleQueryExe { + private static final String COMPANY_GROUP_UNMAPPED = "UNMAPPED"; + private static final String COMPANY_GROUP_UNMAPPED_NAME = "未归属企业"; + private static final String COMPANY_GROUP_ID_PREFIX = "ID:"; + private static final String COMPANY_GROUP_ORG_PREFIX = "ORG:"; + private static final String COMPANY_GROUP_NAME_PREFIX = "NAME:"; + private final AlarmRuleRepository alarmRuleRepository; private final AlarmRuleFenceRelRepository alarmRuleFenceRelRepository; private final AlarmRuleItemRepository alarmRuleItemRepository; @@ -58,6 +64,7 @@ public class AlarmRuleQueryExe { if (StringUtils.hasText(ruleStatus)) { params.put("eqRuleStatus", ruleStatus); } + normalizeCompanyGroupParams(params); PageResponse pageResponse = alarmRuleRepository.listCompanyStats(params); List data = alarmRuleCoConvertor .converCompanyStatDOsToCOs(pageResponse.getData()); @@ -83,7 +90,7 @@ public class AlarmRuleQueryExe { params.put("eqDeleteEnum", "FALSE"); normalizeListParams(params); fillCorpinfo(params); - preferCorpinfoId(params); + normalizeCompanyGroupParams(params); PageResponse pageResponse = alarmRuleRepository.listPage(params); List data = alarmRuleCoConvertor.converDOsToCOs(pageResponse.getData()); fillFenceRelations(data); @@ -170,14 +177,46 @@ public class AlarmRuleQueryExe { } } - private void preferCorpinfoId(Map params) { - String corpinfoId = (String) params.get("eqCorpinfoId"); - if (!StringUtils.hasText(corpinfoId)) { + private void normalizeCompanyGroupParams(Map params) { + String corpinfoId = textValue(params.get("eqCorpinfoId")); + String corpinfoName = textValue(params.get("eqCorpinfoName")); + String likeCorpinfoName = textValue(params.get("likeCorpinfoName")); + if (StringUtils.hasText(likeCorpinfoName) + && COMPANY_GROUP_UNMAPPED_NAME.contains(likeCorpinfoName)) { + params.put("includeUnmappedCompany", Boolean.TRUE); + } + if (COMPANY_GROUP_UNMAPPED.equalsIgnoreCase(corpinfoId) + || COMPANY_GROUP_UNMAPPED_NAME.equals(corpinfoName) + || ("0".equals(corpinfoId) && !StringUtils.hasText(corpinfoName))) { + params.remove("eqCorpinfoId"); + params.remove("eqCorpinfoName"); + params.put("unmappedCompany", Boolean.TRUE); return; } + if (StringUtils.hasText(corpinfoId) && corpinfoId.startsWith(COMPANY_GROUP_ID_PREFIX)) { + corpinfoId = corpinfoId.substring(COMPANY_GROUP_ID_PREFIX.length()); + } else if (StringUtils.hasText(corpinfoId) && corpinfoId.startsWith(COMPANY_GROUP_ORG_PREFIX)) { + params.remove("eqCorpinfoId"); + params.remove("eqCorpinfoName"); + params.put("companyGroupOrgCode", corpinfoId.substring(COMPANY_GROUP_ORG_PREFIX.length())); + return; + } else if (StringUtils.hasText(corpinfoId) && corpinfoId.startsWith(COMPANY_GROUP_NAME_PREFIX)) { + params.remove("eqCorpinfoId"); + params.put("eqCorpinfoName", corpinfoId.substring(COMPANY_GROUP_NAME_PREFIX.length())); + return; + } + if (!StringUtils.hasText(corpinfoId) || "0".equals(corpinfoId)) { + params.remove("eqCorpinfoId"); + return; + } + params.put("eqCorpinfoId", corpinfoId); params.remove("eqCorpinfoName"); } + private String textValue(Object value) { + return value == null ? null : String.valueOf(value).trim(); + } + private void applyCorpinfo(QueryWrapper queryWrapper) { SSOUser ssoUser = AuthContext.getCurrentUser(); if (ssoUser != null && ssoUser.getCompanyId() != null) { diff --git a/web-infrastructure/src/main/java/com/zcloud/personnel/positioning/persistence/repository/impl/AlarmRuleRepositoryImpl.java b/web-infrastructure/src/main/java/com/zcloud/personnel/positioning/persistence/repository/impl/AlarmRuleRepositoryImpl.java index bc4ea48..9c4727d 100644 --- a/web-infrastructure/src/main/java/com/zcloud/personnel/positioning/persistence/repository/impl/AlarmRuleRepositoryImpl.java +++ b/web-infrastructure/src/main/java/com/zcloud/personnel/positioning/persistence/repository/impl/AlarmRuleRepositoryImpl.java @@ -15,8 +15,9 @@ import com.jjb.saas.framework.repository.repo.impl.BaseRepositoryImpl; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import java.util.Map; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * web-infrastructure @@ -32,8 +33,19 @@ public class AlarmRuleRepositoryImpl extends BaseRepositoryImpl listPage(Map params) { IPage iPage = new Query().getPage(params); + Map queryParams = new HashMap<>(params); + Object companyGroupOrgCode = queryParams.remove("companyGroupOrgCode"); + boolean unmappedCompany = Boolean.TRUE.equals(queryParams.remove("unmappedCompany")); QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper = PageQueryHelper.createPageQueryWrapper(queryWrapper, params); + queryWrapper = PageQueryHelper.createPageQueryWrapper(queryWrapper, queryParams); + if (companyGroupOrgCode != null) { + queryWrapper.eq("org_code", companyGroupOrgCode); + } + if (unmappedCompany) { + queryWrapper.and(wrapper -> wrapper.isNull("corpinfo_name") + .or().eq("corpinfo_name", "") + .or().apply("TRIM(corpinfo_name) = ''")); + } queryWrapper.orderByDesc("create_time"); IPage result = alarmRuleMapper.selectPage(iPage, queryWrapper); return PageHelper.pageToResponse(result, result.getRecords()); diff --git a/web-infrastructure/src/main/resources/db/migration/mysql/V20260722_01__corp_info_port_area.sql b/web-infrastructure/src/main/resources/db/migration/mysql/V20260722_01__corp_info_port_area.sql deleted file mode 100644 index 6f40549..0000000 --- a/web-infrastructure/src/main/resources/db/migration/mysql/V20260722_01__corp_info_port_area.sql +++ /dev/null @@ -1,5 +0,0 @@ --- Refresh the proxy view after basic-info.corp_info adds port_area. -CREATE OR REPLACE VIEW `corp_info` AS -SELECT * -FROM `jjb-saas-zcloud-basic-info`.`corp_info` -WHERE `delete_enum` = 'FALSE'; diff --git a/web-infrastructure/src/main/resources/mapper/AlarmRuleMapper.xml b/web-infrastructure/src/main/resources/mapper/AlarmRuleMapper.xml index e980ca7..b4b64a7 100644 --- a/web-infrastructure/src/main/resources/mapper/AlarmRuleMapper.xml +++ b/web-infrastructure/src/main/resources/mapper/AlarmRuleMapper.xml @@ -5,35 +5,65 @@