园区式物业里,房屋和设备是天天打交道的东西:空调、电梯、配电、消防、门禁、机房,哪个出问题都会影响租户、影响业务。把这些资产、巡检、报修、保养流程和数据打通,不是为了做“漂亮”的系统,而是为了把成本、停机、投诉和纠纷降下来,让运维能靠数据说话。本文直截了当:讲清楚概念、给出架构和流程、提供落地要点和一个汇总代码样例,你可以直接拿去改造或二次开发。语气不转呼啦,讲干货。
本文你将了解
- 到底什么是物业管理系统里的“房屋与设备设施”板块?
- 总体架构(含架构图)
- 核心数据模型与建表思路
- 四大功能
- 关键实现建议
- 实现效果与可衡量KPI
- 一份可直接拿去跑的汇总代码
一、什么是“房屋与设备设施”板块?
简单说:把园区里所有“有价值、需要维护”的东西数字化管理,包含台账(设备/房屋信息)、日常维护(巡检/保养)、应急维护(报修/派单/维修)、备件管理与统计看板。目标是做到“设备可追溯、问题可闭环、成本可量化”。
二、总体架构(简述 + 架构图)
- 前端:PC 管理端(React + AntD)+ 移动端(React Native / Flutter)用于巡检与报修。
- 后端:模块化服务(设备服务、工单服务、巡检服务、通知服务),REST API。
- 数据:Postgres 作为主库,时序数据走 Influx/Prometheus,文件走对象存储(S3/MinIO)。
- 消息:Kafka / Rabbit 做异步派单、通知与告警。
- 调度:Quartz / Kubernetes CronJob 或服务内注册 Cron(用于巡检计划与保养提醒)。
mermaid
flowchart LR
A[用户/租户/巡检员] -->|Web/Mobile| B[前端]
B -->|REST| C[API Gateway]
C --> D[设备服务]
C --> E[工单服务]
C --> F[巡检/保养服务]
D --> PG[(Postgres)]
E --> PG
F --> PG
D -->|metrics| TSDB[(InfluxDB)]
C --> MQ[(消息队列)]
MQ --> G[通知(短信/微信/Push)]
subgraph IoT
IOT[传感器/网关] -->|MQTT| C
end
三、核心数据模型
主要表:device(设备台账)、location(位置/房屋结构)、repair_ticket(报修单)、inspection_plan(巡检计划)、inspection_record(巡检记录)、maintenance_record(保养/维修记录)、spare_part(备件库存)。设计要点:常用查询列单独字段并建索引;大文件、图片存对象存储;巡检 checklist 和 parts 用 JSONB,但常查字段拆列。
四、四大功能(每项包含功能点、业务流程、开发技巧)
1.设备信息管理(功能)
- 台账:编码、类别、位置、制造商、型号、序列号、采购/保修信息、状态(在用/停用/报废)。
- 二维码绑定:每台设备生成编码与二维码,移动端扫码进入详情。
- 关联:设备 ↔ 位置 ↔ 维保合同 ↔ 备件。
业务流程:资产入库 → 验收并生成台账 → 上线并分配设备责任人 → 更新状态与生命周期跟踪。
开发技巧:编码规则统一(如 EQ-CampusB1-0001),code 列唯一并索引;图片保存在对象存储,DB 只存路径;支持批量导入/导出。
2.报修与维修(功能)
- 报修单:创建、优先级、附件、指派、处理、质检、关闭。
- 派单与外包:支持自动派单规则(按设备类别/位置/工种)及手动派单。
- SLA 跟踪、响应时长统计。
业务流程:用户报修 → 系统建单 → 自动/手动派单 → 技术员接单→ 维修中→ 完工并上传维修记录与凭证→ 质检→ 关闭。异常升级(超时自动上报经理)。
开发技巧:使用状态机控制工单流转(避免乱改状态);重要操作用事务和乐观锁;派单和通知走消息队列,避免请求阻塞。
3.设备巡检(功能)
- 巡检计划(周期/自定义Cron/检查项)→ 生成巡检任务 → 移动端巡检记录(支持离线) → 异常自动生成工单。
- 巡检项模板可配置(电压、温度、目测、润滑等)。
业务流程:计划触发 → 任务派发给巡检员 → 巡检员移动端执行并上传照片/状态 → 若异常自动生成报修单或提醒。
开发技巧:巡检离线支持(本地存 SQLite,恢复网路后同步);每个巡检记录带本地唯一 ID 做幂等处理;触发遵循分布式调度器注册任务而非频繁 DB 轮询。
4.设备保养(功能)
- 保养计划(预防性维护),维护记录、使用备件记录、下次保养到期时间。
- 保养历史用于计算 MTBF/MTTR,支持周期提醒。
业务流程:保养计划触发→ 保养人员执行并记录→ 如发现异常生成工单→ 更新下次保养周期或调整计划。
开发技巧:对定期保养使用专门调度服务;保养记录绑定备件出库,自动扣减库存并计成本;保养完成写入设备生命周期日志。
五、关键实现建议
- 索引与分区:巡检/维修历史量大,采用时间分区或按月分表。
- 缓存:设备详情使用 Redis 缓存热点,避免频繁 join。
- 幂等与版本控制:工单状态、库存变更使用 version 字段做乐观锁。
- 异步:照片压缩、通知、外包派单用异步队列处理。
- 离线优先:移动端优先保存本地,再同步,保证用户体验。
- IoT接入:阈值告警直接触发工单或告警,不要在传感器网关做复杂逻辑,放到后端规则引擎执行。
- 日志审计:关键操作均需审计(谁改、改什么、什么时间)。
六、实现效果与KPI(可量化)
- 报修响应时间(平均)降低 20%-40%(可按历史对比设定目标)。
- 计划保养完成率 ≥95%。
- 设备停机时长(MTTR)下降,设备可用率提升。
- 备件库存周转率提升,库存成本下降。 这些效果依赖先把台账做对、流程执行到位、数据持续沉淀。
七、汇总代码
下面是一个可以作为参考的一体化样例(可把它拆到不同文件)。在生产之前请按公司规范拆模组、加错误处理与鉴权。
ts
/* =========================
简化示例:Postgres 建表 + Node.js (TypeScript) + Express + TypeORM + node-cron
一块代码包含:
- 建表 SQL(Postgres)
- TypeORM 实体
- 简单 Express 路由(设备、报修、巡检、保养)
- 调度示例(node-cron)
- 简单前端 fetch 示例
========================= */
/* ---------- Postgres 建表(复制到 psql 执行) ---------- */
-- device, location, repair_ticket, inspection_plan, inspection_record, maintenance_record, spare_part
CREATE TABLE IF NOT EXISTS location (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
parent_id BIGINT,
type VARCHAR(50),
address TEXT
);
CREATE TABLE IF NOT EXISTS device (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(64) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
category VARCHAR(100),
location_id BIGINT,
manufacturer VARCHAR(255),
model VARCHAR(255),
serial_no VARCHAR(255),
purchase_date DATE,
warranty_until DATE,
status VARCHAR(50) DEFAULT 'in_service',
metadata JSONB,
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now()
);
CREATE TABLE IF NOT EXISTS repair_ticket (
id BIGSERIAL PRIMARY KEY,
ticket_no VARCHAR(64) UNIQUE NOT NULL,
device_id BIGINT REFERENCES device(id),
reporter_id BIGINT,
priority SMALLINT DEFAULT 3,
status VARCHAR(50) DEFAULT 'reported',
description TEXT,
assigned_to BIGINT,
attachments JSONB,
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now()
);
CREATE TABLE IF NOT EXISTS inspection_plan (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255),
device_category VARCHAR(100),
frequency VARCHAR(50),
cron_expr VARCHAR(255),
checklist JSONB,
active BOOLEAN DEFAULT true
);
CREATE TABLE IF NOT EXISTS inspection_record (
id BIGSERIAL PRIMARY KEY,
plan_id BIGINT REFERENCES inspection_plan(id),
device_id BIGINT REFERENCES device(id),
inspector_id BIGINT,
result JSONB,
status VARCHAR(50),
local_id VARCHAR(128), -- 离线同步幂等
created_at TIMESTAMP DEFAULT now()
);
CREATE TABLE IF NOT EXISTS maintenance_record (
id BIGSERIAL PRIMARY KEY,
device_id BIGINT REFERENCES device(id),
maintenance_type VARCHAR(50),
performed_by BIGINT,
notes TEXT,
parts_used JSONB,
next_due DATE,
created_at TIMESTAMP DEFAULT now()
);
CREATE TABLE IF NOT EXISTS spare_part (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(64) UNIQUE,
name VARCHAR(255),
qty INT DEFAULT 0,
location VARCHAR(255)
);
/* ---------- TypeORM 实体(示例) ---------- */
// src/entities/device.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from "typeorm";
@Entity()
export class Device {
@PrimaryGeneratedColumn('increment') id: number;
@Column({ unique: true }) code: string;
@Column() name: string;
@Column({ nullable: true }) category: string;
@Column({ nullable: true }) location_id: number;
@Column({ nullable: true }) manufacturer: string;
@Column({ nullable: true }) model: string;
@Column({ nullable: true }) serial_no: string;
@Column({ type: 'date', nullable: true }) purchase_date: string;
@Column({ default: 'in_service' }) status: string;
@Column({ type: 'jsonb', nullable: true }) metadata: any;
@CreateDateColumn() created_at: Date;
@UpdateDateColumn() updated_at: Date;
}
// 省略其它实体,按上面 SQL 映射
/* ---------- Express 简单路由(示例) ---------- */
import express from 'express';
import bodyParser from 'body-parser';
import { createConnection, getRepository } from 'typeorm';
import cron from 'node-cron';
import { Device } from './entities/device.entity';
import { RepairTicket } from './entities/repair_ticket.entity';
import { InspectionPlan } from './entities/inspection_plan.entity';
import { InspectionRecord } from './entities/inspection_record.entity';
import { MaintenanceRecord } from './entities/maintenance_record.entity';
async function main() {
await createConnection(); // 请在 ormconfig.json 配置数据库连接
const app = express();
app.use(bodyParser.json());
// 设备:创建与列表
app.post('/api/devices', async (req, res) => {
const repo = getRepository(Device);
const { code, name, category, location_id } = req.body;
if (!code || !name) return res.status(400).json({ message: 'code/name 必填' });
try {
const exists = await repo.findOne({ where: { code }});
if (exists) return res.status(400).json({ message: '设备编码已存在' });
const d = repo.create({ code, name, category, location_id });
await repo.save(d);
return res.json(d);
} catch (e) { return res.status(500).json({ message: 'error', e }); }
});
app.get('/api/devices', async (req, res) => {
const repo = getRepository(Device);
const list = await repo.find({ take: 200 });
res.json(list);
});
// 报修:新建、指派、关闭(简化)
app.post('/api/tickets', async (req, res) => {
const repo = getRepository(RepairTicket);
const { device_id, description, reporter_id, priority } = req.body;
const ticketNo = `T${Date.now()}`;
const t = repo.create({ ticket_no: ticketNo, device_id, description, reporter_id, priority, status: 'reported' });
await repo.save(t);
// 这里可以 publish 到消息队列用于派单
res.json(t);
});
app.post('/api/tickets/:id/assign', async (req, res) => {
const repo = getRepository(RepairTicket);
const ticket = await repo.findOne(req.params.id);
if (!ticket) return res.status(404).send('ticket not found');
ticket.assigned_to = req.body.assigned_to;
ticket.status = 'assigned';
await repo.save(ticket);
res.json(ticket);
});
// 巡检计划:创建、列表
app.post('/api/inspection/plans', async (req, res) => {
const repo = getRepository(InspectionPlan);
const p = repo.create(req.body);
await repo.save(p);
// 如果需要,可以在这里把计划注册到实际 cron
res.json(p);
});
app.get('/api/inspection/plans', async (req, res) => {
const repo = getRepository(InspectionPlan);
res.json(await repo.find());
});
// 巡检记录(移动端同步)
app.post('/api/inspection/records', async (req, res) => {
const repo = getRepository(InspectionRecord);
// local_id 用于离线同步幂等
const r = repo.create(req.body);
await repo.save(r);
// 如果记录包含异常,自动生成工单(示例)
const hasIssue = (r.result && JSON.stringify(r.result).includes('"status":"issue"'));
if (hasIssue) {
const ticketRepo = getRepository(RepairTicket);
const ticket = ticketRepo.create({
ticket_no: `T${Date.now()}`,
device_id: r.device_id,
description: `巡检发现异常: ${JSON.stringify(r.result)}`,
reporter_id: r.inspector_id,
priority: 2
});
await ticketRepo.save(ticket);
}
res.json(r);
});
// 保养记录
app.post('/api/maintenance', async (req, res) => {
const repo = getRepository(MaintenanceRecord);
const m = repo.create(req.body);
await repo.save(m);
res.json(m);
});
// 简单搜索接口
app.get('/api/search', async (req, res) => {
const repo = getRepository(Device);
const q = req.query.q as string || '';
const list = await repo.createQueryBuilder('d')
.where('d.name ILIKE :q OR d.code ILIKE :q', { q: `%${q}%` })
.limit(100).getMany();
res.json(list);
});
// 启动定时任务:扫描 active inspection_plan 并生成任务(示例;生产请做更稳健的实现)
cron.schedule('* * * * *', async () => {
const planRepo = getRepository(InspectionPlan);
const plans = await planRepo.find({ where: { active: true }});
for (const p of plans) {
if (!p.cron_expr) continue;
// 生产中建议把计划解析成独立 cron 任务注册,而非每分钟扫描 DB
// 这里简化:若 cron_expr 匹配当前分钟则生成任务(伪逻辑)
// TODO: 使用 cron-parser 或者在启动时注册每个计划的 cron job
}
});
app.listen(3000, () => console.log('server started on 3000'));
}
main().catch(e => console.error(e));
/* ---------- 前端调用示例(浏览器 fetch) ---------- */
// 提交报修
fetch('/api/tickets', { method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ device_id: 1, description: '电机异常振动', reporter_id: 1001, priority: 1 })
}).then(r=>r.json()).then(console.log);
// 移动端离线后同步巡检记录(带 local_id)
fetch('/api/inspection/records', { method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({
plan_id: 2,
device_id: 1,
inspector_id: 1001,
result: [{ item: '温度', value: '75C', status: 'ok' }, { item: '振动', value: '高', status: 'issue' }],
status: 'issue',
local_id: 'mobile-uuid-xxxx'
})
}).then(r=>r.json()).then(console.log);
在这里我给大家推荐一个业务人员就能够直接上手的高性价比、零代码平台——简道云物业管理系统,简道云物业管理系统将各部分收集到的数据信息汇总在数据报表,对运营、设备、值班和行政办公情况进行直观展示,便于管理人员处理工作。
八、FAQ
FAQ 1:巡检发现问题要不要自动生成工单?
建议默认自动生成但做去重:巡检人员在移动端发现异常时,一键生成工单可以大幅减少人工录单成本并加快响应。但要避免重复工单造成资源浪费:生成前检查同一设备在一定时间窗口(如最近72小时)是否存在未关闭或相同类型的工单;若存在则把巡检详情追加到已有工单而不是新建。系统应把“谁发现”“发现时间”“巡检记录ID”都写入工单,形成可追溯链路。这样既保证快速响应,又利于统计分析和责任追溯。
FAQ 2:移动端离线如何避免重复/丢失数据?
移动端离线场景很常见。设计时要求每条本地记录带唯一 local_id(如设备 UUID + 时间戳),在同步时后端校验 local_id 是否已存在,做到幂等写入;文件应先上传到对象存储并返回文件路径,再把记录写到 DB;若上传失败则重试并记录日志。同步成功后客户端应删除本地缓存或标记为已同步。对于冲突(多人编辑同一巡检),可以采用“最后编辑覆盖”或把冲突上报人工处理;重要的是记录变更历史以便审计。
FAQ 3:怎么保证保养计划不会被漏掉或重复触发?
推荐把保养计划在调度服务启动时解析并注册为独立的定时任务(如 Quartz 或 Kubernetes CronJob),而不是靠服务不停轮询数据库。独立任务能被单独管理、暂停、调整并带有重试策略;生产环境若是多实例部署,选用支持分布式协调的调度器(避免任务在多个实例重复执行)。另外,生成保养任务后应写入任务表并记录触发时间与状态,保养完成后更新下次到期,这样能在界面上直观看到计划覆盖率并做 SLA 监督。
小结
把房屋与设备做成一个可操作、可统计、可闭环的模块,能把日常运维从“靠人情和经验”变成“靠流程和数据”。上面给出的架构、流程、落地技巧和一个汇总代码样例,能帮你快速搭建起一个能跑的原型,建议先在小园区灰度试点,收集数据再迭代策略。