fix(api): 避免恢复后二次确认重复执行已完成平台

This commit is contained in:
renzhiye 2026-04-03 13:44:43 +08:00
parent fba6dd5272
commit fff35a157c
2 changed files with 121 additions and 4 deletions

View File

@ -317,6 +317,97 @@ describe("API server", () => {
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 () => {
const app = createServer();
await app.ready();

View File

@ -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(
taskId: string,
timestamp: string
@ -452,9 +462,16 @@ export class InMemoryTaskStore {
const selectedCandidateIds = selectionMap.get(run.platform) ?? [];
if (selectedCandidateIds.length > 0) {
run.status = "Selected";
const selectionChanged = !hasSameCandidateSelection(
run.selectedCandidateIds,
selectedCandidateIds
);
run.selectedCandidateIds = selectedCandidateIds;
if (run.status !== "Completed" || selectionChanged) {
run.status = "Selected";
run.lastUpdatedAt = nowIso();
}
} else if (run.status === "AwaitingSelection") {
run.status = "Skipped";
run.selectedCandidateIds = [];
@ -466,16 +483,25 @@ export class InMemoryTaskStore {
task.updatedAt = nowIso();
this.pushEvent(task, "task.confirmed", "候选确认已提交。");
const selectedRuns = task.platformRuns.filter(
const confirmedRuns = task.platformRuns.filter(
(run) => run.selectedCandidateIds.length > 0
);
if (selectedRuns.length === 0) {
if (confirmedRuns.length === 0) {
task.taskStatus = "NoSelection";
this.pushEvent(task, "task.no_selection", "用户未确认任何商品链接,任务结束。");
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.taskStage = "session_check";
this.pushEvent(task, "task.running", "系统开始执行抓取前校验。");