# 购物车 — 技术方案（C 端）

> **依据：** 《购物车功能需求.md》v1.0  
> **关联：** 平台《商品管理功能需求》v1.3.3、《商品审核功能需求》v1.0、《商品分类技术方案》v1.2、《店铺管理技术方案》v1.3.6、《会员管理技术方案》v1.3、《订单管理技术方案》v1.0.1、《关联需求分析.md》v1.6 §11；C 端《商品详情内页技术方案》v1.0、《我的服务技术方案》v1.1  
> **范围：** C 端 **`/api/cart/**`** 购物车行 CRUD、加购写入、勾选态、失效判定、同店结算预校验；**不含** 确认订单、支付、履约。  
> **原则：** **新建** `biz_member_cart_item`；商品/店铺/分类 **只读** 实时档案；**不预扣库存**；可购 **四条件** 复用 **`IGoodsPurchaseFacade`**；v1.0 **统一规格**（`spec_key` 固定默认键）。

---

## 1. 技术架构

| 项 | 选型 |
|----|------|
| 基础框架 | RuoYi **v3.9.2**（`springboot2` 分支） |
| 数据库 | **MySQL 5.7.39** |
| ORM / 响应 | MyBatis；`AjaxResult`（`code`、`msg`、`data`） |
| 鉴权 | 全部接口须 **会员 Token**（`MemberWebConfig` → `/api/cart/**`） |
| 分页 | 购物车 **不分页**（单会员行数可控）；列表一次返回 |
| 缓存 | **本期不做** Redis |

### 1.1 模块落位

```text
baqing-shop/src/main/java/com/ruoyi/web/modules/cart/
├── controller/CartAppController.java           # /api/cart/**
├── service/ICartAppService.java
├── service/impl/CartAppServiceImpl.java
├── mapper/CartAppMapper.java
├── domain/BizMemberCartItem.java
├── dto/
│   ├── CartItemAddDTO.java                     # 加购
│   ├── CartItemQuantityDTO.java
│   ├── CartItemCheckedBatchDTO.java
│   ├── CartItemBatchDeleteDTO.java
│   └── CartCheckoutPrepareDTO.java             # 去结算
├── vo/
│   ├── CartListVO.java                         # 按店分组列表
│   ├── CartShopGroupVO.java
│   ├── CartItemVO.java
│   └── CartCheckoutPrepareVO.java
└── constant/CartConstants.java

resources/mapper/cart/CartAppMapper.xml
sql/biz_member_cart_item.sql                    # 新建

account/（已有 · 协作）
├── config/MemberWebConfig.java                 # 追加 /api/cart/**
├── support/MemberContext.java
└── facade/IMemberFacade.isMemberEnabled

goods/（已有 · 协作）
├── facade/IGoodsPurchaseFacade.canPurchase
├── mapper/BizGoodsMapper
└── constant/GoodsConstants

store/（已有 · 协作）
└── facade/IAgriShopFacade.isShopOpenForOrder

category/（已有 · 协作）
└── facade/ICategoryFacade.isCategoryVisible
```

### 1.2 协作链

```text
【上游写入】
    商品详情 POST /api/cart/items
        → 四条件校验 + 同规格合并累加

【本模块】
    GET  /api/cart                         → 条目列表（cartItemId 倒序，含店铺）+ 勾选合计
    PUT  /api/cart/items/{id}/quantity     → 改数量（≤库存）
    PUT  /api/cart/items/checked           → 批量更新勾选
    DELETE /api/cart/items/{id}            → 移出
    DELETE /api/cart/items                 → 批量移出
    DELETE /api/cart/invalid               → 清理失效行
    POST /api/cart/checkout/prepare        → 同店结算预校验 → 确认订单（待建）

【下游】
    确认订单（待建）← checkout/prepare 返回 shopId + 行快照
    下单成功（待建）→ ICartFacade.removeByOrderItems（占位）

biz_member_cart_item
    ├── JOIN biz_goods     → 名称/主图/价/库/状态
    ├── JOIN biz_shop      → 店名/店态
    └── Facade             → 四条件、分类可见
```

| 关联模块 | 协作 |
|----------|------|
| **平台 · 商品管理/审核** | 维护 `biz_goods`；下架 **不删** 购物车行；列表 **实时** 读态 |
| **平台 · 店铺管理** | 维护 `biz_shop`；停业 **可展示**、**禁结算** |
| **平台 · 商品分类** | 分类隐藏 → 行 **可展示**、**禁结算** |
| **平台 · 会员管理** | 禁用会员 **不可** 调本模块 |
| **平台 · 订单管理** | 支付成功扣库存；下单后 **移除/扣减** 购物车行（确认订单专册） |
| **C 端 · 商品详情** | **唯一** 加购入口 `POST /api/cart/items` |
| **C 端 · 确认订单** | **待建设**；本模块 **仅 prepare** |
| **C 端 · 我的服务** | 地址 **不在** 购物车；结算页引用地址簿 |

### 1.3 跨模块 Facade

| 接口 | 本模块用法 |
|------|------------|
| **`IGoodsPurchaseFacade.canPurchase(goodsId)`** | 加购、去结算 **四条件**（出售中/店开业/分类可见/库存>0） |
| **`BizGoodsMapper.selectById`** | 读实时 `sale_price`、`stock`；改量/累加上限 |
| **`IMemberFacade.isMemberEnabled`** | 写操作前校验会员 **启用** |
| **`IAgriShopFacade` / `ICategoryFacade`** | 已含于 `canPurchase`；列表失效 **细分文案** 时 **补充** 读档 |
| **`ICartFacade`（规划）** | 供 **订单模块** 下单成功后 `removeItems(memberId, cartItemIds)` |

**库存与数量：** `canPurchase` **仅校验 stock>0**；本 Service **额外** 校验 `quantity <= stock`（加购累加、改量、prepare）。

---

## 2. 数据库设计

### 2.1 新建 · 会员购物车行 `biz_member_cart_item`

| 字段 | 类型 | 说明 |
|------|------|------|
| cart_item_id | bigint PK | 自增 |
| member_id | bigint | 会员 ID（`biz_member.member_id`） |
| shop_id | bigint | 店铺 ID（加购时自 `biz_goods.shop_id` **冗余**，便于分组） |
| goods_id | bigint | 商品 ID |
| spec_key | varchar(64) | 规格键；v1.0 统一规格固定 **`''`（空串）** |
| spec_text | varchar(256) | 加购时 **规格展示快照**（如「默认」或规格项拼接） |
| quantity | int | 数量，≥1 |
| checked | char(1) | `0` 未勾选 / `1` 已勾选；**新行默认 `1`** |
| create_time | datetime | 首次加购 |
| update_time | datetime | 最近改量/勾选 |

**约束与索引：**

| 项 | 说明 |
|----|------|
| 唯一 | **`uk_member_goods_spec (member_id, goods_id, spec_key)`** — 同规格合并 |
| 索引 | `idx_member_update (member_id, update_time DESC)` — 列表排序 |
| 索引 | `idx_member_shop (member_id, shop_id)` — 按店分组 |

**DDL：** `sql/biz_member_cart_item.sql`

```sql
CREATE TABLE IF NOT EXISTS `biz_member_cart_item` (
  `cart_item_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '购物车行ID',
  `member_id` bigint(20) NOT NULL COMMENT '会员ID',
  `shop_id` bigint(20) NOT NULL COMMENT '店铺ID',
  `goods_id` bigint(20) NOT NULL COMMENT '商品ID',
  `spec_key` varchar(64) NOT NULL DEFAULT '' COMMENT '规格键；v1统一规格为空串',
  `spec_text` varchar(256) NOT NULL DEFAULT '默认' COMMENT '规格展示快照',
  `quantity` int(11) NOT NULL DEFAULT '1' COMMENT '数量',
  `checked` char(1) NOT NULL DEFAULT '1' COMMENT '0未勾选1已勾选',
  `create_time` datetime DEFAULT NULL COMMENT '加购时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`cart_item_id`),
  UNIQUE KEY `uk_member_goods_spec` (`member_id`, `goods_id`, `spec_key`),
  KEY `idx_member_update` (`member_id`, `update_time`),
  KEY `idx_member_shop` (`member_id`, `shop_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员购物车';
```

### 2.2 复用表（无 DDL 变更）

| 表 | 本模块用途 | 权威 DDL |
|----|------------|----------|
| `biz_goods` | 主图、名称、售价、库存、状态 | `sql/biz_goods.sql` |
| `biz_shop` | 店名、店态、删标 | `sql/biz_shop.sql` |
| `biz_goods_category` | 经 `ICategoryFacade` 判可见 | `sql/biz_goods_category.sql` |
| `biz_member` | 会员启用态 | `sql/biz_member.sql` |

### 2.3 规格字段（v1.0 定稿）

| 项 | 定稿 |
|----|------|
| SKU 矩阵 | **无**；与详情 v1 **统一规格** 一致 |
| `spec_key` | 固定 **`''`**；预留多规格扩展 |
| `spec_text` | 加购请求传入；空则 Service 读 `biz_goods_attr`（`attr_type=2`）拼接，仍无则 **「默认」** |
| 合并维度 | `member_id + goods_id + spec_key` |

### 2.4 失效判定（读时计算，**不落库**）

| invalidType | 条件 | 展示文案方向 | 可勾选 |
|-------------|------|--------------|:------:|
| `NONE` | 四条件满足且 `quantity <= stock` | — | ✓ |
| `OUT_OF_STOCK` | 出售中但 `stock=0` 或 `quantity>stock` | 缺货 / 库存不足 | ✗ |
| `OFF_SHELF` | `goods_status != '2'` | 已下架 / 不可购买 | ✗ |
| `SHOP_CLOSED` | `shop_status='1'` | 店铺休息中 | ✗ |
| `CATEGORY_HIDDEN` | 分类不可见 | 商品不可购买 | ✗ |
| `GOODS_DELETED` | 商品 `del_flag='2'` | 商品失效 | ✗ |
| `SHOP_DELETED` | 店铺 `del_flag='2'` | 店铺失效 | ✗ |

> **浏览与下单分离：** 历史行 **保留**；`invalidType != NONE` 时 `purchasable=false`，**不可** 参与 prepare。

---

## 3. C 端接口设计

**基路径：** `/api/cart`  
**鉴权：** 全部须 **`Authorization: Bearer {token}`**；未登录 → **401**「请先登录」  
**MemberWebConfig 追加：** `.addPathPatterns(..., "/api/cart/**")`

### 3.1 接口一览

| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/cart` | 购物车条目列表 |
| POST | `/api/cart/items` | **加购**（详情页调用） |
| PUT | `/api/cart/items/{cartItemId}/quantity` | 修改数量 |
| PUT | `/api/cart/items/checked` | 批量更新勾选 |
| DELETE | `/api/cart/items/{cartItemId}` | 移出单行 |
| DELETE | `/api/cart/items` | 批量移出（body 传 id 列表） |
| DELETE | `/api/cart/invalid` | 清理当前会员全部失效行 |
| POST | `/api/cart/checkout/prepare` | **去结算** 预校验 |

**本期不提供：**

| 项 | 说明 |
|----|------|
| 跨店合单 prepare | CT-S1 **不支持** |
| 平台/商家查会员购物车 | 无 B 端 API |
| 购物车搜索/推荐 | 需求 CT 非本期 |

### 3.2 购物车列表 `GET /api/cart`

| 项 | 说明 |
|----|------|
| 数据范围 | **仅当前会员** |
| 响应 | `AjaxResult` → `data: CartListVO` |

**`CartListVO`：**

| 字段 | 类型 | 说明 |
|------|------|------|
| items | array | **购物车条目列表**（主结构），见下 |
| checkedSummary | object | 当前 **已勾选且可购** 行合计（前端底栏参考） |

**`items[]` → `CartItemVO`：**

| 字段 | 类型 | 说明 |
|------|------|------|
| cartItemId | long | |
| goodsId | long | 进详情 |
| goodsName | string | 实时 |
| mainPic | string | 实时 |
| specText | string | **加购快照** |
| salePrice | decimal | **当前售价** |
| quantity | int | |
| subtotal | decimal | `salePrice × quantity` |
| checked | boolean | |
| purchasable | boolean | 是否可勾选结算 |
| invalidType | string | §2.4 枚举；可购时为 `NONE` |
| invalidMsg | string | 前端 Toast/标签；可购时 null |
| shopId | long | 所属店铺 |
| shopName | string | 店铺名称（实时） |
| shopAvatar | string | 店铺头像 |
| shopStatus | string | `0` 开业 / `1` 停业 |

**排序：** `cart_item_id DESC`（最新加购/合并行在前）。

**空态：** `items=[]` → 前端「购物车是空的」。

**`checkedSummary`：**

| 字段 | 说明 |
|------|------|
| checkedCount | 已勾选 **且 purchasable** 的行数 |
| checkedQuantity | 上述行数量之和 |
| checkedAmount | 上述行 `subtotal` 之和 |

> **同店结算：** 前端底栏以 **当前操作店** 内勾选行为准；若勾选跨店，**不得** 合单 prepare（CT-S4）。

### 3.3 加购 `POST /api/cart/items`

| 项 | 说明 |
|----|------|
| 调用方 | **商品详情**「加入购物车」 |
| 校验 | 会员启用；`quantity >= 1`；**四条件** + `quantity <= stock` |

**Body · `CartItemAddDTO`：**

| 字段 | 必填 | 说明 |
|------|:----:|------|
| goodsId | ✓ | |
| quantity | ✓ | 默认 1 |
| specKey | 否 | v1 **不传或传 `''`** |
| specText | 否 | 不传则 Service 生成 |

**逻辑：**

```text
canPurchase(goodsId) → 不通过 → 400 + reason
quantity > stock     → 400「库存不足」
已存在同 member+goods+spec_key 行
    → newQty = min(quantity + 旧quantity, stock)
    → UPDATE quantity, update_time；checked 保持
不存在
    → INSERT；checked='1'；shop_id 取自 goods
```

**响应：** `AjaxResult.success(data)` → `{ cartItemId, quantity }`

### 3.4 修改数量 `PUT /api/cart/items/{cartItemId}/quantity`

**Body：** `{ "quantity": 3 }`

| 规则 | 说明 |
|------|------|
| 归属 | 行须属于 **当前会员** |
| 下限 | `quantity >= 1` |
| 上限 | `quantity <= biz_goods.stock`（实时） |
| 失效 | 改量后 **不重算** checked；列表读时刷新 `purchasable` |

失败 → `400` + 明确 msg；成功 → `{ cartItemId, quantity }`。

### 3.5 批量勾选 `PUT /api/cart/items/checked`

**Body · `CartItemCheckedBatchDTO`：**

```json
{
  "items": [
    { "cartItemId": 1, "checked": true },
    { "cartItemId": 2, "checked": false }
  ]
}
```

| 规则 | 说明 |
|------|------|
| 归属 | 全部 id 须属当前会员 |
| 失效行 | **允许** 写入 checked；前端 **置灰不可勾选**；prepare 时 **过滤** |

### 3.6 移出

| 接口 | Body | 说明 |
|------|------|------|
| `DELETE .../items/{cartItemId}` | — | 单行；非本人 → 404 |
| `DELETE .../items` | `{ "cartItemIds": [1,2] }` | 批量；**忽略** 不存在 id |

均 **物理删除** 购物车行；幂等。

### 3.7 清理失效 `DELETE /api/cart/invalid`

| 项 | 说明 |
|----|------|
| 范围 | 当前会员全部 `purchasable=false` 行 |
| 响应 | `{ "removedCount": n }` |

### 3.8 去结算预校验 `POST /api/cart/checkout/prepare`

| 项 | 说明 |
|----|------|
| 用途 | 跳转 **确认订单** 前 **服务端** 再校验（CT-S2、§10） |

**Body · `CartCheckoutPrepareDTO`：**

| 字段 | 必填 | 说明 |
|------|:----:|------|
| cartItemIds | ✓ | 用户勾选行 ID，**至少 1 条** |

**校验顺序：**

```text
1. 全部 cartItemId 属于当前会员
2. 解析 shop_id → 必须 **同一店铺**（否则 400「请选择同一店铺的商品结算」）
3. 逐行：canPurchase + quantity <= stock
4. 通过 → 返回 CheckoutPrepareVO
```

**`data` · `CartCheckoutPrepareVO`：**

| 字段 | 类型 | 说明 |
|------|------|------|
| shopId | long | |
| shopName | string | |
| items | array | `{ cartItemId, goodsId, goodsName, mainPic, specText, salePrice, quantity, subtotal }` |
| goodsAmount | decimal | 商品合计（不含运费） |

> **确认订单（待建）：** 接收 `shopId + items[]` 做地址/运费/提交订单；下单成功后调用 **`ICartFacade.removeItems`** 删除对应 `cart_item_id`。

### 3.9 错误与边界

| 情形 | 行为 |
|------|------|
| Token 失效 | 401 |
| 会员禁用 | 403「账号已禁用」 |
| 商品/行不存在 | 404 |
| 库存/四条件不满足 | 400 + `reason` / `invalidMsg` |
| prepare 跨店 | 400「请选择同一店铺的商品结算」 |

---

## 4. Service 分层

```text
CartAppController
    → ICartAppService
        ├── list(memberId)              → 分组 + 失效计算 + checkedSummary
        ├── addItem(memberId, dto)      → canPurchase + merge/insert
        ├── updateQuantity(...)         → stock cap
        ├── updateChecked(...)          → batch
        ├── remove / removeBatch
        ├── cleanInvalid(memberId)
        └── prepareCheckout(memberId, ids) → 同店 + 四条件

CartAppMapper
    ├── selectByMemberId(memberId)        → JOIN goods + shop
    ├── selectByIdAndMember(...)
    ├── insert / updateQuantity / updateChecked
    └── deleteByIds / deleteInvalidByMember
```

| 要点 | 说明 |
|------|------|
| memberId | **仅** 从 `MemberContext` 取，**禁止** 客户端传 |
| 价格 | 列表/prepare **读实时** `sale_price`；**不** 在 cart 表存价 |
| 事务 | 加购 merge、批量删 **单事务** |
| 日志 | 写操作 `@Log`（可选） |

---

## 5. 与平台后台联动

| 平台/商家操作 | 购物车行为 |
|---------------|------------|
| 商品 **下架/审核失败/待审** | 行 **保留**；列表 `OFF_SHELF`；**不可** prepare |
| 商品 **改价** | 列表读 **新价**；prepare 用 **新价** |
| 商品 **减库存** | 列表可能 `OUT_OF_STOCK`；改量/prepare **拦截** |
| 店铺 **停业** | 行 **保留**；`SHOP_CLOSED`；prepare **拦截** |
| 店铺 **删店** | `SHOP_DELETED`；可 **清理失效** |
| 分类 **隐藏** | `CATEGORY_HIDDEN` |
| 会员 **禁用** | **403**，全接口不可用 |
| 订单 **支付成功** | 确认订单模块 **删行**（不占本模块） |

> 对齐《关联需求分析》§8：**不自动删** 购物车行；仅 **标失效** + 用户 **批量清理**。

---

## 6. 与兄弟模块分工

| 能力 | 商品详情 `/api/goods` | 本模块 `/api/cart` | 确认订单（待建） |
|------|----------------------|-------------------|------------------|
| 详情展示 | ✓ | — | — |
| `GET .../can-purchase` | ✓ | 内部复用 Facade | — |
| POST 加购 | 前端调 **cart** | ✓ | — |
| 列表/改量/勾选/清理 | — | ✓ | — |
| prepare | — | ✓ | 接收参数 |
| 地址/运费/提交订单 | — | — | ✓ |

**MemberWebConfig 变更：**

```java
.addPathPatterns("/api/member/**", "/api/merchant/entry/apply", "/api/merchant/entry/my",
        "/api/shop/*/follow", "/api/cart/**")
```

---

## 7. 业务规则映射

| 需求规则 | 实现 |
|----------|------|
| CT0 须登录 | `/api/cart/**` 拦截器 |
| CT1 仅当前会员 | SQL `member_id = #{memberId}` |
| CT2 按店分组 | `shop_id` 冗余 + 列表 `groups[]` |
| CT3 行字段 | `CartItemVO` |
| CT4 勾选 | `checked` 字段 + `PUT .../checked` |
| CT5 改量 ≥1 ≤库存 | `updateQuantity` |
| CT6 移出/清理失效 | DELETE 系列 |
| CT7 不支持跨店 | `prepareCheckout` 校验同 `shop_id` |
| CT8 库存/可购校验 | 列表读时 + prepare |
| CT9 失效进详情 | 前端 `goodsId` 路由 |
| CT10 详情加购、同规格合并 | `POST /items` + `uk_member_goods_spec` |
| 新行默认勾选 | INSERT `checked='1'` |
| 不预扣库存 | 无库存锁定表 |

---

## 8. 规划 · `ICartFacade`（供订单模块）

```java
public interface ICartFacade {
    /** 下单成功后移除已结算购物车行 */
    void removeItems(Long memberId, List<Long> cartItemIds);
}
```

实现落位 `cart.facade.CartFacadeImpl`；订单模块 **待实现** 前可不建。

---

## 9. 修订记录

| 版本 | 说明 |
|------|------|
| **v1.0** | 首版：`biz_member_cart_item`、8 个 C 端 API、按店分组、四条件、同店 prepare；关联平台商品/店铺/分类/会员/订单与详情 |

---

*文档版本：v1.0 · 关联《购物车功能需求.md》v1.0、《商品详情内页技术方案》v1.0、《订单管理技术方案》v1.0.1、《店铺管理技术方案》v1.3.6、《关联需求分析.md》v1.6 · 技术栈 RuoYi v3.9.2-springboot2 + MySQL 5.7.39*
