智慧物业平台实战:Spring Boot 整合小程序实现报修巡检费用全流程

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 优化和台账导出的分页流式处理。有在做类似项目的同学,欢迎在评论区聊聊你们的状态机是怎么设计的。

相关推荐
考虑考虑1 小时前
@JsonInclude注解使用
spring boot·后端·spring
IT_陈寒2 小时前
Vite的这个问题坑了我三天,原来如此解决!
前端·人工智能·后端
网易云信2 小时前
大模型能上岗做销售吗?我们让 12 款国产模型交了 420 份作业
前端·后端
CCblandy2 小时前
HTTP 长连接 3 类故障实测:[WinError 10053]、99 个 TIME_WAIT、69 次多余握手,4 种对策怎么选
后端·python·网络协议·http·云原生·性能优化·架构
蜗牛互联网2 小时前
Python Agent工具调用的白名单、参数校验与审批门禁
java·服务器·前端·人工智能·后端·python
dd聊技术3 小时前
Prompt 从代码常量搬进数据库:改一句话,不用再发一次版
java·后端
9命怪猫3 小时前
关于eBPF的JIT运维需要了解的
后端
9命怪猫3 小时前
运维eBPF 常用命令清单
后端
136096757233 小时前
服务器不能编译怎么办
后端