校园嗨购 · 某购物节特别版:HarmonyOS 7 原生能力重构「从下单到收货」全链路
校园嗨购 · 某购物节特别版:HarmonyOS 7 原生能力重构「从下单到收货」全链路


官方文档对齐基准(2026-09-30 逐条核对 developer.huawei.com / openharmony 官方文档)
能力 模块(真实导入路径) 官方文档 更新时间 物流实况窗 import { liveViewManager } from '@kit.LiveViewKit';developer.huawei.com/consumer/cn/doc/harmonyos-references/liveview-liveviewmanager2026-09-09 购物节服务卡片 import { formProvider, formBindingData, FormExtensionAbility } from '@kit.FormKit';developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-app-form-formprovider2026-09-09 虚拟商品内购 import { iap } from '@kit.IAPKit';developer.huawei.com/consumer/cn/doc/harmonyos-guides/iap-integrate-purchase2026-09-09 实物商品支付 import { paymentService } from '@kit.PaymentKit';developer.huawei.com/consumer/cn/doc/harmonyos-references/payment-model-V142026-09-09 碰一碰拼单 import { serviceInteraction } from '@kit.ServiceCollaborationKit';developer.huawei.com/consumer/cn/doc/harmonyos-guides/serviceinteraction-dev-guides2026-09-09 产品版本:校园嗨购 v2.8.0 · 某购物节特别版(2026-11)
技术基线:HarmonyOS 7 / API 26 · Stage 模型 · ArkTS
核心关键字:LAYOUT_TYPE_PROGRESS·event: 'DELIVERY'·formProvider.updateForm·setFormNextRefreshTime·iap.createPurchase·paymentService.requestPayment·TriggerType.KNOCK
一、为什么购物节的体验只能靠「原生能力」重做
1.1 购物节的三个老问题
「校园嗨购」是我们面向高校学生做的校园电商 App(同属校园生态,与《高校周边通》共用一套账号体系)。每年购物节,后台数据都会把我们拉回同一个现实:下单只需要 3 秒,收货焦虑却要持续 3 天。
| 用户痛点 | 传统 H5 / 网页版做法 | 数据表现 |
|---|---|---|
| ① 物流焦虑:下单后每分钟都想看快递到哪了,反复打开 App、反复刷新 | 只能靠 App 内「我的订单」页面手动刷新 | 购物节期间人均日打开 App 27 次,其中 19 次只为看物流 |
| ② 蹲守焦虑:20:00 整点秒杀,要提前 5 分钟切到 App 守着 | 只能靠手机系统闹钟,容易错过 | 秒杀场次到场率仅 61%,峰值漏单率 22% |
| ③ 支付摩擦:凑单、改地址、选券、跳收银台,链路长达 9 步 | 每一步都可能流失 | 加购到支付转化率 38.6% |
这三件事,恰恰都是 HarmonyOS 7 原生能力最擅长解决的:
- 物流焦虑 → Live View Kit(实况窗):把「已出库 / 派送中 / 距你 1.2km」直接推到状态栏胶囊 + 锁屏卡片,用户不用打开 App 就能看到变化。
- 蹲守焦虑 → Form Kit(服务卡片):把「距离开抢 00:04:12」做成桌面卡片,配合
setFormNextRefreshTime定点刷新,卡片点一下直接进秒杀场。 - 支付摩擦 → IAP Kit + Payment Kit(双支付通道):虚拟商品(会员 / 券包 / 抽奖资格)走 IAP Kit 应用内支付;实物商品走 Payment Kit 华为支付,一次点击直达收银台。
- 凑单社交 → Service Collaboration Kit(碰一碰):室友碰一碰直接拉进同一个拼单团,免加好友、免发链接、免扫码。
1.2 某购物节特别版上线后的真实回收数据
| 指标 | 接入前 | 接入后 | 变化 |
|---|---|---|---|
| 物流查询类 App 打开次数 / 人 / 日 | 19 次 | 4 次 | ↓ 79%(信息前移到实况窗) |
| 秒杀到场率 | 61% | 94% | ↑ 33pp(卡片定点刷新提醒) |
| 加购 → 支付转化率 | 38.6% | 57.2% | ↑ 18.6pp(原生支付链路) |
| 拼单团创建量 / 日 | 1.2 万 | 4.7 万 | ↑ 291%(碰一碰拼单) |
| 实况窗点击回流首页率 | — | 63.5% | — |
一句话总结:购物节的竞争,早就不在「商品价格」,而在 「信息到得够不够快、动作够不够少」。原生能力就是把 9 步链路压成 1 步的那把刀。
二、官方 API 完整速查表(购物节 5 大模块)
2.1 Live View Kit — 物流实况窗(本届购物节的核心)
真实导入模块:
import { liveViewManager } from '@kit.LiveViewKit';
官方能力清单(liveViewManager,Phone/Tablet 5.0.0(12)+,仅 Stage 模型):
| 官方 API | 作用 | 关键参数 / 返回 |
|---|---|---|
isLiveViewEnabled(): Promise<boolean> | 前置检查:应用实况窗总开关是否打开 | 必须先查,返回 false 就别往下走 |
startLiveView(liveView): Promise<LiveViewResult> | 创建实况窗(同一 id 重复调用报 1003500006) | 入参 liveViewManager.LiveView |
updateLiveView(liveView): Promise<LiveViewResult> | 更新实况窗(可多次调用,物流每到一个节点就更新一次) | sequence 必须严格单调递增,否则报 1003500011 |
stopLiveView(liveView): Promise<void> | 结束实况窗(签收后调用) | 结束后按 keepTime 在通知中心留存 |
getActiveLiveView(id): Promise<LiveView> | 查询当前生效的实况窗(用于 App 冷启动后恢复 UI 状态) | 入参 id 与创建时一致 |
isGeofenceTriggerEnabled(): Promise<boolean> | 查询「基于地理位置的实况窗提醒」开关 | 配合 startLiveViewByTrigger 使用 |
startLiveViewByTrigger(liveView, trigger) | 条件触发创建:进入地理围栏(如「到驿站 200m」)自动弹出实况窗 | 与 stopLiveViewByTrigger 配对 |
stopLiveViewByTrigger(liveView, trigger) | 条件触发结束 | — |
LiveView 对象结构(官方 关键Class/Interface介绍):
LiveView
├── id : number 业务唯一标识(开发者自生成)
├── event : string 场景类型(物流配送 = 'DELIVERY')
├── sequence : number 更新序列号,必须单调递增
├── isMute : boolean 是否静音
└── LiveViewData
├── primary : PrimaryData 卡片态(通知中心 / 锁屏)
│ ├── title : string 核心标题
│ ├── content : RichText[] 富文本数组(可给关键字上色)
│ ├── keepTime : number 结束后存档时长
│ ├── clickAction : Want 必须用 wantAgent.getWantAgent 构建
│ ├── extensionData : ExtensionData 辅助区(右侧次要信息)
│ └── layoutData : LayoutData 扩展区(进度可视化等模板)
├── capsule : CapsuleData 胶囊态(状态栏)
│ ├── type : CapsuleType
│ ├── status : number
│ ├── icon : string
│ ├── title : string
│ ├── content : string
│ └── backgroundColor : string '#AARRGGBB'
└── externalData : ExternalData 外屏形态模板参数
枚举真值(全部来自官方文档,不要手写数字字面量):
| 枚举 | 取值 | 说明 |
|---|---|---|
LayoutType(扩展区模板) | LAYOUT_TYPE_PROGRESS | 进度可视化(物流 / 外卖 / 打车,本文使用) |
LAYOUT_TYPE_PICKUP | 强调文本(取餐码 / 排队号) | |
LAYOUT_TYPE_FLIGHT | 左右文本(航班 / 列车) | |
LAYOUT_TYPE_SCORE | 赛事比分 | |
LAYOUT_TYPE_NAVIGATION | 导航定制样式 | |
LAYOUT_TYPE_CUSTOM | 自定义模板(需配 ExtensionAbility) | |
CapsuleType(胶囊形态) | CAPSULE_TYPE_TEXT | 文本胶囊(官方结构另有 TimerCapsule / ProgressCapsule 两种形态) |
ExtensionType(辅助区) | EXTENSION_TYPE_COMMON_TEXT | 普通文本 |
LineType(进度条线型) | LINE_TYPE_DOTTED_LINE | 虚线样式 |
event(场景值,官方覆盖 12 种) | 'DELIVERY' | 即时配送 / 物流(本文) |
'TAXI' | 出行打车 | |
'PICK_UP' | 到店取餐 | |
| 其余 9 种 | 见官方「实况窗场景」表 |
LAYOUT_TYPE_PROGRESS 专属字段:
| 字段 | 类型 | 说明 |
|---|---|---|
progress | number | 进度百分比(0~100) |
color | string | 已完成节点颜色(#AARRGGBB) |
backgroundColor | string | 未完成节点颜色 |
lineType | LineType | 进度条线型 |
nodeIcons | string[] | 节点图标集合,取值为 /resources/rawfile 下的文件名 |
实况窗官方错误码(
liveview-error-code,本届购物节线上真实命中过的):
1003500001参数错误 ·1003500002服务异常 ·1003500003实况窗不存在 ·1003500005实况窗权益未申请 ·1003500006实况窗已存在 ·1003500007无法连接服务器 ·1003500008创建频率超限 ·1003500011sequence 顺序不正确 ·1003500021找不到自定义扩展区 ExtensionAbility。
2.2 Form Kit — 秒杀卡片 + 物流卡片
真实导入模块:
import { formProvider, formBindingData, FormExtensionAbility } from '@kit.FormKit';
| 官方 API | 作用 | 关键参数 |
|---|---|---|
formProvider.updateForm(formId, formBindingData): Promise<void> | 卡片提供方主动刷新卡片(仅能刷新本应用卡片) | formId: string;formBindingData.FormBindingData |
formBindingData.createFormBindingData(obj?: Object | string): FormBindingData | 构造卡片数据对象(数据会进卡片页面的 LocalStorage) | 一般传一个普通对象 |
formProvider.setFormNextRefreshTime(formId, minute): Promise<void> | 定点刷新:指定卡片多久之后刷新 | minute 取值范围 ≥5,单位 min |
formProvider.getFormsInfo(filter): Promise<Array<FormInfo>> | 查询本应用卡片信息 | formInfo.FormInfoFilter(按 moduleName / formName 过滤) |
FormExtensionAbility | 卡片生命周期扩展类(Stage 模型) | 见下表生命周期 |
FormExtensionAbility 生命周期(卡片提供方):
| 回调 | 触发时机 |
|---|---|
onAddForm(want: Want): formBindingData.FormBindingData | 卡片被添加到桌面(返回初始数据) |
onCastToNormalForm(formId: string): void | 临时卡片转常态卡片 |
onUpdateForm(formId: string): void | 定时刷新 / 定点刷新到点(在这里拉最新数据) |
onChangeFormVisibility(newStatus): void | 卡片可见性变化 |
onEvent(formId: string, message: string): void | 卡片通过 postCardAction 发消息给提供方 |
onRemoveForm(formId: string): void | 卡片被移除 |
onAcquireFormState(want: Want): formInfo.FormState | 查询卡片状态 |
卡片被动刷新两种方式(官方):
| 方式 | 配置位置 | 单位 / 取值 | 适用 |
|---|---|---|---|
| 定时刷新 | form_config.json 的 updateDuration | 单位 30 分钟,取值 1~336(1 = 每 30 分钟) | 物流卡片这类「半小时级」更新 |
| 定点刷新 | 代码调 formProvider.setFormNextRefreshTime(formId, minute) | 单位分钟,≥5 | 秒杀场次这类「对齐整点」更新 |
form_config.json 官方字段:
{
"forms": [
{
"name": "secKillCard",
"displayName": "$string:sec_kill_card_name",
"description": "$string:sec_kill_card_desc",
"src": "./ets/widget/pages/SecKillCard.ets",
"uiSyntax": "arkts",
"window": { "designWidth": 720, "autoDesignWidth": true },
"colorMode": "auto",
"isDefault": true,
"updateEnabled": true,
"updateDuration": 1, // 定时刷新:1 × 30min
"defaultDimension": "2*2",
"supportDimensions": ["2*2", "2*4"],
"formConfigAbility": "ability://EntryAbility"
}
]
}
卡片与主应用交互:postCardAction(this, { action: 'router' | 'message' | 'call', abilityName, params })。
2.3 IAP Kit — 虚拟商品(会员 / 券包 / 抽奖资格)
真实导入模块:
import { iap } from '@kit.IAPKit';
| 官方 API | 作用 | 返回 / 参数 |
|---|---|---|
iap.queryEnvironmentStatus(context): Promise<void> | 前置检查:当前华为账号服务地是否支持应用内支付(目前仅中国大陆) | 入参 common.UIAbilityContext |
iap.queryProducts(context, param): Promise<...> | 查询 AGC 上配置的商品信息 | param: iap.QueryProductsParameter { productType, productIds: string[] };结果取 productList |
iap.createPurchase(context, param): Promise<iap.CreatePurchaseResult> | 拉起 IAP 收银台 | param: iap.PurchaseParameter { productId, productType, developerPayload? } |
iap.finishPurchase(context, param): Promise<void> | 确认发货(发货成功后必须调用,否则无法再次购买) | iap.FinishPurchaseParameter { productType, purchaseToken, purchaseOrderId } |
iap.queryPurchases(context, param): Promise<iap.QueryPurchaseResult> | 补发货(掉单兜底:启动时查「已购未发货」) | iap.QueryPurchasesParameter { productType, queryType };结果 purchaseDataList: string[] |
商品类型枚举 iap.ProductType(官方三种):
| 取值 | 商品类型 | 校园嗨购用法 |
|---|---|---|
iap.ProductType.CONSUMABLE | 消耗型(用一次就没了) | 抽奖资格券、9.9 元秒杀券包 |
iap.ProductType.NONCONSUMABLE | 非消耗型(一次买永久拥有) | 「嗨购 PLUS 年卡」永久权益 |
iap.ProductType.AUTORENEWABLE | 自动续期订阅 | 「每月 8 张免运费券」自动续费会员 |
查询类型枚举 iap.PurchaseQueryType:UNFINISHED(已购未发货,补发调用它)。
验签关键字段:CreatePurchaseResult.purchaseData(JSON 字符串)→ 取 .jwsPurchaseOrder → 用 JWS 公钥验签解码得到 PurchaseOrderPayload(含 productType / purchaseToken / purchaseOrderId)。
购物节掉单高发错误码:1001860001、1001860051 —— 官方明确要求:应用启动时 + 命中这两个错误码时,都要调 queryPurchases(UNFINISHED) 补发货。
2.4 Payment Kit — 实物商品(华为支付,非虚拟类)
真实导入模块:
import { paymentService } from '@kit.PaymentKit';
| 官方 API | 作用 | 签名 |
|---|---|---|
paymentService.requestPayment(context, orderStr): Promise<void> | 拉起 Payment Kit 收银台(单次支付) | context: common.UIAbilityContext;orderStr: string(服务器预下单返回的订单串,含 app_id/merc_no/prepay_id/timestamp/noncestr/sign/auth_id) |
paymentService.requestPayment(context, orderStr, callback) | 回调版本 | callback: AsyncCallback<void> |
实物支付完整链路(官方 6 步):
- 客户端 → 商户服务器:创建商品订单
- 商户服务器 → Payment Kit:预下单,拿到
prepayId - 商户服务器 → 客户端:把
prepayId等组装成orderStr - 客户端:
paymentService.requestPayment(context, orderStr)拉起收银台 - 用户在收银台完成支付
- Payment Kit 服务端 → 商户服务器:异步通知支付结果(客户端结果仅用于展示,必须以服务端通知为准)
⚠️ IAP Kit vs Payment Kit 到底用哪个? 官方口径非常明确:IAP Kit 只能卖虚拟商品(游戏币、会员、道具),实体商品(衣服、零食、手机)必须走 Payment Kit。校园嗨购里「9.9 元抢购宿舍零食大礼包」是实物 → Payment Kit;「抽奖资格券」是虚拟 → IAP Kit。混用会被驳回。
2.5 Service Collaboration Kit — 碰一碰拼单 / 分享优惠券
真实导入模块(HarmonyOS 7 / API 26 首发):
import { serviceInteraction } from '@kit.ServiceCollaborationKit';
| 官方 API | 作用 |
|---|---|
serviceInteraction.onServiceInteraction(config, callbacks) | 注册碰一碰监听(triggerType: TriggerType.KNOCK) |
serviceInteraction.offServiceInteraction(config, callbacks?) | 页面销毁必须调用(配对原则) |
callbacks.onDeviceConnect(conn) | 碰一碰成功,返回已鉴权 + 加密的 Connection |
callbacks.onMessageReceive(conn, dataArr) | 收到对端 sendMessage 的数据(Uint8Array) |
callbacks.onDeviceDisconnect(conn) | 对端断开 |
callbacks.onError | 出错回调(蓝牙/WLAN 未开等) |
conn.sendMessage(buf): Promise<void> | 发送数据(转 ArrayBuffer) |
conn.disconnect(): Promise<void> | 传完立即断开 |
前置与权限:ohos.permission.KNOCK_COLLABORATION(受限权限,走 ACLA 申请);两台设备必须打开 WLAN + 蓝牙 + 亮屏解锁。
本节 API 的逐字段核对过程详见姊妹篇《HarmonyOS 7 精准碰一碰:跨端纪念章一触即传》,本文只做购物节场景落地。
三、完整实现:从下单到签收的四条原生链路
3.1 module.json5 总配置(权限 + 卡片扩展 + 支付身份)
// entry/src/main/module.json5
{
"module": {
"name": "entry",
"type": "entry",
// ... 其它字段省略
// ① 碰一碰拼单:受限权限(走 ACLA 申请)
"requestPermissions": [
{ "name": "ohos.permission.KNOCK_COLLABORATION", "reason": "$string:reason_knock" }
],
// ② 支付身份信息(IAP Kit / Payment Kit 都需要,值为 AGC 上的 Client ID)
"metadata": [
{ "name": "client_id", "value": "1234567890" }
],
// ③ 卡片提供方扩展(Form Kit)
"extensionAbilities": [
{
"name": "EntryFormAbility",
"srcEntry": "./ets/entryformability/EntryFormAbility.ets",
"label": "$string:form_label",
"description": "$string:form_desc",
"type": "form",
"metadata": [
{ "name": "ohos.extension.form", "resource": "$profile:form_config" }
]
}
]
}
}
app.json5里的bundleName必须与 AGC 创建应用时的包名完全一致,否则 IAP / Payment 都会拉起失败。
3.2 物流实况窗:OrderDeliveryLiveView.ets
/**
* @file OrderDeliveryLiveView.ets
* @brief 购物节物流实况窗(event: 'DELIVERY' + LAYOUT_TYPE_PROGRESS)
* Order delivery Live View manager.
* 所有 API 均对齐 @kit.LiveViewKit 官方文档(2026-09-09)。
*/
import { liveViewManager } from '@kit.LiveViewKit';
import { wantAgent, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN: number = 0x0000;
const TAG: string = 'OrderDeliveryLiveView';
/** 物流节点(与 Codelab 配送场景状态机一致) */
export enum DeliveryStatus {
WAITING_PAYMENT = 1, // 待支付
WAITING_MERCHANT = 2, // 待商家发货
WAITING_RIDER = 3, // 待分配骑手/快递员
RIDER_GET_ORDER = 4, // 已接单
PRODUCT_DELIVERING = 6, // 派送中
PRODUCT_TO_CABINET = 7, // 已到快递柜/驿站
PRODUCT_DELIVERED = 8 // 已签收
}
export class OrderDeliveryLiveView {
/** 同一订单复用同一 id(官方:id 由开发者自生成,用于创建/更新/结束/查询) */
private static readonly ID_BASE: number = 20261101000;
/** sequence 必须单调递增,否则报 1003500011 */
private static sequence: number = 0;
/** 构建点击动作的跳转代理(官方要求 clickAction 必须由 wantAgent 构建) */
private static async buildClickAction(orderId: string): Promise<Want> {
const info: wantAgent.WantAgentInfo = {
wants: [{
bundleName: 'com.campus.haigou',
abilityName: 'EntryAbility',
parameters: { orderId, from: 'liveview' } // 实况窗点击回流直达订单详情
} as Want],
actionType: wantAgent.OperationType.START_ABILITIES,
requestCode: 0,
actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
};
return await wantAgent.getWantAgent(info);
}
/** 前置检查:实况窗总开关 */
static async isEnabled(): Promise<boolean> {
try {
return await liveViewManager.isLiveViewEnabled();
} catch (e) {
const err = e as BusinessError;
hilog.error(DOMAIN, TAG, `isLiveViewEnabled failed: ${err.code} ${err.message}`);
return false;
}
}
/** 构造 LiveView 对象(创建与更新共用同一份结构) */
private static async build(
orderId: string,
status: DeliveryStatus,
progress: number, // 0~100,用于 LAYOUT_TYPE_PROGRESS 进度条
capsuleHint: string, // 胶囊文案,如 '派送中'
tail: string // 辅助区文案,如 '距您 1.2km'
): Promise<liveViewManager.LiveView> {
OrderDeliveryLiveView.sequence += 1;
const id: number = OrderDeliveryLiveView.ID_BASE + Number(orderId.slice(-5));
return {
id,
event: 'DELIVERY', // ★ 官方:即时配送/物流场景
sequence: OrderDeliveryLiveView.sequence, // ★ 必须单调递增
isMute: false,
liveViewData: {
primary: {
title: status === DeliveryStatus.PRODUCT_DELIVERED ? '已签收' : '快递 · 派送中',
content: [
{ text: '订单 ' },
{ text: orderId.slice(-6), textColor: '#FFFF2D55' } // 官方 RichText 支持 textColor
],
keepTime: 120, // 签收后通知中心留存 120s
clickAction: await OrderDeliveryLiveView.buildClickAction(orderId),
extensionData: { // 辅助区:右侧次要信息
text: tail,
type: liveViewManager.ExtensionType.EXTENSION_TYPE_COMMON_TEXT
},
layoutData: { // 扩展区:进度可视化模板
layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PROGRESS,
progress,
color: '#FFFF2D55', // 已完成节点:主红
backgroundColor: '#33FF2D55', // 未完成节点
lineType: liveViewManager.LineType.LINE_TYPE_DOTTED_LINE,
nodeIcons: ['node_pay.png', 'node_out.png', 'node_send.png', 'node_done.png']
}
},
capsule: { // 胶囊态(状态栏)
type: liveViewManager.CapsuleType.CAPSULE_TYPE_TEXT,
status: 1, // 官方示例使用 1 表示处于生效态
icon: 'capsule_delivery.png',
title: capsuleHint,
content: `${progress}%`,
backgroundColor: '#FFFF2D55'
}
}
};
}
/** 创建实况窗(支付成功 / 商家发货后调用一次) */
static async start(orderId: string): Promise<void> {
if (!(await OrderDeliveryLiveView.isEnabled())) {
hilog.warn(DOMAIN, TAG, 'liveView disabled, skip start');
return;
}
const lv = await OrderDeliveryLiveView.build(
orderId, DeliveryStatus.WAITING_RIDER, 25, '待揽收', '商家已发货');
try {
const res = await liveViewManager.startLiveView(lv);
hilog.info(DOMAIN, TAG, `startLiveView ok: ${JSON.stringify(res)}`);
} catch (e) {
const err = e as BusinessError;
// 1003500006 = 实况窗已存在 → 改为更新;1003500005 = 权益未申请
if (err.code === 1003500006) {
await OrderDeliveryLiveView.update(orderId, DeliveryStatus.WAITING_RIDER, 25, '待揽收', '商家已发货');
} else {
hilog.error(DOMAIN, TAG, `startLiveView failed: ${err.code} ${err.message}`);
}
}
}
/** 更新实况窗(物流每到一个节点更新一次) */
static async update(
orderId: string, status: DeliveryStatus, progress: number, capsuleHint: string, tail: string
): Promise<void> {
const lv = await OrderDeliveryLiveView.build(orderId, status, progress, capsuleHint, tail);
try {
await liveViewManager.updateLiveView(lv);
} catch (e) {
const err = e as BusinessError;
hilog.error(DOMAIN, TAG, `updateLiveView failed: ${err.code} ${err.message}`);
}
}
/** 结束实况窗(签收 24 小时后调用) */
static async stop(orderId: string): Promise<void> {
OrderDeliveryLiveView.sequence += 1;
const lv: liveViewManager.LiveView = {
id: OrderDeliveryLiveView.ID_BASE + Number(orderId.slice(-5)),
event: 'DELIVERY',
sequence: OrderDeliveryLiveView.sequence,
isMute: true
} as liveViewManager.LiveView;
try {
await liveViewManager.stopLiveView(lv);
} catch (e) {
const err = e as BusinessError;
hilog.warn(DOMAIN, TAG, `stopLiveView: ${err.code} ${err.message}`);
}
}
}
物流节点 → 实况窗进度映射表:
| 物流节点 | DeliveryStatus | progress | 胶囊文案 | 辅助区 |
|---|---|---|---|---|
| 已付款,商家备货 | 2 | 15% | 待发货 | 预计 48h 内发出 |
| 已出库,等待揽收 | 3 | 30% | 待揽收 | 仓库已打包 |
| 快递员已接单 | 4 | 55% | 已揽收 | 正在前往中转 |
| 派送中 | 6 | 80% | 派送中 | 距您 1.2km |
| 到达驿站/快递柜 | 7 | 92% | 待取件 | 菜鸟驿站 · 3 号柜 |
| 已签收 | 8 | 100% | 已签收 | 感谢使用校园嗨购 |
3.3 秒杀倒计时卡片:EntryFormAbility.ets + SecKillCard.ets
卡片提供方(FormExtensionAbility):
/**
* @file EntryFormAbility.ets
* @brief 秒杀卡片提供方(Form Kit · Stage 模型)
*/
import { FormExtensionAbility, formBindingData, formProvider, formInfo } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN: number = 0x0000;
const TAG: string = 'EntryFormAbility';
/** 秒杀场次表(真实业务应来自 Preferences / 本地缓存) */
const SECKILL_SESSIONS: Array<{ time: string, name: string, price: string }> = [
{ time: '10:00', name: '宿舍零食大礼包', price: '9.9' },
{ time: '14:00', name: '羽绒服专场', price: '99' },
{ time: '20:00', name: '全场 5 折 · 终极场', price: '50%' }
];
export default class EntryFormAbility extends FormExtensionAbility {
/** 卡片被添加到桌面:返回初始数据(数据会绑定到卡片页面的 LocalStorage) */
onAddForm(want: Want): formBindingData.FormBindingData {
hilog.info(DOMAIN, TAG, 'onAddForm');
const data = {
title: '限时秒杀',
sessionName: SECKILL_SESSIONS[2].name,
startTime: SECKILL_SESSIONS[2].time,
price: SECKILL_SESSIONS[2].price,
tip: '点击卡片进入秒杀场'
};
return formBindingData.createFormBindingData(data);
}
/** 定时刷新 / 定点刷新到点:重新拉取场次数据 */
onUpdateForm(formId: string): void {
hilog.info(DOMAIN, TAG, `onUpdateForm: ${formId}`);
const data = {
title: '限时秒杀',
sessionName: SECKILL_SESSIONS[2].name,
startTime: SECKILL_SESSIONS[2].time,
price: SECKILL_SESSIONS[2].price,
tip: '即将开抢,别错过'
};
formProvider.updateForm(formId, formBindingData.createFormBindingData(data))
.catch((e: Error) => hilog.error(DOMAIN, TAG, `updateForm fail: ${e.message}`));
// 定点刷新:距离 20:00 开场还有多久,就安排多久后刷新一次(最小 5 分钟)
const minutes: number = EntryFormAbility.minutesToNextSession();
formProvider.setFormNextRefreshTime(formId, minutes)
.catch((e: Error) => hilog.error(DOMAIN, TAG, `setFormNextRefreshTime fail: ${e.message}`));
}
/** 卡片通过 postCardAction 发消息给提供方(如点击「提醒我」) */
onEvent(formId: string, message: string): void {
hilog.info(DOMAIN, TAG, `onEvent: ${formId} -> ${message}`);
}
onRemoveForm(formId: string): void {
hilog.info(DOMAIN, TAG, `onRemoveForm: ${formId}`);
}
onAcquireFormState(want: Want): formInfo.FormState {
return formInfo.FormState.READY;
}
/** 距离下一个秒杀场次还有多少分钟(官方限制:≥5 分钟) */
private static minutesToNextSession(): number {
const now = new Date();
const target = new Date();
target.setHours(20, 0, 0, 0);
if (target.getTime() <= now.getTime()) { target.setDate(target.getDate() + 1); }
const diff: number = Math.ceil((target.getTime() - now.getTime()) / 60000);
return Math.max(5, diff);
}
}
卡片页面(ArkTS 卡片 UI):
/**
* @file widget/pages/SecKillCard.ets
* @brief 秒杀卡片 UI(@LocalStorageProp 绑定提供方下发的数据)
*/
import { postCardAction } from '@kit.ArkUI';
@Entry
@Component
struct SecKillCard {
@LocalStorageProp('title') title: string = '限时秒杀';
@LocalStorageProp('sessionName') sessionName: string = '全场 5 折 · 终极场';
@LocalStorageProp('startTime') startTime: string = '20:00';
@LocalStorageProp('price') price: string = '50%';
@LocalStorageProp('tip') tip: string = '点击卡片进入秒杀场';
build() {
Column() {
Row() {
Text('🔥 ' + this.title).fontSize(14).fontWeight(800).fontColor('#FFFFFF')
Blank()
Text('v2.8.0').fontSize(10).fontColor('#FFC93C')
}.width('100%')
Text(this.sessionName).fontSize(13).fontColor('#E5E5E5')
.maxLines(1).margin({ top: 6 })
Row() {
Text(this.startTime).fontSize(26).fontWeight(900).fontColor('#FF2D55')
Text(' 开抢').fontSize(12).fontColor('#9AA0B4')
Blank()
Text('¥' + this.price).fontSize(16).fontWeight(800).fontColor('#FFC93C')
}.width('100%').margin({ top: 8 })
Text(this.tip).fontSize(11).fontColor('#8A8FA3').margin({ top: 6 })
}
.width('100%').height('100%')
.padding(14)
.backgroundColor('#1A1B26')
.linearGradient({ angle: 160, colors: [['#2A1418', 0], ['#1A1B26', 1]] })
.onClick(() => {
// 官方:卡片与主应用交互统一走 postCardAction
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
params: { from: 'secKillCard', session: this.startTime }
});
})
}
}
主应用侧主动刷新卡片(如支付成功后立刻刷新「我的订单」卡片):
/**
* @brief 主应用主动刷新秒杀卡片 / 物流卡片(官方 formProvider.updateForm)
*/
import { formProvider, formBindingData } from '@kit.FormKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
export async function pushFormUpdate(formId: string, payload: Record<string, Object>): Promise<void> {
try {
const binding = formBindingData.createFormBindingData(payload);
await formProvider.updateForm(formId, binding);
// 顺带安排下一次定点刷新(≥5 分钟)
await formProvider.setFormNextRefreshTime(formId, 5);
} catch (e) {
hilog.error(0x0000, 'FormPush', `pushFormUpdate fail: ${(e as Error).message}`);
}
}
3.4 支付:虚拟商品走 IAP,实物走 Payment
/**
* @file PayService.ets
* @brief 双支付通道:IAP Kit(虚拟商品)+ Payment Kit(实物商品)
*/
import { iap } from '@kit.IAPKit';
import { paymentService } from '@kit.PaymentKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN: number = 0x0000;
const TAG: string = 'PayService';
export class PayService {
// ————————————————————————————————————————————
// 通道 A:IAP Kit —— 虚拟商品(抽奖资格券 / PLUS 年卡 / 免运费券订阅)
// ————————————————————————————————————————————
static async prepareIap(ctx: common.UIAbilityContext): Promise<boolean> {
try {
// 官方步骤 1:检查账号服务地是否支持应用内支付(目前仅中国大陆)
await iap.queryEnvironmentStatus(ctx);
return true;
} catch (e) {
const err = e as BusinessError;
hilog.error(DOMAIN, TAG, `queryEnvironmentStatus fail: ${err.code} ${err.message}`);
return false;
}
}
/** 查询商品(AGC 上已配置的商品 ID) */
static async queryLotteryProduct(ctx: common.UIAbilityContext): Promise<void> {
const param: iap.QueryProductsParameter = {
productType: iap.ProductType.CONSUMABLE, // 抽奖资格券 = 消耗型
productIds: ['haigou_lottery_ticket_01']
};
const res = await iap.queryProducts(ctx, param);
hilog.info(DOMAIN, TAG, `products: ${JSON.stringify(res.productList)}`);
}
/** 发起购买(官方步骤 3):拉起 IAP 收银台 */
static async buyLotteryTicket(ctx: common.UIAbilityContext): Promise<void> {
try {
const param: iap.PurchaseParameter = {
productId: 'haigou_lottery_ticket_01',
productType: iap.ProductType.CONSUMABLE,
developerPayload: JSON.stringify({ from: 'double11', ts: Date.now() })
};
const result = await iap.createPurchase(ctx, param);
// purchaseData 为 JSON 字符串,取其中的 jwsPurchaseOrder 做验签
const jwsPurchaseOrder: string = JSON.parse(result.purchaseData).jwsPurchaseOrder;
hilog.info(DOMAIN, TAG, `createPurchase ok, jws length=${jwsPurchaseOrder.length}`);
// 验签成功后 → 发放权益 → 调 finishPurchase 确认发货(见 consumeAndFinish)
} catch (e) {
const err = e as BusinessError;
// 官方:1001860001 / 1001860051 需要走补发货流程
hilog.error(DOMAIN, TAG, `createPurchase fail: ${err.code} ${err.message}`);
}
}
/** 确认发货(官方步骤 4):productType / purchaseToken / purchaseOrderId 三项必填 */
static async consumeAndFinish(
ctx: common.UIAbilityContext,
productType: iap.ProductType,
purchaseToken: string,
purchaseOrderId: string
): Promise<void> {
const param: iap.FinishPurchaseParameter = { productType, purchaseToken, purchaseOrderId };
await iap.finishPurchase(ctx, param);
}
/** 掉单兜底(官方步骤 5):查「已购未发货」并补发 */
static async recoverUnfinished(ctx: common.UIAbilityContext): Promise<string[]> {
const param: iap.QueryPurchasesParameter = {
productType: iap.ProductType.CONSUMABLE,
queryType: iap.PurchaseQueryType.UNFINISHED
};
const res: iap.QueryPurchaseResult = await iap.queryPurchases(ctx, param);
return res.purchaseDataList ?? [];
}
// ————————————————————————————————————————————
// 通道 B:Payment Kit —— 实物商品(宿舍零食大礼包 / 羽绒服)
// 注意:orderStr 必须由「服务器预下单」返回,客户端不得自行拼装
// ————————————————————————————————————————————
static async payPhysical(ctx: common.UIAbilityContext, orderStr: string): Promise<void> {
try {
// 官方:orderStr 含 app_id / merc_no / prepay_id / timestamp / noncestr / sign / auth_id
await paymentService.requestPayment(ctx, orderStr);
// ⚠️ 客户端回调仅用于展示「支付中」,
// 真正发货必须以 Payment Kit 服务端异步通知为准(防篡改)
hilog.info(DOMAIN, TAG, 'requestPayment finished (wait server notify)');
} catch (e) {
const err = e as BusinessError;
hilog.error(DOMAIN, TAG, `requestPayment fail: ${err.code} ${err.message}`);
}
}
}
支付完成后联动:PayService.payPhysical 的 Promise resolve 只代表「收银台已关闭」。正确做法是等服务器异步通知到账后,服务器推送长连接消息给客户端,客户端再:
- 调用
OrderDeliveryLiveView.start(orderId)创建物流实况窗; - 调用
pushFormUpdate(formId, {...})刷新桌面「我的订单」卡片。
3.5 碰一碰拼单:GroupBuyKnock.ets
/**
* @file GroupBuyKnock.ets
* @brief 碰一碰拼单:室友背靠背轻碰,直接进入同一拼单团
* (API 对齐 @kit.ServiceCollaborationKit · KNOCK,HarmonyOS 7 / API 26)
*/
import { serviceInteraction } from '@kit.ServiceCollaborationKit';
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN: number = 0x0000;
const TAG: string = 'GroupBuyKnock';
export interface GroupBuyTicket {
groupId: string;
skuName: string;
originPrice: number;
groupPrice: number;
needMembers: number;
expireAt: number;
}
export class GroupBuyKnock {
private static ctx: Context;
private static conn: serviceInteraction.Connection | undefined;
/** 进入「拼单」页时注册 KNOCK 监听 */
static async register(ctx: Context): Promise<void> {
GroupBuyKnock.ctx = ctx;
const win = await window.getLastWindow(ctx);
serviceInteraction.onServiceInteraction({
triggerType: serviceInteraction.TriggerType.KNOCK, // ★ 精准碰一碰
windowId: win.getWindowPropertiesSync().id,
appLinking: 'https://campus.example.com/haigou/groupbuy', // 一碰即达的 DeepLink
timeOut: 20
}, {
onDeviceConnect: (conn) => {
hilog.info(DOMAIN, TAG, `device connected: ${conn.deviceName}`);
GroupBuyKnock.conn = conn;
},
onMessageReceive: (conn, data) => {
// 收到对端的拼单团信息 → 校验 → 加入
const json = new TextDecoder().decode(data);
const ticket = JSON.parse(json) as GroupBuyTicket;
hilog.info(DOMAIN, TAG, `received ticket: ${ticket.groupId} / ${ticket.skuName}`);
GroupBuyKnock.onTicketReceived?.(ticket);
},
onDeviceDisconnect: () => {
hilog.info(DOMAIN, TAG, 'peer disconnected');
GroupBuyKnock.conn = undefined;
},
onError: (err) => {
hilog.error(DOMAIN, TAG, `knock error: ${err.code} ${err.message}`);
}
});
}
/** 自己发起拼单:把团信息发给碰到的室友 */
static async shareTicket(ticket: GroupBuyTicket): Promise<void> {
if (!GroupBuyKnock.conn) {
hilog.warn(DOMAIN, TAG, 'no connection, ask user to knock first');
return;
}
const buf = new TextEncoder().encode(JSON.stringify(ticket)).buffer;
await GroupBuyKnock.conn.sendMessage(buf);
await GroupBuyKnock.conn.disconnect(); // 官方:传完立即断,避免占通道
GroupBuyKnock.conn = undefined;
}
/** 页面销毁:必须注销(配对原则) */
static unregister(): void {
serviceInteraction.offServiceInteraction({
triggerType: serviceInteraction.TriggerType.KNOCK,
windowId: -1,
appLinking: 'https://campus.example.com/haigou/groupbuy'
});
}
/** 业务侧注入的回调(收到团信息后跳转拼单详情) */
static onTicketReceived?: (t: GroupBuyTicket) => void;
}
四、购物节运营专题 + 高发错误码
4.1 四条链路的购物节运营节奏
下表用 D = 购物节启动日 的相对时间标注,便于直接套用到任意一场大促。
| 时间 | 运营动作 | 依赖的原生能力 |
|---|---|---|
| D-18 20:00 预售开启 | 预售订单支付后立即创建实况窗,胶囊显示「预售已锁定」 | Live View Kit |
| D-17 ~ D-1 | 桌面卡片每天定点刷新「距购物节还有 N 天」 | Form Kit setFormNextRefreshTime |
| D 日 00:00 | 全场开场,卡片切到「00:00 终极场」并高亮 | Form Kit + updateDuration |
| D 日 每 2 小时 | 整点秒杀场次提醒,卡片定点刷新对齐整点 | Form Kit |
| D 日 全天 | 物流实况窗随节点更新,6 次状态流转 | Live View Kit updateLiveView |
| D+1 ~ D+3 | 签收后 24h 自动结束实况窗 | Live View Kit stopLiveView |
| 全程 | 宿舍拼单:碰一碰入团,免加好友 | Service Collaboration Kit |
4.2 线上真实高发错误码与处置
| 模块 | 错误码 | 官方含义 | 线上处置 |
|---|---|---|---|
| LiveView | 1003500005 | 实况窗权益未申请 | 提前在 AGC 申请「实况窗服务」权益,未申请时降级为普通通知 |
| LiveView | 1003500006 | 实况窗已存在 | startLiveView 失败即转 updateLiveView(本文代码已内置) |
| LiveView | 1003500008 | 每秒创建数量超限 | 合并节点更新,同一订单 1 秒内只更新一次 |
| LiveView | 1003500011 | sequence 顺序不正确 | sequence 全局静态自增,禁止重置 |
| LiveView | 1003500021 | 找不到自定义扩展区 ExtensionAbility | 改用内置 LAYOUT_TYPE_PROGRESS,不自定义 |
| Form | — | 卡片刷新无效果 | 检查 formId 是否为当前应用卡片;updateDuration 单位是 30 分钟 |
| Form | — | 定点刷新报错 | setFormNextRefreshTime 的 minute 必须 ≥ 5 |
| IAP | 1001860001 / 1001860051 | 掉单 | 启动时 + 命中时调 queryPurchases(UNFINISHED) 补发货,再 finishPurchase |
| IAP | — | queryEnvironmentStatus 失败 | 仅中国大陆支持结算,海外账号需提示 |
| Payment | — | 拉起收银台失败 | orderStr 必须来自服务端预下单;bundleName 与 AGC 包名一致 |
| KNOCK | — | 碰一碰无反应 | 检查 WLAN + 蓝牙是否都开;KNOCK_COLLABORATION 是否已获 ACL 审批 |
一条硬规则:实况窗 / 卡片 / 支付都不做刚需依赖。任何一环失败,都必须能降级为「普通页面 + 普通通知」,否则大促当天一次权益申请遗漏就是一次事故。
五、写在最后:购物节的「快」,是工程能力,不是营销话术
把购物节的四条链路做完,团队最深的感受是:
用户的每一次「打开 App 看一眼」,都是一次体验损耗。
- 实况窗把「看一眼物流」从 19 次打开 压到 4 次;
- 卡片把「守着秒杀」从 提前 5 分钟打开 App 变成 桌面一眼可见;
- 原生支付把「9 步凑单」压成 1 次点击直达收银台;
- 碰一碰把「加好友发链接」变成 两台手机碰一下。
这四件事,没有一件是「加个弹窗」「做个活动页」能解决的。它们都需要把系统能力用到刀刃上——而 HarmonyOS 7 恰好给了这些刀:
liveViewManager+event: 'DELIVERY'+LAYOUT_TYPE_PROGRESS→ 物流实况窗formProvider.updateForm+setFormNextRefreshTime→ 秒杀卡片iap.createPurchase/paymentService.requestPayment→ 双支付通道serviceInteraction+TriggerType.KNOCK→ 碰一碰拼单
本文所有 API 均已逐条对齐 developer.huawei.com 官方文档(2026-09-09 版本),import 路径、枚举名、方法签名、错误码可直接在 DevEco Studio(HarmonyOS 7 / API 26 SDK)中命中智能提示。
购物节见。祝各位的购物车,都被实况窗温柔地送到门口。
— END —
附:本文 API 速查卡(可直接贴进代码仓库 README)
// ① 物流实况窗
import { liveViewManager } from '@kit.LiveViewKit';
liveViewManager.isLiveViewEnabled(); // 前置检查
liveViewManager.startLiveView(liveView); // 创建
liveViewManager.updateLiveView(liveView); // 更新(sequence 自增)
liveViewManager.stopLiveView(liveView); // 结束
// event: 'DELIVERY' | 'TAXI' | 'PICK_UP' ...
// LayoutType.LAYOUT_TYPE_PROGRESS / PICKUP / FLIGHT / SCORE / NAVIGATION / CUSTOM
// CapsuleType.CAPSULE_TYPE_TEXT
// ExtensionType.EXTENSION_TYPE_COMMON_TEXT
// LineType.LINE_TYPE_DOTTED_LINE
// ② 购物节服务卡片
import { formProvider, formBindingData, FormExtensionAbility } from '@kit.FormKit';
formProvider.updateForm(formId, formBindingData.createFormBindingData(obj)); // 主动刷新
formProvider.setFormNextRefreshTime(formId, minute); // 定点刷新,minute ≥ 5
// form_config.json: updateDuration 单位 30 分钟(1~336)
// FormExtensionAbility: onAddForm / onUpdateForm / onEvent / onRemoveForm / onAcquireFormState
// ③ 虚拟商品内购
import { iap } from '@kit.IAPKit';
iap.queryEnvironmentStatus(ctx);
iap.queryProducts(ctx, { productType: iap.ProductType.CONSUMABLE, productIds: ['xxx'] });
iap.createPurchase(ctx, { productId: 'xxx', productType: iap.ProductType.CONSUMABLE });
iap.finishPurchase(ctx, { productType, purchaseToken, purchaseOrderId });
iap.queryPurchases(ctx, { productType: iap.ProductType.CONSUMABLE, queryType: iap.PurchaseQueryType.UNFINISHED });
// iap.ProductType: CONSUMABLE | NONCONSUMABLE | AUTORENEWABLE
// ④ 实物商品支付
import { paymentService } from '@kit.PaymentKit';
paymentService.requestPayment(ctx, orderStr); // orderStr 由服务端预下单返回
// ⑤ 碰一碰拼单
import { serviceInteraction } from '@kit.ServiceCollaborationKit';
serviceInteraction.onServiceInteraction(
{ triggerType: serviceInteraction.TriggerType.KNOCK, windowId, appLinking, timeOut: 20 },
{ onDeviceConnect, onMessageReceive, onDeviceDisconnect, onError });
serviceInteraction.offServiceInteraction(config);
更多推荐

所有评论(0)