diff --git a/.gitignore b/.gitignore index c7a13d0..30831d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/* ~$* +.reasonix/* \ No newline at end of file diff --git a/jizhi_fill.js b/jizhi_fill.js new file mode 100644 index 0000000..4056746 --- /dev/null +++ b/jizhi_fill.js @@ -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(); diff --git a/proxynt/config_c copy.json b/proxynt/config_c2.json similarity index 54% rename from proxynt/config_c copy.json rename to proxynt/config_c2.json index 3f8601c..3eb1a43 100644 --- a/proxynt/config_c copy.json +++ b/proxynt/config_c2.json @@ -1,7 +1,7 @@ -{ - "server": { - "url": "ws://localhost:18888/websocket_path", - "password": "helloworld" - }, - "client_name": "cheney" +{ + "server": { + "url": "ws://bh.vps.honor3.com:18888/websocket_path", + "password": "helloworld" + }, + "client_name": "cheney" } \ No newline at end of file