buyer-api/src/main/java/cn/lili/controller/lmk/PrizeController.java
New file @@ -0,0 +1,52 @@ package cn.lili.controller.lmk; import cn.lili.base.Result; import cn.lili.modules.lmk.domain.entity.PrizeRecord; import cn.lili.modules.lmk.service.PrizeService; import io.swagger.annotations.Api; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @Validated @RequiredArgsConstructor @Api(value = "小程序抽奖活动接口", tags = "小程序抽奖活动接口") @RestController @RequestMapping("/buyer/lmk/prize") public class PrizeController { private final PrizeService prizeService; /** * 抽奖 * * @param prizeActivityId * @return */ @PostMapping("/{prizeActivityId}") public Result prize(@PathVariable String prizeActivityId) { return prizeService.prize(prizeActivityId); } /** * 获取抽奖次数 * * @param prizeActivityId * @return */ @PostMapping("/prizeNum/{prizeActivityId}") public Result prizeNum(@PathVariable String prizeActivityId) { return prizeService.prizeNum(prizeActivityId); } /** * 获取获取抽奖页面信息 * * @param prizeActivityId * @return */ @PostMapping("/prizeInfo/{prizeActivityId}") public Result prizeInfo(@PathVariable String prizeActivityId) { return prizeService.prizeInfo(prizeActivityId); } } framework/src/main/java/cn/lili/common/exception/GlobalControllerExceptionHandler.java
@@ -57,7 +57,7 @@ } //如果有扩展消息,则输出异常中,跟随补充异常 if (!serviceException.getMsg().equals(ServiceException.DEFAULT_MESSAGE)) { message += ":" + serviceException.getMsg(); message = serviceException.getMsg(); } // 对一些特殊异常处理,不再打印error级别的日志 framework/src/main/java/cn/lili/common/properties/RocketmqCustomProperties.java
@@ -27,6 +27,13 @@ private String commentGroup; /** * 抽奖活动 */ private String prizeTopic; private String prizeGroup; /** * 视频 */ private String videoTopic; framework/src/main/java/cn/lili/modules/goods/entity/dos/Goods.java
@@ -164,11 +164,13 @@ @ApiModelProperty(value = "预售结束时间") @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") @DateTimeFormat(pattern = "yyyy-MM-dd") @TableField(updateStrategy = FieldStrategy.IGNORED) private Date preSaleEndDate; @ApiModelProperty(value = "预售开始时间") @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") @DateTimeFormat(pattern = "yyyy-MM-dd") @TableField(updateStrategy = FieldStrategy.IGNORED) private Date preSaleBeginDate; @ApiModelProperty(value = "抽成比例") private BigDecimal commission; @@ -192,6 +194,11 @@ this.mobileIntro = goodsOperationDTO.getMobileIntro(); this.goodsVideo = goodsOperationDTO.getGoodsVideo(); this.price = goodsOperationDTO.getPrice(); //不是预售商品预售时间置空 if (!GoodsSalesModeEnum.PRESALE.name().equals(goodsOperationDTO.getSalesModel())) { goodsOperationDTO.setPreSaleBeginDate(null); goodsOperationDTO.setPreSaleEndDate(null); } this.preSaleEndDate = goodsOperationDTO.getPreSaleEndDate(); this.preSaleBeginDate = goodsOperationDTO.getPreSaleBeginDate(); this.commission = goodsOperationDTO.getCommission(); framework/src/main/java/cn/lili/modules/lmk/domain/entity/PrizeActivity.java
@@ -3,9 +3,14 @@ import cn.lili.mybatis.BaseEntity; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import java.io.Serializable; import java.time.LocalDateTime; import java.util.Date; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import org.apache.ibatis.type.LocalDateTimeTypeHandler; /** * 抽奖活动 @@ -27,13 +32,15 @@ /** 活动描述 */ private String activityDes; @TableField("begin_time") @TableField(value = "begin_time", typeHandler = LocalDateTimeTypeHandler.class) /** 活动开始时间 */ private LocalDateTime beginTime; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date beginTime; @TableField("end_time") @TableField(value = "end_time", typeHandler = LocalDateTimeTypeHandler.class) /** 活动结束时间 */ private LocalDateTime endTime; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date endTime; @TableField("max_prize") /** 每日最大抽奖上限 */ framework/src/main/java/cn/lili/modules/lmk/domain/entity/PrizeGrantRecord.java
New file @@ -0,0 +1,62 @@ package cn.lili.modules.lmk.domain.entity; import cn.lili.mybatis.BaseEntity; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import java.io.Serializable; import lombok.Data; /** * 奖品发放记录 * * @author peng * @since 2025-08-20 */ @Data @TableName("lmk_prize_grant_record") public class PrizeGrantRecord extends BaseEntity { private static final long serialVersionUID = 1L; @TableField("user_id") /** 用户id */ private Long userId; @TableField("nick_name") /** 用户昵称 */ private String nickName; @TableField("activity_id") /** 抽奖活动id */ private Long activityId; @TableField("activity_name") /** 活动名称 */ private String activityName; @TableField("prize_id") /** 奖品id */ private Long prizeId; @TableField("prize_name") /** 奖品名称 */ private String prizeName; @TableField("prize_content") /** 奖品内容 */ private String prizeContent; @TableField("grant_status") /** 发放状态 */ private String grantStatus; @TableField("prize_num_id") /** 抽奖机会id */ private Long prizeNumId; @TableField("des") /** 描述 */ private String des; } framework/src/main/java/cn/lili/modules/lmk/domain/entity/PrizeRecord.java
@@ -54,6 +54,11 @@ /** 奖品id */ private Long prizeId; @TableField("prize_name") /** 奖品名称 */ private String prizeName; @TableField("prize_img") /** 奖品封面 */ private String prizeImg; framework/src/main/java/cn/lili/modules/lmk/domain/form/PrizeGrantRecordForm.java
New file @@ -0,0 +1,74 @@ package cn.lili.modules.lmk.domain.form; import cn.lili.group.Update; import cn.lili.group.Add; import cn.lili.base.AbsForm; import cn.lili.modules.lmk.domain.entity.PrizeGrantRecord; import org.springframework.beans.BeanUtils; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import org.springframework.lang.NonNull; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.util.Date; /** * 奖品发放记录表单 * * @author peng * @since 2025-08-20 */ @Data @ApiModel(value = "PrizeGrantRecord表单", description = "奖品发放记录表单") public class PrizeGrantRecordForm extends AbsForm { @NotNull(message = "用户id不能为空", groups = {Add.class, Update.class}) @ApiModelProperty("用户id") private Long userId; @NotBlank(message = "用户昵称不能为空", groups = {Add.class, Update.class}) @ApiModelProperty("用户昵称") private String nickName; @NotNull(message = "抽奖活动id不能为空", groups = {Add.class, Update.class}) @ApiModelProperty("抽奖活动id") private Long activityId; @NotBlank(message = "活动名称不能为空", groups = {Add.class, Update.class}) @ApiModelProperty("活动名称") private String activityName; @NotNull(message = "奖品id不能为空", groups = {Add.class, Update.class}) @ApiModelProperty("奖品id") private Long prizeId; @NotBlank(message = "奖品名称不能为空", groups = {Add.class, Update.class}) @ApiModelProperty("奖品名称") private String prizeName; @NotBlank(message = "奖品内容不能为空", groups = {Add.class, Update.class}) @ApiModelProperty("奖品内容") private String prizeContent; @NotBlank(message = "发放状态不能为空", groups = {Add.class, Update.class}) @ApiModelProperty("发放状态") private String grantStatus; @NotNull(message = "抽奖机会id不能为空", groups = {Add.class, Update.class}) @ApiModelProperty("抽奖机会id") private Long prizeNumId; @NotBlank(message = "描述不能为空", groups = {Add.class, Update.class}) @ApiModelProperty("描述") private String des; public static PrizeGrantRecord getEntityByForm(@NonNull PrizeGrantRecordForm form, PrizeGrantRecord entity) { if(entity == null) { entity = new PrizeGrantRecord(); } BeanUtils.copyProperties(form, entity); return entity; } } framework/src/main/java/cn/lili/modules/lmk/domain/query/PrizeGrantRecordQuery.java
New file @@ -0,0 +1,22 @@ package cn.lili.modules.lmk.domain.query; import cn.lili.base.AbsQuery; import java.util.List; import org.springframework.lang.NonNull; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; /** * 奖品发放记录查询 * * @author peng * @since 2025-08-20 */ @Data @ApiModel(value = "PrizeGrantRecord查询参数", description = "奖品发放记录查询参数") public class PrizeGrantRecordQuery extends AbsQuery { } framework/src/main/java/cn/lili/modules/lmk/domain/query/PrizeRecordTimeQuery.java
New file @@ -0,0 +1,21 @@ package cn.lili.modules.lmk.domain.query; import lombok.Data; import java.util.Date; @Data public class PrizeRecordTimeQuery { /** * 开始时间 */ private Date startTime; /** * 结束时间 */ private Date endTime; /** * 活动id */ private String recordActivityId; } framework/src/main/java/cn/lili/modules/lmk/domain/vo/PrizeGrantRecordVO.java
New file @@ -0,0 +1,71 @@ package cn.lili.modules.lmk.domain.vo; import cn.lili.base.AbsVo; import cn.lili.modules.lmk.domain.entity.PrizeGrantRecord; import java.util.List; import org.springframework.lang.NonNull; import org.springframework.beans.BeanUtils; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.util.Date; /** * 奖品发放记录展示 * * @author peng * @since 2025-08-20 */ @Data @ApiModel(value = "奖品发放记录响应数据", description = "奖品发放记录响应数据") public class PrizeGrantRecordVO extends AbsVo { /** 用户id */ @ApiModelProperty("用户id") private Long userId; /** 用户昵称 */ @ApiModelProperty("用户昵称") private String nickName; /** 抽奖活动id */ @ApiModelProperty("抽奖活动id") private Long activityId; /** 活动名称 */ @ApiModelProperty("活动名称") private String activityName; /** 奖品id */ @ApiModelProperty("奖品id") private Long prizeId; /** 奖品名称 */ @ApiModelProperty("奖品名称") private String prizeName; /** 奖品内容 */ @ApiModelProperty("奖品内容") private String prizeContent; /** 发放状态 */ @ApiModelProperty("发放状态") private String grantStatus; /** 抽奖机会id */ @ApiModelProperty("抽奖机会id") private Long prizeNumId; /** 描述 */ @ApiModelProperty("描述") private String des; public static PrizeGrantRecordVO getVoByEntity(@NonNull PrizeGrantRecord entity, PrizeGrantRecordVO vo) { if(vo == null) { vo = new PrizeGrantRecordVO(); } BeanUtils.copyProperties(entity, vo); return vo; } } framework/src/main/java/cn/lili/modules/lmk/domain/vo/PrizeProbabilityVO.java
New file @@ -0,0 +1,11 @@ package cn.lili.modules.lmk.domain.vo; import lombok.Data; import java.math.BigDecimal; @Data public class PrizeProbabilityVO { private Long prizeId; private BigDecimal[][] probability; } framework/src/main/java/cn/lili/modules/lmk/domain/vo/PrizeRecordTimeVO.java
New file @@ -0,0 +1,24 @@ package cn.lili.modules.lmk.domain.vo; import lombok.Data; import java.util.Date; @Data public class PrizeRecordTimeVO { /** * 奖品id */ private String prizeId; /** * 活动id */ private String prizeActivityId; /** * 被抽中数量 */ private Integer total; } framework/src/main/java/cn/lili/modules/lmk/enums/general/EnableStatusEnums.java
@@ -10,9 +10,9 @@ @AllArgsConstructor public enum EnableStatusEnums { /** * 中奖状态 * 启用状态 */ ENABLE("中奖"), DISABLE("未中奖"); ENABLE("启用"), DISABLE("禁用"); private final String description; } framework/src/main/java/cn/lili/modules/lmk/enums/general/PrizeDistributeStatusEnum.java
@@ -11,6 +11,7 @@ */ NOT_WAIT("未中奖"), WAIT("待发放"), FAILED("发放失败"), SUCCESS("已发放"); private final String description; } framework/src/main/java/cn/lili/modules/lmk/enums/general/PrizeGrantStatusEnums.java
New file @@ -0,0 +1,15 @@ package cn.lili.modules.lmk.enums.general; import lombok.AllArgsConstructor; import lombok.Getter; @AllArgsConstructor @Getter public enum PrizeGrantStatusEnums { /** * 奖品发放记录 */ FAILED("发放失败"), SUCCESS("发放成功"); private final String description; } framework/src/main/java/cn/lili/modules/lmk/enums/general/PrizeStatusEnum.java
@@ -9,7 +9,7 @@ /** * 中奖状态 */ WIN("未中奖"), NOT_WIN("中奖"); WIN("中奖"), NOT_WIN("未中奖"); private final String description; } framework/src/main/java/cn/lili/modules/lmk/mapper/ActivityRefPrizeMapper.java
@@ -31,4 +31,6 @@ */ IPage getPage(IPage page, @Param("query") ActivityRefPrizeQuery query); String lockPrizeRef(@Param("refId") String refId);; } framework/src/main/java/cn/lili/modules/lmk/mapper/PrizeGrantRecordMapper.java
New file @@ -0,0 +1,34 @@ package cn.lili.modules.lmk.mapper; import cn.lili.modules.lmk.domain.entity.PrizeGrantRecord; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import cn.lili.modules.lmk.domain.vo.PrizeGrantRecordVO; import cn.lili.modules.lmk.domain.form.PrizeGrantRecordForm; import cn.lili.modules.lmk.domain.query.PrizeGrantRecordQuery; import java.util.List; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; /** * 奖品发放记录 Mapper 接口 * * @author peng * @since 2025-08-20 */ @Mapper public interface PrizeGrantRecordMapper extends BaseMapper<PrizeGrantRecord> { /** * id查找奖品发放记录 * @param id * @return */ PrizeGrantRecordVO getById(String id); /** * 分页 */ IPage getPage(IPage page, @Param("query") PrizeGrantRecordQuery query); } framework/src/main/java/cn/lili/modules/lmk/mapper/PrizeRecordMapper.java
@@ -1,6 +1,8 @@ package cn.lili.modules.lmk.mapper; import cn.lili.modules.lmk.domain.entity.PrizeRecord; import cn.lili.modules.lmk.domain.query.PrizeRecordTimeQuery; import cn.lili.modules.lmk.domain.vo.PrizeRecordTimeVO; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import cn.lili.modules.lmk.domain.vo.PrizeRecordVO; @@ -31,4 +33,5 @@ */ IPage getPage(IPage page, @Param("query") PrizeRecordQuery query); List<PrizeRecordTimeVO> getPrizeRecordListByTime(@Param("query") PrizeRecordTimeQuery query); } framework/src/main/java/cn/lili/modules/lmk/service/ActivityRefPrizeService.java
@@ -5,6 +5,8 @@ import cn.lili.base.Result; import cn.lili.modules.lmk.domain.form.ActivityRefPrizeForm; import cn.lili.modules.lmk.domain.query.ActivityRefPrizeQuery; import org.apache.ibatis.annotations.Param; import java.util.List; /** @@ -62,4 +64,6 @@ * @return */ Result all(); String lockPrizeRef(String refId); } framework/src/main/java/cn/lili/modules/lmk/service/PrizeGrantRecordService.java
New file @@ -0,0 +1,65 @@ package cn.lili.modules.lmk.service; import cn.lili.modules.lmk.domain.entity.PrizeGrantRecord; import com.baomidou.mybatisplus.extension.service.IService; import cn.lili.base.Result; import cn.lili.modules.lmk.domain.form.PrizeGrantRecordForm; import cn.lili.modules.lmk.domain.query.PrizeGrantRecordQuery; import java.util.List; /** * 奖品发放记录 服务类 * * @author peng * @since 2025-08-20 */ public interface PrizeGrantRecordService extends IService<PrizeGrantRecord> { /** * 添加 * @param form * @return */ Result add(PrizeGrantRecordForm form); /** * 修改 * @param form * @return */ Result update(PrizeGrantRecordForm form); /** * 批量删除 * @param ids * @return */ Result remove(List<String> ids); /** * id删除 * @param id * @return */ Result removeById(String id); /** * 分页查询 * @param query * @return */ Result page(PrizeGrantRecordQuery query); /** * 根据id查找 * @param id * @return */ Result detail(String id); /** * 列表 * @return */ Result all(); } framework/src/main/java/cn/lili/modules/lmk/service/PrizeRecordService.java
@@ -1,6 +1,8 @@ package cn.lili.modules.lmk.service; import cn.lili.modules.lmk.domain.entity.PrizeRecord; import cn.lili.modules.lmk.domain.query.PrizeRecordTimeQuery; import cn.lili.modules.lmk.domain.vo.PrizeRecordTimeVO; import com.baomidou.mybatisplus.extension.service.IService; import cn.lili.base.Result; import cn.lili.modules.lmk.domain.form.PrizeRecordForm; @@ -62,4 +64,6 @@ * @return */ Result all(); List<PrizeRecordTimeVO> getPrizeRecordListByTime(PrizeRecordTimeQuery query); } framework/src/main/java/cn/lili/modules/lmk/service/PrizeService.java
New file @@ -0,0 +1,15 @@ package cn.lili.modules.lmk.service; import cn.lili.base.Result; import org.springframework.web.bind.annotation.PathVariable; public interface PrizeService { Result prize(String prizeId); Result prizeNum(String prizeId); Result prizeInfo(String prizeActivityId); } framework/src/main/java/cn/lili/modules/lmk/service/impl/ActivityRefPrizeServiceImpl.java
@@ -116,4 +116,9 @@ .collect(Collectors.toList()); return Result.ok().data(vos); } @Override public String lockPrizeRef(String refId) { return baseMapper.lockPrizeRef(refId); } } framework/src/main/java/cn/lili/modules/lmk/service/impl/PrizeGrantRecordServiceImpl.java
New file @@ -0,0 +1,119 @@ package cn.lili.modules.lmk.service.impl; import com.baomidou.mybatisplus.core.metadata.IPage; import cn.lili.modules.lmk.domain.entity.PrizeGrantRecord; import cn.lili.modules.lmk.mapper.PrizeGrantRecordMapper; import cn.lili.modules.lmk.service.PrizeGrantRecordService; import cn.lili.base.Result; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import cn.lili.modules.lmk.domain.form.PrizeGrantRecordForm; import cn.lili.modules.lmk.domain.vo.PrizeGrantRecordVO; import cn.lili.modules.lmk.domain.query.PrizeGrantRecordQuery; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; import cn.lili.utils.PageUtil; import org.springframework.beans.BeanUtils; import org.springframework.util.Assert; import java.util.List; import java.util.stream.Collectors; /** * 奖品发放记录 服务实现类 * * @author peng * @since 2025-08-20 */ @Service @RequiredArgsConstructor public class PrizeGrantRecordServiceImpl extends ServiceImpl<PrizeGrantRecordMapper, PrizeGrantRecord> implements PrizeGrantRecordService { private final PrizeGrantRecordMapper prizeGrantRecordMapper; /** * 添加 * @param form * @return */ @Override public Result add(PrizeGrantRecordForm form) { PrizeGrantRecord entity = PrizeGrantRecordForm.getEntityByForm(form, null); baseMapper.insert(entity); return Result.ok("添加成功"); } /** * 修改 * @param form * @return */ @Override public Result update(PrizeGrantRecordForm form) { PrizeGrantRecord entity = baseMapper.selectById(form.getId()); // 为空抛IllegalArgumentException,做全局异常处理 Assert.notNull(entity, "记录不存在"); BeanUtils.copyProperties(form, entity); baseMapper.updateById(entity); return Result.ok("修改成功"); } /** * 批量删除 * @param ids * @return */ @Override public Result remove(List<String> ids) { baseMapper.deleteBatchIds(ids); return Result.ok("删除成功"); } /** * id删除 * @param id * @return */ @Override public Result removeById(String id) { baseMapper.deleteById(id); return Result.ok("删除成功"); } /** * 分页查询 * @param query * @return */ @Override public Result page(PrizeGrantRecordQuery query) { IPage<PrizeGrantRecordVO> page = PageUtil.getPage(query, PrizeGrantRecordVO.class); baseMapper.getPage(page, query); return Result.ok().data(page.getRecords()).total(page.getTotal()); } /** * 根据id查找 * @param id * @return */ @Override public Result detail(String id) { PrizeGrantRecordVO vo = baseMapper.getById(id); Assert.notNull(vo, "记录不存在"); return Result.ok().data(vo); } /** * 列表 * @return */ @Override public Result all() { List<PrizeGrantRecord> entities = baseMapper.selectList(null); List<PrizeGrantRecordVO> vos = entities.stream() .map(entity -> PrizeGrantRecordVO.getVoByEntity(entity, null)) .collect(Collectors.toList()); return Result.ok().data(vos); } } framework/src/main/java/cn/lili/modules/lmk/service/impl/PrizeRecordServiceImpl.java
@@ -1,5 +1,7 @@ package cn.lili.modules.lmk.service.impl; import cn.lili.modules.lmk.domain.query.PrizeRecordTimeQuery; import cn.lili.modules.lmk.domain.vo.PrizeRecordTimeVO; import com.baomidou.mybatisplus.core.metadata.IPage; import cn.lili.modules.lmk.domain.entity.PrizeRecord; import cn.lili.modules.lmk.mapper.PrizeRecordMapper; @@ -116,4 +118,9 @@ .collect(Collectors.toList()); return Result.ok().data(vos); } @Override public List<PrizeRecordTimeVO> getPrizeRecordListByTime(PrizeRecordTimeQuery query) { return baseMapper.getPrizeRecordListByTime(query); } } framework/src/main/java/cn/lili/modules/lmk/service/impl/PrizeServiceImpl.java
New file @@ -0,0 +1,413 @@ package cn.lili.modules.lmk.service.impl; import cn.lili.base.Result; import cn.lili.common.exception.ServiceException; import cn.lili.common.properties.RocketmqCustomProperties; import cn.lili.common.security.AuthUser; import cn.lili.common.security.context.UserContext; import cn.lili.common.utils.StringUtils; import cn.lili.modules.lmk.domain.entity.*; import cn.lili.modules.lmk.domain.query.PrizeRecordTimeQuery; import cn.lili.modules.lmk.domain.vo.PrizeProbabilityVO; import cn.lili.modules.lmk.domain.vo.PrizeRecordTimeVO; import cn.lili.modules.lmk.enums.general.*; import cn.lili.modules.lmk.service.*; import cn.lili.mybatis.BaseEntity; import cn.lili.rocketmq.RocketmqSendCallbackBuilder; import cn.lili.rocketmq.tags.CommentTagsEnum; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.RequiredArgsConstructor; import org.apache.rocketmq.spring.core.RocketMQTemplate; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class PrizeServiceImpl implements PrizeService { private final PrizeActivityService prizeActivityService; private final PrizeNumberService prizeNumberService; private final PrizeDrawService prizeDrawService; private final ActivityRefPrizeService activityRefPrizeService; private final PrizeRecordService prizeRecordService; private static final String PRIZE_PREFIX = "prize_draw:"; private static final String PRIZE_NUMBER = "prize_number:"; private final RedisTemplate<String, String> redisTemplate; private final RedissonClient redissonClient; private final RocketmqCustomProperties rocketmqCustomProperties; private final RocketMQTemplate rocketMQTemplate; @Override @Transactional(rollbackFor = Exception.class) public Result prize(String prizeId) { AuthUser currentUser = UserContext.getCurrentUser(); if (currentUser == null) { throw new RuntimeException("当前用户没有登录无法抽奖"); } String userId = currentUser.getId(); if (StringUtils.isBlank(userId)) { throw new RuntimeException("当前用户没有登录无法抽奖"); } Result result; String lock = IdWorker.get32UUID(); RLock redissonLock = redissonClient.getLock(PRIZE_PREFIX + prizeId); try { // Boolean lockResult = redisTemplate.opsForValue().setIfAbsent(PRIZE_PREFIX + prizeId, lock, 60, TimeUnit.SECONDS); // if (Boolean.FALSE.equals(lockResult)) { // throw new ServiceException("当前活动太火爆了请稍后再试"); // } redissonLock.lock(); result = prizeDraw(prizeId, userId, lock); } finally { if (redissonLock.isHeldByCurrentThread()) { redissonLock.unlock(); } // //释放锁 // String newLock = redisTemplate.opsForValue().get(PRIZE_PREFIX + prizeId); // if (lock.equals(newLock)) // redisTemplate.delete(PRIZE_PREFIX + prizeId); } return result; } @Transactional(rollbackFor = Exception.class) public Result prizeDraw(String prizeId, String userId, String lock) { //活动 PrizeActivity prizeActivity; //待使用的抽奖机会 PrizeNumber waitUserPrize; //当天开始时间 Date beginTime; //当天结束时间 Date endTime; //使用了的抽奖机会 List<PrizeNumber> usedPrize; //没被使用的抽奖机会 List<PrizeNumber> canUsedPrize; //抽奖商品集合 List<ActivityRefPrize> refPrizes; //查询当天抽奖记录入参 PrizeRecordTimeQuery prizeRecordTimeQuery; //当天抽奖记录汇总 List<PrizeRecordTimeVO> prizeRecordListByTime; //还能抽奖商品map集合 Map<Long, ActivityRefPrize> canPrizeMap; //待构建抽奖概率商品集合 List<ActivityRefPrize> refPrizeList; //概率集合 List<PrizeProbabilityVO> prizeProbabilityList; //中奖商品id Long prizeWon = null; prizeActivity = prizeActivityService.getById(prizeId); if (prizeActivity == null) { throw new ServiceException("当前活动不存在"); } Date activityEndtime = prizeActivity.getEndTime(); if (new Date().after(activityEndtime)) { throw new ServiceException("当前活动已经结束"); } if (!EnableStatusEnums.ENABLE.name().equals(prizeActivity.getEnableStatus())) { throw new ServiceException("活动还没有开始"); } //查询当天的抽奖卷 LocalDate now = LocalDate.now(); LocalDateTime maxTime = LocalDateTime.of(now, LocalTime.MAX); LocalDateTime minTime = LocalDateTime.of(now, LocalTime.MIN); beginTime = Date.from( minTime.atZone(ZoneId.systemDefault()).toInstant() ); endTime = Date.from( maxTime.atZone(ZoneId.systemDefault()).toInstant() ); LambdaQueryWrapper<PrizeNumber> queryNumber = Wrappers.<PrizeNumber>lambdaQuery(); queryNumber.eq(PrizeNumber::getUserId, userId).eq(PrizeNumber::getActivityPrizeId, prizeId); queryNumber.between(BaseEntity::getCreateTime, beginTime, endTime); //判断用户是否有抽奖机会有取出一条未抽奖的数据待使用 List<PrizeNumber> list = prizeNumberService.list(queryNumber); if (list == null || list.isEmpty()) { throw new ServiceException("抽奖次数用完了"); } usedPrize = list.stream().filter(item -> { return PrizeNumberUseEnum.USED.name().equals(item.getUseStatus()); }).collect(Collectors.toList()); canUsedPrize = list.stream().filter(item -> { return PrizeNumberUseEnum.WAIT.name().equals(item.getUseStatus()); }).collect(Collectors.toList()); if (usedPrize.size() > prizeActivity.getMaxPrize()) { throw new ServiceException("抽奖已达到上限"); } if (canUsedPrize.isEmpty()) { throw new ServiceException("抽奖次数用完了"); } waitUserPrize = canUsedPrize.get(0); //判断奖品是否还有以及当天是否还有奖品 LambdaQueryWrapper<ActivityRefPrize> prizeRefQuery = Wrappers.<ActivityRefPrize>lambdaQuery(); prizeRefQuery.eq(ActivityRefPrize::getPrizeActivityId, prizeId); refPrizes = activityRefPrizeService.list(prizeRefQuery); canPrizeMap = refPrizes.stream().filter(item -> { return item.getRemainNum() > 0; }).collect(Collectors.toMap(ActivityRefPrize::getPrizeId, Function.identity())); prizeRecordTimeQuery = new PrizeRecordTimeQuery(); prizeRecordTimeQuery.setStartTime(beginTime); prizeRecordTimeQuery.setEndTime(endTime); prizeRecordTimeQuery.setRecordActivityId(prizeId); prizeRecordListByTime = prizeRecordService.getPrizeRecordListByTime(prizeRecordTimeQuery); prizeRecordListByTime.stream().filter(item->{ return item.getPrizeId() != null; }).forEach(item -> { ActivityRefPrize activityRefPrize = canPrizeMap.get(Long.parseLong(item.getPrizeId())); if (activityRefPrize != null) { //移除当日上限的奖品并且添加到不能抽奖商品中去 if (activityRefPrize.getMaxPreDay() <= item.getTotal()) { canPrizeMap.remove(Long.parseLong(item.getPrizeId())); } } }); //准备构建范围集合 refPrizeList = new ArrayList<>(canPrizeMap.values()); //当前中奖比例 BigDecimal currentProbability = refPrizeList.stream().map(ActivityRefPrize::getPrizeProbability).reduce(BigDecimal.ZERO, BigDecimal::add); //用不能中奖的商品补齐 BigDecimal subtract = new BigDecimal(100).subtract(currentProbability); if (subtract.compareTo(BigDecimal.ZERO) > 0) { ActivityRefPrize activityRefPrize = new ActivityRefPrize(); activityRefPrize.setPrizeId(null); activityRefPrize.setPrizeProbability(subtract); refPrizeList.add(activityRefPrize); currentProbability = new BigDecimal(100); } //打乱排序 Collections.shuffle(refPrizeList); //构建抽奖范围集合 prizeProbabilityList = new ArrayList<>(); BigDecimal probabilityBegin = BigDecimal.ZERO; for (ActivityRefPrize item : refPrizeList) { PrizeProbabilityVO prizeProbabilityVO = new PrizeProbabilityVO(); prizeProbabilityVO.setPrizeId(item.getPrizeId()); BigDecimal multiply = item.getPrizeProbability().multiply(BigDecimal.valueOf(100)); BigDecimal[][] position = {{probabilityBegin, multiply.add(probabilityBegin)}}; prizeProbabilityVO.setProbability(position); probabilityBegin = multiply.add(probabilityBegin); prizeProbabilityList.add(prizeProbabilityVO); } BigDecimal max = currentProbability.multiply(BigDecimal.valueOf(100)); BigDecimal bigDecimal = generateRandom(BigDecimal.ONE, max); for (PrizeProbabilityVO prizeProbabilityVO : prizeProbabilityList) { BigDecimal minP = prizeProbabilityVO.getProbability()[0][0]; BigDecimal maxP = prizeProbabilityVO.getProbability()[0][1]; if (bigDecimal.compareTo(minP) > 0 && bigDecimal.compareTo(maxP) <= 0) { prizeWon = prizeProbabilityVO.getPrizeId(); break; } } //未中奖的情况 if (prizeWon == null) { //写入抽奖记录返回没有中奖信息 //写入抽奖记录 PrizeRecord prizeRecord = new PrizeRecord(); prizeRecord.setUserId(Long.parseLong(userId)); prizeRecord.setNickName(Objects.requireNonNull(UserContext.getCurrentUser()).getNickName()); prizeRecord.setPrizeActivityId(Long.parseLong(prizeId)); prizeRecord.setPrizeActivityName(prizeActivity.getActivityName()); prizeRecord.setPrizeActivityCover(prizeActivity.getActivityCover()); prizeRecord.setPrizeStatus(PrizeStatusEnum.NOT_WIN.name()); prizeRecord.setPrizeContent(null); prizeRecord.setPrizeId(null); prizeRecord.setPrizeNumId(Long.parseLong(waitUserPrize.getId())); prizeRecord.setActivityPrizeRefId(null); prizeRecord.setDistributeStatus(PrizeDistributeStatusEnum.NOT_WAIT.name()); prizeRecordService.save(prizeRecord); waitUserPrize.setUseStatus(PrizeNumberUseEnum.USED.name()); prizeNumberService.updateById(waitUserPrize); return Result.error("未中奖"); } ActivityRefPrize activityRefPrize; String nowLock; activityRefPrize = canPrizeMap.get(prizeWon); //将活动当前活动奖品库存使用排他锁锁住 activityRefPrizeService.lockPrizeRef(activityRefPrize.getId()); //修改抽奖机会状态 waitUserPrize.setUseStatus(PrizeNumberUseEnum.USED.name()); prizeNumberService.updateById(waitUserPrize); //扣库存 LambdaUpdateWrapper<ActivityRefPrize> set = Wrappers.lambdaUpdate(ActivityRefPrize.class) .eq(ActivityRefPrize::getId, activityRefPrize.getId()) .eq(ActivityRefPrize::getRemainNum, activityRefPrize.getRemainNum()) .eq(ActivityRefPrize::getVersion, activityRefPrize.getVersion()) .gt(ActivityRefPrize::getRemainNum, 0) .set(ActivityRefPrize::getRemainNum, activityRefPrize.getRemainNum() - 1) .set(ActivityRefPrize::getVersion, activityRefPrize.getVersion() + 1); boolean update = activityRefPrizeService.update(set); if (!update) { throw new ServiceException("当前活动太火爆了请稍后再试"); } PrizeDraw prizeDraw = prizeDrawService.getById(prizeWon); //写入抽奖记录 PrizeRecord prizeRecord = new PrizeRecord(); prizeRecord.setUserId(Long.parseLong(userId)); prizeRecord.setNickName(Objects.requireNonNull(UserContext.getCurrentUser()).getNickName()); prizeRecord.setPrizeActivityId(Long.parseLong(prizeId)); prizeRecord.setPrizeActivityName(prizeActivity.getActivityName()); prizeRecord.setPrizeActivityCover(prizeActivity.getActivityCover()); prizeRecord.setPrizeStatus(PrizeStatusEnum.WIN.name()); prizeRecord.setPrizeContent(prizeDraw.getPrizeContent()); prizeRecord.setPrizeImg(prizeActivity.getActivityImg()); prizeRecord.setPrizeNumId(Long.parseLong(waitUserPrize.getId())); prizeRecord.setActivityPrizeRefId(Long.parseLong(activityRefPrize.getId())); prizeRecord.setDistributeStatus(PrizeDistributeStatusEnum.WAIT.name()); prizeRecord.setPrizeId(prizeWon); prizeRecord.setPrizeName(prizeDraw.getPrizeName()); prizeRecord.setPrizeActivityCover(prizeActivity.getActivityCover()); prizeRecord.setPrizeImg(prizeDraw.getPrizeImg()); prizeRecordService.save(prizeRecord); //判断当前锁还是不是当前的锁如果不是直接抛出异常回滚 // nowLock = redisTemplate.opsForValue().get(PRIZE_PREFIX + prizeId); // if (!lock.equals(nowLock)) { // throw new RuntimeException("当前活动太火爆了请稍后再试"); // } // 走mq异步处理 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void afterCommit() { String destination = rocketmqCustomProperties.getPrizeTopic() + ":" + PrizeStatusEnum.WIN.name(); rocketMQTemplate.asyncSend(destination, JSON.toJSONString(prizeRecord), RocketmqSendCallbackBuilder.commonCallback()); } }); return Result.ok().data(activityRefPrize); } public static BigDecimal generateRandom(BigDecimal min, BigDecimal max) { if (min.compareTo(max) >= 0) { throw new IllegalArgumentException("最小值必须小于最大值"); } // 计算范围差值 BigDecimal range = max.subtract(min); // 生成0-1之间的随机double数 double randomDouble = new Random().nextDouble(); // 计算随机数并设置精度 return min.add(range.multiply(new BigDecimal(randomDouble))) .setScale(0, RoundingMode.HALF_UP); } @Override @Transactional(rollbackFor = Exception.class) public Result prizeNum(String prizeId) { String userId; AuthUser currentUser = UserContext.getCurrentUser(); if (currentUser == null) { throw new RuntimeException("当前用户没有登录"); } userId = currentUser.getId(); //获取redis锁 RLock lock = redissonClient.getLock(PRIZE_NUMBER + userId); PrizeActivity activity = prizeActivityService.getById(prizeId); if (activity == null) { throw new RuntimeException("当前活动不存在"); } //获取用户抽奖次数 Integer prizeNum = activity.getPrizeNum(); //加锁判断添加系统赠送次数 List<PrizeNumber> prizeNumberList; try { lock.lock(); prizeNumberList = getPrizeNumberList(prizeId, userId); // 当前用户没有初始化抽奖数据需要初始化当天抽奖数据 if (prizeNumberList == null || prizeNumberList.isEmpty()) { //没有默认抽奖次数直接返回0次 if (prizeNum == null || prizeNum <= 0) { return Result.ok().data(0); } prizeNumberList = new ArrayList<>(); //设置默认抽奖次数并返回默认抽奖次数 for (int i = 0; i < prizeNum; i++) { PrizeNumber prizeNumber = new PrizeNumber(); prizeNumber.setActivityPrizeId(Long.parseLong(prizeId)); prizeNumber.setUserId(Long.parseLong(userId)); prizeNumber.setUserAction(PrizeUserActionEnum.SYSTEM.name()); prizeNumber.setUseStatus(PrizeNumberUseEnum.WAIT.name()); prizeNumberList.add(prizeNumber); } //添加抽奖次数并返回 prizeNumberService.saveBatch(prizeNumberList); return Result.ok().data(prizeNumberList.size()); } } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } Integer maxPrize = activity.getMaxPrize(); //其他情况 int useNum = 0; int notUseNum = 0; for (PrizeNumber prizeNumber : prizeNumberList) { if (PrizeNumberUseEnum.WAIT.name().equals(prizeNumber.getUseStatus())) { notUseNum++; } else if (PrizeNumberUseEnum.USED.name().equals(prizeNumber.getUseStatus())) { useNum++; } } int userPrizeNum = prizeNumberList.size(); if (useNum >= maxPrize) { return Result.ok().data(0); } else { return Result.ok().data(userPrizeNum>maxPrize?maxPrize-useNum:notUseNum); } } /** * 获取当天某个用户的所有的优惠卷 * * @param prizeId 活动id * @param userId 用户id * @return 抽奖次数集合 */ public List<PrizeNumber> getPrizeNumberList(String prizeId, String userId) { Date beginTime; Date endTime; //查询当天的优惠卷 LocalDate now = LocalDate.now(); LocalDateTime maxTime = LocalDateTime.of(now, LocalTime.MAX); LocalDateTime minTime = LocalDateTime.of(now, LocalTime.MIN); beginTime = Date.from( minTime.atZone(ZoneId.systemDefault()).toInstant() ); endTime = Date.from( maxTime.atZone(ZoneId.systemDefault()).toInstant() ); LambdaQueryWrapper<PrizeNumber> queryNumber = Wrappers.<PrizeNumber>lambdaQuery(); queryNumber.eq(PrizeNumber::getUserId, userId).eq(PrizeNumber::getActivityPrizeId, prizeId); queryNumber.between(BaseEntity::getCreateTime, beginTime, endTime); return prizeNumberService.list(queryNumber); } @Override public Result prizeInfo(String prizeActivityId) { return null; } } framework/src/main/resources/mapper/lmk/ActivityRefPrizeMapper.xml
@@ -52,5 +52,12 @@ WHERE LARP.delete_flag = 0 </select> <select id="lockPrizeRef" resultType="string"> SELECT * FROM lmk_activity_ref_prize WHERE id = #{refId} FOR UPDATE; </select> </mapper> framework/src/main/resources/mapper/lmk/PrizeGrantRecordMapper.xml
New file @@ -0,0 +1,65 @@ <?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="cn.lili.modules.lmk.mapper.PrizeGrantRecordMapper"> <!-- 通用查询映射结果 --> <resultMap id="BaseResultMap" type="cn.lili.modules.lmk.domain.vo.PrizeGrantRecordVO"> <id column="id" property="id"/> <result column="user_id" property="userId" /> <result column="nick_name" property="nickName" /> <result column="activity_id" property="activityId" /> <result column="activity_name" property="activityName" /> <result column="prize_id" property="prizeId" /> <result column="prize_name" property="prizeName" /> <result column="prize_content" property="prizeContent" /> <result column="grant_status" property="grantStatus" /> <result column="prize_num_id" property="prizeNumId" /> <result column="des" property="des" /> </resultMap> <select id="getById" resultMap="BaseResultMap"> SELECT LPGR.user_id, LPGR.nick_name, LPGR.activity_id, LPGR.activity_name, LPGR.prize_id, LPGR.prize_name, LPGR.prize_content, LPGR.grant_status, LPGR.prize_num_id, LPGR.des, LPGR.id FROM lmk_prize_grant_record LPGR WHERE LPGR.id = #{id} AND LPGR.delete_flag = 0 </select> <select id="getPage" resultMap="BaseResultMap"> SELECT LPGR.user_id, LPGR.nick_name, LPGR.activity_id, LPGR.activity_name, LPGR.prize_id, LPGR.prize_name, LPGR.prize_content, LPGR.grant_status, LPGR.prize_num_id, LPGR.des, LPGR.id FROM lmk_prize_grant_record LPGR WHERE LPGR.delete_flag = 0 </select> </mapper> framework/src/main/resources/mapper/lmk/PrizeRecordMapper.xml
@@ -16,6 +16,7 @@ <result column="prize_img" property="prizeImg" /> <result column="prize_num_id" property="prizeNumId" /> <result column="activity_prize_ref_id" property="activityPrizeRefId" /> <result column="distribute_status" property="distributeStatus" /> </resultMap> @@ -37,7 +38,8 @@ LPR.prize_img, LPR.prize_num_id, LPR.activity_prize_ref_id, LPR.id LPR.id, LPR.distribute_status FROM lmk_prize_record LPR WHERE @@ -64,5 +66,25 @@ WHERE LPR.delete_flag = 0 </select> <resultMap id="getPrizeRecordListByTimeMap" type="cn.lili.modules.lmk.domain.vo.PrizeRecordTimeVO"> <result property="prizeId" column="prize_id"/> <result property="prizeActivityId" column="prize_activity_id"/> <result property="total" column="total"/> </resultMap> <select id="getPrizeRecordListByTime" resultMap="getPrizeRecordListByTimeMap"> SELECT prize_id, prize_activity_id, count(*) total FROM lmk_prize_record WHERE prize_activity_id = #{query.recordActivityId} AND delete_flag = 0 AND create_time BETWEEN #{query.startTime} AND #{query.endTime} GROUP BY prize_id, prize_activity_id; </select> </mapper>