fix(api): 避免恢复后二次确认重复执行已完成平台
This commit is contained in:
parent
fba6dd5272
commit
fff35a157c
@ -317,6 +317,97 @@ describe("API server", () => {
|
|||||||
await app.close();
|
await app.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not rerun completed platforms when confirming a newly recovered platform", async () => {
|
||||||
|
const app = createServer();
|
||||||
|
await app.ready();
|
||||||
|
|
||||||
|
const createdTask = await createTask(app, "iPhone 15 Pro");
|
||||||
|
const firstCandidatesResponse = await app.inject({
|
||||||
|
method: "GET",
|
||||||
|
url: `/api/tasks/${createdTask.taskId}/candidates`
|
||||||
|
});
|
||||||
|
const firstCandidates = firstCandidatesResponse.json().candidates;
|
||||||
|
|
||||||
|
const firstConfirmResponse = await app.inject({
|
||||||
|
method: "POST",
|
||||||
|
url: `/api/tasks/${createdTask.taskId}/confirm`,
|
||||||
|
payload: {
|
||||||
|
selections: [
|
||||||
|
{
|
||||||
|
platform: "tmall",
|
||||||
|
candidateIds: [firstCandidates.tmall[0].candidateId]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(firstConfirmResponse.statusCode).toBe(200);
|
||||||
|
expect(firstConfirmResponse.json().task.taskStatus).toBe("Completed");
|
||||||
|
expect(firstConfirmResponse.json().task.reportVersions).toEqual([1]);
|
||||||
|
|
||||||
|
const attemptsAfterFirstConfirm = await app.inject({
|
||||||
|
method: "GET",
|
||||||
|
url: `/api/tasks/${createdTask.taskId}/strategy-attempts`
|
||||||
|
});
|
||||||
|
const firstTmallExecutionAttempts = attemptsAfterFirstConfirm
|
||||||
|
.json()
|
||||||
|
.attempts.filter(
|
||||||
|
(attempt: { platform: string; capability: string }) =>
|
||||||
|
attempt.platform === "tmall" &&
|
||||||
|
(attempt.capability === "detail" || attempt.capability === "reviews")
|
||||||
|
);
|
||||||
|
expect(firstTmallExecutionAttempts).toHaveLength(2);
|
||||||
|
|
||||||
|
await preparePlatform(app, "jd");
|
||||||
|
const retryResponse = await app.inject({
|
||||||
|
method: "POST",
|
||||||
|
url: `/api/tasks/${createdTask.taskId}/platforms/jd/retry`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retryResponse.statusCode).toBe(200);
|
||||||
|
|
||||||
|
const recoveredCandidatesResponse = await app.inject({
|
||||||
|
method: "GET",
|
||||||
|
url: `/api/tasks/${createdTask.taskId}/candidates`
|
||||||
|
});
|
||||||
|
const recoveredCandidates = recoveredCandidatesResponse.json().candidates;
|
||||||
|
|
||||||
|
const secondConfirmResponse = await app.inject({
|
||||||
|
method: "POST",
|
||||||
|
url: `/api/tasks/${createdTask.taskId}/confirm`,
|
||||||
|
payload: {
|
||||||
|
selections: [
|
||||||
|
{
|
||||||
|
platform: "jd",
|
||||||
|
candidateIds: [recoveredCandidates.jd[0].candidateId]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(secondConfirmResponse.statusCode).toBe(200);
|
||||||
|
expect(secondConfirmResponse.json().task.taskStatus).toBe("Completed");
|
||||||
|
expect(secondConfirmResponse.json().task.reportVersions).toEqual([1, 2]);
|
||||||
|
|
||||||
|
const attemptsAfterSecondConfirm = await app.inject({
|
||||||
|
method: "GET",
|
||||||
|
url: `/api/tasks/${createdTask.taskId}/strategy-attempts`
|
||||||
|
});
|
||||||
|
const executionAttempts = attemptsAfterSecondConfirm.json().attempts.filter(
|
||||||
|
(attempt: { platform: string; capability: string }) =>
|
||||||
|
attempt.capability === "detail" || attempt.capability === "reviews"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
executionAttempts.filter((attempt: { platform: string }) => attempt.platform === "tmall")
|
||||||
|
).toHaveLength(2);
|
||||||
|
expect(
|
||||||
|
executionAttempts.filter((attempt: { platform: string }) => attempt.platform === "jd")
|
||||||
|
).toHaveLength(2);
|
||||||
|
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
it("records recovery audit entries and retry metrics for recovered platforms", async () => {
|
it("records recovery audit entries and retry metrics for recovered platforms", async () => {
|
||||||
const app = createServer();
|
const app = createServer();
|
||||||
await app.ready();
|
await app.ready();
|
||||||
|
|||||||
@ -46,6 +46,16 @@ function createPlatformCandidatesRecord(): Record<PlatformId, CandidateRecord[]>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasSameCandidateSelection(current: string[], next: string[]): boolean {
|
||||||
|
if (current.length !== next.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSorted = [...current].sort();
|
||||||
|
const nextSorted = [...next].sort();
|
||||||
|
return currentSorted.every((candidateId, index) => candidateId === nextSorted[index]);
|
||||||
|
}
|
||||||
|
|
||||||
function createPlatformRunMetricsRecord(
|
function createPlatformRunMetricsRecord(
|
||||||
taskId: string,
|
taskId: string,
|
||||||
timestamp: string
|
timestamp: string
|
||||||
@ -452,9 +462,16 @@ export class InMemoryTaskStore {
|
|||||||
const selectedCandidateIds = selectionMap.get(run.platform) ?? [];
|
const selectedCandidateIds = selectionMap.get(run.platform) ?? [];
|
||||||
|
|
||||||
if (selectedCandidateIds.length > 0) {
|
if (selectedCandidateIds.length > 0) {
|
||||||
run.status = "Selected";
|
const selectionChanged = !hasSameCandidateSelection(
|
||||||
|
run.selectedCandidateIds,
|
||||||
|
selectedCandidateIds
|
||||||
|
);
|
||||||
run.selectedCandidateIds = selectedCandidateIds;
|
run.selectedCandidateIds = selectedCandidateIds;
|
||||||
|
|
||||||
|
if (run.status !== "Completed" || selectionChanged) {
|
||||||
|
run.status = "Selected";
|
||||||
run.lastUpdatedAt = nowIso();
|
run.lastUpdatedAt = nowIso();
|
||||||
|
}
|
||||||
} else if (run.status === "AwaitingSelection") {
|
} else if (run.status === "AwaitingSelection") {
|
||||||
run.status = "Skipped";
|
run.status = "Skipped";
|
||||||
run.selectedCandidateIds = [];
|
run.selectedCandidateIds = [];
|
||||||
@ -466,16 +483,25 @@ export class InMemoryTaskStore {
|
|||||||
task.updatedAt = nowIso();
|
task.updatedAt = nowIso();
|
||||||
this.pushEvent(task, "task.confirmed", "候选确认已提交。");
|
this.pushEvent(task, "task.confirmed", "候选确认已提交。");
|
||||||
|
|
||||||
const selectedRuns = task.platformRuns.filter(
|
const confirmedRuns = task.platformRuns.filter(
|
||||||
(run) => run.selectedCandidateIds.length > 0
|
(run) => run.selectedCandidateIds.length > 0
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedRuns.length === 0) {
|
if (confirmedRuns.length === 0) {
|
||||||
task.taskStatus = "NoSelection";
|
task.taskStatus = "NoSelection";
|
||||||
this.pushEvent(task, "task.no_selection", "用户未确认任何商品链接,任务结束。");
|
this.pushEvent(task, "task.no_selection", "用户未确认任何商品链接,任务结束。");
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedRuns = task.platformRuns.filter((run) => run.status === "Selected");
|
||||||
|
|
||||||
|
if (selectedRuns.length === 0) {
|
||||||
|
task.taskStatus = deriveTaskStatusFromConfirmedPlatforms(task.platformRuns);
|
||||||
|
task.updatedAt = nowIso();
|
||||||
|
this.publishReportIfNeeded(task);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
task.taskStatus = "Running";
|
task.taskStatus = "Running";
|
||||||
task.taskStage = "session_check";
|
task.taskStage = "session_check";
|
||||||
this.pushEvent(task, "task.running", "系统开始执行抓取前校验。");
|
this.pushEvent(task, "task.running", "系统开始执行抓取前校验。");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user