资讯动态

SSM电影网站毕业设计:分层、多对多与事务避坑指南

发布时间:2026/10/22 18:53:34 来源:尧图企业网站定制
简介这份资源是面向计算机、通信、人工智能、自动化等相关专业学生与教师的SSM电影网站毕业设计完整源码包采用SSM框架结合MySQL数据库实现适合作为毕业设计、期末课程设计或课程大作业的参考方案也可供基础较好的学习者在此基础上二次开发、调整功能。压缩包共78个文件约7.02MB包含15个jsp页面文件、12个java后端源码、7个xml配置、4个css样式、2个js脚本以及1个sql数据库脚本另附图片、字体等静态资源覆盖前端展示、后端控制与数据持久化的完整结构。目前已有114人浏览学习项目经过调试测试可正常运行答辩评审分达到98分具有较高的学习借鉴价值。读者可从中获取清晰的SSM分层目录结构、数据库建表脚本与页面交互实现思路便于快速理解电影网站的业务流程与开发要点。1. 从零搭一套 SSM 电影网站毕业设计里最容易被低估的三个坑很多同学拿到「基于 SSMMySQL 的电影网站设计与实现」这个题目时第一反应是去搜一套现成源码跑起来能登录、能看列表、能下单就觉得稳了。真正答辩被问住的地方往往不是功能多少而是三件事SSM 三层到底怎么分工、电影这种「多对多 图片 分类」的数据模型怎么设计、以及前后端分离和传统 JSP 混用时请求怎么走。这篇笔记就按我实际带过几套类似系统的经验把从建库到跑通核心业务的路径拆开讲重点放在能复现、能改参数、能排错上。适合正在做 JavaWeb 方向毕业设计、需要一套能讲清楚也能跑起来的电影网站的同学也适合想拿 SSM 练手但一直卡在配置上的开发者。2. SSM 三层怎么切别把 Service 写成第二个 ControllerSSM 是 Spring SpringMVC MyBatis 的合称但真正决定代码质量的不是这三个框架本身而是你把业务逻辑放在哪一层。我见过太多毕业设计项目Controller 里直接调 MapperService 层只剩一个空壳转发答辩时老师一句「你的业务逻辑在哪」就答不上来。这一章先把分层讲透再落到具体目录结构和依赖配置。2.1 为什么电影网站特别需要 Service 层电影网站的核心业务不是单表增删改查。一部电影属于多个分类一个分类下有多部电影用户可以收藏、评分、评论排片和场次又跟影院、时间绑定。这些操作天然是跨表的如果全塞进 Controller事务根本没法控制。举个最典型的场景用户下单买票。你需要同时做四件事——扣减场次余票、生成订单记录、写入订单明细、更新用户积分。这四步必须在一个事务里任何一步失败都要回滚。如果写在 Controller 里你得手动开事务代码又臭又长写在 Service 里一个Transactional注解就解决了。Service public class OrderServiceImpl implements OrderService { Autowired private ScheduleMapper scheduleMapper; Autowired private OrderMapper orderMapper; Autowired private UserMapper userMapper; // 整个下单流程在一个事务里任一步抛异常全部回滚 Override Transactional(rollbackFor Exception.class) public OrderResult createOrder(Integer userId, Integer scheduleId, Integer seatCount) { // 1. 乐观锁扣减余票affectedRows 为 0 说明票被抢光 int affected scheduleMapper.reduceStock(scheduleId, seatCount); if (affected 0) { throw new BizException(余票不足请重新选择场次); } // 2. 生成订单主记录 Order order new Order(); order.setUserId(userId); order.setScheduleId(scheduleId); order.setSeatCount(seatCount); order.setStatus(0); // 0 待支付 orderMapper.insert(order); // 3. 更新用户积分积分规则写在 Service 而不是 SQL 里方便调整 userMapper.addPoints(userId, seatCount * 10); return new OrderResult(order.getId(), 下单成功); } }这段代码的关键点有三个。第一Transactional的rollbackFor必须写Exception.class因为 Spring 默认只对运行时异常回滚业务里抛的自定义异常如果是受检异常就不会回滚这是血泪经验。第二扣库存用update ... set stock stock - #{count} where id #{id} and stock #{count}这种写法靠数据库行锁保证并发安全比先查再改可靠得多。第三积分规则写在 Java 里而不是 SQL 里后面产品说「积分翻倍」你改一个数字就行不用动数据库。2.2 目录结构与 Maven 依赖的最小集合分层讲完落到工程结构。我一般用这样的包结构清晰且答辩好讲com.example.movie ├── controller // 接收请求参数校验返回 JSON ├── service │ ├── impl // 业务逻辑实现事务边界在这里 │ └── MovieService.java ├── mapper // MyBatis 接口只做数据访问 ├── entity // 数据库实体和表一一对应 ├── dto // 前端传参和返回对象和 entity 分开 ├── config // Spring、MyBatis、事务配置 └── common // 统一返回结果、异常处理、工具类Maven 依赖不用贪多下面这几个是跑起来的最小集合dependencies !-- Spring 核心版本用 5.3.x 稳定 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.30/version /dependency !-- SpringMVC处理 web 请求 -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.30/version /dependency !-- MyBatis 与 Spring 整合包 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.2/version /dependency !-- MySQL 驱动8.x 对应数据库 8.0 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- 连接池Druid 监控页面答辩时是加分项 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency !-- JSON 处理 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.3/version /dependency /dependencies参数说明Spring 5.3.x 是 5.x 最后一个稳定分支兼容 JDK 8 到 17毕业设计环境一般用 JDK 8 或 11选它最稳。MyBatis 用 3.5.x 配合 mybatis-spring 2.1.x注意 mybatis-spring 2.x 要求 Spring 5.x别混用 1.x 版本否则启动报NoClassDefFoundError。MySQL 驱动 8.x 的类名是com.mysql.cj.jdbc.DriverURL 要带时区参数serverTimezoneAsia/Shanghai否则连接报时区错误。2.3 Spring 与 MyBatis 整合的两个配置文件整合的核心是把SqlSessionFactory交给 Spring 管理同时开启 Mapper 接口扫描。我一般用 Java Config 而不是 XML因为答辩时讲起来更直观也不容易写错命名空间。Configuration MapperScan(com.example.movie.mapper) // 扫描 Mapper 接口省去每个都加 Mapper public class MyBatisConfig { Bean public DataSource dataSource() { DruidDataSource ds new DruidDataSource(); ds.setUrl(jdbc:mysql://localhost:3306/movie_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai); ds.setUsername(root); ds.setPassword(your_password); ds.setInitialSize(5); // 初始连接数 ds.setMaxActive(20); // 最大连接数毕业设计 20 足够 ds.setValidationQuery(SELECT 1); // 连接有效性检测 return ds; } Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factory new SqlSessionFactoryBean(); factory.setDataSource(dataSource); // 开启驼峰映射user_name 自动映射到 userName org.apache.ibatis.session.Configuration config new org.apache.ibatis.session.Configuration(); config.setMapUnderscoreToCamelCase(true); factory.setConfiguration(config); return factory.getObject(); } }MapperScan的值必须和你的 Mapper 包路径完全一致写错了启动时报No qualifying bean of type xxxMapper。mapUnderscoreToCamelCase打开后数据库的movie_name会自动映射到实体的movieName省掉大量resultMap配置但前提是你的字段命名规范别一会儿下划线一会儿驼峰。连接池的maxActive别设太大本地开发 20 足够设成 100 反而可能因为数据库最大连接数限制导致获取连接超时。3. 电影网站的数据模型多对多关系怎么落地才不返工数据模型是电影网站的地基地基歪了后面全是补丁。这一章把核心表设计、多对多关系处理、以及图片和分类的存储方案讲清楚每个决策都给出理由。3.1 核心表结构与字段类型选择电影网站最少需要这几张表用户表、电影表、分类表、电影分类关联表、评论表、订单表、场次表。下面给出关键表的建表语句重点看字段类型和索引。-- 电影表核心表字段类型要经得起推敲 CREATE TABLE movie ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, movie_name VARCHAR(100) NOT NULL COMMENT 电影名, director VARCHAR(50) DEFAULT NULL COMMENT 导演, actors VARCHAR(255) DEFAULT NULL COMMENT 主演逗号分隔, release_date DATE DEFAULT NULL COMMENT 上映日期, duration SMALLINT UNSIGNED DEFAULT NULL COMMENT 时长分钟, score DECIMAL(3,1) DEFAULT 0.0 COMMENT 评分一位小数, poster_url VARCHAR(255) DEFAULT NULL COMMENT 海报地址, intro TEXT COMMENT 简介, status TINYINT DEFAULT 1 COMMENT 1 上架 0 下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_name (movie_name), -- 按名称搜索 KEY idx_status (status) -- 按状态过滤 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 分类表 CREATE TABLE category ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, cate_name VARCHAR(30) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 电影分类关联表多对多的关键联合主键防重复 CREATE TABLE movie_category ( movie_id INT UNSIGNED NOT NULL, category_id INT UNSIGNED NOT NULL, PRIMARY KEY (movie_id, category_id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段类型的选择理由score用DECIMAL(3,1)而不是FLOAT因为浮点数存 8.7 可能变成 8.699999前端显示会出问题DECIMAL 精确存储。duration用SMALLINT UNSIGNED最大 65535 分钟足够比 INT 省空间。intro用TEXT而不是VARCHAR(2000)因为简介长度不可控TEXT 不占行内空间。关联表用联合主键(movie_id, category_id)天然防止同一部电影重复关联同一分类比加唯一索引更直接。3.2 多对多查询一次查出电影及其分类多对多最麻烦的是查询。列表页要显示每部电影的分类标签如果先查电影再循环查分类就是典型的 N1 问题20 部电影要发 21 条 SQL。正确做法是用LEFT JOIN一次查出来在 Java 里组装。!-- MovieMapper.xml 中的列表查询 -- select idselectMovieWithCategory resultMapMovieWithCategoryMap SELECT m.id, m.movie_name, m.score, m.poster_url, c.id AS cate_id, c.cate_name FROM movie m LEFT JOIN movie_category mc ON m.id mc.movie_id LEFT JOIN category c ON mc.category_id c.id WHERE m.status 1 ORDER BY m.create_time DESC LIMIT #{offset}, #{limit} /select !-- 自定义 resultMap 处理一对多嵌套 -- resultMap idMovieWithCategoryMap typecom.example.movie.dto.MovieVO id propertyid columnid/ result propertymovieName columnmovie_name/ result propertyscore columnscore/ result propertyposterUrl columnposter_url/ !-- collection 把同一电影的多个分类聚合成 List -- collection propertycategories ofTypecom.example.movie.entity.Category id propertyid columncate_id/ result propertycateName columncate_name/ /collection /resultMap逻辑说明LEFT JOIN保证即使电影没有分类也能查出来。resultMap里的id标签很关键MyBatis 靠它判断哪些行属于同一部电影如果写成result而不是id同一部电影的多个分类会被拆成多条记录前端看到重复电影。collection的ofType是集合元素类型不是集合本身类型写错会报类型转换异常。参数说明LIMIT #{offset}, #{limit}中 offset 是起始行limit 是每页条数分页参数在 Service 里算offset (pageNum - 1) * pageSize。别用LIMIT #{pageNum}, #{pageSize}直接传页码那是常见错误第二页会从第 2 行开始而不是第 11 行。3.3 图片存储存路径还是存二进制海报图片的存储方案直接影响数据库性能和部署难度。我见过把图片转成 Base64 存进 TEXT 字段的一部电影海报几百 KB列表页查 20 部就是几 MB 的数据传输数据库直接拖垮。常见做法有三种对比如下方案优点缺点适用场景存文件路径数据库轻量图片可 CDN 加速需管理文件目录迁移要同步毕业设计首选存 Base64无需文件服务单表搞定数据库膨胀查询慢仅头像等小图存对象存储 URL扩展性好不占服务器需第三方服务有成本生产环境毕业设计我一般选第一种图片上传到服务器某个目录数据库只存相对路径如/upload/poster/20250101_abc.jpg。上传接口用 SpringMVC 的MultipartFile接收注意配置MultipartResolver的最大文件大小默认是 1MB海报图很容易超。PostMapping(/upload) public Result uploadPoster(RequestParam(file) MultipartFile file) throws IOException { // 校验文件类型防止上传脚本文件 String original file.getOriginalFilename(); String suffix original.substring(original.lastIndexOf(.)).toLowerCase(); if (!Arrays.asList(.jpg, .jpeg, .png, .webp).contains(suffix)) { return Result.fail(只支持 jpg/png/webp 格式); } // 用时间戳重命名避免中文名和重名问题 String newName System.currentTimeMillis() suffix; File dest new File(/data/upload/poster/ newName); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); // 目录不存在就创建 } file.transferTo(dest); return Result.success(/upload/poster/ newName); }关键点文件后缀必须校验否则有人上传.jsp文件再访问就可能执行脚本这是安全红线。重命名用时间戳而不是原文件名避免中文乱码和覆盖。transferTo的目标目录要提前确认存在Spring 不会自动建目录报FileNotFoundException多半是这个原因。4. 从建库到跑通第一个接口可复现的操作顺序这一章给出一条从零到能访问接口的完整路径每一步都有命令和验证方法。按这个顺序走能避开大部分环境问题。4.1 建库建表与初始化数据先建库字符集必须用utf8mb4否则电影名里的特殊字符和 Emoji 存不进去。CREATE DATABASE movie_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE movie_db; -- 执行前面的建表语句再插入测试数据 INSERT INTO category (cate_name) VALUES (动作), (喜剧), (科幻), (爱情); INSERT INTO movie (movie_name, director, score, status) VALUES (示例电影一, 某导演, 8.5, 1), (示例电影二, 某导演, 7.2, 1); INSERT INTO movie_category (movie_id, category_id) VALUES (1, 1), (1, 3), (2, 2);验证方法执行SELECT m.movie_name, c.cate_name FROM movie m LEFT JOIN movie_category mc ON m.id mc.movie_id LEFT JOIN category c ON mc.category_id c.id;应该看到示例电影一对应动作和科幻两个分类示例电影二对应喜剧。如果分类显示为 NULL检查关联表数据是否插入成功。4.2 web.xml 与 SpringMVC 配置传统 SSM 项目用 web.xml 启动虽然现在流行 SpringBoot但毕业设计用传统方式更能体现你对原理的理解。核心配置是DispatcherServlet和ContextLoaderListener。web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee version4.0 !-- Spring 容器加载 Service 和 Mapper -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener !-- SpringMVC 前端控制器加载 Controller -- servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping !-- 字符编码过滤器解决 POST 中文乱码 -- filter filter-nameencoding/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-nameencoding/filter-name url-pattern/*/url-pattern /filter-mapping /web-appurl-pattern写/而不是/*区别在于/只拦截静态资源以外的请求/*会连 jsp 都拦截导致死循环。forceEncoding设为 true 保证响应也强制 UTF-8否则返回的 JSON 中文可能乱码。这两个参数是排查乱码问题的第一站。4.3 写第一个查询接口并验证Controller 只做参数接收和结果返回业务逻辑调 Service。RestController RequestMapping(/api/movie) public class MovieController { Autowired private MovieService movieService; GetMapping(/list) public Result list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize) { // 参数边界校验防止负数导致 SQL 报错 if (pageNum 1) pageNum 1; if (pageSize 1 || pageSize 50) pageSize 10; ListMovieVO list movieService.listMovies(pageNum, pageSize); return Result.success(list); } }验证步骤启动 Tomcat 后访问http://localhost:8080/api/movie/list?pageNum1pageSize5应该返回 JSON 数组。如果返回 404检查RequestMapping路径和 web.xml 的 url-pattern如果返回 500 且日志有Invalid bound statement说明 Mapper XML 的 namespace 或 id 和接口对不上如果返回空数组但数据库有数据检查status过滤条件和分页参数。5. 避坑与排查那些让项目卡半天的典型问题这一章记录 5 个我实际踩过或帮别人排查过的问题每个都按现象、原因、解决三步写照着查能省大量时间。5.1 启动报 DataSource 初始化失败现象Tomcat 启动时报Error creating bean with name dataSource后面跟着Access denied for user。原因数据库用户名密码错误或者 MySQL 8 的认证插件caching_sha2_password和旧版驱动不兼容。解决先确认用户名密码能在命令行mysql -u root -p登录成功。如果是认证插件问题执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;切换插件或者升级驱动到 8.0.x。注意改完要FLUSH PRIVILEGES;。5.2 查询返回字段全是 null现象接口返回 200但对象里字段都是 null数据库明明有值。原因数据库字段是下划线命名实体是驼峰命名但没开启驼峰映射。解决在 MyBatis 配置里加config.setMapUnderscoreToCamelCase(true)或者在 XML 里手写resultMap做映射。前者更省事但要求命名严格对应movie_name只能映射到movieName映射到movie_name或movieNAME都不行。5.3 事务不回滚现象下单接口抛异常了但库存已经扣了订单也生成了。原因Transactional没生效常见有三种方法不是 public、同类内部调用、异常类型不在回滚范围。解决确认注解方法用public修饰如果是在同一个类里 A 方法调 B 方法B 的事务不会生效要拆到不同类或用代理加rollbackFor Exception.class覆盖受检异常。另外确认配置类上加了EnableTransactionManagement。5.4 分页第二页数据重复现象第一页显示 1 到 10 条第二页还是从第 1 条开始。原因LIMIT参数传的是页码而不是偏移量。解决LIMIT第一个参数是偏移量公式是(pageNum - 1) * pageSize。在 Service 里算好再传给 Mapper别在 SQL 里写LIMIT #{pageNum}, #{pageSize}。这个错误很隐蔽因为第一页偏移量是 0看起来正常翻页才暴露。5.5 静态资源 404现象接口能访问但 CSS、JS、图片全部 404。原因DispatcherServlet的 url-pattern 是/把所有请求都拦截了包括静态资源。解决在 spring-mvc.xml 里加mvc:default-servlet-handler/让找不到的请求交回给 Tomcat 默认 Servlet 处理。或者用mvc:resources mapping/static/** location/static//显式映射静态资源目录。前者更省事后者更可控。6. 让答辩加分的一个技巧用 AOP 统一记录接口耗时功能跑通只是及格线答辩想拿高分得展示你对工程化的理解。这里给一个具体技巧用 Spring AOP 给所有 Controller 方法加耗时统计既能体现你懂切面编程又能在演示时拿出真实数据说明性能。思路是定义一个切面拦截controller包下所有方法记录执行时间并打印。代码不长但讲清楚原理能撑起一个答辩问题。Aspect Component public class ApiLogAspect { private static final Logger log LoggerFactory.getLogger(ApiLogAspect.class); // 切点controller 包下所有类的所有方法 Pointcut(execution(* com.example.movie.controller..*(..))) public void apiPoint() {} // 环绕通知能拿到方法执行前后的时间 Around(apiPoint()) public Object logCost(ProceedingJoinPoint joinPoint) throws Throwable { long start System.currentTimeMillis(); String method joinPoint.getSignature().toShortString(); try { Object result joinPoint.proceed(); // 执行原方法 long cost System.currentTimeMillis() - start; // 超过 500ms 打警告方便定位慢接口 if (cost 500) { log.warn([慢接口] {} 耗时 {}ms, method, cost); } else { log.info([接口] {} 耗时 {}ms, method, cost); } return result; } catch (Throwable e) { long cost System.currentTimeMillis() - start; log.error([异常] {} 耗时 {}ms异常{}, method, cost, e.getMessage()); throw e; // 异常必须继续抛否则全局异常处理器收不到 } } }参数说明execution(* com.example.movie.controller..*(..))中第一个*是返回类型任意..表示包及子包第二个*是方法名任意(..)是参数任意。Around比BeforeAfter好在能拿到返回值和控制执行时机。joinPoint.proceed()必须调用否则原方法不执行。异常捕获后要重新抛出不然事务和全局异常处理都会失效。启用 AOP 需要在配置类加EnableAspectJAutoProxy并引入aspectjweaver依赖。演示时你可以故意调一个没加索引的查询让日志打出「慢接口」然后现场加索引对比耗时这个操作比背概念有说服力得多。我自己的习惯是每套项目都留一个这样的切面不只为答辩调试时哪个接口慢一眼就能看到比在几十个方法里加System.currentTimeMillis()干净得多。做毕业设计别只盯着功能清单留一两个能讲出所以然的工程细节往往比多写三个 CRUD 页面更值钱。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑