feat(goods): 商品模块快照机制引入

- 新增商品默认待审核,下架状态
- 仅审核通过的商品可上架
- 每次上架商品均产生商品及sku的快照记录
This commit is contained in:
huk 2025-09-22 16:10:34 +08:00
parent d99ac96aac
commit 08af9c4f8f
16 changed files with 413 additions and 17 deletions

View File

@ -1,5 +1,7 @@
package com.wzj.soopin.goods.business;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.Assert;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@ -8,12 +10,19 @@ import com.wzj.soopin.goods.convert.SkuConvert;
import com.wzj.soopin.goods.domain.bo.ProductBo;
import com.wzj.soopin.goods.domain.bo.SkuBo;
import com.wzj.soopin.goods.domain.entity.Product;
import com.wzj.soopin.goods.domain.entity.ProductSnapshot;
import com.wzj.soopin.goods.domain.entity.Sku;
import com.wzj.soopin.goods.domain.entity.SkuSnapshot;
import com.wzj.soopin.goods.domain.vo.ProductVO;
import com.wzj.soopin.goods.enums.ProductAuthFlag;
import com.wzj.soopin.goods.service.ProductService;
import com.wzj.soopin.goods.service.ProductSnapshotService;
import com.wzj.soopin.goods.service.SkuService;
import com.wzj.soopin.goods.service.SkuSnapshotService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.event.Constants;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.common.web.core.BusinessImpl;
import org.springframework.stereotype.Service;
@ -31,6 +40,8 @@ public class ProductBusinessImpl extends BusinessImpl<ProductService, ProductCon
private final ProductService productService;
private final SkuService skuService;
private final SkuConvert skuConvert;
private final ProductSnapshotService productSnapshotService;
private final SkuSnapshotService skuSnapshotService;
@Override
@ -55,11 +66,23 @@ public class ProductBusinessImpl extends BusinessImpl<ProductService, ProductCon
@Transactional(rollbackFor = Exception.class)
@Override
public boolean save(ProductBo bo) {
boolean result;
Product product = converter.toPo(bo);
product.setType(1);
product.setSales("0");
product.setTenantId(TenantHelper.getTenantId());
boolean result = productService.saveOrUpdate(product);
if (bo.getId() == null) {
product.setAuthFlag(ProductAuthFlag.WAIT_AUDIT.getCode());
product.setPublishStatus(Constants.PublishStatus.UNDERCARRIAGE);
result = productService.save(product);
} else {
Product po = productService.getById(bo.getId());
Assert.notNull(po, () -> new ServiceException("商品信息不存在"));
Assert.isTrue(po.getPublishStatus().equals(Constants.PublishStatus.UNDERCARRIAGE)
&& (po.getAuthFlag().equals(ProductAuthFlag.WAIT_AUDIT.getCode()) || po.getAuthFlag().equals(ProductAuthFlag.AUDIT_REJECT.getCode()))
, () -> new ServiceException("仅下架且未审核通过的商品可编辑"));
result = productService.updateById(product);
}
// 新增则直接保存sku信息
if (bo.getId() == null) {
List<Sku> skus = bo.getSkuList().stream().map(skuBO -> {
@ -68,7 +91,7 @@ public class ProductBusinessImpl extends BusinessImpl<ProductService, ProductCon
sku.setTenantId(TenantHelper.getTenantId());
return sku;
}).collect(Collectors.toList());
result = skuService.saveBatch(skus);
result = skuService.insertBatch(skus);
} else {
// 更新需按照新sku信息和旧sku信息对比id一致的数据更新新sku信息中id缺失的要删除没有id的要新增
List<SkuBo> skuList = bo.getSkuList();
@ -117,7 +140,7 @@ public class ProductBusinessImpl extends BusinessImpl<ProductService, ProductCon
// 执行新增操作
if (CollectionUtils.isNotEmpty(skusToSave)) {
result = skuService.saveBatch(skusToSave) && result;
result = skuService.insertBatch(skusToSave) && result;
}
// 执行删除操作
@ -135,6 +158,7 @@ public class ProductBusinessImpl extends BusinessImpl<ProductService, ProductCon
if (productToUpdate == null) {
throw new RuntimeException("商品不存在");
}
Assert.isTrue(!productToUpdate.getPublishStatus().equals(Constants.PublishStatus.GROUNDING), () -> new ServiceException("上架中的商品不可审核"));
productToUpdate.setAuthFlag(authFlag);
productToUpdate.setReasons(reasons);
productService.updateById(productToUpdate);
@ -142,14 +166,32 @@ public class ProductBusinessImpl extends BusinessImpl<ProductService, ProductCon
}
@Override
@Transactional
public boolean publish(Long id, Integer publishStatus) {
Product product = productService.getById(id);
if (product == null) {
throw new RuntimeException("商品不存在");
}
Assert.isTrue(product.getAuthFlag().equals(ProductAuthFlag.AUDIT_PASS.getCode()), () -> new ServiceException("商品未通过审核"));
product.setPublishStatus(publishStatus);
productService.updateById(product);
return true;
// 发布上架的商品及sku创建商品和sku快照
if(Constants.PublishStatus.GROUNDING.equals(publishStatus)){
List<Sku> skuList = skuService.getByProductId(id);
ProductSnapshot productSnapshot = BeanUtil.copyProperties(product, ProductSnapshot.class, "id");
productSnapshot.setProductId(product.getId());
productSnapshotService.save(productSnapshot);
product.setProductSnapshotId(productSnapshot.getId());
List<SkuSnapshot> skuSnapshotList = skuList.stream().map(sku -> {
SkuSnapshot skuSnapshot = BeanUtil.copyProperties(sku, SkuSnapshot.class, "id");
skuSnapshot.setSkuId(sku.getId());
skuSnapshot.setProductSnapshotId(productSnapshot.getId());
return skuSnapshot;
}).toList();
skuSnapshotService.insertBatch(skuSnapshotList);
}else{
product.setAuthFlag(ProductAuthFlag.WAIT_AUDIT.getCode());
}
return productService.updateById(product);
}
@Override

View File

@ -6,10 +6,12 @@ import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.ibatis.type.JdbcType;
import org.dromara.common.excel.annotation.Excel;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.io.Serial;
import java.math.BigDecimal;
/**
@ -17,15 +19,20 @@ import java.math.BigDecimal;
*
* @author zcc
*/
@EqualsAndHashCode(callSuper = true)
@Schema(description = "商品信息对象")
@Data
@TableName("pms_product")
public class Product extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "ID")
private Long id;
@Schema(description = "当前快照id")
private Long productSnapshotId;
@Schema(description = "BRAND_ID")
@Excel(name = "BRAND_ID")
private Long brandId;
@ -111,7 +118,6 @@ public class Product extends BaseEntity {
private String reasons;
@Schema(description = "删除标志0代表存在1代表删除")
@Excel(name = "删除标志0代表存在1代表删除")
@TableLogic(value ="0", delval = "1")
@TableField(value ="del_flag",fill = FieldFill.INSERT, jdbcType = JdbcType.CHAR)
private String delFlag;

View File

@ -0,0 +1,163 @@
package com.wzj.soopin.goods.domain.entity;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.ibatis.type.JdbcType;
import java.math.BigDecimal;
/**
* 商品信息
*/
@Schema(description = "商品信息")
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "wzj_soopin.pms_product_snapshot")
public class ProductSnapshot {
/**
* 主键id
*/
@TableId(value = "id")
@Schema(description = "主键id")
private Long id;
/**
* 商品id
*/
@TableField(value = "product_id")
@Schema(description = "商品id")
private Long productId;
/**
* 品牌id
*/
@TableField(value = "brand_id")
@Schema(description = "品牌id")
private Long brandId;
/**
* 分类id
*/
@TableField(value = "category_id")
@Schema(description = "分类id")
private Long categoryId;
/**
* 商品编码
*/
@TableField(value = "out_product_id")
@Schema(description = "商品编码")
@Size(max = 64, message = "商品编码最大长度要小于 64")
private String outProductId;
/**
* 商品名称
*/
@TableField(value = "`name`")
@Schema(description = "商品名称")
@Size(max = 64, message = "商品名称最大长度要小于 64")
@NotBlank(message = "商品名称不能为空")
private String name;
/**
* 主图
*/
@TableField(value = "pic")
@Schema(description = "主图")
@Size(max = 255, message = "主图最大长度要小于 255")
private String pic;
/**
* 画册图片连产品图片限制为5张以逗号分割
*/
@TableField(value = "album_pics")
@Schema(description = "画册图片连产品图片限制为5张以逗号分割")
@Size(max = 1000, message = "画册图片连产品图片限制为5张以逗号分割最大长度要小于 1000")
private String albumPics;
/**
* 排序
*/
@TableField(value = "sort")
@Schema(description = "排序")
private Integer sort;
/**
* 价格
*/
@TableField(value = "price")
@Schema(description = "价格")
private BigDecimal price;
/**
* 单位
*/
@TableField(value = "unit")
@Schema(description = "单位")
@Size(max = 16, message = "单位最大长度要小于 16")
private String unit;
/**
* 商品重量默认为克
*/
@TableField(value = "weight")
@Schema(description = "商品重量,默认为克")
private BigDecimal weight;
/**
* 产品详情网页内容
*/
@TableField(value = "detail_html")
@Schema(description = "产品详情网页内容")
private String detailHtml;
/**
* 移动端网页详情
*/
@TableField(value = "detail_mobile_html")
@Schema(description = "移动端网页详情")
private String detailMobileHtml;
/**
* 品牌名称
*/
@TableField(value = "brand_name")
@Schema(description = "品牌名称")
@Size(max = 255, message = "品牌名称最大长度要小于 255")
private String brandName;
/**
* 商品分类名称
*/
@TableField(value = "product_category_name")
@Schema(description = "商品分类名称")
@Size(max = 255, message = "商品分类名称最大长度要小于 255")
private String productCategoryName;
/**
* 商品销售属性json格式
*/
@TableField(value = "product_attr")
@Schema(description = "商品销售属性json格式")
@Size(max = 500, message = "商品销售属性json格式最大长度要小于 500")
private String productAttr;
/**
* 租户id
*/
@TableField(value = "tenant_id")
@Schema(description = "租户id")
@Size(max = 255, message = "租户id最大长度要小于 255")
private String tenantId;
@Schema(description = "删除标志0代表存在1代表删除")
@TableLogic(value ="0", delval = "1")
@TableField(value ="del_flag",fill = FieldFill.INSERT, jdbcType = JdbcType.CHAR)
private String delFlag;
}

View File

@ -0,0 +1,82 @@
package com.wzj.soopin.goods.domain.entity;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.ibatis.type.JdbcType;
import java.math.BigDecimal;
/**
* sku信息
*/
@Schema(description="sku信息")
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "wzj_soopin.pms_sku_snapshot")
public class SkuSnapshot {
@TableId(value = "id")
private Long id;
/**
* skuid
*/
@TableField(value = "sku_id")
@Schema(description="skuid")
private Long skuId;
/**
* 商品快照id
*/
@TableField(value = "product_snapshot_id")
@Schema(description="商品快照id")
private Long productSnapshotId;
/**
* sku编码
*/
@TableField(value = "out_sku_id")
@Schema(description="sku编码")
@Size(max = 64,message = "sku编码最大长度要小于 64")
private String outSkuId;
@TableField(value = "price")
@Schema(description="")
@NotNull(message = "不能为null")
private BigDecimal price;
/**
* 展示图片
*/
@TableField(value = "pic")
@Schema(description="展示图片")
@Size(max = 255,message = "展示图片最大长度要小于 255")
private String pic;
/**
* 商品销售属性json格式
*/
@TableField(value = "sp_data")
@Schema(description="商品销售属性json格式")
@Size(max = 500,message = "商品销售属性json格式最大长度要小于 500")
private String spData;
/**
* 租户id
*/
@TableField(value = "tenant_id")
@Schema(description="租户id")
@Size(max = 255,message = "租户id最大长度要小于 255")
private String tenantId;
@Schema(description = "删除标志0代表存在1代表删除")
@TableLogic(value ="0", delval = "1")
@TableField(value ="del_flag",fill = FieldFill.INSERT, jdbcType = JdbcType.CHAR)
private String delFlag;
}

View File

@ -0,0 +1,20 @@
package com.wzj.soopin.goods.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 商品审核状态 1.待审核;->2.审核通过;3->审核驳回
*
*/
@Getter
@AllArgsConstructor
public enum ProductAuthFlag {
WAIT_AUDIT(1, "待审核"),
AUDIT_PASS(2, "审核通过"),
AUDIT_REJECT(3, "审核驳回");
private final Integer code;
private final String message;
}

View File

@ -0,0 +1,9 @@
package com.wzj.soopin.goods.mapper;
import com.wzj.soopin.goods.domain.entity.ProductSnapshot;
import org.apache.ibatis.annotations.Mapper;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
@Mapper
public interface ProductSnapshotMapper extends BaseMapperPlus<ProductSnapshot, ProductSnapshot> {
}

View File

@ -1,17 +1,15 @@
package com.wzj.soopin.goods.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.wzj.soopin.goods.domain.bo.SkuBo;
import com.wzj.soopin.goods.domain.entity.Sku;
import com.wzj.soopin.goods.domain.vo.SkuVO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import java.time.LocalDateTime;
import java.util.List;
/**
* sku信息Mapper接口
@ -19,7 +17,7 @@ import java.util.List;
* @author zcc
*/
@Mapper
public interface SkuMapper extends BaseMapper<Sku> {
public interface SkuMapper extends BaseMapperPlus<Sku, SkuVO> {
int updateStockById(@Param("skuId")Long skuId, @Param("optDate")LocalDateTime optDate, @Param("quantity")Integer quantity);
@ -28,11 +26,4 @@ public interface SkuMapper extends BaseMapper<Sku> {
SkuVO selectSkuByid(@Param("id") Long id);
@Insert("<script>" +
"INSERT INTO pms_sku (product_id, out_sku_id, price, pic, sp_data, stock) VALUES " +
"<foreach collection='list' item='item' separator=','>" +
"(#{item.productId}, #{item.outSkuId}, #{item.price}, #{item.pic}, #{item.spData}, #{item.stock})" +
"</foreach>" +
"</script>")
void batchInsert(List<Sku> skus);
}

View File

@ -0,0 +1,9 @@
package com.wzj.soopin.goods.mapper;
import com.wzj.soopin.goods.domain.entity.SkuSnapshot;
import org.apache.ibatis.annotations.Mapper;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
@Mapper
public interface SkuSnapshotMapper extends BaseMapperPlus<SkuSnapshot, SkuSnapshot> {
}

View File

@ -0,0 +1,8 @@
package com.wzj.soopin.goods.service;
import com.wzj.soopin.goods.domain.entity.ProductSnapshot;
import com.baomidou.mybatisplus.extension.service.IService;
public interface ProductSnapshotService extends IService<ProductSnapshot>{
}

View File

@ -10,4 +10,8 @@ public interface SkuService extends IService<Sku> {
void add(Sku sku);
List<Sku> getByProductId(Long productId);
boolean insertBatch(List<Sku> skus);
boolean updateBatchById(List<Sku> skus);
}

View File

@ -0,0 +1,12 @@
package com.wzj.soopin.goods.service;
import com.wzj.soopin.goods.domain.entity.SkuSnapshot;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
public interface SkuSnapshotService extends IService<SkuSnapshot>{
void insertBatch(List<SkuSnapshot> skuSnapshotList);
}

View File

@ -0,0 +1,13 @@
package com.wzj.soopin.goods.service.impl;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wzj.soopin.goods.mapper.ProductSnapshotMapper;
import com.wzj.soopin.goods.domain.entity.ProductSnapshot;
import com.wzj.soopin.goods.service.ProductSnapshotService;
@Service
public class ProductSnapshotServiceImpl extends ServiceImpl<ProductSnapshotMapper, ProductSnapshot> implements ProductSnapshotService{
}

View File

@ -56,4 +56,14 @@ public class SkuServiceImpl extends ServiceImpl<SkuMapper, Sku> implements SkuSe
public List<Sku> getByProductId(Long productId) {
return skuMapper.selectList(Wrappers.lambdaQuery(Sku.class).eq(Sku::getProductId, productId));
}
@Override
public boolean insertBatch(List<Sku> skus) {
return skuMapper.insertBatch(skus);
}
@Override
public boolean updateBatchById(List<Sku> skus) {
return skuMapper.updateBatchById(skus);
}
}

View File

@ -0,0 +1,17 @@
package com.wzj.soopin.goods.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wzj.soopin.goods.domain.entity.SkuSnapshot;
import com.wzj.soopin.goods.mapper.SkuSnapshotMapper;
import com.wzj.soopin.goods.service.SkuSnapshotService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class SkuSnapshotServiceImpl extends ServiceImpl<SkuSnapshotMapper, SkuSnapshot> implements SkuSnapshotService{
@Override
public void insertBatch(List<SkuSnapshot> skuSnapshotList) {
baseMapper.insertBatch(skuSnapshotList);
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wzj.soopin.goods.mapper.ProductSnapshotMapper">
</mapper>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wzj.soopin.goods.mapper.SkuSnapshotMapper">
</mapper>