多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

SSM+JSP校园帮跑腿平台毕设:从订单状态机到部署踩坑全解析

SSM+JSP校园帮跑腿平台毕设:从订单状态机到部署踩坑全解析 很久没写这种完整的 JavaWeb 课程设计/毕设级别的项目了。前阵子帮一个学弟把关他的毕业设计题目就是这个“校园帮跑腿代办管理平台”。我一看技术栈——SSM JSP Layui ECharts MySQL心里大概就有数了。这组合放在今天虽然不算新潮但确实是校园里最“稳”的一套SSM 足够经典面试能聊JSP 上手直观代码结构一眼就能看懂Layui 和 ECharts 又解决了后端同学不太会写漂亮前端的痛点。整套做下来业务逻辑清晰数据流转直观答辩演示也拿得出手。这篇文章我想换个写法不给你贴一份完整的“一键复制粘贴”代码而是把整个平台从需求拆解、表结构设计、后端分层、前端渲染到最终部署踩坑的一条完整链路讲明白。因为我发现很多同学做这类项目最大的问题不是不会写代码而是不知道代码该怎么组织、表该怎么设计、一个订单从发起到完成中间到底经过哪些状态。这些才是答辩时老师真正会追问的东西。1. 跑腿平台的业务边界三类角色与六个核心功能点先说清楚这个平台到底“跑”的是什么业务。校园帮跑腿代办本质上是连接“有需求但没时间/没意愿跑腿的人”和“有时间且愿意跑腿赚钱的人”。放到系统里就是三类角色普通用户下单方发布代取快递、代买饭、代打印、代排队这类任务支付跑腿费确认收货后评价。跑腿员接单方浏览待接单任务抢单/接单完成配送获得跑腿费。管理员审核用户资质监管订单状态查看平台数据看板处理异常订单。这里我想强调一个很多同学容易忽略的点校园帮跑腿平台和外卖平台有个本质区别——它的下单方和接单方都是普通注册用户而不是平台自己的配送员。所以你在做需求分析时必须给“接单”这个动作留出足够的设计空间。是“抢单模式”先到先得还是“指派模式”管理员指定还是说可以两者并存学弟最初的需求文档里根本没写这个我直接建议他做成先到先得的抢单模式理由很简单校园场景跑腿员供给充足抢单能最大化配送效率同时系统实现也更简单不需要额外做任务分配算法。核心功能点我给他整理了六个用户注册登录与角色切换一个账号只能绑定一个角色但管理员可以在后台修改用户角色发布跑腿任务带地址、酬劳、物品描述、期望完成时间任务大厅与抢单列表筛选、分页、一键抢单订单状态流转待接单→已接单→配送中→已完成以及取消操作个人中心我发布的、我接到的、我的余额、我的评价管理后台用户管理、订单管理、数据统计可视化为什么要把功能砍到只有这六个因为这是个课程设计/毕设级别的项目不是创业产品。你的目的是把 SSM 这套框架、JSP 渲染方式、MySQL 设计能力完整展示出来而不是做一个功能堆砌的“大杂烩”。功能越多代码越乱答辩越容易翻车。六个核心功能每一个都能对应到 SSM 的某一层职责足够了。2. 订单状态机与数据库设计所有业务逻辑的源头很多同学做这类系统上来就建表结果表建了十几张字段加了五六十个写到一半自己都糊涂了。我的建议是反过来先把订单状态机画清楚再设计表结构。因为订单是这个平台的核心实体所有角色的操作本质上都是对订单状态的推动。2.1 四态流转模型参照状态机设计我把订单状态定义为四个0 待接单已发布等待跑腿员1 进行中包含已接单→配送中合二为一简化逻辑2 已完成用户确认收货3 已取消发布方主动取消或超时系统取消为什么把“已接单”和“配送中”合并成“进行中”你去查饿了么的订单会有很多状态但那是重度运营产品。校园跑腿的业务链条很短跑腿员接了单取到东西送到楼下这个过程的中间节点对于平台管理来说没有区分价值。你硬拆成两个状态就得引入“开始配送”和“送达”两个额外的操作按钮徒增代码量。合并后状态流转只需要处理四种变化发布0、接单0→1、完成1→2、取消0或1→3清爽利落。2.2 四张核心表的字段设计整个系统我最后保留了四张业务表 一张用户表的方案users —— 用户表字段类型说明idint主键自增usernamevarchar(20)登录名唯一索引passwordvarchar(64)MD5加盐后的密文phonevarchar(11)手机号roletinyint0普通用户 1跑腿员 2管理员balancedecimal(10,2)账户余额statustinyint0正常 1禁用create_timedatetime注册时间密码加密这一条我劝你别省。哪怕只是个课程设计明文密码在答辩时被老师看到印象分会大打折扣。用MD5(password salt)的方式就行简单有效还能讲出“防彩虹表”的设计意图。orders —— 订单表字段类型说明idint主键自增order_novarchar(32)订单号全局唯一publisher_idint发布人IDrunner_idint接单跑腿员ID可空item_namevarchar(50)代辦事项名称如“取快递”item_descvarchar(255)详细描述pickup_addrvarchar(100)取件地址快递点/食堂窗口delivery_addrvarchar(100)送达地址rewarddecimal(10,2)跑腿酬劳statustinyint状态机四态create_timedatetime发布时间accept_timedatetime接单时间finish_timedatetime完成时间订单号不能随便用主键自增要单独一个 order_no 字段。我推荐生成规则yyyyMMddHHmmss 三位随机数比如20250501103025783。原因有两个对外展示订单号时是流水编号方便用户报单号查单对内防止通过遍历id嗅探订单信息。实现上就是一个工具类的事但细节能体现出你有工程意识。order_logs —— 订单日志表字段类型说明idint主键order_idint关联订单IDoperator_idint操作人IDactionvarchar(20)动作publish/accept/finish/cancelremarkvarchar(255)备注create_timedatetime操作时间这张表是整个平台最有含金量的一张表。每一张订单的每一次状态变化都往里面插一条记录。这样当管理员在后台看到一张订单可以完整回溯出它的生命周期“谁在几点几分发布了任务谁在几秒后抢了单什么时候完成的”。答辩时你把这个设计讲出来基本就能证明你不是只会写 CRUD 的。evaluations —— 评价表字段类型说明idint主键order_idint关联订单user_idint评价人runner_idint被评价跑腿员ratingtinyint1-5星contentvarchar(255)评价内容create_timedatetime评价时间评价是完成的订单才能进行的操作。前台校验要注意只能评价自己发布且状态为已完成的订单。这不是业务冗余而是防止刷评的底线设计。2.3 索引与事务的取舍orders 表的查询场景主要有三个按发布人查用户中心、按跑腿员查跑腿员接单列表、按状态筛选任务大厅。所以建索引的时候ALTER TABLE orders ADD INDEX idx_publisher (publisher_id); ALTER TABLE orders ADD INDEX idx_runner (runner_id); ALTER TABLE orders ADD INDEX idx_status (status);事务方面最重要的就是“发布任务”和“接单”两个动作。发布任务时要同时插入 orders 表和 order_logs 表必须保证原子性接单时要同时修改 orders 的 runner_id、status、accept_time 并插入日志也要保证原子性。在 Service 层的对应方法上加Transactional注解就行。这里顺带一提如果你用 Spring 的声明式事务默认只对 RuntimeException 回滚所以 Service 方法里如果 catch 了 Exception一定要手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()或者干脆不 catch让它向上抛。3. SSM 三层的代码组织方式Controller-Service-Mapper 各司其职SSM 框架的项目结构本质上是把 JavaWeb 时代的 Model1 模式拆成了更清晰的三层。面试时老师最爱问“你们项目怎么分层”答案就在这个章节里。3.1 包结构规划com.campus.errand ├── controller // 控制层接收请求、参数校验、返回视图或JSON │ ├── UserController.java │ ├── OrderController.java │ ├── RunnerController.java │ └── AdminController.java ├── service // 业务层核心逻辑 │ ├── UserService.java │ ├── OrderService.java │ └── impl │ ├── UserServiceImpl.java │ └── OrderServiceImpl.java ├── mapper // 持久层接口对应MyBatis的Mapper │ ├── UserMapper.java │ ├── OrderMapper.java │ ├── OrderLogMapper.java │ └── EvaluationMapper.java ├── entity // 实体类 ├── dto // 数据传输对象如OrderQueryDTO、OrderStatsDTO ├── util // 工具类MD5加密、订单号生成、日期处理 └── interceptor // 拦截器登录检查、角色权限检查Controller 只做三件事接收参数、调用 Service、返回结果。Service 里只写业务规则。Mapper 里只有数据库交互。一旦你发现某个 Controller 里写了一长串 if-else 处理业务就该收敛到 Service 里去了。3.2 一个完整的接单流程看代码怎么走以“跑腿员接单”这个核心动作为例全链路是这样的第一步Controller 层接收请求并做基础校验Controller RequestMapping(/runner) public class RunnerController { Autowired private OrderService orderService; PostMapping(/accept) ResponseBody public Result accept(Integer orderId, HttpSession session) { User loginUser (User) session.getAttribute(loginUser); if (loginUser null) { return Result.error(请先登录); } if (loginUser.getRole() ! 1) { return Result.error(只有跑腿员才能接单); } orderService.acceptOrder(orderId, loginUser.getId()); return Result.success(); } }第二步Service 层处理核心业务Service Transactional public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Autowired private OrderLogMapper orderLogMapper; Override public void acceptOrder(Integer orderId, Integer runnerId) { // 查出订单 Order order orderMapper.selectById(orderId); if (order null) { throw new BusinessException(订单不存在); } // 校验状态只有待接单才能被接 if (order.getStatus() ! 0) { throw new BusinessException(订单已被接走或已取消); } // 校验不能接自己发的订单 if (order.getPublisherId().equals(runnerId)) { throw new BusinessException(不能接自己发布的订单); } // 乐观锁式的更新status0作为条件 int rows orderMapper.acceptOrder(orderId, runnerId); if (rows 0) { throw new BusinessException(手慢了订单已被别人接走); } // 记录日志 OrderLog log new OrderLog(); log.setOrderId(orderId); log.setOperatorId(runnerId); log.setAction(accept); log.setCreateTime(new Date()); orderLogMapper.insert(log); } }关键就在orderMapper.acceptOrder()这个方法。SQL 长这样UPDATE orders SET runner_id #{runnerId}, status 1, accept_time NOW() WHERE id #{orderId} AND status 0注意这个AND status 0这就是乐观锁的核心思想。哪怕两个跑腿员同时点击抢单数据库层面也能保证只有一条 update 能成功rows 0 的那个人就会收到“手慢了”的提示。这个设计我在很多企业项目里都见过用在这里算是降维打击了。你要在答辩里点出这一句比背十遍“单例模式”都管用。第三步Mapper 接口与 XML 配置。MyBatis 的 Mapper 接口里声明方法XML 里写 SQL。要注意设置useGeneratedKeystrue和keyPropertyid这样 insert 之后实体类里自动带上自增主键方便紧接着插日志。3.3 拦截器与过滤器权限控制的最佳实践这个项目有三种角色如果每个 Controller 方法里都写一遍if (loginUser.getRole() ! 1)那代码就没法看了。我建议把权限校验统一放到 SpringMVC 的拦截器里。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User loginUser (User) session.getAttribute(loginUser); if (loginUser null) { response.sendRedirect(request.getContextPath() /login.jsp); return false; } return true; } }然后再定义一个角色拦截器检查当前请求的路径前缀是否匹配用户角色。比如/admin/**路径必须 role2/runner/**必须 role1。在 springmvc.xml 里配置拦截路径mvc:interceptors mvc:interceptor mvc:mapping path/admin/**/ bean classcom.campus.errand.interceptor.AdminInterceptor/ /mvc:interceptor mvc:interceptor mvc:mapping path/**/ bean classcom.campus.errand.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors有一个容易踩坑的地方静态资源css、js、图片也会被拦截到。所以要么在 web.xml 里给静态资源单独配置一个 servlet mapping要么在拦截器中排除.css/.js/.png后缀。我习惯用 exclude 的方式mvc:exclude-mapping path/static/**/这个细节没处理好你会发现 Layui 的样式全丢了页面惨不忍睹。4. JSP Layui ECharts传统渲染与现代交互的结合JSP 是这套系统里最“古早”但也最稳的部分。你不需要用 Vue 或 React 去重构前端那会偏离 SSM 课程设计的考察范围。JSP 负责页面整体结构Layui 负责组件和交互ECharts 负责看板图表三者分工明确。4.1 任务大厅页面JSTL EL 渲染数据列表任务大厅是整个平台流量最大的页面展示所有待接单任务。JSP 里用 JSTL 循环渲染列表c:forEach items${page.list} varorder div classorder-card div classorder-title${order.itemName}/div div classorder-desc${order.itemDesc}/div div classorder-meta span取件${order.pickupAddr}/span span送達${order.deliveryAddr}/span span classreward¥${order.reward}/span /div div classorder-time发布时间fmt:formatDate value${order.createTime} patternyyyy-MM-dd HH:mm//div button classlayui-btn layui-btn-normal accept-btn>% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt %很多同学页面 500 报错十有八九是忘了引这两个 taglib。4.2 Layui 组件集成弹窗确认与分页抢单按钮的交互用 Layui 的 layer 弹窗组件做二次确认防止误触layui.use([layer, jquery], function () { var layer layui.layer; var $ layui.jquery; // 抢单按钮 $(document).on(click, .accept-btn, function () { var orderId $(this).data(id); layer.confirm(确认接下这个跑腿任务吗, {icon: 3, title: 抢单确认}, function (index) { $.post(/runner/accept, {orderId: orderId}, function (res) { if (res.code 0) { layer.msg(抢单成功, {icon: 1}); setTimeout(function () { location.reload(); }, 1000); } else { layer.msg(res.msg, {icon: 2}); } }, json); layer.close(index); }); }); });Layui 的 table 模块我也建议好好利用管理后台的订单列表用它渲染服务端返回 JSON 数据table 模块自动分页、自动排序能省掉大量手写 HTML 表格的功夫。4.3 ECharts 看板渲染管理后台的数据可视化管理后台需要展示两个重要的图表订单状态分布饼图和近 7 日订单趋势折线图。实现思路很简单后端提供一个 JSON 接口返回统计数据前端用 AJAX 拉取后塞给 ECharts。后端接口Controller RequestMapping(/admin) public class AdminController { Autowired private OrderMapper orderMapper; GetMapping(/stats) ResponseBody public Result stats() { ListOrderStatsDTO statusStats orderMapper.countByStatus(); ListOrderTrendDTO trendStats orderMapper.countLast7Days(); MapString, Object data new HashMap(); data.put(statusStats, statusStats); data.put(trendStats, trendStats); return Result.success(data); } }对应的 Mapper SQL 是统计任务的核心select idcountByStatus resultTypecom.campus.errand.dto.OrderStatsDTO SELECT status, COUNT(*) AS count FROM orders GROUP BY status /select select idcountLast7Days resultTypecom.campus.errand.dto.OrderTrendDTO SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS date, COUNT(*) AS count FROM orders WHERE create_time DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY DATE_FORMAT(create_time, %Y-%m-%d) ORDER BY date ASC /select前端页面用一个 .jsp 文件在 jQuery 的 ready 方法里初始化两个图表var statusChart echarts.init(document.getElementById(statusChart)); var trendChart echarts.init(document.getElementById(trendChart)); $.get(/admin/stats, function (res) { var data res.data; // 饼图数据组装 var statusMap {0: 待接单, 1: 进行中, 2: 已完成, 3: 已取消}; var pieData data.statusStats.map(function (item) { return {name: statusMap[item.status] || 未知, value: item.count}; }); statusChart.setOption({ title: {text: 订单状态分布, left: center}, tooltip: {trigger: item}, series: [{ type: pie, radius: 60%, data: pieData }] }); // 折线图数据组装 trendChart.setOption({ title: {text: 近7日订单趋势, left: center}, xAxis: {type: category, data: data.trendStats.map(function (item) { return item.date; })}, yAxis: {type: value}, series: [{ name: 订单量, type: line, data: data.trendStats.map(function (item) { return item.count; }) }] }); }, json);这里有个很常见的坑ECharts 的图表容器必须有固定高度。你写div idstatusChart如果没有设置 height图表会渲染不出来或者高度为 0。记得在 CSS 里给这两个 div 加上height: 400px。5. 并发场景与安全设计这个项目能挖出的加分项光把功能做出来只能及格答辩想拿高分必须能回答“你这个系统怎么应对并发”和“安全方面做了哪些设计”这两个问题。5.1 超时未接单自动取消校园跑腿平台有一个真实存在的问题任务发布后如果一直没人接单会一直挂在任务大厅里导致用户体验越来越差。解决方式发布任务时设置期望完成时间如果超过某个时限仍未有人接单系统自动取消。实现方式有两种一种是定时任务Spring 的 Scheduled轮询扫表另一种是懒取消——用户每次查询时顺带把超时的订单状态改掉。我推荐后者因为课程设计级别的项目引入独立的定时任务调度会增加复杂度。查询任务大厅的 Service 方法里加一步// 1. 先把超时订单批量更新为取消状态 orderMapper.cancelTimeoutOrders(new Date()); // 2. 再查询正常状态的订单列表批量更新的 SQLUPDATE orders SET status 3 WHERE status 0 AND expect_time NOW()这算是个比较聪明的取巧方案不需要额外引入 Quartz、不需要配置调度线程用最小的成本实现了业务闭环。你可以在答辩时主动讲这个设计动机老师会认为你考虑了真实业务场景。5.2 SQL 注入与 XSS 防护JSP 页面如果用 EL 表达式直接渲染用户输入的内容会有 XSS 攻击风险。比如用户在小任务描述里写了scriptalert(hack)/script页面加载时会直接执行。简单的解决方式是引入 JSTL 的转义标签c:out value${order.itemDesc} /c:out默认会转义 HTML 标签把它替换掉裸的${}输出。SQL 注入方面因为项目整个用的 MyBatisSQL 都写在 XML 里只要不用${}拼接参数就天然免疫 SQL 注入。这是 MyBatis 相比传统 JDBC 拼接 Statement 的最大优势也是答辩时的常规问题“你怎么防 SQL 注入”的标准答案——用#{}预编译本质上就是 PreparedStatement。5.3 接口幂等与请求频率控制抢单接口如果用户快速点了两下会触发两次请求。虽然乐观锁保证了第二次一定失败但可以在 Controller 层做更友好的拦截——用 Session 记录最近一次抢单时间10 秒内重复提交直接提示“操作过于频繁”。这个属于 Voluntarily 加分的细节代码量只有三五行但体现了防重意识。6. IDEA 运行配置项目最全踩坑指南这部分是我最想写给新手的因为哪怕代码全部正确环境配不对项目也跑不起来。学弟当时卡在启动环节整整浪费了一天我把他的问题整理成下面几个高频坑。6.1 JDK 版本选择与项目结构SSM 项目建议使用JDK 8不要用 JDK 11 以上的版本。不是不能用而是 Spring 5.x 对 JDK 11 的支持在部分老版本上会有兼容问题而且 JSP 在 JDK 11 之后被移除出默认模块额外配置很麻烦。IDEA 里设置 Project Structure 的 Project SDK 和 Language Level 都为 8同时确认 Maven 的 pom.xml 里也指定了 source 和 targetproperties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties6.2 Tomcat 配置与 Artifacts 的坑IDEA 里运行 JavaWeb 项目最绕的就是 Artifacts 配置。新手经常会犯的错是没把项目打成 war exploded 包关联到 Tomcat导致启动 Tomcat 后访问页面直接 404。正确的配置步骤File → Project Structure → Artifacts → 点击 → Web Application: Exploded → 选择你的模块在 Output Layout 里确认 lib 目录下包含了所有 Maven 依赖Run → Edit Configurations → 点 → Tomcat Server → LocalDeployment 标签页里点 → Artifact → 选刚刚配置的 exploded 包在 Server 标签页设置 Application Context 为/这样访问的时候不用带项目名这里还有个细节如果用Application Context /errand那么所有页面的跳转路径都得带上这个前缀我用的是/开发环境裸跑省心很多。6.3 MySQL 8.0 驱动的兼容问题MySQL 8.0 的 JDBC 驱动和 5.x 时代有两个很大的不同驱动类名变成了com.mysql.cj.jdbc.Driver连接 URL 必须带时区参数否则报Server returns invalid timezone错误正确的配置jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/campus_errand?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password你的密码useSSLfalse也很重要。MySQL 8 默认 SSL 连接如果你本地服务器证书有问题会报 SSL 相关错误加了这个参数直接走非加密连接。当时学弟看到Communications link failure的报错一脸懵其实就是时区参数没加。6.4 JSP 中文乱码的三层排查JSP 页面中文乱码是课程设计级项目的“保留节目”。一次彻底解决需要确认三层IDE 文件编码IDEA 里 Settings → Editor → File Encodings把 Global Encoding、Project Encoding、Properties Files 全部设为 UTF-8JSP 页面声明每个 JSP 顶部必须写% page contentTypetext/html;charsetUTF-8 languagejava %请求编码过滤器在 web.xml 里配一个 CharacterEncodingFilter注意这个玩意儿要放在最前面filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping如果全部设对了还是乱码检查 MySQL 的数据库编码。建库的时候执行CREATE DATABASE campus_errand DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;这就是标准的“三码合一”——页面、框架、数据库全部 UTF-8。7. 一套可以直接照抄的扩展优化思路系统做完了答辩演示通过了如果你想让它变得更“值钱”——无论是放到简历上还是作为开源项目展示下面这几个优化点都是低成本高收益的。7.1 本地内存缓存热门任务任务大厅的列表页可能被频繁访问但订单不是高频变更的数据。可以在 Service 层加一层简单的本地缓存第一次查库后将首页第一页数据放入ConcurrentHashMap设置 30 秒过期过期后重新查库。代码量不大但能讲出一套缓存思想。7.2 接口返回统一包装上面代码里我已经使用了Result这个统一返回类。它的结构是三件套code0成功1失败、msg提示信息、data业务数据。所有 Ajax 接口统一返回这个格式前端判断res.code就好。这个习惯必须养成后面对接任何前端框架都适用。7.3 登录验证码与错误次数锁定正式上线的话登录接口要做两层加固图形验证码用 Java 的 BufferedImage 手写一个或者引入 kaptcha连续输错 5 次密码锁定账号 15 分钟在 users 表加两个字段error_count和lock_time这两点不做也不会有人骂你做了一定是加分项。尤其答辩时老师几乎必问“安全方面做了哪些考虑”你顺势把这个讲出来就是全场最亮的崽。校园帮跑腿代办这个项目表面上看是个常规的管理系统但只要你把状态机设计、乐观锁防并发、日志表追踪、权限拦截器这套完整链路跑通它就是一份“麻雀虽小五脏俱全”的 SSM 综合体。代码写完不是终点能把这个项目的每一个设计决策讲明白、讲出理由才是真正把它吃透了。
返回列表