
做一个小型仓库进销存管理系统标题听起来像是入门练手项目但我真把这套东西从需求梳理到上线维护完整走了一遍之后最直接的感受是规模小不等于简单。商品档案、采购入库、销售出库、库存盘点、补货预警、数据统计每一个环节背后都有一堆现实业务约束。账面库存和实际库存对不上、出库时发现库存是负数、导入 Excel 时规格乱掉这些问题如果不在一开始把数据结构和业务流程想清楚后面全是要加班擦的坑。这套基于 Vue 的小型仓库进销存管理系统主要面向还在用手工台账或 Excel 管理库存的中小仓库。系统把“进、销、存”三个动作串成闭环采购入库时更新可用库存销售出库时扣减库存所有数量变化都留流水记录最终通过库存表和统计看板把账面情况实时反映出来。整套系统可以独立部署也可以作为现有后台管理系统的一个子模块嵌进去。这篇文章我会按“设计与实现”的过程来写先讲需求拆解和技术选型再讲数据库表结构然后是后端接口和业务逻辑接着是 Vue 前端页面的具体实现最后把部署上线和日常维护容易踩的坑一并整理出来。适合三类人看准备自己动手做仓库管理系统的开发者、想找个真实业务场景练手的 Vue 初学者以及要给客户交付小型管理系统但不想只交一堆 Excel 宏的初级项目负责人。1. 需求拆解和技术选型先想清楚再做1.1 小型仓库在什么情况下需要一套管理系统先别急着写代码。我接触过的几个需要这类系统的仓库业务规模都不大一般就是两三个人负责采购、销售和仓管仓库面积也不大SKU 数量从几十到三四百个不等。真正让他们头疼的不是“东西多到管不过来”而是下面几个具体场景商品种类多了以后Excel 里一行一个商品规格、单位、采购价、销售价全挤在几列里录入不统一。两个人同时改同一张库存表一人改了出货量另一人不知道账就对不上了。月底盘库发现实物数量和账面数量差了一大截又说不清是哪几笔出入库造成的。老板问“现在有多少库存”“最近一周进了多少货、出了多少货”的时候临时统计要花很长时间还未必准确。所以这套系统的核心目标不是做得多大而是把“商品档案→入库→库存变化→出库→库存变化→统计报表”这个最小闭环跑通并且保证每一步都有记录、可追溯。在需求阶段我最强调的一件事是不要一开始就想着做权限管理、多仓库、审批流这些扩展功能。小型系统最怕功能堆砌重点把出入库流程和库存一致性做扎实就已经解决了用户 80% 的痛点。权限可以只做管理员和操作员两种角色多仓库也完全可以通过在商品编码上做前缀区分留到后续再演进。1.2 技术方案对比与选型结论前端框架我选了 Vue 3搭配 Vite 构建、Vue Router 做路由、Pinia 做状态管理UI 组件库选了一款成熟的中后台组件库图表使用 ECharts。后端选用 Node.js 的 Express数据库用 MySQL。选型理由我逐一说一下这部分是很多新人最容易忽略的。Vue 3 Composition API 的写法对中后台系统非常合适。一个页面往往包含表单、表格、弹窗、分页多个状态用 setup 函数可以把同一块业务的逻辑集中在一处而不是像 Vue 2 的 Options API 那样数据在 data、方法在 methods、计算属性在 computed 里来回跳。而且 Vue 3 对 TypeScript 支持更好虽然这个小系统我用的是 JavaScript但后续如果要加人维护迁移成本会低很多。为什么不选 React也不是不能选而是考虑到这套系统的目标用户多数在中文技术社区找资料Vue 的生态资料更集中组件库也更贴合同类管理系统页面风格。这套系统本身没有复杂的跨端需求Vue 已经完全够用。状态管理用 Pinia 而不是 Vuex。Pinia 的 API 更简单去掉了 mutations 这个概念直接在 store 里定义 state、getters、actions写起来更接近普通对象操作。对于进销存这种需要频繁在页面之间共享“当前用户信息”“最近选择的供应商列表”这类数据的场景Pinia 轻量且好调试。后端用 Node.js Express是因为我自己熟悉这套链路前端写 Vue后端写 Node全栈都用 JavaScript语言统一小项目就少一套维护成本。如果你更熟悉 Java Spring Boot那后端完全可以用 Java 重写本文的核心在数据结构和业务流程这部分逻辑对语言不敏感。数据库选了 MySQL关键是事务能力。入库、出库一定会涉及“写单据 更新库存 写流水”三个操作必须保证要么全部成功、要么全部回滚这个要求在文件型数据库上实现起来要多费不少劲。MySQL 用 InnoDB 引擎开启事务后就能很自然地把这些写操作用事务包起来。2. 数据库设计先把业务关系理清楚2.1 核心实体与业务闭环数据库是这套系统的地基。我设计表结构时先画了业务闭环再反推需要哪些表和字段。整个过程可以这样描述采购人员到货后录入入库单入库单里有多条商品明细每一条明细对应一个商品 SKU入库同时增加库存表中对应商品的库存数量并且往库存流水表里写一条“入库”记录销售人员下单发货时创建出库单和明细库存表中对应商品数量扣减流水表里写一条“出库”记录整体库存表只用来冗余汇总当前数量流水表才是判断数据是否正确的依据。按照这个闭环最少需要这几张表商品表goods存每个 SKU 的基础档案信息。供应商表supplier入库单关联的往来方。入库单表inbound_order 入库单明细表inbound_item一张主表对应多张明细表记录每次采购入库。出库单表outbound_order 出库单明细表outbound_item记录每次销售出库。库存表stock保存每个商品当前最新的库存数量这是冗余字段但它能直接支撑列表展示和查询不用每次动态汇总流水。库存流水表stock_flow记录每一笔库存变化的明细包括变化前数量、变化后数量和来源单据号。用户表user存登录账号和角色系统虽然小但至少要有谁能操作、操作人是谁这个审计能力。2.2 商品、库存、流水三张表的设计商品表是整个系统的数据基础我最初的版本漏掉了两个字段后面又补上了现在看看设计得非常必要。第一是商品编码这个字段必须唯一且一旦创建尽量不要修改因为后面所有单据明细都通过商品编码关联到历史记录第二是预警库存量用于在列表页做低库存提醒。商品表的核心字段如下字段名类型说明idint自增主键goods_codevarchar(32)商品编码唯一索引namevarchar(100)商品名称categoryvarchar(50)分类比如食品、日用品、电子配件unitvarchar(10)单位件、箱、个specvarchar(100)规格描述比如 500ml/瓶purchase_pricedecimal(10,2)采购参考价sale_pricedecimal(10,2)销售参考价warning_stockint预警库存量statustinyint1 启用0 禁用created_atdatetime创建时间这里的 decimal(10,2) 是故意为之价格不能用 float原因在后面“金额精度”部分单独说。库存表结构简单但有一处需要提前想好商品表和库存表是一对一关系还是先建一条记录再更新我建议在创建商品时同步初始化一条库存记录数量为 0这样后面入库出库时永远用 UPDATE 操作不需要每次判断“记录存不存在”能减少很多分支逻辑。库存表字段id、goods_id、quantity、updated_at其中 goods_id 唯一索引。流水表是这套系统排查数据问题的灵魂。我设计它的核心思路是单看库存表能知道“现在有多少”但永远不知道“为什么变成这个数”流水表就是补齐这个“为什么”。每次入库、出库、手动盘盈盘亏调整都必须写一条流水。流水表字段字段名类型说明idint自增主键goods_idint商品 idchange_typevarchar(20)inbound / outbound / adjust / initchange_qtyint变动数量入库为正出库为负before_qtyint变动前库存after_qtyint变动后库存source_novarchar(32)来源单号比如入库单号created_atdatetime创建时间我在 before_qty 和 after_qty 上吃过亏最初版本这两个字段没加结果有一天库存表被人手动 UPDATE 改错了一个数字我根本查不出来是从哪笔操作开始的。加了变动前和变动后数量以后只要对比相邻流水号的 after_qty 和下一行 before_qty 是否相等就能快速定位断档。2.3 订单表结构和其他辅助表入库单主表字段包括入库单号、供应商 id、操作员 id、备注、创建时间。明细表包括入库单 id、商品 id、数量、采购单价、金额小计。一个容易忽略的细节是入库单号不要直接用自增 id 当单号对外展示。一是用户习惯看到“RK20250101001”这种有业务含义的单号二是在出库单里我们还要引用入库单号做来源追溯带前缀的单号在 Excel 对账时更好辨认。我的单号生成规则是业务前缀 日期 三位流水号比如 RK20250101001当天流水号满了就自动重置为 001同时用数据库唯一索引保证不重复生成。出库单表结构和入库单类似只是把供应商换成了客户名称。有朋友问为什么不是客户表小系统我建议先用一个 varchar 字段存客户名称不要为了扩展性一开始就建客户主数据表。等以后有固定客户再单独拆客户表也不迟否则前期录入成本会拖慢整个系统的上手速度。另外我设计了一张 system_config 表就两个字段config_key、config_value用来存系统名称、默认供应商之类的配置项。进销存系统看起来是业务系统其实也有不少界面参数要改把所有硬编码的前端展示文本收拢到这张表后期改起来不用重新发版。3. 后端业务逻辑入库、出库、库存更新的实现3.1 接口清单和项目分层这套系统的后端接口数量控制在 15 个以内按模块划分为认证、商品、入库、出库、库存、统计六个部分。认证相关登录、获取当前用户信息、退出登录商品模块商品列表分页 搜索、新增商品、编辑商品、删除商品入库模块创建入库单、入库单列表查询出库模块创建出库单、出库单列表查询库存模块库存列表、盘点调整统计模块首页看板数据。对于这种规模的系统我不会把后端分层搞得太重Controller、Service、Model 三层足够了。Controller 只做参数校验和封装返回格式Service 层处理核心业务逻辑Model 层通过数据库访问层操作表数据。重点业务逻辑全部集中在 Service 层好处是未来如果前端换掉api 层不用动。3.2 入库单创建的完整实现入库是库存增加的唯一起点在事务处理上要非常慎重。我用伪代码把入库单创建的完整过程拆解一下这部分是整个后端最核心的地方。async function createInbound(req, res) { const { supplierId, items, remark } req.body; // 1. 参数校验items 不能为空数量必须大于 0单价不能是负数 // 2. 生成入库单号例如 RK 日期 三位流水号 // 3. 开启数据库事务 return await db.transaction(async (tx) { // 4. 插入入库单主表记录 const orderId await tx.insert(inbound_order, { order_no, supplier_id: supplierId, operator_id: userId, remark }); // 5. 遍历明细插入入库单明细表 for (const item of items) { const amount Number((item.price * item.quantity).toFixed(2)); await tx.insert(inbound_item, { order_id: orderId, goods_id: item.goodsId, quantity: item.quantity, price: item.price, amount }); // 6. 更新库存表 await tx.execute( UPDATE stock SET quantity quantity ? WHERE goods_id ?, [item.quantity, item.goodsId] ); // 7. 查询当前库存写入流水表 const currentStock await tx.selectOne( SELECT quantity FROM stock WHERE goods_id ? FOR UPDATE, [item.goodsId] ); await tx.insert(stock_flow, { goods_id: item.goodsId, change_type: inbound, change_qty: item.quantity, before_qty: currentStock.quantity - item.quantity, after_qty: currentStock.quantity, source_no: orderNo }); } return { orderNo }; }); }这段逻辑里有三个关键点第一事务包装。入库单、入库明细、库存更新、流水写入这四件事必须在一个数据库事务里完成。如果只插入单据而库存更新失败或者库存更新成功但流水没写上库存数据就失真了。第二SELECT ... FOR UPDATE。库存更新后我立刻把这条库存记录锁住再查询避免并发情况下读到过期数量。虽然小型系统的并发量不大但这个习惯是必须的不然两个入库单同时操作同一个商品时流水里面的 before_qty 和 after_qty 会乱。第三金额字段用 toFixed(2) 保留两位但要注意这只是展示层面的技巧真正可靠的做法是前端传过来金额就按“分”为单位后端处理时也按整数运算最后再转成“元”展示。3.3 出库与库存扣减的防呆处理出库逻辑和入库在结构上是对称的但要额外做三件事检查库存是否充足、扣减库存时禁止出现负数、处理部分出库的特殊情况。检查库存是否充足最直观的做法是在事务开始前查询库存数量。但不要只查一次因为在“查询”和“扣减”之间别的请求可能已经把库存改了。正确做法是在 UPDATE 语句里写条件判断const result await tx.execute( UPDATE stock SET quantity quantity - ? WHERE goods_id ? AND quantity ?, [item.quantity, item.goodsId, item.quantity] ); if (result.affectedRows 0) { // 说明库存不足抛出业务异常整体回滚 throw new Error(商品库存不足); }这个写法利用数据库行锁保证原子性比“先 SELECT 再 UPDATE”更安全。affectedRows 为 0 表示没有一行满足条件也就是当前库存不够扣这时回滚整个出库单直接给前端返回“库存不足”的提示。部分出库的场景也要考虑比如客户下单 100 件仓库里只有 80 件业务上可能允许先发 80 件、剩余 20 件等有货再发。这种情况下我建议不要把“创建出库单”和“扣减库存”强绑定可以在出库单明细上增加一个“已发货数量”字段前端只允许用户对确定有货的部分做扣减操作。小型系统可以先不做预订单模式但不要把部分出库的需求完全忽视否则客户用一段时间后一定会提这个需求。4. 前端页面实现围绕 Vue 的组件化落地4.1 工程目录、路由与状态管理后端业务逻辑清楚之后前端实现其实就变成了“把这些数据用合适的页面展示出来并且操作起来顺手”的过程。我的 Vue 前端工程目录结构是这样的src/ api/ // 接口封装按模块拆文件 goods.js inbound.js outbound.js stock.js dashboard.js assets/ components/ // 公共组件 PageHeader.vue GoodsSelector.vue GoodsSkuTable.vue router/ index.js stores/ user.js app.js views/ login/ dashboard/ goods/ inbound/ outbound/ stock/ statistics/组件拆分的原则很直接一个页面里被重复使用的部分就抽成组件。我最开始写的时候入库单和出库单页面都用到了“商品明细编辑表格”表格里每一行要选择商品、填数量、填单价、自动计算金额这个组件我在入库和出库两个页面里各写了一遍后来统一抽成了 GoodsSkuTable改 bug 时只改一处节省了很多时间。路由设计我用了嵌套路由。整体布局是一个 Layout 组件内部包含侧边菜单和主内容区各个业务页面挂在 Layout 下面。路由配置里设置了一个前置守卫检查本地是否有 token没有就强制跳到登录页这个功能看起来简单但没有它系统就会变成“任何人打开页面都能看到所有数据”。4.2 通用列表页的实现套路商品管理页是最典型的列表页面顶部是搜索栏和新增按钮中间是表格底部是分页。我用代码片段展示一个列表页的核心逻辑你会看到 Vue 3 Composition API 在组织这类页面时非常规整。script setup import { ref, onMounted } from vue import { getGoodsList } from /api/goods const loading ref(false) const list ref([]) const total ref(0) const queryParams ref({ pageNum: 1, pageSize: 10, keyword: }) async function fetchList() { loading.value true try { const res await getGoodsList(queryParams.value) list.value res.data.list total.value res.data.total } finally { loading.value false } } function handleSearch() { queryParams.value.pageNum 1 fetchList() } function handlePageChange(page) { queryParams.value.pageNum page fetchList() } onMounted(fetchList) /script这个页面的关键点在于让搜索、分页、列表重新加载三个操作共用同一个 queryParams 对象和一个 fetchList 方法避免把逻辑写散。搜索按钮点击时把页码强制重置为 1否则会出现“搜索后停留在一个不存在的大页码”的问题。商品列表表格中商品编码列我会加一个等宽字体样式因为编码经常要跟人电话沟通等宽字体读起来不容易看错。状态列用 tag 标签展示“启用/禁用”库存数量列做条件样式库存为 0 显示红色小于预警库存显示橙色。这些样式判断直接在表格列渲染函数里写就可以了。4.3 入库单、出库单的明细编辑表单出入库单页面是整个系统里交互最复杂的部分因为要面对一个“动态表格”供应商或客户信息是表头商品明细是若干行每一行都要选商品、填数量、填价格还要在底部实时汇总总金额。我在这个页面上用了一个相对成熟的做法明细数据用一个响应式数组维护每一行是一个对象用户点击“添加行”时向数组 push 一个空对象删除行时从数组里 splice 掉对应位置。要注意的是Element Plus 的表格在动态添加行时必须给每一行绑定唯一的 key我直接使用自增的临时字段 rowKey避免 Vue 复用 DOM 导致输入框内容错乱。选择商品这一列我封装了一个商品选择器组件弹窗里支持搜索选中后把商品 id、编码、名称、参考价回填到当前行。这里有一个组件通信的细节子组件选中商品后不能直接修改 prop 对象必须通过 emit 事件把商品数据传给父组件由父组件更新当前行数据。有些新手会直接在子组件里改 prop运行起来偶尔能生效但一遇到复杂交互就开始出现数据更新不同步的诡异 bug。每行数据里数量和单价变化时金额自动重算。我用 watch 监听每一行的 quantity 和 price只要变化就重新计算 amount 字段。这里有个性能问题行数上百以后逐个监听会产生大量 watcher所以我在封装这个表格组件时实际上是在一个统一的 input 事件处理函数里做重算而不是给每一行单独建立 watch。4.4 库存看板与报表可视化库存看板是这套系统最能“让用户觉得钱没白花”的页面。我在首页集成了四个统计卡片商品总数、库存总价值、今日入库单数、今日出库单数下面接两个图表最近 7 天出入库趋势折线图、库存数量 Top 10 的条形图。图表我使用的是 EChartsVue 封装方式没有用第三方封装库而是自己写了一个简单的图表组件核心逻辑是组件初始化时创建 echarts 实例当传入的 option 发生变化时调用 setOption 更新组件卸载时销毁实例。import * as echarts from echarts const chartEl ref(null) let chartInstance null onMounted(() { chartInstance echarts.init(chartEl.value) chartInstance.setOption(props.option) }) watch(() props.option, (newOption) { chartInstance.setOption(newOption) }) onBeforeUnmount(() { chartInstance.dispose() })关键点有两个第一ECharts 实例必须在 mounted 之后创建因为此时 DOM 才真正渲染出来第二组件卸载时一定要调用 dispose 释放资源否则页面多次切换路由后会出现内存泄漏图表响应也会变得迟钝。仪表盘数据在后端查询时最近 7 天数据可以用一个日期分组查询实现前端只需要把查询结果转换成 ECharts 需要的格式。我建议这种统计接口单独做一张汇总查询不要在前端对明细数据做 reduce数据量大了以后浏览器会卡。5. 项目落地过程中的高频问题和避坑经验5.1 商品编码与 SKU 设计这个坑我差点踩进去却没完全避开。最初设计商品表时商品就是商品名称加个型号字段就完事了。结果客户在录入一款 T 恤时发现同一款 T 恤有红、白、黑三个颜色每个颜色还有 S、M、L 三个尺码如果他们按一个商品录入库存就会混在一起没法回答“黑色 M 码还有几件”。解决方案是引入 SKU 概念每一个颜色加尺码的组合都单独建一条商品记录商品编码按规则拼接比如 TSHIRT-BK-M。商品名称可以保持一致但编码、库存、价格全部独立。听起来是这个系统变复杂了但这是仓库管理的真实需求。实际操作中我在新增商品弹窗里增加了一个“批量生成 SKU”的辅助功能用户填好基础名称和规格维度后系统自动生成多条 SKU 记录省的让用户一条条输入。这个功能并不复杂后端接一个数组参数循环插入商品表但使用体验提升非常明显。5.2 金额精度和浮点运算问题进销存系统涉及大量金额计算JavaScript 的浮点数精度问题是必须面对的。0.1 加 0.2 在控制台跑一下就知道结果是 0.30000000000000004。如果直接把这种值存入数据库、再跟 Excel 对账很快就会发现差了几分钱。我的处理方式是两层配合数据库层面所有金额字段使用 DECIMAL(10,2)由数据库保证精度稳定后端与前端传参时金额统一以“分”为单位用整数传输需要展示成“元”时再除以 100 并用 toFixed(2) 格式化。这样把浮点运算降到最少比较容易避免“差一分钱”的问题。前端展示时我封装了一个 formatMoney 函数它接受以分为单位的整数返回带上千分位和小数点的字符串例如 12345 显示为 123.45。这个函数被用在订单金额列、库存总价值、报表汇总等多个地方统一来源避免各写各的格式化逻辑。5.3 分页、搜索和列表刷新分页搜索最常见的 bug 就是删除列表某一行后当前页码超出总页数上限导致表格出现空白。比如用户在第 3 页这一页只有 1 条数据把这 1 条删掉以后总页数从 3 变成 2但前端 queryParams.pageNum 还停留在 3请求过来自然查不到数据。我在删除接口拉取最新列表之后加了一步判断async function handleDelete(row) { await deleteGoods(row.id) const maxPage Math.max(1, Math.ceil((total.value - 1) / queryParams.value.pageSize)) if (queryParams.value.pageNum maxPage) { queryParams.value.pageNum maxPage } fetchList() }搜索防抖也是一个细节。商品列表页面我监听了 keyword 的变化但不会每次输入都发请求而是用了一个 300ms 的防抖函数。不然用户输入一个字请求一次中文输入法组合拼音时还会触发无效请求这个体验非常糟糕。5.4 库存一致性和数据回滚问题进销存系统上线后我建议开发一个“库存校验”脚本定期比对每个商品当前的库存表数量是否等于该商品所有入库流水正向变动之和加上所有出库流水负向变动之和。这个脚本我只用了一条 SQL 就能实现SELECT s.goods_id, s.quantity AS stock_qty, SUM(CASE WHEN f.change_type inbound THEN f.change_qty WHEN f.change_type outbound THEN f.change_qty ELSE f.change_qty END) AS flow_qty FROM stock s LEFT JOIN stock_flow f ON f.goods_id s.goods_id GROUP BY s.goods_id, s.quantity HAVING stock_qty ! flow_qty;只要结果不为空就说明某笔操作没有完整执行——要么库存更新了但流水没写要么流水写了但没更新库存。这个校验脚本我放在了一个管理页面里手动触发因为自动跑定时任务在小系统里容易被忽视手动触发反而简单直接。6. 部署与后续维护建议6.1 前端打包与 Nginx 配置系统开发完成后前端通过 Vite 直接打包成 dist 静态目录丢到 Nginx 上就能跑。我第一次部署时踩了个经典坑直接配置完 root 和 index 就访问结果是首页正常但刷新二级页面全部 404。原因很简单Vue Router 使用了 history 模式页面 URL 类似 /goods/listNginx 找不到对应的静态文件就报 404。解决办法是配置 try_files把所有路径都回退到 index.htmllocation / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }另外还要在根目录放一个 .env.production 文件把接口地址换成生产环境的域名。前端代码里所有接口调用都走一个 api 基础路径不要散落在各个页面里写死地址否则上线改接口地址要全局替换实在痛苦。6.2 数据备份与日常检查小型仓库系统很可能没有专职运维数据库备份这件事必须提前自动化。我在服务器上写了一个简单的 shell 定时任务每天凌晨执行 mysqldump 导出 SQL 文件保留最近 30 天然后同步备份到另一个目录。日常检查比备份还重要因为你总不能等数据丢了再恢复。我建议每周看一次后台的“库存校验”结果顺便看一眼流水表里有没有异常的负库存记录。负库存通常说明出库接口校验逻辑有漏也可能是有人手动改了数据库早发现比晚发现好处理得多。6.3 从这套系统还可以怎么延伸系统做完以后客户会自然提出更多需求这些需求完全可以在当前表结构上延伸。比如给商品表增加条形码字段配合扫码枪就能在出入库时直接扫码录入不需要手输编码比如增加 Excel 导入导出客户可以把旧系统里的商品档案批量迁移过来比如增加操作日志表把用户登录、新建订单、修改商品这些关键动作都记录下来方便追溯。如果要扩展多仓库只需要在 stock 表和库存流水表上都增加一个 warehouse_id 字段同时让入库出库单也带上仓库属性查询时按仓过滤即可。这套改造在当前数据结构上做起来比较平滑不会推倒重来。做这类管理系统我个人最深的体会是系统能不能跑起来取决于前后端代码的质量系统好不好用取决于表结构和业务逻辑是否贴近真实场景。一套仓库进销存管理系统真正的价值不是那个漂亮的表格页面而是每一次出入库操作都能准确反映到库存数量上每一笔数量变化都能在流水里找到源头。这中间没有太多需要炫技的地方需要的是把每一步业务逻辑老老实实设计清楚把每一处防呆校验做扎实。技术选型上选 Vue 还是 React、后端用 Node 还是 Java都可以按团队情况换但库存一致性这个事情做得稳比做得炫更重要。