feat(M09-B): 完成清洁规则与照片验收闭环

This commit is contained in:
Codex
2026-08-10 21:49:07 +08:00
parent b507e87999
commit bc7cb8fab9
24 changed files with 1650 additions and 81 deletions
+91 -5
View File
@@ -77,6 +77,25 @@ const app = await buildApp({
},
async assertCanUploadPhoto(input) {
calls.push(['assertCanUploadPhoto', input]);
return { storeId: '11', maxPhotoCount: 3 };
},
async recordPhotoUpload(input) {
calls.push(['recordPhotoUpload', input]);
return { id: '701', ...input.image };
},
async listTemplates(input) {
calls.push(['listTemplates', input]);
return [{
id: '801', scopeKey: 'STORE:11', scopeType: 'STORE', storeId: '11', roomId: null,
name: '门店清洁', requirement: '桌面与地面', photoRequired: true,
minPhotoCount: 2, maxPhotoCount: 3, exemptPolicy: 'BEFORE_START',
version: 2, status: 'ACTIVE', appliesToExistingTasks: false
}];
},
async upsertTemplate(input) {
calls.push(['upsertTemplate', input]);
return { id: '801', scopeKey: `STORE:${input.scopeId}`, ...input,
storeId: input.scopeId, roomId: null, version: 3, appliesToExistingTasks: false };
},
async submit(input) {
calls.push(['submit', input]);
@@ -116,6 +135,15 @@ const app = await buildApp({
createdAt: new Date()
}];
},
async listSubmissions(input) {
calls.push(['listSubmissions', input]);
return [{
id: '9101', taskId: input.taskId, revision: 2, status: 'REJECTED',
photoUrls: ['https://api.txyundm.cn/uploads/old.webp'], note: 'first',
rejectReason: 'photo is unclear', submittedBy: '31', reviewedBy: '31',
submittedAt: new Date(), reviewedAt: new Date()
}];
},
async addMember(input) {
calls.push(['addMember', input]);
return [member('LEAD', '31', 500), member('ASSIST', input.cleanerUserId, input.rewardCents)];
@@ -171,6 +199,14 @@ const app = await buildApp({
calls.push(['reclaimTimeouts', input]);
return { reclaimed: 2, taskIds: ['101', '102'] };
},
async claimExpiredPhotoUploads(input) {
calls.push(['claimExpiredPhotoUploads', input]);
return [{ id: '701', storagePath: 'tenants/7/stores/11/orphan.webp' }];
},
async markPhotoUploadsDeleted(input) {
calls.push(['markPhotoUploadsDeleted', input]);
return { deleted: input.photoIds.length };
},
async stats(input) {
calls.push(['stats', input]);
return {
@@ -281,19 +317,44 @@ const app = await buildApp({
async storeImage(input) {
calls.push(['storeImage', input]);
return {
storagePath: 'tenants/7/shared/cleaning.webp',
publicUrl: 'https://api.txyundm.cn/uploads/tenants/7/shared/cleaning.webp',
storagePath: 'tenants/7/stores/11/cleaning.webp',
publicUrl: 'https://api.txyundm.cn/uploads/tenants/7/stores/11/cleaning.webp',
mimeType: 'image/webp',
byteSize: input.body.length,
width: 640,
height: 480,
checksumSha256: 'abc'
};
},
async deleteImage(storagePath) {
calls.push(['deleteImage', storagePath]);
}
}
}
});
const templates = await app.inject({
method: 'GET',
url: '/admin-api/cleaning/templates',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(templates.statusCode, 200);
assert.equal(templates.json().data[0].scopeKey, 'STORE:11');
const updatedTemplate = await app.inject({
method: 'PUT',
url: '/admin-api/cleaning/templates',
headers: { authorization: `Bearer ${token}` },
payload: {
scopeType: 'STORE', scopeId: '11', name: '门店清洁', requirement: '桌面与地面',
photoRequired: true, minPhotoCount: 2, maxPhotoCount: 3,
exemptPolicy: 'BEFORE_START', status: 'ACTIVE'
}
});
assert.equal(updatedTemplate.statusCode, 200);
assert.equal(updatedTemplate.json().data.appliesToExistingTasks, false);
assert.equal(calls.at(-1)[0], 'upsertTemplate');
const hall = await app.inject({
method: 'GET',
url: '/app-api/cleaning/tasks/hall?page=1&pageSize=10',
@@ -396,9 +457,11 @@ const photo = await app.inject({
payload: Buffer.from('fake-image')
});
assert.equal(photo.statusCode, 201);
assert.equal(photo.json().data.publicUrl, 'https://api.txyundm.cn/uploads/tenants/7/shared/cleaning.webp');
assert.equal(calls.at(-2)[0], 'assertCanUploadPhoto');
assert.equal(calls.at(-1)[0], 'storeImage');
assert.equal(photo.json().data.publicUrl, 'https://api.txyundm.cn/uploads/tenants/7/stores/11/cleaning.webp');
assert.equal(calls.at(-3)[0], 'assertCanUploadPhoto');
assert.equal(calls.at(-2)[0], 'storeImage');
assert.equal(calls.at(-2)[1].storeId, '11');
assert.equal(calls.at(-1)[0], 'recordPhotoUpload');
const submit = await app.inject({
method: 'POST',
@@ -498,6 +561,16 @@ assert.equal(calls.at(-1)[0], 'listEvents');
assert.equal(calls.at(-1)[1].taskId, '101');
assert.equal(events.json().data[0].action, 'SUBMIT');
const submissions = await app.inject({
method: 'GET',
url: '/admin-api/cleaning/tasks/101/submissions',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(submissions.statusCode, 200);
assert.equal(calls.at(-1)[0], 'listSubmissions');
assert.equal(submissions.json().data[0].revision, 2);
assert.equal(submissions.json().data[0].photoUrls[0], 'https://api.txyundm.cn/uploads/old.webp');
const addedMember = await app.inject({
method: 'POST',
url: '/admin-api/cleaning/tasks/101/members',
@@ -664,6 +737,19 @@ assert.equal(reclaimed.statusCode, 200);
assert.equal(calls.at(-1)[0], 'reclaimTimeouts');
assert.equal(calls.at(-1)[1].olderThanMinutes, 30);
const cleanedPhotos = await app.inject({
method: 'POST',
url: '/admin-api/cleaning/photos/cleanup',
headers: { authorization: `Bearer ${token}` },
payload: { limit: 10 }
});
assert.equal(cleanedPhotos.statusCode, 200);
assert.deepEqual(cleanedPhotos.json().data, { claimed: 1, deleted: 1, failedIds: [] });
assert.equal(calls.at(-3)[0], 'claimExpiredPhotoUploads');
assert.equal(calls.at(-2)[0], 'deleteImage');
assert.equal(calls.at(-1)[0], 'markPhotoUploadsDeleted');
assert.deepEqual(calls.at(-1)[1].photoIds, ['701']);
const stats = await app.inject({
method: 'GET',
url: '/app-api/cleaning/stats',