首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >多租户报修巡检 SaaS 的数据隔离、附件存储与消息触达设计

多租户报修巡检 SaaS 的数据隔离、附件存储与消息触达设计

原创
作者头像
用户3066938
发布于 2026-11-05 10:34:54
发布于 2026-11-05 10:34:54
930
举报

把报修巡检做成 SaaS,服务多家物业公司、园区、学校,技术难点不在功能,而在三件事:数据必须严格隔离(A 物业绝不能看到 B 物业的工单)、附件要省钱又要快(现场照片是第一大存储开销)、消息要送到人(工单派下去没人知道等于没派)。本文结合一个多租户维保 SaaS 的实践展开。技术栈:PHP 8 + MySQL + 对象存储 + 微信服务号/小程序订阅消息。

一、租户隔离:单库多租户 + 强制过滤

多租户有三种常见方案:独立库、独立 schema、共享表加租户字段。维保 SaaS 的租户规模中等(几十到几千家),共享表加 tenant_id 是成本与运维的最优解,但必须解决"某个查询忘了带 tenant_id"这个致命风险。

三道防线缺一不可:

第一道,应用层的全局作用域。 所有查询默认注入租户条件,业务代码不手写 where tenant_id:

代码语言:// 基类模型:查询自动附加租户条件,写入自动填充 tenant_id
复制
abstract class TenantModel extends Model
{
    protected static function booted(): void
    {
        static::addGlobalScope('tenant', function (Builder $q) {
            $tid = app('tenant')->id();   // 由中间件解析并绑定
            if ($tid) {
                $q->where($q->getModel()->getTable() . '.tenant_id', $tid);
            }
        });

        static::creating(function (Model $m) {
            $m->tenant_id ??= app('tenant')->id();
        });
    }
}

app('tenant') 由请求中间件从登录态或域名解析后绑定,禁止从请求参数里取租户 ID——参数可伪造,这是越权的最大来源。

第二道,数据库行级约束。 关键表的唯一索引必须带 tenant_id,否则跨租户数据会"撞"在一起:

-- 错误:全局唯一,A 租户建的点位码会让 B 租户无法使用同一个码

-- UNIQUE KEY uk_code (point_code)

-- 正确:租户内唯一

UNIQUE KEY uk_tenant_code (tenant_id, point_code)

第三道,越权测试。 用两个租户的账号反复交叉访问对方资源 ID(工单、点位、设备),断言全部返回 404 而不是 403——返回 403 等于告诉攻击者"这个 ID 存在但不是你的"。这项工作自动化成回归用例,新接口上线必须过。

二、附件存储:现场照片是第一大成本

报修巡检的照片量级远超一般业务:一次巡检每点位两三张,一个中型物业每月上万张。设计要点如下:

存储放对象存储,不入库、不上应用服务器磁盘。 数据库只存 key 与元数据:

代码语言:CREATE TABLE attachment (
复制
  id          BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id   BIGINT NOT NULL,
  biz_type    VARCHAR(20) NOT NULL COMMENT 'order/inspect/device',
  biz_id      BIGINT NOT NULL,
  object_key  VARCHAR(200) NOT NULL,
  mime        VARCHAR(50) NOT NULL,
  size_bytes  INT NOT NULL,
  sha256      CHAR(64) NOT NULL COMMENT '内容指纹,用于秒传与去重',
  uploader_id BIGINT NOT NULL,
  create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  KEY idx_biz (tenant_id, biz_type, biz_id),
  KEY idx_sha (tenant_id, sha256)
);

上传走客户端直传,服务端只发临时凭证。 照片经应用服务器中转会占满带宽、拖慢提交,师傅在现场网络差时体验极差。流程是:客户端向服务端申请上传凭证(限定对象前缀为 tenant/{tid}/、限定大小与类型、有效期 10 分钟),直接传到对象存储,成功后回调服务端登记元数据。

压缩与缩略图分离。 原图(控制在 2MB 内,客户端上传前压缩)用于留证,同时生成 400px 缩略图用于列表展示。列表页永远加载缩略图,点开才加载原图——这一条能把流量成本降一个量级。

按租户统计用量。 SaaS 的存储成本必须能摊到租户头上,否则免费租户会把成本吃掉。attachment 表按 tenant_id 汇总月度用量,超出套餐额度告警或限制上传,这是商业闭环的一部分。

三、消息触达:三条通道与失败兜底

工单派下去,师傅不看手机等于没派。消息设计要按"紧急程度"分层:

场景 首选通道 兜底通道 说明

新工单派单 小程序订阅消息 服务号模板消息 + 短信 师傅多在小程序端操作

紧急工单(漏水/停电) 电话外呼 + 短信 小程序消息 分钟级响应要求

SLA 超时提醒 服务号消息 短信 给处理人与主管双发

巡检漏检提醒 服务号消息 站内信 每日定时汇总下发

业主完工通知 小程序订阅消息 短信(可选增值) 需用户授权订阅

工程上有三个坑必须处理:

一是订阅消息的授权是一次性的。 微信小程序订阅消息每次授权只能发一条,用户点"允许"时是明确的。解决方案是在关键交互点请求授权(提交报修时申请"处理进度通知",师傅接单时申请"派单通知"),并把授权结果落库(msg_quota 表记剩余可发条数),发不出去时自动降级到服务号模板消息。

二是消息必须异步。 派单接口里同步调微信接口,一旦微信侧抖动,工单派单就失败,这是最典型的架构错误。正确做法是写 notify_outbox 表,独立投递进程消费,失败重试三次后落站内信并记录。

代码语言:CREATE TABLE notify_outbox (
复制
  id          BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id   BIGINT NOT NULL,
  channel     VARCHAR(20) NOT NULL COMMENT 'mp_subscribe/oa_template/sms/inapp',
  target_type VARCHAR(20) NOT NULL,
  target_id   BIGINT NOT NULL,
  payload     JSON NOT NULL,
  status      TINYINT NOT NULL DEFAULT 1 COMMENT '1待发 2成功 3失败 4降级',
  retry_count TINYINT NOT NULL DEFAULT 0,
  next_retry  DATETIME NULL,
  create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  KEY idx_status_retry (status, next_retry)
);

三是幂等。 同一条工单的超时提醒可能被多个扫描任务命中,用 (channel, target_type, target_id, biz_key) 唯一键去重,biz_key 用状态+时间窗拼接,保证同一事件只提醒一次。

四、跨租户的运营后台

SaaS 需要一个平台侧后台:查看租户列表、订阅套餐、用量统计、封停与续费,但平台客服不应默认看到租户业务数据。实践做法是数据权限分级:平台超管可查看聚合统计(租户数、工单总量、存储用量),具体工单内容需租户授权或走工单审批,且查看行为记录审计日志。维保 SaaS 的客户多是物业与园区,对数据敏感度不低,这条边界不做,销售阶段就可能被卡。

租户注销与数据导出也值得提前设计。客户流失时要求导出全部工单、巡检记录与附件,是 SaaS 合约里几乎必有的条款;系统要能按租户一键打包导出(工单 CSV + 巡检 CSV + 附件压缩包链接),并在保留期后执行真正的物理删除。删除策略写进合同附件,比事后扯皮省事。

五、小结

多租户报修巡检 SaaS 的三条主线:全局作用域强制租户过滤 + 索引带租户 + 越权回归测试;附件直传对象存储 + 内容指纹去重 + 按租户计费;消息按紧急度分层 + outbox 异步 + 唯一键幂等。青海青帝信息科技有限公司在维保 SaaS 方向的实践里,最深的一条体会是:多租户系统的安全性不是靠"小心编码",而是靠框架层面的强制约束——让"忘记过滤"这件事在代码层面无法发生,比写一百条规范都有效。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档