# 确认订单页（单商品）— 前端技术方案（C 端 · shop-app）

> **依据：** 《确认订单页（单商品）功能需求.md》v1.0、《确认订单页（单商品）技术方案.md》v1.2  
> **关联：** 《商品详情内页前端技术方案》v1.0、《确认订单页（多商品）技术方案》v1.3（接口同源）  
> **范围：** 消费者 APP **`shop-app`** 立即购买确认页、支付结果交互；**不** 实现购物车合单页、我的订单列表。  
> **实现状态：** 页面与 API 已落地；支付走 **`POST /api/order/{id}/pay`**（默认 **Mock 同步落单**，无需绑 openid）。

---

## 1. 技术栈与约定

| 项 | 说明 |
|----|------|
| 框架 | uni-app **Vue 3** + **uview-plus** |
| 请求 | `@/utils/request`；须会员 Token |
| 鉴权 | 进入页 **`getToken()`**；无 Token → 引导登录 |
| 规格 v1 | **统一规格**；`specText` 由 `formatSpecDisplayText(specDisplay)` 生成 |
| 数据源 | **`source=BUY_NOW`**；preview/submit **items 恒 1 行** |
| 参考 | `pages/cart/index.vue`（底栏合计）、`subpackage/goods/detail.vue`（数量/spec） |

---

## 2. 页面与路由

| 页面 | 路径 | 入口 |
|------|------|------|
| 确认订单（单商品） | `subpackage/order/checkout` | 商品详情 · **立即购买** |
| 支付结果（可选） | `subpackage/order/pay-result` | 支付成功/关闭后跳转 |

**Query / 入参（推荐）：**

| 参数 | 必填 | 说明 |
|------|:----:|------|
| goodsId | ✓ | 商品 ID |
| quantity | ✓ | 购买数量（详情带入，页内可改） |
| specText | 否 | 规格展示文案；缺省后端生成 |

> **不推荐** 仅依赖 query 传价/图：确认页 **以 preview 接口为准**（防篡改）。

**路径常量（建议新增）：**

```javascript
// utils/pageRoute.js
export const PAGE_CHECKOUT = '/subpackage/order/checkout'
export const PAGE_PAY_RESULT = '/subpackage/order/pay-result'
```

---

## 3. 文件清单

| 类型 | 路径 | 说明 |
|------|------|------|
| 确认页 | `shop-app/subpackage/order/checkout.vue` | 地址 + 商品 + 金额 + 备注 + 提交 + Mock 支付弹窗 |
| 支付结果 | `shop-app/subpackage/order/pay-result.vue` | 成功/关闭结果页 |
| 地址条 | `shop-app/components/order/AddressBar.vue` | 展示/点击切换 |
| 商品行 | `shop-app/components/order/CheckoutGoodsRow.vue` | 单商品 + 规格列表 + 改量 |
| API | `shop-app/api/checkout.js` | preview / submit |
| API | `shop-app/api/order.js` | pay / cancel / detail |
| 映射 | `shop-app/utils/checkoutDisplay.js` | preview VO → 页面模型 |
| 常量 | `shop-app/constants/checkout.js` | `SOURCE_BUY_NOW`、`PAY_TYPE=1`、备注上限 |

**已改上游：**

| 文件 | 改动 |
|------|------|
| `subpackage/goods/detail.vue` | `doBuyNow` → `PAGE_CHECKOUT` 带 `goodsId/quantity/specText` |
| `pages.json` | 分包 `pkg-order` |
| `utils/pageRoute.js` | `PAGE_CHECKOUT`、`PAGE_PAY_RESULT` |
| `utils/goodsDetail.js` | `formatSpecDisplayText`（`§` 分隔规格） |
| `utils/cartSpec.js` | `parseCartSpecText` 确认页规格展示复用 |

---

## 4. 接口封装

### 4.1 确认页 `api/checkout.js`

| 方法 | HTTP | 路径 | 说明 |
|------|------|------|------|
| `previewCheckout(data)` | POST | `/api/checkout/preview` | 进入/改量/换地址 |
| `submitCheckout(data)` | POST | `/api/checkout/submit` | 提交订单 |

**preview / submit 公共 Body（BUY_NOW）：**

```javascript
{
  source: 'BUY_NOW',
  addressId: 123,          // preview 可选；submit 必填
  items: [{
    goodsId: 1,
    quantity: 2,
    specText: '批次：2024春季 / 包装：袋装',  // 可选
    buyerRemark: ''        // 仅 submit
  }]
}
```

### 4.2 支付 `api/order.js`

| 方法 | HTTP | 路径 | 说明 |
|------|------|------|------|
| `payOrder(orderId)` | POST | `/api/order/{orderId}/pay` | Mock：`data.mock=true` 已落单；生产：返回 `payParams` |
| `cancelPay(orderId)` | POST | `/api/order/{orderId}/pay/cancel` | 用户取消收银台 |
| `getOrderDetail(orderId)` | GET | `/api/order/{orderId}` | 结果页 |

**Mock 响应示例：** `{ orderId, orderNo, mock: true }` — 前端直接跳支付成功页，**无需** `wx.bind`。

**生产（待联调）：** 须先绑 openid → 调 `payOrder` → `WeixinJSBridge` 调起 → 成功查详情 / 取消调 `cancelPay`。

### 4.3 地址（复用）

| 方法 | HTTP | 路径 |
|------|------|------|
| `getAddressList` | GET | `/api/member/address/list` |

维护地址 → 跳转 **我的服务 · 收货地址**（已有路由）。

---

## 5. 页面结构（checkout.vue）

```text
确认订单 checkout.vue
├── 导航栏「确认订单」
├── AddressBar（当前地址 / 切换 / 新增引导）
├── 店铺头（avatar + shopName）
├── CheckoutGoodsRow
│     ├── 主图、名称、specText、serviceDesc
│     ├── 单价、数量 stepper（1～maxStock）
│     └── 行小计
├── 金额区
│     ├── 商品总价 goodsAmountText
│     ├── 运费 freightAmountText + freightDesc
│     └── 应付 payAmountText
├── 备注 u-textarea（buyerRemark，≤200）
├── 支付方式（微信支付，固定选中）
└── 底栏：应付总额 + 「提交订单」
```

---

## 6. 交互流程

### 6.1 详情 → 确认页

```text
detail · 立即购买
    → ensureCanPurchase（已有 purchaseAction.js）
    → uni.navigateTo({
         url: `${PAGE_CHECKOUT}?goodsId=${id}&quantity=${qty}&specText=${encodeURIComponent(specText)}`
       })
checkout onLoad
    → previewCheckout({ source: 'BUY_NOW', items: [{ goodsId, quantity, specText }] })
    → 渲染 address / shop / item / 金额
```

### 6.2 改数量

```text
用户 +/- quantity
    → 本地更新 quantity（≥1，≤ maxStock）
    → debounce 300ms → previewCheckout（带 addressId）
    → 刷新 goodsAmount / freight / payAmount
```

### 6.3 切换地址

```text
点击 AddressBar
    → 弹层列表 getAddressList()
    → 选择 addressId → previewCheckout
    → 若 freight 变化，刷新应付
无地址
    → 展示「请添加收货地址」→ 跳转我的服务新增
```

### 6.4 提交与支付（取消支付前闭环）

```text
点击「提交订单」
    → 校验 addressId 已选
    → submitCheckout({ source, addressId, payType: 1, items: [{..., buyerRemark}] })
    → 成功得 orderId
    → 唤起支付（v1：直接调 payOrder(orderId) 模拟微信）
        ├── pay 成功
        │     → navigateTo pay-result?status=success&orderId=
        └── 用户取消 / pay 失败 / 调 cancelPay
              → Toast「订单已关闭」
              → 停留或返回详情
```

| 规则 | 前端 |
|------|------|
| COS-S5 防重复 | submit/pay 中 **loading + 按钮 disabled** |
| COS11 取消关闭 | 微信 SDK 取消回调 → **cancelPay** |
| 库存/下架失败 | 展示后端 `msg`；**不** 生成订单 |

---

## 7. 数据映射 `checkoutDisplay.js`

```javascript
export function mapCheckoutPreview(data) {
  const item = (data.items || [])[0] || {}
  return {
    shop: data.shop || {},
    address: data.address || null,
    goods: {
      goodsId: item.goodsId,
      goodsName: item.goodsName,
      mainPic: resolveFileUrl(item.mainPic),
      specText: normalizeSpecText(item.specText),
      serviceDesc: item.serviceDesc || '',
      salePrice: item.salePrice,
      quantity: item.quantity,
      maxStock: item.maxStock,
      subtotal: item.subtotal,
      priceText: formatPrice(item.salePrice),
      subtotalText: formatPrice(item.subtotal)
    },
    goodsAmount: data.goodsAmount,
    freightAmount: data.freightAmount,
    freightDesc: data.freightDesc || '',
    payAmount: data.payAmount,
    goodsAmountText: formatPrice(data.goodsAmount),
    freightAmountText: formatPrice(data.freightAmount),
    payAmountText: formatPrice(data.payAmount)
  }
}
```

`normalizeSpecText` 可复用 `cartDisplay.js` 对 `[object Object]` 的兜底。

---

## 8. 详情页改造要点

**`detail.vue` · `doBuyNow`：**

```javascript
async function doBuyNow() {
  const ok = await ensureCanPurchase(goodsId.value, detail.value?.purchase?.reason)
  if (!ok) return
  const specText = formatSpecDisplayText(detail.value?.specDisplay)
  const q = [
    `goodsId=${goodsId.value}`,
    `quantity=${quantity.value}`,
    specText ? `specText=${encodeURIComponent(specText)}` : ''
  ].filter(Boolean).join('&')
  uni.navigateTo({ url: `${PAGE_CHECKOUT}?${q}` })
}
```

移除占位 Toast「确认订单功能开发中」。

---

## 9. 与购物车确认页分工

| 项 | 单商品（本方案） | 多商品（购物车） |
|----|------------------|------------------|
| 路由 | `subpackage/order/checkout` | 可 **共用同页** 或 `checkout-cart` |
| source | `BUY_NOW` | `CART` + cartItemIds |
| 进页 | 详情立即购买 | prepare + navigate |
| pay 后 | **不** 清购物车 | 删 cart 行 |

**v1 建议：** 单商品 **独立路由**；若 UI 高度一致，可后续合并为 `checkout.vue` + `mode` 参数。

---

## 10. 联调检查清单

| # | 项 |
|---|-----|
| 1 | 未登录点立即购买 → 引导登录 |
| 2 | preview 返回 1 行商品、默认地址 |
| 3 | 无地址时 submit 拦截并引导新增 |
| 4 | 改数量后 freight / payAmount 刷新 |
| 5 | 换地址后模版运费变化 |
| 6 | submit 成功 → pay → 订单待发货 |
| 7 | cancelPay 后订单已关闭，可回详情重购 |
| 8 | 下架商品 submit 失败且未生成订单 |
| 9 | specText 不为 `[object Object]` |

---

## 11. 非本期

| 项 | 说明 |
|----|------|
| 微信 H5 真实调起（WeixinJSBridge） | 后端已就绪；前端待联调 |
| 我的订单列表续付 | 另册 |
| 购物车多商品确认页 | 多商品前端专册 |
| 多规格 SKU 矩阵 | 商品 SKU 落地后扩展 |

---

## 12. 修订记录

| 版本 | 说明 |
|------|------|
| **v1.2** | 对齐后端微信支付 v1.2；Mock pay 说明、生产联调前置 |
| **v1.0** | 首版：BUY_NOW 确认页路由、checkout/order API、详情跳转、支付取消闭环 |
| **v1.1** | 落地 checkout/pay-result 页面与组件；规格展示复用 `parseCartSpecText` |

---

*文档版本：v1.2 · 工程目录 `shop-app` · 后端见《确认订单页（单商品）技术方案.md》v1.2*
