变更问题修复

This commit is contained in:
andy committed 2026-09-14 16:34:36 +08:00
1 parent 6de4422733
commit 9625110f44
62 files changed
+18116 -95

No files matched your search

@@ -11,9 +11,13 @@ Chrome Manifest V3 扩展,在用户已登录的 LTJT ERP 页面内执行经过
## 当前版本
当前源码版本为 `0.5.180`。版本化 ZIP、文件哈希和 Skill/DOCX 基线见 [`../../dist/release-manifest.json`](../../dist/release-manifest.json)。旧版本实现流水已冻结在 [`../../archive/project-history/2026-08-16/chrome-extension-README.pre-governance.md`](../../archive/project-history/2026-08-16/chrome-extension-README.pre-governance.md)。
当前源码版本为 `0.5.182`。版本化 ZIP、文件哈希和 Skill/DOCX 基线见 [`../../dist/release-manifest.json`](../../dist/release-manifest.json)。旧版本实现流水已冻结在 [`../../archive/project-history/2026-08-16/chrome-extension-README.pre-governance.md`](../../archive/project-history/2026-08-16/chrome-extension-README.pre-governance.md)。
0.5.180 当前重点:
0.5.182 当前重点:
- 修复 0.5.181 安排变更的页面依赖漏注入:在 ERP MAIN world 先加载 operation-plans.js,再加载页面适配器。缺失日期组件单独报错,不再误报原安排为 0 条。回归测试从后台实际注入列表加载全新页面环境,覆盖四类日期变更;服务端已为 v1.0.12 时,本修复只需更新插件并刷新 ERP 页面。真实 ERP 验收仍待部署方执行。
- 酒店、用车、大交通和其他/备案变更支持仅填写原日期的月日,由指定团队中唯一原安排确定实际年份;完整年份精确匹配。多条候选显示日期供核对,找不到或不唯一时停止。服务端解析器与插件需同步更新,不新增数据库迁移。
- 用车、大交通、其他费用/备案新增与酒店共用空行选择,按原生 MaxI 和每行完整控件判定可用表单;已有安排时选择后续空行,原生选择、日期、数量和保存回查绑定同一条记录。保存前后保护原有安排;其他费用保留未填写的数量及团队备案信息。导游经操作方确认只能安排一位,已有导游需走“安排导游变更”。本项只需更新插件,无服务端运行逻辑或数据库变更;真实 ERP 验收待部署方完成。
@@ -39,6 +39,7 @@ const TIMED_PAGE_ACTIONS = new Set([
]);
const DEFAULT_PAGE_ACTION_FILES = Object.freeze([
'source-region.js',
'operation-plans.js',
'team-batch.js',
'product-lookup.js',
'split-plan-reconciliation.js',
@@ -2391,15 +2392,27 @@ function erpResolutionFailure(operation, report = {}) {
function lifecycleRouteFailure(operation, report = {}) {
const blockers = Array.isArray(report.blockers) ? report.blockers.map(String) : [];
if (blockers.includes('arrangement_date_matcher_unavailable')) {
return {
errorCode: 'arrangement_date_matcher_unavailable',
message: '插件日期匹配组件未加载,请更新插件并刷新 ERP 页面后重试;未写入 ERP。'
};
}
const targetCount = blockers.find((item) => /^arrangement_update_target_candidate_count:\d+$/.test(item));
if (targetCount) {
const count = Number(targetCount.split(':').at(-1));
const label = { arrangement_hotel: '入住及离店日期均匹配的酒店', arrangement_vehicle: '用车日期范围匹配的车辆', arrangement_transport: '交通日期匹配的大交通', arrangement_other: '业务日期匹配的其他/备案', arrangement_guide: '当前团队的导游' }[operation?.action] || '原';
const candidates = report.preflight?.resource_resolution?.candidates || [];
const candidateText = candidates.slice(0, 5).map((row, index) =>
`${index + 1}. ${row.date}${row.end_date ? ` 至 ${row.end_date}` : ''} ${row.resource || ''} ${row.item || ''}`.trim()).join('\n');
const guidance = count > 1 && candidateText
? `\n原安排候选${count > 5 ? '(仅显示前 5 条)' : ''}:\n${candidateText}\n请核对后,用所选原安排的完整年月日重新提交;同一完整日期仍有多条时,请在 ERP 中核对处理。`
: '';
return {
errorCode: 'arrangement_update_target_not_unique',
message: count === 0
? `ERP 中没有找到${label}安排,请核对原安排;未写入 ERP。`
: `ERP 中找到 ${count} 条${label}安排,无法唯一确定可变更的记录,请核对原安排;未写入 ERP。`
: `ERP 中找到 ${count} 条${label}安排,无法唯一确定可变更的记录,请核对原安排;未写入 ERP。${guidance}`
};
}
const noopBlocker = blockers.find((item) => item.startsWith('lifecycle_transition_target_already_current:'));
+44 -19
View File
@@ -3130,6 +3130,36 @@
};
}
function originalArrangementMatches(form, action, selection) {
const dates = globalThis.LTJTOperationPlans;
const indices = [...new Set(Array.from(form.elements || []).map((control) => /^id(\d+)$/.exec(control.name || control.id || '')?.[1]).filter(Boolean).map(Number))];
const ranged = ['arrangement_hotel', 'arrangement_vehicle'].includes(action);
return indices.filter((index) => {
const current = (name) => controlCanonicalValue(form, `${name}${index}`);
const start = strictIsoDate(current('riqi'));
const end = strictIsoDate(current('riqis'));
return current('id') && current('id') !== '0'
&& dates.matchesOriginalArrangementDate(start, ranged ? selection.start_date : selection.date)
&& (!ranged || (dates.matchesOriginalArrangementDate(end, selection.end_date)
&& (action === 'arrangement_hotel' ? end > start : end >= start)))
&& (action !== 'arrangement_other' || !selection.filing_date
|| dates.matchesOriginalArrangementDate(strictIsoDate(controlCanonicalValue(form, 'beianri')), selection.filing_date));
});
}
function originalArrangementCandidates(form, action, indices) {
const text = (value) => String(value || '').replace(/[\r\n\t]/g, ' ').slice(0, 100);
return indices.slice(0, 5).map((index) => {
const current = (name) => controlCanonicalValue(form, `${name}${index}`);
return {
date: strictIsoDate(current('riqi')),
...(['arrangement_hotel', 'arrangement_vehicle'].includes(action) ? { end_date: strictIsoDate(current('riqis')) } : {}),
resource: text(current('danwei')),
item: text(current(action === 'arrangement_hotel' ? 'fangxing' : action === 'arrangement_vehicle' ? 'chehao' : 'shuoming'))
};
});
}
function resolveArrangementExecutionOperation(form, operation = {}) {
const resolved = cloneOperation(operation);
const arrangement = { ...(resolved.data?.arrangement || {}) };
@@ -3142,6 +3172,13 @@
const blockers = [];
let evidence = null;
// Check the MAIN-world dependency before treating a failed lookup as an
// absent ERP record. Worker importScripts does not install page globals.
if (arrangement.mode === 'update' && !guide && (!hotel || arrangement.selection)
&& typeof globalThis.LTJTOperationPlans?.matchesOriginalArrangementDate !== 'function') {
return { ok: false, operation: resolved, blockers: ['arrangement_date_matcher_unavailable'], evidence: { value_redacted: true } };
}
if (!hotel && arrangement.mode === 'update') {
let slotIndex = 0;
const selection = arrangement.selection || {};
@@ -3149,14 +3186,8 @@
const existing = Array.from(form.elements || []).filter((control) => /^daoyouid\d+$/.test(control.name || control.id || '') && String(control.value || '').trim() && String(control.value).trim() !== '0');
if (existing.length !== 1 || (existing[0].name || existing[0].id) !== 'daoyouid0') return { ok: false, operation: resolved, blockers: [`arrangement_update_target_candidate_count:${existing.length}`], evidence: { value_redacted: true } };
} else {
const indices = [...new Set(Array.from(form.elements || []).map((control) => /^id(\d+)$/.exec(control.name || control.id || '')?.[1]).filter(Boolean).map(Number))];
const matching = indices.filter((index) => {
const id = controlCanonicalValue(form, `id${index}`);
return id && id !== '0' && (action === 'arrangement_vehicle'
? strictIsoDate(controlCanonicalValue(form, `riqi${index}`)) === selection.start_date && strictIsoDate(controlCanonicalValue(form, `riqis${index}`)) === selection.end_date
: strictIsoDate(controlCanonicalValue(form, `riqi${index}`)) === selection.date);
});
if (matching.length !== 1) return { ok: false, operation: resolved, blockers: [`arrangement_update_target_candidate_count:${matching.length}`], evidence: { value_redacted: true } };
const matching = originalArrangementMatches(form, action, selection);
if (matching.length !== 1) return { ok: false, operation: resolved, blockers: [`arrangement_update_target_candidate_count:${matching.length}`], evidence: { match_count: matching.length, candidates: originalArrangementCandidates(form, action, matching), value_redacted: true } };
slotIndex = matching[0];
}
const current = (name) => controlCanonicalValue(form, `${name}${slotIndex}`);
@@ -3191,7 +3222,7 @@
const value = controlCanonicalValue(form, name);
if (value) filing[key] = key === 'date' ? strictIsoDate(value) : value;
}
if (selection.filing_date && selection.filing_date !== filing.date) blockers.push('arrangement_update_original_filing_date_mismatch');
if (selection.filing_date && !globalThis.LTJTOperationPlans.matchesOriginalArrangementDate(filing.date, selection.filing_date)) blockers.push('arrangement_update_original_filing_date_mismatch');
arrangement.filing = filing;
}
const lookup = form.elements?.namedItem?.(`${guide ? 'daoyou' : 'danwei'}${slotIndex}`);
@@ -3216,14 +3247,8 @@
const selection = arrangement.selection;
let slotIndex = 0;
if (selection) {
const indices = [...new Set(Array.from(form.elements || []).map((control) => /^id(\d+)$/.exec(control.name || control.id || '')?.[1]).filter(Boolean).map(Number))];
const matching = indices.filter((index) => {
const id = controlCanonicalValue(form, `id${index}`);
return id && id !== '0'
&& strictIsoDate(controlCanonicalValue(form, `riqi${index}`)) === selection.start_date
&& strictIsoDate(controlCanonicalValue(form, `riqis${index}`)) === selection.end_date;
});
if (matching.length !== 1) return { ok: false, operation: resolved, blockers: [`arrangement_update_target_candidate_count:${matching.length}`], evidence: { match_count: matching.length, value_redacted: true } };
const matching = originalArrangementMatches(form, action, selection);
if (matching.length !== 1) return { ok: false, operation: resolved, blockers: [`arrangement_update_target_candidate_count:${matching.length}`], evidence: { match_count: matching.length, candidates: originalArrangementCandidates(form, action, matching), value_redacted: true } };
slotIndex = matching[0];
}
const current = (name) => controlCanonicalValue(form, `${name}${slotIndex}`);
@@ -3427,7 +3452,7 @@
if (blockers.length) {
return lifecycleRouteOutcome('lifecycle_route_blocked', operation, {
blockers,
preflight: { form_path: formDocument.location.pathname, expected_tid: list.tid }
preflight: { form_path: formDocument.location.pathname, expected_tid: list.tid, resource_resolution: arrangementResolution.evidence }
});
}
return lifecycleRouteOutcome('lifecycle_route_ready', arrangementResolution.operation, {
@@ -6518,7 +6543,7 @@
}
window.LTJTOrderAssistant = {
version: '0.5.180',
version: '0.5.182',
resolveNativeListSearchValues,
lookupKeywordMatchesText,
inspectLifecycleSearchCriteria: lifecycleSearchCriteria,
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "联泰下单助手",
"version": "0.5.180",
"version": "0.5.182",
"description": "在已登录 LTJT ERP 页面内规划并受控执行联泰 ERP 业务操作。",
"permissions": [
"activeTab",
@@ -1172,6 +1172,25 @@
if (manualReviewTargets.length) warnings.push(`已解析但尚未纳入当前自动写入白名单的字段将转人工复核:${[...new Set(manualReviewTargets)].join('、')}。`);
}
// Original selectors preserve an omitted year. Only ERP records may supply it.
function isOriginalArrangementDate(value) {
if (typeof value !== 'string') return false;
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return isValidDate(value);
return /^\d{2}-\d{2}$/.test(value) && isValidDate(`2000-${value}`);
}
function isOriginalArrangementRange(start, end, allowSameDay = false) {
if (!isOriginalArrangementDate(start) || !isOriginalArrangementDate(end)) return false;
// Partial ranges may cross December/January; ordering is checked on the ERP row.
return start.length !== 10 || end.length !== 10 || (allowSameDay ? end >= start : end > start);
}
function matchesOriginalArrangementDate(actual, selector) {
return typeof actual === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(actual)
&& isValidDate(actual) && isOriginalArrangementDate(selector)
&& (selector.length === 10 ? actual === selector : actual.slice(5) === selector);
}
function validateOriginalArrangementUpdate(value, action, errors) {
const guide = action === 'arrangement_guide';
const vehicle = action === 'arrangement_vehicle';
@@ -1192,9 +1211,9 @@
const dateKeys = vehicle ? ['start_date', 'end_date'] : ['date', ...(other ? ['filing_date'] : [])];
if (!isPlainObject(selection) || Object.keys(selection).some((key) => !dateKeys.includes(key))) errors.push('安排变更需要原安排日期。');
if (vehicle) {
if (!isValidDate(selection?.start_date) || !isValidDate(selection?.end_date) || selection.end_date < selection.start_date) errors.push('原用车日期范围无效。');
} else if (!isValidDate(selection?.date)) errors.push('原业务日期无效。');
if (selection?.filing_date !== undefined && !isValidDate(selection.filing_date)) errors.push('原备案日期无效。');
if (!isOriginalArrangementRange(selection?.start_date, selection?.end_date, true)) errors.push('原用车日期范围无效。');
} else if (!isOriginalArrangementDate(selection?.date)) errors.push('原业务日期无效。');
if (selection?.filing_date !== undefined && !isOriginalArrangementDate(selection.filing_date)) errors.push('原备案日期无效。');
}
if (changes.filing !== undefined) {
if (!other || !isPlainObject(changes.filing) || !Object.keys(changes.filing).length
@@ -1232,7 +1251,7 @@
if (value.selection !== undefined) {
const selection = value.selection;
if (!isPlainObject(selection) || Object.keys(selection).some((key) => !['start_date', 'end_date'].includes(key))
|| !isValidDate(selection.start_date) || !isValidDate(selection.end_date) || selection.end_date <= selection.start_date) blockers.push('酒店变更必须提供有效的原安排日期。');
|| !isOriginalArrangementRange(selection.start_date, selection.end_date)) blockers.push('酒店变更必须提供有效的原安排日期。');
}
if (changes?.resource !== undefined) {
validateDispatchNamedRef(changes.resource, 'data.arrangement.changes.resource', blockers, { required: true });
@@ -1848,6 +1867,9 @@
}
return {
isOriginalArrangementDate,
isOriginalArrangementRange,
matchesOriginalArrangementDate,
OPERATION_DEFINITIONS,
ARRANGEMENT_ACTIONS,
CONTRACT_VERSION,
@@ -944,7 +944,7 @@
window.LTJTOrderAssistant = {
...(window.LTJTOrderAssistant || {}),
version: '0.5.180',
version: '0.5.182',
openTeamBatchForm,
pingTeamBatchFrame,
preflightTeamBatchNative,