这个项目是去年朋友开宠物咖啡馆时拉我一起做的。他的店不算小,两层楼,一层饮品区加零售货架,二层是宠物互动区和寄养间,周末高峰期一天要接待上百拨客人。开业不到两个月,纸质本子和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状态码和错误码,前端拿到统一结构Result<T>,用code字段判断是否成功。接口鉴权用JWT,登录后生成token,前端每次请求在Authorization头里携带,后端拦截器里解析校验,白名单之外的接口一律拦截。用户角色就是member、cashier、manager三种,权限控制用简单的注解+AOP实现,manager可以访问所有接口,cashier能处理订单,member只能操作自己的数据。
事务这里我要多说一句:订单、库存扣减、积分变化必须放在同一个事务里,任何一步失败都要回滚。我有一次排查线上问题,发现订单创建成功但库存没减少,最后定位就是Service方法没加@Transactional,MyBatis的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); List<OrderVO> list = orderMapper.selectPageList(query); PageInfo<OrderVO> pageInfo = new PageInfo<>(list);但是有几个细节必须注意:PageHelper.startPage()后面必须紧跟第一条Mapper查询,中间不能插其他查询,否则分页条件会错乱;用完之后PageHelper会通过ThreadLocal自动清理,但如果你在代码里把查询放到了异步线程里,分页就会失效,因为ThreadLocal对不上。还有一个我踩过的坑:PageHelper的count查询如果碰到复杂SQL,可能生成效率很低的count语句,这时候可以手动指定count查询,在XML里写一个专门的countSelect。
动态SQL是MyBatis最实用的功能。列表页的筛选条件,我用<where>标签加<if>动态拼接,比如按商品分类、按订单状态、按时间范围筛选。批量插入订单明细用<foreach>,collection对应参数名,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; List<OrderDetail> 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 id="deductStock"> UPDATE product SET stock = stock - #{quantity}, update_time = NOW() WHERE id = #{productId} AND stock >= #{quantity} </update>这条SQL用stock >= #{quantity}作为条件,天然实现了乐观锁的效果。如果库存不足,受影响行数是0,Java代码里就能感知到并抛异常回滚。注意不能用先查询再判断再更新的方式,那个在并发下会超卖,我们实际压测过,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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true如果不加serverTimezone=Asia/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 CLI:
npm 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,或者用sass@1.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?orderId=123,刷新页面参数还在;params传参则不会在URL中体现,刷新页面params对象会消失。所以跳转订单详情页这种场景,一定要用query方式传id,否则用户刷个屏就回到列表页了。
组件化开发的心得是:页面不要写成一个巨大的模板文件。我们把座位预约页拆成了SeatGrid(座位格子组件)、ReservationModal(预约弹窗)、SeatLegend(图例说明)。点单页拆成ProductList、CartPanel、MemberBar。这样每个组件只干一件事,自己管自己的数据,出问题也只改自己那部分,测试和维护都轻松很多。
这里额外补一个和宠物咖啡馆场景相关的:互动区装了摄像头,店长希望在大屏上能看到实时画面。最初方案是后端推RTSP流,但是浏览器原生不支持RTSP,最后通过转流服务生成HLS切片,前端用video.js+hls.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.active=prod > logs/app.log 2>&1 &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中文乱码 | 缺少characterEncoding=utf8 | 连接串加编码参数,服务端统一UTF-8 |
| 前端请求后端跨域 | 开发环境/生产环境未配置代理 | 开发用devServer.proxy,生产用Nginx反向代理 |
| LocalDateTime序列化报错或差8小时 | Jackson未配置JavaTimeModule / 数据库时区不对 | 注入Jackson2ObjectMapperBuilderCustomizer,连接串加serverTimezone=Asia/Shanghai |
| PageHelper分页失效,第一个查询没分页 | startPage()和查询之间插了其他Mapper调用 | 确保startPage后紧跟第一条查询,必要时手动PageHelper.clearPage() |
| 上传PDF文件损坏 | 全局XSS过滤器处理了二进制内容 | 过滤器忽略multipart/form-data和二进制流 |
@Update执行特别慢 | where条件无索引、表锁、大事务 | 先EXPLAIN,再看锁等待,最后拆分大事务 |
MyBatis报Parameter 'xxx' not found | 多参数未加@Param | 所有Mapper参数统一加@Param注解 |
| 前端打包后刷新404 | Nginx未配置history模式回退 | 加try_files $uri $uri/ /index.html; |
| MySQL连接报时区错误 | 驱动版本或连接串缺时区 | 使用8.x驱动并加serverTimezone=Asia/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写代码重要十倍。