feat(M08-B): 补任务事件流水

This commit is contained in:
Codex
2026-06-30 18:26:53 +08:00
parent 957a29b11e
commit 986a90806b
12 changed files with 178 additions and 14 deletions
@@ -56,6 +56,17 @@ interface CleaningTaskMemberRow extends RowDataPacket {
removedAt: Date | null;
settledAt: Date | null;
}
interface CleaningTaskEventRow extends RowDataPacket {
id: string;
taskId: string;
fromStatus: CleaningTaskStatus | null;
toStatus: CleaningTaskStatus;
action: string;
actorId: string;
traceId: string;
note: string;
createdAt: Date;
}
interface CountRow extends RowDataPacket { total: number }
interface StatusCountRow extends RowDataPacket { status: CleaningTaskStatus; total: number }
interface AmountRow extends RowDataPacket { amount: number | null }
@@ -284,6 +295,22 @@ export class CleaningTaskRepository {
return this.getMembers(input.tenantId, input.taskId);
}
async listEvents(input: CleaningActor & { taskId: string }) {
this.assertCleaner(input.access, 'read');
await this.assertStoreVisible(input, input.taskId);
const [rows] = await this.pool.execute<CleaningTaskEventRow[]>(
`SELECT e.id, e.task_id AS taskId, e.from_status AS fromStatus,
e.to_status AS toStatus, e.action, e.actor_id AS actorId,
e.trace_id AS traceId, e.note, e.created_at AS createdAt
FROM qipai_cleaning_task_events e
WHERE e.tenant_id = ? AND e.task_id = ?
ORDER BY e.id DESC
LIMIT 50`,
[input.tenantId, input.taskId]
);
return rows.map(publicTaskEvent);
}
async addMember(input: CleaningActor & {
taskId: string; cleanerUserId: string; rewardCents: number; note?: string;
}) {
@@ -1307,6 +1334,20 @@ function publicTaskMember(row: CleaningTaskMemberRow) {
};
}
function publicTaskEvent(row: CleaningTaskEventRow) {
return {
id: String(row.id),
taskId: String(row.taskId),
fromStatus: row.fromStatus,
toStatus: row.toStatus,
action: row.action,
actorId: String(row.actorId),
traceId: row.traceId,
note: row.note,
createdAt: row.createdAt
};
}
function publicSettlement(row: SettlementRow) {
return {
id: String(row.id),
+13 -1
View File
@@ -100,7 +100,7 @@ const reclaimSchema = z.object({
export interface CleaningRouteOptions {
repository: Pick<CleaningTaskRepository,
'listHall' | 'listMine' | 'listManage' | 'claim' | 'start' | 'rework' | 'submit'
| 'assign' | 'complete' | 'reject' | 'listMembers' | 'addMember' | 'removeMember' | 'settlementCandidates'
| 'assign' | 'complete' | 'reject' | 'listMembers' | 'listEvents' | 'addMember' | 'removeMember' | 'settlementCandidates'
| 'listSettlements' | 'getSettlementDetail' | 'generateSettlement' | 'confirmSettlement' | 'markSettlementPaid'
| 'recordSettlementPayoutFailure' | 'reclaimTimeouts'
| 'assertCanUploadPhoto' | 'stats' | 'managerStatistics'>;
@@ -264,6 +264,18 @@ export async function registerCleaningRoutes(
}));
});
app.get('/admin-api/cleaning/tasks/:taskId/events', async (request, reply) => {
const actor = await requireActor(request, reply, options, 'read');
if (!actor) return;
const params = paramsSchema.safeParse(request.params);
if (!params.success) return invalid(reply, request.traceId);
return handle(reply, request.traceId, async () => ({
code: 0,
data: await options.repository.listEvents({ ...actor, taskId: params.data.taskId }),
traceId: request.traceId
}));
});
app.post('/admin-api/cleaning/tasks/:taskId/members', async (request, reply) => {
const actor = await requireActor(request, reply, options, 'write');
if (!actor) return;
+24
View File
@@ -98,6 +98,20 @@ const app = await buildApp({
calls.push(['listMembers', input]);
return [member('LEAD', '31', 500), member('ASSIST', '33', 100)];
},
async listEvents(input) {
calls.push(['listEvents', input]);
return [{
id: '9001',
taskId: input.taskId,
fromStatus: 'STARTED',
toStatus: 'SUBMITTED',
action: 'SUBMIT',
actorId: input.userId,
traceId: input.traceId,
note: 'done',
createdAt: new Date()
}];
},
async addMember(input) {
calls.push(['addMember', input]);
return [member('LEAD', '31', 500), member('ASSIST', input.cleanerUserId, input.rewardCents)];
@@ -393,6 +407,16 @@ assert.equal(members.statusCode, 200);
assert.equal(calls.at(-1)[0], 'listMembers');
assert.equal(members.json().data.length, 2);
const events = await app.inject({
method: 'GET',
url: '/admin-api/cleaning/tasks/101/events',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(events.statusCode, 200);
assert.equal(calls.at(-1)[0], 'listEvents');
assert.equal(calls.at(-1)[1].taskId, '101');
assert.equal(events.json().data[0].action, 'SUBMIT');
const addedMember = await app.inject({
method: 'POST',
url: '/admin-api/cleaning/tasks/101/members',