feat(M08-C): 补保洁与设备运营

This commit is contained in:
Codex
2026-08-10 10:52:06 +08:00
parent a95e8bf8c8
commit 65932a8000
17 changed files with 815 additions and 104 deletions
+48
View File
@@ -324,6 +324,15 @@ assert.equal(calls.at(-1)[1].status, 'SUBMITTED');
assert.equal(calls.at(-1)[1].storeId, '11');
assert.equal(calls.at(-1)[1].cleanerUserId, '31');
const appManageList = await app.inject({
method: 'GET',
url: '/app-api/management/cleaning/tasks?status=SUBMITTED&storeId=11',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(appManageList.statusCode, 200);
assert.equal(calls.at(-1)[0], 'listManage');
assert.equal(calls.at(-1)[1].storeId, '11');
const managerStats = await app.inject({
method: 'GET',
url: '/admin-api/cleaning/statistics?from=2026-06-01&to=2026-07-01&storeId=11&cleanerUserId=31',
@@ -341,6 +350,15 @@ assert.equal(managerStats.json().data.members[0].rejectedTaskCount, 1);
assert.equal(managerStats.json().data.trend[0].exempted, 1);
assert.equal(managerStats.json().data.trend[0].paidSettlementCents, 600);
const appManagerStats = await app.inject({
method: 'GET',
url: '/app-api/management/cleaning/statistics?storeId=11',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(appManagerStats.statusCode, 200);
assert.equal(calls.at(-1)[0], 'managerStatistics');
assert.equal(calls.at(-1)[1].storeId, '11');
const claim = await app.inject({
method: 'POST',
url: '/app-api/cleaning/tasks/101/claim',
@@ -410,6 +428,16 @@ const complete = await app.inject({
assert.equal(complete.statusCode, 200);
assert.equal(calls.at(-1)[0], 'complete');
const appComplete = await app.inject({
method: 'POST',
url: '/app-api/management/cleaning/tasks/101/complete',
headers: { authorization: `Bearer ${token}` },
payload: { note: 'miniapp approval' }
});
assert.equal(appComplete.statusCode, 200);
assert.equal(calls.at(-1)[0], 'complete');
assert.equal(calls.at(-1)[1].note, 'miniapp approval');
const reject = await app.inject({
method: 'POST',
url: '/admin-api/cleaning/tasks/101/reject',
@@ -420,6 +448,16 @@ assert.equal(reject.statusCode, 200);
assert.equal(calls.at(-1)[0], 'reject');
assert.equal(calls.at(-1)[1].reason, 'photo is unclear');
const appReject = await app.inject({
method: 'POST',
url: '/app-api/management/cleaning/tasks/101/reject',
headers: { authorization: `Bearer ${token}` },
payload: { reason: 'miniapp asks for rework' }
});
assert.equal(appReject.statusCode, 200);
assert.equal(calls.at(-1)[0], 'reject');
assert.equal(calls.at(-1)[1].reason, 'miniapp asks for rework');
const exempt = await app.inject({
method: 'POST',
url: '/admin-api/cleaning/tasks/101/exempt',
@@ -431,6 +469,16 @@ assert.equal(exempt.json().data.status, 'EXEMPT');
assert.equal(calls.at(-1)[0], 'exempt');
assert.equal(calls.at(-1)[1].note, 'no cleaning required');
const appExempt = await app.inject({
method: 'POST',
url: '/app-api/management/cleaning/tasks/101/exempt',
headers: { authorization: `Bearer ${token}` },
payload: { note: 'miniapp exemption' }
});
assert.equal(appExempt.statusCode, 200);
assert.equal(calls.at(-1)[0], 'exempt');
assert.equal(calls.at(-1)[1].note, 'miniapp exemption');
const members = await app.inject({
method: 'GET',
url: '/admin-api/cleaning/tasks/101/members',
+24
View File
@@ -12,6 +12,7 @@ const managerAccess = {
storeIds: ['11']
};
const issued = [];
const audits = [];
const service = new DeviceControlService({
async execute(sql) {
if (sql.includes('FROM qipai_devices')) {
@@ -26,6 +27,7 @@ const service = new DeviceControlService({
deviceType: 'CONTROL_BOX', status: 'ONLINE'
}], []];
}
if (sql.includes('DEVICE_COMMAND_REQUESTED')) audits.push(sql);
return [[], []];
}
}, {
@@ -37,6 +39,7 @@ const service = new DeviceControlService({
});
const context = {
tenantId: '7', storeId: '11', roomId: '31', traceId: 'trace',
actorType: 'ADMIN', actorId: '22', ip: '127.0.0.1', userAgent: 'test',
access: managerAccess
};
@@ -75,6 +78,8 @@ assert.equal(issued.at(-1).payload.action, 'localtask');
assert.equal(issued.at(-1).payload.switch, 'on');
await service.clearSmartSocketTask(context, 1);
assert.equal(issued.at(-1).payload.action, 'clearTask');
assert.equal(audits.length, 14);
assert.match(audits[0], /DEVICE_COMMAND_REQUESTED/);
await assert.rejects(
() => service.controlSubLock(context, {
@@ -160,6 +165,25 @@ const response = await app.inject({
});
assert.equal(response.statusCode, 200);
assert.equal(routed.slot1, 'on');
const appPowerResponse = await app.inject({
method: 'POST',
url: '/app-api/management/device-control/power',
headers: { authorization: `Bearer ${token}` },
payload: { storeId: '11', roomId: '31', slotall: 'off' }
});
assert.equal(appPowerResponse.statusCode, 200);
assert.equal(routed.slotall, 'off');
const appDoorResponse = await app.inject({
method: 'POST',
url: '/app-api/management/device-control/door',
headers: { authorization: `Bearer ${token}` },
payload: { storeId: '11', roomId: '31', order: 'open', holdopen: 0, delayTime: 4 }
});
assert.equal(appDoorResponse.statusCode, 200);
assert.equal(routed.order, 'open');
assert.equal(routedContext.storeId, '11');
assert.equal(routedContext.actorType, 'ADMIN');
assert.equal(routedContext.actorId, '22');
const socketResponse = await app.inject({
method: 'POST',
url: '/admin-api/device-control/socket/switch',
+14
View File
@@ -69,6 +69,20 @@ const app = await buildApp({
}
}
});
const appAssets = await app.inject({
method: 'GET', url: '/app-api/management/devices?storeId=11',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(appAssets.statusCode, 200);
assert.deepEqual(appAssets.json().data, []);
const appTopology = await app.inject({
method: 'GET', url: '/app-api/management/device-topology?storeId=11',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(appTopology.statusCode, 200);
assert.deepEqual(appTopology.json().data.openAlerts, []);
const created = await app.inject({
method: 'POST', url: '/admin-api/devices',
headers: { authorization: `Bearer ${token}` },
@@ -37,6 +37,8 @@ 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 { IotMessageService } from '../dist/devices/iot-message-service.js';
import { DeviceCommandService } from '../dist/devices/device-command-service.js';
import { DeviceControlService } from '../dist/devices/device-control-service.js';
import { MemberProfileService } from '../dist/wallets/member-profile-service.js';
import {
executeMigrationPlan,
@@ -1724,6 +1726,44 @@ async function assertDeviceTopology(pool, context) {
signalStrength: 22, firmwareVersion: '1.0.1',
snapshot: { slot1: true, door: 'closed' }
});
const published = [];
const control = new DeviceControlService(
pool,
new DeviceCommandService(new IotMessageService(pool), {
async publishDeviceCommand(deviceId, payload) {
published.push({ deviceId, payload: JSON.parse(payload) });
}
})
);
const doorResult = await control.controlDoor({
tenantId: context.tenantId,
storeId,
roomId,
traceId: 'm08c-manager-door',
actorType: 'ADMIN',
actorId: adminId,
ip: '127.0.0.1',
userAgent: 'M08-C manager miniapp',
access
}, { order: 'open', holdopen: 0, delayTime: 4 });
assert.equal(doorResult.status, 'PUBLISHED');
assert.equal(published[0].deviceId, 'M06B_BOX_001');
assert.equal(published[0].payload.action, 'Crldoor');
const [commandAuditRows] = await pool.query(
`SELECT actor_type AS actorType, actor_id AS actorId, action,
JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.commandType')) AS commandType,
JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.roomId')) AS roomId
FROM qipai_audit_logs
WHERE tenant_id = ? AND trace_id = 'm08c-manager-door'`,
[context.tenantId]
);
assert.deepEqual(commandAuditRows, [{
actorType: 'ADMIN',
actorId: Number(adminId),
action: 'DEVICE_COMMAND_REQUESTED',
commandType: 'Crldoor',
roomId
}]);
await repository.addMaintenance(actor, {
assetId: controlBox.assetId, storeId, roomId,
recordType: 'INSPECTION', status: 'OPEN', description: 'M06-B inspection'
@@ -2051,6 +2091,8 @@ try {
'Sub-1G parent-child topology',
'device status snapshots and maintenance state'
,
'manager device command actor audit attribution'
,
'13-digit IoT command state transition',
'QoS 1 duplicate event receive count',
'ACK correlation without duplicate side effects',