Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 19680aaf90 | |||
| e51c262673 | |||
| 477914858b | |||
| 11451aac40 | |||
| c1e378a631 | |||
| 5424d61de9 | |||
| 0ad25ce261 | |||
| 359fd38bd6 | |||
| 01b4a000bd | |||
| 99c33220b8 | |||
| 7081eae1a3 | |||
| 64f62be356 | |||
| 73ace78017 | |||
| 3d8fd41127 | |||
|
|
887b68d259 | ||
| f87061266a | |||
| cdfdd9b7e4 |
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules/*
|
||||
~$*
|
||||
.reasonix/*
|
||||
11
Readme.md
Normal file
11
Readme.md
Normal file
@ -0,0 +1,11 @@
|
||||
## 开票业务
|
||||
excle 结算状态过滤
|
||||
登录
|
||||
总进度
|
||||
|
||||
|
||||
|
||||
## 避免锁屏
|
||||
打开目录:D:\Cheney\2\geely-kaipiao\
|
||||
地址栏删空,输入 cmd,打开黑框。
|
||||
输入命令:bun.exe keep-awake.js
|
||||
283
compare.js
Normal file
283
compare.js
Normal file
@ -0,0 +1,283 @@
|
||||
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();
|
||||
165
compare_kaipiao.js
Normal file
165
compare_kaipiao.js
Normal file
@ -0,0 +1,165 @@
|
||||
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}`);
|
||||
200
duizhang.js
Normal file
200
duizhang.js
Normal file
@ -0,0 +1,200 @@
|
||||
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); });
|
||||
255
jizhi_fill.js
Normal file
255
jizhi_fill.js
Normal file
@ -0,0 +1,255 @@
|
||||
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();
|
||||
379
kaipiao.js
Normal file
379
kaipiao.js
Normal file
@ -0,0 +1,379 @@
|
||||
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);
|
||||
});
|
||||
36
keep-awake.js
Normal file
36
keep-awake.js
Normal file
@ -0,0 +1,36 @@
|
||||
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);
|
||||
});
|
||||
185
merge_kaipiao.js
Normal file
185
merge_kaipiao.js
Normal file
@ -0,0 +1,185 @@
|
||||
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
Normal file
164
package-lock.json
generated
Normal file
@ -0,0 +1,164 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
package.json
Normal file
19
package.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
17
proxy/config.ini
Normal file
17
proxy/config.ini
Normal file
@ -0,0 +1,17 @@
|
||||
[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
|
||||
@ -1,297 +1,132 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
import configparser
|
||||
import os
|
||||
|
||||
# Configuration
|
||||
TARGET_HOST = "zentao.sunyard.com.cn"
|
||||
TARGET_PORT = 9788
|
||||
B_HOST = "localhost"
|
||||
B_PORT = 8080
|
||||
|
||||
# Global lock for socket write operations
|
||||
socket_write_lock = threading.Lock()
|
||||
def load_config(config_path='config.ini'):
|
||||
"""加载配置文件"""
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
# 如果配置文件不存在,创建默认配置
|
||||
if not os.path.exists(config_path):
|
||||
print(f"配置文件 {config_path} 不存在,使用默认配置")
|
||||
return {
|
||||
'protocol': 'https',
|
||||
'target_host': 'zentao.sunyard.com.cn',
|
||||
'target_port': 9788
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
def get_thread_id():
|
||||
"""Get thread identifier for logging"""
|
||||
return threading.current_thread().getName()
|
||||
def handle_request(sock, target_host, target_port):
|
||||
"""处理从A设备收到的请求,转发到目标网站"""
|
||||
|
||||
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: http://%s:%d" % (TARGET_HOST, TARGET_PORT))
|
||||
elif line_str.startswith("Referer:"):
|
||||
new_lines.append("Referer: http://%s:%d/" % (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 = "http://%s:%d%s" % (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 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 - Body: %d bytes - ID: %s" % (method, "http://" + TARGET_HOST + ":" + str(TARGET_PORT), path, body_size, request_id))
|
||||
|
||||
target_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
target_sock.settimeout(30)
|
||||
|
||||
try:
|
||||
target_sock.connect((TARGET_HOST, TARGET_PORT))
|
||||
log_info("System", "Connected to target: %s:%d" % (TARGET_HOST, TARGET_PORT))
|
||||
|
||||
if isinstance(modified_data, str):
|
||||
modified_data = modified_data.encode('utf-8')
|
||||
target_sock.sendall(modified_data)
|
||||
|
||||
response = b""
|
||||
content_length = 0
|
||||
is_chunked = False
|
||||
|
||||
# 接收来自A设备的请求
|
||||
request = b""
|
||||
while True:
|
||||
chunk = target_sock.recv(8192)
|
||||
if not chunk:
|
||||
data = sock.recv(4096)
|
||||
if not data:
|
||||
break
|
||||
request += data
|
||||
if b"\r\n\r\n" in request:
|
||||
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 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
|
||||
|
||||
status_code = "500"
|
||||
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]
|
||||
break
|
||||
|
||||
log_info("System", "Response: %s %s - Status: %s - ID: %s" % (method, path, status_code, request_id))
|
||||
|
||||
return response
|
||||
|
||||
except socket.error as e:
|
||||
if e.errno == 111 or e.errno == 10061:
|
||||
log_error("System", "Cannot connect to target: %s:%d" % (TARGET_HOST, TARGET_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" % (TARGET_HOST, TARGET_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
|
||||
finally:
|
||||
target_sock.close()
|
||||
|
||||
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))
|
||||
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)
|
||||
|
||||
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_b_device(b_host, b_port):
|
||||
"""Connect to device B and handle requests"""
|
||||
def connect_to_a_device(a_host, a_port, target_host, target_port):
|
||||
"""连接到A设备"""
|
||||
while True:
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(10)
|
||||
log_info("System", "Connecting to device B: %s:%d" % (b_host, b_port))
|
||||
sock.connect((b_host, b_port))
|
||||
sock.connect((a_host, a_port))
|
||||
print(f"成功连接到A设备: {a_host}:{a_port}")
|
||||
print(f"准备转发请求到: {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)
|
||||
handle_request(sock, target_host, target_port)
|
||||
|
||||
if not response or b"PROXY_CONNECTED" not in response:
|
||||
log_error("System", "Failed to receive connection confirmation")
|
||||
sock.close()
|
||||
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))
|
||||
|
||||
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))
|
||||
sock.close()
|
||||
print("与A设备的连接已断开")
|
||||
except ConnectionRefusedError:
|
||||
print(f"A设备未就绪,重试中...")
|
||||
import time
|
||||
time.sleep(3)
|
||||
except Exception as e:
|
||||
log_error("System", "Unexpected error: %s" % str(e))
|
||||
print(f"连接出错: {e}")
|
||||
import time
|
||||
time.sleep(3)
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print(" Proxy Client (Run on Computer A)")
|
||||
print("=" * 60)
|
||||
print("Target website:", 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)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 加载配置
|
||||
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__':
|
||||
main()
|
||||
|
||||
1
proxynt/.gitignore
vendored
Normal file
1
proxynt/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
log/
|
||||
11
proxynt/Readme.md
Normal file
11
proxynt/Readme.md
Normal file
@ -0,0 +1,11 @@
|
||||
pip install -U proxynt
|
||||
|
||||
|
||||
|
||||
nt_server -c config_s.json
|
||||
nt_client -c config_c.json
|
||||
|
||||
|
||||
http://<A电脑的IP>:18888/websocket_path/admin
|
||||
|
||||
|
||||
7
proxynt/config_c.json
Normal file
7
proxynt/config_c.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"server": {
|
||||
"url": "ws://localhost:18888/websocket_path",
|
||||
"password": "helloworld"
|
||||
},
|
||||
"client_name": "fyx"
|
||||
}
|
||||
7
proxynt/config_c2.json
Normal file
7
proxynt/config_c2.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"server": {
|
||||
"url": "ws://bh.vps.honor3.com:18888/websocket_path",
|
||||
"password": "helloworld"
|
||||
},
|
||||
"client_name": "cheney"
|
||||
}
|
||||
21
proxynt/config_s.json
Normal file
21
proxynt/config_s.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
7
proxynt/qinglong.json
Normal file
7
proxynt/qinglong.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"server": {
|
||||
"url": "ws://121.43.151.143:18888/websocket_path",
|
||||
"password": "helloworld"
|
||||
},
|
||||
"client_name": "fyx"
|
||||
}
|
||||
39
read_team_ids.js
Normal file
39
read_team_ids.js
Normal file
@ -0,0 +1,39 @@
|
||||
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));
|
||||
41
send-move.ps1
Normal file
41
send-move.ps1
Normal file
@ -0,0 +1,41 @@
|
||||
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
|
||||
65
src/login.js
Normal file
65
src/login.js
Normal file
@ -0,0 +1,65 @@
|
||||
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;
|
||||
});
|
||||
270
src/state.json
Normal file
270
src/state.json
Normal file
File diff suppressed because one or more lines are too long
52
test-move.ps1
Normal file
52
test-move.ps1
Normal file
@ -0,0 +1,52 @@
|
||||
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 "测试完成"
|
||||
40
test-size.ps1
Normal file
40
test-size.ps1
Normal file
@ -0,0 +1,40 @@
|
||||
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)"
|
||||
}
|
||||
BIN
分销-吉利商旅服务费-2026.6月.xlsx
Normal file
BIN
分销-吉利商旅服务费-2026.6月.xlsx
Normal file
Binary file not shown.
BIN
新建 XLSX 工作表.xlsx
Normal file
BIN
新建 XLSX 工作表.xlsx
Normal file
Binary file not shown.
BIN
杭州滨江希尔顿欢朋酒店-吉利商旅6月账单.xlsx
Normal file
BIN
杭州滨江希尔顿欢朋酒店-吉利商旅6月账单.xlsx
Normal file
Binary file not shown.
BIN
杭州碧丽酒店管理有限公司 2026年06月 对账单(0601-0630).xlsx
Normal file
BIN
杭州碧丽酒店管理有限公司 2026年06月 对账单(0601-0630).xlsx
Normal file
Binary file not shown.
72
脚本/unlock.js
Normal file
72
脚本/unlock.js
Normal file
@ -0,0 +1,72 @@
|
||||
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);
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user