HarmonyOS 订单售后闭环实战:申请、审核、退款、通知与追踪
HarmonyOS 订单售后闭环实战:申请、审核、退款、通知与追踪
商业化功能上线以后,售后链路会直接影响用户信任。用户不关心系统内部叫订单、退款单还是工单,他只想知道:能不能申请、现在谁在处理、钱什么时候退、失败后找谁。很多应用把支付做得很完整,却把售后放在客服群或表格里处理,最后导致订单详情页没有进度、客服说法不一致、研发无法追踪退款结果。本文用 HarmonyOS 应用的订单售后场景,拆出申请、审核、退款、通知和追踪五个环节。

这篇文章关注四个结果:
- 用户能在订单详情里发起售后,并看到明确限制。
- 审核状态和退款状态分开维护,避免一笔订单多个说法。
- 退款失败有补偿记录,不靠人工口头同步。
- 通知和详情页使用同一套状态,减少客服解释成本。
1. 售后闭环先拆申请和退款
售后不是退款按钮。申请可能被驳回,审核可能需要补充材料,退款可能处理中或失败,通知可能需要多次触达。把这些动作挤进一个 refundStatus 字段,会让页面和客服都无法解释。

| 阶段 | 典型状态 | 说明 |
|---|---|---|
| 申请 | 可申请、不可申请、已提交 | 判断订单是否满足售后条件 |
| 审核 | 待审核、需补充、已通过、已拒绝 | 决定是否进入退款 |
| 退款 | 待退款、退款中、退款成功、退款失败 | 和支付渠道、平台订单关联 |
| 通知 | 待通知、已通知、通知失败 | 保证用户知道处理结果 |
| 追踪 | 工单关闭、重新打开 | 保留客服和研发可复查记录 |
拆分阶段以后,页面可以展示“审核通过,退款处理中”,而不是只给用户一个含糊的“处理中”。
2. 官方资料边界和售后模块落点
华为 Payment Kit 提供退款申请相关接口,商户平台也支持退款记录查询;对于应用内支付订单,用户侧也有自助退款和进度查询路径。应用自己的售后系统要做的是把业务订单、退款申请、平台退款结果串起来。
| 资料入口 | 工程落点 |
|---|---|
| Payment Kit 申请退款 | 成功交易可以发起退款申请,并通过回调处理退款结果 |
| 退款记录查询 | 客服和财务可根据订单号查询退款记录 |
| 退款管理-数字商品 | 用户侧退款入口和条件需要在页面中解释清楚 |
| 应用内支付订单退款说明 | 用户可能从系统入口申请退款,应用需要能同步外部结果 |
建议目录:
entry/src/main/ets/
common/aftersale/AfterSaleRequest.ets
common/aftersale/ReviewWorkflow.ets
common/aftersale/RefundCoordinator.ets
common/aftersale/AfterSaleNotifier.ets
common/aftersale/AfterSaleTimeline.ets
pages/order/AfterSalePage.ets
售后模块不应直接依赖某个页面。订单详情、客服入口、消息通知都应该读取同一份售后时间线。
3. AfterSaleRequest 校验申请条件
申请入口首先要判断订单是否允许售后。比如未支付订单不能申请退款,已退款订单不能重复申请,超过业务期限的订单要给出原因。这里的校验要返回用户可读的拒绝原因。
export type OrderPayState = 'UNPAID' | 'PAID' | 'REFUNDING' | 'REFUNDED' | 'CLOSED'
export interface PaidOrder {
orderNo: string
productName: string
payState: OrderPayState
paidAt?: number
amountFen: number
}
export interface ApplyCheckResult {
allowed: boolean
reason?: string
}
export class AfterSaleRequest {
canApply(order: PaidOrder, now: number): ApplyCheckResult {
if (order.payState !== 'PAID') {
return { allowed: false, reason: '当前订单状态不支持发起售后' }
}
if (!order.paidAt) {
return { allowed: false, reason: '订单缺少支付完成时间,需联系客服处理' }
}
const sevenDays = 7 * 24 * 60 * 60 * 1000
if (now - order.paidAt > sevenDays) {
return { allowed: false, reason: '订单已超过可申请售后的时间范围' }
}
return { allowed: true }
}
}
这段代码的重点是返回原因。不要只给用户一个灰色按钮却不解释,否则用户会转向评论区或客服投诉。
4. 售后申请要保留凭证和来源
用户提交售后时,至少要记录原因、描述、凭证、来源页面和关联订单。凭证不是一定要图片,也可以是日志编号、异常截图说明、客服会话 ID。
export type AfterSaleReason = 'DUPLICATE_PAY' | 'SERVICE_UNAVAILABLE' | 'WRONG_PRODUCT' | 'OTHER'
export interface AfterSaleApplyForm {
orderNo: string
reason: AfterSaleReason
description: string
evidenceUrls: string[]
source: 'order_detail' | 'customer_service' | 'system_refund_sync'
}
export function normalizeApplyForm(form: AfterSaleApplyForm): AfterSaleApplyForm {
return {
...form,
description: form.description.trim().slice(0, 300),
evidenceUrls: form.evidenceUrls.filter(url => url.startsWith('https://')).slice(0, 6)
}
}
source 字段很实用。用户可能在应用内申请,也可能通过系统退款入口申请,或者客服代提交。不同来源的处理时效和凭证完整度不同,后台审核时需要区分。
5. ReviewWorkflow 维护审核状态
审核状态不要和退款状态混在一起。审核通过只代表同意进入退款流程,不代表钱已经到账。审核拒绝也要保留拒绝原因,方便用户补充材料或联系客服。
export type ReviewState = 'SUBMITTED' | 'NEED_MORE_INFO' | 'APPROVED' | 'REJECTED'
export interface ReviewRecord {
requestNo: string
state: ReviewState
reason?: string
operator: 'system' | 'service_staff' | 'risk_rule'
updatedAt: number
}
export class ReviewWorkflow {
approve(record: ReviewRecord): ReviewRecord {
if (record.state === 'REJECTED') {
throw new Error('已拒绝申请不能直接改为通过,需要重新提交')
}
return { ...record, state: 'APPROVED', operator: 'service_staff', updatedAt: Date.now() }
}
reject(record: ReviewRecord, reason: string): ReviewRecord {
return { ...record, state: 'REJECTED', reason, operator: 'service_staff', updatedAt: Date.now() }
}
requireMoreInfo(record: ReviewRecord, reason: string): ReviewRecord {
return { ...record, state: 'NEED_MORE_INFO', reason, operator: 'risk_rule', updatedAt: Date.now() }
}
}
这段代码保护了审核流转边界。售后是用户敏感链路,状态跳转必须可追溯,不能让任意页面直接改成“通过”。
6. RefundCoordinator 处理退款和补偿
退款动作应绑定原支付订单、退款单号和金额。退款失败不能只弹 Toast,而要记录失败原因,等待重试或人工介入。

export type RefundState = 'WAITING' | 'PROCESSING' | 'SUCCESS' | 'FAILED'
export interface RefundTask {
refundNo: string
orderNo: string
amountFen: number
state: RefundState
failReason?: string
updatedAt: number
}
export class RefundCoordinator {
async submitRefund(task: RefundTask): Promise<RefundTask> {
if (task.amountFen <= 0) {
return { ...task, state: 'FAILED', failReason: '退款金额必须大于 0', updatedAt: Date.now() }
}
// 实际项目中这里由服务端调用 Payment Kit 退款能力,客户端只展示结果。
return { ...task, state: 'PROCESSING', updatedAt: Date.now() }
}
mergeCallback(task: RefundTask, success: boolean, message?: string): RefundTask {
return {
...task,
state: success ? 'SUCCESS' : 'FAILED',
failReason: success ? undefined : message,
updatedAt: Date.now()
}
}
}
退款入口建议放在服务端,客户端不要直接持有敏感退款参数。ArkTS 侧重点是展示状态、收集申请材料、提示用户下一步动作。
7. AfterSaleNotifier 通知进度
售后状态变化后,订单详情页、站内消息、推送通知要使用同一套文案。否则页面写“退款中”,消息写“已处理”,用户会认为系统前后矛盾。
export interface AfterSaleNotice {
requestNo: string
title: string
content: string
actionText: string
}
export class AfterSaleNotifier {
buildNotice(review: ReviewRecord, refund?: RefundTask): AfterSaleNotice {
if (review.state === 'NEED_MORE_INFO') {
return {
requestNo: review.requestNo,
title: '售后申请需要补充材料',
content: review.reason ?? '请补充订单问题说明后重新提交。',
actionText: '补充材料'
}
}
if (refund?.state === 'SUCCESS') {
return {
requestNo: review.requestNo,
title: '退款已完成',
content: '退款已按支付渠道返回,请关注到账信息。',
actionText: '查看详情'
}
}
return {
requestNo: review.requestNo,
title: '售后申请处理中',
content: '我们会在订单详情中同步最新处理进度。',
actionText: '查看进度'
}
}
}
通知不只是提醒,也是在降低客服压力。用户能自己看到下一步动作,就不会反复追问同一个问题。
8. 订单详情只展示可解释状态
订单详情页建议展示一个时间线,而不是只展示最终状态。时间线包含申请提交、审核结果、退款处理、通知结果,让用户理解“现在卡在哪一步”。
export interface TimelineNode {
label: string
description: string
time?: number
active: boolean
}
export function buildAfterSaleTimeline(review: ReviewRecord, refund?: RefundTask): TimelineNode[] {
return [
{
label: '提交申请',
description: '售后申请已进入处理队列',
time: review.updatedAt,
active: true
},
{
label: '审核处理',
description: review.reason ?? '审核人员正在确认订单和凭证',
time: review.updatedAt,
active: review.state === 'APPROVED' || review.state === 'REJECTED'
},
{
label: '退款执行',
description: refund ? `退款状态:${refund.state}` : '审核通过后发起退款',
time: refund?.updatedAt,
active: refund?.state === 'PROCESSING' || refund?.state === 'SUCCESS'
},
{
label: '结果通知',
description: refund?.state === 'SUCCESS' ? '已通知用户查看到账情况' : '处理完成后会同步通知',
time: refund?.updatedAt,
active: refund?.state === 'SUCCESS'
}
]
}
时间线的好处是把内部流程翻译成用户语言。即使退款还没到账,用户也能知道申请没有丢。
9. 售后验收动作
| 场景 | 操作 | 预期结果 |
|---|---|---|
| 可申请订单 | 已支付订单进入售后页 | 展示申请入口和退款说明 |
| 不可申请订单 | 未支付或已退款订单进入售后页 | 禁用申请入口并展示原因 |
| 需要补充材料 | 审核返回 NEED_MORE_INFO |
页面和通知都引导补充材料 |
| 退款处理中 | 审核通过后提交退款 | 订单详情展示退款处理中,不显示已完成 |
| 退款失败 | 平台返回失败 | 记录失败原因,保留重试或人工处理入口 |
可以用一个状态一致性函数保护详情页展示。
export function assertAfterSaleConsistent(review: ReviewRecord, refund?: RefundTask): void {
if (review.state !== 'APPROVED' && refund) {
throw new Error('审核未通过前不能存在退款任务')
}
if (refund?.state === 'SUCCESS' && refund.amountFen <= 0) {
throw new Error('退款成功记录必须包含有效金额')
}
}
这类断言能提前发现“审核还没通过,页面却显示退款成功”的错误。
10. 售后工单异常排查表
| 现象 | 优先查看 | 处理建议 |
|---|---|---|
| 用户看不到售后入口 | 订单支付状态、可申请期限、商品类型 | 给出不可申请原因,不要只隐藏按钮 |
| 退款状态和客服说法不一致 | 审核记录、退款任务、通知记录 | 所有入口统一读取售后时间线 |
| 退款失败无人处理 | 退款回调、失败原因、重试队列 | 增加补偿台账和人工介入标记 |
| 用户重复提交申请 | 订单号和当前售后单 | 同一订单只允许一个进行中的售后单 |
| 系统入口退款后应用不同步 | 平台退款记录、服务端同步任务 | 定时同步退款结果并更新订单详情 |
11. 小结:售后要让用户看见进度
订单售后的质量取决于“可解释”。用户能不能申请、审核到了哪一步、退款是否发起、失败后谁处理,都应该在订单详情和通知里说清楚。ArkTS 侧要做的是把售后状态展示稳定,把申请材料收集完整,把时间线和通知统一;最终退款动作和平台结果由服务端闭环处理。这样商业化链路才不会在支付成功之后断掉。
更多推荐



所有评论(0)