目录
7.2interface IExchangeCodeService
7.3ExchangeCodeServiceImpl.java
8.3把id改成自己填IdType.INPUT(ExchangeCode.java)
8.7interface IExchangeCodeService
8.8ExchangeCodeServiceImpl.java
9.1PointsBoardPersistentHandler.java
10.1PointsBoardPersistentHandler.java
10.2interface ICouponService.java
12.1ExchangeCodeController.java
12.2interface IExchangeCodeService
12.3ExchangeCodeServiceImpl.java
3.2interface IUserCouponService
4.2interface IUserCouponService
7.2interface IUserCouponService
8.2interface IUserCouponService
1.优惠券管理-新增优惠券
1.1CouponController.java
package com.tianji.promotion.controller;
@RestController
@RequestMapping("/coupons")
@Api(tags = "优惠券相关接口")
@RequiredArgsConstructor
public class CouponController {
private final ICouponService couponService;
// TODO 新增优惠券
@PostMapping
@ApiOperation("新增优惠券")
public void saveCoupon(@Valid @RequestBody CouponFormDTO dto){
couponService.saveCoupon(dto);
}
}
1.2interface ICouponService
package com.tianji.promotion.service;
public interface ICouponService extends IService<Coupon> {
// 新增优惠券
void saveCoupon(@Valid CouponFormDTO dto);
}
1.3CouponServiceImpl.java
package com.tianji.promotion.service.impl;
@Service
@RequiredArgsConstructor
public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> implements ICouponService {
private final ICouponScopeService scopeService;
// 新增优惠券
@Transactional
@Override
public void saveCoupon(CouponFormDTO dto) {
// 1.保存优惠券
// 1.1转po
Coupon coupon = BeanUtils.copyBean(dto, Coupon.class);
// 1.2保存
save(coupon);
if (!dto.getSpecific()){
// 没有限定范围
return;
}
// 2.保存限定范围
List<Long> scopes = dto.getScopes();
if (CollUtils.isEmpty(scopes)){
throw new BadRequestException("限定范围不能为空");
}
// 2.1转换po
Long couponId = coupon.getId();
List<CouponScope> list = scopes.stream().map(bizId -> new CouponScope().setBizId(bizId).setCouponId(couponId)).collect(Collectors.toList());
// 2.2保存
scopeService.saveBatch(list);
}
}
2.优惠券管理-分页查询优惠券
2.1CouponController.java
package com.tianji.promotion.controller;
// TODO 分页查询优惠券
@GetMapping("/page")
@ApiOperation("分页查询优惠券")
public PageDTO<CouponPageVO> queryCouponPage(CouponQuery query){
return couponService.queryCouponPage(query);
}
2.2interface ICouponService
package com.tianji.promotion.service;
// 分页查询优惠券
PageDTO<CouponPageVO> queryCouponPage(CouponQuery query);
2.3CouponServiceImpl.java
package com.tianji.promotion.service.impl;
// 分页查询优惠券
@Override
public PageDTO<CouponPageVO> queryCouponPage(CouponQuery query) {
Integer type = query.getType();
Integer status = query.getStatus();
String name = query.getName();
// 1.分页查询
Page<Coupon> page = lambdaQuery()
.eq(type != null, Coupon::getDiscountType, type)
.eq(status != null, Coupon::getStatus, status)
.like(name != null, Coupon::getName, name)
.page(query.toMpPageDefaultSortByCreateTimeDesc());
// 2.处理VO
List<Coupon> records = page.getRecords();
if (CollUtils.isEmpty(records)){
return PageDTO.empty(page);
}
List<CouponPageVO> list = BeanUtils.copyList(records, CouponPageVO.class);
// 3.返回
return PageDTO.of(page,list);
}
3.优惠券-根据id查询优惠券
3.1CouponController.java
package com.tianji.promotion.controller;
// TODO 根据id查询优惠券
@GetMapping("/{id}")
@ApiOperation("根据id查询优惠券")
public Coupon getCouponById(@PathVariable Long id){
return couponService.getCouponById(id);
}
3.2interface ICouponService
package com.tianji.promotion.service;
// 根据id查询优惠券
Coupon getCouponById(Long id);
3.3CouponServiceImpl.java
package com.tianji.promotion.service.impl;
// 根据id查询优惠券
@Override
public Coupon getCouponById(Long id) {
// 返回
return lambdaQuery()
.eq(Coupon::getId, id)
.one();
}
4.优惠券-编辑优惠券
4.1CouponController.java
package com.tianji.promotion.controller;
// TODO 编辑优惠券
@PutMapping("/{id}")
@ApiOperation("编辑优惠券")
public void updateCouponById(@Valid @RequestBody CouponFormDTO dto){
couponService.updateCouponById(dto);
}
4.2interface ICouponService
package com.tianji.promotion.service;
// 编辑优惠券
void updateCouponById(Long id, CouponFormDTO dto);
4.3CouponServiceImpl.java
package com.tianji.promotion.service.impl;
// 编辑优惠券
@Override
public void updateCouponById(CouponFormDTO dto) {
// 1.属性拷贝
Coupon coupon = BeanUtils.copyBean(dto, Coupon.class);
// 2.更新表数据
updateById(coupon);
}
5.优惠券-删除优惠券
5.1CouponController.java
package com.tianji.promotion.controller;
5.2interface ICouponService
package com.tianji.promotion.service;
5.3CouponServiceImpl.java
package com.tianji.promotion.service.impl;
6.优惠券发放-实现发放接口
6.1CouponController.java
package com.tianji.promotion.controller;
// TODO 发放优惠券
@PutMapping("/{id}/issue")
@ApiOperation("发放优惠券")
public void BeginIssueCoupon(@RequestBody CouponIssueFormDTO dto){
couponService.BeginIssueCoupon(dto);
}
6.2interface ICouponService
package com.tianji.promotion.service;
// 发放优惠券
void BeginIssueCoupon(CouponIssueFormDTO dto);
6.3CouponServiceImpl.java
package com.tianji.promotion.service.impl;
// 发放优惠券
@Override
public void BeginIssueCoupon(CouponIssueFormDTO dto) {
// 1.查询优惠券
Coupon coupon = getById(dto.getId());
if (coupon == null){
throw new BadRequestException("该优惠券不存在!");
}
// 2.判断优惠券状态,是否是暂停或待发放
if (coupon.getStatus() != CouponStatus.DRAFT && coupon.getStatus() != CouponStatus.PAUSE){
throw new BizIllegalException("优惠券状态错误!");
}
// 3.判断是否是立刻发放
LocalDateTime issueBeginTime = dto.getIssueBeginTime();
LocalDateTime now = LocalDateTime.now();
// 开始时间为null或者发送时间小于等于当前时间,都代表立刻发放
boolean isBegin = issueBeginTime == null || !issueBeginTime.isAfter(now);
// 4.更新优惠券
// 4.1拷贝属性
Coupon c = BeanUtils.copyBean(dto, Coupon.class);
// 4.2更新状态
if (isBegin){
c.setStatus(CouponStatus.ISSUING);
c.setIssueBeginTime(now);
}else {
c.setStatus(CouponStatus.UN_ISSUE);
}
// 4.3写入数据库
updateById(c);
}
7.优惠券发放-异步生成兑换码(上)
7.1CouponServiceImpl.java
package com.tianji.promotion.service.impl;
// 发放优惠券
@Override
public void BeginIssueCoupon(CouponIssueFormDTO dto) {
// 1.查询优惠券
Coupon coupon = getById(dto.getId());
if (coupon == null){
throw new BadRequestException("该优惠券不存在!");
}
// 2.判断优惠券状态,是否是暂停或待发放
if (coupon.getStatus() != CouponStatus.DRAFT && coupon.getStatus() != CouponStatus.PAUSE){
throw new BizIllegalException("优惠券状态错误!");
}
// 3.判断是否是立刻发放
LocalDateTime issueBeginTime = dto.getIssueBeginTime();
LocalDateTime now = LocalDateTime.now();
// 开始时间为null或者发送时间小于等于当前时间,都代表立刻发放
boolean isBegin = issueBeginTime == null || !issueBeginTime.isAfter(now);
// 4.更新优惠券
// 4.1拷贝属性
Coupon c = BeanUtils.copyBean(dto, Coupon.class);
// 4.2更新状态
if (isBegin){
c.setStatus(CouponStatus.ISSUING);
c.setIssueBeginTime(now);
}else {
c.setStatus(CouponStatus.UN_ISSUE);
}
// 4.3写入数据库
updateById(c);
// 5.判断是否需要生成兑换码,优惠券类型必须是兑换码,优惠券状态必须是待发放
if (coupon.getObtainWay() == ObtainType.ISSUE && coupon.getStatus() == CouponStatus.DRAFT){
coupon.setIssueEndTime(c.getIssueEndTime());
// 异步生成兑换码
codeService.asyncGenerateCode(coupon);
}
}
7.2interface IExchangeCodeService
package com.tianji.promotion.service;
// 异步生成兑换码
void asyncGenerateCode(Coupon coupon);
7.3ExchangeCodeServiceImpl.java
package com.tianji.promotion.service.impl;
// 异步生成兑换码
@Override
@Async("generateExchangeCodeExecutor")
public void asyncGenerateCode(Coupon coupon) {
}
7.4PromotionApplication.java
开启异步的注解@EnableAsync
package com.tianji.promotion;
@EnableAsync
@SpringBootApplication
@MapperScan("com.tianji.promotion.mapper")
@Slf4j
public class PromotionApplication {
public static void main(String[] args) throws UnknownHostException {
SpringApplication app = new SpringApplicationBuilder(PromotionApplication.class).build(args);
Environment env = app.run(args).getEnvironment();
String protocol = "http";
if (env.getProperty("server.ssl.key-store") != null) {
protocol = "https";
}
log.info("--/\n---------------------------------------------------------------------------------------\n\t" +
"Application '{}' is running! Access URLs:\n\t" +
"Local: \t\t{}://localhost:{}\n\t" +
"External: \t{}://{}:{}\n\t" +
"Profile(s): \t{}" +
"\n---------------------------------------------------------------------------------------",
env.getProperty("spring.application.name"),
protocol,
env.getProperty("server.port"),
protocol,
InetAddress.getLocalHost().getHostAddress(),
env.getProperty("server.port"),
env.getActiveProfiles());
}
}
8.优惠券发放-异步生成兑换码(下)
8.1生成兑换码工具类01(Base32.java)
package com.tianji.promotion.utils;
import cn.hutool.core.text.StrBuilder;
/**
* 将整数转为base32字符的工具,因为是32进制,所以每5个bit位转一次
*/
public class package com.tianji.promotion.utils;
import cn.hutool.core.text.StrBuilder;
/**
* 将整数转为base32字符的工具,因为是32进制,所以每5个bit位转一次
*/
public class Base32 {
private final static String baseChars = "6CSB7H8DAKXZF3N95RTMVUQG2YE4JWPL";
public static String encode(long raw) {
StrBuilder sb = new StrBuilder();
while (raw != 0) {
int i = (int) (raw & 0b11111);
sb.append(baseChars.charAt(i));
raw = raw >>> 5;
}
return sb.toString();
}
public static long decode(String code) {
long r = 0;
char[] chars = code.toCharArray();
for (int i = chars.length - 1; i >= 0; i--) {
long n = baseChars.indexOf(chars[i]);
r = r | (n << (5*i));
}
return r;
}
public static String encode(byte[] raw) {
StrBuilder sb = new StrBuilder();
int size = 0;
int temp = 0;
for (byte b : raw) {
if (size == 0) {
// 取5个bit
int index = (b >>> 3) & 0b11111;
sb.append(baseChars.charAt(index));
// 还剩下3位
size = 3;
temp = b & 0b111;
} else {
int index = temp << (5 - size) | (b >>> (3 + size) & ((1 << 5 - size) - 1)) ;
sb.append(baseChars.charAt(index));
int left = 3 + size;
size = 0;
if(left >= 5){
index = b >>> (left - 5) & ((1 << 5) - 1);
sb.append(baseChars.charAt(index));
left = left - 5;
}
if(left == 0){
continue;
}
temp = b & ((1 << left) - 1);
size = left;
}
}
if(size > 0){
sb.append(baseChars.charAt(temp));
}
return sb.toString();
}
public static byte[] decode2Byte(String code) {
char[] chars = code.toCharArray();
byte[] bytes = new byte[(code.length() * 5 )/ 8];
byte tmp = 0;
byte byteSize = 0;
int index = 0;
int i = 0;
for (char c : chars) {
byte n = (byte) baseChars.indexOf(c);
i++;
if (byteSize == 0) {
tmp = n;
byteSize = 5;
} else {
int left = Math.min(8 - byteSize, 5);
if(i == chars.length){
bytes[index] =(byte) (tmp << left | (n & ((1 << left) - 1)));
break;
}
tmp = (byte) (tmp << left | (n >>> (5 - left)));
byteSize += left;
if (byteSize >= 8) {
bytes[index++] = tmp;
byteSize = (byte) (5 - left);
if (byteSize == 0) {
tmp = 0;
} else {
tmp = (byte) (n & ((1 << byteSize) - 1));
}
}
}
}
return bytes;
}
}
{
private final static String baseChars = "6CSB7H8DAKXZF3N95RTMVUQG2YE4JWPL";
public static String encode(long raw) {
StrBuilder sb = new StrBuilder();
while (raw != 0) {
int i = (int) (raw & 0b11111);
sb.append(baseChars.charAt(i));
raw = raw >>> 5;
}
return sb.toString();
}
public static long decode(String code) {
long r = 0;
char[] chars = code.toCharArray();
for (int i = chars.length - 1; i >= 0; i--) {
long n = baseChars.indexOf(chars[i]);
r = r | (n << (5*i));
}
return r;
}
public static String encode(byte[] raw) {
StrBuilder sb = new StrBuilder();
int size = 0;
int temp = 0;
for (byte b : raw) {
if (size == 0) {
// 取5个bit
int index = (b >>> 3) & 0b11111;
sb.append(baseChars.charAt(index));
// 还剩下3位
size = 3;
temp = b & 0b111;
} else {
int index = temp << (5 - size) | (b >>> (3 + size) & ((1 << 5 - size) - 1)) ;
sb.append(baseChars.charAt(index));
int left = 3 + size;
size = 0;
if(left >= 5){
index = b >>> (left - 5) & ((1 << 5) - 1);
sb.append(baseChars.charAt(index));
left = left - 5;
}
if(left == 0){
continue;
}
temp = b & ((1 << left) - 1);
size = left;
}
}
if(size > 0){
sb.append(baseChars.charAt(temp));
}
return sb.toString();
}
public static byte[] decode2Byte(String code) {
char[] chars = code.toCharArray();
byte[] bytes = new byte[(code.length() * 5 )/ 8];
byte tmp = 0;
byte byteSize = 0;
int index = 0;
int i = 0;
for (char c : chars) {
byte n = (byte) baseChars.indexOf(c);
i++;
if (byteSize == 0) {
tmp = n;
byteSize = 5;
} else {
int left = Math.min(8 - byteSize, 5);
if(i == chars.length){
bytes[index] =(byte) (tmp << left | (n & ((1 << left) - 1)));
break;
}
tmp = (byte) (tmp << left | (n >>> (5 - left)));
byteSize += left;
if (byteSize >= 8) {
bytes[index++] = tmp;
byteSize = (byte) (5 - left);
if (byteSize == 0) {
tmp = 0;
} else {
tmp = (byte) (n & ((1 << byteSize) - 1));
}
}
}
}
return bytes;
}
}
8.2生成兑换码工具类02(CodeUtil.java)
package com.tianji.promotion.utils;
import com.tianji.common.constants.RegexConstants;
import com.tianji.common.exceptions.BadRequestException;
/**
* <h1 style='font-weight:500'>1.兑换码算法说明:</h1>
* <p>兑换码分为明文和密文,明文是50位二进制数,密文是长度为10的Base32编码的字符串 </p>
* <h1 style='font-weight:500'>2.兑换码的明文结构:</h1>
* <p style='padding: 0 15px'>14(校验码) + 4 (新鲜值) + 32(序列号) </p>
* <ul style='padding: 0 15px'>
* <li>序列号:一个单调递增的数字,可以通过Redis来生成</li>
* <li>新鲜值:可以是优惠券id的最后4位,同一张优惠券的兑换码就会有一个相同标记</li>
* <li>载荷:将新鲜值(4位)拼接序列号(32位)得到载荷</li>
* <li>校验码:将载荷4位一组,每组乘以加权数,最后累加求和,然后对2^14求余得到</li>
* </ul>
* <h1 style='font-weight:500'>3.兑换码的加密过程:</h1>
* <ol type='a' style='padding: 0 15px'>
* <li>首先利用优惠券id计算新鲜值 f</li>
* <li>将f和序列号s拼接,得到载荷payload</li>
* <li>然后以f为角标,从提前准备好的16组加权码表中选一组</li>
* <li>对payload做加权计算,得到校验码 c </li>
* <li>利用c的后4位做角标,从提前准备好的异或密钥表中选择一个密钥:key</li>
* <li>将payload与key做异或,作为新payload2</li>
* <li>然后拼接兑换码明文:f (4位) + payload2(36位)</li>
* <li>利用Base32对密文转码,生成兑换码</li>
* </ol>
* <h1 style='font-weight:500'>4.兑换码的解密过程:</h1>
* <ol type='a' style='padding: 0 15px'>
* <li>首先利用Base32解码兑换码,得到明文数值num</li>
* <li>取num的高14位得到c1,取num低36位得payload </li>
* <li>利用c1的后4位做角标,从提前准备好的异或密钥表中选择一个密钥:key</li>
* <li>将payload与key做异或,作为新payload2</li>
* <li>利用加密时的算法,用payload2和s1计算出新校验码c2,把c1和c2比较,一致则通过 </li>
* </ol>
*/
public class CodeUtil {
/**
* 异或密钥表,用于最后的数据混淆
*/
private final static long[] XOR_TABLE = {
61261925471L, 61261925523L, 58169127203L, 64169927267L,
64169927199L, 61261925629L, 58169127227L, 64169927363L,
59169127063L, 64169927359L, 58169127291L, 61261925739L,
59169127133L, 55139281911L, 56169127077L, 59169127167L
};
/**
* fresh值的偏移位数
*/
private final static int FRESH_BIT_OFFSET = 32;
/**
* 校验码的偏移位数
*/
private final static int CHECK_CODE_BIT_OFFSET = 36;
/**
* fresh值的掩码,4位
*/
private final static int FRESH_MASK = 0xF;
/**
* 验证码的掩码,14位
*/
private final static int CHECK_CODE_MASK = 0b11111111111111;
/**
* 载荷的掩码,36位
*/
private final static long PAYLOAD_MASK = 0xFFFFFFFFFL;
/**
* 序列号掩码,32位
*/
private final static long SERIAL_NUM_MASK = 0xFFFFFFFFL;
/**
* 序列号加权运算的秘钥表
*/
private final static int[][] PRIME_TABLE = {
{23, 59, 241, 61, 607, 67, 977, 1217, 1289, 1601},
{79, 83, 107, 439, 313, 619, 911, 1049, 1237},
{173, 211, 499, 673, 823, 941, 1039, 1213, 1429, 1259},
{31, 293, 311, 349, 431, 577, 757, 883, 1009, 1657},
{353, 23, 367, 499, 599, 661, 719, 929, 1301, 1511},
{103, 179, 353, 467, 577, 691, 811, 947, 1153, 1453},
{213, 439, 257, 313, 571, 619, 743, 829, 983, 1103},
{31, 151, 241, 349, 607, 677, 769, 823, 967, 1049},
{61, 83, 109, 137, 151, 521, 701, 827, 1123},
{23, 61, 199, 223, 479, 647, 739, 811, 947, 1019},
{31, 109, 311, 467, 613, 743, 821, 881, 1031, 1171},
{41, 173, 367, 401, 569, 683, 761, 883, 1009, 1181},
{127, 283, 467, 577, 661, 773, 881, 967, 1097, 1289},
{59, 137, 257, 347, 439, 547, 641, 839, 977, 1009},
{61, 199, 313, 421, 613, 739, 827, 941, 1087, 1307},
{19, 127, 241, 353, 499, 607, 811, 919, 1031, 1301}
};
/**
* 生成兑换码
*
* @param serialNum 递增序列号
* @return 兑换码
*/
public static String generateCode(long serialNum, long fresh) {
// 1.计算新鲜值
fresh = fresh & FRESH_MASK;
// 2.拼接payload,fresh(4位) + serialNum(32位)
long payload = fresh << FRESH_BIT_OFFSET | serialNum;
// 3.计算验证码
long checkCode = calcCheckCode(payload, (int) fresh);
System.out.println("checkCode = " + checkCode);
// 4.payload做大质数异或运算,混淆数据
payload ^= XOR_TABLE[(int) (checkCode & FRESH_MASK)];
// 5.拼接兑换码明文: 校验码(14位) + payload(36位)
long code = checkCode << CHECK_CODE_BIT_OFFSET | payload;
// 6.转码
return Base32.encode(code);
}
private static long calcCheckCode(long payload, int fresh) {
// 1.获取码表
int[] table = PRIME_TABLE[fresh];
// 2.生成校验码,payload每4位乘加权数,求和,取最后13位结果
long sum = 0;
int index = 0;
while (payload > 0) {
sum += (payload & 0xf) * table[index++];
payload >>>= 4;
}
return sum & CHECK_CODE_MASK;
}
public static long parseCode(String code) {
if (code == null || !code.matches(RegexConstants.COUPON_CODE_PATTERN)) {
// 兑换码格式错误
throw new BadRequestException("无效兑换码");
}
// 1.Base32解码
long num = Base32.decode(code);
// 2.获取低36位,payload
long payload = num & PAYLOAD_MASK;
// 3.获取高14位,校验码
int checkCode = (int) (num >>> CHECK_CODE_BIT_OFFSET);
// 4.载荷异或大质数,解析出原来的payload
payload ^= XOR_TABLE[(checkCode & FRESH_MASK)];
// 5.获取高4位,fresh
int fresh = (int) (payload >>> FRESH_BIT_OFFSET & FRESH_MASK);
// 6.验证格式:
if (calcCheckCode(payload, fresh) != checkCode) {
throw new BadRequestException("无效兑换码");
}
return payload & SERIAL_NUM_MASK;
}
}
8.3把id改成自己填IdType.INPUT(ExchangeCode.java)
一定一定要改
@TableId(value = "id", type = IdType.INPUT)
private Integer id;
package com.tianji.promotion.domain.po;
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("exchange_code")
public class ExchangeCode implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 兑换码id
*/
@TableId(value = "id", type = IdType.INPUT)
private Integer id;
/**
* 兑换码
*/
private String code;
/**
* 兑换码状态, 1:待兑换,2:已兑换,3:兑换活动已结束
*/
private ExchangeCodeStatus status;
/**
* 兑换人
*/
private Long userId;
/**
* 兑换类型,1:优惠券,以后再添加其它类型
*/
private Integer type;
/**
* 兑换码目标id,例如兑换优惠券,该id则是优惠券的配置id
*/
private Long exchangeTargetId;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 兑换码过期时间
*/
private LocalDateTime expiredTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
8.4CouponController.java
package com.tianji.promotion.controller;
// TODO 发放优惠券
@PutMapping("/{id}/issue")
@ApiOperation("发放优惠券")
public void BeginIssueCoupon(@RequestBody CouponIssueFormDTO dto){
couponService.BeginIssueCoupon(dto);
}
8.5interface ICouponService
package com.tianji.promotion.service;
// 发放优惠券
void BeginIssueCoupon(CouponIssueFormDTO dto);
8.6CouponServiceImpl.java
package com.tianji.promotion.service.impl;
// 发放优惠券
@Override
public void BeginIssueCoupon(CouponIssueFormDTO dto) {
// 1.查询优惠券
Coupon coupon = getById(dto.getId());
if (coupon == null){
throw new BadRequestException("该优惠券不存在!");
}
// 2.判断优惠券状态,是否是暂停或待发放
if (coupon.getStatus() != CouponStatus.DRAFT && coupon.getStatus() != CouponStatus.PAUSE){
throw new BizIllegalException("优惠券状态错误!");
}
// 3.判断是否是立刻发放
LocalDateTime issueBeginTime = dto.getIssueBeginTime();
LocalDateTime now = LocalDateTime.now();
// 开始时间为null或者发送时间小于等于当前时间,都代表立刻发放
boolean isBegin = issueBeginTime == null || !issueBeginTime.isAfter(now);
// 4.更新优惠券
// 4.1拷贝属性
Coupon c = BeanUtils.copyBean(dto, Coupon.class);
// 4.2更新状态
if (isBegin){
c.setStatus(CouponStatus.ISSUING);
c.setIssueBeginTime(now);
}else {
c.setStatus(CouponStatus.UN_ISSUE);
}
// 4.3写入数据库
updateById(c);
// 5.判断是否需要生成兑换码,优惠券类型必须是兑换码,优惠券状态必须是待发放
if (coupon.getObtainWay() == ObtainType.ISSUE && coupon.getStatus() == CouponStatus.DRAFT){
coupon.setIssueEndTime(c.getIssueEndTime());
// 异步生成兑换码
codeService.asyncGenerateCode(coupon);
}
}
8.7interface IExchangeCodeService
package com.tianji.promotion.service;
// 异步生成兑换码
void asyncGenerateCode(Coupon coupon);
8.8ExchangeCodeServiceImpl.java
package com.tianji.promotion.service.impl;
@Service
public class ExchangeCodeServiceImpl extends ServiceImpl<ExchangeCodeMapper, ExchangeCode> implements IExchangeCodeService {
private BoundValueOperations<String, String> serialOps;
public ExchangeCodeServiceImpl(StringRedisTemplate redisTemplate) {
this.serialOps = redisTemplate.boundValueOps(COUPON_CODE_SERIAL_KEY);
}
// 异步生成兑换码
@Override
@Async("generateExchangeCodeExecutor")
public void asyncGenerateCode(Coupon coupon) {
// 发放数量
Integer totalNum = coupon.getTotalNum();
// 1.获取Redis自增序列号
Long result = serialOps.increment(totalNum);
if (result == null) {
return;
}
int maxSerialNum = result.intValue();
List<ExchangeCode> list = new ArrayList<>(totalNum);
for (int serialNum = maxSerialNum - totalNum + 1; serialNum <= maxSerialNum; serialNum++) {
// 2.生成兑换码
String code = CodeUtil.generateCode(serialNum, coupon.getId());
ExchangeCode e = new ExchangeCode();
e.setCode(code);
e.setId(serialNum);
e.setExchangeTargetId(coupon.getId());
e.setExpiredTime(coupon.getIssueEndTime());
list.add(e);
}
// 3.保存数据库
saveBatch(list);
}
}
9.优惠券管理-定时开始发放优惠券
9.1PointsBoardPersistentHandler.java
package com.tianji.promotion.handler;
@Component
@Slf4j
@RequiredArgsConstructor
public class PointsBoardPersistentHandler {
private final ICouponService couponService;
// 定时开始发放优惠券
@XxlJob("BeginIssueCoupon")
public void BeginIssueCoupon(){
// 定时更新优惠券状态
couponService.BeginIssueCoupon();
}
}
9.2interface ICouponService
package com.tianji.promotion.service;
// 定时开始发放优惠券
void BeginIssueCoupon();
9.3CouponServiceImpl.java
package com.tianji.promotion.service.impl;
// 定时开始发放优惠券(支持分片处理)
@Override
public void BeginIssueCoupon() {
LocalDateTime now = LocalDateTime.now();
int index = XxlJobHelper.getShardIndex();
int total = XxlJobHelper.getShardTotal();
int pageNo = index + 1;
int pageSize = 1000;
Page<Coupon> page;
List<Coupon> records;
while (true) {
// 分页查询 状态为【未开始】 且 发放开始时间 <= 当前时间 的优惠券
page = lambdaQuery()
.eq(Coupon::getStatus, CouponStatus.UN_ISSUE)
.le(Coupon::getIssueBeginTime, now)
.page(new Page<>(pageNo, pageSize));
pageNo += total; // 分片后翻页
records = page.getRecords();
if (CollUtils.isEmpty(records)) {
log.debug("暂无需要发放的优惠券");
break;
}
// 提取所有优惠券 ID
List<Long> ids = records.stream()
.map(Coupon::getId)
.collect(Collectors.toList());
// 批量更新
lambdaUpdate()
.in(Coupon::getId, ids)
.eq(Coupon::getStatus, CouponStatus.UN_ISSUE)
.set(Coupon::getStatus, CouponStatus.ISSUING)
.update();
log.info("发现 {} 个优惠券需要从【未开始】更新为【发放中】", ids.size());
}
}
10.优惠券管理-定时结束发放优惠券
10.1PointsBoardPersistentHandler.java
package com.tianji.promotion.handler;
// 定时结束发放优惠券
@XxlJob("EndIssueCoupon")
public void EndIssueCoupon(){
// 定时结束优惠券状态
couponService.EndIssueCoupon();
}
10.2interface ICouponService.java
package com.tianji.promotion.service;
// 定时结束优惠券状态
void EndIssueCoupon();
10.3CouponServiceImpl.java
package com.tianji.promotion.service.impl;
// 定时结束优惠券状态(支持分片处理)
@Override
public void EndIssueCoupon() {
LocalDateTime now = LocalDateTime.now();
// 查询:状态为“发放中” 且 发放结束时间 <= 当前时间
List<Coupon> list = lambdaQuery()
.eq(Coupon::getStatus, CouponStatus.ISSUING) // 正在发放中
.le(Coupon::getIssueEndTime, now) // 已过结束时间
.list();
if (CollUtils.isEmpty(list)) {
log.debug("暂无需要结束的优惠券");
return;
}
// 提取所有需要更新的优惠券ID
List<Long> ids = list.stream()
.map(Coupon::getId)
.collect(Collectors.toList());
// 批量更新为【已结束】
lambdaUpdate()
.in(Coupon::getId, ids)
.eq(Coupon::getStatus, CouponStatus.ISSUING) // 乐观锁:防止并发重复更新
.set(Coupon::getStatus, CouponStatus.FINISHED)
.update();
log.info("发现 {} 个优惠券需要从【发放中】更新为【已结束】", ids.size());
}
11.优惠券管理-暂停发放优惠券
11.1CouponController.java
package com.tianji.promotion.controller;
// TODO 暂停发放优惠券
@PutMapping("/{id}/pause")
@ApiOperation("暂停发放优惠券")
public void stopCouponStatus(@PathVariable Long id){
couponService.stopCouponStatus(id);
}
11.2interface ICouponService
package com.tianji.promotion.service;
// 暂停发放优惠券
void stopCouponStatus(Long id);
11.3CouponServiceImpl.java
package com.tianji.promotion.service.impl;
// 暂停发放优惠券
@Override
public void stopCouponStatus(Long id) {
// 设置状态为暂停
lambdaUpdate()
.set(Coupon::getStatus,CouponStatus.PAUSE)
.eq(Coupon::getId,id)
.update();
}
12.优惠券管理-分页查询兑换码
12.1ExchangeCodeController.java
package com.tianji.promotion.controller;
@RestController
@RequestMapping("/codes")
@Api(tags = "兑换码相关接口")
@RequiredArgsConstructor
public class ExchangeCodeController {
private final IExchangeCodeService codeService;
// TODO 分页查询兑换码
@GetMapping("/page")
@ApiOperation("分页查询兑换码")
public PageDTO<ExchangeCodePageVO> queryExchangeCodePage(ExchangeCodeQuery query){
return codeService.queryExchangeCodePage(query);
}
}
12.2interface IExchangeCodeService
package com.tianji.promotion.service;
// 分页查询兑换码
PageDTO<ExchangeCodePageVO> queryExchangeCodePage(ExchangeCodeQuery query);
12.3ExchangeCodeServiceImpl.java
package com.tianji.promotion.service.impl;
// 分页查询兑换码
@Override
public PageDTO<ExchangeCodePageVO> queryExchangeCodePage(ExchangeCodeQuery query) {
// 分页查询
Page<ExchangeCode> page = lambdaQuery()
.eq(query.getId() != null,ExchangeCode::getId, query.getId())
.eq(query.getStatus() != null,ExchangeCode::getStatus, query.getStatus())
.page(query.toMpPageDefaultSortByCreateTimeDesc());
List<ExchangeCode> records = page.getRecords();
if (CollUtils.isEmpty(records)){
return PageDTO.empty(page);
}
List<ExchangeCodePageVO> voList = new ArrayList<>(records.size());
for (ExchangeCode e : records) {
ExchangeCodePageVO vo = new ExchangeCodePageVO();
vo.setId(e.getId());
vo.setCode(e.getCode());
voList.add(vo);
}
return PageDTO.of(page,voList);
}
day10.领取优惠券
1.查询发放中的优惠券
1.1CouponController.java
package com.tianji.promotion.controller;
// TODO 查询发放中的优惠券列表
@GetMapping("/list")
@ApiOperation("查询发放中的优惠券列表")
public List<CouponVO> queryIssuingCoupon(){
return couponService.queryIssuingCoupon();
}
1.2interface ICouponService
package com.tianji.promotion.service;
// 查询发放中的优惠券列表
List<CouponVO> queryIssuingCoupon();
1.3CouponServiceImpl.java
package com.tianji.promotion.service.impl;
// 查询发放中的优惠券列表
@Override
public List<CouponVO> queryIssuingCoupon() {
// 1.查询发放中的优惠券列表
List<Coupon> coupons = lambdaQuery()
.eq(Coupon::getStatus, CouponStatus.ISSUING)
.eq(Coupon::getObtainWay, ObtainType.PUBLIC)
.list();
if (CollUtils.isEmpty(coupons)){
return CollUtils.emptyList();
}
// 2.统计当前用户已经领取的优惠券信息
List<Long> couponIds = coupons.stream().map(Coupon::getId).collect(Collectors.toList());
// 2.1查询当前用户已经领取的优惠券的数据
List<UserCoupon> userCoupons = userCouponService.lambdaQuery()
.eq(UserCoupon::getUserId, UserContext.getUser())
.in(UserCoupon::getCouponId, couponIds)
.list();
// 2.2统计当前用户对优惠券的已经领取数量
Map<Long, Long> issuedMap = userCoupons.stream().collect(Collectors.groupingBy(UserCoupon::getCouponId, Collectors.counting()));
// 2.3统计当前用户对优惠券的已经领取并且未使用的数量
Map<Long, Long> unusedMap = userCoupons.stream()
.filter(us -> us.getStatus() == UserCouponStatus.UNUSED)
.collect(Collectors.groupingBy(UserCoupon::getCouponId, Collectors.counting()));
// 3.封装VO结果
List<CouponVO> list = new ArrayList<>(coupons.size());
for (Coupon c : coupons) {
// 3.1拷贝PO属性到VO
CouponVO vo = BeanUtils.copyBean(c, CouponVO.class);
list.add(vo);
// 3.2是否可以领取,已经被领取的数量 < 优惠券的总数量 && 当前用户已经领取的数量 < 每人限领数量
vo.setAvailable(
c.getIssueNum() < c.getTotalNum()
&& issuedMap.getOrDefault(c.getId(),0L) < c.getUserLimit()
);
// 3.3是否可以使用,当前用户已经领取并且未使用的优惠券数量 > 0
vo.setReceived(unusedMap.getOrDefault(c.getId(),0L) > 0);
}
return list;
}
2.解决登录拦截放行问题

2.1bootstrap.yml
server:
port: 8092 #端口
tomcat:
uri-encoding: UTF-8 #服务编码
spring:
profiles:
active: dev
application:
name: promotion-service
cloud:
nacos:
config:
file-extension: yaml
shared-configs: # 共享配置
- data-id: shared-spring.yaml # 共享spring配置
refresh: false
- data-id: shared-redis.yaml # 共享redis配置
refresh: false
- data-id: shared-mybatis.yaml # 共享mybatis配置
refresh: false
- data-id: shared-logs.yaml # 共享日志配置
refresh: false
- data-id: shared-feign.yaml # 共享feign配置
refresh: false
- data-id: shared-xxljob.yaml # 共享xxljob配置
refresh: false
tj:
swagger:
enable: true
enableResponseWrap: true
package-path: com.tianji.promotion.controller
title: 天机学堂 - 促销中心接口文档
description: 该服务包含优惠促销的相关功能
contact-name: 传智教育·研究院
contact-url: http://www.itcast.cn/
contact-email: zhanghuyi@itcast.cn
version: v1.0
jdbc:
database: tj_promotion
auth:
resource:
enable: true #开启登录拦截的功能
exclude-login-paths: #添加排除拦截路径
- /coupons/list
3.领取优惠券
3.1UserCouponController.java
package com.tianji.promotion.controller;
@RestController
@RequestMapping("/user-coupons")
@Api(tags = "用户优惠券相关接口")
@RequiredArgsConstructor
public class UserCouponController {
private final IUserCouponService userCouponService;
// TODO 领取优惠券
@ApiOperation("领取优惠券")
@PostMapping("/{couponId}/receive")
public void receiveCoupon(@PathVariable("couponId") Long couponId){
userCouponService.receiveCoupon(couponId);
}
}
3.2interface IUserCouponService
package com.tianji.promotion.service;
// 领取优惠券
void receiveCoupon(Long couponId);
3.3UserCouponServiceImpl.java
package com.tianji.promotion.service.impl;
@Service
@RequiredArgsConstructor
public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {
private final CouponMapper couponMapper;
// 领取优惠券
@Override
@Transactional
public void receiveCoupon(Long couponId) {
// 1.查询优惠券
Coupon coupon = couponMapper.selectById(couponId);
if (coupon == null) {
throw new BadRequestException("优惠券不存在!");
}
// 2.校验发放时间
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())){
throw new BadRequestException("优惠券尚未开始发放或已过期!");
}
// 3.校验库存
if (coupon.getIssueNum() >= coupon.getTotalNum()) {
throw new BadRequestException("优惠券库存不足!");
}
// 4.校验每人领取数量
Long userId = UserContext.getUser();
// 4.1统计当前用户对当前优惠券的已经领取数量
Integer count = lambdaQuery()
.eq(UserCoupon::getUserId, userId)
.eq(UserCoupon::getCouponId, couponId)
.count();
// 4.2判断
if (count != null && count >= coupon.getUserLimit()){
throw new BadRequestException("优惠券领取次数太多,需使用完再次领取!");
}
// 5.更新优惠券的已经发放数量+1
couponMapper.incrIssueNum(couponId);
// 6.新增一个用户劵
UserCoupon uc = new UserCoupon();
uc.setUserId(userId);
uc.setCouponId(couponId);
LocalDateTime termBeginTime = coupon.getTermBeginTime();
LocalDateTime termEndTime = coupon.getTermEndTime();
if (termBeginTime == null){
termBeginTime = LocalDateTime.now();
termEndTime = termBeginTime.plusDays(coupon.getTermDays());
}
uc.setTermBeginTime(termBeginTime);
uc.setTermEndTime(termEndTime);
// 新增保存到数据库
save(uc);
}
}
3.4CouponMapper.java
package com.tianji.promotion.mapper;
public interface CouponMapper extends BaseMapper<Coupon> {
// 更新优惠券的已经发放数量+1
@Update("update coupon set issue_num = issue_num + 1 where id = #{couponId}")
void incrIssueNum(Long couponId);
}
4.兑换码兑换优惠券
4.1UserCouponController.java
package com.tianji.promotion.controller;
// TODO 兑换码兑换优惠券
@ApiOperation("兑换码兑换优惠券")
@PostMapping("/{code}/exchange")
public void exchangeCoupon(@PathVariable("code") String code){
userCouponService.exchangeCoupon(code);
}
4.2interface IUserCouponService
package com.tianji.promotion.service;
// 兑换码兑换优惠券
void exchangeCoupon(String code);
4.3UserCouponServiceImpl.java
package com.tianji.promotion.service.impl;
@Service
@RequiredArgsConstructor
public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {
private final CouponMapper couponMapper;
private final IExchangeCodeService codeService;
// 兑换码兑换优惠券
@Override
@Transactional
public void exchangeCoupon(String code) {
// 1.校验并解析兑换码
long serialNum = CodeUtil.parseCode(code);
// 2.校验是否已经兑换(Redis) SETBIT KEY 4 1
boolean exchanged = codeService.updateExchangeMark(serialNum,true);
if (exchanged){
throw new BizIllegalException("兑换码已经被兑换过了");
}
try {
// 3.查询兑换码(MySql)
ExchangeCode exchangeCode = codeService.getById(serialNum);
if (exchangeCode == null) {
throw new BizIllegalException("兑换码不存在");
}
// 4.是否过期(MySql)
LocalDateTime now = LocalDateTime.now();
if (now.isAfter(exchangeCode.getExpiredTime())){
throw new BizIllegalException("兑换码已过期");
}
// 5.校验每人领取数量
Coupon coupon = couponMapper.selectById(exchangeCode.getExchangeTargetId());
Long userId = UserContext.getUser();
// 4.1统计当前用户对当前优惠券的已经领取数量
Integer count = lambdaQuery()
.eq(UserCoupon::getUserId, userId)
.eq(UserCoupon::getCouponId, coupon.getId())
.count();
// 4.2判断
if (count != null && count >= coupon.getUserLimit()){
throw new BadRequestException("优惠券领取次数太多,需使用完再次领取!");
}
// 5.更新优惠券的已经发放数量+1
couponMapper.incrIssueNum(coupon.getId());
// 6.新增一个用户劵
UserCoupon uc = new UserCoupon();
uc.setUserId(userId);
uc.setCouponId(coupon.getId());
LocalDateTime termBeginTime = coupon.getTermBeginTime();
LocalDateTime termEndTime = coupon.getTermEndTime();
if (termBeginTime == null){
termBeginTime = LocalDateTime.now();
termEndTime = termBeginTime.plusDays(coupon.getTermDays());
}
uc.setTermBeginTime(termBeginTime);
uc.setTermEndTime(termEndTime);
// 新增保存到数据库
save(uc);
// 8.更新兑换码状态
codeService.lambdaUpdate()
.set(ExchangeCode::getUserId,userId)
.set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
.eq(ExchangeCode::getId,exchangeCode.getId())
.update();
} catch (Exception e) {
// 重置兑换的标记 SETBIT KEY 4 0
codeService.updateExchangeMark(serialNum,false);
throw e;
}
}
}
4.4CouponMapper.java
package com.tianji.promotion.mapper;
public interface CouponMapper extends BaseMapper<Coupon> {
// 更新优惠券的已经发放数量+1
@Update("update coupon set issue_num = issue_num + 1 where id = #{couponId}")
void incrIssueNum(Long couponId);
}
5.并发安全问题-乐观锁解决超卖问题
5.1UserCouponServiceImpl.java
package com.tianji.promotion.service.impl;
@Service
@RequiredArgsConstructor
public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {
private final CouponMapper couponMapper;
private final IExchangeCodeService codeService;
// 领取优惠券
@Override
@Transactional
public void receiveCoupon(Long couponId) {
// 1.查询优惠券
Coupon coupon = couponMapper.selectById(couponId);
if (coupon == null) {
throw new BadRequestException("优惠券不存在!");
}
// 2.校验发放时间
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())){
throw new BadRequestException("优惠券尚未开始发放或已过期!");
}
// 3.校验库存
if (coupon.getIssueNum() >= coupon.getTotalNum()) {
throw new BadRequestException("优惠券库存不足!");
}
// 4.校验每人领取数量
Long userId = UserContext.getUser();
// 4.1统计当前用户对当前优惠券的已经领取数量
Integer count = lambdaQuery()
.eq(UserCoupon::getUserId, userId)
.eq(UserCoupon::getCouponId, couponId)
.count();
// 4.2判断
if (count != null && count >= coupon.getUserLimit()){
throw new BadRequestException("优惠券领取次数太多,需使用完再次领取!");
}
// 5.更新优惠券的已经发放数量+1
int c = couponMapper.incrIssueNum(couponId);
if (c == 0) {
throw new BizIllegalException("优惠券库存不足");
}
// 6.新增一个用户劵
UserCoupon uc = new UserCoupon();
uc.setUserId(userId);
uc.setCouponId(couponId);
LocalDateTime termBeginTime = coupon.getTermBeginTime();
LocalDateTime termEndTime = coupon.getTermEndTime();
if (termBeginTime == null){
termBeginTime = LocalDateTime.now();
termEndTime = termBeginTime.plusDays(coupon.getTermDays());
}
uc.setTermBeginTime(termBeginTime);
uc.setTermEndTime(termEndTime);
// 新增保存到数据库
save(uc);
}
// 兑换码兑换优惠券
@Override
@Transactional
public void exchangeCoupon(String code) {
// 1.校验并解析兑换码
long serialNum = CodeUtil.parseCode(code);
// 2.校验是否已经兑换(Redis) SETBIT KEY 4 1
boolean exchanged = codeService.updateExchangeMark(serialNum,true);
if (exchanged){
throw new BizIllegalException("兑换码已经被兑换过了");
}
try {
// 3.查询兑换码(MySql)
ExchangeCode exchangeCode = codeService.getById(serialNum);
if (exchangeCode == null) {
throw new BizIllegalException("兑换码不存在");
}
// 4.是否过期(MySql)
LocalDateTime now = LocalDateTime.now();
if (now.isAfter(exchangeCode.getExpiredTime())){
throw new BizIllegalException("兑换码已过期");
}
// 5.校验每人领取数量
Coupon coupon = couponMapper.selectById(exchangeCode.getExchangeTargetId());
Long userId = UserContext.getUser();
// 4.1统计当前用户对当前优惠券的已经领取数量
Integer count = lambdaQuery()
.eq(UserCoupon::getUserId, userId)
.eq(UserCoupon::getCouponId, coupon.getId())
.count();
// 4.2判断
if (count != null && count >= coupon.getUserLimit()){
throw new BadRequestException("优惠券领取次数太多,需使用完再次领取!");
}
// 5.更新优惠券的已经发放数量+1
int c = couponMapper.incrIssueNum(coupon.getId());
if (c == 0) {
throw new BizIllegalException("优惠券库存不足");
}
// 6.新增一个用户劵
UserCoupon uc = new UserCoupon();
uc.setUserId(userId);
uc.setCouponId(coupon.getId());
LocalDateTime termBeginTime = coupon.getTermBeginTime();
LocalDateTime termEndTime = coupon.getTermEndTime();
if (termBeginTime == null){
termBeginTime = LocalDateTime.now();
termEndTime = termBeginTime.plusDays(coupon.getTermDays());
}
uc.setTermBeginTime(termBeginTime);
uc.setTermEndTime(termEndTime);
// 新增保存到数据库
save(uc);
// 8.更新兑换码状态
codeService.lambdaUpdate()
.set(ExchangeCode::getUserId,userId)
.set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
.eq(ExchangeCode::getId,exchangeCode.getId())
.update();
} catch (Exception e) {
// 重置兑换的标记 SETBIT KEY 4 0
codeService.updateExchangeMark(serialNum,false);
throw e;
}
}
}
5.2CouponMapper.java
package com.tianji.promotion.mapper;
public interface CouponMapper extends BaseMapper<Coupon> {
// 更新优惠券的已经发放数量+1
@Update("update coupon set issue_num = issue_num + 1 where id = #{couponId} and issue_num < total_num")
int incrIssueNum(@Param("couponId") Long couponId);
}
6.并发安全问题-锁失效和锁边界问题
把校验并生成用户券抽取成checkAndCreateUser方法
6.1UserCouponServiceImpl.java
package com.tianji.promotion.service.impl;
@Service
@RequiredArgsConstructor
public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {
private final CouponMapper couponMapper;
private final IExchangeCodeService codeService;
// 领取优惠券
@Override
public void receiveCoupon(Long couponId) {
// 1.查询优惠券
Coupon coupon = couponMapper.selectById(couponId);
if (coupon == null) {
throw new BadRequestException("优惠券不存在!");
}
// 2.校验发放时间
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())) {
throw new BadRequestException("优惠券尚未开始发放或已过期!");
}
// 3.校验库存
if (coupon.getIssueNum() >= coupon.getTotalNum()) {
throw new BadRequestException("优惠券库存不足!");
}
Long userId = UserContext.getUser();
// 4.校验并生成用户券
// 加入悲观锁
synchronized (userId.toString().intern()) {
checkAndCreateUser(userId, coupon);
}
}
// 兑换码兑换优惠券
@Override
public void exchangeCoupon(String code) {
// 1.校验并解析兑换码
long serialNum = CodeUtil.parseCode(code);
// 2.校验是否已经兑换(Redis) SETBIT KEY 4 1
boolean exchanged = codeService.updateExchangeMark(serialNum, true);
if (exchanged) {
throw new BizIllegalException("兑换码已经被兑换过了");
}
try {
// 3.查询兑换码(MySql)
ExchangeCode exchangeCode = codeService.getById(serialNum);
if (exchangeCode == null) {
throw new BizIllegalException("兑换码不存在");
}
// 4.是否过期(MySql)
LocalDateTime now = LocalDateTime.now();
if (now.isAfter(exchangeCode.getExpiredTime())) {
throw new BizIllegalException("兑换码已过期");
}
Coupon coupon = couponMapper.selectById(exchangeCode.getExchangeTargetId());
Long userId = UserContext.getUser();
// 5.校验并生成用户券
// 加入悲观锁
synchronized (userId.toString().intern()) {
checkAndCreateUser(userId, coupon);
}
// 6.更新兑换码状态
codeService.lambdaUpdate()
.set(ExchangeCode::getUserId, userId)
.set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
.eq(ExchangeCode::getId, exchangeCode.getId())
.update();
} catch (Exception e) {
// 重置兑换的标记 SETBIT KEY 4 0
codeService.updateExchangeMark(serialNum, false);
throw e;
}
}
// 校验并生成用户券(方法)
@Transactional
public void checkAndCreateUser(Long userId, Coupon coupon) {
// 1.1统计当前用户对当前优惠券的已经领取数量
Integer count = lambdaQuery()
.eq(UserCoupon::getUserId, userId)
.eq(UserCoupon::getCouponId, coupon.getId())
.count();
// 1.2判断
if (count != null && count >= coupon.getUserLimit()) {
throw new BadRequestException("优惠券领取次数太多,需使用完再次领取!");
}
// 2.更新优惠券的已经发放数量+1
// 加入乐观锁incrIssueNum
int c = couponMapper.incrIssueNum(coupon.getId());
if (c == 0) {
throw new BizIllegalException("优惠券库存不足");
}
// 3.新增一个用户劵
UserCoupon uc = new UserCoupon();
uc.setUserId(userId);
uc.setCouponId(coupon.getId());
LocalDateTime termBeginTime = coupon.getTermBeginTime();
LocalDateTime termEndTime = coupon.getTermEndTime();
if (termBeginTime == null) {
termBeginTime = LocalDateTime.now();
termEndTime = termBeginTime.plusDays(coupon.getTermDays());
}
uc.setTermBeginTime(termBeginTime);
uc.setTermEndTime(termEndTime);
// 新增保存到数据库
save(uc);
}
}
7.并发安全问题-事务失效问题


7.1UserCouponServiceImpl.java
package com.tianji.promotion.service.impl;
@Service
@RequiredArgsConstructor
public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {
private final CouponMapper couponMapper;
private final IExchangeCodeService codeService;
// 领取优惠券
@Override
public void receiveCoupon(Long couponId) {
// 1.查询优惠券
Coupon coupon = couponMapper.selectById(couponId);
if (coupon == null) {
throw new BadRequestException("优惠券不存在!");
}
// 2.校验发放时间
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())) {
throw new BadRequestException("优惠券尚未开始发放或已过期!");
}
// 3.校验库存
if (coupon.getIssueNum() >= coupon.getTotalNum()) {
throw new BadRequestException("优惠券库存不足!");
}
Long userId = UserContext.getUser();
// 4.校验并生成用户券
// 加入悲观锁
synchronized (userId.toString().intern()) {
IUserCouponService userCouponService = (IUserCouponService) AopContext.currentProxy();
userCouponService.checkAndCreateUser(userId, coupon);
}
}
// 兑换码兑换优惠券
@Override
public void exchangeCoupon(String code) {
// 1.校验并解析兑换码
long serialNum = CodeUtil.parseCode(code);
// 2.校验是否已经兑换(Redis) SETBIT KEY 4 1
boolean exchanged = codeService.updateExchangeMark(serialNum, true);
if (exchanged) {
throw new BizIllegalException("兑换码已经被兑换过了");
}
try {
// 3.查询兑换码(MySql)
ExchangeCode exchangeCode = codeService.getById(serialNum);
if (exchangeCode == null) {
throw new BizIllegalException("兑换码不存在");
}
// 4.是否过期(MySql)
LocalDateTime now = LocalDateTime.now();
if (now.isAfter(exchangeCode.getExpiredTime())) {
throw new BizIllegalException("兑换码已过期");
}
Coupon coupon = couponMapper.selectById(exchangeCode.getExchangeTargetId());
Long userId = UserContext.getUser();
// 5.校验并生成用户券
// 加入悲观锁
synchronized (userId.toString().intern()) {
IUserCouponService userCouponService = (IUserCouponService) AopContext.currentProxy();
userCouponService.checkAndCreateUser(userId, coupon);
}
// 6.更新兑换码状态
codeService.lambdaUpdate()
.set(ExchangeCode::getUserId, userId)
.set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
.eq(ExchangeCode::getId, exchangeCode.getId())
.update();
} catch (Exception e) {
// 重置兑换的标记 SETBIT KEY 4 0
codeService.updateExchangeMark(serialNum, false);
throw e;
}
}
// 校验并生成用户券(方法)
@Transactional
@Override
public void checkAndCreateUser(Long userId, Coupon coupon) {
// 1.1统计当前用户对当前优惠券的已经领取数量
Integer count = lambdaQuery()
.eq(UserCoupon::getUserId, userId)
.eq(UserCoupon::getCouponId, coupon.getId())
.count();
// 1.2判断
if (count != null && count >= coupon.getUserLimit()) {
throw new BadRequestException("优惠券领取次数太多,需使用完再次领取!");
}
// 2.更新优惠券的已经发放数量+1
// 加入乐观锁incrIssueNum
int c = couponMapper.incrIssueNum(coupon.getId());
if (c == 0) {
throw new BizIllegalException("优惠券库存不足");
}
// 3.新增一个用户劵
UserCoupon uc = new UserCoupon();
uc.setUserId(userId);
uc.setCouponId(coupon.getId());
LocalDateTime termBeginTime = coupon.getTermBeginTime();
LocalDateTime termEndTime = coupon.getTermEndTime();
if (termBeginTime == null) {
termBeginTime = LocalDateTime.now();
termEndTime = termBeginTime.plusDays(coupon.getTermDays());
}
uc.setTermBeginTime(termBeginTime);
uc.setTermEndTime(termEndTime);
// 新增保存到数据库
save(uc);
}
}
7.2interface IUserCouponService
package com.tianji.promotion.service;
public interface IUserCouponService extends IService<UserCoupon> {
// 校验并生成用户券(方法)
void checkAndCreateUser(Long userId, Coupon coupon);
}
8.查询发放中的优惠券
8.1UserCouponController.java
package com.tianji.promotion.controller;
// TODO 查询发放中的优惠券
@ApiOperation("查询发放中的优惠券")
@GetMapping("/page")
public PageDTO<UserCouponPageVO> queryUserCouponPage(UserCouponQuery query){
return userCouponService.queryUserCouponPage(query);
}
8.2interface IUserCouponService
package com.tianji.promotion.service;
// 查询发放中的优惠券
PageDTO<UserCouponPageVO> queryUserCouponPage(UserCouponQuery query);
8.3UserCouponServiceImpl.java
package com.tianji.promotion.service.impl;
// 查询发放中的优惠券
@Override
public PageDTO<UserCouponPageVO> queryUserCouponPage(UserCouponQuery query) {
// 1.分页查询
Page<UserCoupon> page = lambdaQuery()
.eq(UserCoupon::getStatus, query.getStatus())
.page(query.toMpPageDefaultSortByCreateTimeDesc());
List<UserCoupon> records = page.getRecords();
if (CollUtils.isEmpty(records)){
return PageDTO.empty(page);
}
// 2.收集优惠券id集合
List<Long> couponIds = records.stream().map(UserCoupon::getCouponId).collect(Collectors.toList());
// 3.根据couponId查询coupon表信息
List<Coupon> coupons = couponMapper.selectBatchIds(couponIds);
// 4.属性拷贝
List<UserCouponPageVO> list = BeanUtils.copyList(coupons, UserCouponPageVO.class);
return PageDTO.of(page,list);
}
1274

被折叠的 条评论
为什么被折叠?



