feat: 添加对 Kafka CurrentStatus 的 restart 值支持,更新 G5 入库逻辑及相关测试
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# Change: add restart current_status mapping
|
||||
|
||||
## Why
|
||||
上游 Kafka 的 `CurrentStatus` 新增了 `restart` 值。现有处理逻辑仍按 `on/off` 处理,导致 `restart` 在入库时无法被正确标记为 3。
|
||||
|
||||
## What Changes
|
||||
- 让 Kafka 解析链路接受 `CurrentStatus=restart`。
|
||||
- 将 G5 入库链路中的 `current_status=restart` 映射为 `3`。
|
||||
- 更新相关测试与 OpenSpec 说明,确保 `restart` 是受支持状态。
|
||||
|
||||
## Impact
|
||||
- Affected specs: `openspec/specs/onoffline/spec.md`
|
||||
- Affected code: `src/processor/index.js`, `src/db/g5DatabaseManager.js`, `tests/processor.test.js`
|
||||
@@ -0,0 +1,14 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 重启数据处理
|
||||
系统 SHALL 在 `CurrentStatus` 为 `restart` 时将 `current_status` 保留为 `restart`,并在 G5 入库链路中映射为 `3`。
|
||||
|
||||
#### Scenario: restart 状态写入
|
||||
- **GIVEN** Kafka 消息中的 `CurrentStatus` 为 `restart`
|
||||
- **WHEN** 消息被处理并写入数据库
|
||||
- **THEN** 普通入库链路保留 `restart`,G5 入库链路将其写入为 `3`
|
||||
|
||||
#### Scenario: 其他状态保持原样
|
||||
- **GIVEN** Kafka 消息中的 `CurrentStatus` 为 `on` 或 `off`
|
||||
- **WHEN** 消息被处理并写入数据库
|
||||
- **THEN** 系统按既有规则处理该状态值
|
||||
@@ -0,0 +1,10 @@
|
||||
## 1. Implementation
|
||||
- [x] 1.1 Update Kafka row building logic to preserve `restart` as a valid `current_status` value.
|
||||
- [x] 1.2 Update G5 database mapping so `restart` maps to `3`.
|
||||
- [x] 1.3 Update processor tests for the `restart` case.
|
||||
- [x] 1.4 Update OpenSpec requirements for supported current status values.
|
||||
|
||||
## 2. Validation
|
||||
- [x] 2.1 Run `npm run test`.
|
||||
- [x] 2.2 Run `npm run build`.
|
||||
- [x] 2.3 Run `openspec validate add-restart-current-status-mapping --strict`.
|
||||
@@ -12,12 +12,17 @@
|
||||
- **THEN** current_status 等于 CurrentStatus (截断至 255 字符)
|
||||
|
||||
### Requirement: 重启数据处理
|
||||
系统 SHALL 在 RebootReason 非空时强制 current_status 为 on。
|
||||
系统 SHALL 在 `CurrentStatus` 为 `restart` 时将 `current_status` 保留为 `restart`,并在 G5 入库链路中映射为 `3`。
|
||||
|
||||
#### Scenario: 重启数据写入
|
||||
- **GIVEN** RebootReason 为非空值
|
||||
- **WHEN** 消息被处理
|
||||
- **THEN** current_status 等于 on
|
||||
#### Scenario: restart 状态写入
|
||||
- **GIVEN** Kafka 消息中的 `CurrentStatus` 为 `restart`
|
||||
- **WHEN** 消息被处理并写入数据库
|
||||
- **THEN** 普通入库链路保留 `restart`,G5 入库链路将其写入为 `3`
|
||||
|
||||
#### Scenario: 其他状态保持原样
|
||||
- **GIVEN** Kafka 消息中的 `CurrentStatus` 为 `on` 或 `off`
|
||||
- **WHEN** 消息被处理并写入数据库
|
||||
- **THEN** 系统按既有规则处理该状态值
|
||||
|
||||
### Requirement: 空值保留
|
||||
系统 SHALL 保留上游空值,不对字段进行补 0。
|
||||
|
||||
@@ -41,10 +41,10 @@ G5库结构(双写,临时接入):
|
||||
差异字段:
|
||||
- guid 为 int4,由库自己生成。
|
||||
- record_source 固定为 CRICS。
|
||||
- current_status 为 int2,on映射为1,off映射为2,其余为0。
|
||||
- current_status 为 int2,on映射为1,off映射为2,restart映射为3,其余为0。
|
||||
支持通过环境变量开关双写。
|
||||
|
||||
4. 数据处理规则
|
||||
非重启数据:reboot_reason 为空或不存在,current_status 取 CurrentStatus
|
||||
重启数据:reboot_reason 不为空,current_status 固定为 on
|
||||
重启数据:reboot_reason 不为空时保留 Kafka 上游 current_status 值;若上游值为 restart,则入库标记为 restart,G5 库映射为 3
|
||||
其余字段直接按 Kafka 原值落库,空值不补 0
|
||||
|
||||
@@ -18,6 +18,18 @@ const g5Columns = [
|
||||
'record_source'
|
||||
];
|
||||
|
||||
export const mapCurrentStatusToG5Code = (value) => {
|
||||
if (value === 1 || value === 2 || value === 3) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
||||
if (normalized === 'on') return 1;
|
||||
if (normalized === 'off') return 2;
|
||||
if (normalized === 'restart') return 3;
|
||||
return 0;
|
||||
};
|
||||
|
||||
export class G5DatabaseManager {
|
||||
constructor(dbConfig) {
|
||||
if (!dbConfig.enabled) return;
|
||||
@@ -64,9 +76,7 @@ export class G5DatabaseManager {
|
||||
}
|
||||
if (column === 'current_status') {
|
||||
// current_status in G5 is int2
|
||||
if (row.current_status === 'on') return 1;
|
||||
if (row.current_status === 'off') return 2;
|
||||
return 0;
|
||||
return mapCurrentStatusToG5Code(row.current_status);
|
||||
}
|
||||
return row[column] ?? null;
|
||||
});
|
||||
|
||||
@@ -20,6 +20,16 @@ const normalizeText = (value, maxLength) => {
|
||||
return str;
|
||||
};
|
||||
|
||||
const normalizeCurrentStatus = (value) => {
|
||||
const currentStatus = normalizeText(value, 255);
|
||||
if (currentStatus === null) return null;
|
||||
const normalized = currentStatus.toLowerCase();
|
||||
if (normalized === 'on' || normalized === 'off' || normalized === 'restart') {
|
||||
return normalized;
|
||||
}
|
||||
return currentStatus;
|
||||
};
|
||||
|
||||
export const buildRowsFromMessageValue = (value) => {
|
||||
const payload = parseKafkaPayload(value);
|
||||
return buildRowsFromPayload(payload);
|
||||
@@ -30,9 +40,7 @@ export const buildRowsFromPayload = (rawPayload) => {
|
||||
|
||||
// Database limit is VARCHAR(255)
|
||||
const rebootReason = normalizeText(payload.RebootReason, 255);
|
||||
const currentStatusRaw = normalizeText(payload.CurrentStatus, 255);
|
||||
const hasRebootReason = rebootReason !== null && rebootReason !== '';
|
||||
const currentStatus = hasRebootReason ? 'on' : currentStatusRaw;
|
||||
const currentStatus = normalizeCurrentStatus(payload.CurrentStatus);
|
||||
|
||||
// Derive timestamp: UnixTime -> CurrentTime -> Date.now()
|
||||
let tsMs = payload.UnixTime;
|
||||
|
||||
15
bls-onoffline-backend/tests/g5DatabaseManager.test.js
Normal file
15
bls-onoffline-backend/tests/g5DatabaseManager.test.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mapCurrentStatusToG5Code } from '../src/db/g5DatabaseManager.js';
|
||||
|
||||
describe('G5 current_status mapping', () => {
|
||||
it('maps on/off/restart to numeric codes', () => {
|
||||
expect(mapCurrentStatusToG5Code('on')).toBe(1);
|
||||
expect(mapCurrentStatusToG5Code('off')).toBe(2);
|
||||
expect(mapCurrentStatusToG5Code('restart')).toBe(3);
|
||||
});
|
||||
|
||||
it('returns 0 for unknown values', () => {
|
||||
expect(mapCurrentStatusToG5Code('idle')).toBe(0);
|
||||
expect(mapCurrentStatusToG5Code(null)).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -26,13 +26,19 @@ describe('Processor Logic', () => {
|
||||
expect(rows[0].reboot_reason).toBeNull();
|
||||
});
|
||||
|
||||
it('should override current_status to on for reboot data', () => {
|
||||
const rows = buildRowsFromPayload({ ...basePayload, CurrentStatus: 'off', RebootReason: '0x01' });
|
||||
it('should preserve restart current_status for reboot data', () => {
|
||||
const rows = buildRowsFromPayload({ ...basePayload, CurrentStatus: 'restart', RebootReason: '0x01' });
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].current_status).toBe('on');
|
||||
expect(rows[0].current_status).toBe('restart');
|
||||
expect(rows[0].reboot_reason).toBe('0x01');
|
||||
});
|
||||
|
||||
it('should preserve restart current_status for non-reboot data', () => {
|
||||
const rows = buildRowsFromPayload({ ...basePayload, CurrentStatus: 'restart', RebootReason: null });
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].current_status).toBe('restart');
|
||||
});
|
||||
|
||||
it('should keep empty optional fields as empty strings', () => {
|
||||
const rows = buildRowsFromPayload({
|
||||
...basePayload,
|
||||
|
||||
Reference in New Issue
Block a user