竞赛项目复盘:智慧农业监测平台从第三到更优的关键改进
发布时间:2026/8/30 1:29:22 作者:尧图编辑部 阅读量:1,286

拿到“全省第三”这个名次很多团队会觉得已经不错了但真正参与过竞赛项目的人往往会有一种说不出的遗憾。名次越靠前越容易看清自己和“第一”之间的差距而这些差距通常不是运气而是技术方案、工程规范、性能优化和答辩演示里的具体问题。这篇文章不是鸡汤复盘而是一份竞赛项目技术复盘笔记。我会结合一个常见的省级竞赛项目——智慧农业环境监测平台从系统设计、核心代码、性能优化、答辩演示到常见问题完整拆解“全省第三的遗憾”到底遗憾在哪里以及下一次如何让遗憾不再重复。1. 为什么全省第三还会遗憾1.1 名次背后的三个真相先说一个很多参赛团队都遇到过的情况比赛结束时你以为的排名和实际排名有差距而且差距往往不在功能完整度上。第一个真相是评委看到的不是你的全部代码而是现场演示效果、答辩逻辑和文档质量。很多团队功能做得很全但演示时紧张、系统卡顿、数据是假的甚至现场连不上数据库最后分数被拉低。第二个真相是竞赛项目的评分权重往往是“能跑通”只占一部分技术难度、创新点、性能设计、工程规范各占一部分。全省第三看似不错但如果第三名和第二名之间的差距只有零点几分那丢分点一定藏在细节里。第三个真相是很多遗憾不是能力问题而是没有提前做“对抗性检查”也就是站在评委的角度去挑自己项目的毛病。1.2 竞赛项目常见的“遗憾清单”复盘过多个竞赛项目后我发现“全省第三的遗憾”几乎都集中在下面几类功能演示顺畅度不够现场临时报错或白屏。技术栈偏老或偏简单评委追问“为什么不用 XX”时答不上来。数据库查询慢演示大屏加载数据要好几秒。报警或推送功能在演示时重复触发显得设计不严谨。文档缺少架构图、接口说明和部署手册评委无法快速理解项目。没有做权限控制答辩被问到“如果部署到生产环境怎么办”时暴露短板。演示数据写死在前端没有模拟真实传感器数据缺乏说服力。这些问题单看都不致命但叠加起来就会形成“技术完成度不错但工程成熟度不够”的整体印象。而这正是很多全省第三名到最后没能再进一步的直接原因。1.3 复盘的目的把模糊的遗憾变成可修复的问题复盘不是写一份“痛定思痛”的感想而是把遗憾拆成可执行的技术改进项。比如“演示时页面加载慢”不是遗憾真正的技术问题是“MySQL 没有加联合索引导致时间范围查询走了全表扫描”。再比如“报警重复触发”不是玄学而是“高并发插入用户请求时没有做幂等控制”。只有把模糊的遗憾翻译成具体的问题才能在下一个项目中避免。下面我用智慧农业环境监测平台这个竞赛项目作为案例完整复盘一遍从数据采集、阈值报警到可视化大屏的实现与优化过程。这个项目在省级比赛里拿过第三名但也正因如此复盘才更有价值。2. 项目背景与技术架构用一个省级竞赛项目做复盘2.1 项目定位智慧农业环境监测平台智慧农业环境监测平台是竞赛项目中非常经典的选题。它贴近实际生产场景又具备足够的技术扩展空间。系统需要实时采集大棚里的温度、湿度、土壤墒情等环境数据当数据超过阈值时产生报警同时通过可视化大屏直观展示历史趋势和当前状态。这类项目很受竞赛评委欢迎因为它既包含物联网数据接入又包含后端业务处理、数据存储、前端可视化、报警推送等完整链路。更重要的是它天然适合考察性能优化和工程化能力。比如传感器每小时上报一条数据一个月就有几十万条记录查询性能、数据清洗、报警去重都是有价值的技术问题。2.2 功能范围与模块拆解竞赛项目不能太贪心功能要少而精。智慧农业环境监测平台的核心功能可以拆成四个模块设备与传感器管理维护设备信息、传感器类型和数据上报状态。环境数据采集与存储接收传感器上报的数据落库并支持时间范围查询。报警规则配置与执行管理员设置温度、湿度、土壤墒情的上下限系统实时判定并产生报警记录。可视化大屏展示实时数据、历史趋势、报警统计和设备状态。如果时间充裕可以增加消息推送、大屏自动刷新、数据导出等功能。但如果连核心模块都没打磨好不建议在比赛前临时加功能。很多团队就是因为“什么都想做”而死在最后一公里。2.3 技术选型和架构说明以这个竞赛项目为例我采用的技术方案是当前比较主流的组合后端Spring Boot 2.7 MyBatis Plus 3.5数据库MySQL 8.0缓存Redis 6.x前端Vue 3 ECharts 5部署Docker 或单机 jar 包部署选型理由是Spring Boot 生态成熟、资料多、搭建快适合竞赛周期MyBatis Plus 简化了单表 CRUD节省大量开发时间Redis 用于报警去重和热点数据缓存也方便在答辩时说明系统考虑了并发和性能问题Vue ECharts 开发大屏非常高效图表效果也专业。这里要说明一点版本号需要根据你的项目实际情况调整。不同版本的 Spring Boot 和 MyBatis Plus 在 API 上会有差异本文以常见环境为例重点演示配置和设计思路。2.4 环境准备与版本说明在开始写代码之前建议按以下环境准备开发机操作系统Windows 10/11 或 macOS 均可生产部署建议使用 Linux。JDK1.8 或 11Spring Boot 2.7 推荐 JDK 8/11。Maven3.6 以上。MySQL5.7 或 8.0。Redis5.x 以上版本。Node.js14 以上用于前端项目构建。IDEIntelliJ IDEA 社区版或旗舰版均可。如果本地没有 Redis可以使用 Docker 快速启动docker run -d --name redis -p 6379:6379 redis:6.2同样MySQL 也可以用 Docker 启动但要注意容器数据卷的挂载避免容器重启后数据丢失。这些基础环境中最容易被忽略的是 MySQL 字符集和时区配置。建议在连接串中显式指定useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai否则会出现中文乱码和日期时间偏差。3. 核心代码拆解从数据采集到报警推送3.1 数据库表设计竞赛项目的数据表不需要太多但设计一定要规范。智慧农业环境监测平台需要三张核心表传感器数据表、报警规则表、报警记录表。-- 文件路径doc/schema.sql CREATE TABLE sensor_data ( id BIGINT AUTO_INCREMENT PRIMARY KEY, device_id VARCHAR(32) NOT NULL, temperature DECIMAL(5,2), humidity DECIMAL(5,2), soil_moisture DECIMAL(5,2), collect_time DATETIME NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_device_time (device_id, collect_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE alarm_rule ( id BIGINT AUTO_INCREMENT PRIMARY KEY, device_id VARCHAR(32), metric VARCHAR(32) NOT NULL, min_value DECIMAL(10,2), max_value DECIMAL(10,2), enabled TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE alarm_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, device_id VARCHAR(32) NOT NULL, metric VARCHAR(32) NOT NULL, actual_value DECIMAL(10,2), alarm_time DATETIME NOT NULL, handle_status TINYINT DEFAULT 0, KEY idx_device_time (device_id, alarm_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里重点说两个设计细节。第一sensor_data表一定要加联合索引(device_id, collect_time)。因为最频繁的查询是“查某个设备某段时间的数据”没有这个索引数据量上来后查询会非常慢后面我会用实际案例说明。第二alarm_rule表的metric字段用于标识规则针对的是哪个指标比如temperature、humidity、soilMoisture。这里不要使用中文作为字段值因为在代码中做比较和扩展都不方便。min_value和max_value允许为空表示只有一个边界比如“温度不能超过 40℃”就只配置max_value。3.2 Spring Boot 项目配置创建一个新的 Spring Boot 项目核心依赖包括spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-j、spring-boot-starter-data-redis和lombok。在 Maven 的pom.xml中引入这些依赖后重点配置application.yml。# 文件路径src/main/resources/application.yml server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/agri_monitor?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 database: 0 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0配置项不多但有几个注意点。password在 YAML 中如果以数字开头需要用引号包裹否则会被解析成数字类型。jackson.date-format和time-zone决定了接口返回日期时间时的格式避免前端拿到一串时间戳。mybatis-plus.configuration.log-impl在开发阶段可以打印 SQL但比赛演示和生产环境建议关闭否则控制台大量 SQL 日志反而影响现场性能展示。3.3 Mapper 层与核心查询使用 MyBatis Plus 时单表查询可以直接继承BaseMapper但一些复杂查询需要在自定义方法中编写 SQL。这里的关键场景是查询某设备最近一条数据用于报警判定。// 文件路径src/main/java/com/example/agrimonitor/mapper/SensorDataMapper.java public interface SensorDataMapper extends BaseMapperSensorData { Select(SELECT * FROM sensor_data WHERE device_id #{deviceId} ORDER BY collect_time DESC LIMIT 1) SensorData selectLatestByDeviceId(Param(deviceId) String deviceId); }// 文件路径src/main/java/com/example/agrimonitor/mapper/AlarmRuleMapper.java public interface AlarmRuleMapper extends BaseMapperAlarmRule { Select(SELECT * FROM alarm_rule WHERE (device_id #{deviceId} OR device_id IS NULL) AND enabled 1) ListAlarmRule selectEnabledByDeviceId(Param(deviceId) String deviceId); }第二个 SQL 里的device_id IS NULL表示全局规则也就是不绑定具体设备的默认规则。这种设计在业务上很常见赛事评委也会认可。注意Select注解方式适合简单查询如果 SQL 越来越复杂建议把 SQL 写到 XML 文件中更容易维护和格式化。3.4 报警规则判定与去重推送报警判定是项目的核心业务逻辑。它的流程是从传感器数据表中查出该设备最新一条数据然后遍历启用中的报警规则判断数据是否超过边界。如果超过边界则写入报警记录并通过 Redis 做 10 分钟去重。// 文件路径src/main/java/com/example/agrimonitor/service/impl/AlarmServiceImpl.java Service RequiredArgsConstructor public class AlarmServiceImpl implements AlarmService { private final AlarmRuleMapper alarmRuleMapper; private final SensorDataMapper sensorDataMapper; private final AlarmRecordMapper alarmRecordMapper; private final StringRedisTemplate stringRedisTemplate; Override public void checkThreshold(String deviceId) { // 1. 查询该设备最近一条采集数据 SensorData latest sensorDataMapper.selectLatestByDeviceId(deviceId); if (latest null) { return; } // 2. 查询启用中的报警规则 ListAlarmRule rules alarmRuleMapper.selectEnabledByDeviceId(deviceId); for (AlarmRule rule : rules) { double actual getMetricValue(latest, rule.getMetric()); boolean hit (rule.getMinValue() ! null actual rule.getMinValue().doubleValue()) || (rule.getMaxValue() ! null actual rule.getMaxValue().doubleValue()); if (hit) { handleAlarm(deviceId, rule, actual); } } } private double getMetricValue(SensorData data, String metric) { switch (metric) { case temperature: return data.getTemperature().doubleValue(); case humidity: return data.getHumidity().doubleValue(); case soilMoisture: return data.getSoilMoisture().doubleValue(); default: throw new IllegalArgumentException(unsupported metric: metric); } } private void handleAlarm(String deviceId, AlarmRule rule, double actual) { String key alarm: deviceId : rule.getMetric(); Boolean first stringRedisTemplate.opsForValue().setIfAbsent(key, 1, Duration.ofMinutes(10)); if (!Boolean.TRUE.equals(first)) { return; } AlarmRecord record new AlarmRecord(); record.setDeviceId(deviceId); record.setMetric(rule.getMetric()); record.setActualValue(BigDecimal.valueOf(actual)); record.setAlarmTime(LocalDateTime.now()); alarmRecordMapper.insert(record); // 可扩展调用钉钉/企业微信机器人推送告警消息 } }这里最值得在答辩时讲清楚的是 Redis 去重机制。如果没有去重假设传感器每隔几秒上报一次数据系统会在几分钟内产生大量重复报警既浪费存储也会让用户对报警失去信任。setIfAbsent保证了同一设备同一指标在 10 分钟内最多产生一条报警记录。这个概念叫幂等评委一般会追问所以要把这一小块逻辑理解透。3.5 前端可视化大屏可视化大屏是竞赛项目中视觉效果最好的部分也是评委最直观的感受来源。前端这里以 Vue ECharts 为例展示近 24 小时温度趋势。首先在接口层封装一个查询方法然后通过 ECharts 渲染折线图。// 文件路径src/views/Dashboard.vue 中的核心片段 template div classdashboard div idtemperatureChart stylewidth: 100%; height: 400px/div /div /template script setup import * as echarts from echarts import { onMounted, onBeforeUnmount } from vue import { getTrendData } from /api/dashboard let chart null function renderChart() { getTrendData({ deviceId: device_001 }).then(res { const timeList res.data.map(item item.collectTime) const temperatureList res.data.map(item item.temperature) chart echarts.init(document.getElementById(temperatureChart)) chart.setOption({ title: { text: 近24小时温度趋势 }, tooltip: { trigger: axis }, xAxis: { type: category, data: timeList }, yAxis: { type: value, name: 温度(°C) }, series: [{ name: 温度, type: line, smooth: true, data: temperatureList }] }) }) } onMounted(() { renderChart() window.addEventListener(resize, () chart chart.resize()) }) onBeforeUnmount(() { window.removeEventListener(resize, () chart chart.resize()) chart chart.dispose() }) /script这段代码的核心点是onBeforeUnmount里要移除事件监听并释放图表实例否则页面切换时会出现内存泄漏。这个细节在开发中容易被忽略但在竞赛答辩时如果被问到“前端如何避免性能问题”能答出来就是加分项。4. 遗憾点一性能问题没有提前暴露4.1 慢查询忘了联合索引复盘这个项目时最典型的性能问题出在“查询近 24 小时温度趋势”这个接口。最初表里只有主键索引当我模拟 10 万台设备、每台每天上报 24 条数据时数据量很快达到百万级别。此时按设备和时间范围查询会非常慢一个接口可能需要 3 到 5 秒大屏看起来就像卡死了一样。解决办法就是给表加联合索引ALTER TABLE sensor_data ADD INDEX idx_device_time (device_id, collect_time);加了索引之后同样的查询时间从 3 秒降到几十毫秒。这个优化效果在答辩现场演示时非常直观评委即使不关心 SQL 执行计划也能看到页面加载速度的差别。这里也提醒大家竞赛演示一定要准备足够的数据量否则根本暴露不了问题评委也看不出你的优化能力。4.2 接口响应慢缺少缓存另一个容易被忽视的问题是大屏上的实时数据和统计指标会被频繁查询。比如设备状态统计、今日报警数量这些数据可能每次刷新页面都要查数据库。如果数据量不大问题不明显但当大屏同时展示五六个图表时每个图表都查一次数据库接口响应就会变得不稳定。一个比较稳妥的方案是使用 Redis 缓存热点查询结果。例如报警统计这种“不要求秒级精确”的数据可以缓存 30 秒// 文件路径src/main/java/com/example/agrimonitor/service/impl/DashboardServiceImpl.java Autowired private StringRedisTemplate stringRedisTemplate; Override public Long countTodayAlarm() { String key dashboard:alarmCount:today; String cached stringRedisTemplate.opsForValue().get(key); if (cached ! null) { return Long.valueOf(cached); } Long count alarmRecordMapper.selectCountToday(); stringRedisTemplate.opsForValue().set(key, String.valueOf(count), Duration.ofSeconds(30)); return count; }引入缓存后数据库压力明显下降接口响应也更稳定。在答辩时可以说“用 Redis 缓存了热点数据设置了合理的过期时间”这比“我用了 Redis”要有说服力。4.3 报警重复并发下的幂等设计在没有 Redis 去重时报警模块的并发缺陷非常隐蔽。假设系统使用定时任务每隔 10 秒扫描一次传感器数据任务并发执行时同一个设备可能会被两个线程同时判定为“超阈值”从而插入两条几乎一样的报警记录。用setIfAbsent结合设置过期时间后只有第一次判定能拿到 Redis 锁后续重复判定会被直接忽略。这个设计还能扩展成更通用的分布式锁思路。如果竞赛项目的业务是“库存扣减”“优惠券领取”也可以用同样的方式保证并发安全。理解这个场景后在答辩中回答“你的系统如何应对并发”就不再是背概念而是有理有据地描述自己的实现。4.4 演示数据没有模拟足够多的历史数据很多团队在项目开发时用几条手工测试数据认为功能没问题就行了。但竞赛评委往往会在演示时随机选择一个设备、随机选择一个时间段查看数据。如果历史数据只有寥寥几条图表空荡荡的给人的第一印象就是“系统没有经过真实环境验证”。建议写一个数据生成脚本批量模拟一万台设备一个月的传感器数据。这样既能展示大屏效果也能提前发现性能瓶颈。数据量充足的情况下再去讨论索引、缓存和分页才有实际意义。5. 遗憾点二演示与答辩环节丢分5.1 演示脚本和备用方案竞赛项目技术再好演示环节出了问题分数都会受影响。最常见的场景是现场网络突然断开前端页面调用不了后端接口结果页面白屏。很多团队没有处理失败请求的兜底方案此时只能尴尬地对着评委说“刚才还好好的”。建议在演示前准备一套完整的备用方案前端所有接口请求设置超时时间超时后给出友好提示。准备一份本地的演示数据快照必要时可以用mock数据兜底。提前录制一份 3 到 5 分钟的演示视频万一现场实在跑不起来可以播视频。确保演示设备电源、网络、投屏线都提前检查一遍。演示脚本也要提前演练。每个功能点讲多久、先点哪里后点哪里都要固定下来。不要现场临场发挥因为临场发挥很容易忽略关键功能或者讲得过于啰嗦导致评委失去耐心。5.2 答辩常见追问答辩环节是拉开分差的关键。评委围绕项目提出的问题通常有几类技术选型类为什么选 MySQL 而不是 PostgreSQL为什么用 Redis性能类数据量达到千万级怎么办接口如何优化安全类系统如何防止 SQL 注入是否有权限控制业务类如果传感器数据丢失怎么办报警阈值如何动态调整这些问题都需要在赛前准备。我的建议是不要背答案而是从自己的代码出发去讲。比如回答“为什么用 Redis 去重报警”时可以直接拿出handleAlarm方法说明自己的实现逻辑和过期时间设置。评委更愿意看到“你真正做过并理解”的内容而不是背诵的面试八股文。5.3 文档和源码规范竞赛评分通常包括项目文档。很多团队代码写得不错但文档只有一篇简单的 README缺少架构图、接口文档、数据库设计和部署说明。评委在短时间内无法理解项目全貌印象分会受影响。建议文档至少包含以下内容项目背景与功能清单。系统架构图可以用 Visio、ProcessOn 或 draw.io 绘制。数据库表设计说明。核心接口文档包含请求参数和返回示例。部署步骤和运行环境要求。项目亮点和后续优化方向。源码规范同样重要。包名统一、类名清晰、Controller 只做参数接收和返回、Service 写业务逻辑、Mapper 只负责数据访问这些基本分层规范能极大提升代码的可读性。如果一个项目的 Controller 里堆了几百行业务代码即使功能正常也很难给评委留下好印象。6. 常见问题与排查清单竞赛项目开发过程中下面这些问题出现的频率很高整理成一份排查清单方便对照检查。问题现象可能原因解决思路接口响应慢数据库没有索引、循环查询加联合索引、使用批量查询大屏加载白屏前端接口地址配置错误用环境变量区分开发/生产环境报警记录重复没有做幂等控制使用 RedissetIfAbsent设置过期键演示时数据为空数据库没有准备演示数据编写数据生成脚本批量造数中文乱码连接串缺字符集参数添加characterEncodingutf8部署后时间差 8 小时MySQL 时区未配置连接串加serverTimezoneAsia/Shanghai答辩被问如何抗并发缺少缓存/限流方案引入 Redis 缓存和接口限流策略打包后找不到配置文件配置文件未包含在 jar 中检查resources目录和 Maven 打包配置排查时建议先复现问题再根据日志定位。比如接口慢先看一下 MySQL 慢查询日志确定是 SQL 问题还是网络问题不要盲目加缓存。7. 竞赛项目最佳实践从“第三”往前走的改进路径7.1 需求要做减法竞赛项目最容易翻车的点就是需求过载。很多团队比赛前一周还在加功能结果核心模块反而不稳定。建议在项目开始时用表格列出所有想做的功能然后按“核心功能、加分功能、备选功能”分类。核心功能必须稳定跑通加分功能在时间充足时再实现备选功能干脆放弃。智慧农业监测平台里数据采集、报警、大屏展示是核心功能消息推送、数据导出、多租户是加分功能而这些加分功能只有在核心功能稳定后才能考虑。7.2 性能优化要有优先级性能优化也要分优先级不要一上来就搞分布式、消息队列。竞赛项目的数据量和并发量有限直接用 Redis 解决缓存和去重问题用索引解决慢查询用分页解决大数据量查询已经能覆盖 90% 的性能需求。盲目引入复杂中间件只会增加部署和演示的不确定性。优化的顺序建议是先造数据再压测再找瓶颈最后针对瓶颈优化。没有经过压测的性能优化都是“自我感动”。7.3 工程规范比炫技重要竞赛项目是团队协作的产物工程规范直接影响交付质量。统一代码格式、统一接口返回结构、统一异常处理这些看起来不亮眼但能避免很多协作问题。接口返回结构建议统一为包含code、message、data三个字段的对象。这样前端处理逻辑一致后端也不容易出现“有的接口返回了数组、有的返回了对象”的情况。统一异常处理可以用RestControllerAdvice捕获异常并转换为统一结构避免把异常堆栈直接暴露给前端。7.4 时间安排与里程碑竞赛项目一般持续几周到几个月。建议把时间分成四个阶段第 1 周确定需求、技术选型、搭建项目骨架。第 2~3 周完成核心模块开发包括数据库设计、后端接口、前端页面。第 4 周完善性能优化、演示数据、文档和答辩 PPT。最后 3 天只做演示演练和风险排查不再新增功能。如果比赛最后一天还在写代码大概率是因为前期规划不够。省下来的时间应该用来做性能测试、文档整理和演示彩排这些才是决定名次的关键因素。8. 总结把遗憾变成下一次的起点全省第三的遗憾往往不是因为第三名不够好而是因为复盘后发现很多问题原本可以避免。数据库索引没加、报警没有去重、演示数据太少、答辩没有准备、文档不够规范这些问题每一个都不难解决难的是在开发过程中保持“交付视角”——始终想着评委和用户会怎么使用这个系统。回头看这个智慧农业监测平台项目我们获得的不是一块奖牌而是一套关于系统设计、性能优化、工程规范和答辩表达的完整经验。如果你也正在准备竞赛项目我建议你从第一天开始就把性能测试和演示脚本纳入计划不要等到比赛前一周才开始考虑这些问题。如果下次再参加比赛我会先问自己三个问题演示时会不会卡数据是不是真实可解释答辩时能不能经得起追问把这三个问题放在交付之前那么全省第三大概率就不是终点而是起点。