32 changed files with 1533 additions and 31 deletions
@ -0,0 +1,3 @@ |
|||||
|
{ |
||||
|
"java.compile.nullAnalysis.mode": "automatic" |
||||
|
} |
||||
@ -0,0 +1,216 @@ |
|||||
|
package cc.hiver.mall.controller; |
||||
|
|
||||
|
import cc.hiver.core.common.utils.ResultUtil; |
||||
|
import cc.hiver.core.common.vo.Result; |
||||
|
import cc.hiver.mall.entity.SeckillGroupCategory; |
||||
|
import cc.hiver.mall.entity.SeckillGroupProduct; |
||||
|
import cc.hiver.mall.pojo.dto.SeckillGroupProductDTO; |
||||
|
import cc.hiver.mall.pojo.query.SeckillGroupProductQuery; |
||||
|
import cc.hiver.mall.pojo.vo.SeckillGroupProductVO; |
||||
|
import cc.hiver.mall.service.mybatis.SeckillGroupCategoryService; |
||||
|
import cc.hiver.mall.service.mybatis.SeckillGroupProductService; |
||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
||||
|
import com.baomidou.mybatisplus.core.metadata.IPage; |
||||
|
import io.swagger.annotations.Api; |
||||
|
import io.swagger.annotations.ApiOperation; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.apache.commons.lang3.StringUtils; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.transaction.annotation.Transactional; |
||||
|
import org.springframework.web.bind.annotation.*; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
@Slf4j |
||||
|
@RestController |
||||
|
@Api(tags = "后台管理系统秒杀团接口") |
||||
|
@RequestMapping("/hiver/mall/admin/seckillGroup") |
||||
|
@Transactional |
||||
|
public class AdminSeckillGroupController { |
||||
|
|
||||
|
@Autowired |
||||
|
private SeckillGroupCategoryService seckillGroupCategoryService; |
||||
|
|
||||
|
@Autowired |
||||
|
private SeckillGroupProductService seckillGroupProductService; |
||||
|
|
||||
|
@RequestMapping(value = "/category/save", method = RequestMethod.POST) |
||||
|
@ApiOperation(value = "新增秒杀团分类") |
||||
|
public Result saveCategory(@RequestBody SeckillGroupCategory category) { |
||||
|
if (category == null || StringUtils.isBlank(category.getCategoryName())) { |
||||
|
return ResultUtil.error("分类名称不能为空"); |
||||
|
} |
||||
|
if (StringUtils.isBlank(category.getRegionId())) { |
||||
|
return ResultUtil.error("区域ID不能为空"); |
||||
|
} |
||||
|
category.setStatus(category.getStatus() == null ? 1 : category.getStatus()); |
||||
|
category.setSort(category.getSort() == null ? 0 : category.getSort()); |
||||
|
boolean result = seckillGroupCategoryService.save(category); |
||||
|
if (result) { |
||||
|
seckillGroupCategoryService.refreshCategoryCache(category.getRegionId()); |
||||
|
return ResultUtil.success("添加成功"); |
||||
|
} |
||||
|
return ResultUtil.error("添加失败"); |
||||
|
} |
||||
|
|
||||
|
@RequestMapping(value = "/category/edit", method = RequestMethod.POST) |
||||
|
@ApiOperation(value = "修改秒杀团分类") |
||||
|
public Result editCategory(@RequestBody SeckillGroupCategory category) { |
||||
|
if (category == null || StringUtils.isBlank(category.getId())) { |
||||
|
return ResultUtil.error("分类ID不能为空"); |
||||
|
} |
||||
|
SeckillGroupCategory old = seckillGroupCategoryService.getById(category.getId()); |
||||
|
if (old == null) { |
||||
|
return ResultUtil.error("分类不存在"); |
||||
|
} |
||||
|
boolean result = seckillGroupCategoryService.updateById(category); |
||||
|
if (result) { |
||||
|
seckillGroupCategoryService.refreshCategoryCache(old.getRegionId()); |
||||
|
if (StringUtils.isNotBlank(category.getRegionId()) && !category.getRegionId().equals(old.getRegionId())) { |
||||
|
seckillGroupCategoryService.refreshCategoryCache(category.getRegionId()); |
||||
|
} |
||||
|
return ResultUtil.success("修改成功"); |
||||
|
} |
||||
|
return ResultUtil.error("修改失败"); |
||||
|
} |
||||
|
|
||||
|
@RequestMapping(value = "/category/delete", method = RequestMethod.POST) |
||||
|
@ApiOperation(value = "删除秒杀团分类") |
||||
|
public Result deleteCategory(String id) { |
||||
|
if (StringUtils.isBlank(id)) { |
||||
|
return ResultUtil.error("分类ID不能为空"); |
||||
|
} |
||||
|
SeckillGroupCategory category = seckillGroupCategoryService.getById(id); |
||||
|
if (category == null) { |
||||
|
return ResultUtil.error("分类不存在"); |
||||
|
} |
||||
|
category.setDelFlag(1); |
||||
|
boolean result = seckillGroupCategoryService.updateById(category); |
||||
|
if (result) { |
||||
|
seckillGroupCategoryService.removeCategoryCache(category.getRegionId(), id); |
||||
|
seckillGroupCategoryService.refreshCategoryCache(category.getRegionId()); |
||||
|
seckillGroupProductService.removeProductCache(id); |
||||
|
return ResultUtil.success("删除成功"); |
||||
|
} |
||||
|
return ResultUtil.error("删除失败"); |
||||
|
} |
||||
|
|
||||
|
@RequestMapping(value = "/category/list", method = RequestMethod.GET) |
||||
|
@ApiOperation(value = "查询秒杀团分类列表") |
||||
|
public Result<List<SeckillGroupCategory>> listCategory(@RequestParam(value = "regionId") String regionId) { |
||||
|
LambdaQueryWrapper<SeckillGroupCategory> wrapper = new LambdaQueryWrapper<>(); |
||||
|
wrapper.eq(SeckillGroupCategory::getDelFlag, 0); |
||||
|
if (StringUtils.isNotBlank(regionId)) { |
||||
|
wrapper.eq(SeckillGroupCategory::getRegionId, regionId); |
||||
|
} |
||||
|
wrapper.orderByDesc(SeckillGroupCategory::getSort) |
||||
|
.orderByAsc(SeckillGroupCategory::getCreateTime); |
||||
|
return new ResultUtil<List<SeckillGroupCategory>>().setData(seckillGroupCategoryService.list(wrapper)); |
||||
|
} |
||||
|
|
||||
|
@RequestMapping(value = "/category/sort", method = RequestMethod.POST) |
||||
|
@ApiOperation(value = "批量调整秒杀团分类排序") |
||||
|
public Result sortCategory(@RequestBody List<SeckillGroupCategory> categories) { |
||||
|
if (categories == null || categories.isEmpty()) { |
||||
|
return ResultUtil.error("分类列表不能为空"); |
||||
|
} |
||||
|
boolean result = seckillGroupCategoryService.updateBatchById(categories); |
||||
|
if (result) { |
||||
|
for (SeckillGroupCategory category : categories) { |
||||
|
if (StringUtils.isNotBlank(category.getRegionId())) { |
||||
|
seckillGroupCategoryService.refreshCategoryCache(category.getRegionId()); |
||||
|
} else if (StringUtils.isNotBlank(category.getId())) { |
||||
|
SeckillGroupCategory old = seckillGroupCategoryService.getById(category.getId()); |
||||
|
if (old != null) { |
||||
|
seckillGroupCategoryService.refreshCategoryCache(old.getRegionId()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return ResultUtil.success("排序成功"); |
||||
|
} |
||||
|
return ResultUtil.error("排序失败"); |
||||
|
} |
||||
|
|
||||
|
@RequestMapping(value = "/product/save", method = RequestMethod.POST) |
||||
|
@ApiOperation(value = "新增秒杀团商品") |
||||
|
public Result<SeckillGroupProduct> saveProduct(@RequestBody SeckillGroupProductDTO dto) { |
||||
|
try { |
||||
|
return new ResultUtil<SeckillGroupProduct>().setData(seckillGroupProductService.saveFromProduct(dto)); |
||||
|
} catch (IllegalArgumentException e) { |
||||
|
return ResultUtil.error(e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@RequestMapping(value = "/product/edit", method = RequestMethod.POST) |
||||
|
@ApiOperation(value = "修改秒杀团商品") |
||||
|
public Result<SeckillGroupProduct> editProduct(@RequestBody SeckillGroupProductDTO dto) { |
||||
|
try { |
||||
|
return new ResultUtil<SeckillGroupProduct>().setData(seckillGroupProductService.editSeckillProduct(dto)); |
||||
|
} catch (IllegalArgumentException e) { |
||||
|
return ResultUtil.error(e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@RequestMapping(value = "/product/page", method = RequestMethod.POST) |
||||
|
@ApiOperation(value = "分页查询秒杀团商品") |
||||
|
public Result<IPage<SeckillGroupProductVO>> pageProduct(@RequestBody SeckillGroupProductQuery query) { |
||||
|
if (query == null) { |
||||
|
query = new SeckillGroupProductQuery(); |
||||
|
} |
||||
|
return new ResultUtil<IPage<SeckillGroupProductVO>>().setData(seckillGroupProductService.pageProducts(query)); |
||||
|
} |
||||
|
|
||||
|
@RequestMapping(value = "/product/up", method = RequestMethod.POST) |
||||
|
@ApiOperation(value = "上架秒杀团商品") |
||||
|
public Result upProduct(String id) { |
||||
|
return updateProductStatus(id, 1, "上架"); |
||||
|
} |
||||
|
|
||||
|
@RequestMapping(value = "/product/down", method = RequestMethod.POST) |
||||
|
@ApiOperation(value = "下架秒杀团商品") |
||||
|
public Result downProduct(String id) { |
||||
|
return updateProductStatus(id, 0, "下架"); |
||||
|
} |
||||
|
|
||||
|
@RequestMapping(value = "/product/delete", method = RequestMethod.POST) |
||||
|
@ApiOperation(value = "删除秒杀团商品") |
||||
|
public Result deleteProduct(String id) { |
||||
|
if (StringUtils.isBlank(id)) { |
||||
|
return ResultUtil.error("商品ID不能为空"); |
||||
|
} |
||||
|
SeckillGroupProduct product = seckillGroupProductService.getById(id); |
||||
|
if (product == null) { |
||||
|
return ResultUtil.error("商品不存在"); |
||||
|
} |
||||
|
product.setDelFlag(1); |
||||
|
boolean result = seckillGroupProductService.updateById(product); |
||||
|
if (result) { |
||||
|
seckillGroupProductService.refreshProductCache(product.getCategoryId()); |
||||
|
seckillGroupProductService.removeProductDetailCache(id); |
||||
|
return ResultUtil.success("删除成功"); |
||||
|
} |
||||
|
return ResultUtil.error("删除失败"); |
||||
|
} |
||||
|
|
||||
|
private Result updateProductStatus(String id, Integer status, String actionName) { |
||||
|
if (StringUtils.isBlank(id)) { |
||||
|
return ResultUtil.error("商品ID不能为空"); |
||||
|
} |
||||
|
SeckillGroupProduct product = seckillGroupProductService.getById(id); |
||||
|
if (product == null) { |
||||
|
return ResultUtil.error("商品不存在"); |
||||
|
} |
||||
|
product.setStatus(status); |
||||
|
boolean result = seckillGroupProductService.updateById(product); |
||||
|
if (result) { |
||||
|
if (status == 1) { |
||||
|
seckillGroupProductService.refreshProductCacheByProductId(id); |
||||
|
} else { |
||||
|
seckillGroupProductService.refreshProductCache(product.getCategoryId()); |
||||
|
seckillGroupProductService.removeProductDetailCache(id); |
||||
|
} |
||||
|
return ResultUtil.success(actionName + "成功"); |
||||
|
} |
||||
|
return ResultUtil.error(actionName + "失败"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
package cc.hiver.mall.controller; |
||||
|
|
||||
|
import cc.hiver.core.common.annotation.RateLimiter; |
||||
|
import cc.hiver.core.common.utils.ResultUtil; |
||||
|
import cc.hiver.core.common.vo.Result; |
||||
|
import cc.hiver.mall.entity.SeckillGroupCategory; |
||||
|
import cc.hiver.mall.pojo.query.SeckillGroupProductQuery; |
||||
|
import cc.hiver.mall.pojo.vo.SeckillGroupProductVO; |
||||
|
import cc.hiver.mall.service.mybatis.SeckillGroupCategoryService; |
||||
|
import cc.hiver.mall.service.mybatis.SeckillGroupProductService; |
||||
|
import com.baomidou.mybatisplus.core.metadata.IPage; |
||||
|
import io.swagger.annotations.Api; |
||||
|
import io.swagger.annotations.ApiOperation; |
||||
|
import org.apache.commons.lang3.StringUtils; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.web.bind.annotation.*; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
@RestController |
||||
|
@Api(tags = "秒杀团接口") |
||||
|
@RequestMapping("/hiver/app/seckillGroup") |
||||
|
public class SeckillGroupController { |
||||
|
|
||||
|
@Autowired |
||||
|
private SeckillGroupCategoryService seckillGroupCategoryService; |
||||
|
|
||||
|
@Autowired |
||||
|
private SeckillGroupProductService seckillGroupProductService; |
||||
|
|
||||
|
@RequestMapping(value = "/category/list", method = RequestMethod.GET) |
||||
|
@ApiOperation(value = "查询秒杀团横排分类") |
||||
|
public Result<List<SeckillGroupCategory>> listCategory(String regionId) { |
||||
|
if (StringUtils.isBlank(regionId)) { |
||||
|
return ResultUtil.error("区域ID不能为空"); |
||||
|
} |
||||
|
return new ResultUtil<List<SeckillGroupCategory>>().setData(seckillGroupCategoryService.listFromCache(regionId)); |
||||
|
} |
||||
|
|
||||
|
@RequestMapping(value = "/product/page", method = RequestMethod.POST) |
||||
|
@ApiOperation(value = "分页查询秒杀团商品") |
||||
|
@RateLimiter(name = "seckillGroupProductPage", ipLimit = true) |
||||
|
public Result<IPage<SeckillGroupProductVO>> pageProduct(@RequestBody SeckillGroupProductQuery query) { |
||||
|
if (query == null) { |
||||
|
query = new SeckillGroupProductQuery(); |
||||
|
} |
||||
|
if (StringUtils.isBlank(query.getRegionId())) { |
||||
|
return ResultUtil.error("区域ID不能为空"); |
||||
|
} |
||||
|
query.setOnlyActive(true); |
||||
|
return new ResultUtil<IPage<SeckillGroupProductVO>>().setData(seckillGroupProductService.appPageProducts(query)); |
||||
|
} |
||||
|
|
||||
|
@RequestMapping(value = "/product/detail/{id}", method = RequestMethod.GET) |
||||
|
@ApiOperation(value = "查询秒杀团商品详情") |
||||
|
public Result<SeckillGroupProductVO> detail(@PathVariable("id") String id) { |
||||
|
if (StringUtils.isBlank(id)) { |
||||
|
return ResultUtil.error("商品ID不能为空"); |
||||
|
} |
||||
|
SeckillGroupProductVO detail = seckillGroupProductService.getDetail(id); |
||||
|
if (detail == null) { |
||||
|
return ResultUtil.error("商品不存在"); |
||||
|
} |
||||
|
return new ResultUtil<SeckillGroupProductVO>().setData(detail); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
package cc.hiver.mall.dao.mapper; |
||||
|
|
||||
|
import cc.hiver.mall.entity.SeckillGroupCategory; |
||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
||||
|
import org.springframework.stereotype.Repository; |
||||
|
|
||||
|
@Repository |
||||
|
public interface SeckillGroupCategoryMapper extends BaseMapper<SeckillGroupCategory> { |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
package cc.hiver.mall.dao.mapper; |
||||
|
|
||||
|
import cc.hiver.mall.entity.SeckillGroupProduct; |
||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
||||
|
import org.springframework.stereotype.Repository; |
||||
|
|
||||
|
@Repository |
||||
|
public interface SeckillGroupProductMapper extends BaseMapper<SeckillGroupProduct> { |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
package cc.hiver.mall.entity; |
||||
|
|
||||
|
import cc.hiver.core.base.HiverBaseEntity; |
||||
|
import com.baomidou.mybatisplus.annotation.TableName; |
||||
|
import io.swagger.annotations.ApiModel; |
||||
|
import io.swagger.annotations.ApiModelProperty; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
@Data |
||||
|
@ApiModel(value = "秒杀团横排分类") |
||||
|
@TableName(value = "t_seckill_group_category", autoResultMap = true) |
||||
|
public class SeckillGroupCategory extends HiverBaseEntity { |
||||
|
private static final long serialVersionUID = 1L; |
||||
|
|
||||
|
@ApiModelProperty(value = "分类名称") |
||||
|
private String categoryName; |
||||
|
|
||||
|
@ApiModelProperty(value = "分类图标") |
||||
|
private String categoryIcon; |
||||
|
|
||||
|
@ApiModelProperty(value = "区域ID") |
||||
|
private String regionId; |
||||
|
|
||||
|
@ApiModelProperty(value = "排序值,越大越靠前") |
||||
|
private Integer sort; |
||||
|
|
||||
|
@ApiModelProperty(value = "状态:0禁用 1启用") |
||||
|
private Integer status; |
||||
|
|
||||
|
@ApiModelProperty(value = "备注") |
||||
|
private String remark; |
||||
|
} |
||||
@ -0,0 +1,95 @@ |
|||||
|
package cc.hiver.mall.entity; |
||||
|
|
||||
|
import cc.hiver.core.base.HiverBaseEntity; |
||||
|
import com.baomidou.mybatisplus.annotation.TableName; |
||||
|
import com.fasterxml.jackson.annotation.JsonFormat; |
||||
|
import io.swagger.annotations.ApiModel; |
||||
|
import io.swagger.annotations.ApiModelProperty; |
||||
|
import lombok.Data; |
||||
|
import org.springframework.format.annotation.DateTimeFormat; |
||||
|
|
||||
|
import java.math.BigDecimal; |
||||
|
import java.util.Date; |
||||
|
|
||||
|
@Data |
||||
|
@ApiModel(value = "秒杀团商品") |
||||
|
@TableName(value = "t_seckill_group_product", autoResultMap = true) |
||||
|
public class SeckillGroupProduct extends HiverBaseEntity { |
||||
|
private static final long serialVersionUID = 1L; |
||||
|
|
||||
|
@ApiModelProperty(value = "秒杀分类ID") |
||||
|
private String categoryId; |
||||
|
|
||||
|
@ApiModelProperty(value = "原商品ID") |
||||
|
private String productId; |
||||
|
|
||||
|
@ApiModelProperty(value = "商品名称快照") |
||||
|
private String productName; |
||||
|
|
||||
|
@ApiModelProperty(value = "商品图片快照") |
||||
|
private String productPicture; |
||||
|
|
||||
|
@ApiModelProperty(value = "单位快照") |
||||
|
private String unit; |
||||
|
|
||||
|
@ApiModelProperty(value = "商品属性列表/规格快照") |
||||
|
private String attributeList; |
||||
|
|
||||
|
@ApiModelProperty(value = "商品简介快照") |
||||
|
private String productIntro; |
||||
|
|
||||
|
@ApiModelProperty(value = "原价/划线价快照") |
||||
|
private BigDecimal originalPrice; |
||||
|
|
||||
|
@ApiModelProperty(value = "秒杀价格") |
||||
|
private BigDecimal seckillPrice; |
||||
|
|
||||
|
@ApiModelProperty(value = "秒杀总库存") |
||||
|
private Integer totalStock; |
||||
|
|
||||
|
@ApiModelProperty(value = "已售库存") |
||||
|
private Integer soldStock; |
||||
|
|
||||
|
@ApiModelProperty(value = "锁定库存") |
||||
|
private Integer lockStock; |
||||
|
|
||||
|
@ApiModelProperty(value = "每人限购数量,空表示不限") |
||||
|
private Integer limitNum; |
||||
|
|
||||
|
@ApiModelProperty(value = "商家ID") |
||||
|
private String shopId; |
||||
|
|
||||
|
@ApiModelProperty(value = "商家名称快照") |
||||
|
private String shopName; |
||||
|
|
||||
|
@ApiModelProperty(value = "商家电话快照") |
||||
|
private String shopPhone; |
||||
|
|
||||
|
@ApiModelProperty(value = "商家地址快照") |
||||
|
private String shopAddress; |
||||
|
|
||||
|
@ApiModelProperty(value = "取货区域ID,便于下单传参") |
||||
|
private String getAreaId; |
||||
|
|
||||
|
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") |
||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
||||
|
@ApiModelProperty(value = "秒杀开始时间") |
||||
|
private Date startTime; |
||||
|
|
||||
|
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") |
||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
||||
|
@ApiModelProperty(value = "秒杀结束时间") |
||||
|
private Date endTime; |
||||
|
|
||||
|
@ApiModelProperty(value = "人工排序值,越大越靠前") |
||||
|
private Integer orderFiled; |
||||
|
|
||||
|
@ApiModelProperty(value = "稳定随机排序值") |
||||
|
private Long sortHash; |
||||
|
|
||||
|
@ApiModelProperty(value = "状态:0下架 1上架") |
||||
|
private Integer status; |
||||
|
|
||||
|
@ApiModelProperty(value = "备注") |
||||
|
private String remark; |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
package cc.hiver.mall.pojo.dto; |
||||
|
|
||||
|
import com.fasterxml.jackson.annotation.JsonFormat; |
||||
|
import io.swagger.annotations.ApiModel; |
||||
|
import io.swagger.annotations.ApiModelProperty; |
||||
|
import lombok.Data; |
||||
|
import org.springframework.format.annotation.DateTimeFormat; |
||||
|
|
||||
|
import java.math.BigDecimal; |
||||
|
import java.util.Date; |
||||
|
|
||||
|
@Data |
||||
|
@ApiModel("秒杀团商品保存对象") |
||||
|
public class SeckillGroupProductDTO { |
||||
|
|
||||
|
@ApiModelProperty(value = "秒杀团商品ID,编辑时必传") |
||||
|
private String id; |
||||
|
|
||||
|
@ApiModelProperty(value = "秒杀分类ID", required = true) |
||||
|
private String categoryId; |
||||
|
|
||||
|
@ApiModelProperty(value = "原商品ID", required = true) |
||||
|
private String productId; |
||||
|
|
||||
|
@ApiModelProperty(value = "秒杀价格", required = true) |
||||
|
private BigDecimal seckillPrice; |
||||
|
|
||||
|
@ApiModelProperty(value = "秒杀库存", required = true) |
||||
|
private Integer totalStock; |
||||
|
|
||||
|
@ApiModelProperty(value = "每人限购数量") |
||||
|
private Integer limitNum; |
||||
|
|
||||
|
@ApiModelProperty(value = "取货区域ID,不传则使用商家所属区域ID") |
||||
|
private String getAreaId; |
||||
|
|
||||
|
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") |
||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
||||
|
@ApiModelProperty(value = "秒杀开始时间") |
||||
|
private Date startTime; |
||||
|
|
||||
|
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") |
||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
||||
|
@ApiModelProperty(value = "秒杀结束时间") |
||||
|
private Date endTime; |
||||
|
|
||||
|
@ApiModelProperty(value = "人工排序值,越大越靠前") |
||||
|
private Integer orderFiled; |
||||
|
|
||||
|
@ApiModelProperty(value = "状态:0下架 1上架") |
||||
|
private Integer status; |
||||
|
|
||||
|
@ApiModelProperty(value = "备注") |
||||
|
private String remark; |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
package cc.hiver.mall.pojo.query; |
||||
|
|
||||
|
import cc.hiver.core.base.HiverBasePageQuery; |
||||
|
import io.swagger.annotations.ApiModel; |
||||
|
import io.swagger.annotations.ApiModelProperty; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
@Data |
||||
|
@ApiModel("秒杀团商品分页查询对象") |
||||
|
public class SeckillGroupProductQuery extends HiverBasePageQuery { |
||||
|
|
||||
|
@ApiModelProperty(value = "秒杀分类ID") |
||||
|
private String categoryId; |
||||
|
|
||||
|
@ApiModelProperty(value = "区域ID") |
||||
|
private String regionId; |
||||
|
|
||||
|
@ApiModelProperty(value = "商家ID") |
||||
|
private String shopId; |
||||
|
|
||||
|
@ApiModelProperty(value = "关键字") |
||||
|
private String keywords; |
||||
|
|
||||
|
@ApiModelProperty(value = "商品名称") |
||||
|
private String productName; |
||||
|
|
||||
|
@ApiModelProperty(value = "状态:0下架 1上架") |
||||
|
private Integer status; |
||||
|
|
||||
|
@ApiModelProperty(value = "是否仅查询当前有效活动商品") |
||||
|
private Boolean onlyActive; |
||||
|
} |
||||
@ -0,0 +1,89 @@ |
|||||
|
package cc.hiver.mall.pojo.vo; |
||||
|
|
||||
|
import com.fasterxml.jackson.annotation.JsonFormat; |
||||
|
import io.swagger.annotations.ApiModelProperty; |
||||
|
import lombok.Data; |
||||
|
import org.springframework.format.annotation.DateTimeFormat; |
||||
|
|
||||
|
import java.math.BigDecimal; |
||||
|
import java.util.Date; |
||||
|
|
||||
|
@Data |
||||
|
public class SeckillGroupProductVO { |
||||
|
|
||||
|
@ApiModelProperty(value = "秒杀团商品ID") |
||||
|
private String id; |
||||
|
|
||||
|
@ApiModelProperty(value = "秒杀分类ID") |
||||
|
private String categoryId; |
||||
|
|
||||
|
@ApiModelProperty(value = "原商品ID") |
||||
|
private String productId; |
||||
|
|
||||
|
@ApiModelProperty(value = "商品名称") |
||||
|
private String productName; |
||||
|
|
||||
|
@ApiModelProperty(value = "商品图片") |
||||
|
private String productPicture; |
||||
|
|
||||
|
@ApiModelProperty(value = "单位") |
||||
|
private String unit; |
||||
|
|
||||
|
@ApiModelProperty(value = "商品属性列表/规格") |
||||
|
private String attributeList; |
||||
|
|
||||
|
@ApiModelProperty(value = "商品简介") |
||||
|
private String productIntro; |
||||
|
|
||||
|
@ApiModelProperty(value = "原价/划线价") |
||||
|
private BigDecimal originalPrice; |
||||
|
|
||||
|
@ApiModelProperty(value = "秒杀价格") |
||||
|
private BigDecimal seckillPrice; |
||||
|
|
||||
|
@ApiModelProperty(value = "秒杀总库存") |
||||
|
private Integer totalStock; |
||||
|
|
||||
|
@ApiModelProperty(value = "已售库存") |
||||
|
private Integer soldStock; |
||||
|
|
||||
|
@ApiModelProperty(value = "锁定库存") |
||||
|
private Integer lockStock; |
||||
|
|
||||
|
@ApiModelProperty(value = "可售库存") |
||||
|
private Integer availableStock; |
||||
|
|
||||
|
@ApiModelProperty(value = "每人限购数量") |
||||
|
private Integer limitNum; |
||||
|
|
||||
|
@ApiModelProperty(value = "商家ID") |
||||
|
private String shopId; |
||||
|
|
||||
|
@ApiModelProperty(value = "商家名称") |
||||
|
private String shopName; |
||||
|
|
||||
|
@ApiModelProperty(value = "商家电话") |
||||
|
private String shopPhone; |
||||
|
|
||||
|
@ApiModelProperty(value = "商家地址") |
||||
|
private String shopAddress; |
||||
|
|
||||
|
@ApiModelProperty(value = "取货区域ID") |
||||
|
private String getAreaId; |
||||
|
|
||||
|
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") |
||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
||||
|
@ApiModelProperty(value = "秒杀开始时间") |
||||
|
private Date startTime; |
||||
|
|
||||
|
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") |
||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
||||
|
@ApiModelProperty(value = "秒杀结束时间") |
||||
|
private Date endTime; |
||||
|
|
||||
|
@ApiModelProperty(value = "人工排序值") |
||||
|
private Integer orderFiled; |
||||
|
|
||||
|
@ApiModelProperty(value = "状态:0下架 1上架") |
||||
|
private Integer status; |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
package cc.hiver.mall.service.mybatis; |
||||
|
|
||||
|
import cc.hiver.mall.entity.SeckillGroupCategory; |
||||
|
import com.baomidou.mybatisplus.extension.service.IService; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
public interface SeckillGroupCategoryService extends IService<SeckillGroupCategory> { |
||||
|
|
||||
|
List<SeckillGroupCategory> listForApp(String regionId); |
||||
|
|
||||
|
List<SeckillGroupCategory> listFromCache(String regionId); |
||||
|
|
||||
|
void refreshCategoryCache(String regionId); |
||||
|
|
||||
|
void removeCategoryCache(String regionId, String categoryId); |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
package cc.hiver.mall.service.mybatis; |
||||
|
|
||||
|
import cc.hiver.mall.entity.SeckillGroupProduct; |
||||
|
import cc.hiver.mall.pojo.dto.SeckillGroupProductDTO; |
||||
|
import cc.hiver.mall.pojo.query.SeckillGroupProductQuery; |
||||
|
import cc.hiver.mall.pojo.vo.SeckillGroupProductVO; |
||||
|
import com.baomidou.mybatisplus.core.metadata.IPage; |
||||
|
import com.baomidou.mybatisplus.extension.service.IService; |
||||
|
|
||||
|
public interface SeckillGroupProductService extends IService<SeckillGroupProduct> { |
||||
|
|
||||
|
SeckillGroupProduct saveFromProduct(SeckillGroupProductDTO dto); |
||||
|
|
||||
|
SeckillGroupProduct editSeckillProduct(SeckillGroupProductDTO dto); |
||||
|
|
||||
|
IPage<SeckillGroupProductVO> pageProducts(SeckillGroupProductQuery query); |
||||
|
|
||||
|
IPage<SeckillGroupProductVO> appPageProducts(SeckillGroupProductQuery query); |
||||
|
|
||||
|
SeckillGroupProductVO getDetail(String id); |
||||
|
|
||||
|
void refreshProductCache(String categoryId); |
||||
|
|
||||
|
void refreshProductCacheByProductId(String id); |
||||
|
|
||||
|
void removeProductCache(String categoryId); |
||||
|
|
||||
|
void removeProductDetailCache(String id); |
||||
|
} |
||||
@ -0,0 +1,97 @@ |
|||||
|
package cc.hiver.mall.serviceimpl.mybatis; |
||||
|
|
||||
|
import cc.hiver.core.common.redis.RedisTemplateHelper; |
||||
|
import cc.hiver.mall.dao.mapper.SeckillGroupCategoryMapper; |
||||
|
import cc.hiver.mall.entity.SeckillGroupCategory; |
||||
|
import cc.hiver.mall.service.mybatis.SeckillGroupCategoryService; |
||||
|
import cn.hutool.core.text.CharSequenceUtil; |
||||
|
import cn.hutool.json.JSONUtil; |
||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
@Service |
||||
|
public class SeckillGroupCategoryServiceImpl extends ServiceImpl<SeckillGroupCategoryMapper, SeckillGroupCategory> implements SeckillGroupCategoryService { |
||||
|
|
||||
|
private static final String CATEGORY_CACHE_KEY = "SECKILL_GROUP_CATEGORY_CACHE"; |
||||
|
|
||||
|
@Autowired |
||||
|
private RedisTemplateHelper redisTemplateHelper; |
||||
|
|
||||
|
@Override |
||||
|
public List<SeckillGroupCategory> listForApp(String regionId) { |
||||
|
LambdaQueryWrapper<SeckillGroupCategory> wrapper = new LambdaQueryWrapper<>(); |
||||
|
wrapper.eq(SeckillGroupCategory::getDelFlag, 0) |
||||
|
.eq(SeckillGroupCategory::getStatus, 1) |
||||
|
.eq(SeckillGroupCategory::getRegionId, regionId) |
||||
|
.orderByDesc(SeckillGroupCategory::getSort) |
||||
|
.orderByAsc(SeckillGroupCategory::getCreateTime); |
||||
|
return list(wrapper); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<SeckillGroupCategory> listFromCache(String regionId) { |
||||
|
if (CharSequenceUtil.isBlank(regionId)) { |
||||
|
return new ArrayList<>(); |
||||
|
} |
||||
|
List<Object> values = redisTemplateHelper.hValues(categoryCacheKey(regionId)); |
||||
|
if (values != null && !values.isEmpty()) { |
||||
|
List<SeckillGroupCategory> categories = new ArrayList<>(); |
||||
|
for (Object value : values) { |
||||
|
categories.add(JSONUtil.toBean(value.toString(), SeckillGroupCategory.class)); |
||||
|
} |
||||
|
sortCategories(categories); |
||||
|
return categories; |
||||
|
} |
||||
|
List<SeckillGroupCategory> categories = listForApp(regionId); |
||||
|
refreshCategoryCache(regionId, categories); |
||||
|
return categories; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void refreshCategoryCache(String regionId) { |
||||
|
if (CharSequenceUtil.isBlank(regionId)) { |
||||
|
return; |
||||
|
} |
||||
|
refreshCategoryCache(regionId, listForApp(regionId)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void removeCategoryCache(String regionId, String categoryId) { |
||||
|
if (CharSequenceUtil.isBlank(regionId) || CharSequenceUtil.isBlank(categoryId)) { |
||||
|
return; |
||||
|
} |
||||
|
redisTemplateHelper.hDelete(categoryCacheKey(regionId), categoryId); |
||||
|
} |
||||
|
|
||||
|
private void refreshCategoryCache(String regionId, List<SeckillGroupCategory> categories) { |
||||
|
String key = categoryCacheKey(regionId); |
||||
|
redisTemplateHelper.delete(key); |
||||
|
if (categories == null || categories.isEmpty()) { |
||||
|
return; |
||||
|
} |
||||
|
for (SeckillGroupCategory category : categories) { |
||||
|
redisTemplateHelper.hPut(key, category.getId(), JSONUtil.toJsonStr(category)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private String categoryCacheKey(String regionId) { |
||||
|
return CATEGORY_CACHE_KEY + ":" + regionId; |
||||
|
} |
||||
|
|
||||
|
private void sortCategories(List<SeckillGroupCategory> categories) { |
||||
|
categories.sort((o1, o2) -> { |
||||
|
Integer sort1 = o1.getSort() == null ? 0 : o1.getSort(); |
||||
|
Integer sort2 = o2.getSort() == null ? 0 : o2.getSort(); |
||||
|
int sortCompare = sort2.compareTo(sort1); |
||||
|
if (sortCompare != 0) { |
||||
|
return sortCompare; |
||||
|
} |
||||
|
return o1.getId().compareTo(o2.getId()); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,509 @@ |
|||||
|
package cc.hiver.mall.serviceimpl.mybatis; |
||||
|
|
||||
|
import cc.hiver.core.common.redis.RedisTemplateHelper; |
||||
|
import cc.hiver.mall.dao.mapper.SeckillGroupProductMapper; |
||||
|
import cc.hiver.mall.entity.Product; |
||||
|
import cc.hiver.mall.entity.SeckillGroupCategory; |
||||
|
import cc.hiver.mall.entity.SeckillGroupProduct; |
||||
|
import cc.hiver.mall.entity.Shop; |
||||
|
import cc.hiver.mall.pojo.dto.SeckillGroupProductDTO; |
||||
|
import cc.hiver.mall.pojo.query.SeckillGroupProductQuery; |
||||
|
import cc.hiver.mall.pojo.vo.SeckillGroupProductVO; |
||||
|
import cc.hiver.mall.service.ShopService; |
||||
|
import cc.hiver.mall.service.mybatis.ProductService; |
||||
|
import cc.hiver.mall.service.mybatis.SeckillGroupCategoryService; |
||||
|
import cc.hiver.mall.service.mybatis.SeckillGroupProductService; |
||||
|
import cn.hutool.core.text.CharSequenceUtil; |
||||
|
import cn.hutool.json.JSONArray; |
||||
|
import cn.hutool.json.JSONObject; |
||||
|
import cn.hutool.json.JSONUtil; |
||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
||||
|
import com.baomidou.mybatisplus.core.metadata.IPage; |
||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.springframework.transaction.annotation.Transactional; |
||||
|
|
||||
|
import java.nio.charset.StandardCharsets; |
||||
|
import java.time.LocalDateTime; |
||||
|
import java.time.format.DateTimeFormatter; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Date; |
||||
|
import java.util.List; |
||||
|
import java.util.zip.CRC32; |
||||
|
|
||||
|
@Service |
||||
|
public class SeckillGroupProductServiceImpl extends ServiceImpl<SeckillGroupProductMapper, SeckillGroupProduct> implements SeckillGroupProductService { |
||||
|
|
||||
|
private static final String PRODUCT_LIST_CACHE_PREFIX = "SECKILL_GROUP_PRODUCTS:"; |
||||
|
private static final String PRODUCT_REGION_LIST_CACHE_PREFIX = "SECKILL_GROUP_PRODUCTS_REGION:"; |
||||
|
private static final String PRODUCT_DETAIL_CACHE_PREFIX = "SECKILL_GROUP_PRODUCT:"; |
||||
|
private static final DateTimeFormatter SEED_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHH"); |
||||
|
|
||||
|
@Autowired |
||||
|
private ProductService productService; |
||||
|
|
||||
|
@Autowired |
||||
|
private ShopService shopService; |
||||
|
|
||||
|
@Autowired |
||||
|
private SeckillGroupCategoryService seckillGroupCategoryService; |
||||
|
|
||||
|
@Autowired |
||||
|
private RedisTemplateHelper redisTemplateHelper; |
||||
|
|
||||
|
@Transactional(rollbackFor = Exception.class) |
||||
|
@Override |
||||
|
public SeckillGroupProduct saveFromProduct(SeckillGroupProductDTO dto) { |
||||
|
validateSaveDto(dto, false); |
||||
|
Product product = productService.getById(dto.getProductId()); |
||||
|
if (product == null) { |
||||
|
throw new IllegalArgumentException("原商品不存在"); |
||||
|
} |
||||
|
Shop shop = shopService.get(product.getShopId()); |
||||
|
|
||||
|
SeckillGroupProduct seckillProduct = new SeckillGroupProduct(); |
||||
|
fillSnapshot(seckillProduct, dto, product, shop); |
||||
|
seckillProduct.setSoldStock(0); |
||||
|
seckillProduct.setLockStock(0); |
||||
|
seckillProduct.setStatus(dto.getStatus() == null ? 0 : dto.getStatus()); |
||||
|
seckillProduct.setSortHash(stableHash(seckillProduct.getId())); |
||||
|
save(seckillProduct); |
||||
|
refreshProductCache(seckillProduct.getCategoryId()); |
||||
|
refreshProductDetailCache(seckillProduct); |
||||
|
return seckillProduct; |
||||
|
} |
||||
|
|
||||
|
@Transactional(rollbackFor = Exception.class) |
||||
|
@Override |
||||
|
public SeckillGroupProduct editSeckillProduct(SeckillGroupProductDTO dto) { |
||||
|
validateSaveDto(dto, true); |
||||
|
SeckillGroupProduct old = getById(dto.getId()); |
||||
|
if (old == null) { |
||||
|
throw new IllegalArgumentException("秒杀团商品不存在"); |
||||
|
} |
||||
|
|
||||
|
String productId = CharSequenceUtil.isBlank(dto.getProductId()) ? old.getProductId() : dto.getProductId(); |
||||
|
Product product = productService.getById(productId); |
||||
|
if (product == null) { |
||||
|
throw new IllegalArgumentException("原商品不存在"); |
||||
|
} |
||||
|
Shop shop = shopService.get(product.getShopId()); |
||||
|
|
||||
|
SeckillGroupProduct updated = new SeckillGroupProduct(); |
||||
|
updated.setId(old.getId()); |
||||
|
updated.setSoldStock(old.getSoldStock() == null ? 0 : old.getSoldStock()); |
||||
|
updated.setLockStock(old.getLockStock() == null ? 0 : old.getLockStock()); |
||||
|
fillSnapshot(updated, dto, product, shop); |
||||
|
updated.setStatus(dto.getStatus() == null ? old.getStatus() : dto.getStatus()); |
||||
|
updated.setSortHash(stableHash(updated.getId())); |
||||
|
|
||||
|
int usedStock = safeStock(updated.getSoldStock()) + safeStock(updated.getLockStock()); |
||||
|
if (updated.getTotalStock() != null && updated.getTotalStock() < usedStock) { |
||||
|
throw new IllegalArgumentException("秒杀库存不能小于已售和锁定库存"); |
||||
|
} |
||||
|
|
||||
|
updateById(updated); |
||||
|
refreshProductCache(old.getCategoryId()); |
||||
|
if (!old.getCategoryId().equals(updated.getCategoryId())) { |
||||
|
refreshProductCache(updated.getCategoryId()); |
||||
|
} |
||||
|
refreshProductDetailCache(updated); |
||||
|
return updated; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public IPage<SeckillGroupProductVO> pageProducts(SeckillGroupProductQuery query) { |
||||
|
Page<SeckillGroupProduct> page = new Page<>(query.getPageNum(), query.getPageSize()); |
||||
|
IPage<SeckillGroupProduct> result = page(page, buildQueryWrapper(query, false)); |
||||
|
return convertPage(result); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public IPage<SeckillGroupProductVO> appPageProducts(SeckillGroupProductQuery query) { |
||||
|
int pageNum = query.getPageNum() > 0 ? query.getPageNum() : 1; |
||||
|
int pageSize = query.getPageSize() > 0 ? query.getPageSize() : 10; |
||||
|
List<SeckillGroupProductVO> allProducts = filterProducts(getRegionAppListFromCache(query.getRegionId()), query); |
||||
|
applyQuerySort(allProducts, query); |
||||
|
|
||||
|
int fromIndex = (pageNum - 1) * pageSize; |
||||
|
int toIndex = Math.min(fromIndex + pageSize, allProducts.size()); |
||||
|
List<SeckillGroupProductVO> records = fromIndex >= allProducts.size() |
||||
|
? new ArrayList<>() |
||||
|
: allProducts.subList(fromIndex, toIndex); |
||||
|
|
||||
|
Page<SeckillGroupProductVO> page = new Page<>(pageNum, pageSize, allProducts.size()); |
||||
|
page.setRecords(records); |
||||
|
return page; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public SeckillGroupProductVO getDetail(String id) { |
||||
|
if (CharSequenceUtil.isBlank(id)) { |
||||
|
return null; |
||||
|
} |
||||
|
String cached = redisTemplateHelper.get(PRODUCT_DETAIL_CACHE_PREFIX + id); |
||||
|
if (CharSequenceUtil.isNotBlank(cached)) { |
||||
|
return JSONUtil.toBean(cached, SeckillGroupProductVO.class); |
||||
|
} |
||||
|
SeckillGroupProduct product = getById(id); |
||||
|
if (product == null || product.getDelFlag() != 0) { |
||||
|
return null; |
||||
|
} |
||||
|
SeckillGroupProductVO vo = toVo(product); |
||||
|
redisTemplateHelper.set(PRODUCT_DETAIL_CACHE_PREFIX + id, JSONUtil.toJsonStr(vo)); |
||||
|
return vo; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void refreshProductCache(String categoryId) { |
||||
|
if (CharSequenceUtil.isBlank(categoryId)) { |
||||
|
return; |
||||
|
} |
||||
|
List<SeckillGroupProductVO> products = toVoList(list(buildActiveCategoryWrapper(categoryId))); |
||||
|
sortForApp(products); |
||||
|
redisTemplateHelper.set(productListCacheKey(categoryId), JSONUtil.toJsonStr(products)); |
||||
|
removeRegionProductCacheByCategory(categoryId); |
||||
|
for (SeckillGroupProductVO product : products) { |
||||
|
redisTemplateHelper.set(PRODUCT_DETAIL_CACHE_PREFIX + product.getId(), JSONUtil.toJsonStr(product)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void refreshProductCacheByProductId(String id) { |
||||
|
SeckillGroupProduct product = getById(id); |
||||
|
if (product != null) { |
||||
|
refreshProductCache(product.getCategoryId()); |
||||
|
refreshProductDetailCache(product); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void removeProductCache(String categoryId) { |
||||
|
if (CharSequenceUtil.isBlank(categoryId)) { |
||||
|
return; |
||||
|
} |
||||
|
redisTemplateHelper.deleteByPattern(PRODUCT_LIST_CACHE_PREFIX + categoryId + ":*"); |
||||
|
removeRegionProductCacheByCategory(categoryId); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void removeProductDetailCache(String id) { |
||||
|
if (CharSequenceUtil.isNotBlank(id)) { |
||||
|
redisTemplateHelper.delete(PRODUCT_DETAIL_CACHE_PREFIX + id); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private List<SeckillGroupProductVO> getAppListFromCache(String categoryId) { |
||||
|
String cacheKey = productListCacheKey(categoryId); |
||||
|
String cached = redisTemplateHelper.get(cacheKey); |
||||
|
if (CharSequenceUtil.isNotBlank(cached)) { |
||||
|
return JSONUtil.toList(cached, SeckillGroupProductVO.class); |
||||
|
} |
||||
|
List<SeckillGroupProductVO> products = toVoList(list(buildActiveCategoryWrapper(categoryId))); |
||||
|
sortForApp(products); |
||||
|
redisTemplateHelper.set(cacheKey, JSONUtil.toJsonStr(products)); |
||||
|
return products; |
||||
|
} |
||||
|
|
||||
|
private List<SeckillGroupProductVO> getRegionAppListFromCache(String regionId) { |
||||
|
String cacheKey = regionProductListCacheKey(regionId); |
||||
|
String cached = redisTemplateHelper.get(cacheKey); |
||||
|
if (CharSequenceUtil.isNotBlank(cached)) { |
||||
|
return JSONUtil.toList(cached, SeckillGroupProductVO.class); |
||||
|
} |
||||
|
SeckillGroupProductQuery query = new SeckillGroupProductQuery(); |
||||
|
query.setRegionId(regionId); |
||||
|
List<SeckillGroupProductVO> products = toVoList(list(buildQueryWrapper(query, true))); |
||||
|
sortForApp(products); |
||||
|
redisTemplateHelper.set(cacheKey, JSONUtil.toJsonStr(products)); |
||||
|
return products; |
||||
|
} |
||||
|
|
||||
|
private List<SeckillGroupProductVO> filterProducts(List<SeckillGroupProductVO> products, SeckillGroupProductQuery query) { |
||||
|
List<SeckillGroupProductVO> filtered = new ArrayList<>(); |
||||
|
if (products == null || products.isEmpty()) { |
||||
|
return filtered; |
||||
|
} |
||||
|
String keywords = CharSequenceUtil.isNotBlank(query.getKeywords()) ? query.getKeywords() : query.getProductName(); |
||||
|
for (SeckillGroupProductVO product : products) { |
||||
|
if (CharSequenceUtil.isNotBlank(query.getCategoryId()) && !query.getCategoryId().equals(product.getCategoryId())) { |
||||
|
continue; |
||||
|
} |
||||
|
if (CharSequenceUtil.isNotBlank(query.getShopId()) && !query.getShopId().equals(product.getShopId())) { |
||||
|
continue; |
||||
|
} |
||||
|
if (query.getStatus() != null && !query.getStatus().equals(product.getStatus())) { |
||||
|
continue; |
||||
|
} |
||||
|
if (CharSequenceUtil.isNotBlank(keywords)) { |
||||
|
String productName = product.getProductName() == null ? "" : product.getProductName(); |
||||
|
String shopName = product.getShopName() == null ? "" : product.getShopName(); |
||||
|
if (!productName.contains(keywords) && !shopName.contains(keywords)) { |
||||
|
continue; |
||||
|
} |
||||
|
} |
||||
|
filtered.add(product); |
||||
|
} |
||||
|
return filtered; |
||||
|
} |
||||
|
|
||||
|
private LambdaQueryWrapper<SeckillGroupProduct> buildQueryWrapper(SeckillGroupProductQuery query, boolean onlyActive) { |
||||
|
LambdaQueryWrapper<SeckillGroupProduct> wrapper = new LambdaQueryWrapper<>(); |
||||
|
wrapper.eq(SeckillGroupProduct::getDelFlag, 0); |
||||
|
if (CharSequenceUtil.isNotBlank(query.getCategoryId())) { |
||||
|
wrapper.eq(SeckillGroupProduct::getCategoryId, query.getCategoryId()); |
||||
|
} |
||||
|
appendRegionCondition(wrapper, query.getRegionId()); |
||||
|
if (CharSequenceUtil.isNotBlank(query.getShopId())) { |
||||
|
wrapper.eq(SeckillGroupProduct::getShopId, query.getShopId()); |
||||
|
} |
||||
|
if (query.getStatus() != null) { |
||||
|
wrapper.eq(SeckillGroupProduct::getStatus, query.getStatus()); |
||||
|
} |
||||
|
String keywords = CharSequenceUtil.isNotBlank(query.getKeywords()) ? query.getKeywords() : query.getProductName(); |
||||
|
if (CharSequenceUtil.isNotBlank(keywords)) { |
||||
|
wrapper.and(w -> w.like(SeckillGroupProduct::getProductName, keywords) |
||||
|
.or() |
||||
|
.like(SeckillGroupProduct::getShopName, keywords)); |
||||
|
} |
||||
|
if (onlyActive || Boolean.TRUE.equals(query.getOnlyActive())) { |
||||
|
appendActiveCondition(wrapper); |
||||
|
} |
||||
|
if (isSeckillPriceSort(query)) { |
||||
|
boolean asc = "asc".equalsIgnoreCase(query.getOrder()) || "asc".equalsIgnoreCase(query.getSort()); |
||||
|
wrapper.orderBy(true, asc, SeckillGroupProduct::getSeckillPrice); |
||||
|
} else { |
||||
|
wrapper.orderByDesc(SeckillGroupProduct::getCreateTime); |
||||
|
} |
||||
|
return wrapper; |
||||
|
} |
||||
|
|
||||
|
private LambdaQueryWrapper<SeckillGroupProduct> buildActiveCategoryWrapper(String categoryId) { |
||||
|
LambdaQueryWrapper<SeckillGroupProduct> wrapper = new LambdaQueryWrapper<>(); |
||||
|
wrapper.eq(SeckillGroupProduct::getDelFlag, 0) |
||||
|
.eq(SeckillGroupProduct::getStatus, 1) |
||||
|
.eq(SeckillGroupProduct::getCategoryId, categoryId); |
||||
|
appendActiveCondition(wrapper); |
||||
|
return wrapper; |
||||
|
} |
||||
|
|
||||
|
private void appendActiveCondition(LambdaQueryWrapper<SeckillGroupProduct> wrapper) { |
||||
|
Date now = new Date(); |
||||
|
wrapper.eq(SeckillGroupProduct::getStatus, 1) |
||||
|
.and(w -> w.isNull(SeckillGroupProduct::getStartTime).or().le(SeckillGroupProduct::getStartTime, now)) |
||||
|
.and(w -> w.isNull(SeckillGroupProduct::getEndTime).or().ge(SeckillGroupProduct::getEndTime, now)); |
||||
|
} |
||||
|
|
||||
|
private void appendRegionCondition(LambdaQueryWrapper<SeckillGroupProduct> wrapper, String regionId) { |
||||
|
if (CharSequenceUtil.isBlank(regionId)) { |
||||
|
return; |
||||
|
} |
||||
|
List<SeckillGroupCategory> categories = seckillGroupCategoryService.listForApp(regionId); |
||||
|
if (categories == null || categories.isEmpty()) { |
||||
|
wrapper.eq(SeckillGroupProduct::getCategoryId, "__none__"); |
||||
|
return; |
||||
|
} |
||||
|
List<String> categoryIds = new ArrayList<>(); |
||||
|
for (SeckillGroupCategory category : categories) { |
||||
|
categoryIds.add(category.getId()); |
||||
|
} |
||||
|
wrapper.in(SeckillGroupProduct::getCategoryId, categoryIds); |
||||
|
} |
||||
|
|
||||
|
private boolean categoryBelongsToRegion(String categoryId, String regionId) { |
||||
|
if (CharSequenceUtil.isBlank(regionId)) { |
||||
|
return true; |
||||
|
} |
||||
|
SeckillGroupCategory category = seckillGroupCategoryService.getById(categoryId); |
||||
|
return category != null |
||||
|
&& Integer.valueOf(0).equals(category.getDelFlag()) |
||||
|
&& Integer.valueOf(1).equals(category.getStatus()) |
||||
|
&& regionId.equals(category.getRegionId()); |
||||
|
} |
||||
|
|
||||
|
private IPage<SeckillGroupProductVO> convertPage(IPage<SeckillGroupProduct> result) { |
||||
|
Page<SeckillGroupProductVO> page = new Page<>(result.getCurrent(), result.getSize(), result.getTotal()); |
||||
|
page.setRecords(toVoList(result.getRecords())); |
||||
|
return page; |
||||
|
} |
||||
|
|
||||
|
private List<SeckillGroupProductVO> toVoList(List<SeckillGroupProduct> products) { |
||||
|
List<SeckillGroupProductVO> voList = new ArrayList<>(); |
||||
|
if (products == null || products.isEmpty()) { |
||||
|
return voList; |
||||
|
} |
||||
|
for (SeckillGroupProduct product : products) { |
||||
|
voList.add(toVo(product)); |
||||
|
} |
||||
|
return voList; |
||||
|
} |
||||
|
|
||||
|
private SeckillGroupProductVO toVo(SeckillGroupProduct product) { |
||||
|
SeckillGroupProductVO vo = new SeckillGroupProductVO(); |
||||
|
vo.setId(product.getId()); |
||||
|
vo.setCategoryId(product.getCategoryId()); |
||||
|
vo.setProductId(product.getProductId()); |
||||
|
vo.setProductName(product.getProductName()); |
||||
|
vo.setProductPicture(product.getProductPicture()); |
||||
|
vo.setUnit(product.getUnit()); |
||||
|
vo.setAttributeList(product.getAttributeList()); |
||||
|
vo.setProductIntro(product.getProductIntro()); |
||||
|
vo.setOriginalPrice(product.getOriginalPrice()); |
||||
|
vo.setSeckillPrice(product.getSeckillPrice()); |
||||
|
vo.setTotalStock(product.getTotalStock()); |
||||
|
vo.setSoldStock(product.getSoldStock()); |
||||
|
vo.setLockStock(product.getLockStock()); |
||||
|
vo.setAvailableStock(Math.max(0, safeStock(product.getTotalStock()) - safeStock(product.getSoldStock()) - safeStock(product.getLockStock()))); |
||||
|
vo.setLimitNum(product.getLimitNum()); |
||||
|
vo.setShopId(product.getShopId()); |
||||
|
vo.setShopName(product.getShopName()); |
||||
|
vo.setShopPhone(product.getShopPhone()); |
||||
|
vo.setShopAddress(product.getShopAddress()); |
||||
|
vo.setGetAreaId(product.getGetAreaId()); |
||||
|
vo.setStartTime(product.getStartTime()); |
||||
|
vo.setEndTime(product.getEndTime()); |
||||
|
vo.setOrderFiled(product.getOrderFiled()); |
||||
|
vo.setStatus(product.getStatus()); |
||||
|
return vo; |
||||
|
} |
||||
|
|
||||
|
private void sortForApp(List<SeckillGroupProductVO> products) { |
||||
|
products.sort((o1, o2) -> { |
||||
|
int hashCompare = Long.compare(stableHash(o1.getId()), stableHash(o2.getId())); |
||||
|
if (hashCompare != 0) { |
||||
|
return hashCompare; |
||||
|
} |
||||
|
Integer order1 = o1.getOrderFiled() == null ? 0 : o1.getOrderFiled(); |
||||
|
Integer order2 = o2.getOrderFiled() == null ? 0 : o2.getOrderFiled(); |
||||
|
int orderCompare = order2.compareTo(order1); |
||||
|
if (orderCompare != 0) { |
||||
|
return orderCompare; |
||||
|
} |
||||
|
return o1.getId().compareTo(o2.getId()); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private void applyQuerySort(List<SeckillGroupProductVO> products, SeckillGroupProductQuery query) { |
||||
|
if (!isSeckillPriceSort(query)) { |
||||
|
return; |
||||
|
} |
||||
|
boolean asc = "asc".equalsIgnoreCase(query.getOrder()) || "asc".equalsIgnoreCase(query.getSort()); |
||||
|
products.sort((o1, o2) -> { |
||||
|
java.math.BigDecimal p1 = o1.getSeckillPrice() == null ? java.math.BigDecimal.ZERO : o1.getSeckillPrice(); |
||||
|
java.math.BigDecimal p2 = o2.getSeckillPrice() == null ? java.math.BigDecimal.ZERO : o2.getSeckillPrice(); |
||||
|
int compare = p1.compareTo(p2); |
||||
|
return asc ? compare : -compare; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private boolean isSeckillPriceSort(SeckillGroupProductQuery query) { |
||||
|
return "seckillPrice".equalsIgnoreCase(query.getSort()) |
||||
|
|| "seckill_price".equalsIgnoreCase(query.getSort()) |
||||
|
|| "seckillPrice".equalsIgnoreCase(query.getOrder()) |
||||
|
|| "seckill_price".equalsIgnoreCase(query.getOrder()); |
||||
|
} |
||||
|
|
||||
|
private void fillSnapshot(SeckillGroupProduct target, SeckillGroupProductDTO dto, Product product, Shop shop) { |
||||
|
target.setCategoryId(dto.getCategoryId()); |
||||
|
target.setProductId(product.getId()); |
||||
|
target.setProductName(product.getProductName()); |
||||
|
target.setProductPicture(product.getProductPicture()); |
||||
|
target.setUnit(product.getUnit()); |
||||
|
target.setAttributeList(product.getAttributeList()); |
||||
|
target.setProductIntro(product.getProductIntro()); |
||||
|
target.setOriginalPrice(resolveOriginalPrice(product)); |
||||
|
target.setSeckillPrice(dto.getSeckillPrice()); |
||||
|
target.setTotalStock(dto.getTotalStock()); |
||||
|
target.setLimitNum(dto.getLimitNum()); |
||||
|
target.setStartTime(dto.getStartTime()); |
||||
|
target.setEndTime(dto.getEndTime()); |
||||
|
target.setOrderFiled(dto.getOrderFiled() == null ? 0 : dto.getOrderFiled()); |
||||
|
target.setRemark(dto.getRemark()); |
||||
|
target.setShopId(product.getShopId()); |
||||
|
if (shop != null) { |
||||
|
target.setShopName(shop.getShopName()); |
||||
|
target.setShopPhone(shop.getContactPhone()); |
||||
|
target.setShopAddress(shop.getShopAddress()); |
||||
|
target.setGetAreaId(CharSequenceUtil.isBlank(dto.getGetAreaId()) ? shop.getRegionId() : dto.getGetAreaId()); |
||||
|
} else { |
||||
|
target.setGetAreaId(dto.getGetAreaId()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void validateSaveDto(SeckillGroupProductDTO dto, boolean edit) { |
||||
|
if (dto == null) { |
||||
|
throw new IllegalArgumentException("参数不能为空"); |
||||
|
} |
||||
|
if (edit && CharSequenceUtil.isBlank(dto.getId())) { |
||||
|
throw new IllegalArgumentException("秒杀团商品ID不能为空"); |
||||
|
} |
||||
|
if (CharSequenceUtil.isBlank(dto.getCategoryId())) { |
||||
|
throw new IllegalArgumentException("秒杀分类不能为空"); |
||||
|
} |
||||
|
if (!edit && CharSequenceUtil.isBlank(dto.getProductId())) { |
||||
|
throw new IllegalArgumentException("原商品不能为空"); |
||||
|
} |
||||
|
if (dto.getSeckillPrice() == null || dto.getSeckillPrice().signum() <= 0) { |
||||
|
throw new IllegalArgumentException("秒杀价格必须大于0"); |
||||
|
} |
||||
|
if (dto.getTotalStock() == null || dto.getTotalStock() < 0) { |
||||
|
throw new IllegalArgumentException("秒杀库存不能小于0"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void refreshProductDetailCache(SeckillGroupProduct product) { |
||||
|
redisTemplateHelper.set(PRODUCT_DETAIL_CACHE_PREFIX + product.getId(), JSONUtil.toJsonStr(toVo(product))); |
||||
|
} |
||||
|
|
||||
|
private java.math.BigDecimal resolveOriginalPrice(Product product) { |
||||
|
if (product == null || CharSequenceUtil.isBlank(product.getAttributeListPrice())) { |
||||
|
return product == null ? null : product.getPrice(); |
||||
|
} |
||||
|
try { |
||||
|
JSONArray specs = JSONUtil.parseArray(product.getAttributeListPrice()); |
||||
|
if (!specs.isEmpty()) { |
||||
|
JSONObject firstSpec = specs.getJSONObject(0); |
||||
|
String specPrice = firstSpec.getStr("specPrice"); |
||||
|
if (CharSequenceUtil.isNotBlank(specPrice)) { |
||||
|
return new java.math.BigDecimal(specPrice); |
||||
|
} |
||||
|
} |
||||
|
} catch (Exception ignored) { |
||||
|
} |
||||
|
return product.getPrice(); |
||||
|
} |
||||
|
|
||||
|
private String productListCacheKey(String categoryId) { |
||||
|
return PRODUCT_LIST_CACHE_PREFIX + categoryId + ":" + currentHalfHourSeed(); |
||||
|
} |
||||
|
|
||||
|
private String regionProductListCacheKey(String regionId) { |
||||
|
return PRODUCT_REGION_LIST_CACHE_PREFIX + regionId + ":" + currentHalfHourSeed(); |
||||
|
} |
||||
|
|
||||
|
private void removeRegionProductCacheByCategory(String categoryId) { |
||||
|
SeckillGroupCategory category = seckillGroupCategoryService.getById(categoryId); |
||||
|
if (category != null && CharSequenceUtil.isNotBlank(category.getRegionId())) { |
||||
|
redisTemplateHelper.deleteByPattern(PRODUCT_REGION_LIST_CACHE_PREFIX + category.getRegionId() + ":*"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private int safeStock(Integer value) { |
||||
|
return value == null ? 0 : value; |
||||
|
} |
||||
|
|
||||
|
private long stableHash(String id) { |
||||
|
CRC32 crc32 = new CRC32(); |
||||
|
String seed = currentHalfHourSeed(); |
||||
|
crc32.update((id + ":" + seed).getBytes(StandardCharsets.UTF_8)); |
||||
|
return crc32.getValue(); |
||||
|
} |
||||
|
|
||||
|
private String currentHalfHourSeed() { |
||||
|
LocalDateTime now = LocalDateTime.now(); |
||||
|
String minuteBucket = now.getMinute() < 30 ? "00" : "30"; |
||||
|
return now.format(SEED_FORMATTER) + minuteBucket; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
-- 为 mall_order 表添加快照字段(用户/商家联系信息) |
||||
|
ALTER TABLE `mall_order` |
||||
|
ADD COLUMN `receiver_name` VARCHAR(64) NULL COMMENT '收货人姓名(下单快照)' AFTER `remark`, |
||||
|
ADD COLUMN `receiver_phone` VARCHAR(20) NULL COMMENT '收货人电话(下单快照)' AFTER `receiver_name`, |
||||
|
ADD COLUMN `receiver_address` VARCHAR(255) NULL COMMENT '收货地址(下单快照,areaName+floor+roomNum)' AFTER `receiver_phone`, |
||||
|
ADD COLUMN `shop_name` VARCHAR(128) NULL COMMENT '商家名称(下单快照)' AFTER `receiver_address`, |
||||
|
ADD COLUMN `shop_phone` VARCHAR(20) NULL COMMENT '商家联系电话(下单快照)' AFTER `shop_name`, |
||||
|
ADD COLUMN `shop_address` VARCHAR(255) NULL COMMENT '商家地址(下单快照)' AFTER `shop_phone`; |
||||
|
|
||||
|
-- 为 mall_delivery_order 表添加快照字段(配送员看到的收货人及商家信息) |
||||
|
ALTER TABLE `mall_delivery_order` |
||||
|
ADD COLUMN `receiver_name` VARCHAR(64) NULL COMMENT '收货人姓名(快照)' AFTER `status`, |
||||
|
ADD COLUMN `receiver_phone` VARCHAR(20) NULL COMMENT '收货人电话(快照)' AFTER `receiver_name`, |
||||
|
ADD COLUMN `receiver_address` VARCHAR(255) NULL COMMENT '收货地址(快照,areaName+floor+roomNum)' AFTER `receiver_phone`, |
||||
|
ADD COLUMN `shop_name` VARCHAR(128) NULL COMMENT '商家名称(快照)' AFTER `receiver_address`, |
||||
|
ADD COLUMN `shop_phone` VARCHAR(20) NULL COMMENT '商家联系电话(快照)' AFTER `shop_name`, |
||||
|
ADD COLUMN `shop_address` VARCHAR(255) NULL COMMENT '商家地址(快照)' AFTER `shop_phone`; |
||||
@ -0,0 +1,54 @@ |
|||||
|
CREATE TABLE `t_seckill_group_category` ( |
||||
|
`id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID', |
||||
|
`create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建者', |
||||
|
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', |
||||
|
`update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新者', |
||||
|
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', |
||||
|
`del_flag` int DEFAULT 0 COMMENT '删除标志 默认0', |
||||
|
`category_name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '分类名称', |
||||
|
`category_icon` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '分类图标', |
||||
|
`region_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '区域ID', |
||||
|
`sort` int DEFAULT 0 COMMENT '排序值,越大越靠前', |
||||
|
`status` tinyint DEFAULT 1 COMMENT '状态:0禁用 1启用', |
||||
|
`remark` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注', |
||||
|
PRIMARY KEY (`id`), |
||||
|
KEY `idx_region_status_sort` (`region_id`, `status`, `del_flag`, `sort`) |
||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='秒杀团横排分类表'; |
||||
|
|
||||
|
CREATE TABLE `t_seckill_group_product` ( |
||||
|
`id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID', |
||||
|
`create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建者', |
||||
|
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', |
||||
|
`update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新者', |
||||
|
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', |
||||
|
`del_flag` int DEFAULT 0 COMMENT '删除标志 默认0', |
||||
|
`category_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '秒杀分类ID', |
||||
|
`product_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '原商品ID', |
||||
|
`product_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '商品名称快照', |
||||
|
`product_picture` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '商品图片快照', |
||||
|
`unit` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '单位快照', |
||||
|
`attribute_list` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '商品属性列表/规格快照', |
||||
|
`product_intro` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '商品简介快照', |
||||
|
`original_price` decimal(10,2) DEFAULT NULL COMMENT '原价/划线价快照', |
||||
|
`seckill_price` decimal(10,2) NOT NULL COMMENT '秒杀价格', |
||||
|
`total_stock` int NOT NULL DEFAULT 0 COMMENT '秒杀总库存', |
||||
|
`sold_stock` int NOT NULL DEFAULT 0 COMMENT '已售库存', |
||||
|
`lock_stock` int NOT NULL DEFAULT 0 COMMENT '锁定库存', |
||||
|
`limit_num` int DEFAULT NULL COMMENT '每人限购数量,空表示不限', |
||||
|
`shop_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '商家ID', |
||||
|
`shop_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '商家名称快照', |
||||
|
`shop_phone` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '商家电话快照', |
||||
|
`shop_address` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '商家地址快照', |
||||
|
`get_area_id` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '取货区域ID,便于下单传参', |
||||
|
`start_time` datetime DEFAULT NULL COMMENT '秒杀开始时间', |
||||
|
`end_time` datetime DEFAULT NULL COMMENT '秒杀结束时间', |
||||
|
`order_filed` int DEFAULT 0 COMMENT '人工排序值,越大越靠前', |
||||
|
`sort_hash` bigint unsigned DEFAULT 0 COMMENT '稳定随机排序值', |
||||
|
`status` tinyint DEFAULT 0 COMMENT '状态:0下架 1上架', |
||||
|
`remark` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注', |
||||
|
PRIMARY KEY (`id`), |
||||
|
KEY `idx_category_status_time` (`category_id`, `status`, `del_flag`, `start_time`, `end_time`), |
||||
|
KEY `idx_sort_hash` (`category_id`, `status`, `del_flag`, `sort_hash`, `order_filed`), |
||||
|
KEY `idx_product_id` (`product_id`), |
||||
|
KEY `idx_shop_id` (`shop_id`) |
||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='秒杀团商品表'; |
||||
@ -0,0 +1,18 @@ |
|||||
|
<?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="cc.hiver.mall.dao.mapper.SeckillGroupCategoryMapper"> |
||||
|
<resultMap id="BaseResultMap" type="cc.hiver.mall.entity.SeckillGroupCategory"> |
||||
|
<id column="id" jdbcType="VARCHAR" property="id"/> |
||||
|
<result column="create_by" jdbcType="VARCHAR" property="createBy"/> |
||||
|
<result column="create_time" jdbcType="TIMESTAMP" property="createTime"/> |
||||
|
<result column="update_by" jdbcType="VARCHAR" property="updateBy"/> |
||||
|
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/> |
||||
|
<result column="del_flag" jdbcType="INTEGER" property="delFlag"/> |
||||
|
<result column="category_name" jdbcType="VARCHAR" property="categoryName"/> |
||||
|
<result column="category_icon" jdbcType="VARCHAR" property="categoryIcon"/> |
||||
|
<result column="region_id" jdbcType="VARCHAR" property="regionId"/> |
||||
|
<result column="sort" jdbcType="INTEGER" property="sort"/> |
||||
|
<result column="status" jdbcType="INTEGER" property="status"/> |
||||
|
<result column="remark" jdbcType="VARCHAR" property="remark"/> |
||||
|
</resultMap> |
||||
|
</mapper> |
||||
@ -0,0 +1,36 @@ |
|||||
|
<?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="cc.hiver.mall.dao.mapper.SeckillGroupProductMapper"> |
||||
|
<resultMap id="BaseResultMap" type="cc.hiver.mall.entity.SeckillGroupProduct"> |
||||
|
<id column="id" jdbcType="VARCHAR" property="id"/> |
||||
|
<result column="create_by" jdbcType="VARCHAR" property="createBy"/> |
||||
|
<result column="create_time" jdbcType="TIMESTAMP" property="createTime"/> |
||||
|
<result column="update_by" jdbcType="VARCHAR" property="updateBy"/> |
||||
|
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/> |
||||
|
<result column="del_flag" jdbcType="INTEGER" property="delFlag"/> |
||||
|
<result column="category_id" jdbcType="VARCHAR" property="categoryId"/> |
||||
|
<result column="product_id" jdbcType="VARCHAR" property="productId"/> |
||||
|
<result column="product_name" jdbcType="VARCHAR" property="productName"/> |
||||
|
<result column="product_picture" jdbcType="VARCHAR" property="productPicture"/> |
||||
|
<result column="unit" jdbcType="VARCHAR" property="unit"/> |
||||
|
<result column="attribute_list" jdbcType="VARCHAR" property="attributeList"/> |
||||
|
<result column="product_intro" jdbcType="VARCHAR" property="productIntro"/> |
||||
|
<result column="original_price" jdbcType="DECIMAL" property="originalPrice"/> |
||||
|
<result column="seckill_price" jdbcType="DECIMAL" property="seckillPrice"/> |
||||
|
<result column="total_stock" jdbcType="INTEGER" property="totalStock"/> |
||||
|
<result column="sold_stock" jdbcType="INTEGER" property="soldStock"/> |
||||
|
<result column="lock_stock" jdbcType="INTEGER" property="lockStock"/> |
||||
|
<result column="limit_num" jdbcType="INTEGER" property="limitNum"/> |
||||
|
<result column="shop_id" jdbcType="VARCHAR" property="shopId"/> |
||||
|
<result column="shop_name" jdbcType="VARCHAR" property="shopName"/> |
||||
|
<result column="shop_phone" jdbcType="VARCHAR" property="shopPhone"/> |
||||
|
<result column="shop_address" jdbcType="VARCHAR" property="shopAddress"/> |
||||
|
<result column="get_area_id" jdbcType="VARCHAR" property="getAreaId"/> |
||||
|
<result column="start_time" jdbcType="TIMESTAMP" property="startTime"/> |
||||
|
<result column="end_time" jdbcType="TIMESTAMP" property="endTime"/> |
||||
|
<result column="order_filed" jdbcType="INTEGER" property="orderFiled"/> |
||||
|
<result column="sort_hash" jdbcType="BIGINT" property="sortHash"/> |
||||
|
<result column="status" jdbcType="INTEGER" property="status"/> |
||||
|
<result column="remark" jdbcType="VARCHAR" property="remark"/> |
||||
|
</resultMap> |
||||
|
</mapper> |
||||
@ -0,0 +1,73 @@ |
|||||
|
-- 1. 优惠券模板表 / 配置表 |
||||
|
CREATE TABLE `t_mall_coupon` ( |
||||
|
`id` varchar(32) NOT NULL COMMENT '唯一标识', |
||||
|
`name` varchar(100) NOT NULL COMMENT '优惠券名称,如:5元无门槛红包、满20减5券', |
||||
|
|
||||
|
-- 发放方与适用范围规则 |
||||
|
`issuer_type` tinyint(2) NOT NULL DEFAULT '1' COMMENT '发放方:1-平台,2-商家', |
||||
|
`issuer_id` varchar(32) NOT NULL DEFAULT '0' COMMENT '发放方ID,平台为0,商家则为具体商户ID', |
||||
|
`apply_scene` tinyint(2) NOT NULL DEFAULT '0' COMMENT '适用场景:0-通用,1-外卖/买饭,2-快递/跑腿,3-二手物品交易', |
||||
|
|
||||
|
-- 面额与门槛规则 |
||||
|
`type` tinyint(2) NOT NULL DEFAULT '1' COMMENT '优惠券类型:1-满减券,2-无门槛直减券', |
||||
|
`min_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '使用门槛金额,0或没填表示无门槛', |
||||
|
`discount_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '抵扣面额(直减金额)', |
||||
|
|
||||
|
-- 库存与限领规则 |
||||
|
`total_count` int(11) NOT NULL DEFAULT '0' COMMENT '发行总数,-1表示不限制数量', |
||||
|
`remain_count` int(11) NOT NULL DEFAULT '0' COMMENT '剩余库存可领取数量', |
||||
|
`limit_per_user` int(11) NOT NULL DEFAULT '1' COMMENT '每人最多限领张数', |
||||
|
|
||||
|
-- 有效期规则 |
||||
|
`valid_type` tinyint(2) NOT NULL DEFAULT '1' COMMENT '有效期计算类型:1-绝对时间段有效,2-领取后相对天数有效', |
||||
|
`valid_start_time` datetime DEFAULT NULL COMMENT '有效期开始时间(适用绝对时间段)', |
||||
|
`valid_end_time` datetime DEFAULT NULL COMMENT '有效期结束时间(适用绝对时间段)', |
||||
|
`valid_days` int(11) DEFAULT '0' COMMENT '自领取之日起有效天数(适用相对天数)', |
||||
|
|
||||
|
-- 状态控制 |
||||
|
`status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态:0-已下架/停发,1-发放中', |
||||
|
|
||||
|
-- 基础字段 |
||||
|
`create_by` varchar(32) DEFAULT NULL COMMENT '创建者', |
||||
|
`create_time` datetime DEFAULT NULL COMMENT '创建时间', |
||||
|
`update_by` varchar(32) DEFAULT NULL COMMENT '更新者', |
||||
|
`update_time` datetime DEFAULT NULL COMMENT '更新时间', |
||||
|
`del_flag` int(1) DEFAULT '0' COMMENT '删除标志 默认0', |
||||
|
PRIMARY KEY (`id`), |
||||
|
KEY `idx_issuer` (`issuer_type`, `issuer_id`) USING BTREE |
||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠券模板配置表'; |
||||
|
|
||||
|
|
||||
|
-- 2. 用户领取的优惠券资产表 |
||||
|
CREATE TABLE `t_mall_user_coupon` ( |
||||
|
`id` varchar(32) NOT NULL COMMENT '唯一标识', |
||||
|
`user_id` varchar(32) NOT NULL COMMENT '领取用户ID', |
||||
|
`coupon_id` varchar(32) NOT NULL COMMENT '关联的优惠券模板ID', |
||||
|
|
||||
|
-- 冗余核心匹配字段(空间换时间,避免下单查询时疯狂Join) |
||||
|
`issuer_type` tinyint(2) NOT NULL COMMENT '冗余:发放方(1平台/2商家)', |
||||
|
`issuer_id` varchar(32) NOT NULL DEFAULT '0' COMMENT '冗余:发放方ID', |
||||
|
`apply_scene` tinyint(2) NOT NULL COMMENT '冗余:适用场景', |
||||
|
`min_amount` decimal(10,2) NOT NULL COMMENT '冗余:使用门槛', |
||||
|
`discount_amount` decimal(10,2) NOT NULL COMMENT '冗余:抵扣金额', |
||||
|
|
||||
|
-- 用户实体的生命周期 |
||||
|
`status` tinyint(2) NOT NULL DEFAULT '0' COMMENT '状态:0-未使用,1-已挂起(下单锁定中),2-已使用,3-已过期', |
||||
|
`receive_time` datetime DEFAULT NULL COMMENT '领取时间', |
||||
|
`valid_start_time` datetime DEFAULT NULL COMMENT '实际可使用生效时间', |
||||
|
`valid_end_time` datetime DEFAULT NULL COMMENT '实际可使用失效时间', |
||||
|
|
||||
|
-- 核销与使用记录 |
||||
|
`use_time` datetime DEFAULT NULL COMMENT '真实核销时间', |
||||
|
`order_id` varchar(32) DEFAULT NULL COMMENT '核销时使用的订单ID', |
||||
|
|
||||
|
-- 基础字段 |
||||
|
`create_by` varchar(32) DEFAULT NULL COMMENT '创建者', |
||||
|
`create_time` datetime DEFAULT NULL COMMENT '创建时间', |
||||
|
`update_by` varchar(32) DEFAULT NULL COMMENT '更新者', |
||||
|
`update_time` datetime DEFAULT NULL COMMENT '更新时间', |
||||
|
`del_flag` int(1) DEFAULT '0' COMMENT '删除标志 默认0', |
||||
|
PRIMARY KEY (`id`), |
||||
|
KEY `idx_user_valid` (`user_id`, `status`, `valid_end_time`) USING BTREE, |
||||
|
KEY `idx_order_id` (`order_id`) USING BTREE |
||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户领取的优惠券明细表'; |
||||
Loading…
Reference in new issue