Files
LWLT-AIBOT/chrome-extension/ltjt-order-assistant/team-batch-inpage.js
T
2026-09-22 18:11:28 +08:00

955 lines
46 KiB
JavaScript

(function installLTJTNativeTeamBatchAdapter() {
'use strict';
const helpers = window.LTJTTeamBatchHelpers || {};
const productLookup = window.LTJTProductLookup || {};
const sourceRegion = window.LTJTSourceRegion || {};
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function waitFor(condition, timeoutMs = 20000, intervalMs = 250) {
const deadline = Date.now() + timeoutMs;
let last = null;
while (Date.now() < deadline) {
last = await condition();
if (last) return last;
await sleep(intervalMs);
}
return last;
}
function sameOriginDocuments(rootDocument = document, depth = 0, output = []) {
if (!rootDocument || depth > 6) return output;
output.push(rootDocument);
Array.from(rootDocument.querySelectorAll?.('iframe,frame') || []).forEach((frame) => {
try {
if (frame.contentDocument) sameOriginDocuments(frame.contentDocument, depth + 1, output);
} catch (error) {
// Cross-origin and unloaded frames are intentionally ignored.
}
});
return output;
}
function mainFrame() {
return document.getElementById('Iframe_Home')
|| document.querySelector('iframe[name="MainIframe"], iframe');
}
function dispatchFieldEvents(element) {
if (!element) return;
const ownerWindow = element.ownerDocument?.defaultView || window;
['input', 'change', 'blur'].forEach((eventName) => {
element.dispatchEvent(new ownerWindow.Event(eventName, { bubbles: true }));
});
}
function field(form, name) {
return form?.querySelector(`#${name}, [name="${name}"]`) || form?.elements?.[name] || null;
}
function fieldValue(form, name) {
const element = field(form, name);
return element && 'value' in element ? String(element.value || '') : '';
}
function setField(form, name, value, options = {}) {
const element = field(form, name);
if (!element) {
if (options.optional) return false;
throw new Error(`missing ERP field: ${name}`);
}
element.value = String(value ?? '');
dispatchFieldEvents(element);
return true;
}
function textOf(element) {
return String(element?.innerText || element?.textContent || '').replace(/\s+/g, ' ').trim();
}
function formForCurrentFrame() {
if (!/\/orders_adds\.asp/i.test(location.pathname)) return null;
return document.querySelector('#InfoForm1') || null;
}
function frameVisible() {
try {
if (!window.frameElement) return true;
return Boolean(window.frameElement.offsetParent || window.frameElement.getClientRects().length);
} catch (error) {
return false;
}
}
function normalizeRowDate(value) {
return typeof helpers.canonicalDate === 'function' ? helpers.canonicalDate(value) : String(value || '');
}
function cleanupBatchLoading(ownerWindow) {
const cleanup = window.LTJTOrderAssistant?.closeLingeringLoading;
if (typeof cleanup !== 'function') {
return {
attempted: [],
closed_by_api: 0,
removed_elements: 0,
errors: ['shared_loading_cleanup_not_available']
};
}
try {
return cleanup(ownerWindow?.jQuery || ownerWindow?.$ || window.jQuery || window.$);
} catch (error) {
return {
attempted: ['shared_loading_cleanup'],
closed_by_api: 0,
removed_elements: 0,
errors: [String(error?.message || error).slice(0, 160)]
};
}
}
async function openTeamBatchForm({ fabudanwei = '老挝联泰' } = {}) {
if (/login/i.test(location.href) || /登录|登陆/i.test(document.title || '')) {
return { status: 'login_required', blockers: ['login_required'] };
}
const frame = mainFrame();
if (!frame) return { status: 'main_iframe_not_found', blockers: ['main_iframe_not_found'] };
let frameDocument = frame.contentDocument;
if (!frameDocument) return { status: 'main_iframe_loading', blockers: ['main_iframe_document_not_ready'] };
const hasBatchButton = (candidateDocument) => Array.from(candidateDocument.querySelectorAll('input,button,a'))
.some((element) => /批量下单/.test(String(element.value || element.innerText || element.textContent || '')));
const findBatchListDocument = (rootDocument) => sameOriginDocuments(rootDocument)
.find((candidate) => hasBatchButton(candidate)) || null;
let listDocument = findBatchListDocument(frameDocument);
if (!listDocument) {
frame.src = new URL(`/System/Business/orders.asp?fabudanwei=${encodeURIComponent(fabudanwei)}`, location.origin).href;
listDocument = await waitFor(() => {
const candidate = frame.contentDocument;
return candidate ? findBatchListDocument(candidate) : null;
}, 20000, 300);
}
if (!listDocument) return { status: 'team_batch_entry_not_ready', blockers: ['未找到独立团批量下单入口。'] };
const batchButton = Array.from(listDocument.querySelectorAll('input,button,a')).find((element) =>
/批量下单/.test(String(element.value || element.innerText || element.textContent || ''))
|| /OPEN_ProEdits\s*\(/i.test(element.getAttribute('onclick') || '')
);
const ownerWindow = listDocument.defaultView || frame.contentWindow;
if (batchButton) {
batchButton.click();
} else if (typeof ownerWindow?.OPEN_ProEdits === 'function') {
ownerWindow.OPEN_ProEdits();
} else {
return { status: 'team_batch_entry_not_found', blockers: ['独立团批量下单按钮或 OPEN_ProEdits 函数不存在。'] };
}
return {
status: 'team_batch_form_open_requested',
entry_stamp: String(Date.now()),
source_path: listDocument.location?.pathname || '',
frame_id_hint: frame.id || frame.name || '',
fabudanwei,
url_redacted: true
};
}
async function pingTeamBatchFrame() {
const form = formForCurrentFrame();
if (!form) return { status: 'not_team_batch_frame', url: location.href, title: document.title };
const required = ['Riqi1', 'Riqi2', 'darenshu', 'zutuanshe', 'jiedairen', 'S_chanpinming'];
const missing = required.filter((name) => !field(form, name));
const customerDataReady = String(field(form, 'zutuanshe')?.getAttribute('data') || '').length > 0;
if (!customerDataReady) missing.push('zutuanshe.data');
return {
status: missing.length ? 'team_batch_frame_loading' : 'team_batch_frame_ready',
url: location.href,
title: document.title,
form_element_count: form.elements.length,
missing,
visible: frameVisible()
};
}
function selectCustomer(form, reference) {
const input = field(form, 'zutuanshe');
const keyword = String(reference?.keyword || reference?.name || '').trim();
if (!input || !keyword) return { ok: false, blocker: '预订客户输入框或客户关键字缺失。' };
const serialized = input.getAttribute('data') || '';
let result = helpers.resolveUniqueSelectBoxRow(serialized, keyword, {
label: '预订客户',
searchableColumns: [1, 2, 3, 4, 5]
});
if (!result.ok && typeof sourceRegion.uniqueSourceRegionCandidate === 'function' && typeof helpers.parseSelectBoxRows === 'function') {
const sourceRegionMatch = sourceRegion.uniqueSourceRegionCandidate(
helpers.parseSelectBoxRows(serialized).map((row) => ({ row, name: row.columns[2] || '' })),
reference
);
if (sourceRegionMatch.ok && sourceRegionMatch.candidate?.row) {
result = {
ok: true,
row: sourceRegionMatch.candidate.row,
rule: 'unique_source_region_tokens',
exact_count: 0,
contains_count: 0,
token_match_count: 0,
source_region_match_count: sourceRegionMatch.match_count,
source_region_tokens: sourceRegionMatch.source_tokens,
candidate_count: 1
};
} else if (sourceRegionMatch.applicable && sourceRegionMatch.match_count > 1) {
result = {
ok: false,
row: null,
rule: 'ambiguous_source_region_tokens',
exact_count: 0,
contains_count: 0,
token_match_count: 0,
source_region_match_count: sourceRegionMatch.match_count,
source_region_tokens: sourceRegionMatch.source_tokens,
candidate_count: sourceRegionMatch.match_count,
blocker: `预订客户: source-region keywords matched ${sourceRegionMatch.match_count} customer options`
};
}
}
if (!result.ok) return result;
const ownerWindow = form.ownerDocument.defaultView || window;
const selectBox = ownerWindow.SelectBox;
if (!selectBox || typeof selectBox.SetValToObj !== 'function') {
return { ok: false, blocker: 'ERP 客户 SelectBox 映射函数不存在。' };
}
const previousSetVal = selectBox.SetVal;
selectBox.SetVal = input.getAttribute('setval') || input.getAttribute('SetVal') || previousSetVal || '';
selectBox.SetValToObj(result.row.raw);
dispatchFieldEvents(input);
const columns = result.row.columns || [];
return {
...result,
keyword,
selected_name: columns[2] || '',
selected_id_present: Boolean(String(columns[1] || '').trim()),
selected_contact_present: Boolean(String(columns[3] || '').trim()),
selected_manager_present: Boolean(String(columns[4] || '').trim())
};
}
function productCandidates(form) {
return Array.from(form.querySelectorAll('#DateTable input[name="cp_id"]')).map((input) => {
const row = input.closest('tr') || input.parentElement;
if (typeof productLookup.productCandidateFromRow === 'function') {
return productLookup.productCandidateFromRow(row, input);
}
const label = textOf(row) || String(input.value || '');
return { input, label, name: label, duration: '', text: label, cpid: String(input.value || '').trim() };
});
}
async function waitForProductList(form) {
return waitFor(() => {
const jq = form.ownerDocument.defaultView?.jQuery || form.ownerDocument.defaultView?.$;
const active = jq && Number.isFinite(Number(jq.active)) ? Number(jq.active) : 0;
const candidates = productCandidates(form);
return active === 0 ? candidates : null;
}, 20000, 300) || [];
}
function resolveProductCandidates(candidates, keyword) {
if (typeof productLookup.matchProductCandidates !== 'function') {
return helpers.resolveUniqueTextCandidate(candidates, keyword, { label: '产品' });
}
const matched = productLookup.matchProductCandidates(candidates, keyword);
if (matched.length === 1) {
return {
ok: true,
candidate: matched[0],
rule: matched[0].match.rule,
exact_count: 0,
contains_count: matched[0].match.rule === 'unique_contiguous' ? 1 : 0,
token_match_count: matched[0].match.rule === 'unique_contiguous' ? 0 : 1,
candidate_count: matched.length
};
}
return {
ok: false,
candidate: null,
rule: matched.length > 1 ? 'ambiguous_product_match' : 'not_found',
exact_count: 0,
contains_count: matched.filter((item) => item.match.rule === 'unique_contiguous').length,
token_match_count: matched.filter((item) => item.match.rule !== 'unique_contiguous').length,
candidate_count: matched.length,
blocker: `产品: expected exactly one deterministic match, found ${matched.length}`
};
}
function nativeProductSearchQueries(keyword) {
if (typeof productLookup.nativeProductSearchQueries === 'function') {
return productLookup.nativeProductSearchQueries(keyword);
}
const normalized = String(keyword || '').trim();
return normalized ? [normalized, ''] : [''];
}
async function reloadProductsForLocalMatch(form, input, keyword, nativeQuery = '') {
const previousSignature = productCandidates(form)
.map((candidate) => `${candidate.cpid || candidate.input?.value || ''}:${candidate.label || ''}`)
.join('|');
const ownerWindow = form.ownerDocument.defaultView || window;
const jq = ownerWindow.jQuery || ownerWindow.$;
const startedAt = Date.now();
// Trigger the form's native blur-driven AjaxLoadData(true) search. The
// shared helper preserves complete product names and removes only a
// trailing numeric duration shorthand from this contiguous LIKE query.
setField(form, 'S_chanpinming', nativeQuery);
const candidates = await waitFor(() => {
const current = productCandidates(form);
const signature = current
.map((candidate) => `${candidate.cpid || candidate.input?.value || ''}:${candidate.label || ''}`)
.join('|');
const active = jq && Number.isFinite(Number(jq.active)) ? Number(jq.active) : 0;
if (current.length > 0 && signature !== previousSignature) return current;
// An empty native query can return the same candidate set. Wait for the
// request to settle rather than imposing the full fallback timeout.
if (Date.now() - startedAt >= 500 && active === 0) return current;
return null;
}, 8000, 200) || productCandidates(form);
input.value = String(keyword || '');
return {
candidates,
strategy: nativeQuery ? 'native_query_then_local' : 'native_empty_then_local',
native_query: nativeQuery,
previous_candidate_count: previousSignature ? previousSignature.split('|').length : 0,
refreshed: candidates.length > 0 && candidates
.map((candidate) => `${candidate.cpid || candidate.input?.value || ''}:${candidate.label || ''}`)
.join('|') !== previousSignature
};
}
async function selectProduct(form, reference) {
const ownerWindow = form.ownerDocument.defaultView || window;
const keyword = String(reference?.keyword || reference?.name || '').trim();
if (!keyword) return { ok: false, blocker: '产品关键字缺失。' };
const input = field(form, 'S_chanpinming');
if (!input) return { ok: false, blocker: '产品搜索输入框 S_chanpinming 不存在。' };
input.value = keyword;
let candidates = await waitForProductList(form);
let search = { strategy: 'loaded_candidates', native_query: null, refreshed: false };
const searchAttempts = [];
let result = resolveProductCandidates(candidates, keyword);
if (!result.ok) {
if (typeof ownerWindow.AjaxLoadData !== 'function') return { ...result, blocker: result.blocker || 'ERP 产品 AjaxLoadData 函数不存在。' };
for (const nativeQuery of nativeProductSearchQueries(keyword)) {
search = await reloadProductsForLocalMatch(form, input, keyword, nativeQuery);
searchAttempts.push(search);
candidates = search.candidates;
result = resolveProductCandidates(candidates, keyword);
if (result.ok) break;
}
}
if (!result.ok) return {
...result,
...search,
search_attempt_count: searchAttempts.length,
search_attempt_strategies: searchAttempts.map((attempt) => attempt.strategy)
};
const selected = result.candidate.input;
selected.click();
selected.checked = true;
dispatchFieldEvents(selected);
const onclick = selected.getAttribute('onclick') || '';
const groupMatch = onclick.match(/set_th\s*\(\s*['"]([^'"]*)['"]\s*,\s*['"]([^'"]*)['"]\s*\)/i);
if (groupMatch && typeof ownerWindow.set_th === 'function') ownerWindow.set_th(groupMatch[1], groupMatch[2]);
await sleep(150);
return {
...result,
keyword,
search_strategy: search.strategy,
native_query: search.native_query,
refreshed: search.refreshed,
search_attempt_count: searchAttempts.length,
search_attempt_strategies: searchAttempts.map((attempt) => attempt.strategy),
selected_label_length: String(result.candidate.label || '').length,
selected_value_present: Boolean(String(selected.value || '').trim()),
group_prefix_present: Boolean(fieldValue(form, 'tuanxuhao1').trim()),
group_suffix_present: Boolean(fieldValue(form, 'tuanxuhao2').trim())
};
}
function clearCheckboxes(form, name) {
Array.from(form.querySelectorAll(`input[name="${name}"]`)).forEach((input) => {
input.checked = false;
input.dispatchEvent(new input.ownerDocument.defaultView.Event('change', { bubbles: true }));
});
}
function applyCycle(form, data) {
const recurrence = data.recurrence || {};
const start = helpers.erpDate(recurrence.start_date || data.departure_dates?.[0] || '');
const end = helpers.erpDate(recurrence.end_date || data.departure_dates?.[data.departure_dates.length - 1] || start);
if (!start || !end) return { ok: false, blocker: '批量下单日期范围无效。' };
setField(form, 'Riqi1', start);
setField(form, 'Riqi2', end);
const ownerWindow = form.ownerDocument.defaultView || window;
if (typeof ownerWindow.ShowFoucsBox !== 'function') return { ok: false, blocker: 'ERP 日期枚举函数 ShowFoucsBox 不存在。' };
ownerWindow.ShowFoucsBox();
clearCheckboxes(form, 'zhouqi');
clearCheckboxes(form, 'zhidingzhouqi');
const selectionPlan = typeof helpers.cycleSelectionPlan === 'function'
? helpers.cycleSelectionPlan(data)
: { ok: false, blocker: '共享发团周期选择器不可用。' };
if (!selectionPlan.ok) return selectionPlan;
if (selectionPlan.mode === 'explicit_dates') {
const selected = [];
for (const wanted of selectionPlan.explicit_dates) {
const boxes = Array.from(form.querySelectorAll('input[name="zhidingzhouqi"]'))
.filter((input) => normalizeRowDate(input.value) === wanted);
if (!boxes.length) return { ok: false, blocker: `ERP 日期枚举中未找到 ${wanted}。` };
boxes[0].checked = true;
boxes[0].dispatchEvent(new boxes[0].ownerDocument.defaultView.Event('change', { bubbles: true }));
selected.push(String(boxes[0].value || ''));
}
return { ok: true, mode: 'explicit_dates', selected_dates: selected, selected_count: selected.length };
}
const shortcutBoxes = Array.from(form.querySelectorAll('input[name="zhouqi"]'))
.filter((input) => selectionPlan.shortcut_values.includes(String(input.value || '')));
if (shortcutBoxes.length !== selectionPlan.shortcut_values.length) {
return { ok: false, blocker: '发团周期未能映射到 ERP 的日期枚举或快捷标签。' };
}
shortcutBoxes.forEach((input) => {
input.checked = true;
input.dispatchEvent(new input.ownerDocument.defaultView.Event('change', { bubbles: true }));
});
return {
ok: true,
mode: 'shortcut',
shortcut_values: selectionPlan.shortcut_values,
selected_dates: selectionPlan.expanded_dates,
selected_count: shortcutBoxes.length
};
}
function fillCountsAndRooms(form, data) {
const counts = data.passenger_counts || {};
const rooms = data.room_counts || {};
[['darenshu', counts.adult], ['xiaorenshu', counts.child_bed], ['ertrenshu', counts.child_no_bed], ['yingrenshu', counts.infant], ['quanrenshu', counts.leader]]
.forEach(([name, value]) => setField(form, name, Math.max(0, Math.trunc(Number(value || 0)))));
const ownerWindow = form.ownerDocument.defaultView || window;
if (typeof ownerWindow.GetVisitorsHtml === 'function') ownerWindow.GetVisitorsHtml();
const roomMap = { SGL: 0, TWN: 1, TRP: 2, DBL: 3 };
for (let index = 0; index <= 5; index += 1) {
const key = Object.keys(roomMap).find((room) => roomMap[room] === index);
setField(form, `frenshu${index}`, key ? Math.max(0, Math.trunc(Number(rooms[key] || 0))) : 0);
}
if (typeof ownerWindow.sum_fang === 'function') ownerWindow.sum_fang();
return {
passenger_total: Number(textOf(form.querySelector('#zongrenshu')) || 0),
room_total: Number(fieldValue(form, 'frenshu6') || textOf(form.querySelector('#zongfangshu')) || 0)
};
}
function requiredMissing(form, cycle) {
const required = ['Riqi1', 'Riqi2', 'darenshu', 'jiedairen', 'zutuanshe', 'xiaoshouren', 'tuanxuhao1', 'tuanxuhao2'];
const missing = required.filter((name) => !fieldValue(form, name).trim());
if (!form.querySelector('input[name="cp_id"]:checked')) missing.push('cp_id');
if (!cycle?.selected_count) missing.push('zhidingzhouqi');
if (Number(fieldValue(form, 'frenshu6') || textOf(form.querySelector('#zongfangshu')) || 0) <= 0) missing.push('frenshu6');
return [...new Set(missing)];
}
function serializeForm(form) {
const ownerWindow = form.ownerDocument.defaultView || window;
const jq = ownerWindow.jQuery || ownerWindow.$;
if (jq) return jq(form).serialize();
return Array.from(new FormData(form).entries())
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
}
async function sha256(value) {
const buffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(String(value || '')));
return Array.from(new Uint8Array(buffer)).map((byte) => byte.toString(16).padStart(2, '0')).join('');
}
function cleanMessage(value) {
return String(value || '')
.replace(/\r/g, '')
.replace(/<br\s*\/?>(?=.)/gi, '\n')
.replace(/<\/(p|div|li|tr)>/gi, '\n')
.replace(/<[^>]+>/g, '')
.replace(/&nbsp;/gi, ' ')
.replace(/&amp;/gi, '&')
.replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>')
.trim();
}
function extractGroupNumbers(value) {
const text = cleanMessage(value);
const matches = text.match(/[A-Z]{1,4}-\d{6,8}[A-Z0-9]*(?:-[A-Z0-9_-]+)*/gi) || [];
return [...new Set(matches)];
}
function receiptFromText(value) {
const text = cleanMessage(value);
const groupNumbers = extractGroupNumbers(text);
const success = /下单成功|保存成功|操作成功|成功|success|完成/i.test(text);
if (!text && !groupNumbers.length) return null;
if (!success && !groupNumbers.length) return null;
return {
status: success ? 'submit_success_receipt' : 'submit_receipt',
success,
group_number: groupNumbers[0] || '',
group_numbers: groupNumbers,
text_length: text.length,
message: text.slice(0, 500),
received_at: new Date().toISOString()
};
}
function alertText(message) {
if (message && typeof message === 'object') return message.content || message.message || message.msg || message.text || '';
return String(message || '');
}
async function interceptNativeSubmit(form) {
const ownerWindow = form.ownerDocument.defaultView || window;
const jq = ownerWindow.jQuery || ownerWindow.$;
if (!jq || typeof jq.ajax !== 'function') return { status: 'submit_intercept_blocked', blockers: ['jquery_ajax_not_found'] };
if (typeof ownerWindow.SubmitInfoForm !== 'function') return { status: 'submit_intercept_blocked', blockers: ['SubmitInfoForm_not_found'] };
const originalAjax = jq.ajax;
const originalAlert = ownerWindow.alert;
const originalDialogAlert = jq.dialog?.alert;
const originalMessagerAlert = jq.messager?.alert;
const intercepted = [];
const alerts = [];
ownerWindow.alert = (message) => alerts.push(cleanMessage(alertText(message)));
if (jq.dialog && typeof jq.dialog.alert === 'function') jq.dialog.alert = (message) => alerts.push(cleanMessage(alertText(message)));
if (jq.messager && typeof jq.messager.alert === 'function') jq.messager.alert = (...args) => alerts.push(cleanMessage(args.filter(Boolean).map(alertText).join('\n')));
jq.ajax = function ajaxIntercept(optionsArg, ...rest) {
const config = typeof optionsArg === 'string' ? { ...(rest[0] || {}), url: optionsArg } : { ...(optionsArg || {}) };
const requestData = String(config.data || '');
const isSubmit = /\/System\/DAT\/orders\.asp/i.test(String(config.url || '')) && requestData.startsWith('Act=DoInfoJHs&');
if (!isSubmit) return originalAjax.call(this, optionsArg, ...rest);
const params = new URLSearchParams(requestData.slice('Act=DoInfoJHs&'.length));
intercepted.push({
url_redacted: '/System/DAT/orders.asp',
method: config.type || config.method || 'GET',
payload_sha256: '',
payload: requestData,
payload_summary: {
starts_with_DoInfoJHs: true,
field_count: Array.from(params.keys()).length,
field_names: [...new Set(Array.from(params.keys()))].sort(),
byte_length: new Blob([requestData]).size,
value_redacted: true
},
prevented_from_network: true
});
const fake = { readyState: 4, status: 0, statusText: 'intercepted_by_ltjt_order_assistant', abort() {} };
return fake;
};
let thrown = '';
try {
ownerWindow.SubmitInfoForm();
} catch (error) {
thrown = error.message || String(error);
}
await sleep(700);
jq.ajax = originalAjax;
ownerWindow.alert = originalAlert;
if (jq.dialog && typeof originalDialogAlert === 'function') jq.dialog.alert = originalDialogAlert;
if (jq.messager && typeof originalMessagerAlert === 'function') jq.messager.alert = originalMessagerAlert;
for (const item of intercepted) item.payload_sha256 = await sha256(item.payload || '');
intercepted.forEach((item) => { delete item.payload; });
const blockers = [];
if (thrown) blockers.push('SubmitInfoForm 在预检期间抛出异常。');
if (intercepted.length !== 1) blockers.push('原生批量 SubmitInfoForm 未产生唯一 DoInfoJHs 提交。');
if (alerts.length) blockers.push('ERP 原生必填校验在预检期间弹出提示。');
return {
status: blockers.length ? 'submit_intercept_blocked' : 'submit_intercept_captured',
intercepted_submit_count: intercepted.length,
intercepted_submits: intercepted,
alerts: alerts.map((message) => ({ message_length: message.length, message_redacted: true })),
blockers,
DoInfoJHs_network_prevented: true
};
}
async function preflightTeamBatchNative(operation = {}, options = {}) {
const form = formForCurrentFrame();
if (!form) return { status: 'team_batch_preflight_blocked', blockers: ['当前不是 orders_adds.asp 原生批量表单。'], no_erp_write: true };
const data = operation.data || {};
const blockers = [];
if (operation.action !== 'team_order_batch_create') blockers.push('原生批量适配器只接受 team_order_batch_create。');
if (operation.order_nature !== 'formal') blockers.push('order_nature 必须为 formal。');
if (operation.submit_mode !== 'dry_run') blockers.push('submit_mode 必须保持 dry_run。');
if (!helpers.resolveUniqueSelectBoxRow || !helpers.resolveUniqueTextCandidate) blockers.push('批量模糊匹配辅助器未加载。');
if (typeof productLookup.matchProductCandidates !== 'function') blockers.push('产品名称/天数匹配辅助器未加载。');
const recurrence = data.recurrence || {};
if (!helpers.canonicalDate(recurrence.start_date) || !helpers.canonicalDate(recurrence.end_date)) blockers.push('批量日期范围无效。');
const rangeFrom = helpers.canonicalDate(recurrence.start_date);
const rangeTo = helpers.canonicalDate(recurrence.end_date);
const requestedDates = Array.isArray(data.departure_dates) ? data.departure_dates.map(helpers.canonicalDate).filter(Boolean) : [];
if (!requestedDates.length) blockers.push('未提供有效发团日期。');
if (requestedDates.some((date) => !helpers.dateInRange(date, rangeFrom, rangeTo))) blockers.push('发团日期不在发团日期范围内。');
if (blockers.length) return { status: 'team_batch_preflight_blocked', blockers, no_erp_write: true };
const lookupChecks = [];
await waitFor(() => String(field(form, 'zutuanshe')?.getAttribute('data') || '').length > 0 ? true : null, 15000, 300);
let customerResult;
try {
customerResult = selectCustomer(form, data.customer);
} catch (error) {
customerResult = { ok: false, blocker: error.message || String(error) };
}
lookupChecks.push({
name: 'customer',
ok: customerResult.ok === true,
exact_match_count: customerResult.exact_count || 0,
contains_match_count: customerResult.contains_count || 0,
token_match_count: customerResult.token_match_count || 0,
match_rule: customerResult.rule || 'blocked',
selected_name: customerResult.selected_name || ''
});
if (!customerResult.ok) blockers.push(customerResult.blocker || '预订客户未能唯一匹配。');
const ownerWindow = form.ownerDocument.defaultView || window;
if (customerResult.ok && typeof ownerWindow.set_travel === 'function') {
ownerWindow.set_travel();
await waitFor(() => {
const jq = ownerWindow.jQuery || ownerWindow.$;
return !jq || Number(jq.active || 0) === 0 ? true : null;
}, 15000, 300);
}
let productResult;
try {
productResult = await selectProduct(form, data.product);
} catch (error) {
productResult = { ok: false, blocker: error.message || String(error) };
}
lookupChecks.push({
name: 'product',
ok: productResult.ok === true,
exact_match_count: productResult.exact_count || 0,
contains_match_count: productResult.contains_count || 0,
token_match_count: productResult.token_match_count || 0,
match_rule: productResult.rule || 'blocked',
selected_label_length: productResult.selected_label_length || 0,
search_strategy: productResult.search_strategy || 'unknown',
native_query: productResult.native_query,
refreshed: productResult.refreshed === true,
candidate_count: productResult.candidate_count || 0,
search_attempt_count: productResult.search_attempt_count || 0,
search_attempt_strategies: productResult.search_attempt_strategies || []
});
if (!productResult.ok) blockers.push(productResult.blocker || '业务产品未能唯一匹配。');
const requestedOrderSuffix = String(data.order_number?.suffix || '').trim();
if (productResult.ok && requestedOrderSuffix) {
setField(form, 'tuanxuhao2', requestedOrderSuffix);
await sleep(100);
const appliedOrderSuffix = fieldValue(form, 'tuanxuhao2').trim();
lookupChecks.push({
name: 'order_number_suffix',
ok: appliedOrderSuffix === requestedOrderSuffix,
requested: true,
marker_present: requestedOrderSuffix.includes('TEST-202609'),
value_redacted: true
});
if (appliedOrderSuffix !== requestedOrderSuffix) blockers.push('批量下单团号后缀未按 operation.data.order_number.suffix 稳定写入。');
}
let countsRooms = { passenger_total: 0, room_total: 0 };
if (!blockers.length) {
try {
countsRooms = fillCountsAndRooms(form, data);
} catch (error) {
blockers.push(error.message || String(error));
}
}
let cycle = { ok: false, blocker: '日期周期未执行。' };
if (!blockers.length) {
try {
cycle = applyCycle(form, data);
} catch (error) {
cycle = { ok: false, blocker: error.message || String(error) };
}
if (!cycle.ok) blockers.push(cycle.blocker);
}
const missing = requiredMissing(form, cycle);
if (missing.length) blockers.push(`ERP 原生批量表单仍缺少必填值:${missing.join(', ')}`);
const serializedForm = serializeForm(form);
const payload = `Act=DoInfoJHs&${serializedForm}`;
const serializedPayloadHash = await sha256(payload);
let payloadHash = serializedPayloadHash;
let submitIntercept = null;
const warnings = ['原生批量提交动作已在 ERP 页面内拦截验证,预检阶段没有写入网络。'];
if (options.interceptSubmit && !blockers.length) {
submitIntercept = await interceptNativeSubmit(form);
if (submitIntercept.status !== 'submit_intercept_captured') blockers.push('原生批量提交拦截未捕获唯一干净的 DoInfoJHs 分支。');
}
if (submitIntercept?.intercepted_submits?.[0]?.payload_sha256) {
const interceptedHash = submitIntercept.intercepted_submits[0].payload_sha256;
// SubmitInfoForm runs the ERP's own input normalizer immediately before
// serializing. The intercepted native request is therefore the canonical
// payload that the approved live-submit check must compare against.
payloadHash = interceptedHash;
if (interceptedHash !== serializedPayloadHash) {
warnings.push('ERP 原生提交函数在发送前执行了输入规范化;已将其实际拦截载荷作为批准基准。');
}
}
return {
status: blockers.length ? 'team_batch_preflight_blocked' : 'team_batch_preflight_passed',
ok: blockers.length === 0,
no_erp_write: true,
page: { url: location.href, title: document.title, form_element_count: form.elements.length },
parsed_instruction: {
action: operation.action,
order_nature: operation.order_nature,
order_suffix_requested: Boolean(requestedOrderSuffix),
order_suffix_marker_present: requestedOrderSuffix.includes('TEST-202609'),
date_range: { from: rangeFrom, to: rangeTo },
departure_dates: requestedDates,
passenger_total: countsRooms.passenger_total,
room_total: countsRooms.room_total,
values_are_test_only: false
},
lookup_checks: lookupChecks,
cycle,
final_form: {
serialized_field_count: Array.from(new URLSearchParams(serializedForm).keys()).length,
submit_payload_sha256: payloadHash,
serialized_payload_sha256: serializedPayloadHash,
serialized_form_redacted: true,
required_missing: missing
},
submit_intercept: submitIntercept,
blockers,
warnings,
submit_safety: { live_submit_attempted: false, DoInfoJHs_network_prevented: Boolean(submitIntercept?.DoInfoJHs_network_prevented) }
};
}
async function liveSubmitTeamBatchApproved({ expectedPayloadSha256 = '', expectedFieldCount = 0 } = {}) {
const form = formForCurrentFrame();
if (!form) return { status: 'team_batch_live_submit_blocked', blockers: ['当前不是 orders_adds.asp 原生批量表单。'] };
const ownerWindow = form.ownerDocument.defaultView || window;
const jq = ownerWindow.jQuery || ownerWindow.$;
if (!jq || typeof jq.ajax !== 'function') return { status: 'team_batch_live_submit_blocked', blockers: ['jquery_ajax_not_found'] };
if (typeof ownerWindow.SubmitInfoForm !== 'function') return { status: 'team_batch_live_submit_blocked', blockers: ['SubmitInfoForm_not_found'] };
if (!expectedPayloadSha256) return { status: 'team_batch_live_submit_blocked', blockers: ['expectedPayloadSha256_missing'] };
const serializedForm = serializeForm(form);
const requestBody = `Act=DoInfoJHs&${serializedForm}`;
const currentHash = await sha256(requestBody);
const currentFieldCount = Array.from(new URLSearchParams(serializedForm).keys()).length;
const preBlockers = [];
if (currentHash !== expectedPayloadSha256) preBlockers.push('当前原生批量载荷哈希与预检批准哈希不一致。');
if (expectedFieldCount && currentFieldCount !== expectedFieldCount) preBlockers.push('当前原生批量字段数量与预检批准字段数量不一致。');
if (preBlockers.length) return {
status: 'team_batch_live_submit_blocked',
current_payload: { payload_sha256: currentHash, field_count: currentFieldCount, value_redacted: true },
blockers: preBlockers,
submit_safety: { live_submit_attempted: false }
};
const originalAjax = jq.ajax;
const originalAlert = ownerWindow.alert;
const originalDialogAlert = jq.dialog?.alert;
const originalMessagerAlert = jq.messager?.alert;
const ajaxRecords = [];
const alerts = [];
function captureAlert(type, message) {
const text = cleanMessage(alertText(message));
const receipt = receiptFromText(text);
alerts.push({ type, message_length: text.length, message_redacted: !receipt, message: receipt ? text.slice(0, 500) : undefined, erp_receipt: receipt || undefined });
}
ownerWindow.alert = (message) => captureAlert('alert', message);
if (jq.dialog && typeof jq.dialog.alert === 'function') jq.dialog.alert = (message) => captureAlert('dialog.alert', message);
if (jq.messager && typeof jq.messager.alert === 'function') jq.messager.alert = (...args) => captureAlert('messager.alert', args.filter(Boolean).map(alertText).join('\n'));
jq.ajax = function liveAjax(optionsArg, ...rest) {
const config = typeof optionsArg === 'string' ? { ...(rest[0] || {}), url: optionsArg } : { ...(optionsArg || {}) };
const body = String(config.data || '');
const isSubmit = /\/System\/DAT\/orders\.asp/i.test(String(config.url || '')) && body.startsWith('Act=DoInfoJHs&');
if (!isSubmit) return originalAjax.call(this, optionsArg, ...rest);
const record = {
url_redacted: '/System/DAT/orders.asp',
method: config.type || config.method || 'GET',
request_payload_sha256: '',
request_field_count: Array.from(new URLSearchParams(body.slice('Act=DoInfoJHs&'.length)).keys()).length,
live_submit_attempted: true,
prevented_from_network: false,
completed: false,
ok: false,
response_byte_length: 0,
response_contains_login_timeout: false,
response_contains_permission_text: false,
response_contains_success_hint: false,
response_contains_error_hint: false,
response_text: ''
};
record.request_payload_sha256 = currentHash;
ajaxRecords.push(record);
if (record.request_payload_sha256 !== expectedPayloadSha256) {
record.prevented_from_network = true;
record.completed = true;
return { readyState: 4, status: 0, statusText: 'blocked_hash_mismatch_before_network', abort() {} };
}
const originalSuccess = config.success;
const originalError = config.error;
const originalComplete = config.complete;
config.success = function successWrapper(responseText, textStatus, jqXHR) {
const text = cleanMessage(jqXHR?.responseText || responseText || '');
record.ok = true;
record.text_status = textStatus || '';
record.response_byte_length = new Blob([text]).size;
record.response_text = text.slice(0, 4000);
record.response_contains_login_timeout = /登陆|登录|login/i.test(text);
record.response_contains_permission_text = /权限|permission/i.test(text);
record.response_contains_success_hint = /成功|保存|添加|完成|success|ok/i.test(text);
record.response_contains_error_hint = /失败|错误|异常|error/i.test(text) && !record.response_contains_success_hint;
const receipt = receiptFromText(text);
if (receipt && !alerts.some((item) => item.erp_receipt?.group_number === receipt.group_number)) {
alerts.push({ type: 'ajax_response', message_length: text.length, message_redacted: false, message: text.slice(0, 500), erp_receipt: receipt });
}
return typeof originalSuccess === 'function' ? originalSuccess.apply(this, arguments) : undefined;
};
config.error = function errorWrapper(jqXHR, textStatus, errorThrown) {
record.ok = false;
record.text_status = textStatus || '';
record.response_text = cleanMessage(jqXHR?.responseText || '').slice(0, 4000);
record.response_byte_length = new Blob([record.response_text]).size;
record.response_contains_login_timeout = /登陆|登录|login/i.test(record.response_text);
record.response_contains_permission_text = /权限|permission/i.test(record.response_text);
record.error_text_redacted = String(errorThrown || '').slice(0, 100);
return typeof originalError === 'function' ? originalError.apply(this, arguments) : undefined;
};
config.complete = function completeWrapper() {
record.completed = true;
return typeof originalComplete === 'function' ? originalComplete.apply(this, arguments) : undefined;
};
return originalAjax.call(this, config, ...rest);
};
let thrown = '';
try {
ownerWindow.SubmitInfoForm();
} catch (error) {
thrown = error.message || String(error);
}
const started = Date.now();
while (Date.now() - started < 30000 && !ajaxRecords.some((record) => record.completed)) await sleep(250);
const visibleText = Array.from(document.querySelectorAll('.messager-window,.messager-body,.aui_dialog,[role="dialog"]'))
.map((node) => textOf(node)).filter(Boolean).join('\n');
if (visibleText) {
const receipt = receiptFromText(visibleText);
if (receipt && !alerts.some((item) => item.erp_receipt?.group_number === receipt.group_number)) {
alerts.push({ type: 'visible_dialog', message_length: visibleText.length, message_redacted: false, message: visibleText.slice(0, 500), erp_receipt: receipt });
}
}
jq.ajax = originalAjax;
ownerWindow.alert = originalAlert;
if (jq.dialog && typeof originalDialogAlert === 'function') jq.dialog.alert = originalDialogAlert;
if (jq.messager && typeof originalMessagerAlert === 'function') jq.messager.alert = originalMessagerAlert;
const responseTexts = [
...alerts.map((item) => item.message || ''),
...ajaxRecords.map((record) => record.response_text || '')
];
const groupNumbers = [...new Set(responseTexts.flatMap(extractGroupNumbers))];
const erpReceipt = alerts.map((item) => item.erp_receipt).find(Boolean)
|| (groupNumbers[0] ? { status: 'submit_success_receipt', success: true, group_number: groupNumbers[0], group_numbers: groupNumbers, received_at: new Date().toISOString() } : null);
const immediateLoadingCleanup = cleanupBatchLoading(ownerWindow);
window.setTimeout(() => cleanupBatchLoading(ownerWindow), 800);
const blockers = [];
if (thrown) blockers.push('SubmitInfoForm 在真实保存期间抛出异常。');
if (ajaxRecords.length !== 1) blockers.push('原生批量真实保存未产生唯一 DoInfoJHs 请求。');
if (ajaxRecords[0] && !ajaxRecords[0].completed) blockers.push('原生批量 DoInfoJHs 请求在超时前未完成。');
if (ajaxRecords[0]?.prevented_from_network) blockers.push('原生批量请求因哈希不一致而在网络前被拦截。');
if (ajaxRecords[0]?.response_contains_login_timeout) blockers.push('原生批量响应疑似登录失效。');
if (ajaxRecords[0]?.response_contains_permission_text) blockers.push('原生批量响应包含权限错误。');
if (!groupNumbers.length) blockers.push('原生批量保存回执中未解析到团号。');
return {
status: blockers.length ? 'team_batch_live_submit_uncertain' : 'team_batch_live_submit_completed',
no_erp_write: false,
write_attempted: true,
current_payload: { payload_sha256: currentHash, field_count: currentFieldCount, value_redacted: true },
ajax_records: ajaxRecords.map((record) => ({ ...record, response_text: undefined, response_redacted: true })),
alerts,
group_numbers: groupNumbers,
erp_receipt: erpReceipt || undefined,
blockers,
ui_cleanup: {
immediate_loading_cleanup: immediateLoadingCleanup,
followup_loading_cleanup_scheduled: true
},
submit_safety: {
live_submit_attempted: ajaxRecords.some((record) => record.live_submit_attempted),
approved_payload_sha256_matched: currentHash === expectedPayloadSha256
}
};
}
async function verifyTeamBatchReceipts({ identifiers = [], dateFrom = '', dateTo = '', customerKeyword = '', productKeyword = '' } = {}) {
const wanted = [...new Set((Array.isArray(identifiers) ? identifiers : [identifiers]).map((value) => String(value || '').trim()).filter(Boolean))];
if (!wanted.length) return { status: 'team_batch_verification_blocked', blockers: ['缺少 ERP 团号回查标识。'], no_erp_write: false, write_attempted: false };
const receipts = [];
for (const identifier of wanted) {
const body = new URLSearchParams({
Act: 'JH_OrderList',
Tpage: '1',
P_Size: '50',
riqi: 'chufari',
S_chufariqi: helpers.erpDate(dateFrom),
S_chufarizhi: helpers.erpDate(dateTo || dateFrom),
S_fabudanwei: '老挝联泰',
S_tuanxuhao: identifier,
S_kehuming: '',
S_chanpinming: '',
S_gendanren: '',
S_youkexinxi: '',
S_daoyou: '',
S_lingdui: '',
S_querenshu: '',
S_zhuanxianming: '',
S_zhuangtai: ''
});
const response = await fetch('/System/DAT/orders.asp', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: body.toString()
});
const text = await response.text();
const occurrences = Math.max(0, text.split(identifier).length - 1);
receipts.push({
identifier,
found: occurrences > 0,
marker_occurrences: occurrences,
http_status: response.status,
byte_length: new Blob([text]).size,
contains_login_timeout_text: /登陆|登录|login/i.test(text),
contains_permission_text: /权限|permission/i.test(text),
value_redacted: true
});
}
const failed = receipts.filter((item) => !item.found || item.contains_login_timeout_text || item.contains_permission_text);
return {
status: failed.length ? 'team_batch_verification_incomplete' : 'team_batch_verification_completed',
no_erp_write: false,
write_attempted: true,
identifiers: wanted,
date_range: { from: helpers.canonicalDate(dateFrom), to: helpers.canonicalDate(dateTo || dateFrom) },
lookup_context: { customer_keyword_present: Boolean(customerKeyword), product_keyword_present: Boolean(productKeyword) },
receipts,
blockers: failed.map((item) => `ERP 回查未命中团号 ${item.identifier}。`),
submit_safety: { live_submit_attempted: false, verification_only: true }
};
}
window.LTJTOrderAssistant = {
...(window.LTJTOrderAssistant || {}),
version: '0.5.189',
openTeamBatchForm,
pingTeamBatchFrame,
preflightTeamBatchNative,
liveSubmitTeamBatchApproved,
verifyTeamBatchReceipts
};
}());