HarmonyOS 订单售后闭环实战:申请、审核、退款、通知与追踪

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

请添加图片描述

这篇文章关注四个结果:

  1. 用户能在订单详情里发起售后,并看到明确限制。
  2. 审核状态和退款状态分开维护,避免一笔订单多个说法。
  3. 退款失败有补偿记录,不靠人工口头同步。
  4. 通知和详情页使用同一套状态,减少客服解释成本。

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 侧要做的是把售后状态展示稳定,把申请材料收集完整,把时间线和通知统一;最终退款动作和平台结果由服务端闭环处理。这样商业化链路才不会在支付成功之后断掉。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐