1. 项目概述先弄清楚这是个什么系统有没有发现很多“基于Spring Boot的XXX管理系统”最后都做成了给人添麻烦的后台表格这个“社区智能垃圾管理系统”如果只停在“增删改查、导出报表”那就把项目做窄了。我做这套系统的第一反应是它表面是个 Spring Boot 后端项目实际上是在管理“居民投放、设备上报、分类识别、督导巡检、清运回收、积分激励”这六件事组成的闭环。我先说一个容易被标题误导的点这里的“智能”不一定是让你从零训练一个垃圾分类模型。真实落地时“智能”主要体现在三个地方垃圾投放点有摄像头和传感器能自动识别投入的是哪类垃圾检测桶是不是快满了后端拿到设备上报数据后能自动生成满溢预警和清运工单而不是等居民投诉管理者能从投递记录里算出分类正确率、时段高峰、常见误投类型用数据反向优化宣传和巡检路线。所以 Spring Boot 在这里的核心角色不是做图像识别算法而是做“可信的业务平台”接收设备侧结果、保证数据不重复、分配积分、生成工单、提供统计接口。想明白这一点再开始设计表和写接口思路会顺很多。1.1 别被“智能”两个字带偏我做这套系统时最怕的事是甲方张口就要“人脸识别垃圾桶”“语音提示”“自动称重”“满溢报警”最后全堆在 PPT 上结果后台连一条投递记录都查不清楚。真正应该先把稳的是下面这条业务链路居民扫码或刷卡打开对应分类的桶门居民投入垃圾设备拍照并做本地识别设备把识别结果、重量、时间、桶状态一起上报给 Spring Boot 后端后端校验数据是否合法生成投递流水给居民加积分或推送错误提示垃圾桶满溢时系统生成清运工单清运员上门清运督导员处理识别置信度低或居民反复投错的记录管理后台按社区、按点位、按分类类型做统计。这套链路里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) ) ENGINEInnoDB;第二张是设备表。设备表要区分“硬件设备”和“桶位”。一个设备可能管多个投放口比如四个桶所以我把桶位单独拆出来。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) ) ENGINEInnoDB;第三张是投递记录这是全系统最核心的表。它保存每一次投放的设备侧数据和最终分类结果。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) ) ENGINEInnoDB;后面还有积分流水表、工单表、巡检表但核心逻辑都在用户、设备、投递记录这三张表上。索引设计上我特别克制投递记录上加request_id唯一索引用在幂等判断加(user_id, put_at)联合索引用在“用户投递历史”查询不在所有字段上加索引因为上报表是写多读少索引太多会拖慢写入速度。2.4 角色与权限边界这套系统里权限并不复杂但最容易搞混的是“居民能看到什么”和“管理员能看到什么”。居民端不需要看到其他居民的名字不需要知道清运人员是谁管理员端要能看投诉记录、设备状态、积分流水。我用 Spring Security 自定义注解做了一套很轻量的鉴权把用户分成居民、督导员、清运员、管理员四种角色。接口上的权限控制主要靠PreAuthorizePreAuthorize(hasAnyRole(ADMIN, INSPECTOR)) GetMapping(/inspection/tasks) public ResultListInspectionTaskVO 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 满溢检测不要把状态更新写成无脑覆盖满溢检测看起来很简单——设备上报一个“当前高度”或“当前重量”超过阈值就变满。但真实设备数据并不是理想曲线。桶盖没关、居民手扶着桶边、阴雨天气、设备离线重传都会导致异常数据。我最初的做法是收到一条上报就直接更新桶状态结果一天能收到几十条误报清运员骂街。后来改成“滑动窗口确认”每条上报先把“传感器读数”存到临时表或 Redis统计最近 10 分钟内连续 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 ResultLong 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 分布式锁。每个节点执行定时任务前先尝试设置一个带过期时间的 keyString 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 做削峰。主服务收到上报后只做三件最要紧的事幂等校验插入投递记录更新桶状态。然后发一条消息到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 上报断线重连和消息推送更合适增加预约上门回收让居民可以在小程序下单清运员接单把积分跟个人环保档案绑定做长期环保行为的可视化增加可回收物的“按重量计价”模式让居民投放饮料瓶、纸箱时获得更直接的反馈。这里面每一步其实都可以独立成一个小项目。但从这一版系统的经验来说最应该保住的是“投递记录可信、桶状态可信、积分账本可信”。只要这三根柱子稳了后面往上盖多少层功能都不怕。我个人实际感受是这类系统做久了之后最值钱的不是写代码而是能看懂一个小区里“垃圾从产生到清运”的完整过程。把这个过程中的每个坑都解决掉比堆一堆炫酷技术更有成就感。
阅读完成 · 觉得有帮助?