做垃圾分类这块,其实是我自己小区垃圾桶旁边站出来的想法。当时垃圾督导员阿姨拿着夹子,对着我手里拎的袋子反复问“这是什么垃圾”,我低头看了半天也没敢确定。那一刻我就琢磨:能不能做一个系统,拍照就能告诉你这是什么垃圾、应该扔哪个桶?这个念头后来演变成了一个完整的项目:基于Spring Boot的智能垃圾分类系统。整套系统包含前端小程序/移动端H5、后端管理平台、图像识别服务和积分激励体系,核心解决三个问题——用户不知道垃圾怎么分、管理者不知道分类效果怎么样、居民参与积极性起不来。这篇文章把我从需求拆解到部署上线的全过程写透,包括数据库怎么设计、识别接口怎么调、积分怎么防刷、部署踩了哪些坑,尽量让拿到源码的同学能直接跑起来,而不是对着代码干瞪眼。
1. 项目需求与业务场景拆解
1.1 垃圾分类这个场景到底要解决什么问题
垃圾分类看起来是个“识别”问题,本质上是个“行为引导”问题。一个居民站在垃圾桶前,他要的不是一个多复杂的算法,而是“这个东西到底扔哪”的确定性答案。所以系统的第一优先级不是技术炫技,而是准确、快速、低门槛。
从用户侧看,真实使用场景主要有三个:
- 不确定某样东西属于哪类垃圾,想拍照问一下
- 知道分类但记不准,例如“脏塑料袋”和“干净塑料袋”是不是一个扔法
- 想参与分类但缺少动力,需要一些激励手段
从管理侧看,社区或物业关心的是:分类参与度高不高、准确率高不高、哪些垃圾类别容易分错、积分活动有没有人玩。这些都需要数据支撑,而不是靠人工抽查。
所以我把系统拆成了四个核心模块:用户端(小程序/H5)、识别服务、管理后台、积分激励。这四个模块缺一个都不完整。很多同类项目只做了“拍照识别”就收工了,但实际用起来你会发现,没有积分和后台统计,整个系统就是一个“一次性玩具”,用户用完就走,运营毫无抓手。
1.2 核心角色与功能清单
系统涉及三类角色,权限并不复杂但边界要清楚。
普通用户:
- 注册登录(手机号+验证码,或第三方登录)
- 拍照/上传图片识别垃圾类别
- 查看识别结果与分类详情(包括投放指导)
- 积分查询、签到、兑换
- 误判反馈
管理员/运营:
- 登录后台
- 垃圾类别与物品库管理
- 识别记录与用户行为统计
- 积分规则配置
- 误判反馈处理、知识库维护
系统层面:
- 统一鉴权与数据权限
- 定时任务(积分结算、统计报表)
- 日志与异常监控
这里有个容易被忽略的点:垃圾知识库是持续更新的。今天居民问“玉米皮是什么垃圾”,明天可能问“口红管是什么垃圾”。如果知识库不能由运营在线维护,系统上线三个月后就废了。所以后台的可配置能力比识别能力本身更影响项目寿命。
1.3 技术选型:为什么是Spring Boot而不是别的
后台技术选型让我犹豫过一轮,核心候选是Spring Boot和Python(FastAPI/Django)。最后选了Spring Boot,理由很实际:
第一,生态成熟且招人好招。国内做这类管理系统,Java技术栈的人才储备和开源组件生态明显更适合团队协作。我见过很多用Python快速搭起来的后台,后期加需求时被GIL和异步模型折腾到怀疑人生。Spring Boot在这方面的上限要高得多。
第二,开箱即用的东西多。Spring Boot对Spring生态的整合能力不用我吹,Spring Security做登录、MyBatis-Plus操作数据库、Redis做缓存和分布式锁、XXL-JOB做定时任务,这些都是现成的轮子。我这次还把Spring Boot Admin接进来了,用来监控应用的健康状态和内存指标,省掉了自建监控面板的功夫。
第三,部署形态对中小项目友好。一个可执行JAR包搞定所有,不依赖外部容器,内存控制在512MB以内也能跑。配合Docker做镜像,部署一台2核4G的云服务器完全够支撑一个小区的使用量。
版本上我用了Spring Boot 2.6.x。不选3.0的原因很朴素:稳定性优先。3.x虽然已经出了很久,但部分第三方组件和自动配置的兼容性在2.6.x上更稳。网上那些“Spring Boot 2.3.x 2.6.x”的版本对比贴我基本都翻过,2.6.x处于一个非常微妙的平衡点——既没有2.3那么老,又不像3.x那样大面积改动底层。
2. 系统总体架构与核心流程设计
2.1 前后端分离的整体拓扑
这个项目我采用了前后端完全分离的架构,后端只提供RESTful API,前端分两个独立工程:一个面向居民的H5/小程序端,一个面向管理员的Vue后台。两者的入口不同,但共用同一套鉴权体系,只是角色权限不同。
整体交互链路是这样的:
用户在小程序端上传一张垃圾照片,前端先把图片传到一个临时存储(我用的是MinIO,本地也能起),拿到文件地址后再调用后台的识别接口。后台接收图片地址后,调用图像识别服务,拿到一组带置信度的识别结果,再结合本地垃圾物品库做二次映射,最终把“物品名称、所属类别、投放建议、常见误判提醒”返回给前端。整个过程大约3秒以内,用户体感是“拍一下,马上知道扔哪个桶”。
这里我建议把图片上传和识别拆成两步,而不是把图片二进制直接塞进识别接口。好处是识别服务挂了的话,图片还在存储里,可以后端重试,不至于让用户白白传一次。
2.2 模块划分与核心流程
后端我把项目拆成了五个子模块,用Maven多模块管理:
waste-classification ├── waste-common // 通用工具类、统一返回结果、全局异常 ├── waste-system // 用户、权限、菜单等基础模块 ├── waste-api // 对外API接口层 ├── waste-service // 业务逻辑层:识别、积分、统计等 └── waste-admin // 后台管理接口识别流程的完整串联是这样的:
- 用户上传图片
- 后端校验图片格式和大小(超过10MB直接拦截)
- 图片存储到MinIO,返回URL
- 调用识别服务,获取物品标签和置信度
- 通过标签匹配本地垃圾物品库,得到分类结果
- 如果是“可回收物”,额外返回对应的投放要求(比如“请清空内容物并压扁”)
- 保存识别记录,同步更新用户积分
- 返回完整结果给前端
这个链路里最微妙的一环是第5步。第三方图像识别返回的标签往往不是垃圾名,而是“塑料瓶”“易拉罐”这种物品名;如果识别模型不够准,可能返回“玻璃杯碎片”这种非常细的标签。所以本地物品库必须做同义词和别名映射,比如“矿泉水瓶”“PET瓶”“塑料瓶”全部映射到同一个物品ID上,否则知识库会越维护越乱。
2.3 图像识别方案:第三方API还是自建模型
识别这块是很多初学者最纠结的地方。我的建议是:现阶段别自建模型,先接成熟API,跑通业务后再考虑私有化。
我自己对比了多个方案,最终选了成熟API方案。原因有三个:
- 自建分类模型需要大量标注数据,垃圾种类上千种,标注成本高到离谱
- 通用图像识别API对常见物品的识别准确率已经足够,先跑通MVP才是关键
- 后期数据积累多了,可以用识别记录里的“高置信度+用户确认”数据做增量训练,再考虑本地化
实现上我封装了一层IdentifyService接口,底层默认实现是调第三方API,但这层封装的价值在于:以后想换成自建模型,只需要新增一个实现类,业务层一行都不用改。这就是面向接口编程的意义。
public interface IdentifyService { IdentifyResult identify(String imageUrl); }这里提一句:不要迷信识别结果,置信度低于0.6的返回结果我会在业务层做二次确认,让用户看到一个“可能识别有误,请确认”的提示。这种设计比直接甩一个错误答案体验好得多。
3. 数据库设计与核心表结构
3.1 核心数据表逻辑
垃圾分类系统的数据量不会特别大,但表的设计要有扩展余地。我最终设计了8张核心表,这里挑最重要的几张说。
用户表(t_user)
用户表必须跟居民信息打通,以后才能做“这个小区的参与率是多少”这类统计。字段上除了基本资料,我特意加了bound_address(绑定地址)字段,虽然前期不一定录入,但以后按小区/街道维度做统计时,这个字段就是切片维度。
垃圾物品表(t_garbage_item)
这是整个知识库的核心。字段设计如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| item_name | varchar | 物品标准名称 |
| aliases | json | 别名/同义词列表 |
| category_code | varchar | 分类编码(recyclable/kitchen/harmful/other) |
| confidence_threshold | decimal | 识别置信度阈值 |
| instruction | text | 投放指导说明 |
| status | tinyint | 启用状态 |
aliases字段用JSON存储,这个设计很实用。比如标准名称是“一次性纸杯”,别名可以是“纸杯”“一次性杯子”“咖啡纸杯”。识别服务返回的标签会跟这些别名做模糊匹配,匹配成功率大幅提升。
识别记录表(t_recognize_record)
CREATE TABLE t_recognize_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT '用户ID', image_url VARCHAR(255) NOT NULL COMMENT '图片地址', item_id BIGINT COMMENT '匹配到的物品ID', result_label VARCHAR(100) COMMENT '识别原始标签', confidence DECIMAL(5,4) COMMENT '置信度', category_code VARCHAR(20) COMMENT '最终分类', is_correct TINYINT DEFAULT NULL COMMENT '用户是否确认识别正确', create_time DATETIME NOT NULL ) COMMENT '识别记录表';这张表是系统的“金矿”。is_correct字段是用户对识别结果的二次确认,攒到一定量以后,这些数据可以用来评估识别准确率、优化物品库映射关系,甚至做后续的模型微调。很多项目不太在意这个字段,我强烈建议保留。
3.2 积分体系和防刷设计
积分表的设计关系到整个激励体系能不能跑起来。我用了积分流水表来记录每一次积分变动,而不是在用户表里只存一个总分。原因很简单:运营会查“这个用户上周签到给了多少分”“今天哪个时段积分发放最多”,只有流水表才能支撑这种查询。
积分的来源目前有三种:注册奖励、每日签到、识别成功奖励。针对防刷,我做了几个非常关键的限制:
- 同一用户每日识别最高计分次数设为30次,超过后识别功能正常但不再累加积分
- 图片识别接口必须携带用户Token,匿名用户不奖励
- 签到积分用Redis的
SETNX做“每日一签”校验,防止通过切换会话绕过多端签到 - 兑换奖品前校验用户积分的“可兑换”余额(排除冻结部分)
这个防刷设计执行后,后台的积分异常增长趋势明显消停了。有人可能觉得30次有点少,但正常用户一天扔垃圾根本到不了这个量级,而对刷分党来说,30次基本断了念想。
3.3 数据库索引与性能设计
一开始我把索引建得很随意,后期数据量上来后,识别记录表的查询明显变慢。我后来做了一次索引优化:
ALTER TABLE t_recognize_record ADD INDEX idx_user_time (user_id, create_time); ALTER TABLE t_recognize_record ADD INDEX idx_category_time (category_code, create_time); ALTER TABLE t_integral_flow ADD INDEX idx_user_time (user_id, create_time);第一个索引支撑“我的历史记录”分页查询,第二个支撑管理后台“某个分类的识别趋势”统计。这里给个提醒:统计类SQL尽量走日期范围+单维度分组,那种“双维度GROUP BY再排序”的报表SQL,数据量一大必挂。实在扛不住就上定时任务提前算汇总表,别硬查。
4. 核心功能实现与代码解读
4.1 识别接口的实现与参数细节
先看最核心的识别接口。我在WasteIdentifyController里暴露了一个POST /api/waste/identify,接收imageUrl参数,内部串联存储、识别、匹配、积分四个环节:
@PostMapping("/identify") @ApiOperation("垃圾识别") public Result<IdentifyVO> identify(@RequestBody IdentifyRequest request, @RequestHeader("token") String token) { Long userId = userService.getUserIdByToken(token); // 1. 检查今日识别积分次数 int todayCount = recognizeRecordService.countTodayByUser(userId); if (todayCount >= MAX_SCORE_COUNT) { // 超过阈值:正常返回结果,但标记不计分 request.setScored(false); } // 2. 识别 IdentifyResult raw = identifyService.identify(request.getImageUrl()); // 3. 匹配物品库 GarbageItem item = garbageItemService.matchItem(raw.getLabel()); // 4. 保存记录 RecognizeRecord record = buildRecord(userId, request.getImageUrl(), raw, item); recognizeRecordService.save(record); // 5. 积分奖励(如果允许) if (request.isScored()) { integralService.reward(userId, IntegralType.RECOGNIZE, record.getId()); } return Result.success(buildVO(item, raw)); }这里Step 1有个小设计:先查次数再决定是否计分,而不是识别失败后再查。因为识别接口本身有延迟,大量无效请求会白白消耗识别API额度。先拦截计分资格,可以顺带减少无意义调用。
matchItem的实现我用的是“精确匹配 -> 别名匹配 -> 模糊匹配”三级降级策略:
public GarbageItem matchItem(String label) { // 1. 精确匹配标准名称 GarbageItem item = itemMapper.selectByItemName(label); if (item != null) return item; // 2. 别名匹配(JSON字段里的任意一个别名) item = itemMapper.selectByAlias(label); if (item != null) return item; // 3. 模糊匹配(LIKE %label%) item = itemMapper.selectByFuzzyName(label); if (item != null) return item; // 4. 匹配不到,返回默认的“其他垃圾”兜底 return itemMapper.selectByDefaultOther(); }这套三级策略实测下来命中率提升非常明显。识别API偶尔返回的品牌名、口语化表达,都可以通过别名表兜住。
4.2 积分发放与流水记录的幂等设计
积分发放必须保证幂等,否则重复提交会导致用户积分暴涨。我采用的是“业务单号去重”策略:每个识别记录ID天然可以作为幂等键。
@Transactional public void reward(Long userId, IntegralType type, Long bizId) { // 利用数据库唯一索引防止同一次奖励被重复发放 try { integralFlowMapper.insert(userId, type, bizId, SCORE); } catch (DuplicateKeyException e) { // 已经发放过,直接忽略 log.warn("duplicate integral reward: userId={}, bizId={}", userId, bizId); return; } userMapper.increaseIntegral(userId, SCORE); }t_integral_flow表在(user_id, type, biz_id)上建了唯一索引,这一步是整个积分体系的基石。注释里的逻辑很简单,但真上线后你会感谢这个设计。Redis分布式锁也是一种方案,但用唯一索引更轻量,也不会有锁过期之类的问题。
4.3 管理后台的统计报表实现
管理后台我用了Vue + ECharts做数据可视化,后端接口只返回原始数据,图表交给前端渲染。这里分享一个我踩过的坑:统计接口一定要做数据脱敏和访问权限控制。后台数据比用户端敏感得多,一定要单独走一套登录鉴权体系,并且对用户手机号、地址等敏感信息做脱敏脱敏处理,比如后台列表里手机号只显示138****1234。
统计维度上我做了三个固定的:按天识别量趋势、按垃圾类别占比、按用户活跃度排行。这三个指标能覆盖90%的运营场景。至于更复杂的“各小区分类准确率对比”,我放在二期规划里,因为涉及到小区维度的数据完善,前期先不做。
@GetMapping("/api/v1/stats/category") public Result<List<CategoryStatVO>> categoryStats(@RequestParam(required = false) String startDate, @RequestParam(required = false) String endDate) { String start = (startDate == null ? LocalDate.now().minusDays(30) : LocalDate.parse(startDate)).toString(); String end = (endDate == null ? LocalDate.now() : LocalDate.parse(endDate)).toString(); List<CategoryStatVO> list = recognizeRecordMapper.selectCategoryStats(start, end); return Result.success(list); }这里注意一点,日期参数我建议始终用字符串传递,不要用时间戳。字符串可读性强、能直接拼SQL、也方便前端传参。时间戳在跨时区场景下容易出幺蛾子,这里完全没必要引入这种复杂度。
5. 部署实操与避坑指南
5.1 本地开发环境的搭建要点
先解决一个问题:IntelliJ IDEA社区版能不能开发Spring Boot项目?答案是完全能。社区版免费,虽然没有Spring Initializr向导和Spring Boot的专属Run Dashboard,但你可以直接去Spring官网的 start.spring.io 生成项目压缩包,解压后导入IDEA社区版,照样开发运行。我平时很多Demo项目就是这么干的,没有明显卡顿。
环境清单如下:
| 组件 | 版本 | 备注 |
|---|---|---|
| JDK | 1.8 / 11 | Spring Boot 2.6.x两者皆可 |
| Maven | 3.6+ | 简化为内置Maven也行 |
| MySQL | 5.7 / 8.0 | 推荐8.0,字符集utf8mb4 |
| Redis | 3.x以上 | 用于Token缓存、签到锁 |
| MinIO | 最新稳定版 | 图片存储,可替换为OSS |
| Node.js | 14+ | 前端Vue项目用 |
这里有个坑要提醒:JDK版本别乱升。我见过有人用JDK 17跑Spring Boot 2.6.x,虽然能启动,但部分反射和代理相关的操作会报InaccessibleObjectException。要么老老实实JDK 8/11,要么直接升级Spring Boot 3.x配JDK 17,别混搭。
5.2 配置文件中的关键项
核心配置文件application.yml里,有几个配置项我单独强调一下:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB datasource: url: jdbc:mysql://localhost:3306/waste_classification?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: root password: root123 hikari: maximum-pool-size: 20 minimum-idle: 5 mybatis-plus: 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 minio: endpoint: http://localhost:9000 access-key: minioadmin secret-key: minioadmin bucket: waste-imageserverTimezone=Asia/Shanghai一定要显式配置,否则MySQL驱动按UTC处理时间,数据库里的时间跟本地时间差了8个小时,排查起来非常痛苦。
5.3 Docker部署与上线注意事项
我项目的部署形态是:后端JAR包打成一个Docker镜像,前端打完包通过Nginx托管,MySQL和Redis用Docker Compose管理,MinIO单独一台。
Dockerfile很简单:
FROM openjdk:8-jre-alpine RUN adduser -D -u 1000 app WORKDIR /app COPY target/waste-classification.jar app.jar USER app EXPOSE 8080 ENTRYPOINT ["java","-jar","-Xms256m","-Xmx512m","app.jar","--spring.profiles.active=prod"]这里有个容易被忽视的参数:Docker容器里JVM的堆内存设置。如果不显式设置-Xmx,JVM在容器里会按宿主机内存的1/4自动分配,可能直接撑爆容器配额导致OOM被Kill。我在2核4G的云服务器上设了-Xmx512m,既够用又不抢占宿主机内存。
Nginx那边核心配置是前端静态文件托管 + 后端API反代:
server { listen 80; server_name waste.example.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } client_max_body_size 20m; }client_max_body_size 20m这行容易被忽略,但如果没有它,上传稍大点的图片直接返413,排查半天才发现是Nginx的默认1MB限制在拦截。
6. 常见问题与排查技巧实录
6.1 我真实遇到过的5个问题
这里整理一下我开发部署过程中遇到过的典型问题,写成速查表,大概率你也会撞上:
| 问题现象 | 根本原因 | 解决办法 |
|---|---|---|
| 上传图片后Nginx返回413 | Nginx默认限制请求体1MB | 在server块加client_max_body_size 20m |
| 数据库时间比北京时间晚8小时 | JDBC连接参数未指定时区 | 使用serverTimezone=Asia/Shanghai |
| Docker容器启动后不久被Kill | JVM自动按宿主机内存分配堆内存 | 显式设置-Xmx512m |
| 积分重复发放 | 多端并发请求同一个识别回执 | 在流水表建(user_id,type,biz_id)唯一索引 |
| 识别接口偶尔挂掉导致核心功能不可用 | 第三方识别API不稳定 | 接口降级:识别失败时用关键词本地匹配兜底 |
第五个问题值得展开说。识别API是外部依赖,这堵墙倒了,系统其他功能不能跟着瘫。我在设计时加了兜底:如果识别服务超时或报错,可以根据图片文件名里的用户手动输入关键词,走本地知识库简单匹配。虽然准确率会下降,但至少用户还能拿到一个“参考分类”,而不是直接看到“系统繁忙”。
6.2 处理接口超时的经验
识别服务我设置了超时时间,连接超时5秒、读取超时15秒。为什么读取要15秒?因为我用的是同步调用,而第三方识别服务的响应时间在2-8秒之间波动,太短容易误杀正常请求,太长又会拖垮线程池。
另外一个经验教训是:千万别在主线程同步调识别服务。用户点击识别后,前端会一直转圈等待,如果识别服务响应慢,用户直接就退出了。我最终的方案是:识别接口同步返回,前端3秒内拿到结果,超过3秒轮询一次识别结果异步查询接口。但这样增加了不少开发量。如果是MVP阶段,直接同步返回也能接受,只是要对用户体验的损失有心理准备。说到底这是个取舍,不是技术能力问题。
6.3 一套我私藏的排查流程
碰到问题时,我习惯按以下顺序排查,基本能定位90%的问题,特别适合刚接触Spring Boot的新手参照:
- 先看日志:
tail -fn 200 application.log,抓最新的异常栈 - 再确认端口:
netstat -tunlp | grep 8080,排除端口占用 - 确认数据库连接:
redis-cli ping、mysql -uroot -p -e "select 1",排除基础中间件故障 - 用Postman直接调接口,绕过前端定位是前端问题还是后端问题
- 看Nginx日志:
error.log里有大量有用的线索
排错最忌讳上来就改代码。先确认环境、网络、依赖这些基础项,往往能省下大量时间。
7. 源码结构与获取方式说明
7.1 源码目录和运行说明
老规矩,说说源码怎么跑。项目源码我已经整理好放在文末联系即可获取,包含了后端完整代码、前端管理后台代码、数据库初始化脚本和部署文档。结构说明如下:
后端(Java Spring Boot 2.6.x) ├── controller/ // 接口层,只做参数接收和结果包装 ├── service/ // 业务逻辑层,核心逻辑都在这里 ├── mapper/ // MyBatis-Plus的Mapper接口 ├── entity/ // 数据库实体类 ├── config/ // 各种配置类(Redis、MinIO、WebMvc等) └── resources/ ├── mapper/ // XML文件(复杂SQL写在这里) └── application.yml 前端(Vue 3 + Element Plus) ├── views/ // 页面组件 ├── api/ // 统一API封装 └── router/ // 路由配置 数据库 ├── schema.sql // 建表脚本 └── init_data.sql // 垃圾物品库种子数据运行顺序:导入数据库脚本 -> 启动Redis和MinIO -> 修改application.yml为本地环境 -> 启动Spring Boot工程 -> 启动前端工程 -> 访问后台登录页。全程文档里都写了,照着做就行。
7.2 获取方式与二次开发建议
源码获取方式,直接文末联系即可。我这边会把完整的项目压缩包发给你。另外给拿到源码的同学几个实在的建议:
第一,**
这个项目天然适合做毕业设计或课程设计。需求明确而且有真实社会价值,技术栈主流不偏门,能讲的亮点很多——智能识别、积分激励、统计分析,每一个模块都能在答辩时拿出实际效果来展示。
第二,**
拿到源码别直接把别人的名字改成自己的就交差**,这是最蠢的做法。我的建议是至少替换掉其中两个模块的逻辑——比如把积分规则改造一下,或者把识别服务换成另一个API——这样既能跟老师解释清楚,又不浪费这个项目本身的价值。
第三,上线部署前记得改掉所有默认密码和密钥**。开发环境里用的是minioadmin/minioadmin这种默认值,上线环境一定得换成复杂密码,数据库账号同理。这是最基本的安全习惯,很多初学者容易忽略。
我在实际开发这个系统的过程中,最深的体会是:技术难点从来不在怎么写一个“Hello World”级别的小接口,而是怎么把一个带真实业务场景的系统,从需求理解、表设计到部署运维完整地串起来。垃圾分类这个项目恰好把这种复杂度浓缩得刚刚好——不会过于庞大让人失去信心,又方方面面都涉及到了。希望这篇分享能帮你在做类似项目的路上少踩几个坑,跑通之后回头再看,你是真的能学到东西的。要源码直接文末找我,顺手的事。