优化回执排序

This commit is contained in:
andy committed 2026-09-22 18:44:41 +08:00
1 parent 1797c713af
commit a7c9cc358a
7 files changed
+168 -36

No files matched your search

+23 -11
View File
@@ -48,6 +48,17 @@ function firstText(...values: unknown[]): string {
return texts(...values)[0] || '';
}
function businessText(lines: string[]): string {
const [title, ...details] = lines.filter(Boolean);
return [title, details.join('\n')].filter(Boolean).join('\n\n');
}
function labelledList(label: string, values: string[]): string {
return values.length > 1
? `${label}:\n${values.map(value => ` ${value}`).join('\n')}`
: `${label}:${values[0]}`;
}
function firstNumber(...values: unknown[]): number | null {
for (const value of values) {
if (value === undefined || value === null || value === '') continue;
@@ -144,7 +155,7 @@ function identifierLine(
|| refs.kind === 'shared_child_order';
if (child && orderNumbers.length) {
return {
text: `子单号:${orderNumbers.join('、')}`,
text: labelledList('子单号', orderNumbers),
identifier: orderNumbers[0],
groupNumbers,
orderNumbers
@@ -152,7 +163,7 @@ function identifierLine(
}
if (groupNumbers.length) {
return {
text: `团号:${groupNumbers.join('、')}`,
text: labelledList('团号', groupNumbers),
identifier: groupNumbers[0],
groupNumbers,
orderNumbers
@@ -160,7 +171,7 @@ function identifierLine(
}
if (orderNumbers.length) {
return {
text: `订单号:${orderNumbers.join('、')}`,
text: labelledList('订单号', orderNumbers),
identifier: orderNumbers[0],
groupNumbers,
orderNumbers
@@ -303,6 +314,7 @@ function hotelNewBookingReply(
`IN. ${englishDate(values.start)}`,
`OUT ${englishDate(values.end)}`,
`${nights}${nightLabel} ${values.roomCount}${englishRoom}`,
'',
'②新订单',
`团号:${identifier}${customerSuffix}`,
`入住日期:${chineseDate(values.start)}`,
@@ -413,7 +425,7 @@ function arrangementReply(
const id = identifier ? `团号:${identifier}` : '';
if (mode === 'clear') {
return {
text: [`${actionTitle(action).replace('成功', '已清除')}`, id].filter(Boolean).join('\n'),
text: businessText([`${actionTitle(action).replace('成功', '已清除')}`, id]),
profile: 'standard-arrangement-v1',
fields: { identifier, mode: 'clear' }
};
@@ -448,7 +460,7 @@ function arrangementReply(
else if (date) lines.push(`日期:${date}`);
if (quantity !== null) lines.push(action === 'arrangement_hotel' ? `房数:${quantity}间` : `数量:${quantity}`);
return {
text: lines.filter(Boolean).join('\n'),
text: businessText(lines),
profile: 'standard-arrangement-v1',
fields: { identifier, resource: text(ref.name), item, start_date: start, end_date: end, date, quantity, mode: mode || 'create' }
};
@@ -506,7 +518,7 @@ export function buildCustomerSuccessReply(
fields = { ...fields, ...reply.fields };
} else if (action.startsWith('order_update_')) {
const lines = [actionTitle(action), identity.text, ...updateLines(operationValue, result, context)].filter(Boolean);
textValue = lines.join('\n');
textValue = businessText(lines);
profile = 'standard-update-v1';
fields = { ...fields, updated_fields: updateLines(operationValue, result, context) };
} else if (action === 'passenger_list_import') {
@@ -520,7 +532,7 @@ export function buildCustomerSuccessReply(
object(data.passenger_list).row_count,
requery.expected_persisted_count
);
textValue = [actionTitle(action), identity.text, count === null ? '' : `本次导入:${count}人`].filter(Boolean).join('\n');
textValue = businessText([actionTitle(action), identity.text, count === null ? '' : `本次导入:${count}人`]);
profile = 'standard-passenger-import-v1';
fields = { ...fields, imported_count: count };
} else if (action === 'order_cancel' || action === 'order_restore') {
@@ -531,19 +543,19 @@ export function buildCustomerSuccessReply(
&& result.side_effects.some((value) => /child|子单/iu.test(String(value)))
? '子单状态已同步'
: '';
textValue = [actionTitle(action), identity.text, targetStatus ? `目标状态:${targetStatus}` : '', sideEffect].filter(Boolean).join('\n');
textValue = businessText([actionTitle(action), identity.text, targetStatus ? `目标状态:${targetStatus}` : '', sideEffect]);
profile = 'standard-lifecycle-v1';
fields = { ...fields, target_status: targetStatus, side_effect: sideEffect };
} else if (action === 'confirmation_export') {
const names = attachments.map((item) => item.name);
textValue = [actionTitle(action), identity.text, names.length ? `文件:${names.join('、')}` : '文件已保存到任务附件'].filter(Boolean).join('\n');
textValue = businessText([actionTitle(action), identity.text, names.length ? labelledList('文件', names) : '文件已保存到任务附件']);
profile = 'standard-file-delivery-v1';
fields = { ...fields, file_names: names, attachment_count: attachments.length };
} else if (['team_order_create', 'team_order_batch_create', 'shared_plan_create', 'shared_child_order_create', 'shared_child_order_batch_create'].includes(action)) {
textValue = [actionTitle(action), identity.text].filter(Boolean).join('\n');
textValue = businessText([actionTitle(action), identity.text]);
profile = 'standard-newbooking-v1';
} else {
textValue = [actionTitle(action), identity.text].filter(Boolean).join('\n');
textValue = businessText([actionTitle(action), identity.text]);
}
if (!textValue) return null;
+22 -17
View File
@@ -146,16 +146,18 @@ function displayTime(value: string): string {
hour: '2-digit', minute: '2-digit' }).format(new Date(value));
}
function arrangementLine(action: string, details: Json): string {
const fields = [txt(details.resource_name), txt(details.item),
details.start_date ? `${txt(details.start_date)} 至 ${txt(details.end_date) || '离店/结束日未记录'}` : txt(details.date),
details.quantity === undefined ? '' : `${txt(details.quantity)}${action === 'arrangement_hotel' ? '间' : '(数量)'}`,
const hotel = action === 'arrangement_hotel';
const fields = [txt(details.resource_name), details.item ? `${hotel ? '房型' : '项目'}:${txt(details.item)}` : '',
details.start_date ? `${hotel ? '入住日期' : '开始日期'}:${txt(details.start_date)}` : details.date ? `日期:${txt(details.date)}` : '',
details.start_date ? `${hotel ? '离店日期' : '结束日期'}:${txt(details.end_date) || '未记录'}` : '',
details.quantity === undefined ? '' : `${hotel ? '房数' : '数量'}:${txt(details.quantity)}${hotel ? '间' : ''}`,
details.phone ? `${action === 'arrangement_vehicle' ? '司机电话' : '电话'}:${txt(details.phone)}` : '',
details.vehicle_number ? `车号:${txt(details.vehicle_number)}` : '', details.grade ? `等级:${txt(details.grade)}` : '',
details.filing_number ? `备案号:${txt(details.filing_number)}` : '', details.filing_date ? `备案日期:${txt(details.filing_date)}` : '',
details.filing_entry_port ? `入境口岸:${txt(details.filing_entry_port)}` : '', details.filing_exit_port ? `出境口岸:${txt(details.filing_exit_port)}` : '',
details.remark ? `备注:${txt(details.remark)}` : ''].filter(Boolean);
if (!details.resource_name) fields.unshift('资源名称未记录');
return fields.length > 1 ? fields.join('|') : '已安排,明细未记录';
return fields.length > 1 || details.resource_name ? fields.join('\n ') : '已安排,明细未记录';
}
export function teamProgressText(value: unknown): string {
@@ -172,49 +174,52 @@ export function teamProgressText(value: unknown): string {
if (!facts.product && facts.product_condition) lines.push(`产品条件:${txt(facts.product_condition)}`);
const status = latest(team, e => e.action === 'order_cancel' || e.action === 'order_restore'
|| ['team_order_create','team_order_batch_create','shared_plan_create'].includes(e.action));
lines.push(`订单状态:${status ? txt(status.details.order_status) || '已创建' : '暂无本系统状态记录'}`);
lines.push('', '【办理进度】', `订单状态:${status ? txt(status.details.order_status) || '已创建' : '暂无本系统状态记录'}`);
const subjects = [...new Set(events.filter(e => e.subject_kind === 'child').map(e => e.subject_number))].sort();
const shared = facts.team_kind === 'shared_plan' || subjects.length > 0;
const creation = latest(team, e => ['team_order_create','team_order_batch_create','shared_plan_create'].includes(e.action));
lines.push(`${shared ? '建计划' : '下单'}:${creation ? `已完成(${displayTime(creation.effective_at)})` : '暂无本系统成功记录'}`);
lines.push(`${shared ? '建计划' : '下单'}:${creation ? `已完成\n 时间:${displayTime(creation.effective_at)}` : '暂无本系统成功记录'}`);
const subjectProgress = (subset: ProgressEvent[], label: string, parent = false): void => {
const roster = latest(subset, e => e.action === 'passenger_list_import');
if (!parent) lines.push(`${label}名单:${roster ? `已导入,最近一次${roster.details.roster_count === undefined ? '人数未记录' : `${roster.details.roster_count}人`}(${displayTime(roster.effective_at)})` : '暂无本系统导入记录'}`);
if (!parent) lines.push('', `${label}名单:${roster ? `已导入,最近一次${roster.details.roster_count === undefined ? '人数未记录' : `${roster.details.roster_count}人`}\n 时间:${displayTime(roster.effective_at)}` : '暂无本系统导入记录'}`);
const update = latest(subset, e => e.action.startsWith('order_update_'));
lines.push(`${label}最近修改:${update ? `${list(update.details.updates).map(u => `${txt(u.label)}${u.mode === 'append' ? '追加' : '改为'}${txt(u.value)}`).join(';') || '修改成功,明细未记录'}(${displayTime(update.effective_at)})` : '暂无本系统修改记录'}`);
lines.push('', `${label}最近修改:${update ? `\n ${list(update.details.updates).map(u => `${txt(u.label)}${u.mode === 'append' ? '追加' : '改为'}${txt(u.value)}`).join('\n ') || '修改成功,明细未记录'}\n 时间:${displayTime(update.effective_at)}` : '暂无本系统修改记录'}`);
const exports = new Map<string, ProgressEvent>();
for (const e of subset) if (e.action === 'confirmation_export') for (const f of strings(e.details.files)) exports.set(f, e);
const unknownExport = latest(subset, e => e.action === 'confirmation_export' && !strings(e.details.files).length);
if (!exports.size && !unknownExport) lines.push(`${label}文件:暂无本系统导出记录`);
if (unknownExport) lines.push(`${label}文件:已导出,文件类型未记录(${displayTime(unknownExport.effective_at)})`);
if (!exports.size && !unknownExport) lines.push('', `${label}文件:暂无本系统导出记录`);
if (unknownExport) lines.push('', `${label}文件:已导出,文件类型未记录\n 时间:${displayTime(unknownExport.effective_at)}`);
for (const [file, e] of exports) {
const changed = events.some(change => change.action !== 'confirmation_export' && change.effective_at > e.effective_at
&& (change.subject_kind === 'team' || subset.includes(change) || (file === 'visitor-list' && shared)));
lines.push(`${label}${parent && file === 'visitor-list' ? '整团游客信息' : FILES[file] || '团队文件'}:已导出(${displayTime(e.effective_at)})${changed ? ';之后有业务变更,可能需要重新导出' : ''}`);
lines.push('', `${label}${parent && file === 'visitor-list' ? '整团游客信息' : FILES[file] || '团队文件'}:已导出\n 时间:${displayTime(e.effective_at)}${changed ? '\n 之后有业务变更,可能需要重新导出' : ''}`);
}
};
if (!shared) subjectProgress(team, '');
else {
lines.push(`本系统记录子单:${subjects.length}个(不代表母团全部子单)`);
lines.push('', `本系统记录子单:${subjects.length}个`, '(不代表母团全部子单)');
// Parent modifications and whole-group exports belong to the parent.
subjectProgress(team, '母团', true);
for (const child of subjects) {
const subset = events.filter(e => e.subject_kind === 'child' && e.subject_number === child);
const created = subset.some(e => ['shared_child_order_create','shared_child_order_batch_create'].includes(e.action));
const state = latest(subset, e => ['order_cancel','order_restore','shared_child_order_create','shared_child_order_batch_create'].includes(e.action));
lines.push(`子单 ${child}:${created ? '已新增' : '暂无本系统新增记录'};状态:${state ? txt(state.details.order_status) || '已创建' : '未记录'}`);
lines.push('', `子单 ${child}:${created ? '已新增' : '暂无本系统新增记录'};状态:${state ? txt(state.details.order_status) || '已创建' : '未记录'}`);
subjectProgress(subset, ` ${child} `);
}
}
lines.push('【安排情况与明细】');
lines.push('', '【安排情况与明细】');
for (const [action, label] of Object.entries(ARRANGEMENT_LABELS)) {
const items = entry.arrangements.filter(a => a.action === action);
lines.push(`${label}:${items.length ? `已安排,共${items.length}条` : '未安排'}`);
items.forEach((a,i) => lines.push(` ${i+1}. ${arrangementLine(action, a.details)}`));
lines.push('', `${label}:${items.length ? `已安排,共${items.length}条` : '未安排'}`);
items.forEach((a,i) => {
if (i > 0) lines.push('');
lines.push(` ${i+1}. ${arrangementLine(action, a.details)}`);
});
}
}
if (!snap.groups.length || snap.unlinked_subjects.length) lines.push(`\n${snap.unlinked_subjects.length ? `对象:${snap.unlinked_subjects.join('、')}。` : ''}暂无法关联团号,未推断安排状态。`);
lines.push('\n仅统计当前账号通过本系统成功完成的操作;未记录不代表 ERP 中没有。已安排表示至少一条,不代表全行程已安排齐全。');
lines.push('\n仅统计当前账号通过本系统成功完成的操作。', '未记录不代表 ERP 中没有。', '已安排表示至少一条,不代表全行程已安排齐全。');
return lines.join('\n');
}
+9 -8
View File
@@ -43,6 +43,7 @@ test('hotel arrangement uses the customer bilingual booking template', () => {
'IN. 03/06/2026',
'OUT 04/06/2026',
'1Night. 6Twin',
'',
'②新订单',
'团号:LW-260603Z-A 老挝联泰',
'入住日期:2026.06.03',
@@ -65,7 +66,7 @@ test('hotel clear and update replies stay business-readable when the booking tem
}
);
assert.ok(clearReply);
assert.equal(clearReply.text, '酒店安排已清除\n团号:LW-260603Z-A');
assert.equal(clearReply.text, '酒店安排已清除\n\n团号:LW-260603Z-A');
const updateReply = buildCustomerSuccessReply(
{ success: true, group_number: 'LW-260603Z-A' },
@@ -79,7 +80,7 @@ test('hotel clear and update replies stay business-readable when the booking tem
}
);
assert.ok(updateReply);
assert.equal(updateReply.text, '酒店安排成功\n团号:LW-260603Z-A\n退房日期:2026-06-05\n房数:9间');
assert.equal(updateReply.text, '酒店安排成功\n\n团号:LW-260603Z-A\n退房日期:2026-06-05\n房数:9间');
});
test('hotel replacement reply describes the new hotel and count while retaining the original dates', () => {
@@ -100,7 +101,7 @@ test('hotel replacement reply describes the new hotel and count while retaining
}
}
);
assert.equal(reply?.text, '酒店安排成功\n团号:LW-TEST-A\n资源:测试新酒店\n入住日期:2026-09-15\n退房日期:2026-09-16\n房数:9间');
assert.equal(reply?.text, '酒店安排成功\n\n团号:LW-TEST-A\n资源:测试新酒店\n入住日期:2026-09-15\n退房日期:2026-09-16\n房数:9间');
assert.equal(reply?.fields.resource, '测试新酒店');
assert.equal(reply?.fields.quantity, 9);
});
@@ -112,7 +113,7 @@ test('batch newbooking receipt preserves every group number', () => {
{ action: 'team_order_batch_create', data: {} }
);
assert.ok(reply);
assert.equal(reply.text, '批量下单成功\n团号:LW-260603A-A、LW-260605A-A');
assert.equal(reply.text, '批量下单成功\n\n团号:\n LW-260603A-A\n LW-260605A-A');
});
test('update and file replies expose business fields without technical evidence', () => {
@@ -128,7 +129,7 @@ test('update and file replies expose business fields without technical evidence'
}
);
assert.ok(updateReply);
assert.equal(updateReply.text, '散拼母团信息修改成功\n团号:LW-260603A-A\n计划收客数:45');
assert.equal(updateReply.text, '散拼母团信息修改成功\n\n团号:LW-260603A-A\n计划收客数:45');
assert.doesNotMatch(updateReply.text, /sha256|verification|blocker|execution/iu);
const baseReceipt = buildCustomerSuccessReceipt(
@@ -146,7 +147,7 @@ test('update and file replies expose business fields without technical evidence'
},
{ action: 'confirmation_export', data: { existing_refs: { identifier: 'LW-260603A-A' } } }
);
assert.equal(replyTextFromReceipt(baseReceipt), '团队文件已准备\n团号:LW-260603A-A\n文件:liantai-confirm.docx');
assert.equal(replyTextFromReceipt(baseReceipt), '团队文件已准备\n\n团号:LW-260603A-A\n文件:liantai-confirm.docx');
assert.deepEqual(replyAttachmentsFromReceipt(baseReceipt), [{
name: 'liantai-confirm.docx',
content_type: 'application/msword',
@@ -181,7 +182,7 @@ test('visitor export reply exposes only the mobile XLSX delivery artifact', () =
},
{ action: 'confirmation_export', data: { existing_refs: { identifier: 'LW-260816Z-A' } } }
);
assert.equal(replyTextFromReceipt(receipt!), '团队文件已准备\n团号:LW-260816Z-A\n文件:LW-260816Z-A Visitor.xlsx');
assert.equal(replyTextFromReceipt(receipt!), '团队文件已准备\n\n团号:LW-260816Z-A\n文件:LW-260816Z-A Visitor.xlsx');
assert.deepEqual(replyAttachmentsFromReceipt(receipt!), [{
name: 'LW-260816Z-A Visitor.xlsx',
content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
@@ -206,7 +207,7 @@ test('legacy ERP mojibake filenames are repaired when a stored receipt is read',
},
{ action: 'confirmation_export', data: { existing_refs: { identifier: 'LW-261202A-A' } } }
);
assert.equal(replyTextFromReceipt(receipt), '团队文件已准备\n团号:LW-261202A-A\n文件:LLW-261202A-A 客户确认书.doc');
assert.equal(replyTextFromReceipt(receipt), '团队文件已准备\n\n团号:LW-261202A-A\n文件:LLW-261202A-A 客户确认书.doc');
assert.deepEqual(replyAttachmentsFromReceipt(receipt), [{
name: 'LLW-261202A-A 客户确认书.doc',
content_type: 'application/msword',