如果你正在找一个既能完整跑通前后端、又能讲清楚设计思路、还能拿来做毕设和面试项目的SSM练手场景,校园代送服务平台是我见过最接地气的选择之一。它不像电商系统那样庞大吓人,也不像图书管理系统那样千篇一律,业务逻辑恰好卡在“够用但又不简单”的位置上。不管你是准备毕业设计,还是想巩固Spring+SpringMVC+MyBatis这套经典组合,这个项目都值得认真做一遍。
我最近刚把一套基于SSM的校园代送服务平台从零到一完整实现出来,从需求梳理、数据库设计、接口划分,到前后端联调、部署上线,整个过程踩了不少坑。这篇就把当时的设计决策、开发步骤、排错经验全部写出来,重点讲清楚为什么这样做、哪里容易出问题、出了问题怎么查。想把这个项目做扎实的同学,按这条路线走,能省掉大量瞎折腾的时间。
1. 为什么选校园代送服务平台作为SSM练手项目
很多同学做SSM项目时,习惯去找商城、博客、学生管理系统这类模板。这些项目不能说没价值,但业务逻辑太简单,最后写进简历或者答辩时,很难体现你真正理解了三层架构和数据库建模。校园代送平台表面上是“跑腿订单”,实际上涵盖用户权限、订单状态流转、抢单派单、支付模拟、信息发布等多个完整模块,很适合用来展示工程能力。
1.1 业务场景的典型性
校园代送的实质是连接“发单用户”和“接单用户”的撮合平台。一个学生没时间去快递点取件,就在平台下单,写明取件地点、送达地址、期望时间、跑腿费;另一个学生有空闲时间,看到订单后接单并完成配送,最后双方确认完成。
这个场景天然包含几组核心关系:用户与订单、发单人与其代送需求、接单人与执行过程、管理员与服务监督。这样一来,CRUD不再只是抽象增删改查,而是被业务串起来:用户下单要校验地址信息,接单要操作订单状态,配送完成要触发结算逻辑。你在做这个项目时,每张表每个字段都能找到真实业务语义,这种“有目的感的编码”和“为了做功能而做功能”是完全不同的。
从技术学习角度看,它又不会像大型电商那样引入高并发、分布式事务等超纲概念。SSM这套框架组合承载这种量级的业务刚刚好:Spring负责对象管理和事务声明,SpringMVC负责请求路由和参数绑定,MyBatis负责SQL映射与数据库访问。三者分工清晰,初学者容易理解边界,又不至于觉得“太简单没东西可写”。
1.2 SSM三件套在这个项目里的实际分工
很多人学SSM时总喜欢一个问题一个框架孤立地看,等到真做项目就懵了。实际在一个业务里,三个框架是流水线配合:浏览器发来请求,Web容器把请求交给SpringMVC的DispatcherServlet,HandlerMapping找到对应的Controller方法,Controller里调用Service层接口,Service实现类在Spring容器中是单例Bean,需要操作数据库时就注入Mapper接口,Mapper接口对应MyBatis的XML文件,里面写了具体SQL。
放到校园代送项目里,一条典型链路长这样:用户在小程序或网页上点击“发布代送订单”,请求到OrderController的createOrder方法,参数被SpringMVC自动绑定到OrderDTO。Controller调用OrderService的createOrder方法,这个方法上加了@Transactional,内部先检查用户信用状态和订单参数,再插入订单主表数据,同时往订单日志表写入一条操作记录。MyBatis通过动态SQL完成两处数据插入,整个事务由Spring统一管理,任何一步失败都全部回滚。
这套流程跑通后,你就真正理解了为什么SSM不是“三个框架的简单相加”。Spring是黏合剂,SpringMVC解决Web层,MyBatis解决持久层,各干各的活,通过约定和数据模型连接起来。
1.3 功能模块全景
我当时梳理功能时,没有一上来就画表格写字段,而是先把角色列出来,再给每个角色配操作权限,最后把操作落到页面上。这样不容易漏功能,也方便后续写文档和接口清单。
系统一共拆成四大模块:用户模块、订单模块、管理模块、辅助模块。
- 用户模块:注册登录、身份切换、个人主页、信用分展示。学生有两种身份,一种是发起代送的发单人,一种是接单跑腿的接单人。账号体系早期可以都用同一个用户表,加一个user_type字段区分,登录后根据类型加载不同操作菜单。
- 订单模块:发布订单、订单列表、订单详情、抢单/取消/确认送达。这是整个项目的心脏。订单状态维护,我是用状态字段来控制的,初始状态是待接单,有人接单后变配送中,发单人确认后变已完成,取消订单要区分是发单人取消还是接单后取消,不同情况的退费逻辑不一样。
- 管理模块:管理员登录、用户管理、订单审查、数据概览。管理员不参与具体配送,主要看平台数据、处理异常订单、封禁违规用户。
- 辅助模块:公告管理、地址簿、消息通知。这些功能可以撑起列表查询和富文本框操作,也给项目增加丰富度。
这样一套模块下来,既有SpringMVC的Controller和过滤器使用场景,也有MyBatis复杂的多表关联查询和动态SQL场景,还有Spring声明式事务的典型应用场景。无论做课题答辩还是面试讲项目,都有足够的“料”可以讲。
2. 系统设计:从需求到数据模型
业务梳理完之后,下一步是设计。在这个阶段偷懒,后面写代码一定返工。我一般先做角色权限矩阵,再定核心流程,最后才落数据库表。顺序反了容易出现“表建好了但业务跑不通”的尴尬局面。
2.1 角色与核心业务流程
先看角色。校园代送平台有发单人、接单人、管理员三种角色。发单人可以发布代送需求、查看自己订单、确认收货、评价接单人。接单人最核心的权限是浏览待接单的订单池、抢单、执行配送。管理员负责平台侧的审核与监管。
再看流程。整个项目的业务主流程是一条状态机驱动的单链:
- 发单人填写代送信息,保存后生成待接单订单。
- 待接单订单进入公共订单池,任何接单人都能看到并抢单。
- 接单人抢单成功,订单变成配送中,同时锁定接单人,防止被重复抢。
- 接单人完成配送后,可以标记为待确认,也可以等待系统超时自动确认。
- 发单人确认收货后,订单变成已完成,平台记录完成时间,接单人获得跑腿费收入。
状态机这一点,我强烈建议你在设计数据库之前就画清楚。因为订单状态字段到底设几种枚举,哪些操作合法、哪些非法,全部由状态机决定。我当时就吃过亏,一开始给订单只设计了待接单、进行中、已完成三个状态,后来发现无法区分“发单人下单后正在选择接单人”和“接单人配送中”,导致查询列表时逻辑混乱,不得不回头加字段改代码。
2.2 数据库设计的核心表与要点
数据库设计是这个项目最值得讲的部分。我最终用了六张核心表,每张表都有明确的职责边界。因为采用的是MySQL,字符集我统一用utf8mb4,排序规则用utf8mb4_general_ci,这样能正确存储中文和表情符号,避免乱码问题。
用户表(user):id, username, password, nickname, phone, user_type, credit_score, status, create_time。密码必须加密存储,一般是MD5加盐或者用Spring自带的加密工具。如果你不想在答辩时被问住,建议不要自己造加密算法,直接讲清楚加盐原理就行。user_type字段用tinyint,0表示发单人,1表示接单人,2表示管理员。credit_score信用分初始设为100,每次违规扣分,扣到低于60分禁止发单。
订单表(orders):id, order_no, publisher_id, receiver_id, pickup_location, deliver_location, expected_time, fee, status, remark, create_time, finish_time。order_no我用时间戳加随机数生成,保证唯一性。status字段对应状态机中的不同状态,0待接单,1配送中,2待确认,3已完成,4已取消。receiver_id在订单刚创建时为空,抢单成功后更新。fee字段表示跑腿费,属于额度较小但逻辑敏感的字段,要防止被恶意篡改,实际操作中最好只在后端计算和更新。
地址表(address):id, user_id, contact_name, contact_phone, location, is_default。这是为了提升发单体验设计的。用户可以直接从常用地址里选,不用每次手输。很多同学可能觉得这个表可有可无,但我建议保留,因为这正好演示了MyBatis里的嵌套查询场景。
订单日志表(order_log):id, order_id, operator_id, action_type, action_desc, create_time。这张表非常有用,记录每一次状态变更。后面排查问题、写展示用例、回答面试官“如何追踪订单状态”这个问题时,全靠它。我当时该表被问到的次数远超订单主表。
公告表(notice)、管理员表(admin):这两张表逻辑比较简单,主要给管理端做列表展示。其中公告表可以直接用MyBatis的PageHelper分页插件做分页查询,顺便演示一下分页通用插件的用法。
设计这些表时,我会强调一个原则:能冗余就冗余,能标识就标识。很多学生做设计时舍不得冗余,喜欢把所有字段都拆成关联表,最后每次查询都要join四五个表,性能差不说,代码也不好写。比如订单表里我特意存了publisher_id和receiver_id两个用户外键,而不是通过关联用户表去取,这样列表展示时一次单表查询就能拿到发单人和接单人的ID,再配合用户表的二次查询或者联查,速度和代码可读性都更好。
2.3 接口设计原则
接口设计直接决定前后端联调是否顺畅。如果前后端都是你自己写,更要提前约定好统一返回格式,不然前端拿到的数据结构一会儿是Map一会儿是List,调试会抓狂。
我用的统一返回对象是一个Result类,包含三个字段:code、message、data。code=200表示成功,code=500表示业务异常,code=401表示未登录或权限不足。所有Controller方法返回的都是Result对象,SpringMVC通过@ResponseBody把对象转成JSON,前端再统一解析。这个模式现在很常见,学过SpringBoot的人可能习以为常,但在SSM项目里自己动手封装一遍,能加深对JSON序列化和HTTP状态码的理解。
接口路径按模块划分,比如:
- /api/user/register、/api/user/login、/api/user/info
- /api/order/create、/api/order/list、/api/order/detail、/api/order/accept、/api/order/finish、/api/order/cancel
- /api/admin/userlist、/api/admin/orderlist
接口之间不要交叉,保持单一职责。比如取消订单,不要在删除接口里顺带把订单状态改了,而是单独走cancel接口,业务逻辑彼此独立。
3. 手把手实现:从搭建骨架到跑通流程
设计阶段完成后,立刻进入编码。这里我按实际开发的顺序来讲,不是按照代码层级从Controller写到Mapper,而是按“能跑起来”的优先级组织,先搭工程,再做认证,再做核心业务,最后补前端和联调。这样整个过程始终能看到阶段性成果,不至于写了三天还看不到界面。
3.1 环境与工程结构
工具版本建议直接用经典稳定组合:JDK1.8、Tomcat8.5、Maven3.6、MySQL5.7或8.0。太高版本反而容易遇到兼容问题。IDE用IDEA,社区版也能跑,但付费版对Tomcat和数据库插件的支持更顺手。
创建一个Maven工程,packaging是war。如果你的IDE没有自带Tomcat运行插件,也可以直接把工程打包war丢进Tomcat的webapps目录,启动bin目录下的startup.bat访问。我建议开发阶段直接用IDE内置Tomcat,方便热部署和断点调试。
工程目录沿用标准结构:
- com.example.campusdelivery.controller(控制层)
- com.example.campusdelivery.service以及impl(业务层)
- com.example.campusdelivery.mapper(MyBatis的Mapper层)
- com.example.campusdelivery.entity(实体类)
- com.example.campusdelivery.interceptor(拦截器)
- com.example.campusdelivery.common(通用类)
- resources目录下放mybatis-config.xml、spring-mvc.xml、spring-mybatis.xml以及mapperXML
- webapp目录下放静态页面
这个结构不是随便分的,记住一条原则:调用方向必须是路由从上到下,Controller不允许直接操作SqlSession,Service不允许直接操作HttpServletRequest。如果发现代码里出现跨层调用,说明设计有问题,赶紧拆开。这样做的好处是后续AOP切面、事务管理、单元测试都能顺利搞定。
3.2 登录认证的实现
登录认证我用的方案是拦截器加Session。用户登录成功后,把User对象放进Session,写一个LoginInterceptor,在preHandle方法里检查当前请求路径是否在放行列表里。请求路径包含/api/user/register、/api/user/login的放行,其他路径全部校验Session里是否存在user对象。如果不存在,返回统一消息code=401。
很多学生用的方案是每个Controller方法里手动判断Session,这样做第一个缺点是代码重复,第二个缺点是很容易漏判。拦截器方案把登录控制收敛到一处,新增接口只要没有意外暴露,默认都要求登录。判断路径时可以简单判断是否包含“/admin/”,管理端用另一个AdminInterceptor做权限校验。注意只校验请求头或路径,不要试图在登录接口里用拦截器,否则会出现“登录时还没登录”的逻辑死锁。
密码部分我加密后存入数据库,我用的加盐方式是salt字段加密码拼接后进行SHA-256散列。注册时生成随机盐,登录时先根据用户名查盐和密文,再把用户输入的密码加盐散列,与库中密文对比。这样即便数据库泄露,密码也不是明文。
Session会话超时时间需要单独配置。Tomcat默认是30分钟,如果用户操作到一半去吃饭,回来发现会话失效被踢回登录页,体验很差。我在web.xml里设置了会话超时时间为60分钟,同时前端每次请求在Ajax的回调里对code=401做统一跳转,所以实际体验是当前接口返回未登录,前端跳去登录页而不是整个页面白屏。
3.3 订单状态机与业务闭环
订单模块是整个项目的核心,我单独说说实现思路。创建订单时,Controller接收前端传来的pickupLocation、deliverLocation、expectedTime、fee等字段,转成OrderDTO,调用OrderService.createOrder。Service里做几件必做事情:校验参数非空、校验用户状态正常、生成orderNo、插入订单、插入日志。整个过程一个事务,任何一个环节失败都回滚。
抢单接口是重点。当接单人点击抢单时,需要做两件事:先把订单状态从0改成1,再把receiverId设置为当前用户。这里有一个并发安全性问题,如果两个接单人同时抢同一单怎么办?只用简单的updateById加状态判断可能会出错,我用的是原子更新:
UPDATE orders SET status = 1, receiver_id = #{receiverId} WHERE id = #{orderId} AND status = 0然后检查受影响行数。如果受影响行数为1,说明抢单成功;如果为0,说明订单已经被别人抢走,返回“手慢了”。这一招在面试时真的很加分,它会向面试官证明你考虑过并发问题。这在SSM项目里用MyBatis一个动态update就能实现,不需要引入分布式锁,也不需要悲观锁,性价比很高。
完成配送和确认收货也走同样的路径。接单人把订单状态从1改成2,发单人把订单状态从2改成3。每次变更前先执行条件更新,再插入日志,保证日志记录与状态变更一致。日志内容除了actionType,还可以把旧状态和新状态都存进去,方便后面查历史轨迹。
3.4 前端页面与交互细节
说实话,SSM项目如果强制要求纯JSP,写起来确实头大。但很多课程设计和毕设的场景就是SSM项目,我建议不要为了炫技强行换Vue前后端分离,因为那样会偏离“学SSM”这个目标,还要额外处理跨域问题。我当时用的办法是JSP作为视图层,页面通过Ajax请求后端接口,后端返回JSON,前端用jQuery操作DOM渲染数据。
页面整体包括登录页、注册页、用户主页、订单发布页、订单列表页、订单详情页、管理后台几个主要页面。不需要做得多花哨,但交互细节要到位。我在发布订单页做了一个地址选择器,用户可以先维护常用地址,发布时直接从列表选取,不用手动重复输入。这个功能就是调地址表的数据,前端每次加载地址列表,后端提供一个/list接口。交互设计不用复杂,但能明显提升体验,也让你在答辩时有东西可以演示。
列表页我实现了按状态筛选和分页。筛选是前端用一个下拉框,把状态条件拼到Ajax请求的query参数里。后端先用PageHelper插件做分页,再按状态字段筛选。这里提醒一下,前端分页和后台分页不要混在一起。前端拿到PageInfo之后,渲染的是当前页的list,总页数由total字段控制。如果筛选条件变动,要重新加载列表,而不是停留在原页上。
3.5 本地部署与联调
本地联调我有一个固定流程:先开MySQL,验证数据库连接;再启动Tomcat看日志是否打印出Spring容器初始化的信息;然后用浏览器访问登录页,登录成功后打开控制台看网络请求。每一步都确认没问题,再进入下一步,不要一口气写五个接口然后一次性调试,那样出错了根本不知道是哪个环节的问题。
Tomcat端口我改成了8080。这里有个小细节,如果8080被占用,可以在conf/server.xml里改成8081或别的端口,但记得前端Ajax请求里的baseURL也要同步改,否则会出现页面能打开但数据加载不出来的情况。
数据库连接配置我放在jdbc.properties里,然后在spring-mybatis.xml里用context:property-placeholder引入。driver用com.mysql.jdbc.Driver,如果MySQL8以上要用com.mysql.cj.jdbc.Driver,连接URL里还要带serverTimezone=Asia/Shanghai,不然日期字段会报时区错误。这些小坑看起来不大,实际能卡一下午。
4. 典型问题与排查手册
这次开发中我记录了不少踩坑过程,这里挑最具代表性的几个写成排查实录,都是在SSM项目中高频出现、值得收藏的问题。
4.1 中文乱码问题
像这种平台上地址、备注全是中文,一旦乱码基本没法用。乱码有三个层面的来源:页面编码、Tomcat请求编码、数据库编码。页面方面我在JSP顶部设置contentType为text/html; charset=utf-8,模板本身也用UTF-8保存,这能解决大多数显示乱码。请求参数乱码则要在web.xml里配置CharacterEncodingFilter,强制所有请求和响应使用UTF-8。数据库层面,所有表字符集统一用utf8mb4,连接URL加characterEncoding=utf8参数。
这里有个容易忽略的坑:配置文件里数据库连接URL如果没有加characterEncoding,即便页面和过滤器都正确,存进去的中文入库后会变成问号。而且这个问题通常在我们往数据库插入数据时才暴露,排查起来绕一圈。建议一步到位,字符集相关的配置全都配齐。
4.2 404与请求路径问题
开发阶段404高频出现,大部分不是代码逻辑问题,而是路径写成相对地址或者少了前缀。在SpringMVC的配置里,DispatcherServlet映射的是斜杠,Controller方法上的RequestMapping和视图解析器前缀后缀都决定最终URL。改完Controller路径后,记得观察Tomcat控制台日志里的Mapping信息,看是否加载了你写的路径。
还有一种情况是JSP页面里的Ajax请求地址写成了相对路径,导致当前页面层级变化后请求路径不一致。我的习惯是全站Ajax请求路径都带上下文根,比如前后端分离部署inline不行,不用代码块包整体,但代码片段是允许的。避免“通过本文”. 使用编号.
另外不要出现“全文共计”等. 最后检查一遍安全合规. 没问题.
现在书写正文。会自我检查每个段落字数。注意不要有 # 主标题。从 ## 开始。
文章会比较长,要控制质量。开始。 如果你正在找一个既能完整跑通前后端、又能讲清楚设计思路、还能拿来做毕设和面试项目的SSM练手场景,校园代送服务平台是我见过最接地气的选择之一。它不像电商系统那样庞大吓人,也不像图书管理系统那样千篇一律,业务逻辑恰好卡在“够用但又不简单”的位置上。不管你是准备毕业设计,还是想巩固Spring+SpringMVC+MyBatis这套经典组合,这个项目都值得认真做一遍。
我最近刚把一套基于SSM的校园代送服务平台从零到一完整实现出来,从需求梳理、数据库设计、接口划分,到前后端联调、部署运行,整个过程踩了不少坑。这篇就把当时的设计决策、开发步骤、排错经验全部写出来,重点讲清楚为什么这样做、哪里容易出问题、出了问题怎么查。想把这个项目做扎实的同学,按这条路线走,能省掉大量瞎折腾的时间。
1. 为什么选校园代送服务平台作为SSM练手项目
很多同学做SSM项目时,习惯去找商城、博客、学生管理系统这类模板。这些项目不能说没价值,但业务逻辑太简单,最后写进简历或者答辩时,很难体现你真正理解了三层架构和数据库建模。校园代送平台表面上是“跑腿订单”,实际上涵盖用户权限、订单状态流转、抢单派单、支付模拟、信息发布等多个完整模块,很适合用来展示工程能力。
1.1 业务场景的典型性
校园代送的实质是连接“发单用户”和“接单用户”的撮合平台。一个学生没时间去快递点取件,就在平台下单,写明取件地点、送达地址、期望时间、跑腿费;另一个学生有空闲时间,看到订单后接单并完成配送,最后双方确认完成。
这个场景天然包含几组核心关系:用户与订单、发单人与其代送需求、接单人与执行过程、管理员与服务监督。这样一来,CRUD不再只是抽象增删改查,而是被业务串起来:用户下单要校验地址信息,接单要操作订单状态,配送完成要触发结算逻辑。你在做这个项目时,每张表每个字段都能找到真实业务语义,这种“有目的感的编码”和“为了做功能而做功能”是完全不同的。
从技术学习角度看,它又不会像大型电商那样引入高并发、分布式事务等超纲概念。SSM这套框架组合承载这种量级的业务刚刚好:Spring负责对象管理和事务声明,SpringMVC负责请求路由和参数绑定,MyBatis负责SQL映射与数据库访问。三者分工清晰,初学者容易理解边界,又不至于觉得“太简单没东西可写”。
1.2 SSM三件套在这个项目里的实际分工
很多人学SSM时总喜欢一个问题一个框架孤立地看,等到真做项目就懵了。实际在一个业务里,三个框架是流水线配合:浏览器发来请求,Web容器把请求交给SpringMVC的DispatcherServlet,HandlerMapping找到对应的Controller方法,Controller里调用Service层接口,Service实现类在Spring容器中是单例Bean,需要操作数据库时就注入Mapper接口,Mapper接口对应MyBatis的XML文件,里面写了具体SQL。
放到校园代送项目里,一条典型链路长这样:用户在小程序或网页上点击“发布代送订单”,请求到OrderController的createOrder方法,参数被SpringMVC自动绑定到OrderDTO。Controller调用OrderService的createOrder方法,这个方法上加了@Transactional,内部先检查用户信用状态和订单参数,再插入订单主表数据,同时往订单日志表写入一条操作记录。MyBatis通过动态SQL完成两处数据插入,整个事务由Spring统一管理,任何一步失败都全部回滚。
这套流程跑通后,你就真正理解了为什么SSM不是“三个框架的简单相加”。Spring是黏合剂,SpringMVC解决Web层,MyBatis解决持久层,各干各的活,通过约定和数据模型连接起来。
1.3 功能模块全景
我当时梳理功能时,没有一上来就画表格写字段,而是先把角色列出来,再给每个角色配操作权限,最后把操作落到页面上。这样不容易漏功能,也方便后续写文档和接口清单。
系统一共拆成四大模块:用户模块、订单模块、管理模块、辅助模块。
- 用户模块:注册登录、身份切换、个人主页、信用分展示。学生有两种身份,一种是发起代送的发单人,一种是接单跑腿的接单人。账号体系早期可以都用同一个用户表,加一个user_type字段区分,登录后根据类型加载不同操作菜单。
- 订单模块:发布订单、订单列表、订单详情、抢单/取消/确认送达。这是整个项目的心脏。订单状态维护,我是用状态字段来控制的,初始状态是待接单,有人接单后变配送中,发单人确认后变已完成,取消订单要区分是发单人取消还是接单后取消,不同情况的退费逻辑不一样。
- 管理模块:管理员登录、用户管理、订单审查、数据概览。管理员不参与具体配送,主要看平台数据、处理异常订单、封禁违规用户。
- 辅助模块:公告管理、地址簿、消息通知。这些功能可以撑起列表查询和富文本框操作,也给项目增加丰富度。
这样一套模块下来,既有SpringMVC的Controller和过滤器使用场景,也有MyBatis复杂的多表关联查询和动态SQL场景,还有Spring声明式事务的典型应用场景。无论做课题答辩还是面试讲项目,都有足够的“料”可以讲。
2. 系统设计:从需求到数据模型
业务梳理完之后,下一步是设计。在这个阶段偷懒,后面写代码一定返工。我一般先做角色权限矩阵,再定核心流程,最后才落数据库表。顺序反了容易出现“表建好了但业务跑不通”的尴尬局面。
2.1 角色与核心业务流程
先看角色。校园代送平台有发单人、接单人、管理员三种角色。发单人可以发布代送需求、查看自己订单、确认收货、评价接单人。接单人最核心的权限是浏览待接单的订单池、抢单、执行配送。管理员负责平台侧的审核与监管。
再看流程。整个项目的业务主流程是一条状态机驱动的单链:
- 发单人填写代送信息,保存后生成待接单订单。
- 待接单订单进入公共订单池,任何接单人都能看到并抢单。
- 接单人抢单成功,订单变成配送中,同时锁定接单人,防止被重复抢。
- 接单人完成配送后,可以标记为待确认,也可以等待系统超时自动确认。
- 发单人确认收货后,订单变成已完成,平台记录完成时间,接单人获得跑腿费收入。
状态机这一点,我强烈建议你在设计数据库之前就画清楚。因为订单状态字段到底设几种枚举,哪些操作合法、哪些非法,全部由状态机决定。我当时就吃过亏,一开始给订单只设计了待接单、进行中、已完成三个状态,后来发现无法区分“发单人下单后正在选择接单人”和“接单人配送中”,导致查询列表时逻辑混乱,不得不回头加字段改代码。
2.2 数据库设计的核心表与要点
数据库设计是这个项目最值得讲的部分。我最终用了六张核心表,每张表都有明确的职责边界。因为采用的是MySQL,字符集我统一用utf8mb4,排序规则用utf8mb4_general_ci,这样能正确存储中文和特殊字符,避免乱码问题。
用户表(user):id, username, password, nickname, phone, user_type, credit_score, status, create_time。密码必须加密存储,一般做法是加盐后做SHA-256散列。如果你不想在答辩时被问住,建议不要自己造加密算法,直接讲清楚加盐原理就行。user_type字段用tinyint,0表示发单人,1表示接单人,2表示管理员。credit_score信用分初始设为100,每次违规扣分,扣到低于60分禁止发单。
订单表(orders):id, order_no, publisher_id, receiver_id, pickup_location, deliver_location, expected_time, fee, status, remark, create_time, finish_time。order_no我用时间戳加随机数生成,保证唯一性。status字段对应状态机中的不同状态,0待接单,1配送中,2待确认,3已完成,4已取消。receiver_id在订单刚创建时为空,抢单成功后更新。fee字段表示跑腿费,属于逻辑敏感字段,只能在后端计算和更新,不能由前端任意传入。
地址表(address):id, user_id, contact_name, contact_phone, location, is_default。这是为了提升发单体验设计的。用户可以直接从常用地址里选,不用每次手输。很多同学可能觉得这个表可有可无,但我建议保留,因为这正好演示了MyBatis里的嵌套查询场景。
订单日志表(order_log):id, order_id, operator_id, action_type, action_desc, create_time。这张表非常有用,记录每一次状态变更。后面排查问题、写展示用例、回答面试官“如何追踪订单状态”这个问题时,全靠它。我当时该表被问到的次数远超订单主表。
公告表(notice)、管理员表(admin):这两张表逻辑比较简单,主要给管理端做列表展示。其中公告表可以直接用MyBatis的PageHelper分页插件做分页查询,顺便演示一下分页通用插件的用法。
设计这些表时,我会强调一个原则:能冗余就冗余,能标识就标识。很多学生做设计时舍不得冗余,喜欢把所有字段都拆成关联表,最后每次查询都要join四五个表,性能差不说,代码也不好写。比如订单表里我特意存了publisher_id和receiver_id两个用户外键,列表展示时一次单表查询就能拿到两个ID,再配合用户表的二次查询,速度和代码可读性都更好。
2.3 接口设计原则
接口设计直接决定前后端联调是否顺畅。如果前后端都是你自己写,更要提前约定好统一返回格式,不然前端拿到的数据结构一会儿是Map一会儿是List,调试会抓狂。
我用的统一返回对象是一个Result类,包含三个字段:code、message、data。code=200表示成功,code=500表示业务异常,code=401表示未登录或权限不足。所有Controller方法返回的都是Result对象,SpringMVC通过@ResponseBody把对象转成JSON,前端再统一解析。这个模式现在很常见,但在SSM项目里自己动手封装一遍,能加深对JSON序列化和HTTP状态码的理解。
接口路径按模块划分,比如:
- /api/user/register、/api/user/login、/api/user/info
- /api/order/create、/api/order/list、/api/order/detail、/api/order/accept、/api/order/finish、/api/order/cancel
- /api/admin/userlist、/api/admin/orderlist
接口之间不要交叉,保持单一职责。比如取消订单,不要在删除接口里顺带把订单状态改了,而是单独走cancel接口,业务逻辑彼此独立。
3. 手把手实现:从搭建骨架到跑通流程
设计阶段完成后,立刻进入编码。这里我按实际开发的顺序来讲,不是按照代码层级从Controller写到Mapper,而是按“能跑起来”的优先级组织,先搭工程,再做认证,再做核心业务,最后补前端和联调。这样整个过程始终能看到阶段性成果,不至于写了三天还看不到界面。
3.1 环境与工程结构
工具版本建议直接用经典稳定组合:JDK1.8、Tomcat8.5、Maven3.6、MySQL5.7或8.0。太高版本反而容易遇到兼容问题。IDE用IDEA,社区版也能跑,但付费版对Tomcat和数据库插件的支持更顺手。
创建一个Maven工程,packaging是war。如果你的IDE没有自带Tomcat运行插件,也可以直接把工程打包war丢进Tomcat的webapps目录,启动bin目录下的startup.bat访问。我建议开发阶段直接用IDE内置Tomcat,方便热部署和断点调试。
工程目录沿用标准结构:
- com.example.campusdelivery.controller(控制层)
- com.example.campusdelivery.service以及impl(业务层)
- com.example.campusdelivery.mapper(MyBatis的Mapper层)
- com.example.campusdelivery.entity(实体类)
- com.example.campusdelivery.interceptor(拦截器)
- com.example.campusdelivery.common(通用类)
- resources目录下放mybatis-config.xml、spring-mvc.xml、spring-mybatis.xml以及mapperXML
- webapp目录下放静态页面
这个结构不是随便分的,记住一条原则:调用方向必须是自上而下,Controller不允许直接操作SqlSession,Service不允许直接操作HttpServletRequest。如果发现代码里出现跨层调用,说明设计有问题,赶紧拆开。这样做的好处是后续AOP切面、事务管理、单元测试都能顺利搞定。
3.2 登录认证的实现
登录认证我用的方案是拦截器加Session。用户登录成功后,把User对象放进Session,写一个LoginInterceptor,在preHandle方法里检查当前请求路径是否在放行列表里。请求路径包含/api/user/register、/api/user/login的放行,其他路径全部校验Session里是否存在user对象。如果不存在,返回统一消息code=401。
很多学生用的方案是每个Controller方法里手动判断Session,这样做第一个缺点是代码重复,第二个缺点是很容易漏判。拦截器方案把登录控制收敛到一处,新增接口只要没有意外暴露,默认都要求登录。判断路径时可以简单判断是否包含“/admin/”,管理端用另一个AdminInterceptor做权限校验。注意不要试图在登录接口里用拦截器,否则会出现“登录时还没登录”的逻辑死锁。
密码部分我加密后存入数据库,我用的加盐方式是salt字段加密码拼接后进行SHA-256散列。注册时生成随机盐,登录时先根据用户名查盐和密文,再把用户输入的密码加盐散列,与库中密文对比。这样即便数据库泄露,密码也不是明文。
Session会话超时时间需要单独配置。Tomcat默认是30分钟,如果用户操作到一半去吃饭,回来发现会话失效被踢回登录页,体验很差。我在web.xml里设置了会话超时时间为60分钟,同时前端每次请求在Ajax的回调里对code=401做统一跳转,所以实际体验是当前接口返回未登录,前端跳去登录页而不是整个页面白屏。
3.3 订单状态机与业务闭环
订单模块是整个项目的核心,我单独说说实现思路。创建订单时,Controller接收前端传来的pickupLocation、deliverLocation、expectedTime、fee等字段,转成OrderDTO,调用OrderService.createOrder。Service里做几件必做事情:校验参数非空、校验用户状态正常、生成orderNo、插入订单、插入日志。整个过程跑在一个@Transactional事务里,任何一个环节失败都回滚。
抢单接口是重点。当接单人点击抢单时,需要做两件事:先把订单状态从0改成1,再把receiverId设置为当前用户。这里有一个并发安全性问题,如果两个接单人同时抢同一单怎么办?只用简单的updateById加状态判断可能会出错,我用的是原子更新:
UPDATE orders SET status = 1, receiver_id = #{receiverId} WHERE id = #{orderId} AND status = 0然后检查受影响行数。如果受影响行数为1,说明抢单成功;如果为0,说明订单已经被别人抢走,返回“手慢了”。这一招在面试时真的很加分,因为它证明你考虑过并发问题。这在SSM项目里用MyBatis一个动态update就能实现,不需要引入分布式锁,也不阻塞其他订单的读取操作,性价比很高。
完成配送和确认收货也走同样的路径。接单人把订单状态从1改成2,发单人把订单状态从2改成3。每次变更前先执行条件更新,再插入日志,保证日志记录与状态变更一致。日志内容除了actionType,还可以把旧状态和新状态都存进去,方便后面查历史轨迹。
3.4 前端页面与交互细节
说实话,SSM项目如果强制要求纯JSP,写起来确实头大。但很多课程设计和毕设的场景就是SSM项目,我建议不要为了炫技强行换Vue前后端分离,因为那样会偏离“学SSM”这个目标,还要额外处理跨域问题。我当时用的办法是JSP作为视图层,页面通过Ajax请求后端接口,后端返回JSON,前端用jQuery操作DOM渲染数据。
页面整体包括登录页、注册页、用户主页、订单发布页、订单列表页、订单详情页、管理后台几个主要页面。不需要做得多花哨,但交互细节要到位。我在发布订单页做了一个地址选择器,用户可以先维护常用地址,发布时直接从列表选取,不用手动重复输入。这个功能就是调地址表的数据,前端每次加载地址列表,后端提供一个/list接口。交互设计不用复杂,但能明显提升体验,也让你在答辩时有东西可以演示。
列表页我实现了按状态筛选和分页。筛选是前端用一个下拉框,把状态条件拼到Ajax请求的query参数里。后端先用PageHelper插件做分页,再按状态字段筛选。这里提醒一下,前端分页和后台分页不要混在一起。前端拿到PageInfo之后,渲染的是当前页的list,总页数由total字段控制。如果筛选条件变动,要重新加载列表,而不是停留在原页上。
3.5 本地运行与联调
本地联调我有一个固定流程:先开MySQL,验证数据库连接;再启动Tomcat看日志是否打印出Spring容器初始化的信息;然后用浏览器访问登录页,登录成功后打开控制台看网络请求。每一步都确认没问题,再进入下一步,不要一口气写五个接口然后一次性调试,那样出错了根本不知道是哪个环节的问题。
Tomcat端口我改成8080。这里有个小细节,如果8080被占用,可以在conf/server.xml里改成8081或别的端口,但记得前端Ajax请求里的baseURL也要同步改,否则会出现页面能打开但数据加载不出来的情况。
数据库连接配置我放在jdbc.properties里,然后在spring-mybatis.xml里用context:property-placeholder引入。driver用com.mysql.jdbc.Driver,如果MySQL8以上要用com.mysql.cj.jdbc.Driver,连接URL里还要带serverTimezone=Asia/Shanghai,不然日期字段会报时区错误。这些小坑看起来不大,实际能卡一下午。
4. 典型问题与排查手册
这次开发中我记录了不少踩坑过程,这里挑最具代表性的几个写成排查实录,都是在SSM项目中高频出现、收藏价值很高的经验。
4.1 中文乱码问题
像这种平台上地址、备注全是中文,一旦乱码基本没法用。乱码有三个层面的来源:页面编码、Tomcat请求编码、数据库编码。页面方面我在JSP顶部设置contentType为text/html; charset=utf-8,模板本身也用UTF-8保存,这能解决大多数显示乱码。请求参数乱码则要在web.xml里配置CharacterEncodingFilter,强制所有请求和响应使用UTF-8。数据库层面,所有表字符集统一用utf8mb4,连接URL加characterEncoding=utf8参数。
这里有个容易忽略的坑:配置文件里数据库连接URL如果没有加characterEncoding,即便页面和过滤器都正确,存进去的中文入库后会变成问号。而且这个问题通常在我们往数据库插入数据时才暴露,排查起来绕一圈。建议一步到位,字符集相关的配置全都配齐再开始开发。
4.2 404与请求路径问题
开发阶段404高频出现,大部分不是代码逻辑问题,而是路径写成相对地址或者少了前缀。在SpringMVC的配置里,DispatcherServlet映射的是斜杠,Controller方法上的RequestMapping和视图解析器前缀后缀都决定最终URL。改完Controller路径后,记得观察Tomcat控制台日志里的Mapping信息,看是否加载了你写的路径。
还有一种情况是JSP页面里的Ajax请求地址写成了相对路径,导致当前页面层级变化后请求路径不一致。我的习惯是全站Ajax请求路径都带上下文根,比如${pageContext.request.contextPath}/api/order/list。上下文根就是工程名,如果你把工程改名,所有前端请求拼接的基础路径也要同步调整。否则前端访问其他页面没问题,一刷新详情页就404,排查很久才发现是路径少了一个斜杠。
4.3 数据库连接与事务问题
数据库连接池我用的Druid,配了初始连接数5、最大连接数20,连接URL里设置useUnicode=true。这个配置要放在spring-mybatis.xml的dataSource里,MyBatis的SqlSessionFactory会引用这个数据源。常见问题有两个:一是连接池配置写错,导致启动报错无法注入;二是手抖把用户名密码写错,报Access denied for user,这时先单独用数据库客户端测一下凭证,再回看配置文件。
事务问题的经典场景是Service方法内部调用另一个Service方法。如果内部方法自己标注了@Transactional,而外部方法没有开事务,内部方法的回滚只能作用在自己的调用范围内,无法覆盖外部的其他数据库操作。更安全的做法是,事务边界尽量放在对外的服务方法入口上。校园代送平台里,抢单并写日志这段逻辑就应该整体在一个事务下,如果拆到两个Service方法分别管理事务,就可能出现“订单状态改了但日志没记”或者反过来,数据一致性很难保证。
4.4 MyBatis动态SQL的易错点
MyBatis的XML文件里写动态SQL,最常见的错误是if标签判断参数时用了错误的条件。比如状态筛选,前端传了status=0,但如果你在if里判断status != null,MyBatis会自动拆包装箱,这里还好;但如果判断的是status != '',遇到Integer类型0时会出问题,因为MyBatis的OGNL表达式在比较时可能把空字符串和数字混淆,导致条件不生效。解决方法是统一约定:前端不传的状态字段要么不放在请求里,要么传null,后端在实体类里用Integer接收,判断时只用status != null和status >= 0这种明确写法。
另一个常见问题是XML文件里的小于号需要转义。在写时间范围查询时,如果直接写<,XML解析器会报错。必须写成<或者用CDATA标记。我第一次写超时订单查询时就栽在这上面,日志里一直提示元素类型必须匹配,后来才发现是这里少了个转义。
5. 把项目做成能展示的作品
开发完成只是第一步,真正让这个项目“拿得出手”,还需要在演示准备、资料整理、讲解思路上面下功夫。很多同学代码写得不错,但一上场演示就手忙脚乱,或者被面试官一问就说不清设计考虑,非常可惜。
5.1 演示数据与演示脚本
在答辩或测试阶段,最好准备一套完整的演示数据,而不是临时随便插几条。我的做法是写一个data.sql文件,里面包含三个演示用户:一个发单人账号、一个接单人账号、一个管理员账号。订单数据也按照状态机不同阶段各准备几条,让每个功能按钮都有对应可视化结果。
演示脚本是更重要的东西。把操作路径固定下来,按顺序点,这样不会紧张忘词。我习惯的演示顺序是:注册新用户、维护一个常用地址、以发单人身份发布代送订单、切到接单人身份抢单、标记完成配送、切回发单人确认收货、去订单列表看状态变化、最后登录管理员账号查看订单总览。每一步配合一句话讲解,比如“现在可以看到订单状态从待接单变成配送中,因为抢单时我们用了一条条件更新,确保同一订单只能被抢一次”。
这里需要提前准备一份简洁的数据库说明文档,写清楚每张表的作用和关键字段。不用多长,但要能让你在回答“为什么订单表里要冗余userId”这种问题时拿出依据。
5.2 展示要点与答辩思路
答辩或讲项目时,常见误区是只讲我用了Spring、SpringMVC、MyBatis,然后就开始读日志。实际上对方更想听的是你对业务的理解和关键设计取舍。我建议准备三个“小专题”:
第一个专题是业务状态机。画一个订单状态迁移说明,讲清楚为什么订单会有5个状态而不是3个,以及状态变化如何保证数据一致性。这个专题能同时展示你的业务分析能力和事务意识。
第二个专题是登录认证链路。讲清楚拦截器如何工作、Session如何维护、密码如何加盐散列。这是面试官大概率追问的点,提前准备好回答比临场发挥稳得多。
第三个专题是并发抢单设计。重点讲那条条件更新SQL,说明为什么它比“先查询再更新”更安全。这个问题一旦讲透,面试官对你的好感度会明显上升,因为它直接体现你对多线程和数据一致性的理解,哪怕你没有实际接触过大型并发系统。
5.3 项目复盘与后续扩展
做得差不多后,我强烈建议做一次代码和设计的复盘。不要急着开始下一个项目,先把已经写过的代码通读一遍,看看有没有可以重构成更清晰的接口,有没有在Service里塞了太多Controller逻辑,有没有重复代码可以抽取。这个过程本身提升非常大。
如果想继续扩展,可以往几个方向加内容。比如增加基于Excel的订单导出功能,用SpringMVC的文件下载方式实现;比如接入一个简单支付模拟,把订单费用流转做成两条流水记录;再比如把Web端的接口抽出来,配一个移动端小程序。每个扩展方向都能带出新的技术点,且仍然围绕原有业务,不会破坏整体结构。
我在实际做这个项目时最大的体会是,不要总想着源码哪里看不懂、哪里背下来就好,而是要把自己当成平台的开发者,去思考每一个按钮背后需要哪些数据、每一次状态变化需要哪些约束。一旦你开始用这种思路去写代码,SSM就不再是一堆配置文件的堆砌,而成为一套顺手的工具。现在这套校园代送平台的完整过程我已经复盘到文档里,配合设计说明、SQL脚本和演示数据,就是希望后面做这个题目的同学能少走弯路,把时间花在理解业务和优化设计上,而不是浪费在配置报错和路径404上。
最后再分享一个小技巧:开发过程中把每次改动记录下来,不必写多正式,只要能回忆起“我为什么这样改”“当时报的是什么错”就行。这些小记录,最后整理成一篇开发笔记,比任何现成都保存的文档都有说服力,因为那是你真实走过的路。