每年新生报到那几天,宿舍分配这件事总能把后勤老师逼到崩溃边缘。床位是否够用、哪些专业需要集中在同一层、报到现场有没有房间可分配、紧急报修该找谁,这些信息如果还在靠Excel表格和微信群来回传递,效率低不说,特别容易出错。我最近在业余时间做了一套新生宿舍管理系统,客户端用的是Flutter,同时适配OpenHarmony环境,服务端自己用Java搭了一套轻量接口。项目做下来,收获很大,尤其是整体架构设计和数据结构这部分,踩了不少坑,也沉淀出一些很实用的经验。
当初定技术栈时,我其实纠结过一阵子。Flutter的优势很明显,一套Dart代码能同时跑Android、iOS,现在OpenHarmony生态也在逐步接入Flutter引擎,意味着未来可以低成本覆盖更多设备形态。而OpenHarmony这边对Flutter的适配虽然还处在快速迭代期,但社区已经有人在生产项目里跑通了。这个场景很适合宿舍管理这类工具型App:界面偏表单、列表、卡片,不需要复杂的原生能力,Flutter完全能扛。加上我对状态管理和数据层封装比较熟,所以最终敲定了Flutter加OpenHarmony的组合。
这套系统覆盖的流程不算简单:新生报到后要录入基础信息,管理员要能查看床位余量、手动或自动分配宿舍,学生入住后要能发起报修,辅导员要能发通知公告。业务流程多点,所以数据表不少。这篇文章我不想只贴一堆代码,而是想把“整体架构怎么拆、表结构为什么这么设计、分配逻辑怎么保证不出错”这些思路完整梳理一遍,尤其重点讲数据建模和几个容易翻车的细节。
1. 项目需求画像与整体架构设计
1.1 宿舍管理业务到底要解决哪些事
开始动工前,我先把业务流程走了一遍。宿舍管理的日常操作场景,大致能分成四个板块:
一是新生报到和宿舍分配。这属于链条的入口,涉及新生信息录入、宿舍楼栋和房间的容量管理、分配规则处理、分配记录留痕。分配规则是这里的核心难点:同专业尽量集中、男女生不同楼栋或不同楼层、部分楼栋按学院统一划分,规则一旦变化,数据模型必须能兜得住。
二是住宿日常管理。包括入住登记、退宿登记、调宿申请。这个板块虽然看着简单,但它会直接影响房间实时的“已住人数”和“床位状态”。很多系统做到后面数据对不上,不是因为逻辑复杂,而是退宿和调宿时忘记同步更新房间的占用状态。
三是报修和后勤处理。学生端发起报修,后勤管理员接单、派单、处理、反馈。报修工单的“状态”是数据设计的重点,需要从“待受理”一步步流转到“已完成”,每一步都有操作人和时间记录。
四是对外通知触达。例如入住须知、停水停电提醒、违规电器检查通知。通知要能按楼栋或特定年级范围推送,所以在表结构上不能只做全局广播,还需要支持定向范围。
把这四个板块拆开以后,我的直观感受是:这个系统的业务逻辑不算重,但“状态”特别多,而且状态之间还有依赖关系。比如新生必须完成报到,才能进入待分配状态;必须分配了宿舍,才能发起报修。所以我在设计数据结构时,几乎把所有核心实体都加上了“状态字段”,并且用类型约束和枚举规范来保证状态不被人为写花。
1.2 Flutter与OpenHarmony结合时的架构取舍
客户端架构我参考了经典的Clean Architecture思路,但没有把它做得很重,而是结合Flutter开发习惯做了简化。整体分成三层:UI表现层、业务逻辑层、数据持久层。
UI表现层全用Flutter的Widget搭建,页面严格控制为“瘦页面”,只负责渲染和用户交互事件,不直接写业务代码。业务逻辑层用ChangeNotifier配合Repository模式来处理状态和调度数据,例如宿舍分配、报修提交这些动作都在这一层完成。数据持久层把本地SQLite和远端API封装成统一的Repository接口,UI层拿到的是干净的领域模型,完全不感知数据来源。
选择这套架构最主要的原因,是OpenHarmony环境和标准Android环境在某些原生能力上存在差异。如果把数据操作直接写在页面里,后续要换底层存储或者适配新的平台接口时,改动量会很大。但用Repository把数据源头隔离以后,我只改每个平台的实现类,UI层和业务层完全不用动。实测下来,这种解耦方式在跨端项目里比什么都重要。
通信机制上,客户端与服务端走的是RESTful API,本地用SQLite做缓存和离线兜底。可能会有朋友问:既然有服务端,为什么还要本地缓存?因为在宿舍这种场景里,楼道里的网络信号真的说断就断。如果宿舍管理员在负一层巡查时想查某个房间的入住情况,网络却连不上,本地缓存就能保证基本查询不中断。这个设计后来在实际测试中救了我好几次。
1.3 工程结构长什么样
工程代码我按业务模块做了目录拆分。项目根目录下主要分出这几个一级目录:app(Flutter入口和页面路由)、domain(实体模型和仓储接口定义)、data(本地数据库、网络服务和仓储实现)、core(组件、工具类、常量枚举),然后OpenHarmony相关的平台适配代码单独放在platform目录下。
这样的模块切分让我在开发时特别省心。比如后面要调整数据库表结构,我只需要去data目录下修改数据库版本迁移脚本,完全不会碰UI代码。再比如想给某个界面增加一个新字段展示,我只需要在domain模型里加属性,然后改对应的详情页。边界清晰,自然就不容易出现“改一处崩三处”的问题。
不过架子搭得好,不代表代码量会减少。事实上,分层架构的前期会比直接堆代码多写不少接口类和适配类。它的收益在中后期才明显:当业务逻辑复杂到一定程度时,只有边界清晰的架构才能撑住。
2. 数据结构设计实战
2.1 理清实体关系是第一步
数据结构是整个项目的灵魂。我画完业务流程图后,梳理出的核心实体包括:学生(新生)、楼栋、房间、床位、宿舍分配记录、报修工单、公告通知、系统用户。它们之间的关系大概是:
一个楼栋下挂多个房间,每个房间有多个床位。一个学生只能有一条在住状态的有效分配记录,但历史分配记录可以有多条。一个房间可以有多条报修工单,一条工单归属某个房间或某个学生。公告则可以关联到多个楼栋,形成发布范围。
实体关系图里最容易忽略的是“床位”这个实体。很多人会把床位做成房间的一个数量字段,例如“房间容量4人,已住3人”,但这样做有隐患:如果我想单独记录某个床位的状态(比如靠窗床位、下铺床位优先分配),或者想精确追踪某个学生具体住在哪个床位,就无从下手。所以我在设计时还是专门把床位拆成独立实体,键关联到房间。
2.2 核心表与字段设计详解
数据库我选的是SQLite,主要考虑是本地优先、零配置、适合工具类应用。服务端如果以后要换MySQL或PostgreSQL,我会在仓储实现层做切换,前端代码不受影响。
学生信息表设计时,我特别关注“报到状态”和“分配状态”的区分。新生报到一个流程,宿舍分配是另一个流程,两者不能混在一个状态里,否则会出现量状态判断困难的问题。表中的核心字段大概是:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INTEGER | 主键,自增 |
| student_no | TEXT | 学号(业务唯一键) |
| name | TEXT | 姓名 |
| gender | INTEGER | 性别,0男1女 |
| college | TEXT | 学院 |
| major | TEXT | 专业 |
| report_status | INTEGER | 报到状态:0未报到,1已报到 |
| assign_status | INTEGER | 分配状态:0未分配,1已分配 |
| phone | TEXT | 联系电话 |
| create_time | TEXT | 创建时间 |
这里有一个细节很关键:学号一定要加唯一索引。虽然系统里主键用的是自增id,但业务上层同学号不能重复导入两次,当Excel批量导入新生数据时,唯一索引能从数据库层面挡住重复数据,而不是靠应用层判断。
房间表的核心字段是楼栋关联、房号、性别限制、容量和当前已住人数。特别注意,已住人数这个字段不能依赖“count(床位)”实时统计——在分配场景里,查余量是非常高频的操作,每次都去做聚合查询会慢。更好的做法是维护“已住人数”字段,通过分配和退宿操作来自增自减,同时用事务保证数据一致。用空间换时间,在这种业务场景下非常划算。
分配记录表是最关键的表之一。每一次宿舍分配操作都要在这里留痕,无论这个分配是成功还是回滚。它要记录学生id、房间id、床位id、分配类型(初分配、调宿、退宿)、操作人id、操作时间、状态。有了这张表,以后审计“为什么这个学生住了这间房”、“这个床位被谁住过”都能追溯清楚。
报修工单表我会单独强调一下,它的字段不少:报修人id、关联的房间id、报修类型(水、电、门窗、空调、其他)、问题描述文字、现场图片路径(本地缓存)、状态、受理人id、处理备注、创建时间和完成时间。其中状态我用的是一个整形常量:0待受理、1处理中、2已完成、3已关闭。用整数存状态,界面映射成不同的标签颜色和文案,这样比直接存中文状态值更省空间,也更规范。
整体来看,这套数据结构不算复杂,但它覆盖了业务流程所需的全部关键路径,同时也为后续功能扩展留了余地。加个“晚归登记”的子系统,也不用动核心这几张表。
2.3 状态机设计与数据约束的细节
很多初学者设计表结构时,只关注字段够不够,却忽略了业务状态之间的约束。宿舍管理这个场景里我最看重三个约束:
第一个是分配状态与入住状态的一致性。学生状态是“已分配”时,分配记录表中必须存在一条关联到该学生的在住记录。理论上不能出现“状态显示已分配,但查不到具体房间”的矛盾数据。
第二个是房间容量与床位状态的强同步。当我分配一个学生进某个房间时,房间的“已住人数”字段必须一起变,这个过程要放在同一个数据库事务里。我甚至把分配逻辑写成一条事务方法,任何一个环节失败了,整个事务回滚,不会留下半分配状态。
第三个是报修工单状态的合法流转。状态只能从“待受理”到“处理中”,再到“已完成”,不允许从“待受理”直接跳到“已完成”,也不允许逆向流转。我在代码里做了一个状态流转表,每次更新都先校验当前状态和下一个状态的合法性。
3. 核心模块实现与关键流程
3.1 初始化和建库的落地方式
我用了sqflite这个Flutter插件来做本地数据库,在OpenHarmony环境里,需要配合对应的适配库。数据库初始化的步骤,通常写成异步方法,在应用启动后的初始化阶段调用。
建立数据库时,我把建表语句放在一个常量列表里,版本号从1开始。每次数据库结构发生变化,我不会直接改旧表,而是新增迁移脚本。这样做的好处是,老用户从旧版本升级时,系统能自动执行新增字段的Alter语句,不会因为缺字段直接崩溃。
建表语句最值得注意的就是各种约束。例如房间容量必须大于0,楼层号必须大于等于1,床位号唯一约束采用复合唯一索引(房间id加床位号)。这些约束能挡住很多应用层漏掉的脏数据,实测下来,写SQL时把约束考虑进去,后期调试会轻松很多。
3.2 宿舍自动分配的核心逻辑
宿舍自动分配是这个系统最有技术含量的部分。我实现的基本思路是先分组再塞房间:把未分配的新生按“性别、学院”两个维度分成若干小组,然后遍历每个小组,组内学生按学号排序,逐个分配到匹配的房间中。
匹配房间的规则是:先找同一栋楼中“性别限制匹配、当前已住人数小于容量”的房间,优先选已住人数不为0但没满的房间;如果找不到,再开新房间。这种策略能让房间尽量紧凑使用,不至于住得稀稀拉拉。
核心一段分配逻辑的流程是这样的:先查出待分配学生列表,然后查出所有候选房间和床位,在内存中组合匹配,组装好分配记录和房间更新数据后,统一在一个数据库事务里执行。这里我没有一条条插入和更新,而是使用批量操作,在数据量大的时候性能差异非常明显,几百个新生一瞬间就能分完。
当然,自动分配的前提是房间和床位数据必须是“干净”的。如果房间的已住人数字段有误,分配结果就会错乱,所以我设计了“房间床位数量校准”工具:系统提供一个手动触发功能,去统计一个房间节点下实际入住的分配记录条数,并同步更新已住人数字段。这个工具在后来的测试中帮了大忙,因为最早的旧数据里很多已住人数都是不准的。
3.3 Flutter界面与状态管理的配合
Flutter端的界面我用的是常见的BottomNavigationBar结构,分成三个主Tab:首页(我的住宿)、服务(报修和公告)、管理(宿舍管理员的分配入口)。状态管理我选择了Provider加ChangeNotifier的方式,没有引入太重的大型状态管理框架。因为系统页面数量不多,状态管理用Provider已经绰绰有余。
不过有个地方值得单独分享:同一个页面在普通用户跟管理员之间,展示的数据和操作入口完全不同。比如学生端看“宿舍详情”,只需要看自己的床位号和报修按钮;管理员端看“宿舍详情”,需要看到所有入住的床位列表、可调整床位的按钮。我在设计时没有做两套页面,而是共用一套Widget,根据角色字段做条件渲染。这样代码复用性高,但判断逻辑会在页面里增加一些复杂度,所以我封装了一个“当前用户角色”的Provider,界面通过读取角色状态来做分支。
表单录入这块,我尽量用Form加TextFormField实现统一校验。新生报到时,要填姓名、学号、学院、专业、手机号。校验规则并不复杂,但学号和手机号的格式校验一定要做,不然脏数据进了数据库,后续查起来非常麻烦。
4. 常见问题与排查技巧实录
4.1 本地缓存与服务端数据不一致
开发过程中我遇到的第一个比较严重的问题是本地SQLite缓存的脏数据问题。有时候服务端数据变了,但本地缓存没有同步更新,界面就显示旧数据。排查下来发现原因是我的Repository层没有做“缓存过期”控制:本地有缓存,就直接返回缓存,完全不走网络。
后来我加了一个简单的过期机制:每次App从后台恢复时,强制刷新核心数据(房间余量、分配状态);同时每个Repository的查询方法都支持带一个“forceRefresh”参数,强制刷新时直接跳过本地缓存。改完以后一致性好了很多。
4.2 Flutter在OpenHarmony平台上的字体和布局兼容
Flutter跑到OpenHarmony设备上后,我发现部分中文字体渲染的排版效果跟Android上不太一样。具体表现是某些页面的文字没有完全撑满,或者间距偏大。排查后确认这是不同平台默认字体度量差异导致的。解决方案是在MaterialApp主题里显式设置统一的中文字体族,并在容易出问题的页面上使用固定的TextStyle行高倍数。
在这个坑上我建议多写“自适应”而非“固定尺寸”,例如用Flexible配合Expanded控制布局。毕竟宿舍管理员的终端设备五花八门,屏幕尺寸差距很大,固定宽高很容易在小屏设备上溢出。
4.3 自动分配万一出现并发冲突怎么办
宿舍分配这个场景通常不会有太高的并发,但迎新报到高峰时,多个管理员可能同时操作分配功能。比如A管理员正在给某学生分配一间还剩一个床位的房间,B管理员同时也在分配同一个床位,两边读到的数据都是“还有1个床位”,结果就会重复分配。
为了解决这个问题,我在分配逻辑里加入了数据库行锁。在更新房间的已住人数之前,先用“SELECT … FOR UPDATE”锁定房间记录,确保只有拿到锁的会话才能继续操作。这个修改虽然简单,但从根本上避免了超卖式分配冲突。代码部署层面,如果以后愿意,可以换到服务端Redis分布式锁,但在单机数据库环境下,行锁已经完全够用。
4.4 数据库升级时遇到旧版本脏数据
有次更新表结构后,我发现部分老设备的App升级后崩溃率特别高。查日志定位到是新加了一个非空字段,但数据库迁移时没有给默认值,导致旧数据读取失败。
从那以后我在编写迁移脚本时会额外加一步“历史数据清洗”:先备份原表,然后执行SQL补全缺失字段的默认值,最后再改名替换回正式表。同时,所有迁移脚本都必须先在有几万条旧数据的测试库里跑一遍,确认执行时间和兼容性后再合入正式版本。这是给我印象很深的一个教训,虽然过程痛苦,但之后就再也没有出现升级即崩溃的问题。
5. 实测体验与经验总结
5.1 系统功能实测记录
系统跑通后,我在一个模拟的数据集里做了完整的功能测试。数据集模拟了一个五层楼宿舍,每层有十六个房间,要求里面男女生各分配一栋,并按照学院分区域集中。测试数据包含两百多个新生。实测起来,自动分配的耗时不到一秒,所有学生都正确落入房间,房间余量也都正确更新。整体流畅度,在几年前的硬件设备上依然很稳。
界面的响应速度是我比较满意的点。得益于本地缓存和轻量状态管理,页面跳转基本没有卡顿感,列表滚动也很顺滑。在宿舍管理系统这种密集操作表单和列表的场景里,这个体验我已经很满意了。
5.2 整体复盘与经验心得
做完整个项目,我最大的体会是:这类管理系统,数据结构设计一定要先于界面开发。很多功能看着简单,但是一旦数据表没有设计好,后期加字段、修数据会把人折腾到崩溃。我个人现在做项目的习惯是,先花时间梳理业务实体和状态流转,把表结构和关键事务逻辑确认清楚,然后再开始写页面代码。这个习惯能节省至少三分之一的返工时间。
另外一点:跨端开发中,官方原生的适配和社区插件往往跟不上最新版本的节奏。所以我在技术选型时都会谨慎评估插件维护的状态,宁可自己封装一个简洁的Platform Channel,也不用一个看起来功能强大但维护不活跃的第三方库。这个策略让我在OpenHarmony适配时少走了很多弯路。
这套系统现在还在持续迭代中,后续我计划加入宿舍水电表读数导入、晚归登记、节假日留校统计等模块。骨架已经搭好,加新功能不会伤筋动骨,这也是当初严格做架构设计和数据建模换来的长期收益。