导读:每天早高峰后,写字楼下的车位空着;晚高峰后,小区的车位空着。车位共享系统的核心挑战是:如何在正确的时间把正确的车位分配给正确的人?本文剖析这套系统的车位管理引擎设计。
本文结合实际项目设计进行拆解,建议先看看设计成果《车位共享预约云平台》,再阅读本文。
一、车位数据模型:多维度的资产描述
一个车位在系统中承载的信息远不止"可用/不可用"这么简单:
ZnParkingSpaceEntity
├── 物理属性: SpaceArea(分区) + SpaceSN(编号) + Location(位置)
├── 所属关系: ParkId(停车场) + PropComId(地产) + PropSerComId(物业)
├── 个人持有: PersonalUserId(车位主) + PersonalPercent(个人收益比)
├── 共享状态: Share(是否共享) + CheckStatus(审核状态)
├── 设备关联: LockId(地锁) → ZnLockEntity
├── 分账比例: PropComPercent + PropSerComPercent
└── 使用记录: → ZnParkingSpaceUsedEntity(一对多)
从资产角度看,一个车位 = 物理位置 + 所有权 + 收益权 + 设备绑定。
二、共享申请与审核流程
个人车位主要发布共享车位,需要走完整的审核流程:
第一步:申请发布
用户在小程序「我的车位」页面添加车位信息,提交后状态为:
Share = 0(未共享)CheckStatus = 0(待审核)
第二步:运营审核
运营后台管理人员审核车位的资质(是否确实拥有该车位、地锁是否安装完善等),审核通过后:
Share = 1(已共享)CheckStatus = 1(审核通过)
第三步:上架预约
只有 Share == 1 && CheckStatus == 1 的车位才会出现在用户的预约列表中:
strWhere = strWhere.And(t => t.Share == 1 && t.CheckStatus == 1 && t.ZnLock.Ng == 0);
同时还要求车位已安装地锁且地锁状态正常(Ng == 0),从物理层面保证共享车位可管控。
三、时间片碰撞检测:带缓冲区的排重算法
这是整个共享系统的核心技术难点——如何判断一个车位在某个时段是否可用?
朴素思路
一个车位用了就不能再预约——这过于简单粗暴,因为车位是按"时间段"共享的,而不是按"天"。
最终方案:区间重叠检测 + 缓冲时间
// 新预约时间: preST ~ preET
// 已有预约: 占用记录的 PreST ~ PreET
var buffer = 60; // 缓冲时间:分钟
// 碰撞条件:两个时间段有重叠(考虑了缓冲期)
var isUsed = parkingSpaceUsedRepository.GetQuery(x =>
x.Status == 1 // 占用记录有效
&& x.PreET.Value.AddMinutes(buffer) > preST // 旧结束+缓冲 > 新开始
&& x.PreST.Value.AddMinutes(-buffer) < preET // 旧开始-缓冲 < 新结束
).Any();
碰撞逻辑图解(时间轴从左往右走,[ ] 代表一个占用的时间段):
9:00 10:00 11:00 12:00 13:00
──┼──────────┼──────────┼──────────┼──────────┼──→
[我要预约: 10:30 ~ 11:30]
情况A:旧占用完全在左边,不重叠 ✓
[旧占用: 9:00 ~ 10:00]
[我要预约: 10:30 ~ 11:30]
情况B:旧占用的右端伸进了我的区间,重叠 ✗
[旧占用: 10:00 ~ 10:45]
[我要预约: 10:30 ~ 11:30]
情况C:旧占用被我完全包含,重叠 ✗
[旧占用: 10:40 ~ 11:00]
[我要预约: 10:30 ~ 11:30]
情况D:旧占用完全在右边,不重叠 ✓
[旧占用: 11:45 ~ 12:30]
[我要预约: 10:30 ~ 11:30]
判断是否重叠的数学本质是两个区间 [a, b] 和 [c, d] 重叠的条件是 a < d 且 c < b。翻译成车位逻辑即:旧占用的结束时间+缓冲 > 新预约的开始时间 并且 旧占用的开始时间-缓冲 < 新预约的结束时间 → 重叠,不可预约。
这里的 60分钟缓冲期 是人性化设计:前一个用户离场后,车位"休息"60分钟才释放给下一位。为什么需要缓冲?因为前一个用户可能延迟离场,如果无缝衔接,后一个用户到了发现车位还被占着,体验极差。
四、智能推荐:车位随机排序
用户选择车位时,系统并不是简单按编号排列,而是做了随机推荐:
// 后端:随机取 size 个可用车位
var source = currentRepository.GetQuery(strWhere)
.OrderBy(x => Guid.NewGuid()) // 随机排序
.Take(size) // 取8个
.ToList()
.OrderBy(x => x.SpaceSN); // 再按编号排列展示
// 小程序端:随机推荐
autoSelect() {
const size = this.data.parkingSpaceList.length;
const index = Math.floor(Math.random() * (size - 1));
// 自动填入一个随机车位
}
这种设计的巧妙之处在于:避免"热区效应"。如果没有随机化,某些编号靠前的车位会被优先选走,而编号靠后的车位无人问津,导致资源利用不均衡。
五、IoT地锁联动:从云端到物理世界
地锁数据模型
ZnLockEntity
├── LockSN — 地锁编号(硬件标识)
├── LockState — 地锁状态(0未知/1升/2降)
├── OnlineStatus — 在线状态
├── BattaryVol — 电池电压(设备运维)
├── Ng — 异常标识(故障检测)
└── InstallStatus — 安装状态
控制链路
用户点击"打开地锁"
→ 小程序API请求
→ 后端 Entrance() 方法
→ 调用IoT云平台(造作云)
→ 地锁执行器接收指令
→ 地锁下降
→ IoT上报结果
→ 后端更新 LockState
→ 小程序反馈
入场/出场联动
入场时:Entrance() 不仅控制地锁,还做了三件事:
- 校验订单状态是否为"已支付"
- 记录入场时间
ST = DateTime.Now - 订单状态流转至"停车中"
出场时:IoT上报关锁事件(携带 LiftNum 地锁编号),后端根据锁号找到对应订单,触发 Leave():
- 根据
LockSN找到ZnParkingSpace,再找到ZnOrder - 计算实际费用
- 处理退款/欠费
- 释放车位
// 关键:通过地锁编号反向查找订单
ZnOrderEntity order = await currentRepository
.FirstOrDefaultAsync(x => x.Status == 3
&& x.ZnParkingSpace.ZnLock.LockSN == model.LiftNum);
这种 锁号→订单 的反查机制意味着:IoT设备只需要上报"我是哪个锁、我怎么了",系统就能自动完成业务闭环,设备和业务完全解耦。
设备运维
系统还支持远程维护地锁:
UpdateStatus() — 更新在线状态(设备心跳)
UpdateBattery() — 更新电池信息(低电量预警)
UpdateNg() — 更新异常标识(故障上报)
每个运维操作都通过 [Logger] 注解记录了审计日志,便于排查问题。
六、数据看板:各方收益一目了然
订单查询时,系统会自动计算并展示丰富的统计信息:
预约时长 = Ceil((预约出场时间 - 预约入场时间).总分钟数)
实际停车时长 = Ceil((出场时间 - 入场时间).总分钟数)
地产分账 = 实际金额 × 地产收益比例
物业分账 = 实际金额 × 物业收益比例
个人分账 = 实际金额 × 个人收益比例
运营后台可以按停车场、地产公司、物业公司、个人等多维度筛选和查看经营数据。
七、设计精髓总结
- 审核-共享两层分离:审核通过后才可上架,保障交易安全
- 带缓冲的时间片排重:避免前后用户"撞车"
- 随机推荐机制:均衡车位利用率
- 锁号反查订单:IoT设备与业务逻辑解耦
- 设备远程运维:在线状态、电池、异常全程可监控
- 多方数据看板:各角色的收益清晰可查
从物理世界的车位锁,到云端的时间片调度算法,再到资金分账的自动化,「共享车位」的共享引擎打通了信息流、资金流和物流(设备控制流),是一套完整的物联网共享经济实践。
(全文完)有疑问或者想讨论的点,留言区见。