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