完善告警企业分组及定位联调配置

修复告警规则企业分组与未归属企业查询,补充Flyway迁移恢复和WebSocket模拟定位配置。
dev
shenzhidan 2026-07-27 18:17:52 +08:00
parent 4bf82db05a
commit 4cada74fd3
9 changed files with 323 additions and 44 deletions

View File

@ -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.
*
* <p>The migration is idempotent and can be safely rerun after Flyway removes
* its failed history entry. Other failed migrations are never repaired
* <p>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.</p>
*/
@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);
}
}

View File

@ -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

View File

@ -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}

View File

@ -1,5 +1,6 @@
sdk:
server:
# app-key: 4e11af8a2b224f6fb1c680df53c72457
app-key: 9f32c2300c4444a6a2ab8f48a15802a0
client:
gateway:

View File

@ -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<RealtimeLocationPublisher> 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<RealtimeLocationPublisher> 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<String> nullFields = new ArrayList<>();
Iterator<Map.Entry<String, JsonNode>> fields = payload.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> 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);
}
}
}

View File

@ -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<AlarmRuleCompanyStatDO> pageResponse = alarmRuleRepository.listCompanyStats(params);
List<AlarmRuleCompanyStatCO> 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<AlarmRuleDO> pageResponse = alarmRuleRepository.listPage(params);
List<AlarmRuleCO> data = alarmRuleCoConvertor.converDOsToCOs(pageResponse.getData());
fillFenceRelations(data);
@ -170,12 +177,44 @@ public class AlarmRuleQueryExe {
}
}
private void preferCorpinfoId(Map<String, Object> params) {
String corpinfoId = (String) params.get("eqCorpinfoId");
if (!StringUtils.hasText(corpinfoId)) {
private void normalizeCompanyGroupParams(Map<String, Object> 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<AlarmRuleDO> queryWrapper) {

View File

@ -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<AlarmRuleMapper,
@Override
public PageResponse<AlarmRuleDO> listPage(Map<String, Object> params) {
IPage<AlarmRuleDO> iPage = new Query<AlarmRuleDO>().getPage(params);
Map<String, Object> queryParams = new HashMap<>(params);
Object companyGroupOrgCode = queryParams.remove("companyGroupOrgCode");
boolean unmappedCompany = Boolean.TRUE.equals(queryParams.remove("unmappedCompany"));
QueryWrapper<AlarmRuleDO> 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<AlarmRuleDO> result = alarmRuleMapper.selectPage(iPage, queryWrapper);
return PageHelper.pageToResponse(result, result.getRecords());

View File

@ -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';

View File

@ -5,35 +5,65 @@
<mapper namespace="com.zcloud.personnel.positioning.persistence.mapper.AlarmRuleMapper">
<select id="listCompanyStats"
resultType="com.zcloud.personnel.positioning.persistence.dataobject.AlarmRuleCompanyStatDO">
SELECT corpinfo_id,
corpinfo_name,
SELECT company_key AS corpinfo_id,
CASE WHEN company_key = 'UNMAPPED' THEN '未归属企业'
ELSE MAX(corpinfo_name) END AS corpinfo_name,
COUNT(1) AS config_count,
SUM(CASE WHEN enable = 1 OR rule_status = 'ENABLED' THEN 1 ELSE 0 END) AS enabled_count,
COALESCE(MAX(last_sync_time), MAX(update_time)) AS last_sync_time
FROM alarm_rule
WHERE (delete_enum IS NULL OR delete_enum = 'FALSE')
FROM (
SELECT ar.*,
CASE
WHEN ar.corpinfo_name IS NULL OR TRIM(ar.corpinfo_name) = '' THEN 'UNMAPPED'
WHEN ar.corpinfo_id IS NOT NULL AND TRIM(ar.corpinfo_id) != ''
AND TRIM(ar.corpinfo_id) != '0' THEN TRIM(ar.corpinfo_id)
WHEN ar.org_code IS NOT NULL AND TRIM(ar.org_code) != ''
THEN CONCAT('ORG:', TRIM(ar.org_code))
ELSE CONCAT('NAME:', TRIM(ar.corpinfo_name))
END AS company_key
FROM alarm_rule ar
WHERE (ar.delete_enum IS NULL OR ar.delete_enum = 'FALSE')
<if test="params.eqCorpinfoId != null and params.eqCorpinfoId != ''">
AND corpinfo_id = #{params.eqCorpinfoId}
AND ar.corpinfo_id = #{params.eqCorpinfoId}
</if>
<if test="params.companyGroupOrgCode != null and params.companyGroupOrgCode != ''">
AND ar.org_code = #{params.companyGroupOrgCode}
</if>
<if test="params.unmappedCompany == true">
AND (ar.corpinfo_name IS NULL OR TRIM(ar.corpinfo_name) = '')
</if>
<if test="params.likeCorpinfoName != null and params.likeCorpinfoName != ''">
AND corpinfo_name LIKE CONCAT('%', #{params.likeCorpinfoName}, '%')
<choose>
<when test="params.includeUnmappedCompany == true">
AND (ar.corpinfo_name LIKE CONCAT('%', #{params.likeCorpinfoName}, '%')
OR ar.corpinfo_name IS NULL OR TRIM(ar.corpinfo_name) = '')
</when>
<otherwise>
AND ar.corpinfo_name LIKE CONCAT('%', #{params.likeCorpinfoName}, '%')
</otherwise>
</choose>
</if>
<if test="params.likeContactName != null and params.likeContactName != ''">
AND EXISTS (
SELECT 1
FROM corp_info ci
WHERE CAST(ci.corpinfo_id AS CHAR) = CAST(alarm_rule.corpinfo_id AS CHAR)
AND (ci.delete_enum IS NULL OR ci.delete_enum = 'FALSE')
WHERE (ci.delete_enum IS NULL OR ci.delete_enum = 'FALSE')
AND (
CAST(ci.corpinfo_id AS CHAR) = CAST(ar.corpinfo_id AS CHAR)
OR ((ar.corpinfo_id IS NULL OR TRIM(ar.corpinfo_id) = '' OR ar.corpinfo_id = '0')
AND ci.corp_name = ar.corpinfo_name)
)
AND ci.contacts LIKE CONCAT('%', #{params.likeContactName}, '%')
)
</if>
<if test="params.eqRuleStatus != null and params.eqRuleStatus != ''">
AND rule_status = #{params.eqRuleStatus}
AND ar.rule_status = #{params.eqRuleStatus}
</if>
<if test="params.eqEnable != null">
AND enable = #{params.eqEnable}
AND ar.enable = #{params.eqEnable}
</if>
GROUP BY corpinfo_id, corpinfo_name
) grouped_rule
GROUP BY company_key
ORDER BY COALESCE(MAX(last_sync_time), MAX(update_time)) DESC
</select>