这个项目管理的是一个共有 1178 个摊位的农贸市场。摊位分布在不同区域,既有室内铺位,也有室外摊位。管理员每天要确认哪些位置空着、哪些租约快到期、某个租户租了哪些位置,以及一笔钱到底对应哪份租约。

这些事情单看都不复杂,放在一起就很容易变成一堆互相打架的表格。项目最后落成了一套由 API、管理后台和微信小程序组成的系统,管理员可以在手机上完成大多数日常操作,复杂配置和集中处理则放在后台完成。

先把摊位放回真实位置

租赁管理最先遇到的问题不是表单,而是“这个编号在市场的什么地方”。只看列表,管理员还要在脑子里把编号和现场位置对应起来;摊位数量一多,这种记忆成本很快就会变成错误。

小程序首页先展示区域概览,再进入楼层或区域平面图。平面图上的每个标记都和一个摊位绑定,点击后可以看到编号、类型、面积、租金、标签和当前状态。标记颜色随着状态变化,空置、已租、即将到期和禁用不需要再靠人工解释。

资产管理首页

平面图使用小程序原生 canvas 绘制,支持拖动、双指缩放、按钮缩放和重置视图。下面的内容面板可以继续搜索和筛选摊位,选中后再查看租户、租约和收款信息。地图和详情没有被拆成两个互不相干的页面,现场人员可以从位置直接进入业务操作。

状态由租约计算出来

摊位状态不能只靠一个手动字段维护。系统会读取当前生效租约,根据开始时间和结束时间计算实际状态:没有生效租约的是空置,距离结束超过 30 天的是已租,30 天内到期的是即将到期,手动禁用的摊位则始终保持禁用状态。

平面图与摊位状态

这样做有两个好处。仪表盘、平面图和租赁列表看到的是同一套结果,管理员也不需要在续租或到期后再到多个页面改状态。到期提醒同样建立在租约日期上,登录后可以直接看到需要跟进的记录。

租户、租约和收款各自负责一件事

数据模型没有把所有内容塞进“摊位表”。摊位记录位置、类型、面积和基础价格;租户记录联系人、电话、身份证和地址;租约记录起止日期、押金、租金、合同图片和备注;收款记录则保存收款方式、金额、日期、经办人、收据号和附件。

这种拆分让几个常见场景变得直接:一个租户可以同时租多个摊位,一个摊位可以保留历史租约,同一份租约下可以连续添加多次收款记录。查看租户时能看到他的全部租赁和收款,查看摊位时也能沿着当前租约追到对应的人和金额。

系统里的收款是线下业务的存档,不负责发起支付。微信转账和现金只是收款方式,金额、收据和图片被记录下来,后续查询时有明确的业务上下文。

小程序负责现场操作,后台负责集中管理

微信小程序的入口围绕日常动作组织:区域和摊位、租户、租赁、收款。新增租赁时可以从资产选择租户,也可以从租户反向选择资产,新增完成后直接回到详情页。列表接口都支持分页,避免一次把几百条甚至更多数据塞进首屏。

管理后台提供了更适合桌面端的统计和维护界面,包括摊位区域、平面图标记、租户、租约、收款和标签等模块。后台和小程序共用同一个 GraphQL API,业务规则只在服务端实现一份,前端只负责展示和交互。

后端按业务模块组合

API 使用 Rust 编写,基于 Axum 和 async-graphql 提供 GraphQL 服务,数据层使用 Toasty。项目把认证、媒体、摊位、租户、租约、收款记录和仪表盘拆成独立模块,再通过 bozor profile 组合成一个可运行的网关。

摊位模块还单独处理了规格、价格模板、位置绑定和平面图标记。媒体文件可以走本地存储或对象存储,合同、身份证和收据图片不会和业务字段混在一起。模块化的好处是,业务变化时可以沿着边界修改,不必在一个巨大的接口文件里寻找所有关联逻辑。

落地时真正花时间的地方

这类系统的难点通常不在增删改查,而在细节是否能对上现场。

一是编号和位置必须稳定。平面图标记单独保存几何信息,并通过绑定关系指向摊位,调整标记不会误删摊位资料。二是状态要有统一来源,列表、统计和详情不能各算一遍。三是数据量要按真实规模设计,区域、摊位、租户和租赁列表都使用分页和搜索。四是历史信息不能被覆盖,续租和换租之后仍然需要查到过去的租户和合同。

项目已经完成落地,日常管理所需要的核心链路已经收进同一套系统:从平面图找到摊位,查看当前租约,补录收款,再回到仪表盘跟进即将到期的记录。在线支付、租户自助入口等不属于当前范围的能力则被明确留在系统之外,避免为了“看起来完整”把业务边界做得含糊。

对这类管理系统来说,真正有价值的结果不是页面数量,而是现场人员能不能少做几次重复确认,少在不同表格之间来回查找。把位置、人员、合同和收款放进同一条可追溯的链路,系统才算真正进入日常工作。