简介:这是一套面向计算机专业本科生的高分毕业设计实战资源,基于SpringBoot后端与微信小程序前端构建校园自助打印系统,解决高校学生文档远程提交、设备扫码取件、费用在线支付等实际场景需求,特别适合毕业设计、课程设计及Java全栈项目练手。资源包共882个文件,含102个Java核心业务类、99个Vue前端组件、100个JS交互逻辑、242张PNG/SVG界面素材及1个SQL数据库脚本,辅以演示视频(mp4)和3个批处理脚本(bat)实现一键安装、运行与构建,整体压缩包42.09MB,结构清晰、模块完整。目前已有65人学习下载,所有代码经导师评审获99分,确保可直接运行,小白也能通过配套文件快速部署调试。读者将获得完整可执行源码、数据库表结构与初始化数据、全流程操作演示视频、前后端分离目录规范及常见问题排错提示,切实提升工程化开发与答辩应对能力。
1. 校园自助打印系统不是“小程序+后台”拼凑,而是用 SpringBoot 做稳服务中枢、微信小程序做轻量触点、MySQL 做可靠数据底座的闭环实践
很多同学把“Java毕业设计-基于SpringBoot+微信小程序的校园自助打印系统”简单理解为“后端写个接口、前端调个 wx.request”,结果部署时文件上传失败、订单状态不同步、用户扫码取件超时,答辩被问“并发 50 人同时上传 PDF 怎么扛?”直接卡壳。这项目真正的技术锚点不在“能跑”,而在“可验证、可追溯、可运维”:SpringBoot 要管好文件分片上传与异步转码(PDF → 打印机可识别格式)、微信小程序要处理离线缓存与扫码权限校验、MySQL 必须通过事务+索引+字段约束保障“下单→支付→排队→出纸”全链路数据一致。它适合已学完 Java 基础、SpringBoot 核心(@RestController、@Transactional、Spring Security)、微信小程序基础 API(wx.uploadFile、wx.scanCode)的同学,作为课程设计或毕设,重点考察你能否把教科书里的单点知识,拧成一条从用户点击“立即打印”到打印机吐纸的完整数据流。
2. 用 SpringBoot 搭建高可用打印服务中枢:文件接收、任务调度与状态同步三件套
校园场景下,学生常传 Word、PPT、图片甚至扫描件 PDF,大小从几十 KB 到上百 MB 不等;打印机分布在教学楼、宿舍区多个物理点位,网络环境不稳定;管理员需实时查看各设备排队数、耗材余量。这就要求后端不能只做 CRUD,而要构建“接收—转换—分发—反馈”四层能力。常见做法是:用 SpringBoot WebMvc 接收小程序上传的 base64 或 multipart 文件,经 Apache POI / PDFBox 解析元信息并校验页数/尺寸,再交由 Quartz 定时扫描待处理队列,调用 CUPS 或厂商 SDK 发送至指定打印机。关键不在功能堆砌,而在每一步都留痕、可回溯。
2.1 文件上传与安全校验:拒绝“传啥接啥”,强制白名单与大小熔断
学生可能误传 exe、zip 甚至恶意脚本,必须在网关层拦截。SpringBoot 中不推荐直接用MultipartFile接收原始流,而应配置spring.servlet.multipart.max-file-size=10MB和max-request-size=10MB防止 OOM,再在 Controller 层做二次校验:
@PostMapping("/api/v1/upload") public ResponseEntity<UploadResult> handleUpload( @RequestParam("file") MultipartFile file, @RequestParam("printerId") Long printerId, @RequestHeader("X-User-Id") String userId) { // 1. 后缀白名单(仅允许 .pdf .docx .xlsx .jpg .png) String originalName = file.getOriginalFilename(); String ext = originalName.substring(originalName.lastIndexOf(".") + 1).toLowerCase(); if (!Set.of("pdf", "docx", "xlsx", "jpg", "png").contains(ext)) { return ResponseEntity.badRequest().body(new UploadResult(false, "不支持的文件类型")); } // 2. 内容头校验(防伪后缀) try (InputStream is = file.getInputStream()) { byte[] header = new byte[4]; is.read(header); String mimeType = detectMimeType(header); if (!mimeType.contains(ext)) { return ResponseEntity.badRequest().body(new UploadResult(false, "文件内容与后缀不符")); } } catch (IOException e) { return ResponseEntity.status(500).body(new UploadResult(false, "文件读取异常")); } // 3. 保存至本地临时目录(非 webapp 下!),生成唯一 task_id String taskId = UUID.randomUUID().toString().replace("-", ""); Path tempPath = Paths.get("/data/print/temp/", taskId + "." + ext); Files.createDirectories(tempPath.getParent()); file.transferTo(tempPath); // 4. 写入数据库:task_id, user_id, printer_id, status='WAITING', created_time PrintTask task = new PrintTask(); task.setTaskId(taskId); task.setUserId(userId); task.setPrinterId(printerId); task.setStatus("WAITING"); task.setFilePath(tempPath.toString()); task.setFileSize(file.getSize()); printTaskMapper.insert(task); return ResponseEntity.ok(new UploadResult(true, taskId)); }注意:
detectMimeType(byte[])需用Files.probeContentType()或 Apache Tika 库实现,仅靠后缀判断极易被绕过;/data/print/temp/目录必须由运维提前创建并赋予 SpringBoot 进程写权限,禁止使用System.getProperty("user.dir")动态路径——上线后因工作目录变更导致写入失败是高频事故。
2.2 异步任务调度:用 Quartz 管理“等待中”任务的生命周期
学生上传后不希望干等,系统需在后台持续轮询待处理任务,并触发转码与下发。若用@Scheduled(fixedDelay = 5000)简单轮询,集群部署时会重复执行。正确做法是引入 Quartz,配置 JDBCJobStore 实现分布式锁:
# application.yml spring: quartz: job-store-type: jdbc jdbc: initialize-schema: never # 手动建表,避免自动初始化污染生产库 properties: org.quartz.scheduler.instanceName: print-scheduler org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate org.quartz.jobStore.tablePrefix: QRTZ_ org.quartz.jobStore.isClustered: true org.quartz.threadPool.threadCount: 3建表语句需执行 Quartz 官方提供的tables_mysql_innodb.sql(注意选 InnoDB 引擎,支持行级锁)。然后定义一个PrintJob类,继承QuartzJobBean,在executeInternal中查询status='WAITING' AND created_time < NOW() - INTERVAL 30 SECOND的任务(防刚上传就抢),调用 PDFBox 将 PDF 转为标准 PostScript 或直接生成打印机可识别的 PCL 流:
@Component public class PrintJob extends QuartzJobBean { @Autowired private PrintTaskMapper taskMapper; @Autowired private PrinterService printerService; // 封装 CUPS 调用或厂商 SDK @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { // 查询待处理任务(加 for update 防并发) List<PrintTask> waitingTasks = taskMapper.selectWaitingTasksForUpdate(3); // 最多取 3 条 for (PrintTask task : waitingTasks) { try { // 1. 转码:PDF → PS(示例,实际按打印机型号适配) File psFile = PdfToPsConverter.convert(task.getFilePath()); // 2. 下发至打印机(含重试逻辑) boolean success = printerService.sendToPrinter(task.getPrinterId(), psFile); // 3. 更新状态 task.setStatus(success ? "PRINTING" : "FAILED"); task.setUpdateTime(new Date()); taskMapper.updateById(task); } catch (Exception e) { task.setStatus("FAILED"); task.setErrorMessage(e.getMessage()); taskMapper.updateById(task); } } } }提示:
selectWaitingTasksForUpdate(3)对应的 XML Mapper 必须显式写SELECT ... FOR UPDATE,且该 SQL 需走printer_id索引,否则全表锁将拖垮性能;PdfToPsConverter不建议用 iText 直接渲染(版权风险),PDFBox 是更稳妥的开源选择。
2.3 状态同步机制:用 WebSocket 实现实时进度推送,替代轮询
小程序端若每 2 秒setInterval调一次/api/v1/task/status?taskId=xxx,50 人同时在线即产生 25 QPS 无效请求,服务器 CPU 持续 40%+。应改用 SpringBoot 集成 WebSocket,建立长连接通道。关键配置如下:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(printWebSocketHandler(), "/ws/print-status") .setAllowedOrigins("https://your-miniprogram-domain.com"); // 微信小程序域名白名单 } }当任务状态更新时(如从WAITING→PRINTING),服务端主动推送:
@Service public class PrintStatusNotifier { @Autowired private SimpMessagingTemplate messagingTemplate; public void notifyStatusChange(String taskId, String status) { messagingTemplate.convertAndSend("/topic/print/" + taskId, Map.of("taskId", taskId, "status", status, "timestamp", System.currentTimeMillis())); } }小程序端监听:
// pages/print/print.js onLoad() { this.ws = wx.connectSocket({ url: 'wss://api.yourschool.edu/ws/print-status' }); this.ws.onOpen(() => { console.log('WebSocket 连接成功'); }); this.ws.onMessage((res) => { const data = JSON.parse(res.data); if (data.taskId === this.data.taskId) { this.setData({ currentStatus: data.status }); if (data.status === 'SUCCESS') { wx.showToast({ title: '打印完成', icon: 'success' }); } } }); }注意:微信小程序 WebSocket 仅支持
wss://(必须 HTTPS),且域名需在小程序后台「开发管理 → 开发者工具」中提前配置;/topic/print/{taskId}是 STOMP 协议路径,需引入spring-boot-starter-websocket和spring-websocket依赖。
3. 微信小程序端:从扫码取件到离线排队,用本地缓存与权限控制补足网络短板
校园内 WiFi 信号在楼梯间、地下室常中断,若小程序完全依赖实时接口,学生扫完码发现“网络错误,请重试”,体验极差。真实项目中,小程序必须具备“弱网兜底”能力:上传前预检网络、上传中显示分片进度、取件时先查本地缓存再联网校验。同时,扫码取件不是简单调wx.scanCode,而要结合wx.getSetting检查相机权限、wx.authorize动态申请,并对扫码结果做业务规则过滤(如只接受以PRINT_开头的字符串)。
3.1 扫码取件全流程:权限检查、结果解析与本地缓存联动
学生在打印机旁扫码,期望 1 秒内弹出“正在出纸”,而非加载转圈。为此,小程序需在扫码前完成权限预检,并将历史取件记录存入wx.setStorageSync:
// utils/scanUtils.js export function scanForPickup() { return new Promise((resolve, reject) => { // 1. 检查相机权限 wx.getSetting({ success: (res) => { if (!res.authSetting['scope.camera']) { wx.authorize({ scope: 'scope.camera', success: () => doScan(resolve, reject), fail: () => reject(new Error('未授权相机')) }); } else { doScan(resolve, reject); } } }); }); } function doScan(resolve, reject) { wx.scanCode({ onlyFromCamera: true, scanType: ['barCode', 'qrCode'], success: (res) => { const { result } = res; // 2. 业务校验:必须是 PRINT_ 开头的合法 task_id if (!/^PRINT_[a-zA-Z0-9]{20,}$/.test(result)) { wx.showToast({ title: '无效二维码', icon: 'none' }); return reject(new Error('非法二维码')); } const taskId = result.replace('PRINT_', ''); // 3. 先查本地缓存(上次成功取件记录) const cache = wx.getStorageSync(`pickup_${taskId}`) || {}; if (cache.status === 'SUCCESS' && Date.now() - cache.timestamp < 1000 * 60 * 60) { wx.showToast({ title: '已取件', icon: 'success' }); return resolve(cache); } // 4. 联网校验并更新缓存 wx.request({ url: 'https://api.yourschool.edu/api/v1/pickup/verify', method: 'POST', data: { taskId }, success: (r) => { if (r.data.code === 200) { const pickupData = r.data.data; wx.setStorageSync(`pickup_${taskId}`, { ...pickupData, timestamp: Date.now() }); resolve(pickupData); } else { reject(new Error(r.data.msg)); } } }); } }); }提示:
wx.setStorageSync存储上限为 10MB,但单条取件记录仅几 KB,存 1000 条无压力;Date.now() - cache.timestamp < 1h是防止缓存长期失效,1 小时后强制联网刷新。
3.2 离线排队页面:用wx.getStorageInfoSync预判缓存容量,动态降级 UI
当学生进入“我的排队”页面,若网络不可用,不能直接报错,而应展示本地已缓存的最近 5 条记录,并灰显“刷新”按钮。关键是要预判缓存是否充足:
// pages/queue/queue.js Page({ data: { queueList: [], isOffline: false, offlineTip: '' }, onLoad() { this.checkNetworkAndLoad(); }, checkNetworkAndLoad() { wx.getNetworkType({ success: (res) => { if (res.networkType === 'none') { this.setData({ isOffline: true, offlineTip: '当前无网络,仅显示本地缓存' }); this.loadFromCache(); } else { this.loadFromServer(); } } }); }, loadFromCache() { try { const info = wx.getStorageInfoSync(); // 获取当前缓存总大小与数量 console.log('缓存总量:', info.currentSize, 'KB, 条数:', info.keys.length); // 取 key 匹配 'task_' 的最近 5 条 const keys = info.keys.filter(k => k.startsWith('task_')).slice(-5); const list = keys.map(key => { const data = wx.getStorageSync(key); return { taskId: key.replace('task_', ''), fileName: data.fileName || '未知文件', status: data.status || 'WAITING', createTime: data.createTime || Date.now() }; }).sort((a, b) => b.createTime - a.createTime); this.setData({ queueList: list }); } catch (e) { this.setData({ queueList: [] }); } }, loadFromServer() { wx.request({ url: 'https://api.yourschool.edu/api/v1/task/my', header: { 'Authorization': wx.getStorageSync('token') }, success: (res) => { if (res.data.code === 200) { const list = res.data.data.map(item => ({ ...item, createTime: new Date(item.createdTime).toLocaleString() })); this.setData({ queueList: list }); // 同步写入缓存(仅成功时) list.slice(0, 5).forEach(item => { wx.setStorageSync(`task_${item.taskId}`, { fileName: item.fileName, status: item.status, createTime: item.createdTime }); }); } } }); } });注意:
wx.getStorageInfoSync()返回的是整个小程序存储信息,不是单个 key;slice(-5)取最后 5 个 key,因wx.setStorageSync写入顺序与keys数组顺序一致,可近似认为是最新记录。
3.3 修改刚进入的加载页面:用wx.showLoading+wx.hideLoading控制感知节奏
微信小程序默认启动页是白屏,用户易误以为卡死。应在app.js的onLaunch中尽早显示 loading,并在首页onLoad完成数据拉取后隐藏:
// app.js App({ onLaunch() { // 启动即显示全局 loading(避免白屏) wx.showLoading({ title: '加载中...', mask: true }); // 检查登录态 const token = wx.getStorageSync('token'); if (!token) { wx.redirectTo({ url: '/pages/login/login' }); return; } // 预加载打印机列表(首页需展示) wx.request({ url: 'https://api.yourschool.edu/api/v1/printer/list', header: { 'Authorization': token }, success: (res) => { if (res.data.code === 200) { getApp().globalData.printers = res.data.data; } }, complete: () => wx.hideLoading() // 无论成功失败,都关闭 loading }); } });提示:
wx.showLoading必须配对wx.hideLoading,否则 loading 会一直遮盖页面;mask: true是关键,否则用户仍可点击空白区域造成误操作。
4. MySQL 数据库设计:用复合索引+事务隔离+字段约束守住“下单→出纸”数据一致性
很多同学数据库只建了print_task一张表,字段status VARCHAR(20),结果测试时并发插入 100 条,查出来 3 条status是NULL,排查半天发现没加NOT NULL约束。校园打印系统核心是状态流转,必须用数据库原生能力兜底:status字段设ENUM('WAITING','PRINTING','SUCCESS','FAILED','CANCELED') NOT NULL DEFAULT 'WAITING';关键查询如“查某打印机当前排队数”必须走索引;跨表操作如“扣费+改状态+记日志”必须包在同一个事务里。
4.1 核心表结构与索引策略:让SELECT COUNT(*) WHERE printer_id=123 AND status='WAITING'毫秒返回
-- 打印任务主表 CREATE TABLE `print_task` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', `task_id` VARCHAR(64) NOT NULL COMMENT '全局唯一任务ID', `user_id` VARCHAR(64) NOT NULL COMMENT '用户ID(微信openid)', `printer_id` BIGINT NOT NULL COMMENT '打印机ID', `file_name` VARCHAR(255) NOT NULL COMMENT '原始文件名', `file_path` VARCHAR(512) NOT NULL COMMENT '服务器存储路径', `file_size` BIGINT NOT NULL COMMENT '文件大小(字节)', `page_count` INT NOT NULL DEFAULT 0 COMMENT '解析出的页数', `status` ENUM('WAITING','PRINTING','SUCCESS','FAILED','CANCELED') NOT NULL DEFAULT 'WAITING', `created_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_task_id` (`task_id`), KEY `idx_printer_status` (`printer_id`, `status`) COMMENT '查某打印机排队数', KEY `idx_user_created` (`user_id`, `created_time`) COMMENT '查某用户历史记录', KEY `idx_status_updated` (`status`, `updated_time`) COMMENT '查所有失败任务用于重试' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='打印任务表'; -- 打印机设备表 CREATE TABLE `printer_device` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `name` VARCHAR(100) NOT NULL COMMENT '打印机名称(如:教一楼101-HP-M402)', `location` VARCHAR(200) NOT NULL COMMENT '物理位置', `ip_address` VARCHAR(45) NOT NULL COMMENT 'IP或主机名', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1-在线,0-离线', `paper_level` TINYINT NOT NULL DEFAULT 100 COMMENT '纸张余量(0-100%)', `toner_level` TINYINT NOT NULL DEFAULT 100 COMMENT '碳粉余量(0-100%)', `last_heartbeat` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_location` (`location`), KEY `idx_status_heartbeat` (`status`, `last_heartbeat`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='打印机设备表';注意:
idx_printer_status是复合索引,查询WHERE printer_id=123 AND status='WAITING'时可直接走索引覆盖,无需回表;ENUM类型比VARCHAR节省空间且防止非法值写入,比CHECK约束性能更好。
4.2 关键事务场景:用@Transactional(isolation = Isolation.REPEATABLE_READ)保证资金与状态强一致
学生支付后,系统需同时完成三件事:1)在user_account表扣费;2)在print_task表将status改为PRINTING;3)在print_log表记操作日志。任一环节失败,全部回滚。SpringBoot 中必须显式声明事务传播行为与隔离级别:
@Service public class PaymentService { @Autowired private UserAccountMapper accountMapper; @Autowired private PrintTaskMapper taskMapper; @Autowired private PrintLogMapper logMapper; @Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class) public boolean confirmPayment(String taskId, BigDecimal amount) { // 1. 查询任务(for update 锁住该行) PrintTask task = taskMapper.selectByIdForUpdate(taskId); if (task == null || !"WAITING".equals(task.getStatus())) { throw new BusinessException("任务不存在或状态非法"); } // 2. 扣用户余额(需检查余额是否足够) int affected = accountMapper.deductBalance(task.getUserId(), amount); if (affected == 0) { throw new BusinessException("余额不足"); } // 3. 更新任务状态 task.setStatus("PRINTING"); task.setPayTime(new Date()); taskMapper.updateById(task); // 4. 记日志 PrintLog log = new PrintLog(); log.setTaskId(taskId); log.setOperator(task.getUserId()); log.setOperation("PAY_SUCCESS"); log.setCreateTime(new Date()); logMapper.insert(log); return true; } }提示:
selectByIdForUpdate对应的 XML 必须写SELECT ... FROM print_task WHERE task_id = #{taskId} FOR UPDATE;Isolation.REPEATABLE_READ是 MySQL 默认级别,可防止不可重复读,确保同一事务内多次查task状态一致;rollbackFor = Exception.class确保所有异常都触发回滚,不只是RuntimeException。
4.3 数据库课程设计必查项:用EXPLAIN验证慢查询是否走索引
答辩时老师常问:“你这个查询快吗?”。不能只说“很快”,而要拿出EXPLAIN结果。例如查“教一楼所有在线打印机”:
EXPLAIN SELECT * FROM printer_device WHERE location LIKE '教一楼%' AND status = 1;理想输出中type应为range或ref,key显示idx_location,rows小于 100。若type是ALL(全表扫描),说明idx_location没生效,需检查location字段是否用了函数(如UPPER(location))或模糊查询位置(LIKE '%教一楼'无法走索引)。
5. 演示视频制作要点:聚焦“可验证动作”,用 OBS 录屏+剪映粗剪,避开版权与隐私雷区
毕业设计演示视频不是功能秀,而是向答辩老师证明“你真做了、能跑通、有思考”。常见错误是录 10 分钟界面切换,却没一处打开开发者工具展示网络请求、没一行终端日志证明后端在运行。高分视频必须包含三个硬核片段:1)小程序扫码,屏幕右上角实时显示WebSocket connected;2)SpringBoot 控制台打印PrintJob executed, task_id=abc123, status=SUCCESS;3)MySQL 执行SELECT * FROM print_task WHERE task_id='abc123',结果中status确实为SUCCESS。全程禁用任何商业字体、背景音乐、未授权图标。
5.1 录屏准备:OBS 设置 1080p/30fps + 终端高亮 + 网络面板固定
用 OBS Studio 录屏时,务必开启“显示器捕获”而非“窗口捕获”,避免切屏时画面中断。关键窗口布局如下:
| 区域 | 内容 | 设置要点 |
|---|---|---|
| 主屏幕中央 | 微信开发者工具(小程序界面) | 分辨率设为 iPhone 12,打开Console和Network面板,Network中勾选WS(WebSocket)和XHR |
| 左侧小窗 | IntelliJ IDEA 终端(SpringBoot 控制台) | 字体放大至 16px,背景设为黑色,日志级别调为INFO,确保PrintJob executed日志清晰可见 |
| 右侧小窗 | Navicat 连接 MySQL 的查询窗口 | 执行SELECT * FROM print_task ORDER BY updated_time DESC LIMIT 5,保持结果集展开 |
提示:OBS 输出设置为
1920x1080,FPS设30(够流畅且文件不大),编码器选x264,Bitrate6000 Kbps;录制前在 IDEA 中Ctrl+F9重新编译,确保控制台日志是最新版。
5.2 剪辑逻辑:用“问题-动作-结果”三幕结构,每段不超过 45 秒
剪映中不加转场、不加字幕动画,只用最简白字幕标关键动作。示例时间轴:
- 0:00–0:25(问题):手机摄像头对准打印机旁贴纸上的二维码,字幕:“扫码取件——模拟学生操作”
- 0:26–0:45(动作):微信开发者工具 Network 面板中
ws://请求变绿,Console 打印WebSocket connected,字幕:“WebSocket 连接建立,实时通道就绪” - 0:46–1:10(结果):IDEA 终端滚动
PrintJob executed... status=SUCCESS,Navicat 中SELECT结果status列高亮,字幕:“后端任务完成,数据库状态已更新”
注意:所有敏感信息必须打码——小程序中的
appid、后端日志里的user_id(替换成user_***)、MySQL 中的file_path(只留/temp/abc123.pdf);演示用测试数据,如用户名为“张三(测试)”,打印机名为“教一楼101(测试)”。
5.3 视频交付:MP4 格式 + H.264 编码 + 100MB 内,附 README.md 说明技术栈版本
最终提交的demo.mp4必须用ffmpeg二次压缩,确保兼容性:
ffmpeg -i demo_raw.mp4 -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k -movflags +faststart demo.mp4同时提供同目录下的README.md,明确写出:
## 技术栈版本 - 后端:SpringBoot 2.7.18(JDK 11) - 前端:微信小程序基础库 2.28.2 - 数据库:MySQL 8.0.33(InnoDB 引擎) - 开发工具:IntelliJ IDEA 2022.3,Navicat 16.0,OBS Studio 28.1提示:
-crf 23是画质与体积平衡点,-movflags +faststart让 MP4 可边下边播,避免老师点击后等待 30 秒才出画面;README.md中版本号必须与你实际运行环境一致,答辩时老师可能现场java -version核对。
本文还有配套的精品资源,点击获取