☰
Java毕业设计:基于SSM框架的广告管理系统实战指南
2026/10/25 10:24:53 网站建设 项目流程

简介:这是一套面向高校计算机相关专业学生的Java毕业设计完整项目源码,采用SSM框架搭配Vue前端,B/S架构与MySQL数据库,适合作为毕业设计、课程设计参考或SSM+Vue全栈练手项目。系统围绕广告业务全流程设计,划分管理员、广告客户、广告商、广告制作商四类角色,覆盖广告投放渠道、广告需求、提案、设计预览、调整信息、签署与结算等模块,业务链路完整,便于理解多角色协作的权限与流程设计。压缩包共540个文件,约83.42MB,包含135个Java后端源码、50个Vue组件、23个XML配置、19个JS脚本,以及大量SVG图标、JPG图片和SQL脚本,前后端与数据库资源齐备。目前已有150人学习下载,可帮助读者快速掌握项目结构、接口组织与页面实现方式,并在此基础上进行二次开发或功能扩展。

1. 从一份“广告管理系统”说起:SSM 到底在毕设里扮演什么角色

如果你正在搜“Java毕业设计-基于SSM框架的广告管理系统”,大概率是三种人之一:要交毕设但不知道从哪下手、想找一个能跑通的 SSM 项目照着改、或者答辩前想搞清楚自己写的这套东西到底算不算“工程”。我带的几届学生里,广告管理系统是出现频率最高的选题之一,原因很现实——业务闭环清晰(广告主投广告、平台审核、展示统计),表结构不复杂,但又能塞进权限、分页、文件上传、定时任务这些加分点,答辩时不容易被问穿。

SSM 就是 Spring + SpringMVC + MyBatis 这套组合,在 Spring Boot 一统天下之前,它是 Java Web 项目最主流的落地方式。放到毕设场景里,它的价值不在于“新”,而在于分层清楚:Controller 收请求、Service 写业务、Mapper 碰数据库,答辩老师一眼能看出你懂 MVC。这篇笔记不吹这套技术多先进,只讲一件事——拿到这个选题,怎么把它从一堆 .java 文件变成一个能演示、能讲清楚、能改得动的系统。适合刚学完 Java 基础、会写增删改查但没搭过完整项目的人,也适合想快速判断这套代码值不值得参考的熟手。

2. 拆开 SSM 广告管理系统的骨架:三层结构怎么落地

2.1 为什么毕设仍然值得用 SSM 而不是直接上 Spring Boot

先说选型。现在网上到处是“Spring Boot + MyBatis 多商户商城源码”,看起来比 SSM 高级,但毕设答辩的逻辑不是比谁新,而是比谁能讲清楚。SSM 的配置文件虽然多,但每一份都有明确职责,你被问到“这个 bean 是怎么注入的”“事务在哪一层生效”,能指着 XML 说清楚。Spring Boot 把大量配置自动化了,反而容易让基础不牢的人答不上来“自动配置背后发生了什么”。

从工作量看,SSM 项目天然包含 web.xml、applicationContext.xml、spring-mvc.xml、mybatis-config.xml 这几份核心配置,正好对应答辩常问的“项目怎么启动的”“请求怎么找到 Controller 的”。广告管理系统本身业务不重,用 SSM 不会出现“配置比业务还多”的尴尬。我一般建议:如果你 Java 基础还行、想稳一点过答辩,SSM 是性价比最高的选择;如果你已经会 Spring Boot,那用 Boot 重写一遍当加分项也行,但别为了追新把项目搞成四不像。

2.2 广告管理系统的核心表结构和实体映射

不管代码包怎么组织,广告管理系统的数据模型基本跑不出这几张表:广告表(ad)、广告位/分类表(ad_category)、广告主或用户表(user)、投放记录或点击统计表(ad_record)。下面是我常用的建表 SQL,字段命名尽量和 Java 实体类对上,减少 MyBatis 映射时的手工对照成本。

-- 广告主/用户表 CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', `password` VARCHAR(64) NOT NULL COMMENT 'MD5存储', `role` TINYINT DEFAULT 0 COMMENT '0普通用户 1管理员', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 广告分类/广告位表 CREATE TABLE `ad_category` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL COMMENT '如首页banner、侧边栏', `width` INT COMMENT '广告位宽度', `height` INT COMMENT '广告位高度' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 广告主表 CREATE TABLE `ad` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `title` VARCHAR(100) NOT NULL, `content` TEXT COMMENT '广告文案', `image_url` VARCHAR(255) COMMENT '图片路径', `category_id` INT COMMENT '关联广告位', `user_id` INT COMMENT '投放人', `status` TINYINT DEFAULT 0 COMMENT '0待审核 1已通过 2已拒绝', `start_time` DATETIME, `end_time` DATETIME, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (`category_id`) REFERENCES `ad_category`(`id`), FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里有几个参数要留意。status用 TINYINT 而不是布尔,是因为广告有“待审核、通过、拒绝”三态,布尔不够用。start_time和end_time决定广告是否在有效期内,展示逻辑里必须判断,否则答辩时被问“过期广告怎么处理”会卡壳。外键在毕设里加不加有争议,加了能体现数据完整性,但批量删数据时容易报错,我一般保留外键约束,删除用逻辑删除(加is_deleted字段)绕开。

2.3 用 MyBatis 逆向或手写 Mapper 完成第一轮 CRUD

实体类建好后,Mapper 层是最容易出成果的地方。如果你用 MyBatis Generator 逆向生成,能省掉大量重复代码;但毕设答辩更希望你手写几个关键 SQL,体现你懂动态 SQL。下面是一个广告查询的 Mapper 接口和 XML 片段,覆盖了条件筛选和分页。

// AdMapper.java public interface AdMapper { // 条件查询广告列表,支持按状态和分类过滤 List<Ad> selectByCondition(@Param("status") Integer status, @Param("categoryId") Integer categoryId, @Param("offset") Integer offset, @Param("limit") Integer limit); int countByCondition(@Param("status") Integer status, @Param("categoryId") Integer categoryId); }
<!-- AdMapper.xml --> <select id="selectByCondition" resultType="com.demo.entity.Ad"> SELECT * FROM ad <where> <if test="status != null">AND status = #{status}</if> <if test="categoryId != null">AND category_id = #{categoryId}</if> </where> ORDER BY create_time DESC LIMIT #{offset}, #{limit} </select>

逻辑说明:<where>标签会自动处理第一个AND,避免拼接出WHERE AND这种语法错误,这是 MyBatis 动态 SQL 最常用的技巧。offset和limit由 Service 层根据页码算出来,比如第 2 页每页 10 条,offset 就是 10。参数用#{}而不是${},前者是预编译占位符能防 SQL 注入,后者是字符串拼接,答辩时如果被看到用${}拼查询条件,基本会被追问安全问题。分页这里用的是物理分页(LIMIT),数据量大时比内存分页靠谱,毕设数据量小两种都行,但写物理分页更显专业。

3. 把项目跑起来:环境、配置和第一个能点的页面

3.1 JDK、Tomcat、Maven 的版本搭配与踩坑点

SSM 项目对版本比较敏感,配错了启动就报错。我一般用这套组合:JDK 8(别用太高,老版本 Spring 对 JDK 17 支持不好)、Tomcat 8.5 或 9、Maven 3.6+、MySQL 5.7 或 8.0。JDK 环境变量配置是老生常谈,但“java 环境变量使用多个 jdk”这个热搜说明很多人在这翻车——如果你机器上有多个 JDK,一定要把JAVA_HOME指向项目要用的那个,并且把%JAVA_HOME%\bin放在 Path 最前面,否则命令行java -version和 IDE 里用的可能不是同一个。

Maven 的settings.xml记得配国内镜像,不然拉依赖能等到怀疑人生。依赖版本上,Spring 用 5.2.x、MyBatis 用 3.5.x、mybatis-spring 用 2.0.x,这几个版本互相兼容。如果你看到java.lang.OutOfMemoryError,先别急着调堆大小,多半是 Maven 编译时内存不够,在MAVEN_OPTS里加-Xmx1024m通常能解决,而不是去改 Tomcat 的catalina.bat。

3.2 web.xml 与 spring-mvc.xml 的关键配置

SSM 项目启动的核心在web.xml,它负责加载 Spring 容器和 SpringMVC 前端控制器。下面这段是必须有的骨架。

<!-- web.xml 关键片段 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>dispatcher</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>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>

ContextLoaderListener加载的是 Service 和 Dao 层的配置,DispatcherServlet加载的是 Controller 相关的配置,两者分工不能混。url-pattern写/表示拦截所有请求交给 SpringMVC 处理,静态资源(css、js、图片)需要在spring-mvc.xml里加<mvc:default-servlet-handler/>放行,否则页面样式全丢。这是新手最常见的翻车点之一:页面能打开但没样式,八成是静态资源被 DispatcherServlet 拦了。

3.3 写一个广告列表接口并验证前后端连通

配置通了之后,写第一个能返回数据的接口。Controller 里注入 Service,Service 调 Mapper,返回 JSON 给前端。

@Controller @RequestMapping("/ad") public class AdController { @Autowired private AdService adService; @RequestMapping("/list") @ResponseBody public Map<String, Object> list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(required = false) Integer status) { int pageSize = 10; int offset = (page - 1) * pageSize; List<Ad> list = adService.query(status, offset, pageSize); int total = adService.count(status); Map<String, Object> result = new HashMap<>(); result.put("list", list); result.put("total", total); return result; } }

@ResponseBody让返回值直接序列化成 JSON,不经过视图解析器。page给了默认值 1,前端不传也不会报错。status用required = false,因为“查全部”和“查某个状态”都要支持。验证时先在浏览器直接访问http://localhost:8080/项目名/ad/list,能看到 JSON 就说明 SSM 三层已经打通。如果报 404,检查spring-mvc.xml的组件扫描包路径有没有覆盖到 Controller;如果报 500 且提示找不到 Service,检查applicationContext.xml的扫描配置和@Service注解有没有加。

4. 广告业务里真正要写对的三块逻辑

4.1 广告审核状态流转与权限拦截

广告管理系统区别于普通 CRUD 的地方就在审核。普通用户提交广告后是待审核状态,管理员登录后才能看到审核入口并改变状态。这块逻辑要写对两件事:状态流转的合法性、接口的权限控制。

状态流转我一般用一个简单的状态机思路:只允许 0→1、0→2 两种跳转,已经审核过的(1 或 2)不允许再改,除非管理员重置。Service 层加判断,别把逻辑丢给前端,前端传什么状态都信是典型的安全漏洞。

public boolean audit(Integer adId, Integer newStatus) { Ad ad = adMapper.selectById(adId); if (ad == null) return false; // 只有待审核状态才能被审核 if (ad.getStatus() != 0) { throw new RuntimeException("该广告已审核,不能重复操作"); } if (newStatus != 1 && newStatus != 2) { throw new RuntimeException("非法状态值"); } return adMapper.updateStatus(adId, newStatus) > 0; }

权限拦截用 Spring 拦截器实现,判断 session 里的用户角色。管理员接口统一放在/admin/路径下,拦截器只拦这个前缀,普通用户接口不拦。这样比在每个方法里写 if 判断干净得多。拦截器注册在spring-mvc.xml里,注意放行登录接口和静态资源,否则登录页都打不开。

4.2 广告图片上传与路径存储

广告离不开图片,文件上传是必做项。用 SpringMVC 的MultipartFile接收,存到服务器本地目录,数据库只存相对路径。这里有个血泪经验:千万别把绝对路径存进数据库,换台机器部署路径就失效了。

@RequestMapping(value = "/upload", method = RequestMethod.POST) @ResponseBody public Map<String, Object> upload(@RequestParam("file") MultipartFile file, HttpServletRequest request) throws IOException { Map<String, Object> result = new HashMap<>(); if (file.isEmpty()) { result.put("code", 400); result.put("msg", "文件为空"); return result; } // 用时间戳重命名,避免同名覆盖 String original = file.getOriginalFilename(); String suffix = original.substring(original.lastIndexOf(".")); String newName = System.currentTimeMillis() + suffix; // 存到 webapp 下的 upload 目录 String realPath = request.getServletContext().getRealPath("/upload/"); File dir = new File(realPath); if (!dir.exists()) dir.mkdirs(); file.transferTo(new File(dir, newName)); result.put("code", 200); result.put("url", "upload/" + newName); // 存相对路径 return result; }

参数上要注意spring-mvc.xml里必须配置CommonsMultipartResolver,否则MultipartFile注入不进来。文件大小限制在解析器里设maxUploadSize,默认值偏小,传大图会报MaxUploadSizeExceededException。后缀名要做白名单校验,只允许 jpg、png、gif,防止上传可执行文件。存相对路径的好处是前端直接用<img src="${url}">就能显示,部署到哪台机器都不用改数据。

4.3 广告有效期判断与定时下架

广告有start_time和end_time,展示时只查有效期内的。这个判断放在 SQL 里比放 Java 里高效,因为数据库能走索引。

<select id="selectValidAds" resultType="com.demo.entity.Ad"> SELECT * FROM ad WHERE status = 1 AND start_time &lt;= NOW() AND end_time &gt;= NOW() ORDER BY create_time DESC </select>

XML 里<和>要转义成&lt;和&gt;,或者用 CDATA 包起来,这是 MyBatis 写比较条件时的固定坑。定时下架可以用 Spring 的@Scheduled注解,每天凌晨把过期广告状态改成下架,配置<task:annotation-driven/>开启定时任务支持。毕设里定时任务不是必须,但加上去答辩时是个亮点,说明你考虑了数据时效性。注意@Scheduled的方法不能有参数,且所在类要被 Spring 扫描到。

5. 避坑与排查:SSM 项目最容易翻车的 5 个地方

5.1 现象:启动报 NoClassDefFoundError 或 ClassNotFoundException

原因:依赖没拉全或版本冲突。SSM 涉及 spring-core、spring-webmvc、mybatis、mybatis-spring、数据库驱动、连接池等多个 jar,少一个就起不来。常见的是 mybatis-spring 版本和 Spring 版本不匹配,或者 MySQL 驱动版本和数据库版本对不上(MySQL 8 要用com.mysql.cj.jdbc.Driver,5.x 用com.mysql.jdbc.Driver)。

解决:在 IDEA 的 Maven 面板执行mvn dependency:tree看依赖树,找红色冲突项用<exclusions>排除。驱动类名和 URL 参数按数据库版本改,MySQL 8 的 URL 还要加serverTimezone=Asia/Shanghai,否则报时区错误。

5.2 现象:页面 404,Controller 方法进不去

原因:组件扫描路径不对,或者spring-mvc.xml没配<mvc:annotation-driven/>。前者导致 Controller 没被注册成 bean,后者导致@RequestMapping不生效。

解决:检查spring-mvc.xml里<context:component-scan base-package="com.demo.controller"/>的包路径是否和实际 Controller 所在包一致。加上<mvc:annotation-driven/>开启注解驱动。如果用了 REST 风格返回 JSON,还要确认 Jackson 依赖在不在。

5.3 现象:数据库能连上但查询返回 null

原因:实体类字段名和表字段名对不上,或者 MyBatis 没开启驼峰映射。比如表字段create_time,实体类属性createTime,不开驼峰映射就映射不上。

解决:在mybatis-config.xml里加<setting name="mapUnderscoreToCamelCase" value="true"/>。或者老老实实在 Mapper XML 里写<resultMap>手动映射。前者省事,后者可控,我一般开驼峰映射,特殊字段再单独写 resultMap。

5.4 现象:事务不生效,插入失败后数据没回滚

原因:@Transactional加在了 Controller 层,或者 Service 类没被 Spring 代理。Spring 事务默认基于代理,同类内部方法调用不走代理,事务也会失效。

解决:@Transactional加在 Service 实现类的方法上,确保该类被 Spring 扫描并注入。如果是同类内部调用,把方法拆到另一个 Service 里,或者用 AopContext 拿代理对象。另外注意applicationContext.xml里要配DataSourceTransactionManager并开启<tx:annotation-driven/>。

5.5 现象:中文乱码,页面显示问号

原因:数据库字符集、连接 URL 字符集、页面编码三处不一致。常见的是数据库建表用了 latin1,或者 URL 没加characterEncoding=utf8。

解决:建库建表统一用utf8mb4,JDBC URL 加useUnicode=true&characterEncoding=utf8,JSP 页面顶部加<%@ page contentType="text/html;charset=UTF-8" %>,web.xml 里加CharacterEncodingFilter统一设置请求编码。三处都对了才不会乱码。

6. 让这套系统在答辩里多拿几分:两个进阶技巧

6.1 用拦截器 + 注解实现细粒度权限

前面说的拦截器只判断了“是不是管理员”,但广告系统里可能还有“只能改自己的广告”这种需求。我一般自定义一个@RequireRole注解,拦截器里读注解值判断角色,比在方法里写 if 优雅。

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireRole { int value() default 1; // 默认管理员 }

拦截器preHandle里通过HandlerMethod拿到方法上的注解,再比对 session 里的 role。这样权限规则跟着方法走,一眼能看出哪个接口要什么权限,答辩时老师问“权限怎么控制的”,你指着注解就能讲清楚。注意拦截器要放行登录接口,否则登录本身就被拦了。

6.2 用 PageHelper 替换手写分页

手写 LIMIT 分页能体现你懂原理,但代码重复度高。PageHelper 是 MyBatis 生态里最常用的分页插件,一行代码搞定。

// Service 层 public PageInfo<Ad> queryByPage(Integer page, Integer pageSize, Integer status) { PageHelper.startPage(page, pageSize); List<Ad> list = adMapper.selectByCondition(status); return new PageInfo<>(list); }

PageHelper.startPage只对紧接着的第一条查询生效,中间不能插入其他查询,否则分页会串。返回的PageInfo自带总页数、总记录数、当前页,前端直接拿来渲染分页条。引入方式是 Maven 加pagehelper依赖,mybatis-config.xml里注册PageInterceptor插件。用了 PageHelper 之后,Mapper 里就不用写 LIMIT 了,SQL 更干净。

最后说个我自己的习惯:每次改完配置或 SQL,先别急着点页面,直接在浏览器访问接口看返回的 JSON,JSON 对了再调前端。这样能把“后端问题”和“前端问题”分开,排查效率高很多。毕设这东西,跑通一次之后你会发现大部分时间都花在配置和字段对上,业务逻辑本身并不难。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询