校园视频监控系统源码解析:SSM+MySQL数据库设计与ffmpeg集成实践

发布时间:2026/9/16 15:05:08
校园视频监控系统源码解析:SSM+MySQL数据库设计与ffmpeg集成实践 简介基于 SSM 与 MySQL 的校园视频监控系统源码面向 Java 毕业设计或课程设计场景涵盖个人中心、用户管理、摄像头管理、校园监控、留言板、系统管理等常用模块。功能上支持实时画面查看、录像回放与移动侦测代码采用 Spring、SpringMVC、MyBatis 分层设计既演示了 SSM 整合方式也包含权限分配、数据库持久化等关键实现。资源包共 1218 个文件、约 26.41MB包含 88 个 Java 源文件、79 个 JSP 页面、364 个 JS、146 个 CSS 以及图片素材另有 SQL 脚本、说明文档和介绍 PPT可覆盖从环境配置到项目部署的完整过程。内容预览显示项目保留了开发环境配置与备份文件说明目录结构完整、可直接导入开发工具使用适合按模块拆解学习或二次开发。已有 128 人学习下载对于需要快速完成相似毕设课题、梳理 SSM 技术栈或撰写论文的同学是一份可直接参考的完整实例。1. 校园视频监控系统源码里真正值钱的部分SSM、MySQL 与文件流的边界校园视频监控系统是毕业设计里少见的“后端、数据库、文件流、硬件模拟”四类问题都要碰的选题。反直觉的是它出问题最多的地方从来不是摄像头而是 SSM 整合时 service 层事务失控和 MySQL 里时间字段类型选错。很多拿到 ssmmysql 源码包的人第一周能跑起来第二周开始改功能时就发现视频路径硬编码、录像文件表没有索引、报警记录重复插入。这套源码真正的价值不在页面而在于把 Spring、SpringMVC、MyBatis 三层结构和 MySQL 业务表串成一条可演示的链路摄像头拉流、文件存储、报警记录、后台管理。适合已经学过 Java 基础和 SSM 框架但缺少一个完整工程来练手的人。2. SSM 与 MySQL 的选型逻辑一张视频监控表的字段如何决定工程质量2.1 为什么是 SSM 而不是 Spring BootMVC 分层让论文和答辩都更好讲毕业设计评审通常先看系统架构。SSM 把 Spring、SpringMVC、MyBatis 三个框架分别对应业务容器、请求分发、数据访问说明文档里能直接画出“浏览器 - Controller - Service - Mapper - MySQL”的时序图。Spring Boot 的自动配置虽然省事但定位问题要翻 autoconfigure 源码反而不适合做源码讲解。另一个现实理由是很多校园实训和课程设计题目本身就是按 SSM 设计的拿到的是一个可运行的 ssm 项目功能改造比框架迁移更合理。如果后面参加 java 面试SSM 里涉及的 IoC、AOP、动态代理和事务传播都是 java 面试题的高频内容这套代码可以当复习材料用。单从摄像头业务看Java 并不直接和设备打交道而是通过进程调用外部解码器。但这不意味着开发量少摄像头启停、录像计划、报警去重、文件清理、用户权限这些都是纯后端逻辑。SSM 的三层边界正好把这些逻辑拆开Controller 只接收前端请求Service 持有业务规则Mapper 只负责 SQL。论文“系统详细设计”一章也就有了现成的类图素材。2.2 从监控需求反推 8 张核心表摄像机、区域、用户、录像、报警不要一上来就只建一张 camera_info 表。校园监控必然涉及“哪个校区、哪栋楼、哪个摄像头、什么时间、录了什么、有没有报警”这条链数据模型最少要有区域、设备、录像、报警、用户和角色。用一张表凑合建的系统后面做按区域筛选和按摄像头查录像时一定会出现重复数据或查不到数据的尴尬。下面是按照 SSM 源码里常见做法整理的 8 张表表名用途关键字段campus_area校区/楼栋区域树id, parent_id, area_namecamera_info摄像头设备台账camera_no, rtsp_url, status, record_pathrecord_file录像文件生命周期camera_id, file_path, start_time, end_timealarm_log报警事件与重复计数camera_id, alarm_type, alarm_date, alarm_countsys_user登录账号username, password, salt, statussys_role角色与权限标识role_code, role_nameuser_role用户角色关联user_id, role_idoper_log用户操作日志user_id, module, method, create_time区域表用自关联 parent_id 做树形结构可以支持“东校区 - 实验楼 - 一层”三级路径。用户、角色单独拆表是因为监控后台的查看权限需要区分管理员、保卫处、普通用户授权逻辑写在拦截器里只需要判断 role_code 即可。录像文件表一定不要和摄像头表合并一台摄像头每天可能产生几十条录像记录合并后一张表数据量会快速膨胀查询和备份都越来越慢。2.3 建表语句里的 3 个坑时间字段、状态字段、相对路径字段先看摄像头信息表的常见建表方式CREATE TABLE camera_info ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 摄像头主键, area_id BIGINT NOT NULL COMMENT 所属区域关联 campus_area.id, camera_no VARCHAR(32) NOT NULL COMMENT 设备编号唯一, camera_name VARCHAR(64) NOT NULL COMMENT 显示名称, rtsp_url VARCHAR(255) NOT NULL COMMENT 拉流地址, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用 2掉线, record_path VARCHAR(255) DEFAULT NULL COMMENT 录像基础目录, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_camera_no (camera_no), KEY idx_area_status (area_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT摄像头信息表;这里其实踩了 3 个高频坑。第一个是时间字段start_time、end_time、create_time 都使用 DATETIME不要使用 TIMESTAMP。TIMESTAMP 在 2038 年会有溢出且受数据库时区影响本地开发机与服务器时区不一致时录像记录会整体偏移 8 小时。JDBC 连接串里加serverTimezoneAsia/Shanghai后DATETIME 的表现更稳定。第二个是状态字段status 用 TINYINT不要用 VARCHAR 存“在线/离线/停用”。MyBatis 映射时 TINYINT 可以直接转为 IntegerService 层用一个常量类定义 1、2、3 的含义。用 VARCHAR 的后果是筛选时容易把“在线”写成“再线”且索引长度更大。第三个是路径字段record_path 只存相对路径例如/data/campus/record/2025/05/17/截图和录像完成后再拼文件名。不要存http://开头的完整访问地址。因为视频文件通过服务器虚拟目录映射域名或盘符变化时只需要改服务器配置不需要 UPDATE 几十万条路径记录。再给录像文件表这张表是回放查询的核心CREATE TABLE record_file ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, camera_id BIGINT NOT NULL COMMENT 摄像头ID, file_path VARCHAR(255) NOT NULL COMMENT 录像相对路径, file_size BIGINT NOT NULL DEFAULT 0 COMMENT 字节数, duration INT NOT NULL DEFAULT 0 COMMENT 时长秒, start_time DATETIME NOT NULL COMMENT 开始时间, end_time DATETIME NOT NULL COMMENT 结束时间, PRIMARY KEY (id), KEY idx_camera_start (camera_id, start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT录像文件表;duration 用 INT 存秒数避免用 VARCHAR 存储00:10:00这种格式后续要统计总录制时长时可以直接用 SUM(duration)。联合索引 idx_camera_start 服务于“按摄像头与时间范围查录像”这是监控后台最高频的查询路径。摄像头表和录像表都建好之后再设计报警表就会顺很多。3. 用 Maven 搭起 SSM 工程从 web.xml 到 MyBatis 映射的最小可运行配置3.1 标准包结构controller / service / mapper 三层加一个 dto 层SSM 项目最容易翻车的地方是包结构混乱。常见做法是四层controller 放请求入口service 放业务接口service/impl 放实现类mapper 放 MyBatis 接口entity 放数据库实体dto 放查询参数和返回模型。下面这个结构在源码包里最常看到campus-vms ├── pom.xml └── src/main ├── java/com/campus/vms │ ├── controller │ ├── service │ │ ├── impl │ │ └── CameraService.java │ ├── mapper │ ├── dto │ │ └── CameraQuery.java │ └── entity ├── resources │ ├── mappers │ ├── jdbc.properties │ ├── spring-mvc.xml │ └── spring-mybatis.xml └── webapp/WEB-INF/web.xmlcom.campus.vms 这样的域名反写是约定不要用 test、demo 这类包名。dto 层很重要因为摄像头列表查询有分页、区域过滤、状态过滤直接把前端参数塞给 Mapper 会造成 XML 判空代码越来越复杂。Mapper 接口里只写ListCameraInfo selectCameras(CameraQuery query)所有条件都放在 CameraQuery 对象里扩展时只需要加字段和 XML 里的 test 判断。3.2 web.xml 只做三件事启动 Spring 容器、注册 DispatcherServlet、加编码过滤web.xml 配置太多项目就越难排查。这里只需要三件套context-param 指向 spring-mybatis.xmlDispatcherServlet 指向 spring-mvc.xml再放一个 CharacterEncodingFilter。很多源码会把 Spring 容器监听器和 DispatcherServlet 混在一起导致同一个 Service 被创建两次最终事务注解失效。context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mybatis.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener 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 /filter filter-mapping filter-nameencoding/filter-name url-pattern/*/url-pattern /filter-mapping servlet servlet-namespringmvc/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-namespringmvc/servlet-name url-pattern//url-pattern /servlet-mapping当 Spring 容器只加载 service、mapper 等数据层组件SpringMVC 容器只加载 controller 和视图解析器时事务边界最清晰。url-pattern 用/而不是*.do配合RequestMapping后前端请求路径更干净也不容易因为后缀影响方法签名映射。3.3 spring-mybatis.xml 里的 4 个必调参数数据源、Mapper 扫描、事务管理器、连接池spring-mybatis.xml 是整套源码能否跑起来的关键。除了常规配置有 4 个参数必须检查。看下面的配置bean iddataSource classorg.apache.commons.dbcp2.BasicDataSource destroy-methodclose property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ property nameinitialSize value5/ property namemaxActive value20/ property namemaxWait value60000/ property nameminEvictableIdleTimeMillis value300000/ property nametestWhileIdle valuetrue/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mappers/*.xml/ property nametypeAliasesPackage valuecom.campus.vms.entity/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.campus.vms.mapper/ property namesqlSessionFactoryBeanName valuesqlSessionFactory/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/这 4 个参数分别是 driverClassName、mapperLocations、basePackage、transaction-manager。驱动类必须写com.mysql.cj.jdbc.Driver这是 MySQL 8.0 之后的写法如果本地安装的是 MySQL 5.x旧驱动类虽然能用但会报时区警告。mapperLocations 指向classpath:mappers/*.xml保证每个 Mapper 接口都有对应的 XML 文件。basePackage 是 Mapper 接口的包根路径MapperScannerConfigurer 会为每个接口动态生成代理实现这也是 java 动态代理在项目中最直观的落地场景。连接池也需要重点说明。默认参数在监控场景下不够用因为录像写入和后台查询会并发访问数据库。常用参数建议如下参数建议值作用initialSize5初始化连接数maxActive20最大活跃连接数避免数据库被视频写入拖垮maxWait60000获取连接超时毫秒minEvictableIdleTimeMillis300000空闲连接回收时间testWhileIdletrue空闲时验证连接避免 MySQL 8 小时超时断开如果启动后每隔一段时间就报 “Connection is not available, request timed out”优先检查 maxWait 是否太短以及视频录制的保存任务是否占满了连接池。经常有同学把 maxActive 调到 200以为能解决并发结果 MySQL 默认最大连接数只有 151直接超限。3.4 MyBatis 多条件筛选 SQL一段可以直接抄走的分页查询后台管理页最常见的接口是“按摄像头编号、区域、状态分页查询”。用 MyBatis 动态 SQL 处理多条件比在 Java 里拼接 SQL 字符串安全得多。下面的 XML 片段是这类页面的标准写法select idselectCameras resultTypecom.campus.vms.entity.CameraInfo SELECT id, camera_no, camera_name, area_id, status FROM camera_info where if testcameraNo ! null and cameraNo ! AND camera_no LIKE CONCAT(%, #{cameraNo}, %) /if if testareaId ! null AND area_id #{areaId} /if if teststatus ! null AND status #{status} /if /where ORDER BY id DESC LIMIT #{offset}, #{limit} /selectwhere标签会自动去掉第一个条件前的 AND避免出现WHERE AND status 1。cameraNo 的模糊查询用 CONCAT 拼不要写成%${cameraNo}%后者会引发 SQL 注入。status 判断用! null如果把 status0 传过来默认的非空判断会认为 0 是空值因为 MyBatis 里的 null 和数值 0 不同但有些同学会写teststatus ! null and status ! 这会把 Integer 类型的 0 误判为空字符串吗不会但稳妥写法是只判断 null。在 Mapper 接口中需要给分页参数加一行注解ListCameraInfo selectCameras(CameraQuery query, Param(offset) int offset, Param(limit) int limit);CameraQuery 负责承载筛选条件offset 和 limit 单独用Param传递虽然 MyBatis 支持直接取对象属性但分页参数单独传可以让 XML 里的#{offset}语义更明确也方便后续改成 PageHelper 插件。4. 业务实现Java 调用 ffmpeg 拉流截图与录像文件落库的可靠写法4.1 摄像头拉流用 ProcessBuilder 调用 ffmpeg规避 Runtime.exec 的坑摄像头接入在毕设里通常是模拟的但为了把链路讲清楚最好还是按真实设备来设计。常见做法是通过 Java 启动一个 ffmpeg 子进程去拉 RTSP 流并截图。不要用Runtime.getRuntime().exec(cmd)直接传一串字符串因为视频 URL 里可能带账号密码和特殊字符空格、引号一旦被 shell 二次解析就会出错。更好的写法是 ProcessBuilder参数以 List 形式逐个传递ListString command new ArrayList(); command.add(/usr/local/bin/ffmpeg); command.add(-rtsp_transport); command.add(tcp); command.add(-i); command.add(cameraUrl); command.add(-frames:v); command.add(1); command.add(-f); command.add(image2); command.add(snapshotPath); ProcessBuilder pb new ProcessBuilder(command); pb.redirectErrorStream(false); Process process pb.start();这里有一个关键点ffmpeg 执行时会把调试信息写到 stderr如果不消费这个流子进程缓冲区写满后会被阻塞。启动后要开一个线程去读 stderr或者用pb.redirectErrorStream(true)把 stderr 合到 stdout再统一读取。截图命令的-frames:v 1表示只取一帧如果放在-i之前就会作用到输入源上导致行为不同。常用参数整理如下ffmpeg 参数说明常见坑-rtsp_transport tcp用 TCP 拉流减少花屏UDP 在某些校园网下丢包严重-frames:v 1只取一帧截图放在 -i 前会作用到输入源-f image2强制输出 image2 格式不指定时可能按扩展名生成其他格式-y覆盖已有文件不加会因文件已存在而退出截图成功后再向数据库插入一条记录或者更新摄像头最近截图路径。顺序不能反。很多源码是先把数据库状态改成“在线”再去调 ffmpeg结果是进程启动失败但页面上仍然显示摄像头正常。正确做法是先等 exitCode 返回 0再落库。4.2 录像文件落库分段录制时文件名和时间戳如何对齐录像回放的难点不是播放而是时间戳对齐。录制任务通常以固定时长分段执行比如每 600 秒生成一个 mp4 文件。启动录制前记录 startMillis进程正常退出后记录 endMillis然后写入 record_file 表long startMillis System.currentTimeMillis(); Process process processBuilder.start(); int exitCode process.waitFor(); long endMillis System.currentTimeMillis(); if (exitCode 0) { RecordFile record new RecordFile(); record.setCameraId(cameraId); record.setFilePath(/record/ fileName); record.setFileSize(new File(targetPath).length()); record.setStartTime(new Timestamp(startMillis)); record.setEndTime(new Timestamp(endMillis)); record.setDuration((int) ((endMillis - startMillis) / 1000)); recordFileService.insert(record); }start_time 和 end_time 用实际进程执行时间而不是用 System.currentTimeMillis() 之前拼接的计划时间原因是 RTSP 建立连接可能耗时几秒甚至几十秒如果按计划时间生成录像列表会出现重叠或者空洞。录像文件名建议用摄像头编号加时间戳例如CAM_001_20250517_180000.mp4这样即使数据库记录丢失运维人员也能从文件名判断录像归属。录像文件与数据库的一致性是另一个容易忽略的问题。定时清理历史录像时先删除文件还是先删数据库记录会产生两种不同状态的脏数据。常见做法是每次循环记录一批待删除的 record_file先执行文件删除全部成功后统一执行 DELETE如果中途有文件删除失败就跳过该条数据库记录下次清理时重试。反过来先删数据库一旦文件还在残留文件会越积越多。4.3 报警记录联动用唯一键去重避免同一个摄像头短时间刷屏监控系统的报警页面很容易被同一个摄像头刷屏原因是没有做时间窗口去重。比较省事的方案是在报警日志表上建一个按天聚合的唯一键再用 INSERT ... ON DUPLICATE KEY UPDATE 实现“10 秒内同一摄像头同类型报警只累加计数不新增行”。报警表可以这样建CREATE TABLE alarm_log ( id BIGINT NOT NULL AUTO_INCREMENT, camera_id BIGINT NOT NULL, alarm_type VARCHAR(20) NOT NULL COMMENT MOTION/SIGNAL_LOST, alarm_date DATE NOT NULL, alarm_count INT NOT NULL DEFAULT 1, first_time DATETIME NOT NULL, last_time DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_camera_alarm_day (camera_id, alarm_type, alarm_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT报警记录表;对应的写入 SQL 是INSERT INTO alarm_log (camera_id, alarm_type, alarm_date, alarm_count, first_time, last_time) VALUES (#{cameraId}, #{alarmType}, CURDATE(), 1, NOW(), NOW()) ON DUPLICATE KEY UPDATE alarm_count alarm_count 1, last_time VALUES(last_time);第一次插入时影响行数是 1代表这是一个新报警Service 层可以立刻发通知第二次触发时走到 update影响行数是 2代表这是重复报警只需要更新时间不需要再次弹窗。MySQL 的这个特性虽然简单但比“先 select 再 update”的原始写法少一次网络往返也避免了两条并发请求同时查出不存在记录然后各插一条的重复问题。如果报警后要绑定截图建议把截图的相对路径存在另一个 alarm_image 表中通过 alarm_id 关联不要直接塞到 alarm_log 的同一个字段里。5. 论文与源码一致性的 3 个验证技巧从 LW 文档倒推代码检查单5.1 用启动日志反查论文中的架构图与配置项拿到带有 LW 的源码包后第一件事不是改功能而是验证论文和代码是否对得上。启动项目时打开控制台日志重点看 Spring 容器的扫描日志、MyBatis 的 Mapper 注册日志以及数据库连接日志。如果论文架构图画的 Mapper 接口有 8 个而日志里只注册了 6 个说明源码被裁剪过或文档是拼凑的需要补充缺失的接口。配置项也一样论文里写的 MySQL 版本和 jdbc.properties 里的驱动类必须一致一个写 MySQL 5.7、一个用com.mysql.cj.jdbc.Driver答辩时会露馅。5.2 ER 图与建表语句做字段比对论文中的 ER 图是检查数据库设计最直接的对照物。把代码里的建表语句和 ER 图实体逐字段比对找是否存在同名不同类型。可以执行一段 SQL 快速导出现有表结构SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA campus_vms AND TABLE_NAME camera_info ORDER BY ORDINAL_POSITION;在 Navicat 或 MySQL Workbench 里执行后导出结果和论文实体属性图并排看。重点检查 id 是否都是 BIGINT、时间是否都是 DATETIME、状态字段是否都是 TINYINT。如果论文里写着camera_address而代码里字段名是camera_location尽量以代码为准因为 5.2 的查询结果可以直接作为论文数据字典表的内容改论文比改代码代价低。5.3 答辩演示时固定数据集与设备模拟状态答辩现场最怕网络波动导致摄像头拉流失败。建议在数据库里准备一台状态为停用、一台状态为刷新正常、一台状态为报警中的摄像头演示时只播放一个本地测试视频文件不依赖真实 RTSP 源。把 ffmpeg 可执行文件路径提取到app.properties例如ffmpeg.path/usr/local/bin/ffmpeg代码里通过Value(${ffmpeg.path})读取换电脑演示时只改这个配置项不需要重新编译。演示前运行一次DELETE FROM alarm_log; UPDATE camera_info SET status 1 WHERE id 1;把报警恢复初始状态确保点开报警查询时表格是干净的。最后把app.properties里的小写配置项和论文中系统配置表逐一对照你会发现大部分源码包不值得改业务只需要改配置文件就能完成一场完整答辩。本文还有配套的精品资源点击获取