Files
youlegames/codes/games/sql/export/export_summary.js

261 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* export_summary.js
* 通过 SSH 隧道连接 RDS导出转卡汇总报表为 Excel
*
* 用法:
* node export_summary.js # 交互式输入年月
* node export_summary.js -y 2026 -m 3 # 直接指定年月
*
* 依赖: npm install mysql2 exceljs tunnel-ssh
*/
"use strict";
const path = require("path");
const fs = require("fs");
const readline = require("readline");
const { createTunnel } = require("tunnel-ssh");
const mysql = require("mysql2/promise");
const ExcelJS = require("exceljs");
// ─────────────────────────────────────────────
// SSH 配置(跳板机)
// ─────────────────────────────────────────────
const SSH_CONFIG = {
host: "47.98.203.17",
port: 22,
username: "root",
privateKey: fs.readFileSync(
path.join(process.env.USERPROFILE || process.env.HOME, ".ssh", "id_ed25519")
),
};
// ─────────────────────────────────────────────
// 数据库配置RDS通过 SSH 隧道访问)
// ─────────────────────────────────────────────
const DB_HOST = "rm-bp1btyuwq77591x0jpo.mysql.rds.aliyuncs.com";
const DB_PORT = 3306;
const DB_CONFIG = {
host: "127.0.0.1",
port: 13306, // 本地隧道转发端口
user: "games",
password: "Games0791!!",
database: "agent_db",
charset: "utf8mb4",
connectTimeout: 15000,
};
// ─────────────────────────────────────────────
// 默认业务参数
// ─────────────────────────────────────────────
const DEFAULT_FROM_SALES = 10437216;
const DEFAULT_AGENT_ID = "veRa0qrBf0df2K1G4de2tgfmVxB2jxpv";
// ─────────────────────────────────────────────
// Excel 样式
// ─────────────────────────────────────────────
const COLOR_HEADER = "2E6DA4";
const COLOR_TOTAL_ROW = "FFF2CC";
const COLOR_ALT_ROW = "EBF3FB";
function headerStyle() {
return {
font: { bold: true, color: { argb: "FFFFFFFF" }, size: 11 },
fill: { type: "pattern", pattern: "solid", fgColor: { argb: "FF" + COLOR_HEADER } },
alignment: { horizontal: "center", vertical: "middle", wrapText: true },
border: thinBorder(),
};
}
function thinBorder() {
const s = { style: "thin", color: { argb: "FFBFBFBF" } };
return { left: s, right: s, top: s, bottom: s };
}
function applyStyle(cell, style) {
Object.assign(cell, style);
}
// ─────────────────────────────────────────────
// 汇总 SQL不含 ROLLUP合计行由 JS 端计算)
// ─────────────────────────────────────────────
const SQL_SUMMARY = `
SELECT
t.satr_salesid AS agentId,
MAX(su.saus_nickname) AS agentName,
COUNT(t.idx) AS transferCount,
SUM(t.satr_amount) AS transferTotal
FROM
sales_transferbill t
LEFT JOIN sales_user su
ON t.satr_agentid = su.saus_agentid
AND t.channel_id = su.saus_channelid
AND t.satr_salesid = su.saus_salesid
WHERE
t.satr_agentid = ?
AND t.from_sales = ?
AND YEAR(t.satr_transfertime) = ?
AND MONTH(t.satr_transfertime) = ?
GROUP BY t.satr_salesid
ORDER BY SUM(t.satr_amount) DESC
`;
// ─────────────────────────────────────────────
// 建立 SSH 隧道tunnel-ssh v5
// ─────────────────────────────────────────────
async function openTunnel() {
const tunnelOptions = { autoClose: true };
const serverOptions = { port: DB_CONFIG.port };
const sshOptions = SSH_CONFIG;
const forwardOptions = {
srcAddr: "127.0.0.1",
srcPort: DB_CONFIG.port,
dstAddr: DB_HOST,
dstPort: DB_PORT,
};
const [server] = await createTunnel(tunnelOptions, serverOptions, sshOptions, forwardOptions);
return server;
}
// ─────────────────────────────────────────────
// 写 Excel
// ─────────────────────────────────────────────
async function writeExcel(rows, year, month, fromSales, outputPath) {
const wb = new ExcelJS.Workbook();
const ws = wb.addWorksheet("转卡汇总");
ws.properties.tabColor = { argb: "FF" + COLOR_HEADER };
ws.views = [{ state: "frozen", ySplit: 1 }];
// 列定义
const columns = [
{ header: "代理ID", key: "agentId", width: 14 },
{ header: "代理昵称", key: "agentName", width: 20 },
{ header: "转卡次数", key: "transferCount", width: 12 },
{ header: "转卡总量", key: "transferTotal", width: 12 },
];
ws.columns = columns;
// 表头样式
const hs = headerStyle();
ws.getRow(1).height = 22;
ws.getRow(1).eachCell(cell => applyStyle(cell, hs));
// 数据行
let totalCount = 0, totalAmount = 0;
rows.forEach((row, idx) => {
const r = ws.addRow({
agentId: row.agentId,
agentName: row.agentName || "—",
transferCount: Number(row.transferCount),
transferTotal: Number(row.transferTotal),
});
r.height = 20;
const isAlt = (idx % 2 === 1);
r.eachCell(cell => {
cell.border = thinBorder();
cell.alignment = { horizontal: "center", vertical: "middle" };
if (isAlt) cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FF" + COLOR_ALT_ROW } };
});
totalCount += Number(row.transferCount);
totalAmount += Number(row.transferTotal);
});
// 合计行
const totalRow = ws.addRow({
agentId: "★ 合计",
agentName: "—",
transferCount: totalCount,
transferTotal: totalAmount,
});
totalRow.height = 22;
totalRow.eachCell(cell => {
cell.border = thinBorder();
cell.font = { bold: true, size: 11 };
cell.alignment = { horizontal: "center", vertical: "middle" };
cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FF" + COLOR_TOTAL_ROW } };
});
await wb.xlsx.writeFile(outputPath);
}
// ─────────────────────────────────────────────
// 交互式询问
// ─────────────────────────────────────────────
async function prompt(question) {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans.trim()); }));
}
async function getParams() {
const args = process.argv.slice(2);
const get = (flag) => { const i = args.indexOf(flag); return i !== -1 ? args[i + 1] : null; };
const now = new Date();
let year = get("-y") ? parseInt(get("-y")) : null;
let month = get("-m") ? parseInt(get("-m")) : null;
let fromSales = get("-f") ? parseInt(get("-f")) : DEFAULT_FROM_SALES;
let agentId = get("-a") ? get("-a") : DEFAULT_AGENT_ID;
if (!year) {
const raw = await prompt(`请输入年份(直接回车使用 ${now.getFullYear()}: `);
year = raw ? parseInt(raw) : now.getFullYear();
}
if (!month) {
const raw = await prompt(`请输入月份 1-12直接回车使用 ${now.getMonth() + 1}: `);
month = raw ? parseInt(raw) : now.getMonth() + 1;
}
if (month < 1 || month > 12) { console.error("[错误] 月份必须在 1-12 之间"); process.exit(1); }
return { year, month, fromSales, agentId };
}
// ─────────────────────────────────────────────
// 主流程
// ─────────────────────────────────────────────
async function main() {
const { year, month, fromSales, agentId } = await getParams();
const outputDir = path.join(__dirname, "output");
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
const outputFile = path.join(outputDir, `转卡汇总_${year}${String(month).padStart(2, "0")}_${fromSales}.xlsx`);
console.log("=".repeat(55));
console.log(` 导出参数: ${year}${String(month).padStart(2, "0")}`);
console.log(` 发卡代理: ${fromSales}`);
console.log(` 代理商ID: ${agentId}`);
console.log(` 输出文件: ${outputFile}`);
console.log("=".repeat(55));
console.log("\n[SSH] 正在建立隧道 47.98.203.17 → RDS ...");
const tunnel = await openTunnel();
console.log("[SSH] 隧道建立成功");
let conn;
try {
console.log("[数据库] 正在连接 ...");
conn = await mysql.createConnection(DB_CONFIG);
console.log("[数据库] 连接成功");
console.log(`[查询] 执行汇总查询 (${year}${String(month).padStart(2, "0")}月) ...`);
const [rows] = await conn.execute(SQL_SUMMARY, [agentId, fromSales, year, month]);
console.log(`${rows.length} 条代理记录`);
if (rows.length === 0) {
console.log("[提示] 查询结果为空,未生成文件");
} else {
await writeExcel(rows, year, month, fromSales, outputFile);
console.log(`\n[完成] 文件已保存: ${outputFile}`);
}
} finally {
if (conn) await conn.end();
tunnel.close();
console.log("[SSH] 隧道已关闭");
}
}
main().catch(err => {
console.error("\n[错误]", err.message);
process.exit(1);
});