feat(M08-B): 接入保洁任务端基础流程
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { buildApp } from '../dist/app.js';
|
||||
import { signAccessToken } from '../dist/auth/jwt.js';
|
||||
|
||||
const secret = 'test-only-cleaning-route-secret';
|
||||
const token = signAccessToken({
|
||||
sub: '31', sid: '9c4d3af8-c63c-4edb-bf95-b84127bb3f6e',
|
||||
tid: '7', aid: '9', rv: 1
|
||||
}, secret, 900);
|
||||
const forbiddenToken = signAccessToken({
|
||||
sub: '32', sid: '8c4d3af8-c63c-4edb-bf95-b84127bb3f6e',
|
||||
tid: '7', aid: '9', rv: 1
|
||||
}, secret, 900);
|
||||
|
||||
const calls = [];
|
||||
const app = await buildApp({
|
||||
cleaning: {
|
||||
jwtSecret: secret,
|
||||
authRepository: {
|
||||
async validateSession(sessionId) {
|
||||
return {
|
||||
id: sessionId,
|
||||
tenantId: '7',
|
||||
platformAppId: '9',
|
||||
expiresAt: new Date(Date.now() + 60000),
|
||||
user: {
|
||||
id: sessionId.startsWith('8') ? '32' : '31',
|
||||
tenantId: '7',
|
||||
userType: 'CUSTOMER',
|
||||
status: 'ACTIVE',
|
||||
roleVersion: 1,
|
||||
nickname: '',
|
||||
avatarUrl: '',
|
||||
phone: ''
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
accessControl: {
|
||||
async getAccessProfile(tenantId, userId) {
|
||||
return userId === '31'
|
||||
? {
|
||||
roles: ['CLEANER'],
|
||||
capabilities: ['cleaning.task.read', 'cleaning.task.write', 'cleaning.statistics.read'],
|
||||
storeIds: ['11']
|
||||
}
|
||||
: { roles: ['CUSTOMER'], capabilities: ['profile.read'], storeIds: [] };
|
||||
}
|
||||
},
|
||||
repository: {
|
||||
async listHall(input) {
|
||||
calls.push(['listHall', input]);
|
||||
return { items: [task('WAITING')], total: 1, page: input.page, pageSize: input.pageSize };
|
||||
},
|
||||
async listMine(input) {
|
||||
calls.push(['listMine', input]);
|
||||
return { items: [task('CLAIMED')], total: 1, page: input.page, pageSize: input.pageSize };
|
||||
},
|
||||
async claim(input) {
|
||||
calls.push(['claim', input]);
|
||||
return task('CLAIMED');
|
||||
},
|
||||
async start(input) {
|
||||
calls.push(['start', input]);
|
||||
return task('STARTED');
|
||||
},
|
||||
async submit(input) {
|
||||
calls.push(['submit', input]);
|
||||
return { ...task('SUBMITTED'), photoUrls: input.photoUrls };
|
||||
},
|
||||
async stats(input) {
|
||||
calls.push(['stats', input]);
|
||||
return { byStatus: { SUBMITTED: 2 }, pendingSettlementCents: 1200 };
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const hall = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/app-api/cleaning/tasks/hall?page=1&pageSize=10',
|
||||
headers: { authorization: `Bearer ${token}` }
|
||||
});
|
||||
assert.equal(hall.statusCode, 200);
|
||||
assert.equal(hall.json().data.items[0].status, 'WAITING');
|
||||
assert.equal(calls.at(-1)[1].tenantId, '7');
|
||||
assert.equal(calls.at(-1)[1].userId, '31');
|
||||
|
||||
const mine = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/app-api/cleaning/tasks/mine?status=CLAIMED',
|
||||
headers: { authorization: `Bearer ${token}` }
|
||||
});
|
||||
assert.equal(mine.statusCode, 200);
|
||||
assert.equal(calls.at(-1)[0], 'listMine');
|
||||
assert.equal(calls.at(-1)[1].status, 'CLAIMED');
|
||||
|
||||
const claim = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/app-api/cleaning/tasks/101/claim',
|
||||
headers: { authorization: `Bearer ${token}` }
|
||||
});
|
||||
assert.equal(claim.statusCode, 200);
|
||||
assert.equal(calls.at(-1)[0], 'claim');
|
||||
assert.equal(calls.at(-1)[1].taskId, '101');
|
||||
|
||||
const start = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/app-api/cleaning/tasks/101/start',
|
||||
headers: { authorization: `Bearer ${token}` }
|
||||
});
|
||||
assert.equal(start.statusCode, 200);
|
||||
assert.equal(calls.at(-1)[0], 'start');
|
||||
|
||||
const submit = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/app-api/cleaning/tasks/101/submit',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { photoUrls: ['https://api.txyundm.cn/uploads/cleaning/101.jpg'], note: 'ok' }
|
||||
});
|
||||
assert.equal(submit.statusCode, 200);
|
||||
assert.deepEqual(calls.at(-1)[1].photoUrls, ['https://api.txyundm.cn/uploads/cleaning/101.jpg']);
|
||||
|
||||
const stats = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/app-api/cleaning/stats',
|
||||
headers: { authorization: `Bearer ${token}` }
|
||||
});
|
||||
assert.equal(stats.statusCode, 200);
|
||||
assert.equal(stats.json().data.pendingSettlementCents, 1200);
|
||||
|
||||
const unauthorized = await app.inject({ method: 'GET', url: '/app-api/cleaning/tasks/hall' });
|
||||
assert.equal(unauthorized.statusCode, 401);
|
||||
|
||||
const forbidden = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/app-api/cleaning/tasks/hall',
|
||||
headers: { authorization: `Bearer ${forbiddenToken}` }
|
||||
});
|
||||
assert.equal(forbidden.statusCode, 403);
|
||||
|
||||
await app.close();
|
||||
console.log('PASS: M08-B cleaning routes authenticate and forward cleaner task workflows.');
|
||||
|
||||
function task(status) {
|
||||
return {
|
||||
id: '101',
|
||||
taskNo: 'CLN-20260625-0001',
|
||||
storeId: '11',
|
||||
storeName: 'Test Store',
|
||||
roomId: '21',
|
||||
roomName: 'A Room',
|
||||
roomNo: 'A01',
|
||||
orderId: '301',
|
||||
orderNo: 'O301',
|
||||
status,
|
||||
rewardCents: 600,
|
||||
photoUrls: []
|
||||
};
|
||||
}
|
||||
@@ -81,6 +81,9 @@ const benefitVerifySql = read('database/migrations/2026062423_m07c_benefits.veri
|
||||
const rechargeWechatUpSql = read('database/migrations/2026062524_m08a_recharge_wechat.up.sql');
|
||||
const rechargeWechatDownSql = read('database/migrations/2026062524_m08a_recharge_wechat.down.sql');
|
||||
const rechargeWechatVerifySql = read('database/migrations/2026062524_m08a_recharge_wechat.verify.sql');
|
||||
const cleaningUpSql = read('database/migrations/2026062525_m08b_cleaner_tasks.up.sql');
|
||||
const cleaningDownSql = read('database/migrations/2026062525_m08b_cleaner_tasks.down.sql');
|
||||
const cleaningVerifySql = read('database/migrations/2026062525_m08b_cleaner_tasks.verify.sql');
|
||||
|
||||
const coreTables = [
|
||||
'qipai_schema_migrations',
|
||||
@@ -378,4 +381,21 @@ assert.match(rechargeWechatDownSql, /DROP COLUMN provider_payment_id/);
|
||||
assert.match(rechargeWechatVerifySql, /'provider_payment_id'/);
|
||||
assert.match(rechargeWechatVerifySql, /'uq_qipai_recharge_provider_callback'/);
|
||||
|
||||
console.log('PASS: M01-B through M08-A migration contracts are present.');
|
||||
for (const table of ['qipai_cleaning_tasks', 'qipai_cleaning_task_events']) {
|
||||
assert.match(cleaningUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`));
|
||||
assert.match(cleaningDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}`));
|
||||
assert.match(cleaningVerifySql, new RegExp(`'${table}'`));
|
||||
}
|
||||
for (const status of [
|
||||
'WAITING', 'CLAIMED', 'STARTED', 'SUBMITTED', 'COMPLETED',
|
||||
'REJECTED', 'EXEMPT', 'SETTLED', 'CANCELLED'
|
||||
]) {
|
||||
assert.match(cleaningUpSql, new RegExp(status));
|
||||
}
|
||||
assert.match(cleaningUpSql, /cleaner_user_id BIGINT UNSIGNED NULL/);
|
||||
assert.match(cleaningUpSql, /photo_urls_json JSON NOT NULL/);
|
||||
assert.match(cleaningUpSql, /uq_qipai_cleaning_task_order/);
|
||||
assert.match(cleaningUpSql, /cleaning\.task\.write/);
|
||||
assert.match(cleaningUpSql, /cleaning\.statistics\.read/);
|
||||
|
||||
console.log('PASS: M01-B through M08-B migration contracts are present.');
|
||||
|
||||
@@ -34,7 +34,8 @@ assert.match(plan.file, /2026062219_m06b_device_topology\.up\.sql/);
|
||||
assert.match(plan.file, /2026062220_m06c_iot_messages\.up\.sql/);
|
||||
assert.match(plan.file, /2026062421_m07a_wallet_ledger\.up\.sql/);
|
||||
assert.match(plan.file, /2026062422_m07b_recharge_plans\.up\.sql/);
|
||||
assert.match(plan.file, /2026062524_m08a_recharge_wechat\.up\.sql$/);
|
||||
assert.match(plan.file, /2026062524_m08a_recharge_wechat\.up\.sql/);
|
||||
assert.match(plan.file, /2026062525_m08b_cleaner_tasks\.up\.sql$/);
|
||||
assert.match(plan.checksum, /^[a-f0-9]{64}$/);
|
||||
assert.ok(plan.statements.length >= 11);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user