这个项目是去年朋友开宠物咖啡馆时拉我一起做的。他的店不算小两层楼一层饮品区加零售货架二层是宠物互动区和寄养间周末高峰期一天要接待上百拨客人。开业不到两个月纸质本子和Excel已经扛不住了点单记错、座位冲突、宠物寄养信息对不上、会员储值还要翻聊天记录。他跟我说“要做一套以后能开连锁也不用重写的系统”于是就有了这个基于SpringBoot Vue MyBatis MySQL的企业级宠物咖啡馆平台管理系统。这篇就把整个项目的真实复盘写出来包括技术选型为什么这么定、数据库表怎么设计、核心业务代码怎么写、前后端联调踩了哪些坑、上线后怎么排查问题。不管你是要拿来做课程设计、毕业设计还是真的要给实体店做一套能落地的管理系统这套思路都参考得上。1. 项目整体设计与技术选型思路1.1 宠物咖啡馆的业务形态决定了系统边界动手之前先花了一个多星期蹲在店里看他们怎么营业也翻了各种行业资料。宠物咖啡馆和普通饮品店最大的区别在于它的业务是“零售 服务业 会员运营”的混合体而不只是卖咖啡。我当时梳理出来的核心业务链路是这样的用户到店散客直接点单会员可提前预约座位或包间到店后扫码核销。点单消费门店卖饮品、宠物零食、宠物用品周边部分商品需要库存管理。互动区管理宠物互动区有独立容纳上限需要控制同一时段进入的人数避免动物应激。宠物档案每位会员可以登记自己的宠物品种、年龄、疫苗情况、绝育情况、性格备注寄养或洗澡前店员要能快速查到。寄养与洗护类似酒店预订有入店时间和出店时间需要生成订单并关联宠物档案。会员储值与积分充值赠送、消费积分、积分抵扣是这类门店现金流的大头。员工角色店长、收银员、服务员的权限不一样不能所有员工都能看营业额和会员手机号。所以这套系统的边界不能只做成一个“点单收银”的小工具而是要覆盖预约、商品、订单、会员、宠物、寄养、权限这几个域。这也是为什么标题敢叫“企业级”——它从一开始就是按可以支撑连锁化、可以横向扩展门店数据的规模来设计的不是demo级的CRUD。1.2 技术栈选型的底层逻辑很多同学看到“SpringBoot Vue MyBatis MySQL”会觉得这就是个常见的Java课设组合。但实际上这套选型在中小型企业管理系统中是目前性价比最高的组合之一我来逐个说理由。SpringBoot作为后端框架最大的价值是“约定大于配置”。内嵌Tomcat、自动装配、起步依赖意味着我一个空项目三分钟就能跑起来不用像传统SSM那样配置一堆XML。而且SpringBoot自带的生产级特性比如actuator健康检查、ConfigurationProperties配置绑定、内嵌容器打包这些在项目上线后都直接能用上。团队如果有同学只会写SSH老代码SpringBoot的学习曲线也最平滑。Vue做前端核心优势是组件化和渐进式。页面可以拆成订单卡片、座位格子、宠物档案标签这样的独立组件复用性很高而且Vue的响应式机制让点单页面改数量、算总价这种交互不需要手动操作DOM开发效率高。和Element UI/Element Plus这类组件库配合后台管理界面很快就能搭出专业感。MyBatis和JPA的选择我重点说一下。我们的系统里有大量统计类SQL比如“某时间段内各商品销量排行”“会员消费频次分析”还有复杂的库存条件更新。MyBatis作为半自动ORM把SQL完全交给你控制写复杂查询、做SQL优化都比JPA方便不会出现那种自动生成的SQL带不动报表的情况。它的缺点是样板代码多但配合MyBatis Generator可以大幅减少重复工作。MySQL没什么悬念稳定的开源事务型数据库支持行级锁、事务隔离级别对订单、库存这类强一致性业务够用。而且团队里基本人人都会写MySQL招聘和后期维护成本都低。我当时也考虑过要不要引入Redis做缓存、用微服务拆分模块。最后判断是宠物咖啡馆单店的并发量远没到需要Redis扛压力的程度引入缓存反而增加了脏数据和缓存一致性成本微服务更没必要团队小、节奏快单体应用先把业务跑通才是正路。这个取舍思路值得每个做中小型系统的人参考——技术选型不是越复杂越好是匹配业务阶段才最好。2. 后端设计与核心功能实现2.1 SpringBoot项目结构与事务设计后端代码我按标准的分层结构组织每个包职责单一后续扩展不会互相纠缠。com.petcafe ├── controller // 接口层接收参数、返回Result ├── service // 业务逻辑层事务边界在这里 │ └── impl ├── mapper // MyBatis数据访问层 ├── entity // 数据库实体 ├── dto // 请求参数对象 ├── vo // 返回视图对象 ├── config // 配置类跨域、拦截器、Jackson ├── common // 常量、枚举、统一返回结果、异常处理 └── utils // JWT工具、日期工具等Controller层只负责参数接收和结果返回不写业务逻辑。Service层是事务的核心边界凡是涉及多表写入的操作比如下单、库存扣减、积分增加、预约锁定我都加Transactional。这里有一个很关键的细节事务一定要加在Service的public方法上不能加在Controller的method上也不能是同类内部调用的private方法否则会失效。全局异常处理用的RestControllerAdvice把业务异常、参数校验异常、系统异常分别映射到不同的HTTP状态码和错误码前端拿到统一结构ResultT用code字段判断是否成功。接口鉴权用JWT登录后生成token前端每次请求在Authorization头里携带后端拦截器里解析校验白名单之外的接口一律拦截。用户角色就是member、cashier、manager三种权限控制用简单的注解AOP实现manager可以访问所有接口cashier能处理订单member只能操作自己的数据。事务这里我要多说一句订单、库存扣减、积分变化必须放在同一个事务里任何一步失败都要回滚。我有一次排查线上问题发现订单创建成功但库存没减少最后定位就是Service方法没加TransactionalMyBatis的update执行完后自动提交了根本没有事务边界。这个坑新手特别容易踩。2.2 MyBatis映射细节缓存、分页与SQL控制MyBatis这块是整套代码里坑最密集的地方单独拆开来写。首先是Param注解。MyBatis的Mapper方法只要参数多于一个就必须用Param给每个参数起名字否则XML里写#{userId}会直接报Parameter userId not found。这听着很简单但一旦SQL写在XML里报错信息又不是很直观很容易让人卡很久。我的建议是所有Mapper方法哪怕只有一个参数也统一加上Param形成习惯就不会再踩。然后是#{}和${}的区别。#{}是预编译参数占位符MyBatis会给它自动加引号能防SQL注入${}是字符串拼接直接替换SQL片段。排序字段、表名这种没法用参数占位符的地方才用${}但绝对不能让用户直接传值进来不校验。用户输入任何时候都只能进#{}。MyBatis的缓存分两级。一级缓存是SqlSession级别的本地缓存默认开启同一个SqlSession内两次相同查询不会重复查库。二级缓存是namespace级别的需要显式开启多个SqlSession可以共享。我当时分析过要不要开二级缓存最后决定不开。原因很简单这个系统的订单、库存、会员余额都是强实时数据二级缓存一旦没有做好失效清理用户充值后查询还是旧余额这个体验是完全不能接受的。如果一定要做性能优化优先在SQL和索引上下功夫而不是上缓存。分页插件用的是最主流的PageHelper用法一句话就能说清楚PageHelper.startPage(pageNum, pageSize); ListOrderVO list orderMapper.selectPageList(query); PageInfoOrderVO pageInfo new PageInfo(list);但是有几个细节必须注意PageHelper.startPage()后面必须紧跟第一条Mapper查询中间不能插其他查询否则分页条件会错乱用完之后PageHelper会通过ThreadLocal自动清理但如果你在代码里把查询放到了异步线程里分页就会失效因为ThreadLocal对不上。还有一个我踩过的坑PageHelper的count查询如果碰到复杂SQL可能生成效率很低的count语句这时候可以手动指定count查询在XML里写一个专门的countSelect。动态SQL是MyBatis最实用的功能。列表页的筛选条件我用where标签加if动态拼接比如按商品分类、按订单状态、按时间范围筛选。批量插入订单明细用foreachcollection对应参数名item是每次遍历的元素separator是逗号。注意foreach里面字段不是null的才插入否则批量插入会因为有空值列直接报错。2.3 核心业务代码落地与关键SQL实现订单业务是整个系统的核心。用户从前端点单后端一次性接收商品列表、数量、会员编号然后在一个事务里完成三件事创建订单主表、批量插入订单明细、更新商品库存。订单创建主表的核心代码简写如下Transactional(rollbackFor Exception.class) public OrderVO createOrder(OrderCreateDTO dto) { // 1. 生成订单号日期 随机数避免并发重复 String orderNo generateOrderNo(); // 2. 计算总金额同时校验商品是否在售 BigDecimal totalAmount BigDecimal.ZERO; ListOrderDetail details new ArrayList(); for (OrderItemDTO item : dto.getItems()) { Product product productMapper.selectById(item.getProductId()); if (product null || product.getStatus() ! 1) { throw new BizException(500, 商品不存在或已下架); } BigDecimal amount product.getPrice().multiply(new BigDecimal(item.getQuantity())); totalAmount totalAmount.add(amount); // 组合明细对象... } // 3. 插入订单主表 Order order new Order(); order.setOrderNo(orderNo); order.setMemberId(dto.getMemberId()); order.setTotalAmount(totalAmount); order.setStatus(1); // 待支付 orderMapper.insert(order); // 4. 批量插入明细 orderDetailMapper.batchInsert(details); // 5. 扣减库存乐观锁方式 for (OrderItemDTO item : dto.getItems()) { int rows productMapper.deductStock(item.getProductId(), item.getQuantity()); if (rows 0) { throw new BizException(500, 商品库存不足 item.getProductId()); } } // 6. 增加积分按1元1分 memberMapper.addPoints(dto.getMemberId(), totalAmount.intValue()); return buildOrderVO(order); }库存扣减的SQL是重点我在productMapper里写了这么一句update iddeductStock UPDATE product SET stock stock - #{quantity}, update_time NOW() WHERE id #{productId} AND stock #{quantity} /update这条SQL用stock #{quantity}作为条件天然实现了乐观锁的效果。如果库存不足受影响行数是0Java代码里就能感知到并抛异常回滚。注意不能用先查询再判断再更新的方式那个在并发下会超卖我们实际压测过100个并发请求抢最后3件商品用查询判断的方式能卖出12件换这个SQL后最多只卖出3件。3. 数据库设计一张表对应一个业务场景3.1 核心表结构与设计思路数据库设计是这次复盘里我觉得最值得讲的部分。宠物咖啡馆的系统一张表就应该对应一个真实的业务场景而不是为了建表而建表。重点表先说会员表。它不只存姓名手机号还要冗余存储值余额、积分、会员等级因为列表页和核销页都要高频展示这三项每次join会员卡表会很慢。字段包括balance DECIMAL(10,2)、points INT、level TINYINT用逻辑删除deleted字段做假删除避免误操作导致历史订单关联断裂。宠物档案表是宠物咖啡馆区别于普通餐饮系统的标志性表。字段设计的时候我专门找宠物医生聊过name、species猫/狗/兔/其他、breed品类、gender、birthday、vaccine_status疫苗状态已接种/未接种/未知、neutered_status绝育状态、temperament性格亲人/胆小/警惕、medical_history病史、avatar_url。其中vaccine_status和neutered_status直接做成TINYINT枚举方便前端渲染标签。寄养和洗护前店员必须查看这些字段避免对生病的动物操作。座位预约表设计了seat_id、member_id、reservation_date、start_time、end_time、status四个核心字段。这里最关键的是时间冲突检测SQL条件用区间重叠判断start_time #{end} AND end_time #{start}座位在这个时间段如果存在未取消的预约就提示不可订。订单主表和明细表做成了经典的1:N结构。订单主表存流水号、会员、总金额、折扣、实付金额、状态、支付时间明细表存商品ID、商品名、单价、数量、小计。这里有一个重要设计明细表里要把商品名称和单价冗余存一份而不是只存product_id。为什么因为三个月后商品可能改名、涨价甚至下架但历史订单里必须有当时成交的商品信息和价格快照否则对账和售后根本没法做。用数据库术语说这叫“历史事实不可变”。商品表加了category_id外键。点赞一个细节所有金额字段都用DECIMAL(10,2)而不是FLOAT或DOUBLE因为浮点数在计算金额时会产生精度丢失1.1 2.2可能等于3.3000000000000003这在财务上是不能接受的。核心建表SQL简洁示例如下CREATE TABLE orders ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单号, member_id BIGINT NOT NULL COMMENT 会员ID, total_amount DECIMAL(10,2) NOT NULL COMMENT 总金额, discount_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT 优惠金额, pay_amount DECIMAL(10,2) NOT NULL COMMENT 实付金额, status TINYINT NOT NULL DEFAULT 1 COMMENT 1待支付 2已支付 3已完成 4已取消, remark VARCHAR(255) DEFAULT NULL COMMENT 备注, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_member_id (member_id), KEY idx_status_create_time (status, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;idx_status_create_time这个联合索引值得说它是为了支撑“按订单状态 按时间范围”查询的常见场景。如果单独在status和时间列上建两个索引MySQL一般只会用其中一个联合索引可以同时命中两个条件。3.2 MySQL安装配置与SQL执行陷阱MySQL这块单独讲因为项目开发期间环境问题比业务问题还多。MySQL安装配置第一件事就是版本选择。生产用的MySQL 8.0开发机和Windows本地用官方安装包即可装的时候注意选择utf8mb4作为默认字符集排序规则选utf8mb4_unicode_ci。utf8mb4相比utf8能完整存储emoji宠物档案的性格备注里客人经常写这些符号以及生僻字企业系统最好别省这个空间。连接串必须加时区参数这是高频报错点jdbc:mysql://localhost:3306/pet_cafe?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue如果不加serverTimezoneAsia/Shanghai用MySQL 8.0连接的时候会直接抛The server time zone value ʱ is unrecognized而且就算连上了LocalDateTime和数据库时间也会差8小时。我当时排查这个问题用了半天最后发现是驱动版本和时区配置的锅所以这里一定要写出来。datetime字段如果想让MySQL自动填入当前时间可以设置默认值为CURRENT_TIMESTAMP更新时间字段再加ON UPDATE CURRENT_TIMESTAMP。但注意MySQL 8.0的默认sql_mode里NO_ZERO_DATE是启用的如果想把某个日期字段默认值设为0000-00-00 00:00:00是会被拒的这也是网上很多答案过时了的原因。ONLY_FULL_GROUP_BY这个sql_mode也要熟悉。在MySQL 5.7以后它是默认开启的意味着SELECT * FROM orders GROUP BY status这种查询会直接报错因为select的列必须包含在group by里或者被聚合函数包住。这个规则很多人刚接触时很蒙但习惯之后写出的汇总SQL反而更规范。例如查各状态订单数必须这样写SELECT status, COUNT(*) AS cnt, SUM(pay_amount) AS total FROM orders WHERE create_time #{startTime} GROUP BY status;还有一次线上用户反馈“订单详情页打开特别慢”排查日志发现明细表查询走了全表扫描。原因是order_detail表给order_id建了索引但联表查询时订单主表的id是BIGINT明细表order_id也是BIGINT类型匹配没问题可是查询条件里隐含了order_id IN (SELECT id FROM orders WHERE ...)MySQL优化器没走索引直接扫了明细表。最终改成先查出订单ID列表再WHERE order_id IN (...)几千条明细秒开。4. 前端Vue实现与联调细节4.1 Vue项目搭建与环境配置前端环境这块Vue的安装配置有非常多的坑我按实操顺序来。Node.js建议装16.x或18.x LTS版本不要追最新大版本因为一些老依赖可能不兼容。然后用npm全局安装Vue CLInpm config set registry https://registry.npmmirror.com npm install -g vue/cli vue create pet-cafe选择Vue 3 Babel Router Vuex Axios这套组合。Vue CLI创建完项目后第一件事就是检查npm run serve能不能正常编译。这里最常见的报错是node-sass版本不兼容因为node-sass是在安装时下载二进制文件编译的Node版本一变就失效。解决方案是卸载node-sass换成dart-sass或者用sass1.32.x这种和Node匹配的版本号。开发调试强烈建议装Vue Devtools浏览器插件。它能直接在控制台看到每个组件的data、props、computed还能做组件树跳转排查数据绑定的问题效率高很多。我用它定位过“页面显示了但是表单项更新不同步”的问题就是组件里用了非响应式属性导致。项目目录结构按功能拆模块src ├── api // 所有接口请求封装 ├── assets ├── components // 公共组件订单卡片、座位格子、宠物档案卡片 ├── router // 路由配置 ├── store // Vuex模块化 ├── views // 页面登录、工作台、订单管理、商品管理、会员管理、宠物管理、座位预约、寄养管理 ├── utils // axios封装、token操作、日期格式化 └── App.vue4.2 路由管理、组件化与前端状态共享路由这块项目里用了懒加载按需加载页面组件配合路由守卫做登录验证。没登录就访问后台页面统一跳登录页代码如下router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() } else if (!token) { next(/login) } else { next() } })路由参数传递有两个方式query和params。query传参会跟在URL后面形如/order/detail?orderId123刷新页面参数还在params传参则不会在URL中体现刷新页面params对象会消失。所以跳转订单详情页这种场景一定要用query方式传id否则用户刷个屏就回到列表页了。组件化开发的心得是页面不要写成一个巨大的模板文件。我们把座位预约页拆成了SeatGrid座位格子组件、ReservationModal预约弹窗、SeatLegend图例说明。点单页拆成ProductList、CartPanel、MemberBar。这样每个组件只干一件事自己管自己的数据出问题也只改自己那部分测试和维护都轻松很多。这里额外补一个和宠物咖啡馆场景相关的互动区装了摄像头店长希望在大屏上能看到实时画面。最初方案是后端推RTSP流但是浏览器原生不支持RTSP最后通过转流服务生成HLS切片前端用video.jshls.js播放以.m3u8结尾的地址。Vue组件里延时几行就搞定import Hls from hls.js function playM3u8(videoEl, url) { if (Hls.isSupported()) { const hls new Hls() hls.loadSource(url) hls.attachMedia(videoEl) } else if (videoEl.canPlayType(application/vnd.apple.mpegurl)) { videoEl.src url } }这个需求最初不在计划里但做出来后很有实用价值。如果你们店里也装了摄像头可以直接参考这个方案。4.3 前后端联调跨域与接口鉴权前后端分离的项目联调阶段最常见的三个问题跨域、鉴权、文件上传被过滤器误伤。开发环境下跨域解决很简单用Vue CLI的devServer代理把所有/api开头的请求转发到后端服务// vue.config.js module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true, pathRewrite: { ^/api: } } } } }这样前端代码里请求地址写/api/order/list即可浏览器里不会出现跨域报错。生产环境用Nginx反向代理同样的逻辑前端静态资源和服务端接口都挂在同域下从根本上避免了跨域。后端config包里的跨域配置类也可以加上CorsFilter作为开发兜底但生产环境不依赖它。鉴权联调时封装axios拦截器是基础操作service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { if (res.code 401) { router.push(/login) } return Promise.reject(new Error(res.message)) } return res.data }, error { return Promise.reject(error) } )所有请求自动携带token后端拦截器校验401统一跳登录。这套机制做到位前后端联调基本不需要手动处理鉴权。文件上传是个容易翻车的地方。我们系统允许店员上传宠物疫苗接种证明的PDF于是后端加了一个全局XSS过滤器对所有请求参数做转义处理。结果上线后发现上传的PDF文件全部损坏了。排查原因过滤器没有判断Content-Type把PDF的二进制内容也当作普通字符串做了一遍转义文件自然就废了。修复方案很简单过滤器里加判断只对application/json和application/x-www-form-urlencoded这类文本请求做XSS过滤multipart/form-data和二进制流直接放行。这个坑在网络上的资料很少提到但做管理系统早晚会遇到。5. 部署上线与常见问题排查实录5.1 生产环境部署要点开发完成后部署上线我整理了一份可复用的操作清单。后端用Maven打成可执行jar包mvn clean package -DskipTests启动命令nohup java -jar pet-cafe-server.jar --spring.profiles.activeprod logs/app.log 21 JVM参数按服务器内存来-Xms256m -Xmx512m基本够用如果要支撑更高并发可以调大并加上GC日志参数。生产环境配置放在application-prod.yml里数据库地址、密码、日志级别都从外部配置读取不要把生产密码写死在代码里。前端构建产物是静态文件npm run build生成的dist目录丢到Nginx的/var/www/pet-cafe下配置要点是SPA路由的history模式需要处理刷新404问题server { listen 80; server_name petcafe.example.com; root /var/www/pet-cafe; index index.html; location /api/ { proxy_pass http://127.0.0.1:8081/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }try_files $uri $uri/ /index.html;这一行是必须的如果没有它用户直接在浏览器里访问/order/list这个前端路由地址Nginx会返回404因为服务端根本没有这个文件。加上这行后所有未知路径都回退到index.html由前端路由接管。数据库初始化脚本里除了建表还要把初始管理员账号、商品分类、座位号段都insert进去不然系统跑起来是空的。生产库每周备份一次用mysqldump定时任务即可mysqldump -u petcafe -p pet_cafe --single-transaction --routines /backup/pet_cafe_$(date \%F).sql--single-transaction参数在InnoDB下可以不锁表备份不影响白天业务运行。5.2 高频问题排查速查表把这次项目里遇到的所有典型问题汇总成一张速查表每一行都是真实踩过的坑。症状可能原因解决办法接口返回的JSON中文乱码缺少characterEncodingutf8连接串加编码参数服务端统一UTF-8前端请求后端跨域开发环境/生产环境未配置代理开发用devServer.proxy生产用Nginx反向代理LocalDateTime序列化报错或差8小时Jackson未配置JavaTimeModule / 数据库时区不对注入Jackson2ObjectMapperBuilderCustomizer连接串加serverTimezoneAsia/ShanghaiPageHelper分页失效第一个查询没分页startPage()和查询之间插了其他Mapper调用确保startPage后紧跟第一条查询必要时手动PageHelper.clearPage()上传PDF文件损坏全局XSS过滤器处理了二进制内容过滤器忽略multipart/form-data和二进制流Update执行特别慢where条件无索引、表锁、大事务先EXPLAIN再看锁等待最后拆分大事务MyBatis报Parameter xxx not found多参数未加Param所有Mapper参数统一加Param注解前端打包后刷新404Nginx未配置history模式回退加try_files $uri $uri/ /index.html;MySQL连接报时区错误驱动版本或连接串缺时区使用8.x驱动并加serverTimezoneAsia/Shanghai订单库存扣成了负数先查后更新导致的并发问题改为UPDATE ... WHERE stock #{quantity}条件扣减表格查询很慢没建索引或索引失效用EXPLAIN分析建联合索引避免隐式类型转换其中Update执行慢这个问题排查过两次一次是订单表update_time字段上没索引WHERE create_time ?扫全表另一次是更新语句在事务里长时间持锁被后到的更新堵住。定位方法都一样先EXPLAIN看执行计划再SHOW ENGINE INNODB STATUS看锁等待。SQL优化的核心永远先看执行计划别猜原因。还有一个不太常见但可能遇上的需求接手了一套只有jar包的SpringBoot项目没有源码。可以用CFR或JD-GUI这类的反编译工具把class文件还原成大概能看的Java代码但注释全部丢失、泛型部分可能还原不完整而且反编译出来的代码不能直接编译只能作为参考。以我的经验除非万不得已否则不要指望反编译“还原整个项目”靠它理清业务逻辑都费劲更靠谱的是根据数据库表结构和接口文档重新梳理。这也是我把源码完整归档的原因。部署完成后我还顺手加了几个实用功能大屏看板展示今日订单数和营业额、会员消费排行榜、商品销量TOP10。这些用MyBatis写统计SQL查出来前端用简单的图表组件渲染店长在店里挂个电视就能实时看到经营状况客户满意度提升非常明显。这套系统上线运行了几个月整体很稳定。回到最初那套“企业级”的标准多门店扩展的字段预留做了角色权限做了数据备份做了核心业务的SQL也优化过。我个人最大的体会是这类管理系统的技术难点从来不是框架本身而是对业务模型的理解。你能不能在数据库设计阶段就想清楚“订单明细需要快照”“库存扣减要用条件更新”“宠物档案要单独建表”决定了系统上线后是稳定的拐杖还是满地补丁的灾难。做这类项目先蹲在店里把店长的日常流程看明白比先打开IDE写代码重要十倍。
