From a4afcfdb179e62a498b62cc6860d8cd46701a08f Mon Sep 17 00:00:00 2001
From: zjk <852185829@qq.com>
Date: 星期三, 14 十月 2026 09:53:47 +0800
Subject: [PATCH] feat(user, miniapp): 完成C端小程序收货地址完整功能闭环
---
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountAddressMapper.java | 7
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/impl/AccountUserServiceImpl.java | 61 +++
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/AccountAddressVo.java | 77 ++++
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/IAccountUserService.java | 38 ++
doc/design/用户端_收货地址_20261014.md | 276 +++++++++++++++++
hugray-user-miniapp/pages/address/list.vue | 102 ++++--
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/controller/AccountProfileController.java | 130 ++++++++
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/dto/AccountAddressDto.java | 88 +++++
hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountAddressMapper.xml | 6
hugray-user-miniapp/pages/address/add.vue | 73 ++++
hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountUserMapper.xml | 10
hugray-user-miniapp/api/index.js | 44 ++
12 files changed, 872 insertions(+), 40 deletions(-)
diff --git "a/doc/design/\347\224\250\346\210\267\347\253\257_\346\224\266\350\264\247\345\234\260\345\235\200_20261014.md" "b/doc/design/\347\224\250\346\210\267\347\253\257_\346\224\266\350\264\247\345\234\260\345\235\200_20261014.md"
new file mode 100644
index 0000000..3948c17
--- /dev/null
+++ "b/doc/design/\347\224\250\346\210\267\347\253\257_\346\224\266\350\264\247\345\234\260\345\235\200_20261014.md"
@@ -0,0 +1,276 @@
+# 用户端小程序 收货地址 开发设计文档
+
+- 模块:ruoyi-user(会员收货地址)+ hugray-user-miniapp(前端)
+- 日期:2026-10-14
+- 需求来源:"完善收货地址的后端功能,编辑地址的时候也要跳转到编辑页面,复用新增页面,注意管理后台的用户管理详情里面也涉及到收货地址展示,但是你只需要实现小程序侧的功能,只需要考虑到管理后台的问题"
+- 关联文档:`doc/design/用户端_个人中心_20261013.md`(入口所在页)
+- 数据权威源:`192.168.110.111:3306/hugray-account`(实际表结构优先于本文档)
+
+---
+
+## 一、功能概述
+
+会员中心「收货地址」当前为**半成品**:列表页用本地 mock 数据、编辑按钮弹「敬请期待」、新增页提交仅本地 toast 后返回,后端只有**管理后台侧**的改/删两个接口。
+
+本期只做**小程序(C 端)**闭环:
+
+1. **后端补齐 C 端收货地址接口**(列表 / 详情 / 新增 / 编辑 / 删除),落既有 `AccountProfileController`,会员身份取自登录态;
+2. **列表页接后端**(`pages/address/list.vue`):去掉 mock,进入/返回时实时拉取;
+3. **编辑跳转编辑页并复用新增页**(`pages/address/add.vue`):`onLoad` 读 `id`,有 id 则回填并切换为「编辑地址 / 保存」,无 id 则为「新增地址 / 提交」;
+4. **管理后台零改动**:`AccountUserController` 的 `PUT/DELETE /address`、`selectAccountUserDetail` 的地址展示**保持原样**,仅保证不被破坏。
+
+---
+
+## 二、现状与关键约束
+
+### 2.1 数据库与 Mapper 已齐备,**本期无 DDL**
+
+`account_address` 表与 `AccountAddress` 实体、`AccountAddressMapper`(接口 + XML)**已全部具备**:
+
+| 列 | 类型 | 说明 |
+| --- | --- | --- |
+| `id` | bigint | 主键(自增,见下方注意事项) |
+| `user_id` | bigint | 会员ID,索引 `idx_address_user_id` |
+| `consignee` | varchar(64) | 收货人 |
+| `consignee_phone` | varchar(20) | 收货人电话 |
+| `address` | varchar(255) | **单字段**存完整地址(无独立省/市/区列) |
+| `is_default` | char(1) | 是否默认(0否 1是) |
+| `del_flag` | char(1) | 逻辑删除(0存在 2删除) |
+
+现成 SQL:`selectAddressByUserId`(`del_flag='0'` 过滤 + 默认在前)、`insertAddress`、`updateAddress`(`where` 带 `user_id`)、`deleteAddressByIds`(批量逻辑删除)、`clearDefaultByUserId`。
+
+> 说明:表主键为 `AUTO_INCREMENT`,是早期建表遗留,**本期不动**(改主键策略影响面大,超出本次范围)。
+
+### 2.2 C 端落点:`AccountProfileController`
+
+- `@RequestMapping("/user")`,外部实际路径 `/miniapp/account/user/**`(网关 `StripPrefix=2`),路由已存在,**无需改网关**。
+- 会员身份取 `SecurityUtils.getUserId()`,**C 端不接受前端传 userId**(天然防水平越权);未登录由网关 401 拦截。
+
+### 2.3 路径必须避开 `/user/address`(关键约束)
+
+`AccountProfileController` 与后台的 `AccountUserController` **都是 `@RequestMapping("/user")`**,且同在一个服务内。后台已有:
+
+- `PUT /user/address`(改)
+- `DELETE /user/address/{ids}`(删)
+
+若 C 端沿用 `/user/address`,Spring 会因 **Ambiguous mapping 启动失败**。故 C 端统一用**复数 `/user/addresses`**:
+
+| C 端接口(服务内) | 后台既有(服务内) | 是否冲突 |
+| --- | --- | --- |
+| `GET /user/addresses` | 无 | 否 |
+| `GET /user/addresses/{id}` | `GET /user/detail/{userId}`(literal 不同) | 否 |
+| `POST /user/addresses` | `POST /user`、`POST /user/export`(段数/字面量不同) | 否 |
+| `PUT /user/addresses` | `PUT /user/address`(literal 不同) | 否 |
+| `DELETE /user/addresses/{id}` | `DELETE /user/address/{ids}`(literal 不同) | 否 |
+
+### 2.4 单字段 `address` 的格式约定(关键)
+
+`address` 是 varchar(255) 单字段,**没有独立的省/市/区列**。本期不改表,采用**空格分隔约定**:
+
+```
+address = [省, 市, 区, 详细地址].join(' ') // 例:四川省 成都市 武侯区 府城大道399号天府新谷9号楼
+```
+
+- **写入**:前端把 `region`(picker 固定返回三级)与 `detail` 用**单个空格**拼接。
+- **回填**:`address.split(' ')`,前 3 段为省/市/区,其余段用空格 rejoin 还原为详细地址。
+- **兜底**:段数 < 4(如后台手工录入的非约定格式)时,地区留空、整串填入详细地址,由用户重新选择地区(不报错)。
+
+### 2.5 「每会员至多一条默认地址」
+
+MySQL 无部分唯一索引,约束由**应用层在同一事务内**保证:置默认前先 `clearDefaultByUserId`,再写目标行。既有 `updateAccountAddress` 已实现,C 端编辑**直接复用**。新增同样需要此逻辑。
+
+### 2.6 归属校验(防越权)
+
+- **编辑**:`updateAddress` 的 `where` 自带 `user_id = #{userId}`,只要 Service 用登录态 userId,改别人的 id 会更新 0 行 → 既有逻辑抛异常回滚,**天然安全**。
+- **删除**:既有 `deleteAddressByIds` 仅按 `id` 逻辑删除、**不带 `user_id`**,C 端**不可直接复用**(会误删他人地址)。需先按 `selectAddressById` 查归属再删。
+- **详情**:同样需按归属校验。
+
+### 2.7 管理后台兼容(不得破坏)
+
+后台 `hugray-admin/src/views/member/user/detail.vue` 的「收货地址」Tab 依赖 `detail.addresses` 的字段:`id / userId / consignee / consigneePhone / address / isDefault`,并调用后台 `PUT /user/address`、`DELETE /user/address/{ids}`。
+
+**本期不修改 `AccountUserController`,也不修改 `selectAddressByUserId` 的查询口径与字段**,后台完全不受影响。
+
+### 2.8 手机号展示
+
+收货地址是**本人数据**,且编辑回填必须拿到明文手机号,故 C 端列表/详情**下发明文**(不再脱敏),列表页也**展示明文**(与后台详情页一致)。这是相对 mock「`185****0110`」的有意调整。
+
+### 2.9 电话校验口径
+
+后台地址弹窗的注释明确「收货电话可能是固话,写死 11 位手机号正则会挡住合法数据」。故 **后端不强制手机号正则**,只校验非空 + 长度(≤20);前端保留现有 `/^1[3-9]\d{9}$/`(原型设计),后端更宽松,不产生矛盾。
+
+---
+
+## 三、接口设计
+
+统一响应体:C 端 `R<T>`(成功 `code=200`,数据在 `data`)。
+
+### 3.1 查询本人收货地址列表(本次新增 · ruoyi-user)
+
+- **外部路径**:`GET /miniapp/account/user/addresses`(→ 服务内 `GET /user/addresses`)
+- **鉴权**:需登录;会员身份取自登录态。
+- **入参**:无(地址数量少,**不分页**)。
+- **出参**:
+
+| 字段 | 类型 | 说明 |
+| --- | --- | --- |
+| id | Long | 地址ID |
+| consignee | String | 收货人 |
+| consigneePhone | String | 收货人电话(明文) |
+| address | String | 完整地址(空格分隔:省 市 区 详细) |
+| isDefault | String | 是否默认(`0`否 / `1`是) |
+
+- **排序**:默认地址在前,其余按 `id` 升序(复用现有 SQL)。
+
+### 3.2 查询单条收货地址详情(本次新增 · ruoyi-user)
+
+- **外部路径**:`GET /miniapp/account/user/addresses/{id}`(→ 服务内 `GET /user/addresses/{id}`)
+- **用途**:编辑页回填(列表页不再传递长文本参数)。
+- **鉴权**:需登录;**校验归属**,非本人地址抛业务异常「收货地址不存在或不属于该会员」。
+- **出参**:同 3.1 单条对象。
+
+### 3.3 新增收货地址(本次新增 · ruoyi-user)
+
+- **外部路径**:`POST /miniapp/account/user/addresses`(→ 服务内 `POST /user/addresses`)
+- **鉴权**:需登录;`userId` 由登录态注入(前端**不传** userId)。
+- **入参** `AccountAddressDto`:
+
+| 字段 | 类型 | 必填 | 校验 | 说明 |
+| --- | --- | --- | --- | --- |
+| consignee | String | 是 | `@NotBlank` + `@Size(max=64)` | 收货人 |
+| consigneePhone | String | 是 | `@NotBlank` + `@Size(max=20)` | 收货人电话(不强制手机号) |
+| address | String | 是 | `@NotBlank` + `@Size(max=255)` | 完整地址 |
+| isDefault | String | 否 | 值域 `0`/`1`,默认 `0` | 是否默认 |
+
+- **业务规则**(`@Transactional`):
+ 1. **是否默认完全以用户勾选为准**:`isDefault` 为空按 `0` 处理,后端**不做**「首条自动默认」的强制改写(该会员可能存在零条默认地址);
+ 2. 若为默认,先 `clearDefaultByUserId` 再插入(同事务,保证唯一默认)。
+- **出参**:`R<Void>`。
+
+### 3.4 编辑收货地址(本次新增 · ruoyi-user)
+
+- **外部路径**:`PUT /miniapp/account/user/addresses`(→ 服务内 `PUT /user/addresses`)
+- **鉴权**:需登录;`userId` 由登录态注入。
+- **入参** `AccountAddressDto`(多一个必填 `id`):
+
+| 字段 | 类型 | 必填 | 校验 | 说明 |
+| --- | --- | --- | --- | --- |
+| id | Long | 是 | `@NotNull` | 地址ID |
+| consignee | String | 是 | `@NotBlank` + `@Size(max=64)` | 收货人 |
+| consigneePhone | String | 是 | `@NotBlank` + `@Size(max=20)` | 收货人电话 |
+| address | String | 是 | `@NotBlank` + `@Size(max=255)` | 完整地址 |
+| isDefault | String | 否 | 值域 `0`/`1` | 是否默认 |
+
+- **业务规则**:复用既有 `updateAccountAddress`(事务内先清旧默认再更新;`where` 带 `user_id`,非本人更新 0 行抛异常回滚)。
+- **出参**:`R<Void>`。
+
+### 3.5 删除收货地址(本次新增 · ruoyi-user)
+
+- **外部路径**:`DELETE /miniapp/account/user/addresses/{id}`(→ 服务内 `DELETE /user/addresses/{id}`)
+- **鉴权**:需登录;**校验归属**后逻辑删除(`del_flag='2'`)。
+- **出参**:`R<Void>`。
+
+---
+
+## 四、数据库设计
+
+**本期无 DDL 变更**,不改表结构、不改后台查询口径。仅在 Mapper 增加一个查询方法:
+
+- `AccountAddress selectAddressById(Long id)`:`select ... from account_address where id = #{id} and del_flag = '0'`(显式列名,禁止 `SELECT *`)。
+- 服务层 `getAddressDetail` 与 `deleteMyAddress` 复用它做**归属校验**(DRY)。
+- 删除沿用既有 `deleteAddressByIds`(批量逻辑删除)。
+
+---
+
+## 五、后端改动清单(ruoyi-user)
+
+| 文件 | 改动 |
+| --- | --- |
+| `domain/dto/AccountAddressDto.java` | **新增**:C 端收货地址入参(id/consignee/consigneePhone/address/isDefault + 校验注解) |
+| `domain/vo/AccountAddressVo.java` | **新增**:C 端收货地址出参(id/consignee/consigneePhone/address/isDefault) |
+| `mapper/AccountAddressMapper.java` | **新增** `selectAddressById(Long id)` |
+| `resources/mapper/user/AccountAddressMapper.xml` | **新增** `selectAddressById` 语句 |
+| `service/IAccountUserService.java` | **新增** 4 个方法:`getAddressList` / `getAddressDetail` / `insertMyAddress` / `deleteMyAddress`(编辑复用既有 `updateAccountAddress`) |
+| `service/impl/AccountUserServiceImpl.java` | **实现** 上述 4 个方法(`insertMyAddress` 带 `@Transactional`;新增/删除做归属与默认约束处理) |
+| `controller/AccountProfileController.java` | **新增** 5 个 C 端接口(3.1~3.5),身份取 `SecurityUtils.getUserId()` |
+| `controller/AccountUserController.java` | **不改动**(后台兼容) |
+
+Service 方法签名(草案):
+
+```java
+/** C端:查询本人收货地址列表(默认在前,不分页) */
+List<AccountAddress> getAddressList(Long userId);
+
+/** C端:查询单条地址(校验归属,非本人抛异常)——供编辑页回填 */
+AccountAddress getAddressDetail(Long userId, Long id);
+
+/** C端:新增地址(事务内处理「首条自动默认」与默认唯一约束) */
+int insertMyAddress(AccountAddress accountAddress);
+
+/** C端:删除地址(校验归属后逻辑删除) */
+int deleteMyAddress(Long userId, Long id);
+```
+
+---
+
+## 六、前端页面设计
+
+### 6.1 列表页 `pages/address/list.vue`
+
+- **去 mock**:删除 `MOCK_ADDRESS_LIST`,改为 `onShow` 调用 `getAddressList()` 拉取(用 `onShow` 保证新增/编辑返回后自动刷新)。
+- **字段映射**:`item.consignee → 收货人`、`item.consigneePhone → 电话(明文)`、`item.address → 详细地址`、`item.isDefault === '1' → 「默认」标签`。
+- **编辑入口**:`handleEdit(item)` → `uni.navigateTo({ url: '/pages/address/add?id=' + item.id })`。
+- **删除入口(已确认)**:**列表页每条记录增加删除图标**。点击后 `uni.showModal` 二次确认,确认调 `deleteAddress(item.id)`,成功后 toast + 重新拉取列表;删除当前默认地址后按后端返回的最新列表状态渲染。
+- **空态 / 底部新增按钮**:样式不变。
+- **导航标题**:由 `/pages/address/list` 的「选择地址」保持不变(不在本次范围内)。
+
+### 6.2 新增/编辑页 `pages/address/add.vue`(复用双模式)
+
+- **`onLoad(options)`**:读取 `options.id`;有 id → 编辑模式(`isEdit = true`),调 `getAddressDetail(id)` 回填表单,`uni.setNavigationBarTitle({ title: '编辑地址' })`;无 id → 新增模式(标题「新增地址」)。
+- **回填映射**:
+ - `form.name = vo.consignee`,`form.phone = vo.consigneePhone`,`form.isDefault = vo.isDefault === '1'`;
+ - `form.address` 按 **2.4** 拆分:`tokens = address.split(' ')`;`tokens.length >= 4` 时 `form.region = tokens.slice(0,3)`、`form.detail = tokens.slice(3).join(' ')`;否则 `form.region = []`、`form.detail = address`。
+- **提交**:
+ - `address = [...form.region, form.detail.trim()].join(' ')`;
+ - 新增调 `createAddress({ consignee, consigneePhone, address, isDefault })`;
+ - 编辑调 `updateAddress({ id, consignee, consigneePhone, address, isDefault })`;
+ - 成功 toast 后 `navigateBack()`(列表页 `onShow` 自动刷新)。
+- **文案**:新增模式按钮「提交」、导航「新增地址」;编辑模式按钮「保存」、导航「编辑地址」。
+- **校验**:保留现有 `_validateForm()`(姓名/手机号/地区/详细地址 + 手机号正则)。
+
+---
+
+## 七、前端改动清单
+
+| 文件 | 改动 |
+| --- | --- |
+| `pages/address/list.vue` | 去 mock 接 `getAddressList()`;改为 `onShow` 刷新;`handleEdit` 跳转带 `id`;**每条增加删除图标 + 二次确认删除** |
+| `pages/address/add.vue` | 补 `onLoad(options)` 双模式;新增/编辑提交分别调接口;文案切换;地址拼接与拆分 |
+| `api/index.js` | import 补 `put`、`del`;**新增** `getAddressList` / `getAddressDetail(id)` / `createAddress(data)` / `updateAddress(data)` / `deleteAddress(id)` |
+| `pages.json` | **不改动**(复用 `pages/address/add`) |
+| `pages/profile/index.vue` | **不改动**(入口已指向 `/pages/address/list`) |
+
+---
+
+## 八、影响范围与测试要点
+
+- **影响面**:新增 C 端接口与前端联调,**不改表结构**、**不改后台侧接口与页面**;列表页由 mock 切真实数据。
+- **测试要点**:
+ 1. 列表:默认地址置顶、明文手机号、空态、返回后刷新;
+ 2. 新增:未勾选默认时 `isDefault` 落库为 `0`(不再强制首条为默认);勾选默认时原默认被顶掉(同会员仅一条默认);
+ 3. 编辑:回填正确(省市区与详细地址正确拆分)、保存后列表即时更新;
+ 4. 删除(列表页删除图标 + 二次确认):仅能删自己的地址;删默认地址后列表状态正确;
+ 5. 越权:构造他人 `id` 调编辑/详情/删除 → 统一抛「收货地址不存在或不属于该会员」,不产生写入;
+ 6. 校验:空值 / 超长(`consignee>64`、`address>255`、`phone>20`)被 `@Validated` 拦下;
+ 7. **后台回归**:会员详情「收货地址」Tab 展示与改/删仍正常。
+
+---
+
+## 九、已确认决策
+
+1. **列表页删除入口**:**已确认采用「列表页每条加删除图标」**(点击弹 `uni.showModal` 二次确认后删除)。后端 C 端删除接口(3.5)按此实现。
+2. **地址格式约定**:**已确认沿用单字段空格分隔**(2.4 约定),本期**不改表**,不加 `province/city/district/detail` 列。
+3. **管理后台**:**已确认零改动**(`AccountUserController` 与后台页面均不改,仅保证不被破坏)。
+4. **列表分页**:本期按不分页实现(地址数量少)。
+5. **电话校验**:后端仅校验非空 + 长度(允许固话),前端保留手机号正则。
+6. **默认地址取值(2026-10-14 修订)**:取消原「首条地址自动设为默认」,**完全以用户勾选为准**;未勾选时 `isDefault` 落库为 `0`,允许会员没有任何默认地址。
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/controller/AccountProfileController.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/controller/AccountProfileController.java
index 72258a6..9257e08 100644
--- a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/controller/AccountProfileController.java
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/controller/AccountProfileController.java
@@ -1,20 +1,28 @@
package com.ruoyi.user.controller;
+import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.web.controller.BaseController;
import com.ruoyi.common.core.web.page.TableDataInfo;
import com.ruoyi.common.security.utils.SecurityUtils;
+import com.ruoyi.user.domain.AccountAddress;
+import com.ruoyi.user.domain.dto.AccountAddressDto;
import com.ruoyi.user.domain.dto.AccountCertificationApplyDto;
import com.ruoyi.user.domain.dto.AccountUserAvatarDto;
import com.ruoyi.user.domain.dto.CompanyCertificationApplyDto;
+import com.ruoyi.user.domain.vo.AccountAddressVo;
import com.ruoyi.user.domain.vo.AccountCertificationStatusVo;
import com.ruoyi.user.domain.vo.AccountInviteeVo;
import com.ruoyi.user.domain.vo.AccountReferrerVo;
@@ -181,4 +189,126 @@
{
return R.ok(accountUserService.getReferrer(SecurityUtils.getUserId()));
}
+
+ /**
+ * 查询本人收货地址列表
+ *
+ * 默认地址排前,其余按 id 升序;地址数量少,不分页。
+ * 会员身份取自登录态,不接受前端传入 userId。
+ *
+ * @return 地址列表(无地址返回空集合)
+ */
+ @GetMapping("/addresses")
+ public R<List<AccountAddressVo>> addressList()
+ {
+ List<AccountAddress> list = accountUserService.getAddressList(SecurityUtils.getUserId());
+ return R.ok(toAddressVoList(list));
+ }
+
+ /**
+ * 查询本人单条收货地址详情
+ *
+ * 供编辑页回填;校验归属,非本人地址抛业务异常。
+ * 会员身份取自登录态,不接受前端传入 userId。
+ *
+ * @param id 地址ID
+ * @return 地址详情
+ */
+ @GetMapping("/addresses/{id}")
+ public R<AccountAddressVo> addressDetail(@PathVariable("id") Long id)
+ {
+ AccountAddress address = accountUserService.getAddressDetail(SecurityUtils.getUserId(), id);
+ return R.ok(toAddressVo(address));
+ }
+
+ /**
+ * 新增本人收货地址
+ *
+ * 首条地址自动设为默认;会员身份取自登录态,不接受前端传入 userId。
+ *
+ * @param dto 地址入参
+ * @return 操作结果
+ */
+ @PostMapping("/addresses")
+ public R<Void> createAddress(@Validated @RequestBody AccountAddressDto dto)
+ {
+ accountUserService.insertMyAddress(buildAddress(dto, SecurityUtils.getUserId()));
+ return R.ok();
+ }
+
+ /**
+ * 编辑本人收货地址
+ *
+ * 复用后台改地址事务逻辑(含默认唯一约束);会员身份取自登录态。
+ *
+ * @param dto 地址入参(必含 id)
+ * @return 操作结果
+ */
+ @PutMapping("/addresses")
+ public R<Void> updateAddress(@Validated @RequestBody AccountAddressDto dto)
+ {
+ if (dto.getId() == null)
+ {
+ throw new ServiceException("地址ID不能为空");
+ }
+ accountUserService.updateAccountAddress(buildAddress(dto, SecurityUtils.getUserId()));
+ return R.ok();
+ }
+
+ /**
+ * 删除本人收货地址
+ *
+ * 校验归属后逻辑删除;会员身份取自登录态,不接受前端传入 userId。
+ *
+ * @param id 地址ID
+ * @return 操作结果
+ */
+ @DeleteMapping("/addresses/{id}")
+ public R<Void> deleteAddress(@PathVariable("id") Long id)
+ {
+ accountUserService.deleteMyAddress(SecurityUtils.getUserId(), id);
+ return R.ok();
+ }
+
+ /**
+ * DTO 转实体(userId 由登录态注入,不接受前端传值)
+ */
+ private AccountAddress buildAddress(AccountAddressDto dto, Long userId)
+ {
+ AccountAddress address = new AccountAddress();
+ address.setId(dto.getId());
+ address.setUserId(userId);
+ address.setConsignee(dto.getConsignee());
+ address.setConsigneePhone(dto.getConsigneePhone());
+ address.setAddress(dto.getAddress());
+ address.setIsDefault(dto.getIsDefault());
+ return address;
+ }
+
+ /**
+ * 实体转出参 VO(列表)
+ */
+ private List<AccountAddressVo> toAddressVoList(List<AccountAddress> list)
+ {
+ List<AccountAddressVo> voList = new ArrayList<>(list.size());
+ for (AccountAddress address : list)
+ {
+ voList.add(toAddressVo(address));
+ }
+ return voList;
+ }
+
+ /**
+ * 实体转出参 VO(单条)
+ */
+ private AccountAddressVo toAddressVo(AccountAddress address)
+ {
+ AccountAddressVo vo = new AccountAddressVo();
+ vo.setId(address.getId());
+ vo.setConsignee(address.getConsignee());
+ vo.setConsigneePhone(address.getConsigneePhone());
+ vo.setAddress(address.getAddress());
+ vo.setIsDefault(address.getIsDefault());
+ return vo;
+ }
}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/dto/AccountAddressDto.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/dto/AccountAddressDto.java
new file mode 100644
index 0000000..a34d774
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/dto/AccountAddressDto.java
@@ -0,0 +1,88 @@
+package com.ruoyi.user.domain.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+
+/**
+ * C端会员 收货地址 新增/编辑入参
+ *
+ * 会员身份取自登录态(网关注入的 user_id 头),不接受前端传入 userId,天然防水平越权。
+ * 编辑场景需携带 id(由 Controller 人工校验非空);新增场景 id 为空。
+ *
+ * address 为单字段完整地址,约定格式:省 市 区 详细地址(空格分隔),详见设计文档。
+ *
+ * @author hugray
+ */
+public class AccountAddressDto
+{
+ /** 地址ID(新增时为空;编辑时必填,由 Controller 校验) */
+ private Long id;
+
+ /** 收货人;必填,长度不超过 64(对应 account_address.consignee varchar(64)) */
+ @NotBlank(message = "收货人不能为空")
+ @Size(max = 64, message = "收货人姓名过长")
+ private String consignee;
+
+ /** 收货人电话;必填,长度不超过 20;不强制手机号(允许固话) */
+ @NotBlank(message = "收货人电话不能为空")
+ @Size(max = 20, message = "收货人电话过长")
+ private String consigneePhone;
+
+ /** 完整地址(约定格式:省 市 区 详细地址);必填,长度不超过 255 */
+ @NotBlank(message = "收货地址不能为空")
+ @Size(max = 255, message = "收货地址过长")
+ private String address;
+
+ /** 是否默认(0否 1是);非必填,为空时按 0 处理 */
+ private String isDefault;
+
+ public Long getId()
+ {
+ return id;
+ }
+
+ public void setId(Long id)
+ {
+ this.id = id;
+ }
+
+ public String getConsignee()
+ {
+ return consignee;
+ }
+
+ public void setConsignee(String consignee)
+ {
+ this.consignee = consignee;
+ }
+
+ public String getConsigneePhone()
+ {
+ return consigneePhone;
+ }
+
+ public void setConsigneePhone(String consigneePhone)
+ {
+ this.consigneePhone = consigneePhone;
+ }
+
+ public String getAddress()
+ {
+ return address;
+ }
+
+ public void setAddress(String address)
+ {
+ this.address = address;
+ }
+
+ public String getIsDefault()
+ {
+ return isDefault;
+ }
+
+ public void setIsDefault(String isDefault)
+ {
+ this.isDefault = isDefault;
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/AccountAddressVo.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/AccountAddressVo.java
new file mode 100644
index 0000000..af2c62f
--- /dev/null
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/domain/vo/AccountAddressVo.java
@@ -0,0 +1,77 @@
+package com.ruoyi.user.domain.vo;
+
+/**
+ * C端会员 收货地址 VO(列表 / 详情 / 编辑回填用)
+ *
+ * 仅暴露页面所需字段;收货地址为本人数据,电话下发明文(编辑回填需要)。
+ * 会员身份由服务端从登录态解析,不接受前端传入。
+ *
+ * @author hugray
+ */
+public class AccountAddressVo
+{
+ /** 地址ID */
+ private Long id;
+
+ /** 收货人 */
+ private String consignee;
+
+ /** 收货人电话(明文,本人数据) */
+ private String consigneePhone;
+
+ /** 完整地址(空格分隔:省 市 区 详细地址) */
+ private String address;
+
+ /** 是否默认(0否 1是) */
+ private String isDefault;
+
+ public Long getId()
+ {
+ return id;
+ }
+
+ public void setId(Long id)
+ {
+ this.id = id;
+ }
+
+ public String getConsignee()
+ {
+ return consignee;
+ }
+
+ public void setConsignee(String consignee)
+ {
+ this.consignee = consignee;
+ }
+
+ public String getConsigneePhone()
+ {
+ return consigneePhone;
+ }
+
+ public void setConsigneePhone(String consigneePhone)
+ {
+ this.consigneePhone = consigneePhone;
+ }
+
+ public String getAddress()
+ {
+ return address;
+ }
+
+ public void setAddress(String address)
+ {
+ this.address = address;
+ }
+
+ public String getIsDefault()
+ {
+ return isDefault;
+ }
+
+ public void setIsDefault(String isDefault)
+ {
+ this.isDefault = isDefault;
+ }
+}
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountAddressMapper.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountAddressMapper.java
index 6f8c35f..8bd9b9e 100644
--- a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountAddressMapper.java
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/mapper/AccountAddressMapper.java
@@ -16,6 +16,13 @@
public List<AccountAddress> selectAddressByUserId(Long userId);
/**
+ * 根据主键查询单条收货地址(仅查未删除)
+ *
+ * C端详情与删除前的归属校验复用;查不到返回 null。
+ */
+ public AccountAddress selectAddressById(Long id);
+
+ /**
* 新增收货地址
*/
public int insertAddress(AccountAddress accountAddress);
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/IAccountUserService.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/IAccountUserService.java
index aacd34d..3289e39 100644
--- a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/IAccountUserService.java
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/IAccountUserService.java
@@ -100,6 +100,44 @@
public int deleteAccountAddressByIds(Long[] ids);
/**
+ * C端:查询本人收货地址列表(默认地址在前,不分页)
+ *
+ * @param userId 会员ID(取自登录态,非前端传入)
+ * @return 地址列表(无地址返回空集合)
+ */
+ public List<AccountAddress> getAddressList(Long userId);
+
+ /**
+ * C端:查询单条收货地址(编辑页回填用)
+ *
+ * 校验归属:地址不存在或不属于该会员时抛业务异常。
+ *
+ * @param userId 会员ID(取自登录态)
+ * @param id 地址ID
+ * @return 地址对象
+ */
+ public AccountAddress getAddressDetail(Long userId, Long id);
+
+ /**
+ * C端:新增本人收货地址
+ *
+ * 事务内处理「首条地址自动默认」与「每会员至多一条默认」约束。
+ *
+ * @param accountAddress 地址入参(userId 由服务端注入)
+ * @return 影响行数
+ */
+ public int insertMyAddress(AccountAddress accountAddress);
+
+ /**
+ * C端:删除本人收货地址(校验归属后逻辑删除)
+ *
+ * @param userId 会员ID(取自登录态)
+ * @param id 地址ID
+ * @return 影响行数
+ */
+ public int deleteMyAddress(Long userId, Long id);
+
+ /**
* C端会员注册
*
* 校验手机号未注册、推荐码有效后,生成唯一推荐码并落库(无密码)。
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/impl/AccountUserServiceImpl.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/impl/AccountUserServiceImpl.java
index f939d2e..a26c589 100644
--- a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/impl/AccountUserServiceImpl.java
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/impl/AccountUserServiceImpl.java
@@ -297,6 +297,67 @@
return accountAddressMapper.deleteAddressByIds(ids);
}
+ @Override
+ public List<AccountAddress> getAddressList(Long userId)
+ {
+ return accountAddressMapper.selectAddressByUserId(userId);
+ }
+
+ @Override
+ public AccountAddress getAddressDetail(Long userId, Long id)
+ {
+ // 编辑页回填:需校验归属,防止拿到他人地址
+ return queryOwnedAddress(userId, id);
+ }
+
+ @Override
+ @Transactional
+ public int insertMyAddress(AccountAddress accountAddress)
+ {
+ // STEP 1: 会员身份防御性校验(正常由登录态注入,必非空)
+ if (accountAddress.getUserId() == null)
+ {
+ throw new ServiceException("会员身份缺失");
+ }
+ // STEP 2: 是否默认完全以用户勾选为准,不做「首条自动默认」的强制改写;
+ // 为空时明确兜底为 0,避免落库为 null 影响前端判定
+ if (!"1".equals(accountAddress.getIsDefault()))
+ {
+ accountAddress.setIsDefault("0");
+ }
+ // STEP 3: 置为默认前先清掉旧默认,保证"每会员至多一条默认"(与插入同事务)
+ if ("1".equals(accountAddress.getIsDefault()))
+ {
+ accountAddressMapper.clearDefaultByUserId(accountAddress.getUserId());
+ }
+ // STEP 4: 落库
+ return accountAddressMapper.insertAddress(accountAddress);
+ }
+
+ @Override
+ @Transactional
+ public int deleteMyAddress(Long userId, Long id)
+ {
+ // 校验归属:非本人地址直接抛异常,避免越权删除(deleteAddressByIds 本身不带 user_id)
+ queryOwnedAddress(userId, id);
+ return accountAddressMapper.deleteAddressByIds(new Long[]{ id });
+ }
+
+ /**
+ * 查询地址并校验归属
+ *
+ * 地址不存在或不属于该会员时统一抛业务异常(避免暴露他人地址是否存在)。
+ */
+ private AccountAddress queryOwnedAddress(Long userId, Long id)
+ {
+ AccountAddress address = accountAddressMapper.selectAddressById(id);
+ if (address == null || !Objects.equals(address.getUserId(), userId))
+ {
+ throw new ServiceException("收货地址不存在或不属于该会员");
+ }
+ return address;
+ }
+
private Date resolveBindTime(Long oldId, Long newId, Date oldTime, Date now)
{
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountAddressMapper.xml b/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountAddressMapper.xml
index 72be901..2b956e8 100644
--- a/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountAddressMapper.xml
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountAddressMapper.xml
@@ -32,6 +32,12 @@
order by is_default desc, id asc
</select>
+ <!-- 按主键查单条(C端详情/删除归属校验用);显式列名,避免 SELECT * -->
+ <select id="selectAddressById" parameterType="Long" resultMap="AccountAddressResult">
+ <include refid="selectAddressVo"/>
+ where id = #{id} and del_flag = '0'
+ </select>
+
<insert id="insertAddress" parameterType="AccountAddress" useGeneratedKeys="true" keyProperty="id">
insert into account_address(
<if test="userId != null">user_id,</if>
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountUserMapper.xml b/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountUserMapper.xml
index be38ef6..9f322dd 100644
--- a/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountUserMapper.xml
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/resources/mapper/user/AccountUserMapper.xml
@@ -110,6 +110,16 @@
</select>
<!--
+ 按推荐码查询会员:注册时定位推荐人 + 生成推荐码时库内查重。
+ 命中唯一索引 uk_account_user_referral_code;del_flag = '0' 保证推荐人未被逻辑删除,
+ 与 selectAccountUserByPhone/ByUsername 口径一致。
+ -->
+ <select id="selectAccountUserByReferralCode" parameterType="String" resultMap="AccountUserResult">
+ <include refid="selectAccountUserVo"/>
+ where del_flag = '0' and my_referral_code = #{referralCode}
+ </select>
+
+ <!--
详情查询:会员本体 + 推荐人/供货人的手机号、等级、昵称。
两次自连接都走 user_id 主键,成本极低。
条件里的 del_flag = '0' 不可省:推荐人可能已被逻辑删除(造数脚本的 user_id=125
diff --git a/hugray-user-miniapp/api/index.js b/hugray-user-miniapp/api/index.js
index 372c062..3f4851e 100644
--- a/hugray-user-miniapp/api/index.js
+++ b/hugray-user-miniapp/api/index.js
@@ -7,7 +7,7 @@
* /miniapp/product/** -> ruoyi-product 商品/订单
* /miniapp/system/** -> ruoyi-system 系统信息
*/
-import { get, post, uploadFile } from '@/utils/request.js'
+import { get, post, put, del, uploadFile } from '@/utils/request.js'
// ======================== 认证相关 ========================
@@ -199,6 +199,48 @@
return get('/miniapp/account/user/referrer')
}
+// ======================== 收货地址 ========================
+
+/**
+ * 查询本人收货地址列表(默认地址在前,不分页)
+ * 返回 data 为数组,元素含 id / consignee / consigneePhone / address / isDefault
+ */
+export function getAddressList() {
+ return get('/miniapp/account/user/addresses')
+}
+
+/**
+ * 查询单条收货地址详情(编辑页回填用)
+ * @param {String|Number} id 地址ID
+ */
+export function getAddressDetail(id) {
+ return get('/miniapp/account/user/addresses/' + id)
+}
+
+/**
+ * 新增收货地址(会员身份由网关注入,前端不传 userId)
+ * @param {Object} data { consignee, consigneePhone, address, isDefault }
+ */
+export function createAddress(data) {
+ return post('/miniapp/account/user/addresses', data)
+}
+
+/**
+ * 编辑收货地址(会员身份由网关注入)
+ * @param {Object} data { id, consignee, consigneePhone, address, isDefault }
+ */
+export function updateAddress(data) {
+ return put('/miniapp/account/user/addresses', data)
+}
+
+/**
+ * 删除收货地址(后端校验归属)
+ * @param {String|Number} id 地址ID
+ */
+export function deleteAddress(id) {
+ return del('/miniapp/account/user/addresses/' + id)
+}
+
// ======================== 订单相关 ========================
/**
diff --git a/hugray-user-miniapp/pages/address/add.vue b/hugray-user-miniapp/pages/address/add.vue
index 1c2a00b..bf9eab0 100644
--- a/hugray-user-miniapp/pages/address/add.vue
+++ b/hugray-user-miniapp/pages/address/add.vue
@@ -72,17 +72,19 @@
<!-- 底部固定提交栏:尺寸/圆角与地址列表页「新增地址」按钮一致 -->
<view class="submit-bar">
- <view class="submit-btn" @tap="_handleSubmit">提交</view>
+ <view class="submit-btn" @tap="_handleSubmit">{{ isEdit ? '保存' : '提交' }}</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { getAddressDetail, createAddress, updateAddress } from '@/api/index.js'
// ======================== 数据定义区域 ========================
-// 新增地址表单
+// 地址表单(新增/编辑共用)
const form = reactive({
name: '',
phone: '',
@@ -97,7 +99,48 @@
// 地区文本:拼接为「省 市 区」,为空时由模板展示占位文案
const regionText = computed(() => form.region.join(' '))
+// 是否编辑模式(onLoad 带 id 时为编辑,复用新增页)
+const isEdit = ref(false)
+
+// 编辑模式下的地址ID
+const addressId = ref(null)
+
// ======================== 方法定义区域 ========================
+
+/** 拉取地址详情并回填表单(编辑模式) */
+const _loadAddressDetail = async (id) => {
+ try {
+ const res = await getAddressDetail(id)
+ const data = res.data || {}
+ form.name = data.consignee || ''
+ form.phone = data.consigneePhone || ''
+ form.isDefault = data.isDefault === '1'
+ // 拆分单字段 address:约定前 3 段为省/市/区,其余段 rejoin 还原详细地址;
+ // 段数 < 4(历史/后台手工录入的非约定格式)时地区留空、整串填入详细地址
+ const tokens = (data.address || '').split(' ')
+ if (tokens.length >= 4) {
+ form.region = tokens.slice(0, 3)
+ form.detail = tokens.slice(3).join(' ')
+ } else {
+ form.region = []
+ form.detail = data.address || ''
+ }
+ regionValue.value = [...form.region]
+ } catch (e) {
+ console.error('[ADDRESS_DETAIL] 加载地址详情失败', e)
+ uni.showToast({ title: e.message || '加载失败', icon: 'none' })
+ }
+}
+
+// 页面加载:带 id → 编辑模式(回填表单 + 切换标题);不带 id → 新增模式
+onLoad((options) => {
+ const id = options && options.id
+ if (!id) return
+ isEdit.value = true
+ addressId.value = id
+ uni.setNavigationBarTitle({ title: '编辑地址' })
+ _loadAddressDetail(id)
+})
/** 切换「设为默认」 */
const _handleDefaultChange = (e) => {
@@ -121,16 +164,32 @@
return ''
}
-/** 提交:校验通过后保存并返回地址列表页(地址新增接口未就绪,暂以本地成功反馈占位) */
-const _handleSubmit = () => {
+/** 提交:校验通过后按模式调用新增/编辑接口,成功返回列表页(列表页 onShow 自动刷新) */
+const _handleSubmit = async () => {
const errorMsg = _validateForm()
if (errorMsg) {
uni.showToast({ title: errorMsg, icon: 'none' })
return
}
-
- uni.showToast({ title: '保存成功', icon: 'none' })
- setTimeout(() => uni.navigateBack(), 600)
+ // 单字段 address 约定:省 市 区 详细地址(空格分隔)
+ const payload = {
+ consignee: form.name.trim(),
+ consigneePhone: form.phone.trim(),
+ address: [...form.region, form.detail.trim()].join(' '),
+ isDefault: form.isDefault ? '1' : '0'
+ }
+ try {
+ if (isEdit.value) {
+ await updateAddress({ ...payload, id: addressId.value })
+ } else {
+ await createAddress(payload)
+ }
+ uni.showToast({ title: '保存成功', icon: 'none' })
+ setTimeout(() => uni.navigateBack(), 600)
+ } catch (e) {
+ console.error('[ADDRESS_SAVE] 保存收货地址失败', e)
+ uni.showToast({ title: e.message || '保存失败', icon: 'none' })
+ }
}
</script>
diff --git a/hugray-user-miniapp/pages/address/list.vue b/hugray-user-miniapp/pages/address/list.vue
index d8332a1..4718dd1 100644
--- a/hugray-user-miniapp/pages/address/list.vue
+++ b/hugray-user-miniapp/pages/address/list.vue
@@ -1,19 +1,23 @@
<template>
<view class="page">
- <!-- 有数据:渲染地址列表,右侧为编辑入口 -->
+ <!-- 有数据:渲染地址列表,右侧为编辑/删除入口 -->
<view v-if="addressList.length">
<view v-for="item in addressList" :key="item.id" class="addr-item">
<view class="addr-main">
<!-- 姓名 + 手机号 -->
<view class="addr-head">
- <text class="addr-name">{{ item.name }}</text>
- <text class="addr-phone">{{ item.phone }}</text>
+ <text class="addr-name">{{ item.consignee }}</text>
+ <text class="addr-phone">{{ item.consigneePhone }}</text>
</view>
<!-- 默认地址标记 -->
- <view v-if="item.isDefault" class="addr-tag">默认</view>
- <text class="addr-detail">{{ item.detail }}</text>
+ <view v-if="item.isDefault === '1'" class="addr-tag">默认</view>
+ <text class="addr-detail">{{ item.address }}</text>
</view>
- <view class="icon icon-edit" @tap="handleEdit(item)"></view>
+ <!-- 操作区:编辑 / 删除 -->
+ <view class="addr-actions">
+ <view class="icon icon-edit" @tap="handleEdit(item)"></view>
+ <view class="icon icon-delete" @tap="handleDelete(item)"></view>
+ </view>
</view>
</view>
@@ -38,35 +42,52 @@
<script setup>
import { ref } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
+import { getAddressList, deleteAddress } from '@/api/index.js'
+import { ensureLogin } from '@/utils/request.js'
-/** 收货地址列表:地址接口未就绪,先本地 mock(联调后由收货地址列表接口下发);手机号按规范脱敏展示 */
-const MOCK_ADDRESS_LIST = [
- {
- id: '1',
- name: '张三',
- phone: '185****0110',
- detail: '四川省成都市武侯区府城大道399号天府新谷9号楼2单元1005a',
- isDefault: true
- },
- {
- id: '2',
- name: '李四',
- phone: '185****0110',
- detail: '四川省成都市武侯区府城大道399号天府新谷9号楼2单元1005a'
- },
- {
- id: '3',
- name: '王五',
- phone: '185****0110',
- detail: '四川省成都市武侯区府城大道399号天府新谷9号楼2单元1005a'
+/** 收货地址列表(来自后端 /miniapp/account/user/addresses) */
+const addressList = ref([])
+
+/** 拉取本人收货地址列表(默认地址在前) */
+const _loadAddressList = async () => {
+ try {
+ const res = await getAddressList()
+ addressList.value = res.data || []
+ } catch (e) {
+ console.error('[ADDRESS_LIST] 加载收货地址失败', e)
+ uni.showToast({ title: e.message || '加载失败', icon: 'none' })
}
-]
+}
-const addressList = ref(MOCK_ADDRESS_LIST)
+/** onShow:从新增/编辑页返回时自动刷新列表 */
+onShow(() => {
+ if (!ensureLogin()) return
+ _loadAddressList()
+})
-/** 编辑地址:编辑页未实现,暂为占位反馈 */
-const handleEdit = () => {
- uni.showToast({ title: '敬请期待', icon: 'none' })
+/** 编辑地址:跳转新增页并携带 id,复用为编辑模式 */
+const handleEdit = (item) => {
+ uni.navigateTo({ url: '/pages/address/add?id=' + item.id })
+}
+
+/** 删除地址:二次确认后删除并刷新列表 */
+const handleDelete = (item) => {
+ uni.showModal({
+ title: '提示',
+ content: '确定要删除该收货地址吗?',
+ success: async (res) => {
+ if (!res.confirm) return
+ try {
+ await deleteAddress(item.id)
+ uni.showToast({ title: '删除成功', icon: 'none' })
+ _loadAddressList()
+ } catch (e) {
+ console.error('[ADDRESS_DELETE] 删除收货地址失败', e)
+ uni.showToast({ title: e.message || '删除失败', icon: 'none' })
+ }
+ }
+ })
}
/** 新增地址:跳转到新增地址页 */
@@ -96,6 +117,15 @@
.addr-main {
flex: 1;
min-width: 0;
+}
+
+/* 操作区:编辑 + 删除图标并排 */
+.addr-actions {
+ display: flex;
+ align-items: center;
+ gap: 28rpx;
+ flex-shrink: 0;
+ margin-left: 24rpx;
}
.addr-head {
@@ -212,6 +242,7 @@
/* ==================== 图标(内联 base64 SVG) ==================== */
.icon-edit,
+.icon-delete,
.icon-plus,
.icon-empty {
background-repeat: no-repeat;
@@ -224,10 +255,17 @@
width: 44rpx;
height: 44rpx;
flex-shrink: 0;
- margin-left: 24rpx;
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMUU1RUJDIiBzdHJva2Utd2lkdGg9IjEuOCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMTIgM0g1YTIgMiAwIDAgMC0yIDJ2MTRhMiAyIDAgMCAwIDIgMmgxNGEyIDIgMCAwIDAgMi0ydi03Ii8+PHBhdGggZD0iTTE4LjM3NSAyLjYyNWExIDEgMCAwIDEgMyAzbC05LjAxMyA5LjAxNGEyIDIgMCAwIDEtLjg1My41MDVsLTIuODczLjg0YS41LjUgMCAwIDEtLjYyLS42MmwuODQtMi44NzNhMiAyIDAgMCAxIC41MDYtLjg1MnoiLz48L3N2Zz4=');
}
+/* 删除:垃圾桶(#EF4444) */
+.icon-delete {
+ width: 44rpx;
+ height: 44rpx;
+ flex-shrink: 0;
+ background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRUY0NDQ0IiBzdHJva2Utd2lkdGg9IjEuOCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMyA2aDE4Ii8+PHBhdGggZD0iTTggNlY0YTEgMSAwIDAgMSAxLTFoNmExIDEgMCAwIDEgMSAxdjIiLz48cGF0aCBkPSJNMTkgNmwtMSAxNGEyIDIgMCAwIDEtMiAySDhhMiAyIDAgMCAxLTItMkw1IDYiLz48cGF0aCBkPSJNMTAgMTF2NiIvPjxwYXRoIGQ9Ik0xNCAxMXY2Ii8+PC9zdmc+');
+}
+
/* 新增:白色加号 */
.icon-plus {
width: 34rpx;
--
Gitblit v1.7.1