拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Spring Boot宠物店管理系统核心设计与踩坑实录

Spring Boot宠物店管理系统核心设计与踩坑实录

同行们,最近完成了一套基于Spring Boot的宠物店管理系统,从需求调研到表结构设计,再到前后端联调、打包部署,整个过程踩了不少坑,也总结出一些可以直接复用的经验。宠物店这种业务场景很有代表性——它既有进销存的库存逻辑,又有会员充值、服务预约这类偏C端的功能,还有宠物档案这种强行业属性的数据,非常适合拿来练手或接私活。这篇文章我会把整个系统的设计思路、核心表结构、关键代码实现,以及我在项目里实际遇到过的问题和排查过程都写出来,希望对正在做同类项目,或者准备用Spring Boot搭建中小型管理系统的朋友有所帮助。

1. 项目设计与技术选型:为什么用Spring Boot做这类系统最合适

1.1 宠物店的实际业务到底在管什么

很多人在动手写代码之前,习惯先画ER图、先建工程,但我的习惯是先弄清楚店里每天到底要发生哪些事。就拿一家中等规模的宠物店来说,日常业务基本上逃不出这几条线:

第一是宠物档案。宠物店不是单纯的卖货,很多服务是围绕宠物本身展开的,比如洗澡、美容、寄养、驱虫、疫苗。每一只宠物都需要记录名字、品种、年龄、体重、是否绝育、疫苗情况、主人联系方式。这个数据不建立好,后面的预约和服务记录都是空中楼阁。

第二是商品库存和销售。宠物店会卖猫粮狗粮、零食、玩具、驱虫药、猫砂这类快消品。这里就涉及采购入库、零售出库、库存预警,以及批号效期管理——尤其是宠物药品,效期管理做得不好容易出大事。

第三是会员与储值。宠物店非常依赖回头客,所以几乎家家都有会员卡和储值赠送的玩法。储值余额、消费扣款、充值记录、积分累计,这些都需要系统支撑,而且钱有关的东西最容易扯皮,所以流水记录必须清晰。

第四是服务预约。洗澡、美容、护理这类项目客单价高,需要和店员的时间绑定,预约之后就涉及排班和到店确认。有些店还有寄养服务,需要记录入店时间、离店时间、每日喂养情况。

把这四条业务线理清楚之后,你会发现这本质上就是个标准的进销存+会员管理系统的组合,没有什么特别炫技的地方。但正因为业务面覆盖广,用Spring Boot这种“约定大于配置”的框架来落地是最舒服的,它自带starter机制,数据库、缓存、权限、文件上传这些能力都可以通过依赖快速集成,省去大量XML配置的重复劳动。

1.2 技术选型的三个关键考量

技术栈我最终敲定为:Spring Boot 2.7.x + MyBatis-Plus + MySQL 8.0 + Redis + Vue 2/3前后端分离。这里逐个说下选择理由。

Spring Boot版本我特意选了2.7系列,没有直接追新用3.x。原因很简单,3.x基于Jakarta命名空间,部分老版本的MyBatis-Plus、生成器工具和网上的大部分教程都不兼容,新手照着踩坑会非常痛苦。2.7虽然没有3.x新,但胜在稳定、生态兼容性好,对于管理系统这种对稳定性要求远高于新特性的项目来说,够用而且放心。

持久层选择MyBatis-Plus,核心原因是它的代码生成器配合Wrapper查询能让CRUD开发量减少一半。宠物店管理系统的表数量大概在15到20张,每张表都要写基础增删改查,如果用原生MyBatis手写XML,工作量会淹没在重复劳动里,而MyBatis-Plus的BaseMapper已经把这些内置好了。对于多表关联查询,我依然选择手写XML,避免因为滥用MP的嵌套查询导致性能问题。

Redis在这个项目里不是必需品,但我还是引入进来了,主要用来做三件事:登录token的存储、首页看板数据的缓存、以及商品库存的缓存扣减。尤其是会员储值余额的查询,每次下单都要读,用Redis扛住热点访问后,数据库的压力会小很多。

前端部分,考虑到很多做Spring Boot开发的人的前端水平停留在能用的阶段,我建议采用Vue + Element UI的经典组合,最后打包成静态文件放进Spring Boot的resources目录,这样部署时只需要一个jar包,省去配置Nginx的环节。这个方案我实测下来非常省心,适合中小项目和个人开发者。

1.3 系统模块划分:单体应用也要有清晰边界

虽然这是个单体项目,但代码结构上我还是按照“职责分包”的思路来划分,而不是controller/service/mapper三层打天下。最终包结构如下:

com.petshop ├── common # 通用模块:统一返回体、异常处理、常量、工具类 ├── config # 配置类:MyBatis-Plus、Redis、跨域、拦截器 ├── controller # 控制层:按业务模块细分 ├── service # 业务层:接口 + 实现 ├── mapper # 持久层接口 ├── entity # 数据库实体 ├── dto # 前端交互对象:请求参数、响应视图 ├── vo # 视图对象:组合查询结果 └── job # 定时任务:库存预警、寄养到期提醒

按业务模块划分controller,我分成了PetController、ProductController、StockController、MemberController、RechargeController、OrderController、AppointmentController、DashboardController这么几个。

这里有个经验想分享:很多初学者喜欢建一个CommonController放所有接口,图省事。但等接口数量超过50个之后,维护成本会急剧上升,别人接手根本不知道某个接口该去哪里找。按业务划分controller,配合统一返回体Result类,定位问题会快很多,这也是后期维护少掉头发的重要原因。

2. 数据库设计:核心表结构和建表时的取舍

2.1 表关系梳理:15张表怎么组织

整个数据库我最终拆成了15张表,按业务域归类成四组。

宠物档案组有宠物品种表、宠物信息表。商品与库存组有商品分类表、商品信息表、库存流水表、批次表。会员与营销组有会员表、会员储值流水表、积分明细表。交易与预约组有商品订单表、订单明细表、服务项目表、预约单表、寄养记录表。每个业务域之间通过外键逻辑关联,并不在数据库层面强加物理外键,这个后面会解释原因。

在设计的时候,我踩了一个比较典型的坑:一开始把宠物表和会员表做成强关联,认为宠物必须属于某个会员。后来发现宠物店的实际场景里,经常有非会员带宠物来洗澡、买药,你总不能逼人家先办会员。于是我把宠物表的主人字段改成了owner_name、owner_phone两个冗余字段,非会员也能建宠物档案,会员则额外关联member_id。这个改动让我意识到,表结构设计不能完全照搬理论上的范式,要充分考虑线下门店的真实操作习惯。

提示:做业务系统设计时,“归属”关系往往是变化的,尽量把强关联改成弱关联,用冗余字段降低耦合度,后期改动成本会小很多。

2.2 核心表现场实现:建表SQL可以直接参考

我挑几张最核心的表来展示DDL,这些都是在实际项目里验证过的结构。

宠物信息表是整个系统的地基,它的设计直接影响洗护、寄养功能的联动。最关键的字段是pet_type、sterilization_status和vaccine_status,这三个字段直接决定服务端是否有权限接单。比如部分美容项目对未绝育宠物有额外收费规则,疫苗信息则影响寄养区域的分配。

CREATE TABLE pet_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', pet_name VARCHAR(50) NOT NULL COMMENT '宠物名', pet_type TINYINT NOT NULL COMMENT '宠物类型:1猫 2狗 3其他', breed_name VARCHAR(50) COMMENT '品种名称', birthday DATE COMMENT '出生日期', weight DECIMAL(5,2) COMMENT '体重kg', sterilization_status TINYINT DEFAULT 0 COMMENT '绝育:0未 1已', vaccine_status TINYINT DEFAULT 0 COMMENT '疫苗:0未完成 1已完成', allergy_info VARCHAR(255) COMMENT '过敏史', owner_name VARCHAR(50) NOT NULL COMMENT '主人姓名', owner_phone VARCHAR(20) NOT NULL COMMENT '主人联系电话', member_id BIGINT COMMENT '关联会员ID', remark VARCHAR(500), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted TINYINT DEFAULT 0 COMMENT '逻辑删除' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宠物信息表';

逻辑删除字段deleted我几乎是每张表都加的,这对门店场景特别重要。员工误删一条宠物档案,如果真物理删除了,后续主人带宠物来消费时所有历史记录都对不上,连疫苗记录都没了,非常麻烦。用逻辑删除后还能通过回收站功能找回来。

商品表的设计需要注意的一个细节是单位换算。宠物食品和药品经常有“粒”和“盒”的差异,我的做法是在商品表里加一个sale_unit字段,同时设定stock_warning_line作为库存预警阈值。库存预警不是写死在代码里的,而是每个商品单独设置,比如皇家猫粮可以设置10袋预警,体内驱虫药可以设置5盒预警。

库存流水表是容易被忽略的。很多初版系统只更新商品表的库存数字,不做流水记录,一旦盘点对不上账,完全无法追溯。我的做法是每一次入库、出库、盘点调整都必须往stock_log表里写一条记录,包含变更前数量、变更后数量、操作类型和关联业务单号。这套机制在后面的对账和排查问题时帮了大忙。

2.3 为什么要放弃物理外键

这是个在老程序员之间经常争论的话题,我的个人选择是:所有表之间的关联都靠应用层逻辑维护,数据库不建物理外键。

原因其实很实际。宠物店管理系统面向的是门店员工,误操作在所难免,如果强外键约束存在,删除一条主表数据时报错会让员工完全摸不着头脑。更重要的是,MyBatis-Plus做分页查询和逻辑删除时,物理外键还会造成不少额外困扰,比如逻辑删除的ID还被引用时,外键约束本身并不认deleted字段。

放弃物理外键并不代表放弃数据一致性,核心的一致性靠事务和业务代码来保证。我在代码里处理关联数据时,始终遵循先查后改、事务包裹的原则,加上统一的异常处理,数据出错的风险完全可控。

3. 环境搭建与框架配置:这一步稳了,后面全顺

3.1 Maven依赖和版本选型实战

项目构建工具我用的是Maven,版本选型这块我直接贴最终的pom核心依赖,方便参考。

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <properties> <java.version>1.8</java.version> <mybatis-plus.version>3.5.3.1</mybatis-plus.version> <hutool.version>5.8.22</hutool.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>${hutool.version}</version> </dependency> </dependencies>

这里要提醒一下,many people遇到过一个典型的噩梦,就是Spring Boot版本和MyBatis-Plus版本不兼容。如果你的Spring Boot是3.x,那么MyBatis-Plus需要引入mybatis-plus-spring-boot3-starter而不是mybatis-plus-boot-starter,这个差异非常隐蔽,很多人栽在这里。我的建议是老老实实用2.7.x版本线,这是目前网上教程覆盖最全、问题排查最容易的版本组合。

Java版本我没有追高,依然使用JDK 8。对于这个项目来说JDK 8完全够用,而且不用担心服务器上没有高版本运行时。用JDK 8还能兼容大部分老项目的部署环境,实用性最强。

3.2 application.yml配置里的几个细节

Spring Boot的配置看似简单,但实际有非常多的细节决定系统是否好用。我贴出核心配置段落并逐条解释。

server: port: 8080 servlet: context-path: /api spring: datasource: url: jdbc:mysql://localhost:3306/pet_shop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true username: root password: root redis: host: localhost port: 6379 mybatis-plus: mapper-locations: classpath:mapper/**/*.xml type-aliases-package: com.petshop.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

允许SQL查询尾部分号和多条语句的allowMultiQueries=true这个参数,是我在线下踩过坑之后特意加上的,不过在此建议在明确需要时才开放,避免SQL注入面扩大。

MyBatis-Plus的逻辑删除配置,必须在global-config.db-config里声明logic-delete-field: deleted,同时实体类字段上加@TableLogic注解,两者缺一不可。如果不加,MP的deleteById只是普通删除,查数据时deleted=1的脏数据会混进来。log-impl我保留了一个StdOutImpl,开发阶段能看到完整的SQL日志,但线上一定要关掉,否则日志文件会爆炸,而且会暴露表结构信息。

3.3 统一返回体和全局异常:提升接口规范性的王道

统一返回体我定义了一个Result类,核心结构是code、message、data三个字段。所有controller的返回值都统一用这个结构包装,前端只需要解析固定格式即可,不用每个接口都定制。

@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }

全局异常处理我用了一个@RestControllerAdvice类,把参数校验异常、业务异常、系统异常分层处理。业务异常我自定义了一个BizException,在service层检测到不合理状态时直接抛出,由全局处理器统一捕获并返回给前端。这样controller的参数校验、service的业务校验、dao的SQL异常,全部都能在统一的入口处理掉,返回给前端的错误信息也能保证格式整齐。

注意:不要把SQL异常等系统异常信息原样抛给前端,既泄露底层结构又让用户看不懂。全局异常里对系统级异常必须打日志,同时返回“系统繁忙,请稍后再试”这样的通用信息。

4. 核心功能模块实操:从接口设计到代码落地

4.1 宠物档案模块:一行代码带出多维联动

宠物档案模块的接口设计并不复杂,核心就是CRUD,但有几个细节直接决定系统是否好用。第一个是查询接口必须支持多条件组合,包括宠物类型、品种、主人姓名、手机号、会员状态。前台店员最常用的场景是客户打电话来预约,店员只记得“张姐的柯基”,这时候要能通过模糊查询快速定位到宠物,离职率高、新员工上手快是门店软件的普遍需求。

第二个细节是详情接口要一次性返回带出所有关联信息,包括宠物照片、主人联系方式、最近的服务记录、未完成的寄养订单。这里我通过自定义SQL联表查询实现,避免前端在一次展示页面上发五六次请求。

第三方接口设计方面,我添加了预约服务时校验宠物疫苗状态的逻辑,未完成疫苗的宠物不允许预约寄养或者需要弹窗提醒。这个逻辑就写在业务service里,前端同步做交互限制,后端做二次校验,双保险。

宠物档案的service层我用到了事务注解@Transactional(rollbackFor = Exception.class),因为创建宠物同时要写宠物表和宠物照片表,任何一个失败都要整体回滚。一个非常容易踩的坑是@Transactional默认只回滚RuntimeException,如果业务代码抛出的是自定义Exception子类,默认不回滚,所以必须显式声明rollbackFor = Exception.class。

4.2 商品与库存模块:库存扣减的并发安全问题

商品模块引入了一个非常现实的场景:双11、节假日促销时,多个人同时下单抢同一个猫粮商品,库存只有3件,结果订单生成了5件,库存变成负数。这个问题是所有进销存系统的经典难题。

我的解决思路是采用原子更新加乐观锁控制。在库存扣减的SQL语句上,不使用先查后改的方式,而是直接在update语句中做条件判断:

UPDATE product_info SET stock = stock - #{count} WHERE id = #{productId} AND stock >= #{count}

这条SQL保证了扣库存和检查库存是原子操作,不会出现超卖。同时配合@Transactional,整个订单生成和库存扣减在一个事务里完成。如果update影响行数为0,说明库存不足,直接抛出业务异常。

高并发场景下订单量非常大时,单条update会成为数据库热行竞争瓶颈,可以引入Redis的预扣库存方案。但在宠物店这种门店级系统里,单条update已经足够,引入Redis缓存方案反而要考虑缓存和数据库一致性问题,得不偿失。根据实际业务量选择合适的技术方案,这是我在做系统设计时反复给自己强调的原则。

库存预警我用Spring Boot自带定时任务@Scheduled实现,每天凌晨检查一次商品表,把stock低于stock_warning_line的商品列表整理出来,通过企业微信机器人Webhook推送通知店长。定时任务最大的坑是默认单线程串行执行,如果有多个定时任务在相同时间点触发,其中一个阻塞全部影响。我特意通过配置类设置了线程池,让不同任务互不干扰。

4.3 会员储值与订单模块:金额相关必须流水可溯

会员储值模块是宠物店系统里最敏感的模块,涉及到真金白银,设计核心就一条:每次变动必须留痕。

我的表结构是member_account和member_recharge_log两张表。会员表的balance字段只做展示和校验,真正的余额变动全部通过流水表来记录。充值业务发生时,事务内做两件事:更新会员余额,插入一条type=充值 的流水记录。消费扣款时,同样插入一条type=消费 的流水记录,同时关联到订单id。

这里有一个实际业务中反复出现的需求:储值赠送。门店经常搞“充500送100”的活动,这100块的赠送金额如果直接并进余额,那用户在退卡时就会面临“赠送金额是否退还”的纠纷。我的做法是区分充值本金和赠送金,在会员余额表里维护available_balance和bonus_balance两个字段,消费时默认先花赠送金再花本金,退款时优先退本金。这套规则虽然简单,但实际门店几乎都对这种细节特别讲究,是增强系统粘性的关键。

订单模块我用主从表结构,主表存订单号、会员id、总金额、支付方式、状态,明细表存商品快照。商品快照是整个订单系统的要点,下单时就把商品名称、单价、数量原样保存进订单明细表,后续即使商品价格调整,订单记录仍保持下单时的价格,避免财务对账纠纷。如果不做快照,只关联商品id,三个月后商品改价了,翻旧账时会发现所有历史订单金额都是错的。

4.4 服务预约与寄养模块:时间冲突校验是关键

预约模块的代码难点在于时间冲突判断。客户预订某个时间段给宠物洗澡,系统必须判断该时段美容师是否已排满。我的预约表结构里有一个time_slot字段,存的是时间段编号,比如上午第一时段、下午第二时段。

为了避免冲突,我在service层的处理逻辑是:查询指定美容师在指定日期、指定时间段内是否有已确认的预约,如果有且未取消则拒绝新预约。这条查询加上唯一索引(employee_id, appointment_date, time_slot, status)后,能保证极端情况下也不会出现重复预约。这里的status必须是非取消状态。

寄养模块需要注意的是自动计算费用的逻辑。寄养费用按天计费,取宠和送宠的时间要精确到小时,但实际结算时门店通行的规则是“不满一天按一天算”。我在代码里用一个简单的日期计算逻辑来覆盖这个规则,传入入店时间和离店时间,计算出应该收取的天数。这个逻辑看起来简单,但实际能避免大量和客户的纠纷。

5. 踩坑实录:那些让我印象深刻的线上问题

5.1 日期格式化引发的前后端数据错乱

这是几乎每个Spring Boot项目都会遇到的问题。后端返回的时间格式是2024-03-15T09:30:00,前端直接原样显示出来,门店店员看到这个带T的日期一头雾水。原因在于Spring Boot默认的Jackson序列化对LocalDateTime采用的是ISO-8601格式,并不是我们习惯的yyyy-MM-dd HH:mm:ss。

解决方案是在application.yml里配置全局日期格式:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

但是这个方法只对java.util.Date生效,对LocalDateTime无效。这也是最隐蔽的地方。要彻底解决LocalDateTime的格式问题,需要添加一个Jackson自定义配置类,注册LocalDateTimeSerializer和LocalDateTimeDeserializer。我上线第一周就接到门店反馈“时间显示不对”,排查了快两个小时才发现是LocalDateTime没有全局格式化覆盖,而 Date类型正常。这个教训让我明白了:前后端联调开始前,必须在接口文档里固定所有日期时间的传输格式。

5.2 事务失效:没走代理的坑

我有一个在service层内部的私有方法调用事务方法的场景,结果发现事务根本没有生效。查下来才知道事务是通过Spring AOP代理实现的,内部this调用不经过代理对象,所以@Transactional注解根本不会生效。

这件事发生在会员退款业务里。我在withdrawRefund方法里调用了内部private方法updateBalance,updateBalance上有@Transactional,按预期应该和外部方法一起组成一个事务,结果updateBalance执行失败抛出异常后,前面的操作已经提交了,没法回滚。

解决方案非常粗暴:把事务注解加到外部公开方法withdrawRefund上,或者在类内部通过代理对象调方法。这个坑特别隐蔽,尤其对于前期不是特别熟悉Spring底层原理的人,建议直接把事务边界放在controller调用的第一个service方法上,这样最稳妥。

提示:事务注解加在public方法上才生效,加在private方法上静默失效。调用必须走代理对象,同类内部直接this调用同样失效。这是Spring事务最常见的问题来源。

5.3 逻辑删除和唯一索引的冲突

这是实践里非常头疼的一个问题。我为会员表的phone字段设置了唯一索引,用户注销后逻辑删除deleted=1,数据还在表里,结果新注册用户用了同一个手机号,插入操作直接触发了唯一索引冲突。

常规解决办法有三条路。第一条是唯一索引不能设置在phone这个字段上,可以改成phone + deleted的方式,但MySQL唯一索引中deleted全是0和1,两个逻辑删除的用户仍然冲突。第二条是把deleted改成随机的唯一字符串,比如删除时deleted=当前时间戳拼接随机数,保证每次删除的deleted值都不同,这样联合唯一索引不冲突。第三条是把已删除的数据物理迁走,比如独立归档表。

我目前选择的是第二种方案,实现成本最低。系统里删除用户时,将deleted字段更新为业务主键的负值和随机串,确保联合唯一索引的唯一性。这个方案在维护性和实现成本上都比较平衡。

5.4 枚举映射:tinyint还是字符串

我在宠物类型字段上使用了TINYINT类型存储1猫、2狗、3其他。这个设计看似没有错,但联调时前端传了一个字符串"2"给后端,MyBatis-Plus自动类型转换失败,导致接口直接报500。排查后发现问题根源在于前端传参时没有严格遵守类型约束。

我的建议是在项目里用枚举类型而不是直接用Integer和String来回倒,在实体类属性上配合@EnumValue注解使用MyBatis-Plus的枚举映射功能。这个方案能让代码里只出现PetTypeEnum.CAT这样的语义化写法,既避免魔法数字又是一处硬约束。从项目管理角度看,很久之后再看代码,一眼就能明白这个字段的含义,维护成本大幅降低。

6. 打包部署与上线:从一个jar包说起

6.1 Spring Boot的打包策略

Spring Boot项目最终产物就是一个可执行的jar包,这是它相比传统SSH项目最大的便捷点。打包时我用的是Maven的spring-boot-maven-plugin,执行mvn clean package -DskipTests即可。跳过测试这个参数建议日常就用上,不是因为测试不重要,而是大部分私活项目根本没有完善的测试用例,每次打包跑一遍全是红色报错,只会浪费时间。

前端Vue项目打包后生成dist目录,把dist目录里的static和index.html复制到Spring Boot项目的src/main/resources/static路径下,重新打包Java工程,最终一个jar包就同时包含了后端接口和前端页面。访问时直接通过同一端口进入,省去了配置Nginx和跨域的工作。这种方式对中小型系统非常友好,唯一需要注意的问题是前端路由模式必须改成hash模式,否则刷新页面时会404。

虽然把前端放进jar包在工程上不是最优解,但对个人或小团队接门店项目来说,客户只关心能不能双击启动,部署人员只需要一个jar包,这台机器上跑起来就完了。按场景选择方案,而不是追求架构的绝对先进,这是我长期接小项目的核心心得。

6.2 上线环境里常见的两个坑

第一个坑是MySQL 8.0的密码加密规则。MySQL 8.0默认的caching_sha2_password认证插件,和老版本的JDBC驱动不兼容,项目启动时就会报Public Key Retrieval is not allowed的错。解决方案是把JDBC驱动版本升级到8.0.x,或者在连接串中加allowPublicKeyRetrieval=true。类似问题在首次部署时特别常见,提前排掉这个坑能省很多事。

第二个坑是服务器时区问题。新买的云服务器默认时区是UTC,和本地时区不一致,插入数据库的时间就会晚8小时,所有报表数据对不上。启动jar包时建议在JVM参数里加上-Duser.timezone=Asia/Shanghai,同时在application.yml里也固定了serverTimezone,两处都设置才能万无一失。

磁盘空间问题也值得一提。应用日志默认按天滚动,但门店这种体量的系统如果不加日志清理策略,一两年后日志就能占满磁盘。我在配置里把日志保留期设置为30天,大小超过100MB自动切割,并且做了定时清理的脚本。这些细节可能看起来不起眼,但上线维护的投入会被这些细枝末节无限放大。

7. 复盘与经验沉淀

这个宠物店管理系统从立项到上线,前后大约用了六周时间。整个过程中我最大的感受是:技术选型上不需要追求新潮,稳定可靠才是第一位的。Spring Boot 2.7 + MyBatis-Plus + Vue这套组合,虽然不算前沿,但放到门店管理这个场景里非常匹配,团队协作效率高、问题排查快、客户反馈好。

很多开发者在做类似项目时,容易陷入“为了解决一个不存在的高并发问题而把系统架构搞复杂”的陷阱。宠物店的门店规模决定了它的并发量,白天高峰期同时在线操作的员工能超过10个人就算不错了。真正考验系统的不是并发,而是业务逻辑的完整性、数据的一致性、操作的便捷性。把会员储值流水做清楚、把库存扣减做成原子操作、把预约时的时间冲突校验做严谨,这些才是这个系统真正的价值所在。这也是我在复盘之后最大的收获——好的管理系统不是炫技的舞台,而是把矛盾规避在过程里的工具。

最后再分享一个经验:接这种传统行业的软件需求,一定要提前去门店现场蹲半天。看看店员结账时是右手拿扫码枪还是两手打字,看看柜台上客户等待的耐心极限,甚至看看墙面贴着的价格表是什么样。这些细节很多是写进需求文档里根本不会出现的盲区,但决定了做出来的系统店员愿不愿意用、真正用不用的起来。我这次去蹲点的时候发现店员结账时经常需要一手抱宠物一手操作电脑,所以整个前端的按钮都做得足够大、步骤压到最少,操作路径短的方案得到了门店的一致好评。技术是死的,但场景是活的,蹲下去才能看到真问题。

返回列表