(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(/(?=.)/gi, '\n') .replace(/<\/(p|div|li|tr)>/gi, '\n') .replace(/<[^>]+>/g, '') .replace(/ /gi, ' ') .replace(/&/gi, '&') .replace(/</gi, '<') .replace(/>/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 }; }());