news 2026/10/21 22:26:32

Spring Boot实战:社区智能垃圾管理系统的设计与实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot实战:社区智能垃圾管理系统的设计与实现

1. 项目概述:先弄清楚这是个什么系统

有没有发现,很多“基于Spring Boot的XXX管理系统”最后都做成了给人添麻烦的后台表格?这个“社区智能垃圾管理系统”如果只停在“增删改查、导出报表”,那就把项目做窄了。我做这套系统的第一反应是:它表面是个 Spring Boot 后端项目,实际上是在管理“居民投放、设备上报、分类识别、督导巡检、清运回收、积分激励”这六件事组成的闭环。

我先说一个容易被标题误导的点:这里的“智能”,不一定是让你从零训练一个垃圾分类模型。真实落地时,“智能”主要体现在三个地方:

  • 垃圾投放点有摄像头和传感器,能自动识别投入的是哪类垃圾,检测桶是不是快满了;
  • 后端拿到设备上报数据后,能自动生成满溢预警和清运工单,而不是等居民投诉;
  • 管理者能从投递记录里算出分类正确率、时段高峰、常见误投类型,用数据反向优化宣传和巡检路线。

所以 Spring Boot 在这里的核心角色,不是做图像识别算法,而是做“可信的业务平台”:接收设备侧结果、保证数据不重复、分配积分、生成工单、提供统计接口。想明白这一点,再开始设计表和写接口,思路会顺很多。

1.1 别被“智能”两个字带偏

我做这套系统时最怕的事,是甲方张口就要“人脸识别垃圾桶”“语音提示”“自动称重”“满溢报警”,最后全堆在 PPT 上,结果后台连一条投递记录都查不清楚。真正应该先把稳的,是下面这条业务链路:

  1. 居民扫码或刷卡,打开对应分类的桶门;
  2. 居民投入垃圾,设备拍照并做本地识别;
  3. 设备把识别结果、重量、时间、桶状态一起上报给 Spring Boot 后端;
  4. 后端校验数据是否合法,生成投递流水,给居民加积分或推送错误提示;
  5. 垃圾桶满溢时,系统生成清运工单,清运员上门清运;
  6. 督导员处理识别置信度低或居民反复投错的记录;
  7. 管理后台按社区、按点位、按分类类型做统计。

这套链路里,1 和 2 是硬件和边缘推理的事,3 到 7 才是我们做 Spring Boot 要负责的。很多项目把精力浪费在人脸识别、语音提示这些花边上,忽略了最核心的“投递记录不丢、不重、不乱”这三个基本要求。

1.2 功能清单不是越多越好

实际我最终确定的功能模块大概是这样:

角色核心功能
居民扫码投放、投递结果查询、积分明细、错误纠正查看
督导员/巡检员待处理任务列表、现场拍照上传、标记整改结果
清运人员满溢点位列表、清运工单、完成打卡
社区管理员数据大屏、用户管理、设备管理、积分规则配置、黑白名单
平台维护方设备密钥管理、消息重推、系统日志

我砍掉了几个看着很酷的功能,比如“邻里排行版”“积分兑换商城”“AI 客服”。原因很简单:第一版要解决的是“垃圾有没有分对、桶有没有满、工单有没有处理”,而不是流量运营。排行榜和商城一旦积分模型没想清楚,日常运维会被刷分问题烦死。

1.3 适合谁参考

如果你正在做 Spring Boot 相关的毕设或小项目,这个标题是很好的练手对象,但它不等于简单 CRUD。它更适合那些已经能写登录注册、分页查询,但想再往前走一步的同学:

  • 想了解设备端上传类业务怎么做幂等;
  • 想了解定时任务如何避免重复执行;
  • 想了解积分账户这类资金敏感数据怎么设计;
  • 想了解一个项目从单体起步,如何给后面留扩展位。

下面我会把“为什么这样设计”讲得更细,而不是只给一堆表结构。

2. 总体架构与核心表设计

这个项目里,我选了非常“稳”的一套组合:Spring Boot 2.x + MySQL 8.0 + Redis + RabbitMQ,前端管理端用 Vue,居民端用微信小程序。这不算什么新潮架构,但足够支撑一个几千户居民的小区。

2.1 一次“扔垃圾”动作如何走完整套系统

我用一个最简单的“可回收物投递”来串一下数据流。

用户扫码后,小程序调用/api/user/openDoor,后端先判断这个用户是否在允许名单里,再向设备代理层下发开锁指令。这里的“设备代理层”不是直接控制硬件,而是转发给本地的设备网关。因为垃圾桶设备通常走内网或 NB-IoT,后端不能直接连硬件,所以中间有一层网关更安全。

设备门打开后,居民投放垃圾,顶部摄像头拍两张照片。边缘盒子先跑一次目标检测,判断出“这看起来是塑料瓶”还是“这看起来是剩饭剩菜”。然后设备通过 HTTP 上报给后端,上报内容类似:

{ "requestId": "DEV-20250601-000123", "deviceNo": "RJ-001-02", "bucketNo": 2, "bucketType": 1, "imageUrl": "http://storage.local/images/xxx.jpg", "recognitionType": "plastic_bottle", "confidence": 0.92, "weight": 0.85, "reportTime": "2025-06-01 08:32:10" }

注意这个requestId,它是设备端生成的唯一流水号。后端最优先做的事,不是加积分,而是检查这个requestId有没有处理过。如果设备离线时重试上报,同一个requestId会重复到达,数据库唯一索引能直接挡住,从而避免一条投递记录被重复创建。

后端确认这是一条新记录后,会做三件事:更新投递流水、更新桶状态、触发积分计算。接着,如果桶达到满溢阈值,系统还会发一个“满溢预警”消息;如果识别置信度很低,系统会生成一条“人工复核”任务给督导员。

这一步流程理顺之后,后面所有模块都变得很简单:积分、工单、统计,本质都是在“投递流水”这张表上做衍生操作。

2.2 为什么选 Spring Boot + MySQL + Redis

这套组合不是最先进的,但在这个场景下最合适。Spring Boot 的起步依赖、自动配置和事务支持,让我能把多数精力放在业务规则上;MySQL 用来存投递流水、用户、积分这种强一致数据;Redis 用于设备状态缓存、防刷计数、分布式锁和热点数据。

有人可能会问:设备上报那么多,不是应该用 TimescaleDB 或 ClickHouse 吗?一个小区的设备数量通常不超过几十台,每台每天上报几百次,日流水也就是万级。MySQL 完全扛得住,而且查询管理端报表也很方便。真正到十万级日流水时,再做历史分表也不迟。

RabbitMQ 在这个项目里不是必需,但我还是加了。因为设备上报是异步且零散的事件,后端处理完主流程后,还要发通知、写统计、触发工单。如果不做解耦,设备一多,接口响应时间就会被这些旁路逻辑拖慢。使用消息队列后,主链路只做“保存投递记录 + 更新桶状态”,其他消费内容异步处理。

2.3 核心数据库表与索引

我最终留下的核心表不算多,但每张表都踩过不少坑。设计时最重要的原则是:状态变化要留痕,金额/积分变化要可追溯。

第一张表是用户:

CREATE TABLE `t_user` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `open_id` VARCHAR(64) NOT NULL, `phone` VARCHAR(20), `real_name` VARCHAR(32), `community_id` BIGINT NOT NULL, `points_balance` INT NOT NULL DEFAULT 0, `status` TINYINT NOT NULL DEFAULT 0, `create_time` DATETIME NOT NULL, UNIQUE KEY `uk_open_id` (`open_id`) ) ENGINE=InnoDB;

第二张是设备表。设备表要区分“硬件设备”和“桶位”。一个设备可能管多个投放口,比如四个桶,所以我把桶位单独拆出来。

CREATE TABLE `t_device` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `device_no` VARCHAR(32) NOT NULL, `community_id` BIGINT NOT NULL, `location_name` VARCHAR(64), `longitude` DECIMAL(10, 7), `latitude` DECIMAL(10, 7), `secret_key` VARCHAR(64), `online_status` TINYINT DEFAULT 0, `last_online_time` DATETIME, UNIQUE KEY `uk_device_no` (`device_no`) ) ENGINE=InnoDB;

第三张是投递记录,这是全系统最核心的表。它保存每一次投放的设备侧数据和最终分类结果。

CREATE TABLE `t_delivery_record` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `request_id` VARCHAR(64) NOT NULL, `user_id` BIGINT NOT NULL, `device_no` VARCHAR(32) NOT NULL, `bucket_no` INT NOT NULL, `bucket_type` TINYINT NOT NULL COMMENT '1可回收 2厨余 3其他 4有害', `image_url` VARCHAR(255), `recognition_type` VARCHAR(32), `confidence` DECIMAL(5, 4), `final_type` TINYINT COMMENT '人工复核后的最终分类', `weight` DECIMAL(6, 2), `put_at` DATETIME NOT NULL, `status` TINYINT NOT NULL DEFAULT 0, `create_time` DATETIME NOT NULL, UNIQUE KEY `uk_request_id` (`request_id`), KEY `idx_user_put_at` (`user_id`, `put_at`) ) ENGINE=InnoDB;

后面还有积分流水表、工单表、巡检表,但核心逻辑都在用户、设备、投递记录这三张表上。索引设计上,我特别克制:

  • 投递记录上加request_id唯一索引,用在幂等判断;
  • 加(user_id, put_at)联合索引,用在“用户投递历史”查询;
  • 不在所有字段上加索引,因为上报表是写多读少,索引太多会拖慢写入速度。

2.4 角色与权限边界

这套系统里权限并不复杂,但最容易搞混的是“居民能看到什么”和“管理员能看到什么”。居民端不需要看到其他居民的名字,不需要知道清运人员是谁;管理员端要能看投诉记录、设备状态、积分流水。

我用 Spring Security + 自定义注解做了一套很轻量的鉴权,把用户分成居民、督导员、清运员、管理员四种角色。接口上的权限控制主要靠@PreAuthorize:

@PreAuthorize("hasAnyRole('ADMIN', 'INSPECTOR')") @GetMapping("/inspection/tasks") public Result<List<InspectionTaskVO>> listTasks() { // ... }

真正要注意的不是角色枚举,而是“数据范围”。比如督导员只能看到他所在社区的任务,不能看到别的社区。所以我所有查询都强制拼接community_id,而不是单纯靠前端传参过滤。这一步在一开始没做好,后面权限漏洞会很难补。

3. 智能识别与业务规则实现

标题里有“智能”,所以这一章专门讲设备识别结果、满溢检测、积分规则和工单生成。业务规则不写清楚,系统就是一堆没法解释的按钮。

3.1 设备上报与AI识别结果怎么接入

AI 识别不是我们后端训练出来的,而是边缘侧一个推理服务提供的。设备传上来的recognition_type和confidence就是它的结论。后端要做的,是接受这个结论,并判断要不要人工复核。

我定的规则是:

  • confidence >= 0.85:直接采用识别结果作为最终分类;
  • 0.60 <= confidence < 0.85:标记为“待复核”,推送给督导员;
  • confidence < 0.60:标记为“无法识别”,同时给居民推送一条“请重新分类投放”的提示。

设备上报接口的验签也特别重要。如果在公网上部署,随便构造一个 HTTP 请求就能刷积分,这是相当常见的事故。我用的方案是:设备端用secretKey对报文做 HMAC-SHA256 签名,后端在拦截器里校验时间戳和签名。放一个精简版代码:

public boolean verifySignature(DeviceReportDTO dto) { String payload = dto.getRequestId() + dto.getDeviceNo() + dto.getReportTime(); String expected = HmacUtils.hmacSha256Hex(device.getSecretKey(), payload); return MessageDigest.isEqual( expected.getBytes(StandardCharsets.UTF_8), dto.getSign().getBytes(StandardCharsets.UTF_8) ); }

这里一定要用MessageDigest.isEqual做摘要比较,直接equals会存在时间侧信道问题,虽然在小项目里风险不高,但习惯要养好。

3.2 满溢检测:不要把状态更新写成无脑覆盖

满溢检测看起来很简单——设备上报一个“当前高度”或“当前重量”,超过阈值就变满。但真实设备数据并不是理想曲线。桶盖没关、居民手扶着桶边、阴雨天气、设备离线重传,都会导致异常数据。

我最初的做法是收到一条上报就直接更新桶状态,结果一天能收到几十条误报,清运员骂街。后来改成“滑动窗口确认”:

  1. 每条上报先把“传感器读数”存到临时表或 Redis;
  2. 统计最近 10 分钟内连续 3 次上报都超过阈值,才把桶状态置为“已满”;
  3. 桶状态从“已满”变回“正常”,也需要连续 2 次低读数确认。

也就是说,后端不是直接用上报值覆盖状态,而是把上报值当成“证据”。

boolean shouldFull = fullDetector.shouldMarkFull(deviceNo, bucketNo); if (shouldFull && bucket.getStatus() != BucketStatus.FULL) { bucket.setStatus(BucketStatus.FULL); bucketMapper.updateById(bucket); collectionNotifyService.notifyFull(bucket); }

这个细节看着小,但直接决定了清运员每天是“按指令出车”还是“来回跑空车”。满溢检测的准确性,才是智能管理的可信度来源。

3.3 积分激励与防刷:先想清楚用户体验

积分规则我设为可配置。后台管理员可以设置:正确投放 +10 分,错误投放 +1 分鼓励分,每日有效计分上限 3 次。为什么不鼓励多投?因为垃圾管理的目标是“分对”,不是“多扔”。如果按投放次数无限加分,反而会助长多制造垃圾。

积分发放的坑主要在防刷。设备上报接口只要验签通过,就能触发积分。但有些居民会在垃圾桶前反复投递小物件来凑积分。我的处理是加了多层限制:

  • 同一用户每天最多累计 3 次有效计分;
  • 同一设备同一桶位、同一用户,10 分钟内的重复投放只计一次;
  • 使用 Redis 计数器做小时级限流:同一用户每小时投递次数超过 5 次,只记录不积分。

积分账户的更新不能用先查询再更新,因为并发情况下会丢更新。我会在事务里直接做条件更新:

UPDATE t_user SET points_balance = points_balance + #{delta} WHERE id = #{userId} AND (points_balance + #{delta}) >= 0;

这条 SQL 的意义是:把余额检查和变更放到一次原子操作里。如果返回的影响行数是 0,说明积分不够扣,直接抛业务异常,事务回滚。所有积分变动同时要写一条流水到t_integral_ledger,方便以后对账。

3.4 巡检任务和清运工单生成

巡检任务有两个来源:

  • 每天凌晨按巡检规则批量生成,比如每个投放点每天至少检查一次;
  • 实时生成,比如识别置信度过低、居民被连续纠正多次、设备离线超过 24 小时。

清运工单来源也很清晰:桶状态为已满,或到达固定清运周期。生成工单时,我不是一台一台生成,而是把同一片区内所有“已满”桶聚合到一张工单里,再按位置排好顺序。

位置排序我一开始想得很复杂,又是图算法又是动态规划。后来发现对一个小区来说,投放点之间距离很近,最值得做的是“按片区聚合,再按经纬度计算顺序”。我写了一个基于球面距离的排序函数,实际效果已经够用。

工单状态机是:待清运 -> 清运中 -> 已完成。清运员到达后点击“开始”,清运后点击“完成”,并上传照片。这个操作最好放到小程序或 App 里,因为 Web 后台在现场不方便用。

4. 关键代码和服务实现拆解

前面讲完了设计,这一章讲实现。我从实际项目中挑出四个最关键的服务代码来拆解。

4.1 投递接口的幂等与事务处理

设备上报接口是整套系统的入口,我把它设计成“先幂等,再处理”。上报接口大致是这样:

@PostMapping("/devices/report") public Result<Long> report(@RequestBody @Valid DeviceReportDTO dto) { if (!deviceAuth.verify(dto)) { return Result.fail("设备签名校验失败"); } Long recordId = deliveryService.report(dto); return Result.ok(recordId); }

deliveryService.report里有几个关键点:

@Transactional(rollbackFor = Exception.class) public Long report(DeviceReportDTO dto) { Long existId = deliveryRecordMapper.selectIdByRequestId(dto.getRequestId()); if (existId != null) { return existId; } DeliveryRecord record = new DeliveryRecord(); // ... 字段赋值 deliveryRecordMapper.insert(record); bucketService.updateSensorInfo(record); integralService.grantForDelivery(record); TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void afterCommit() { if (record.getConfidence() < 0.85) { mqTemplate.convertAndSend("exchange.task", "task.review", record.getId()); } if (record.isFull()) { mqTemplate.convertAndSend("exchange.task", "task.collection", record.getId()); } } }); return record.getId(); }

这里最值得学的习惯是:事务提交成功后再发消息。如果把消息发送放在事务内,事务回滚后消息可能已经发出去,消费者读到一条不存在的记录,会产生脏任务。用afterCommit可以避免这个问题。

4.2 积分账户的扣减与并发

积分账户是典型的“多读少写但必须强一致”的数据。我做得比较笨但很可靠:用数据库条件更新代替分布式锁。

给用户加分:

@Transactional(rollbackFor = Exception.class) public void grantForDelivery(DeliveryRecord record) { int delta = ruleService.getPoints(record); if (delta <= 0) { return; } int updated = userMapper.increaseBalanceWithCheck(record.getUserId(), delta); if (updated == 0) { throw new BizException("积分账户更新失败"); } IntegralLedger ledger = new IntegralLedger(); ledger.setUserId(record.getUserId()); ledger.setChangeType("DELIVERY"); ledger.setAmount(delta); ledger.setBizId(record.getId()); ledger.setBalanceAfter(userMapper.getBalance(record.getUserId())); integralLedgerMapper.insert(ledger); }

扣分场景类似,比如管理员责令整改扣 20 分。扣分时increaseBalanceWithCheck的 SQL 会带上AND points_balance + #{delta} >= 0,这样负数不会出现。

有人坚持要用 Redis 分布式锁来防并发,我也试过。但积分类业务最后还是要落库,数据库条件更新本身已经保证了原子性,锁反而把问题复杂化。只有跨表复杂事务才建议引入锁。

4.3 管理端统计大屏的数据计算

管理端的大屏无非是几个数字:今日投递次数、今日分类正确率、满溢桶数量、各类型垃圾占比。如果每次刷新都COUNT(*)一次,数据量稍微上来就会卡。

我的做法是“分层统计”:

  • 投递发生时,把t_delivery_record的增量更新到小时表t_stat_hourly;
  • 凌晨定时任务把小时表聚合成天表t_stat_daily;
  • 大屏只查t_stat_daily和t_stat_hourly。

真正要跑明细查询的场景是“管理员想看某个人最近 7 天的投递记录”,那才走线上表并加 limit。大屏上的数字允许有最多几分钟的延迟,不需要实时到秒。

聚合统计核心 SQL 大概是:

SELECT DATE_FORMAT(put_at, '%Y-%m-%d') AS day, SUM(CASE WHEN final_type = bucket_type THEN 1 ELSE 0 END) / COUNT(*) AS correct_rate, COUNT(*) AS total_count FROM t_delivery_record WHERE put_at >= #{startTime} GROUP BY day ORDER BY day;

这种方法听起来不高级,但非常稳。我见过很多团队把统计大屏做出实时数仓,最后维护成本远超收益。

4.4 定时任务在集群下的重复执行问题

项目部署成多实例后,Spring Boot 自带的@Scheduled会在每个实例上都执行一遍。如果定时任务只是“清理过期日志”还好,但如果是“生成清运工单”,重复执行会生成两批工单。

我的方案是 Redis 分布式锁。每个节点执行定时任务前,先尝试设置一个带过期时间的 key:

String lockKey = "job:generate:collection:" + LocalDate.now(); Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", Duration.ofMinutes(10)); if (!Boolean.TRUE.equals(locked)) { log.info("工单生成任务已被其他节点执行,本次跳过"); return; } try { generateCollectionOrder(); } finally { redisTemplate.delete(lockKey); }

这里有个细节:如果任务执行时间超过锁的过期时间,第二次执行会拿到锁,导致重复。所以锁过期时间要设得比任务最大执行时间更长,并且任务最好拆成小批量执行,避免单次跑太久。更稳妥的方案是引入分布式任务调度组件,但对于这种单类定时任务,Redis 锁完全够用。

5. 部署、性能与稳定性细节

项目能跑起来和能稳定跑下去是两码事。这套系统涉及设备端上报,稳定性要求比普通管理系统高。

5.1 单体起步的部署方式

第一版部署我建议直接一台 Spring Boot + MySQL + Redis。用一个docker-compose.yml把 MySQL、Redis、RabbitMQ 拉起来,Spring Boot 打成一个 jar 跑在宿主机上。初期用户量小,不需要上 K8s。

生产环境要区分配置:

spring: datasource: url: ${DB_URL} username: ${DB_USERNAME} password: ${DB_PASSWORD} data: redis: host: ${REDIS_HOST} port: ${REDIS_PORT}

环境变量不要在配置文件里写死,否则一换环境就要重新打包。我吃过这个亏:开发环境连的是本机 MySQL,生产环境连的是另一台机器,因为配置写死,上线时才现改,结果手滑改漏了端口,排查了半天。

5.2 缓存、消息队列和削峰

设备上报集中在早晚高峰,比如早上 7 点到 9 点。这个时段如果清运工单、积分通知、统计更新全部同步处理,接口 P99 会明显变差。所以我用 RabbitMQ 做削峰。

主服务收到上报后,只做三件最要紧的事:

  1. 幂等校验;
  2. 插入投递记录;
  3. 更新桶状态。

然后发一条消息到delivery.report.queue,由另一个消费者服务去处理积分发放、通知推送、统计更新。这样做的好处是主链路响应很快,消息堆积也不怕,消费者慢慢消费即可。

消费者要做好失败重试。我把 RabbitMQ 的消费者设为手动 ack,业务处理成功才确认消息。如果异常,重试三次后进入死信队列,方便人工排查。否则一条坏消息会把整个消费队列堵住。

5.3 日志与接口安全

设备上报接口的日志是排查问题的关键。我给每个请求都生成了一个traceId,并放到 MDC 里:

String traceId = UUID.randomUUID().toString().replace("-", ""); MDC.put("traceId", traceId);

日志配置里把traceId打印出来,后面查“某台设备某次上报为什么没有积分”,直接按 traceId 搜日志,比翻数据库快得多。

接口安全除了验签,还要做访问限流。我基于 Redis 对设备号做了单机限流,比如同一设备每秒最多上报 10 次。防止设备端程序 bug 导致突发流量打垮后端。

6. 常见问题与排查实录

这个项目做完后,我总结了几类最常见的问题。这些问题不是从书本上抄来的,是运行过程中真实出现过的,每一项都可以直接抄答案。

6.1 重复积分:看起来是Redis问题,其实是业务幂等没做

现象:居民反馈投一次垃圾加了两次积分;后台查流水,同一条requestId出现了两条记录。

原因:设备侧断网重试。设备上报超时后,业务端其实已经把记录写入了,但设备没收到响应,于是又发了一次。如果后端只看是否查到记录,不加唯一索引,就会重复插入。

排查过程:先打开投递记录表,按request_id分组,发现有 ID 不同但request_id相同的记录。然后检查接口日志,确认同一时间点收到两次上报。最终处理方式是给request_id加唯一索引,并在插入前先查一次。这个唯一索引才是最后一道兜底。

6.2 满溢状态误报:设备数据也要“滤波”

现象:清运员到达现场后,桶只有三分之一,根本不满。管理端设备状态却是“已满”。

原因:设备传感器读到了瞬时噪声,比如居民掀桶盖时盖子挡了一下传感器,或者雨水附着在探头表面。后端收到一次超阈值上报就直接改状态,没有做连续性判断。

处理:改成滑动窗口确认,最近 10 分钟内至少 3 次超阈值才标记为满。同时在前端显示“最近上报时间”,如果设备长时间没上报,状态自动置为“离线”,防止用旧数据误导调度。

6.3 分配路线不合理:先解决“桶是否真的满了”

现象:系统生成工单要求清运员去 5 个点位,结果其中 2 个实际是空的,清运员觉得自己被系统耍了。

原因:本质上不是路线算法的问题,是“桶满”这一状态本身就不准确。路线分配再怎么优化,喂进去的数据是脏的,出来也是错的。把满溢判断改稳定后,路线问题基本消失。

所以我建议做“智能调度”之前,先花时间保证“状态数据可信”。数据不准,算法再高级都是空中楼阁。

6.4 一个我没想清楚的坑:图片上传直接进了业务库

我最初图省事,把设备上报的图片转成 Base64 直接存到t_delivery_record.image_url这个字段里。后来发现这个字段越来越大,备份一次要十分钟,查询也变慢。

处理:图片只存对象存储路径,数据库字段只存 URL。我切到本地对象存储服务后,把历史图片做了一次迁移,删除掉数据库里的 Base64 大字段。这里得到的教训是:数据库存状态,存关系,存金额,但不要存大文件内容。

7. 复盘与后续扩展

项目做到这里,已经不只是“管理系统”了。它更像一个轻量物联网平台的雏形。我最后说一下做完之后的一些感受。

7.1 做得比较顺手的地方

让我最满意的是业务边界的划分。我没有去纠结“智能”这个词,而是把能力拆成“设备提供识别结果,后端处理业务动作”。这种分层让后端代码很干净,识别模型升级时,后端接口完全不用改。

幂等设计和积分账本设计也给我后面省了很多事。投递记录有唯一索引,积分流水可追溯,这些基础规范看着不起眼,但在出问题时能救命。

7.2 如果可以再来一次,我会调整什么

第一,我会在一开始就引入多租户字段。哪怕第一版只有一个社区,也会给t_user、t_device、t_delivery_record都加上community_id。后边扩展到第二个社区时,不需要改表和迁移数据。

第二,设备注册流程会做得更完整。第一版我把设备信息直接写死在数据库,没有设备上线自动注册的概念。设备换一台维修机,就要手动配device_no和secretKey,很麻烦。

第三,我会早一点写好接口文档。Spring Boot 后端千万别等到联调前才补文档,设备端是外部团队对接的,他们需要稳定的接口约定。我中间因为“字段含义理解不一致”改了好几次参数。

7.3 后续还能往哪扩展

如果继续做下去,最自然的扩展方向是:

  • 设备端从 HTTP 改成 MQTT 上报,断线重连和消息推送更合适;
  • 增加预约上门回收,让居民可以在小程序下单,清运员接单;
  • 把积分跟个人环保档案绑定,做长期环保行为的可视化;
  • 增加可回收物的“按重量计价”模式,让居民投放饮料瓶、纸箱时获得更直接的反馈。

这里面每一步其实都可以独立成一个小项目。但从这一版系统的经验来说,最应该保住的,是“投递记录可信、桶状态可信、积分账本可信”。只要这三根柱子稳了,后面往上盖多少层功能都不怕。

我个人实际感受是,这类系统做久了之后,最值钱的不是写代码,而是能看懂一个小区里“垃圾从产生到清运”的完整过程。把这个过程中的每个坑都解决掉,比堆一堆炫酷技术更有成就感。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 22:26:24

LangChain+百度搜索API搭建RAG人物知识库:从公开资料到数字分身

把LangChain和百度搜索API串起来&#xff0c;给一位公众人物做数字分身&#xff0c;听起来像是营销噱头&#xff0c;但拆开看&#xff0c;它就是一个非常典型的RAG项目&#xff1a;先搜资料、再建知识库、最后让大模型带着人设去回答问题。我最近用这套框架完整跑通了一遍&…

作者头像 李华
网站建设 2026/10/21 22:25:56

连接池引发的网关偶发延迟:TCP半开连接排查与修复

1. 故障表现与第一轮排查&#xff1a;为什么一开始就把方向带偏了事情发生在项目X做版本迭代后的第三天。线上反馈陆续过来&#xff0c;说有几个查询接口会偶发卡顿&#xff0c;点一次请求要转圈好几秒才出结果&#xff0c;刷新一下又正常了。这种问题最磨人——它不像服务挂了…

作者头像 李华
网站建设 2026/10/21 22:25:55

知识工作插件实战:从工作流体检到自动化效率提升

1. 从标题到工作流&#xff1a;为什么要做一套知识工作插件看见“knowledge-work-plugins”这个标题&#xff0c;第一反应不是“又是个工具集合”&#xff0c;而是背后隐藏了一条很真实的痛点链&#xff1a;知识工作者的日常&#xff0c;其实被大量低价值的“复制、粘贴、转换、…

作者头像 李华
网站建设 2026/10/21 22:25:38

Java操作HBase实战:从建表到性能调优的完整指南

1. 为什么要在Java里直接操作HBase&#xff0c;而不是绕道走 很多刚接触大数据存储层的人会有一个疑问&#xff1a;HBase本身已经提供了Shell客户端&#xff0c;敲几行命令就能建表、插数据、查记录&#xff0c;为什么还要费劲用Java写代码去操作它&#xff1f;这个问题我在带新…

作者头像 李华
网站建设 2026/10/21 22:25:19

从排期卡壳到技能地图:团队能力盘点的完整实践指南

1. 从一次排期事故说起&#xff1a;为什么团队需要一张"技能地图"关于"skills"这件事&#xff0c;我最早是在一次项目排期卡壳时被逼着认真对待的。当时负责一个跨模块的版本迭代&#xff0c;任务拆解完以后&#xff0c;我突然发现一个尴尬的事实&#xff…

作者头像 李华