Commit 1e09e12b authored by zhaobiyan's avatar zhaobiyan

merge

parents c32fef46 621eb138
......@@ -3,8 +3,6 @@ package cn.iocoder.yudao.framework.common.util.date;
import cn.hutool.core.date.DateUtil;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Calendar;
import java.util.Date;
......@@ -152,11 +150,15 @@ public class DateUtils {
return null == date ? "" : DateUtil.format(date, FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND);
}
public static Date getNextDayStart(Date date) {
LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
localDateTime = localDateTime.plusDays(1).withHour(0).withMinute(0).withSecond(0);
java.time.LocalDate localDate = localDateTime.toLocalDate();
return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
public static Date getNextNDayStart(Date date, int days) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DAY_OF_MONTH, days);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar.getTime();
}
public static Date addDays(Date date ,int days) {
......
......@@ -6,8 +6,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import sun.util.locale.BaseLocale;
import sun.util.locale.LocaleUtils;
import java.io.IOException;
import java.util.Locale;
......
......@@ -14,6 +14,7 @@ public interface ErrorCodeConstants {
ErrorCode CONFIG_KEY_DUPLICATE = new ErrorCode(1001000002, "参数配置 key 重复");
ErrorCode CONFIG_CAN_NOT_DELETE_SYSTEM_TYPE = new ErrorCode(1001000003, "不能删除类型为系统内置的参数配置");
ErrorCode CONFIG_GET_VALUE_ERROR_IF_SENSITIVE = new ErrorCode(1001000004, "不允许获取敏感配置到前端");
ErrorCode GET_LOCK_FAILED = new ErrorCode(1001000005, "get.lock.failed");
// ========== 定时任务 1001001000 ==========
ErrorCode JOB_NOT_EXISTS = new ErrorCode(1001001000, "定时任务不存在");
......
package cn.iocoder.yudao.module.member.api.score;
import cn.iocoder.yudao.module.member.api.score.dto.MemberUserScoreBatchOperateReqDTO;
import cn.iocoder.yudao.module.member.api.score.dto.MemberUserScoreOperateReqDTO;
import cn.iocoder.yudao.module.member.api.score.dto.MemberUserScoreOperateRespDTO;
import java.util.List;
public interface MemberUserScoreApi {
MemberUserScoreOperateRespDTO operateScore(MemberUserScoreOperateReqDTO req);
List<MemberUserScoreOperateRespDTO> batchOperateScore(MemberUserScoreBatchOperateReqDTO req);
}
package cn.iocoder.yudao.module.member.api.score.dto;
import cn.iocoder.yudao.module.member.enums.ScoreOperateTypeEnum;
import cn.iocoder.yudao.module.member.enums.ScoreSourceTypeEnum;
import lombok.Builder;
import lombok.Data;
import lombok.ToString;
import java.util.List;
import java.util.Map;
@Data
@Builder
@ToString
public class MemberUserScoreBatchOperateReqDTO {
/**
* 会员id
*/
private List<Long> memberIds;
/**
* 积分数量
*/
private Integer scoreCount;
/**
* 积分来源
*/
private ScoreSourceTypeEnum sourceType;
/**
* 人工操作时必传,其他来源不需要传
*/
private ScoreOperateTypeEnum operateType;
/**
* 积分规则id
*/
private Long ruleId;
/**
* 积分过期时间
*/
private Integer expireDays;
/**
* 扩展参数
*/
private Map<String, Object> extParam;
}
......@@ -46,4 +46,6 @@ public class UserRespDTO {
*/
private String controlPassword;
private Integer holdScore;
}
package cn.iocoder.yudao.module.member.api.score;
import cn.iocoder.yudao.framework.redis.helper.RedisDistributedLock;
import cn.iocoder.yudao.module.member.api.score.dto.MemberUserScoreDetailUpdateReqDto;
import cn.iocoder.yudao.module.member.api.score.dto.MemberUserScoreOperateReqDTO;
import cn.iocoder.yudao.module.member.api.score.dto.MemberUserScoreOperateRespDTO;
import cn.iocoder.yudao.module.member.api.score.dto.MemberUserScoreUpdateReqDTO;
import cn.iocoder.yudao.module.member.api.score.dto.*;
import cn.iocoder.yudao.module.member.dal.dataobject.memberUserScore.MemberUserScoreDO;
import cn.iocoder.yudao.module.member.enums.ScoreOperateTypeEnum;
import cn.iocoder.yudao.module.member.enums.ScoreSourceTypeEnum;
import cn.iocoder.yudao.module.member.service.memberUserScore.MemberUserScoreService;
import cn.iocoder.yudao.module.member.service.memberUserScoreDetail.MemberUserScoreDetailService;
import cn.iocoder.yudao.module.member.service.memberUserScoreLog.MemberUserScoreLogService;
import cn.iocoder.yudao.module.member.vo.memberUserScoreLog.MemberUserScoreLogCreateReq;
import com.alibaba.excel.util.CollectionUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.member.enums.ErrorCodeConstants.MEMBER_ID_IS_NULL;
import static cn.iocoder.yudao.module.member.enums.ErrorCodeConstants.SCORE_COUNT_ERROR;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.GET_LOCK_FAILED;
import static cn.iocoder.yudao.module.member.enums.ErrorCodeConstants.*;
@Slf4j
@Service
......@@ -42,11 +46,10 @@ public class MemberUserScoreApiImpl implements MemberUserScoreApi{
if (req.getScoreCount() <= 0) {
throw exception(SCORE_COUNT_ERROR);
}
//todo 校验
String lockKey = "member:operate:socre:" + req.getMemberId();
boolean lock = redisDistributedLock.lock(lockKey, 5000, 3, 100);
if (!lock) {
return MemberUserScoreOperateRespDTO.builder().success(false).code(500).reqDTO(req).build();
throw exception(GET_LOCK_FAILED);
}
try {
Long logId = saveScoreLog(req);
......@@ -58,6 +61,36 @@ public class MemberUserScoreApiImpl implements MemberUserScoreApi{
return MemberUserScoreOperateRespDTO.success(req);
}
@Override
@Transactional
public List<MemberUserScoreOperateRespDTO> batchOperateScore(MemberUserScoreBatchOperateReqDTO req) {
if (CollectionUtils.isEmpty(req.getMemberIds())) {
throw exception(MEMBER_ID_IS_NULL);
}
if (req.getScoreCount() <= 0) {
throw exception(SCORE_COUNT_ERROR);
}
// 校验用户当前积分是否满足扣减要求
if (req.getOperateType() == ScoreOperateTypeEnum.REDUCE) {
LambdaQueryWrapper<MemberUserScoreDO> wrapper = Wrappers.lambdaQuery();
wrapper.in(MemberUserScoreDO::getMemberId, req.getMemberIds());
List<MemberUserScoreDO> userScoreDOList = memberUserScoreService.list(wrapper);
List<MemberUserScoreDO> notEnoughScoreList = userScoreDOList.stream()
.filter(item -> item.getHoldScore() < req.getScoreCount()).collect(Collectors.toList());
if (!notEnoughScoreList.isEmpty()) {
throw exception(MEMBER_SCORE_NOT_ENOUGH);
}
}
return req.getMemberIds().stream().map(memberId -> operateScore(MemberUserScoreOperateReqDTO.builder()
.memberId(memberId)
.scoreCount(req.getScoreCount())
.operateType(req.getOperateType())
.sourceType(req.getSourceType())
.extParam(req.getExtParam())
.build()))
.collect(Collectors.toList());
}
private void updateUserScore(MemberUserScoreOperateReqDTO req) {
memberUserScoreService.updateUserScore(MemberUserScoreUpdateReqDTO.builder()
.memberId(req.getMemberId())
......
......@@ -21,6 +21,6 @@ public class MemberCodeFlushTask implements JobHandler {
@Override
public String execute(String param) throws Exception {
userService.historyCodeFlush();
return "";
return "success";
}
}
package cn.iocoder.yudao.module.member.controller.admin.job;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
import cn.iocoder.yudao.module.member.api.score.MemberUserScoreApi;
import cn.iocoder.yudao.module.member.api.score.dto.MemberUserScoreOperateReqDTO;
import cn.iocoder.yudao.module.member.dal.dataobject.memberUserScoreDetail.MemberUserScoreDetailDO;
import cn.iocoder.yudao.module.member.enums.MemberScoreStatueEnum;
import cn.iocoder.yudao.module.member.enums.ScoreSourceTypeEnum;
import cn.iocoder.yudao.module.member.service.memberUserScoreDetail.MemberUserScoreDetailService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 会员积分过期定时任务
*/
@Component
@Slf4j
public class MemberUserScoreExpireTask implements JobHandler {
/**
* 会员积分的过期时间为每日的0点整
* 任务每天0点整运行,扫描当天0点过期的积分进行状态修改
* @param param 参数
* @return
* @throws Exception
*/
@Resource
private MemberUserScoreDetailService scoreDetailService;
@Resource
private MemberUserScoreApi memberUserScoreApi;
@Override
public String execute(String param) throws Exception {
log.info("member user score expire task running");
LambdaQueryWrapper<MemberUserScoreDetailDO> wrapper = Wrappers.lambdaQuery();
wrapper.in(MemberUserScoreDetailDO::getStatus, Lists.newArrayList(MemberScoreStatueEnum.AVAILABLE.getValue(),
MemberScoreStatueEnum.PART_AVAILABLE.getValue()));
wrapper.eq(MemberUserScoreDetailDO::getExpireTime, DateUtils.getNextNDayStart(new Date(), 0));
wrapper.orderByAsc(MemberUserScoreDetailDO::getCreateTime);
List<MemberUserScoreDetailDO> todoList = scoreDetailService.list(wrapper);
log.info("member user score expire task, to expire record count :{}", todoList.size());
if (CollectionUtils.isEmpty(todoList)) {
return "success";
}
for (MemberUserScoreDetailDO memberUserScoreDetailDO : todoList) {
try {
log.info("score expire, score detail id :{}", memberUserScoreDetailDO.getId());
List logIds = (List) memberUserScoreDetailDO.getExtParamByKey(MemberUserScoreDetailDO.MemberUserScoreDetailExtKey.LOG_IDS);
Map<String, Object> extParam = new HashMap<>();
extParam.put("scoreLogIds", logIds);
memberUserScoreApi.operateScore(MemberUserScoreOperateReqDTO.builder()
.memberId(memberUserScoreDetailDO.getMemberId())
.scoreCount(memberUserScoreDetailDO.getRemainCount())
.sourceType(ScoreSourceTypeEnum.SYSTEM_EXPIRED)
.extParam(extParam)
.build());
} catch (Exception e) {
log.error("member user score expire exception, data:{}", memberUserScoreDetailDO, e);
}
}
log.info("member user score expire task finished");
return "success";
}
}
......@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.member.controller.admin.memberUserScore;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.member.api.score.MemberUserScoreApiImpl;
import cn.iocoder.yudao.module.member.api.score.dto.MemberUserScoreOperateReqDTO;
import cn.iocoder.yudao.module.member.api.score.dto.MemberUserScoreBatchOperateReqDTO;
import cn.iocoder.yudao.module.member.api.score.dto.MemberUserScoreOperateRespDTO;
import cn.iocoder.yudao.module.member.enums.ScoreOperateTypeEnum;
import cn.iocoder.yudao.module.member.enums.ScoreSourceTypeEnum;
......@@ -22,8 +22,8 @@ import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
......@@ -52,17 +52,15 @@ public class MemberUserScoreController {
@ApiOperation("操作积分")
@PreAuthorize("@ss.hasPermission('member:user-score:operate')")
public CommonResult<Boolean> operate(@Valid @RequestBody MemberUserScoreOperateQueryVO query) {
query.getMemberIds().forEach(memberId -> {
Map<String,Object> extParam = new HashMap<>();
extParam.put("comment", query.getComment());
MemberUserScoreOperateRespDTO memberUserScoreOperateRespDTO = scoreApi.operateScore(MemberUserScoreOperateReqDTO.builder()
.memberId(memberId)
List<MemberUserScoreOperateRespDTO> memberUserScoreOperateRespDTOS = scoreApi.batchOperateScore(MemberUserScoreBatchOperateReqDTO.builder()
.memberIds(query.getMemberIds())
.scoreCount(query.getScoreCount())
.operateType(ScoreOperateTypeEnum.parseByValue(query.getOperateType()))
.sourceType(ScoreSourceTypeEnum.MANUAL_OPERATE)
.extParam(extParam)
.build());
});
return success(null);
}
}
package cn.iocoder.yudao.module.member.dal.dataobject.memberUserScoreDetail;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.vavr.collection.List;
import lombok.*;
import org.apache.commons.lang.StringUtils;
import java.util.Date;
import java.util.Map;
/**
* 会员积分 DO
......@@ -40,8 +45,7 @@ public class MemberUserScoreDetailDO extends BaseDO {
@Getter
public enum MemberUserScoreDetailExtKey {
LOG_IDS("logIds");
LOG_IDS("scoreLogIds");
private final String key;
......@@ -50,4 +54,9 @@ public class MemberUserScoreDetailDO extends BaseDO {
}
}
public Object getExtParamByKey(MemberUserScoreDetailExtKey key) {
JSONObject extParamJsonObject = JSONUtil.parseObj(extParam);
return extParamJsonObject.get(key.getKey(), List.class);
}
}
......@@ -236,4 +236,5 @@ public class MemberUserDO extends TenantBaseDO {
private String code;
private Integer holdScore;
}
......@@ -160,11 +160,13 @@ public interface MemberUserMapper extends BaseMapperX<MemberUserDO> {
" uea.`name` as enterprise_name,",
" uea.`create_time` as enterprise_audit_create_time,",
" uea.`audit_time` as enterprise_audit_time," ,
" uea.`audit_remark` as enterprise_audit_remark",
" uea.`audit_remark` as enterprise_audit_remark,",
" ifnull( mus.`hold_score`, 0) as hold_score",
"from member_user u ",
"left join member_user_enterprise e on e.user_id = u.id and e.deleted = 0 ",
"left join member_user_card_auth uca on u.id = uca.user_id and uca.deleted = 0 " ,
"left join member_user_enterprise_auth uea on u.id = uea.user_id and uea.deleted = 0 ",
"left join member_user_score mus on u.id = mus.member_id and mus.deleted = 0 ",
"where ",
"u.deleted = 0 ",
"AND u.id = #{id} ",
......
......@@ -19,7 +19,7 @@ import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.addDays;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.getNextDayStart;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.getNextNDayStart;
import static cn.iocoder.yudao.module.member.enums.ErrorCodeConstants.MEMBER_ID_IS_NULL;
import static cn.iocoder.yudao.module.member.enums.ErrorCodeConstants.MEMBER_SCORE_NOT_ENOUGH;
......@@ -90,7 +90,7 @@ public class MemberUserScoreDetailServiceImpl extends AbstractService<MemberUser
memberUserScoreDetailDO.setRemainCount(reqDto.getScoreCount());
memberUserScoreDetailDO.setCreateTime(new Date());
if (reqDto.getExpireDays() != null) {
memberUserScoreDetailDO.setExpireTime(addDays(getNextDayStart(memberUserScoreDetailDO.getCreateTime()), reqDto.getExpireDays()));
memberUserScoreDetailDO.setExpireTime(addDays(getNextNDayStart(memberUserScoreDetailDO.getCreateTime(), 1), reqDto.getExpireDays()));
}
Map<String, Object> extParma = new HashMap<>();
extParma.put(MemberUserScoreDetailDO.MemberUserScoreDetailExtKey.LOG_IDS.getKey(), Lists.newArrayList(reqDto.getScoreLogId()));
......
......@@ -12,6 +12,7 @@ import cn.iocoder.yudao.framework.i18n.core.I18nMessage;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.framework.mybatis.core.vo.PageVO;
import cn.iocoder.yudao.framework.redis.helper.RedisDistributedLock;
import cn.iocoder.yudao.framework.redis.helper.RedisHelper;
import cn.iocoder.yudao.module.ecw.api.customer.CustomerApi;
import cn.iocoder.yudao.module.ecw.api.internalMessage.ClientInternalMessageApi;
......@@ -63,6 +64,7 @@ import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.GET_LOCK_FAILED;
import static cn.iocoder.yudao.module.member.enums.ErrorCodeConstants.*;
/**
......@@ -111,6 +113,9 @@ public class MemberUserServiceImpl implements MemberUserService {
@Resource
private RedisHelper redisHelper;
@Resource
private RedisDistributedLock redisDistributedLock;
@Override
public MemberUserDO getUserByMobile(String mobile) {
......@@ -580,7 +585,11 @@ public class MemberUserServiceImpl implements MemberUserService {
if (nextMemberCodeNumber != null) {
return MemberUserCodeUtils.generateMemberCode(nextMemberCodeNumber);
}
synchronized(this) {
boolean lock = redisDistributedLock.lock("next:member:code:lock", 2000, 3, 500);
if (!lock) {
throw exception(GET_LOCK_FAILED);
}
try {
nextMemberCodeNumber = redisHelper.execute4Long(redisScript, Collections.singletonList(key));
if (nextMemberCodeNumber != null) {
return MemberUserCodeUtils.generateMemberCode(nextMemberCodeNumber);
......@@ -589,6 +598,8 @@ public class MemberUserServiceImpl implements MemberUserService {
Long memberCodeMaxNumber = MemberUserCodeUtils.getMemberCodeNumber(currentMaxMemberCode);
redisHelper.set(key, String.valueOf(memberCodeMaxNumber), 5, TimeUnit.MINUTES);
return MemberUserCodeUtils.generateMemberCode(redisHelper.incrBy(key, 1));
} finally {
redisDistributedLock.releaseLock("next:member:code:lock");
}
}
......
......@@ -28,6 +28,8 @@ public class MemberUserScoreBackVO {
private Integer holdScore;
@ApiModelProperty(value = "已兑换积分")
private Integer usedScore;
@ApiModelProperty(value = "已失效积分")
private Integer expiredScore;
@ApiModelProperty(value = "会员注册时间")
private Date createTime;
}
......@@ -10,8 +10,9 @@
mu.mobile,
re.title_zh as countryNameZh,
re.title_en as countryNameEn,
mus.hold_score as holdScore,
mus.used_score as usedScore,
ifnull(mus.hold_score, 0) as holdScore,
ifnull(mus.used_score, 0) as usedScore,
ifnull(mus.expired_score, 0) as expiredScore,
mu.create_time as createTime
from member_user mu
left join member_user_score mus on mus.member_id = mu.id
......
......@@ -20,7 +20,6 @@ import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.sun.org.apache.xpath.internal.operations.Or;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
......
......@@ -5,6 +5,7 @@ import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.util.Date;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
......@@ -12,26 +13,23 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
@Data
@ApiModel("管理后台 - 礼品兑换 Request VO")
public class RedeemRewardReqVO {
@ApiModelProperty(value = "会员名称")
private String memberName;
@ApiModelProperty(value = "当前积分")
private Integer score;
@ApiModelProperty(value = "会员id")
private Long memberId;
@ApiModelProperty(value = "礼品id")
private Long rewardId;
@ApiModelProperty(value = "兑换数量")
private Integer count;
private Integer rewardCount;
@ApiModelProperty(value = "兑换方式,同领取方式(1上门领取,2包邮到家,3邮寄到付)")
private Integer pickMethod;
private Integer redeemType;
@ApiModelProperty(value = "兑换入口(后台,app,web)")
private String entrance;
private Integer entrance;
@ApiModelProperty(value = "费用数字(两位小数)")
private Integer expenses;
private BigDecimal expenses;
@ApiModelProperty(value = "费用币种(字典配置)")
private Integer currency;
......
......@@ -17,5 +17,11 @@ public interface ErrorCodeConstants {
ErrorCode REWARD_PICK_METHOD_NOT_ALLOW_CREATE = new ErrorCode(1001011009, "领取方式不合法");
ErrorCode REWARD_STATUS_NOT_ALLOW_UPDATE = new ErrorCode(1001011010, "礼物状态不允许编辑");
ErrorCode REWARD_STATUS_NOT_ALLOW_ENABLE = new ErrorCode(1001011011, "礼物不能启用");
ErrorCode REWARD_NOT_ENABLE = new ErrorCode(1001011012, "礼物未启用");
ErrorCode REWARD_SCORE_NOT_ENOUGH = new ErrorCode(1001011013, "会员积分不够");
ErrorCode REWARD_COUNT_NOT_ENOUGH = new ErrorCode(1001011014, "礼品数量不够");
ErrorCode REWARD_REDEEM_FAIL = new ErrorCode(1001011015, "批量兑换失败");
ErrorCode REWARD_REDEEM_COUNT_NOT_ALLOW = new ErrorCode(1001011016, "批量兑换每次最多十条");
ErrorCode REWARD_REDEEM_ALLOW_COUNT_ERROR = new ErrorCode(1001011017, "超出允许兑换次数");
}
package cn.iocoder.yudao.module.reward.enums;
public enum RewardRedeemStatusEnum {
REDEEMING(1, "兑换中"),
REDEEMED(2, "已兑换"),
CANCELED(3, "已取消")
;
private final int value;
private final String name;
RewardRedeemStatusEnum(int value, String name) {
this.value = value;
this.name = name;
}
public int getValue() {
return value;
}
public String getName() {
return name;
}
}
......@@ -26,7 +26,16 @@
<artifactId>yudao-module-reward-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-infra-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-member-api</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</project>
\ No newline at end of file
package cn.iocoder.yudao.module.reward.api.reward;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.generator.SnowflakeGenerator;
import cn.iocoder.yudao.framework.redis.helper.RedisDistributedLock;
import cn.iocoder.yudao.module.member.api.score.MemberUserScoreApi;
import cn.iocoder.yudao.module.member.api.score.dto.MemberUserScoreOperateReqDTO;
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.member.api.user.dto.UserRespDTO;
import cn.iocoder.yudao.module.member.enums.ScoreSourceTypeEnum;
import cn.iocoder.yudao.module.reward.api.reward.dto.RedeemRewardReqVO;
import cn.iocoder.yudao.module.reward.api.reward.dto.RedeemRewardRespDTO;
import cn.iocoder.yudao.module.reward.dal.dataobject.redeem.RewardRedeemDO;
import cn.iocoder.yudao.module.reward.dal.dataobject.reward.RewardDO;
import cn.iocoder.yudao.module.reward.dal.mysql.redeem.RewardRedeemMapper;
import cn.iocoder.yudao.module.reward.dal.mysql.reward.RewardMapper;
import cn.iocoder.yudao.module.reward.enums.RewardRedeemStatusEnum;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.List;
import javax.annotation.Resource;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.GET_LOCK_FAILED;
import static cn.iocoder.yudao.module.member.enums.ErrorCodeConstants.USER_NOT_EXISTS;
import static cn.iocoder.yudao.module.reward.enums.ErrorCodeConstants.*;
@Service
@Validated
public class RedeemRewardApiImpl implements RedeemRewardApi {
@Resource
private RewardMapper rewardMapper;
@Resource
private RewardRedeemMapper rewardRedeemMapper;
@Resource
private MemberUserApi memberUserApi;
@Resource
private RedisDistributedLock redisDistributedLock;
@Resource
private MemberUserScoreApi memberUserScoreApi;
@Resource
private SnowflakeGenerator snowflakeGenerator;
@Override
@Transactional(rollbackFor = Exception.class)
public RedeemRewardRespDTO redeemReward(RedeemRewardReqVO redeemRewardReqVO) {
// 查询礼品
RewardDO rewardDO = rewardMapper.selectById(redeemRewardReqVO.getRewardId());
if (rewardDO == null) {
throw exception(REWARD_NOT_EXISTS);
}
// 礼品未开启
if (rewardDO.getStatus() != 1) {
throw exception(REWARD_NOT_ENABLE);
}
// 礼品数量不够
if (rewardDO.getQuantityRemain() < redeemRewardReqVO.getRewardCount()) {
throw exception(REWARD_COUNT_NOT_ENOUGH);
}
// 兑换方式不匹配
if (!Objects.equals(rewardDO.getPickMethod(), redeemRewardReqVO.getRedeemType())) {
throw exception(REWARD_PICK_METHOD_NOT_ALLOW_CREATE);
}
verifyMemberUser(redeemRewardReqVO, rewardDO);
boolean lock = false;
try {
lock = redisDistributedLock.lock("reward:redeem:lock:" + redeemRewardReqVO.getRewardId());
if (!lock) {
throw exception(GET_LOCK_FAILED);
}
// 更新礼品
redeemReward(rewardDO, redeemRewardReqVO.getRewardCount());
// 添加兑换记录
Long redeemId = addRedeemRecord(redeemRewardReqVO);
// 更新会员积分
updateMemberScore(redeemRewardReqVO, rewardDO, redeemId);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
redisDistributedLock.releaseLock("reward:redeem:lock:" + redeemRewardReqVO.getRewardId());
}
return null;
}
private void redeemReward(RewardDO rewardDO, Integer rewardCount) {
// 更新时校验礼品数量是否足够
RewardDO rewardDO1 = rewardMapper.selectById(rewardDO.getId());
if (rewardDO1.getQuantityRemain() < rewardCount) {
throw exception(REWARD_COUNT_NOT_ENOUGH);
}
rewardDO.setExchangeCount(rewardDO.getExchangeCount() + 1);
rewardDO.setQuantityRemain(rewardDO.getQuantityRemain() - rewardCount);
rewardMapper.updateById(rewardDO);
}
private void updateMemberScore(RedeemRewardReqVO redeemRewardReqVO, RewardDO rewardDO, Long redeemId) {
Map<String, Object> extParam = new HashMap<>();
extParam.put("redeemId", redeemId);
memberUserScoreApi.operateScore(MemberUserScoreOperateReqDTO.builder()
.memberId(redeemRewardReqVO.getMemberId())
.sourceType(ScoreSourceTypeEnum.EXCHANGE_REWARD)
.scoreCount(redeemRewardReqVO.getRewardCount() * rewardDO.getPointsRequire())
.extParam(extParam)
.build());
}
private Long addRedeemRecord(RedeemRewardReqVO redeemRewardReqVO) {
RewardRedeemDO rewardRedeemDO = BeanUtil.copyProperties(redeemRewardReqVO, RewardRedeemDO.class);
rewardRedeemDO.setId(snowflakeGenerator.next());
rewardRedeemDO.setStatus(RewardRedeemStatusEnum.REDEEMING.getValue());
rewardRedeemMapper.insert(rewardRedeemDO);
return rewardRedeemDO.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public List<RedeemRewardRespDTO> redeemRewards(List<RedeemRewardReqVO> redeemRewardReqVOList) {
// 批量兑换每次最多十条
if (redeemRewardReqVOList.size() > 10) {
throw exception(REWARD_REDEEM_COUNT_NOT_ALLOW);
}
Long rewardId = redeemRewardReqVOList.get(0).getRewardId();
// 查询礼品
RewardDO rewardDO = rewardMapper.selectById(rewardId);
// 礼品未开启
if (rewardDO.getStatus() != 1) {
throw exception(REWARD_NOT_ENABLE);
}
int totalCount = 0;
for (RedeemRewardReqVO redeemRewardReqVO : redeemRewardReqVOList) {
// 每个兑换VO校验一遍
// 兑换方式不匹配
if (!Objects.equals(rewardDO.getPickMethod(), redeemRewardReqVO.getRedeemType())) {
throw exception(REWARD_PICK_METHOD_NOT_ALLOW_CREATE);
}
verifyMemberUser(redeemRewardReqVO, rewardDO);
// 记录兑换总数
totalCount += redeemRewardReqVO.getRewardCount();
// 判断兑换总数是否大于礼物数量
if (totalCount > rewardDO.getQuantityRemain()) {
throw exception(REWARD_COUNT_NOT_ENOUGH);
}
}
boolean lock = false;
try {
lock = redisDistributedLock.lock("reward:redeem:lock:" + rewardId);
if (!lock) {
throw exception(GET_LOCK_FAILED);
}
for (RedeemRewardReqVO redeemRewardReqVO : redeemRewardReqVOList) {
// 更新礼品
redeemReward(rewardDO, redeemRewardReqVO.getRewardCount());
// 添加兑换记录
Long redeemId = addRedeemRecord(redeemRewardReqVO);
// 更新会员积分
updateMemberScore(redeemRewardReqVO, rewardDO, redeemId);
}
} catch (Exception e) {
throw exception(REWARD_REDEEM_FAIL);
} finally {
redisDistributedLock.releaseLock("reward:redeem:lock:" + rewardId);
}
return null;
}
private void verifyMemberUser(RedeemRewardReqVO redeemRewardReqVO, RewardDO rewardDO) {
// 查询会员积分
UserRespDTO memberUser = memberUserApi.getUser(redeemRewardReqVO.getMemberId());
if (memberUser == null) {
throw exception(USER_NOT_EXISTS);
}
Integer holdScore = memberUser.getHoldScore();
// 会员积分不够
if (holdScore < rewardDO.getPointsRequire() * redeemRewardReqVO.getRewardCount()) {
throw exception(REWARD_SCORE_NOT_ENOUGH);
}
// 校验兑换次数
LambdaQueryWrapper<RewardRedeemDO> rewardRedeemDOWrapper = new LambdaQueryWrapper<>();
rewardRedeemDOWrapper.eq(RewardRedeemDO::getRewardId, redeemRewardReqVO.getRewardId())
.eq(RewardRedeemDO::getMemberId, redeemRewardReqVO.getMemberId());
Long count = rewardRedeemMapper.selectCount(rewardRedeemDOWrapper);
if (count > rewardDO.getAllowCount()) {
throw exception(REWARD_REDEEM_ALLOW_COUNT_ERROR);
}
}
}
package cn.iocoder.yudao.module.reward.dal.dataobject.redeem;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.util.Date;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("ecw_reward_redeem")
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class RewardRedeemDO extends BaseDO {
/**
* 主键
*/
@TableId
private Long id;
/**
* 会员id
*/
private Long memberId;
/**
* 礼品id
*/
private Long rewardId;
/**
* 兑换状态
*/
private Integer status;
/**
* 兑换数量
*/
private Integer rewardCount;
/**
* 兑换方式
*/
private Integer redeemType;
/**
* 兑换入口
*/
private Integer entrance;
/**
* 费用
*/
private BigDecimal expenses;
/**
* 币种
*/
private Integer currency;
/**
* 收件人姓名
*/
private String recipientName;
/**
* 收件人电话
*/
private String recipientPhoneNum;
/**
* 收件人地址
*/
private String recipientAddress;
/**
* 兑换人
*/
private String redeemer;
/**
* 兑换时间
*/
private Date redemptionTime;
/**
* 快递公司
*/
private String courierCompany;
/**
* 快递单号
*/
private String expressNo;
/**
* 快递日期
*/
private String expressDate;
/**
* 快递寄出人
*/
private String expressSender;
/**
* 上传附件
*/
private String annex;
/**
* 备注
*/
private String remark;
}
......@@ -80,6 +80,7 @@ public class RewardDO extends BaseDO {
private Date endTime;
/**
* 领取方式(1上门领取,2包邮到家,3邮寄到付)
* TODO :改为枚举
*/
private Integer pickMethod;
/**
......@@ -100,6 +101,7 @@ public class RewardDO extends BaseDO {
private String remarkFr;
/**
* 礼品状态(1已启用,2未启用,3已关闭,4已过期)
* TODO :改为枚举
*/
private Integer status;
......
package cn.iocoder.yudao.module.reward.dal.mysql.redeem;
import cn.iocoder.yudao.framework.mybatis.core.mapper.AbstractMapper;
import cn.iocoder.yudao.module.reward.dal.dataobject.redeem.RewardRedeemDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 礼品 Mapper
* @author 系统管理员
*/
@Mapper
public interface RewardRedeemMapper extends AbstractMapper<RewardRedeemDO> {
}
package cn.iocoder.yudao.module.reward.service.redeem;
import cn.iocoder.yudao.framework.mybatis.core.service.IService;
import cn.iocoder.yudao.module.reward.dal.dataobject.redeem.RewardRedeemDO;
/**
* 礼品兑换 Service 接口
*
* @author 系统管理员
*/
public interface RewardRedeemService extends IService<RewardRedeemDO> {
}
package cn.iocoder.yudao.module.reward.service.redeem;
import cn.iocoder.yudao.framework.mybatis.core.service.AbstractService;
import cn.iocoder.yudao.framework.mybatis.core.service.IService;
import cn.iocoder.yudao.module.reward.dal.dataobject.redeem.RewardRedeemDO;
import cn.iocoder.yudao.module.reward.dal.dataobject.reward.RewardDO;
import cn.iocoder.yudao.module.reward.dal.mysql.redeem.RewardRedeemMapper;
import cn.iocoder.yudao.module.reward.dal.mysql.reward.RewardMapper;
import cn.iocoder.yudao.module.reward.service.reward.RewardService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 礼品兑换 Service
*
* @author 系统管理员
*/
@Service
@Slf4j
public class RewardRedeemServiceImpl extends AbstractService<RewardRedeemMapper, RewardRedeemDO> implements RewardRedeemService {
}
......@@ -56,6 +56,10 @@ public class RewardServiceImpl extends AbstractService<RewardMapper, RewardDO> i
// 插入
RewardDO rewardDO = RewardConvert.INSTANCE.convert(createReqVO);
rewardDO.setCode(generateRewardCode());
// 剩余数量若没传给默认值0
if (rewardDO.getQuantityRemain() == null) {
rewardDO.setQuantityRemain(0);
}
//如果插入失败,重新生成code再次插入
try {
rewardMapper.insert(rewardDO);
......@@ -68,6 +72,7 @@ public class RewardServiceImpl extends AbstractService<RewardMapper, RewardDO> i
}
@Override
// TODO :完善校验
public void update(RewardUpdateReqVO updateReqVO) {
// 校验存在
RewardDO rewardDO = rewardMapper.selectById(updateReqVO.getId());
......@@ -271,7 +276,9 @@ public class RewardServiceImpl extends AbstractService<RewardMapper, RewardDO> i
}
//校验礼品是否过期并修改礼品状态
//TODO:异步更新礼品状态
private void validateExpire(RewardDO rewardDO) {
if (rewardDO.getEndTime() != null){
if (rewardDO.getStatus() == 1 && rewardDO.getEndTime().toInstant().isBefore(Instant.now())) {
RewardDO expireReward = new RewardDO();
expireReward.setId(rewardDO.getId());
......@@ -279,4 +286,5 @@ public class RewardServiceImpl extends AbstractService<RewardMapper, RewardDO> i
rewardMapper.updateById(expireReward);
}
}
}
}
......@@ -51,6 +51,7 @@ public class RewardUpdateReqVO {
private Integer nodeId;
@ApiModelProperty(value = "剩余数量")
@NotNull(message = "剩余数量不能为空")
@Min(value = 0)
private Integer quantityRemain;
......
<?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.iocoder.yudao.module.reward.dal.mysql.redeem.RewardRedeemMapper">
<!--
一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
-->
</mapper>
......@@ -1010,3 +1010,4 @@ reward.status.change.error=The reward status change error
reward.status.not.allow.delay=Only enable status can delay
reward.status.not.allow.create=Only enabled or disabled reward status can be created
reward.time.not.allow=The reward time is not allow
get.lock.failed = The service is busy, please try again later
\ No newline at end of file
......@@ -1014,3 +1014,4 @@ reward.status.change.error=\u793C\u54C1\u72B6\u6001\u64CD\u4F5C\u4E0D\u7B26\u540
reward.status.not.allow.delay=\u53EA\u5141\u8BB8\u5EF6\u671F\u542F\u7528\u72B6\u6001\u793C\u54C1
reward.status.not.allow.create=\u521B\u5EFA\u7684\u793C\u54C1\u72B6\u6001\u53EA\u80FD\u662F\u542F\u7528\u6216\u672A\u542F\u7528
reward.time.not.allow=\u6D3B\u52A8\u65F6\u95F4\u4E0D\u5408\u6CD5
get.lock.failed = \u670D\u52A1\u7E41\u5FD9\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment