HarmonyOS开发:订单管理订单流程
HarmonyOS开发:订单管理订单流程
📌 核心要点:订单是电商的核心数据,状态机设计决定流程正确性,下单→支付→发货→收货→退换货全链路,每个状态转换都要有据可查。
背景与动机
用户点了"立即购买",然后呢?创建订单→支付→等待发货→确认收货,看起来一条直线,实际上每个环节都可能出岔子。
支付超时了订单怎么办?用户付了钱商家不发货怎么办?发货后用户想退货怎么办?退货后退款怎么处理?订单状态怎么回滚?并发下单怎么防重复?
订单管理不是简单的CRUD,它是一个状态机。每个订单有明确的状态,状态之间的转换有严格的规则——你不能从"待支付"直接跳到"已完成",也不能从"已取消"变成"待发货"。状态转错了,数据就乱了,钱就错了。
更麻烦的是退换货。用户收到货不满意,申请退货→商家审核→用户退货→商家收货→退款,这条链路比下单还长。哪个环节卡住了,用户就在那等着,等急了就是差评。
这篇文章把订单状态机、下单流程、订单列表、退换货全拆开讲。
核心原理
订单的核心是有限状态机(FSM)。每个订单在任意时刻只处于一个状态,状态之间的转换由事件触发,转换规则是预定义的。
flowchart TD
A[创建订单] --> B[待支付]
B -->|支付成功| C[待发货]
B -->|支付超时/取消| D[已取消]
C -->|商家发货| E[待收货]
C -->|商家缺货| F[退款中]
E -->|确认收货| G[已完成]
E -->|申请退货| H[退货审核]
H -->|审核通过| I[退货中]
H -->|审核拒绝| E
I -->|商家收货确认| J[退款中]
I -->|商家拒收| K[退货争议]
J -->|退款成功| L[已退款]
F -->|退款成功| L
G -->|申请售后| H
classDef pending fill:#1565C0,color:#fff,stroke:#0D47A1
classDef success fill:#2E7D32,color:#fff,stroke:#1B5E20
classDef warning fill:#E65100,color:#fff,stroke:#BF360C
classDef error fill:#C62828,color:#fff,stroke:#B71C1C
classDef refund fill:#6A1B9A,color:#fff,stroke:#4A148C
class A,B,pending
class C,E,G,success
class D,warning
class K,error
class F,H,I,J,L,refund
订单状态定义
| 状态 | 说明 | 可执行操作 |
|---|---|---|
| 待支付 | 订单已创建,等待用户支付 | 支付、取消 |
| 待发货 | 用户已支付,等待商家发货 | 提醒发货、申请退款 |
| 待收货 | 商家已发货,等待用户确认 | 确认收货、申请退货 |
| 已完成 | 交易完成 | 申请售后、评价 |
| 已取消 | 订单已取消 | 重新下单 |
| 退货审核 | 用户申请退货,等待商家审核 | 取消申请 |
| 退货中 | 审核通过,用户退货中 | 填写物流单号 |
| 退款中 | 等待退款到账 | 等待 |
| 已退款 | 退款完成 | 无 |
| 退货争议 | 商家拒收退货 | 联系客服 |
状态转换规则
状态转换必须满足两个条件:
- 源状态合法:只有特定状态才能转换到目标状态
- 事件合法:只有特定事件才能触发状态转换
任何不满足条件的状态转换都应该被拒绝,而不是静默失败。
代码实战
基础用法:订单状态机
先定义状态机,这是订单管理的基础。
// OrderStateMachine.ets — 订单状态机
// 订单状态枚举
enum OrderStatus {
PENDING_PAYMENT = 'PENDING_PAYMENT', // 待支付
PENDING_SHIPMENT = 'PENDING_SHIPMENT', // 待发货
PENDING_RECEIPT = 'PENDING_RECEIPT', // 待收货
COMPLETED = 'COMPLETED', // 已完成
CANCELLED = 'CANCELLED', // 已取消
RETURN_REVIEW = 'RETURN_REVIEW', // 退货审核
RETURNING = 'RETURNING', // 退货中
REFUNDING = 'REFUNDING', // 退款中
REFUNDED = 'REFUNDED', // 已退款
RETURN_DISPUTE = 'RETURN_DISPUTE', // 退货争议
}
// 订单事件枚举
enum OrderEvent {
PAY = 'PAY', // 支付
CANCEL = 'CANCEL', // 取消
SHIP = 'SHIP', // 发货
RECEIVE = 'RECEIVE', // 收货
APPLY_RETURN = 'APPLY_RETURN', // 申请退货
APPROVE_RETURN = 'APPROVE_RETURN', // 审核通过
REJECT_RETURN = 'REJECT_RETURN', // 审核拒绝
SEND_BACK = 'SEND_BACK', // 退货发出
CONFIRM_RECEIPT = 'CONFIRM_RECEIPT', // 商家确认收货
REJECT_RECEIPT = 'REJECT_RECEIPT', // 商家拒收
REFUND_SUCCESS = 'REFUND_SUCCESS', // 退款成功
TIMEOUT = 'TIMEOUT', // 超时
}
// 状态转换定义
interface StateTransition {
from: OrderStatus
event: OrderEvent
to: OrderStatus
}
// 合法的状态转换表
const TRANSITIONS: StateTransition[] = [
// 待支付
{ from: OrderStatus.PENDING_PAYMENT, event: OrderEvent.PAY, to: OrderStatus.PENDING_SHIPMENT },
{ from: OrderStatus.PENDING_PAYMENT, event: OrderEvent.CANCEL, to: OrderStatus.CANCELLED },
{ from: OrderStatus.PENDING_PAYMENT, event: OrderEvent.TIMEOUT, to: OrderStatus.CANCELLED },
// 待发货
{ from: OrderStatus.PENDING_SHIPMENT, event: OrderEvent.SHIP, to: OrderStatus.PENDING_RECEIPT },
{ from: OrderStatus.PENDING_SHIPMENT, event: OrderEvent.APPLY_RETURN, to: OrderStatus.REFUNDING },
// 待收货
{ from: OrderStatus.PENDING_RECEIPT, event: OrderEvent.RECEIVE, to: OrderStatus.COMPLETED },
{ from: OrderStatus.PENDING_RECEIPT, event: OrderEvent.APPLY_RETURN, to: OrderStatus.RETURN_REVIEW },
// 已完成
{ from: OrderStatus.COMPLETED, event: OrderEvent.APPLY_RETURN, to: OrderStatus.RETURN_REVIEW },
// 退货审核
{ from: OrderStatus.RETURN_REVIEW, event: OrderEvent.APPROVE_RETURN, to: OrderStatus.RETURNING },
{ from: OrderStatus.RETURN_REVIEW, event: OrderEvent.REJECT_RETURN, to: OrderStatus.PENDING_RECEIPT },
{ from: OrderStatus.RETURN_REVIEW, event: OrderEvent.CANCEL, to: OrderStatus.COMPLETED },
// 退货中
{ from: OrderStatus.RETURNING, event: OrderEvent.CONFIRM_RECEIPT, to: OrderStatus.REFUNDING },
{ from: OrderStatus.RETURNING, event: OrderEvent.REJECT_RECEIPT, to: OrderStatus.RETURN_DISPUTE },
// 退款中
{ from: OrderStatus.REFUNDING, event: OrderEvent.REFUND_SUCCESS, to: OrderStatus.REFUNDED },
]
// 状态机类
class OrderStateMachine {
// 尝试状态转换
static tryTransition(
currentStatus: OrderStatus,
event: OrderEvent
): OrderStatus | null {
const transition = TRANSITIONS.find(
t => t.from === currentStatus && t.event === event
)
if (!transition) {
console.error(`[OrderFSM] 非法转换: ${currentStatus} + ${event}`)
return null // 转换不合法
}
return transition.to
}
// 获取当前状态可执行的操作
static getAvailableEvents(status: OrderStatus): OrderEvent[] {
return TRANSITIONS
.filter(t => t.from === status)
.map(t => t.event)
}
// 检查状态转换是否合法
static canTransition(
currentStatus: OrderStatus,
event: OrderEvent
): boolean {
return TRANSITIONS.some(
t => t.from === currentStatus && t.event === event
)
}
}
进阶用法:订单列表与详情
状态机搭好了,接下来是订单列表展示和订单详情页。
// OrderListPage.ets — 订单列表页
import { router } from '@kit.ArkUI'
// 订单数据模型
interface OrderInfo {
id: string
orderNo: string // 订单号
status: OrderStatus
totalPrice: number
items: OrderItem[] // 订单商品列表
createTime: string
payTime?: string
shipTime?: string
receiveTime?: string
trackingNo?: string // 物流单号
shopName: string
}
interface OrderItem {
productId: string
skuId: string
title: string
imageUrl: string
specDesc: string
price: number
quantity: number
}
// 订单Tab类型
enum OrderTab {
ALL = '全部',
PENDING_PAYMENT = '待付款',
PENDING_SHIPMENT = '待发货',
PENDING_RECEIPT = '待收货',
COMPLETED = '已完成',
RETURN = '退换货',
}
@Entry
@Component
struct OrderListPage {
@State currentTab: OrderTab = OrderTab.ALL
@State orderList: OrderInfo[] = []
@State tabs: OrderTab[] = [
OrderTab.ALL, OrderTab.PENDING_PAYMENT,
OrderTab.PENDING_SHIPMENT, OrderTab.PENDING_RECEIPT,
OrderTab.COMPLETED, OrderTab.RETURN
]
aboutToAppear() {
this.loadOrders()
}
build() {
Column() {
// 顶部导航
Row() {
Image($r('app.media.ic_back'))
.width(24)
.height(24)
.fillColor('#333333')
.onClick(() => { router.back() })
Text('我的订单')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ left: 12 })
}
.width('100%')
.height(48)
.padding({ left: 16 })
.backgroundColor(Color.White)
// Tab栏
Tabs() {
ForEach(this.tabs, (tab: OrderTab) => {
TabContent() {
this.OrderListContent()
}
.tabBar(tab)
}, (tab: OrderTab) => tab)
}
.barMode(BarMode.Scrollable)
.onChange((index: number) => {
this.currentTab = this.tabs[index]
this.loadOrders()
})
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
// ========== 订单列表内容 ==========
@Builder
OrderListContent() {
if (this.orderList.length === 0) {
Column() {
Text('暂无订单')
.fontSize(14)
.fontColor('#999999')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
} else {
List() {
ForEach(this.orderList, (order: OrderInfo) => {
ListItem() {
this.OrderCard(order)
}
}, (order: OrderInfo) => order.id)
}
.padding({ left: 12, right: 12, top: 8 })
.scrollBar(BarState.Off)
}
}
// ========== 订单卡片 ==========
@Builder
OrderCard(order: OrderInfo) {
Column() {
// 店铺名 + 订单状态
Row() {
Text(order.shopName)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
Blank()
Text(this.getStatusText(order.status))
.fontSize(13)
.fontColor(this.getStatusColor(order.status))
}
.width('100%')
// 商品列表
ForEach(order.items, (item: OrderItem) => {
Row() {
Image(item.imageUrl)
.width(64)
.height(64)
.borderRadius(4)
.objectFit(ImageFit.Cover)
Column() {
Text(item.title)
.fontSize(13)
.fontColor('#333333')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(item.specDesc)
.fontSize(11)
.fontColor('#999999')
.margin({ top: 4 })
Row() {
Text(`¥${item.price}`)
.fontSize(13)
.fontColor('#333333')
Blank()
Text(`x${item.quantity}`)
.fontSize(12)
.fontColor('#999999')
}
.width('100%')
.margin({ top: 4 })
}
.layoutWeight(1)
.margin({ left: 8 })
.alignItems(HorizontalAlign.Start)
}
.margin({ top: 8 })
}, (item: OrderItem, index?: number) => `${item.skuId}_${index}`)
// 总价
Row() {
Blank()
Text('共')
.fontSize(12)
.fontColor('#999999')
Text(`${order.items.reduce((sum, i) => sum + i.quantity, 0)}`)
.fontSize(12)
.fontColor('#333333')
Text('件商品 合计: ')
.fontSize(12)
.fontColor('#999999')
Text(`¥${order.totalPrice.toFixed(2)}`)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FF4444')
}
.width('100%')
.margin({ top: 8 })
// 操作按钮
Row() {
Blank()
this.OrderActions(order)
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor(Color.White)
.borderRadius(8)
.margin({ bottom: 8 })
.onClick(() => {
router.pushUrl({
url: 'pages/OrderDetailPage',
params: { orderId: order.id }
})
})
}
// ========== 订单操作按钮 ==========
@Builder
OrderActions(order: OrderInfo) {
Row() {
// 根据状态显示不同按钮
if (order.status === OrderStatus.PENDING_PAYMENT) {
Button('取消订单')
.fontSize(12)
.fontColor('#666666')
.backgroundColor(Color.White)
.border({ width: 1, color: '#E0E0E0' })
.borderRadius(16)
.height(28)
.margin({ right: 8 })
.onClick(() => {
this.cancelOrder(order.id)
})
Button('去支付')
.fontSize(12)
.fontColor(Color.White)
.backgroundColor('#FF4444')
.borderRadius(16)
.height(28)
.onClick(() => {
router.pushUrl({ url: 'pages/PaymentPage', params: { orderId: order.id } })
})
}
if (order.status === OrderStatus.PENDING_RECEIPT) {
Button('确认收货')
.fontSize(12)
.fontColor(Color.White)
.backgroundColor('#FF4444')
.borderRadius(16)
.height(28)
.onClick(() => {
this.confirmReceive(order.id)
})
}
if (order.status === OrderStatus.COMPLETED) {
Button('申请售后')
.fontSize(12)
.fontColor('#666666')
.backgroundColor(Color.White)
.border({ width: 1, color: '#E0E0E0' })
.borderRadius(16)
.height(28)
.onClick(() => {
router.pushUrl({ url: 'pages/ReturnApplyPage', params: { orderId: order.id } })
})
}
if (order.status === OrderStatus.RETURN_REVIEW) {
Text('退货审核中')
.fontSize(12)
.fontColor('#E65100')
}
}
}
// ========== 辅助方法 ==========
getStatusText(status: OrderStatus): string {
const map: Record<string, string> = {
[OrderStatus.PENDING_PAYMENT]: '待付款',
[OrderStatus.PENDING_SHIPMENT]: '待发货',
[OrderStatus.PENDING_RECEIPT]: '待收货',
[OrderStatus.COMPLETED]: '已完成',
[OrderStatus.CANCELLED]: '已取消',
[OrderStatus.RETURN_REVIEW]: '退货审核中',
[OrderStatus.RETURNING]: '退货中',
[OrderStatus.REFUNDING]: '退款中',
[OrderStatus.REFUNDED]: '已退款',
[OrderStatus.RETURN_DISPUTE]: '退货争议',
}
return map[status] || '未知'
}
getStatusColor(status: OrderStatus): string {
if (status === OrderStatus.COMPLETED) return '#2E7D32'
if (status === OrderStatus.CANCELLED) return '#999999'
if (status === OrderStatus.REFUNDED) return '#6A1B9A'
if ([OrderStatus.RETURN_REVIEW, OrderStatus.RETURNING, OrderStatus.REFUNDING].includes(status)) return '#E65100'
return '#FF4444'
}
async cancelOrder(orderId: string) {
// 调用取消订单接口
console.info(`[OrderList] 取消订单: ${orderId}`)
this.loadOrders()
}
async confirmReceive(orderId: string) {
console.info(`[OrderList] 确认收货: ${orderId}`)
this.loadOrders()
}
loadOrders() {
// 模拟数据
this.orderList = [
{
id: '1', orderNo: '202412250001', status: OrderStatus.PENDING_PAYMENT,
totalPrice: 498, createTime: '2024-12-25 10:30:00',
shopName: '品牌旗舰店',
items: [
{ productId: 'p1', skuId: 'sku1', title: '纯棉短袖T恤', imageUrl: 'https://picsum.photos/200/200?random=1', specDesc: '黑色 XL', price: 299, quantity: 1 },
{ productId: 'p2', skuId: 'sku2', title: '运动休闲裤', imageUrl: 'https://picsum.photos/200/200?random=2', specDesc: '深灰 L', price: 199, quantity: 1 },
]
},
{
id: '2', orderNo: '202412240001', status: OrderStatus.PENDING_RECEIPT,
totalPrice: 159, createTime: '2024-12-24 15:20:00', shipTime: '2024-12-25 08:00:00',
trackingNo: 'SF1234567890', shopName: '数码专营店',
items: [
{ productId: 'p3', skuId: 'sku3', title: '无线蓝牙耳机', imageUrl: 'https://picsum.photos/200/200?random=3', specDesc: '白色', price: 159, quantity: 1 },
]
},
]
}
}
完整示例:订单详情与退换货
把订单详情页、状态流转、退换货申请串成完整链路。
// OrderDetailPage.ets — 订单详情页
import { router } from '@kit.ArkUI'
@Entry
@Component
struct OrderDetailPage {
@State order: OrderInfo | null = null
@State statusSteps: string[] = [] // 状态步骤条
@State currentStep: number = 0
aboutToAppear() {
const params = router.getParams() as Record<string, string>
this.loadOrderDetail(params?.orderId || '')
}
build() {
Column() {
// 顶部导航
Row() {
Image($r('app.media.ic_back'))
.width(24)
.height(24)
.fillColor('#333333')
.onClick(() => { router.back() })
Text('订单详情')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ left: 12 })
}
.width('100%')
.height(48)
.padding({ left: 16 })
.backgroundColor(Color.White)
if (this.order) {
Scroll() {
Column() {
// 订单状态
this.StatusSection()
// 物流信息
if (this.order.trackingNo) {
this.LogisticsSection()
}
// 收货地址
this.AddressSection()
// 商品信息
this.ItemsSection()
// 订单信息
this.OrderInfoSection()
}
}
.layoutWeight(1)
.scrollBar(BarState.Off)
// 底部操作按钮
this.BottomActions()
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
// ========== 订单状态 ==========
@Builder
StatusSection() {
Column() {
// 状态步骤条
Row() {
ForEach(this.statusSteps, (step: string, index?: number) => {
Column() {
Circle()
.width(12)
.height(12)
.fill((index ?? 0) <= this.currentStep ? '#FF4444' : '#E0E0E0')
if ((index ?? 0) < this.statusSteps.length - 1) {
Divider()
.width(40)
.color((index ?? 0) < this.currentStep ? '#FF4444' : '#E0E0E0')
.margin({ top: 4 })
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (step: string, index?: number) => `${step}_${index}`)
}
.width('100%')
.padding({ left: 24, right: 24 })
Row() {
ForEach(this.statusSteps, (step: string) => {
Text(step)
.fontSize(10)
.fontColor('#999999')
.layoutWeight(1)
.textAlign(TextAlign.Center)
}, (step: string, index?: number) => `${step}_${index}`)
}
.width('100%')
.padding({ left: 8, right: 8 })
// 当前状态描述
Text(this.getStatusDescription())
.fontSize(14)
.fontColor('#333333')
.margin({ top: 16 })
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(8)
.margin({ top: 8, left: 12, right: 12 })
}
// ========== 物流信息 ==========
@Builder
LogisticsSection() {
Column() {
Row() {
Image($r('app.media.ic_logistics'))
.width(20)
.height(20)
.fillColor('#333333')
Text('物流信息')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.margin({ left: 8 })
}
Row() {
Text('物流单号:')
.fontSize(13)
.fontColor('#999999')
Text(this.order?.trackingNo || '')
.fontSize(13)
.fontColor('#333333')
.margin({ left: 8 })
}
.margin({ top: 8 })
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(8)
.margin({ top: 8, left: 12, right: 12 })
.alignItems(HorizontalAlign.Start)
}
// ========== 收货地址 ==========
@Builder
AddressSection() {
Column() {
Row() {
Image($r('app.media.ic_location'))
.width(20)
.height(20)
.fillColor('#333333')
Text('收货地址')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.margin({ left: 8 })
}
Text('张三 138****8888')
.fontSize(13)
.fontColor('#333333')
.margin({ top: 8 })
Text('北京市朝阳区xxx街道xxx小区')
.fontSize(12)
.fontColor('#999999')
.margin({ top: 4 })
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(8)
.margin({ top: 8, left: 12, right: 12 })
.alignItems(HorizontalAlign.Start)
}
// ========== 商品信息 ==========
@Builder
ItemsSection() {
Column() {
Text(this.order?.shopName || '')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.width('100%')
ForEach(this.order?.items || [], (item: OrderItem) => {
Row() {
Image(item.imageUrl)
.width(64)
.height(64)
.borderRadius(4)
.objectFit(ImageFit.Cover)
Column() {
Text(item.title)
.fontSize(13)
.fontColor('#333333')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(item.specDesc)
.fontSize(11)
.fontColor('#999999')
.margin({ top: 4 })
Row() {
Text(`¥${item.price}`)
Blank()
Text(`x${item.quantity}`)
.fontSize(12)
.fontColor('#999999')
}
.width('100%')
.margin({ top: 4 })
}
.layoutWeight(1)
.margin({ left: 8 })
.alignItems(HorizontalAlign.Start)
}
.margin({ top: 8 })
}, (item: OrderItem, index?: number) => `${item.skuId}_${index}`)
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(8)
.margin({ top: 8, left: 12, right: 12 })
.alignItems(HorizontalAlign.Start)
}
// ========== 订单信息 ==========
@Builder
OrderInfoSection() {
Column() {
Text('订单信息')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.width('100%')
Row() {
Text('订单编号:')
.fontSize(12)
.fontColor('#999999')
Text(this.order?.orderNo || '')
.fontSize(12)
.fontColor('#333333')
.margin({ left: 8 })
}
.margin({ top: 8 })
Row() {
Text('下单时间:')
.fontSize(12)
.fontColor('#999999')
Text(this.order?.createTime || '')
.fontSize(12)
.fontColor('#333333')
.margin({ left: 8 })
}
.margin({ top: 4 })
Row() {
Text('商品总价:')
.fontSize(12)
.fontColor('#999999')
Text(`¥${this.order?.totalPrice.toFixed(2) || '0.00'}`)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#FF4444')
.margin({ left: 8 })
}
.margin({ top: 4 })
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(8)
.margin({ top: 8, left: 12, right: 12, bottom: 16 })
.alignItems(HorizontalAlign.Start)
}
// ========== 底部操作 ==========
@Builder
BottomActions() {
Row() {
Blank()
if (this.order?.status === OrderStatus.PENDING_PAYMENT) {
Button('取消订单')
.fontSize(13)
.fontColor('#666666')
.backgroundColor(Color.White)
.border({ width: 1, color: '#E0E0E0' })
.borderRadius(20)
.height(36)
.margin({ right: 8 })
Button('去支付')
.fontSize(13)
.fontColor(Color.White)
.backgroundColor('#FF4444')
.borderRadius(20)
.height(36)
}
if (this.order?.status === OrderStatus.PENDING_RECEIPT) {
Button('确认收货')
.fontSize(13)
.fontColor(Color.White)
.backgroundColor('#FF4444')
.borderRadius(20)
.height(36)
}
if (this.order?.status === OrderStatus.COMPLETED) {
Button('申请售后')
.fontSize(13)
.fontColor('#666666')
.backgroundColor(Color.White)
.border({ width: 1, color: '#E0E0E0' })
.borderRadius(20)
.height(36)
.onClick(() => {
router.pushUrl({ url: 'pages/ReturnApplyPage', params: { orderId: this.order?.id } })
})
}
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor(Color.White)
}
// ========== 辅助方法 ==========
getStatusDescription(): string {
if (!this.order) return ''
const map: Record<string, string> = {
[OrderStatus.PENDING_PAYMENT]: '请在30分钟内完成支付,超时订单将自动取消',
[OrderStatus.PENDING_SHIPMENT]: '商家正在为您备货,请耐心等待',
[OrderStatus.PENDING_RECEIPT]: '商品正在配送中,请注意查收',
[OrderStatus.COMPLETED]: '交易已完成,感谢您的购买',
[OrderStatus.CANCELLED]: '订单已取消',
[OrderStatus.RETURN_REVIEW]: '退货申请已提交,等待商家审核',
[OrderStatus.RETURNING]: '请尽快将商品寄回',
[OrderStatus.REFUNDING]: '退款处理中,预计1-3个工作日到账',
[OrderStatus.REFUNDED]: '退款已完成',
}
return map[this.order.status] || ''
}
loadOrderDetail(orderId: string) {
// 模拟数据
this.order = {
id: '1', orderNo: '202412250001', status: OrderStatus.PENDING_RECEIPT,
totalPrice: 498, createTime: '2024-12-25 10:30:00',
payTime: '2024-12-25 10:31:00', shipTime: '2024-12-26 08:00:00',
trackingNo: 'SF1234567890', shopName: '品牌旗舰店',
items: [
{ productId: 'p1', skuId: 'sku1', title: '纯棉短袖T恤', imageUrl: 'https://picsum.photos/200/200?random=1', specDesc: '黑色 XL', price: 299, quantity: 1 },
{ productId: 'p2', skuId: 'sku2', title: '运动休闲裤', imageUrl: 'https://picsum.photos/200/200?random=2', specDesc: '深灰 L', price: 199, quantity: 1 },
]
}
// 设置步骤条
this.statusSteps = ['下单', '付款', '发货', '收货']
this.currentStep = this.getStepIndex(this.order.status)
}
getStepIndex(status: OrderStatus): number {
const map: Record<string, number> = {
[OrderStatus.PENDING_PAYMENT]: 0,
[OrderStatus.PENDING_SHIPMENT]: 1,
[OrderStatus.PENDING_RECEIPT]: 2,
[OrderStatus.COMPLETED]: 3,
}
return map[status] ?? 0
}
}
踩坑与注意事项
坑1:支付超时订单处理
用户创建了订单但没支付,30分钟后订单应该自动取消。你怎么实现?
方案1:服务端定时任务扫描超时订单,每隔1分钟检查一次,超时的自动取消。简单可靠,但有1分钟延迟。
方案2:用延迟消息队列,创建订单时发一条30分钟的延迟消息,到时间后消费消息取消订单。精确但依赖消息队列。
方案3:客户端倒计时,到时间后调用取消接口。不可靠,用户关掉App倒计时就没了。
建议:服务端方案1或方案2,客户端只做展示(倒计时提醒用户),不做业务逻辑。
坑2:并发下单导致库存超卖
用户同时点了两次"立即购买",创建了两个订单,但库存只有1件。
解决方案:下单时加分布式锁,同一个SKU同一时间只能有一个下单请求在处理。或者用数据库乐观锁,UPDATE stock SET count = count - 1 WHERE sku_id = ? AND count > 0,如果影响行数为0说明库存不足。
坑3:状态转换没有记录
订单从"待发货"变成"退款中",但你不知道是谁操作的、什么时候操作的、为什么操作的。
解决方案:每次状态转换都记录一条日志,包含操作人、操作时间、操作原因。
interface OrderStatusLog {
orderId: string
fromStatus: OrderStatus
toStatus: OrderStatus
operator: string // 操作人
operateTime: string // 操作时间
reason: string // 操作原因
}
坑4:退换货流程和订单流程混在一起
退换货是一个独立的流程,不应该和订单状态混在同一个状态机里。订单状态是"待支付→待发货→待收货→已完成",退货是"申请→审核→退货→退款"。混在一起状态爆炸。
解决方案:退换货用独立的售后单,和订单关联但独立管理。订单状态保持简洁,退货状态在售后单上流转。
坑5:订单号生成规则
订单号不能重复、不能被猜测、最好能从订单号看出一些信息(日期、店铺等)。
建议:日期(8位) + 随机数(6位) + 序号(4位),如202412251234560001。或者用雪花算法生成唯一ID。
HarmonyOS 6适配说明
HarmonyOS 6对订单管理相关能力做了以下更新:
-
Tabs组件增强:Tabs新增
customContentTransition属性,支持自定义Tab切换动画。订单列表的Tab切换可以做成卡片翻转效果,提升体验。 -
Steps步骤条组件:新增
Steps组件,专门用于展示流程步骤。订单详情的状态步骤不再需要手动用Circle+Divider拼凑,直接用Steps组件。
Steps() {
Step() { Text('下单') }
Step() { Text('付款') }
Step() { Text('发货') }
Step() { Text('收货') }
}
.current(this.currentStep)
.status(StepsStatus.PROCESS)
-
倒计时组件:新增
CountDown组件,支持倒计时显示和回调。待支付订单的倒计时不用自己写定时器了。 -
后台任务优化:订单超时取消等后台任务,HarmonyOS 6优化了
BackgroundTask的调度策略,减少电量消耗的同时保证任务准时执行。 -
数据持久化增强:关系型数据库RDB新增了事务支持,订单创建和库存扣减可以放在同一个事务里,要么同时成功要么同时回滚,不会出现"订单创建了但库存没扣"的问题。
总结
订单管理的核心是状态机。状态定义清楚、转换规则明确、每次转换有记录——这三点做到了,订单系统就不会出大问题。
核心记住三点:
- 状态机是基础,所有状态转换必须通过状态机校验,不允许直接修改状态
- 状态转换要有日志,谁操作的、什么时候、为什么,每个环节都要可追溯
- 退换货独立管理,不要和订单状态混在一起,否则状态爆炸
| 评估维度 | 说明 |
|---|---|
| 学习难度 | ⭐⭐⭐⭐ 状态机设计需要经验,退换货流程复杂 |
| 使用频率 | ⭐⭐⭐⭐⭐ 所有电商App都有订单管理 |
| 重要程度 | ⭐⭐⭐⭐⭐ 订单状态错了就是钱的问题,没有比这更重要的了 |
订单状态转错了,用户付了钱显示"已取消"——这不是bug,这是事故。
- 点赞
- 收藏
- 关注作者
评论(0)