orders.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. const express = require('express');
  2. const router = express.Router();
  3. const { getPool } = require('../db');
  4. const ExcelJS = require('exceljs');
  5. const multer = require('multer');
  6. const fs = require('fs');
  7. const path = require('path');
  8. const { CODE_TO_NAME, NAME_TO_CODE } = require('../logistics');
  9. const upload = multer({ storage: multer.memoryStorage() });
  10. // 日志文件目录
  11. const LOG_DIR = path.join(__dirname, '..', 'logs');
  12. if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
  13. // 生成物流公司代码转中文名的 CASE 语句
  14. function companyCaseExpr(alias = 'l.company') {
  15. const cases = Object.entries(CODE_TO_NAME)
  16. .map(([code, name]) => `WHEN '${code}' THEN '${name}'`)
  17. .join('\n ');
  18. return `CASE ${alias}\n ${cases}\n ELSE ${alias}\n END`;
  19. }
  20. // 用户账号配置
  21. const USERS = [
  22. { username: 'admin', password: 'admin123', role: 'admin' },
  23. { username: 'user', password: 'user123', role: 'user' },
  24. ];
  25. // 根据请求头选择数据库中间件
  26. function dbPool(req, res, next) {
  27. req.db = getPool(req.headers['x-project']);
  28. next();
  29. }
  30. // 登录接口
  31. router.post('/login', async (req, res) => {
  32. const { username, password } = req.body;
  33. const user = USERS.find(u => u.username === username && u.password === password);
  34. if (user) {
  35. const token = Buffer.from(`${username}:${user.role}:${Date.now()}`).toString('base64');
  36. await logOperation(getPool('js'), user.username, user.role, '用户登录', `${user.username} 登录系统`, req.ip);
  37. res.json({ success: true, token, username: user.username, role: user.role });
  38. } else {
  39. res.json({ success: false, message: '用户名或密码错误' });
  40. }
  41. });
  42. // 验证token中间件(解析用户名和角色)
  43. function authMiddleware(req, res, next) {
  44. const token = req.headers['authorization'];
  45. if (!token) {
  46. return res.status(401).json({ success: false, message: '未登录' });
  47. }
  48. try {
  49. const decoded = Buffer.from(token, 'base64').toString();
  50. const parts = decoded.split(':');
  51. req.user = { username: parts[0], role: parts[1] || 'user' };
  52. } catch (e) {
  53. return res.status(401).json({ success: false, message: '无效的登录状态' });
  54. }
  55. next();
  56. }
  57. // admin权限中间件
  58. function adminOnly(req, res, next) {
  59. if (req.user && req.user.role === 'admin') {
  60. next();
  61. } else {
  62. return res.status(403).json({ success: false, message: '需要管理员权限' });
  63. }
  64. }
  65. // 记录操作日志(数据库 + 本地文件)
  66. async function logOperation(db, username, role, action, detail, ip) {
  67. try {
  68. await db.query(
  69. `INSERT INTO sys_operation_log (username, role, action, detail, ip) VALUES ($1, $2, $3, $4, $5)`,
  70. [username, role, action, detail, ip]
  71. );
  72. } catch (e) {
  73. console.error('记录数据库日志失败:', e.message);
  74. }
  75. try {
  76. const now = new Date();
  77. const dateStr = now.toISOString().replace('T', ' ').substring(0, 19);
  78. const line = `[${dateStr}] [${role}] ${username} | ${action} | ${detail} | IP: ${ip || '-'}\n`;
  79. const logFile = path.join(LOG_DIR, `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}.log`);
  80. fs.appendFileSync(logFile, line, 'utf-8');
  81. } catch (e) {
  82. console.error('写入本地日志失败:', e.message);
  83. }
  84. }
  85. // 状态映射
  86. const STATUS_MAP = {
  87. 0: '待处理',
  88. 1: '已完成',
  89. 2: '已取消',
  90. 3: '已发货',
  91. };
  92. // 动态查询订单
  93. router.post('/query', authMiddleware, dbPool, async (req, res) => {
  94. try {
  95. const { receiver_name, receiver_phone, order_no, start_date, end_date, status, page = 1, pageSize = 20 } = req.body;
  96. let sql = `
  97. SELECT
  98. c.batch_no AS "批次号",
  99. o.order_no AS "订单号",
  100. to_char(o.create_time, 'YYYY-MM-DD HH24:MI:SS') AS "下单时间",
  101. o.card_no AS "卡号",
  102. o.department AS "部门",
  103. o.employee_id AS "工号",
  104. o.employee_name AS "员工姓名",
  105. o.receiver_name AS "收货人",
  106. o.receiver_phone AS "手机号",
  107. concat(o.receiver_province, o.receiver_city, o.receiver_district, o.receiver_detail) AS "收货地址",
  108. oi.package_name AS "商品名称",
  109. oi.quantity AS "数量",
  110. oi.price AS "单价",
  111. oi.subtotal AS "小计",
  112. o.remark AS "备注",
  113. ${companyCaseExpr()} AS "物流公司",
  114. l.tracking_no AS "物流单号",
  115. CASE o.status
  116. WHEN 0 THEN '待处理'
  117. WHEN 1 THEN '已完成'
  118. WHEN 2 THEN '已取消'
  119. WHEN 3 THEN '已发货'
  120. END AS "订单状态"
  121. FROM gift_order o
  122. JOIN gift_card c ON c.id = o.card_id AND c.del_flag = '0'
  123. JOIN gift_order_item oi ON oi.order_id = o.id AND oi.del_flag = '0'
  124. LEFT JOIN gift_logistics l ON l.order_id = o.id AND l.del_flag = '0'
  125. WHERE o.del_flag = '0'
  126. `;
  127. const params = [];
  128. let paramIndex = 1;
  129. if (status !== undefined && status !== null && status !== '') {
  130. sql += ` AND o.status = $${paramIndex}`;
  131. params.push(status);
  132. paramIndex++;
  133. }
  134. if (receiver_name) {
  135. sql += ` AND o.receiver_name LIKE $${paramIndex}`;
  136. params.push(`%${receiver_name}%`);
  137. paramIndex++;
  138. }
  139. if (receiver_phone) {
  140. sql += ` AND o.receiver_phone LIKE $${paramIndex}`;
  141. params.push(`%${receiver_phone}%`);
  142. paramIndex++;
  143. }
  144. if (order_no) {
  145. sql += ` AND o.order_no = $${paramIndex}`;
  146. params.push(order_no);
  147. paramIndex++;
  148. }
  149. if (start_date) {
  150. sql += ` AND o.create_time >= $${paramIndex}`;
  151. params.push(start_date);
  152. paramIndex++;
  153. }
  154. if (end_date) {
  155. sql += ` AND o.create_time < $${paramIndex}`;
  156. params.push(end_date);
  157. paramIndex++;
  158. }
  159. const countSql = sql.replace(/SELECT[\s\S]*?FROM/, 'SELECT COUNT(*) AS total FROM');
  160. const countResult = await req.db.query(countSql, params);
  161. const total = parseInt(countResult.rows[0].total);
  162. const pageNum = Math.max(1, parseInt(page));
  163. const pageSizeNum = Math.max(1, Math.min(100, parseInt(pageSize)));
  164. sql += ' ORDER BY c.batch_no, o.create_time, o.order_no';
  165. sql += ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
  166. params.push(pageSizeNum, (pageNum - 1) * pageSizeNum);
  167. const result = await req.db.query(sql, params);
  168. await logOperation(req.db, req.user.username, req.user.role, '查询订单', `条件: 状态=${status||'全部'}, 收货人=${receiver_name||'-'}, 手机号=${receiver_phone||'-'}, 订单号=${order_no||'-'}, 日期=${start_date||'-'}~${end_date||'-'}, 第${pageNum}页`, req.ip);
  169. res.json({ success: true, data: result.rows, total, page: pageNum, pageSize: pageSizeNum });
  170. } catch (err) {
  171. console.error('查询订单失败:', err);
  172. res.status(500).json({ success: false, message: err.message });
  173. }
  174. });
  175. // 导出 Excel
  176. router.post('/export/excel', authMiddleware, dbPool, async (req, res) => {
  177. try {
  178. const { batch_no, card_no, order_no, start_date, end_date, status } = req.body;
  179. let sql = `
  180. SELECT
  181. c.batch_no AS "批次号",
  182. o.order_no AS "订单号",
  183. to_char(o.create_time, 'YYYY-MM-DD HH24:MI:SS') AS "下单时间",
  184. o.card_no AS "卡号",
  185. o.department AS "部门",
  186. o.employee_id AS "工号",
  187. o.employee_name AS "员工姓名",
  188. o.receiver_name AS "收货人",
  189. o.receiver_phone AS "手机号",
  190. concat(o.receiver_province, o.receiver_city, o.receiver_district, o.receiver_detail) AS "收货地址",
  191. oi.package_name AS "商品名称",
  192. oi.quantity AS "数量",
  193. oi.price AS "单价",
  194. oi.subtotal AS "小计",
  195. o.remark AS "备注",
  196. ${companyCaseExpr()} AS "物流公司",
  197. l.tracking_no AS "物流单号",
  198. CASE o.status
  199. WHEN 0 THEN '待处理'
  200. WHEN 1 THEN '已完成'
  201. WHEN 2 THEN '已取消'
  202. WHEN 3 THEN '已发货'
  203. END AS "订单状态"
  204. FROM gift_order o
  205. JOIN gift_card c ON c.id = o.card_id AND c.del_flag = '0'
  206. JOIN gift_order_item oi ON oi.order_id = o.id AND oi.del_flag = '0'
  207. LEFT JOIN gift_logistics l ON l.order_id = o.id AND l.del_flag = '0'
  208. WHERE o.del_flag = '0'
  209. `;
  210. const params = [];
  211. let paramIndex = 1;
  212. if (status !== undefined && status !== null && status !== '') {
  213. sql += ` AND o.status = $${paramIndex}`;
  214. params.push(status);
  215. paramIndex++;
  216. }
  217. if (batch_no) {
  218. sql += ` AND c.batch_no = $${paramIndex}`;
  219. params.push(batch_no);
  220. paramIndex++;
  221. }
  222. if (card_no) {
  223. sql += ` AND o.card_no = $${paramIndex}`;
  224. params.push(card_no);
  225. paramIndex++;
  226. }
  227. if (order_no) {
  228. sql += ` AND o.order_no = $${paramIndex}`;
  229. params.push(order_no);
  230. paramIndex++;
  231. }
  232. if (start_date) {
  233. sql += ` AND o.create_time >= $${paramIndex}`;
  234. params.push(start_date);
  235. paramIndex++;
  236. }
  237. if (end_date) {
  238. sql += ` AND o.create_time < $${paramIndex}`;
  239. params.push(end_date);
  240. paramIndex++;
  241. }
  242. sql += ' ORDER BY c.batch_no, o.create_time, o.order_no';
  243. const result = await req.db.query(sql, params);
  244. const workbook = new ExcelJS.Workbook();
  245. const sheet = workbook.addWorksheet('订单列表');
  246. const columns = [
  247. { header: '批次号', key: '批次号', width: 20 },
  248. { header: '订单号', key: '订单号', width: 18 },
  249. { header: '下单时间', key: '下单时间', width: 20 },
  250. { header: '卡号', key: '卡号', width: 15 },
  251. { header: '部门', key: '部门', width: 12 },
  252. { header: '工号', key: '工号', width: 12 },
  253. { header: '员工姓名', key: '员工姓名', width: 12 },
  254. { header: '收货人', key: '收货人', width: 12 },
  255. { header: '手机号', key: '手机号', width: 15 },
  256. { header: '收货地址', key: '收货地址', width: 40 },
  257. { header: '商品名称', key: '商品名称', width: 20 },
  258. { header: '数量', key: '数量', width: 8 },
  259. { header: '单价', key: '单价', width: 10 },
  260. { header: '小计', key: '小计', width: 10 },
  261. { header: '备注', key: '备注', width: 20 },
  262. { header: '物流公司', key: '物流公司', width: 15 },
  263. { header: '物流单号', key: '物流单号', width: 20 },
  264. { header: '订单状态', key: '订单状态', width: 12 },
  265. ];
  266. sheet.columns = columns;
  267. // 创建隐藏sheet存放物流公司列表,用于数据验证
  268. const hiddenSheet = workbook.addWorksheet('logistics_list', { properties: { tabVisible: false } });
  269. const companyNames = Object.values(CODE_TO_NAME);
  270. companyNames.forEach((name, i) => {
  271. hiddenSheet.getCell(i + 1, 1).value = name;
  272. });
  273. result.rows.forEach(row => {
  274. sheet.addRow(row);
  275. });
  276. // 给物流公司列(P列)加下拉验证
  277. const lastRow = result.rows.length + 1;
  278. if (lastRow > 1) {
  279. sheet.dataValidations.add(`P2:P${lastRow}`, {
  280. type: 'list',
  281. allowBlank: true,
  282. formulae: ['logistics_list!$A$1:$A$' + companyNames.length],
  283. showErrorMessage: true,
  284. errorTitle: '无效物流公司',
  285. error: '请从下拉列表中选择物流公司',
  286. });
  287. }
  288. res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
  289. res.setHeader('Content-Disposition', `attachment; filename=orders_${Date.now()}.xlsx`);
  290. await workbook.xlsx.write(res);
  291. res.end();
  292. await logOperation(req.db, req.user.username, req.user.role, '导出Excel', `导出${result.rows.length}条订单`, req.ip);
  293. } catch (err) {
  294. console.error('导出Excel失败:', err);
  295. res.status(500).json({ success: false, message: err.message });
  296. }
  297. });
  298. // 导入运单信息 - 批量更新物流(不允许重复发货)
  299. router.post('/import/logistics', authMiddleware, dbPool, upload.single('file'), async (req, res) => {
  300. try {
  301. if (!req.file) {
  302. return res.status(400).json({ success: false, message: '请上传文件' });
  303. }
  304. const workbook = new ExcelJS.Workbook();
  305. await workbook.xlsx.load(req.file.buffer);
  306. const sheet = workbook.worksheets[0];
  307. const rows = [];
  308. const headers = [];
  309. sheet.eachRow((row, rowNumber) => {
  310. if (rowNumber === 1) {
  311. row.eachCell((cell, colNumber) => {
  312. headers[colNumber] = cell.value;
  313. });
  314. } else {
  315. const rowData = {};
  316. row.eachCell((cell, colNumber) => {
  317. rowData[headers[colNumber]] = cell.value;
  318. });
  319. rows.push(rowData);
  320. }
  321. });
  322. const results = { success: 0, failed: 0, errors: [] };
  323. // 去重:同一订单号只取第一行(导出表一个订单可能有多商品行)
  324. const seenOrders = new Set();
  325. const uniqueRows = [];
  326. for (const row of rows) {
  327. const orderNo = row['订单号'] || row['order_no'];
  328. if (orderNo && !seenOrders.has(orderNo)) {
  329. seenOrders.add(orderNo);
  330. uniqueRows.push(row);
  331. }
  332. }
  333. for (let i = 0; i < uniqueRows.length; i++) {
  334. const row = uniqueRows[i];
  335. const orderNo = row['订单号'] || row['order_no'];
  336. let company = row['物流公司'] || row['company'];
  337. const trackingNo = row['物流单号'] || row['tracking_no'];
  338. const remark = row['备注'] || row['remark'];
  339. // 验证物流公司
  340. if (!company) {
  341. results.failed++;
  342. results.errors.push(`第${i + 2}行: 缺少物流公司`);
  343. continue;
  344. }
  345. // 物流公司中文名转代码
  346. if (NAME_TO_CODE[company]) {
  347. company = NAME_TO_CODE[company];
  348. } else if (!CODE_TO_NAME[company]) {
  349. results.failed++;
  350. results.errors.push(`第${i + 2}行: 物流公司 "${company}" 无效,请从下拉列表中选择`);
  351. continue;
  352. }
  353. if (!orderNo) {
  354. results.failed++;
  355. results.errors.push(`第${i + 2}行: 缺少订单号`);
  356. continue;
  357. }
  358. if (!trackingNo || trackingNo === '-' || trackingNo === '—' || trackingNo === '--') {
  359. results.failed++;
  360. results.errors.push(`第${i + 2}行: 缺少物流单号`);
  361. continue;
  362. }
  363. try {
  364. await req.db.query('BEGIN');
  365. // 查询订单是否存在及状态
  366. const orderResult = await req.db.query(
  367. `SELECT id, status FROM gift_order WHERE order_no = $1 AND del_flag = '0'`,
  368. [orderNo]
  369. );
  370. if (orderResult.rows.length === 0) {
  371. await req.db.query('ROLLBACK');
  372. results.failed++;
  373. results.errors.push(`第${i + 2}行: 订单 ${orderNo} 不存在`);
  374. continue;
  375. }
  376. const orderId = orderResult.rows[0].id;
  377. const orderStatus = orderResult.rows[0].status;
  378. // 不允许重复发货/已完成
  379. if (orderStatus === 3) {
  380. await req.db.query('ROLLBACK');
  381. results.failed++;
  382. results.errors.push(`第${i + 2}行: 订单 ${orderNo} 已发货,不能重复导入`);
  383. continue;
  384. }
  385. if (orderStatus === 1) {
  386. await req.db.query('ROLLBACK');
  387. results.failed++;
  388. results.errors.push(`第${i + 2}行: 订单 ${orderNo} 已完成,不能导入`);
  389. continue;
  390. }
  391. // 更新订单状态为已发货,同时更新备注
  392. await req.db.query(
  393. `UPDATE gift_order SET status = 3, remark = COALESCE($1, remark), update_time = now(), update_by = $2
  394. WHERE id = $3 AND del_flag = '0'`,
  395. [remark, 'import', orderId]
  396. );
  397. // 插入或更新物流信息
  398. await req.db.query(
  399. `INSERT INTO gift_logistics (order_id, order_no, company, tracking_no, status, waybill_status, create_time, create_by, update_time, update_by, del_flag)
  400. VALUES ($1, $2, $3, $4, 0, '已发货', now(), 'import', now(), 'import', '0')
  401. ON CONFLICT (order_id) DO UPDATE
  402. SET tracking_no = $4, company = $3, status = 0, waybill_status = '已发货', update_time = now(), update_by = 'import', del_flag = '0'`,
  403. [orderId, orderNo, company || '', trackingNo]
  404. );
  405. await req.db.query('COMMIT');
  406. results.success++;
  407. } catch (err) {
  408. await req.db.query('ROLLBACK');
  409. results.failed++;
  410. results.errors.push(`第${i + 2}行: ${err.message}`);
  411. }
  412. }
  413. await logOperation(req.db, req.user.username, req.user.role, '导入运单', `成功${results.success}条, 失败${results.failed}条`, req.ip);
  414. res.json({ success: true, results });
  415. } catch (err) {
  416. console.error('导入运单失败:', err);
  417. res.status(500).json({ success: false, message: err.message });
  418. }
  419. });
  420. // 下载导入模板
  421. router.get('/import/template', authMiddleware, dbPool, async (req, res) => {
  422. try {
  423. const workbook = new ExcelJS.Workbook();
  424. const sheet = workbook.addWorksheet('运单导入模板');
  425. sheet.columns = [
  426. { header: '订单号', key: '订单号', width: 20 },
  427. { header: '物流公司', key: '物流公司', width: 18 },
  428. { header: '物流单号', key: '物流单号', width: 25 },
  429. { header: '备注', key: '备注', width: 30 },
  430. ];
  431. // 创建隐藏sheet存放物流公司列表
  432. const hiddenSheet = workbook.addWorksheet('logistics_list', { properties: { tabVisible: false } });
  433. const companyNames = Object.values(CODE_TO_NAME);
  434. companyNames.forEach((name, i) => {
  435. hiddenSheet.getCell(i + 1, 1).value = name;
  436. });
  437. // 数据验证引用隐藏sheet的范围
  438. const dataValidation = {
  439. type: 'list',
  440. allowBlank: false,
  441. formulae: ['logistics_list!$A$1:$A$' + companyNames.length],
  442. showErrorMessage: true,
  443. errorTitle: '无效物流公司',
  444. error: '请从下拉列表中选择物流公司',
  445. };
  446. sheet.dataValidations.add('B2:B1000', dataValidation);
  447. // 添加示例数据
  448. sheet.addRow({ '订单号': 'LP202607210007', '物流公司': '圆通快递', '物流单号': 'YT7631984758887', '备注': '示例备注' });
  449. sheet.addRow({ '订单号': 'LP202607210008', '物流公司': '顺丰速运', '物流单号': 'YT7631984758888' });
  450. res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
  451. res.setHeader('Content-Disposition', 'attachment; filename=import_template.xlsx');
  452. await workbook.xlsx.write(res);
  453. res.end();
  454. await logOperation(req.db, req.user.username, req.user.role, '下载导入模板', '下载运单导入模板', req.ip);
  455. } catch (err) {
  456. console.error('下载模板失败:', err);
  457. res.status(500).json({ success: false, message: err.message });
  458. }
  459. });
  460. // 统计数据接口
  461. router.get('/stats', dbPool, async (req, res) => {
  462. try {
  463. const result = await req.db.query(`
  464. SELECT
  465. COUNT(*) AS total,
  466. COUNT(*) FILTER (WHERE o.status = 0) AS pending,
  467. COUNT(*) FILTER (WHERE o.status = 1) AS completed,
  468. COUNT(*) FILTER (WHERE o.status = 2) AS cancelled,
  469. COUNT(*) FILTER (WHERE o.status = 3) AS shipped
  470. FROM gift_order o
  471. WHERE o.del_flag = '0'
  472. `);
  473. const row = result.rows[0];
  474. res.json({
  475. success: true,
  476. data: {
  477. total: parseInt(row.total),
  478. pending: parseInt(row.pending),
  479. completed: parseInt(row.completed),
  480. cancelled: parseInt(row.cancelled),
  481. shipped: parseInt(row.shipped),
  482. }
  483. });
  484. } catch (err) {
  485. console.error('获取统计失败:', err);
  486. res.status(500).json({ success: false, message: err.message });
  487. }
  488. });
  489. // 查询操作日志(仅admin)
  490. router.get('/logs', authMiddleware, adminOnly, dbPool, async (req, res) => {
  491. try {
  492. const { page = 1, pageSize = 20, username, action, start_date, end_date } = req.query;
  493. let sql = `SELECT * FROM sys_operation_log WHERE 1=1`;
  494. const params = [];
  495. let paramIndex = 1;
  496. if (username) {
  497. sql += ` AND username = $${paramIndex}`;
  498. params.push(username);
  499. paramIndex++;
  500. }
  501. if (action) {
  502. sql += ` AND action LIKE $${paramIndex}`;
  503. params.push(`%${action}%`);
  504. paramIndex++;
  505. }
  506. if (start_date) {
  507. sql += ` AND create_time >= $${paramIndex}`;
  508. params.push(start_date);
  509. paramIndex++;
  510. }
  511. if (end_date) {
  512. sql += ` AND create_time < $${paramIndex}::timestamp + interval '1 day'`;
  513. params.push(end_date);
  514. paramIndex++;
  515. }
  516. // 总数
  517. const countSql = sql.replace('SELECT *', 'SELECT COUNT(*) as total');
  518. const countResult = await req.db.query(countSql, params);
  519. const total = parseInt(countResult.rows[0].total);
  520. sql += ' ORDER BY create_time DESC';
  521. sql += ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
  522. params.push(pageSize, (page - 1) * pageSize);
  523. const result = await req.db.query(sql, params);
  524. res.json({ success: true, data: result.rows, total, page: parseInt(page), pageSize: parseInt(pageSize) });
  525. } catch (err) {
  526. console.error('查询日志失败:', err);
  527. res.status(500).json({ success: false, message: err.message });
  528. }
  529. });
  530. module.exports = router;