```
feat: 实现用户地址管理和购物车功能 - 新增收货地址模块,包括地址的增删改查、设置默认地址等功能 - 实现购物车模块,支持添加商品、修改数量、选中状态管理、批量操作等 - 添加相应的DTO、Entity、VO和Mapper层实现 - 集成SaToken登录验证和数据权限控制 - 实现地址数量限制和购物车商品数量限制防刷单机制 ```
This commit is contained in:
parent
1595dc8c35
commit
e5ac034349
|
|
@ -0,0 +1,79 @@
|
|||
package com.snack.server.module.address.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||
import com.snack.server.common.Result;
|
||||
import com.snack.server.module.address.dto.req.AddressSaveReq;
|
||||
import com.snack.server.module.address.service.AddressService;
|
||||
import com.snack.server.module.address.vo.AddressVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 收货地址(用户端)
|
||||
*/
|
||||
@Tag(name = "收货地址")
|
||||
@RestController
|
||||
@RequestMapping("/api/address")
|
||||
@RequiredArgsConstructor
|
||||
public class AddressController {
|
||||
|
||||
private final AddressService addressService;
|
||||
|
||||
@Operation(summary = "获取当前用户的所有地址")
|
||||
@SaCheckLogin
|
||||
@GetMapping
|
||||
public Result<List<AddressVO>> list() {
|
||||
return Result.ok(addressService.listByCurrentUser());
|
||||
}
|
||||
|
||||
@Operation(summary = "获取默认地址")
|
||||
@SaCheckLogin
|
||||
@GetMapping("/default")
|
||||
public Result<AddressVO> getDefault() {
|
||||
return Result.ok(addressService.getDefault());
|
||||
}
|
||||
|
||||
@Operation(summary = "获取地址详情")
|
||||
@SaCheckLogin
|
||||
@GetMapping("/{id}")
|
||||
public Result<AddressVO> detail(@Parameter(description = "地址 ID") @PathVariable Long id) {
|
||||
return Result.ok(addressService.getById(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "新增地址")
|
||||
@SaCheckLogin
|
||||
@PostMapping
|
||||
public Result<Long> create(@Valid @RequestBody AddressSaveReq req) {
|
||||
return Result.ok(addressService.create(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新地址")
|
||||
@SaCheckLogin
|
||||
@PutMapping
|
||||
public Result<Void> update(@Valid @RequestBody AddressSaveReq req) {
|
||||
addressService.update(req);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "删除地址")
|
||||
@SaCheckLogin
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@Parameter(description = "地址 ID") @PathVariable Long id) {
|
||||
addressService.delete(id);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "设为默认地址")
|
||||
@SaCheckLogin
|
||||
@PutMapping("/{id}/default")
|
||||
public Result<Void> setDefault(@Parameter(description = "地址 ID") @PathVariable Long id) {
|
||||
addressService.setDefault(id);
|
||||
return Result.ok();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.snack.server.module.address.dto.req;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 新增/更新地址请求
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "收货地址请求")
|
||||
public class AddressSaveReq implements Serializable {
|
||||
|
||||
@Schema(description = "地址 ID(更新时必填)")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "收货人姓名不能为空")
|
||||
@Size(max = 50, message = "姓名最长 50 字")
|
||||
@Schema(description = "收货人姓名", example = "张三")
|
||||
private String receiver;
|
||||
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
@Schema(description = "手机号", example = "13800138000")
|
||||
private String phone;
|
||||
|
||||
@Schema(description = "省", example = "广东省")
|
||||
private String province;
|
||||
|
||||
@Schema(description = "市", example = "深圳市")
|
||||
private String city;
|
||||
|
||||
@Schema(description = "区/县", example = "南山区")
|
||||
private String district;
|
||||
|
||||
@NotBlank(message = "详细地址不能为空")
|
||||
@Size(max = 255, message = "详细地址最长 255 字")
|
||||
@Schema(description = "详细地址", example = "科技园南路 88 号")
|
||||
private String detail;
|
||||
|
||||
@Size(max = 20, message = "标签最长 20 字")
|
||||
@Schema(description = "标签:家/公司/学校")
|
||||
private String tag;
|
||||
|
||||
@Schema(description = "是否设为默认:0-否 1-是", example = "0")
|
||||
private Integer isDefault = 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.snack.server.module.address.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 收货地址
|
||||
*
|
||||
* 对应数据库表:address
|
||||
*/
|
||||
@Data
|
||||
@TableName("address")
|
||||
public class Address implements Serializable {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 用户 ID */
|
||||
private Long userId;
|
||||
|
||||
/** 收货人姓名 */
|
||||
private String receiver;
|
||||
|
||||
/** 收货人手机号 */
|
||||
private String phone;
|
||||
|
||||
/** 省 */
|
||||
private String province;
|
||||
|
||||
/** 市 */
|
||||
private String city;
|
||||
|
||||
/** 区/县 */
|
||||
private String district;
|
||||
|
||||
/** 详细地址 */
|
||||
private String detail;
|
||||
|
||||
/** 标签:家/公司/学校 */
|
||||
private String tag;
|
||||
|
||||
/** 是否默认地址:0-否 1-是 */
|
||||
private Integer isDefault;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.snack.server.module.address.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.snack.server.module.address.entity.Address;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
/**
|
||||
* 收货地址 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface AddressMapper extends BaseMapper<Address> {
|
||||
|
||||
/**
|
||||
* 取消该用户所有地址的默认状态
|
||||
*/
|
||||
@Update("UPDATE address SET is_default = 0 WHERE user_id = #{userId}")
|
||||
int clearDefaultByUserId(@Param("userId") Long userId);
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.snack.server.module.address.service;
|
||||
|
||||
import com.snack.server.module.address.dto.req.AddressSaveReq;
|
||||
import com.snack.server.module.address.entity.Address;
|
||||
import com.snack.server.module.address.vo.AddressVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 收货地址业务接口
|
||||
*/
|
||||
public interface AddressService {
|
||||
|
||||
/**
|
||||
* 获取当前登录用户的所有地址
|
||||
*/
|
||||
List<AddressVO> listByCurrentUser();
|
||||
|
||||
/**
|
||||
* 获取默认地址
|
||||
*/
|
||||
AddressVO getDefault();
|
||||
|
||||
/**
|
||||
* 获取地址详情
|
||||
*/
|
||||
AddressVO getById(Long id);
|
||||
|
||||
/**
|
||||
* 创建地址
|
||||
*/
|
||||
Long create(AddressSaveReq req);
|
||||
|
||||
/**
|
||||
* 更新地址
|
||||
*/
|
||||
void update(AddressSaveReq req);
|
||||
|
||||
/**
|
||||
* 删除地址
|
||||
*/
|
||||
void delete(Long id);
|
||||
|
||||
/**
|
||||
* 设为默认地址
|
||||
*/
|
||||
void setDefault(Long id);
|
||||
|
||||
/**
|
||||
* 内部使用:根据 ID 获取实体(订单快照)
|
||||
*/
|
||||
Address getEntityById(Long id);
|
||||
}
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
package com.snack.server.module.address.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.snack.server.common.ResultCode;
|
||||
import com.snack.server.exception.BusinessException;
|
||||
import com.snack.server.module.address.dto.req.AddressSaveReq;
|
||||
import com.snack.server.module.address.entity.Address;
|
||||
import com.snack.server.module.address.mapper.AddressMapper;
|
||||
import com.snack.server.module.address.service.AddressService;
|
||||
import com.snack.server.module.address.vo.AddressVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 收货地址业务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AddressServiceImpl implements AddressService {
|
||||
|
||||
/** 单用户最大地址数 */
|
||||
private static final int MAX_ADDRESS_PER_USER = 20;
|
||||
|
||||
private final AddressMapper addressMapper;
|
||||
|
||||
// ==================== 查询 ====================
|
||||
|
||||
@Override
|
||||
public List<AddressVO> listByCurrentUser() {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
List<Address> list = addressMapper.selectList(
|
||||
new LambdaQueryWrapper<Address>()
|
||||
.eq(Address::getUserId, userId)
|
||||
.orderByDesc(Address::getIsDefault)
|
||||
.orderByDesc(Address::getId)
|
||||
);
|
||||
return list.stream().map(this::toVO).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AddressVO getDefault() {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
Address addr = addressMapper.selectOne(
|
||||
new LambdaQueryWrapper<Address>()
|
||||
.eq(Address::getUserId, userId)
|
||||
.eq(Address::getIsDefault, 1)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
return addr == null ? null : toVO(addr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AddressVO getById(Long id) {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
Address addr = addressMapper.selectOne(
|
||||
new LambdaQueryWrapper<Address>()
|
||||
.eq(Address::getId, id)
|
||||
.eq(Address::getUserId, userId) // 只能看自己的
|
||||
);
|
||||
if (addr == null) {
|
||||
throw new BusinessException(1000, "地址不存在或无权访问");
|
||||
}
|
||||
return toVO(addr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getEntityById(Long id) {
|
||||
return addressMapper.selectById(id);
|
||||
}
|
||||
|
||||
// ==================== 创建 ====================
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long create(AddressSaveReq req) {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
|
||||
// 1. 单用户地址数量限制
|
||||
Long count = addressMapper.selectCount(
|
||||
new LambdaQueryWrapper<Address>().eq(Address::getUserId, userId)
|
||||
);
|
||||
if (count >= MAX_ADDRESS_PER_USER) {
|
||||
throw new BusinessException(1000, "地址数量已达上限(" + MAX_ADDRESS_PER_USER + ")");
|
||||
}
|
||||
|
||||
// 2. 保存
|
||||
Address address = new Address();
|
||||
BeanUtil.copyProperties(req, address, "id");
|
||||
address.setUserId(userId);
|
||||
address.setIsDefault(req.getIsDefault() == null ? 0 : req.getIsDefault());
|
||||
|
||||
// 3. 默认地址处理
|
||||
if (address.getIsDefault() == 1) {
|
||||
addressMapper.clearDefaultByUserId(userId);
|
||||
} else {
|
||||
// 若用户没有地址,自动设为默认
|
||||
if (count == 0) {
|
||||
address.setIsDefault(1);
|
||||
}
|
||||
}
|
||||
addressMapper.insert(address);
|
||||
log.info("用户 {} 新增地址 id={}", userId, address.getId());
|
||||
return address.getId();
|
||||
}
|
||||
|
||||
// ==================== 更新 ====================
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void update(AddressSaveReq req) {
|
||||
if (req.getId() == null) {
|
||||
throw new BusinessException(1000, "ID 不能为空");
|
||||
}
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
Address existing = addressMapper.selectOne(
|
||||
new LambdaQueryWrapper<Address>()
|
||||
.eq(Address::getId, req.getId())
|
||||
.eq(Address::getUserId, userId)
|
||||
);
|
||||
if (existing == null) {
|
||||
throw new BusinessException(1000, "地址不存在或无权访问");
|
||||
}
|
||||
|
||||
// 默认地址处理
|
||||
Integer isDefault = req.getIsDefault() == null ? 0 : req.getIsDefault();
|
||||
if (isDefault == 1 && existing.getIsDefault() == 0) {
|
||||
addressMapper.clearDefaultByUserId(userId);
|
||||
}
|
||||
|
||||
Address address = new Address();
|
||||
BeanUtil.copyProperties(req, address);
|
||||
address.setUserId(userId); // 防止被改
|
||||
address.setIsDefault(isDefault);
|
||||
addressMapper.updateById(address);
|
||||
log.info("更新地址 id={}", address.getId());
|
||||
}
|
||||
|
||||
// ==================== 删除 ====================
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long id) {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
Address existing = addressMapper.selectOne(
|
||||
new LambdaQueryWrapper<Address>()
|
||||
.eq(Address::getId, id)
|
||||
.eq(Address::getUserId, userId)
|
||||
);
|
||||
if (existing == null) {
|
||||
throw new BusinessException(1000, "地址不存在或无权访问");
|
||||
}
|
||||
addressMapper.deleteById(id);
|
||||
|
||||
// 若删除的是默认地址,自动将剩余地址中最近的一个设为默认
|
||||
if (existing.getIsDefault() != null && existing.getIsDefault() == 1) {
|
||||
Address next = addressMapper.selectOne(
|
||||
new LambdaQueryWrapper<Address>()
|
||||
.eq(Address::getUserId, userId)
|
||||
.orderByDesc(Address::getId)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
if (next != null) {
|
||||
next.setIsDefault(1);
|
||||
addressMapper.updateById(next);
|
||||
}
|
||||
}
|
||||
log.info("用户 {} 删除地址 id={}", userId, id);
|
||||
}
|
||||
|
||||
// ==================== 设为默认 ====================
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void setDefault(Long id) {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
Address existing = addressMapper.selectOne(
|
||||
new LambdaQueryWrapper<Address>()
|
||||
.eq(Address::getId, id)
|
||||
.eq(Address::getUserId, userId)
|
||||
);
|
||||
if (existing == null) {
|
||||
throw new BusinessException(1000, "地址不存在或无权访问");
|
||||
}
|
||||
if (existing.getIsDefault() != null && existing.getIsDefault() == 1) {
|
||||
return; // 已经是默认
|
||||
}
|
||||
// 先清掉其他默认,再设自己
|
||||
addressMapper.clearDefaultByUserId(userId);
|
||||
existing.setIsDefault(1);
|
||||
addressMapper.updateById(existing);
|
||||
log.info("用户 {} 将地址 id={} 设为默认", userId, id);
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
private AddressVO toVO(Address a) {
|
||||
AddressVO vo = new AddressVO();
|
||||
BeanUtil.copyProperties(a, vo);
|
||||
// 拼接完整地址
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (StrUtil.isNotBlank(a.getProvince())) sb.append(a.getProvince()).append(" ");
|
||||
if (StrUtil.isNotBlank(a.getCity())) sb.append(a.getCity()).append(" ");
|
||||
if (StrUtil.isNotBlank(a.getDistrict())) sb.append(a.getDistrict()).append(" ");
|
||||
if (StrUtil.isNotBlank(a.getDetail())) sb.append(a.getDetail());
|
||||
vo.setFullAddress(sb.toString().trim());
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.snack.server.module.address.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 收货地址 VO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "收货地址")
|
||||
public class AddressVO implements Serializable {
|
||||
|
||||
@Schema(description = "地址 ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "收货人姓名")
|
||||
private String receiver;
|
||||
|
||||
@Schema(description = "手机号")
|
||||
private String phone;
|
||||
|
||||
@Schema(description = "省")
|
||||
private String province;
|
||||
|
||||
@Schema(description = "市")
|
||||
private String city;
|
||||
|
||||
@Schema(description = "区/县")
|
||||
private String district;
|
||||
|
||||
@Schema(description = "详细地址")
|
||||
private String detail;
|
||||
|
||||
@Schema(description = "标签")
|
||||
private String tag;
|
||||
|
||||
@Schema(description = "是否默认:0-否 1-是")
|
||||
private Integer isDefault;
|
||||
|
||||
/** 拼接的完整地址(省市区+详细),供订单快照使用 */
|
||||
@Schema(description = "完整地址")
|
||||
private String fullAddress;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package com.snack.server.module.cart.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||
import com.snack.server.common.Result;
|
||||
import com.snack.server.module.cart.dto.req.CartAddReq;
|
||||
import com.snack.server.module.cart.dto.req.CartUpdateReq;
|
||||
import com.snack.server.module.cart.service.CartService;
|
||||
import com.snack.server.module.cart.vo.CartVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 购物车(用户端)
|
||||
*/
|
||||
@Tag(name = "购物车")
|
||||
@RestController
|
||||
@RequestMapping("/api/cart")
|
||||
@RequiredArgsConstructor
|
||||
public class CartController {
|
||||
|
||||
private final CartService cartService;
|
||||
|
||||
@Operation(summary = "获取购物车")
|
||||
@SaCheckLogin
|
||||
@GetMapping
|
||||
public Result<CartVO> list() {
|
||||
return Result.ok(cartService.getCurrentCart());
|
||||
}
|
||||
|
||||
@Operation(summary = "添加商品到购物车")
|
||||
@SaCheckLogin
|
||||
@PostMapping
|
||||
public Result<Void> add(@Valid @RequestBody CartAddReq req) {
|
||||
cartService.addItem(req);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "修改购物车商品数量")
|
||||
@SaCheckLogin
|
||||
@PutMapping("/quantity")
|
||||
public Result<Void> updateQuantity(@Valid @RequestBody CartUpdateReq req) {
|
||||
cartService.updateQuantity(req);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "切换选中状态")
|
||||
@SaCheckLogin
|
||||
@PutMapping("/{skuId}/selected")
|
||||
public Result<Void> updateSelected(
|
||||
@Parameter(description = "SKU ID") @PathVariable Long skuId,
|
||||
@Parameter(description = "0-未选 1-已选") @RequestParam Integer selected) {
|
||||
cartService.updateSelected(skuId, selected);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "全选 / 全不选")
|
||||
@SaCheckLogin
|
||||
@PutMapping("/all-selected")
|
||||
public Result<Void> updateAllSelected(
|
||||
@Parameter(description = "0-全不选 1-全选") @RequestParam Integer selected) {
|
||||
cartService.updateAllSelected(selected);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "删除购物车项")
|
||||
@SaCheckLogin
|
||||
@DeleteMapping("/{skuId}")
|
||||
public Result<Void> delete(@Parameter(description = "SKU ID") @PathVariable Long skuId) {
|
||||
cartService.deleteItem(skuId);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "批量删除(结算页)")
|
||||
@SaCheckLogin
|
||||
@DeleteMapping
|
||||
public Result<Void> batchDelete(@RequestBody List<Long> skuIds) {
|
||||
cartService.deleteItems(skuIds);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "清空购物车")
|
||||
@SaCheckLogin
|
||||
@DeleteMapping("/all")
|
||||
public Result<Void> clear() {
|
||||
cartService.clear();
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "购物车商品数量(用于角标)")
|
||||
@SaCheckLogin
|
||||
@GetMapping("/count")
|
||||
public Result<Integer> count() {
|
||||
Long userId = cn.dev33.satoken.stp.StpUtil.getLoginIdAsLong();
|
||||
return Result.ok(cartService.countByUserId(userId));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.snack.server.module.cart.dto.req;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 加入购物车请求
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "加入购物车请求")
|
||||
public class CartAddReq implements Serializable {
|
||||
|
||||
@NotNull(message = "商品 ID 不能为空")
|
||||
@Schema(description = "商品 SPU ID")
|
||||
private Long productId;
|
||||
|
||||
@NotNull(message = "SKU ID 不能为空")
|
||||
@Schema(description = "商品 SKU ID")
|
||||
private Long skuId;
|
||||
|
||||
@NotNull(message = "数量不能为空")
|
||||
@Min(value = 1, message = "数量至少为 1")
|
||||
@Schema(description = "数量", example = "1")
|
||||
private Integer quantity = 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.snack.server.module.cart.dto.req;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 修改购物车数量
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "修改数量请求")
|
||||
public class CartUpdateReq implements Serializable {
|
||||
|
||||
@NotNull(message = "SKU ID 不能为空")
|
||||
@Schema(description = "SKU ID")
|
||||
private Long skuId;
|
||||
|
||||
@NotNull(message = "数量不能为空")
|
||||
@Min(value = 1, message = "数量至少为 1")
|
||||
@Schema(description = "新的数量", example = "2")
|
||||
private Integer quantity;
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.snack.server.module.cart.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 购物车
|
||||
*
|
||||
* 对应数据库表:cart
|
||||
* 唯一约束:(user_id, sku_id)
|
||||
*/
|
||||
@Data
|
||||
@TableName("cart")
|
||||
public class Cart implements Serializable {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 用户 ID */
|
||||
private Long userId;
|
||||
|
||||
/** 商品 SPU ID */
|
||||
private Long productId;
|
||||
|
||||
/** 商品 SKU ID */
|
||||
private Long skuId;
|
||||
|
||||
/** 数量 */
|
||||
private Integer quantity;
|
||||
|
||||
/** 选中状态:0-未选 1-已选 */
|
||||
private Integer selected;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.snack.server.module.cart.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.snack.server.module.cart.entity.Cart;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface CartMapper extends BaseMapper<Cart> {
|
||||
|
||||
/**
|
||||
* 数量累加(已存在记录时调用)
|
||||
*/
|
||||
@Update("UPDATE cart SET quantity = quantity + #{delta}, selected = 1 " +
|
||||
"WHERE user_id = #{userId} AND sku_id = #{skuId}")
|
||||
int incrQuantity(@Param("userId") Long userId,
|
||||
@Param("skuId") Long skuId,
|
||||
@Param("delta") Integer delta);
|
||||
|
||||
/**
|
||||
* 设置数量(用户手动修改购物车数量)
|
||||
*/
|
||||
@Update("UPDATE cart SET quantity = #{quantity} " +
|
||||
"WHERE user_id = #{userId} AND sku_id = #{skuId}")
|
||||
int updateQuantity(@Param("userId") Long userId,
|
||||
@Param("skuId") Long skuId,
|
||||
@Param("quantity") Integer quantity);
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package com.snack.server.module.cart.service;
|
||||
|
||||
import com.snack.server.module.cart.dto.req.CartAddReq;
|
||||
import com.snack.server.module.cart.dto.req.CartUpdateReq;
|
||||
import com.snack.server.module.cart.entity.Cart;
|
||||
import com.snack.server.module.cart.vo.CartVO;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 购物车业务接口
|
||||
*/
|
||||
public interface CartService {
|
||||
|
||||
/**
|
||||
* 获取当前用户的购物车(含统计)
|
||||
*/
|
||||
CartVO getCurrentCart();
|
||||
|
||||
/**
|
||||
* 添加商品到购物车(已存在则累加数量)
|
||||
*/
|
||||
void addItem(CartAddReq req);
|
||||
|
||||
/**
|
||||
* 修改购物车某项数量
|
||||
*/
|
||||
void updateQuantity(CartUpdateReq req);
|
||||
|
||||
/**
|
||||
* 切换选中状态
|
||||
*/
|
||||
void updateSelected(Long skuId, Integer selected);
|
||||
|
||||
/**
|
||||
* 全选 / 全不选
|
||||
*/
|
||||
void updateAllSelected(Integer selected);
|
||||
|
||||
/**
|
||||
* 删除购物车项
|
||||
*/
|
||||
void deleteItem(Long skuId);
|
||||
|
||||
/**
|
||||
* 批量删除(按 SKU ID 列表)
|
||||
*/
|
||||
void deleteItems(Collection<Long> skuIds);
|
||||
|
||||
/**
|
||||
* 清空购物车
|
||||
*/
|
||||
void clear();
|
||||
|
||||
/**
|
||||
* 内部:获取当前用户已选中的购物车项(订单提交时使用)
|
||||
*/
|
||||
List<Cart> listSelectedItems();
|
||||
|
||||
/**
|
||||
* 内部:删除已选中的购物车项(订单创建成功后调用)
|
||||
*/
|
||||
void deleteSelectedItems();
|
||||
|
||||
/**
|
||||
* 内部:获取购物车项数量(角标)
|
||||
*/
|
||||
int countByUserId(Long userId);
|
||||
}
|
||||
|
|
@ -0,0 +1,309 @@
|
|||
package com.snack.server.module.cart.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.snack.server.common.ResultCode;
|
||||
import com.snack.server.exception.BusinessException;
|
||||
import com.snack.server.module.cart.dto.req.CartAddReq;
|
||||
import com.snack.server.module.cart.dto.req.CartUpdateReq;
|
||||
import com.snack.server.module.cart.entity.Cart;
|
||||
import com.snack.server.module.cart.mapper.CartMapper;
|
||||
import com.snack.server.module.cart.service.CartService;
|
||||
import com.snack.server.module.cart.vo.CartItemVO;
|
||||
import com.snack.server.module.cart.vo.CartVO;
|
||||
import com.snack.server.module.product.entity.Product;
|
||||
import com.snack.server.module.product.entity.ProductSku;
|
||||
import com.snack.server.module.product.mapper.ProductMapper;
|
||||
import com.snack.server.module.product.mapper.ProductSkuMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 购物车业务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CartServiceImpl implements CartService {
|
||||
|
||||
/** 购物车单 SKU 数量上限(防止恶意刷单) */
|
||||
private static final int MAX_QTY_PER_ITEM = 99;
|
||||
|
||||
private final CartMapper cartMapper;
|
||||
private final ProductMapper productMapper;
|
||||
private final ProductSkuMapper productSkuMapper;
|
||||
|
||||
// ==================== 查询 ====================
|
||||
|
||||
@Override
|
||||
public CartVO getCurrentCart() {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
List<Cart> carts = cartMapper.selectList(
|
||||
new LambdaQueryWrapper<Cart>()
|
||||
.eq(Cart::getUserId, userId)
|
||||
.orderByDesc(Cart::getId)
|
||||
);
|
||||
if (CollUtil.isEmpty(carts)) {
|
||||
return emptyCart();
|
||||
}
|
||||
return enrichCartVO(carts);
|
||||
}
|
||||
|
||||
private CartVO emptyCart() {
|
||||
CartVO vo = new CartVO();
|
||||
vo.setItems(new ArrayList<>());
|
||||
vo.setTotalCount(0);
|
||||
vo.setSelectedCount(0);
|
||||
vo.setTotalAmount(BigDecimal.ZERO);
|
||||
vo.setSelectedAmount(BigDecimal.ZERO);
|
||||
vo.setAllSelected(0);
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 装配购物车 VO(含商品 / SKU 信息)
|
||||
*/
|
||||
private CartVO enrichCartVO(List<Cart> carts) {
|
||||
// 1. 批量查 SPU / SKU
|
||||
Set<Long> productIds = carts.stream().map(Cart::getProductId).collect(Collectors.toSet());
|
||||
Set<Long> skuIds = carts.stream().map(Cart::getSkuId).collect(Collectors.toSet());
|
||||
|
||||
Map<Long, Product> productMap = productMapper.selectBatchIds(productIds).stream()
|
||||
.collect(Collectors.toMap(Product::getId, p -> p));
|
||||
Map<Long, ProductSku> skuMap = productSkuMapper.selectBatchIds(skuIds).stream()
|
||||
.collect(Collectors.toMap(ProductSku::getId, s -> s));
|
||||
|
||||
// 2. 转 VO + 统计
|
||||
int totalCount = 0;
|
||||
int selectedCount = 0;
|
||||
BigDecimal totalAmount = BigDecimal.ZERO;
|
||||
BigDecimal selectedAmount = BigDecimal.ZERO;
|
||||
int allSelected = 1;
|
||||
|
||||
List<CartItemVO> items = new ArrayList<>();
|
||||
for (Cart c : carts) {
|
||||
Product p = productMap.get(c.getProductId());
|
||||
ProductSku s = skuMap.get(c.getSkuId());
|
||||
if (p == null || s == null) {
|
||||
// 商品 / SKU 已被删除,跳过(或保留用于用户清理)
|
||||
continue;
|
||||
}
|
||||
CartItemVO vo = new CartItemVO();
|
||||
vo.setId(c.getId());
|
||||
vo.setProductId(p.getId());
|
||||
vo.setProductName(p.getName());
|
||||
vo.setProductImage(StrUtil.blankToDefault(p.getMainImage(), s.getImage()));
|
||||
vo.setSkuId(s.getId());
|
||||
vo.setSkuName(s.getSkuName());
|
||||
vo.setSkuImage(s.getImage());
|
||||
vo.setPrice(s.getPrice());
|
||||
vo.setQuantity(c.getQuantity());
|
||||
vo.setSubtotal(s.getPrice().multiply(BigDecimal.valueOf(c.getQuantity())));
|
||||
vo.setStock(s.getStock());
|
||||
vo.setSelected(c.getSelected());
|
||||
vo.setProductStatus(p.getStatus());
|
||||
items.add(vo);
|
||||
|
||||
totalCount += c.getQuantity();
|
||||
totalAmount = totalAmount.add(vo.getSubtotal());
|
||||
if (c.getSelected() != null && c.getSelected() == 1) {
|
||||
selectedCount += c.getQuantity();
|
||||
selectedAmount = selectedAmount.add(vo.getSubtotal());
|
||||
} else {
|
||||
allSelected = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果购物车里没有未选中的项,才算全选
|
||||
if (items.stream().anyMatch(i -> i.getSelected() == 0)) {
|
||||
allSelected = 0;
|
||||
}
|
||||
|
||||
CartVO vo = new CartVO();
|
||||
vo.setItems(items);
|
||||
vo.setTotalCount(totalCount);
|
||||
vo.setSelectedCount(selectedCount);
|
||||
vo.setTotalAmount(totalAmount);
|
||||
vo.setSelectedAmount(selectedAmount);
|
||||
vo.setAllSelected(allSelected);
|
||||
return vo;
|
||||
}
|
||||
|
||||
// ==================== 添加 ====================
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void addItem(CartAddReq req) {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
|
||||
// 1. 校验商品
|
||||
Product product = productMapper.selectById(req.getProductId());
|
||||
if (product == null) {
|
||||
throw new BusinessException(ResultCode.PRODUCT_NOT_EXIST);
|
||||
}
|
||||
if (product.getStatus() == null || product.getStatus() == 0) {
|
||||
throw new BusinessException(ResultCode.PRODUCT_OFF_SHELF);
|
||||
}
|
||||
ProductSku sku = productSkuMapper.selectById(req.getSkuId());
|
||||
if (sku == null || !sku.getProductId().equals(req.getProductId())) {
|
||||
throw new BusinessException(1000, "SKU 不存在或与商品不匹配");
|
||||
}
|
||||
if (sku.getStock() == null || sku.getStock() < req.getQuantity()) {
|
||||
throw new BusinessException(ResultCode.STOCK_INSUFFICIENT);
|
||||
}
|
||||
|
||||
// 2. 已存在则累加
|
||||
Cart existing = cartMapper.selectOne(
|
||||
new LambdaQueryWrapper<Cart>()
|
||||
.eq(Cart::getUserId, userId)
|
||||
.eq(Cart::getSkuId, req.getSkuId())
|
||||
);
|
||||
|
||||
if (existing != null) {
|
||||
int newQty = existing.getQuantity() + req.getQuantity();
|
||||
if (newQty > MAX_QTY_PER_ITEM) {
|
||||
throw new BusinessException(1000, "单 SKU 数量不能超过 " + MAX_QTY_PER_ITEM);
|
||||
}
|
||||
if (newQty > sku.getStock()) {
|
||||
throw new BusinessException(ResultCode.STOCK_INSUFFICIENT);
|
||||
}
|
||||
cartMapper.updateQuantity(userId, req.getSkuId(), newQty);
|
||||
log.info("用户 {} 累加购物车 skuId={} -> {}", userId, req.getSkuId(), newQty);
|
||||
} else {
|
||||
if (req.getQuantity() > MAX_QTY_PER_ITEM) {
|
||||
throw new BusinessException(1000, "单 SKU 数量不能超过 " + MAX_QTY_PER_ITEM);
|
||||
}
|
||||
Cart cart = new Cart();
|
||||
cart.setUserId(userId);
|
||||
cart.setProductId(req.getProductId());
|
||||
cart.setSkuId(req.getSkuId());
|
||||
cart.setQuantity(req.getQuantity());
|
||||
cart.setSelected(1); // 新加入默认选中
|
||||
cartMapper.insert(cart);
|
||||
log.info("用户 {} 加入购物车 skuId={} qty={}", userId, req.getSkuId(), req.getQuantity());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 修改数量 ====================
|
||||
|
||||
@Override
|
||||
public void updateQuantity(CartUpdateReq req) {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
Cart existing = cartMapper.selectOne(
|
||||
new LambdaQueryWrapper<Cart>()
|
||||
.eq(Cart::getUserId, userId)
|
||||
.eq(Cart::getSkuId, req.getSkuId())
|
||||
);
|
||||
if (existing == null) {
|
||||
throw new BusinessException(1000, "购物车中不存在该商品");
|
||||
}
|
||||
if (req.getQuantity() > MAX_QTY_PER_ITEM) {
|
||||
throw new BusinessException(1000, "单 SKU 数量不能超过 " + MAX_QTY_PER_ITEM);
|
||||
}
|
||||
// 校验库存
|
||||
ProductSku sku = productSkuMapper.selectById(req.getSkuId());
|
||||
if (sku != null && sku.getStock() != null && req.getQuantity() > sku.getStock()) {
|
||||
throw new BusinessException(ResultCode.STOCK_INSUFFICIENT);
|
||||
}
|
||||
cartMapper.updateQuantity(userId, req.getSkuId(), req.getQuantity());
|
||||
}
|
||||
|
||||
// ==================== 选中状态 ====================
|
||||
|
||||
@Override
|
||||
public void updateSelected(Long skuId, Integer selected) {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
Cart existing = cartMapper.selectOne(
|
||||
new LambdaQueryWrapper<Cart>()
|
||||
.eq(Cart::getUserId, userId)
|
||||
.eq(Cart::getSkuId, skuId)
|
||||
);
|
||||
if (existing == null) {
|
||||
throw new BusinessException(1000, "购物车中不存在该商品");
|
||||
}
|
||||
if (existing.getSelected() != null && existing.getSelected().equals(selected)) {
|
||||
return;
|
||||
}
|
||||
existing.setSelected(selected);
|
||||
cartMapper.updateById(existing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAllSelected(Integer selected) {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
List<Cart> carts = cartMapper.selectList(
|
||||
new LambdaQueryWrapper<Cart>().eq(Cart::getUserId, userId)
|
||||
);
|
||||
for (Cart c : carts) {
|
||||
c.setSelected(selected);
|
||||
cartMapper.updateById(c);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 删除 ====================
|
||||
|
||||
@Override
|
||||
public void deleteItem(Long skuId) {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
cartMapper.delete(
|
||||
new LambdaQueryWrapper<Cart>()
|
||||
.eq(Cart::getUserId, userId)
|
||||
.eq(Cart::getSkuId, skuId)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteItems(Collection<Long> skuIds) {
|
||||
if (CollUtil.isEmpty(skuIds)) return;
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
cartMapper.delete(
|
||||
new LambdaQueryWrapper<Cart>()
|
||||
.eq(Cart::getUserId, userId)
|
||||
.in(Cart::getSkuId, skuIds)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
cartMapper.delete(new LambdaQueryWrapper<Cart>().eq(Cart::getUserId, userId));
|
||||
}
|
||||
|
||||
// ==================== 内部 ====================
|
||||
|
||||
@Override
|
||||
public List<Cart> listSelectedItems() {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
return cartMapper.selectList(
|
||||
new LambdaQueryWrapper<Cart>()
|
||||
.eq(Cart::getUserId, userId)
|
||||
.eq(Cart::getSelected, 1)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteSelectedItems() {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
cartMapper.delete(
|
||||
new LambdaQueryWrapper<Cart>()
|
||||
.eq(Cart::getUserId, userId)
|
||||
.eq(Cart::getSelected, 1)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countByUserId(Long userId) {
|
||||
return cartMapper.selectCount(
|
||||
new LambdaQueryWrapper<Cart>().eq(Cart::getUserId, userId)
|
||||
).intValue();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.snack.server.module.cart.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 购物车项 VO(含商品信息)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "购物车项")
|
||||
public class CartItemVO implements Serializable {
|
||||
|
||||
@Schema(description = "购物车记录 ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "商品 SPU ID")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "商品名称")
|
||||
private String productName;
|
||||
|
||||
@Schema(description = "商品主图")
|
||||
private String productImage;
|
||||
|
||||
@Schema(description = "SKU ID")
|
||||
private Long skuId;
|
||||
|
||||
@Schema(description = "SKU 规格名称")
|
||||
private String skuName;
|
||||
|
||||
@Schema(description = "SKU 图片")
|
||||
private String skuImage;
|
||||
|
||||
@Schema(description = "单价")
|
||||
private BigDecimal price;
|
||||
|
||||
@Schema(description = "购买数量")
|
||||
private Integer quantity;
|
||||
|
||||
@Schema(description = "小计金额")
|
||||
private BigDecimal subtotal;
|
||||
|
||||
@Schema(description = "当前库存")
|
||||
private Integer stock;
|
||||
|
||||
@Schema(description = "是否已选中")
|
||||
private Integer selected;
|
||||
|
||||
@Schema(description = "商品状态:0-下架 1-上架")
|
||||
private Integer productStatus;
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.snack.server.module.cart.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 购物车完整数据(含统计)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "购物车")
|
||||
public class CartVO implements Serializable {
|
||||
|
||||
@Schema(description = "购物车项列表")
|
||||
private List<CartItemVO> items;
|
||||
|
||||
@Schema(description = "商品总件数(含未选中)")
|
||||
private Integer totalCount;
|
||||
|
||||
@Schema(description = "已选中商品件数")
|
||||
private Integer selectedCount;
|
||||
|
||||
@Schema(description = "购物车总金额(含未选中)")
|
||||
private BigDecimal totalAmount;
|
||||
|
||||
@Schema(description = "已选中商品总金额")
|
||||
private BigDecimal selectedAmount;
|
||||
|
||||
@Schema(description = "全选状态:0-未全选 1-全选")
|
||||
private Integer allSelected;
|
||||
}
|
||||
Loading…
Reference in New Issue