Compare commits

..

4 Commits

Author SHA1 Message Date
0d0a29a465 30x 支持 2026-05-26 21:53:34 +08:00
60beb1f363 client 3 次自动退出 2026-05-26 21:50:53 +08:00
11f0f89727 ssl 支持 2026-05-26 21:46:49 +08:00
021738a8bb 回滚代码 2026-05-26 21:26:24 +08:00
32 changed files with 424 additions and 2448 deletions

3
.gitignore vendored
View File

@ -1,3 +0,0 @@
node_modules/*
~$*
.reasonix/*

View File

@ -1,11 +0,0 @@
## 开票业务
excle 结算状态过滤
登录
总进度
## 避免锁屏
打开目录D:\Cheney\2\geely-kaipiao\
地址栏删空,输入 cmd打开黑框。
输入命令bun.exe keep-awake.js

0
Readme.txt Normal file
View File

BIN
bun.exe

Binary file not shown.

View File

@ -1,283 +0,0 @@
const XLSX = require('xlsx');
const fs = require('fs');
/**
* 功能读取Excel文件并解析为JSON
* 参数filePath - 文件路径
* 返回值解析后的工作簿对象
*/
function readExcel(filePath) {
return XLSX.readFile(filePath);
}
/**
* 功能判断是否为有效订单号FR开头的订单号
* 参数orderNo - 订单号
* 返回值是否有效
*/
function isValidOrderNo(orderNo) {
if (!orderNo) return false;
const str = String(orderNo).trim();
if (!str) return false;
if (str === '总计' || str === '合计' || str === '汇总') return false;
if (str.startsWith('FR')) return true;
return /^\d+$/.test(str);
}
/**
* 功能从酒店账单提取订单号和金额
* 参数workbook - Excel工作簿对象
* 返回值订单Map {订单号: {金额, 入住人, 入住时间, 离店时间}}
*/
function extractHotelOrders(workbook) {
const sheet = workbook.Sheets['.'];
const data = XLSX.utils.sheet_to_json(sheet);
const orders = new Map();
data.forEach(row => {
const orderNo = row['吉利订单号'];
if (isValidOrderNo(orderNo)) {
orders.set(String(orderNo).trim(), {
结算金额: parseFloat(row['结算金额']) || 0,
系统金额: parseFloat(row['系统金额']) || 0,
差异: parseFloat(row['差异']) || 0,
入住人: row['入住人姓名'] || '',
入住时间: row['入住时间'] || '',
离店时间: row['离店时间'] || '',
房型: row['房型'] || '',
房号: row['房号'] || '',
房间数: row['房间数'] || 0,
间夜数: row['间夜数'] || 0
});
}
});
return orders;
}
/**
* 功能从商旅账单Export表提取订单号和金额合并同一订单的正负金额
* 参数workbook - Excel工作簿对象
* 返回值订单Map {订单号: 金额对象}
*/
function extractGeelyExportOrders(workbook) {
const sheet = workbook.Sheets['Export'];
const data = XLSX.utils.sheet_to_json(sheet, { header: 1 }); // 使用数组索引访问
const orders = new Map();
// 从第2行开始第0行是标题第1行是表头
for (let i = 2; i < data.length; i++) {
const row = data[i];
const orderNo = row[5]; // 供应商订单号在第5列
if (isValidOrderNo(orderNo)) {
// 金额在第11列纯数字已包含正负值
const amount = parseFloat(row[11]) || 0;
const key = String(orderNo).trim();
// 合并同一订单号的金额
if (orders.has(key)) {
const existing = orders.get(key);
existing.金额 += amount;
// 保留最后一条记录的其他信息
existing.对账明细名称 = row[6] || existing.对账明细名称;
existing.结算状态 = row[9] || existing.结算状态;
existing.对账单号 = row[0] || existing.对账单号;
} else {
orders.set(key, {
金额: amount,
对账明细名称: row[6] || '',
结算状态: row[9] || '',
对账单号: row[0] || '',
订单号: row[4] || ''
});
}
}
}
return orders;
}
/**
* 功能对比两个订单集合的差异
* 参数hotelOrders - 酒店方订单Map, geelyOrders - 商旅方订单Map
* 返回值差异结果对象
*/
function compareOrders(hotelOrders, geelyOrders) {
const hotelOnly = [];
const geelyOnly = [];
const amountDiff = [];
const matched = [];
// 检查酒店有但商旅没有的订单
for (const [orderNo, hotelInfo] of hotelOrders) {
if (!geelyOrders.has(orderNo)) {
hotelOnly.push({ 订单号: orderNo, ...hotelInfo });
} else {
const geelyInfo = geelyOrders.get(orderNo);
const hotelAmount = hotelInfo.结算金额;
const geelyAmount = geelyInfo.金额;
if (Math.abs(hotelAmount - geelyAmount) > 0.01) {
amountDiff.push({
订单号: orderNo,
酒店金额: hotelAmount,
商旅金额: geelyAmount,
差额: hotelAmount - geelyAmount,
入住人: hotelInfo.入住人,
酒店明细: hotelInfo,
商旅明细: geelyInfo
});
} else {
matched.push({
订单号: orderNo,
金额: hotelAmount,
入住人: hotelInfo.入住人
});
}
}
}
// 检查商旅有但酒店没有的订单
for (const [orderNo, geelyInfo] of geelyOrders) {
if (!hotelOrders.has(orderNo)) {
geelyOnly.push({ 订单号: orderNo, ...geelyInfo });
}
}
return { hotelOnly, geelyOnly, amountDiff, matched };
}
/**
* 功能生成差异报告并输出
* 参数result - 对比结果对象
*/
function generateReport(result, hotelOrdersCount, geelyOrdersCount) {
console.log('\n' + '='.repeat(70));
console.log(' 吉利商旅 vs 酒店账单 差异报告');
console.log(' (仅使用Export表已合并同一订单的正负金额)');
console.log('='.repeat(70));
console.log(`\n【统计概览】`);
console.log(` 酒店账单有效订单数: ${hotelOrdersCount}`);
console.log(` 商旅账单有效订单数: ${geelyOrdersCount}`);
console.log(` ✓ 匹配成功订单数: ${result.matched.length}`);
console.log(` ⚠ 金额差异订单数: ${result.amountDiff.length}`);
console.log(` ✗ 酒店有-商旅无: ${result.hotelOnly.length}`);
console.log(` ? 商旅有-酒店无: ${result.geelyOnly.length}`);
if (result.hotelOnly.length > 0) {
console.log(`\n【酒店有但商旅没有的订单 - 共${result.hotelOnly.length}条】`);
console.log('-'.repeat(70));
result.hotelOnly.forEach((item, i) => {
console.log(`${i+1}. ${item.订单号} | ${item.入住人.padEnd(6)} | ¥${String(item.结算金额).padStart(6)} | ${item.房型}`);
});
}
if (result.geelyOnly.length > 0) {
console.log(`\n【商旅有但酒店没有的订单 - 共${result.geelyOnly.length}条】`);
console.log('-'.repeat(70));
result.geelyOnly.forEach((item, i) => {
console.log(`${i+1}. ${item.订单号} | ¥${String(item.金额).padStart(6)} | ${item.对账明细名称 || ''}`);
});
}
if (result.amountDiff.length > 0) {
console.log(`\n【金额存在差异的订单 - 共${result.amountDiff.length}条】`);
console.log('-'.repeat(70));
result.amountDiff.forEach((item, i) => {
console.log(`${i+1}. ${item.订单号} | ${item.入住人}`);
console.log(` 酒店: ¥${item.酒店金额} | 商旅: ¥${item.商旅金额} | 差额: ¥${item.差额}`);
});
}
console.log('\n' + '='.repeat(70));
// 计算总金额
let hotelTotal = 0;
let geelyTotal = 0;
for (const item of result.matched) {
hotelTotal += item.金额;
geelyTotal += item.金额;
}
for (const item of result.amountDiff) {
hotelTotal += item.酒店金额;
geelyTotal += item.商旅金额;
}
for (const item of result.hotelOnly) {
hotelTotal += item.结算金额;
}
for (const item of result.geelyOnly) {
geelyTotal += item.金额;
}
console.log(`\n【金额汇总】`);
console.log(` 酒店账单总金额: ¥${hotelTotal}`);
console.log(` 商旅账单总金额: ¥${geelyTotal}`);
console.log(` 总差额: ¥${hotelTotal - geelyTotal}`);
console.log('');
return {
统计: {
酒店订单总数: hotelOrdersCount,
商旅订单总数: geelyOrdersCount,
匹配成功: result.matched.length,
金额差异: result.amountDiff.length,
酒店有商旅无: result.hotelOnly.length,
商旅有酒店无: result.geelyOnly.length,
酒店总金额: hotelTotal,
商旅总金额: geelyTotal,
总差额: hotelTotal - geelyTotal
},
酒店有商旅无: result.hotelOnly,
商旅有酒店无: result.geelyOnly,
金额差异: result.amountDiff,
匹配成功: result.matched
};
}
/**
* 功能将结果保存为JSON文件
* 参数report - 报告对象, filename - 文件名
*/
function saveReportToJson(report, filename) {
const jsonReport = JSON.parse(JSON.stringify(report, (key, value) => {
if (typeof value === 'bigint') return value.toString();
return value;
}));
fs.writeFileSync(filename, JSON.stringify(jsonReport, null, 2), 'utf8');
console.log(`详细结果已保存到: ${filename}`);
}
// 主函数
function main() {
const geelyFile = '杭州碧丽酒店管理有限公司 2026年06月 对账单(0601-0630).xlsx';
const hotelFile = '杭州滨江希尔顿欢朋酒店-吉利商旅6月账单.xlsx';
console.log('正在读取Excel文件...');
const geelyWorkbook = readExcel(geelyFile);
const hotelWorkbook = readExcel(hotelFile);
console.log('正在提取订单数据...');
const hotelOrders = extractHotelOrders(hotelWorkbook);
const geelyOrders = extractGeelyExportOrders(geelyWorkbook);
console.log(`酒店有效订单数: ${hotelOrders.size}`);
console.log(`商旅Export表有效订单数(合并后): ${geelyOrders.size}`);
console.log('\n正在对比订单...');
const result = compareOrders(hotelOrders, geelyOrders);
const report = generateReport(result, hotelOrders.size, geelyOrders.size);
saveReportToJson(report, '账单差异报告.json');
console.log('\n对比完成');
}
main();

View File

@ -1,165 +0,0 @@
const XLSX = require('xlsx');
const fs = require('fs');
const path = require('path');
/**
* 对比脚本商旅 vs 吉智 金额核对
*
* 输入一个包含商旅吉智两个 sheet Excel merge_kaipiao.js 的合并结果
*
* 逻辑
* 1. 商旅 sheet结算账户聚合账单总金额相加
* 2. 吉智 sheet*对账单/项目名称聚合*账单应收总金额相加
* 吉智的*对账单/项目名称对应商旅的结算账户
* 3. 对比两侧同一 key 的金额找出不同
* - 仅商旅有的 key
* - 仅吉智有的 key
* - 两侧都有但金额不一致的 key
*
* 用法
* node compare_kaipiao.js <合并excel路径> [输出结果路径]
*
* 例如
* node compare_kaipiao.js "团队开票申请表-合并.xlsx" "对比结果.xlsx"
*/
// ---------- 参数 ----------
const inFile = process.argv[2];
const outFile = process.argv[3] || path.join(process.cwd(), '对比结果.xlsx');
if (!inFile) {
console.error('用法: node compare_kaipiao.js <合并excel路径> [输出结果路径]');
process.exit(1);
}
if (!fs.existsSync(inFile)) {
console.error(`输入文件不存在: ${inFile}`);
process.exit(1);
}
// 金额比较容差(避免浮点误差)
const EPS = 0.005;
// ---------- 工具 ----------
// 按关键字找 sheet 名
function findSheetName(workbook, keyword) {
return workbook.SheetNames.find(n => n.includes(keyword));
}
// 找列下标(精确匹配去空格后的表头)
function findColIndex(header, name) {
return header.findIndex(h => h != null && String(h).trim() === name);
}
// 规范化 key去首尾空格
function normKey(v) {
return v == null ? '' : String(v).trim();
}
// 解析金额为数字(去掉逗号/空格/货币符号)
function toNumber(v) {
if (v == null || v === '') return 0;
if (typeof v === 'number') return v;
const cleaned = String(v).replace(/[,\s\uFFE5\u00A5¥]/g, '');
const n = parseFloat(cleaned);
return isNaN(n) ? 0 : n;
}
// 保留两位小数
function round2(n) {
return Math.round(n * 100) / 100;
}
// 按 keyCol 聚合 amountCol
function aggregate(sheet, keyColName, amountColName, label) {
const rows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: null, blankrows: false });
if (rows.length === 0) return new Map();
const header = rows[0];
const keyIdx = findColIndex(header, keyColName);
const amtIdx = findColIndex(header, amountColName);
if (keyIdx === -1) {
console.error(`${label}: 未找到列 "${keyColName}"`);
process.exit(1);
}
if (amtIdx === -1) {
console.error(`${label}: 未找到列 "${amountColName}"`);
process.exit(1);
}
const map = new Map();
for (let i = 1; i < rows.length; i++) {
const key = normKey(rows[i][keyIdx]);
if (key === '') continue;
const amt = toNumber(rows[i][amtIdx]);
const cur = map.get(key) || { sum: 0, count: 0 };
cur.sum += amt;
cur.count += 1;
map.set(key, cur);
}
return map;
}
// ---------- 读取 ----------
const wb = XLSX.readFile(inFile);
const shangluName = findSheetName(wb, '商旅');
const jizhiName = findSheetName(wb, '吉智');
if (!shangluName) { console.error('未找到含"商旅"的 sheet'); process.exit(1); }
if (!jizhiName) { console.error('未找到含"吉智"的 sheet'); process.exit(1); }
const shangluMap = aggregate(wb.Sheets[shangluName], '结算账户', '账单总金额', '商旅');
const jizhiMap = aggregate(wb.Sheets[jizhiName], '*对账单/项目名称', '*账单应收总金额', '吉智');
// ---------- 对比 ----------
const allKeys = new Set([...shangluMap.keys(), ...jizhiMap.keys()]);
const diffRows = [['结算账户/项目名称', '商旅账单总金额', '吉智账单应收总金额', '差额(商旅-吉智)', '差异类型']];
let onlyShanglu = 0, onlyJizhi = 0, mismatch = 0, matched = 0;
for (const key of [...allKeys].sort()) {
const s = shangluMap.get(key);
const j = jizhiMap.get(key);
if (s && !j) {
diffRows.push([key, round2(s.sum), null, round2(s.sum), '仅商旅有']);
onlyShanglu++;
} else if (!s && j) {
diffRows.push([key, null, round2(j.sum), round2(-j.sum), '仅吉智有']);
onlyJizhi++;
} else {
const delta = s.sum - j.sum;
if (Math.abs(delta) > EPS) {
diffRows.push([key, round2(s.sum), round2(j.sum), round2(delta), '金额不一致']);
mismatch++;
} else {
matched++;
}
}
}
// ---------- 输出 ----------
const outWb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet(diffRows);
XLSX.utils.book_append_sheet(outWb, ws, '差异');
// 汇总 sheet
const summary = [
['项目', '数量'],
['商旅去重结算账户数', shangluMap.size],
['吉智去重项目名称数', jizhiMap.size],
['仅商旅有', onlyShanglu],
['仅吉智有', onlyJizhi],
['金额不一致', mismatch],
['金额一致', matched],
];
XLSX.utils.book_append_sheet(outWb, XLSX.utils.aoa_to_sheet(summary), '汇总');
XLSX.writeFile(outWb, outFile);
console.log('对比完成:');
console.log(` 商旅去重结算账户数: ${shangluMap.size}`);
console.log(` 吉智去重项目名称数: ${jizhiMap.size}`);
console.log(` 仅商旅有: ${onlyShanglu}`);
console.log(` 仅吉智有: ${onlyJizhi}`);
console.log(` 金额不一致: ${mismatch}`);
console.log(` 金额一致: ${matched}`);
console.log(` 输出文件: ${outFile}`);

View File

@ -1,200 +0,0 @@
const https = require('https');
const XLSX = require('xlsx');
const path = require('path');
// ======================== 配置 ========================
const EXCEL_FILE = '新建 XLSX 工作表.xlsx';
const SHEET_NAME = '2026';
const BASE_HOST = 'superstar.geelytravel.com';
const API_BASE = '/api/tmc/tmc';
const STEP_DELAY_MS = 3000;
const API_HEADERS = {
'accept': 'application/json, text/plain, */*',
'accept-language': 'zh-CN,zh;q=0.9',
'authorization': 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKV1QtRC1TRVFfVVNFUl9UTUNfMDAwMDAxODMiLCJ1c2VyX2NvZGUiOiJTRVFfVVNFUl9UTUNfMDAwMDAxODMiLCJyb2xlcyI6WyLnu5PnrpfkuJPlkZgiLCLnu5PnrpfkuJPlkZgiXSwiZXhwIjoxNzg2NTMzNTUxLCJpYXQiOjE3ODM5NDE1NTF9.1Mcmf5IM07Scrry4FzPs6TAcfUu0Nqtjre_2iJlNd5c',
'cache-control': 'no-cache',
'pragma': 'no-cache',
'requestsource': 'D',
'usercode': 'SEQ_USER_TMC_00000183',
'cookie': '_c_WBKFRo=CefecIUfnnuqejh60diYtytjDPwI9Qptslcwakc4; acw_tc=731dc87617839414337367659e81455072769ade5ba31e7a9d3e4d5d817123',
'Referer': 'https://superstar.geelytravel.com/new-tmc/finance/account/saleBill/list',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
};
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function httpRequest(method, path, headers, body) {
return new Promise((resolve, reject) => {
const opts = {
hostname: BASE_HOST,
path,
method,
headers: headers || API_HEADERS,
rejectUnauthorized: false,
};
const req = https.request(opts, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
resolve({ status: res.statusCode, data: JSON.parse(data) });
} catch {
resolve({ status: res.statusCode, data });
}
});
});
req.on('error', reject);
if (body) req.write(body);
req.end();
});
}
// ======================== 1. 读取 Excel ========================
function readTeamIds() {
const workbook = XLSX.readFile(path.join(__dirname, EXCEL_FILE));
const sheet = workbook.Sheets[SHEET_NAME];
if (!sheet) { console.error('Sheet not found'); process.exit(1); }
const rows = XLSX.utils.sheet_to_json(sheet, { defval: null });
const teamIds = [];
for (const row of rows) {
const val = row['团号'];
const kaipiaoStatus = row['开票状态'];
if (!val || !/^TEAM-\d+$/.test(String(val).trim())) continue;
if (kaipiaoStatus && String(kaipiaoStatus).trim() !== '未开票') {
console.log(`${String(val).trim()} 开票状态: "${kaipiaoStatus}",跳过`);
continue;
}
teamIds.push(String(val).trim());
}
return teamIds;
}
// ======================== 2. 获取 settlementAccountIds ========================
async function querySettlementAccountIds(teamId) {
const path = `${API_BASE}/customerAccount/settlement/querySettlementAccountIds?customerSettlementAccountName=${encodeURIComponent(teamId)}`;
console.log(` [GET] ${path}`);
const res = await httpRequest('GET', path);
console.log(` [OK]`, JSON.stringify(res.data));
return (res.data && res.data.code === 'Z000' && Array.isArray(res.data.result)) ? res.data.result : [];
}
// ======================== 3. 获取结算单列表 ========================
async function listReceiveStatements(settlementAccountId) {
const path = `${API_BASE}/settlement/receiveStatements/listPageReceiveStatements?settlementAccountId=${settlementAccountId}&sortType=1&pageIndex=1&pageSize=10`;
console.log(` [GET] ${path}`);
const res = await httpRequest('GET', path);
console.log(` [OK]`, JSON.stringify(res.data));
return (res.data && res.data.code === 'Z000' && res.data.result && res.data.result.list) ? res.data.result.list : [];
}
// ======================== 4. 自动对账POST ========================
async function autoReconciliation(statementSeq, id) {
const path = '/api/tmc/finance/frond/receiveStatements/autoReconciliation';
const headers = {
...API_HEADERS,
'content-type': 'application/json;charset=UTF-8',
'Referer': `https://superstar.geelytravel.com/new-tmc/finance/account/saleBill/detail/${id}/${statementSeq}`,
};
const body = JSON.stringify({ statementSeq });
console.log(` [POST] ${path} body=${body}`);
const res = await httpRequest('POST', path, headers, body);
console.log(` [OK]`, JSON.stringify(res.data));
return res.data;
}
// ======================== 5. 确认对账POST ========================
async function confirmReconciliation(statementSeq, id) {
const path = `${API_BASE}/settlement/receiveStatementsGroup/confirmCompleteReceiveReconciliation?statementSeq=${statementSeq}`;
const headers = {
...API_HEADERS,
'Referer': `https://superstar.geelytravel.com/new-tmc/finance/account/saleBill/detail/${id}/${statementSeq}`,
};
console.log(` [POST] ${path}`);
const res = await httpRequest('POST', path, headers, null);
console.log(` [OK]`, JSON.stringify(res.data));
return res.data;
}
// ======================== 主流程 ========================
async function main() {
const teamIds = readTeamIds();
console.log(`
TEAM IDs: ${teamIds.join(', ')}
`);
// 预查询所有结算单,计算总任务数
console.log('预查询结算单列表...');
const allTasks = [];
for (const teamId of teamIds) {
const accounts = await querySettlementAccountIds(teamId);
if (!accounts.length) continue;
for (const account of accounts) {
const statements = await listReceiveStatements(account.id);
for (const st of statements) {
allTasks.push({ teamId, accountId: account.id, statement: st });
}
}
}
const total = allTasks.length;
const pendingTasks = allTasks.filter(t => t.statement.statementStatus !== '5');
const pendingTotal = pendingTasks.length;
const skipCount = total - pendingTotal;
console.log(`总任务: ${total} | 跳过(已完成): ${skipCount} | 待处理: ${pendingTotal}
`);
if (pendingTotal === 0) {
console.log('所有结算单已完成,无需处理。');
return;
}
const allResults = [];
let completed = 0;
for (const task of allTasks) {
const { teamId, accountId, statement: st } = task;
const { id, statementSeq, statementName, receivableTotalPrice, statementStatus } = st;
const pct = Math.round((completed / pendingTotal) * 100);
console.log(`
${'='.repeat(50)}`);
console.log(` 进度: ${pct}% (${completed}/${pendingTotal}) | ${teamId} | ${statementSeq}`);
console.log(`${'='.repeat(50)}`);
console.log(` 结算单: ${statementName}`);
console.log(` 应收: \u00a5${receivableTotalPrice} | 状态: ${statementStatus}`);
if (statementStatus === '5') {
console.log(` \u26a0 已完成,跳过`);
allResults.push({ teamId, settlementAccountId: accountId, statementSeq, statementName, receivableTotalPrice, autoReconResult: { code: 'SKIP' }, confirmResult: { code: 'SKIP' } });
continue;
}
const autoResult = await autoReconciliation(statementSeq, id);
console.log(` \u23f3 等待 ${STEP_DELAY_MS / 1000}s...`);
await sleep(STEP_DELAY_MS);
const confirmResult = await confirmReconciliation(statementSeq, id);
allResults.push({ teamId, settlementAccountId: accountId, statementSeq, statementName, receivableTotalPrice, autoReconResult: autoResult, confirmResult });
completed++;
console.log(` >>> 进度: ${Math.round((completed / pendingTotal) * 100)}% (${completed}/${pendingTotal})`);
}
// ======================== 汇总 ========================
console.log('\n\n' + '='.repeat(70));
console.log(' 最终汇总');
console.log('='.repeat(70));
for (const item of allResults) {
const aOk = item.autoReconResult && (item.autoReconResult.code === 'Z000' || item.autoReconResult.code === 'SKIP');
const cOk = item.confirmResult && (item.confirmResult.code === 'Z000' || item.confirmResult.code === 'SKIP');
console.log(`\n${aOk ? '\u2705' : '\u274c'} 自动 | ${cOk ? '\u2705' : '\u274c'} 确认 | ${item.teamId} | ${item.statementSeq}`);
console.log(` ${item.statementName} \u00a5${item.receivableTotalPrice}`);
console.log(` 自动: ${JSON.stringify(item.autoReconResult)}`);
console.log(` 确认: ${JSON.stringify(item.confirmResult)}`);
}
}
main().catch(err => { console.error('Error:', err); process.exit(1); });

View File

@ -1,255 +0,0 @@
const XLSX = require('xlsx');
const path = require('path');
/**
* 开票清单 -> 吉智对账单 填列脚本
*
* 输入 excel1: sheet="发票清单"
* col "资源类型" = 大类(火车票/国际机票/国内机票/国际酒店/酒店/附加项)
* col "开票明细" = 小类(包含字符: 机票/机票服务费/住宿费/住宿服务费/火车票/火车票服务费)
* col "开票金额" = 金额
* col "结算账户" = 归属 TEAM 编号(TEAM-XXXX XXXXTEAM-XXXX, 结尾 : 去掉)
*
* 输出 excel2: sheet 名含"吉智"(只有一个)
* 有一行数据的 col "*对账单/项目名称" = TEAM 编号
* 填写以下列(每个项名对应一个 col):
* 吉智服务费 = 酒店服务费 + 机票服务费 + 火车票服务费
* 酒店房费 = 国际酒店房费 + 酒店房费
* 酒店服务费 = 国际酒店服务费 + 酒店服务费
* 机票服务费 = 国际机票服务费 + 国内机票服务费
* 火车票服务费 = 火车票服务费
*
* 中断条件: excel2 中找不到对应 TEAM -> 提示并中断
* 找到多个对应 TEAM -> 提示并中断
*
* 用法: node jizhi_fill.js [excel1路径] [excel2路径]
* 不带参数则使用下方配置常量
*/
// ======================== 配置 ========================
const EXCEL1 = path.join(__dirname, '开票清单.xlsx'); // excel1 路径可修改
const EXCEL2 = path.join(__dirname, '吉智对账单.xlsx'); // excel2 路径可修改
const SHEET1_NAME = '发票清单';
// 需要填入 excel2 的字段: [列名(兼容*与空格), 取值函数]
const FIELDS = [
{ name: '吉智服务费', get: s => s.totalService },
{ name: '酒店房费', get: s => s.hotelRoom },
{ name: '酒店服务费', get: s => s.hotelService },
{ name: '机票服务费', get: s => s.flightService },
{ name: '火车票服务费', get: s => s.trainService },
];
// ======================== 工具 ========================
function round2(n) {
return Math.round((Number(n) + Number.EPSILON) * 100) / 100;
}
// 去掉全角/半角冒号(仅结尾), 以及空格
function normalizeTeam(v) {
let s = String(v == null ? '' : v).trim();
s = s.replace(/[:]\s*$/, '').trim();
return s;
}
// 在 sheet 名中查找包含关键字的 sheet(恰好一个)
function findSheet(workbook, keyword, label) {
const names = workbook.SheetNames.filter(n => n.includes(keyword));
if (names.length === 0) {
console.error(`[中断] ${label}: 未找到名称包含"${keyword}"的 sheet`);
process.exit(1);
}
if (names.length > 1) {
console.error(`[中断] ${label}: 找到多个名称包含"${keyword}"的 sheet: ${names.join(', ')}`);
process.exit(1);
}
return names[0];
}
// 精确(去星号/空格)匹配列名 -> 列下标; 找不到返回 -1
function findColIndex(rows, targetName) {
const want = targetName.replace(/[*\s]/g, '');
const header = rows[0];
if (!header) return -1;
for (let c = 0; c < header.length; c++) {
const h = header[c];
if (h == null) continue;
if (String(h).replace(/[*\s]/g, '') === want) return c;
}
return -1;
}
// 读取一个 sheet 成二维数组(含表头)
function sheetToRows(sheet) {
return XLSX.utils.sheet_to_json(sheet, { header: 1, defval: null, blankrows: false });
}
// ======================== 1. 读取 excel1 ========================
function readInvoice(excel1Path) {
const wb = XLSX.readFile(excel1Path);
const sheet = wb.Sheets[SHEET1_NAME];
if (!sheet) {
console.error(`[中断] excel1: 未找到 sheet "${SHEET1_NAME}"`);
process.exit(1);
}
const rows = sheetToRows(sheet);
const header = rows[0];
const col = name => findColIndex(rows, name);
const idxResource = col('资源类型');
const idxDetail = col('开票明细');
const idxAmount = col('开票金额');
const idxAccount = col('结算账户');
if ([idxResource, idxDetail, idxAmount, idxAccount].some(i => i === -1)) {
console.error(`[中断] excel1: 缺少必要列(资源类型/开票明细/开票金额/结算账户), 现有表头: ${JSON.stringify(header)}`);
process.exit(1);
}
// 每个 TEAM 的累加结果
const sums = new Map(); // key: teamId -> {totalService, hotelRoom, hotelService, flightService, trainService}
const newSum = () => ({ totalService: 0, hotelRoom: 0, hotelService: 0, flightService: 0, trainService: 0 });
for (let r = 1; r < rows.length; r++) {
const row = rows[r];
const resource = row[idxResource] == null ? '' : String(row[idxResource]).trim();
const detail = row[idxDetail] == null ? '' : String(row[idxDetail]).trim();
const amount = Number(row[idxAmount]);
const accountRaw = row[idxAccount];
if (resource === '' && detail === '') continue; // 空行
// 归属 TEAM
const match = normalizeTeam(accountRaw).match(/TEAM-\d+/);
if (!match) {
console.warn(` [警告] 第${r + 1}行: 无法从结算账户提取 TEAM (值: "${accountRaw}"),跳过`);
continue;
}
const teamId = match[0];
const s = sums.get(teamId) || newSum();
sums.set(teamId, s);
if (Number.isNaN(amount)) {
console.warn(` [警告] 第${r + 1}行: 开票金额无法解析 (值: "${row[idxAmount]}"),跳过`);
continue;
}
// 按"开票明细"小类包含字符归类(注意先匹配更具体的"服务费")
if (detail.includes('住宿服务费')) {
s.hotelService += amount;
} else if (detail.includes('住宿费')) {
s.hotelRoom += amount;
} else if (detail.includes('机票服务费')) {
s.flightService += amount;
} else if (detail.includes('火车票服务费')) {
s.trainService += amount;
} else {
console.warn(` [警告] 第${r + 1}行: 开票明细"${detail}"未识别到服务费/住宿费类别,跳过 (${teamId}, ${amount})`);
continue;
}
}
// 计算吉智服务费 = 各服务费之和
for (const s of sums.values()) {
s.totalService = round2(s.hotelService + s.flightService + s.trainService);
s.hotelRoom = round2(s.hotelRoom);
s.hotelService = round2(s.hotelService);
s.flightService = round2(s.flightService);
s.trainService = round2(s.trainService);
}
if (sums.size === 0) {
console.error('[中断] excel1: 未提取到任何 TEAM 数据');
process.exit(1);
}
console.log(`[excel1] 解析完成, 共 ${sums.size} 个 TEAM:`);
for (const [tid, s] of sums) {
console.log(` ${tid}: 吉智服务费=${s.totalService}, 酒店房费=${s.hotelRoom}, 酒店服务费=${s.hotelService}, 机票服务费=${s.flightService}, 火车票服务费=${s.trainService}`);
}
return sums;
}
// ======================== 2. 写入 excel2 ========================
function writeJizhi(excel2Path, sums) {
const wb = XLSX.readFile(excel2Path);
const sheetName = findSheet(wb, '吉智', 'excel2');
const sheet = wb.Sheets[sheetName];
const rows = sheetToRows(sheet);
const header = rows[0];
const nameIdx = findColIndex(rows, '对账单/项目名称');
if (nameIdx === -1) {
console.error(`[中断] excel2: 未找到列"对账单/项目名称", 现有表头: ${JSON.stringify(header)}`);
process.exit(1);
}
// 收集每个 TEAM 匹配到的行
const matched = new Map(); // teamId -> [rowIdx...]
for (let r = 1; r < rows.length; r++) {
const val = rows[r][nameIdx];
if (val == null || String(val).trim() === '') continue;
const teamId = normalizeTeam(val).match(/TEAM-\d+/);
if (!teamId) continue;
const tid = teamId[0];
if (!matched.has(tid)) matched.set(tid, []);
matched.get(tid).push(r);
}
// 校验: 每个需要填写的 TEAM 在 excel2 中必须恰好匹配一行
for (const [tid] of sums) {
const rowsFound = matched.get(tid) || [];
if (rowsFound.length === 0) {
console.error(`[中断] excel2: 找不到 TEAM "${tid}" 对应的行, 请检查"对账单/项目名称"列`);
process.exit(1);
}
if (rowsFound.length > 1) {
console.error(`[中断] excel2: TEAM "${tid}" 在 excel2 中找到 ${rowsFound.length} 行(第 ${rowsFound.map(x => x + 1).join(', ')} 行), 存在重复, 无法确定唯一目标`);
process.exit(1);
}
}
// 写入各字段
for (const field of FIELDS) {
const colIdx = findColIndex(rows, field.name);
if (colIdx === -1) {
console.error(`[中断] excel2: 未找到列"${field.name}", 现有表头: ${JSON.stringify(header)}`);
process.exit(1);
}
for (const [tid, s] of sums) {
const rowIdx = matched.get(tid)[0];
XLSX.utils.sheet_add_aoa(sheet, [[field.get(s)]], { origin: { r: rowIdx, c: colIdx } });
}
}
XLSX.writeFile(wb, excel2Path);
console.log(`\n[excel2] 已写入: ${excel2Path} (sheet: ${sheetName})`);
for (const [tid] of sums) {
console.log(` 填写完成: ${tid} (第 ${matched.get(tid)[0] + 1} 行)`);
}
}
// ======================== 主流程 ========================
function main() {
const excel1Path = process.argv[2] || EXCEL1;
const excel2Path = process.argv[3] || EXCEL2;
if (!require('fs').existsSync(excel1Path)) {
console.error(`[中断] excel1 文件不存在: ${excel1Path}`);
process.exit(1);
}
if (!require('fs').existsSync(excel2Path)) {
console.error(`[中断] excel2 文件不存在: ${excel2Path}`);
process.exit(1);
}
console.log(`excel1: ${excel1Path}`);
console.log(`excel2: ${excel2Path}\n`);
const sums = readInvoice(excel1Path);
writeJizhi(excel2Path, sums);
console.log('\n========== 完成 ==========');
}
main();

View File

@ -1,379 +0,0 @@
const XLSX = require('xlsx');
const path = require('path');
// ========== 配置 ==========
const EXCEL_FILE = path.join(__dirname, '新建 XLSX 工作表.xlsx');
const SHEET_NAME = '2026';
const BASE_URL = 'https://superstar.geelytravel.com';
const AUTH_TOKEN = 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKV1QtRC1TRVFfVVNFUl9UTUNfMDAwMDAxODMiLCJ1c2VyX2NvZGUiOiJTRVFfVVNFUl9UTUNfMDAwMDAxODMiLCJyb2xlcyI6WyLnu5PnrpfkuJPlkZgiLCLnu5PnrpfkuJPlkZgiXSwiZXhwIjoxNzg2NTMzNTUxLCJpYXQiOjE3ODM5NDE1NTF9.1Mcmf5IM07Scrry4FzPs6TAcfUu0Nqtjre_2iJlNd5c';
const COMMON_HEADERS = {
'accept': 'application/json, text/plain, */*',
'accept-language': 'zh-CN,zh;q=0.9',
'authorization': AUTH_TOKEN,
'cache-control': 'no-cache',
'pragma': 'no-cache',
'requestsource': 'D',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'usercode': 'SEQ_USER_TMC_00000183',
'cookie': '_c_WBKFRo=CefecIUfnnuqejh60diYtytjDPwI9Qptslcwakc4; acw_tc=731dc87617840296932491914e815aef28b42e276e1a01eb53ed0f548bd5e3',
};
const INVOICE_APPLY_TITLE = '浙江吉利商务服务有限公司';
const INVOICE_TITLE = '吉智(杭州)文化创意有限公司';
const TAXPAYER_ID = '91330109MA2GM71J64';
const ADDRESS = '杭州市萧山区经纪技术开发区启迪路198号A-B102-126B室';
const TELEPHONE = '0571-85321518';
const DEPOSIT_BANK = '中国民生银行股份有限公司杭州分行';
const BANK_ACCOUNT = '631101003';
// ========== 1. 读取 Excel ==========
function readTeamIds() {
const workbook = XLSX.readFile(EXCEL_FILE);
const sheet = workbook.Sheets[SHEET_NAME];
if (!sheet) {
console.error('Sheet "' + SHEET_NAME + '" 不存在');
process.exit(1);
}
const rows = XLSX.utils.sheet_to_json(sheet, { defval: null });
const teamIds = [];
for (const row of rows) {
const val = row['团号'];
if (val && /^TEAM-\d+$/.test(String(val).trim())) {
teamIds.push(String(val).trim());
}
}
console.log('[Excel] 找到 ' + teamIds.length + ' 个 TEAM ID: ' + teamIds.join(', ') + '\n');
return teamIds;
}
// ========== 2. 查询 settlement account ==========
async function querySettlementAccountIds(teamId) {
const url = BASE_URL + '/api/tmc/tmc/customerAccount/settlement/querySettlementAccountIds?customerSettlementAccountName=' + teamId;
const resp = await fetch(url, { headers: COMMON_HEADERS, method: 'GET' });
return resp.json();
}
// ========== 3. 查询 bill list ==========
async function queryBillList(settlementAccountId) {
const url = BASE_URL + '/api/tmc/tmc/settlement/receiveBill/queryBillList?settlementAccountId=' + settlementAccountId + '&pageIndex=1&pageSize=10';
const resp = await fetch(url, { headers: COMMON_HEADERS, method: 'GET' });
return resp.json();
}
// ========== 4. 批量调整发票类型 ==========
async function batchAdjustInvoiceOrderPrice(settlementSeq, receiveBillId, originInvoiceType, targetInvoiceType) {
const url = BASE_URL + '/api/tmc/tmc/settlement/receiveInvoice/batchAdjustInvoiceOrderPrice';
const headers = {
...COMMON_HEADERS,
'content-type': 'application/json;charset=UTF-8',
'Referer': BASE_URL + '/new-tmc/finance/settlement/pay/invoice/billing/' + receiveBillId,
};
const body = JSON.stringify({
settlementSeq: settlementSeq,
orderMode: '1',
originInvoiceType: originInvoiceType,
targetInvoiceType: targetInvoiceType,
});
const resp = await fetch(url, { headers: headers, method: 'POST', body: body });
return resp.json();
}
// ========== 5. 确认开票 ==========
async function confirmInvoiceOrder(settlementSeq, receiveBillId) {
const url = BASE_URL + '/api/tmc/tmc/settlement/receiveInvoice/confirmInvoiceOrder';
const headers = {
...COMMON_HEADERS,
'content-type': 'application/json;charset=UTF-8',
'Referer': BASE_URL + '/new-tmc/finance/settlement/pay/invoice/billing/' + receiveBillId,
};
const body = JSON.stringify({ settlementSeq: settlementSeq, orderMode: '1' });
const resp = await fetch(url, { headers: headers, method: 'POST', body: body });
return resp.json();
}
// ========== 6. 提交开票 ==========
async function submitInvoiceApply(confirmInvoiceKey, invoiceInfoList, receiveBillId) {
const url = BASE_URL + '/api/tmc/tmc/settlement/receiveInvoice/submitInvoiceApply';
const headers = {
...COMMON_HEADERS,
'content-type': 'application/json;charset=UTF-8',
'Referer': BASE_URL + '/new-tmc/finance/settlement/pay/invoice/billing/' + receiveBillId,
};
const body = JSON.stringify({
confirmInvoiceKey: confirmInvoiceKey,
invoiceInfoList: invoiceInfoList,
});
const resp = await fetch(url, { headers: headers, method: 'POST', body: body });
return resp.json();
}
// ========== 7. 填充公司信息 ==========
function fillCompanyInfo(item) {
return {
...item,
invoiceApplyTitle: INVOICE_APPLY_TITLE,
invoiceTitle: INVOICE_TITLE,
taxpayerIdentityNumber: TAXPAYER_ID,
address: ADDRESS,
telephone: TELEPHONE,
depositBank: DEPOSIT_BANK,
bankAccount: BANK_ACCOUNT,
};
}
// ========== 8. 映射火车票开票项 ==========
function mapTrainInvoice(serverItem) {
const invoiceType = serverItem.invoiceType || '';
const item = {
invoiceSerialNum: serverItem.invoiceSerialNum,
invoiceType: invoiceType,
invoicePrice: serverItem.invoicePrice,
taxRate: invoiceType === '13' ? 6 : 0,
invoiceSettlementDetail: serverItem.settlementDetails || '',
invoiceQuantity: serverItem.invoiceQuantity || 0,
ticketList: serverItem.ticketList || null,
};
if (invoiceType === '13') {
item.invoiceContent = '*生产生活服务*代订火车票服务费';
}
return fillCompanyInfo(item);
}
// ========== 9. 映射酒店开票项 ==========
function mapHotelInvoice(serverItem) {
const invoiceType = serverItem.invoiceType || '';
const item = {
invoiceSerialNum: serverItem.invoiceSerialNum,
invoiceType: invoiceType,
invoicePrice: serverItem.invoicePrice,
taxRate: 6,
invoiceSettlementDetail: serverItem.settlementDetails || '',
invoiceQuantity: serverItem.invoiceQuantity || 0,
ticketList: serverItem.ticketList || null,
};
if (invoiceType === '2' || invoiceType === '1') {
item.invoiceContent = '*生产生活服务*代订住宿费';
} else if (invoiceType === '13') {
item.invoiceContent = '*生产生活服务*代订住宿服务费';
}
return fillCompanyInfo(item);
}
// ========== 10. 映射机票开票项(国内+国际通用) ==========
function mapFlightInvoice(serverItem) {
const invoiceType = serverItem.invoiceType || '';
const item = {
invoiceSerialNum: serverItem.invoiceSerialNum,
invoiceType: invoiceType,
invoicePrice: serverItem.invoicePrice,
taxRate: 6,
invoiceSettlementDetail: serverItem.settlementDetails || '',
invoiceQuantity: serverItem.invoiceQuantity || 0,
ticketList: serverItem.ticketList || null,
};
if (invoiceType === '2') {
item.invoiceContent = '*生产生活服务*代订机票';
} else if (invoiceType === '13') {
item.invoiceContent = '*生产生活服务*代订机票服务费';
}
return fillCompanyInfo(item);
}
// ========== 11. 处理火车票 ==========
async function processTrainBill(bill, billIndex, totalBills) {
const settlementSeq = bill.settlementSeq;
const receiveBillId = bill.receiveBillId;
console.log(' --- 火车票 [' + billIndex + '/' + totalBills + '] ' + bill.statementName + ' ---');
const confirmResp = await confirmInvoiceOrder(settlementSeq, receiveBillId);
console.log(' -> confirmInvoiceOrder:', JSON.stringify(confirmResp));
const confirmResult = confirmResp?.result;
if (!confirmResult || !confirmResult.invoiceAble) {
console.log(' ⚠ 跳过: ' + (confirmResp?.message || '') + '\n');
return;
}
const confirmInvoiceKey = confirmResult.confirmInvoiceKey;
const submitList = (confirmResult.invoiceInfoList || []).map(mapTrainInvoice);
const submitResp = await submitInvoiceApply(confirmInvoiceKey, submitList, receiveBillId);
console.log(' -> submitInvoiceApply:', JSON.stringify(submitResp));
console.log('');
}
// ========== 12. 处理酒店 ==========
async function processHotelBill(bill, billIndex, totalBills) {
const settlementSeq = bill.settlementSeq;
const receiveBillId = bill.receiveBillId;
console.log(' --- 酒店 [' + billIndex + '/' + totalBills + '] ' + bill.statementName + ' ---');
const adjustResp = await batchAdjustInvoiceOrderPrice(settlementSeq, receiveBillId, '1', '2');
console.log(' -> batchAdjust (1→2):', JSON.stringify(adjustResp));
if (adjustResp?.code !== 'Z000') {
console.log(' ⚠ 调整失败: ' + (adjustResp?.message || '') + '\n');
return;
}
const confirmResp = await confirmInvoiceOrder(settlementSeq, receiveBillId);
console.log(' -> confirmInvoiceOrder:', JSON.stringify(confirmResp));
const confirmResult = confirmResp?.result;
if (!confirmResult || !confirmResult.invoiceAble) {
console.log(' ⚠ 跳过: ' + (confirmResp?.message || '') + '\n');
return;
}
const confirmInvoiceKey = confirmResult.confirmInvoiceKey;
const submitList = (confirmResult.invoiceInfoList || []).map(mapHotelInvoice);
const submitResp = await submitInvoiceApply(confirmInvoiceKey, submitList, receiveBillId);
console.log(' -> submitInvoiceApply:', JSON.stringify(submitResp));
console.log('');
}
// ========== 13. 处理国内机票 ==========
async function processFlightBill(bill, billIndex, totalBills) {
const settlementSeq = bill.settlementSeq;
const receiveBillId = bill.receiveBillId;
console.log(' --- 国内机票 [' + billIndex + '/' + totalBills + '] ' + bill.statementName + ' ---');
const adjust1Resp = await batchAdjustInvoiceOrderPrice(settlementSeq, receiveBillId, '4', '2');
console.log(' -> batchAdjust (4→2):', JSON.stringify(adjust1Resp));
if (adjust1Resp?.code !== 'Z000') {
console.log(' ⚠ 调整失败: ' + (adjust1Resp?.message || '') + '\n');
return;
}
const adjust2Resp = await batchAdjustInvoiceOrderPrice(settlementSeq, receiveBillId, '30', '2');
console.log(' -> batchAdjust (30→2):', JSON.stringify(adjust2Resp));
if (adjust2Resp?.code !== 'Z000') {
console.log(' ⚠ 调整失败: ' + (adjust2Resp?.message || '') + '\n');
return;
}
const confirmResp = await confirmInvoiceOrder(settlementSeq, receiveBillId);
console.log(' -> confirmInvoiceOrder:', JSON.stringify(confirmResp));
const confirmResult = confirmResp?.result;
if (!confirmResult || !confirmResult.invoiceAble) {
console.log(' ⚠ 跳过: ' + (confirmResp?.message || '') + '\n');
return;
}
const confirmInvoiceKey = confirmResult.confirmInvoiceKey;
const submitList = (confirmResult.invoiceInfoList || []).map(mapFlightInvoice);
const submitResp = await submitInvoiceApply(confirmInvoiceKey, submitList, receiveBillId);
console.log(' -> submitInvoiceApply:', JSON.stringify(submitResp));
console.log('');
}
// ========== 14. 处理国际机票 ==========
async function processInternationalFlightBill(bill, billIndex, totalBills) {
const settlementSeq = bill.settlementSeq;
const receiveBillId = bill.receiveBillId;
console.log(' --- 国际机票 [' + billIndex + '/' + totalBills + '] ' + bill.statementName + ' ---');
const confirmResp = await confirmInvoiceOrder(settlementSeq, receiveBillId);
console.log(' -> confirmInvoiceOrder:', JSON.stringify(confirmResp));
const confirmResult = confirmResp?.result;
if (!confirmResult || !confirmResult.invoiceAble) {
console.log(' ⚠ 跳过: ' + (confirmResp?.message || '') + '\n');
return;
}
const confirmInvoiceKey = confirmResult.confirmInvoiceKey;
const submitList = (confirmResult.invoiceInfoList || []).map(mapFlightInvoice);
const submitResp = await submitInvoiceApply(confirmInvoiceKey, submitList, receiveBillId);
console.log(' -> submitInvoiceApply:', JSON.stringify(submitResp));
console.log('');
}
// ========== 主流程 ==========
async function main() {
const teamIds = readTeamIds();
if (teamIds.length === 0) {
console.log('没有找到 TEAM ID退出');
return;
}
for (let i = 0; i < teamIds.length; i++) {
const teamId = teamIds[i];
console.log('[' + (i + 1) + '/' + teamIds.length + '] 处理 ' + teamId + ' ...');
try {
const settlementResp = await querySettlementAccountIds(teamId);
const resultList = settlementResp?.result || [];
if (resultList.length === 0) {
console.log(' ⚠ ' + teamId + ' 没有 settlement account跳过\n');
continue;
}
const settlementAccountId = resultList[0]?.id;
console.log(' -> settlementAccountId: ' + settlementAccountId);
const billResp = await queryBillList(settlementAccountId);
const allBills = billResp?.result?.list || [];
const total = billResp?.result?.total || 0;
console.log(' -> 共 ' + total + ' 条对账单');
const skippedBills = allBills.filter(function (b) { return String(b.settlementStatus) === '3'; });
skippedBills.forEach(function (b) {
console.log(' ⚠ 跳过 settlementStatus=3 的对账单: ' + (b.statementName || b.receiveBillId || '') + '\n');
});
const bills = allBills.filter(function (b) { return String(b.settlementStatus) !== '3'; });
console.log(' -> 实际处理 ' + bills.length + ' 条对账单\n');
const trainBills = bills.filter(function (b) { return (b.statementName || '').includes('火车票'); });
const hotelBills = bills.filter(function (b) { return (b.statementName || '').includes('酒店'); });
const flightBills = bills.filter(function (b) { return (b.statementName || '').includes('国内机票'); });
const intlFlightBills = bills.filter(function (b) { return (b.statementName || '').includes('国际机票'); });
const otherBills = bills.filter(function (b) {
const name = b.statementName || '';
return !name.includes('火车票') && !name.includes('酒店') && !name.includes('国内机票') && !name.includes('国际机票');
});
console.log(' -> 火车票: ' + trainBills.length + ', 酒店: ' + hotelBills.length + ', 国内机票: ' + flightBills.length + ', 国际机票: ' + intlFlightBills.length + ', 其他: ' + otherBills.length + '\n');
for (let j = 0; j < trainBills.length; j++) {
await processTrainBill(trainBills[j], j + 1, trainBills.length);
}
for (let j = 0; j < hotelBills.length; j++) {
await processHotelBill(hotelBills[j], j + 1, hotelBills.length);
}
for (let j = 0; j < flightBills.length; j++) {
await processFlightBill(flightBills[j], j + 1, flightBills.length);
}
for (let j = 0; j < intlFlightBills.length; j++) {
await processInternationalFlightBill(intlFlightBills[j], j + 1, intlFlightBills.length);
}
if (otherBills.length > 0) {
console.log(' === 以下对账单未处理(未知类型) ===');
otherBills.forEach(function (b) { console.log(' - ' + b.statementName); });
console.log('');
}
} catch (err) {
console.error(' ✗ ' + teamId + ' 出错:', err.message);
}
console.log('');
}
console.log('========== 完成 ==========');
}
main().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});

View File

@ -1,36 +0,0 @@
const { execSync } = require('child_process');
const path = require('path');
const SCRIPT = path.join(__dirname, 'send-move.ps1');
const PS = `powershell -NoProfile -ExecutionPolicy Bypass -File "${SCRIPT}"`;
function moveMouse(dx) {
execSync(`${PS} ${dx}`, { windowsHide: true });
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function main() {
console.log('已启动防息屏,每 15 秒触发真实鼠标事件SendInput。Ctrl+C 停止。\n');
process.on('SIGINT', () => {
console.log('\n已停止。');
process.exit(0);
});
let tick = 0;
while (true) {
tick++;
moveMouse(1);
console.log(`${tick}】→ 右移 1 像素 (SendInput)`);
await sleep(14000);
moveMouse(-1);
console.log(`${tick}】← 移回原位 (SendInput)`);
await sleep(1000);
}
}
main().catch((err) => {
console.error('出错:', err.message);
process.exit(1);
});

View File

@ -1,185 +0,0 @@
const XLSX = require('xlsx');
const fs = require('fs');
const path = require('path');
/**
* 合并团队开票申请表脚本
*
* 目录结构假设
* <根目录>/
* 7.xxx/
* *团队开票申请表*.xlsx (含4个sheet其中一个含"商旅"一个含"吉智")
* 其它两个表格...
* 7.yyy/
* ...
*
* 用法
* node merge_kaipiao.js <根目录> [输出文件路径]
*
* 例如
* node merge_kaipiao.js "D:\\某个目录" "D:\\合并结果.xlsx"
*/
// ---------- 参数 ----------
const rootDir = process.argv[2];
const outFile = process.argv[3] || path.join(process.cwd(), '团队开票申请表-合并.xlsx');
if (!rootDir) {
console.error('用法: node merge_kaipiao.js <根目录> [输出文件路径]');
process.exit(1);
}
if (!fs.existsSync(rootDir) || !fs.statSync(rootDir).isDirectory()) {
console.error(`根目录不存在或不是文件夹: ${rootDir}`);
process.exit(1);
}
// ---------- 工具函数 ----------
// 判断是否为 Excel 文件(排除临时文件 ~$ 开头)
function isExcelFile(name) {
return /\.(xlsx|xls)$/i.test(name) && !name.startsWith('~$');
}
// 在一个工作簿里按关键字查找 sheet 名
function findSheetName(workbook, keyword) {
return workbook.SheetNames.find(n => n.includes(keyword));
}
// 把一个 sheet 读成二维数组(含表头行)
function sheetToRows(sheet) {
return XLSX.utils.sheet_to_json(sheet, { header: 1, defval: null, blankrows: false });
}
// 去掉指定列(按表头名精确匹配)单元格值中的所有冒号(含全角)
function stripColonInColumn(rows, columnName) {
if (!rows || rows.length === 0) return 0;
const header = rows[0];
const colIndex = header.findIndex(h => h != null && String(h).trim() === columnName);
if (colIndex === -1) {
console.warn(` [警告] 未找到列 "${columnName}",跳过冒号清理`);
return 0;
}
let changed = 0;
for (let i = 1; i < rows.length; i++) {
const v = rows[i][colIndex];
if (v == null) continue;
const str = String(v);
if (str.includes(':') || str.includes('')) {
rows[i][colIndex] = str.replace(/[:]/g, '');
changed++;
}
}
return changed;
}
// ---------- 收集子文件夹 ----------
const subDirs = fs.readdirSync(rootDir, { withFileTypes: true })
.filter(d => d.isDirectory() && d.name.startsWith('7.'))
.map(d => d.name)
.sort();
if (subDirs.length === 0) {
console.error('未找到以 "7." 开头的子文件夹');
process.exit(1);
}
console.log(`找到 ${subDirs.length} 个 "7." 子文件夹`);
// 合并结果:分别累积 商旅 / 吉智 两组数据
let shangluHeader = null;
const shangluRows = [];
let jizhiHeader = null;
const jizhiRows = [];
let processedCount = 0;
for (const sub of subDirs) {
const subPath = path.join(rootDir, sub);
// 找到名字含「团队开票申请表」的表格
const files = fs.readdirSync(subPath).filter(isExcelFile);
const targetFile = files.find(f => f.includes('团队开票申请表'));
if (!targetFile) {
console.warn(` [跳过] ${sub}: 未找到含"团队开票申请表"的表格`);
continue;
}
const filePath = path.join(subPath, targetFile);
let workbook;
try {
workbook = XLSX.readFile(filePath);
} catch (e) {
console.warn(` [跳过] ${sub}: 读取失败 - ${e.message}`);
continue;
}
const shangluName = findSheetName(workbook, '商旅');
const jizhiName = findSheetName(workbook, '吉智');
if (!shangluName) console.warn(` [警告] ${sub}: 未找到含"商旅"的sheet`);
if (!jizhiName) console.warn(` [警告] ${sub}: 未找到含"吉智"的sheet`);
// 处理 商旅
if (shangluName) {
const rows = sheetToRows(workbook.Sheets[shangluName]);
if (rows.length > 0) {
const [header, ...body] = rows;
if (!shangluHeader) shangluHeader = header;
shangluRows.push(...body);
}
}
// 处理 吉智
if (jizhiName) {
const rows = sheetToRows(workbook.Sheets[jizhiName]);
if (rows.length > 0) {
const [header, ...body] = rows;
if (!jizhiHeader) jizhiHeader = header;
jizhiRows.push(...body);
}
}
processedCount++;
console.log(` [完成] ${sub}: ${targetFile}`);
}
if (processedCount === 0) {
console.error('没有可合并的数据');
process.exit(1);
}
// ---------- 生成输出工作簿 ----------
const outWb = XLSX.utils.book_new();
if (shangluHeader) {
const data = [shangluHeader, ...shangluRows];
const n1 = stripColonInColumn(data, '结算账户');
if (n1) console.log(` 商旅"结算账户"列清理冒号: ${n1} 个单元格`);
const ws = XLSX.utils.aoa_to_sheet(data);
XLSX.utils.book_append_sheet(outWb, ws, '商旅');
} else {
console.warn('未收集到任何"商旅"数据');
}
if (jizhiHeader) {
const data = [jizhiHeader, ...jizhiRows];
const n2 = stripColonInColumn(data, '*对账单/项目名称');
if (n2) console.log(` 吉智"*对账单/项目名称"列清理冒号: ${n2} 个单元格`);
const ws = XLSX.utils.aoa_to_sheet(data);
XLSX.utils.book_append_sheet(outWb, ws, '吉智');
} else {
console.warn('未收集到任何"吉智"数据');
}
if (outWb.SheetNames.length === 0) {
console.error('没有生成任何 sheet未写出文件');
process.exit(1);
}
XLSX.writeFile(outWb, outFile);
console.log('\n合并完成:');
console.log(` 商旅: ${shangluRows.length} 行数据`);
console.log(` 吉智: ${jizhiRows.length} 行数据`);
console.log(` 输出文件: ${outFile}`);

164
package-lock.json generated
View File

@ -1,164 +0,0 @@
{
"name": "geely-kaipiao",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "geely-kaipiao",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"playwright": "^1.61.0",
"xlsx": "^0.18.5"
}
},
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.61.0",
"resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.61.0.tgz",
"integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.61.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.61.0",
"resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.61.0.tgz",
"integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmmirror.com/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
}
}
}

View File

@ -1,19 +0,0 @@
{
"name": "geely-kaipiao",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "http://git.honor3.com/Cheney/geely-kaipiao.git"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"playwright": "^1.61.0",
"xlsx": "^0.18.5"
}
}

View File

@ -1,17 +1,9 @@
[proxy]
# 目标服务器配置
# 协议支持: http, https
# protocol = https
# target_host = zentao.sunyard.com.cn
# target_port = 9788
# 备用配置示例(取消注释即可使用)
protocol = https
target_host = superstar.geelytravel.com
target_port = 443
# 另一个备用配置
# protocol = http
# target_host = 192.168.1.100
# target_port = 8080
target_protocol = https
; target_host = zentao.sunyard.com.cn
; target_port = 9788
; target_protocol = http
b_host = localhost
b_port = 8080

View File

@ -1,132 +1,449 @@
# -*- coding: utf-8 -*-
import socket
import threading
import time
import sys
import configparser
import os
import ssl
# Configuration from config.ini
CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'config.ini')
def load_config(config_path='config.ini'):
"""加载配置文件"""
def load_config():
config = configparser.ConfigParser()
config.read(CONFIG_FILE)
# 如果配置文件不存在,创建默认配置
if not os.path.exists(config_path):
print(f"配置文件 {config_path} 不存在,使用默认配置")
return {
'protocol': 'https',
'target_host': 'zentao.sunyard.com.cn',
'target_port': 9788
}
target_host = config.get('proxy', 'target_host', fallback='zentao.sunyard.com.cn')
target_port = config.getint('proxy', 'target_port', fallback=9788)
target_protocol = config.get('proxy', 'target_protocol', fallback='http').lower()
b_host = config.get('proxy', 'b_host', fallback='localhost')
b_port = config.getint('proxy', 'b_port', fallback=8080)
config.read(config_path, encoding='utf-8')
if 'proxy' not in config:
print(f"配置文件格式错误,使用默认配置")
return {
'protocol': 'https',
'target_host': 'zentao.sunyard.com.cn',
'target_port': 9788
}
protocol = config['proxy'].get('protocol', 'https').lower()
target_host = config['proxy'].get('target_host', 'zentao.sunyard.com.cn')
target_port = config['proxy'].getint('target_port', 9788)
# 如果没有设置端口,根据协议设置默认端口
if target_port == 0:
target_port = 443 if protocol == 'https' else 80
return {
'protocol': protocol,
'target_host': target_host,
'target_port': target_port
}
return target_host, target_port, target_protocol, b_host, b_port
def handle_request(sock, target_host, target_port):
"""处理从A设备收到的请求转发到目标网站"""
TARGET_HOST, TARGET_PORT, TARGET_PROTOCOL, B_HOST, B_PORT = load_config()
# Global lock for socket write operations
socket_write_lock = threading.Lock()
def get_thread_id():
"""Get thread identifier for logging"""
return threading.current_thread().getName()
def log_info(source, message):
"""Log info message with thread ID"""
thread_id = get_thread_id()
log_line = "[%s] [INFO] [%s] [Thread:%s] %s" % (
time.strftime("%Y-%m-%d %H:%M:%S"),
source,
thread_id,
message
)
print(log_line)
sys.stdout.flush()
def log_error(source, message):
"""Log error message with thread ID"""
thread_id = get_thread_id()
log_line = "[%s] [ERROR] [%s] [Thread:%s] %s" % (
time.strftime("%Y-%m-%d %H:%M:%S"),
source,
thread_id,
message
)
print(log_line)
sys.stdout.flush()
def extract_request_id(request_data):
"""Extract X-Proxy-Request-ID from request headers"""
if "\r\n\r\n" in request_data:
headers_end = request_data.find("\r\n\r\n")
headers = request_data[:headers_end]
for line in headers.split("\r\n"):
if line.lower().startswith("x-proxy-request-id:"):
return line.split(":", 1)[1].strip()
return None
def modify_request_headers(request_data):
"""Modify request headers: replace Origin, Referer, Host; remove Connection headers"""
lines = request_data.split("\r\n")
new_lines = []
method = ""
path = ""
if lines:
first_line = lines[0]
parts = first_line.split()
if len(parts) >= 2:
method = parts[0]
path = parts[1]
for line in lines:
line_str = line
if line_str.startswith("Origin:"):
new_lines.append("Origin: %s://%s:%d" % (TARGET_PROTOCOL, TARGET_HOST, TARGET_PORT))
elif line_str.startswith("Referer:"):
new_lines.append("Referer: %s://%s:%d/" % (TARGET_PROTOCOL, TARGET_HOST, TARGET_PORT))
elif line_str.startswith("Host:"):
new_lines.append("Host: %s:%d" % (TARGET_HOST, TARGET_PORT))
elif line_str.lower().startswith("connection:"):
continue
elif line_str.lower().startswith("keep-alive:"):
continue
elif line_str.lower().startswith("proxy-connection:"):
continue
elif line_str.lower().startswith("x-forwarded-for:"):
continue
elif line_str.lower().startswith("x-proxy-request-id:"):
continue
else:
new_lines.append(line_str)
modified_data = "\r\n".join(new_lines)
if path.startswith("/"):
new_path = "%s://%s:%d%s" % (TARGET_PROTOCOL, TARGET_HOST, TARGET_PORT, path)
if method and modified_data:
modified_data = modified_data.replace(path, new_path, 1)
return modified_data, method, path
def send_request_to_target(host, port, protocol, request_data, request_id):
"""Send request to target server and return response"""
target_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
target_sock.settimeout(30)
try:
# 接收来自A设备的请求
request = b""
target_sock.connect((host, port))
log_info("System", "Connected to target: %s:%d" % (host, port))
if protocol == 'https':
context = ssl.create_default_context()
target_sock = context.wrap_socket(target_sock, server_hostname=host)
log_info("System", "SSL connection established with target")
if isinstance(request_data, str):
request_data = request_data.encode('utf-8')
target_sock.sendall(request_data)
response = b""
content_length = 0
is_chunked = False
while True:
data = sock.recv(4096)
if not data:
break
request += data
if b"\r\n\r\n" in request:
chunk = target_sock.recv(8192)
if not chunk:
break
response += chunk
if b"\r\n\r\n" in response and content_length == 0 and not is_chunked:
headers_end = response.find(b"\r\n\r\n")
headers = response[:headers_end].decode('utf-8', errors='replace')
for line in headers.split("\r\n"):
if line.lower().startswith("content-length:"):
content_length = int(line.split(":", 1)[1].strip())
elif line.lower().startswith("transfer-encoding:"):
if "chunked" in line.lower():
is_chunked = True
if content_length > 0:
body_start = headers_end + 4
if len(response) - body_start >= content_length:
break
elif is_chunked:
if response.endswith(b"0\r\n\r\n"):
break
if not request:
return
# 连接到目标网站
target_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
target_sock.connect((target_host, target_port))
# 发送请求
target_sock.send(request)
# 接收响应并转发回A设备
while True:
response = target_sock.recv(4096)
if not response:
break
sock.send(response)
if request_id:
if b"\r\n\r\n" in response:
headers_end = response.find(b"\r\n\r\n")
headers = response[:headers_end]
body = response[headers_end:]
response = headers + ("\r\nX-Proxy-Request-ID: " + request_id).encode('utf-8') + body
return response
finally:
target_sock.close()
except Exception as e:
print(f"处理请求出错: {e}")
try:
sock.send(b"HTTP/1.1 500 Internal Server Error\r\n\r\n")
except:
pass
def connect_to_a_device(a_host, a_port, target_host, target_port):
"""连接到A设备"""
def parse_location(location):
"""Parse Location header to get protocol, host, port, and path"""
protocol = TARGET_PROTOCOL
host = TARGET_HOST
port = TARGET_PORT
path = "/"
if location.startswith("http://") or location.startswith("https://"):
# Full URL
protocol_end = location.find("://")
protocol = location[:protocol_end]
remaining = location[protocol_end + 3:]
path_start = remaining.find("/")
if path_start == -1:
host_part = remaining
path = "/"
else:
host_part = remaining[:path_start]
path = remaining[path_start:]
port_pos = host_part.find(":")
if port_pos != -1:
host = host_part[:port_pos]
port = int(host_part[port_pos + 1:])
else:
host = host_part
port = 443 if protocol == 'https' else 80
elif location.startswith("/"):
# Absolute path
path = location
else:
# Relative path
path = "/" + location
return protocol, host, port, path
def modify_redirect_request(original_request, new_protocol, new_host, new_port, new_path):
"""Modify request for redirect"""
lines = original_request.split("\r\n")
new_lines = []
first_line = lines[0]
parts = first_line.split()
if len(parts) >= 3:
method = parts[0]
http_version = parts[2]
new_first_line = "%s %s %s" % (method, new_path, http_version)
new_lines.append(new_first_line)
for line in lines[1:]:
if not line:
continue
if line.lower().startswith("host:"):
new_lines.append("Host: %s:%d" % (new_host, new_port))
elif line.lower().startswith("origin:"):
new_lines.append("Origin: %s://%s:%d" % (new_protocol, new_host, new_port))
elif line.lower().startswith("referer:"):
new_lines.append("Referer: %s://%s:%d/" % (new_protocol, new_host, new_port))
elif line.lower().startswith("connection:"):
continue
elif line.lower().startswith("keep-alive:"):
continue
elif line.lower().startswith("proxy-connection:"):
continue
elif line.lower().startswith("x-forwarded-for:"):
continue
elif line.lower().startswith("x-proxy-request-id:"):
continue
else:
new_lines.append(line)
new_lines.append("")
new_lines.append("")
if "\r\n\r\n" in original_request:
body_start = original_request.find("\r\n\r\n") + 4
body = original_request[body_start:]
modified_data = "\r\n".join(new_lines[:-2]) + "\r\n\r\n" + body
else:
modified_data = "\r\n".join(new_lines[:-2]) + "\r\n\r\n"
return modified_data
def handle_request(request_data):
"""Handle a single request from device B - completely independent thread"""
if isinstance(request_data, bytes):
request_data_str = request_data.decode('utf-8', errors='replace')
else:
request_data_str = request_data
request_id = extract_request_id(request_data_str)
modified_data, method, path = modify_request_headers(request_data_str)
body_size = 0
if "\r\n\r\n" in modified_data:
body_size = len(modified_data) - modified_data.find("\r\n\r\n") - 4
log_info("System", "Request: %s %s://%s:%d%s - Body: %d bytes - ID: %s" % (method, TARGET_PROTOCOL, TARGET_HOST, TARGET_PORT, path, body_size, request_id))
current_protocol = TARGET_PROTOCOL
current_host = TARGET_HOST
current_port = TARGET_PORT
current_request = modified_data
redirect_count = 0
max_redirects = 5
try:
while redirect_count <= max_redirects:
response = send_request_to_target(current_host, current_port, current_protocol, current_request, request_id)
status_code = "500"
location = None
if b"\r\n\r\n" in response:
headers_end = response.find(b"\r\n\r\n")
headers = response[:headers_end].decode('utf-8', errors='replace')
for line in headers.split("\r\n"):
if line.startswith("HTTP/"):
status_code = line.split()[1]
elif line.lower().startswith("location:"):
location = line.split(":", 1)[1].strip()
log_info("System", "Response: %s %s - Status: %s - ID: %s" % (method, path, status_code, request_id))
if status_code not in ("301", "302") or not location:
return response
redirect_count += 1
log_info("System", "Redirect %d: %s" % (redirect_count, location))
new_protocol, new_host, new_port, new_path = parse_location(location)
current_protocol = new_protocol
current_host = new_host
current_port = new_port
current_request = modify_redirect_request(request_data_str, new_protocol, new_host, new_port, new_path)
path = new_path
log_error("System", "Too many redirects (%d)" % max_redirects)
error_response = ("HTTP/1.1 508 Loop Detected\r\nX-Proxy-Request-ID: " + (request_id if request_id else "") + "\r\nContent-Type: text/plain\r\n\r\nToo many redirects\r\n").encode('utf-8')
return error_response
except socket.error as e:
if e.errno == 111 or e.errno == 10061:
log_error("System", "Cannot connect to target: %s:%d" % (current_host, current_port))
error_response = ("HTTP/1.1 502 Bad Gateway\r\nX-Proxy-Request-ID: " + (request_id if request_id else "") + "\r\nContent-Type: text/plain\r\n\r\nCannot connect to target: %s:%d\r\n" % (current_host, current_port)).encode('utf-8')
return error_response
else:
log_error("System", "Socket error: %s" % str(e))
error_response = ("HTTP/1.1 502 Bad Gateway\r\nX-Proxy-Request-ID: " + (request_id if request_id else "") + "\r\nContent-Type: text/plain\r\n\r\nSocket error\r\n").encode('utf-8')
return error_response
except socket.timeout:
log_error("System", "Target connection timeout")
error_response = ("HTTP/1.1 504 Gateway Timeout\r\nX-Proxy-Request-ID: " + (request_id if request_id else "") + "\r\nContent-Type: text/plain\r\n\r\nConnection timeout\r\n").encode('utf-8')
return error_response
except Exception as e:
log_error("System", "Error handling request: %s" % str(e))
error_response = ("HTTP/1.1 500 Internal Server Error\r\nX-Proxy-Request-ID: " + (request_id if request_id else "") + "\r\nContent-Type: text/plain\r\n\r\nInternal error\r\n").encode('utf-8')
return error_response
def process_request(b_socket, request_data):
"""Process a request in separate thread and send response back"""
response = handle_request(request_data)
with socket_write_lock:
try:
log_info("System", "Sending response back to B, length: %d bytes" % len(response))
b_socket.sendall(response)
log_info("System", "Response sent successfully")
except Exception as e:
log_error("System", "Failed to send response: %s" % str(e))
def listen_for_requests(sock):
"""Listen for requests from device B"""
sock.settimeout(300)
while True:
try:
data = b""
while True:
chunk = sock.recv(8192)
if not chunk:
log_info("System", "Connection closed by device B")
return
data += chunk
if b"\r\n\r\n" in data:
headers_end = data.find(b"\r\n\r\n")
headers = data[:headers_end].decode('utf-8', errors='replace')
content_length = 0
for line in headers.split("\r\n"):
if line.lower().startswith("content-length:"):
content_length = int(line.split(":", 1)[1].strip())
break
body_start = headers_end + 4
if len(data) - body_start >= content_length:
break
t = threading.Thread(target=process_request, args=(sock, data), name="ReqHandler-%d" % threading.activeCount())
t.daemon = True
t.start()
except socket.timeout:
log_info("System", "Connection idle timeout, continuing to wait")
continue
except Exception as e:
log_error("System", "Connection error: %s" % str(e))
return
def connect_to_b_device(b_host, b_port):
"""Connect to device B and handle requests"""
failure_count = 0
max_failures = 3
while True:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((a_host, a_port))
print(f"成功连接到A设备: {a_host}:{a_port}")
print(f"准备转发请求到: {target_host}:{target_port}")
sock.settimeout(10)
log_info("System", "Connecting to device B: %s:%d" % (b_host, b_port))
sock.connect((b_host, b_port))
handle_request(sock, target_host, target_port)
log_info("System", "Connected to device B, waiting for PROXY_CONNECTED")
response = sock.recv(1024)
log_info("System", "Received from B: %r" % response)
sock.close()
print("与A设备的连接已断开")
except ConnectionRefusedError:
print(f"A设备未就绪重试中...")
import time
if not response or b"PROXY_CONNECTED" not in response:
log_error("System", "Failed to receive connection confirmation")
sock.close()
failure_count += 1
if failure_count >= max_failures:
log_error("System", "Maximum connection attempts reached (%d), exiting..." % max_failures)
sys.exit(1)
time.sleep(3)
continue
log_info("System", "Sending READY to device B")
sock.sendall(b"READY\r\n")
log_info("System", "Successfully connected to device B: %s:%d" % (b_host, b_port))
failure_count = 0
listen_for_requests(sock)
except socket.error as e:
if e.errno == 111 or e.errno == 10061:
log_info("System", "Device B not ready (%s:%d), retrying in 3 seconds..." % (b_host, b_port))
else:
log_error("System", "Connection error: %s" % str(e))
failure_count += 1
if failure_count >= max_failures:
log_error("System", "Maximum connection attempts reached (%d), exiting..." % max_failures)
sys.exit(1)
time.sleep(3)
except Exception as e:
print(f"连接出错: {e}")
import time
log_error("System", "Unexpected error: %s" % str(e))
failure_count += 1
if failure_count >= max_failures:
log_error("System", "Maximum connection attempts reached (%d), exiting..." % max_failures)
sys.exit(1)
time.sleep(3)
def main():
print("=" * 60)
print(" Proxy Client (Run on Computer A)")
print("=" * 60)
print("Target website:", TARGET_PROTOCOL, "://", TARGET_HOST, ":", TARGET_PORT)
print("Device B address:", B_HOST, ":", B_PORT)
print("=" * 60)
sys.stdout.flush()
try:
connect_to_b_device(B_HOST, B_PORT)
except KeyboardInterrupt:
print("\nShutting down...")
sys.exit(0)
# 加载配置
config = load_config()
# 解析命令行参数
if len(sys.argv) < 3:
print("用法: python proxy_client.py <A设备IP> <A设备端口>")
print(f"当前配置: {config['protocol']}://{config['target_host']}:{config['target_port']}")
print("示例: python proxy_client.py 192.168.1.50 8080")
sys.exit(1)
a_host = sys.argv[1]
a_port = int(sys.argv[2])
# 可选:通过命令行覆盖目标配置
if len(sys.argv) >= 4:
config['target_host'] = sys.argv[3]
if len(sys.argv) >= 5:
config['target_port'] = int(sys.argv[4])
print(f"使用配置: {config['protocol']}://{config['target_host']}:{config['target_port']}")
connect_to_a_device(a_host, a_port, config['target_host'], config['target_port'])
if __name__ == '__main__':
if __name__ == "__main__":
main()

1
proxynt/.gitignore vendored
View File

@ -1 +0,0 @@
log/

View File

@ -1,11 +0,0 @@
pip install -U proxynt
nt_server -c config_s.json
nt_client -c config_c.json
http://<A电脑的IP>:18888/websocket_path/admin

View File

@ -1,7 +0,0 @@
{
"server": {
"url": "ws://localhost:18888/websocket_path",
"password": "helloworld"
},
"client_name": "fyx"
}

View File

@ -1,7 +0,0 @@
{
"server": {
"url": "ws://bh.vps.honor3.com:18888/websocket_path",
"password": "helloworld"
},
"client_name": "cheney"
}

View File

@ -1,21 +0,0 @@
{
"port": 18888,
"password": "helloworld",
"path": "/websocket_path",
"admin": {
"enable": true,
"admin_password": "helloworld"
},
"client_config": {
"fyx": [
{
"name": "superstar",
"remote_port": 4443,
"local_port": 443,
"local_ip": "198.19.0.115",
"speed_limit": 0.0,
"protocol": "tcp"
}
]
}
}

View File

@ -1,7 +0,0 @@
{
"server": {
"url": "ws://121.43.151.143:18888/websocket_path",
"password": "helloworld"
},
"client_name": "fyx"
}

View File

@ -1,39 +0,0 @@
const XLSX = require('xlsx');
const path = require('path');
// 读取 Excel 文件(项目根目录)
const filePath = path.join(__dirname, '新建 XLSX 工作表.xlsx');
const workbook = XLSX.readFile(filePath);
// 目标 sheet
const sheetName = '2026';
const sheet = workbook.Sheets[sheetName];
if (!sheet) {
console.error(`Sheet "${sheetName}" 不存在`);
process.exit(1);
}
// 将 sheet 转为 JSON 数组(以第一行为表头)
const rows = XLSX.utils.sheet_to_json(sheet, { defval: null });
// 查找"团号"列(兼容表头可能的空格差异)
const headerRow = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: null })[0];
const tuanHaoIndex = headerRow.findIndex(h => h && String(h).trim() === '团号');
if (tuanHaoIndex === -1) {
console.error('未找到"团号"列');
process.exit(1);
}
// 提取所有 TEAM-xxxx 格式的 ID
const teamIds = [];
for (const row of rows) {
const val = row['团号'];
if (val && /^TEAM-\d+$/.test(String(val).trim())) {
teamIds.push(String(val).trim());
}
}
console.log(`找到 ${teamIds.length} 个 TEAM ID:`);
teamIds.forEach(id => console.log(id));

View File

@ -1,41 +0,0 @@
param([int]$dx = 1)
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class MouseInput {
[StructLayout(LayoutKind.Sequential)]
public struct MOUSEINPUT {
public int dx;
public int dy;
public uint mouseData;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
public struct INPUT {
public uint type;
public MOUSEINPUT mi;
}
[DllImport("user32.dll")]
public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
public const uint INPUT_MOUSE = 0;
public const uint MOUSEEVENTF_MOVE = 0x0001;
}
"@
$input = New-Object MouseInput+INPUT
$input.type = [MouseInput]::INPUT_MOUSE
$input.mi.dx = $dx
$input.mi.dy = 0
$input.mi.dwFlags = [MouseInput]::MOUSEEVENTF_MOVE
$input.mi.time = 0
$input.mi.dwExtraInfo = [IntPtr]::Zero
$size = [Runtime.InteropServices.Marshal]::SizeOf($input)
[MouseInput]::SendInput(1, @($input), $size) | Out-Null

View File

@ -1,65 +0,0 @@
const { chromium } = require('playwright');
const path = require('path');
const LOGIN_URL = 'https://superstar.geelytravel.com/new-tmc/login';
const STATE_PATH = process.env.STATE_PATH || path.resolve(__dirname, 'state.json');
const USER_AGENT =
process.env.USER_AGENT ||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
async function run() {
// 1. 启动本地 Chrome 浏览器,并添加排除自动化检测的参数
const browser = await chromium.launch({
headless: false,
channel: 'chrome',
args: [
'--disable-blink-features=AutomationControlled',
'--start-maximized',
],
});
// 2. 设置上下文,伪造 User-Agent
const context = await browser.newContext({
noViewport: true,
userAgent: USER_AGENT,
});
const page = await context.newPage();
try {
// 3. 访问登录页
await page.goto(LOGIN_URL);
// 输入信息:如需自动填写,可通过环境变量 USERNAME / PASSWORD 传入
if (process.env.USERNAME) {
await page.getByPlaceholder('用户名').fill(process.env.USERNAME);
}
if (process.env.PASSWORD) {
await page.getByPlaceholder('密码').fill(process.env.PASSWORD);
}
// 4. 进入暂停状态,此时手动滑动验证码并完成登录
console.log('请在浏览器中手动完成滑块验证,登录进入系统首页后回到终端按 Enter...');
await page.pause();
// 5. 关键:等待页面跳转完成,确保已进入登录后的状态
// 如需按业务页面确认登录成功,可打开下面一行并按实际 URL 片段调整。
// await page.waitForURL('**/report/**', { timeout: 60000 });
// 额外等待 3 秒,确保 Cookie 和 LocalStorage 完整写入内存
await page.waitForTimeout(3000);
// 6. 保存状态
await context.storageState({ path: STATE_PATH });
console.log(`登录状态已保存至:${STATE_PATH}`);
} finally {
await context.close();
await browser.close();
}
}
run().catch((error) => {
console.error(error);
process.exitCode = 1;
});

File diff suppressed because one or more lines are too long

View File

@ -1,52 +0,0 @@
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class MouseInput {
[StructLayout(LayoutKind.Sequential)]
public struct MOUSEINPUT {
public int dx;
public int dy;
public uint mouseData;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
public struct INPUT {
public uint type;
public MOUSEINPUT mi;
}
[DllImport("user32.dll")]
public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
public const uint INPUT_MOUSE = 0;
public const uint MOUSEEVENTF_MOVE = 0x0001;
}
"@
Add-Type -AssemblyName System.Windows.Forms
$pos = [System.Windows.Forms.Cursor]::Position
Write-Host "当前位置: X=$($pos.X), Y=$($pos.Y)"
$input = New-Object MouseInput+INPUT
$input.type = [MouseInput]::INPUT_MOUSE
$input.mi.dx = 1
$input.mi.dy = 0
$input.mi.dwFlags = [MouseInput]::MOUSEEVENTF_MOVE
$input.mi.time = 0
$input.mi.dwExtraInfo = [IntPtr]::Zero
$size = [Runtime.InteropServices.Marshal]::SizeOf($input)
$result = [MouseInput]::SendInput(1, @($input), $size)
Write-Host "SendInput 返回: $result (右移1像素)"
Start-Sleep -Seconds 2
$input.mi.dx = -1
$result = [MouseInput]::SendInput(1, @($input), $size)
Write-Host "SendInput 返回: $result (移回原位)"
Write-Host "测试完成"

View File

@ -1,40 +0,0 @@
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class MI {
[StructLayout(LayoutKind.Sequential)]
public struct MOUSEINPUT {
public int dx;
public int dy;
public uint mouseData;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
public struct INPUT {
public uint type;
public MOUSEINPUT mi;
}
}
"@
$type = [MI+INPUT]
Write-Host "Type: $($type.FullName)"
Write-Host "IsValueType: $($type.IsValueType)"
try {
$s = [Runtime.InteropServices.Marshal]::SizeOf($type)
Write-Host "SizeOf(type): $s"
} catch {
Write-Host "SizeOf(type) failed: $($_.Exception.Message)"
}
$inst = [Activator]::CreateInstance($type)
try {
$s2 = [Runtime.InteropServices.Marshal]::SizeOf($inst)
Write-Host "SizeOf(inst): $s2"
} catch {
Write-Host "SizeOf(inst) failed: $($_.Exception.Message)"
}

Binary file not shown.

View File

@ -1,72 +0,0 @@
const { exec } = require('child_process');
const CHECK_INTERVAL = 30 * 1000; // 每 30 秒检查一次
const IDLE_LIMIT = 60 * 1000; // 空闲超过 60 秒则触发
// 封装执行 PowerShell 的方法(使用 UTF-16LE 编码避免引号转义问题)
function runPs(script) {
return new Promise((resolve, reject) => {
const encoded = Buffer.from(script, 'utf16le').toString('base64');
exec(`powershell -NoProfile -EncodedCommand "${encoded}"`, (err, stdout) => {
if (err) reject(err);
else resolve(stdout.trim());
});
});
}
// 获取当前系统空闲毫秒数
async function getIdleTime() {
const ps = `
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class IdleChecker {
[DllImport("user32.dll")]
public static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
public struct LASTINPUTINFO {
public uint cbSize;
public uint dwTime;
}
public static uint GetIdleMilliseconds() {
LASTINPUTINFO lii = new LASTINPUTINFO();
lii.cbSize = (uint)Marshal.SizeOf(typeof(LASTINPUTINFO));
GetLastInputInfo(ref lii);
return (uint)Environment.TickCount - lii.dwTime;
}
}
"@
[IdleChecker]::GetIdleMilliseconds()
`;
const result = await runPs(ps);
return parseInt(result, 10);
}
// 模拟按下 F15 键(无任何副作用)
async function pressF15() {
const ps = `
$wshell = New-Object -ComObject wscript.shell;
$wshell.SendKeys('{F15}');
`;
await runPs(ps);
}
console.log('✅ 防锁屏脚本已启动 (PowerShell 模式)');
console.log(`⏱️ 检测到空闲 ${IDLE_LIMIT/1000} 秒后,自动模拟按键防止锁屏。`);
setInterval(async () => {
try {
const idleMs = await getIdleTime();
if (idleMs > IDLE_LIMIT) {
console.log(`🔄 已空闲 ${Math.round(idleMs/1000)} 秒,模拟 F15 重置计时器...`);
await pressF15();
}
} catch (e) {
console.error('⚠️ 执行出错:', e.message);
}
}, CHECK_INTERVAL);
// 按 Ctrl+C 退出时提示
process.on('SIGINT', () => {
console.log('\n🛑 脚本已停止');
process.exit(0);
});