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

资讯详情

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

基于SpringBoot的衣物干洗预约平台:从订单状态到并发控制的完整实践

基于SpringBoot的衣物干洗预约平台:从订单状态到并发控制的完整实践 做计算机毕业设计最怕的不是不会写代码而是题目选得太大或者太虚。基于SpringBoot的衣物干洗预约平台属于业务场景清晰、技术栈成熟、工作量刚好卡在毕设节奏里的题目。这个项目从用户端下单到门店接单再到洗护完成回传状态整条链路都能演示而且SpringBoot、MyBatis-Plus、Vue这些技术都是后端岗位面试里的高频内容做完之后简历上也能写。如果你是Java方向的应届生或者正在为毕业设计选题发愁这篇文章会把项目从选题、设计、编码到答辩整条线讲清楚。这类系统的核心并不难但很多学生在做的时候容易把边界划得太大一上来就加支付、加优惠券、加会员积分最后代码没写完论文也只能赶工。正确做法是先抓一条订单主流程把预约、接单、洗涤、完成这四个节点串起来然后再考虑扩展。本文会结合我指导项目时的一些习惯做法把框架选型、数据库设计、核心代码、常见坑和答辩准备都过一遍尽量让你少走弯路。1. 干洗预约平台的项目定位与技术选型1.1 需求拆解用户、门店员工、管理员这三类角色怎么分做系统设计第一步不是建表而是搞清楚谁会使用系统、每个角色会做什么操作。干洗预约平台最核心的角色有三种。用户端负责注册登录、浏览服务项目、选择门店和预约时间、填写衣物信息、提交预约订单、查看订单状态、取消未开始的订单、订单完成后进行评价。门店员工端负责处理预约订单包括接单、确认取件、将订单转为洗涤中、洗涤完成后改成待取回以及查看自己门店下的订单列表。管理员端负责维护基础数据比如门店信息、服务项目价格、员工账号、订单统计和系统公告。从业务角度理解用户提交的不是“立即支付”的购买订单而是“预约取件”的预约单。干洗服务大多是先接单、后洗护最后取衣时再支付所以订单状态设计要和普通电商区分开。如果把这个流程理解成“预约单”状态设计会自然很多。1.2 为什么这套技术栈最适合毕业设计SpringBoot在这个项目里的角色是后端服务框架它帮开发者省掉了Spring MVC里大量XML配置内嵌Tomcat可以直接打包成jar运行。配合MyBatis-Plus操作数据库Mapper接口写好之后简单的增删改查甚至连SQL都不用写分页插件也直接用。前端选择Vue Element UI前后端通过JSON交互这种模式在企业里很常见论文里也能画出清晰的前后端分离架构图。很多学生纠结要不要用Spring Cloud。干洗预约平台的业务量还没有到微服务级别一个单体应用完全能覆盖所有功能硬上微服务只会让部署和演示变复杂答辩时还容易被追问注册中心、网关、服务降级这些细节。毕业设计的目标是完整跑通业务闭环不是堆技术名词。版本选择上我更推荐SpringBoot 2.7.18它是2.x系列的收尾版本兼容JDK1.8网上教程和依赖资料都很多。SpringBoot 3以上版本要求JDK17包名也从javax换成了jakarta虽然新但对大多数学生来说迁移成本不低。如果你本机已经有JDK17能用3.x当然更好如果只有JDK8千万别硬升。1.3 数据库表设计订单表是绝对核心状态日志表一定要留干洗预约平台的数据库表不需要设计得特别多围绕业务主流程拆出七八张核心表就够了。用户表保存用户和登录信息包含账户、手机号、角色、昵称、创建时间。服务项目表保存干洗服务的名称、价格、预估时长比如普通上衣、羽绒服、毛毯的清洗价格和时间都不一样。门店表保存门店名称、地址、联系电话、营业时间。衣物信息表保存用户的衣物描述比如类型、品牌、颜色、材质备注如果不单独建表也可以把衣物信息冗余到订单表里。订单表是核心订单号、用户、门店、服务项目、预约时间、状态、支付状态、金额、取消原因这些字段都不能少。订单状态日志表记录每一次状态变更从哪个状态变成哪个状态、操作人是谁、备注是什么、什么时候变的。评价表在订单完成后生成主要存评分和点评内容。预约时段表用于控制某个门店在不同时间段的可预约名额防止用户扎堆。订单表不是简单的“状态字段一改就完事”我建议从一开始就把订单状态日志表加进去。一方面排查线上问题有据可查另一方面论文里可以画状态流转图答辩的时候也能说明自己对数据可追溯性有考虑。每个状态变更都在同一个事务里更新订单状态并写入日志这是成本很低但收益很大的设计。2. SpringBoot核心功能实现预约、订单、权限、定时任务2.1 预约下单用数据库原子更新解决时段超卖预约下单是系统里并发风险最高的接口。假设某门店某个时段只能接10单两个用户同时提交如果代码是“先查剩余数量大于0就插入订单”那么两个请求可能同时读到剩余数量为1然后同时下单最终这个时段实际接了11单数据就错了。解决办法很多最简单可靠的是在数据库层面做原子更新。预约时段表里有一个booked字段记录已预约数量下单时执行类似这样的SQLUPDATE schedule_time SET booked booked 1 WHERE store_id #{storeId} AND appointment_time #{appointmentTime} AND booked capacity这条SQL利用数据库的行级锁保证同一时间只有一个事务能成功更新。如果影响行数是1说明预约名额成功占用如果影响行数是0说明这个时段已经约满直接提示用户更换时间。这种方案不需要引入Redis逻辑清晰也容易在答辩时讲明白。订单号建议使用MyBatis-Plus自带的IdWorker也就是雪花算法生成。订单号在数据库里要加唯一索引哪怕并发再高也不会生成重复订单。下单的核心Service代码可以这样组织Transactional(rollbackFor Exception.class) public Long createOrder(WashOrderCreateDto dto) { Store store storeService.getById(dto.getStoreId()); ServiceItem item serviceItemService.getById(dto.getServiceItemId()); if (store null || item null) { throw new BizException(门店或服务项目不存在); } int rows scheduleTimeMapper.lockTime(dto.getStoreId(), dto.getAppointmentTime(), item.getDuration()); if (rows 0) { throw new BizException(该时段已约满请更换预约时间); } WashOrder order new WashOrder(); order.setOrderNo(IdWorker.getIdStr()); order.setUserId(dto.getUserId()); order.setStoreId(dto.getStoreId()); order.setServiceItemId(item.getId()); order.setAppointmentTime(dto.getAppointmentTime()); order.setAmount(item.getPrice()); order.setStatus(OrderStatus.WAIT_CONFIRM.getCode()); order.setPayStatus(PayStatus.UNPAID.getCode()); washOrderMapper.insert(order); orderStatusLogMapper.insert(new OrderStatusLog(order.getId(), null, order.getStatus(), 用户提交预约)); return order.getId(); }注意订单状态先设置为“待接单”。用户预约之后门店员工需要确认这个时段是否真的有条件接单确认后订单才正式锁定。这样设计也更符合真实门店操作。2.2 订单状态流转状态机在干洗订单里的落地方式订单状态不能随便改程序里要明确“从哪个状态能变到哪个状态”。干洗预约平台的订单状态我建议这样拆待接单是用户刚提交预约。已接单是门店确认接收此时可以生成取件码。洗涤中表示衣物已经进入洗护流程。待取回表示洗涤完成等待用户到店取衣。已完成表示用户确认取走订单结束。已取消表示未开始的订单被取消或者超时未确认被系统自动取消。每个状态的迁移都有前置条件。比如“待接单”只能变成“已接单”或“已取消”不能直接跳成“洗涤中”。代码里可以用一个状态流转表或者校验方法控制。状态变更的Service方法可以这样写public void changeStatus(Long orderId, Integer expectStatus, Integer targetStatus, Long operatorId) { WashOrder order washOrderMapper.selectById(orderId); if (order null || !order.getStatus().equals(expectStatus)) { throw new BizException(当前订单状态不允许该操作); } order.setStatus(targetStatus); washOrderMapper.updateById(order); orderStatusLogMapper.insert(new OrderStatusLog(orderId, expectStatus, targetStatus, operatorId, 状态更新)); }用户端操作“确认取回”时前端传过来的应该是订单号后端先从数据库查出当前状态判断是不是“待取回”只有是才能改成“已完成”。如果状态不匹配直接抛出业务异常。这种方式能避免用户或员工在页面反复点击时把状态改乱。2.3 登录认证用SpringBoot拦截器校验Token不推荐硬上Spring Security干洗预约平台包含用户、员工、管理员三个角色登录认证是必须的。很多学生一开始就想用Spring Security结果光是过滤链、密码加密、自定义登录页就卡了好几天。毕业设计阶段用JWT HandlerInterceptor做登录态管理已经足够代码量小、逻辑清晰、答辩也能讲清楚。JWT本身就是一段加密后的JSON字符串服务端签发之后前端每次请求把它放在请求头里。后端拦截器拿到Token解析出用户ID和角色放到ThreadLocal里Controller和Service里直接取当前用户。拦截器的核心逻辑如下Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { throw new BizException(401, 未登录); } Claims claims JwtUtil.parseToken(token); Long userId Long.valueOf(claims.get(userId).toString()); UserContext.set(userId); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }拦截器注册到WebMvcConfigurer里同时配置放行登录、注册接口等不需要认证的路径。Token方案相比Session的优势在于前后端分离时天然好用不需要考虑Cookie跨域后端服务也可以水平扩展。答辩如果问“为什么不用Session”可以回答Session依赖服务端存储分布式环境下多个实例之间需要共享Session而JWT本身是无状态的只要密钥一致就能校验。2.4 定时清理过期订单Scheduled每5分钟扫一次用户提交预约后可能一直不确认门店的员工也不可能随时盯着后台。为了保证预约时段不浪费系统需要自动取消超过一定时间仍未接单的订单。用SpringBoot内置的Scheduled就能实现。在启动类上加上EnableScheduling然后在定时任务类里写方法Scheduled(fixedDelay 60_000) public void cancelExpiredOrders() { ListWashOrder list washOrderMapper.listExpiredWaitConfirm(30); for (WashOrder order : list) { changeStatus(order.getId(), OrderStatus.WAIT_CONFIRM.getCode(), OrderStatus.CANCELLED.getCode(), SystemConstant.SYSTEM_OPERATOR); scheduleTimeMapper.release(order.getStoreId(), order.getAppointmentTime()); } }这里fixedDelay表示上一次任务执行完再等60秒和cron定时表达式相比fixedDelay更适合任务执行时间不固定的场景。查询超时订单时写一个带条件的SQL比如“状态为待接单且创建时间小于当前时间减去30分钟”每次批量处理100条避免一次性加载太多数据。还需要注意一点释放预约时段的时候要让schedule_time表的booked字段减1。否则用户取消或超时取消后时段名额不会恢复系统越用越拥堵。3. 联调阶段最容易踩的四个SpringBoot坑3.1 SpringBoot版本太高导致JDK8项目跑不起来有学生下载了SpringBoot最新的3.x版本然后在Java8环境下创建项目一启动就报错提示类文件版本不支持。这不是代码问题是版本兼容问题。SpringBoot 3.0之后强制要求JDK17并且原来javax开头的包全部变成了jakarta开头比如javax.servlet变成了jakarta.servlet。网上很多SpringBoot 2.x的教程直接拿来用会出现import报错。如果本机环境是JDK8就老老实实锁版本。可以在pom.xml里加parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent使用SpringBoot 2.7.18配合JDK8几乎所有常用依赖都能找到现成版本MyBatis-Plus、JWT、Redis、FastJSON这些生态也完全兼容。3.2 MyBatis-Plus分页插件总是没效果分页是管理后台列表页面最常用的功能。MyBatis-Plus使用分页时要配置内置拦截器如果没配置执行selectPage时虽然返回了Page对象但SQL里不会拼接LIMIT最后的查询结果是全表数据前端分页自然就乱了。MyBatis-Plus 3.5.x版本的正确配置是Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); interceptor.addInnerInterceptor(pagination); return interceptor; } }注意类名是PaginationInnerInterceptor不是旧版的PaginationInterceptor。如果用了旧包名项目可能直接启动失败也可能运行时不生效。配置好之后再调用Mapper的selectPage方法控制台日志里能看到自动拼接的LIMIT语句。3.3 前端跨域导致登录接口在浏览器里被拦截前后端分离开发时前端跑在http://localhost:8080后端跑在http://localhost:9090两个端口不同就属于跨域。如果后端不做处理浏览器会先发出OPTIONS预检请求接口如果没正确响应前端就看不到真正的业务数据。推荐在后端统一定义CorsFilter而不是在每个Controller上写CrossOrigin。一个全局过滤器就够了Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedHeader(*); config.addAllowedMethod(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }使用Token认证时前端要把JWT放在Authorization请求头里所以后端允许的请求头必须包含所有自定义请求头。如果前端开启了withCredentials后端不能使用addAllowedOrigin()而要使用addAllowedOriginPattern()否则浏览器会拦截。3.4 并发操作导致订单状态被覆盖后台员工可能同时打开两个浏览器标签页处理同一个订单或者在订单状态已经变成“已取消”之后旧页面仍提交“接单”请求。如果不加条件后一次的updateById会把状态覆盖掉造成状态回退。这就需要在更新订单状态时带上前置状态条件。可以定义一个专门的方法int rows washOrderMapper.updateStatusWithExpect(orderId, expectStatus, targetStatus); if (rows 0) { throw new BizException(订单状态已变更请刷新后重试); }对应的Mapper SQL类似UPDATE wash_order SET status #{targetStatus} WHERE id #{orderId} AND status #{expectStatus}只有当前状态确实是预期状态时更新才成功。这个思路和乐观锁的CAS理念一样数据库行级锁保证并发安全代码实现也很简单。4. 测试、演示数据与答辩准备4.1 用Postman把核心接口过一遍整理成接口测试清单系统写完之后不要急着截图写论文先用Postman把所有接口按业务流程跑一遍。接口测试清单可以按模块整理成表格。用户模块需要测试注册、登录、获取当前用户信息三个接口。服务模块测试查询服务项目列表、查看门店列表。预约模块测试提交预约、取消预约、查看我的订单列表、查询预约时段。员工模块测试接单、更新洗涤状态、完成订单。评价模块测试提交评价和查看评价列表。管理模块测试用户管理、门店管理、订单统计接口。每个接口要记录请求方式、请求路径、关键参数、成功返回和失败返回。比如提交预约接口成功时返回订单号失败时可能返回“该时段已约满”。Postman里的每个用例保存好后续写测试报告直接复用论文的“系统测试”章节就能填上内容。4.2 演示数据要提前准备好现场Demo不能现造答辩现场最怕的是登录之后发现服务项目为空、订单列表一片空白。准备演示数据是有技巧的。门店数据准备两三家服务项目准备五六个价格和预估时间要符合常识比如普通T恤清洗10元、羽绒服清洗45元、窗帘清洗80元。用户账号准备两个一个普通用户一个管理账号员工账号至少一个分配给某个门店。演示路径也应该提前走一遍。先用用户账号登录提交一个新订单选好门店和预约时间然后切到员工账号登录看到这个订单并接单再把状态改成洗涤中接着改成待取回最后切回用户账号确认取回订单提交一条评价。整个过程在数据库里会产生一条完整的订单状态日志答辩时直接展示状态日志表比空口讲设计有说服力得多。4.3 答辩高频追问怎么答要能说清每一个“为什么”答辩老师一般不会只问“这个系统有哪些功能”他们更关注设计理由和异常处理思路。问为什么选SpringBoot可以从配置简化、内嵌服务器、生态成熟三个角度回答。问下单并发怎么处理直接说预约时段表更新的原子操作把SQL写出来更稳妥。问订单状态如何在并发情况下不混乱把状态校验、状态日志表、乐观更新方案讲清楚。问为什么用JWT不用Session就围绕前后端分离和无状态认证展开。问定时任务如果多个实例部署会不会重复执行可以承认单体部署没这个问题如果分布式部署需要引入分布式锁比如Redis的setnx命令。答辩的核心思路是讲取舍。每个技术方案都有适用场景不要吹得天花乱坠把当前项目为什么选这个方案讲清楚老师自然认可。5. 还能往哪些方向加亮点5.1 从毕设上升到工程化的几个扩展方向如果核心流程已经跑通还有时间和精力可以从工程化角度增加一两个亮点。引入Redis缓存服务项目列表和门店列表可以减少数据库查询压力也能顺便讲缓存穿透、缓存更新这些面试常问点。引入WebSocket推送订单状态用户不需要反复刷新页面就能看到洗涤完成消息。接入消息队列比如RocketMQ或RabbitMQ把“订单完成”事件异步通知给用户这种设计在真实的系统里也非常常见。接入第三方支付沙箱用户在线支付预约费用系统会更完整。需要注意的是扩展功能不要贪多挑一个方向做到能演示、能讲清楚比堆三个半成品强得多。我自己比较推荐Redis缓存或WebSocket推送这两个方向在面试和论文里的表达效果都很好。5.2 安全优化XSS过滤和接口防刷很多学生在项目里完全没考虑安全但这其实是个很加分的点。全局过滤器可以统一处理XSS攻击例如把请求参数里的
返回列表