基于Node.js和Vue的原材料采购管理系统开发实战

发布时间:2026/9/28 15:17:03
基于Node.js和Vue的原材料采购管理系统开发实战 对于很多中小型制造企业来说采购管理一直是个让人头疼的环节。纸质单据满天飞、Excel表格来回传、供应商报价靠微信聊天记录拼凑一到大促备货期采购员、仓管、财务三头跑数据对不上账的情况时有发生。我自己就经历过这种混乱所以当朋友提出要做一套“基于Node.js和Vue的原材料采购管理系统”时我第一个反应就是这个方向切中要害。这套系统的核心价值就是把手动表格和口头沟通转化为可视化的流程闭环让采购申请、审批、入库、对账都在一个平台里跑通而且技术选型很轻量Node.js扛后端接口Vue做前端交互前后端分离后期扩展也不至于被老架构绑死。这篇博文我会从整套系统的设计拆解开始到具体的后端接口实现、前端页面搭建再到nodejs安装、npm镜像源配置这些环境细节以及我在开发过程中踩过的坑全部梳理出来。不管是准备自学练手的新手还是真想给公司做一套内部工具的开发人员都能从中找到可以直接抄作业的部分。1. 项目整体设计与功能定位1.1 原材料采购的业务流到底怎么拆做这类管理系统最忌讳一上来就建表、写接口。你先得把业务流理清楚否则后面全是返工。原材料采购这条线看起来是个“采购员下单”的事实际上牵扯到好几个角色和环节。我以最常见的工厂场景为例拆解一遍需求发起生产部门根据排产计划提出原料需求或者是仓管发现安全库存告急给出补货申请。供应商寻源采购员拿着物料清单去询价、比价选定供应商确定单价和交期。采购申请与审批需求部门提交申请单采购主管、分管领导依次审批金额大的合同可能还得走财务预审。采购订单生成审批通过后转成正式采购订单推送给供应商进入备货发货流程。到货入库质检验货、仓管点数入库记录与采购订单关联更新库存台账。对账结算月底财务根据入库单、送货单、发票做三单匹配安排付款。明白了这条链路系统设计的核心模块也就清晰了供应商管理、原材料档案、采购申请、审批流、订单管理、入库管理、库存查询、统计报表。再加两个辅助模块用户权限管理、操作日志。这几个功能做扎实了整套系统就是可用状态。我见过不少人做项目把公司组织结构、预算科目全塞进去结果开发周期拖长、数据又填不齐。中小企业采购系统第一版不要去追求大而全聚焦“申请—审批—入库—对账”这四个字先把主链路走通比什么都强。1.2 为什么选Node.js和Vue这个组合说实话这类管理系统用Spring Boot JSP也能做用Python Django也行但Node.js Vue这对组合在中小型内部系统里跑起来是最舒服的。原因有三点第一前后端语言统一都是JavaScript/TypeScript。这意味着后端写接口的人跟写前端页面的可以无缝协作甚至一个人从头干到尾也不会有太大语言切换成本。对很多小团队来说招一个全栈JS工程师就能顶起这个项目比配齐Java后端和Vue前端两队人马要现实得多。第二Node.js做接口服务的开发效率确实高。一个采购审批流核心就是状态字段的流转Node.js配合Express或Koa框架十来行代码就能写清楚一个路由。再加上npm生态里现成的库非常多——JWT鉴权、Excel解析导出、PDF生成、定时任务全是开箱即用省去了从零造轮子的时间。第三Vue的前端开发体验对这类后台系统非常友好。原材料采购管理系统的页面形态主要是表格、表单、弹窗、统计图理解一下它的生命周期和组件通信开发效率就能大幅提升。Vue的模板语法写起来比原生DOM操作香太多数据驱动视图的特性让“库存数量一变、页面自动更新”这种事毫无压力。如果你是在纠结要不要用Spring Boot那套我的建议很直白项目的时间要求、团队的技能储备如果偏向JS生态那么Node.js Vue完全够了。它不是最“企业级”的选型但是是最快速能看见成果的选型。1.3 系统核心功能模块梳理我列一个第一版建议实现的模块清单每项后面标注了它的用途模块名称核心功能重要程度供应商管理供应商档案新增、编辑、停用联系人、银行账号维护高原材料档案物料编码、规格型号、单位、默认供应商、安全库存高采购申请单需求部门提单选择物料、数量、期望到货日期高审批流多级审批、审批记录留痕、驳回重新提交高采购订单审批通过转订单供应商确认状态交期追踪中高到货入库关联订单做收货登记质检结论、入库数量高库存查询实时库存、出入库流水、低于安全库存预警中高统计报表采购金额趋势、供应商交货及时率、物料价格对比中系统管理用户、角色、菜单权限、操作日志高第一版千万不要把供应商协同、电子签章、ERP对接这种重型功能纳入那属于二期、三期的范围。先把内部的这几个模块做闭环数据规范了以后再谈对接的事。2. 后端工程结构与接口设计实战2.1 项目工程目录与初始化我习惯把前后端分成两个目录git仓库也分开管理。落在实际命令上大概是这样的purchase-system/ ├── server/ # Node.js后端 ├── web/ # Vue前端 └── sql/ # 数据库初始化脚本后端我选择Express框架原因没那么多花哨Express的中间件机制灵活、生态最成熟、网上资料最多遇到问题搜一下基本都有答案。初始化后端项目直接用官方脚手架mkdir purchase-system cd purchase-system/server npm init -y npm install express mysql2 sequelize cors jsonwebtoken bcryptjs数据库我用MySQL原因很务实采购系统要处理的是强结构化数据物料编码、数量、金额、状态这些用关系型数据库管理最顺手报表查询写SQL也利落。如果你更熟悉MongoDB那套文档模型也不是不能用但涉及多表关联统计时MySQL会省事得多。Sequelize作为ORM核心价值是让我不用手写一堆冗余的SQL拼接。以原材料表为例定义模型就一段话的事// models/material.js const { DataTypes } require(sequelize); const sequelize require(../config/database); const Material sequelize.define(Material, { code: { type: DataTypes.STRING(50), allowNull: false, unique: true }, name: { type: DataTypes.STRING(100), allowNull: false }, spec: { type: DataTypes.STRING(100), defaultValue: }, unit: { type: DataTypes.STRING(10), allowNull: false }, safetyStock: { type: DataTypes.DECIMAL(12, 2), defaultValue: 0 }, status: { type: DataTypes.TINYINT, defaultValue: 1 } // 1启用 0停用 }); module.exports Material;表结构的设计有个经验可以分享凡是要做统计的字段建表时就用精确数值类型DECIMAL(12,2)就是金额、数量的万金油类型别用FLOAT否则账单算着算着就出现0.10.2不等于0.3的尴尬。2.2 接口设计以业务场景为驱动这块是最重要的后端API的质量直接决定前端开发的体验。我从这套系统里抽三个典型场景做例子。场景一创建采购申请单前端拿到一张表单勾了几种物料每种填了数量和期望日期提交过来是一个嵌套结构。后端的处理要拆成两步先写主表再循环写明细表。采购申请单的主表和明细表是一对多的关系千万别图省事把多个物料塞进一个JSON字段。// routes/purchaseRequest.js router.post(/, async (req, res) { const t await sequelize.transaction(); try { const { title, applicant, department, expectDate, items } req.body; // 第一步创建主表记录 const header await PurchaseRequest.create({ title, applicant, department, expectDate, status: DRAFT // 待审批 }, { transaction: t }); // 第二步批量创建明细记录 const detailData items.map(item ({ headerId: header.id, materialId: item.materialId, quantity: item.quantity, price: item.price || 0, remark: item.remark || })); await PurchaseRequestItem.bulkCreate(detailData, { transaction: t }); await t.commit(); res.json({ success: true, data: { id: header.id } }); } catch (err) { await t.rollback(); console.error(创建采购申请失败:, err); res.status(500).json({ success: false, message: err.message }); } });这里我坚持使用了事务处理。采购申请单涉及主表和明细表至少两条SQL写入不用事务的话主表写成功了、明细写一半报错数据就不一致了。之前我在某项目上没注意这件事结果库存数据和订单数据互相矛盾排查时欲哭无泪。场景二审批流状态流转审批流我采用的是“状态机操作记录”的双轨结构。状态机保证流转合法性比如“已审批”的单子不能再次提交审批“待审批”的单子不能直接变成“已完成”必须经过审批动作。操作记录保证每一次动作都有据可查出问题可回溯。状态定义大致是这样的DRAFT待提交PENDING待审批APPROVED已通过REJECTED已驳回审批接口的部分核心代码如下router.post(/:id/approve, authMiddleware, async (req, res) { const { id } req.params; const { action, comment } req.body; // action: pass | reject const record await PurchaseRequest.findByPk(id); if (!record) return res.status(404).json({ message: 单据不存在 }); if (record.status ! PENDING) return res.status(400).json({ message: 当前状态下不能审批 }); const nextStatus action pass ? APPROVED : REJECTED; const r await sequelize.transaction(); try { await record.update({ status: nextStatus, approver: req.user.id }, { transaction: r }); const log { purchaseRequestId: id, operatorId: req.user.id, action: action pass ? 通过 : 驳回, comment: comment || }; await ApprovalLog.create(log, { transaction: r }); await r.commit(); res.json({ success: true, status: nextStatus }); } catch (e) { await r.rollback(); res.status(500).json({ message: e.message }); } });多级审批怎么办通用做法是加一个approvalStep字段每过一级就加一当前级审批人比对step值就知道自己该不该处理。第一版系统里我建议做成简单两级部门主管初审总经理终审流程跑熟了再加第三级不迟。场景三采购订单转入库这是最容易出问题的环节。采购订单的数量和后续入库数量之间存在“部分入库”“超额入库”“多次入库”三种可能。表设计上必须有receivedQuantity累计入库数量这个字段每次入库做完累加然后判断与订单数量是否一致一致就把订单标记为“已入库”。router.post(/:orderId/receipt, async (req, res) { const { orderId } req.params; const { materialId, quantity, warehouseId, remark } req.body; const order await PurchaseOrder.findByPk(orderId); if (!order) return res.status(404).json({ message: 订单不存在 }); const t await sequelize.transaction(); try { const oldReceived parseFloat(order.receivedQuantity) || 0; const newReceived oldReceived parseFloat(quantity); if (newReceived order.quantity) { await t.rollback(); return res.status(400).json({ message: 入库数量超过订单剩余数量 }); } await order.update({ receivedQuantity: newReceived, status: newReceived order.quantity ? COMPLETED : RECEIVING }, { transaction: t }); await InStockRecord.create({ orderId, materialId, quantity, warehouseId, operatorId: req.user.id, remark }, { transaction: t }); // 同步更新材料库存 const stock await Stock.findOne({ where: { materialId, warehouseId } }); if (stock) { await stock.increment(quantity, { by: quantity, transaction: t }); } else { await Stock.create({ materialId, warehouseId, quantity }, { transaction: t }); } await t.commit(); res.json({ success: true, receivedQuantity: newReceived }); } catch (e) { await t.rollback(); res.status(500).json({ message: e.message }); } });这个接口里库存同步更新这块的逻辑值得反复琢磨。采购入库本质上是一个“事务性”动作订单表累计数量要变、入库记录要新增、库存表要累加三者必须同时成功或者同时失败。任何一步掉了月底账目都会出问题。2.3 用户权限与登录鉴权后台管理系统的权限控制常用方案是RBAC也就是给用户分角色角色挂权限点。具体到代码层面最简的落地方式用户登录后返回一个tokenJWTtoken里面带上用户id同时把用户拥有的权限点返回给前端前端根据权限点决定菜单和按钮显隐。JWT签发没啥技巧关键是密钥管理。开发环境随便写在config里可以理解上了生产环境密钥必须放到环境变量文件中别硬编码在仓库里。项目里我做了个极简的权限中间件// middleware/auth.js const jwt require(jsonwebtoken); module.exports function (req, res, next) { const token req.headers[authorization] req.headers[authorization].split( )[1]; if (!token) return res.status(401).json({ success: false, message: 未登录或登录已过期 }); try { const payload jwt.verify(token, process.env.JWT_SECRET); req.user payload; next(); } catch (err) { return res.status(401).json({ success: false, message: token无效 }); } };这样一个中间件挂载到所有需要登录的路由前前端只要每次请求带上Authorization: Bearer token登录校验这块就闭环了。密码存库一定要用bcryptjs做哈希千万别明文存也别用MD5这块出事的教训太多了不值得冒风险。Vue前端配合vue-router的路由守卫实现“未登录跳转登录页”的逻辑体验上就很完整了。3. Vue前端的核心页面与交互实现3.1 Vue项目搭建与工程化配置前端工程我直接用的Vue CLI初始化命令很简单执行后一路选默认配置就可以cd purchase-system/web npx vue/cli create web创建完以后我通常会马上补几样东西element-ui组件库或者新版选element-plus、axios请求封装、vue-router和vuex/pinia状态管理。这个组合在后台管理系统里属于“标准配置”省下的时间都花在业务逻辑上。axios封装是每个项目都必须做的一步。我不能接受每个组件都去写一遍this.$http.get(/api/xxx)而且要统一处理token注入和错误提示的话封装一层是最省心的// utils/request.js import axios from axios; import { Message } from element-ui; import router from ../router; const request axios.create({ baseURL: process.env.VUE_APP_API_BASE || /api, timeout: 10000 }); request.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] Bearer token; } return config; }); request.interceptors.response.use( response { return response.data; }, error { if (error.response error.response.status 401) { localStorage.removeItem(token); router.push(/login); } else { Message.error(error.response?.data?.message || 网络异常请稍后重试); } return Promise.reject(error); } ); export default request;请求函数单独在api目录里建模块比如api/purchase.js里面的函数名跟后端接口一一对应。前端页面只负责调用这些api函数不关心请求细节后期后端接口变了改api目录一处就行。项目规模一大这种分层的收益就特别明显。3.2 采购申请单页面的组件拆分采购申请单是整个系统的关键页面。它长什么样呢上方是表单区域——申请人、部门、期望到货日期中间是明细表格——物料编码、名称、数量下方是按钮区域——暂存、提交审批。组件的拆分思路是这样的PurchaseRequestForm.vue承载主表单和明细录入MaterialSelectorDialog.vue物料选择弹窗搜索多选RequestDetailTable.vue明细表格展示与编辑ApprovalTimeline.vue审批进度时间线展示其中MaterialSelectorDialog这层的体验对采购员来说最重要。他们在实际操作中经常只记得物料的模糊名称比如“那个30mm的钢板”所以弹窗必须要支持按编码、名称双重搜索。核心代码如下template el-dialog title选择物料 :visible.syncvisible width70% el-input v-modelkeyword placeholder输入物料编码或名称搜索 prefix-iconel-icon-search clearable / el-table :datafilteredMaterials highlight-current-row selection-changehandleSelectionChange el-table-column typeselection width50 / el-table-column propcode label物料编码 width140 / el-table-column propname label物料名称 / el-table-column propspec label规格型号 width180 / el-table-column propunit label单位 width80 / el-table-column propprice label参考单价 width110 / /el-table div slotfooter el-button clickvisible false取消/el-button el-button typeprimary clickconfirm确定/el-button /div /el-dialog /template script export default { name: MaterialSelectorDialog, data() { return { visible: false, keyword: , materials: [], selectedRows: [] }; }, computed: { filteredMaterials() { if (!this.keyword) return this.materials; return this.materials.filter(item item.code.includes(this.keyword) || item.name.includes(this.keyword) ); } }, methods: { open() { this.visible true; this.fetchMaterials(); }, async fetchMaterials() { const res await this.$api.material.list({ status: 1 }); this.materials res.data; }, handleSelectionChange(rows) { this.selectedRows rows; }, confirm() { this.$emit(selected, this.selectedRows); this.visible false; } } }; /script组件对外只暴露一个open()方法和一个selected事件。父组件拿来即用不需要关心弹窗内部到底是怎么搜物料、怎么选行的。处理这类表单弹窗组合的页面我总结了一条经验把弹窗做成独立子组件别把弹窗的显示逻辑堆在父页面里否则父组件的data字段会爆炸维护起来像在翻垃圾堆。3.3 审批页面和统计报表的交互细节审批页面是管理者每天都在用的交互上要照顾他们的决策效率。我做了一个“待办列表 详情抽屉”的模式左侧是待审批单据列表点击某条后右侧抽屉展示这张单据的物料明细和流转记录底部放着通过、驳回两个大按钮。这里最关键的一个交互细节审批人必须具备“带入上下文”的视角也就是在决定同意或驳回之前可以一眼看到剩余物料库存、该供应商历史交货表现等信息。我通过给详情抽屉挂一个页签实现的基础信息、物料明细、供应商评价、历史采购记录。多出来的数据在实际落地时反而减少了大量上下游来回求证。统计报表是另一个常见页面我首选用Vue Element UI的表格做基础数据导出图表部分配合ECharts展示趋势。采购金额按月的柱状图是老板最爱看的一张图。它的数据来源接口写一条SQL就够了SELECT DATE_FORMAT(created_at, %Y-%m) AS month, SUM(total_amount) AS total FROM purchase_orders WHERE status IN (APPROVED, COMPLETED) GROUP BY DATE_FORMAT(created_at, %Y-%m) ORDER BY month;后端查完直接返回给前端前端用一个this.chart.plot(res.data)就完成了。ECharts的官方示例有很多可以直接套用不推荐从头配置绘图参数。报表这块我必须提醒一句不要过度设计。第一版有几个核心报表就足够采购趋势、供应商及时率、库存预警这三个最实用。别急着上仪表盘、多维透视、大屏展示那是数据部门的事你先把数据采对了再说。4. Node.js环境配置与常见坑位4.1 nodejs安装与版本选择这个系统从零开始环境配置倒是拦了我一会儿。给新手一个简单参考nodejs直接去官网下载LTS版本也就是长期支持版别追新。很多系统稳定运行在Node 18或者Node 20如果你们公司有明确的技术栈规范看项目文档选对应版本即可。Windows上安装时有一个默认路径的问题如果没有特殊理由建议保持默认路径。后续在环境变量里把NODE_HOME指向nodejs安装目录然后在PATH追加%NODE_HOME%就行了。安装完后打开终端执行node -v npm -v两个命令能输出版本号说明基本环境就位了。4.2 npm依赖安装慢与报错处理实际开发中npm安装依赖慢是常事特别是首次执行npm install的时候等十分钟都可能。解决方案就是切换镜像源国内环境下用淘宝镜像效果立竿见影。命令是npm config set registry https://registry.npmmirror.com改完之后验证一下npm config get registry看到返回https://registry.npmmirror.com说明切换成功。以后你执行npm install就会快很多。开发时有时候报错提示“无法加载文件...npm.ps1”这是因为Windows PowerShell默认执行策略限制脚本运行。解决办法是用管理员权限打开PowerShell执行Set-ExecutionPolicy -ExecutionPolicy RemoteSigned回车确认y即可。如果公司电脑权限受限、不让改执行策略还有另一种办法完全避开PowerShell打开CMD窗口来跑npm命令CMD里不存在这个限制。这是我在多个办公环境下实测可行的一条路径遇到这个报错的同事可以少走点弯路。4.3 前后端联调与跨域配置前端开发服务器跑在8080端口后端接口跑在3000端口浏览器跨域拦截就会报错。Vue CLI的devServer支持代理配置把/api开头的请求全部转发到后端这样前端请求地址跟后端路由对得上还不存在跨域问题。常规配置写在项目根目录的vue.config.jsmodule.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:3000, changeOrigin: true, pathRewrite: { ^/api: } } } } };后端接口如果习惯不带/api前缀那就用pathRewrite把这个前缀去掉例如前端请求/api/login实际打到后端是/login。这套代理方案在本地联调阶段非常好用部署到服务器时再用nginx把/api反向代理到Node服务端口前后端就一体化了不会出现跨域问题。5. 数据库设计与关键报表查询5.1 核心数据表设计整个系统我设计了十几张表核心几张表的结构专门说明一下供应商表(suppliers)属性包括供应商编码、名称、联系人、手机号、电话、地址、开户行、银行账号、合作状态。银行账号这种东西容易忽略但真到财务对账阶段缺了它财务没法打款。别嫌字段多基础档案补全要趁早。原材料表(materials)核心字段之前在后端章节列出过。补充一点原材料档案必须跟供应商建立关联关系建议专门建一张material_suppliers关联表一个物料允许多个供应商。这样采购员在提单的时候就能看到“这个物料有哪些供应商在供”不用翻开通讯录人肉比对。采购申请主表 明细表(purchase_requests purchase_request_items)主表存单据编号、申请人、部门、期望日期、状态。明细表存物料、数量、单价、备注。单据编号的生成规则我习惯做成PR 年月日 三位流水号比如PR20250108001多了一个序列号字段而已但对账时一眼就能看出单据产生时间相当有用。采购订单表(purchase_orders)跟申请单不同采购订单是跟供应商确认过的正式单据会带到货日期、付款方式、物流信息。它也可以由申请单直接转换生成转换时复制基础字段过去再补充供应商信息。我实现的转换逻辑中通过遍历明细表数据生成订单明细同时保留申请单id字段作为追溯链路后续查问题就顺着一条线揪到底。入库记录表(in_stock_records)字段有订单号、物料、仓库、入库数量、经手人、时间、备注。这个表跟库存表是联动关系前面接口代码里已经体现了。审批日志表(approval_logs)字段有单据类型、单据id、操作人、操作动作、审批意见、操作时间。这个表不能省略它的目的不是展示而是内审和问题追溯。5.2 库存预警和采购趋势的SQL写法库存预警实用的思路是查“当前库存低于安全库存的物料”顺便带出默认供应商联系方式方便采购员直接发起补货。SQL差不多这样SELECT m.code, m.name, m.spec, m.unit, IFNULL(s.quantity, 0) AS stockQuantity, m.safetyStock, su.name AS supplierName, su.contact_person, su.phone FROM materials m LEFT JOIN stock s ON s.materialId m.id LEFT JOIN material_suppliers ms ON ms.materialId m.id LEFT JOIN suppliers su ON su.id ms.supplierId WHERE m.status 1 AND IFNULL(s.quantity, 0) m.safetyStock ORDER BY IFNULL(s.quantity, 0) - m.safetyStock ASC;供应商交货及时率这个指标做起来稍微费点劲需要统计每个供应商订单中按交期完成的占比。核心思路是把每一笔订单的实际完成日期跟期望到货日期做比较SELECT su.name AS supplierName, COUNT(o.id) AS totalOrders, SUM(CASE WHEN o.actualDate o.expectDate THEN 1 ELSE 0 END) AS onTimeOrders, ROUND(SUM(CASE WHEN o.actualDate o.expectDate THEN 1 ELSE 0 END) / COUNT(o.id) * 100, 2) AS onTimeRate FROM purchase_orders o JOIN suppliers su ON su.id o.supplierId WHERE o.status COMPLETED GROUP BY su.id ORDER BY onTimeRate DESC;这种报表查询在Expreess里用sequelize写起来也可以但直接用SQL模板查询然后返回原始结果会更直观。数据量不大性能不是一个需要焦虑的点先把结果查准确。6. 项目运行全流程与部署实践6.1 本地开发环境的完整启动流程把这个项目从零跑到能看到登录页其实没几步但顺序踩错容易出岔子。我采用的顺序如下第一步准备数据库。在MySQL里执行sql/init.sql创建库、建表、插入测试数据。测试数据非常重要我第一次跑项目时造了一批物料和供应商数据后续调试前端列表和搜索功能时所有交互才有东西可以展示。第二步启动后端。cd server npm install cp .env.example .env # 修改数据库账号密码 npm run dev后端启动成功的标志是控制台打印“Server running on port 3000”然后可以用postman先测一个登录接口确认数据库连接没问题再往下走。第三步启动前端。cd web npm install npm run serve浏览器打开http://localhost:8080能看到登录页。用测试账号登录进入主界面整个链路就是通的。我强烈建议后端接口先于前端页面开发完成且用postman自测通过前端页面开发才会变成一件纯粹的拼装活动而不是一边写前端一边排查后端。这个习惯帮我省掉了大量“页面白屏不知道是数据问题还是代码问题”的排查时间。6.2 部署方案与nginx配置小规模内部系统我推荐一种省事的部署方案后端用PM2托管前端打包成静态文件交给nginx然后nginx统一托管静态资源和反向代理接口。构建前端的命令是cd web npm run build产出的是一个dist/目录把里面的文件复制到服务器上规划好路径后nginx配置大概长这样server { listen 80; server_name purchase.example.com; root /var/www/purchase-web; index index.html; location /api/ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }后端用PM2托管的好处是进程崩溃自动重启日志也有落盘pm2 start app.js --name purchase-server pm2 saveNode.js服务挂掉导致整个系统不可用的场景很常见PM2能有效缓解这种尴尬。环境变量文件在服务器上单独维护写入.env里bash里执行命令前先确认所有环境变量都已正确配置否则API启动后连不上数据库又够折腾半天的。6.3 数据备份与日常维护小型内部系统最容易忽视的就是备份。头一个月大家热情高涨数据都在本机丢了也不心疼半年后数据量上来了哪天数据库被误删或服务器坏了才发现没备份那个感觉我非常熟悉因为亲身经历过一次。所以这套采购系统上线第二天我就写了个定时备份脚本每天凌晨2点用mysqldump全量备份数据库备份文件存到独立目录再按天滚动保留最近30天#!/bin/bash BACKUP_DIR/data/backup/mysql DATE$(date %Y%m%d%H%M) /usr/bin/mysqldump -u root -p密码 purchase_system | gzip $BACKUP_DIR/purchase_$DATE.sql.gz find $BACKUP_DIR -name purchase_*.sql.gz -type f -mtime 30 -exec rm {} \;crontab里加上0 2 * * * bash /data/scripts/backup_mysql.sh就完事了。备份这件事花的时间成本几乎为零但出问题时价值无限。7. 开发中踩过的坑与解决过程7.1 库存扣减的并发问题有一段时间我发现库存数据和实际盘点对不上排查了很久才发现是并发问题。采购入库和销售出库同时操作同一个物料时后端先查库存再扣减的“读改写”模式会在并发下产生丢失更新。拿入库来说原来的代码是const stock await Stock.findOne({ where: { materialId } }); const newQty parseFloat(stock.quantity) parseFloat(quantity); await stock.update({ quantity: newQty });两个请求同时读到quantity100一个加10一个加20最后可能只变成120而不是130。解决方案很简单改为原子更新await Stock.increment(quantity, { by: quantity, where: { materialId } });或者直接用SQLUPDATE stock SET quantity quantity ? WHERE material_id ?;这里的过程跟多个人同时修改同一份Excel表格类似后保存的会覆盖先保存的内容库存也是有“状态”的操作必须原子化。这个坑是并发系统必踩的经典坑越早期意识到后面越省心。7.2 前端表格大数据量卡顿采购明细数据一多特别是关联了供应商和物料信息后表格列表几百上千条就很卡。原因通常是前端一次性把所有字段都渲染状态也在频繁响应式更新。我采用的优化方案表格改为分页加载每页20条后端配合LIMIT和OFFSET弹窗里的搜索接口做防抖输入停止300毫秒后才发请求避免每敲一个字母打一次接口只读数据用Object.freeze冻结不让Vue做响应式代理减少内存占用和代理开销。实测下来页面切换流畅度明显提升。Vue的响应式系统很强大但也不是没代价的大数据量的纯展示数据冻结后就没了“数据变动自动更新”的魔法但展示需求根本不需要它“活”着。7.3 npm依赖地狱的教训有一回我亲眼见着一个同事在项目里把同一个UI组件库的不同主版本都装上去了结果样式错乱到没法看。依赖管理这件事实在太重要了平时得留点心。在这个系统里我维护了一套自己的规矩依赖尽量少、版本尽量新但别追完美最新、lock文件必须提交到git仓库。package-lock.json或者yarn.lock锁住依赖树这一件事能保证团队里所有人本地安装的依赖版本完全一致。否则你本地跑得好好的同事一pull代码就报错十有八九是依赖版本漂了。npm install生成的那个lock文件默认情况下应当提交这是团队约定里必须写明的第一条。8. 这套系统未来的可扩展方向第一版功能上线运行之后回头看看哪些地方可以继续前进我心里有几条路线第一条升级报表能力。当前报表只是固定了几个维度后期可以引入更灵活的筛选条件比如按供应商分类、按物料分类、按时间区间任意组合甚至把采购价格波动曲线做出来。每一次采购的单价跟历史采购价对比采购员就能快速判断这次调价是否合理。第二条引入流程引擎。目前的审批流是硬编码的状态机遇到加急采购、变更申请、退换货这些特殊流程时代码改起来麻烦。后期如果流程复杂度上来了可以嵌入简单的流程引擎把审批节点配置化由管理员在界面上拖拽式设定。第三条做供应商协同。给供应商开一个独立的登录窗口能自助查看采购订单、在线确认交期、打印送货单。这个功能砍掉了采购员打电话催交期和手工录入送货单的重复劳动是肉眼可见提效的一种做法。当然这块要评估好你合作的供应商们是否愿意配合使用不然做了个寂寞。第四条对接企业微信或钉钉。审批消息直接推送到管理者的手机上在移动端点提交体验会比专门做App轻量得多。微信小程序或者企业微信里的H5应用成本可控且推广阻力小。技术层面的架构在第一版就预留了扩展位接口都是RESTful风格模块边界清晰后面要加模块不会伤筋动骨。这就是当初坚持前后端分离和模块化设计带来的红利。最后再分享一条我自己体会最深的东西任何系统工具永远排在第二位数据规范和业务流程才是第一位。很多采购项目失败不是程序员不会写代码而是根本没有梳理清楚采购流程就开始埋头建表。花一周时间跟采购员、仓管、财务聊清楚需求把单据流转、字段含义、审批边界这些事定义清楚写代码的时间只会缩短不会拉长。系统上线后定期回头审视一遍数据质量和使用反馈持续微调比憋一个大版本硬切换明智得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询