实用指南:天机学堂day09学习
一、优惠券需求分析
1.1.业务流程梳理

1.2.接口统计

1.3.表结构设计
1.3.1.优惠券表结构设计

1.3.2.兑换码表结构设计

1.4.代码生成
1.4.1.创建新的模块 Promotion

1.4.2.基础配置
1.4.2.1.POM文件
tjxt
com.tianji
1.0.0
4.0.0
tj-promotion
11
11
com.tianji
tj-auth-resource-sdk
1.0.0
com.tianji
tj-api
1.0.0
org.springframework.boot
spring-boot-starter-web
com.baomidou
mybatis-plus-boot-starter
mysql
mysql-connector-java
org.springframework.boot
spring-boot-starter-data-redis
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
com.github.ben-manes.caffeine
caffeine
com.xuxueli
xxl-job-core
org.springframework.cloud
spring-cloud-starter-loadbalancer
${project.artifactId}
org.springframework.boot
spring-boot-maven-plugin
build-info
com.tianji.promotion.PromotionApplication
1.4.2.2.配置文件 bootstrap.yaml
server:
port: 8091 #端口
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-mq.yaml # 共享mq配置
# 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
1.4.2.3.启动类 PromotionApplication
package com.tianji.promotion;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.EnableScheduling;
import java.net.InetAddress;
import java.net.UnknownHostException;
@SpringBootApplication
@EnableScheduling // 开启定时任务
@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());
}
}
1.4.3.MyBatis-Plus生成基础代码

1.4.4.引入枚举

二、优惠券管理
2.1.新增优惠券
2.1.1.接口分析

2.1.2.实体准备
准备 CouponFormDTO
package com.tianji.promotion.domain.dto;
import com.tianji.common.validate.annotations.EnumValid;
import com.tianji.promotion.enums.DiscountType;
import com.tianji.promotion.enums.ObtainType;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.List;
@Data
@ApiModel(description = "优惠券表单数据")
public class CouponFormDTO {
@ApiModelProperty("优惠券id,新增不需要添加,更新必填")
private Long id;
@ApiModelProperty("优惠券名称")
@NotNull(message = "优惠券名称不能为空")
@Size(max = 20, min = 4, message = "优惠券名称长度错误")
private String name;
@ApiModelProperty("是否添限定使用范围,true:限定了,false:没限定")
private Boolean specific;
@ApiModelProperty("优惠券使用范围")
private List scopes;
@ApiModelProperty("优惠券类型,1:每满减,2:折扣,3:无门槛,4:普通满减")
@NotNull(message = "优惠券折扣类型不能为空")
@EnumValid(enumeration = {1,2,3,4})
private DiscountType discountType;
@ApiModelProperty("折扣门槛,0代表无门槛")
private Integer thresholdAmount;
@ApiModelProperty("折扣值,满减填抵扣金额;打折填折扣值:80标示打8折")
private Integer discountValue;
@ApiModelProperty("最大优惠金额")
private Integer maxDiscountAmount;
@ApiModelProperty("优惠券总量")
@Range(max = 5000, min = 1, message = "优惠券总量必须在1~5000")
private Integer totalNum;
@ApiModelProperty("每人领取的上限")
@Range(max = 10, min = 1, message = "每人限领数量必须在1~10")
private Integer userLimit;
@ApiModelProperty("获取方式1:手动领取,2:指定发放(通过兑换码兑换)")
@NotNull(message = "领取方式不能为空")
@EnumValid(enumeration = {1, 2}, message = "领取方式不正确")
private ObtainType obtainWay;
}
准备 po Coupon
package com.tianji.promotion.domain.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import java.io.Serializable;
import com.tianji.promotion.enums.CouponStatus;
import com.tianji.promotion.enums.DiscountType;
import com.tianji.promotion.enums.ObtainType;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
*
* 优惠券的规则信息
*
*
* @author 文涛
* @since 2025-11-24
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("coupon")
public class Coupon implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 优惠券id
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 优惠券名称,可以和活动名称保持一致
*/
@TableField("`name`") //与数据库关键字冲突,需要添加转义字符
private String name;
/**
* 优惠券类型,1:普通券。目前就一种,保留字段
*/
private Integer type;
/**
* 折扣类型,1:满减,2:每满减,3:折扣,4:无门槛
*/
private DiscountType discountType;
/**
* 是否限定作用范围,false:不限定,true:限定。默认false
*/
@TableField("`specific`") //与数据库关键字冲突,需要添加转义字符
private Boolean specific;
/**
* 折扣值,如果是满减则存满减金额,如果是折扣,则存折扣率,8折就是存80
*/
private Integer discountValue;
/**
* 使用门槛,0:表示无门槛,其他值:最低消费金额
*/
private Integer thresholdAmount;
/**
* 最高优惠金额,满减最大,0:表示没有限制,不为0,则表示该券有金额的限制
*/
private Integer maxDiscountAmount;
/**
* 获取方式:1:手动领取,2:兑换码
*/
private ObtainType obtainWay;
/**
* 开始发放时间
*/
private LocalDateTime issueBeginTime;
/**
* 结束发放时间
*/
private LocalDateTime issueEndTime;
/**
* 优惠券有效期天数,0:表示有效期是指定有效期的
*/
private Integer termDays;
/**
* 优惠券有效期开始时间
*/
private LocalDateTime termBeginTime;
/**
* 优惠券有效期结束时间
*/
private LocalDateTime termEndTime;
/**
* 优惠券配置状态,1:待发放,2:未开始 3:进行中,4:已结束,5:暂停
*/
private CouponStatus status;
/**
* 总数量,不超过5000
*/
private Integer totalNum;
/**
* 已发行数量,用于判断是否超发
*/
private Integer issueNum;
/**
* 已使用数量
*/
private Integer usedNum;
/**
* 每个人限领的数量,默认1
*/
private Integer userLimit;
/**
* 拓展参数字段,保留字段
*/
private String extParam;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 创建人
*/
private Long creater;
/**
* 更新人
*/
private Long updater;
}
2.1.3.接口实现
2.1.3.1.新增优惠券 CouponController
package com.tianji.promotion.controller;
import com.tianji.promotion.domain.dto.CouponFormDTO;
import com.tianji.promotion.service.ICouponService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
/**
*
* 优惠券的规则信息 前端控制器
*
*
* @author 文涛
* @since 2025-11-24
*/
@RestController
@RequestMapping("/coupons")
@Api(tags = "优惠券相关接口")
@RequiredArgsConstructor
public class CouponController {
private final ICouponService couponService;
@PostMapping()
@ApiOperation("新增优惠券")
public void saveCoupon(@RequestBody @Valid CouponFormDTO dto) {
couponService.saveCoupon(dto);
}
}
2.1.3.2.新增优惠券 ICouponService
package com.tianji.promotion.service;
import com.tianji.promotion.domain.dto.CouponFormDTO;
import com.tianji.promotion.domain.po.Coupon;
import com.baomidou.mybatisplus.extension.service.IService;
/**
*
* 优惠券的规则信息 服务类
*
*
* @author 文涛
* @since 2025-11-24
*/
public interface ICouponService extends IService {
void saveCoupon(CouponFormDTO dto);
}
2.1.3.3.新增优惠券 CouponServiceImpl
package com.tianji.promotion.service.impl;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.utils.BeanUtils;
import com.tianji.common.utils.CollUtils;
import com.tianji.promotion.domain.dto.CouponFormDTO;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.CouponScope;
import com.tianji.promotion.mapper.CouponMapper;
import com.tianji.promotion.service.ICouponScopeService;
import com.tianji.promotion.service.ICouponService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
/**
*
* 优惠券的规则信息 服务实现类
*
*
* @author 文涛
* @since 2025-11-24
*/
@Service
@RequiredArgsConstructor
public class CouponServiceImpl extends ServiceImpl implements ICouponService {
private final ICouponScopeService scopeService;
@Override
@Transactional
public void saveCoupon(CouponFormDTO dto) {
//1.保存优惠券
//1.1.转为PO
Coupon coupon = BeanUtils.copyBean(dto, Coupon.class);
//1.2.保存
save(coupon);
//判断是否有保存范围限制
if (!dto.getSpecific()) {
//没有范围限制
return;
}
Long couponId = coupon.getId();
//2.保存限定范围
List scopes = dto.getScopes();
if (CollUtils.isEmpty(scopes)) {
throw new BadRequestException("优惠券范围不能为空");
}
//2.1.转为PO
List list = scopes.stream()
.map(bizId -> new CouponScope().setBizId(bizId).setCouponId(couponId))
.collect(Collectors.toList());
//2.2.保存
scopeService.saveBatch(list);
}
}
2.1.3.4.测试
天机学堂管理端 账号:13500010002 密码:admin
天机学堂管理端地址:http://manage.tianji.com/
测试前需要修改 coupon_scope type 字段默认值为1,不然会报错



2.2.分页查询优惠券
2.1.1.接口分析


2.1.2.实体准备
QUERY实体:CouponQuery
package com.tianji.promotion.domain.query;
import com.tianji.common.domain.query.PageQuery;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
@EqualsAndHashCode(callSuper = true)
@Data
@ApiModel(description = "优惠券查询参数")
@Accessors(chain = true)
public class CouponQuery extends PageQuery {
@ApiModelProperty("优惠券折扣类型:1:每满减,2:折扣,3:无门槛,4:满减")
private Integer type;
@ApiModelProperty("优惠券状态,1:待发放,2:发放中,3:已结束, 4:取消/终止")
private Integer status;
@ApiModelProperty("优惠券名称")
private String name;
}
VO实体:CouponPageVO
package com.tianji.promotion.domain.vo;
import com.tianji.promotion.enums.CouponStatus;
import com.tianji.promotion.enums.DiscountType;
import com.tianji.promotion.enums.ObtainType;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@ApiModel(description = "优惠券分页数据")
public class CouponPageVO {
@ApiModelProperty("优惠券id,新增不需要添加,更新必填")
private Long id;
@ApiModelProperty("优惠券名称")
private String name;
@ApiModelProperty("是否限定使用范围")
private Boolean specific;
@ApiModelProperty("优惠券类型,1:每满减,2:折扣,3:无门槛,4:普通满减")
private DiscountType discountType;
@ApiModelProperty("折扣门槛,0代表无门槛")
private Integer thresholdAmount;
@ApiModelProperty("折扣值,满减填抵扣金额;打折填折扣值:80标示打8折")
private Integer discountValue;
@ApiModelProperty("最大优惠金额")
private Integer maxDiscountAmount;
@ApiModelProperty("获取方式1:手动领取,2:指定发放(通过兑换码兑换)")
private ObtainType obtainWay;
@ApiModelProperty("已使用")
private Integer usedNum;
@ApiModelProperty("已发放数量")
private Integer issueNum;
@ApiModelProperty("优惠券总量")
private Integer totalNum;
@ApiModelProperty("优惠券创建时间")
private LocalDateTime createTime;
@ApiModelProperty("发放开始时间")
private LocalDateTime issueBeginTime;
@ApiModelProperty("发放结束时间")
private LocalDateTime issueEndTime;
@ApiModelProperty("有效天数")
private Integer termDays;
@ApiModelProperty("使用有效期开始时间")
private LocalDateTime termBeginTime;
@ApiModelProperty("使用有效期结束时间")
private LocalDateTime termEndTime;
@ApiModelProperty("状态")
private CouponStatus status;
}
2.1.3.接口实现
2.1.3.1.分页查询优惠券 CouponController
package com.tianji.promotion.controller;
import com.tianji.common.domain.dto.PageDTO;
import com.tianji.common.domain.query.PageQuery;
import com.tianji.promotion.domain.dto.CouponFormDTO;
import com.tianji.promotion.domain.dto.CouponIssueFormDTO;
import com.tianji.promotion.domain.query.CouponQuery;
import com.tianji.promotion.domain.vo.CouponPageVO;
import com.tianji.promotion.service.ICouponService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
*
* 优惠券的规则信息 前端控制器
*
*
* @author 文涛
* @since 2025-11-24
*/
@RestController
@RequestMapping("/coupons")
@Api(tags = "优惠券相关接口")
@RequiredArgsConstructor
public class CouponController {
private final ICouponService couponService;
@GetMapping("page")
@ApiOperation("分页查询优惠券接口")
public PageDTO queryCouponByPage(CouponQuery query) {
return couponService.queryCouponByPage(query);
}
}
2.1.3.2.分页查询优惠券 ICouponService
PageDTO queryCouponByPage(CouponQuery query);
2.1.3.3.分页查询优惠券 CouponServiceImpl
@Override
public PageDTO queryCouponByPage(CouponQuery query) {
//1.分页查询
Integer status = query.getStatus();
String name = query.getName();
Integer type = query.getType();
Page page = lambdaQuery()
.eq(status != null, Coupon::getStatus, status)
.eq(type != null, Coupon::getDiscountType, type)
.like(StringUtils.isNotBlank(name), Coupon::getName, name)
.page(query.toMpPageDefaultSortByCreateTimeDesc());
//2.处理VO
List records = page.getRecords();
if (CollUtils.isEmpty(records)) {
return PageDTO.empty(page);
}
List list = BeanUtils.copyList(records, CouponPageVO.class);
//3.返回
return PageDTO.of(page, list);
}
2.1.3.4.测试

三、优惠券发放
3.1.发放优惠券
3.1.1.接口分析

3.1.2.实体准备
DTO实体 CouponIssueFormDTO
package com.tianji.promotion.domain.dto;
import com.tianji.common.utils.DateUtils;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.Future;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
@Data
@ApiModel(description = "优惠券发放的表单实体")
public class CouponIssueFormDTO {
@ApiModelProperty("优惠券id")
private Long id;
@ApiModelProperty("发放开始时间")
@DateTimeFormat(pattern = DateUtils.DEFAULT_DATE_TIME_FORMAT)
@Future(message = "发放开始时间必须晚于当前时间")
private LocalDateTime issueBeginTime;
@ApiModelProperty("发放结束时间")
@Future(message = "发放结束时间必须晚于当前时间")
@NotNull(message = "发放结束时间不能为空")
@DateTimeFormat(pattern = DateUtils.DEFAULT_DATE_TIME_FORMAT)
private LocalDateTime issueEndTime;
@ApiModelProperty("有效天数")
private Integer termDays;
@ApiModelProperty("使用有效期开始时间")
@DateTimeFormat(pattern = DateUtils.DEFAULT_DATE_TIME_FORMAT)
private LocalDateTime termBeginTime;
@ApiModelProperty("使用有效期结束时间")
@DateTimeFormat(pattern = DateUtils.DEFAULT_DATE_TIME_FORMAT)
private LocalDateTime termEndTime;
}
3.1.3.接口实现
3.1.3.1.发放优惠券 CouponController
package com.tianji.promotion.controller;
import com.tianji.common.domain.dto.PageDTO;
import com.tianji.common.domain.query.PageQuery;
import com.tianji.promotion.domain.dto.CouponFormDTO;
import com.tianji.promotion.domain.dto.CouponIssueFormDTO;
import com.tianji.promotion.domain.query.CouponQuery;
import com.tianji.promotion.domain.vo.CouponPageVO;
import com.tianji.promotion.service.ICouponService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
*
* 优惠券的规则信息 前端控制器
*
*
* @author 文涛
* @since 2025-11-24
*/
@RestController
@RequestMapping("/coupons")
@Api(tags = "优惠券相关接口")
@RequiredArgsConstructor
public class CouponController {
private final ICouponService couponService;
@ApiOperation("发放优惠券接口")
@PutMapping("/{id}/issue")
public void beginIssue(@RequestBody @Valid CouponIssueFormDTO dto) {
couponService.beginIssue(dto);
}
}
3.1.3.2.发放优惠券 ICouponService
void beginIssue(@Valid CouponIssueFormDTO dto);
3.1.3.3.发放优惠券 ICouponServiceImpl
@Transactional
@Override
public void beginIssue(CouponIssueFormDTO dto) {
// 1.查询优惠券
Coupon coupon = getById(dto.getId());
if (coupon == null) {
throw new BadRequestException("优惠券不存在!");
}
// 2.判断优惠券状态,是否是暂停或待发放
if(coupon.getStatus() != CouponStatus.DRAFT && coupon.getStatus() != PAUSE){
throw new BizIllegalException("优惠券状态错误!");
}
// 3.判断是否是立刻发放
LocalDateTime issueBeginTime = dto.getIssueBeginTime();
LocalDateTime now = LocalDateTime.now();
boolean isBegin = issueBeginTime == null || !issueBeginTime.isAfter(now);
// 4.更新优惠券
// 4.1.拷贝属性到PO
Coupon c = BeanUtils.copyBean(dto, Coupon.class);
// 4.2.更新状态
if (isBegin) {
c.setStatus(ISSUING);
c.setIssueBeginTime(now);
}else{
c.setStatus(UN_ISSUE);
}
// 4.3.写入数据库
updateById(c);
// TODO 兑换码生成
// 5.判断是否需要生成兑换码,优惠券类型必须是兑换码,优惠券状态必须是待发放
if(coupon.getObtainWay() == ObtainType.ISSUE && coupon.getStatus() == CouponStatus.DRAFT){
coupon.setIssueEndTime(c.getIssueEndTime());
codeService.asyncGenerateCode(coupon);
}
}
3.1.3.4.测试


3.2.兑换码生成算法
3.2.1.兑换码的需求

3.2.2.算法分析


3.2.3.算法实现

3.3.异步生成兑换码
3.3.1.思路分析

3.3.2.代码实现
3.3.2.1. PromotionConfig 中定义线程池
启动类添加@EnableAsync注解,开启异步功能
package com.tianji.promotion.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Slf4j
@Configuration
public class PromotionConfig {
@Bean
public Executor generateExchangeCodeExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 1.核心线程池大小
executor.setCorePoolSize(2);
// 2.最大线程池大小
executor.setMaxPoolSize(5);
// 3.队列大小
executor.setQueueCapacity(200);
// 4.线程名称
executor.setThreadNamePrefix("exchange-code-handler-");
// 5.拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
3.3.2.2. 改造 CouponServiceImpl 中的 beginIssue 方法
@Transactional
@Override
public void beginIssue(CouponIssueFormDTO dto) {
// 1.查询优惠券
Coupon coupon = getById(dto.getId());
if (coupon == null) {
throw new BadRequestException("优惠券不存在!");
}
// 2.判断优惠券状态,是否是暂停或待发放
if(coupon.getStatus() != CouponStatus.DRAFT && coupon.getStatus() != PAUSE){
throw new BizIllegalException("优惠券状态错误!");
}
// 3.判断是否是立刻发放
LocalDateTime issueBeginTime = dto.getIssueBeginTime();
LocalDateTime now = LocalDateTime.now();
boolean isBegin = issueBeginTime == null || !issueBeginTime.isAfter(now);
// 4.更新优惠券
// 4.1.拷贝属性到PO
Coupon c = BeanUtils.copyBean(dto, Coupon.class);
// 4.2.更新状态
if (isBegin) {
c.setStatus(ISSUING);
c.setIssueBeginTime(now);
}else{
c.setStatus(UN_ISSUE);
}
// 4.3.写入数据库
updateById(c);
// 新增 兑换码生成
// 5.判断是否需要生成兑换码,优惠券类型必须是兑换码,优惠券状态必须是待发放
if(coupon.getObtainWay() == ObtainType.ISSUE && coupon.getStatus() == CouponStatus.DRAFT){
coupon.setIssueEndTime(c.getIssueEndTime());
codeService.asyncGenerateCode(coupon);
}
}
3.3.2.3. 在 IExchangeCodeService 声明 asyncGenerateCode
package com.tianji.promotion.service;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.ExchangeCode;
import com.baomidou.mybatisplus.extension.service.IService;
/**
*
* 兑换码 服务类
*
*
* @author 文涛
* @since 2025-11-24
*/
public interface IExchangeCodeService extends IService {
void asyncGenerateCode(Coupon coupon);
}
3.3.2.4. ExchangeCodeServiceImpl 中实现 asyncGenerateCode
package com.tianji.promotion.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.common.utils.CollUtils;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.ExchangeCode;
import com.tianji.promotion.mapper.ExchangeCodeMapper;
import com.tianji.promotion.service.IExchangeCodeService;
import com.tianji.promotion.utils.CodeUtil;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import static com.tianji.promotion.constans.PromotionConstants.COUPON_CODE_SERIAL_KEY;
/**
*
* 兑换码 服务实现类
*
*
* @author 虎哥
*/
@Service
public class ExchangeCodeServiceImpl extends ServiceImpl implements IExchangeCodeService {
private final StringRedisTemplate redisTemplate;
private final BoundValueOperations serialOps;
public ExchangeCodeServiceImpl(StringRedisTemplate redisTemplate) {
this.redisTemplate = 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 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);
// // 4.写入Redis缓存,member:couponId,score:兑换码的最大序列号
// redisTemplate.opsForZSet().add(COUPON_RANGE_KEY, coupon.getId().toString(), maxSerialNum);
}
}
3.3.2.5.测试

四、练习
4.1.修改优惠券
4.1.1. 修改优惠券 CouponController
// TODO 修改优惠券
@PutMapping("/{id}")
@ApiOperation("修改优惠券")
public void updateCouponById(@Valid @RequestBody CouponFormDTO dto){
couponService.updateCouponById(dto);
}
4.1.1. 修改优惠券 CouponServiceImpl(CouponService省略)
// 修改优惠券
@Override
public void updateCouponById(CouponFormDTO dto) {
// 1.属性拷贝
Coupon coupon = BeanUtils.copyBean(dto, Coupon.class);
// 2.更新表数据
updateById(coupon);
}
4.2.删除优惠券
4.2.1. 删除优惠券 CouponController
// TODO 删除优惠券
@ApiOperation("删除优惠券--管理端")
@DeleteMapping("/{id}")
public void removeCouponById(@PathVariable("id") Long id) {
couponService.removeCouponById(id);
}
4.2.2. 删除优惠券 CouponServiceImpl
/**
* 删除优惠券--管理端
*
* @param id
*/
@Override
public void removeCouponById(Long id) {
//1、校验
if (id == null) {
throw new BadRequestException("非法参数!");
}
//2、查询优惠券表
Coupon coupon = this.getById(id);
if (coupon == null) {
throw new BadRequestException("优惠券不存在!");
}
//3、判断优惠券配置状态
if (coupon.getStatus() != CouponStatus.DRAFT) {
throw new BadRequestException("该优惠券不处于待发放状态,不能删除!");
}
//4、删除优惠券
this.removeById(id);
}
4.3.根据id查询优惠券
4.3.1. 根据id查询优惠券 CouponController
// TODO 根据id查询优惠券
@GetMapping("/{id}")
@ApiOperation("根据id查询优惠券")
public Coupon getCouponById(@PathVariable Long id){
return couponService.getCouponById(id);
}
4.3.2. 根据id查询优惠券 CouponServiceImpl
// 根据id查询优惠券
@Override
public Coupon getCouponById(Long id) {
// 返回
return lambdaQuery()
.eq(Coupon::getId, id)
.one();
}
4.4.定时开始发放优惠券(同结束)
4.5.定时结束发放优惠券
4.5.1.创建CouponIssueJobHandler
package com.tianji.promotion.handler;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.tianji.common.utils.CollUtils;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.enums.CouponStatus;
import com.tianji.promotion.service.ICouponService;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
/**
* 定时任务
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class CouponIssueJobHandler {
private final ICouponService couponService;
/**
* 定时开始或结束发放优惠券
*/
@XxlJob("couponIssueJobHandler")
public void IssueCoupon() {
// 1.获取分片信息,作为页码,每页最多查询 20条
int index = XxlJobHelper.getShardIndex() + 1;
int size = Integer.parseInt(XxlJobHelper.getJobParam());
// 2.查询未开始的优惠券
Page page = couponService.lambdaQuery()
.eq(Coupon::getStatus, CouponStatus.UN_ISSUE)
.le(Coupon::getTermBeginTime, LocalDateTime.now())
.page(new Page<>(index, size));
// 3.发放优惠券
List coupons = page.getRecords();
if (CollUtils.isEmpty(coupons)) {
return;
}
couponService.beginIssueBatch(coupons);
// 4.查询发放中的优惠券
Page pageEnd = couponService.lambdaQuery()
.eq(Coupon::getStatus, CouponStatus.ISSUING)
.ge(Coupon::getIssueEndTime, LocalDateTime.now())
.page(new Page<>(index, size));
List couponsEnd = pageEnd.getRecords();
//5、结束发放优惠券
if (CollUtils.isEmpty(couponsEnd)) {
return;
}
couponService.endIssueBatch(couponsEnd);
}
}
4.5.2.定时发放优惠券 ICouponService
/**
* 定时开始发放优惠券
* @param coupons
*/
void beginIssueBatch(List coupons);
/**
* 定时结束发放优惠券
* @param couponsEnd
*/
void endIssueBatch(List couponsEnd);
void pauseCoupon(Long id);
4.5.3.定时发放优惠券 CouponServiceImpl
/**
* 定时结束发放优惠券
*
* @param couponsEnd
*/
@Override
public void endIssueBatch(List couponsEnd) {
//更新券状态为发放结束
for (Coupon coupon : couponsEnd) {
coupon.setStatus(CouponStatus.FINISHED);
}
this.updateBatchById(couponsEnd);
}
4.6.暂停发放优惠券
4.6.1. 暂停发放优惠券 CouponController
/**
* 暂停发放优惠券
*/
@ApiOperation("暂停发放优惠券")
@PutMapping("/{id}/pause")
public void pauseCoupon(@PathVariable("id") Long id) {
couponService.pauseCoupon(id);
}
4.6.2. 暂停发放优惠券 CouponServiceImpl
/**
* 暂停发放优惠券
*
* @param id
*/
@Override
public void pauseCoupon(Long id) {
//1、检验参数
if (id == null) {
throw new BadRequestException("非法参数!");
}
//2、校验优惠券id是否存在
Coupon coupon = this.getById(id);
if (coupon == null) {
throw new BadRequestException("优惠券不存在!");
}
//3、校验优惠券状态 只有发放中的状态才能暂停
if (coupon.getStatus() != CouponStatus.ISSUING) {
throw new BizIllegalException("该优惠券不处于发放中的状态,不能暂停!");
}
//4、修改优惠券状态为暂停
this.lambdaUpdate()
.eq(Coupon::getId, id)
.set(Coupon::getStatus, CouponStatus.PAUSE)
.update();
}
4.7.查询兑换码
4.7.1. 查询兑换码 CouponController
package com.tianji.promotion.controller;
import com.tianji.common.domain.dto.PageDTO;
import com.tianji.promotion.domain.query.CodeQuery;
import com.tianji.promotion.domain.vo.ExchangeCodeVO;
import com.tianji.promotion.service.IExchangeCodeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
*
* 兑换码 前端控制器
*
*
* @author wyy
* @since 2025-04-23
*/
@Api(tags = "兑换码相关接口")
@RequiredArgsConstructor
@RestController
@RequestMapping("/codes")
public class ExchangeCodeController {
private final IExchangeCodeService exchangeCodeService;
/**
* 分页查询兑换码
* @param query
* @return
*/
@ApiOperation("分页查询兑换码")
@GetMapping("/page")
public PageDTO queryExchangeCodePage(CodeQuery query) {
return exchangeCodeService.queryExchangeCodePage(query);
}
}
4.7.2. 查询兑换码 ExchangeCodeServiceImpl
/**
* 分页查询兑换码
* @param query
* @return
*/
@Override
public PageDTO queryExchangeCodePage(CodeQuery query) {
//1、分页查询兑换码表
Page page = this.lambdaQuery()
.eq(ExchangeCode::getExchangeTargetId, query.getCouponId())
.eq(ExchangeCode::getStatus, query.getStatus())
.page(query.toMpPageDefaultSortByCreateTimeDesc());
List records = page.getRecords();
if (CollUtils.isEmpty(records)) {
return PageDTO.empty(page);
}
//2、封装vo,返回结果
List voList = BeanUtils.copyList(records, ExchangeCodeVO.class);
return PageDTO.of(page, voList);
}
4.7.3.ExchangeCodeVO
package com.tianji.promotion.domain.vo;
import com.tianji.promotion.enums.DiscountType;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@ApiModel(description = "优惠券分页数据")
@NoArgsConstructor
@AllArgsConstructor
public class ExchangeCodeVO {
@ApiModelProperty("兑换码id")
private Integer id;
@ApiModelProperty("兑换码")
private String code;
@ApiModelProperty("兑换码状态")
private DiscountType status;
@ApiModelProperty("兑换人")
private Long userId;
@ApiModelProperty("兑换码目标id")
private Long exchangeTargetId;
}
4.7.4.测试

都看到这了,给文涛点个赞支持一下呗!
你的‘赞’,是给与文涛最大的动力鸭
有问题,可以评论区大家一起讨论
后续会在此更新,相关问题及解决方案

浙公网安备 33010602011771号