
1. 项目背景与整体架构思路1.1 服务项目模块到底要解决什么问题做车辆维修管理这类行业应用和我以前做互联网C端产品是两种完全不同的节奏。C端可以快速上线、边做边调但门店系统不行——操作员一天要开几十张工单一个按钮位置不对、一次响应慢半拍都会直接影响效率。这次我们基于 Flutter 和 OpenHarmony 的组合把一个车辆维修管理系统里的“服务项目模块”从零完整做了一遍中间踩了不少坑。这篇文章就把从数据建模到跨端适配的完整过程掰开揉碎说一遍。很多人一听“服务项目模块”觉得不就是增删改查吗实际上它是整个维修业务链路的地基。门店每天开单开单选的就是服务项目保养、小修、大修、钣金喷漆、贴膜美容每一类下面还有具体条目比如“更换机油机滤”“四轮定位”“全车抛光”。这些条目如果不梳理清楚后面工单金额、技师提成、库存出库、财务结算全都对不上号。我这次负责的模块核心就解决两块问题一是让门店管理员能灵活维护一套服务项目库包括分类、标准工时、标准价格、状态开关二是让前台开单时能快速检索、勾选项目并自动带出价格和工时工单保存后这些信息以快照形式跟着工单走不受后续调价影响。这个模块的数据被很多地方引用所以第一件事就是把边界划清楚它只维护“项目定义”和“项目选择”不管开单、不管结算。边界一划清后面和工单模块、财务模块的联调就顺很多。做了几个行业软件之后我的体会是模块越多越要先定义清楚谁管什么否则代码写一半就要返工。1.2 为什么选 Flutter × OpenHarmony 这个组合这个项目的终端形态比较复杂。门店前台有PC车间里有挂墙平板老板手机上要看数据客户还希望自己的App能做预约和查看维修进度。传统做法是Android一套、iOS一套、Web一套维护成本直接乘三。我们最后定的前端方案是 Flutter目标是一套Dart代码能编译到Android、iOS、Windows以及OpenHarmony多端。OpenHarmony 是开源系统底座这几年在办公平板、行业终端上落地不少很多连锁门店的硬件设备已经在使用。Flutter 本身不原生支持 OpenHarmony但社区有对应的适配工程通过替换引擎和构建工具链可以让Flutter应用以HAP包的形式跑在OpenHarmony设备上。这个组合的核心价值在于业务代码只写一次跨端能力直接复用门店以后换终端、加终端前端代价都很小。选型时我也认真对比过自绘引擎、Web套壳、原生ArkTS三条路线。自绘引擎自由度最高但要造大量轮子Web套壳开发快但离线体验和性能都差一截原生ArkTS写OpenHarmony最顺手但对Android和iOS又要重写一遍。Flutter在性能和跨端复用之间找到了平衡点加上团队本来就有Dart基础选它是最实际的方案。这里多说一句选技术栈别只看“新不新”要看团队能不能接得住、目标设备能不能跑得动我们评估下来这套组合当前最合适。1.3 模块在系统里怎么摆整体架构上这个系统是前后端分离的。后端是一套标准REST API负责项目库、工单、客户、财务这些数据前端是Flutter多端工程同一个代码仓库里按模块划分目录。服务项目模块在代码里作为独立feature目录存在不直接依赖其他业务模块只对外暴露一个统一的仓库接口给工单模块调用。这样设计有具体好处。第一编译层面可以单独维护改动服务项目模块不影响主工程第二联调层面可以先造假数据把界面跑起来后端没部署完也能并行开发第三测试层面纯Dart的仓库逻辑可以独立单测不依赖具体设备。这套分层的思路我觉得比纠结“是不是微服务”更实在。行业软件的复杂度主要来自业务规则而不是并发量架构上把依赖方向控制好就够用了。在目录结构上我把模型、接口、页面、状态管理分成四个子目录。模型只放纯数据结构接口负责HTTP请求和缓存页面只写UI状态管理用ChangeNotifier做轻量绑定。这样服务项目模块的代码不会渗透到其他模块里工单模块引用的只是接口层的几个方法后面替换实现也方便。2. 功能拆解与数据建模2.1 先别急着写界面功能清单要这样梳理服务项目模块表面看就是“增删改查”但真开工前我花了一整天把需求方的操作流程走了一遍反复确认他们日常怎么维护项目、开单时怎么找项目最后定下来的功能清单比想象中细项目库校验同一个门店下项目名称不允许重复分类名称也不允许重复。分类管理只做一级分类不做多级树避免把门店维护成本搞得太高。项目的启停用停用后新工单不能选择该项目但历史工单完全不受影响。检索与筛选按名称模糊搜索、按分类筛选、按状态筛选三项可以叠加。快捷选择开单界面用平铺卡片加底部已选列表的方式而不是弹窗多选。审计信息每次改价、改状态都记录操作人和操作时间。这份清单里最容易被人忽略的是“历史工单不受影响”这条。很多新手设计时会直接在外键上做级联更新导致历史工单里的价格跟着新价格一起变财务一下子就对不上。正确做法是在工单明细里保存服务项目的完整快照项目库里后续怎么改价、停用老工单数据都保持稳定。这个思路必须在数据建模阶段就定下来否则后面改表结构代价极大。2.2 数据模型与表结构设计服务项目模块涉及三张核心表分类表、项目表、工单明细表跨模块引用。字段设计我直接给出最终版本并标注每个关键字段为什么这样设计。表字段类型说明service_categoryid / name / status / sort_order / created_at整型文本一级分类sort_order控制排序service_itemid / name / category_id / price / work_hours / commission_type / commission_value / enabled / version整型文本price按分存储version做乐观锁work_order_itemid / work_order_id / service_item_id / item_snapshot_price / item_snapshot_name / work_hours / quantity / discount_amount / assignee_id / status整型文本快照字段冗余保存历史数据永久稳定这里有两个细节值得展开。第一个是金额为什么不直接存“折后价”而是存“标准价折扣明细”。门店业务经常出现“这一单打个折”的场景如果直接存折后价月底统计提成和营收时根本不知道原本该收多少、让利多少。保存标准价加折扣明细报表才能分析让利原因。第二个是work_hours字段一定要用整数分钟数存储不要用浮点数。工时直接关联技师工资和客户账单浮点误差会引发对账差异我用“分钟整数”存储展示时再自行换算成“小时加分钟”格式。Dart侧我写了一个对应的模型类包含稳定ID和copy方法方便生成快照和局部更新。class ServiceItem { final String id; final String name; final String categoryId; final int priceInCents; // 单位分避免浮点误差 final int workMinutes; // 单位分钟 final int commissionType; // 0固定金额 1按比例 final int commissionValue; final bool enabled; ServiceItem copyWith({bool? enabled, int? priceInCents, int? workMinutes}) { return ServiceItem( id: id, name: name, categoryId: categoryId, priceInCents: priceInCents ?? this.priceInCents, workMinutes: workMinutes ?? this.workMinutes, commissionType: commissionType, commissionValue: commissionValue, enabled: enabled ?? this.enabled, ); } MapString, Object? toJson() { id: id, name: name, categoryId: categoryId, priceInCents: priceInCents, workMinutes: workMinutes, commissionType: commissionType, commissionValue: commissionValue, enabled: enabled, }; }2.3 价格、工时与状态流转的细节服务项目的定价逻辑看起来简单实际上要同时支持标准价、会员折扣、整单优惠三种叠加。标准价在项目库里维护会员折扣属于会员模块规则整单优惠在工单模块里计算。服务项目模块只需要确保“项目快照里的标准价是最新的、正确的”后面的事交给下游模块。这里最难的是统一“金额用分存储”的约定每个模块都要遵守否则接口传参一不留神就会把元当分用。状态流转是另一个容易出错的地方。我设计了一套“项目本身轻状态、工单关联强状态”的组合。项目自身只有“启用”和“停用”两个状态不搞审核中、草稿之类因为门店维护人员没有精力去管理复杂状态机。但在工单明细里项目会经历“待分配、进行中、已完成、已结算”四态。这两套状态我分开建模避免出现“项目停用了但工单里还在做”这种逻辑混乱。还有一个人性化约束值得记录项目库里停用某个项目时后端要检查今天是否还有未完成工单引用了该项目。如果有前端要给出明确提示“当前有5张在施工工单正在使用该项目”而不是直接禁止操作。这种细节决定了门店愿不愿意长期用这套系统操作员最怕系统不讲原因地拦住操作。3. 跨端落地的关键实现3.1 环境搭建与工程配置含踩坑Flutter跑OpenHarmony环境搭建是第一道坎。整体流程是安装对应版本的OpenHarmony开发环境用标准flutter create初始化工程再通过适配脚本生成OpenHarmony工程结构最后用系统的构建工具打包成HAP。听起来不难但每一步都有版本陷阱。重点说几个踩过的坑。第一个是版本配对。Flutter版本和OpenHarmony SDK版本之间是严格对应的不能随手拿最新版Flutter配最新SDK我一开始就因为这个浪费了一整天。建议每一步都只参考当前适配分支的官方说明确保Dart SDK、Flutter引擎版本、OpenHarmony SDK版本三者保持一致再往下走。第二个是权限配置差异。同一个Flutter工程跑Android时用AndroidManifest声明权限跑OpenHarmony时要用module.json5声明。这个模块涉及网络请求Android侧要加INTERNET权限OpenHarmony侧要声明ohos.permission.INTERNET。漏掉任何一边线上都会莫名网络失败而且两边报错信息长得还不一样排查起来很费劲。第三个是工具链问题。OpenHarmony构建依赖独立的命令行工具和签名配置不像Android可以直接用Gradle一把梭。我建议把整套环境变量、签名路径写成一个初始化脚本新同事拉代码后跑一次脚本就能编译省得每次有人来都要重新教一遍环境配置。配置完成之后强烈建议先跑一个最小“Hello World”验证链路再往里面加业务代码。跳过这步直接跑完整系统的话出了问题你很难分清楚是适配层的问题还是业务逻辑的问题排查效率非常低。3.2 服务项目列表与筛选的实现服务项目模块的主界面是一个典型的长列表加筛选场景。我没有上复杂的状态管理框架就用了Flutter自带的ChangeNotifier加Repository层数据流向足够清晰团队其他人接手也容易理解。列表部分用ListView.builder配合分页加载。分页不是一次性拉全量而是每页50条滚动到底部时自动加载下一页配合RefreshIndicator做下拉刷新。这个决策的原因很直接门店平板很多是低端配置一次性把上千条项目数据塞进内存即使不崩也会卡顿分页能显著减少首屏压力。筛选交互上顶部分类条用横向滚动加“全部”“已启用”“已停用”的状态过滤。搜索框用防抖处理用户输入停顿500毫秒后才真正发起请求避免每个字符都去查一次接口。核心请求逻辑我封装在一个方法里关键代码如下FutureListServiceItem loadServiceItems({ int page 1, String? keyword, String? categoryId, bool? enabled, }) async { final params String, String{ page: $page, pageSize: 50, if (keyword?.isNotEmpty ?? false) keyword: keyword!, if (categoryId ! null) categoryId: categoryId, if (enabled ! null) enabled: $enabled, }; final result await _api.get(/service-items, query: params); return (result[list] as List) .map((e) ServiceItem.fromJson(e)) .toList(); }有个很实用的细节所有筛选条件都交给服务端处理本地只做渲染。很多项目图省事把全量数据拉到本地再过滤数据量小还能忍超过几百条页面就会明显发闷。服务端分页加筛选从根上规避了这个性能隐患。3.3 计费逻辑与跨端差异收敛计费这块服务项目模块不直接算账单但需要提供一个“项目小计”的计算能力给工单模块复用。计算公式很简单项目小计 数量 × 标准价 工时费 - 折扣工时费 工时 × 每工时单价每工时单价来自门店配置。这个公式我封装成一个纯Dart函数不依赖任何平台能力可以写单测覆盖各种折扣边界这是最让我放心的地方。跨端差异收敛是这个模块里最需要耐心的一环。同一个功能在Android、iOS、OpenHarmony上行为会有细微差别日期选择控件就是典型Android的日期选择是Material风格iOS是滚轮风格OpenHarmony上的原生控件样式又不一样。我没有让每个端各写一套UI而是选定一个跨端日期选择插件统一用自定义主题保证三个平台视觉一致。底部操作栏也做了统一处理非手机端平板和PC直接显示固定按钮手机端则用SafeArea包裹防止底部手势条遮挡控件。尺寸适配我用的是自适应间距而不是写死的px保证不同分辨率下布局不跑偏。还有字体这个隐形大坑OpenHarmony默认字体和Android不完全一样某些字符宽度存在差异用固定宽度判断文本是否超长的逻辑会出问题。我最终的做法是给所有可能超长的文本加“最小宽度约束加省略号”兜底宁可显示省略号也不要截断崩版。4. 常见问题与排坑实录4.1 插件兼容性问题怎么处理跨端项目里最头疼的问题就是第三方插件不支持OpenHarmony。很多pub仓库里的Flutter插件只有Android和iOS的原生实现在OpenHarmony上调用时会直接抛MethodNotImplemented异常。我在开发中遇到两个典型场景一个是图片选择器一个是系统分享。我的处理思路分三步。第一步优先在pub上找平台无关的插件比如纯Dart实现的文件选择方案避开原生通道。第二步如果插件确实依赖原生能力先确认是否有OpenHarmony适配版本没有的话就在项目里做统一封装内部判断平台能调插件就调插件不能调就走自定义实现。第三步把这类兼容性逻辑收敛在一个文件里不要散落在各个页面。我还建了一个简单的“能力探测”工具App启动时统一校验当前平台支持哪些原生功能把结果缓存在内存里。功能入口按能力表动态显示这样用户不会点到一个功能才发现不可用。这个做法在真实门店环境里特别有用因为终端型号杂Android平板和OpenHarmony平板混用界面必须自动适应而不是靠运维人员记住“这台机器不能拍照”。4.2 存储与数据同步服务项目模块的数据量不算大但门店网络环境不一定稳定本地缓存必须有。我采用两层方案网络优先失败时降级到本地缓存。具体实现是每个请求返回的数据在内存缓存一份同时写一份到轻量本地存储请求失败时读取本地缓存展示并提示“当前为离线数据”。操作员这时候还能正常浏览和选择项目只是不能做修改操作。要不要上数据库我的建议是看规模。早期项目库几百条记录用轻量存储完全够引入数据库会带来额外原生依赖在OpenHarmony上又多一层兼容风险。我实测下来JSON文件缓存加内存缓存的组合应对服务项目模块日常使用绰绰有余不值得为了“专业感”过度设计。数据同步冲突主要发生在“多个终端同时改价格”。两个前台同时改同一个项目后提交的会覆盖先提交的这在门店场景里很难完全避免。我实现了一个简化版乐观锁更新接口带上version字段后端发现版本不一致就拒绝并返回最新数据前端弹提示“该项目已被其他终端修改”。机制代码量不大但把最大的数据一致性风险控制住了。4.3 性能与体验优化OpenHarmony的入门平板配置普遍不高列表滚动性能优化特别关键。第一版代码跑上去最明显的卡顿来自图片加载时没有占位图每张图加载都会引起布局抖动。解决方法很朴素项目缩略图固定宽高加载前显示浅色占位加载完成后淡入替换。第二个优化点是列表项的itemExtent。如果列表项高度是固定的声明itemExtent后Flutter不用每次去测量高度滚动性能提升非常明显。服务项目卡片高度统一这个优化几乎是零成本一行代码换来的收益。第三个是页面切换动画。默认路由动画在低端设备上有点飘我统一改成了短促的淡入淡出视觉上更干练也减轻了动画开销。这些优化单看都是小事但叠在一起在终端上的实际体验差别就大了。我自己的习惯是每做一次优化就在真机上跑一次帧率统计用Profile模式关注列表滚动区间的帧耗时对比优化前后的数据而不是凭感觉判断“流畅了”。数据不会骗人。5. 经验总结与下一步扩展5.1 做这个模块的几点体会整个服务项目模块做下来我最大的体会是行业软件里真正的复杂度不在技术本身而在业务规则的边界。服务项目看起来只是增删改查但要处理历史快照、状态分离、跨端兼容、离线降级每一个细节都来自真实门店场景不是坐在电脑前凭空想出来的。每次改设计我都会先问自己一句门店操作员遇到这个情况心里期望系统怎么表现顺着这个思路走设计出来的功能基本不会跑偏。另外一点是关于跨端开发的认知。很多人觉得Flutter跨端就是写一套代码跑三个平台真做下来你才发现跨端不只是代码复用更是一套把平台差异收敛起来的方法论。框架帮你解决了界面渲染的差异但权限配置、插件生态、字体渲染、设备性能这些差异仍然要自己处理而且必须在架构层面提前预留处理位置不能等测试发现了再打补丁。5.2 后续可以怎么扩展这个模块后续还有几个可以扩展的点。项目库可以加“推荐排序”根据过去三个月的开单频率自动把常用项目排在前面减少操作员搜索成本。工单明细可以加“套餐打包”功能把几个关联项目组合成一个套餐价比如“小保养套餐”包含机油、机滤、工时一键勾选。再往后可以对接OBD扫码设备识别车辆VIN码后自动匹配保养手册推荐项目这个在连锁门店很有吸引力。这些扩展都在现有模块边界内继续加功能不需要动架构。我当时把数据库字段、接口语义、页面结构都留了扩展余地就是考虑到这类行业软件是长期演进的产品不是上线就结束的项目。如果让我给同样做跨端行业应用的朋友一个建议我会说先跑通最小链路再谈完整功能先划定模块边界再追求界面完美。架构设计上少一点花活多想想操作员手指点下去的那一刻会发生什么系统离好用就不远了。