955 lines
46 KiB
JavaScript
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(/ /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
|
|
};
|
|
}());
|