
1. 别急着敲代码先想清楚这个项目到底要解决什么拿到java_ssm12二手书图书回收捐赠管理平台这个标题的时候很多人的第一反应是又是一个SSM练手项目。但如果你真的只是把它当成一个普通的CRUD练习那做完之后大概率只会得到一个能跑但没人用的壳子。我当初接手类似项目时踩过的坑以及后来把它做成一个真正能落地的图书循环系统的心得都在这一篇里了按我的路线走你能少走不少弯路。这个平台的本质是什么一句话让闲置的二手书重新流动起来。流动的方式有两类——一类是回收用户把不用的书卖给平台或提交回收申请另一类是捐赠用户直接把书捐出来平台审核后转交给需要的人或机构。同时它还涉及图书的入库、库存管理、订单跟踪、积分激励等环节。所以你面对的不只是一个增删改查系统而是一个带状态流转、角色权限、业务流程的完整管理平台。适合看这篇内容的人我大致分三类第一类是正在做毕业设计或课程设计的学生需要一套能讲清楚原理、能演示运行的SSM项目第二类是自学Java想找项目练手、顺便充实简历的开发者第三类是已经有SSM基础、想了解图书回收这类业务系统怎么设计的人。无论你是哪一类这篇内容都不会只给你粘贴代码——我会把项目的完整结构、核心流程、关键配置和那些文档里不会写的坑都讲清楚。2. 技术选型为什么这个项目用SSM IDEA最稳而不是Spring Boot2.1 SSM在校园项目和工作场景中的真实地位现在一提起Java后端开发铺天盖地都是Spring Boot。但我仍然坚持把这个项目做成SSM原因很简单很多高校的教学大纲、课程设计要求和毕业设计模板仍然是围绕SSM展开的。SSM指的是Spring、Spring MVC、MyBatis三个框架的组合它曾经是Java Web开发的主流方案现在虽然被Spring Boot大幅取代但理解SSM的底层整合逻辑对于理解Spring Boot的自动配置到底自动了什么、省掉了什么反而更有帮助。另外从面试和答辩的角度讲SSM项目能讲的东西比Spring Boot多得多。Spring Boot把xml配置、DispatcherServlet配置、MyBatis映射都帮你自动搞定之后很多人反而不清楚请求是怎么从浏览器一路走到数据库再返回的。而SSM项目里每一条配置都是你亲手写上去的你对整个调用链路的理解是实打实的。面试官问起来你能把Spring容器怎么启动、Spring MVC怎么分发请求、MyBatis怎么代理Mapper接口讲得明明白白这比单纯说我用Spring Boot写了个接口要有说服力得多。2.2 IDEA在写SSM项目时的优势开发工具选IDEA没什么好犹豫的。IDEA对Maven、Tomcat、MyBatis插件的支持非常成熟尤其是它的Database工具窗口可以直接连接MySQL查看表结构、跑SQL写Mapper.xml的时候还能自动提示表字段名。对比EclipseIDEA的自动导入类、重构功能、Git集成都做得更流畅对于这个项目的开发效率提升是很明显的。需要提醒的一点是这个项目我建议用IDEA社区版Community Edition或者教育版授权就足够了不需要折腾那些来路不明的激活方式。社区版虽然不直接支持Tomcat的图形化集成运行Ultimate版才有但你完全可以通过配置Tomcat的本地启动方式或者用Maven的tomcat7插件来运行项目效果没差别。我后面会专门讲怎么配置。2.3 核心依赖版本选择把坑提前堵上SSM项目最怕的就是版本不兼容。给你一份我实测能跑通的版本组合直接抄作业组件版本说明JDK1.8和Spring 5.x、Tomcat 8.5兼容性最好Maven3.6.3稳定IDEA内置可用Spring5.2.15.RELEASE注意是RELEASE版别用5.3.x加老配置互相打架MyBatis3.5.6常用版本和spring-mybatis 2.0.6配合成熟MyBatis-Spring2.0.6必须与MyBatis版本匹配这步最容易出错MySQL5.7或8.05.7最省心8.0要改驱动名和时区配置druid1.2.8阿里的连接池监控页面很方便jackson-databind2.9.8用来处理JSON数据注意与JDK8的兼容我为什么会选Spring 5.2.x而不是更老的4.x因为5.x对JDK8的支持非常完善而且Spring 5开始全面采用注解配置风格比4.x时代大量依赖xml配置要简洁得多。但你同时要知道Spring 5.x已经不需要web.xml里的spring监听器和DispatcherServlet配置了吗其实还是要的只是可以用代码配置替代。考虑到这个项目的教学性我保留了web.xml spring-mvc.xml spring-mybatis.xml这三件套这样你对于Spring容器怎么在Web应用里启动会有一个完整认知。2.4 补充一个冷门但实用的选择理由数据库结构容易迁移很多同学会纠结一个问题我以后要做Spring Boot版本SSM写的代码是不是全浪费了完全不会。因为这个项目的核心资产是数据库设计——图书表、用户表、订单表、回收单表、捐赠单表这些表结构放到任何技术栈里都能复用。你之后用Spring Boot重写只需要把Mapper接口和Service层拿过去改个注解前端页面几乎原样保留。所以从一开始就要把数据库设计做扎实这是我反复强调的。3. 数据库设计图书回收捐赠平台的表结构要这样拆3.1 从业务出发反推表结构做表设计之前我习惯先列业务清单而不是直接打开Navicat建表。对于二手书回收捐赠平台核心业务有这些用户注册、登录、个人信息管理用户提交回收申请我要把书卖给平台用户提交捐赠申请我要把书捐出去管理员审核回收/捐赠申请图书入库、库存管理订单管理回收订单、捐赠订单积分管理用户捐赠或回收后获得积分可兑换小礼品把这些业务过一遍你会发现最重要的抽象是书这个概念。用户手里有一本书提交回收或捐赠申请后书的状态就变了——从用户持有变成平台待入库再变成已入库在库最后可能变成已寄出/已售出。这是整个系统的核心状态机。3.2 核心表结构详解基于上述分析我设计了5张核心业务表加2张辅助表。给你看一下关键表的字段设计思路用户表t_user字段名类型说明idint(11)主键自增usernamevarchar(32)登录名唯一passwordvarchar(64)密码建议MD5加密存储nicknamevarchar(32)昵称phonevarchar(11)手机号pointsint(11)积分总额默认0roletinyint(4)角色1管理员 2普通用户statustinyint(4)状态1正常 2禁用create_timedatetime注册时间注意这里的role字段我用数字而不是字符串来区分角色理由是在后端判断权限时if(user.getRole() 1)比if(admin.equals(user.getRole()))更高效也更安全。而且这个项目不打算引入Spring Security那套复杂权限框架所以用一个简单的拦截器加role判断就够了这也是SSM项目的常规做法。图书表t_book字段名类型说明idint(11)主键自增book_namevarchar(64)书名authorvarchar(32)作者publishervarchar(64)出版社isbnvarchar(32)ISBN号categoryvarchar(32)分类original_pricedecimal(10,2)原价recycle_pricedecimal(10,2)回收价donate_flagtinyint(4)是否捐赠1是 2否stockint(11)库存数量statustinyint(4)状态1待入库 2在库 3已售出 4已捐赠user_idint(11)关联用户表表示这本书来源create_timedatetime入库时间这个表是核心中的核心。isbn字段建议做索引因为用户搜索书的时候最常用的就是书名和ISBN。category字段不要用数字直接用字符串分类名就好这个系统分类不会特别多不需要单独建一张分类表过度设计在这里没有意义。回收订单表t_recycle_order字段名类型说明idint(11)主键自增order_novarchar(32)订单编号user_idint(11)提交回收申请的用户book_idint(11)关联图书表recycle_pricedecimal(10,2)回收金额statustinyint(4)1待审核 2已通过 3已入库 4已打款 5已拒绝audit_remarkvarchar(255)审核备注create_timedatetime申请时间audit_timedatetime审核时间回收订单是状态最多的一个流程。我故意把待审核到已打款拆成多个节点因为现实中回收一本书要经过用户申请、管理员审核书况、审核通过后用户寄书、平台收到书验收入库、最后打款给用户。每一步状态的变化都对应系统的某个操作这也让答辩时能展示的按钮和逻辑更多更完整。捐赠订单表t_donate_order这张表和回收订单类似区别是它没有打款环节多了一个受赠机构/受赠人字段。状态流转是1待审核 → 2已通过 → 3已入库 → 4已寄出/已领取 → 5已完成。积分记录表t_points_log字段名类型说明idint(11)主键自增user_idint(11)用户change_typetinyint(4)1回收加分 2捐赠加分 3兑换扣减change_pointsint(11)变动积分正负descriptionvarchar(255)变动说明create_timedatetime变动时间积分表的设计要点是只做流水记录不做余额存储。余额直接从t_user表的points字段读取每次变动时更新即可。这样做的好处是积分流水可追溯算积分总额时可以SQL汇总也可以直接读用户表的冗余字段。为了简单我采用了流水表 冗余总额字段的方案。3.3 一个实际业务的SQL串联示例拿用户提交回收申请这个动作来说一次请求要操作的表至少有3张在t_book表插入一条书记录status设为1待入库关联当前user_id在t_recycle_order表插入一条订单记录status设为1待审核如果订单审核通过并入库再更新t_book的status为2更新t_recycle_order的status为3同时给用户加积分这3步必须放在同一个事务里执行。我实际写的时候Service层用Transactional注解包住整个方法通过spring-mybatis.xml里配置的事务管理器来生效。你要是把这3步拆开写而不加事务一旦中间某一步失败就会出现订单创建了但书没有插入或者相反的脏数据。这是新手最容易犯的错误。4. 项目结构与核心代码SSM三件套是怎么协作的4.1 标准Maven目录结构创建项目时我推荐直接用IDEA的Maven Archetype - maven-archetype-webapp来生成骨架然后手动补全目录。最终结构应该是这样的src ├── main │ ├── java │ │ └── com.example.bookrecycle │ │ ├── controller // 控制器层 │ │ ├── service // 业务逻辑层 │ │ │ ├── impl │ │ ├── mapper/dao // MyBatis接口层 │ │ ├── entity/pojo // 实体类 │ │ ├── interceptor // 拦截器登录校验、权限校验 │ │ ├── common // 通用工具类、常量类 │ │ └── config // 配置类如果需要 │ ├── resources │ │ ├── mapper // MyBatis的xml映射文件 │ │ ├── jdbc.properties // 数据库连接配置 │ │ ├── spring-mybatis.xml // Spring MyBatis整合配置 │ │ ├── spring-mvc.xml // Spring MVC配置 │ │ └── log4j.properties // 日志配置 │ └── webapp │ ├── WEB-INF │ │ ├── web.xml // Web部署描述符 │ │ └── jsp // 视图页面 │ ├── static // css、js、图片 │ └── index.jsp └── pom.xml有一点需要特别注意resources/mapper目录下的xml文件build时一定要被拷贝到classes目录下否则MyBatis启动时会报Invalid bound statement (not found)。如果你用IDEA默认的Maven设置src/main/java下的xml文件扫描不到所以我们的mapper.xml必须放在resources目录下或者手动在pom.xml里配置resources标签包含xml后缀。这是我见过排第一的SSM报错原因后文踩坑篇会详细讲。4.2 三个核心配置文件的职责边界spring-mybatis.xml负责Spring容器、数据源、事务管理器和MyBatis的SqlSessionFactory。核心配置是这样context:component-scan base-packagecom.example.bookrecycle.service / bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.example.bookrecycle.entity/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.bookrecycle.mapper/ /bean这段配置的重心是MapperScannerConfigurer它会自动扫描mapper包下的所有接口为每个接口生成代理实现类。这样你就能在Service里直接Autowired注入Mapper接口不用手写实现类。这一点是MyBatis和传统的DAO层最大的区别理解了它你就理解了MyBatis-Spring的核心机制。spring-mvc.xml负责控制器扫描、视图解析器、静态资源映射、JSON消息转换。关键配置context:component-scan base-packagecom.example.bookrecycle.controller / bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp// property namesuffix value.jsp/ /bean mvc:annotation-driven / mvc:default-servlet-handler /需要特别强调的是mvc:annotation-driven /这一行很多教程会一带而过但它非常重要——它为Controller方法提供了ResponseBody的JSON转换、参数校验、类型转换等基础能力。不写这行你Controller里的RequestBody、日期格式化都会失效。web.xml把上面两个容器串起来外加编码过滤器和Spring的上下文加载监听器。这里有个经典坑因为spring-mvc.xml里已经扫描了controller层而spring-mybatis.xml扫描了service和mapper层如果你在spring-mybatis.xml里也开启context:component-scan同时扫了controller包会导致Spring容器创建两遍Controller轻则浪费内存重则造成AOP事务失效。所以我的做法是spring-mybatis.xml只管service mapper 数据源spring-mvc.xml只管controller互不越界。4.3 Controller层代码示例回收申请接口我把用户提交回收申请的Controller写出来重点不是代码本身而是你看完能知道一个请求完整走过了哪些层。前端传过来的是图书信息和回收价格Controller接收后调用Service层的方法。Controller RequestMapping(/recycle) public class RecycleOrderController { Autowired private RecycleOrderService recycleOrderService; RequestMapping(value /submit, method RequestMethod.POST) ResponseBody public Result submit(RequestBody RecycleApplyVO applyVO, HttpSession session) { User user (User) session.getAttribute(loginUser); if (user null) { return Result.error(请先登录); } try { boolean success recycleOrderService.submitRecycleOrder(user.getId(), applyVO); if (success) { return Result.success(回收申请提交成功等待审核); } else { return Result.error(提交失败请检查图书信息); } } catch (Exception e) { log.error(提交回收申请异常, e); return Result.error(系统繁忙请稍后重试); } } }Service层的方法要用Transactional包住三步数据库操作这一步在写代码时一定要记得加后续事务切面才会生效。4.4 JSP页面的角色判断与页面跳转技巧视图层用的是JSP JSTL。页面大体分为两类一类是用户端功能页面比如首页书城我的回收我的捐赠积分商城另一类是管理员端管理页面比如回收审核捐赠审核图书管理用户管理。管理员端和用户端如何共用一套登录逻辑我采取的方案是在拦截器里根据role判断然后从session里读用户对象设置不同的页面跳转。比如访问/admin/**路径时拦截器校验当前用户role是否为1不是就重定向到登录页。这里用拦截器而不是在每个Controller方法里重复判断好处是以后新增管理员功能页面不用再逐个加权限校验。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /login); return false; } // 管理员模块额外校验 String uri request.getRequestURI(); if (uri.contains(/admin/) user.getRole() ! 1) { response.sendRedirect(request.getContextPath() /403); return false; } return true; } }然后在spring-mvc.xml里注册拦截器mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/register/ mvc:exclude-mapping path/static/**/ bean classcom.example.bookrecycle.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors这个方案简单直接但对静态资源要记得加exclude排除否则css和js文件都会被拦截页面样式全丢了。我在第一次写的时候就是因为没加/static/**排除折腾了半天才发现是拦截器把静态文件拦了。5. 流程与状态流转回收和捐赠两条业务线的完整闭环5.1 用户视角从提交申请到收到款项的完整过程我把回收业务线完整走一遍你就能明白这个项目的前端页面到底需要哪些按钮后端接口需要支持哪些操作。首先用户登录后在我要回收页面填写图书信息——书名、作者、出版社、ISBN、原价、期望回收价然后提交。系统生成一条t_book记录status1和一条t_recycle_order记录status1。管理员登录后在回收审核页面看到这条待审核记录可以查看用户填写的详细信息。管理员审核通过后订单状态变成2已通过页面提示用户请您将图书寄至平台指定地址。用户确认寄出后可以点击确认已寄出按钮订单状态不变但需要把这个动作记录到日志里。这里我为了简化操作把寄书设计成订单状态变化的触发点但其实也可以不变状态只加一条备注。平台收到书、验收入库时管理员执行入库操作。这个操作是一个事务性的动作t_book.status更新为2在库t_recycle_order.status更新为3已入库同时给用户增加回收积分。最后管理员可以执行打款操作订单状态更新为4已打款并在备注里写明打款方式。到这一步回收业务闭环完成。对应到前端页面用户能看到的是我的回收列表每一条记录都显示当前状态和审核备注管理员看到的是带操作按钮的表格不同状态的行展示不同的操作按钮。我建议在页面用不同颜色的badge区分状态比如灰色待审核、蓝色已通过、绿色已入库、橙色已打款、红色已拒绝这样管理员的视觉负担会小很多。5.2 管理员视角待审核队列的设计与处理效率管理员端的审核页面其实是个队列消费场景和外卖骑手接单的逻辑很像。我设计了一个待审核队列按时间倒序展示所有status1的记录支持按图书分类或用户ID筛选。管理员点击审核按钮后弹出模态框显示图书详细信息同时可以选择通过或拒绝拒绝时必填原因。这个拒绝必填原因的设计是从客服工单系统学来的它强制了操作责任也让用户知道被拒是什么原因不会反复提交同样的问题。在实现多条件查询时不可避免要用到MyBatis的动态SQL。这是SSM项目里MyBatis最值得炫耀的能力。比如图书管理的分页条件查询select idselectBookList resultTypecom.example.bookrecycle.entity.Book select * from t_book where if testbookName ! null and bookName ! and book_name like concat(%, #{bookName}, %) /if if testcategory ! null and category ! and category #{category} /if if teststatus ! null and status #{status} /if /where order by create_time desc /selectwhere标签会自动处理首个条件的AND前缀问题这是个很实用的细节。如果你自己拼SQL字符串很容易在条件为空时多出一个WHERE或者AND而且存在SQL注入风险MyBatis的#{}预编译则天然防注入。5.3 积分激励的浅层设计算清楚这笔账积分模块是这个项目的一个加分项但它容易做好也容易做烂。我做了一套简单的规则回收一本书加10积分捐赠一本书加20积分积分可以在积分商城兑换小礼品如5积分兑换定制书签、20积分兑换笔记本、50积分兑换帆布袋。这个设计的好处是让测试数据很容易造出来演示效果也很直观。积分变动的实现要点是不要在每个业务方法里直接修改t_user.points而是统一封装一个PointsService.changePoints(userId, changeType, changePoints, description)方法。这个方法内部做两件事更新用户表的points字段 插入一条积分流水。然后回收入库、捐赠完成、积分兑换这三个场景都调用它。这样积分规则如果需要调整只改这一个方法不会出现有的地方加了积分、有的地方忘了加的问题。5.4 一个前车之鉴数据统计报表别最后再做我给这个项目添加了一个数据统计页面展示总用户数、总图书数、总回收订单数、总捐赠订单数以及每日新增趋势。这个功能放在最后做是对的但我的建议是——在数据库设计阶段就要为统计预留字段。比如订单表的create_time是必填的你才能按天分组统计用户表也要有create_time你才能画出注册趋势图。统计SQL本身很简单就是select count(*) from t_recycle_order where status 4这样的聚合查询或者按日期分组select date_format(create_time, %Y-%m-%d) as day, count(*) as cnt from t_recycle_order group by date_format(create_time, %Y-%m-%d) order by day desc limit 7如果这个功能放到项目最后再做而之前你没在表中保留create_time字段那就只能改表结构甚至重导数据非常痛苦。所以我在设计表的时候就列了一个规矩每张业务表必须有create_time这几乎是所有管理系统的基本要求。6. 环境配置与启动IDEA里跑通SSM项目的完整操作6.1 Maven配置和JDK的坑先说环境层面最容易出问题的地方。Maven仓库地址如果还在用中央仓库国内网络环境下下载依赖会非常慢甚至卡死。我的建议是修改settings.xml把镜像源换成阿里云镜像。这一步在IDEA里的操作是File → Settings → Build, Execution, Deployment → Build Tools → Maven → 在User settings file里指定你自己的settings.xml。JDK版本这里我要特别强调这个项目用的是JDK 1.8IDEA里需要把Project Structure里的Project SDK和Project language level都设置为8。如果你用的IDEA是最新版可能默认SDK是17甚至21这时候直接运行SSM项目可能会遇到一些兼容性问题。解决办法有两个一是安装JDK 8并切换过来二是升级Spring到5.3.x并继续用JDK 8。为了稳妥我用的就是JDK 8 Tomcat 8.5 Spring 5.2.x这个组合实测下来几乎所有老项目代码都不会报错。6.2 三种运行方式选你顺手的一种方式一IDEA集成TomcatUltimate版。这是最直观的方式。Run → Edit Configurations → 添加Tomcat Server → Local在Deployment标签页把war包加进去Application context设置为/或项目名。然后直接点运行按钮。方式二Maven tomcat7插件社区版也适用。在pom.xml里添加plugin groupIdorg.apache.tomcat.maven/groupId artifactIdtomcat7-maven-plugin/artifactId version2.2/version configuration port8080/port path//path uriEncodingUTF-8/uriEncoding /configuration /plugin然后在IDEA右侧Maven面板中执行tomcat7:run。这个方法的好处是不需要单独装Tomcat插件会自动下载一个内嵌Tomcat。注意tomcat7插件虽然名字带7但它可以兼容运行Servlet 3.0规范的Web应用对这个项目完全够用。方式三外置Tomcat部署war包。把项目打包成war手动扔到Tomcat的webapps目录下启动Tomcat。这种方式适合最终部署演示但日常开发调试用前两种就够。我用得最多的是方式二因为它完全绕开了IDEA版本限制也避免了Tomcat版本和IDEA集成配置不匹配的问题。但注意tomcat7插件对JDK8是OK的如果你用了比JDK8更高的版本可能要选tomcat9插件或换方式一。6.3 数据库初始化的完整流程在项目启动前必须先建好数据库。我用Navicat或IDEA自带的Database工具连接MySQL执行项目根目录下的db/book_recycle.sql。这个sql文件包含建库、建表和少量测试数据。测试数据我建议插几条管理员账号usernameadminpasswordadmin123和几个测试用户不然你登录进去都是空页面演示效果会很差。需要注意MySQL 8.0和5.7的驱动差异。如果你用的是MySQL 8.0jdbc.properties里的driverClassName要写com.mysql.cj.jdbc.Driverurl要加serverTimezoneAsia/Shanghai和useSSLfalse否则启动就报时区错误。这是MySQL 8时代最经典的一个坑很多同学卡在这里就放弃了。jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/book_recycle?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password你的密码我建议你在这份properties里就把编码参数写全否则后面中文乱码问题会让你怀疑人生。6.4 启动成功的标志别只看Tomcat日志很多同学看到Tomcat启动日志里出现Started就以为运行成功了其实还要多看两个标志spring-mvc.xml的组件扫描日志里出现了你的Controller类名MyBatis的Mapper映射文件被正确加载没有Invalid bound statement的异常如果你用的log4j配置了debug级别你会看到Creating bean with name bookController和Building JAXBContext之类的日志。当浏览器访问http://localhost:8080/能正常打开首页并且登录后跳转正常时这个项目才算是真正跑通了。7. 踩坑实录SSM项目最容易翻车的五个问题7.1 报错Invalid bound statement (not found)Mapper接口和XML没绑定这是SSM新手的噩梦。现象是项目能启动但一调用某个Mapper方法就抛org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)。排查思路如下检查MapperScannerConfigurer的basePackage是否包含了你的Mapper接口所在的包检查SqlSessionFactoryBean的mapperLocations是否配成classpath:mapper/*.xml检查src/main/java下是否有Mapper接口的同名xml文件如果有说明你把它放错位置了必须挪到resources/mapper下用IDEA的构建输出窗口查看target/classes下是否真的有对应的xml文件如果没有就是build时没有把xml资源拷贝过去我的解决方案是把所有的mapper.xml统一放在src/main/resources/mapper目录下并在pom.xml的resources配置中明确包含**/*.xml。这样最稳。7.2 中文乱码三个地方都要改成UTF-8中文乱码是这个项目里最常见的问题而且它可能出现在三处数据库连接URL没加characterEncodingutf8导致从数据库读出的数据乱码。我上面的jdbc.properties已经写了。web.xml里的编码过滤器没配置或者顺序不对。Spring提供的CharacterEncodingFilter必须配置在filter-mapping的最前面forceEncoding设置为true否则MySQL传回的中文依然乱。JSP页面本身没设置pageEncodingUTF-8或者Tomcat的URIEncoding不是UTF-8。我的做法是三者同时处理缺一不可。特别是第2点配置如下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 init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping7.3 日期类型处理JSON序列化变成时间戳当Controller返回包含create_time等Date字段的对象时用ResponseBody转JSON前端拿到的是一个数字时间戳不是2024-06-01 12:00:00这样的格式。解决这个问题需要在spring-mvc.xml里配置Jackson的ObjectMapper设置日期格式化mvc:annotation-driven mvc:message-converters bean classorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter property nameobjectMapper bean classcom.fasterxml.jackson.databind.ObjectMapper property namedateFormat bean classjava.text.SimpleDateFormat constructor-arg valueyyyy-MM-dd HH:mm:ss/ /bean /property /bean /property /bean /mvc:message-converters /mvc:annotation-driven这一步很容易被忽略但演示时非常影响观感。同样的前端JSP页面用JSTL的fmt:formatDate标签直接格式化后台传来的Date就不需要额外处理。7.4 事务注解不生效查一下有没有启用事务管理Transactional注解加在Service方法上但如果没有在spring-mybatis.xml里配置tx:annotation-driven transaction-managerdataSourceTransactionManager/这个注解就是废的方法执行过程中出了异常前面的SQL也不会回滚。这个问题隐蔽性很强因为平时操作成功时你看不出任何区别只有测试第二步故意失败、看第一步是否回滚才能暴露。我的测试方法是在recycleOrderService.submitRecycleOrder方法中故意让第三步抛一个RuntimeException然后检查数据库里t_book表是否还插入了记录。如果没有回滚说明事务配置有问题。确认无误后删掉测试代码。7.5 Druid连接池启动成功却报错防火墙或时区问题Druid连接池初始化失败时通常报Could not create connection to database server。排查时要先确认MySQL服务是否启动、端口是否是3306、防火墙是否放行。然后按照我之前说的MySQL 8.0把驱动换成cj版本url加上时区参数。还有一个容易被忽略的点MySQL 8.0默认的认证插件是caching_sha2_password而老版本Druid或MySQL驱动可能不支持会报Public Key Retrieval is not allowed。解决方法是配置allowPublicKeyRetrievaltruejdbc.urljdbc:mysql://localhost:3306/book_recycle?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue这个参数我一般在本地开发时都会加上省得每次连数据库还要搞证书。8. 如何给项目加分让这个管理平台从能跑到能答辩8.1 前端页面不用写得很花哨但流程要完整这个项目的核心竞争力在业务流程而不在页面美化。所以我不建议你去套一个看不懂的复杂前端框架。用JSP Bootstrap jQuery就完全够了。Bootstrap负责栅格布局和按钮样式jQuery负责发Ajax请求和渲染表格。这套组合的优点是你自己写的每行代码你都能解释清楚答辩时老师问起来你不慌。我实际使用的页面结构是这样的每个功能模块对应一个JSP页面页面主体是一张表格表格数据由后端接口返回JSON前端用jQuery的$.ajax获取后拼HTML渲染或者直接用JSTL在服务端渲染。我的建议是优先用JSTL服务端渲染因为SSM项目最常见的误区是把大量逻辑堆在前端导致后端看起来就是个纯API服务器。对于这个项目服务端渲染少量Ajax交互是最平衡的方案。8.2 答辩时老师最爱问的几个问题提前准备好答案根据我带过的几个学生项目的经验答辩老师通常会围绕为什么这么做来提问你要能答出设计背后的理由。高频问题有这么几个为什么要用SSM而不是直接Servlet答Servlet是Web应用的底层规范但如果每个请求都手写Servlet的话代码重复度太高项目可维护性差。Spring帮我们管理对象依赖Spring MVC帮我们简化请求分发和参数绑定MyBatis帮我们解耦数据库操作和业务逻辑这个组合让代码分层更清晰、复用性更强。图书状态是怎么管理的答我在t_book表设计了status字段表示图书的生命周期在t_recycle_order表设计了一套状态码表示回收订单的流转每个状态都对应一个明确的业务动作和管理员操作前端也针对不同状态展示不同按钮。这样整个图书从用户手里到平台入库再到售出/捐出全程可追踪。如果回收订单审核拒绝图书数据怎么办答在提交回收申请时我先把图书插入t_book表并且status置为待入库。如果管理员审核拒绝我会在同一次事务里把订单状态更新为已拒绝图书如果还未入库可以直接删除对应的t_book记录或者标记为审核未通过。实际我在代码里是做了回滚删除的保证用户下次还能重新提交申请。积分规则写在哪里如何保证不重复加积分答积分变动统一封装在PointsService里每次变动时先查一下这条订单有没有加过积分。我在t_points_log里存储了业务关联ID如orderId并且加了唯一索引这样一个订单只能产生一次积分记录不会因为重复点击而重复加分。8.3 在此基础上扩展的三个方向如果这个项目你想继续做下去而不是答辩完就删掉我给三个可落地的扩展方向方向一引入Redis缓存热门图书列表。首页书城每次打开都要查数据库如果数据量上来会越来越慢。用Redis缓存前20本热门图书缓存20分钟失效代码只改Service层的一个方法收益却很直观。这个扩展在简历上写出来也比较亮眼。方向二增加图书评论和评分功能。这是平台从交易工具向社区演进的标志。数据库加一张t_comment表前端在书详情页多一个评论区后端新增评论增删查接口。逻辑不复杂但会让项目看起来有社交属性。方向三对接支付宝沙箱支付。如果平台要做回收打款或购买图书的真实支付链路可以接入支付宝沙箱环境。支付宝官方有完整的Java SDK和demo你只需要在申请回收订单打款时调用支付接口即可。这个扩展的难点不在写代码而在你想清楚业务闭环怎么走。根据我个人经验答辩时如果能展示其中一个扩展哪怕只是演示了Redis缓存生效的过程老师的评价通常都会比基础功能完整再高一个台阶。9. 最后再分享一点实际开发体会这个项目我前后迭代过两个版本第一个版本只顾着把功能堆出来结果数据库改了三次表结构前端页面也返工了好几轮。第二个版本我学乖了——先画业务流程图再定状态码然后把表结构设计评审一遍才动手写代码。整个过程下来最大的体会就是SSM项目的代码量其实不算大真正花时间的全在业务逻辑梳理和配置调试上。另外我强烈建议你把项目的数据库脚本、导入说明、运行步骤整理成一份README.md放在项目根目录下。这个习惯对你自己的复盘、老师查看项目、以及以后写简历上的项目描述都有直接帮助。很多人觉得代码能跑就行了其实项目文档同样是你专业度的体现。最后一个小技巧IDEA里给项目配一个.gitignore把target目录、.idea目录、*.iml文件忽略掉用Git管理源码。哪怕只是自己一个人开发commit历史也会在你改出问题时帮你挽回大量时间。这个项目从里到外都不复杂但每一步都做扎实了它就变成一个能经得起问、拿得出手的完整作品。