fix: 校验批量保存响应一致性

This commit is contained in:
2026-08-17 21:37:12 +08:00
parent 357ef89d3e
commit dc6341d24e
2 changed files with 117 additions and 8 deletions

View File

@@ -233,6 +233,99 @@ describe('batch-form-model', () => {
);
});
it('keeps every row when failures repeat the same known row', () => {
const rows = [createBatchRow(), createBatchRow()];
const failure = {
clientKey: rows[0]!.clientKey,
rowIndex: 1,
errorCode: 'INVALID',
message: '第一行失败',
};
const result = applyBatchResult(rows, {
successCount: 0,
failureCount: 2,
failures: [failure, failure],
});
const generalError = '批量保存结果无法匹配,请核对后重试';
expect(result.rows).toEqual(rows);
expect(result.rowErrors).toEqual(
new Map(rows.map((row) => [row.clientKey, generalError])),
);
});
it('keeps every row when unknown failure keys resolve to the same row', () => {
const rows = [createBatchRow(), createBatchRow()];
const result = applyBatchResult(rows, {
successCount: 0,
failureCount: 2,
failures: [
{
clientKey: 'unknown-a',
rowIndex: 1,
errorCode: 'INVALID_A',
message: '失败 A',
},
{
clientKey: 'unknown-b',
rowIndex: 1,
errorCode: 'INVALID_B',
message: '失败 B',
},
],
});
const generalError = '批量保存结果无法匹配,请核对后重试';
expect(result.rows).toEqual(rows);
expect(result.rowErrors).toEqual(
new Map(rows.map((row) => [row.clientKey, generalError])),
);
});
it('keeps every row when response counts do not cover all input rows', () => {
const rows = [createBatchRow(), createBatchRow()];
const result = applyBatchResult(rows, {
successCount: 0,
failureCount: 1,
failures: [
{
clientKey: rows[0]!.clientKey,
rowIndex: 1,
errorCode: 'INVALID',
message: '第一行失败',
},
],
});
const generalError = '批量保存结果无法匹配,请核对后重试';
expect(result.rows).toEqual(rows);
expect(result.rowErrors).toEqual(
new Map(rows.map((row) => [row.clientKey, generalError])),
);
});
it('keeps every row when failure count differs from failure details', () => {
const rows = [createBatchRow(), createBatchRow()];
const result = applyBatchResult(rows, {
successCount: 0,
failureCount: 2,
failures: [
{
clientKey: rows[0]!.clientKey,
rowIndex: 1,
errorCode: 'INVALID',
message: '第一行失败',
},
],
});
const generalError = '批量保存结果无法匹配,请核对后重试';
expect(result.rows).toEqual(rows);
expect(result.rowErrors).toEqual(
new Map(rows.map((row) => [row.clientKey, generalError])),
);
});
it('returns no rows or errors when every row succeeds', () => {
const result = applyBatchResult([createBatchRow()], {
successCount: 1,

View File

@@ -81,17 +81,29 @@ export function applyBatchResult(
result: LmsSubPackageRelationApi.BatchCreateResult,
): { rows: BatchFormRow[]; rowErrors: Map<string, string> } {
const generalError = '批量保存结果无法匹配,请核对后重试';
const unsafeResult = () => ({
rows,
rowErrors: new Map(rows.map((item) => [item.clientKey, generalError])),
});
const rowByClientKey = new Map<string, BatchFormRow>();
for (const row of rows) {
if (rowByClientKey.has(row.clientKey)) {
return {
rows,
rowErrors: new Map(rows.map((item) => [item.clientKey, generalError])),
};
return unsafeResult();
}
rowByClientKey.set(row.clientKey, row);
}
if (
!Number.isInteger(result.successCount) ||
result.successCount < 0 ||
!Number.isInteger(result.failureCount) ||
result.failureCount < 0 ||
result.successCount + result.failureCount !== rows.length ||
result.failureCount !== result.failures.length
) {
return unsafeResult();
}
if (result.failures.length === 0) {
return { rows: [], rowErrors: new Map() };
}
@@ -105,17 +117,21 @@ export function applyBatchResult(
(rowByKey && rowByIndex?.clientKey !== rowByKey.clientKey) ||
(!rowByKey && !rowByIndex)
) {
return {
rows,
rowErrors: new Map(rows.map((item) => [item.clientKey, generalError])),
};
return unsafeResult();
}
const matchedRow = rowByKey ?? rowByIndex!;
if (failedClientKeys.has(matchedRow.clientKey)) {
return unsafeResult();
}
failedClientKeys.add(matchedRow.clientKey);
rowErrors.set(matchedRow.clientKey, failure.message);
}
if (failedClientKeys.size !== result.failureCount) {
return unsafeResult();
}
return {
rows: rows.filter((row) => failedClientKeys.has(row.clientKey)),
rowErrors,