简介:这是一套基于Web的智慧社区设计与实现的计算机毕业设计资源包,面向需要完成毕设或学习SpringBoot与Java Web开发的学生和开发者,围绕社区管理、用户服务、数据处理等核心功能提供闭环方案。包内共929个文件,压缩后约20.98MB;178个Java源文件、60个Vue组件、153个JS文件与58个HTML文件构成前后端主体,162个SVG、44个CSS管理界面表现,81张JPG与79张GIF多为页面效果和功能截图,SQL脚本描述数据库结构,3个BAT脚本简化安装、构建与运行流程。项目文档涵盖开题报告、论文说明与部署提示,可帮助理解需求分析、系统设计、测试结果及数据表关联;源码直观展现用户管理、社区服务等模块的SpringBoot实现细节,配套配置文件与工程文件便于直接导入开发环境调试。已有17人学习下载,对毕业设计选题、系统改造或框架实践均具参考价值。
1. 拿到基于web的智慧社区springboot源码包,先弄清楚它比普通管理系统多在哪
打开 springboot076 基于 web 的智慧社区设计与实现(文档+源码) 这个压缩包时,大多数人第一反应是找后端 Java 代码,但真正决定项目上限的是数据模型和业务边界。智慧社区在毕设里算是烂大街选题,烂大街也说明它的形态是标准的:楼栋、房屋、业主、物业、缴费、报修、访客、公告,再加一套平台/物业/业主三级角色。这套东西跑通不难,难的是把账单幂等、工单状态机、访客时间校验这些细节做对。下面按我做交付的顺序拆:先定数据模型,再定工程结构,然后写核心链路,最后聊排查和验收。
2. 智慧社区的业务模块与数据模型:从楼栋树到账单状态怎么落表
我拿到这类压缩包,第一步不是看代码,而是先打开数据库脚本看表。为什么?因为 Spring Boot 代码是套路化的,controller/service/mapper 翻来覆去就那些写法,反而是表结构直接暴露了设计者对业务的理解深度。智慧社区听上去大,落地成表其实只有七张核心表,先把它们的关系画清楚,后面写接口就是往里面填数据。
2.1 业务模块全景:业主、房产、缴费、报修、访客、公告
先按模块把业务边界画出来,再动手建表。下面这张表是我习惯用的模块划分方式,按“业务对象 + 典型交互”来列,而不是按页面来列:
| 模块 | 核心对象 | 典型交互 |
|---|---|---|
| 房产管理 | 楼栋 Building、房屋 House | 物业录入楼栋,绑定业主,房屋状态从空置变为已入住 |
| 缴费管理 | 账单 PaymentOrder | 每月批量生成账单,业主在线缴费,财务对账 |
| 报修管理 | 报修单 RepairOrder | 业主提交报修,维修工接单,状态流转,完成后评价 |
| 访客管理 | 访客记录 Visitor | 业主预约访客,门卫核销,访客离开后自动过期 |
| 公告管理 | 公告 Notice | 物业发布公告,业主在首页查看滚动列表 |
这张表最重要的作用是定边界。很多翻车项目就是边界没定清楚,把访客记录塞进缴费表,把公告字段挂到用户表上,最后接口越写越拧巴。智慧社区和普通增删改查系统的区别在于:房产有层级关系,账单有状态,报修有状态机,访客有时间和状态双重约束。这四个点决定了它不是一个无脑 CRUD 项目,这也是它适合拿来练手的原因。
2.2 核心表结构:房产树和订单状态字段这么定
建表时我会把房子相关表拆成 building 和 house 两张表,因为一个楼栋下有多个房屋,这是天然的一对多关系。房屋通过 owner_id 和用户表关联,入住前为空,入住后写入业主 ID。下面这段 DDL 是我常用的最小可运行版本:
CREATE TABLE building ( id BIGINT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(32) NOT NULL COMMENT '楼栋编号', name VARCHAR(64) NOT NULL COMMENT '楼栋名称', address VARCHAR(128) COMMENT '楼栋地址', deleted TINYINT DEFAULT 0 COMMENT '逻辑删除:0正常 1已删除' ); CREATE TABLE house ( id BIGINT PRIMARY KEY AUTO_INCREMENT, building_id BIGINT NOT NULL COMMENT '所属楼栋', unit VARCHAR(16) COMMENT '单元号', room VARCHAR(16) COMMENT '房间号', area DECIMAL(10,2) COMMENT '建筑面积(平方米)', owner_id BIGINT COMMENT '绑定业主用户ID', status TINYINT DEFAULT 0 COMMENT '0空置 1入住' );这里有两个细节建议直接抄走:一是所有表都带逻辑删除字段 deleted,交付后运维误删数据时没有后悔药,逻辑删除至少能给恢复留条路;二是 area 用 DECIMAL 不要用 DOUBLE,涉及账单金额计算时,DOUBLE 的浮点误差会让你对账对到怀疑人生。房产树关系在 service 层处理,不要为了省事把楼栋和单元拼成一个大字符串字段,后面做统计、按楼栋筛选时会非常痛苦。
账单表是业务的核心,我单独再给一张更完整的 DDL。这里有一个关键设计:账单月份 bill_month 用 VARCHAR(7) 存“2024-06”,不用 DATETIME。原因很简单,日期类型在跨时区环境下容易被 JDBC 驱动加上 8 小时偏移,而字符串月份永远不会:
CREATE TABLE payment_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, house_id BIGINT NOT NULL COMMENT '房屋ID', bill_month VARCHAR(7) NOT NULL COMMENT '账单月份,如2024-06', amount DECIMAL(10,2) NOT NULL COMMENT '应缴金额', status TINYINT DEFAULT 0 COMMENT '0待缴 1已缴 2逾期', created_at DATETIME COMMENT '生成时间', paid_at DATETIME COMMENT '缴费时间', UNIQUE KEY uk_house_month (house_id, bill_month) );注意最后那个联合唯一索引,它是在数据库层面防重复账单的硬兜底。很多项目只在代码里判断有没有重复,但高并发下两个请求同时插数据,代码判断会全部通过,唯一索引才是靠谱的底裤。报修单、访客表也结构类似,只是状态字段含义不同,我一般会在枚举类里把每个状态的中文含义写清楚,这个稍后在第 4 章展开。
2.3 一套用户表加 type 字段,还是拆三张表?
权限模型是这个项目最容易过度设计的地方。常见做法有两种:一张 user 表加 type 字段区分 0 平台管理员、1 物业、2 业主;或者拆分 owner、property_worker、admin 三张表。我一般会选前者。原因很简单:智慧社区的用户量级就是一个小区的几千人,单表足够支撑,而且登录认证时可以一次查询拿到用户完整信息,不用三张表轮询。type 字段带来的代码侵入也不大,JWT 里带上 userId 和 userType 就够了。
真正需要下功夫的是数据权限:业主登录后只能看到自己房屋的账单和报修记录,物业能看整个小区的数据。这里的实现有个坑,很多人在 service 层用 if 判断角色后拼 SQL,拼着拼着就拼出了 SQL 注入。我习惯先把当前用户的房产范围查出来,再用 in 或 eq 条件去圈定数据:
// 业主只看自己名下的账单 LambdaQueryWrapper<PaymentOrder> wrapper = new LambdaQueryWrapper<>(); if ("owner".equals(currentUserType)) { List<Long> houseIds = houseMapper.selectList( new LambdaQueryWrapper<House>() .eq(House::getOwnerId, currentUserId)) .stream().map(House::getId).toList(); wrapper.in(PaymentOrder::getHouseId, houseIds); }注意不要直接用inSql("house_id", "select id from house where owner_id = " + currentUserId)这种方式,字符串拼接用户输入是标准 SQL 注入写法。先查列表再 in,虽然多一次查询,但安全性和可读性都好得多。这套“单用户表 + type 字段 + 数据权限由 service 层控制”的方案,能覆盖智慧社区 90% 的验收场景,等到业务量真的涨到需要严格 RBAC,再迁移也不迟。
3. Spring Boot工程结构与技术选型:版本、目录、配置文件一次说清
这一章解决的是“项目能跑”和“项目能改”之间的差距。很多压缩包里的代码跑起来没问题,但改一个需求要翻三个小时文件,问题就出在选型和目录结构上。我把这块拆成三个问题:用哪个 Spring Boot 版本,包怎么分,配置怎么设。这三个问题定了,后面写代码就是往框架里填。
3.1 技术选型:Spring Boot 2.7.x 配合 MyBatis-Plus 最稳
选型的第一原则是不要追新。springboot 版本太高反而会带来一串麻烦:Spring Boot 3.x 把 javax 换成 jakarta,很多老教程、老依赖、老代码直接编译不过。如果不需要虚拟线程这类新特性,我建议用 2.7.18,它是 2.x 的最后一个版本,网上遇到的报错几乎全部有人踩过并有解决方案。持久层用 MyBatis-Plus,不是因为它跑得比 JPA 快,而是它的 LambdaQueryWrapper 和分页插件能省大量样板代码,对动手经验不足的开发者更友好。
下面是裁剪过的 pom 核心依赖,去掉注释后可以直接用:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </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.5</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>几个依赖参数说明:mysql-connector-j 是 8.x 驱动的新坐标,旧写法 mysql-connector-java 在 Spring Boot 2.7 下也能用,但建议直接用新的。jjwt 用 0.9.1 而不是 0.11 系列,后面生成 token 的 API 名是Jwts.builder(),网上 90% 的教程对应这个版本,换成 0.11 后类名和方法名全变了,新手容易卡在编译报错上。Redis 在这里不是必须,但公告列表、验证码这类热点数据放 Redis 是常见做法,后续扩展也顺手。
3.2 工程目录按 controller/service/mapper 分层:一个包名对应一个业务边界
Spring Boot 项目结构其实就一句话:按职责分包,不要按页面分包。按页面分包会出现adminController、ownerController、loginController这种类名,看似清晰,但等缴费和报修都要用同一个校验逻辑时,你会在三个 controller 里复制三遍代码。我习惯的分层是这样的:
com.example.smartcommunity ├── config # JWT拦截器、MyBatis-Plus分页插件、CORS跨域配置 ├── controller # 只做参数接收、调用service、返回统一结果R ├── service # 业务规则、事务控制、状态流转 │ └── impl ├── mapper # 继承BaseMapper的接口,复杂查询才写XML ├── entity # 与数据库表一一对应的实体类 ├── dto # 前端传入的请求参数对象 ├── vo # 返回给前端的响应对象 ├── common # R统一返回体、BizException业务异常、常量定义 └── utils # JwtUtil、DateUtils等工具这个结构最核心的约束在 controller:里面不允许出现业务逻辑。一个合格的 controller 方法就是“接收参数、调 service、把结果塞进 R 返回”三步。新手最常见的问题是把状态判断写在 controller 里,接口确实能跑,但报修状态流转分散在三四个方法里,改需求时想摔键盘。service 层负责事务和业务规则,mapper 层只做数据访问,职责边界清楚了,排查问题只看对应层就行。
另一个细节是实体类字段和数据库列名的映射。建表时我习惯用下划线命名(bill_month),Java 实体写成驼峰(billMonth),然后在配置里把 MyBatis-Plus 的map-underscore-to-camel-case打开,这样 CRUD 不用写一行 resultMap,省下的时间够写一个子模块。
3.3 配置文件三件套:数据源、Redis、分页插件一个都不能少
压到最精简,application.yml 里至少要配好三块:数据源、Redis、MyBatis-Plus 行为。下面这份配置是我常用的基础模板,注释里标了我踩过的坑:
server: port: 8080 tomcat: max-swallow-size: 10MB # 不设置的话,大图片上传会被Tomcat二次拦截 spring: datasource: url: jdbc:mysql://localhost:3306/smart_community?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: root redis: host: localhost port: 6379 mybatis-plus: 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数据源 URL 上的serverTimezone=Asia/Shanghai必须写,不然账单时间差 8 小时的坑在后面等着。max-swallow-size是另一个隐藏坑:Spring 的 multipart 限制了上传大小,但请求体被 Tomcat 先读一遍,这里不调大,前端传个证件照就会报org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException,而且这个错误在 Spring 层很难拦截到。log-impl开发阶段一定要开,它能让你在控制台看到每条 SQL 的完整参数,排查 N+1 查询和慢 SQL 全靠它,但打包交付前记得关掉或改成 slf4j。
分页插件是一个独立的 Bean,不配它的话 MyBatis-Plus 的 selectPage 返回的 total 永远是 0。很多项目页面显示“共 0 条”就是这个原因,配套的拦截器代码如下:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }PaginationInnerInterceptor的参数里 DbType 要和你用的数据库一致,写错了 MySQL 分页语法也能跑,但换库的时候会出问题。到这里工程骨架就齐了,接下来进核心代码。
4. 核心业务代码落地:缴费、报修、访客三条主链路的实现与边界
前面几章把表和框架定好了,这一章写真正的业务逻辑。智慧社区的功能列表看着多,但衡量一个实现好坏,就看三条链路:缴费有没有幂等,报修状态能不能受控流转,访客时间有没有被校验。这三条链路写扎实了,公告、车位、投诉这些功能就是往里加 CRUD。
4.1 账单按月生成:幂等和事务一个不能少
账单生成是每月 1 号的定时任务,点击“生成本月账单”后要给所有已入住房屋创建缴费单。这里最容易翻车的点是重复点击:按钮没做 loading,用户连点两下,账单生成了两份,业主一看欠两倍费用,物业电话被打爆。所以在 service 层必须做幂等判断,我在实现里同时用了代码判断和数据库唯一索引两道防线:
@Service public class PaymentOrderServiceImpl extends ServiceImpl<PaymentOrderMapper, PaymentOrder> implements PaymentOrderService { @Transactional @Override public void generateMonthBills(List<Long> houseIds, String month, BigDecimal unitPrice) { for (Long houseId : houseIds) { Long count = baseMapper.selectCount( new LambdaQueryWrapper<PaymentOrder>() .eq(PaymentOrder::getHouseId, houseId) .eq(PaymentOrder::getBillMonth, month)); if (count != null && count > 0) { log.warn("房屋{}的{}月账单已存在,跳过", houseId, month); continue; } House house = houseMapper.selectById(houseId); if (house == null || house.getOwnerId() == null) { continue; // 未绑定业主的房屋不生成账单 } PaymentOrder order = new PaymentOrder(); order.setHouseId(houseId); order.setBillMonth(month); order.setAmount(unitPrice.multiply(house.getArea())); order.setStatus(0); this.save(order); } } }方法上加了@Transactional,生成多套账单时只要中间有一户失败,全部回滚,不会出现“前三户有账单、后两户没有”的脏数据。unitPrice.multiply(house.getArea())是单位面积单价乘建筑面积,这里必须用 BigDecimal 的 multiply,不能直接用house.getArea() * unitPrice做浮点运算。save方法来自 MyBatis-Plus 的 ServiceImpl,本质是调 baseMapper.insert。幂等判断除了代码里的 selectCount,更硬的兜底是 2.2 里那个联合唯一索引,两个请求同时到达时,索引会拒绝第二条插入并抛 DuplicateKeyException,把这个异常加进全局异常处理器返回“重复提交”提示即可。
4.2 报修工单状态机:用状态流转表代替一长串 if else
报修工单是智慧社区里状态逻辑最复杂的一块,常见的状态有:0 待接单、1 维修中、2 待评价、3 已完成、4 已取消。很多同学会直接在 service 里写“如果现在是待接单,就能改成维修中和取消;如果正在维修,只能改成待评价……”写五个方法就要重复五遍判断。我一般会把状态流转收敛成一张不可变 Map,类似一个轻量状态机:
public class RepairOrderStatus { private static final Map<Integer, List<Integer>> TRANSITIONS = new HashMap<>(); static { // 待接单 -> 维修中 / 取消 TRANSITIONS.put(0, Arrays.asList(1, 4)); // 维修中 -> 待评价 TRANSITIONS.put(1, Arrays.asList(2)); // 待评价 -> 已完成 TRANSITIONS.put(2, Arrays.asList(3)); } public static boolean canTransit(Integer current, Integer target) { return TRANSITIONS.getOrDefault(current, Collections.emptyList()) .contains(target); } }调用方的逻辑就变成了先查当前状态,再走状态机判断,最后更新。这个设计的价值在于所有“能不能转”的规则集中在一张表里,以后业务加一个“已拒绝”状态,只改静态块就够了,不用满项目搜 if。在写不要用 enum 实现,交付后运营方加状态要改代码重新编译,Map 加个 put 同样要编译,所以选哪种无所谓,关键是规则要有单一来源。
@Transactional public void changeStatus(Long orderId, Integer targetStatus, Long operatorId) { RepairOrder order = getById(orderId); if (!RepairOrderStatus.canTransit(order.getStatus(), targetStatus)) { throw new BizException("当前状态不允许变更为: " + targetStatus); } // 数据权限:业主只能取消自己的报修单 if (targetStatus == 4 && !order.getOwnerId().equals(operatorId)) { throw new BizException("只能取消自己提交的报修单"); } order.setStatus(targetStatus); updateById(order); }这里有两个边界必须守住:一是状态校验要在事务最前面,不能先更新再判断,不然直接污染数据;二是数据权限不能只依赖前端隐藏按钮,后端接口要再查一次,比如取消报修必须校验当前操作人就是报修提交人。判断逻辑里order.getOwnerId().equals(operatorId)要记得判空,ID 为 null 时会抛空指针。
4.3 访客预约与核销:时间校验和状态核销
访客模块相对简单,但有一个高频投诉点:业主填的来访时间晚于离开时间,系统照样保存,门卫按访客记录放行时发现人还没来,记录已经过了有效期。前端表单日期选择器能拦一部分,后端接口必须再拦一次。我用 JSR 303 的 @Valid 注解配合手动比较,代码如下:
@PostMapping("/visitor") public R<Void> registerVisitor(@RequestBody @Valid VisitorRequestDTO dto) { DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); LocalDateTime visitTime = LocalDateTime.parse(dto.getVisitTime(), fmt); LocalDateTime expireTime = LocalDateTime.parse(dto.getExpireTime(), fmt); if (visitTime.isAfter(expireTime)) { return R.fail("来访时间不能晚于离开时间"); } if (expireTime.isBefore(LocalDateTime.now())) { return R.fail("离开时间不能早于当前时间"); } Visitor visitor = new Visitor(); BeanUtils.copyProperties(dto, visitor); visitor.setStatus(0); // 0待来 1在访 2已离开 3已过期 visitorService.save(visitor); return R.ok(); }DTO 上用了@NotBlank @Pattern这类注解做字段级校验,比如手机号正则、车牌号可空,方法里做的是跨字段逻辑校验,因为注解很难表达“visitTime 早于 expireTime”这种关系。两个校验都通过后再落库,状态默认 0。门卫核销时调另一个接口把状态改成 1 或 2,这里要注意“已过期”这个状态不一定要人工触发,可以用定时任务把当前时间大于 expire_time 且状态还是 0 的记录更新成 3,这样超时不处理也能自动清理。BeanUtils.copyProperties 在字段名一致时好用,但 visitTime 从字符串到 LocalDateTime 的转换它做不了,所以我在上面手动 parse 了。
5. 常见问题排查与避坑:让智慧社区项目从能跑变成能交付
项目跑通和能交付之间大概隔着十条已知的坑。我按“现象 → 原因 → 解决”的格式写四条最常遇到、并且搜索引擎很难一次解决的,这四条我在不同项目里都踩过,不是凭空的玄学。
5.1 账单时间显示差 8 小时,凌晨缴费记录到了下午
现象:业主凌晨 0 点缴了费,后台账单列表显示缴费时间是当天 8 点。打电话给物业,物业截图发过来,时间对不上。
原因:MySQL 连接串里没有指定 serverTimezone,JDBC 驱动用默认时区解析 DATETIME,和 Linux 服务器的 UTC 时区一叠加,就出现了 8 小时偏移。这类问题不是数据存错了,而是读取时被转换了。
解决:数据源 URL 加上serverTimezone=Asia/Shanghai,同时把 MyBatis-Plus 或 Jackson 的时间格式统一成yyyy-MM-dd HH:mm:ss,在 application.yml 里设置spring.jackson.date-format和time-zone: GMT+8。如果项目已经跑了一段时间,只需要重新保存一次实体类,时间显示就会恢复正常,不用改数据库数据。
5.2 分页查询 total 一直为 0,第二页查不出数据
现象:前端列表翻页时显示“共 0 条”,但数据库里明明有几十条数据。有时第一页能出数据,点第二页就空白。
原因:没有配置 MybatisPlusInterceptor 的 PaginationInnerInterceptor,selectPage 方法虽然返回了 Page 对象,但 total 和分页 SQL 都没真正生效。这是 MyBatis-Plus 最常见的翻车点,因为 BaseMapper 里带分页参数的方法默认就能编译通过,不配置拦截器也不报错。
解决:在 config 包里加第 3.3 节那个 MybatisPlusConfig,注册分页拦截器,DbType 指定为 MYSQL。配置后还不行,检查控制台 SQL 日志里有没有LIMIT ?和COUNT(*),如果日志里没有 count 语句,就是拦截器没加载进来,确认 Spring Boot 的包扫描覆盖到了 config 包。
5.3 前端登录后接口全部 401,浏览器控制台报跨域
现象:前端 Vue 项目联调时,所有带 token 的请求都失败,控制台显示CORS policy: No 'Access-Control-Allow-Origin'。用 Postman 测后端接口又是正常的,于是两个人互相甩锅。
原因:浏览器先发了一个 OPTIONS 预检请求,这个请求默认不带 token,被后端的 JWT 拦截器直接拦截返回 401,预检失败就报跨域。Postman 不发预检,所以正常。根因是拦截器没有放行 OPTIONS 请求,CORS 配置也加得靠后。
解决:在拦截器 preHandle 里第一行判断if ("OPTIONS".equalsIgnoreCase(request.getMethod())) return true;,让预检请求直接通过。再在 config 里单独配一个 CorsFilter,不要只加 @CrossOrigin 注解,因为注解作用在单个 controller 上,漏一个接口就翻车一次。配置allowedOriginPatterns("*")和allowedMethods("*"),开发阶段够用,上线前再把允许的域名收紧。
5.4 图片上传提示超过 1MB,业主证件照传不上去
现象:上传身份证照片时后端报MaxUploadSizeExceededException,但代码里明明设置了 max-file-size 为 10MB,前端也确认图片只有几百 KB。
原因:Spring Boot 的 multipart 配置是spring.servlet.multipart.max-file-size,默认 1MB;但内嵌 Tomcat 还有一层server.tomcat.max-swallow-size限制请求体大小,默认 2MB。Spring 的报错能把第一层暴露出来,第二层直接吞掉请求,表现就是传小图也失败。
解决:在 application.yml 里同时调大三个参数:spring.servlet.multipart.max-file-size、spring.servlet.multipart.max-request-size、server.tomcat.max-swallow-size,全部设成 10MB 或更大。如果后端还有一层网关,网关的请求体限制也要同步调,这里少了任何一个都会被卡住三天。
6. 交付前最后一件事:用冒烟测试把三条主链路串起来验收
功能开发完后不要再手动点页面了,页面点击只能覆盖你记得住的路径,而且测完就忘。我在交付前会写一个冒烟测试类,把缴费、报修、访客三条主链路串起来,用 Maven 跑一遍。这个测试类长期保留,每次改动后跑一次,比用 Postman 手工点一个月都可靠。
6.1 用 @SpringBootTest 跑通“缴费→报修→访客”冒烟链路
下面这段测试验证的是“重复生成账单只会保留一份”,这是全项目最容易出真金白银事故的地方,所以必须自动化:
@SpringBootTest @Transactional class CoreLinkSmokeTest { @Autowired private PaymentOrderService paymentOrderService; @Test void generateSameMonthBillShouldBeIdempotent() { List<Long> houseIds = List.of(1L, 2L); // 第一次生成 paymentOrderService.generateMonthBills(houseIds, "2024-06", BigDecimal.TEN); // 模拟按钮重复点击,再次生成 paymentOrderService.generateMonthBills(houseIds, "2024-06", BigDecimal.TEN); long count = paymentOrderService.count( new LambdaQueryWrapper<PaymentOrder>() .eq(PaymentOrder::getBillMonth, "2024-06")); assertEquals(2, count, "两套房各应该只有一份账单"); } }@Transactional加在测试类上,跑完自动回滚,不会污染开发库数据,可以放心反复执行。断言用 assertEquals,第二个参数是期望值,两个房屋各生成一份,所以总数是 2。如果幂等判断失效,第二次生成会把 count 变成 4,测试立刻失败。按同样的思路,可以再写两条:报修工单从待接单改维修中成功、从已完成改待接单失败;访客预约时 visitTime 晚于 expireTime 返回提示而不是保存。三条断言覆盖了全项目最关键的业务规则。
6.2 跑测试前先打开 SQL 日志,跑完再看一眼输出
我有一次交付,功能全对,但业主反馈首页公告加载很慢。一查 SQL 日志,公告列表接口先查公告,再循环查每条公告的发布人信息,标准的 N+1 查询。从那以后我跑冒烟测试前都先确认log-impl: StdOutImpl是开着的,测试跑完顺手翻一下日志,看看有没有循环 SQL 或者不带条件的全表扫描。这个习惯帮我挡住过好几次“功能没问题但一上线就卡”的尴尬。
另一个收尾习惯是检查统一返回结构。controller 所有接口都返回 R 对象,code=200 表示成功,异常统一走全局异常处理器,不要自己随手返回一个 Map。测试里我没有验证这一项,但交付前我会花三分钟全局搜一下new HashMap出现在 controller 里的位置,把它改成 R。这些细节单看不值钱,串起来就是专业交付和作业 demo 的差距。希望帮到你。
本文还有配套的精品资源,点击获取