一、多角色权限模型:RBAC 与数据可见性
1.1 四类角色的权限边界
物业系统的权限模型比普通商城复杂,因为它不仅有"功能权限"(能不能操作某个功能),还有"数据权限"(能看到哪些数据)。
业主 只能看到自己的房产、自己的缴费记录、自己的报修工单。数据权限是"自己"。
物业人员 能看到自己负责区域的报修工单、通知和活动。数据权限是"区域"。
商户 只能看到自己的商品、订单和结算记录。数据权限是"自己"。
管理员 能看到全部数据。数据权限是"全部"。
这种多层次的权限模型,用标准的 RBAC(基于角色的访问控制)只能解决功能权限,数据权限需要额外的设计。
-- 角色表 CREATE TABLE `role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '角色名称', `code` varchar(50) NOT NULL COMMENT '角色标识', `data_scope` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0=全部 1=本区域 2=本部门 3=仅本人', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_code` (`code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 用户角色关联表 CREATE TABLE `user_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `role_id` int(11) NOT NULL, `community_id` int(11) DEFAULT NULL COMMENT '所属小区', `building_id` int(11) DEFAULT NULL COMMENT '所属楼栋', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_role` (`user_id`, `role_id`), KEY `idx_community` (`community_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
data_scope 字段是数据权限的核心。它定义了角色的数据可见范围:0 表示全部数据,1 表示本区域数据,2 表示本部门数据,3 表示仅本人的数据。
1.2 数据权限的自动注入
数据权限的实现方式是在查询时自动注入过滤条件。这与多商户系统的租户隔离类似,但更加灵活——不同角色的过滤维度不同。
<?php // app/common/service/DataScopeService.php namespace app\common\service; use think\Db; class DataScopeService { /** * 为查询注入数据权限过滤条件 */ public function apply($query, $userId, $table = '') { $user = $this->getUserWithRoles($userId); if (!$user) { return $query->where('1=0'); // 无权限,返回空集 } // 超级管理员不做限制 if ($user['is_admin']) { return $query; } // 取权限范围最宽的角色 $scopes = array_column($user['roles'], 'data_scope'); $minScope = min($scopes); switch ($minScope) { case 0: // 全部数据 return $query; case 1: // 本区域 $communityIds = $this->getUserCommunityIds($user); return $query->whereIn('community_id', $communityIds); case 2: // 本部门 $deptIds = $this->getUserDeptIds($user); return $query->whereIn('dept_id', $deptIds); case 3: // 仅本人 return $query->where('user_id', $userId); default: return $query->where('1=0'); } } protected function getUserWithRoles(int $userId): ?array { $user = Db::name('user')->where('id', $userId)->find(); if (!$user) { return null; } $roles = Db::name('user_role') ->alias('ur') ->join('role r', 'ur.role_id = r.id') ->where('ur.user_id', $userId) ->field('r.*, ur.community_id, ur.building_id') ->select(); $user['roles'] = $roles; $user['is_admin'] = in_array('admin', array_column($roles, 'code')); return $user; } protected function getUserCommunityIds(array $user): array { return array_unique(array_filter( array_column($user['roles'], 'community_id') )); } protected function getUserDeptIds(array $user): array { return array_unique(array_filter( array_column($user['roles'], 'building_id') )); } }
这个服务的核心设计是:在数据访问层统一注入过滤条件,而不是在每个业务方法中手动编写。 这避免了"某个接口忘记加权限过滤"导致的数据泄露。
1.3 房产与业主的绑定关系
物业系统的一个特殊之处是"房产"是核心实体。业主不是抽象的用户,而是"某个房产的业主"。一个业主可能拥有多套房产,一个房产可能有多个业主(如夫妻共有)。
-- 房产表 CREATE TABLE `property` ( `id` int(11) NOT NULL AUTO_INCREMENT, `community_id` int(11) NOT NULL COMMENT '小区ID', `building_id` int(11) NOT NULL COMMENT '楼栋ID', `unit` varchar(20) NOT NULL COMMENT '单元', `floor` int(11) NOT NULL COMMENT '楼层', `room_no` varchar(20) NOT NULL COMMENT '房号', `area` decimal(10,2) NOT NULL COMMENT '建筑面积', `property_fee_rate` decimal(10,2) NOT NULL COMMENT '物业费单价(元/㎡/月)', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1=正常 2=空置 3=装修中', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_community_building` (`community_id`, `building_id`), UNIQUE KEY `uk_room` (`community_id`, `building_id`, `unit`, `floor`, `room_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 业主房产关联表 CREATE TABLE `owner_property` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `property_id` int(11) NOT NULL, `relation` varchar(20) NOT NULL COMMENT 'owner/family/tenant', `is_primary` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否主要联系人', `verified_at` datetime DEFAULT NULL COMMENT '认证时间', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_property` (`user_id`, `property_id`), KEY `idx_property` (`property_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
property_fee_rate 字段是物业费计算的基础。每个房产可能有不同的物业费单价(如住宅和商铺不同),因此单价存储在房产层面而非小区层面。
二、物业费的计算、缴纳与催缴
2.1 物业费的计算模型
物业费的计算涉及几个变量:房产面积、单价、缴费周期、滞纳金。
<?php // app/common/service/PropertyFeeService.php namespace app\common\service; use think\Db; class PropertyFeeService { /** * 计算物业费 */ public function calculate(int $propertyId, string $startDate, string $endDate): array { $property = Db::name('property')->where('id', $propertyId)->find(); if (!$property) { throw new \Exception('房产不存在'); } // 计算月数 $start = new \DateTime($startDate); $end = new \DateTime($endDate); $interval = $start->diff($end); $months = $interval->y * 12 + $interval->m; if ($interval->d > 0) { $months += 1; // 不足一月按一月计 } // 基础物业费 $baseFee = $property['area'] * $property['property_fee_rate'] * $months; // 检查是否有历史欠费 $arrears = $this->getArrears($propertyId); // 计算滞纳金(欠费金额的 0.05%/天) $lateFee = 0; foreach ($arrears as $item) { $overdueDays = $this->getOverdueDays($item['due_date']); if ($overdueDays > 0) { $lateFee += $item['amount'] * 0.0005 * $overdueDays; } } return [ 'property_id' => $propertyId, 'area' => $property['area'], 'rate' => $property['property_fee_rate'], 'months' => $months, 'start_date' => $startDate, 'end_date' => $endDate, 'base_fee' => round($baseFee, 2), 'arrears' => round(array_sum(array_column($arrears, 'amount')), 2), 'late_fee' => round($lateFee, 2), 'total' => round($baseFee + $lateFee, 2), ]; } /** * 获取历史欠费 */ protected function getArrears(int $propertyId): array { return Db::name('property_fee_bill') ->where('property_id', $propertyId) ->where('status', 'unpaid') ->where('due_date', '<', date('Y-m-d')) ->select() ->toArray(); } protected function getOverdueDays(string $dueDate): int { $due = strtotime($dueDate); $now = time(); return $now > $due ? (int)(($now - $due) / 86400) : 0; } }
滞纳金的计算是一个需要注意的工程细节。滞纳金不能无限累积,通常有上限(如不超过本金)。同时,滞纳金的计算需要幂等——同一个账单重复计算应该得到相同的结果。
2.2 账单生成与推送
物业费账单通常是按月或按季度批量生成的。系统需要提供一个批量生成的功能,并推送给业主。
<?php // app/common/service/BillGenerateService.php namespace app\common\service; use think\Db; use think\facade\Queue; class BillGenerateService { /** * 批量生成账单 */ public function batchGenerate(int $communityId, string $period): array { // period 格式:2025-01(月度)或 2025-Q1(季度) $properties = Db::name('property') ->where('community_id', $communityId) ->where('status', 1) ->select(); $created = 0; $skipped = 0; foreach ($properties as $property) { // 检查是否已生成 $exists = Db::name('property_fee_bill') ->where('property_id', $property['id']) ->where('period', $period) ->count(); if ($exists) { $skipped++; continue; } // 计算账单金额 $months = $this->periodToMonths($period); $amount = $property['area'] * $property['property_fee_rate'] * $months; $billId = Db::name('property_fee_bill')->insertGetId([ 'property_id' => $property['id'], 'community_id' => $communityId, 'period' => $period, 'amount' => round($amount, 2), 'due_date' => $this->getDueDate($period), 'status' => 'unpaid', 'created_at' => date('Y-m-d H:i:s'), ]); // 异步入队,推送通知 Queue::push('app\job\SendBillNotification', [ 'bill_id' => $billId, ], 'notification'); $created++; } return ['created' => $created, 'skipped' => $skipped]; } protected function periodToMonths(string $period): int { if (strpos($period, 'Q') !== false) { return 3; } return 1; } protected function getDueDate(string $period): string { // 账单生成后 30 天到期 return date('Y-m-d', strtotime('+30 days')); } }
推送通知使用队列异步处理,避免批量生成时阻塞主流程。这是处理批量操作的常见模式。
2.3 缴费流程与对账
缴费涉及支付回调和对账。支付成功后,账单状态更新为"已支付",同时记录支付流水。
<?php // app/common/service/PaymentCallbackService.php namespace app\common\service; use think\Db; class PaymentCallbackService { /** * 处理支付回调 */ public function handle(array $callbackData): bool { $orderNo = $callbackData['out_trade_no']; $transactionId = $callbackData['transaction_id']; // 幂等检查 $exists = Db::name('payment_log') ->where('transaction_id', $transactionId) ->count(); if ($exists) { return true; // 已处理,直接返回成功 } Db::startTrans(); try { // 1. 查找账单 $bill = Db::name('property_fee_bill') ->where('bill_no', $orderNo) ->lock(true) ->find(); if (!$bill) { throw new \Exception('账单不存在'); } if ($bill['status'] === 'paid') { Db::commit(); return true; // 已支付,幂等返回 } // 2. 更新账单状态 Db::name('property_fee_bill') ->where('id', $bill['id']) ->update([ 'status' => 'paid', 'paid_at' => date('Y-m-d H:i:s'), 'transaction_id' => $transactionId, ]); // 3. 记录支付流水 Db::name('payment_log')->insert([ 'order_no' => $orderNo, 'transaction_id' => $transactionId, 'amount' => $callbackData['total_fee'] / 100, 'type' => 'property_fee', 'created_at' => date('Y-m-d H:i:s'), ]); Db::commit(); return true; } catch (\Exception $e) { Db::rollback(); \think\facade\Log::error('支付回调处理失败', [ 'order_no' => $orderNo, 'error' => $e->getMessage(), ]); return false; } } }
幂等性是支付回调的核心要求。 微信支付可能重复发送回调通知,系统必须能够识别并正确处理重复通知。使用 transaction_id 做幂等键是标准做法。
三、报修工单:状态机与派单逻辑
3.1 报修工单的数据模型
CREATE TABLE `repair_order` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '工单号', `user_id` int(11) NOT NULL COMMENT '报修人', `property_id` int(11) NOT NULL COMMENT '报修房产', `community_id` int(11) NOT NULL, `category` varchar(50) NOT NULL COMMENT '报修类别:水电/门窗/电梯等', `title` varchar(200) NOT NULL, `description` text COMMENT '问题描述', `images` text COMMENT '图片列表JSON', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0=待接单 1=处理中 2=已完成 3=待评价 4=已评价 5=已取消', `priority` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0=普通 1=紧急 2=特急', `handler_id` int(11) DEFAULT NULL COMMENT '处理人', `assigned_at` datetime DEFAULT NULL COMMENT '派单时间', `completed_at` datetime DEFAULT NULL, `rating` tinyint(1) DEFAULT NULL COMMENT '评分 1-5', `comment` varchar(500) DEFAULT NULL COMMENT '评价内容', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user` (`user_id`), KEY `idx_community_status` (`community_id`, `status`), KEY `idx_handler` (`handler_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.2 报修工单的状态机
报修工单的状态流转比订单简单,但派单逻辑更复杂。
<?php // app/common/service/RepairService.php namespace app\common\service; use think\Db; class RepairService { // 工单状态 const STATUS_PENDING = 0; // 待接单 const STATUS_PROCESSING = 1; // 处理中 const STATUS_COMPLETED = 2; // 已完成 const STATUS_RATED = 3; // 已评价 const STATUS_CANCELLED = 4; // 已取消 /** * 创建报修工单 */ public function create(array $data): array { $orderNo = date('YmdHis') . mt_rand(1000, 9999); $id = Db::name('repair_order')->insertGetId([ 'order_no' => $orderNo, 'user_id' => $data['user_id'], 'property_id' => $data['property_id'], 'community_id' => $data['community_id'], 'category' => $data['category'], 'title' => $data['title'], 'description' => $data['description'] ?? '', 'images' => json_encode($data['images'] ?? []), 'priority' => $data['priority'] ?? 0, 'status' => self::STATUS_PENDING, 'created_at' => date('Y-m-d H:i:s'), ]); // 自动派单 $this->autoAssign($id); return ['id' => $id, 'order_no' => $orderNo]; } /** * 自动派单 * 根据报修类别和区域,匹配有空的维修人员 */ protected function autoAssign(int $orderId): void { $order = Db::name('repair_order')->where('id', $orderId)->find(); // 查找该小区、该类别下的可用维修人员 $handler = Db::name('staff') ->alias('s') ->join('staff_skill ss', 's.id = ss.staff_id') ->where('s.community_id', $order['community_id']) ->where('ss.category', $order['category']) ->where('s.status', 1) ->where('s.current_load', '<', 5) // 当前工单数少于5 ->order('s.current_load', 'asc') ->find(); if ($handler) { $this->assign($orderId, $handler['id']); } // 如果没有可用人员,工单留在待接单池 } /** * 派单 */ public function assign(int $orderId, int $handlerId): bool { Db::startTrans(); try { $order = Db::name('repair_order') ->where('id', $orderId) ->lock(true) ->find(); if ($order['status'] != self::STATUS_PENDING) { throw new \Exception('工单状态不允许派单'); } Db::name('repair_order') ->where('id', $orderId) ->update([ 'handler_id' => $handlerId, 'status' => self::STATUS_PROCESSING, 'assigned_at' => date('Y-m-d H:i:s'), ]); // 更新维修人员负载 Db::name('staff') ->where('id', $handlerId) ->inc('current_load', 1) ->update(); Db::commit(); return true; } catch (\Exception $e) { Db::rollback(); throw $e; } } /** * 完成工单 */ public function complete(int $orderId, int $handlerId, string $result = ''): bool { Db::startTrans(); try { $order = Db::name('repair_order') ->where('id', $orderId) ->lock(true) ->find(); if ($order['handler_id'] != $handlerId) { throw new \Exception('无权操作此工单'); } if ($order['status'] != self::STATUS_PROCESSING) { throw new \Exception('工单状态不允许完成'); } Db::name('repair_order') ->where('id', $orderId) ->update([ 'status' => self::STATUS_COMPLETED, 'completed_at' => date('Y-m-d H:i:s'), ]); // 减少维修人员负载 Db::name('staff') ->where('id', $handlerId) ->dec('current_load', 1) ->update(); // 发送通知给业主,邀请评价 $this->notifyOwner($order['user_id'], $orderId); Db::commit(); return true; } catch (\Exception $e) { Db::rollback(); throw $e; } } }
自动派单的负载均衡是报修模块的一个关键设计。系统不是随机分配,而是根据维修人员的当前工单数(current_load)选择负载最低的人员。这避免了"某个维修人员忙死,其他人员闲着"的情况。
四、活动报名与商城模块
4.1 活动报名
活动报名是物业系统的社区服务功能。它的核心是"报名-审核-参与-签到"的链路。
CREATE TABLE `activity` ( `id` int(11) NOT NULL AUTO_INCREMENT, `community_id` int(11) NOT NULL, `title` varchar(200) NOT NULL, `cover` varchar(255) DEFAULT NULL, `description` text, `location` varchar(255) DEFAULT NULL, `start_at` datetime NOT NULL, `end_at` datetime NOT NULL, `signup_deadline` datetime NOT NULL, `max_participants` int(11) DEFAULT NULL COMMENT '人数上限,NULL不限', `need_review` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否需要审核', `status` tinyint(1) NOT NULL DEFAULT '1', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_community_status` (`community_id`, `status`), KEY `idx_time` (`start_at`, `end_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `activity_signup` ( `id` int(11) NOT NULL AUTO_INCREMENT, `activity_id` int(11) NOT NULL, `user_id` int(11) NOT NULL, `name` varchar(50) NOT NULL COMMENT '参与人姓名', `phone` varchar(20) NOT NULL, `remark` varchar(200) DEFAULT NULL, `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0=待审核 1=已通过 2=已拒绝 3=已取消', `checked_in` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已签到', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_activity_user` (`activity_id`, `user_id`), KEY `idx_activity_status` (`activity_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
活动报名的并发控制与拼团类似:多人同时报名时,需要保证人数不超过上限。
<?php // app/common/service/ActivityService.php namespace app\common\service; use think\Db; use think\facade\Redis; class ActivityService { /** * 报名活动 */ public function signup(int $activityId, int $userId, array $data): array { $activity = Db::name('activity')->where('id', $activityId)->find(); if (!$activity || $activity['status'] != 1) { throw new \Exception('活动不存在或已结束'); } if (strtotime($activity['signup_deadline']) < time()) { throw new \Exception('报名已截止'); } // 检查是否已报名 $exists = Db::name('activity_signup') ->where('activity_id', $activityId) ->where('user_id', $userId) ->count(); if ($exists) { throw new \Exception('您已报名该活动'); } // 人数限制的原子扣减 if ($activity['max_participants'] > 0) { $key = "activity:quota:{$activityId}"; if (Redis::exists($key)) { $remain = Redis::decr($key); if ($remain < 0) { Redis::incr($key); throw new \Exception('报名人数已满'); } } } Db::startTrans(); try { Db::name('activity_signup')->insert([ 'activity_id' => $activityId, 'user_id' => $userId, 'name' => $data['name'], 'phone' => $data['phone'], 'remark' => $data['remark'] ?? '', 'status' => $activity['need_review'] ? 0 : 1, 'created_at' => date('Y-m-d H:i:s'), ]); Db::commit(); return [ 'need_review' => (bool)$activity['need_review'], 'message' => $activity['need_review'] ? '报名成功,等待审核' : '报名成功', ]; } catch (\Exception $e) { Db::rollback(); if ($activity['max_participants'] > 0) { Redis::incr("activity:quota:{$activityId}"); } throw $e; } } /** * 签到(扫码或输入签到码) */ public function checkIn(int $activityId, int $userId): bool { $signup = Db::name('activity_signup') ->where('activity_id', $activityId) ->where('user_id', $userId) ->where('status', 1) ->find(); if (!$signup) { throw new \Exception('未找到有效的报名记录'); } if ($signup['checked_in']) { throw new \Exception('您已签到'); } Db::name('activity_signup') ->where('id', $signup['id']) ->update([ 'checked_in' => 1, 'checked_in_at' => date('Y-m-d H:i:s'), ]); return true; } }
4.2 商城模块的集成
物业系统的商城模块与独立商城系统类似,但有两个特殊点:商品主要面向本小区业主,配送通常是到物业中心自提或送货上门。
CREATE TABLE `mall_order` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL, `user_id` int(11) NOT NULL, `community_id` int(11) NOT NULL, `property_id` int(11) DEFAULT NULL COMMENT '关联房产,用于送货', `total_amount` decimal(10,2) NOT NULL, `pay_amount` decimal(10,2) NOT NULL, `delivery_type` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0=自提 1=送货上门', `delivery_address` varchar(255) DEFAULT NULL, `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0=待付款 1=已付款 2=配送中 3=已完成 4=已取消', `paid_at` datetime DEFAULT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_status` (`user_id`, `status`), KEY `idx_community` (`community_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
商城模块的工程实现与标准商城系统类似,此处不再展开。关键差异在于:配送范围限定在本小区,订单创建时校验用户的 property_id 是否属于该 community_id。
五、CentOS 部署实践
5.1 环境配置
物业管理系统运行在 CentOS 环境中,需要配置以下组件。
# 1. 安装基础软件 yum install -y epel-release yum install -y nginx mysql-server redis php php-fpm php-mysql \ php-redis php-gd php-curl php-mbstring php-xml # 2. 启动服务 systemctl start nginx systemctl start mysqld systemctl start redis systemctl start php-fpm # 3. 设置开机自启 systemctl enable nginx mysqld redis php-fpm
5.2 Nginx 配置
server { listen 80; server_name property.example.com; root /var/www/property/public; index index.php index.html; # ThinkPHP 伪静态 location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; break; } } # PHP 处理 location ~ \.php$ { fastcgi_pass unix:/run/php-fpm/www.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } # 禁止访问敏感目录 location ~ ^/(runtime|vendor|\.env) { deny all; } # 静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { expires 30d; add_header Cache-Control "public, immutable"; } # 上传文件大小限制 client_max_body_size 20m; }
5.3 定时任务
物业系统需要多个定时任务来处理账单生成、逾期提醒、工单超时等。
# 编辑 crontab crontab -e # 每月1日生成物业费账单 0 2 1 * * cd /var/www/property && php think bill:generate --period=$(date +\%Y-\%m) # 每天上午9点发送缴费提醒 0 9 * * * cd /var/www/property && php think bill:remind # 每小时检查超时工单 0 * * * * cd /var/www/property && php think repair:check_timeout # 每天凌晨清理过期数据 0 3 * * * cd /var/www/property && php think system:cleanup # 队列消费者(使用 supervisor 守护) php think queue:work --queue notification,bill --daemon
5.4 数据备份
物业系统的数据涉及业主信息、缴费记录、工单记录,备份至关重要。
#!/bin/bash # /opt/backup/property_backup.sh BACKUP_DIR=/opt/backup/property DATE=$(date +%Y%m%d) DB_NAME=property # 备份数据库 mysqldump -u root -p"$DB_PASSWORD" $DB_NAME | gzip > \ "$BACKUP_DIR/db_${DATE}.sql.gz" # 备份上传文件 tar -czf "$BACKUP_DIR/uploads_${DATE}.tar.gz" \ /var/www/property/public/uploads # 删除7天前的备份 find $BACKUP_DIR -name "*.gz" -mtime +7 -delete echo "Backup completed: $DATE"
六、边界与张力
物业管理系统在工程实践中,有几个需要清醒认识的边界。
第一,数据隐私的敏感性。 物业系统存储业主的姓名、电话、房号、缴费记录,这些是高度敏感的个人信息。系统需要做数据脱敏(如日志中不记录完整手机号)、访问审计(记录谁查看了哪些业主数据)、以及符合《个人信息保护法》的要求。
第二,线下流程与线上流程的耦合。 物业管理的很多流程是线下的:维修人员实际去业主家维修、业主实际到物业中心缴费、活动实际在小区广场举办。系统需要支持这些线下环节的记录和确认(如维修完成需要业主确认、线下缴费需要人工录入)。
第三,多小区 SaaS 化的挑战。 如果系统要服务多个小区(SaaS 模式),数据隔离的要求比单小区更严格。不同小区的业主数据不能互相可见,不同小区的物业人员不能跨小区操作。这需要在数据模型层面引入 community_id 作为隔离维度。
第四,老旧小区的数字化适配。 很多老旧小区的业主以老年人为主,他们可能不熟悉小程序操作。系统需要提供简化版的界面、语音提示、以及线下人工协助的通道。这是产品层面的设计问题,但会影响技术方案的选择。
第五,与现有物业系统的集成。 很多小区已经使用了门禁系统、监控系统、电梯控制系统。新系统需要与这些系统集成,而不是替代它们。这要求系统提供开放的 API 和 webhook 机制。
结语:物业系统的工程本质是"多角色 + 多流程 + 强隐私"
UniApp 小区物业管理系统的核心工程挑战,可以归结为三个关键词。
多角色 意味着权限模型必须同时处理功能权限和数据权限。RBAC 解决功能权限,data_scope 解决数据权限,两者结合才能保证"业主只能看到自己的数据,物业人员只能看到本区域的数据"。
多流程 意味着系统要同时管理物业费缴纳、报修工单、活动报名、商城订单四条独立的业务链路。每条
职场区


暂无评论