每年一到做毕业设计的季节,SSM网上书城系统这种项目就会出现在各大学院的选题列表里。标题里那个96906,其实只是源码包管理平台自己的流水号,跟技术本身没有任何关系,更像是快递单号一类的东西。但能被大量打包成毕设源码的题目,通常意味着它的难度适中、功能覆盖面够全、讲解起来也足够有料。
这篇文章,我就带你从头到尾拆解一套SSM网上书城系统的源码:从架构设计、数据库表结构,到核心业务代码怎么写,再到怎么在本地把项目跑起来,最后是我自己折腾这类项目时踩过的坑和排查思路。如果你正准备做类似题目,或者手里刚好有一套SSM书城源码却不知道怎么讲清楚,这篇文章应该能帮你省不少事。
1. SSM网上书城系统:从需求到架构的整体拆解
1.1 网上书城到底要做什么
一套合格的网上书城系统,表面上看就是“用户逛网站买书”,但从软件开发的角度拆解下来,它其实是一个包含了两个角色、多个业务模块的完整小程序商城。
用户端要有的功能是这几样:注册登录、图书分类浏览、关键字搜索(书名、作者、出版社)、图书详情页、加入购物车、提交订单、模拟付款,以及查看自己的订单列表。管理员端则负责商品管理(上架、下架、修改库存)、分类管理、订单处理(发货、取消)、用户管理等。
这套功能设计看起来中规中矩,但它的巧妙之处在于:覆盖了Java Web开发几乎所有的核心知识点。注册登录对应Session与表单校验,图书检索对应SQL查询与动态条件拼接,购物车和订单对应事务与多表关联,后台管理对应拦截器与权限控制。你把这些模块做完一遍,SSM框架的常规用法基本上就摸透了。
所以这也是为什么“SSM + 书城”这个组合能成为经久不衰的毕业设计选题。它不像“学生管理系统”那样功能单薄,也不像大型电商系统那样复杂到一个人做不出来,正好卡在“能独立完成、能讲清楚、能演示出效果”的理想难度区间。
1.2 为什么是SSM,而不是Spring Boot
先说结论:如果现在让我从头做一个课程设计,我大概率会用Spring Boot;但如果我拿到一套以SSM为基础的源码,我完全不会觉得它过时。SSM组合目前仍然在很多老项目里跑得稳,而且对学习来说,它的配置过程本身就是一种教学。
SSM指的是Spring、Spring MVC和MyBatis三个框架的组合。三个框架各有分工:Spring负责管理所有对象(也就是IoC容器),Spring MVC负责接收HTTP请求并把结果分发回页面,MyBatis负责操作数据库并帮你写JDBC那套重复代码。
选SSM而不是Spring Boot来做毕业设计,一个非常现实的原因是:Spring Boot的“自动配置”把太多细节吞掉了。你写一个Controller只需要加几个注解,但框架为什么能扫描到它、内置Tomcat是怎么嵌入的、数据库连接池是靠什么自动创建的,这些对初学者反而是黑盒。而SSM方案里,从web.xml到spring-mvc.xml,再到spring-mybatis.xml,每一条配置都摆在明面上,源码下载下来能一行一行读。答辩的时候老师一旦问“你这个框架是怎么整合起来的”,你完全可以理直气壮地讲清楚,而不是支支吾吾说“加了依赖它自己就跑了”。
当然,我也建议你心里清楚:SSM是学习的好材料,Spring Boot才是当前生产环境的主流。做完这个项目,如果想找工作,再花一两周熟悉Spring Boot的相关知识,两者可以平滑衔接。
1.3 三层架构与服务边界
网上书城这种业务系统,代码组织上几乎都采用经典的三层架构:Controller层、Service层、Mapper层。
Controller层的职责只有一个:接收参数、校验简单格式、调用Service、把结果放到Model或返回JSON。别让Controller里出现大段业务逻辑,比如“判断库存够不够,如果不够就抛异常”这种代码,绝对不应该写在Controller里。
Service层负责真正的业务规则。以“购物车加购”为例,Service要做的事包括:检查图书是否存在、检查库存是否充足、判断购物车是否已有该商品、决定新增还是更新数量。这些操作往往需要组合多次Mapper调用,而且必须被事务保护起来。
Mapper层就是纯粹的数据库操作接口,一个方法对应一条SQL,不掺业务判断。它做的事情越纯粹越好,这样SQL才好维护,也方便后续优化。
关于分层,答辩时最容易被问的问题是“你为什么要把代码分成三层”。直接答“为了解耦”,太泛了。你可以说:三层分层让每一层只关心自己的事情,Controller不关心SQL怎么写,Mapper不关心请求参数从哪来,业务规则单独沉淀在Service层。一旦需求变化,比如把库存校验策略从“数量大于0”改成“数量大于2”,只需要改Service里一行逻辑,完全不会波及到Controller和Mapper。这种回答有具体场景,比空讲概念要好得多。
1.4 数据库设计:六张表怎么定
书城系统的数据库结构,一般由这么几张表组成。
| 数据表 | 作用 | 关键字段 |
|---|---|---|
| user | 用户表 | user_id、username、password、phone、create_time |
| category | 图书分类表 | category_id、name、sort_order |
| book | 图书表 | book_id、category_id、title、author、publisher、isbn、price、stock、sales、cover、description |
| cart | 购物车表 | cart_id、user_id、book_id、quantity、create_time |
| orders | 订单主表 | order_id、order_no、user_id、total_amount、status、recipient_name、recipient_phone、recipient_address、create_time |
| order_item | 订单明细表 | item_id、order_id、book_id、title、cover、price、quantity、subtotal |
有几处设计是值得仔细琢磨的。
首先是订单主表和订单明细表为什么要分开。因为一个订单里可以有多本不同的书,这就是一对多关系。主表保存“谁在什么时候下了单、总额多少、寄给谁”,明细表保存“这个订单里具体有哪些书、每本多少钱、几本”。如果没有明细表,在一个订单里塞多条商品数据,表的字段会瞬间变得稀奇古怪,范式也被破坏了。
其次,价格字段建议用整数“分”来存,而不是用浮点数。Java里的double做金额运算会因为精度问题出现0.1 + 0.2 != 0.3的情况,数据库的FLOAT也一样。所以更稳妥的做法是:数据库存的是以“分”为单位的整数,页面展示时再除以100转成“元”。支付类的真实项目基本都是这么设计的,毕业设计用这个口径去跟老师解释,反而显得你有工程经验。
再次,订单明细表里的title、cover、price这些字段,其实是从图书表“复制”过来的。这不是冗余设计,而是“快照”。因为图书的价格和封面以后会变,但订单已经成交了,订单里记录的信息必须保持下单那一刻的样子。如果你下单后把图书表的价格改了,再去查历史订单,看到的却是改后的新价格,这显然是业务上的错误。
最后聊一下外键。很多毕设源码里会直接在表之间建立物理外键约束,但从实际开发经验看,更常见的做法是不给表加物理外键,只保留逻辑外键关系(通过普通索引和管理层逻辑来保证一致性)。原因很简单:物理外键在高并发写入时会影响性能,而且一旦数据需要迁移或调整,外键约束很容易成为绊脚石。你可以不加外键,但在Service层要确保“先删主表再删明细表”或“删除用户时同时清空他的购物车数据”,这些业务规则要靠代码保证。
2. 核心技术实现:配置、鉴权、购物车与订单
2.1 SSM整合时最关键的三个配置文件
SSM项目能不能跑起来,九成看配置文件写没写对。很多人拿到一套源码,代码逻辑还没看,先在配置阶段就被卡住了。这一节梳理一下三个核心配置文件的职责。
第一个是web.xml。它是整个Web应用的入口文件,负责装配Spring容器和Spring MVC前端控制器。常见的写法是:用ContextLoaderListener加载Spring根容器(也就是applicationContext.xml或spring-mybatis.xml里的数据源、Service、Mapper这些Bean),再配置DispatcherServlet来加载Spring MVC容器(controller、视图解析器、静态资源映射这些)。
第二个是spring-mvc.xml。它的工作是开启注解驱动、扫描Controller包、配置视图解析器、放过静态资源。代码大致长这样:
<mvc:annotation-driven /> <context:component-scan base-package="com.example.bookstore.controller" /> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean> <mvc:default-servlet-handler />prefix和suffix合起来的意思是:Controller返回到ModelAndView时如果给的是字符串“book/list”,就会映射到/WEB-INF/jsp/book/list.jsp这个文件。default-servlet-handler是专门处理CSS、JS、图片这些静态资源请求的,少了它页面很可能长成一堆纯文本。
第三个是spring-mybatis.xml,负责把数据源、SqlSessionFactory和Mapper接口组装到一起。核心配置如下:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="mapperLocations" value="classpath*:mapper/*.xml" /> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.bookstore.mapper" /> </bean>数据源我习惯用Druid,它自带的监控页面在调试时特别好用。SqlSessionFactoryBean是把MyBatis接入Spring的关键,mapperLocations用来告诉MyBatis去哪找SQL映射XML文件。MapperScannerConfigurer会自动扫描指定包下的Mapper接口,让Spring直接管理它们。
配置文件最容易踩的坑就是包路径写错。basePackage如果写成了接口实际不在的包,启动不会立刻报错,但到真正调用Mapper方法的时候会抛BeanCreationException,排查起来相当头疼。所以动手改配置时,一定要对着项目的包结构反复确认。
2.2 用户模块:注册登录与权限控制
用户模块看似简单,但它牵扯到密码安全、Session状态管理和权限拦截三件事,做毕业设计时一定不能马马虎虎。
密码存储这一块,现在绝大多数正规项目都不会直接存明文密码。更常见的做法是加盐再哈希。具体操作是:用户注册时生成一个随机盐值(比如一串UUID),把“盐值 + 密码”拼起来之后做一次MD5,数据库里同时保存盐值和哈希结果。用户登录时,用提交的密码和库里的盐重新算一次哈希,判断是否相等。这样即使数据库泄露,攻击者拿到的也是一串不可逆的哈希值,而且每个用户的盐不同,没法直接撞库破解。
拦截器的写法其实不复杂,核心逻辑是:用户没有登录就访问需要登录的页面,跳回登录页;不是管理员却去访问后台管理页面,同样拦截下来。在Spring MVC里,实现HandlerInterceptor接口的preHandle方法就可以了。注册到配置类里,指定拦截路径比如“/admin/**”和“/login”、“/register”这些放行路径。
你可能会想:为什么不直接在Controller每个方法里都加一个if判断呢?那样确实也能实现,但代码会非常重复。十几个后台方法全部要写同一段判断,改一处还得跟着改十几处。拦截器把这种横切逻辑集中管理,这也是AOP思想在Web层的一种体现。
2.3 购物车模块:从加入购物车到删除
购物车的设计有两种主流思路:一种是把购物车数据存在Session里,另一种是落库。SSM毕设项目我个人建议选择存数据库,也就是用cart表来存。原因有两点:一是Session存购物车,用户一关浏览器东西就没了,无法体现“用户下次登录还能看到购物车”的效果;二是把购物车落库,能让你的项目多出一张表和多一组CRUD,答辩时展示的数据流更完整,老师也更认可。
购物车加购的Service核心逻辑是这样:
@Override @Transactional public void addToCart(Integer userId, Integer bookId, Integer quantity) { Book book = bookMapper.selectByPrimaryKey(bookId); if (book == null) { throw new RuntimeException("图书不存在"); } if (book.getStock() <= 0) { throw new RuntimeException("图书库存不足"); } CartItem cartItem = cartMapper.selectByUserIdAndBookId(userId, bookId); if (cartItem == null) { cartItem = new CartItem(userId, bookId, quantity); cartMapper.insert(cartItem); } else { int newQuantity = cartItem.getQuantity() + quantity; cartMapper.updateQuantity(cartItem.getCartId(), newQuantity); } }注意方法上加了@Transactional,这是Spring事务注解。因为“查购物车是否存在”和“插入/更新”这几步如果分两次执行,中途一旦出异常就会造成数据不一致。比如先插入了购物车记录,然后后续逻辑抛异常,这条半截数据就留在库里了。加了事务注解,任何一步出问题,全部操作都会回滚。
读取购物车列表时,通常是JOIN查book表把书名、封面、价格带出来,页面展示就是一张清晰的商品清单。购物车总价的合计逻辑,不要在JSP页面用脚本循环累加,更推荐在Service层提前算好放进模型里,这样页面只负责显示。
2.4 订单模块:状态流转是核心
订单模块是整个书城系统里业务逻辑最重的部分,也是最值得在答辩时展开讲的模块。
创建订单的流程,通常用事务包起来,依次做四件事:第一,遍历购物车中选中的商品,检查库存是否足够;第二,根据购物车明细计算出订单总额;第三,在orders表插入主记录,拿到自增的order_id;第四,在order_item表逐条插入明细,同时扣减book表中对应图书的库存。
这里有一个坑很容易忽略:扣减库存不能脱离事务单独执行。如果扣库存失败了,而订单已经插入成功,最终就会出现“订单存在但库存没减少”的问题,这在真实业务里是非常严重的事故。所以四个步骤必须全部包在同一个事务方法里,任一步骤抛异常就整体回滚。
订单状态用整数来存。我见过不少项目直接用字符串存“待付款”“已发货”,这也能跑,但不够规范。更常见的做法是定义常量或枚举:
public enum OrderStatus { UNPAID(0, "待付款"), PAID(1, "已付款"), SHIPPED(2, "已发货"), COMPLETED(3, "已完成"), CANCELED(4, "已取消") }状态流转可以整理成一张清晰的表格,面签的时候直接画给老师看:待付款可以被取消,待付款点击付款变成已付款,已付款由管理员发货变成已发货,已发货在用户确认收货后变成已完成。每个状态变更都要记录操作时间,这是订单生命周期管理的基本要求。
2.5 图书检索与分页:PageHelper的正确用法
图书检索一般会做成多条件组合查询:用户可能按书名、作者、出版社、分类筛选,也可能什么都不填就看全部。在MyBatis里,这种场景最合适的就是动态SQL。
<select id="searchBooks" resultType="com.example.bookstore.entity.Book"> SELECT * FROM book <where> <if test="title != null and title != ''"> AND title LIKE CONCAT('%', #{title}, '%') </if> <if test="categoryId != null and categoryId != ''"> AND category_id = #{categoryId} </if> </where> ORDER BY create_time DESC </select>这里必须强调一个关键点:模糊查询拼接条件时,一定要用#{}而不是${}。SQL注入攻击是Web系统最大的隐患之一,你知道为什么#{}能防住吗?因为MyBatis会把#{}占位符渲染成“?”这个预编译参数,真正传入的值不会参与SQL语句的拼接;而${}是直接把值拼到SQL里,相当于把攻击者的输入变成了SQL代码的一部分。
在登录表单里输入'or'1'='1,如果用了${},SQL就会变成SELECT * FROM user WHERE username = '' or '1'='1',直接把全表用户查出来了。用#{}则完全没有这个问题。这一条,既是安全底线,也是老师最爱考察的点。
分页方面,SSM项目里最主流的选择是PageHelper插件。用法非常简单:在查询方法之前调用PageHelper.startPage(pageNum, pageSize),紧接着的第一次Mapper查询就会被自动拦截并加上LIMIT。查完之后用PageInfo包一层,就能拿到总记录数、总页数、当前页码等全部信息。
PageHelper.startPage(pageNum, pageSize); List<Book> bookList = bookMapper.searchBooks(condition); PageInfo<Book> pageInfo = new PageInfo<>(bookList);使用PageHelper有一个古老但常见的坑:startPage只会对紧接着的第一次查询生效,如果你在startPage和查询之间插入了别的无关数据库操作,分页就会错位。所以项目里要养成习惯,startPage紧贴着Mapper调用写,不要隔东隔西。
3. 源码导入到本地跑通:完整实操流程
内容再好,代码跑不起来也是白搭。带源码的毕设项目,一半以上的时间可能都耗在环境搭建上。我这里按实际操作顺序,把从解压源码到浏览器看到页面的完整流程过一遍。
3.1 环境准备与版本搭配
SSM书城系统最常见的运行环境是:JDK 8、Tomcat 8.x、Maven 3.6.x、MySQL 5.7或8.x。下面这个表格是我推荐组合,按这个搭配,兼容性出问题的概率最小。
| 工具 | 推荐版本 | 备注 |
|---|---|---|
| JDK | 1.8 | 大多数SSM毕设源码基于JDK8编译 |
| Tomcat | 8.5.x | JDK8配Tomcat8最稳,不要用Tomcat10 |
| Maven | 3.6.3 | 3.8以上版本对某些仓库配置更严格 |
| MySQL | 5.7 或 8.0 | 8.0需要额外注意驱动类名和时区 |
| IDEA | 2020-2023均可 | 社区版也能跑Maven和Tomcat |
为什么特别强调不要用Tomcat 10?因为Tomcat 10的Servlet包名从javax.servlet换成了jakarta.servlet,老项目里的javax.*代码会直接编译不过。如果你手里拿到的是传统SSM源码,用Tomcat 8/9几乎零成本就能跑起来。
Maven这边,我建议在IDEA里明确指定一个本地的settings.xml,或者至少检查一下仓库地址能不能正常访问。如果下载依赖速度很慢,可以在Maven的settings.xml里配置一些国内仓库源。这一步做好了,后面基本是一片坦途。
3.2 导入IDEA与识别Maven项目
拿到源码后,不要直接把文件夹拖进IDEA,正确的做法是:IDEA里选择File -> Open,选中项目所在的根目录或pom.xml文件。IDEA检测到pom.xml后,会自动把它识别为Maven项目,并开始下载依赖。
依赖下载阶段,右下角的进度条会转悠很久,第一次等十几分钟都是正常的。这个阶段不要反复点“Reload”,更不要关IDEA。等依赖索引构建完成后,最好再把Maven侧栏打开,点一下“Reimport”,让所有模块的依赖关系重新梳理一遍。
源码到手先别急着启动,先花十分钟环顾一下结构:src/main/java下面包名是什么,mapper目录里有没有XML文件,resources下面有没有spring配置和jdbc.properties,webapp目录里有没有WEB-INF/web.xml。这个动作像是打仗前看地图,能让你对项目心里有数,也能提前发现“web.xml缺失”这类严重问题。
3.3 数据库初始化与连接配置
打开数据库管理工具,新建一个数据库,编码一定要选utf8mb4,然后导入源码里自带的SQL脚本。SQL脚本导入成功之后,打开service层或resources目录下的jdbc.properties文件,把连接信息改成你本地的。
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/bookstore?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=你的数据库密码如果你的MySQL是5.7,把serverTimezone和allowPublicKeyRetrieval这些参数删掉也没关系;但如果是MySQL 8.0,这后面的配置基本都得保留,缺了时区参数会启动就报Server time zone异常。driverClass这里也注意,MySQL 8.0的驱动类名是com.mysql.cj.jdbc.Driver,老项目里如果写的是com.mysql.jdbc.Driver,运行时会警告或直接报错。
改完配置,最稳的检查方式是直接在主方法或测试类里写一段查询——先不启动Tomcat,只测试数据库连接是否正常。能查出数据再往下走,排查起来高效得多。
3.4 启动Tomcat与功能验证清单
配置Tomcat前,先点“Edit Configurations”,新增一个Tomcat Server -> Local,把运行环境指向你本地的Tomcat目录。然后在Deployment页面添加Artifact,选“war exploded”模式,这样支持热部署,修改代码后重启速度也快。
启动后的第一个重点,是看日志而不是看页面。出现类似“Started”或“Server startup”的关键字,说明容器起来了。如果报了一堆红色异常,按下文第4章的表去排查。
页面验证建议按业务流的顺序逐项过一遍,避免遗漏。我给自己惯用一份验证清单,每次都照着走,也会推荐给你:
- 注册一个新账号,看数据库user表新增了一条记录
- 登录,检查Session是否正常,顶部用户名是否显示
- 修改个人资料,刷新后确认数据持久化成功
- 搜索一本不存在的书,确认页面没有报错而是显示“无结果”
- 搜索一本存在的书,确认分页条正确,点击第二页正常
- 把一本书加入购物车重复两次,确认数量累加而不是新增两行
- 对购物车中的书提交订单,确认orders和order_item各多出一条记录,同时book表的stock减了对应数量
- 用管理员账号登录后台,找到新订单,走一遍发货操作,前台用户能看到状态变化
走完这一遍清单,系统几乎所有主干功能就都被你亲手验证过了。这张验证表同时是你写“功能测试”章节最好的素材,答辩的时候直接说“我按业务流程逐项做过验证,数据变化如下”,效果比空口陈述强很多。
4. 启动报错、分页失效与乱码问题速查
这部分是我最想写的内容,因为网上那些源码包的质量参差不齐,拿到手各种报错的情况太常见了。我自己折腾这些项目时,踩过的坑不比写业务代码时少。
4.1 常见错误速查表
下面这张表,覆盖了我遇到的至少九成启动期和运行期问题。
| 报错现象 | 可能原因 | 解决办法 |
|---|---|---|
| ClassNotFoundException: com.mysql.jdbc.Driver | 驱动类名写错或驱动依赖缺失 | 改成com.mysql.cj.jdbc.Driver,pom里加mysql-connector-java依赖 |
| Invalid bound statement (not found) | Mapper接口和Mapper XML没绑定好 | 检查mapper-locations路径,确认XML的namespace与接口全限定名一致 |
| The server time zone value is unrecognized | MySQL 8的时区配置缺失 | URL里加serverTimezone=Asia/Shanghai |
| Port 8080 was already in use | 端口被占用 | 换一个端口,或找到占用进程杀掉 |
| NoClassDefFoundError | 依赖包没有完全打包到部署目录 | 清理target重新部署,检查Maven依赖是否缺失 |
| Access denied for user 'root'@'localhost' | 数据库账号密码不对 | 核对jdbc.properties,用命令行验证能不能连上数据库 |
| 页面能打开但CSS和图片全丢 | 静态资源被DispatcherServlet拦截 | 确认spring-mvc.xml里有mvc:default-servlet-handler |
| JSP页面报500但日志没有详细错误 | JSP语法错误或某个EL表达式不对 | 在页面前加page指令的isErrorPage调试,看完整堆栈 |
4.2 编码与乱码问题
SSM毕设项目里的乱码,简直是日常。网上流传的源码很多是在某个特殊编码环境下写的,到了你电脑上就变成了一堆问号。
排查思路从三个层面来。第一是IDEA的全局编码。File -> Settings -> Editor -> File Encodings,把Global Encoding、Project Encoding、Properties Files这三处全部选成UTF-8。第二是Tomcat的编码。在tomcat的conf/server.xml里,给Connector加一行URIEncoding="UTF-8"。第三是数据库连接的编码。这个已经在jdbc.properties里的URL参数中体现了:useUnicode=true&characterEncoding=utf8。
有同学会遇到“数据库里中文正常,页面显示乱码”的情况,这一般是JSP页面本身的pageEncoding和contentType没设置好。你可以在JSP头部确认pageEncoding和charset是不是UTF-8。如果还是不行,可以试试在Tomcat启动参数的VM options里加-Dfile.encoding=UTF-8,这个操作能解决不少诡异编码问题。
4.3 分页失效与SQL报错
分页失效最典型的场景是:PageHelper引入了,代码也写了,但查出来的数据还是全表数据。大概率就是PageHelper版本和MyBatis版本不匹配。低版本MyBatis配合高版本PageHelper,插件拦截机制会失效。解决办法是去Maven仓库查对应关系,换成兼容版本,或者干脆换一个常规的LIMIT写死分页,无非是参数多一点,但稳定性极高。
如果页面上翻页点击了第二页,但地址栏里的pageNum参数没变,问题多半出在JSP页面的分页链接拼写错误,去看看翻页按钮的href里面有没有把当前页码参数带过去。这种bug不难修,但肉眼排查很费时间,经验就是直接看生成页面的源码里a标签长什么样。
SQL层面还有一个很常见的坑:LIKE模糊查询拼接方式不对。很多人会写成LIKE '%#{keyword}%',这在MyBatis里是语法错误,会因为占位符被引号包裹而解析失败。正确的写法是LIKE CONCAT('%', #{keyword}, '%'),把百分号用函数拼到参数上。
5. 毕业设计答辩加分:从能用到好用
一套SSM书城系统如果只是“能跑”,毕业设计也能及格,但如果想让答辩更有说服力,在运行稳定、功能完整的基础上做几处低成本但高感知的升级,效果会好很多。
5.1 前端观感:引入现成前端框架
网上不少SSM源码的前端页面,还停留在裸JSP加原生HTML的形态,整个页面看上去非常素。观感不好,不代表你必须重写所有页面。一个非常省力的办法是引入Bootstrap或Layui这类现成样式框架,只替换CSS和JS资源,页面结构尽量不动。
具体操作时,把Bootstrap的CSS文件放进webapp的static目录,在原有JSP的head区域引入样式链接,需要写HTML的地方套用现成的组件类名。比如把搜索框换成输入框加按钮的组合,把商品卡片换成card组件,把表格加上table-striped类。这些改动不涉及Java代码,只需要动JSP模板,但整体视觉效果会明显提升一截。
需要注意一点:不要为了换肤而把原有CSS类名全部清掉。很多项目的页面逻辑依赖特定class做样式定位,直接删干净容易引发布局错乱。稳妥的做法是在原有基础上追加样式,而不是推翻重来。
5.2 加Redis缓存提升性能
书城系统是高并发场景的典型代表,图书分类、热销榜单这些数据很少变化,但访问量极大。每次请求都去数据库查一遍,数据库压力很大。这种场景最适合用缓存解决。
具体思路是:配置Redis,在Service层查询分类列表时先从Redis读,读不到再查数据库并把结果回写进Redis,设置一个合理的过期时间,比如3分钟。同时在新增或修改分类的时候,把Redis里的缓存清掉,保证下次查询拿到的是最新数据。
这个设计在答辩时可以讲出一套完整的“缓存穿透、缓存击穿、缓存雪崩”概念,哪怕只是浅浅聊几句缓存穿透是什么意思,老师都会觉得你的系统有真实业务复杂度在里面。但我要提醒你,Redis本身的配置和启动要额外花时间,如果项目原本已经很紧张,就别强行加了。为了加分反而把自己搞到完不成,得不偿失。
5.3 加一个数据统计页
数据统计页是很多同学容易忽略的加分项。书城系统天然有“订单金额”“图书销量”“用户增长”这些统计维度,用可视化图表展示出来,答辩演示的时候非常有效果。
最简单的方案是用ECharts这类图表库。准备一个后台菜单叫“销售统计”,页面用Ajax请求一个接口,后端返回按月统计的订单金额或销量数据,前端用柱状图或折线图渲染。比如按月份统计订单金额,SQL就一个GROUP BY语句:
SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, SUM(total_amount) AS amount FROM orders WHERE status != 4 GROUP BY month ORDER BY month这个需求本身不复杂,也就是Mapper加一条统计SQL、Controller加一个JSON接口、JSP页面加一张图表的事。但它展示出来的效果和“一个表格列一堆记录”完全不同,给人的感觉是“你这套系统做成一个完整产品了”。
关于升级,我最后想多说一句:这些加分项,优先级最高的是保持系统稳定可演示,其次才是炫技。答辩现场最怕的不是功能少,而是演示到一半项目崩了、报错了。你在功能完整、运行稳定、逻辑清晰这三条线下做的优化,才是真正有用的加分项。
我个人在实际折腾这种SSM项目时最深的体会是:拿到任何一套源码,第一步永远不是急着跑起来,而是先把目录结构和数据库表读懂。你每多理解一层设计,后面排查bug的时间就能少一块。另外,如果你手里的项目是JSP模板的版本,我不建议你把这个时间点强行改成前后端分离。毕设时间精力有限,SSM配合JSP这套传统组合,只要你功能完整、能讲清楚、演示顺利,分数完全不会亏待你。
最后再分享一个实用小技巧:在动手改代码之前,先把整个项目用Git做一次初始提交。后面你改配置、调代码,不管改崩了多少次,都能一键回到最初能跑的状态。就这一下,能帮你省出好几个晚上的折腾时间。