前阵子接手了一个街道侧的摊贩管理项目,第一反应是这种系统无非是信息的增删改查,没什么技术含量。真正和街道的人聊了两三轮需求之后才发现,摊贩管理最麻烦的不是写代码,而是把“摊贩信息、证照检查、巡查打卡、费用收缴、居民投诉”这几条业务线串起来。Spring Boot 在这类管理系统里确实是最省事的后端框架,生态成熟、一个人也能撑起整个项目。这篇文章就围绕 springboot 街道摊贩管理系统 从设计、开发到落地的全过程,把业务拆分、技术选型、数据库设计、核心功能实现和常见坑位一次性讲透。
我不打算给你画一堆高深的架构图,而是沿着真实项目的推进顺序走一遍。哪怕你是第一次用 Spring Boot 做毕业设计,也能照着把项目跑起来;如果你已经有经验,重点看后面“常见问题排查”和“部署建议”,很多坑都是常规文档里不会写的。
1. 项目到底在管什么:业务拆解先行
1.1 先把角色和流程理清楚
做管理系统最忌讳一上来就建表写接口。街道摊贩管理系统表面上是对“摊贩”的管理,实际上涉及好几个身份的人:
- 街道管理人员:负责审核摊贩信息、查看统计报表、处理投诉工单。
- 巡查队员:拿着手机到现场确认摊位是否存在、是否占道、是否超范围经营。
- 摊贩:需要登记自己的身份、经营类型、摊位位置,后续还要查看自己的证照状态和缴费记录。
- 居民/投诉人:看到问题后提交投诉,并且能跟踪投诉处理结果。
核心流程也不复杂,可以拆成一条主线、三条辅线。主线是摊贩登记审核后领取摊位二维码,巡查人员扫码或按定位核对现场情况,街道定期生成缴费台账,同时对居民投诉做闭环处理。
我在设计时把整个过程分成了这几个状态节点:
- 摊贩提交登记申请。
- 管理人员审核通过/驳回。
- 审核通过后生成电子牌照和二维码。
- 巡查人员日常检查,记录检查结果。
- 到缴费周期后形成缴费记录,逾期自动标记欠费。
- 投诉工单在待受理、处理中、已办结、已回访之间流转。
先把这条链路想清楚,后面每个功能模块的边界就清晰了。因为这条链路里既有“人”的状态,又有“物”的状态,数据库表之间不能只是简单的主外键关系,还要保留业务状态字段。
1.2 功能模块怎么切才不会做成一堆CRUD
很多人的项目最后看起来像“数据库表反向生成器”,就是因为没有按业务模块划分。我实际划分成了六个模块,每个模块都有明确的核心对象:
| 模块 | 核心对象 | 主要解决的事 |
|---|---|---|
| 用户与权限 | 系统用户 | 管理人员和巡查队员登录、分配角色、操作权限控制 |
| 摊贩登记 | 摊贩档案 | 摊贩信息录入、审核、变更、停用,生成编号 |
| 证照与二维码 | 电子牌照 | 生成牌照记录,二维码绑定摊位信息,扫码可查看 |
| 巡查管理 | 巡查记录 | 巡查队员现场巡检、拍照上传、位置定位、整改反馈 |
| 费用管理 | 缴费记录 | 缴费登记、欠费计算、按月/按季汇总统计 |
| 投诉工单 | 投诉工单 | 居民投诉登记、流转、处理、回访、满意度评价 |
有人会问,一个街道项目需要这么复杂吗?说实话,如果只是为了应付毕业答辩,做一个模块也能交差。但实际甲方在验收时会重点看“业务是否闭环”,比如摊贩被投诉后,巡查记录里能不能体现后续重点检查;摊贩欠费后,管理人员名单里能不能标红。这些跨模块联动,才是系统的价值所在。
开发顺序上我建议先做摊贩登记和证照二维码,再做巡查和缴费,最后做投诉工单。因为前两个模块是基础数据来源,后面的模块都要关联它们。
2. 技术选型:为什么是 Spring Boot 这套组合
2.1 Spring Boot 版本和 JDK 不要盲目追新
看到热搜里很多人问“springboot版本太高怎么办”,我在这个项目里刻意选了稳定性优先的版本组合。
我最终使用的是:
- Spring Boot 2.7.18
- JDK 8
- Maven 3.8+
- MyBatis Plus 3.5.x
- MySQL 5.7 / 8.0
为什么不选 Spring Boot 3.x?因为 3.x 强制要求 JDK 17,很多第三方库的老版本不兼容,网上搜到的教程八成还是基于 2.x 写的,你遇到一个问题可能要踩一晚上版本坑。街道这种内部系统没有高性能压力,稳定性比“新”更重要。Spring Boot 2.7 是 2.x 的最后一个版本,维护周期长,兼容性好,用来做管理类项目非常合适。
如果你一定要用 Spring Boot 3.x,也至少把 JDK 换到 17,并且确认 MyBatis Plus、MinIO、Redis 等依赖都有对应版本,否则会很难受。
2.2 数据访问:MyBatis Plus 而不是 MyBatis 或 JPA
项目里涉及大量条件组合查询,比如按摊贩姓名、经营类型、状态、位置区域查询,还要做分页。如果只用原生 MyBatis,每个查询都要写 XML,后面改需求会非常痛苦。
MyBatis Plus 的优势在于:
- 内置
IService和BaseMapper,单表 CRUD 不用自己写 SQL。 LambdaQueryWrapper可以很方便地拼条件,避免拼接 SQL 出错。- 自带分页插件,一行代码解决分页问题。
- 支持逻辑删除配置,删除记录时自动变成
UPDATE ... SET deleted = 1。
代码里我大量使用了这样的写法:
Page<Vendor> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Vendor> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(name), Vendor::getName, name) .eq(StringUtils.isNotBlank(bizType), Vendor::getBizType, bizType) .eq(status != null, Vendor::getStatus, status) .orderByDesc(Vendor::getCreateTime); vendorService.page(page, wrapper);这段代码等价于一条动态 SQL,但比写 XML 直观多了,后续加筛选条件也很方便。
2.3 Redis 和 MinIO:别让“能跑”和“好用”差太远
这个项目不是非用 Redis 不可,但我在两个地方用了:
- 登录验证码:把验证码存到 Redis,设置 5 分钟过期,比存 Session 更方便管理。
- 登录 token:用 Redis 存登录用户的 token,做到分布式环境下也能统一校验。
配置很简单:
spring: redis: host: localhost port: 6379 password: database: 0 timeout: 3000ms如果本地没装 Redis,代码里要做降级处理,或者在启动时判断连接失败不影响主流程。
MinIO 也一样。巡查时需要上传现场照片,摊贩登记也需要上传身份证照片。一开始图省事直接存本地磁盘,后来发现换服务器时文件全都要迁移,太麻烦。后来接入了 MinIO,后端上传代码本质上就是四步:建 bucket、配 endpoint、初始化客户端、putObject。
MinioClient client = MinioClient.builder() .endpoint("http://127.0.0.1:9000") .credentials("minioadmin", "minioadmin") .build(); boolean exists = client.bucketExists(BucketExistsArgs.builder().bucket("vendor").build()); if (!exists) { client.makeBucket(MakeBucketArgs.builder().bucket("vendor").build()); } client.putObject( PutObjectArgs.builder() .bucket("vendor") .object("photo/" + fileName) .stream(inputStream, size, -1) .contentType("image/jpeg") .build());接入 MinIO 后,前端拿到的是文件访问 URL,和业务数据库完全解耦。以后哪怕系统换服务器,只要对象存储的数据还在,照片就不会丢。
2.4 前端方案:Thymeleaf 还是 Vue
这个选择要看交付场景。如果项目是给街道内部使用,使用人数不超过几十人,那 Spring Boot 集成 Thymeleaf 加 Bootstrap 是最省事的,部署时一个 jar 包全搞定,不需要单独维护前端工程。
如果是毕业设计展示,或者以后要扩展外卖点单类功能,我建议用 Spring Boot + Vue 前后端分离。我这次用的是 Vue 2 + Element UI 加后端接口,理由有两点:
- 接口文档用 springdoc-openapi 生成,前后端联调方便。
- 后续移动端巡查小程序可以直接调同套接口,不用重新写后端。
无论选哪种,后端接口设计都要考虑跨域问题。前后端分离时最容易遇到跨域报错,后面我会专门讲。
3. 数据库设计与工程搭建
3.1 核心表结构不能只靠一个摊贩表
很多人做摊贩管理,数据库里就只有一张“摊贩表”,加了几个字段就完事。这样后面做巡查和缴费时会非常痛苦。我建议至少要有下面这七张表:
sys_user:系统用户表,保存账号、密码、角色、状态。vendor:摊贩档案表,保存基本信息、位置、状态。vendor_license:电子牌照表,记录证照编号、有效期。attachment:附件表,统一存照片、文件。inspection_record:巡查记录表,关联摊贩、巡查员、结果和整改说明。fee_record:缴费记录表,记录每次缴费的金额和周期。complaint:投诉工单表,记录投诉内容、状态、处理结果。
其中vendor表我这样设计:
CREATE TABLE `vendor` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `vendor_no` VARCHAR(32) NOT NULL COMMENT '摊贩编号', `name` VARCHAR(64) NOT NULL COMMENT '摊贩姓名', `id_card` VARCHAR(32) DEFAULT NULL COMMENT '身份证号', `phone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话', `biz_type` VARCHAR(32) DEFAULT NULL COMMENT '经营类型', `stall_location` VARCHAR(128) DEFAULT NULL COMMENT '摊位位置描述', `lat` DECIMAL(10,6) DEFAULT NULL COMMENT '纬度', `lng` DECIMAL(10,6) DEFAULT NULL COMMENT '经度', `status` TINYINT DEFAULT 1 COMMENT '状态:0禁用 1正常', `create_time` DATETIME DEFAULT NULL, `update_time` DATETIME DEFAULT NULL, `deleted` TINYINT DEFAULT 0 COMMENT '逻辑删除', PRIMARY KEY (`id`), UNIQUE KEY `uk_vendor_no` (`vendor_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='摊贩档案表';这里有两个细节值得注意:一是保存经纬度,后面巡查人员可以在手机端做“附近摊贩”检查;二是所有表都保留deleted字段,用逻辑删除而不是物理删除,因为街道管理人员会经常误操作,逻辑删除还能留痕迹。
牌照表的核心字段是证照编号和有效期,我建议和vendor分开。因为一个摊贩的经营范围或摊位位置可能会变,每次变更还要保留历史记录,如果直接改摊贩表,历史就丢了。
3.2 工程初始化和关键配置
创建 Spring Boot 项目时,我建议直接手动编辑pom.xml,不要完全依赖 IDEA 向导,否则版本容易乱。关键依赖如下:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>com.google.zxing</groupId> <artifactId>javase</artifactId> <version>3.5.2</version> </dependency> </dependencies>application.yml里比较关键的配置是这些:
server: port: 8080 servlet: context-path: /api spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/vendor_mgmt?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: map-underscore-to-camel-case: true 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 springdoc: api-docs: path: /v3/api-docs swagger-ui: path: /doc.html接口路径统一带上/api前缀,可以避免静态资源和接口冲突。以后部署时如果要加 Nginx 反向代理,也只要转发/api一个路径。
3.3 代码自动生成,但要做取舍
MyBatis Plus 的代码生成器能根据数据库表生成实体类、Mapper、Service、Controller,这个工具对提高效率很有用。我用的是MyBatisPlusGenerator,生成完会做两件事:
- 把生成的 Controller 改造成“返回统一结果格式”的写法。
- 给实体类的
LocalDateTime字段自动填充create_time和update_time。
统一返回格式非常重要,不然前端处理总是不一致。我定义了这样一个类:
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> ok(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> fail(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }后面所有 Controller 都返回Result,前端就能统一拦截错误。
4. 核心功能实现与避坑要点
4.1 摊贩建档与二维码牌照生成
摊贩登记功能是基础。保存摊贩信息前要做两件事:生成唯一摊贩编号,以及在审核通过后生成二维码。
生成编号时我不喜欢用数据库自增主键直接当编号,因为容易暴露总量。更稳的做法是“日期 + 序号”:
public String genVendorNo() { String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); int seq = vendorMapper.selectCount(new LambdaQueryWrapper<Vendor>() .likeRight(Vendor::getVendorNo, date)) + 1; return "V" + date + String.format("%04d", seq); }二维码生成用 ZXing:
public BufferedImage createQrCode(String content) throws WriterException { int width = 300; int height = 300; Map<EncodeHintType, Object> hints = new HashMap<>(); hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); hints.put(EncodeHintType.MARGIN, 1); BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints); return MatrixToImageWriter.toBufferedImage(bitMatrix); }二维码内容不要直接拼一个“身份证号”或“手机号”进去。我在实际项目里用的是带签名参数的 URL:
https://manage.example.com/vendor/detail?id=1001&sign=md5(id + secret)扫码后前端先校验sign,再显示摊贩详情。这样可以防止别人遍历 ID 获取全部摊位数据。如果只是校园级演示,不做签名也能跑,但要有这个意识。
4.2 巡查打卡:怎么实现“附近摊贩”
巡查队员到现场后,需要立刻知道这个摊位和登记的信息是否吻合。我做了两个能力:
- 通过摊贩编号或扫描二维码直接查出摊位信息。
- 自动定位后显示“附近 50 米内”的摊贩列表。
“附近摊贩”用 MySQL 的经纬度距离计算就能实现,不需要引入 GeoHash。核心 SQL 是这样的:
<select id="selectNearby" resultType="com.example.entity.Vendor"> SELECT id, vendor_no, name, stall_location, lat, lng, (6371000 * acos( cos(radians(#{lat})) * cos(radians(lat)) * cos(radians(lng) - radians(#{lng})) + sin(radians(#{lat})) * sin(radians(lat)) )) AS distance FROM vendor WHERE status = 1 AND deleted = 0 HAVING distance < #{distance} ORDER BY distance </select>这里特别提醒:在 MyBatis XML 中,小于号<必须转义成<,或者把整段 SQL 包在<![CDATA[ ... ]]>里,否则会报 XML 解析错误。这个坑我刚开始写的时候踩过一次,当时排查了很久才搞清楚。
如果以后摊贩数量超过几千条,这种实时计算会变慢,可以换成 Redis GEO 或者提前按网格编码。但现阶段足够用了。
巡查记录表建议加一个result字段,记录“正常、警告、整改、取缔”四种结果。当结果不是“正常”时,表单要弹出整改说明,并且关联到摊贩的后续检查计划。
4.3 费用收缴和台账统计
这个模块很容易被做成“缴费记录增删改查”,但其实核心是“欠费计算”。比如一个摊位每月管理费 200 元,缴费周期是 2024-01 到 2024-03,那系统在 4 月 1 日之后要自动标记为欠费。
欠费不能靠扫描全表,我在fee_record表里直接设计period_start、period_end两个字段,查询时用“当前时间大于period_end且没有对应缴费状态”的条件过滤。更实用的做法是维护一张摊位费用配置表,再把“应缴周期”和“实缴记录”做差。
我来举个例子,摊贩 A 的应缴周期是每月 1 日到月底,每月 200 元。缴费台账按月统计时:
public List<FeeSummaryVO> summaryByMonth(String month) { // 查询该月所有应缴摊贩 List<Vendor> vendors = vendorService.list(); // 查询该月已有缴费记录 List<FeeRecord> records = feeRecordService.list(new LambdaQueryWrapper<FeeRecord>() .likeRight(FeeRecord::getPeriodStart, month)); // 在内存中做差集,得到欠费列表 Map<Long, FeeRecord> recordMap = records.stream() .collect(Collectors.toMap(FeeRecord::getVendorId, Function.identity())); return vendors.stream() .filter(v -> !recordMap.containsKey(v.getId())) .map(...) .collect(Collectors.toList()); }摊贩总数少时,内存计算完全没问题。如果数量大了,再用 SQLLEFT JOIN做。这个模块的关键不是技术,而是业务状态展示:已经缴费的显示“已缴”,未缴的显示“欠费”,超过三个月的标红。
4.4 投诉工单状态机
投诉模块我设计了四个状态:
PENDING:待受理。PROCESSING:处理中。DONE:已办结。REVISITED:已回访。
这里最怕的就是状态乱跳,比如从“待受理”直接跳到“已办结”。我用了枚举加状态校验:
public void process(Long complaintId) { Complaint complaint = getById(complaintId); if (complaint.getStatus() != ComplaintStatus.PENDING) { throw new BizException("当前状态不允许受理"); } complaint.setStatus(ComplaintStatus.PROCESSING); complaint.setAcceptTime(LocalDateTime.now()); updateById(complaint); }只有创建工单的入口能直接把状态设为PENDING,后面的每一个流转都是一个独立方法。这样日志里能清楚看到是谁、在什么时间、把工单状态改成了什么。街道处理投诉时,最在意的就是“过程留痕”,不要省这一步。
5. 常见问题排查实录
这个部分我整理了项目开发中真实遇到的高频问题,直接按“现象、原因、处理办法”列表展示,后面再补充几个典型调试过程。
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 项目启动报 dataSource 相关错误 | 没引入 MySQL 驱动或spring.datasource配置不对 | 检查pom.xml是否有mysql-connector-java,检查数据库地址、账号 |
| Redis 启动时连接被拒 | Redis 服务没启动或端口不是 6379 | 本地先启动redis-server,检查 yml 配置 |
接口返回的LocalDateTime是一串数字 | Jackson 没有正确序列化日期 | 配置spring.jackson.date-format,或给字段加@JsonFormat |
| 前端请求提示跨域 | 后端没有允许跨域 | 添加 CORS 过滤器或@CrossOrigin |
| MyBatis Plus 分页查不到第二页数据 | 没配置分页插件 | 新建MybatisPlusInterceptor并添加PaginationInnerInterceptor |
| 二维码扫码中文乱码 | 二维码内容里的中文没有 URL 编码 | 生成二维码前对参数做URLEncoder.encode |
| 部署到服务器后图片无法访问 | 图片上传到了本地磁盘,不在服务路径内 | 使用 MinIO 或配置静态资源映射 |
5.1 分页插件没有生效
这个非常典型。MyBatis Plus 的分页功能不是加依赖就能用,必须显式配置分页插件。
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }如果漏掉这段配置,Page对象能返回数据,但total永远是 0,前端分页就乱了。
5.2 跨域配置比想象中要早
前后端分离开发时,前端跑在 8081 端口,后端跑在 8080 端口,请求一进来就被浏览器拦截。我在项目里加了一个全局 CORS 配置:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }注意allowCredentials(true)时,allowedOrigins("*")会有问题,用allowedOriginPatterns("*")才能同时支持携带 Cookie 的跨域请求。这也是个小坑。
5.3 Spring Boot 版本兼容性排查
如果你启动的是 Spring Boot 3.x 项目,引入 MyBatis Plus 要用mybatis-plus-spring-boot3-starter,而不是mybatis-plus-boot-starter。我见过很多同学项目里同时引入了两个 starter,启动时出现一堆 Bean 冲突。
统一依赖版本的办法是使用 Maven 的dependencyManagement,或者直接以 Spring Boot 父 pom 版本为准,不要手动指定各个组件版本。
6. 部署、运维和二次开发的建议
6.1 用 Docker 部署 Spring Boot 项目
现在部署 Spring Boot 项目最省心的方式是打 jar 包后用 Docker 跑。先执行mvn clean package -DskipTests,然后在项目根目录写一个 Dockerfile:
FROM openjdk:8-jre-alpine COPY target/vendor-mgmt.jar /app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app.jar"]构建镜像并启动:
docker build -t vendor-mgmt . docker run -d --name vendor-mgmt -p 8080:8080 \ -e SPRING_DATASOURCE_URL="jdbc:mysql://host.docker.internal:3306/vendor_mgmt?useUnicode=true&characterEncoding=utf8" \ -e SPRING_DATASOURCE_USERNAME="root" \ -e SPRING_DATASOURCE_PASSWORD="your_password" \ vendor-mgmt这里用host.docker.internal是因为容器内部访问宿主机 MySQL 时,不能直接用127.0.0.1。这个问题在本地开发机上最容易遇到,部署到服务器后反而简单,直接用内网数据库地址即可。
6.2 前后端分离后的 Nginx 配置
如果前端是 Vue 打包后的静态文件,我一般用 Nginx 来统一入口:静态文件由 Nginx 处理,/api开头的请求反向代理到 Spring Boot。
server { listen 80; server_name manage.example.com; location / { root /opt/vendor-web; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里要注意proxy_pass http://127.0.0.1:8080;末尾不要加/,否则后端收到的路径会丢掉/api前缀。Spring Boot 里配置了context-path: /api的话,最好保持路径原样转发。
6.3 后续还能扩展什么
这个项目如果后续要往更实用的方向走,可以加三个能力:
- 摊贩自己的小程序端,扫码查看缴费记录、申请变更摊位位置。
- 基于规则的自动预警,比如同一摊贩一个月内被投诉超过三次,自动推送重点检查任务。
- 报表可视化,把巡查覆盖率、按期缴费率、投诉办结率做成图表。
这些扩展都建立在最基础的数据模型之上,所以现在设计表的时候不要偷懒,多留几个通用字段(如remark、ext等),以后加需求会轻松很多。
最后再分享一个我个人的实际体会:做这种管理类系统,技术永远是次要的,真正的难度是“把业务状态理清楚”。Spring Boot 解决的是工程化问题,MyBatis Plus 解决的是数据访问效率问题,而你能不能把摊贩从登记到投诉归档的完整闭环串起来,才决定项目能不能真正落地。开发过程中遇到慢的问题不要急着加缓存,先看是不是查询逻辑有问题;遇到版本兼容问题不要硬跳版本,稳定跑起来的方案就是好方案。