feat(M08-B): 增加保洁验收和指派管理
This commit is contained in:
@@ -85,6 +85,21 @@ export class CleaningTaskRepository {
|
||||
return this.listByWhere(where, params, input.page, input.pageSize, 't.updated_at DESC, t.id DESC');
|
||||
}
|
||||
|
||||
async listManage(input: CleaningActor & { page: number; pageSize: number; status?: CleaningTaskStatus }) {
|
||||
this.assertCleaner(input.access, 'read');
|
||||
const where = [
|
||||
't.tenant_id = ?',
|
||||
't.deleted_at IS NULL',
|
||||
storeScopeSql(input.access, 't.store_id')
|
||||
];
|
||||
const params: Array<string | number> = [input.tenantId];
|
||||
if (input.status) {
|
||||
where.push('t.status = ?');
|
||||
params.push(input.status);
|
||||
}
|
||||
return this.listByWhere(where, params, input.page, input.pageSize, 't.updated_at DESC, t.id DESC');
|
||||
}
|
||||
|
||||
async claim(input: CleaningActor & { taskId: string }) {
|
||||
this.assertCleaner(input.access, 'write');
|
||||
await this.assertStoreVisible(input, input.taskId);
|
||||
@@ -111,6 +126,53 @@ export class CleaningTaskRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async assign(input: CleaningActor & { taskId: string; cleanerUserId: string; note?: string }) {
|
||||
this.assertCleaner(input.access, 'write');
|
||||
await this.assertStoreVisible(input, input.taskId);
|
||||
await this.assertCleanerUserForTask(input.tenantId, input.taskId, input.cleanerUserId);
|
||||
const [result] = await this.pool.execute<ResultSetHeader>(
|
||||
`UPDATE qipai_cleaning_tasks
|
||||
SET status = 'CLAIMED', cleaner_user_id = ?, claimed_at = COALESCE(claimed_at, UTC_TIMESTAMP(3)),
|
||||
reject_reason = ''
|
||||
WHERE tenant_id = ? AND id = ? AND status IN ('WAITING', 'CLAIMED', 'REJECTED')
|
||||
AND deleted_at IS NULL`,
|
||||
[input.cleanerUserId, input.tenantId, input.taskId]
|
||||
);
|
||||
if (result.affectedRows !== 1) throw new CleaningTaskError('CLEANING_TASK_ASSIGN_CONFLICT');
|
||||
await this.recordEvent(input, input.taskId, 'WAITING', 'CLAIMED', 'ASSIGN', input.note ?? '');
|
||||
return this.getTask(input, input.taskId);
|
||||
}
|
||||
|
||||
async complete(input: CleaningActor & { taskId: string; note?: string }) {
|
||||
return this.moveManaged(input, 'SUBMITTED', 'COMPLETED', 'COMPLETE', 'completed_at', input.note ?? '');
|
||||
}
|
||||
|
||||
async reject(input: CleaningActor & { taskId: string; reason: string }) {
|
||||
this.assertCleaner(input.access, 'write');
|
||||
await this.assertStoreVisible(input, input.taskId);
|
||||
const [result] = await this.pool.execute<ResultSetHeader>(
|
||||
`UPDATE qipai_cleaning_tasks
|
||||
SET status = 'REJECTED', reject_reason = ?
|
||||
WHERE tenant_id = ? AND id = ? AND status = 'SUBMITTED' AND deleted_at IS NULL`,
|
||||
[input.reason.slice(0, 512), input.tenantId, input.taskId]
|
||||
);
|
||||
if (result.affectedRows !== 1) throw new CleaningTaskError('CLEANING_TASK_STATUS_CONFLICT');
|
||||
await this.recordEvent(input, input.taskId, 'SUBMITTED', 'REJECTED', 'REJECT', input.reason);
|
||||
return this.getTask(input, input.taskId);
|
||||
}
|
||||
|
||||
async settlementCandidates(input: CleaningActor & { page: number; pageSize: number }) {
|
||||
this.assertCleaner(input.access, 'read');
|
||||
const where = [
|
||||
't.tenant_id = ?',
|
||||
't.deleted_at IS NULL',
|
||||
"t.status = 'COMPLETED'",
|
||||
't.settled_at IS NULL',
|
||||
storeScopeSql(input.access, 't.store_id')
|
||||
];
|
||||
return this.listByWhere(where, [input.tenantId], input.page, input.pageSize, 't.completed_at ASC, t.id ASC');
|
||||
}
|
||||
|
||||
async submit(input: CleaningActor & { taskId: string; photoUrls: string[]; note?: string }) {
|
||||
this.assertCleaner(input.access, 'write');
|
||||
if (input.photoUrls.length === 0) throw new CleaningTaskError('CLEANING_PHOTO_REQUIRED');
|
||||
@@ -247,6 +309,34 @@ export class CleaningTaskRepository {
|
||||
return task;
|
||||
}
|
||||
|
||||
private async getTask(input: CleaningActor, taskId: string) {
|
||||
const result = await this.listManage({ ...input, page: 1, pageSize: 50 });
|
||||
const task = result.items.find((item) => item.id === taskId);
|
||||
if (!task) throw new CleaningTaskError('CLEANING_TASK_NOT_FOUND');
|
||||
return task;
|
||||
}
|
||||
|
||||
private async moveManaged(
|
||||
input: CleaningActor & { taskId: string },
|
||||
from: CleaningTaskStatus,
|
||||
to: CleaningTaskStatus,
|
||||
action: string,
|
||||
timestampColumn: 'completed_at',
|
||||
note: string
|
||||
) {
|
||||
this.assertCleaner(input.access, 'write');
|
||||
await this.assertStoreVisible(input, input.taskId);
|
||||
const [result] = await this.pool.execute<ResultSetHeader>(
|
||||
`UPDATE qipai_cleaning_tasks
|
||||
SET status = ?, ${timestampColumn} = UTC_TIMESTAMP(3)
|
||||
WHERE tenant_id = ? AND id = ? AND status = ? AND deleted_at IS NULL`,
|
||||
[to, input.tenantId, input.taskId, from]
|
||||
);
|
||||
if (result.affectedRows !== 1) throw new CleaningTaskError('CLEANING_TASK_STATUS_CONFLICT');
|
||||
await this.recordEvent(input, input.taskId, from, to, action, note);
|
||||
return this.getTask(input, input.taskId);
|
||||
}
|
||||
|
||||
private async moveMine(
|
||||
input: CleaningActor & { taskId: string },
|
||||
from: CleaningTaskStatus,
|
||||
@@ -293,6 +383,24 @@ export class CleaningTaskRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private async assertCleanerUserForTask(tenantId: string, taskId: string, cleanerUserId: string) {
|
||||
const [rows] = await this.pool.execute<RowDataPacket[]>(
|
||||
`SELECT 1
|
||||
FROM qipai_cleaning_tasks t
|
||||
INNER JOIN qipai_users u ON u.tenant_id = t.tenant_id AND u.id = ?
|
||||
AND u.status = 'ACTIVE' AND u.deleted_at IS NULL
|
||||
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.tenant_id = ur.tenant_id AND r.id = ur.role_id
|
||||
AND r.code = 'CLEANER' AND r.status = 'ACTIVE' AND r.deleted_at IS NULL
|
||||
INNER JOIN qipai_user_store_scopes ss ON ss.tenant_id = u.tenant_id
|
||||
AND ss.user_id = u.id AND ss.store_id = t.store_id
|
||||
WHERE t.tenant_id = ? AND t.id = ? AND t.deleted_at IS NULL
|
||||
LIMIT 1`,
|
||||
[cleanerUserId, tenantId, taskId]
|
||||
);
|
||||
if (!rows[0]) throw new CleaningTaskError('CLEANING_ASSIGNEE_INVALID');
|
||||
}
|
||||
|
||||
private async recordEvent(
|
||||
input: CleaningActor,
|
||||
taskId: string,
|
||||
|
||||
Reference in New Issue
Block a user