用户端小程序 收货地址 开发设计文档

  • 模块: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 方法签名(草案):

/** 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,允许会员没有任何默认地址。