Browse Source

docs: 新增 checkout 地址管理改造设计文档和实施计划

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Xiaogang Liao 3 months ago
parent
commit
c4d3e054a5

+ 262 - 0
docs/superpowers/plans/2026-07-29-checkout-address-migration.md

@@ -0,0 +1,262 @@
+# Checkout 地址迁移实施计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 将 checkout 页面从 `consignees` 系统切换到 `user_address` 系统,身份证等购买信息字段保持独立。
+
+**Architecture:** checkout.vue 调用 `/api/user/address/list` 替代 `/api/consignee/list`,地址选择跳转到 `address/address.vue`,新增/编辑走 `address-edit/address-edit.vue`。后端无改动。
+
+**Tech Stack:** uni-app Vue 2, WeChat mini-program, Spring Boot 2.7.18
+
+## Global Constraints
+
+- 小程序禁止可选链 `?.`(用 `&&` 替代)
+- 小程序禁止 CSS Grid(用 flexbox)
+- Vue 2 Options API,禁止 Composition API
+- 所有接口统一 `@PostMapping`,前端 `method: 'POST'`
+- 响应格式:`uni.request` + `Authorization: Bearer {token}`
+
+---
+
+### Task 1: 添加地址 API 函数到 api.js
+
+**Files:**
+- Modify: `cfc-frontend/utils/api.js`
+
+**Interfaces:**
+- Consumes: 无
+- Produces: `addressList()`, `addressDetail(id)`, `addressSave(data)`, `addressDelete(id)`, `addressSetDefault(id)`
+
+- [ ] **Step 1: 在 api.js 末尾添加地址 API 函数**
+
+找到 `consigneeDelete` 函数附近,在 `getProductRequiredFields` 之前添加以下函数:
+
+```javascript
+// ===== 地址管理 =====
+export const addressList = () => request('/api/user/address/list', 'POST', {})
+export const addressDetail = (id) => request('/api/user/address/get', 'POST', { id })
+export const addressSave = (data) => {
+  var url = data.id ? '/api/user/address/update' : '/api/user/address/create'
+  return request(url, 'POST', data)
+}
+export const addressDelete = (id) => request('/api/user/address/delete', 'POST', { id })
+export const addressSetDefault = (id) => request('/api/user/address/setDefault', 'POST', { id })
+```
+
+- [ ] **Step 2: 验证文件修改**
+
+```bash
+grep -n 'addressList\|addressDetail\|addressSave\|addressDelete\|addressSetDefault' cfc-frontend/utils/api.js
+```
+
+---
+
+### Task 2: 修改 checkout.vue — 切换为地址系统
+
+**Files:**
+- Modify: `cfc-frontend/pages/shop/checkout/checkout.vue`
+
+**Interfaces:**
+- Consumes: `addressList()` from api.js
+- Produces: 提交订单时传 `addressId` 代替 `consigneeId`
+
+- [ ] **Step 1: 修改 import,替换 consigneeList 为 addressList**
+
+```javascript
+// 替换这一行:
+import { getCouponList, consigneeList, getProductRequiredFields, getMyMembership } from '../../../utils/api.js'
+// 改为:
+import { getCouponList, addressList, getProductRequiredFields, getMyMembership } from '../../../utils/api.js'
+```
+
+- [ ] **Step 2: 修改 data(),替换 consignee 相关字段为 address 字段**
+
+```javascript
+// 在 data() 中,替换:
+selectedConsignee: null,
+consigneeList: [],
+// 改为:
+selectedAddress: null,
+addressList: [],
+
+// 同时移除:
+showConsigneePicker: false,
+```
+
+- [ ] **Step 3: 修改模板 — 地址卡片区域**
+
+找到地址卡片区域(约第 8-25 行),替换为:
+
+```html
+<!-- Address Section -->
+<view class="section address-section" @click="openAddressPicker">
+  <view v-if="selectedAddress" class="address-content">
+    <view class="address-top">
+      <text class="receiver-name">{{ selectedAddress.receiverName }}</text>
+      <text class="receiver-phone">{{ formatPhone(selectedAddress.phone) }}</text>
+      <text v-if="selectedAddress.isDefault === 1" class="default-badge">默认</text>
+    </view>
+    <text class="address-detail">
+      {{ selectedAddress.province }}{{ selectedAddress.city }}{{ selectedAddress.district }}{{ selectedAddress.street }}
+    </text>
+    <text class="address-arrow">›</text>
+  </view>
+  <view v-else class="address-empty" @click.stop="goAddAddress">
+    <text class="address-empty-icon">📍</text>
+    <text class="address-empty-text">请选择收货地址</text>
+    <text class="address-arrow">›</text>
+  </view>
+</view>
+```
+
+- [ ] **Step 4: 替换 `loadDefaultConsignee` 方法为 `loadDefaultAddress`**
+
+```javascript
+loadDefaultAddress() {
+  this.loading = true
+  var that = this
+  addressList().then(function(res) {
+    that.loading = false
+    if (res.code === 200) {
+      var list = res.data || []
+      that.addressList = list
+      for (var i = 0; i < list.length; i++) {
+        if (list[i].isDefault === 1) {
+          that.selectedAddress = list[i]
+          break
+        }
+      }
+      if (!that.selectedAddress && list.length > 0) {
+        that.selectedAddress = list[0]
+      }
+    }
+  }).catch(function(e) {
+    that.loading = false
+    console.error(e)
+  })
+}
+```
+
+- [ ] **Step 5: 替换 `openConsigneePicker` 方法为 `openAddressPicker`**
+
+```javascript
+openAddressPicker() {
+  uni.navigateTo({ url: '/pages/shop/address/address?selectMode=1' })
+}
+```
+
+- [ ] **Step 6: 替换 `goAddConsignee` 方法为 `goAddAddress`**
+
+```javascript
+goAddAddress() {
+  uni.navigateTo({ url: '/pages/shop/address-edit/address-edit' })
+}
+```
+
+- [ ] **Step 7: 替换 `autoFillPurchaseForm` 中的 `selectedConsignee` 引用**
+
+```javascript
+autoFillPurchaseForm() {
+  if (!this.selectedAddress || this.requiredFields.length === 0) return
+  // ... 其余逻辑不变,只是 selectedConsignee → selectedAddress
+}
+```
+
+- [ ] **Step 8: 修改 `onLoad` 中的调用**
+
+```javascript
+// 替换:
+this.loadDefaultConsignee()
+// 改为:
+this.loadDefaultAddress()
+```
+
+- [ ] **Step 9: 添加 `onShow` 生命周期 — 返回时重新加载地址列表**
+
+```javascript
+onShow() {
+  if (this.addressList.length > 0) {
+    // 从 address-edit 或 address 页面返回后重新加载
+    this.loadDefaultAddress()
+  }
+}
+```
+
+- [ ] **Step 10: 修改 `onSubmit` — 验证和提交参数**
+
+```javascript
+// 验证部分:
+if (!this.selectedAddress) {
+  uni.showToast({ title: '请选择收货地址', icon: 'none' })
+  return
+}
+
+// 提交参数部分:
+data: {
+  items: orderItems,
+  remark: this.remark,
+  addressId: that.selectedAddress.id,
+  purchaseInfo: that.purchaseForm,
+  pointsUsed: that.pointsUsed
+}
+```
+
+- [ ] **Step 11: 移除不再使用的 `consigneeList` 相关代码**
+
+检查并移除:
+- `showConsigneePicker` 相关状态
+- 任何 `consigneeList` 引用
+- 任何 `consignee` 相关的 picker 弹窗
+
+---
+
+### Task 3: 完善 address/address.vue 选择模式回传
+
+**Files:**
+- Modify: `cfc-frontend/pages/shop/address/address.vue`
+
+**Interfaces:**
+- Consumes: 无
+- Produces: 通过 `prevPage.selectedAddress` 回传地址数据
+
+- [ ] **Step 1: 确认 `onSelect` 方法回传完整数据**
+
+当前 `onSelect` 方法已存在(约第 105-113 行),数据来源于 `addressList` 加载的 `UserAddress` 对象,已有 `id`, `receiverName`, `phone`, `province`, `city`, `district`, `street`, `isDefault` 字段。确认 `onSelect` 直接回传 `item` 对象即可:
+
+```javascript
+onSelect(item) {
+  if (this.selectMode) {
+    var pages = getCurrentPages()
+    var prevPage = pages[pages.length - 2]
+    if (prevPage) {
+      prevPage.selectedAddress = item
+    }
+    uni.navigateBack()
+  }
+}
+```
+
+无需修改。
+
+---
+
+### Task 4: 验证构建
+
+**Files:**
+- 无
+
+- [ ] **Step 1: 运行构建验证**
+
+```bash
+cd cfc-frontend && npm run build:mp-weixin
+```
+
+预期:构建成功,无报错。
+
+- [ ] **Step 2: 检查 checkout.vue 中所有 `selectedConsignee` 和 `consigneeList` 引用是否已清理**
+
+```bash
+grep -n 'selectedConsignee\|consigneeList\|consignee' cfc-frontend/pages/shop/checkout/checkout.vue | head -20
+```
+
+预期:仅保留 `consignee` 相关的行不超过 2 行(如注释或无关引用)。

+ 125 - 0
docs/superpowers/specs/2026-07-29-checkout-address-management-design.md

@@ -0,0 +1,125 @@
+# Checkout 地址管理改造设计
+
+## 背景
+
+小程序商城购买流程中,checkout 页面当前使用了 `consignees`(收货人)系统来管理地址信息,但存在以下问题:
+
+1. **收货人系统无省市区字段**:`consignees` 表只有 name/phone/idCard/ethnicity/bloodType/address(纯文本),没有 province/city/district/street 结构化地址
+2. **身份证等字段混入地址管理**:idCard/ethnicity/bloodType 属于商品维度的购买信息,不应与地址绑定
+3. **地址编辑后不保存**:收货人编辑页面没有省市区选择器,输入的地址信息无法正确存储
+
+## 现状分析
+
+### 两个并存的系统
+
+| 系统 | 表 | 有省市区 | 有默认地址 | 当前 checkout 使用 |
+|------|-----|---------|-----------|------------------|
+| 地址系统 | `user_address` | ✅ province/city/district/street | ✅ isDefault | ❌ |
+| 收货人系统 | `consignees` | ❌(只有 text address) | ✅ isDefault | ✅ |
+
+### 已有可用资源
+
+- **前端**:`address-edit.vue` 已有完整的省市区三级联动选择器 + 地址表单
+- **前端**:`address.vue` 已有地址列表 + 选择模式 + 编辑/删除/设为默认
+- **后端**:`UserAddressController` 提供完整 CRUD + setDefault
+- **后端**:`ProductPurchaseFieldService` 已实现按商品配置购买信息字段
+
+## 设计
+
+### 核心变更
+
+**checkout 页面从 `consignees` 系统切换到 `user_address` 系统**,身份证等购买信息字段保持独立。
+
+### 数据流
+
+```
+商品详情 → checkout
+              │
+              ├─ 地址区域 → 调用 /api/user/address/list
+              │              ├─ 有地址 → 跳转 address/address.vue?selectMode=1
+              │              │              ├─ 选择地址 → 回传 → checkout
+              │              │              ├─ 编辑 → address-edit?addressId=X
+              │              │              └─ 新增 → address-edit
+              │              └─ 无地址 → 直接跳转 address-edit(新增)
+              │
+              ├─ 购买信息区域 → 保持 getProductRequiredFields 逻辑
+              │                   身份证/民族/血型等按商品配置显示
+              │
+              └─ 提交订单 → 传 addressId(代替 consigneeId)
+```
+
+### 文件改动清单
+
+#### 1. `checkout.vue`(核心改动)
+
+| 改动项 | 说明 |
+|--------|------|
+| `loadDefaultConsignee()` → `loadDefaultAddress()` | 调用 `/api/user/address/list` 加载地址列表,选中默认地址 |
+| 地址卡片显示 | 改为显示 receiverName、phone、province+city+district+street、isDefault 标签 |
+| 移除购买信息字段 | 地址卡片中不再显示 idCard、ethnicity、bloodType |
+| `openConsigneePicker()` → `openAddressPicker()` | 跳转到 `address/address.vue?selectMode=1` |
+| `goAddConsignee()` → `goAddAddress()` | 跳转到 `address-edit/address-edit` |
+| 提交订单 | `consigneeId` → 改为 `addressId`,取选中地址的 id |
+| `onShow` 生命周期 | 监听返回时重新加载地址列表,自动选中刚保存的地址 |
+| 移除 `consigneeList` API 引用 | 改为调用 `uni.request` 直接访问 `/api/user/address/list` |
+
+#### 2. `address/address.vue`(完善选择模式)
+
+| 改动项 | 说明 |
+|--------|------|
+| 选择模式回传 | 确保 `onSelect` 回传完整数据:`{ id, receiverName, phone, province, city, district, street, isDefault }` |
+| 回传机制 | 当前用 `prevPage.selectedAddress = item` 方式,确认 checkout 能正确读取 |
+
+#### 3. `address-edit/address-edit.vue`
+
+无需改动。已有省市区联动选择器、详细地址、默认地址开关、保存逻辑。
+
+#### 4. 后端
+
+无需改动。`UserAddressController` 已有完整 CRUD + setDefault。
+
+### 购买信息字段的独立机制
+
+身份证号、民族、血型、手签名等字段:
+
+- 由 `ProductPurchaseFieldService` 管理,按商品维度配置
+- 管理端通过 `product_purchase_fields` 表配置每个商品需要哪些字段
+- 接口 `/api/product/purchase_fields/required` 返回该商品需要的字段列表
+- 预填来源:默认收货人 → 用户信息
+- 不在地址卡片中显示,仅在"购买信息"区域按需显示
+
+### 交互流程
+
+```
+checkout 页面加载(onLoad/onShow)
+  ├─ 调用 /api/user/address/list → 获取地址列表
+  ├─ 有地址 → 选中默认地址(isDefault=1)或第一个地址
+  └─ 无地址 → 显示"请选择地址"空状态
+
+点击地址区域
+  ├─ 有地址列表 → navigateTo address/address.vue?selectMode=1
+  │                ├─ 点击地址卡片 → 回传选中地址数据 → navigateBack
+  │                ├─ 点击编辑 → navigateTo address-edit?addressId=X
+  │                └─ 点击新增 → navigateTo address-edit
+  │
+  └─ 无地址 → navigateTo address-edit(新增)
+               └─ 保存成功 → navigateBack
+
+checkout onShow 行为
+  ├─ 每次页面显示时重新调用 /api/user/address/list 加载地址列表
+  ├─ 如果当前未选中地址,自动选中默认地址(isDefault=1)或第一个
+  └─ 如果已选中地址但列表有变化,保持选中状态不变
+
+提交订单
+  ├─ 验证:地址必选、购买信息字段必填校验
+  ├─ 传参:{ addressId, items, remark, purchaseInfo, pointsUsed }
+  └─ 成功 → 跳转支付
+```
+
+## 未改动部分
+
+- `ProductPurchaseFieldService` 和 `ProductPurchaseField` 实体
+- `ConsigneeController` / `ConsigneeService` / `Consignee` 实体(保留,但不再用于地址选择)
+- 后端 `UserAddressController` / `UserAddressService`
+- 数据库表结构
+- `address-picker.vue` 组件(备用,有四级地址选择)