ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot物业管理系统实战:从Excel台账到业务闭环的完整实现

Spring Boot物业管理系统实战:从Excel台账到业务闭环的完整实现 简介这份资源是一份基于Spring Boot的物业管理系统毕业设计文档面向计算机相关专业学生及需要完成课程设计或论文的开发者帮助解决物业管理信息化场景下的系统设计与实现问题。压缩包内共1个docx文件约849KB内容为完整的论文正文涵盖绪论、开发工具与技术、可行性分析、需求分析与设计、系统实现及测试等章节。文档围绕业主与物业管理员两类权限展开详细描述了基本信息管理、维修管理、费用管理、公告管理、投诉管理及系统管理等核心模块并给出数据库设计与E-R关系图。技术选型上采用Eclipse开发、Spring Boot框架、MySQL数据库与Layui前端可行性分析覆盖技术、操作、经济与法律四个维度。目前已有3143人学习下载适合作为同类课题的参考模板也可用于梳理系统功能划分、权限设计与论文写作思路。1. 从一张 Excel 台账说起物业管理系统到底要解决什么我见过太多小区物业的日常前台一台老电脑桌面躺着七八个 Excel收费一张、报修一张、车位一张、门禁授权又是一张。月底对账靠人肉 VLOOKUP报修单靠微信群截图业主打电话问「我上个月交没交物业费」客服得翻三分钟文件。这套流程不是不能用是撑不住规模——一旦楼栋过千户、工单过百条数据就开始互相打架。用 Spring Boot 做物业管理系统的设计与实现本质是把这套散落的台账收进一个带权限、带状态流转、带审计的后台。它要解决的核心问题就三个数据唯一一户一档不重复不遗漏、流程可追报修从提交到完工每一步有记录、权限可控业主、客服、维修工、管理员看到的东西不一样。适合谁做计算机相关专业的课程设计、毕业设计以及中小物业公司想自建一套轻量内部系统的开发者。它不追求高并发追求的是业务闭环完整、代码结构清晰、能跑起来能演示。这一篇不讲空泛的「系统概述」我按一个能真正落地的顺序拆先定技术选型和数据模型再把核心模块的代码写出来然后讲部署和排错最后给你几个能拉开差距的进阶点。中间会穿插我自己踩过的坑比如 Spring Boot 版本选太高导致依赖打架、工单状态机写成 if-else 地狱这类血泪经验。2. 技术选型与工程骨架为什么是 Spring Boot 而不是别的2.1 选型理由单体优先别一上来就微服务物业管理系统的业务体量绝大多数场景下一台 2 核 4G 的云服务器就够跑。这个量级上微服务是负优化——你会花大量时间在服务注册、配置中心、链路追踪上而真正该打磨的收费计算和工单流转反而没时间写。所以我的建议是单体 模块化分包用 Maven 多模块或者单模块内按domain / service / controller / mapper分层。技术栈我一般这么定层次选型说明框架Spring Boot 2.7.x稳定、生态全、教程多别追 3.x 除非你确定 JDK 17 环境持久层MyBatis-Plus单表 CRUD 几乎零 SQL复杂查询再手写 XML数据库MySQL 8.0事务和窗口函数够用字符集统一 utf8mb4缓存Redis可选存登录 token 和字典缓存量小可以先不上前端Vue 3 Element Plus后台管理模板成熟和 Spring Boot 前后端分离鉴权JWT 拦截器无状态适合前后端分离别用 Session 折腾跨域这里有个高频热搜词「springboot版本太高」值得单独说。很多人图新鲜直接上 Spring Boot 3.2结果发现 JDK 还是 8或者 MyBatis-Plus 版本没跟上启动直接报NoSuchMethodError。版本对齐是第一道坎Spring Boot 2.7.x 配 JDK 8/11Spring Boot 3.x 强制 JDK 17。课程设计阶段2.7.18 是甜点版本依赖最稳。2.2 工程骨架搭建从 start.spring.io 到能跑第一步用官方初始化器生成骨架勾选 Spring Web、MySQL Driver、Lombok。命令行方式# 生成一个 Spring Boot 2.7.18 的 Maven 工程 curl https://start.spring.io/starter.zip \ -d typemaven-project \ -d languagejava \ -d bootVersion2.7.18 \ -d javaVersion8 \ -d groupIdcom.example \ -d artifactIdproperty-mgmt \ -d nameproperty-mgmt \ -d packageNamecom.example.property \ -d dependenciesweb,mysql,lombok \ -o property-mgmt.zip unzip property-mgmt.zip -d property-mgmt这段命令做的事向初始化器请求一个 zip 包参数里锁死了 Spring Boot 版本 2.7.18、Java 8、Maven 构建依赖只勾了 web、mysql、lombok 三个。下载后解压得到标准目录结构。参数说明bootVersion决定父 POM 版本javaVersion决定编译级别这两个必须和你的本地 JDK 一致否则mvn compile会报invalid target release。第二步在pom.xml里补 MyBatis-Plus 和 JWT!-- MyBatis-Plus单表 CRUD 免写 SQL -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- JWT生成和校验登录令牌 -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependencyMyBatis-Plus 3.5.3.1 是兼容 Spring Boot 2.7 的稳定版别用 3.5.4 以上那个开始要求 Spring Boot 3。jjwt 0.9.1 虽然老但 API 简单配合 JDK 8 不需要额外引入 JAXB。第三步application.yml配置数据源和 MyBatis-Plusserver: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/property_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true # 数据库下划线字段自动映射 Java 驼峰 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发期打印 SQL上线关掉map-underscore-to-camel-case这个开关很关键它让owner_name自动映射到ownerName省掉大量Results注解。log-impl在开发期打开能看到每条 SQL排错神器但生产环境一定要注释掉否则日志会被 SQL 淹没。2.3 分包结构让三个月后的自己看得懂我习惯的分包是这样的按职责切而不是按技术切com.example.property ├── common # 统一返回体、异常、常量 │ ├── Result.java │ └── GlobalExceptionHandler.java ├── config # 拦截器、跨域、MyBatis-Plus 配置 ├── controller # 只做参数接收和返回 ├── service # 业务逻辑接口 impl ├── mapper # 数据访问接口 ├── entity # 数据库实体 └── dto # 前端传参和返回对象为什么entity和dto要分开因为实体直接暴露给前端会出两个问题一是密码字段容易被带出去二是前端需要的字段往往比表结构多比如工单列表要带业主姓名但工单表里只有业主 ID。用 DTO 做一层转换安全性和灵活性都上来了。这个习惯在课程设计里可能显得啰嗦但工作后你会感谢自己。3. 数据模型设计五张核心表撑起整个系统3.1 实体关系梳理先画清楚再建表物业管理系统的数据模型核心是「人、房、费、单」四个字。人分业主和员工房是楼栋单元房间费是收费项目和账单单是报修工单。它们的关系是一个业主可以有多套房一套房对应多条账单一条账单关联一个收费项目一个业主可以提多条工单一条工单指派给一个维修工。建表之前先把关系画在纸上能省掉后面无数次改表。我见过有人上来就写代码写到一半发现「车位」和「房间」是多对多回头改表改到崩溃。先建模后编码这是铁律。3.2 建表 SQL五张表 索引-- 业主表一户一档的基础 CREATE TABLE owner ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(32) NOT NULL COMMENT 业主姓名, phone VARCHAR(16) NOT NULL COMMENT 手机号登录账号, password VARCHAR(64) NOT NULL COMMENT BCrypt 加密后的密码, id_card VARCHAR(20) DEFAULT NULL COMMENT 身份证号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_phone (phone) -- 手机号唯一防止重复注册 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 房屋表楼栋-单元-房间三级定位 CREATE TABLE house ( id BIGINT NOT NULL AUTO_INCREMENT, building VARCHAR(16) NOT NULL COMMENT 楼栋号, unit VARCHAR(16) NOT NULL COMMENT 单元号, room VARCHAR(16) NOT NULL COMMENT 房间号, area DECIMAL(8,2) NOT NULL COMMENT 建筑面积算物业费用, owner_id BIGINT DEFAULT NULL COMMENT 关联业主空表示未售, PRIMARY KEY (id), UNIQUE KEY uk_location (building,unit,room) -- 同一位置只能有一套房 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 收费项目表物业费、水费、停车费等 CREATE TABLE fee_item ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(32) NOT NULL COMMENT 项目名称, unit_price DECIMAL(8,2) NOT NULL COMMENT 单价, unit VARCHAR(16) NOT NULL COMMENT 计价单位如元/平米/月, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 账单表每个业主每套房每月一条 CREATE TABLE bill ( id BIGINT NOT NULL AUTO_INCREMENT, house_id BIGINT NOT NULL, fee_item_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL COMMENT 应缴金额, period VARCHAR(16) NOT NULL COMMENT 账期如 2025-01, status TINYINT NOT NULL DEFAULT 0 COMMENT 0未缴 1已缴 2逾期, pay_time DATETIME DEFAULT NULL, PRIMARY KEY (id), KEY idx_house_period (house_id,period) -- 按房和账期查账单 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 工单表报修流程的核心 CREATE TABLE work_order ( id BIGINT NOT NULL AUTO_INCREMENT, owner_id BIGINT NOT NULL, content VARCHAR(255) NOT NULL COMMENT 报修内容, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待派单 1处理中 2已完成 3已取消, handler_id BIGINT DEFAULT NULL COMMENT 维修工 ID, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, finish_time DATETIME DEFAULT NULL, PRIMARY KEY (id), KEY idx_status (status) -- 按状态筛选工单 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数说明金额字段一律用DECIMAL不用FLOAT浮点数算钱会出现0.10.20.30000000000000004这种玄学问题对账时能让你怀疑人生。status用TINYINT存状态码而不是字符串省空间且查询快。索引只建在真正会用来筛选的字段上idx_house_period和idx_status是查询热点其他字段别乱加索引写入会变慢。3.3 实体类与 MyBatis-Plus 注解以工单为例实体类这么写Data TableName(work_order) public class WorkOrder { TableId(type IdType.AUTO) // 主键自增交给数据库 private Long id; private Long ownerId; private String content; private Integer status; private Long handlerId; TableField(fill FieldFill.INSERT) // 插入时自动填充 private LocalDateTime createTime; private LocalDateTime finishTime; }TableName指定表名TableId声明主键策略TableField(fill FieldFill.INSERT)配合一个MetaObjectHandler实现类可以在插入时自动写入创建时间不用每次手动setCreateTime。这个自动填充的配置类Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { // 插入时自动填充 createTime 字段 this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { // 更新时自动填充 updateTime如果实体有该字段 this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }这段代码的作用是拦截 MyBatis-Plus 的插入和更新操作在字段为空时自动填时间。注意strictInsertFill只在字段为 null 时填充如果你手动 set 了值它不会覆盖这个行为符合直觉。4. 核心模块实现登录、账单、工单三条主线4.1 JWT 登录从密码加密到令牌校验登录是整个系统的入口做不好后面全是漏洞。流程是前端传手机号和密码后端查库比对通过后签发 JWT前端存起来后续请求带在 Header 里。密码不能明文存用 BCrypt 加密。Spring Security 的BCryptPasswordEncoder可以单独用不必引入整个 Security 框架// 注册时加密 BCryptPasswordEncoder encoder new BCryptPasswordEncoder(); String hashed encoder.encode(rawPassword); // 每次结果不同但 matches 能验证 // 登录时校验 boolean ok encoder.matches(rawPassword, hashedFromDb);BCrypt 的特点是同一密码每次加密结果不同内置随机盐但matches能正确验证。所以别用equals比对密文一定用matches。签发 JWT 的工具类public class JwtUtil { private static final String SECRET your-256-bit-secret-key-keep-it-safe; private static final long EXPIRE 7 * 24 * 3600 * 1000L; // 7 天 public static String generate(Long ownerId, String phone) { return Jwts.builder() .setSubject(String.valueOf(ownerId)) .claim(phone, phone) // 自定义声明 .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) // 对称签名 .compact(); } public static Long parse(String token) { Claims claims Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); return Long.valueOf(claims.getSubject()); } }参数说明SECRET是签名密钥必须足够长且不能硬编码在代码里——生产环境应该从环境变量或配置中心读。EXPIRE设 7 天是后台系统的常见值太长有安全风险太短用户频繁掉线。HS256是对称加密够用如果要做分布式多服务验签才考虑 RS256 非对称。拦截器校验令牌public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { String token req.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { resp.setStatus(401); return false; } try { Long ownerId JwtUtil.parse(token.substring(7)); // 去掉 Bearer req.setAttribute(ownerId, ownerId); // 塞进请求上下文Controller 直接取 return true; } catch (Exception e) { resp.setStatus(401); // 令牌过期或篡改 return false; } } }把解析出的ownerId放进 request 属性Controller 里用req.getAttribute(ownerId)就能拿到当前登录用户不用每个接口都解析一遍令牌。注意拦截器要注册到WebMvcConfigurer里并排除登录、注册这类白名单路径否则用户根本登不进来。4.2 账单生成批量插入与金额计算账单是物业系统的钱袋子逻辑要经得起对账。每月初给所有已售房屋生成当月物业费账单金额 建筑面积 × 单价。Service public class BillService { Autowired private HouseMapper houseMapper; Autowired private BillMapper billMapper; Autowired private FeeItemMapper feeItemMapper; Transactional(rollbackFor Exception.class) // 任何异常都回滚 public int generateMonthlyBill(String period) { // 1. 查物业费单价 FeeItem fee feeItemMapper.selectByName(物业费); // 2. 查所有已售房屋 ListHouse houses houseMapper.selectList( new LambdaQueryWrapperHouse().isNotNull(House::getOwnerId)); // 3. 逐条生成账单 ListBill bills new ArrayList(); for (House h : houses) { Bill b new Bill(); b.setHouseId(h.getId()); b.setFeeItemId(fee.getId()); // 金额 面积 × 单价保留两位小数 b.setAmount(h.getArea().multiply(fee.getUnitPrice()) .setScale(2, RoundingMode.HALF_UP)); b.setPeriod(period); b.setStatus(0); bills.add(b); } // 4. 批量插入比循环单条插入快十倍以上 return billMapper.insertBatch(bills); } }逻辑说明Transactional保证要么全部生成成功要么全部回滚不会出现生成一半的脏数据。金额计算用BigDecimal的multiply和setScaleHALF_UP是四舍五入符合财务习惯。批量插入用自定义的insertBatchMyBatis-Plus 的saveBatch也行比循环insert快得多——一千条数据循环插入要几秒批量插入几百毫秒。参数说明period格式统一为2025-01方便字符串比较和排序。rollbackFor Exception.class是必须的默认只回滚RuntimeException加上这个才能覆盖所有异常。4.3 工单状态流转用状态机替代 if-else工单有四个状态待派单、处理中、已完成、已取消。状态之间的流转是有规则的——待派单只能派单或取消处理中只能完成已完成和已取消是终态。用 if-else 写会变成这样// 反例if-else 地狱加一个状态就要改一堆地方 if (status 0 action assign) { ... } else if (status 0 action cancel) { ... } else if (status 1 action finish) { ... } // ... 越写越长正确做法是用枚举定义状态和允许的动作public enum OrderStatus { PENDING(0, 待派单), PROCESSING(1, 处理中), FINISHED(2, 已完成), CANCELED(3, 已取消); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } // 定义每个状态允许的下一步 public static boolean canTransfer(int from, int to) { if (from PENDING.code) return to PROCESSING.code || to CANCELED.code; if (from PROCESSING.code) return to FINISHED.code; return false; // 终态不允许再流转 } }Service 里调用public void transfer(Long orderId, int targetStatus) { WorkOrder order workOrderMapper.selectById(orderId); if (!OrderStatus.canTransfer(order.getStatus(), targetStatus)) { throw new BizException(当前状态不允许此操作); } order.setStatus(targetStatus); if (targetStatus OrderStatus.FINISHED.getCode()) { order.setFinishTime(LocalDateTime.now()); } workOrderMapper.updateById(order); }为什么这么写状态流转规则集中在一个方法里加新状态只改canTransferService 逻辑不动。这就是状态机的价值——把散落各处的判断收敛成一张表。热搜词里的「设计模式」在这里体现为状态模式的思想但不必真去写一堆 State 类枚举足够。5. 避坑与排查那些让我加班到凌晨的问题5.1 跨域请求被拦截前端一直报 CORS现象前端 Vue 跑在 5173 端口后端 8080登录接口浏览器控制台报Access-Control-Allow-Origin缺失请求发不出去。原因前后端分离后端口不同浏览器同源策略拦截。很多人以为在 Controller 上加CrossOrigin就行但拦截器返回 401 时不会带跨域头导致预检请求失败。解决用全局跨域配置并且保证拦截器放行 OPTIONS 预检请求Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) // 开发期放开生产改成具体域名 .allowedMethods(GET,POST,PUT,DELETE,OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }同时在拦截器preHandle开头加if (OPTIONS.equals(req.getMethod())) return true;让预检请求直接通过。5.2 时间字段差 8 小时账单账期对不上现象数据库里存的时间是对的但接口返回给前端少了 8 小时或者账期算出来是上个月。原因MySQL 连接串没指定时区JDK 8 的LocalDateTime和数据库DATETIME之间转换用了 UTC。或者 Jackson 序列化时用了默认时区。解决连接串加serverTimezoneAsia/Shanghai并在application.yml里配 Jackson 时区spring: jackson: time-zone: Asia/Shanghai date-format: yyyy-MM-dd HH:mm:ss两处都配上时间才不会飘。5.3 批量插入报 SQL 太长MySQL 拒绝执行现象生成账单时一次插入几千条报Packet for query is too large。原因MyBatis-Plus 的saveBatch默认把一批拼成一条大 SQL超过 MySQL 的max_allowed_packet限制。解决分批插入每批 500 条// 手动分批避免单条 SQL 过大 int batchSize 500; for (int i 0; i bills.size(); i batchSize) { ListBill sub bills.subList(i, Math.min(i batchSize, bills.size())); billMapper.insertBatch(sub); }或者调大 MySQL 的max_allowed_packet但分批更稳妥不依赖数据库配置。5.4 工单并发派单同一个工单被两个维修工抢到现象两个管理员同时点派单工单被指派了两次后一次覆盖前一次。原因查询和更新之间有时间窗口典型的并发问题。解决用乐观锁。工单表加version字段更新时带上版本号UPDATE work_order SET status 1, handler_id ?, version version 1 WHERE id ? AND version ?MyBatis-Plus 有Version注解支持实体字段加注解更新时自动带上版本条件。如果返回影响行数为 0说明被别人改过提示用户刷新重试。5.5 启动报 NoSuchMethodError依赖版本打架现象项目编译通过启动时抛NoSuchMethodError或ClassNotFoundException。原因多半是 Spring Boot 版本和某个 starter 版本不匹配或者引入了两个版本的同一个库。解决用mvn dependency:tree看依赖树找冲突的库。常见的是 MyBatis-Plus 和 Spring Boot 版本错配或者 jjwt 缺少 JAXB 依赖JDK 11 以上需要手动加。锁定版本别让 Maven 自动选。6. 进阶技巧让系统从「能跑」到「好用」6.1 用 AOP 记录操作日志出问题能追溯系统上线后最怕的是「谁把这条账单删了」查不到。用 Spring AOP 加一个操作日志切面在关键方法上打注解自动记录操作人、时间、参数。Aspect Component public class LogAspect { Autowired private LogMapper logMapper; Around(annotation(operLog)) // 拦截带 OperLog 注解的方法 public Object record(ProceedingJoinPoint pjp, OperLog operLog) throws Throwable { long start System.currentTimeMillis(); Object result pjp.proceed(); // 执行原方法 // 方法执行后记录日志 OperationLog log new OperationLog(); log.setModule(operLog.module()); log.setAction(operLog.action()); log.setCost(System.currentTimeMillis() - start); log.setCreateTime(LocalDateTime.now()); logMapper.insert(log); return result; } }逻辑说明Around环绕通知能在方法执行前后插入逻辑pjp.proceed()是真正调用目标方法。注解OperLog(module账单, action生成)标在 Service 方法上切面自动捕获。这样加日志不用改业务代码符合开闭原则。6.2 账单金额的三种校验防止算错钱钱算错是物业系统最严重的事故。我一般加三层校验校验层时机内容生成时批量生成账单面积 0单价 0金额 面积 × 单价支付时用户缴费支付金额 账单金额防止篡改对账时每日定时任务汇总当日账单总额与支付流水比对支付时的校验最关键前端传的金额不可信必须用后端查出的账单金额public void pay(Long billId, BigDecimal payAmount) { Bill bill billMapper.selectById(billId); // 用 compareTo 比较不用 equals因为 BigDecimal 的 equals 会比较精度 if (bill.getAmount().compareTo(payAmount) ! 0) { throw new BizException(支付金额与账单不符); } // ... 更新状态 }注意BigDecimal的equals会连精度一起比1.0和1.00不相等所以金额比较一律用compareTo。这个坑我踩过对账时发现明明金额一样却报错查了半天。6.3 用定时任务自动标记逾期账单每月 15 号之后未缴的账单标记为逾期用 Spring 的ScheduledComponent public class BillSchedule { Autowired private BillMapper billMapper; // 每天凌晨 1 点执行 Scheduled(cron 0 0 1 * * ?) public void markOverdue() { // 把账期在当前月之前且未缴的账单标记为逾期 billMapper.update(null, new LambdaUpdateWrapperBill() .eq(Bill::getStatus, 0) .lt(Bill::getPeriod, currentPeriod()) .set(Bill::getStatus, 2)); } }参数说明cron 0 0 1 * * ?表示每天 1 点执行六个字段分别是秒、分、时、日、月、周。LambdaUpdateWrapper是 MyBatis-Plus 的条件构造器lt是小于set是设置新值。这个任务要在启动类加EnableScheduling才生效。6.4 一个我坚持的习惯接口先写文档再写代码最后说个习惯。我见过太多人上来就写 Controller写到一半发现字段对不上回头改 DTO 改到吐。我的做法是先用 Swagger 注解把接口定义写出来字段名、类型、是否必填都定死再写实现。这样前后端可以并行开发前端拿着文档就能 mock 数据不用等后端写完。ApiOperation(生成月度账单) PostMapping(/bill/generate) public ResultInteger generate( ApiParam(value 账期格式 2025-01, required true) RequestParam String period) { return Result.ok(billService.generateMonthlyBill(period)); }ApiOperation和ApiParam是 Swagger 注解启动后访问/swagger-ui.html就能看到在线文档还能直接测试接口。这个习惯让我少加了无数班前端同事也不再一天问我八遍字段名。做这类系统我的教训是别追求功能多追求每条业务线闭环。一个能完整跑通「注册-登录-查账单-缴费-报修-派单-完工」的系统比十个半成品模块有价值得多。把状态流转、金额计算、权限控制这三块打磨扎实课程设计拿优、小公司上线都够用。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进