运营数据管理系统前端开发记录
2025-3-27
最近刚学习完后台管理结构,突然就安排了个独立的管理系统项目。这不巧了吗?
当然,后端也是我负责。因为只熟 Node,express,所以用了 Node 写后端。
sql 脚本都是现成的,但是比较乱。一些条件查询需要自己拼接 sql 语句。
顺便,后端管理架构文章:文档
1. 需求分析
需求很简单,就是运营经常需要查询一些学生和租户(各个学校)数据。之前都是安排个后端同事直接写 sql 查询,然后导出 excel。
领导嫌比较麻烦,就想做个管理系统。基本只需要查询功能,不需要增删改。还需要导出 excel 表格。
2. 技术选型
毫无疑问直接选了刚学完的管理系统模版:Vue3 + Ts + vite + pinia + element-plus。
后端 Node + express + mysql + exceljs
需要的页面增增删删,很快就能完成。
3. 开发过程简述
前端部分
这个没有太多好说的,主要就是把路由修改好,添加相应的页面,写好 api 接口。
excel 导出的地方,模板中有导出的方法,需要后端配合以 blob 格式传回 excel 文件。
后端部分
因为功能比较单一,所以只分了控制层,数据管理层。
控制层控制路由、数据返回的格式。
数据层主要负责拼接 sql 语句,执行查询。
以其中一个路由接口 getSystemRoles 为例:
// app.js
app.use("/api", dataRoutes);
// dataRoutes.js
router.get("/getSystemRoles", systemUserController.getSystemRoles);
// systemUserController.js
const getSystemRoles = async (req, res) => {
try {
const data = await dataService.getSystemRoles();
const result = {
code: 200,
data: data,
msg: "",
};
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
};
// systemUserService.js
const getSystemRoles = async () => {
const sql = `SELECT NAME,CODE FROM xinwei_backstage.system_role WHERE deleted=0`;
const [rows] = await pool.query(sql);
return rows;
};
当然,比较通用的,比如不需要拼接 sql 语句的,或者大量只需要时间范围参数的接口,都可以封装函数减少代码重复率。
4. 遇到的坑
OK,到了重点部分。开发中确实遇到了一些问题。
前端文件下载
前端文件下载有一个 hook,专用于文件下载,useDownload。但是刚开始下载有问题,下载的 excel 中都是 blob 的格式,而不是正常的数据。
查阅后发现需要格式化二进制流。当然,直接在后端修改,将文件整理成 excel 后再发送过来也是可以的。
if (isFormatBuffer) {
res = new Uint8Array(res);
}
前端的一些组件封装
这个不算遇到的问题,只是一些组件封装心得。
根据我目前的项目经验,只要是感觉会复用的组件,都有封装的必要,哪怕只在两个地方用了都是值得的。
尤其是后端管理系统中,重复使用的东西有很多,而且后期的新需求经常和之前做过的比较类似,所以封装组件是非常有性价比的工作。
后端大概摸数据导出
需要导出的 excel 中,有些 sql 的查询时间非常长,大概有 400 秒。
对于查询时间超长的,导出时给出大概时间提醒,并把导出按钮变成 loading 状态。

这个属于没办法的事,领导也同意了这个方案。
另一个是查询时间长的同时,数据量有几十万条。整理成 excel 表格后大概 200M 大小。
如果是之前小数据量的方法:
/*
fieldsMap:{学院id: "college_id",...}
*/
const getExportDatabuffer = async (sql, fieldsMap, params = []) => {
const [rows] = await pool.query(sql, params);
console.log("query is done!");
const fieldEntries = Object.entries(fieldsMap);
const mappedRows = new Array(rows.length);
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const mappedRow = {};
for (const [cnKey, enKey] of fieldEntries) {
mappedRow[cnKey] = row[enKey];
}
mappedRows[i] = mappedRow;
}
const worksheet = xlsx.utils.json_to_sheet(mappedRowsShort, {
header: Object.keys(fieldsMap),
});
const workbook = xlsx.utils.book_new();
xlsx.utils.book_append_sheet(workbook, worksheet, "Sheet1");
const buffer = xlsx.write(workbook, { type: "buffer", bookType: "xlsx" });
console.log("xlsx is done!");
return buffer;
};
这个方法查询是正常的(虽然很慢),rows 数据也有。但是在 worksheet 这一步就会卡住。
可能是因为 excel 的一页数据上限时 60000+,所以不够用了。
解决方法是使用 exceljs 创建可写流,数据分页写入 excel 然后流式传输
const ExcelJS = require("exceljs");
/*.......................................*/
// 查询,查询耗时较大
const [rows] = await pool.query(sql);
// 1. 创建可写流
const workbook = new ExcelJS.stream.xlsx.WorkbookWriter({
stream: res, // 直接输出到HTTP响应流
useStyles: false,
useSharedStrings: false,
});
// 分批次处理数据,每页60000条数据,excel每页有数据上限
// 同事防止内存溢出
let page = 1;
let pageSize = 60000; // 每页处理的数据量
let hasMore = true;
let sheetRows = [];
const keys = Object.keys(fieldMappings);
const sqlKeys = Object.values(fieldMappings);
console.log("数据处理开始...");
while (hasMore) {
sheetRows = rows.splice(0, pageSize); //一页的原始数据
console.log(`正在处理第${page}页数据...`);
if (rows.length == 0) hasMore = false; // 没有更多数据时退出循环
// 创建页
const worksheet = workbook.addWorksheet(`sheet${page}`);
page++;
// 添加标题行(先写入头部)
worksheet.addRow(keys).commit();
// 添加数据行(写入原始数据)
sheetRows.forEach((row) => {
let rowData = sqlKeys.map((key) => row[key]);
worksheet.addRow(rowData).commit(); // 立即提交行
});
}
// 6. 结束写入
await workbook.commit();
思路就是吗,每次取 6W 条数据,然后新建 sheet,写入数据,然后继续下一页。最后结束写入流。
这样虽然查询耗时没有变短,但是能正常的传输,切传输速度挺快的。
其后请教了后端同事,用多线程分页查询应该会优化查询速度。还没有试这个方法,而且不知道 Node 有没有类似多线程的方式。
总结
目前遇到的问题都不算太难,最难的就是上面这个大文件问题了。项目开发的还算顺利。后面的开发过程中遇到问题再继续写。