fix: 防止批量结果映射丢失输入

This commit is contained in:
2026-08-17 21:32:15 +08:00
parent 9e2c5d04d3
commit 934cde0034
2 changed files with 138 additions and 9 deletions

View File

@@ -74,6 +74,18 @@ describe('batch-form-model', () => {
});
});
it('reports a required field when its runtime value is missing', () => {
const row = createBatchRow();
const incompleteRow = {
...row,
containerName: undefined,
} as unknown as typeof row;
expect(validateBatchRows([incompleteRow]).get(0)).toMatchObject({
containerName: '子卷号不能为空',
});
});
it('does not report an error for a complete row', () => {
const row = Object.assign(createBatchRow(), {
packageBoxSn: 'BOX-001',
@@ -95,13 +107,13 @@ describe('batch-form-model', () => {
failures: [
{
clientKey: rows[2]!.clientKey,
rowIndex: 2,
rowIndex: 3,
errorCode: 'INVALID',
message: '第三行失败',
},
{
clientKey: rows[0]!.clientKey,
rowIndex: 0,
rowIndex: 1,
errorCode: 'DUPLICATE',
message: '第一行失败',
},
@@ -117,6 +129,94 @@ describe('batch-form-model', () => {
);
});
it('falls back to one-based row index when the failure client key is unknown', () => {
const rows = [createBatchRow(), createBatchRow()];
const result = applyBatchResult(rows, {
successCount: 1,
failureCount: 1,
failures: [
{
clientKey: 'unknown-key',
rowIndex: 2,
errorCode: 'INVALID',
message: '第二行失败',
},
],
});
expect(result.rows).toEqual([rows[1]]);
expect(result.rowErrors).toEqual(
new Map([[rows[1]!.clientKey, '第二行失败']]),
);
});
it('keeps every row when a known client key conflicts with row index', () => {
const rows = [createBatchRow(), createBatchRow()];
const result = applyBatchResult(rows, {
successCount: 1,
failureCount: 1,
failures: [
{
clientKey: rows[0]!.clientKey,
rowIndex: 2,
errorCode: 'CONFLICT',
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 an unknown client key has an invalid row index', () => {
const rows = [createBatchRow(), createBatchRow()];
const result = applyBatchResult(rows, {
successCount: 1,
failureCount: 1,
failures: [
{
clientKey: 'unknown-key',
rowIndex: 3,
errorCode: 'INVALID_INDEX',
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 input client keys are duplicated', () => {
const rows = [createBatchRow(), createBatchRow()];
rows[1]!.clientKey = rows[0]!.clientKey;
const result = applyBatchResult(rows, {
successCount: 1,
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('returns no rows or errors when every row succeeds', () => {
const result = applyBatchResult([createBatchRow()], {
successCount: 1,

View File

@@ -64,7 +64,7 @@ export function validateBatchRows(
rows.forEach((row, index) => {
const errors: BatchFieldErrors = {};
requiredFields.forEach(([field, message]) => {
if (row[field]?.trim().length === 0) {
if (!row[field]?.trim()) {
errors[field] = message;
}
});
@@ -80,12 +80,41 @@ export function applyBatchResult(
rows: BatchFormRow[],
result: LmsSubPackageRelationApi.BatchCreateResult,
): { rows: BatchFormRow[]; rowErrors: Map<string, string> } {
const failedClientKeys = new Set(
result.failures.map((failure) => failure.clientKey),
);
const rowErrors = new Map(
result.failures.map((failure) => [failure.clientKey, failure.message]),
);
if (result.failures.length === 0) {
return { rows: [], rowErrors: new Map() };
}
const 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])),
};
}
rowByClientKey.set(row.clientKey, row);
}
const failedClientKeys = new Set<string>();
const rowErrors = new Map<string, string>();
for (const failure of result.failures) {
const rowByKey = rowByClientKey.get(failure.clientKey);
const rowByIndex = rows[failure.rowIndex - 1];
if (
(rowByKey && rowByIndex?.clientKey !== rowByKey.clientKey) ||
(!rowByKey && !rowByIndex)
) {
return {
rows,
rowErrors: new Map(rows.map((item) => [item.clientKey, generalError])),
};
}
const matchedRow = rowByKey ?? rowByIndex!;
failedClientKeys.add(matchedRow.clientKey);
rowErrors.set(matchedRow.clientKey, failure.message);
}
return {
rows: rows.filter((row) => failedClientKeys.has(row.clientKey)),