加入购物车

📝 完整带注释代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
package com.easymall.service.impl;

// 导入项目内部类:常量、枚举、实体、查询对象、VO、异常、Mapper、工具类等
import com.easymall.constants.Constants;
import com.easymall.entity.enums.PageSize;
import com.easymall.entity.enums.ProductStatusEnum;
import com.easymall.entity.po.ProductCart;
import com.easymall.entity.po.ProductInfo;
import com.easymall.entity.po.ProductPropertyValue;
import com.easymall.entity.po.ProductSku;
import com.easymall.entity.query.*;
import com.easymall.entity.vo.PaginationResultVO;
import com.easymall.entity.vo.ProductSkuProperDataVO;
import com.easymall.entity.vo.ProductSkuVO;
import com.easymall.exception.BusinessException;
import com.easymall.mappers.ProductCartMapper;
import com.easymall.mappers.ProductInfoMapper;
import com.easymall.mappers.ProductPropertyValueMapper;
import com.easymall.mappers.ProductSkuMapper;
import com.easymall.service.ProductCartService;
import com.easymall.utils.StringTools;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
* 购物车 业务接口实现类
* @Service 注解:声明这是一个 Spring 管理的 Service 组件,名字叫 "productCartService"
*/
@Service("productCartService")
public class ProductCartServiceImpl implements ProductCartService {

// ----- 依赖注入的 Mapper(数据访问层对象)-----
@Resource
private ProductCartMapper<ProductCart, ProductCartQuery> productCartMapper; // 购物车表 Mapper

@Resource
private ProductInfoMapper<ProductInfo, ProductInfoQuery> productInfoMapper; // 商品信息表 Mapper

@Resource
private ProductSkuMapper<ProductSku, ProductSkuQuery> productSkuMapper; // 商品 SKU 表 Mapper

@Resource
private ProductPropertyValueMapper<ProductPropertyValue, ProductPropertyValueQuery> productPropertyValueMapper; // 商品属性值表 Mapper

// ========== 基础 CRUD 方法(与之前分类代码类似,不再重复注释)==========

/**
* 根据条件查询购物车列表
*/
@Override
public List<ProductCart> findListByParam(ProductCartQuery param) {
return this.productCartMapper.selectList(param);
}

/**
* 根据条件查询购物车记录总数
*/
@Override
public Integer findCountByParam(ProductCartQuery param) {
return this.productCartMapper.selectCount(param);
}

/**
* 分页查询购物车
*/
@Override
public PaginationResultVO<ProductCart> findListByPage(ProductCartQuery param) {
int count = this.findCountByParam(param);
int pageSize = param.getPageSize() == null ? PageSize.SIZE15.getSize() : param.getPageSize();

SimplePage page = new SimplePage(param.getPageNo(), count, pageSize);
param.setSimplePage(page);
List<ProductCart> list = this.findListByParam(param);
PaginationResultVO<ProductCart> result = new PaginationResultVO(count, page.getPageSize(), page.getPageNo(), page.getPageTotal(), list);
return result;
}

@Override
public Integer add(ProductCart bean) {
return this.productCartMapper.insert(bean);
}

@Override
public Integer addBatch(List<ProductCart> listBean) {
if (listBean == null || listBean.isEmpty()) return 0;
return this.productCartMapper.insertBatch(listBean);
}

@Override
public Integer addOrUpdateBatch(List<ProductCart> listBean) {
if (listBean == null || listBean.isEmpty()) return 0;
return this.productCartMapper.insertOrUpdateBatch(listBean);
}

@Override
public Integer updateByParam(ProductCart bean, ProductCartQuery param) {
StringTools.checkParam(param);
return this.productCartMapper.updateByParam(bean, param);
}

@Override
public Integer deleteByParam(ProductCartQuery param) {
StringTools.checkParam(param);
return this.productCartMapper.deleteByParam(param);
}

@Override
public ProductCart getProductCartByCartId(String cartId) {
return this.productCartMapper.selectByCartId(cartId);
}

@Override
public Integer updateProductCartByCartId(ProductCart bean, String cartId) {
return this.productCartMapper.updateByCartId(bean, cartId);
}

@Override
public Integer deleteProductCartByCartId(String cartId) {
return this.productCartMapper.deleteByCartId(cartId);
}

/**
* 根据商品ID、属性值哈希、用户ID 查询购物车记录(联合唯一键查询)
*/
@Override
public ProductCart getProductCartByProductIdAndPropertyValueIdHashAndUserId(String productId, String propertyValueIdHash, String userId) {
return this.productCartMapper.selectByProductIdAndPropertyValueIdHashAndUserId(productId, propertyValueIdHash, userId);
}

/**
* 根据商品ID、属性值哈希、用户ID 更新购物车记录
*/
@Override
public Integer updateProductCartByProductIdAndPropertyValueIdHashAndUserId(ProductCart bean, String productId, String propertyValueIdHash, String userId) {
return this.productCartMapper.updateByProductIdAndPropertyValueIdHashAndUserId(bean, productId, propertyValueIdHash, userId);
}

/**
* 根据商品ID、属性值哈希、用户ID 删除购物车记录
*/
@Override
public Integer deleteProductCartByProductIdAndPropertyValueIdHashAndUserId(String productId, String propertyValueIdHash, String userId) {
return this.productCartMapper.deleteByProductIdAndPropertyValueIdHashAndUserId(productId, propertyValueIdHash, userId);
}

// ========== 核心业务方法 ==========

/**
* 添加商品到购物车(核心方法)
* @Transactional 注解:保证数据库操作的原子性,出现异常自动回滚
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void add2Cart(ProductCart cart) {
Date curDate = new Date();

// ----- 步骤1:将同一商品(同一 productId 和 userId)的其他规格购物车项的更新时间提前一秒,使其排序靠后(视觉上“新加的商品排前面”)-----
ProductCartQuery query = new ProductCartQuery();
query.setProductId(cart.getProductId());
query.setUserId(cart.getUserId());

ProductCart productCart = new ProductCart();
productCart.setLastUpdateTime(new Date(curDate.getTime() - 1000)); // 比当前时间早1秒
this.productCartMapper.updateByParam(productCart, query); // 批量更新符合条件的记录的 lastUpdateTime

// ----- 步骤2:根据传入的商品ID和属性值ID串(如 "1-2-3"),查找对应的 SKU(库存量单位)-----
// 注意:购物车存储的是 propertyValueIds(原始ID串),这里要将其 MD5 哈希后再查 SKU 表(因为 SKU 表存的是哈希值)
ProductSku sku = this.productSkuMapper.selectByProductIdAndPropertyValueIdHash(
cart.getProductId(),
StringTools.encodeByMD5(cart.getPropertyValueIds())
);
if (sku == null) {
// 如果找不到对应的 SKU,说明用户选的规格组合不存在,抛出业务异常
throw new BusinessException("商品不存在");
}

// ----- 步骤3:检查该用户购物车中是否已经存在相同商品+相同规格的记录(通过联合唯一键查询)-----
ProductCart dbCart = this.productCartMapper.selectByProductIdAndPropertyValueIdHashAndUserId(
cart.getProductId(),
StringTools.encodeByMD5(cart.getPropertyValueIds()),
cart.getUserId()
);

if (dbCart == null) {
// 不存在 → 新增一条购物车记录
String cartId = StringTools.getRandomString(Constants.LENGTH_15); // 生成15位随机字符串作为购物车ID
cart.setCartId(cartId);
cart.setPropertyValueIdHash(StringTools.encodeByMD5(cart.getPropertyValueIds())); // 存储属性值ID串的 MD5 哈希
cart.setLastUpdateTime(curDate);
cart.setCreateTime(curDate);
this.add(cart); // 插入数据库
} else {
// 已存在 → 更新该记录的购买数量(在原数量上累加传入的 buyCount)
this.productCartMapper.updateCartBuyCount(dbCart.getCartId(), cart.getBuyCount());
}
}

/**
* 加载用户的购物车列表(分页,并组装成前端需要的 VO 对象)
* @param query 查询条件(至少包含 userId,用于分页)
* @return 分页结果,每项为 ProductSkuVO(包含商品详情、SKU 信息、属性描述等)
*/
@Override
public PaginationResultVO<ProductSkuVO> loadProductCart(ProductCartQuery query) {
// 1. 先分页查询购物车基础数据(只包含 productId、propertyValueIds、数量等)
PaginationResultVO<ProductCart> resultVO = this.findListByPage(query);
List<ProductCart> list = resultVO.getList();
if (list.isEmpty()) {
// 购物车为空,直接返回空分页结果
return new PaginationResultVO<>(0, query.getPageSize(), query.getPageNo(), 0, new ArrayList<>());
}

// 2. 收集当前页所有购物车项涉及的商品ID(去重后)
List<String> productIdList = list.stream().map(ProductCart::getProductId).toList();

// ----- 3. 批量查询商品基本信息(ProductInfo)-----
ProductInfoQuery productInfoQuery = new ProductInfoQuery();
productInfoQuery.setProductIdList(productIdList);
List<ProductInfo> productInfoList = productInfoMapper.selectList(productInfoQuery);
// 将商品信息列表转换成 Map,key = productId,value = ProductInfo,方便 O(1) 查找
Map<String, ProductInfo> tempProductInfoMap = productInfoList.stream().collect(Collectors.toMap(
ProductInfo::getProductId,
Function.identity(),
(data1, data2) -> data2
));

// ----- 4. 批量查询商品属性值(ProductPropertyValue),用于展示用户选择的规格文字(如“颜色:红”、“尺寸:L”)-----
ProductPropertyValueQuery productPropertyValueQuery = new ProductPropertyValueQuery();
productPropertyValueQuery.setProductIdList(productIdList);
List<ProductPropertyValue> productPropertyValueList = productPropertyValueMapper.selectList(productPropertyValueQuery);
// 构建 Map,key = productId + propertyValueId,value = 属性值对象
Map<String, ProductPropertyValue> productPropertyValueMap = productPropertyValueList.stream().collect(Collectors.toMap(
item -> item.getProductId() + item.getPropertyValueId(),
Function.identity(),
(data1, data2) -> data2
));

// ----- 5. 批量查询 SKU 信息(用于获取价格、库存、封面等)-----
ProductSkuQuery productSkuQuery = new ProductSkuQuery();
productSkuQuery.setProductIdList(productIdList);
List<ProductSku> productSkuList = productSkuMapper.selectList(productSkuQuery);
// 构建 Map,key = productId + propertyValueIds(原始ID串),value = SKU 对象
Map<String, ProductSku> productSkuMap = productSkuList.stream().collect(Collectors.toMap(
item -> item.getProductId() + item.getPropertyValueIds(),
Function.identity(),
(data1, data2) -> data2
));

// ----- 6. 遍历购物车列表,组装前端需要的 ProductSkuVO 对象 -----
List<ProductSkuVO> productSkuVOList = new ArrayList<>();
for (ProductCart cart : list) {
ProductSkuVO prproductSkuVO = new ProductSkuVO();
prproductSkuVO.setCartId(cart.getCartId()); // 购物车记录ID

// 解析用户选择的规格值ID串,例如 "1-2-3" 拆分成 ["1","2","3"]
String propertyValueIds = cart.getPropertyValueIds();
String[] propertyValueIdArray = propertyValueIds.split("-");
List<ProductSkuProperDataVO> propertyData = new ArrayList<>();

String cover = null; // 用于记录规格封面图(优先取第一个有封面的规格)
for (String propertyValueId : propertyValueIdArray) {
ProductSkuProperDataVO productSkuProperDataVO = new ProductSkuProperDataVO();
// 从 Map 中取出对应的属性值对象
ProductPropertyValue productPropertyValue = productPropertyValueMap.get(cart.getProductId() + propertyValueId);
if (productPropertyValue == null) {
continue; // 理论上不会为空,防御性跳过
}
productSkuProperDataVO.setPropertyName(productPropertyValue.getPropertyName()); // 属性名,如“颜色”
productSkuProperDataVO.setPropertyValue(productPropertyValue.getPropertyValue()); // 属性值,如“红色”
propertyData.add(productSkuProperDataVO);

// 如果还没有设置封面,且该规格有封面图,则使用该图作为展示封面
if (cover == null && !StringTools.isEmpty(productPropertyValue.getPropertyCover())) {
cover = productPropertyValue.getPropertyCover();
}
}
prproductSkuVO.setPropertyData(propertyData); // 设置规格描述列表

// 获取商品基本信息
ProductInfo productInfo = tempProductInfoMap.get(cart.getProductId());

// 获取对应的 SKU 信息(价格、库存等)
ProductSku productSku = productSkuMap.get(cart.getProductId() + cart.getPropertyValueIds());

// 设置封面:优先用规格封面,没有则用商品主图的第一张
cover = cover == null ? productInfo.getCover().split(",")[0] : cover;

prproductSkuVO.setProductId(cart.getProductId());
prproductSkuVO.setProductName(productInfo.getProductName());
prproductSkuVO.setPrice(productSku.getPrice());
prproductSkuVO.setStock(productSku.getStock());
prproductSkuVO.setPropertyValueIds(cart.getPropertyValueIds());
prproductSkuVO.setPropertyValueIdHash(StringTools.encodeByMD5(cart.getPropertyValueIds()));
prproductSkuVO.setBuyCount(cart.getBuyCount()); // 购买数量
prproductSkuVO.setProductCover(cover); // 商品封面图

// 判断商品是否处于上架状态(ON_SALE 表示上架)
prproductSkuVO.setProductOnSale(ProductStatusEnum.ON_SALE.getStatus().equals(productInfo.getStatus()));

productSkuVOList.add(prproductSkuVO);
}

// 7. 返回包装好的分页结果
return new PaginationResultVO<>(resultVO.getTotalCount(), resultVO.getPageSize(),
resultVO.getPageNo(), resultVO.getPageTotal(), productSkuVOList);
}
}

🧠 关键逻辑通俗解释

1. add2Cart 方法 —— 加入购物车

  • 为什么要更新其他同商品记录的 lastUpdateTime 减1秒?
    购物车列表通常是按 lastUpdateTime 倒序排列的(最近操作的排在前面)。当用户添加一个商品的新规格时,我们希望这个新加的项排在最上面,所以把该商品下其他规格的更新时间调早1秒,这样新项的时间最新,自然排在首位。

  • propertyValueIdspropertyValueIdHash 是什么?
    用户选择一个商品的规格(如“红色 + L码”),前端会把选中属性值的ID拼接成字符串,例如 "1-3"

    • 购物车表存储原始的 propertyValueIds(便于展示时拆分)。
    • SKU 表为了索引和查询效率,存储的是该串的 MD5 哈希值
      所以代码中在查询 SKU 前会将 propertyValueIds 进行 MD5 加密。
  • 购物车去重逻辑
    同一个用户、同一个商品、同一组规格,在购物车中应该是一条记录,再次添加时只累加数量,不重复创建新记录。

2. loadProductCart 方法 —— 加载购物车列表

这个方法的核心是将多个表的数据拼装成一个前端友好的对象

提供的数据 用途
product_cart 用户ID、商品ID、规格ID串、数量 购物车基础记录
product_info 商品名称、主图、状态 展示商品基本信息、判断是否上架
product_property_value 属性名(如“颜色”)、属性值(如“红色”)、规格封面图 "1-3" 翻译成用户可读的 颜色:红色;尺寸:L
product_sku 价格、库存 展示当前规格的实际售价和库存量

流程总结

  1. 先查购物车表,拿到当前页的购物车项。
  2. 收集所有商品ID,一次性批量查询关联的商品信息、属性值、SKU 信息(避免循环查库,提高性能)。
  3. 遍历购物车列表,从提前查好的 Map 中取出对应数据,组装成 ProductSkuVO 对象返回。

3. 为什么使用 @Transactional

add2Cart 方法涉及多次数据库操作:

  • 更新同商品其他项的 lastUpdateTime
  • 查询 SKU 是否存在
  • 查询购物车是否已有记录
  • 插入或更新购物车记录

如果中间任何一步出错(比如 SKU 不存在、数据库连接中断),@Transactional回滚所有已执行的操作,保证数据一致性,避免出现“更新了时间但没添加成功”的脏数据。


如果对其中某段代码的细节还有疑问,欢迎随时提出。