ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Vue+Node.js全栈开发:滑雪场雪具租赁管理系统实战解析

Vue+Node.js全栈开发:滑雪场雪具租赁管理系统实战解析 做滑雪场器材雪具租赁管理系统这个项目是我第一次完整走完一套 Vue Node.js Element UI 前后端分离业务系统。当时接这个需求的时候对方雪场还靠纸质单据管雪具一到节假日高峰期柜台前排长队还器材的时候经常出现数量对不上、雪板损坏责任扯不清的情况。整个系统做完核心价值其实就一句话把雪具从入库、出租、归还到结算的全生命周期管起来每一副雪板、每一双雪鞋现在在哪、什么状态在系统里一眼就能看清楚。这篇博文会把项目的设计思路、关键接口、前端页面实现和踩坑过程完整梳理一遍给准备做管理系统或者想了解 Vue Node.js 全栈开发的朋友一份能直接参考的实战记录。1. 为什么滑雪场需要一套独立的雪具租赁系统1.1 雪具租赁业务的原生痛点滑雪场器材租赁和普通商品租借不一样它的业务复杂度体现在几个很具体的地方。第一器材种类和规格组合非常多。单板、双板、雪鞋、雪杖、头盔、护目镜、护臀垫、护膝每一种还有品牌、尺码、左右脚、固定器型号这些维度。雪鞋从 36 码到 46 码双板长度从 130cm 到 180cm单板还有不同硬度、不同板型。如果用 Excel 表格管光维护这份器材档案就够一个人忙的。一旦某个尺码的库存不准确顾客到店发现没鞋穿体验直接崩。第二租赁状态流转快且并发量集中。滑雪场的高峰期非常集中周末和节假日早上开园那两小时同一时间可能有几百个顾客同时涌入。传统人工登记模式下一单单录入柜员根本忙不过来。更麻烦的是归还环节高峰期雪具堆成山哪副雪板是谁租的、有没有损坏、有没有超时全靠人脑记几乎不可能漏登记、错登记的情况特别多。第三押金和费用结算容易产生纠纷。滑雪器材价值不低一套像样的雪板加雪鞋可能上万块所以租借必须收押金。按件收还是按单收超时怎么计费器材损坏了扣多少、怎么记录证据这些规则如果不在系统里固化成流程全靠店员临场判断最后就会变成顾客和商家各说各话很难收场。所以这套系统的核心需求可以拆成四块器材库存管理、租赁订单流程、押金与费用结算、基础报表统计。想清楚这四块后面的表结构和接口设计就顺了。1.2 技术选型为什么是 Vue Node.js Element UI项目立项的时候也考虑过其他方案比如 Spring Boot Vue或者纯 jQuery 服务端渲染。最后确定 Vue 2 Node.js Element UI是从团队情况、业务规模和开发效率三个角度权衡的结果。先说业务规模。滑雪场租赁系统属于典型的中小型业务系统并发量不大但业务规则复杂、页面多、权限角色不少。这种系统不需要上来就上微服务、上消息队列一个 Node.js 单服务完全能扛住。Node.js 在处理这种 IO 密集、增删改查为主的业务上开发效率非常高前后端还统一用 JavaScript团队成员不需要在两套语言之间切换上下文。再说前端。管理后台类系统的页面形态非常固定左侧菜单、顶部导航、中间内容区放表格和表单。Element UI 的 table、form、dialog、select 这些组件基本都是为这类后台场景设计的能省掉大量重复的样式和交互工作。Vue 的响应式数据绑定和组件化开发在维护表格状态、处理表单联动这些场景下比 jQuery 时代舒服太多了。当然如果你面对的是那种需要高并发、强一致性的核心交易系统或者公司强制统一 Java 技术栈那选 Spring Boot 更合理。但就这个滑雪场项目的体量来说Vue Node.js Element UI 是最务实的选择。这里也提醒一句技术选型永远先看业务场景不是越重越好。2. 数据模型设计把雪具库存和订单状态想清楚2.1 核心表结构设计数据模型是整个系统的地基。我第一版设计的时候是参考电商系统的模式把器材当成普通商品来做结果发现根本不行。租赁和买卖最大的区别在于买卖是一次性转移所有权租赁必须追踪每一次借出、归还的过程而且同一件器材会被反复出租。所以我把核心表设计成四张equipment器材档案、customer顾客档案、rental_order租赁订单主表、order_item订单明细表。器材表的设计重点是状态和规格字段CREATE TABLE equipment ( id int NOT NULL AUTO_INCREMENT, equipment_no varchar(32) NOT NULL COMMENT 器材唯一编号, name varchar(64) NOT NULL COMMENT 器材名称, category varchar(32) NOT NULL COMMENT 分类snowboard/ski/shoe/pole/helmet/protector, brand varchar(64) DEFAULT NULL COMMENT 品牌, size varchar(32) DEFAULT NULL COMMENT 尺码或规格, status tinyint NOT NULL DEFAULT 1 COMMENT 1-在库 2-出租中 3-维修中 4-报废, daily_price decimal(10,2) NOT NULL COMMENT 日租金, deposit_price decimal(10,2) NOT NULL COMMENT 押金金额, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_equipment_no (equipment_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT雪具器材表;注意equipment_no加唯一索引。这个编号是每件器材的身份证号门店盘点、归还要扫码或者手动输入就靠这个编号定位。实际业务里一件雪板是单独管理的不能简单用库存数量表示因为每一件都有独立状态和出租历史。订单主表和明细表拆开是为了支持一个订单租多种器材比如顾客一次租了双板、雪鞋、雪杖、头盔四样。订单主表存顾客信息、押金总额、订单状态明细表存每一件器材的具体租赁信息CREATE TABLE rental_order ( id int NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号, customer_id int NOT NULL COMMENT 顾客ID, deposit_total decimal(10,2) NOT NULL COMMENT 应收押金总额, deposit_paid decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 已收押金, rent_start datetime NOT NULL COMMENT 预计开始时间, rent_end datetime NOT NULL COMMENT 预计归还时间, actual_return datetime DEFAULT NULL COMMENT 实际归还时间, total_amount decimal(10,2) DEFAULT 0.00 COMMENT 租金总额, penalty_amount decimal(10,2) DEFAULT 0.00 COMMENT 超时/损坏费用, status tinyint NOT NULL DEFAULT 1 COMMENT 1-待取件 2-租赁中 3-已归还 4-已取消, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_customer_id (customer_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT租赁订单表;CREATE TABLE order_item ( id int NOT NULL AUTO_INCREMENT, order_id int NOT NULL, equipment_id int NOT NULL, quantity int NOT NULL DEFAULT 1, unit_price decimal(10,2) NOT NULL COMMENT 下单时单价快照, return_status tinyint DEFAULT 0 COMMENT 0-未归还 1-完好归还 2-损坏 3-丢失, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;order_item里的unit_price是设计上的一个重要细节。它为单价快照而不是关联器材表的实时价格。为什么不直接取equipment.daily_price因为租金价格可能会调整如果归还结算的时候价格已经变了按哪个价格算快照的意义就是锁定下单那一刻的价格避免后续价格变动影响历史订单这是做租赁系统一个很容易忽略的点。2.2 雪具状态流转与订单状态机状态机是租赁后管理系统和普通商品库存系统的本质区别。我一开始没设计好用了两个互相独立的字段表示器材和订单状态结果业务代码里全是 if-else逻辑乱得没法维护。后来重新梳理了一套状态流转规则。器材状态只有四个在库、出租中、维修中、报废。在库器材可被新订单选择。出租中器材已被订单占用从下单那一刻就改状态防止同一件器材被重复下单。维修中归还检查发现损坏进入维修流程维修完成恢复在库。报废损坏严重无法修复或者达到使用寿命上限直接标记报废。订单状态四个待取件、租赁中、已归还、已取消。待取件顾客已下单、已交押金但还没从柜台取走器材。这时候如果顾客反悔可以取消订单器材状态从出租中恢复为在库。租赁中顾客已经取走器材这时候订单不可随意取消。已归还器材已还、费用已结清、押金已退订单走完整个生命周期。已取消待取件状态下取消或超时未取自动取消不涉及费用结算。这套状态机明确之后后端接口的流转就非常清晰创建订单时把器材从在库改成出租中归还结算时把器材从出租中改成在库或维修中。任何一步都只能从当前状态迁移到合法的下一个状态非法迁移直接抛异常。这是整个系统不出逻辑混乱的关键。3. 后端接口开发Node.js 分层与关键流程实现3.1 项目目录结构与分层思路后端用的是 Express Sequelize MySQL。我见过很多 Node.js 项目把所有逻辑堆在app.js里一上来几十个路由挤在一起改一个功能要翻半天。这个项目我采用了标准的 MVC 分层目录结构如下server/ ├── app.js # 入口文件 ├── config/ │ └── db.js # 数据库连接配置 ├── models/ │ ├── index.js # Sequelize 实例 │ ├── equipment.js # 器材模型 │ ├── customer.js # 顾客模型 │ └── rentalOrder.js # 订单模型 ├── controllers/ │ ├── equipmentController.js │ ├── orderController.js │ └── customerController.js ├── routes/ │ ├── equipment.js │ ├── order.js │ └── customer.js └── middlewares/ └── auth.js # JWT 登录鉴权中间件分层的好处是职责单一。routes 只做 URL 映射和参数校验controllers 处理业务逻辑models 只负责数据访问。比如创建订单这个接口routes 里校验参数是否齐全controller 里处理库存检查、事务提交、状态流转models 里的RentalOrder.create()负责真正的数据库写入。这样以后要加一个预约接口只需要在 controller 里加方法、在 routes 里加一行路由不会动到其他代码。中间件我用了 JWT 做登录态管理。虽然这个小系统的用户量不大但直接裸奔也不合适。员工登录后拿到 token请求时在Authorization头带上鉴权中间件解析通过后才放行。这里有个小细节解析 token 失败要返回 401 而不是 500前端才能正确跳转登录页。3.2 租赁下单的完整链路事务与库存扣减创建租赁订单是这个系统里逻辑最重的接口它同时操作三张表写订单主表、写订单明细表、更新器材状态。任何一个步骤失败数据都会不一致。所以必须用数据库事务。这里我直接贴核心实现// controllers/orderController.js const db require(../models); const { Equipment, RentalOrder, OrderItem } db; exports.createOrder async (req, res) { const { customerId, items, rentStart, rentEnd } req.body; // items: [{ equipmentId, quantity }] const t await db.sequelize.transaction(); try { // 1. 生成订单号 const orderNo R Date.now() Math.floor(Math.random() * 1000); // 2. 计算押金总额和租金总额 let depositTotal 0; let amountTotal 0; const equipmentList await Equipment.findAll({ where: { id: items.map(i i.equipmentId) }, transaction: t }); const equipmentMap {}; equipmentList.forEach(e { equipmentMap[e.id] e; }); for (const item of items) { const eq equipmentMap[item.equipmentId]; if (!eq || eq.status ! 1) { throw new Error(器材 ${item.equipmentId} 不可租赁); } // 同一订单里可能租多件同类器材这里累加计算 depositTotal parseFloat(eq.deposit_price) * item.quantity; amountTotal parseFloat(eq.daily_price) * item.quantity; } // 3. 创建订单主表 const order await RentalOrder.create({ order_no: orderNo, customer_id: customerId, deposit_total: depositTotal, rent_start: rentStart, rent_end: rentEnd, total_amount: amountTotal, status: 1 // 待取件 }, { transaction: t }); // 4. 写入订单明细并更新器材状态 for (const item of items) { await OrderItem.create({ order_id: order.id, equipment_id: item.equipmentId, quantity: item.quantity, unit_price: equipmentMap[item.equipmentId].daily_price }, { transaction: t }); // 器材状态从在库改为出租中 const [updated] await Equipment.update( { status: 2 }, { where: { id: item.equipmentId, status: 1 }, transaction: t } ); if (updated 0) { throw new Error(器材 ${item.equipmentId} 已被占用请刷新后重试); } } await t.commit(); res.json({ code: 0, data: { orderId: order.id, order_no: orderNo } }); } catch (e) { await t.rollback(); res.status(400).json({ code: 1, message: e.message }); } };事务里最核心的是Equipment.update的where条件里带上了status: 1。这一步是乐观锁的思路只有在器材状态确实为在库时才能成功更新如果已经被人抢单改成出租中了这次 update 的影响行数是 0直接抛异常回滚。这样在高并发下单的情况下也不会出现同一副雪板被两个订单同时租走的问题。可能有人会觉得单量也没那么大有必要这么严谨吗我的看法是库存类系统最怕脏数据宁可前期多写几行代码也不要等跑一个雪季之后发现账实不符再回头填坑。3.3 归还结算押金退还是最容易出bug的地方归还接口的复杂度不输下单。归还的时候收银员要做三件事核对器材是否完好、计算租赁费用含超时、结算押金退还。我的实现思路是归还时先从order_item表查出这个订单下的所有器材逐一标记归还状态完好 / 损坏 / 丢失。然后根据标的归还状态计算费用正常租金下单时快照的unit_price× 租借天数。如果超时了超出部分按小时计费每小时费用是日租金的 1/8不满一小时按一小时算。损坏扣款damage_fee根据器材维修成本来定系统里做一个损坏登记记录损坏部位和扣款金额。丢失赔偿按器材原价或双方协商价赔偿。押金退还deposit_paid - 正常租金 - 超时费 - 损坏扣款多退少补。费用计算是典型的需要把规则写清楚的场景。我前后改过三次最开始把押金能不能抵扣租金这个问题没想清楚导致结算结果很混乱。后来明确了规则押金是押金租金是租金两者在账目上分开展示结算时押金用于抵扣应扣费用后多退少补这个逻辑就清晰了。归还流程里的另一个坑是超时时间怎么算。有人直接用实际归还时间减去预计归还时间算出来一个负数就按 0 处理。但滑雪场经常有顾客提前还这种情况不应该有负数。更稳妥的方式是const rentMs Math.max(0, actualReturn.getTime() - rentEnd.getTime()); const overdueHours Math.ceil(rentMs / (1000 * 60 * 60));用Math.max(0, ...)把提前归还的负数情况过滤掉再用Math.ceil向上取整保证超时 1 分钟也算 1 小时。这是业务规则上的一个小细节但客户非常在意因为直接关系收入。4. 前端页面实现Vue Element UI 核心页面拆解4.1 器材管理页表格、搜索、分页的组合前端的核心页面按业务拆成三个器材管理、租赁下单、归还结算。器材管理页是最典型的 Element UI 后台页面——顶部搜索区中间表格区底部卡片式分页器。这个页面开发时有一个经验搜索条件不要在点击搜索按钮后才赋值给表格的查询参数而是通过keyup.enter绑定了回车事件输入完直接回车就能搜索。同时分页组件的current-change和size-change事件都会触发重新查询函数。这样整个页面的交互非常顺滑不用每次都鼠标点搜索按钮。还有一个细节是器材列表的筛选。雪具的尺码和品牌筛选我用的是el-select下拉选择选项从接口返回的聚合数据里取而不是写死。写死的后果是系统里新增了一个品牌前端筛选下拉里没有用户就以为系统漏数据了。这个排查起来很费劲。器材表格的操作列里放了编辑和状态变更两个按钮。编辑弹窗用el-dialog包裹一个el-form打开弹窗时用Object.assign(this.form, row)的方式回填数据而不是直接this.form row避免直接修改表格里正在展示的响应式数据造成表格闪动。这个坑也是实际开发中遇到的。4.2 租赁下单页表单联动与库存校验租赁下单页是整个系统里交互最复杂的页面。顾客来到柜台店员先录入或选择顾客信息然后选择要租的器材系统实时计算押金和租金合计。器材选择这里我没有用简单的下拉框而是做了一个弹窗式的器材选择表格。表格里只展示状态为在库的器材每个器材后面有一个数量输入框店员录入要租的数量后会做前端校验数量不能大于库存。这里有一个 Vue 的经典坑el-table里用变量控制quantity输入框但如果你直接this.items[index].quantity val有时候视图不会更新。这是因为在 table 渲染过程中给某个索引位置新增了对象属性没有触发响应式更新。解决办法是this.$set(this.items, index, { ...this.items[index], quantity: val });或者一开始就把quantity字段初始化好而不是等用户输入时才动态加属性。表单校验用的el-form的rules。这里有个经验租金和押金是系统根据器材单价和数量自动算出来的不需要用户填写但这部分字段也要放到 form 里计算过程用 Vue 的 computed 属性完成computed: { totalDeposit() { return this.selectedItems.reduce((sum, item) { return sum item.deposit_price * (item.quantity || 0); }, 0); } }这样只要器材选择变化押金和租金自动更新而且因为是 computed 依赖响应式数据不需要手动调用更新方法也不会有异步时序问题。4.3 报表统计页Element UI 固定列变透明的坑与修复统计报表页我用了el-table展示租赁数据左侧几列信息固定右侧操作固定中间可以横向滚动。结果遇到了 Element UI 的一个经典 bug固定列在滚动时变透明。现象是这样的表格设置了fixedright的列在数据量比较大、表格出现横向滚动条之后固定列的内容偶尔会闪烁、变透明甚至一整列空白。这个问题在el-table加了height属性时更容易复现。查了 Element UI 的 issue 和社区方案之后我总结出几个有效的修复思路第一个思路是给el-table-column加上:render-header或者给表格设置:keytableKey在数据更新后强制重新渲染表格。这个方法简单粗暴但能解决一部分因为渲染时机导致的固定列错位问题。第二个思路也是我最后稳定使用的方案不要给el-table同时使用height100%这种百分比高度和fixed列。Element UI 的 fixed 列实现原理是复制一份表格内容做覆盖层当高度计算不准确时覆盖层就会出现透明和错位。我把表格包围在一个设置了固定高度的div容器里然后el-table的height绑定容器高度template div classtable-wrapper styleheight: 60vh el-table :datareportList :heighttableHeight el-table-column propdate label日期 fixedleft / !-- 其他列 -- el-table-column propoperator label操作员 fixedright / /el-table /div /template script export default { data() { return { tableHeight: 400 }; }, mounted() { this.tableHeight this.$refs.wrapper?.clientHeight || 400; } } /script第三个思路是修改 CSS给.el-table__fixed-right设置height: 100% !important;同时把.el-table__fixed-body-wrapper的滚动条隐藏。这个方案很多博客提到但我在 Element UI 2.15 版本实测部分场景有效部分场景反而会遮挡内容所以优先用第二种基于容器高度计算的方式。这个问题困扰了我大概半天最后发现本质是 Element UI 的固定列通过绝对定位实现必须依赖父容器的高度计算。很多写的解决方案其实都是绕过真正稳妥的还是让表格高度成为明确的可计算数值而不是依赖百分比这种模糊值。5. 开发与部署阶段踩过的三个典型环境坑5.1 PowerShell 执行策略导致 npm 命令无法运行这个坑估计所有在 Windows 上做 Node.js 开发的人都遇到过。新克隆项目后在 PowerShell 里执行npm install直接报npm : 无法加载文件 D:\Program Files\nodejs\npm.ps1 因为在此系统上禁止运行脚本。 有关详细信息请参阅 about_Execution_Policies。根因是 Windows PowerShell 的默认执行策略是Restricted不允许运行任何.ps1脚本文件。而npm.ps1是 npm 包管理器在 PowerShell 下的包装脚本自然也被拦住了。解决办法有两种。一种是临时绕过直接在 cmd 或 Git Bash 里执行 npm 命令而不是在 PowerShell 里。但这种方法在需要跑 npm script 的时候还是别扭。更根本的办法是修改 PowerShell 执行策略。以管理员身份打开 PowerShell执行Set-ExecutionPolicy RemoteSignedRemoteSigned表示本地创建的脚本可以运行从网络下载的脚本需要数字签名。这是个人开发环境下比较推荐的执行策略。执行后会问你是否确认输入Y回车即可。需要注意这个坑只在 Windows PowerShell 下出现cmd 和 Git Bash 不会遇到。项目组里如果同时有 Windows 和 macOS 的同事建议在 README 里写清楚这个步骤不然新同事第一次拉代码必然卡在这。5.2 Node.js 版本差异导致的依赖安装失败第二坑是 Node.js 版本不一致。项目开发前我用的是 Node 14另一个同事用的 Node 18结果他拉代码npm install的时候node-sass编译报错一查发现 node-sass 和 Node 版本不兼容。node-sass 是一个需要用 C 插件编译原生模块的包它和 Node 的版本绑定非常严格。Node 18 需要 node-sass 7.0 以上而项目的 package.json 里锁的是 4.14自然编不过。后来我做了两个调整。第一项目统一用sass替换node-sass。dart-sass是纯 JavaScript 实现的不依赖原生编译兼容性和安装速度都更好。第二团队统一用nvm管理 Node 版本项目根目录放一个.nvmrc文件14.21.3这样任何人进入项目目录执行nvm use就能自动切到正确的版本。如果你还没用 nvm-windows强烈建议装一个Node 版本切换比你想象的频繁得多。5.3 前后端联调时的跨域处理代理方案跨域是前后端分离项目绕不开的话题。我一开始在开发环境用的是后端 cors 中间件所有接口都放开跨域。这种方式简单但有个问题生产环境如果前后端部署在不同域名cors 配置严格了容易出幺蛾子配置松了又有安全风险。更推荐的做法是开发环境用前端代理生产环境用 Nginx 反向代理。Vue CLI 项目在vue.config.js里这样配module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:3000, changeOrigin: true, pathRewrite: { ^/api: /api } } } } };这样前端代码里所有请求都写/api/xxx开发服务器会把请求代理到后端的 3000 端口浏览器看到的请求是同源的不存在跨域。生产环境则把构建后的 dist 目录交给 Nginx再在 Nginx 配置里把/api开头的请求 reverse proxy 到 Node 服务。这套组合拳下来后端不需要引入 cors 包前端的请求地址也统一联调时唯一的坑就是确保本地后端端口一致。如果你想快速验证接口连通性也可以先用 Postman 或者 Apifox接口通了再联调页面省去很多两头排查的时间。6. 这套系统的可扩展方向与实际维护心得6.1 从小程序端到智能硬件的扩展这个系统跑完第一个雪季之后客户提了三个高频需求正好也是这类系统的常见扩展方向。第一个是能不能让顾客自己扫码下单。现在流程是顾客到柜台排队租器材如果能做成小程序扫码绑定客户档案后自选器材、线上付押金高峰期排队压力能缓解很多。前端小程序可以复用现有接口后端只需要增加一个预约取件接口把订单状态从待取件改成租赁中时需要店员在柜台扫码确认。第二个是能不能搞自动租借柜。类似快递柜雪具放在智能柜里顾客凭取件码取归还时放回对应柜门。这种方案需要对接硬件厂商的 API但核心的订单状态机和库存管理逻辑不变系统里只需要增加一个locker_id字段关联智能柜。第三个是能不能把教学视频也放进来。滑雪场有些顾客是新手取完器材不会穿板。可以在小程序或 H5 页面里嵌入教学视频。这里会涉及视频播放技术方案目前主流是 HTTP-FLV 转 HLS前端用hls.js或成熟的播放器库播放 m3u8 格式视频。6.2 真实维护中积累的几点心得项目上线不代表结束真正长经验的是后续维护的过程。这里分享几个我实际维护中积累的体会。第一业务人员提的小改动往往不小。刚开始雪场老板说加一个雪具状态筛选呗我以为就加个下拉框。做完才知道他要的是今日在库 / 在租 / 维修中 / 报废四个维度的实时统计卡片还要能点击卡片跳转到对应的筛选列表。这类需求核心不在 UI而在数据统计口径。所以接到需求先问清楚数据从哪里来、统计口径是什么、给谁看比直接动手写代码重要得多。第二日志必须从一开始就留好。有这个系统之前雪场盘点靠人肉数数有系统之后盘点变成核对系统记录和实物是否一致。一旦不一致就需要看操作日志。我早期没做操作日志表出了几次顾客说没租过这个板但系统里有订单的纠纷查起来非常被动。后来补充了operation_log表记录谁在什么时间对哪个订单做了什么操作这类纠纷就很容易追溯了。第三数据库备份策略不能马虎。滑雪场旺季一天的订单量能抵淡季一个月数据一旦丢失恢复成本极高。我在服务器上每天凌晨 2 点用 mysqldump 做全量备份保留最近 7 天的备份文件另外每周做一次异地备份。这个习惯帮过我好几次其中有次线上误操作导致一张表被清空靠着备份恢复了数据。6.3 如果你也想做类似的租赁管理系统最后给准备做类似系统的朋友一个建议不要一上来就写代码先把业务流程图和数据状态图画出来。可以先用纸笔或者简单的画图工具把顾客从进门到出门这个完整流程走一遍标注清楚哪个环节需要哪个系统功能、产生什么数据。这套系统真正费时间的不是编码而是把业务规则想明白。如果我重新做一遍这个项目会在数据结构层面做得更通用一些比如把器材规格和价格做成可配置项而不是写死在字段里。这样雪场以后新增品类、调整价格时不需要改代码。同时会把权限系统做得更细区分店长、收银员、库管三种角色的操作范围。这些都是一开始被忽略了、后续想补就要动不少代码的地方。滑雪场的雪季就那么三四个月系统稳定运行、账目清晰是整个雪季顺畅运营的底座。希望这篇项目拆解能帮你少踩一些我在开发过程中踩过的坑。
返回列表