做过宠物店管理系统这类毕设的朋友应该都清楚,一个看起来“只是增删改查”的项目,真要把 SpringBoot、Vue、uniapp 三端串起来跑通,要考虑的东西远比想象中多。我接手过的宠物店管理系统,恰好就是“基于 java 的 SpringBoot/SSM + Vue + uniapp”这套全栈组合,覆盖管理后台、门店收银、移动端预约三大场景。这篇就讲讲整套系统的详细设计、核心实现和部署思路,不管是做毕业设计,还是想入门全栈开发,都能找到可以直接抄作业的部分。
先说明一点,这套系统的定位很明确:宠物店日常运营管理。宠物信息建档、商品上下架、服务预约、订单结算、会员储值、员工权限,这些是一个宠物店最真实的业务流。管理后台用 Vue 写,面向店长和店员;移动端用 uniapp 写,面向顾客,做预约、逛商城、查订单;后端用 SpringBoot 提供 REST API,SSM 作为底层框架兜底。三端分离、接口驱动,既符合当下企业开发的主流模式,也方便在论文里画出漂亮的系统架构图。
下面按从设计到落地的顺序,完整拆一遍这个项目的每一个关键环节。
1. 项目整体设计与技术选型
1.1 核心需求解析:一个宠物店到底需要什么
先别急着写代码,把业务理清楚才是正经事。我见过太多人一上来就建表,结果表建了十几张,功能却对不上门店的真实需求。宠物店管理系统本质上要做三件事:管宠物、管商品、管服务。
管宠物,不只是登记一只狗叫“旺财”,还包括它的品种、年龄、疫苗记录、驱虫记录、体重变化。这些数据看起来琐碎,但宠物主人在意,门店做回访也要靠它。
管商品,就是常规的进销存逻辑,宠物粮、零食、玩具、洗护用品,该有的分类、库存、上下架、价格都不能少。这里容易忽略的是批次和保质期,虽然毕设阶段不需要做那么细,但在数据库设计时预留字段有好处的。
管服务,包括洗澡、美容、寄养、医疗咨询这类非标服务。非标意味着要预约、要排班、要确认状态。这个模块比商品管理更考验流程设计,也是我在后面会重点展开的部分。
这三个核心再加上会员管理、订单管理、员工管理、公告管理,就是一套完整系统的功能闭环。简而言之,这个项目的价值不在某个单一功能有多炫酷,而是把一家宠物店的日常经营链路用数字化方式串了起来。
1.2 技术栈对比:SpringBoot、SSM与Vue+uniapp为什么能搭在一起
很多初学者看到“SpringBoot/SSM”这个写法会懵:到底是 SpringBoot 还是 SSM?其实两者不是二选一的关系。SSM 是 Spring + SpringMVC + MyBatis 的组合,是经典的 Java Web 开发框架;SpringBoot 是 Spring 生态的快速开发框架,内置了 Tomcat,简化了大量 XML 配置。SpringBoot 底层依然可以用 SpringMVC 处理请求、用 MyBatis 操作数据库,所以它本质上是 SSM 的“增强版”。
做这个项目时,我始终推荐直接上 SpringBoot。原因很简单:开发效率高。SSM 时代要写一堆 applicationContext.xml、spring-mvc.xml,SpringBoot 里一个 application.yml 全部搞定。而且 SpringBoot 的自动配置让整合 MyBatis、MySQL、Redis 这些组件变得极其省事。我实测下来,同样一套宠物店代码,SSM 工程从零搭建需要三到五天,SpringBoot 一天就能跑起来。
前端选 Vue 没什么好争议的。Vue 上手曲线平缓,组件化开发适合管理后台这种“表格+表单+弹窗”密集的场景。配合 Element UI,表格分页、对话框、表单校验这些都有现成组件,开发速度非常快。这里也说一句,Vue 2 和 Vue 3 的选择上,毕设项目都用 Vue 2 + Element UI 反而更稳,组件资料多、坑少,答辩时被问懵的概率也低。
uniapp 是移动端的核心。它最吸引人的就是一套代码多端发布——编译成微信小程序、H5、安卓 App,ios 也能出包。对宠物店来说,顾客端用微信小程序最贴近真实使用场景;对毕设来说,能体现“移动端适配”这个亮点。我实测过 uniapp 项目编译到微信开发者工具和 H5 端的兼容性,大部分样式能复用,个别组件需要条件编译处理,这部分后面会细讲。
1.3 系统架构设计:前后端分离+接口驱动
整套系统采用前后端分离架构。后端只负责业务逻辑和数据处理,通过 RESTful API 暴露服务;前端管理后台通过 axios 调用接口;uniapp 移动端通过 uni.request 调用同样的接口。三者共用一套后端,这就是接口驱动的核心价值。
后端按经典分层结构组织:Controller 接收请求、Service 处理业务、Mapper 操作数据库、Entity 映射数据表。如果项目涉及复杂业务,可以在 Service 和 Mapper 之间加一层 DTO/VO 转换,避免实体类直接暴露给前端。我在这个项目里的做法是:所有接口统一返回 Result 对象,结构为 code、message、data 三个字段,code 为 200 表示成功,400 表示参数错误,401 表示未登录,500 表示服务端异常。这个规范极其重要,后面三端联调时能省下大量扯皮的时间。
2. 核心功能拆解与数据库建模
2.1 功能模块全景:管理端、移动端、通用模块各干什么
先画个功能地图,可以对照着建表,就不会漏功能。
管理后台的核心模块包括:仪表盘(今日营业额、订单数、预约数)、宠物管理(增删改查、详情、接种记录)、商品管理(分类、库存、上下架)、服务项目管理(洗护、美容、寄养等)、预约管理(列表、确认、完成、取消)、订单管理(下单、支付状态、退款)、会员管理(等级、储值、积分)、员工管理(账号、角色、排班)、公告管理。
移动端(顾客用)的核心模块包括:首页(轮播图、服务展示)、服务预约(选择服务项目、选择门店/时间)、商城列表与详情、购物车与下单、订单列表与详情、个人中心(注册登录、宠物档案、优惠券、意见反馈)。
有一件事我觉得应该特别提醒:通用模块不要和业务模块混在一起。比如文件上传、短信发送、Excel 导出,这些应该做成独立工具类或公共服务,而不是散落在各个 Controller 里。宠物店系统的图片上传(宠物照片、商品图、轮播图)是典型通用功能,独立出来会清爽很多。
2.2 数据表设计:核心表结构与关联关系
数据库我用 MySQL 5.7,字符集 utf8mb4,排序规则 utf8mb4_general_ci。业务表大概十来张,核心几张挨个过一遍。
用户表(user):id、username、password、phone、avatar、role(顾客/店长/店员)、status、create_time。顾客的登录和员工的后台登录可以放一张表,用 role 字段区分,这样同一个登录接口可以复用。密码必须加密存储,BCrypt 是最稳妥的选择,后面会说具体的加密写法。
宠物表(pet):id、user_id(所属主人)、name、species(猫/狗/其他)、breed、gender、birthday、weight、vaccine_info(疫苗记录,可用文本或 JSON 存)、create_time。这里的关键是 user_id 外键,一个顾客可以养多只宠物,形成一对多关系。
商品表(product):id、name、category_id、price、stock、cover_image、detail、status、sales、create_time。商品分类单独建一张 category 表,形成一对多关联。detail 字段建议用 text 类型,存富文本内容。
订单表(orders):id、order_no(唯一订单号)、user_id、total_amount、status(待支付/已支付/已发货/已完成/已取消)、pay_time、create_time。订单的核心是状态流转,要保证每个状态都有明确的前置状态,避免并发问题。
订单项表(order_item):id、order_id、product_id、product_name(快照)、price(快照)、quantity。这里有个非常值得注意的点:订单里必须冗余商品名称和价格快照,因为商品删除或改价后,历史订单不能跟着变。
预约表(appointment):id、user_id、pet_id、service_id、appointment_date、time_slot(上午/下午/晚上)、status(待确认/已确认/已完成/已取消)、remark。预约是移动端的高频操作,状态流转要清楚。
服务项目表(service_item):id、name、description、price、duration(预计耗时)、cover_image、status。
会员表(member):id、user_id、level(普通/银卡/金卡)、balance、points、create_time。会员和用户是一对一关系,存的是扩展的会员专属信息,比如储值余额和积分。
员工表(employee):id、user_id、position(美容师/前台/店长)、work_status、introduction。这个表和用户表也是关联关系,负责承载岗位信息。
公告表(notice):id、title、content、create_time。
表关系梳理下来,整体就是一张围绕 user 发散的网络,宠物、订单、预约、会员都以用户为中心。数据库设计时只要把握住“外键别乱加、冗余要合理”这两个原则,基本不会出大问题。
2.3 数据库设计的三个实操心得
第一,状态字段用 tinyint 整型而不是字符串。比如订单状态 0-待支付、1-已支付、2-已发货、3-已完成、4-已取消。整型比较高效,代码里用常量类做映射,可读性也有保证。
第二,创建时间统一字段名 create_time,用 datetime 类型。这套规范可以配合 MyBatis-Plus 的自动填充功能,插入时自动 set 当前时间,省得每个插入语句手写。
第三,不要滥用逻辑删除。毕设项目里除非明确需要“回收站”功能,否则用 delete 物理删除就足够了。我在实际项目里 90% 的删除都是物理删除,只有用户数据这类核心资产才考虑逻辑删除。
3. 后端关键实现与编码细节
3.1 SpringBoot 工程从零搭建:目录规划与核心配置
后端工程建议用 SpringBoot 2.7.x,搭配 JDK 8。有人纠结 SpringBoot 3 要不要用,我统一回复:毕设和多数企业项目,SpringBoot 2.7 最稳,SpringBoot 3 要求 JDK 17,部分旧依赖可能不兼容,没必要给自己挖坑。
基础依赖就几个:spring-boot-starter-web、mybatis-plus-boot-starter(注意是 Plus,不是原版 MyBatis,省太多事了)、mysql-connector-java、lombok、jjwt(登录鉴权)、hutool(工具类库)。pom.xml 里加上这些,工程骨架就立住了。
目录规划按照常规包结构:
com.petshop ├── common // 统一返回结果、全局异常处理、常量类 ├── config // 配置类(跨域、拦截器、文件上传) ├── controller // 接口层 ├── service // 业务层 ├── mapper // 数据访问层 ├── entity // 实体类 ├── dto // 请求/响应数据传输对象 └── utils // 工具类(JWT、文件上传、日期处理)application.yml 是核心配置文件,我把自己常用的配置贴出来,你可以直接套用:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/petshop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: mapper-locations: classpath*:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true几个容易被忽略的细节:数据库连接的 serverTimezone 必须配置,否则高版本 MySQL 驱动会报时区错误;map-underscore-to-camel-case 打开后,数据库字段 user_id 自动映射到 userId,省掉大量 resultMap。
3.2 登录鉴权:JWT 方案完整落地
宠物店系统有顾客端和员工端,但鉴权逻辑可以共用一套:JWT + 拦截器。登录成功后签发 token,前端每次请求带上 token,后端拦截器校验有效性。
JWT 工具类核心代码如下:
@Component public class JwtUtils { @Value("${jwt.secret}") private String secret; // 生成 token,建议过期时间设置为 2 小时 public String generateToken(Integer userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 2)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } // 解析 token public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } }用户登录接口的实现逻辑也不复杂:controller 接收用户名和密码,service 查询用户表,用 BCrypt 比对密码,比对成功生成 token 返回前端。这里特别强调一点:登录接口要加验证码,至少在管理后台要加。虽然是毕设项目,但加一个图片验证码(可以用 hutool 的 CaptchaUtil)工作量很小,却能在答辩时多一个可讲的技术点。
拦截器是实现登录校验的关键,继承 HandlerInterceptorAdapter,preHandle 方法里校验 token:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (StringUtils.isBlank(token)) { throw new BusinessException(401, "未登录或登录已过期"); } try { Claims claims = jwtUtils.parseToken(token); request.setAttribute("userId", claims.getSubject()); return true; } catch (Exception e) { throw new BusinessException(401, "token无效"); } }还有一个常见问题:前端跨域。前后端分离项目必须配置跨域过滤器,否则浏览器直接拦截接口请求。最简单的写法是定义一个 WebMvcConfigurer,添加跨域映射:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }实际开发中,跨域问题经常出现在管理后台本地调试的时候,配置好这一处基本可以一次性解决。
3.3 业务接口设计:以预约服务为例的完整流程
预约功能是移动端的核心业务,也是展示后端设计能力的重点模块。一个完整的预约流程涉及三个角色:顾客提交预约、店员确认预约、系统记录状态变更。
创建预约接口的设计要点如下:
@PostMapping("/appointment") public Result addAppointment(@RequestBody AppointmentDTO dto) { // 1. 校验服务项目是否存在且上架 // 2. 校验时间槽位是否可预约 // 3. 校验该顾客的宠物是否存在 // 4. 创建预约记录,状态为待确认 // 5. 如果服务项目需要指定美容师,同步更新对应美容师的排班 }这里面有个业务细节值得思考:预约冲突怎么处理。如果两个顾客同时预约了同一天下午的同一美容师,系统应该在第二个人提交时给出提示。最简单的方案是——查询预约表里是否存在 service_id、appointment_date、time_slot 都一致且状态不是“已取消”的记录,存在则拒绝。虽然并发极端情况下可能有问题,但对毕设项目来说,这个查询逻辑已经足够合理。
时间槽位可以用常量接口定义:
public interface TimeSlotConstant { Integer MORNING = 1; // 上午 9:00-12:00 Integer AFTERNOON = 2; // 下午 13:00-18:00 Integer EVENING = 3; // 晚上 18:00-21:00 }用整型而不是字符串存槽位,后续按时间段筛选统计更方便。
订单模块的关键点则在于状态机的控制和下单事务性。创建订单时涉及多个表的写操作:插入订单主表、插入订单项表、更新商品库存、更新会员积分。这四个操作必须放在同一个事务里,任何一步失败都要整体回滚,不然会出现“订单创建成功但库存没减”这种严重数据不一致的问题。用 Spring 的 @Transactional 注解即可,需要注意事务只对 RuntimeException 生效,要确保自定义异常继承 RuntimeException。
3.4 数据一致性:事务、锁与业务补偿的三层保障
“java怎么保证数据一致性”是搜索热词,也是面试高频题,放在宠物店项目里特别有说头。
事务解决的是单次操作内的原子性。上面说的创建订单,四个写操作绑在一个事务里,是最基本的一层保障。乐观锁/悲观锁解决的是并发问题。库存扣减是典型的并发场景,两个用户同时买最后一个商品,不能两个都成功。悲观锁方案是查询时加 for update,乐观锁方案是在商品表加 version 字段,更新时 set stock = stock - 1 where id = ? and stock >= ?。宠物店系统这种并发量下,直接使用 Update 语句带条件扣减就够了:
UPDATE product SET stock = stock - 1 WHERE id = #{id} AND stock >= 1受影响行数为 0 则说明库存不足,业务层据此抛出异常。这个写法干净利落,是实战里最常用的方案。
业务补偿处理的是跨时间的一致性。比如用户下单一小时后未支付自动取消,这种场景用数据库事件或定时任务实现。SpringBoot 集成 Scheduled 定时任务,每分钟扫描超时订单并自动取消,逻辑很简单,但能体现你对“数据最终一致性”的理解。
3.5 文件上传:宠物照片存储与访问路径处理
宠物店系统几乎每个模块都要上传图片。产品图、宠物照片、店铺环境图、轮播图,图片功能不能只在本地目录存一下完事。
我的做法是配置本地磁盘存储,通过虚拟路径映射访问。Windows 和 Linux 的路径不同,用配置项区分。设计一个上传工具类或者直接写在 Controller 里,具体实现如下:
@Value("${file.upload-path}") private String uploadPath; @PostMapping("/upload") public Result upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Result.error("文件不能为空"); } // 生成唯一文件名:yyyyMMddHHmmss + 随机数 + 扩展名 String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String fileName = DateUtil.format(new Date(), "yyyyMMddHHmmss") + UUID.randomUUID().toString().substring(0, 4) + ext; File dest = new File(uploadPath + fileName); try { file.transferTo(dest); return Result.success("/files/" + fileName); } catch (IOException e) { return Result.error("上传失败"); } }上传成功后返回的相对路径是 /files/xxx.jpg,配置虚拟映射到本地磁盘目录:
@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${file.upload-path}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceLocations("file:" + uploadPath); } }注意 uploadPath 末尾要加斜杠,Linux 下是 /usr/local/petshop/files/,Windows 下是 D:/petshop/files/。这个细节我看着不少人在部署时卡了很久。
如果想让图片上传这个点更有含金量,可以引入 MinIO 做对象存储,本地磁盘文件就迁移到 MinIO 客户端管理了。搜索热词里有“minio加入到springboot”,确实是一个值得扩展的方向:下载 MinIO 服务端、配置 accessKey,然后用 Java SDK 上传文件,返回带签名的访问 URL。不过这是加分项,不属于系统必需,时间紧张的话可以先不做。
4. Vue 管理后台与 uniapp 移动端实现
4.1 管理后台:Vue + Element UI 的工程结构与页面组织
管理后台的 Vue 工程可以用 Vue CLI 创建,或者直接用 vite 搭建。考虑到需要快速实现和稳定,我建议 Vue CLI 4.x + Vue 2.7 + Element UI 2.x,这是最主流也最不容易出错的组合。
工程结构按模块划分:
src ├── api // 接口调用统一封装 │ ├── login.js │ ├── pet.js │ ├── product.js │ └── order.js ├── assets // 静态资源 ├── components // 公共组件(图片上传组件、分页组件) ├── router // 路由配置 ├── store // Vuex 状态管理 ├── views // 页面视图 │ ├── login │ ├── dashboard │ ├── pet │ ├── product │ ├── order │ ├── appointment │ └── user └── utils // 工具函数(axios封装、token处理)axios 封装是重要的一环,不加封装的后果就是每个页面都在写重复的错误处理代码。我的做法是统一设置 baseURL、请求拦截器带 token、响应拦截器统一处理错误码和 401 跳转登录:
import axios from 'axios' import router from '@/router' const service = axios.create({ baseURL: process.env.VUE_APP_BASE_URL || 'http://localhost:8080/api', timeout: 10000 }) 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) { return res } else if (res.code === 401) { localStorage.removeItem('token') router.push('/login') return Promise.reject(new Error(res.message)) } else { return Promise.reject(new Error(res.message)) } }, error => { return Promise.reject(error) } ) export default service页面开发流程基于 Element UI 展开:宠物列表页用 el-table 展示数据、el-dialog 承载新增/编辑表单、el-pagination 处理分页,代码结构相当固定。这种重复度高的页面,如果装配熟练,开发效率极高。
4.2 路由权限控制:不同角色看到不同菜单
管理后台有个容易忽略但答辩时必被问的功能:权限控制。店长和店员登录后台应该看到不同的菜单和功能按钮。
技术上可以用路由守卫 + 动态路由来实现。用户登录后,后端根据角色返回菜单权限列表,前端用 router.addRoutes 动态挂载对应路由模块。简单一点的做法是用 Vue Router 的 beforeEach 守卫做校验:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path === '/login') { next() return } if (!token) { next('/login') return } next() })在菜单渲染层面,用 v-if 根据用户角色过滤菜单项。虽然这只是在展示层做了隐藏,但对于毕设项目展示“权限管理”这个功能点已经足够。想要高分,再往深挖一点——按钮级权限控制,用自定义指令 v-permission 封装控制。
4.3 uniapp 移动端:一套代码适配小程序和 App
uniapp 工程建议直接用 HBuilderX 创建,模板选 uni-ui。移动端页面相对简洁:首页、分类、购物车、我的,四个 tab 页就能覆盖核心功能。
uniapp 里最核心的 API 封装是 uni.request,每次写太啰嗦,可以封装一个统一的 request 工具:
// utils/request.js const BASE_URL = 'http://localhost:8080/api' export function request(url, method = 'GET', data = {}) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + url, method, data, header: { 'Authorization': uni.getStorageSync('token') || '' }, success: (res) => { if (res.statusCode === 200) { resolve(res.data) } else { uni.showToast({ title: '请求失败', icon: 'none' }) reject(res.data) } }, fail: (err) => { uni.showToast({ title: '网络异常', icon: 'none' }) reject(err) } }) }) }移动端有几个跟 H5 明显不同的点需要注意。
图片上传用 uni.uploadFile 而不能用 axios;富文本展示小程序默认不支持 HTML 标签渲染,需要用 mp-html 组件库解析;swiper 轮播图在小程序和 H5 的表现存在差异,需要调试。
uniapp 的生命周期和 Vue 不完全相同,页面加载用 onLoad 而不是 created,下拉刷新用 onPullDownRefresh,触底加载用 onReachBottom,这些在小程序端表现尤其明显。登录态管理用 uni.getStorageSync,不能用 localStorage。
4.4 uniapp 打包与上架的四个注意点
搜索热词里有“uniapp 怎么打包”“uniapp 上架安卓应用市场”,这是移动端绕不开的环节,我从踩过的坑里提炼几个要点。
打包前先配置 manifest.json:小程序的 appid、安卓的包名(比如 com.example.petshop)、应用图标、启动图,这些都必须在打包前配置好。appid 填错会导致微信开发者工具无法打开项目;安卓包名是应用市场审核的硬性要求,上线后不能再修改。
微信小程序开发设置:需要在微信公众平台配置 request 合法域名,否则开发工具里请求后端接口都会被拦截。本地调试时可以勾选“不校验合法域名”,但真机预览时必须配置 HTTPS 域名。
安卓 App 打包:HBuilderX 提供云打包功能,只需在 manifest.json 里配置好证书(测试证书可以直接用公共证书),点“发行-云打包”就能在线打出一个 apk 文件。首次云打包需要实名认证,建议提前搞定,不然项目交付当天才想起来就麻烦大了。
真机调试的跨域问题:App 端请求本地后端接口,不会像浏览器那样限制跨域,但 iPhone 和安卓对一些 HTTPS 证书的处理不一致。如果后端没有配置 HTTPS 证书,安卓设备默认允许 HTTP 明文访问,但 iOS 的 ATS 策略默认禁止 HTTP。解决方法是把后端接口升级为 HTTPS,或者 iOS 打包时配置 NSAppTransportSecurity 允许本地网络访问。这一步比较绕,很多人卡在这里,我建议毕设阶段演示用 H5 端或微信小程序端就够,避开证书问题。
4.5 前后端联调:接口规范对了,联调效率翻倍
联调阶段是很多小组项目翻车的重灾区。接口路径对不上、字段名不一致、状态码不统一,每一个都会浪费大量时间。
我的经验是:先定接口文档再写代码。哪怕不用 Swagger,也要在项目开始时约定好每个接口的路径、请求方式、参数格式、返回结构。项目用到 Swagger 的话,在 SpringBoot 集成 knife4j 也就十几分钟的事。
联调时的经典坑有这几个:vue 工程 axios 请求默认把参数放请求体,后端接口用 @RequestBody 接收,两端没对齐就会报“Required request body is missing”;日期字段后端返回 Date,前端直接展示变成一串英文时间,需要在 entity 里配置 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss");分页参数前端传 pageNum/pageSize,后端用 MyBatis-Plus 的 Page 对象接收,字段名缩写不一致也会影响查询。
我强烈建议后端所有时间字段统一返回格式化字符串,避免前端做第二次转换。在 SpringBoot 全局配置 Jackson 即可:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 default-property-inclusion: non_nulldefault-property-inclusion: non_null 是让 null 字段不出现在 JSON 里,前端不用处理一堆无意义的 null,响应体积也小一些。
5. 部署上线与常见问题排查
5.1 本地跑通项目的完整步骤
很多拿到这个项目源码的人,第一步就卡在不知道从哪里开始。我给你梳理一个标准启动流程,照着做一遍保证跑起来。
第一步,准备基础环境:JDK 8(配置 JAVA_HOME)、MySQL 5.7、Node.js 14+、HBuilderX(如果调试小程序)、Maven 3.6+。
第二步,初始化数据库:用 Navicat 新建数据库 petshop,导入项目里提供的 petshop.sql 文件。注意 MySQL 的字符集要选 utf8mb4,排序规则 utf8mb4_general_ci,不然中文乱码会一直缠着你。
第三步,启动后端:修改 application.yml 里的数据库账号密码,在项目根目录执行mvn spring-boot:run,看到 Tomcat started on port(s): 8080 就说明成功了。可以先用浏览器访问 http://localhost:8080/api/user/login 做登录测试。
第四步,启动管理后台:vue 项目目录下依次执行npm install、npm run serve。如果 node_modules 下载缓慢或失败,配置 npm 淘宝镜像源,执行npm config set registry https://registry.npmmirror.com。启动成功后访问 http://localhost:8081,用店长账号登录。
第五步,启动移动端:HBuilderX 导入 uniapp 项目目录,选择“运行-到浏览器”可以在电脑上预览 H5 版;选择“运行-到微信开发者工具”可以拉起小程序模拟器。
5.2 Linux 服务器部署要点
项目部署到 Linux 服务器,后端、管理后台、移动端三部分分别处理。
后端部署最稳妥的方式是用 Maven 打包成 jar,然后通过 systemd 或 nohup 方式启动:
# 打包(跳过测试,避免打包时跑单元测试失败) mvn clean package -DskipTests # 上传 jar 到服务器后启动 nohup java -jar petshop-server-1.0.0.jar > logs/server.log 2>&1 &管理后台打包成静态文件,交给 Nginx 托管:
npm run build # 将 dist 目录上传到服务器,Nginx 配置指向该目录Nginx 的配置里有两个关键点:一是 root 指向 dist 目录,二是 location /api 的请求代理到后端端口:
server { listen 80; server_name your.domain.com; root /usr/local/petshop/web; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /files/ { alias /usr/local/petshop/files/; } }记得管理后台路由如果用了 history 模式,还要加 try_files 配置,否则刷新页面 404。Vue 的 history 路由和 hash 路由的选择,不建议花时间折腾 history,毕设直接用 hash 路由即可,刷新页面不会出问题。
移动端打包成小程序后,直接在微信公众平台提交审核;打包成 App 就是 apk 文件,上传到应用市场审核。服务器如果没有备案域名,小程序端的 request 合法域名验证这步会卡住,这是毕设演示常常遇到的现实问题。
5.3 常见问题与排查技巧实录
从实际接手这个项目开始,我陆陆续续记录了不少问题,把最高频的整理成一张速查表,建议你直接收藏,出问题了对照着看。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 后端启动报 Access denied for user | 数据库密码错、数据库没建、账号没有远程权限 | 检查 application.yml 配置,先用 Navicat 测试连接 |
| 接口返回 404 | 请求路径不对、Controller 没加 @RestController、SQL 报错未启动事务 | 查看后端控制台日志,确认 Swagger 里的实际路径 |
| 前端请求跨域报错 | 后端未配置 CORS、Nginx 代理未生效 | 检查 CORS 配置类,确认代理地址写对 |
| 图片上传成功但访问 404 | 虚拟路径映射配置错误、磁盘路径不存在 | 检查 /files/** 的映射配置,确认上传目录已创建 |
| 登录成功但页面空白 | 前端 token 未存储、路由守卫阻塞 | 检查 localStorage 是否有 token,查看浏览器控制台报错 |
| uniapp 请求报 URL Scheme 不支持 | 小程序端请求了 http 协议 | 微信后台配置合法域名,开发工具勾选不校验域名 |
| 时间字段显示为时间戳或英文 | 后端未格式化日期 | 配置 Jackson date-format,或在前端过滤器处理 |
| 数据库中文乱码 | 连接字符集、表字符集不一致 | 统一 utf8mb4,连接 URL 加 useUnicode=true&characterEncoding=utf8 |
5.4 答辩讲解与配套文档写作思路
标题里提到“源码+lw+部署文档+讲解”,lw 就是论文,成套交付物要交代清楚才能价值翻倍。
论文结构常规展开为:绪论(研究背景、国内外现状)、相关技术介绍(SpringBoot、Vue、uniapp、MySQL)、系统分析(可行性分析、需求分析、用例图)、系统设计(架构设计、功能设计、数据库设计)、系统实现(各模块截图和关键代码)、系统测试(测试用例表、测试结果)、总结与展望。
文档写作最容易被老师批评的点是“功能写成了软件说明书”。论文里不要只写“系统可以实现宠物信息的增删改查”,而是要从问题出发——“传统宠物店手工登记宠物信息低效易错,本系统提供电子化宠物档案管理……”,把每个模块对应到具体业务痛点。另外,截图一定要完整,运行结果截图、数据库表截图、核心代码截图都要有,答辩 PPT 上也需要。
系统测试部分是很多人的薄弱项。至少准备 10 条左右的测试用例,覆盖登录、宠物新增、商品下单、预约确认、权限拦截这些核心场景。每条用例包含测试步骤、预期结果、实际结果,结尾写“测试结果全部通过,系统达到预期设计目标”。
图表的绘制推荐用 Visio 或者 Draw.io,架构图表示层级关系,用例图表示系统功能边界,流程图表示预约状态流转,E-R 图表示数据库设计。论文里这些图直接支撑答辩评分,图的规范性比画得好看重要得多。
5.5 讲解视频录制与演示环境的准备
“讲解等”这三个字,如果条件允许,值得准备一段 10 分钟左右的演示视频。录制前整理一个简单的脚本:先演示管理后台登录、新增宠物、商品上下架、订单管理;再演示移动端的注册、预约、下单;最后展示部署好的实际环境访问地址。
演示视频容易出问题的地方是操作太快,录制时每个操作停留两三秒,鼠标移动不要太快。关键页面停留时,简单口述目前的业务场景更能有讲清楚的效果。视频可以配合 OBS 录屏,分辨率 1080p,码率默认就行,文件别太大,微信传输和网盘都方便。
讲解环节其实比预想的更重要。很多同学项目代码写完了,但一被问到“为什么用 SpringBoot 不用 SSM”“Vue 和 uniapp 的关系是什么”,就支支吾吾。提前把技术选型理由、遇到的三个主要问题、系统的亮点和不足梳理一遍,在准备讲解时就很从容。系统不足的地方也提前想好,比如“目前缺少消息通知功能,未来可以接入微信模板消息”这种话术,反而显得思考深入。
6. 项目扩展:往生产级方向还能加什么
6.1 性能优化:缓存、索引与定时任务
如果精力允许,有几个点可以低成本地加进系统,让项目更有说服力。
Redis 缓存热点数据。宠物店的商品列表、服务项目、轮播图这类读取频繁、变更不频繁的数据,非常适合缓存。SpringBoot 集成 Redis 非常简单,用 Spring Cache 注解就能实现:
@Cacheable(value = "productList", key = "#categoryId") public List<Product> getProductList(Integer categoryId) { return productMapper.selectList(...); }第一次查询走数据库,后续走缓存,商品变更时用 @CacheEvict 清除对应缓存。这个点在答辩时讲“系统性能优化”,非常加分。
加索引优化慢查询。预约查询经常按日期和状态过滤,所以 appointment 表建议建联合索引 (appointment_date, status)。订单表的 order_no 唯一索引也必须有。用 EXPLAIN 看 SQL 执行计划,找到 type 为 ALL 的查询,逐条优化。
定时清理未支付订单。SpringBoot 的 @Scheduled 注解配合 cron 表达式,每 15 分钟扫描一次超时订单并取消,这是职业生涯里非常有用的一个实战小技巧。
6.2 功能扩展:消息通知与数据统计
宠物店系统的移动端目前以“预约+商城”为主,后续可以加的消息通知功能是很大的加分项。SpringBoot 整合 WebSocket,可以在用户预约被确认、商品发货时实时推送通知。实现方式不算复杂:前端 uni.connectSocket 建立连接,后端用 WebSocket 管理用户会话,业务操作发生时向指定用户推送消息。
数据统计可以在仪表盘上做文章。宠物店的经营分析可以包括:月度销售额变化趋势、各类服务项目的占比、热门商品排行、顾客复购率。后端可以用 SQL 的 GROUP BY 和日期函数写统计接口,前端用 ECharts 绘制折线图、饼图、柱状图,观感极其专业。搜索热词里有“java poi word能生成图表吗”,如果论文需要导出报表,Apache POI 在 Word 里使用 XWPFChart 可以生成简单图表,只是配置略繁琐,报表导出这种活儿,性价比最高的方式是前端导出 Excel 配合 ECharts 图片更优雅,后端不用硬扛。
6.3 移动端进阶:uniapp 与原生能力的结合
uniapp 的扩展方向主要围绕原生能力展开。搜索热词里提到“uniapp使用ios原生插件”“ios safari 使用 uniapp canvas 队列时导出白图”,这类问题属于原生功能调试范畴,也比较考验经验。像优惠券的分享海报生成、会员卡的预览模板,都依赖 canvas 画图。单据生成海报时,小程序端的 canvas 2d 接口参数和 App 端存在差异,真机适配时建议用条件编译分别处理。
音频播放和摄像头调用在宠物店场景里用得不多,但如果要做宠物短视频分享,就需要用 uni.createCameraContext 来调用摄像头。总体来看,uniapp 的原生扩展能力覆盖面很广,只要掌握条件编译和 manifest.json 模块配置这两个基本功,大部分功能都能实现。
写在最后
聊到这里,这套基于 SpringBoot/SSM + Vue + uniapp 的宠物店管理系统,从技术选型、数据库设计、后端接口、前端页面、移动端适配,到部署上线、论文答辩,就完整过了一遍。我做过不少类似的 Java 全栈项目,最大的感受是:这类系统真正的难点不在某一个技术有多深,而在把三端串起来的过程中,那些“文档没写但实际会踩”的坑。比如 JWT 拦截器放行 OPTIONS 请求,比如文件上传路径的斜杠问题,比如小程序端请求合法域名的限制——这些都是只有动手做过一遍才会记住的东西。
最后再分享一个实操习惯:调试这种多端项目,先从后端接口入手,用 Postman 把所有接口调通,再联调管理后台,最后调 uniapp 移动端。这个顺序能帮你把“问题到底出在哪一端”的范围快速缩小。如果你还打算用这个项目去面试,建议重点复盘预约模块的状态机设计和订单模块的库存扣减逻辑,把这两个地方讲透,比背一百道面试八股都管用。祝项目顺利跑通,答辩顺利。