HarmonyOS开发:订单管理订单流程

举报
Jack20 发表于 2026/06/26 17:15:08 2026/06/26
【摘要】 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

订单状态定义

状态 说明 可执行操作
待支付 订单已创建,等待用户支付 支付、取消
待发货 用户已支付,等待商家发货 提醒发货、申请退款
待收货 商家已发货,等待用户确认 确认收货、申请退货
已完成 交易完成 申请售后、评价
已取消 订单已取消 重新下单
退货审核 用户申请退货,等待商家审核 取消申请
退货中 审核通过,用户退货中 填写物流单号
退款中 等待退款到账 等待
已退款 退款完成
退货争议 商家拒收退货 联系客服

状态转换规则

状态转换必须满足两个条件:

  1. 源状态合法:只有特定状态才能转换到目标状态
  2. 事件合法:只有特定事件才能触发状态转换

任何不满足条件的状态转换都应该被拒绝,而不是静默失败。

代码实战

基础用法:订单状态机

先定义状态机,这是订单管理的基础。

// 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对订单管理相关能力做了以下更新:

  1. Tabs组件增强:Tabs新增customContentTransition属性,支持自定义Tab切换动画。订单列表的Tab切换可以做成卡片翻转效果,提升体验。

  2. Steps步骤条组件:新增Steps组件,专门用于展示流程步骤。订单详情的状态步骤不再需要手动用Circle+Divider拼凑,直接用Steps组件。

Steps() {
  Step() { Text('下单') }
  Step() { Text('付款') }
  Step() { Text('发货') }
  Step() { Text('收货') }
}
.current(this.currentStep)
.status(StepsStatus.PROCESS)
  1. 倒计时组件:新增CountDown组件,支持倒计时显示和回调。待支付订单的倒计时不用自己写定时器了。

  2. 后台任务优化:订单超时取消等后台任务,HarmonyOS 6优化了BackgroundTask的调度策略,减少电量消耗的同时保证任务准时执行。

  3. 数据持久化增强:关系型数据库RDB新增了事务支持,订单创建和库存扣减可以放在同一个事务里,要么同时成功要么同时回滚,不会出现"订单创建了但库存没扣"的问题。

总结

订单管理的核心是状态机。状态定义清楚、转换规则明确、每次转换有记录——这三点做到了,订单系统就不会出大问题。

核心记住三点:

  • 状态机是基础,所有状态转换必须通过状态机校验,不允许直接修改状态
  • 状态转换要有日志,谁操作的、什么时候、为什么,每个环节都要可追溯
  • 退换货独立管理,不要和订单状态混在一起,否则状态爆炸
评估维度 说明
学习难度 ⭐⭐⭐⭐ 状态机设计需要经验,退换货流程复杂
使用频率 ⭐⭐⭐⭐⭐ 所有电商App都有订单管理
重要程度 ⭐⭐⭐⭐⭐ 订单状态错了就是钱的问题,没有比这更重要的了

订单状态转错了,用户付了钱显示"已取消"——这不是bug,这是事故。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。