支付宝小程序入门与实战 开发高颜值电商项目

168 阅读5分钟

仿天猫商城支付宝小程序开发:商品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)

	        });

	      }

	    }

	  }

	});

五、总结

通过本文的实现,我们完成了仿天猫商城支付宝小程序中的核心功能:

  1. 完整的商品SKU选择逻辑,支持多属性组合选择
  2. 购物车功能,包括数量增减、选中状态、删除和结算
  3. 后端服务支持,包括购物车数据持久化和业务逻辑处理

关键实现要点:

  • 合理设计SKU数据结构,支持多属性组合
  • 实现动态的SKU选择和匹配逻辑
  • 购物车状态管理,包括选中状态和全选功能
  • 后端服务提供完整的CRUD接口

开发者可以根据实际需求进一步扩展功能,如添加优惠券计算、商品推荐等。