新增数据库相关语句脚本

This commit is contained in:
2026-04-14 12:02:21 +08:00
parent a7c2448207
commit 32f0d87499
9 changed files with 1680 additions and 0 deletions

View File

@@ -0,0 +1,260 @@
#!/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);
});