Spring Boot, 小程序, MySQL, Redis, 智慧物业,迪飞特科技




前言
去年下半年,我们团队接了一个园区+高校+住宅混合业态的智慧物业项目。甲方一开始提的需求很朴素:让住户能在手机上报修,让管理员能在后台派单。结果聊到第三轮,需求文档膨胀到了十四大模块------巡检、费用、访客、车位、应急、投诉、师傅结算、监管看板,还要一个小程序同时装下住户端和师傅端。
这篇文章写给正在做或准备做物业 SaaS 的后端和全栈同学。我会把工单状态机、巡检任务自动生成、费用批量出账、双端小程序鉴权这几块最容易踩坑的地方拆开讲,代码基于 Spring Boot 3 + MyBatis-Plus + MySQL 8 + Redis,小程序端用原生+云函数那套就不展开了。看完你至少能少走两个月弯路。
问题背景
物业系统的难点从来不是 CRUD,而是状态流转 和权限边界。
我们调研过市面上几套现成产品,普遍存在三个问题。一是工单状态靠一个 status 字段硬编码,1 是待派单、2 是已派单、3 是已办结,改一次需求就得改代码。二是巡检任务靠定时任务全表扫描,园区有 800 多个巡检点,扫一次数据库 CPU 直接飙到 70%。三是费用账单按周期生成,高校那种按学期、住宅按自然月、园区按季度,周期规则写死在代码里,运营想调一下就得发版。
有个做园区运营的客户跟我吐槽,他们之前那套系统,住户报修上传的照片存在服务器本地目录,服务器一迁移,历史工单的照片全挂了。这种坑,说白了就是没把文件存储当基础设施来设计。
原理
工单状态机
我们没有用工作流引擎,太重。核心思路是把状态和动作分离:状态是数据,动作是方法,每次动作触发状态校验+日志落库。
java
public enum TicketStatus {
PENDING(1, "待派单"),
DISPATCHED(2, "已派单"),
PROCESSING(3, "处理中"),
FINISHED(4, "已办结"),
CANCELED(5, "已取消");
private final int code;
private final String desc;
// 构造、getter 省略
}
// 状态流转规则表,用 Map 维护,改需求只改配置
private static final Map<TicketStatus, Set<TicketStatus>> FLOW = Map.of(
TicketStatus.PENDING, Set.of(TicketStatus.DISPATCHED, TicketStatus.CANCELED),
TicketStatus.DISPATCHED, Set.of(TicketStatus.PROCESSING, TicketStatus.CANCELED),
TicketStatus.PROCESSING, Set.of(TicketStatus.FINISHED, TicketStatus.CANCELED),
TicketStatus.FINISHED, Set.of(),
TicketStatus.CANCELED, Set.of()
);
public void transit(Long ticketId, TicketStatus target, String operator) {
Ticket t = ticketMapper.selectById(ticketId);
TicketStatus current = TicketStatus.of(t.getStatus());
if (!FLOW.get(current).contains(target)) {
throw new BizException("非法状态流转:" + current + " -> " + target);
}
ticketMapper.updateStatus(ticketId, target.getCode());
logMapper.insert(new TicketLog(ticketId, current.getCode(),
target.getCode(), operator, LocalDateTime.now()));
}
每次变更都写 ticket_log,这张表只增不改,后面做监管台账和审计全靠它。
巡检任务自动生成
巡检计划按周期配置,我们用 cron 表达式存库,定时任务每天凌晨跑一次,只生成未来 24 小时内的任务,避免一次性生成几万条。
java
@Scheduled(cron = "0 0 1 * * ?")
public void generateInspectionTasks() {
List<InspectionPlan> plans = planMapper.selectActivePlans();
LocalDate tomorrow = LocalDate.now().plusDays(1);
for (InspectionPlan plan : plans) {
if (!CronExpression.isValidExpression(plan.getCron())) continue;
CronExpression exp = CronExpression.parse(plan.getCron());
LocalDateTime next = exp.next(LocalDateTime.now());
if (next != null && next.toLocalDate().equals(tomorrow)) {
taskMapper.insertBatch(buildTasks(plan, tomorrow));
}
}
}
巡检点绑定二维码,二维码内容就是 pointId 的加密串,扫码签到后校验时间和人员是否匹配,防止代签。发现异常一键转报修,本质就是调 transit 那套逻辑新建一张工单,把巡检点位置和照片带过去。
费用批量出账
收费标准的坑在于"同一房屋不同费用项不同周期"。我们的做法是账单模板 + 周期规则表,批量生成时按房屋维度聚合。
sql
-- 账单生成核心:按房屋+费用项+周期匹配
INSERT INTO fee_bill (house_id, fee_item_id, period, amount, status)
SELECT h.id, s.fee_item_id, #{period}, s.price, 0
FROM house h
JOIN fee_standard s ON s.house_type = h.type AND s.status = 1
WHERE h.status = 1
AND NOT EXISTS (
SELECT 1 FROM fee_bill b
WHERE b.house_id = h.id AND b.fee_item_id = s.fee_item_id
AND b.period = #{period}
);
NOT EXISTS 那一段是幂等保证,运营手滑点两次"生成账单"也不会重复出账。水电气的模板导入用 EasyExcel,表头校验失败直接整批回滚,不做部分成功------对账场景下部分成功比全失败更麻烦。
实操
双端小程序鉴权
一个小程序装两个角色,登录时用 identity 区分,JWT 里塞角色和租户 ID。
java
public String login(LoginDTO dto) {
User user = userMapper.selectByPhone(dto.getPhone());
if (user == null || !passwordEncoder.matches(dto.getPassword(), user.getPwd())) {
throw new BizException("账号或密码错误");
}
String identity = dto.getIdentity(); // RESIDENT / WORKER
if (!user.hasIdentity(identity)) {
throw new BizException("当前身份无权限登录");
}
return jwtUtil.sign(user.getId(), user.getTenantId(), identity);
}
前端根据返回的 identity 渲染不同 tabBar,住户看到报修、缴费、访客,师傅看到接单、报价、结算。后端每个接口用 @PreAuthorize 做按钮级权限,比如师傅只能看自己接的单。
第三方对接双模式
支付、发票、抄表这些外部能力,我们抽了一层 Gateway 接口,模拟实现和真实实现各一套,用配置开关切换。
java
public interface PayGateway {
PayResult pay(PayRequest req);
}
@Service
@ConditionalOnProperty(name = "gateway.pay.mode", havingValue = "mock")
public class MockPayGateway implements PayGateway {
public PayResult pay(PayRequest req) {
return PayResult.success("MOCK_" + System.currentTimeMillis());
}
}
@Service
@ConditionalOnProperty(name = "gateway.pay.mode", havingValue = "real")
public class RealPayGateway implements PayGateway {
// 真实微信支付对接
}
这样开发环境全走 mock,联调不用等第三方账号,上线改一行配置就行,业务代码零改动。
踩坑
第一个坑:访客通行码的"一码一人一次有效"。 我们最初用 Redis 存 code -> visitorId,核验后删 key。结果安保那边网络抖动,删 key 成功但响应超时,安保以为没核验,又扫了一次,直接报错。后来改成先 SETNX 一个核验锁,核验成功再异步删码,超时重试能兜住。
第二个坑:结算抽成计算。 师傅报价明细里可能有材料费、人工费,平台抽成只抽人工部分。我们一开始按总额抽,师傅直接炸了。后来在报价明细表加了 fee_type 字段,结算时按类型分组算。抽成开关也是后加的,有些园区自营师傅不抽成,硬编码就废了。
第三个坑:监管视图的只读权限。 院方要一个只读看板,我们一开始给了个只读账号,结果对方用这个账号调了导出接口,把全量台账拉走了。后来在网关层加了 readonly 拦截,只读角色禁止一切写操作和批量导出,导出单独走审批。
第四个坑:照片存储。 前面提到的本地目录问题,我们直接上 MinIO,数据库只存 objectKey,前端拿预签名 URL 访问。服务器迁移、扩容都不影响历史数据。
总结
这套系统从立项到上线大概五个多月,中间返工最多的就是状态流转和权限。我的建议是:工单、应急、投诉这三块的状态机一定要先画清楚再写代码,别边写边加状态。费用模块的幂等和事务边界要在设计阶段就定死,出账这种批量操作,宁可整批失败也不要部分成功。第三方对接的双模式设计,看起来多写了一套 mock,但联调阶段省下的时间远超这点成本。
代码量不小,核心逻辑大概两万多行,后面有空再单独写一篇讲统计看板的 SQL 优化和台账导出的分页流式处理。有在做类似项目的同学,欢迎在评论区聊聊你们的状态机是怎么设计的。