仿天猫商城支付宝小程序开发:商品SKU选择与购物车逻辑实现
本文将详细讲解如何实现仿天猫商城支付宝小程序中的商品SKU选择功能和购物车逻辑,包括前端界面实现和后端数据处理。
一、商品SKU选择功能实现
1. 数据结构设计
前端SKU数据结构
javascript
// 商品SKU数据示例
const product = {
id: 123,
name: "智能手机",
price: 2999,
skus: [
{
id: "sku_001",
attributes: {
color: "黑色",
storage: "128GB"
},
price: 2999,
stock: 100,
image: "/images/sku_001.jpg"
},
{
id: "sku_002",
attributes: {
color: "白色",
storage: "256GB"
},
price: 3299,
stock: 50,
image: "/images/sku_002.jpg"
}
],
attributes: [
{
name: "颜色",
values: ["黑色", "白色"]
},
{
name: "存储",
values: ["128GB", "256GB"]
}
]
};
后端SKU接口响应
java
// Java后端SKU响应DTO
@Data
public class ProductDetailDTO {
private Long id;
private String name;
private BigDecimal price;
private List<SkuDTO> skus;
private List<AttributeDTO> attributes;
}
@Data
public class SkuDTO {
private String id;
private Map<String, String> attributes;
private BigDecimal price;
private Integer stock;
private String image;
}
@Data
public class AttributeDTO {
private String name;
private List<String> values;
}
2. 前端SKU选择组件实现
WXML模板
xml
<!-- 商品SKU选择组件 -->
<view class="sku-selector">
<!-- 商品主图 -->
<image src="{{currentSku.image || product.mainImage}}" mode="aspectFit"></image>
<!-- 价格显示 -->
<view class="price">¥{{currentSku.price || product.price}}</view>
<!-- 属性选择 -->
<block wx:for="{{product.attributes}}" wx:key="name">
<view class="attribute">
<view class="attr-name">{{item.name}}</view>
<view class="attr-values">
<block wx:for="{{item.values}}" wx:key="*this">
<view
class="attr-value {{selectedAttributes[item.name] === this ? 'active' : ''}}
{{getDisabledClass(item.name, this)}}"
bindtap="selectAttribute"
data-attr-name="{{item.name}}"
data-attr-value="{{this}}"
>
{{this}}
</view>
</block>
</view>
</view>
</block>
<!-- 购买数量 -->
<view class="quantity-selector">
<view class="label">购买数量</view>
<view class="quantity-control">
<button bindtap="decreaseQuantity">-</button>
<input type="number" value="{{quantity}}" disabled />
<button bindtap="increaseQuantity">+</button>
</view>
</view>
<!-- 加入购物车/立即购买按钮 -->
<view class="action-buttons">
<button class="add-cart" bindtap="addToCart">加入购物车</button>
<button class="buy-now" bindtap="buyNow">立即购买</button>
</view>
</view>
JS逻辑
javascript
Page({
data: {
product: {}, // 商品详情数据
currentSku: {}, // 当前选中的SKU
selectedAttributes: {}, // 已选属性
quantity: 1 // 购买数量
},
onLoad(options) {
const productId = options.id;
this.loadProductDetail(productId);
},
// 加载商品详情
async loadProductDetail(productId) {
const res = await wx.request({
url: `/api/products/${productId}`,
method: 'GET'
});
this.setData({
product: res.data,
currentSku: res.data.skus[0] || {} // 默认选中第一个SKU
});
},
// 选择属性
selectAttribute(e) {
const { attrName, attrValue } = e.currentTarget.dataset;
const { selectedAttributes } = this.data;
// 更新选中属性
const newSelected = {
...selectedAttributes,
[attrName]: attrValue
};
// 查找匹配的SKU
const matchedSku = this.findMatchedSku(newSelected);
this.setData({
selectedAttributes: newSelected,
currentSku: matchedSku || this.data.currentSku
});
},
// 查找匹配的SKU
findMatchedSku(selectedAttributes) {
return this.data.product.skus.find(sku => {
return Object.entries(selectedAttributes).every(([key, value]) => {
return sku.attributes[key] === value;
});
});
},
// 增加数量
increaseQuantity() {
this.setData({
quantity: this.data.quantity + 1
});
},
// 减少数量
decreaseQuantity() {
if (this.data.quantity > 1) {
this.setData({
quantity: this.data.quantity - 1
});
}
},
// 加入购物车
addToCart() {
const { currentSku, quantity } = this.data;
if (!currentSku.id) {
wx.showToast({ title: '请选择商品规格', icon: 'none' });
return;
}
wx.request({
url: '/api/cart',
method: 'POST',
data: {
skuId: currentSku.id,
quantity: quantity
},
success: () => {
wx.showToast({ title: '已加入购物车' });
}
});
},
// 立即购买
buyNow() {
const { currentSku, quantity } = this.data;
if (!currentSku.id) {
wx.showToast({ title: '请选择商品规格', icon: 'none' });
return;
}
// 跳转到确认订单页
wx.navigateTo({
url: `/pages/order/confirm?skuId=${currentSku.id}&quantity=${quantity}`
});
}
});
二、购物车逻辑实现
1. 购物车数据结构设计
前端购物车数据
javascript
// 购物车数据示例
const cart = {
items: [
{
skuId: "sku_001",
name: "智能手机 黑色 128GB",
price: 2999,
quantity: 2,
image: "/images/sku_001.jpg",
selected: true,
stock: 100
},
{
skuId: "sku_002",
name: "智能手机 白色 256GB",
price: 3299,
quantity: 1,
image: "/images/sku_002.jpg",
selected: false,
stock: 50
}
],
totalQuantity: 3,
totalPrice: 2999 * 2 + 3299 * 1
};
后端购物车接口
java
// Java后端购物车DTO
@Data
public class CartItemDTO {
private String skuId;
private Integer quantity;
private Boolean selected;
}
@Data
public class AddToCartRequest {
private String skuId;
private Integer quantity;
}
@Data
public class UpdateCartRequest {
private String skuId;
private Integer quantity;
private Boolean selected;
}
2. 购物车页面实现
WXML模板
xml
<!-- 购物车页面 -->
<view class="cart-page">
<!-- 购物车列表 -->
<view class="cart-list">
<block wx:for="{{cart.items}}" wx:key="skuId">
<view class="cart-item">
<!-- 商品图片 -->
<image src="{{item.image}}" mode="aspectFit"></image>
<!-- 商品信息 -->
<view class="item-info">
<view class="item-name">{{item.name}}</view>
<view class="item-price">¥{{item.price}}</view>
<!-- 数量选择 -->
<view class="quantity-selector">
<button bindtap="decreaseQuantity" data-sku-id="{{item.skuId}}">-</button>
<input type="number" value="{{item.quantity}}" disabled />
<button bindtap="increaseQuantity" data-sku-id="{{item.skuId}}">+</button>
</view>
</view>
<!-- 选中状态 -->
<view class="item-select">
<checkbox checked="{{item.selected}}" bindtap="toggleSelect" data-sku-id="{{item.skuId}}" />
</view>
<!-- 删除按钮 -->
<view class="item-delete" bindtap="deleteItem" data-sku-id="{{item.skuId}}">删除</view>
</view>
</block>
</view>
<!-- 底部结算栏 -->
<view class="cart-footer">
<!-- 全选 -->
<view class="select-all">
<checkbox checked="{{isAllSelected}}" bindtap="toggleSelectAll" />全选
</view>
<!-- 总计 -->
<view class="total">
<view>合计: ¥{{cart.totalPrice}}</view>
<view class="selected-count">已选 {{cart.totalQuantity}} 件</view>
</view>
<!-- 结算按钮 -->
<button class="checkout-btn" bindtap="checkout">结算({{cart.totalQuantity}})</button>
</view>
</view>
JS逻辑
javascript
Page({
data: {
cart: {
items: [],
totalQuantity: 0,
totalPrice: 0
},
isAllSelected: false
},
onShow() {
this.loadCart();
},
// 加载购物车数据
async loadCart() {
const res = await wx.request({
url: '/api/cart',
method: 'GET'
});
this.setData({
cart: res.data,
isAllSelected: this.isAllSelected(res.data.items)
});
},
// 检查是否全选
isAllSelected(items) {
return items.length > 0 && items.every(item => item.selected);
},
// 增加数量
async increaseQuantity(e) {
const skuId = e.currentTarget.dataset.skuId;
await this.updateCartItem(skuId, 1, null);
},
// 减少数量
async decreaseQuantity(e) {
const skuId = e.currentTarget.dataset.skuId;
await this.updateCartItem(skuId, -1, null);
},
// 更新购物车项
async updateCartItem(skuId, quantityDelta, selected) {
const { cart } = this.data;
const item = cart.items.find(item => item.skuId === skuId);
if (!item) return;
let newQuantity = item.quantity + (quantityDelta || 0);
if (newQuantity < 1) newQuantity = 1;
if (newQuantity > item.stock) {
wx.showToast({ title: '库存不足', icon: 'none' });
return;
}
await wx.request({
url: '/api/cart',
method: 'PUT',
data: {
skuId,
quantity: newQuantity,
selected
}
});
this.loadCart();
},
// 切换选中状态
async toggleSelect(e) {
const skuId = e.currentTarget.dataset.skuId;
const { cart } = this.data;
const item = cart.items.find(item => item.skuId === skuId);
if (!item) return;
await this.updateCartItem(skuId, 0, !item.selected);
},
// 全选/取消全选
async toggleSelectAll() {
const { cart, isAllSelected } = this.data;
const newSelectedState = !isAllSelected;
await wx.request({
url: '/api/cart/select-all',
method: 'PUT',
data: { selected: newSelectedState }
});
this.setData({ isAllSelected: newSelectedState });
this.loadCart();
},
// 删除商品
async deleteItem(e) {
const skuId = e.currentTarget.dataset.skuId;
wx.showModal({
title: '提示',
content: '确定要删除该商品吗?',
success: async (res) => {
if (res.confirm) {
await wx.request({
url: `/api/cart/${skuId}`,
method: 'DELETE'
});
this.loadCart();
}
}
});
},
// 结算
checkout() {
const { cart } = this.data;
const selectedItems = cart.items.filter(item => item.selected);
if (selectedItems.length === 0) {
wx.showToast({ title: '请选择商品', icon: 'none' });
return;
}
// 跳转到结算页
const skuIds = selectedItems.map(item => item.skuId).join(',');
const quantities = selectedItems.map(item => item.quantity).join(',');
wx.navigateTo({
url: `/pages/order/confirm?skuIds=${skuIds}&quantities=${quantities}`
});
}
});
三、后端关键逻辑实现
1. 购物车服务接口
java
@RestController
@RequestMapping("/api/cart")
public class CartController {
@Autowired
private CartService cartService;
// 获取购物车
@GetMapping
public ResponseEntity<CartDTO> getCart() {
Long userId = getCurrentUserId();
CartDTO cart = cartService.getCart(userId);
return ResponseEntity.ok(cart);
}
// 添加到购物车
@PostMapping
public ResponseEntity<Void> addToCart(@RequestBody AddToCartRequest request) {
Long userId = getCurrentUserId();
cartService.addToCart(userId, request.getSkuId(), request.getQuantity());
return ResponseEntity.ok().build();
}
// 更新购物车项
@PutMapping
public ResponseEntity<Void> updateCartItem(@RequestBody UpdateCartRequest request) {
Long userId = getCurrentUserId();
cartService.updateCartItem(userId, request.getSkuId(), request.getQuantity(), request.getSelected());
return ResponseEntity.ok().build();
}
// 删除购物车项
@DeleteMapping("/{skuId}")
public ResponseEntity<Void> deleteCartItem(@PathVariable String skuId) {
Long userId = getCurrentUserId();
cartService.deleteCartItem(userId, skuId);
return ResponseEntity.ok().build();
}
// 全选/取消全选
@PutMapping("/select-all")
public ResponseEntity<Void> selectAll(@RequestBody Boolean selected) {
Long userId = getCurrentUserId();
cartService.selectAll(userId, selected);
return ResponseEntity.ok().build();
}
private Long getCurrentUserId() {
// 从token或session中获取当前用户ID
return 123L; // 示例值
}
}
2. 购物车服务实现
java
@Service
public class CartServiceImpl implements CartService {
@Autowired
private CartItemRepository cartItemRepository;
@Autowired
private SkuService skuService;
@Override
public CartDTO getCart(Long userId) {
List<CartItem> cartItems = cartItemRepository.findByUserId(userId);
// 计算总价和总数
int totalQuantity = 0;
BigDecimal totalPrice = BigDecimal.ZERO;
List<CartItemDTO> itemDTOs = cartItems.stream().map(item -> {
SkuDTO sku = skuService.getSkuById(item.getSkuId());
int itemTotal = item.getQuantity() * sku.getPrice().intValue();
totalQuantity += item.getQuantity();
totalPrice = totalPrice.add(new BigDecimal(itemTotal));
return new CartItemDTO(
item.getSkuId(),
sku.getName(),
sku.getPrice(),
item.getQuantity(),
sku.getImage(),
item.getSelected(),
sku.getStock()
);
}).collect(Collectors.toList());
return new CartDTO(itemDTOs, totalQuantity, totalPrice);
}
@Override
public void addToCart(Long userId, String skuId, Integer quantity) {
// 检查库存
SkuDTO sku = skuService.getSkuById(skuId);
if (sku.getStock() < quantity) {
throw new BusinessException("库存不足");
}
// 检查是否已存在
Optional<CartItem> existingItem = cartItemRepository.findByUserIdAndSkuId(userId, skuId);
if (existingItem.isPresent()) {
// 已存在,增加数量
CartItem item = existingItem.get();
int newQuantity = item.getQuantity() + quantity;
if (newQuantity > sku.getStock()) {
throw new BusinessException("库存不足");
}
item.setQuantity(newQuantity);
cartItemRepository.save(item);
} else {
// 新增
CartItem newItem = new CartItem();
newItem.setUserId(userId);
newItem.setSkuId(skuId);
newItem.setQuantity(quantity);
newItem.setSelected(true);
cartItemRepository.save(newItem);
}
}
@Override
public void updateCartItem(Long userId, String skuId, Integer quantity, Boolean selected) {
Optional<CartItem> itemOpt = cartItemRepository.findByUserIdAndSkuId(userId, skuId);
if (!itemOpt.isPresent()) {
throw new BusinessException("购物车项不存在");
}
CartItem item = itemOpt.get();
if (quantity != null) {
SkuDTO sku = skuService.getSkuById(skuId);
if (quantity > sku.getStock()) {
throw new BusinessException("库存不足");
}
item.setQuantity(quantity);
}
if (selected != null) {
item.setSelected(selected);
}
cartItemRepository.save(item);
}
@Override
public void deleteCartItem(Long userId, String skuId) {
cartItemRepository.deleteByUserIdAndSkuId(userId, skuId);
}
@Override
public void selectAll(Long userId, Boolean selected) {
cartItemRepository.updateSelectedStatus(userId, selected);
}
}
四、关键问题与解决方案
1. SKU选择逻辑优化
-
问题:当属性组合不存在时,如何引导用户选择?
-
解决方案:
- 禁用无效的属性组合。
- 自动选择第一个可用的SKU。
javascript
// 在WXML中添加禁用状态判断
<view
class="attr-value {{selectedAttributes[item.name] === this ? 'active' : ''}}
{{getDisabledClass(item.name, this)}}"
bindtap="selectAttribute"
data-attr-name="{{item.name}}"
data-attr-value="{{this}}"
>
{{this}}
</view>
// JS中添加禁用判断方法
getDisabledClass(attrName, attrValue) {
const tempSelected = { ...this.data.selectedAttributes, [attrName]: attrValue };
return this.findMatchedSku(tempSelected) ? '' : 'disabled';
}
2. 购物车并发问题
-
问题:多个设备同时修改购物车可能导致数据不一致。
-
解决方案:
- 使用乐观锁或版本号控制。
- 添加时间戳字段,更新时检查时间戳。
java
// CartItem实体添加版本号字段
@Entity
public class CartItem {
@Id
private String id;
private Long userId;
private String skuId;
private Integer quantity;
private Boolean selected;
@Version
private Long version;
// getters and setters
}
// 更新时检查版本号
@Override
public void updateCartItem(Long userId, String skuId, Integer quantity, Boolean selected, Long version) {
Optional<CartItem> itemOpt = cartItemRepository.findByUserIdAndSkuId(userId, skuId);
if (!itemOpt.isPresent()) {
throw new BusinessException("购物车项不存在");
}
CartItem item = itemOpt.get();
if (version != null && !version.equals(item.getVersion())) {
throw new BusinessException("购物车已被修改,请刷新后重试");
}
// 更新逻辑...
}
3. 性能优化
-
问题:购物车数据量大时加载慢。
-
解决方案:
- 添加本地缓存(如Storage或内存缓存)。
- 分页加载购物车数据。
javascript
// 前端添加本地缓存
Page({
data: {
cart: {
items: [],
totalQuantity: 0,
totalPrice: 0
},
isAllSelected: false,
cachedCart: null
},
onShow() {
const cachedCart = wx.getStorageSync('cachedCart');
if (cachedCart) {
this.setData({ cachedCart });
}
this.loadCart();
},
async loadCart() {
try {
const res = await wx.request({
url: '/api/cart',
method: 'GET'
});
wx.setStorageSync('cachedCart', res.data);
this.setData({
cart: res.data,
isAllSelected: this.isAllSelected(res.data.items)
});
} catch (error) {
if (this.data.cachedCart) {
// 使用缓存数据
this.setData({
cart: this.data.cachedCart,
isAllSelected: this.isAllSelected(this.data.cachedCart.items)
});
}
}
}
});
五、总结
通过本文的实现,我们完成了仿天猫商城支付宝小程序中的核心功能:
- 完整的商品SKU选择逻辑,支持多属性组合选择
- 购物车功能,包括数量增减、选中状态、删除和结算
- 后端服务支持,包括购物车数据持久化和业务逻辑处理
关键实现要点:
- 合理设计SKU数据结构,支持多属性组合
- 实现动态的SKU选择和匹配逻辑
- 购物车状态管理,包括选中状态和全选功能
- 后端服务提供完整的CRUD接口
开发者可以根据实际需求进一步扩展功能,如添加优惠券计算、商品推荐等。