ARTICLE DETAIL

资讯详情

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

基于Spring Boot和Vue的家政服务管理平台设计与实现

基于Spring Boot和Vue的家政服务管理平台设计与实现 简介一套基于Spring BootVueMysql的家政服务管理平台毕业设计项目面向Java全栈学习者或需要完整参考案例的高校学生。采用B/S结构使用Java与MySQL开发压缩包约91MB包含完整源码、毕业论文、答辩PPT、开发文档和演示视频目录清晰便于按需查阅。系统分为用户端和管理端用户端包含首页、服务信息、公告信息、留言反馈、个人中心等模块可实现在线预约、取消及评价管理员端涵盖用户管理、服务人员与服务类型管理、预约分配、进度跟踪、取消处理、评价信息及系统管理等核心功能。配套论文阐述需求分析、数据库设计与实现思路开发文档补充部署步骤演示视频则直观展示前台与后台的关键操作流程。已有60人学习适合作为毕业设计或课程项目的完整参考也可在此之上扩展功能、进行二次开发。 家政公司的订单流转通常卡在四个环节上客户约不到人、派单靠电话、服务进度不透明、完工后评价难追溯。这套基于Spring Boot、Vue和MySQL的家政服务管理平台正是围绕这四条链路设计的前台面向用户提供服务信息浏览、预约、留言反馈、个人中心后台面向管理员完成服务类型维护、服务人员管理、预约分配、进度跟踪和评价管理。项目整体采用B/S结构Java负责后端接口Vue负责前端展示MySQL负责数据存储非常适合做课程设计和毕业设计的参考也能支撑小规模家政公司的信息化改造。接下来重点拆四层架构怎么写、前后端怎么对接、预约到进度怎么流转以及打包部署时容易踩的几个坑。2. Spring Boot四层架构与MySQL表设计先对齐数据模型2.1 包结构划分与四层分工拿到这套家政服务管理平台我一般先不急着看页面而是直接把后端工程拉出来过一遍包结构。只要是Spring Boot项目不管业务是家政还是电商核心都跑不出四条链路接收请求的Controller、处理业务规则的Service、访问数据库的Mapper、对应数据表的Entity。这套家政平台也沿用了同样的组织方式常见的包结构大致是这样com.housekeeping ├── controller // 接收HTTP请求返回JSON数据不写业务逻辑 ├── service // 业务规则处理事务边界放在这一层 ├── mapper // MyBatis或MyBatis-Plus数据访问接口 ├── entity // 与MySQL表字段一一对应的实体类 ├── config // 跨域配置、拦截器、WebMvc相关配置 ├── common // 统一返回结果Result、业务异常、状态枚举Controller层只做参数接收和结果封装不直接操作数据库Service层承载预约、取消、分配这些核心逻辑事务注解加在Service方法上Mapper层负责SQL与Java类型之间的映射。这里有一个选型问题项目正文只写了Java和MySQL没有指定持久层框架。常见做法是用MyBatis-Plus单表CRUD不需要写XML分页插件也内置了开发效率比原生MyBatis高不少如果你更习惯Spring Data JPA也能跑通同样的业务但自定义的动态SQL比如后文查找可分配服务人员的SQL会麻烦一些。实体类用MyBatis-Plus的注解风格会很直观Data TableName(service_order) public class ServiceOrder { TableId(type IdType.AUTO) private Long id; private String orderNo; private Long userId; private Long serviceId; private Long staffId; private Integer orderStatus; private LocalDateTime appointTime; private LocalDateTime createTime; private LocalDateTime updateTime; }TableName指定表名TableId标记主键并声明自增策略字段命名遵循下划线转驼峰规则就能自动映射。把实体类定义清楚后面写Mapper接口和Service实现都会顺畅很多这也是整个后端的起步点。2.2 核心表结构与字段设计家政服务管理平台的业务表大概有十张左右其中最关键的是用户表、服务信息表和服务预约表。服务预约表是整个系统的中枢前后台的每一次操作几乎都在围着它转。我整理了一张核心表清单表名作用关键字段user前台用户与管理员username、password、role、statusstaff服务人员name、service_type、statusservice_info服务信息service_name、price、intro、statusservice_type服务类型type_nameservice_order预约主表user_id、staff_id、service_id、order_statusservice_cancel取消记录order_id、cancel_reason、cancel_timeservice_assign分配记录order_id、staff_id、assign_timeservice_progress进度记录order_id、progress_status、remarkevaluation评价信息order_id、score、contentmessage留言反馈user_id、content、reply用户表和服务信息表的建表语句没什么特别之处但服务预约表的状态字段值得多花一点心思。我见过不少人在这个字段上直接存字符串比如待处理、已完成这样查询时既要处理中文编码又不利于状态统计。更合适的做法是用TINYINT定义状态枚举在代码里维护一套状态字典CREATE TABLE service_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 订单主键, order_no VARCHAR(32) NOT NULL COMMENT 订单编号, user_id BIGINT NOT NULL COMMENT 下单用户ID, service_id BIGINT NOT NULL COMMENT 服务信息ID, staff_id BIGINT DEFAULT NULL COMMENT 被分配的服务人员ID, order_status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0待处理 1已分配 2服务中 3已完成 4已取消, appoint_time DATETIME DEFAULT NULL COMMENT 预约上门时间, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, KEY idx_status_create (order_status, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT服务预约表;订单号没有用自增ID直接展示给用户而是单独生成避免用户通过订单号推断出平台的每日订单量。生成规则常见的做法是JZ前缀加时间戳简单且基本够用如果并发量再大一些可以换成雪花算法。索引方面order_status和create_time经常出现在后台管理列表的查询条件里所以给它们建了一个联合索引user_id也建议建索引因为用户端“我的预约”列表会按这个字段高频查询。MySQL8默认使用utf8mb4字符集不用担心用户留言里带emoji导致写入失败。3. Vue前端与后端API对接Router懒加载、Axios封装与跨域配置3.1 前端路由设计与页面懒加载前端工程分为用户端和管理员端两套页面但实际是一个Vue项目里通过路由做的区隔。用户端包括首页、服务信息、公告信息、留言反馈、个人中心管理员端则挂在/admin路径下包含用户管理、服务人员管理、服务分配管理、服务进度管理等模块。路由配置里比较关键的一点是使用懒加载而不是在入口文件里一次性注册所有组件const routes [ { path: /, component: () import(/views/Index.vue) }, { path: /service, component: () import(/views/ServiceList.vue) }, { path: /message, component: () import(/views/MessageBoard.vue) }, { path: /admin, component: () import(/views/admin/Dashboard.vue), meta: { requiresAuth: true }, children: [ { path: staff, component: () import(/views/admin/StaffManager.vue) }, { path: order, component: () import(/views/admin/OrderManager.vue) } ] } ]component: () import(...)是动态导入的写法访问对应路由时才加载该组件的JS文件减少首屏包体体积。meta.requiresAuth配合路由前置守卫做登录校验未登录访问管理端时直接跳转登录页。管理端子路由用相对路径staff和order最终访问路径是/admin/staff和/admin/order这个层级结构跟用户输入里“服务分配管理”“服务进度管理”这些后台菜单是一一对应的。3.2 Axios封装与服务端Controller的对应前后端交互的起点是定一个统一的接口返回结构否则前端每个页面都要单独做异常处理。我习惯用Result对象统一包装结构是{ code: 200, message: ok, data: ... }。后端所有Controller都返回这个对象前端用Axios拦截器统一处理import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { if (res.code 401) { window.location.href /login } return Promise.reject(new Error(res.message)) } return res.data }, error Promise.reject(error) ) export default request拦截器里把code 200的数据直接返回页面里拿到的就是完整的data字段不需要每个组件重复判断。code 401统一跳转登录页比各页面单独处理更省事。后端对应的Controller长这样RestController RequestMapping(/api/service) public class ServiceInfoController { Autowired private ServiceInfoService serviceInfoService; GetMapping(/list) public Result list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size) { PageServiceInfo result serviceInfoService.page(new Page(page, size)); return Result.ok(result); } }这个接口接收page和size两个参数默认值分别是1和10也就是首页展示第一页、每页10条数据。Page对象来自MyBatis-Plus的分页插件返回给前端时会包含records当前页数据和total总记录数两个字段前端分页组件可以直接使用。前端的调用封装与后端URL保持同构import request from /utils/request export function getServiceList(params) { return request.get(/service/list, { params }) }这样后端接口路径一改前端只需要改这一个文件。用户端首页的服务信息列表、后台管理页面的服务信息管理全部复用同一个API参数不同而已。3.3 开发环境的跨域处理与PublicPath隐患前端开发服务器默认跑在8081端口后端Spring Boot跑在8080端口直接请求必然跨域。开发环境里最常见的解法是在vue.config.js配置请求转发让前端把/api开头的请求统一转发到后端地址module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }changeOrigin: true会把请求头里的Host改成目标地址避免后端在反向代理场景下拿到错误的来源信息。生产环境一般不用这个方案而是由Nginx托管前端打包后的静态文件同时把/api请求转发到后端Java进程。这里容易踩两个坑一是前端路由用了history模式部署到非根路径时刷新页面会404二是打包资源引用的是绝对路径放到子目录后样式和JS全部丢失。这两个问题在第五章还会具体说解法。4. 服务预约到进度分配状态机约束与Service层事务实现4.1 订单状态机的定义与流转边界家政服务管理平台的核心业务链路是用户提交预约管理员分配服务人员服务人员上门服务并更新进度完成后用户评价。这条链路里最需要设计严谨的就是service_order表的状态字段。如果状态流转不受约束就会出现管理员把“已完成”的订单重新分配、用户把“服务中”的订单取消之类的问题。实际开发中我用枚举常量配合Service层校验来控制流转边界状态值含义操作方下一步允许动作0待处理用户用户取消、管理员分配1已分配管理员管理员取消、更新进度2服务中管理员标记完成3已完成用户提交评价4已取消用户/管理员无从表里能看到一个关键规则只有待处理和已分配状态允许取消服务中之后必须走完流程。这个约束写在哪一层很关键放在Controller里容易被绕过放在Service里才靠得住。4.2 用户预约与取消的事务处理用户提交预约时前端把服务ID和预约时间传给后端后端生成订单号并写入预约表。这里有一个细节同一用户在同一时间段预约同一个服务系统应该去重拦截。我通常的处理方式是先查数据再插入但这会引入并发问题更稳妥的做法是在service_order表上加唯一索引或者在Service层做一次幂等校验。下面的示例偏向保留业务判断逻辑的写法Service public class ServiceOrderServiceImpl implements ServiceOrderService { Autowired private ServiceOrderMapper orderMapper; Autowired private ServiceCancelMapper cancelMapper; Transactional Override public void createOrder(OrderCreateDTO dto, Long userId) { ServiceOrder order new ServiceOrder(); order.setOrderNo(JZ System.currentTimeMillis()); order.setUserId(userId); order.setServiceId(dto.getServiceId()); order.setAppointTime(dto.getAppointTime()); order.setOrderStatus(OrderStatusEnum.PENDING.getValue()); orderMapper.insert(order); } Transactional Override public void cancelOrder(Long orderId, Long userId, String cancelReason) { ServiceOrder order orderMapper.selectById(orderId); if (order null || !order.getUserId().equals(userId)) { throw new BusinessException(订单不存在或无权操作); } if (order.getOrderStatus() OrderStatusEnum.SERVING.getValue()) { throw new BusinessException(当前状态不允许取消); } order.setOrderStatus(OrderStatusEnum.CANCELED.getValue()); orderMapper.updateById(order); ServiceCancel cancel new ServiceCancel(); cancel.setOrderId(orderId); cancel.setCancelReason(cancelReason); cancel.setCancelTime(LocalDateTime.now()); cancelMapper.insert(cancel); } }Transactional保证取消操作里“更新订单状态”和“插入取消记录”两步要么全部成功要么全部失败。假如第一步更新成功、第二步插入失败事务回滚后订单仍然是原状态不会出现订单已取消但取消记录缺失的脏数据。判断order.getOrderStatus() OrderStatusEnum.SERVING.getValue()利用了枚举值的顺序性服务中状态的枚举值大于等于2所以待处理和已分配两个状态都能通过取消校验这个写法比逐个if判断更简洁。4.3 服务分配与进度写入的联动管理员在后台把服务人员指派给订单这是整个系统里跨表操作最多的一步。首先需要选出一个合适的服务人员服务类型要匹配而且该人员当前不能已经在服务中或已分配。这个查询在Mapper里写动态SQL最方便Select(SELECT * FROM staff WHERE service_type #{serviceType} AND status 1 AND id NOT IN (SELECT staff_id FROM service_order WHERE order_status IN (1, 2))) ListStaff findAvailableStaff(Integer serviceType, LocalDateTime appointTime);这个SQL先按服务类型过滤再用子查询排除已经处于已分配或服务中状态的人员。管理员选人后代码要同时完成三件事更新订单状态和分配人员、写入分配记录、初始化一条进度记录。这三件事同样要放在同一个事务方法里Transactional public void assignOrder(Long orderId, Long staffId) { ServiceOrder order orderMapper.selectById(orderId); if (order.getOrderStatus() ! OrderStatusEnum.PENDING.getValue()) { throw new BusinessException(只有待处理订单才能分配); } order.setStaffId(staffId); order.setOrderStatus(OrderStatusEnum.ASSIGNED.getValue()); orderMapper.updateById(order); ServiceAssign record new ServiceAssign(); record.setOrderId(orderId); record.setStaffId(staffId); record.setAssignTime(LocalDateTime.now()); assignMapper.insert(record); ServiceProgress progress new ServiceProgress(); progress.setOrderId(orderId); progress.setProgressStatus(0); progress.setRemark(等待服务人员上门); progressMapper.insert(progress); }service_progress表设计成可追加的多条记录而不是只保留一个最新状态是因为用户端需要看到“等待上门、已出发、服务中、已完成”这样的完整时间线。每更新一次进度就往表里插入一条新记录评价页的进度时间线直接按create_time排序查询即可。5. Maven打包与MySQL初始化部署流程和常见运行坑5.1 后端打包与数据库初始化后端拿到源码后先用Maven打成可执行Jar包mvn clean package -DskipTests java -jar target/housekeeping-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod打包跳过测试能节省时间如果项目里配置了多环境用--spring.profiles.active指定启用application-prod.yml。数据库初始化的时候注意MySQL5.7和MySQL8的驱动类名不同8.x必须用com.mysql.cj.jdbc.Driver并且要带时区参数spring: datasource: url: jdbc:mysql://localhost:3306/housekeeping?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: yourpasswordserverTimezoneAsia/Shanghai是用来解决JVM与MySQL数据库时区不一致导致时间错乱问题的不配的话在按日期统计预约量时会偏差8小时。MySQL安装完成后用CREATE DATABASE housekeeping DEFAULT CHARSET utf8mb4建库再导入项目自带的.sql文件注意看建表语句里是否有外键依赖有外键就要按依赖顺序导入否则会报错。5.2 前端构建与打包后布局异常前端在项目根目录执行npm install安装依赖再用npm run build输出静态文件到dist目录。打包之后出现布局异常绝大多数情况是静态资源路径指错了。部署在Nginx根路径下通常没有问题部署到子路径就要显式设置publicPathmodule.exports { publicPath: process.env.NODE_ENV production ? /housekeeping/ : /, outputDir: dist, productionSourceMap: false }productionSourceMap: false关闭生产环境的SourceMap既减小包体又避免源码泄露。前端路由如果用了history模式Nginx还需要配置try_files把路由请求重新指向index.html否则用户直接在浏览器输入/admin/order刷新会得到404。偷懒一点的做法是路由直接用hash模式地址栏带#号部署时不用额外配置Nginx。5.3 安全配置与验证方法如果项目的pom.xml里引入了spring-boot-starter-actuator默认会把/actuator下的若干端点暴露出来生产环境中建议只保留health和infomanagement: endpoints: web: exposure: include: health,info这套家政平台后台涉及用户手机号、留言反馈等业务数据接口层面的鉴权不能省。验证部署是否正常可以先用curl拉一个公开接口看返回结构curl http://localhost:8080/api/service/list?page1size5返回{code:200,message:ok,data:{...}}就说明后端正常再用curl -I http://localhost:8081/检查前端静态资源是否加载。日志文件里重点观察MyBatis打印的SQL语句凡是某个页面数据不对多半能从SQL执行顺序和传入参数里直接定位到问题。本文还有配套的精品资源点击获取
返回列表