优化回执排序
This commit is contained in:
1 parent
1797c713af
commit
a7c9cc358a
7 files changed
+168
-36
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user