# 购物车 — 技术方案(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/**", "/api/shop/*/follow", "/api/cart/**", "/api/checkout/**", "/api/order/**") .excludePathPatterns("/api/member/register", "/api/member/login", ..., "/api/merchant/entry/agreement", "/api/merchant/entry/status"); ``` > 完整 exclude 列表见《会员注册登录技术方案》§3.1。 --- ## 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 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*