feat(M06-B): 建立设备资产与拓扑管理
This commit is contained in:
@@ -35,6 +35,7 @@ import { ProfitSharingService } from '../dist/payments/profit-sharing-service.js
|
||||
import { WechatPayClient } from '../dist/payments/wechat-pay-client.js';
|
||||
import { ThirdPartyClient } from '../dist/third-party/third-party-client.js';
|
||||
import { ThirdPartyService } from '../dist/third-party/third-party-service.js';
|
||||
import { DeviceRepository } from '../dist/devices/device-repository.js';
|
||||
import {
|
||||
executeMigrationPlan,
|
||||
loadMigrationPlan,
|
||||
@@ -47,6 +48,11 @@ const expectedTables = [
|
||||
'qipai_audit_logs',
|
||||
'qipai_auth_sessions',
|
||||
'qipai_collection_accounts',
|
||||
'qipai_device_alerts',
|
||||
'qipai_device_channels',
|
||||
'qipai_device_links',
|
||||
'qipai_device_maintenance_records',
|
||||
'qipai_device_status_snapshots',
|
||||
'qipai_devices',
|
||||
'qipai_direct_bookings',
|
||||
'qipai_group_redemptions',
|
||||
@@ -115,12 +121,12 @@ async function readMigrationVersions(pool) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT version, name
|
||||
FROM qipai_schema_migrations
|
||||
WHERE version IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
WHERE version IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ORDER BY version`,
|
||||
['2026061601', '2026061802', '2026061803', '2026061804',
|
||||
'2026061805', '2026061806', '2026061807', '2026061808', '2026061809',
|
||||
'2026061810', '2026061811', '2026062012', '2026062013', '2026062014',
|
||||
'2026062015', '2026062216', '2026062217', '2026062218']
|
||||
'2026062015', '2026062216', '2026062217', '2026062218', '2026062219']
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
@@ -1578,6 +1584,92 @@ async function assertContentManagement(pool, context) {
|
||||
);
|
||||
}
|
||||
|
||||
async function assertDeviceTopology(pool, context) {
|
||||
const [adminRows] = await pool.query(
|
||||
`SELECT u.id FROM qipai_users u
|
||||
INNER JOIN qipai_user_roles ur ON ur.tenant_id = u.tenant_id AND ur.user_id = u.id
|
||||
INNER JOIN qipai_roles r ON r.id = ur.role_id AND r.tenant_id = ur.tenant_id
|
||||
WHERE u.tenant_id = ? AND r.code = 'TENANT_ADMIN' LIMIT 1`,
|
||||
[context.tenantId]
|
||||
);
|
||||
const [storeRows] = await pool.query(
|
||||
`SELECT s.id AS storeId, r.id AS roomId
|
||||
FROM qipai_stores s
|
||||
INNER JOIN qipai_rooms r ON r.tenant_id = s.tenant_id AND r.store_id = s.id
|
||||
WHERE s.tenant_id = ? AND s.name = 'M03A Store' LIMIT 1`,
|
||||
[context.tenantId]
|
||||
);
|
||||
const rbac = new RbacRepository(pool);
|
||||
const adminId = String(adminRows[0].id);
|
||||
const access = await rbac.getAccessProfile(context.tenantId, adminId);
|
||||
assert.ok(access.capabilities.includes('device.read'));
|
||||
assert.ok(access.capabilities.includes('device.write'));
|
||||
const storeId = String(storeRows[0].storeId);
|
||||
const roomId = String(storeRows[0].roomId);
|
||||
const actor = {
|
||||
tenantId: context.tenantId, userId: adminId, access,
|
||||
traceId: 'm06b-live-test', ip: '127.0.0.1', userAgent: 'M06-B live test'
|
||||
};
|
||||
const repository = new DeviceRepository(pool);
|
||||
const controlBox = await repository.createAsset(actor, {
|
||||
storeId, roomId, deviceId: 'M06B_BOX_001', imei: '860000000000001',
|
||||
iccid: '89860000000000000001', deviceType: 'CONTROL_BOX',
|
||||
model: 'JL-CONTROL', firmwareVersion: '1.0.0', signalStrength: 18,
|
||||
capabilities: ['POWER', 'LOCK', 'TTS']
|
||||
});
|
||||
const socket = await repository.createAsset(actor, {
|
||||
storeId, roomId, deviceId: 'M06B_SOCKET_001', imei: '860000000000002',
|
||||
iccid: '89860000000000000002', deviceType: 'SMART_SOCKET',
|
||||
model: 'JL-SOCKET', firmwareVersion: '1.0.0', signalStrength: 16,
|
||||
capabilities: ['POWER', 'METERING']
|
||||
});
|
||||
const lock = await repository.createAsset(actor, {
|
||||
storeId, roomId, deviceId: 'M06B_LOCK_001', imei: '',
|
||||
iccid: null, deviceType: 'SUB_LOCK', model: '701C',
|
||||
firmwareVersion: '1.0.0', capabilities: ['LOCK', 'CARD', 'PASSWORD']
|
||||
});
|
||||
await repository.bindChannel(actor, {
|
||||
assetId: controlBox.assetId, storeId, roomId,
|
||||
channelCode: 'SLOT1', purpose: 'ROOM_POWER'
|
||||
});
|
||||
await assert.rejects(
|
||||
() => repository.bindChannel(actor, {
|
||||
assetId: socket.assetId, storeId, roomId,
|
||||
channelCode: 'MAIN', purpose: 'ROOM_POWER'
|
||||
}),
|
||||
(error) => error.code === 'DEVICE_CONTROL_TARGET_CONFLICT'
|
||||
);
|
||||
await repository.bindSubDevice(actor, {
|
||||
parentAssetId: controlBox.assetId, childAssetId: lock.assetId,
|
||||
storeId, roomId, subId: 'SUB-001', subtype: '701C'
|
||||
});
|
||||
await repository.recordStatus(actor, {
|
||||
assetId: controlBox.assetId, storeId, onlineStatus: 'ONLINE',
|
||||
signalStrength: 22, firmwareVersion: '1.0.1',
|
||||
snapshot: { slot1: true, door: 'closed' }
|
||||
});
|
||||
await repository.addMaintenance(actor, {
|
||||
assetId: controlBox.assetId, storeId, roomId,
|
||||
recordType: 'INSPECTION', status: 'OPEN', description: 'M06-B inspection'
|
||||
});
|
||||
const assets = await repository.listAssets(actor, storeId);
|
||||
const box = assets.find((item) => item.id === controlBox.assetId);
|
||||
assert.deepEqual(box?.capabilities, ['LOCK', 'POWER', 'TTS']);
|
||||
assert.equal(box?.status, 'ONLINE');
|
||||
assert.equal(box?.maintenanceStatus, 'MAINTENANCE');
|
||||
const [topologyRows] = await pool.query(
|
||||
`SELECT c.purpose, l.sub_id AS subId, l.subtype
|
||||
FROM qipai_device_channels c
|
||||
INNER JOIN qipai_device_links l
|
||||
ON l.tenant_id = c.tenant_id AND l.parent_device_id = c.device_id
|
||||
WHERE c.tenant_id = ? AND c.device_id = ?`,
|
||||
[context.tenantId, controlBox.assetId]
|
||||
);
|
||||
assert.deepEqual(topologyRows, [{
|
||||
purpose: 'ROOM_POWER', subId: 'SUB-001', subtype: '701C'
|
||||
}]);
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
assert.equal(config.mysql.passwordConfigured, true, 'Live migration test requires a temporary password.');
|
||||
assert.match(
|
||||
@@ -1619,7 +1711,8 @@ try {
|
||||
{ version: '2026062015', name: 'm05a_payment_domain' },
|
||||
{ version: '2026062216', name: 'm05b_wechat_refunds' },
|
||||
{ version: '2026062217', name: 'm05c_third_party' },
|
||||
{ version: '2026062218', name: 'm05d_profit_sharing' }
|
||||
{ version: '2026062218', name: 'm05d_profit_sharing' },
|
||||
{ version: '2026062219', name: 'm06b_device_topology' }
|
||||
]);
|
||||
await assertTaskDurability(pool);
|
||||
const loginContext = await assertPlatformTenantIsolation(pool);
|
||||
@@ -1636,13 +1729,14 @@ try {
|
||||
await assertPaymentDomain(pool, loginContext);
|
||||
await assertThirdPartyDomain(pool, loginContext);
|
||||
await assertProfitSharingDomain(pool, loginContext);
|
||||
await assertDeviceTopology(pool, loginContext);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: first up, verify, tenant isolation and revocable auth checks completed.');
|
||||
|
||||
await executeMigrationPlan(pool, plans.down);
|
||||
assert.deepEqual(await readCoreTables(pool), []);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: down removed all M01-B through M05-D tables.');
|
||||
console.log('PASS: down removed all M01-B through M06-B tables.');
|
||||
|
||||
await executeMigrationPlan(pool, plans.up);
|
||||
await executeMigrationPlan(pool, plans.verify);
|
||||
@@ -1665,7 +1759,8 @@ try {
|
||||
{ version: '2026062015', name: 'm05a_payment_domain' },
|
||||
{ version: '2026062216', name: 'm05b_wechat_refunds' },
|
||||
{ version: '2026062217', name: 'm05c_third_party' },
|
||||
{ version: '2026062218', name: 'm05d_profit_sharing' }
|
||||
{ version: '2026062218', name: 'm05d_profit_sharing' },
|
||||
{ version: '2026062219', name: 'm06b_device_topology' }
|
||||
]);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: second up and verify restored the schema.');
|
||||
@@ -1758,6 +1853,11 @@ try {
|
||||
'profit-share percentage total validation',
|
||||
'receiver hash and masked storage',
|
||||
'payment and receiver idempotent profit sharing'
|
||||
,
|
||||
'device asset identity and capabilities',
|
||||
'control target conflict across control box and smart socket',
|
||||
'Sub-1G parent-child topology',
|
||||
'device status snapshots and maintenance state'
|
||||
]
|
||||
}, null, 2));
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user