简介:面向Java毕设与课程设计场景的一份完整项目资源,基于SSM框架与JSP技术实现二手手机回收平台系统。系统涵盖管理员端、前台商城与用户端,包含手机品牌管理、手机回收、估价、订单、购物车、新闻资讯等模块,采用B/S结构与MySQL数据库,适合需要快速搭建可运行毕业设计项目或学习SSM整合流程的读者。压缩包共1354个文件,总大小22.03MB,以js、jsp、java、css、png、gif等类型为主,包含源码、LW(论文)和PPT答辩演示材料,另有SQL脚本、配置文件及环境说明。已有265人学习下载,资源内附完整可运行的程序,配套文档与数据库设计,可帮助理解系统从数据库建模到前后端交互的完整实现路径。资源中LW文档详细阐述系统设计思路与数据库建模过程,PPT可直接用于毕业答辩展示;同时内置MySQL初始化脚本和运行环境说明,可减少本地部署配置时间。
1. 二手手机回收平台的 SSM 项目源码:它到底值不值得你花时间跑通
“二手手机回收平台系统(ssm项目源码+LW+PPT).zip”这个标题,本质上是一个用 SSM(Spring + SpringMVC + MyBatis)搭起来的典型业务闭环:用户发起回收、系统估价、回收员接单、质检定级、确认成交。这类 java 毕业设计在每年提交的项目源码里出现频率极高,因为它不是单纯的学生管理系统那种单表 CRUD,而是带订单状态流转、双角色权限、估价计算的完整案例。适合谁呢?一种是已经拿到源码包却卡在导入 IDEA、改配置、启动 Tomcat 这一步的人;另一种是还没定题,想评估“做这个题目要写多少代码、论文和 PPT 怎么对得上”的人。这篇文章按照毕设 SSM 项目最常见的工程结构,从表设计开始拆,一直拆到启动、避坑和答辩准备。
2. 一个二手回收平台的 SSM 项目:表结构设计决定后面要不要返工
2.1 为什么这种题目用 SSM 而不是 Spring Boot:选型背后的真实逻辑
很多拿到源码的人第一反应是问:现在谁还用 SSM 写新项目?确实 Spring Boot 已经成为主流,但毕设场景里 SSM 依然大量存在,理由很实在:借这个标题练手的人需要把 Spring IOC、AOP、SpringMVC 路由、MyBatis 映射这些“底层概念”亲手配一遍,而不只是交给注解和自动配置。回收平台这个业务规模不大,十几张表、几十个接口,SSM 的 XML 配置虽然繁琐,但每一处配置都能在论文里写成一节“系统实现”,答辩老师问“你对 Spring 的理解”时,你能指着代码说他其实是用 IoC 容器管理 Service 和 Mapper 的。
在这种项目里,框架分工很明确:Spring 管的是对象创建和事务边界,SpringMVC 管的是 URL 到 Controller 方法的映射,MyBatis 管的是 SQL 和 Java 实体之间的转换。常见的包结构是这样的:controller 包放页面跳转和接口返回,service 包放业务逻辑,dao(或者叫 mapper)包放 MyBatis 接口,entity 包放数据库表对应的 JavaBean,interceptor 包放登录和权限拦截器。如果你拿到的 zip 包解压后没有这个分层,那八成是个结构混乱的代码,跑通容易,答辩讲清楚难。
还有一层原因和 java 面试有关:SSM 是面试题和“八股文”的高频区。哪怕是毕业设计答辩,老师也会顺着你的技术选型问 Spring AOP 怎么实现日志、MyBatis 怎么防止 SQL 注入这类问题。用 SSM 做回收平台,至少这些点都能在项目里找到对应落点:AOP 用在 Service 层事务上,MyBatis 的#{}防止注入,SpringMVC 拦截器做登录态校验。以我的经验,与其临时背一套 Spring Boot 的自动配置说辞,不如把 SSM 的 XML 配置吃透,答辩时反而更稳。
2.2 最少 7 张表:用户、手机信息、回收订单、估价记录、质检记录、回收员、公告
不管源码包里的 SQL 脚本长什么样,二手手机回收平台的业务表基本逃不出这 7 张:用户表、手机信息表、回收订单表、估价记录表、质检记录表、回收员表、公告表。复杂的版本还会加管理员表、收货地址表、支付流水表,但核心业务先要能站住。下面用订单表和手机信息表做示例,看关键字段怎么设计:
CREATE TABLE `recycle_order` ( `id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID', `order_no` VARCHAR(32) NOT NULL UNIQUE COMMENT '订单编号,前端展示用', `user_id` INT NOT NULL COMMENT '发布回收的用户ID', `recycler_id` INT DEFAULT NULL COMMENT '接单的回收员ID,初始为空', `phone_info_id` INT NOT NULL COMMENT '关联手机信息表', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待接单 1已接单 2质检中 3已定价 4已完成 5已取消', `estimate_price` DECIMAL(10,2) DEFAULT NULL COMMENT '用户下单时的预估价', `final_price` DECIMAL(10,2) DEFAULT NULL COMMENT '质检后的最终价', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '状态变更时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='回收订单表'; CREATE TABLE `phone_info` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `brand` VARCHAR(20) NOT NULL COMMENT '品牌:如 华为、小米、苹果', `model` VARCHAR(50) NOT NULL COMMENT '型号:如 iPhone 13', `storage` VARCHAR(10) COMMENT '存储:如 128G', `condition_level` TINYINT COMMENT '成色等级:1全新 2靓机 3微损 4损坏', `base_price` DECIMAL(10,2) COMMENT '此型号此成色的基础估价', `description` VARCHAR(255) COMMENT '用户补充描述' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='手机信息表';关键点有两个。第一,order_no建议用唯一编号而不是直接暴露自增 ID,一方面是为了前端展示好看,另一方面是避免别人通过遍历 ID 拿到全站订单数据,这在答辩时是可以拿出来讲的“安全性考虑”。第二,status字段用 TINYINT 而不是 VARCHAR,后面的状态流转代码全围绕着这个字段做前置校验,查询和统计也更快。如果你在源码里看到 status 是 String 类型且值是“待接单”“已完成”这样的中文,那这套项目的状态设计基本不具备可扩展性,后面加状态就要改一堆字符串比较的代码。
需要注意的是,建表脚本里的 CHARSET 一定要用 utf8mb4。很多源码包里的 SQL 文件是从 Windows 本机导出的,库表默认是 latin1 或 utf8,导入 MySQL 后页面显示的中文会变成问号。这个问题在第 5 章会专门展开。
2.3 解压 zip 后别急着点运行:先认清源码包里的三样东西
拿到“项目源码+LW+PPT.zip”这种命名格式的压缩包,第一件事不是双击 IDEA 图标,而是把压缩包的结构看清楚。常见做法是包里有三个目录:源码目录、LW(论文/设计说明书)目录、PPT(答辩演示文稿)目录。源码目录大概率长这样:里面有pom.xml或.classpath文件、src/main/java、src/main/resources、sql或db目录放着建库脚本,有的还带README.md。
这个步骤之所以重要,是因为 IDEA 打开项目的方式直接决定你后面会不会遇到“一堆红叉”或“找不到 Tomcat”的情况。我一般会先把 zip 解压到一个全英文路径下,比如D:/dev/recycle-ssm,然后用 IDEA 的Open选择源码根目录而不是整个 zip 解压出来的外层文件夹。如果 IDEA 提示Unlinked Maven Project,就点右侧 Maven 面板的刷新按钮,或者右键pom.xml选择Add as Maven Project。这些动作错了,后面即使是好端端的代码也会跑出各种莫名其妙的问题。
先不急着写配置,记下这两个信息就够了:Maven 坐标里的groupId和artifactId是什么,Java 代码的根包名是com.xxx.xxx什么开头。这两项决定了后面改配置文件时要对齐的位置,也是排查 class 找不到的关键线索。
3. 把回收业务写成代码:估价、订单状态机和双角色权限拦截
3.1 估价逻辑:查基础价表再乘以成色系数,不要硬编码在 Service 里
二手手机回收的核心动作是“提交手机信息 -> 得到估价”。毕业设计里最常见的估价实现有两种:一种是写死if (brand.equals("iPhone")) price = 5000,另一种是把估价规则落到数据库。前者省事,但老师一句“你这个价格是写死的,业务扩展怎么办”就能让答辩冷场。正确做法是在phone_info表里存base_price(基础价),再根据condition_level乘以不同系数。下面这段代码就写在EstimateService里:
public BigDecimal estimatePrice(PhoneInfo phoneInfo) { // 按品牌和型号查出基础价 PhoneInfo baseInfo = phoneInfoMapper.selectBasePrice( phoneInfo.getBrand(), phoneInfo.getModel(), phoneInfo.getStorage()); if (baseInfo == null || baseInfo.getBasePrice() == null) { throw new BusinessException("暂不支持该型号的估价"); } // 成色系数:全新1.0,靓机0.85,微损0.7,损坏0.45 BigDecimal conditionRate = getConditionRate(phoneInfo.getConditionLevel()); return baseInfo.getBasePrice().multiply(conditionRate); } private BigDecimal getConditionRate(Integer conditionLevel) { switch (conditionLevel) { case 1: return new BigDecimal("1.00"); case 2: return new BigDecimal("0.85"); case 3: return new BigDecimal("0.70"); case 4: return new BigDecimal("0.45"); default: throw new BusinessException("无效的成色等级"); } }逻辑说明:第一步走 MyBatis 的selectBasePrice按品牌和型号精确查基础价;第二步按成色等级折算。把系数抽成独立方法有两个好处,一是后续想改折扣率只动一个方法,二是可以在论文里写“估价规则模块化设计”。这里我给的是最简单的系数模型,实际源码里可能还会加“屏幕划痕减 X 元”“电池效率低于 80% 减 Y 元”这类表驱动规则,那就需要再建一张rule_detail规则表。如果能做到这一步,系统的估价模块在答辩时就是加分项。
参数说明:conditionRate用的是字符串构造 BigDecimal,而不是直接写0.85或new BigDecimal(0.85)。因为new BigDecimal(double)会产生浮点精度误差,这是 java 中做金额计算的典型坑,也正好是面试高频题“为什么金额不用 double”。你甚至可以把这句作为亮点讲出来。
3.2 订单状态机:status 字段加上前置校验,保证流程不能往回跳
回收订单是整个系统最核心的表,因为所有角色都围着订单转:用户创建订单,回收员接单,质检员录入质检结果,系统定价,最后完成。如果只在 Service 里简单地order.setStatus(2); orderMapper.update(order),就会出现一个严重问题:订单可以被任意改成任何状态,甚至从已完成退回质检中。状态机在这里的意义不是引入复杂设计,而是给状态的每次流转设置门槛。
下面这段用 Spring 的声明式事务来保证状态扭转的一致性:
@Transactional(rollbackFor = Exception.class) public void completeOrder(Integer orderId, Integer operatorId) { RecycleOrder order = orderMapper.selectById(orderId); if (order == null) { throw new BusinessException("订单不存在"); } // 只有状态为3(已定价)的订单才能被完成,否则拒绝 if (order.getStatus() != 3) { throw new BusinessException("当前状态不允许完成订单,状态码=" + order.getStatus()); } RecycleOrder update = new RecycleOrder(); update.setId(orderId); update.setStatus(4); update.setFinalPrice(order.getFinalPrice()); int rows = orderMapper.updateByPrimaryKeySelective(update); if (rows != 1) { throw new BusinessException("订单状态更新失败,请重试"); } }逻辑说明:completeOrder这个方法的语义很明确,它只负责“从已定价到已完成”这一步。先查订单,再校验当前状态必须等于 3,然后用updateByPrimaryKeySelective只更新非空字段。这里用@Transactional是习惯,因为真实项目里完成订单往往连带写财务流水、扣库存、发通知三件操作,任何一步失败都要整体回滚。如果源码包里所有业务方法都裸奔没有事务注解,那项目质量就有问题。
参数说明:rows != 1是典型的“影响行数校验”。为什么要判这个?因为 MyBatis 更新默认返回受影响的行数,如果你更新的条件不唯一,可能一次更新多行,而实际业务期望只更新一行。更重要的是,在高并发场景下两个回收员同时接同一单,先更新的那个把 status 从 0 改成 1,后更新的那个因为update ... where id=? and status=0匹配不到行,返回 0,直接被拦截。这就是所谓的乐观锁思想,答辩时很值得讲。
3.3 权限控制:一套 HandlerInterceptor 按角色放行接口,别把权限写在每个 Controller 里
二手回收平台至少有三种角色:普通用户、回收员、管理员。最省事的实现是在每个 Controller 方法里写if (session.getAttribute("userId") == null) return "redirect:/login.jsp",但这会导致代码重复且漏写。SSM 项目里常规做法是用 SpringMVC 的拦截器统一处理。
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); Object user = session.getAttribute("loginUser"); Object recycler = session.getAttribute("loginRecycler"); String uri = request.getRequestURI(); // 回收员端接口以 /recycler/ 开头,必须登录回收员账号 if (uri.contains("/recycler/") && recycler == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return false; } // 用户端接口以 /user/ 开头,必须登录用户账号 if (uri.contains("/user/") && user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return false; } // 部分公开接口直接放行 if (uri.endsWith("/index") || uri.endsWith("/estimate")) { return true; } return true; } }这个拦截器的作用边界是:只要请求路径匹配前缀,就必须有对应登录态,否则跳回登录页。它比每个 Controller 里重复判断要优雅得多,而且你可以在论文里写“采用了 SpringMVC 拦截器实现统一认证”。
需要说明的是,光写这个类还不够,还要在 springmvc.xml 里注册:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/static/**"/> <mvc:exclude-mapping path="/login.jsp"/> <mvc:exclude-mapping path="/user/login"/> <bean class="com.xxx.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>参数说明:/static/**必须放行,否则页面的 CSS、JS、图片全部被拦,页面打开是裸 HTML。这个配置经常被漏掉,表现为“登录页没样式、点按钮没反应、F12 全是红色 404”,在第 5 章会单独讲。
4. 用 IDEA 和 Tomcat 跑通 SSM 项目源码:解压到看到首页的完整步骤
4.1 环境准备:JDK 1.8、Maven 3.6.3、MySQL 5.7、Tomcat 8.5 这套组合最常见
跑 SSM 项目不要盲目追求最新版本。JDK 8、Tomcat 8.5、MySQL 5.7、Maven 3.6.3 是这批项目源码最常见的组合,换任何一环都可能踩到版本不兼容的坑。比如把项目跑在 Tomcat 10 上会报ClassNotFoundException: javax.servlet.*,因为 Tomcat 10 已经 Jakarta EE 化了,很多毕业设计源码根本没适配。还有 Maven 如果装的是 3.9+,配旧版maven-compiler-plugin也可能编译失败。
打开pom.xml确认依赖版本时,重点关注这几项:
<properties> <java.version>1.8</java.version> <spring.version>5.1.8.RELEASE</spring.version> <mybatis.version>3.4.6</mybatis.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> <scope>runtime</scope> </dependency> </dependencies>参数说明:spring-webmvc和spring-context的版本必须一致,否则会出现NoSuchMethodError这类极其迷惑的运行时错误。mysql-connector-java5.1.47 对应 MySQL 5.7,如果数据库换成了 MySQL 8,驱动版本也要换成 8.0.x,并且 JDBC 驱动类名从com.mysql.jdbc.Driver变成com.mysql.cj.jdbc.Driver,这是检查源码包时最容易漏的地方。
这一环节还有一个常被忽略的问题:IDEA 里如果pom.xml引入依赖后一直下载失败,检查 Maven 的 settings.xml 镜像。用阿里云镜像加上http协议时,新版 Maven 默认禁用了 HTTP 仓库地址,需要在 IDEA 的 Maven settings 里临时允许,或者干脆把镜像地址改成 HTTPS。这是很多新手卡在“依赖飘红”的真实原因,和代码本身没关系。
4.2 修改 5 处配置就能启动:数据库连接、JDBC 驱动、包名扫描、web.xml 和部署名
SSM 项目的配置文件主要集中在src/main/resources和WEB-INF下。拿到源码后要改的位置就这几个,我按优先级排:
第一个是数据库连接,通常叫jdbc.properties或db.properties,内容长这样:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/recycle_db?useUnicode=true&characterEncoding=utf8&useSSL=false jdbc.username=root jdbc.password=你的数据库密码修改的关键点在于jdbc.url里的数据库名必须和你在 MySQL 里建的库名一致,characterEncoding=utf8必须保留,否则第 5 章的乱码问题跑不掉。如果你的 MySQL 是 8.x,driver要改成com.mysql.cj.jdbc.Driver,同时 url 里建议加serverTimezone=Asia/Shanghai,不然时间会差 8 小时。
第二个是 MyBatis 的 mapper 扫描。打开spring-mybatis.xml或applicationContext.xml,确认这一段:
<mybatis:scan base-package="com.xxx.mapper"/>base-package必须和你 Mapper 接口所在的包完全一致,少一个字母都扫不到。同时检查mybatis-config.xml里有没有把 mapper XML 的位置指对,常见写法是:
<mappers> <mapper resource="mapper/RecycleOrderMapper.xml"/> </mappers>第三个是 SpringMVC 的组件扫描和视图解析器。在spring-mvc.xml里确认:
<context:component-scan base-package="com.xxx.controller"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean>注意<context:component-scan>只扫 controller 包即可,如果在 Service 包也扫了 controller 注解,容易把事务配置搞乱,表现为“Service 方法上的 @Transactional 不生效”。
第四个是web.xml的 DispatcherServlet 映射:
<servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>这里的关键是contextConfigLocation:有的项目把 Spring 的 applicationContext.xml 和 SpringMVC 的 spring-mvc.xml 分开,有的是合并成一份。如果你只改了classpath:后面的文件名,但实际 resources 里是另一个文件名,启动就会直接报FileNotFoundException。
第五个是 IDEA 里的部署名。跑 SSM 项目通常用 war exploded 方式部署,路径是http://localhost:8080/recycle_ssm/login.jsp,其中的recycle_ssm是项目的 Application context。如果不知道项目名,打开项目的pom.xml看<finalName>标签,或者 IDEA 里 Run Configuration 的 Deployment 页面。很多源码里的页面跳转用的request.getContextPath()是动态获取的,所以 context 名字取什么影响不大,但如果你手动在浏览器输错路径,就会 404。
4.3 启动与验证:从控制台日志到数据库落库
配置改完,先启动 MySQL 并执行源码包里 sql 目录下的建库脚本。用 Navicat 或命令行执行时注意先CREATE DATABASE recycle_db DEFAULT CHARACTER SET utf8mb4;,再 USE 这个库,然后source xxx.sql。如果直接双击运行老旧的 SQL 文件,很容易出现“Unknown database”或乱码导入。
然后配置 Tomcat:IDEA 里 Run -> Edit Configurations -> 左上角加号 -> Tomcat Server -> Local。在 Deployment 选项卡里添加 Artifact,选xxx:war exploded,Application context 写/recycle_ssm占位符也行。启动后看这几行日志:
Initializing Spring root WebApplicationContext:Spring 容器加载成功。Registering beans ...:MyBatis 的 mapper 被扫描到。Starting ProtocolHandler ...:Tomcat 端口 8080 开启成功。
浏览器访问登录页,输入测试账号。如果源码包里没有附带测试账号,去user表里看有没有初始化的 SQL 数据,没有的话自己INSERT INTO user(username,password,nickname,role) VALUES('admin','123456','测试用户',1);。需要注意密码字段如果是 MD5 存储的,直接插明文是登录不了的,此时要先用一个简单的 Controller 接口注册,或把 MD5 算法跑一遍拿到摘要字符串再插库。这一步看起来小,但不少人卡在“页面能打开却登录不了”,最后发现是密码加密方式不匹配。
启动完别急着关,顺手去 MySQL 里执行SELECT * FROM recycle_order;,确认执行过一次下单操作后有记录落库。这能证明 Spring 事务和 MyBatis 映射是通的,而不是页面假死。
5. SSM 项目源码避坑:404、Invalid bound statement、乱码和时区的 5 个排查记录
5.1 一启动就 404:DispatcherServlet 的 url-pattern 和 contextConfigLocation 没对齐
现象:Tomcat 能启动,日志也没有明显报错,但访问http://localhost:8080/recycle_ssm/login.jsp直接 404,IDEA 控制台只打印了一条No mapping found for HTTP request with URI /login。
原因:web.xml里 DispatcherServlet 的<url-pattern>/</url-pattern>拦截了所有请求,包括 JSP 和静态资源,而它内部又没能找到login.jsp对应的视图。更常见的变体是 SpringMVC 配置文件里没有配置InternalResourceViewResolver,导致 Controller 返回的"login"字符串不知道要拼上/WEB-INF/jsp/前缀。
解决:先确认访问路径是不是少了项目上下文前缀,再用curl http://localhost:8080/recycle_ssm/login.jsp看返回头是 200 还是 404。确认前缀无误后,检查spring-mvc.xml里InternalResourceViewResolver的prefix和suffix,如果 JSP 不在/WEB-INF/jsp/下,按实际路径改。还有一个隐蔽点:如果项目同时存在web.xml和web.xml.bak,IDEA 打包时可能选错 web 描述文件,启动后contextConfigLocation完全没生效。
5.2 登录直接报Invalid bound statement (not found):namespace 和 id 没对齐
现象:页面能打开,输入用户名密码点登录,后端抛BindingException: Invalid bound statement (not found): com.xxx.mapper.UserMapper.selectByUsername。
原因:UserMapper 接口文件存在,但对应的UserMapper.xml里<mapper namespace="com.xxx.mapper.UserMapper">写成了别的包名,或者 SQL 的<select id="selectByUsername">和方法名不一致。还有一种情况是 mapper XML 文件没放到被扫描的目录里,Spring 只扫描到了接口,没有扫描到 XML。
解决:打开UserMapper.xml,第一行namespace改成接口的全限定名。把 XML 文件移动到resources/mapper/目录(或mybatis-config.xml的mapperLocations指定的目录),并确保持 Ctl+Shift+N 搜索UserMapper.xml能看到这个文件在资源和代码里都存在。这属于 MyBatis 最常见的装配问题,排查时不要先怀疑 sql 写错,先看 namespace。
5.3 中文全变问号:字符集过滤器只挡了一半
现象:页面布局正常,但数据库里查出来的中文全是??,或者页面上显示测试这类乱码。
原因:这个问题往往不是一处错,而是三处同时错。第一,数据库表创建时 CHARSET 是 latin1,数据库虽是 utf8mb4 但表不是。第二,JSP 页面没有声明<%@ page contentType="text/html;charset=UTF-8" %>。第三,JDBC url 没有characterEncoding=utf8。很多源码包只在 web.xml 里加了 Spring 的CharacterEncodingFilter,但过滤器只能保证请求参数编码,保证不了数据库连接层的编码。
解决:先在 MySQL 命令行执行SHOW CREATE TABLE user;看表的 CHARSET,如果不是 utf8mb4,用ALTER TABLE user CONVERT TO CHARACTER SET utf8mb4;修复。然后确认jdbc.url里带useUnicode=true&characterEncoding=utf8。至于 JSP 页面的编码声明,挨个检查会累死人,用全局替换把pageEncoding="ISO-8859-1"之类的声明统一改成 UTF-8。这三处改完,把旧数据清了重新插入,中文就正常了。
5.4 数据库时间差 8 小时:MySQL 5.7 的 datetime 比本地时间少 8 小时
现象:回收订单创建时间 create_time 是下午 16:00 下的单,页面显示却是早上 08:00。
原因:JDBC url 里没有serverTimezone=Asia/Shanghai,MySQL 5.7 默认时区与 Java 虚拟机所在系统时区不一致,MyBatis 把 DATETIME 转成 java.util.Date 时按数据库 session 时区转换。
解决:在jdbc.properties的 url 末尾追加&serverTimezone=Asia/Shanghai,重启服务再测试。如果源码里用的是 Joda-Time 或 LocalDateTime,还要检查 Spring 的ObjectMapper或 MyBatis 的 typeHandler 有没有配置时间序列化格式。这个问题看起来不起眼,答辩演示早期截图很容易露馅,因为页面上所有时间都对不上。
5.5 静态资源 404:拦截器把 css/js 也拦了,登录页变成裸 HTML
现象:登录页能跳转,但整个页面没有样式,图片裂开,点击按钮无响应,浏览器 F12 的 Console 页全是 404。
原因:SpringMVC 拦截器<mvc:interceptor>拦截了/**,但没有<mvc:exclude-mapping path="/static/**"/>,或者项目的静态资源根本不在以/static、/resources、/assets开头的路径下,而是散落在webapp/js、webapp/css这种根目录路径。这种情况下即使排除了/static/**也没用。
解决:先看 JSP 页面的引用方式,如<link href="${pageContext.request.contextPath}/css/style.css">,说明静态资源在webapp/css下。此时拦截器排除项应写成<mvc:exclude-mapping path="/css/**"/>和<mvc:exclude-mapping path="/js/**"/>。更省心的做法是配置<mvc:resources mapping="/**" location="/"/>,直接不拦截静态资源匹配。同时检查有没有设置<mvc:default-servlet-handler/>,这个配置可以让 Tomcat 默认的 DefaultServlet 处理未映射的静态资源请求。
6. 答辩前最后一步:从跑通到能讲清楚,Lucky Money 和 PPT 都不用慌
6.1 用一条完整链路做验收测试:比对着页面点点点更有意义
项目跑通之后,别急着收拾东西。拿一条业务链路做全流程验收:注册新用户 -> 登录 -> 发布一条 iPhone 13 128G 的回收单 -> 看到系统预估价 -> 用回收员账号登录 -> 接单 -> 填写质检结果 -> 系统把状态改成已定价 -> 回到用户端确认成交。每完成一步,顺手去数据库执行SELECT id, status FROM recycle_order WHERE order_no = 'xxx';,确认状态码和页面保持一致。这一步能暴露两类问题:状态扭转错乱、某一步没写事务导致数据库没更新。答辩时如果老师问“系统真的可用吗”,你能把这条链路的截图和数据库状态变化摆出来,这就是最好的验证素材。
6.2 值得加的两个小功能:业务亮点不够时用它们补
如果项目做完感觉普通,可以在现有代码上加两个低成本功能。第一个是防并发抢单,把接单 SQL 从update recycle_order set recycler_id = ? where id = ?改成update recycle_order set recycler_id = ? where id = ? and status = 0,配合影响行数判断就能保证同一个订单不会被两个回收员同时接走。第二个是导出回收报表,用 POI 把订单表按月统计成 Excel,答辩时演示“导出月度回收数据”,这比单纯增删改查有说服力得多。这两个功能都不需要改表结构,只是往 Service 层加方法,但正好对应了“高并发下的数据一致性”和“运营数据分析”两个常见答辩追问方向。
6.3 答辩时要能说清楚的一件事:你的状态机是怎么设计的
前面所有代码里,最值得在答辩时展开讲的就是订单状态机。老师很可能问:用户取消订单后,回收员还能不能接单?质检不合格时应该进入什么状态?如果只答“前端有个按钮控制显示隐藏”,那明显不够。正确答案是:所有状态变化都写在 Service 方法里,每个方法只允许前置状态,不符合就抛异常;用户取消订单时先校验状态是 0 或 1,已进入质检后的订单不能取消,只能走拒收流程。这套逻辑对应RecoverOrderServiceImpl里的三五个方法,讲的时候最好动手画一张状态流转图,配合 LW 里的流程图和 PPT 里的截图展示。我在跑类似项目时的习惯是跑通后立刻给每个状态写一条正向和逆向测试用例,哪个方向走不通就说明状态机有漏洞,这个习惯帮我躲过了一次答辩现场把订单状态搞乱的尴尬。希望这一点能帮你也顺利过审。
本文还有配套的精品资源,点击获取