
简介一份面向计算机科学与技术、人工智能等专业毕业设计及课程实践的校园垃圾分类管理系统源码包。系统针对校园场景设计了用户交互、分类指导、积分奖励、投放监测与数据分析等模块运用图像识别辅助垃圾自动分类并集成数据库与物联网思路可支撑完成从需求分析到系统实现的完整课设流程。压缩包内共1291个文件约26.99MB以Java类、JSP页面、HTML/CSS/JS前端资源为主另有大量图片、GIF动图、配置文件及jar依赖便于直接导入运行和二次开发。资源已经过运行验证已有54人学习适合作为毕业设计或课程作业的参考蓝本通过阅读源码与项目结构可快速掌握Web系统前后端实现、数据库交互及分类算法调用等关键技能为后续扩展或论文撰写提供扎实基础。1. 打开“校园垃圾分类管理系统”之前先想清楚三件事“毕业设计课设_校园垃圾分类管理系统.zip”这串文件名基本能猜到压缩包里装的是什么一个 Spring Boot 风格的后端工程、一份 SQL 初始化脚本、几个页面截图和可能还没写完的说明文档。很多同学拿到压缩包的第一反应是赶紧解压、点运行看到登录页出来就松一口气。我习惯先想清楚三件事再动手这套系统的技术栈是不是自己讲得明白数据库表的拆分能不能回答导师提问核心业务流程里有没有那种“能跑但经不起追问”的硬伤。毕竟毕设和课设的评分标准从来不是“能运行”而是“你能把运行背后的设计决策讲清楚”。这篇文章就按这个思路从一个 zip 包出发讲透技术选型、表结构、启动流程、核心模块和常见坑适合手头有压缩包、准备改造成自己作品的在校同学。2. 技术栈选择与系统骨架先把能撑住答辩的地基搭对压缩包解压后打开 pom.xml 扫一眼十有八九是 Spring Boot MySQL 的组合。这个组合在毕业设计里几乎成了默认答案。选择它不是因为“大家都用”而是在这个场景下它的收益最明确Spring Boot 内嵌了 Tomcat不需要单独装容器本地一个 main 方法就能起服务依赖用 starter 统一管理新手常见的 jar 包冲突少了一大半MySQL 免费、客户端工具多、网上资料密度高出了问题随便一搜就有答案。相比之下如果为了显得“高级”引入微服务、消息队列、容器编排反而会让课设重心从业务逻辑偏到运维上去答辩时一个“你为什么要用这个”就能问住。2.1 为什么课设场景里 Spring Boot MySQL 是默认答案我把这类系统的选型逻辑分成三层看。第一层是后端框架。Spring Boot 同一时代还有 SSMSpring Spring MVC MyBatis这种老组合但 SSM 需要手动配置一堆 XML做完配置还没来得及写业务就到截止时间了所以新做的课设基本都是 Spring Boot 起步。第二层是持久层框架MyBatis 和 MyBatis-Plus 是主流MyBatis-Plus 提供 selectById、selectList 这类现成方法单表 CRUD 可以不写 SQL省时间。第三层是前端常见的无非两种一种是 Bootstrap 或前端后台模板配服务端渲染页面直接由后端返回另一种是 Vue 脚手架做前后端分离后端只出 JSON 接口。选哪种前端取决于你对工作量的判断。我一般建议如果目标是“两个月内完成并顺利答辩”直接套后台管理模板最划算因为垃圾分类管理系统的页面复杂度不高无外乎登录、列表、表单、统计几个页面模板自带样式和布局改起来快。如果已经学了一个学期 Vue想体现前端工作量那就走前后端分离。这里有个容易被低估的点分离式开发意味着要处理跨域、Token 校验、接口文档维护会把工程量拉大 30% 左右时间不够时很容易翻车。后台管理的角色划分也要提前想清楚别做完整套系统才发现学生和管理员混在同一个菜单里。常见设计是两种角色学生端负责垃圾查询、投放登记、积分查看和兑换管理员端负责垃圾类别维护、投放记录审核、用户管理和数据统计。这个划分会直接影响后面的表结构和接口设计。2.2 六张核心表从用户到积分流水一次说清先给一份简化但完整的建表 SQL。实际工程里字段可能更多但这六张表已经把“分类查询 → 投放 → 积分 → 兑换”的主链路撑起来了。sql -- 用户表角色区分学生和管理员积分冗余在这里方便展示 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT 存储MD5或BCrypt密文, role TINYINT NOT NULL DEFAULT 0 COMMENT 0-学生 1-管理员, points INT NOT NULL DEFAULT 0 COMMENT 累计积分, phone VARCHAR(20), create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP );-- 垃圾类别表可回收、有害、厨余、其他四类固定 CREATE TABLE waste_category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL, description VARCHAR(255) );-- 垃圾物品表物品与类别多对一别名字段用于搜索容错 CREATE TABLE waste_item ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, category_id INT NOT NULL, alias VARCHAR(128) COMMENT 例如塑料瓶的别名可写PET瓶, base_points INT NOT NULL DEFAULT 1 COMMENT 每单位重量基础积分, CONSTRAINT fk_item_category FOREIGN KEY (category_id) REFERENCES waste_category(id) );-- 投放记录表一次投放产生一条行为流水 CREATE TABLE record_drop ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, item_id INT NOT NULL, weight DECIMAL(6,2) DEFAULT 0.00 COMMENT 投放重量单位kg, points_awarded INT DEFAULT 0 COMMENT 本次获得积分, drop_time DATETIME DEFAULT CURRENT_TIMESTAMP, remark VARCHAR(255) );-- 积分流水表账户余额变动的明细账答辩重点 CREATE TABLE points_log ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, change_val INT NOT NULL COMMENT 正数获得、负数支出, reason VARCHAR(64) COMMENT 投放奖励/兑换扣减/管理员调整, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );这段 SQL 里有两个设计点值得在答辩时主动讲。第一是 sys_user 表里冗余了 points 字段同时又有 points_log 流水表这两者看似重复其实一个是对账快照一个是明细账。每次获分或扣分时先写流水再更新冗余字段两个操作放在同一个事务里就能保证“余额和流水永远对得上”。这个设计体现了“冗余字段换查询性能”的思想比单纯堆功能更让导师觉得你理解了系统设计。第二是 record_drop 和 points_log 分开投放记录是行为事实积分流水是账户变动未来如果出现“投放了但没加分”的纠纷只要两张表对一下就能定位。有的项目会在这些基础表之上再加一张兑换记录表 exchange_record字段不外乎 user_id、item_name、points_cost、create_time。这张表其实可以复用 points_log 来推导但单独建表的好处是兑换状态可以扩展比如“已发货、已取消”会更好支撑后台管理功能。我的建议是时间允许就加上它对超市积分兑换类需求很自然也方便在答辩时多讲一个模块。相关表建立完之后索引策略不要忽略。record_drop 表最常用的查询是“查某个学生的投放历史”所以user_id drop_time应该建联合索引points_log 也有类似需求user_id create_time可以建普通索引。数据量小的时候有没有索引感觉不出来但答辩时被问到“数据量大了怎么办”能答出这一条就是加分项。需要注意的是外键设计。上面 SQL 里保留了一个物理外键是为了让字典表关系更直观实际项目里我见过很多团队只建索引、不建外键约束。原因是物理外键会让插入、删除的校验顺序变得很严格后期想在业务里做软删除、批量归档数据时外键往往会成为绊脚石。比较稳妥的做法是表结构上保留 category_id 这个字段逻辑关系在应用层校验数据库层只建普通索引。把“物理外键 vs 逻辑外键”的取舍讲清楚也是一处能扛追问的加分回答。3. 把 zip 变成能运行的系统导入、配置与启动全流程拿到压缩包以后我好几次看到同一种操作直接双击压缩包里的 jar 包或直接把整个文件夹拖进 IDE然后被一堆编译错误吓住。这里建议先花五分钟做三件事解压到不带空格和中文的目录看有没有 README 或 sql 目录确认工程的 pom.xml 在最外层还是嵌套了一层。常见的压缩包结构是school_garbage_system/下面直接是pom.xml、src/、sql/和README.md这种最省事如果外面套了好几层目录就用 IDE 直接打开最内层的 pom.xml而不是打开外层文件夹。3.1 从解压到登录页三个必改配置和一个执行次序按我常用的顺序走一遍能少踩一半的坑。先把压缩包解压用 IDE 打开 pom.xml等依赖下载完。这一步如果网络慢卡在下载阶段非常常见解决办法是给包管理工具配国内镜像具体镜像地址网上都有不在这里展开。依赖下载完成后先执行 sql 目录下的建库脚本再改配置文件最后启动。顺序最好不要反如果先启动项目再导数据MyBatis 启动时检查不到表会直接报表不存在的错误容易让人误以为配置有问题。配置文件在 Spring Boot 工程里一般是src/main/resources/application.yml也可能是 application.properties。改造的核心是数据源三段库名、用户名、密码。给出一个常见的最小配置yaml server: port: 8080spring: datasource: url: jdbc:mysql://localhost:3306/garbage_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Drivermybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这段配置里有几个参数决定了你能不能顺利跑起来。serverTimezoneAsia/Shanghai管的是数据库连接时区不加它会发现查询出来的时间比实际晚 8 小时allowPublicKeyRetrievaltrue专门解决 MySQL 8 驱动的一个认证问题不加时启动阶段可能直接报“Public Key Retrieval is not allowed”useSSLfalse是本地开发关掉 SSL 握手省去证书配置。map-underscore-to-camel-case用来把表里的user_id自动映射成 Java 属性userId如果这里设成 false很多现成的查询接口会因为字段映射不上而返回 null。log-impl这行是开发期福利启动后控制台里能看到每条 SQL 的完整语句和参数调试接口时比盲猜好太多。配置改完执行启动。IDE 里直接运行带有SpringBootApplication注解的主类即可也可以在命令行打包后启动命令如下bash在项目根目录执行跳过测试、打包、启动mvn clean package -DskipTests java -jar target/garbage-system.jar-DskipTests跳过了单元测试适合课设这种测试覆盖不全的工程避免打包时因为某个无关注解报错。target 目录下的 jar 包名字实际以你 pom.xml 里的 artifactId 和 version 为准不确定就执行mvn clean package后看 target 目录下生成了什么文件名。启动后看到Started Application in x.xxx seconds就说明服务起来了浏览器访问http://localhost:8080不出意外能跳到登录页。如果页面打不开先看控制台有没有端口被占用的提示。常见做法是改 application.yml 里server.port换成 8081 再试。排查顺序切记从下往上先看数据库连没连上再看端口通没通最后再看前端静态资源有没有被拦截器挡掉。3.2 登录链路验证接口、数据库与前端三层自检服务起来不等于系统可用。我习惯用一条接口请求把登录链路完整验证一遍。启动后用 curl 模拟学生登录bash curl -X POST http://localhost:8080/api/login-H Content-Type: application/json-d {username:student1,password:123456}正常响应会返回一个 JSON里面可能是 token也可能是用户基本信息这取决于项目用的认证方案。如果返回 404先看项目根路径是什么有的工程设置了server.servlet.context-path: /api那请求地址就要相应调整。如果返回 500直接去看控制台的 SQL 日志刚才配置的log-impl会打印出具体执行的语句和执行到哪一步报错十有八九是密码字段类型或表字段缺失问题。接口通了之后还要去数据库里看一眼数据有没有真正变化。连上库执行SELECT id, username, role, points FROM sys_user;确认初始化脚本里的账号真的在表里。接着用管理员账号登录后台新增一个垃圾类别、编辑一个垃圾物品再切到学生账号查一下这个新类别能不能在前端搜到。这套流程走完才能判断整个项目不是“只有登录页能开其余页面全是空壳”。很多压缩包里的工程只做了管理端页面学生端还在半成品状态提前发现好过答辩当天丢人。4. 核心业务模块拆解检索、投放与积分流水怎么不被问倒系统能跑起来以后就要把精力放到三个核心模块上。校园垃圾分类管理系统这类题目的业务闭环很清晰学生查垃圾属于哪一类 → 投放垃圾 → 系统按重量给积分 → 学生拿积分兑换小奖品。任何一个模块断掉系统都立不住。下面按代码实现逐个拆开讲。4.1 垃圾检索接口模糊查询之外还要考虑别名与排序策略先看控制器入口负责参数合规和返回包装java RestController RequestMapping(/api/waste) public class WasteItemController {private final WasteItemService wasteItemService; public WasteItemController(WasteItemService wasteItemService) { this.wasteItemService wasteItemService; } /** 按用户输入的关键词检索垃圾物品 */ GetMapping(/search) public Result search(RequestParam(keyword) String keyword) { if (keyword null || keyword.trim().isEmpty()) { return Result.error(关键词不能为空); } ListWasteItemVO items wasteItemService.search(keyword.trim()); return Result.ok(items); }}这里把keyword在 Controller 层做了一次 trim避免用户带空格搜索时查不到数据。WasteItemVO是给前端用的视图对象只包含物品 id、名称、分类名、基础积分这些字段不要直接把数据库实体扔给前端避免把不需要的字段暴露出去。很多课设项目把 Entity 直接返回虽然功能没坏但答辩时被问到“VO 和 Entity 为什么要分开”就会卡住。Service 里是核心检索逻辑java Service public class WasteItemServiceImpl implements WasteItemService {Autowired private WasteItemMapper wasteItemMapper; Override public ListWasteItemVO search(String keyword) { // 先精确匹配名称再按别名模糊匹配 LambdaQueryWrapperWasteItem wrapper new LambdaQueryWrapper(); wrapper.eq(WasteItem::getName, keyword) .or() .like(WasteItem::getAlias, keyword); // 可回收类排在前面同类目按名称排序控制返回条数 wrapper.orderByAsc(WasteItem::getCategoryId) .orderByAsc(WasteItem::getName) .last(limit 20); return wasteItemMapper.selectList(wrapper); }}这段代码的逻辑顺序是经过考虑的。先eq精确匹配名称再or对别名做模糊搜索这样输入“PET瓶”也能命中别名里写了“PET瓶”的“塑料瓶”。检索结果按 categoryId 升序排列相当于把可回收类通常 id 最小放在最前面最后.last(limit 20)是 MyBatis-Plus 提供的追加 SQL 方式防止一次查太多把前端列表撑爆。这里有两处要注意。.last()里拼接的内容不能来自前端用户输入否则就是把注入漏洞写进业务代码很多安全类提问就出在这里如果你用的是 MyBatis-Plus 的 Page 分页插件就不要用.last(limit 20)硬限制直接把 Page 对象传给 selectPage 更规范。实际工程里我还见过一种更稳的检索方案把物品名和别名都存成一行冗余的 search_text 字段检索时直接用LIKE CONCAT(%, #{keyword}, %)相当于只匹配一个字段逻辑更直观也更容易在数据库层加索引。4.2 投放记录与积分流水事务边界和重复提交是两个必考点投放模块是所有流程里最容易被追问的地方。学生提交一次投放需要同时完成三件事插入投放记录、写积分流水、累加用户总积分。这三件事必须放在同一个事务里否则可能在某个环节失败后出现“扔了垃圾没加分”的尴尬情况。核心代码参考java Service public class RecordDropServiceImpl implements RecordDropService {Autowired private WasteItemMapper wasteItemMapper; Autowired private RecordDropMapper recordDropMapper; Autowired private PointsLogMapper pointsLogMapper; Autowired private SysUserMapper sysUserMapper; Override Transactional(rollbackFor Exception.class) public void recordDrop(Long userId, Long itemId, BigDecimal weight) { // 1. 查物品拿基础积分 WasteItem item wasteItemMapper.selectById(itemId); Assert.notNull(item, 物品不存在); // 2. 计算本次积分重量向上取整避免0.01kg产生0分 int gained item.getBasePoints() * weight.setScale(0, RoundingMode.CEILING).intValue(); // 3. 插入投放记录 RecordDrop record new RecordDrop(); record.setUserId(userId); record.setItemId(itemId); record.setWeight(weight); record.setPointsAwarded(gained); recordDropMapper.insert(record); // 4. 写积分流水并累加到用户总额 PointsLog log new PointsLog(); log.setUserId(userId); log.setChangeVal(gained); log.setReason(投放奖励- item.getName()); pointsLogMapper.insert(log); sysUserMapper.increasePoints(userId, gained); }}Transactional(rollbackFor Exception.class)是这段代码的命门。默认情况下 Spring 只在运行时异常时回滚加了这句之后检查异常也会触发回滚保证第 3 步和第 4 步要么都成功要么都撤销。Assert.notNull(item, ...)属于前置校验防止传进来一个不存在的 itemId 时下一行直接空指针。积分计算这里用了setScale(0, RoundingMode.CEILING)意思是重量向上取整0.01kg 也会按 1 个单位发积分这对用户更友好也避开了浮点运算的精度问题。weight在数据库里用DECIMAL(6,2)而不是 float/double就是为了避免 0.1 0.2 这种经典的二进制精度问题。sysUserMapper.increasePoints 的实现也值得看一下推荐直接写成一条更新语句sql UPDATE sys_user SET points points #{gained} WHERE id #{userId};这个写法的精髓是“读改写在数据库一行内完成”不需要先 select 出来再加回去天然避免并发下两个请求同时读到旧值、互相覆盖的丢积分问题。很多新手会把这段逻辑拆成先查用户再 set 再 update单机并发测试时不容易暴露答辩演示时一旦被问到“两个人同时投放积分会不会少加”就只能沉默。另外一个高频答辩题是“怎么防止同一条投放被提交两次”。常见做法有两层前端提交后立刻禁用按钮后端在 record_drop 表上建(user_id, drop_time)唯一约束或者让前端生成一个业务单号重复请求时按单号直接拦截。只做前端禁用不够最好能把后端这层也讲出来。5. 部署运行避坑清单五个高频故障的现象、原因与修复这部分内容来自我在多个课设项目上排查过的共性问题。每条都按“现象、原因、解决”展开按这个顺序排查能省很多时间。5.1 数据库连接报“Public Key Retrieval is not allowed”现象项目启动时报SQLNonTransientConnectionException提示 Public Key Retrieval is not allowed直接连不上 MySQL。原因MySQL 8 默认认证插件是 caching_sha2_passwordJDBC 驱动在非 SSL 连接下需要主动获取公钥但 URL 里没允许这个行为。解决在数据源 URL 末尾追加两个参数yaml url: jdbc:mysql://localhost:3306/garbage_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse这是出现频率最高的启动错误几乎每个用 MySQL 8 的课设项目都会遇到一次。如果追加参数后还报错就检查数据库账号密码是否真的正确不要默认 root 密码是空。5.2 时间字段少了 8 小时或接口返回一串数字现象数据库里时间正常前端页面上要么显示乱码、要么显示“2024-01-01T00:00:00”这种带 T 的格式。原因两层问题叠加——数据库连接时区没指定Jackson 序列化 LocalDateTime 时也没有统一格式。解决除数据源 URL 里的 serverTimezone 外再在 application.yml 补一段全局格式化yaml spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这段配置管的是后端返回给前端的 JSON 时间格式。如果项目里用的是 fastjson配置方式不一样需要换成JSONField(format yyyy-MM-dd HH:mm:ss)注解。判断方法很简单看控制器返回的数据是不是被 Spring Boot 默认的 Jackson 处理是的话就用上面的配置。5.3 上传图片后换台电脑就 404现象本机上传头像或垃圾分类凭证图片一切正常换个目录或者部署到服务器后之前上传的图片全打不开。原因代码里写死了本地绝对路径比如File(D:/upload/)路径在另一台机器上不存在。解决把上传目录从系统配置里读取并使用相对路径映射成静态资源java Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadDir System.getProperty(user.dir) /uploads/; registry.addResourceHandler(/uploads/**) .addResourceLocations(file: uploadDir); } }System.getProperty(user.dir)拿到的是启动服务的当前目录也就是说图片会保存在项目运行目录下的 uploads 文件夹里再通过/uploads/**这个 URL 前缀对外访问。部署到服务器后只要启动目录有写权限就能正常读写。还有一个容易被忽略的点生产环境部署时启动脚本所在的目录和打包目录可能不一样最好运行时用-Dupload.dir/data/uploads这类 JVM 参数显式指定而不是靠当前目录猜。5.4 MyBatis-Plus 分页查出来还是全量数据现象列表接口传了 pageNum 和 pageSize但接口返回的是所有记录total 值也不对。原因MyBatis-Plus 的分页能力不是内置的需要注册分页拦截器很多人只 new 了一个 Page 对象就调 selectList这样 Page 根本不会参与 SQL 组装。解决建一个配置类把分页插件注册进去java Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }注册之后再调用wasteItemMapper.selectPage(page, wrapper)才会在 SQL 末尾自动拼LIMIT ? OFFSET ?。排查这个问题的技巧是打开 SQL 日志看打印出来的语句里有没有 LIMIT 关键字没有就是插件没生效或没走对方法。5.5 JDK 17 打开老项目编译报错现象本机装的是 JDK 17打开 Spring Boot 老毕设工程后编译失败报错指向--add-opens或字节码版本不支持。原因老项目通常按 JDK 8 编译部分底层依赖在 JDK 17 下行为不兼容。解决最顺的方法是用 IDE 的 Project Structure 把 SDK 切回 JDK 8如果机器上确实没有 JDK 8才考虑把工程的 java.version 和 Spring Boot 依赖版本往上升。Spring Boot 2.6 之后对 JDK 17 的兼容才逐步完善但这会牵动很多依赖更新不是一个课设周期里适合做的事。这类问题属于环境坑最容易让人误判成代码问题所以遇到奇怪的编译错误先检查工具链版本再查业务代码。下面把这五条汇总成一个快速定位表实际排查时可以对照使用故障现象最快定位方式修复要点连接数据库报 Public Key 错误看启动日志第一段异常URL 加 allowPublicKeyRetrievaltrue时间差 8 小时查询数据库时间再对比接口返回统一 serverTimezone 和 Jackson time-zone上传图片 404看图片 URL 和磁盘路径去掉硬编码绝对路径映射静态资源分页返回全量看 SQL 日志有无 LIMIT注册分页拦截器JDK 17 编译失败看报错里的 class version切 JDK 8 优先6. 答辩前加分的三个小改造统计图表、回收预约与图片凭证功能都通了以后接下来要做的不是加更多页面而是挑三个低成本、高答辩收益的点完善。6.1 用分类占比图讲清“数据从哪里来”第一个推荐做统计图表。管理员后台加一个页面展示四分类投放次数占比。后端一个 SQL 就够了sql SELECT c.name AS categoryName, COUNT(*) AS cnt FROM record_drop r JOIN waste_item i ON r.item_id i.id JOIN waste_category c ON i.category_id c.id GROUP BY c.id;前端用 ECharts 的饼图组件接收这个接口返回的 List就能画出占比图。答辩时这张图直接支撑“垃圾投放数据可视化”这个功能描述比放一堆表格有说服力。6.2 回收预约一张表和两个状态第二个改造是加回收预约。学生端选好可回收物品、预约时间管理员端看到预约列表处理时把状态从 0 改成 1。表结构只需要 user_id、appointment_time、status 三个核心字段。过期未处理的预约可以用一个定时任务每小时扫一次把超过预约时间 2 小时的状态改成 2已取消。这个模块开发量不大但能体现你对“线下流程 线上状态管理”的理解属于老师爱问的完整闭环。6.3 投放凭证上传复用静态资源映射第三个改造是给投放记录加图片凭证。在 record_drop 表加一个 evidence_url 字段上传接口复用第 5 章提到的 uploads 目录映射前端在投放表单里增加一个图片上传控件把返回的 URL 随表单一起提交。演示时现场上传一张照片再去数据库里查到路径画面比纯文字记录完整得多。我最早带一个课设小组的时候他们为了省事把投放记录做成先删除旧记录再插入新记录答辩现场被问到“历史数据去哪了”直接卡住后来改成只更新记录状态并保留流水整个系统的设计逻辑才真正说得通。这三个改造都不复杂但恰好能把“数据链路完整、状态可追溯、业务有闭环”这三点讲出来比多两个漂亮页面更能扛住问答。希望帮到你。本文还有配套的精品资源点击获取