1. 这个项目到底在解决什么问题——维修厂管理的第一块拼图
先聊点实在的。我见过太多维修厂老板的日常:接车靠电话、报价靠嘴说、配件库存一本纸质账本、修到哪一步全凭师傅记性。车子进厂后客户问"师傅,我车什么时候能好",答复永远是"快了快了"——不是老板想糊弄客户,是真的不知道车现在处于哪个环节。这个痛点,就是"车维管家"想做掉的事情。
我最初拿到"车维管家:Flutter × Harmony6.0 车辆维修管理系统"这个需求时,没有急着写代码,先想清楚了一件事:维修管理系统这种工具,第一屏给谁看?看什么?答案不是给师傅看的工序表,而是给老板和前台看的"全局状态"。一个维修厂同时进厂十几台车,每台车在"待接车、检测中、维修中、待质检、待交车"哪个环节,哪台车已经拖了两天没动过,哪台车今天必须交付——这些信息必须在第一屏全部暴露出来,不需要点进任何一台车的详情页。所以"维修状态概览"不是列表页那么简单的存在,它实际上是整个系统的指挥面板,也是我这个项目里第一个动手实现的模块。
抛开 HarmonyOS 还是 Android 的争议不谈,选择 Flutter 做这套系统有几个很实际的理由:第一,Flutter 的组件渲染机制对"状态卡片 + 时间轴 + 进度指示"这类信息密度高的界面非常友好,ListView 加自定义的 StatefulWidget 能做出很流畅的状态切换动画;第二,车维管家后续大概率要同时出手机端、平板端甚至 Windows 桌面端(前台登记用),Flutter 一套代码多端跑的收益比原生开发大得多;第三,Harmony 6.0(我按当前公开的 ArkTS/ArkUI 生态理解,实际项目里用的是 Flutter 的鸿蒙适配分支)对 Flutter 的兼容已经能支撑生产级应用,这是这个项目敢启动的技术前提。
这篇文章就围绕"维修状态概览"这个模块从头拆一遍:从状态机设计、数据层搭建、UI 布局到性能优化和踩坑记录,把我实际开发过程中验证过的东西、推翻过的东西、最后沉淀下来的方案,完整分享出来。适合正在做 Flutter 开发、对鸿蒙端适配感兴趣、或者想用 Flutter 做一套进销存/维修类管理系统的朋友参考。
2. 状态机设计——比 UI 更值得先花时间的东西
2.1 维修状态的边界怎么划
很多新手一上来就写列表页,把"维修状态"当成一个字符串字段存进去,界面上直接 show 出来。这种做法在 Demo 里没问题,但放到真实维修厂场景里马上会露馅:状态之间不是孤立的,它有流转关系,有业务动作触发,有权限约束。比如"检修中"只能由"待检测"流转过来,不能从"待交车"跳回"检修中"(除非质检不合格返工,那是另一条路径)。如果不把状态流转规则定死,代码里就会出现一堆不可控的 if-else,今天改一个状态明天就出 bug。
车维管家的状态机我最终定了 7 个节点:
| 状态值 | 含义 | 触发动作 | 负责人 |
|---|---|---|---|
| PENDING_RECEIVE | 待接车 | 客户预约/到店登记 | 前台 |
| RECEIVED | 已接车待检测 | 确认接车、录入车辆信息 | 前台/接车员 |
| DIAGNOSING | 检测中 | 开始初检 | 检测技师 |
| WAIT_QUOTE | 待报价确认 | 生成维修方案和报价单 | 服务顾问 |
| REPAIRING | 维修中 | 客户确认报价、派工 | 维修技师 |
| QUALITY_CHECK | 质检中 | 维修完工提交质检 | 质检员 |
| WAIT_DELIVERY | 待交车 | 质检通过 | 前台 |
| COMPLETED | 已交车 | 客户确认取车 | 前台 |
这里有一个我在需求阶段纠结过的点:要不要把"待配件到货"单独拆成一个状态?后来没有拆。原因是在真实流程里,"等配件"和"维修中"往往并行——车已经拆开做能做的部分,同时等配件到位,如果拆成独立状态反而会让状态线变复杂,前端展示时一台车容易同时出现在"维修中"和"待配件"两个分组里,造成数据混乱。我用一个额外的布尔字段isWaitingParts+ 超时标记来解决配件等待的可视化,状态机保持单一维度。
2.2 Flutter 里的状态管理选型
状态机定好了,接下来要解决"状态放在哪里、怎么通知 UI 刷新"的问题。车维管家这个项目我调研过几种方案,最终选了Provider + ChangeNotifier,没有上 Bloc 也没有上 Riverpod,团队里有人质疑过,说现在 Flutter 社区不是流行 Riverpod 吗?我说项目体量决定工具复杂度,车维管家目前的页面量和状态共享范围,Provider 完全够用,而且它和 ChangeNotifier 的组合对新手团队的认知负担最小。维修厂的开发团队大概率不是专职 Flutter 工程师,可能还兼顾着其他业务,代码要能让接手的人快速看懂。
具体到代码组织上,我建了一个RepairOrderModel作为全局状态对象,继承ChangeNotifier,内部维护一个List<RepairOrder>和当前筛选条件,暴露loadOrders()、changeStatus()、filterByStatus()这些方法。UI 层通过Consumer或context.watch<RepairOrderModel>()订阅变化,这样无论是首页概览卡片点进去改状态,还是后台收到新派工单推送,只要调用changeStatus()方法,所有监听这个 Model 的页面都会同步刷新,不会出现"订单详情页改了状态,返回列表页还是旧数据"这种经典低级 bug。
class RepairOrderModel extends ChangeNotifier { List<RepairOrder> _orders = []; RepairStatus? _currentFilter; List<RepairOrder> get filteredOrders { if (_currentFilter == null) return _orders; return _orders.where((o) => o.status == _currentFilter).toList(); } void changeStatus(String orderId, RepairStatus newStatus, {String? operatorId}) { final index = _orders.indexWhere((o) => o.id == orderId); if (index == -1) return; final updated = _orders[index].copyWith( status: newStatus, statusHistory: [..._orders[index].statusHistory, StatusRecord( status: newStatus, time: DateTime.now(), operatorId: operatorId ?? '', )], ); _orders[index] = updated; // 这里会触发所有监听者刷新 notifyListeners(); } }关于状态变更的历史轨迹,这个字段是后来补上的,但实际用下来发现它特别重要。维修厂经常出现扯皮:客户说"我昨天就同意报价了,怎么今天还没修",前台说"我昨天确实点了确认报价,但系统里没记录"。有了statusHistory,每一次状态变更的时间点和操作人 ID 都留在数据里,界面上的时间轴组件直接读这个字段渲染,省去了单独建一张状态变更日志表的麻烦。
2.3 边界情况:异常流转和强制跳转
状态机不是死的,真实场景里总有特例。车维管家处理了几个必须允许的"非常规"流转:
- 取消订单:任意状态(已交车除外)都可以跳到一个
CANCELLED终止态,但要记录取消原因。 - 质检失败回退:
QUALITY_CHECK可以回退到REPAIRING,同时自动给维修技师生成一条返工任务。 - 客户到店才发现配件没货:从
WAIT_QUOTE回退到DIAGNOSING重新出方案。 - 紧急插单:新订单直接跳过待接车,进入
RECEIVED,由店长权限操作。
这些特例我统一放在一个StatusTransitionGuard类里做校验,UI 层要调changeStatus之前先问一下 Guard 这次流转是否合法。这样既保证状态机的规范性,又给业务留了足够的灵活性。写清楚这层约束,后续加权限系统或者多门店版的时候就不用重构状态逻辑。
3. 数据层搭建——本地优先加后端同步的取舍
3.1 为什么选本地数据库优先
修车厂的网络环境用过的都懂:车间里信号差,地下室举升机旁边一格信号都没有,前台 WiFi 偶尔还要掉线。如果系统架构做成"每次操作都必须请求后端接口",那么师傅在地沟里举着平板根本没法更新维修进度。所以车维管家的数据层一开始就确定了本地数据库优先、后端同步兜底的架构。
具体来说,业务操作全部先写本地 SQLite,界面即时刷新,然后通过一个同步队列异步往后端推送。等后端确认收到,本地数据打上"已同步"标记。这个思路借鉴了移动端 IM 类 App 的离线消息机制,用在这个场景非常合适。Flutter 里做本地数据库,首选自然是sqflite,它在移动端的生态最成熟、文档最齐全、踩坑解法满大街都是。不过 sqflite 在鸿蒙端适配目前还有些折腾,后面第 6 节我会详细讲这个问题。
本地表结构我只设计了必要的几张,最核心的是repair_orders表和status_history表,外加一张sync_queue表记录待同步的操作。其他涉及配件库存、客户档案这些基础数据,初期先走后端接口读,因为它们在维修过程中属于低频查询,不需要离线也能接受。
3.2 建表语句和 DAO 层封装
repair_orders表的核心字段如下:
CREATE TABLE repair_orders ( id TEXT PRIMARY KEY, car_plate TEXT NOT NULL, owner_name TEXT, owner_phone TEXT, service_advisor TEXT, assigned_technician TEXT, status TEXT NOT NULL DEFAULT 'PENDING_RECEIVE', is_waiting_parts INTEGER DEFAULT 0, description TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, sync_status INTEGER DEFAULT 0 ); CREATE TABLE status_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id TEXT NOT NULL, status TEXT NOT NULL, operator_id TEXT, created_at INTEGER NOT NULL, FOREIGN KEY (order_id) REFERENCES repair_orders(id) );synchronized_status用sync_status字段标记,0 表示未同步,1 表示已同步,2 表示同步失败待重试。注意我没有把时间存成字符串,而是存 Unix 毫秒时间戳。这个细节坑过不少人——如果存"2026-05-20 14:30:00"这种字符串,后续做时间筛选、排序、跨时区比较都要转来转去,存整数最省心。
DAO 层我用了一个比较简单但干净的做法:写一个RepairOrderDao类,所有数据库操作都收敛到这里,供上层Repository调用。没有引入floor这类 ORM 框架,原因同上:维修厂业务表结构不复杂,手写 SQL 反而直观可控。有一点要注意,SQLite 默认的batchUpdate能力有限,当同步队列一次性要刷几十条数据时,我用Batch接口把多条 SQL 打包在一个事务里执行,速度能提升一个量级。
3.3 同步队列的实现思路
同步队列是本地优先架构里最需要用心的地方。我的实现方案是:每次业务操作(改状态、录信息、传完工照片)都同时写两张表——业务表和sync_queue表,然后触发一个后台 isolate 去检查队列里有没有待同步的数据。有的话逐条 POST 到后端接口,成功后把业务表那条数据的sync_status改成 1,同时删除sync_queue里对应的记录。
这里有几个实际开发中确认过的注意事项:
- 网络状态变化要监听:我用
connectivity_plus监听网络恢复事件,从无网变有网的瞬间立刻触发一次同步,而不是等用户下一次操作。不然师傅在车间里改了一下午状态,回到前台连上 WiFi 后数据还躺在本地里,客户查询进度全是旧的,体验很差。 - 同步失败要重试但不是无限重试:连续失败 5 次之后,这条数据标记为"同步失败待人工处理",在设置页里给一个"手动重试同步"按钮,同时把失败原因存下来方便排查。无限重试会把流量耗尽,而且在后端挂掉时造成雪崩。
- 同步顺序必须和操作顺序一致:因为状态流转是有依赖的(不可能还没有"已接车"就先同步"维修中"),所以
sync_queue表里我用created_at排序,严格按时间顺序逐条同步,不搞并发批量提交。用事务包住"读取队列 + 更新同步状态 + 删除记录"这三个动作,防止并发重复提交。
这套设计有代价:后端接口必须支持幂等。同一个请求因为网络超时被客户端重发,服务端不能重复执行导致状态重复流转。我在后端接口里规定每个操作都带一个operationId(本地生成的 UUID),服务端保存已处理过的operationId,遇到重复的直接返回成功。这个约定在前端后端联调时一定要提前说清楚,不然同步队列一旦回放,数据就乱了。
4. 维修状态概览的 UI 实现——从卡片布局到细节打磨
4.1 信息架构:一屏之内看清所有在修车辆
概览页的终极目标是"一屏之内、三秒之内,搞清楚今天厂里所有在修车辆的状态"。所以我的信息架构没有采用常见的"顶部 Tab 切换不同状态"方案——那等于把问题藏起来了,用户还得一次次点击去切换查询。我选的是分组纵向列表 + 每组标题常驻的做法:整个页面是一个ListView,按状态分组展示订单卡片,每组有一个吸顶的分组头(SliverPersistentHeader),显示状态名称、当前组内车辆数、目标交车时间标记。
页面顶部是一排汇总统计卡片:今日进厂 / 待交车 / 维修中 / 超时提醒。用横向滚动的ListView或SingleChildScrollView实现,每张卡片一个 Angled 渐变底色(不同状态不同色系),点击跳转到对应的筛选列表。这排统计卡不只是好看,它承担了"管理动作入口"的功能,比如点"超时提醒"直接看到所有超过承诺交车时间的订单,不用一层层找。
考虑到平板端的使用场景(前台一般放一台平板当看板),我在LayoutBuilder里做了响应式判断:宽度大于 800 时,分组列表变成两列网格;宽度小于 800 时,回到单列卡片流。Flutter 在这方面天然有优势,同一个GridView只要改一下crossAxisCount和子项尺寸就能适配。
4.2 订单卡片的设计细节
单张订单卡片我包含了这些信息:车牌号(大字号,第一视觉层级)、客户姓氏、服务顾问、当前状态徽章、维修项目简述(两行截断)、进厂时间、承诺交车时间、是否在等配件的角标、以及一个圆形按钮用来快速推进到下一个状态(比如从"检测中"直接点按钮出报价单)。
这里的交互有个细节值得展开说:把"推进状态"这个动作直接放在卡片上,而不是让用户点进详情页才能操作。维修厂里最频繁的操作就是师傅修完一台车在工位上掏出手机点一下"完工",如果这个动作要经过"点卡片 - 进详情 - 找按钮 - 点击"四步,师傅嫌麻烦就不爱用。车维管家的卡片右下角放了一个圆形按钮,内容是"下一状态"的图标,点击后弹出ModalBottomSheet,列出所有合法流转目标状态并显示当前操作人,确认后调用changeStatus更新状态,卡片立即切换到新的分组并播放一个轻微的位置动画。
卡片上的"超时"视觉处理是另一个细节。有一条约定:如果当前时间已经超过promiseTime(承诺交车时间)且订单还没到COMPLETED或WAIT_DELIVERY,卡片左侧会多一条 4 像素宽的红条,同时卡片时间区域变红。这个设计非常朴素,但店长反馈这是概览页最有用的功能——以前他每天要靠记忆和翻本子去判断哪台车拖了时间,现在红色扫一眼就知道。
4.3 状态徽章和空状态的处理
状态徽章(StatusBadge)是一个小的StatelessWidget,接收RepairStatus枚举,根据状态返回不同背景色、前景色和文案。这里我要提醒一个容易被忽视的问题:颜色不要只用色相区分,还要兼顾色弱用户和光线不好的车间环境。所以徽章除了颜色,我还加了一个小的几何图标做双重编码——比如"维修中"是一个扳手图标,"待交车"是一个车钥匙图标。真到了阳光直射屏幕的车间里,纯靠颜色区分状态的界面基本上一片惨白。
空状态可以说是 B 端工具里最容易翻车的地方。概览页如果所有状态分组都是空的(比如刚开店还没接车),直接显示一个空白 ListView 会让用户以为系统坏了。我做了两套空状态:全空时显示一个大图标加"今日还没有维修订单"和"去创建第一张接车单"按钮;某个分组为空但其他组有数据时,分组头照常显示,下面显示一行小字"暂无该状态的车辆"。这行小字字号故意做得很小,颜色浅灰,这样它不抢占视觉注意力,但又明确告诉用户"这个分组不是加载失败,是真的没有数据"。
5. 状态流转操作面板——底层逻辑与交互实现
5.1 状态推进按钮的数据关联
考虑一个场景:技师在"维修中"卡片上点了"下一状态",系统应该把他导向"质检中"而不是让他手动选。这个"下一状态"的推导规则由状态机定义。我在RepairStatus枚举里加了一个映射方法:
enum RepairStatus { pendingReceive, received, diagnosing, waitQuote, repairing, qualityCheck, waitDelivery, completed; List<RepairStatus> get nextStatuses { switch (this) { case pendingReceive: return [received]; case received: return [diagnosing, cancelled]; case diagnosing: return [waitQuote, repairing]; case waitQuote: return [repairing, cancelled]; case repairing: return [qualityCheck, waitQuote]; case qualityCheck: return [waitDelivery, repairing]; case waitDelivery: return [completed]; case completed: return []; } } }实际业务里"下一状态"有时不止一个选项。比如waitQuote(待报价确认)下一步,既可能客户同意报价直接进入维修,也可能客户觉得太贵放弃维修,此时订单应该进入取消(cancelled终止态)。这种情况概览页的点按弹出ModalBottomSheet,里面的操作项是动态的,来源就是这个nextStatuses映射。整个操作面板的核心思路是:UI 永远不硬编码状态流转,而是问状态机要选项。后续哪怕需求改成"待报价确认时允许回退到检测中重新检查",也只需要改枚举的映射方法,UI 层完全不用动。
5.2 操作面板的必填项校验
光选状态还不够,有些流转必须附带业务信息。比如从PENDING_RECEIVE推进到RECEIVED,必须填写当前里程数;从DIAGNOSING推进到WAIT_QUOTE,至少得有一条维修项目明细;从QUALITY_CHECK回退REPAIRING,必须填写返工原因。我在StatusTransitionGuard里定义了一个requiredFields的校验逻辑,当某个目标状态存在必填项时,ModalBottomSheet里会动态渲染对应的表单字段。
这个设计是吸取了实际调研的教训。早期版本操作面板只管改状态,结果业务投诉说"好几个单子状态改了,但报价单是空的,根本没法跟客户谈"。后来被迫加了校验:没有维修项目明细的订单,状态根本推不到待报价。与其事后靠人肉检查,不如在入口处就卡死。我建议大家做 B 端状态流转时,一定把"什么状态必须有前置信息"这个规则写清楚,这比在 UI 上弹十个提示框都有效。
5.3 并发冲突处理
维修厂里多个人可能同时操作同一台车——前台在改客户信息,技师在点完工,质检员在录入质检结果。如果没有并发控制,就会出现"A 刚把状态改成质检中,B 又把它撤销成维修中"的混乱。车维管家的处理方式是:本地数据库在更新前先比较updated_at字段,如果发现本地版本的updated_at比当前时间早超过一个阈值(比如 30 秒),就弹一个"该订单状态已被其他操作员更新,请刷新后重试"的提示,同时自动刷新订单详情。
这个方案不完美,但在单个门店内够用。真正的强一致方案需要后端加乐观锁,前端做状态合并,复杂度会高出一个量级。对大多数维修厂的规模来说,时间戳冲突检测已经能拦掉 99% 的并发问题。如果后续车维管家要做多门店、跨区域协同,再换服务端版本号方案不迟。
6. 鸿蒙端适配实践——Flutter 跑在 HarmonyOS 上遇到的坑
6.1 环境配置的大坑
车维管家的目标平台明确包含 HarmonyOS(项目标题里的 Harmony6.0),所以 Flutter 工程要能在鸿蒙设备上编译运行。目前 Flutter 官方对鸿蒙的正式支持还没出来,社区方案用的是 OpenHarmony 的 Flutter 适配分支。我第一次配环境时就在这一步卡了整整两天,各种报错,最后总结出一个经验:先确认你拿到的 Flutter SDK 分支版本和 HarmonyOS 的 SDK 版本匹配,再动手写代码。
具体来说,鸿蒙端需要下载 DevEco Studio 和配套的 HarmonyOS SDK,然后在 Flutter 工程里配置鸿蒙的编译环境。第一次编译大概率会遇到 Gradle 相关的问题(社区里最典型的就是提示You are applying Flutter's main Gradle plugin imperatively using the apply script,这是因为 AGP 插件应用方式在新版本里废弃了命令式 apply,需要改成声明式插件配置)。这个问题说大不大,但对没见过的人来说很容易一头雾水。
我的建议是:如果你的团队没有专门的鸿蒙开发人力,不要贸然在项目初期就把鸿蒙端跑通,先用 Android 模拟器做业务开发;等业务逻辑稳定之后,再单独拉一个分支做鸿蒙适配。适配工作主要集中在这几个地方:权限声明、文件读写路径、相机相册调用(Flutter 的image_picker在鸿蒙端有兼容问题,需要找鸿蒙专用插件或自己写 Platform Channel 调 ArkTS 的接口)、以及数据库文件路径的获取。车维管家的实际做法是,先保证 Android 端业务完整跑通,鸿蒙端作为后续的交付适配项。
6.2 本地数据库在鸿蒙端的适配方案
前面提到 sqflite 在鸿蒙端有兼容问题,车维管家实际遇到过:sqflite底层依赖的是 Android 的 SQLite 实现,在鸿蒙上要用 OpenHarmony 的@ohos.data.relationalStore来访问数据库。两条路:
第一条路,找社区维护的鸿蒙版 sqflite 分支。目前有人维护适配版,能解决基本 CRUD,但方括号、事务边界处理和原生版略有差异,用到复杂 SQL 时要小心。
第二条路,自己封装一个 Platform Channel,在 Dart 层定义统一的数据库接口,Android 端走 sqflite,鸿蒙端走 relationalStore 的 ArkTS 代码。工作量更大,但完全可控,而且未来 Flutter 官方支持鸿蒙后可以直接替换底层实现,业务代码不用动。
我当时选了第一条路先跑通 Demo,然后逐步把 DAO 层的 SQL 语句收敛到一张QueryFactory表里统一管理,避免 SQL 散落在业务代码中。这样如果之后要切第二条路,只需要替换 QueryFactory 的实现即可。
6.3 鸿蒙真机调试的注意点
在鸿蒙真机上调试 Flutter 应用,我补充几个实际经验:
- 日志过少:鸿蒙的日志系统和 Android 的 Logcat 不是一套,Flutter 的
print()输出不一定能在 DevEco Studio 里直接看到。调试时可以临时用debugPrint加上 logcat 过滤,或者干脆在关键节点写日志到本地文件,之后统一导出分析。 - 热重载支持不完整:鸿蒙端的 Flutter 热重载偶尔会出现"改了代码没生效"的假死状态,尤其是在改原生插件相关的代码时。我的经验是:纯 Dart 层改动热重载没问题,涉及 Platform Channel 或原生插件时直接全量重启应用,别浪费时间等热重载。
- 打包生成 HAP:HarmonyOS 应用的交付形态是 HAP 而不是 APK,打包流程要走 DevEco Studio 的构建体系。Flutter 工程在鸿蒙端编译时,Dart 代码会先编译成 so 库再打进 HAP 包,构建时间明显比 Android 长。第一次打包耐心等,中途不要乱切分支,很容易把中间产物搞脏。
7. 性能优化与异常兜底——概览页如何做到秒开秒刷
7.1 列表性能:卡片多的时候不能卡
一台维修厂同时期的在修车辆,实际规模大概在 20 到 80 台左右,一般不会上千。这个数量级对 Flutter 来说压力不大,但如果 UI 写得不讲究,照样会掉帧。车维管家概览页的性能优化主要集中在三处:
- 列表项固定高度:每张订单卡片的高度根据内容动态变化(描述文本可能一行也可能三行),这就导致
ListView.builder无法使用itemExtent优化。我的方案是给卡片内容区设置固定最大行数和overflow: TextOverflow.ellipsis,让卡片高度稳定下来。这样滚动时 Flutter 的渲染引擎可以复用元素,滚动帧率明显提升。 - 避免不必要的 rebuild:状态概览页里每一秒都可能有一台车刷新状态,如果用
setState包住整个ListView,所有卡片都会重建。车维管家把每张卡片包进Consumer<RepairOrderModel>或者用Selector精确监听某张订单的数据变化(按订单 ID 过滤),这样只有状态变更的那张卡片会重建,其他卡片纹丝不动。实测 60 台车同时显示时,滚动流畅度和单张卡片重建的流畅度完全不在一个档次。 - 图片懒加载:维修完工后会上传照片,卡片上要显示缩略图。照片如果直接加载原图,滚动时必然卡顿。我接入了
cached_network_image并生成缩略图 URL(服务端在图片上传时生成一张 200x200 的缩略图),本地数据库只存缩略图 URL,详情页再加载原图。
7.2 数据刷新策略
概览页的数据刷新不能是"每次进入页面都全量拉取",也不能是"永远不刷新"。我的策略是三层结合:
- 进入页面即查本地:从 SQLite 读所有订单,UI 立刻渲染,这个过程应该控制在 50ms 以内,用户无感知。
- 后台静默拉取远端:进入概览页的同时,异步请求后端接口获取最新的订单列表和状态变更,并与本地做合并(以
updated_at较新者为准),合并完再刷新分类统计卡。 - 局部推送兜底:如果有 WebSocket 长连接,收到订单状态变更推送时只更新对应订单,不触发全量刷新。车维管家第一版没上线推送,第二版才加的,这属于可选的进阶优化。
这套策略下,用户在车间没信号时照样能查看和操作本地数据,回到有网环境后最多等一两秒数据就同步到最新,体验上基本做到了"无感"。
7.3 数据库超大订单量下的兜底
虽然初期一个门店在修车辆数不大,但数据库是长期累积的——半年后历史订单可能上万张。如果概览页的查询语句是ORDER BY created_at DESC全表扫,一旦上万条数据,SQLite 虽然不会崩,但查询时间会肉眼可见地变慢。我在repair_orders表上建了三个索引:created_at、status、is_waiting_parts。概览页的分组查询走的是status索引,历史订单搜索走created_at索引,基本能让查询保持毫秒级。
另外我定期做一次数据清理:超过 6 个月的COMPLETED订单,把图片文件和详细的维修报告归档到后端,本地只保留订单基础信息。这是为了避免平板存储暴涨,毕竟维修照片一张好几 MB,几百张照片就能吃掉几个 G 空间。
8. 写在最后——经验沉淀和下一步规划
车维管家的"维修状态概览"模块从设计到落地,前前后后改了三个大版本才稳定下来。第一版是最传统的"顶部 Tab 切换 + 列表",被门店试用后否了,说"看不出整体情况";第二版加了分组列表和统计卡,但没做状态推进按钮,被反映"光看不能用";第三版才最终定成现在这个样子——分组长列表 + 统计卡 + 卡片内状态推进 + 超时标记。每一次推翻重来都不是代码问题,而是对业务场景理解的加深。
我个人在实际开发里最深的体会是:B 端工具的核心从来不是技术炫技,而是对业务状态和行为路径的准确建模。状态机画清楚、数据流向定明白、交互路径缩短到极致,这三件事做好,哪怕 UI 朴素一点,用户都会愿意用。反过来,技术栈再新、动画再炫,如果店长打开软件三秒钟找不到他今天要交车的订单,这个软件就是废的。
有几个小经验,最后按惯例分享一下:
- 状态机枚举的
nextStatuses映射和StatusTransitionGuard校验规则,是我在开发中后期才补上的,建议你们从第一天就开始搞。没有规则约束的"自由"状态流转,越往后越收不住。 - Flutter 里 H2 这种列表分组的吸顶效果,用
SliverPersistentHeader能做到,但pinned参数记得设为 true,否则分组标题会跟着滚走。 - 给每个状态分组加徽标计数,这个小东西看似简单,其实能显著降低店长的认知负担——他们只需要扫一眼数字就能知道压力集中在哪个环节:是不是质检堵了一堆车?是不是待报价的车没人跟?这种"瓶颈在哪"的直觉,对修车厂管理太重要了。
下一步,车维管家准备加两个方向:一个是消息推送,让客户在微信或者 App 上能看到自己车辆的实时维修进度,这个基于现有状态机的每一次 status 变更推一条消息就行,代码改动不大;另一个是老板视角的数据看板,按天、按周统计每台车的平均维修时长、每个技师的工作量,这也是基于状态历史记录做的衍生分析。等这两个方向跑完,我再回来写一篇后续分享,如果你们在状态机设计或 Flutter 鸿蒙适配上有更好的思路,欢迎在评论区一起交流。