半年前我们开始把物流端的配送App往鸿蒙上迁移,项目里最不起眼却让我花时间最多的,不是组件适配,也不是地图SDK,而是三个TypeScript类型定义:LargeItem、DeliveryOrder、DeliveryTeam。没错,就是几个interface和enum。但整个大件配送业务的稳定性、订单全生命周期跟踪的可维护性,几乎都压在它们身上。
这个项目简单说,是用React Native跑在鸿蒙设备上,专门给大件物流的配送员和调度员用的订单管理端。大件商品(冰箱、洗衣机、家具)和普通小件在配送流程上差距极大,订单字段多、状态节点多、异常情况也多。如果类型定义写得含糊,后面接口联调、状态流转、多端一致性全都会出问题。这篇我就把这三个核心类型的设计思路、订单生命周期跟踪的落地方式,以及我们踩过的几个坑完整聊一遍,给正在做鸿蒙跨平台、尤其是做物流订单类项目的朋友做个参考。
1. 项目背景:鸿蒙上的配送App,为什么先写类型
1.1 业务现状:大件订单和普通小件不是一个难度
先说业务场景。我们做的这个大件配送App,核心用户是配送员和调度员,处理的是冰箱、洗衣机、床垫、沙发这类大件商品。这类订单有几个普通小件完全不具备的特点:
- SKU属性复杂:长宽高、重量、是否易碎、是否需要安装、是否需要两个人搬运、能否堆叠装车,每一条都会影响配送执行。
- 时间窗口严格:用户往往预约了上午/下午/晚上某个时间段,早到没人、晚到投诉。
- 异常分支多:联系不上客户、拒收、货损、用户要求改期、地址错误,每个异常都要记录原因凭证。
- 团队协作强:大件经常是两人一组、带车配送,不是单人小电驴能搞定的。
这些特点反映到代码里,就是订单数据Model必须足够“厚”。如果一个订单对象只有单号、客户名、手机号这几个字段,那后面所有业务逻辑都没法写。可如果Model设计得乱,字段命名不统一、类型随意用any,那跨端开发时就是灾难。
我在项目启动时定的规矩是:先定义类型,再写页面,再联接口。业务规则能写进类型系统的,绝对不留在组件里用if-else硬凑。
1.2 为什么用React Native做鸿蒙端
团队预算有限,Android、iOS两端已经用React Native开发了两年,货架上的业务代码基本都是JS/TS。鸿蒙端如果从零用ArkUI另写一套,等于要养三套代码,产品迭代速度完全跟不上。所以我们从一开始就锁定了 react-native-openharmony,也就是社区里说的RNOH方案。
RNOH的大致原理是把React Native的运行时和渲染管线适配到鸿蒙的ArkUI框架上,让JS业务代码不用大改就能跑在鸿蒙设备里。这个方案在2024年到2025年已经比较成熟了,核心组件覆盖面够用,但离“完全无缝”还有距离。最明显的问题是:底层原生能力差异大,比如定位SDK、推送、地图组件,在iOS和Android上各自有原生实现,鸿蒙侧很多第三库没有现成适配,需要自己封装原生模块。
这种情况下,类型定义就成了三端共同的“语言公约”。业务侧不关心底层是Android View还是ArkUI组件,只要拿到的是一个结构稳定、字段明确、状态流转合法的订单对象,UI层就能正确渲染,交互逻辑就能正确执行。
1.3 先定义类型,相当于把业务规则内聚在一起
很多人觉得TypeScript类型定义就是写几个interface给编辑器做提示,这远远不够。在一个订单全生命周期跟踪的App里,类型定义承担的是“业务契约”的职责:
- 编译期约束:状态枚举值写错了、字段名拼错了,编译直接报错,而不是运行时白屏。
- 边界隔离:API返回的数据经过解析层之后,类型已经收敛,UI层拿到的都是可信数据。
- 状态机显式化:订单从创建到完成,哪些状态能流转到哪些状态,用类型系统定义好,防止非法跳转。
我们的经验是,类型定义阶段多花三天,后面联调能省三周。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 类型定义设计:把业务规则写进数据类型
2.1 LargeItem:大件商品的数据画像
第一个核心类型是 LargeItem,代表一个大件商品。它不是简单的商品名+价格,而是承载了配送执行所需的全部物理属性。我们的设计长这样:
typescript复制// 大件商品基础信息
export interface LargeItem {
/** 商品唯一编码 */
skuId: string;
/** 商品名称 */
name: string;
/** 品牌 */
brand?: string;
/** 商品分类,如:冰箱/洗衣机/家具 */
category: string;
/** 长宽高,单位cm */
dimensions: {
length: number;
width: number;
height: number;
};
/** 毛重,单位kg */
weight: number;
/** 体积,单位m³,可以由dimensions自动计算 */
readonly volume: number;
// 配送执行相关标志
/** 是否易碎 */
fragile: boolean;
/** 是否能堆叠装车 */
stackable: boolean;
/** 是否需要安装服务 */
needInstall: boolean;
/** 需要几个人搬运,默认1 */
requiredCrew: number;
/** 是否走楼梯,电梯无法进入时用 */
stairRequired?: boolean;
/** 商品图片URL列表 */
images: string[];
/** 商品附加说明 */
remark?: string;
}
这里有两个设计点我特别想强调。
第一个是 readonly volume。 体积是长宽高算出来的,不应该允许外部直接赋值。用 readonly 加计算属性,既统一了单位,又避免了有人把m³和cm³混着传。实际计算放在构造函数或者工厂函数里:
typescript复制export function createLargeItem(input: Omit<LargeItem, 'volume'>): LargeItem {
const { length, width, height } = input.dimensions;
return {
...input,
// 转成m³:cm * cm * cm / 1_000_000
volume: Number(((length * width * height) / 1_000_000).toFixed(3)),
};
}
第二个是 requiredCrew 和 stairRequired。 这两个字段看起来不起眼,但在调度派车的时候非常关键。一个3人才能搬的钢琴和一个1人就能扛的洗衣机,占用的人力资源完全不同。我们曾经因为没有这两个字段,调度系统把一个需要3人的订单派给了只有2人的小团队,结果现场临时加人,成本直接翻倍。这些字段不是给UI展示用的,是给业务规则计算用的。
2.2 DeliveryOrder:状态机驱动的订单模型
第二个核心类型是 DeliveryOrder,也就是配送订单。这是整个生命周期跟踪的中
