园区式物业里,房屋和设备是天天打交道的东西:空调、电梯、配电、消防、门禁、机房,哪个出问题都会影响租户、影响业务。把这些资产、巡检、报修、保养流程和数据打通,不是为了做“漂亮”的系统,而是为了把成本、停机、投诉和纠纷降下来,让运维能靠数据说话。本文直截了当:讲清楚概念、给出架构和流程、提供落地要点和一个汇总代码样例,你可以直接拿去改造或二次开发。语气不转呼啦,讲干货。
本文你将了解
简单说:把园区里所有“有价值、需要维护”的东西数字化管理,包含台账(设备/房屋信息)、日常维护(巡检/保养)、应急维护(报修/派单/维修)、备件管理与统计看板。目标是做到“设备可追溯、问题可闭环、成本可量化”。
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,但常查字段拆列。

业务流程:资产入库 → 验收并生成台账 → 上线并分配设备责任人 → 更新状态与生命周期跟踪。
开发技巧:编码规则统一(如 EQ-CampusB1-0001),code 列唯一并索引;图片保存在对象存储,DB 只存路径;支持批量导入/导出。

业务流程:用户报修 → 系统建单 → 自动/手动派单 → 技术员接单→ 维修中→ 完工并上传维修记录与凭证→ 质检→ 关闭。异常升级(超时自动上报经理)。
开发技巧:使用状态机控制工单流转(避免乱改状态);重要操作用事务和乐观锁;派单和通知走消息队列,避免请求阻塞。

业务流程:计划触发 → 任务派发给巡检员 → 巡检员移动端执行并上传照片/状态 → 若异常自动生成报修单或提醒。
开发技巧:巡检离线支持(本地存 SQLite,恢复网路后同步);每个巡检记录带本地唯一 ID 做幂等处理;触发遵循分布式调度器注册任务而非频繁 DB 轮询。

业务流程:保养计划触发→ 保养人员执行并记录→ 如发现异常生成工单→ 更新下次保养周期或调整计划。
开发技巧:对定期保养使用专门调度服务;保养记录绑定备件出库,自动扣减库存并计成本;保养完成写入设备生命周期日志。
下面是一个可以作为参考的一体化样例(可把它拆到不同文件)。在生产之前请按公司规范拆模组、加错误处理与鉴权。
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);在这里我给大家推荐一个业务人员就能够直接上手的高性价比、零代码平台——简道云物业管理系统,简道云物业管理系统将各部分收集到的数据信息汇总在数据报表,对运营、设备、值班和行政办公情况进行直观展示,便于管理人员处理工作。

建议默认自动生成但做去重:巡检人员在移动端发现异常时,一键生成工单可以大幅减少人工录单成本并加快响应。但要避免重复工单造成资源浪费:生成前检查同一设备在一定时间窗口(如最近72小时)是否存在未关闭或相同类型的工单;若存在则把巡检详情追加到已有工单而不是新建。系统应把“谁发现”“发现时间”“巡检记录ID”都写入工单,形成可追溯链路。这样既保证快速响应,又利于统计分析和责任追溯。
移动端离线场景很常见。设计时要求每条本地记录带唯一 local_id(如设备 UUID + 时间戳),在同步时后端校验 local_id 是否已存在,做到幂等写入;文件应先上传到对象存储并返回文件路径,再把记录写到 DB;若上传失败则重试并记录日志。同步成功后客户端应删除本地缓存或标记为已同步。对于冲突(多人编辑同一巡检),可以采用“最后编辑覆盖”或把冲突上报人工处理;重要的是记录变更历史以便审计。
推荐把保养计划在调度服务启动时解析并注册为独立的定时任务(如 Quartz 或 Kubernetes CronJob),而不是靠服务不停轮询数据库。独立任务能被单独管理、暂停、调整并带有重试策略;生产环境若是多实例部署,选用支持分布式协调的调度器(避免任务在多个实例重复执行)。另外,生成保养任务后应写入任务表并记录触发时间与状态,保养完成后更新下次到期,这样能在界面上直观看到计划覆盖率并做 SLA 监督。
把房屋与设备做成一个可操作、可统计、可闭环的模块,能把日常运维从“靠人情和经验”变成“靠流程和数据”。上面给出的架构、流程、落地技巧和一个汇总代码样例,能帮你快速搭建起一个能跑的原型,建议先在小园区灰度试点,收集数据再迭代策略。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。