优惠券活动管理
1 需求分析
优惠券活动管理:
运营人员在后台添加优惠券活动,包括:新增优惠券活动、修改优惠券活动、撤销优惠券活动等操作。
抢券:
优惠券到达发放时间用户领取优惠券,因为优惠券的数量有限,当到达发放时间后平台所有用户都可以抢券,先到先得。
优惠券核销:
用户抢到优惠券即可在下单时使用优惠券,享受优惠,如果取消订单将退回优惠券,优惠券退回后可用于其它订单。
界面原型大概如下:
抢券:
到达优惠券发放时间用户开始抢券,在抢券界面列出了进行中的活动和即将开始的活动。
用券:
在下单时选择优惠券:
业务模块如下:
优惠券活动管理:
对优惠券活动进行管理,运营人员新增优惠券活动、修改优惠券活动、撤销优惠券活动及优惠券统计等。
抢券:
到了优惠券发放时间用户进行抢券,抢券过程对优惠券库存、对用户领取优惠券数量等进行校验,抢券成功记录用户领取优惠券的记录。
核销:
用户在下单时使用优惠券得到优惠金额,实付金额等于订单金额减去优惠金额,下单成功优惠券核销成功。
优惠券核销是指:顾客在购买商品使用优惠券,当此次消费符合优惠券的条件时提交订单后将优惠券的折扣应用到顾客的订单中,最后将优惠券标记为已使用或作废。
优惠券核销后还可以取消核销,如果用户取消订单会将优惠券取消核销即退回优惠券,退回优惠券后可以继续使用。
2 需求设计
2.1 数据流
根据需求分析优惠券模块的数据流如下:
优惠券活动表:
优惠券活动管理模块主要操作优惠券活动表。
优惠券活动记录优惠券活动信息,运营人员新增优惠券活动将写入此表,此表是优惠券管理主要维护的表。
关键字段:活动id、活动名称、优惠券类型、折扣、发放时间等。
优惠券表:
抢券模块主要操作优惠券表。
优惠券表记录用户领取的优惠券,用户抢券存在限制,每种优惠券一个用户只允许领取一张,优惠券的总数有限制。
关键字段:用户id、活动id、折扣、优惠券类型、有效期等。
举例:一个优惠券活动发放100张优惠券最多有100个用户去领取,每人领取一张,每个用户领取的一张优惠券会记录在优惠券表中,即该优惠券活动对应优惠券表最多100条记录。
优惠券核销表:
在使用优惠券模块当用户成功使用一张优惠券会在优惠券核销表记录一条记录,记录是哪个用户的哪个订单使用了哪个优惠券。
关键字段 :用户id、优惠券id、订单id,核销时间。
优惠券退回表:
如果用户取消订单,则会退回优惠券,具体操作是向优惠券退回表添加一条记录(记录用户退回优惠券的信息),并向优惠券核销表删除一条对应的记录,表示取消优惠券的核销。
关键字段:用户id、优惠券id、退回时间。
2.2 表结构设计
- 优惠券活动表设计:
create table `jzo2o-market`.activity
(
id bigint not null comment '活动id'
constraint `PRIMARY`
primary key,
name varchar(100) default '0' not null comment '活动名称',
type int not null comment '优惠券类型,1:满减,2:折扣',
amount_condition decimal(10, 2) not null comment '使用条件,0:表示无门槛,其他值:最低消费金额',
discount_rate int default 0 not null comment '折扣率,折扣类型的折扣率,8折就是存80',
discount_amount decimal(10, 2) null comment '优惠金额,满减或无门槛的优惠金额',
validity_days int default 0 not null comment '优惠券有效期天数,0:表示有效期是指定有效期的',
distribute_start_time datetime not null comment '发放开始时间',
distribute_end_time datetime not null comment '发放结束时间',
status int not null comment '活动状态,1:待生效,2:进行中,3:已失效 4:作废',
total_num int default 0 not null comment '发放数量,0:表示无限量,其他正数表示最大发放量',
stock_num int default 0 not null comment '库存',
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
create_by bigint null comment '创建人',
update_by bigint null comment '更新人',
is_deleted tinyint default 0 not null comment '逻辑删除'
)
说明:
amount_condition:0表示无门槛,其它值表示最低消费金额。
discount_rate:当优惠券类型为2折扣时在此字段中存储折扣率。
discount_amount:当优惠券类型为1满减时在此字段中存储优惠券金额。
status:活动状态字段值包括:1:待生效,2:进行中,3:已失效 4:作废 几种,优惠券活动的初始状态是待生效,当到达优惠券发放时间时状态将改为进行中,当到达结束时间时状态改为已失效,当撤销活动后状态为作废。
- 优惠券表设计
create table `jzo2o-market`.coupon
(
id bigint not null comment '优惠券id'
constraint `PRIMARY`
primary key,
name varchar(255) not null comment '优惠券名称',
user_id bigint not null comment '优惠券的拥有者',
user_name varchar(50) null comment '用户姓名',
user_phone varchar(20) null comment '用户手机号',
activity_id bigint not null comment '活动id',
type int not null comment '使用类型,1:满减,2:折扣',
discount_rate int default 0 null comment '折扣',
discount_amount decimal(10, 2) null comment '优惠金额',
amount_condition decimal(10, 2) not null comment '满减金额',
validity_time datetime null comment '有效期',
use_time datetime null comment '使用时间',
status tinyint not null comment '优惠券状态,1:未使用,2:已使用,3:已失效',
orders_id varchar(50) null comment '订单id',
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
is_deleted tinyint default 0 not null comment '逻辑删除'
)
create index user_my_query_index
on `jzo2o-market`.coupon (user_id, status)
- 优惠券核销表:
create table `jzo2o-market`.coupon_write_off
(
id bigint not null
constraint `PRIMARY`
primary key,
coupon_id bigint not null comment '优惠券id',
user_id bigint not null comment '用户id',
orders_id bigint not null comment '核销时使用的订单号',
activity_id bigint not null comment '活动id',
write_off_time datetime not null comment '核销时间',
write_off_man_phone varchar(20) not null comment '核销人手机号',
write_off_man_name varchar(50) not null comment '核销人姓名'
)
- 优惠券退回表
create table `jzo2o-market`.coupon_use_back
(
id bigint not null comment '回退记录id'
constraint `PRIMARY`
primary key,
coupon_id bigint not null comment '优惠券id',
user_id bigint not null comment '用户id',
use_back_time datetime not null comment '回退时间',
write_off_time datetime null comment '核销时间'
)
3 优惠券活动管理功能开发
3.1 新增优惠券活动
3.1.1 需求分析
1.运营人员进入优惠券活动管理界面,点击“新增优惠券”进入如下界面。
优惠券新增页包括两部分内容:配置优惠券活动的基本信息和发放规则。
2. 选择折扣
数据校验
本项目优惠券只支持满减与折扣两种类型。
优惠券类型,1:满减,2:折扣
如果满减:
1、折扣金额必须输入
2、折扣金额必须大于0的整数
如果是折扣:
1、折扣比例必须输入
2、折扣比例必须大于0,小于100的整数
发放时间开始时间不能小于当前时间
发放结束时间不能早于发放开始时间
3.1.2 接口分析
接口名称:保存优惠券活动
接口功能:新增或修改一个优惠券活动信息,本接口支持新和修改。
接口地址:POST/market/operation/activity/save
请求数据类型: application/json
3.1.3 接口开发
Controller
@RestController("operationActivityController")
@RequestMapping("/operation/activity")
@Api(tags = "运营端-优惠券相关接口")
public class ActivityController {
@Autowired
private IActivityService activityService;
@PostMapping("/save")
@ApiOperation("新增优惠券")
public void save(@Validated @RequestBody ActivitySaveReqDTO activitySaveReqDTO) {
activityService.save(activitySaveReqDTO);
}
}
Service
/**
* 新增优惠券接口
* @param activitySaveReqDTO
*/
@Override
public void save(ActivitySaveReqDTO activitySaveReqDTO) {
//数据校验
activitySaveReqDTO.check();
//转换对象
Activity activity = BeanUtils.toBean(activitySaveReqDTO, Activity.class);
activity.setStatus(NO_DISTRIBUTE.getStatus());
activity.setStockNum(activitySaveReqDTO.getTotalNum());
if(activitySaveReqDTO.getId() == null){
activity.setId(IdUtils.getSnowflakeNextId());
}
saveOrUpdate(activity);
}
3.2 查询优惠券活动
3.2.1 需求分析
登录运营端进入“优惠券管理”界面,如下图:
按条件查询优惠券活动信息。
3.2.2 接口分析
接口名称:分页查询优惠券活动
接口功能:运营端分页查询优惠券活动
接口地址:GET/market/operation/activity/page
请求数据类型: application/x-www-form-urlencoded
3.2.3 接口开发
Controller层
@GetMapping("/page")
@ApiOperation("运营端分页查询活动")
public PageResult<ActivityInfoResDTO> queryForPage(ActivityQueryForPageReqDTO activityQueryForPageReqDTO) {
return activityService.queryForPage(activityQueryForPageReqDTO);
}
Service层
/**
* 分页查询优惠券
* @param activityQueryForPageReqDTO
* @return
*/
@Override
public PageResult<ActivityInfoResDTO> queryForPage(ActivityQueryForPageReqDTO activityQueryForPageReqDTO) {
LambdaQueryWrapper<Activity> eq = new LambdaQueryWrapper<Activity>().eq(Activity::getId, activityQueryForPageReqDTO.getId())
.like(Activity::getName, activityQueryForPageReqDTO.getName())
.eq(Activity::getType, activityQueryForPageReqDTO.getType())
.eq(Activity::getStatus, activityQueryForPageReqDTO.getStatus())
.orderByDesc(Activity::getId);
Page<Activity> activityPage = new Page<>(activityQueryForPageReqDTO.getPageNo().intValue(),activityQueryForPageReqDTO.getPageSize().intValue());
activityPage = baseMapper.selectPage(activityPage, eq);
return PageUtils.toPage(activityPage,ActivityInfoResDTO.class);
}
测试接口
进入运营端的优惠券接口:
优惠券查询成功
再测试下新增优惠券:
新增优惠券功能完成
3.3 修改优惠券活动
3.3.1 需求分析
待生效的活动信息可以进行修改。
进入修改页面进行修改:
3.3.2 查询优惠券活动详情接口分析
接口名称:查询优惠券活动详情
接口功能:进入修改页面首先调用此接口根据活动id查询优惠券活动详情在页面显示
接口地址:GET/market/operation/activity/{id}
请求数据类型 application/x-www-form-urlencoded
3.3.2 查询优惠券活动详情接口开发
Controller层
@GetMapping("/{id}")
@ApiOperation("查询活动详情")
@ApiImplicitParam(name = "id", value = "活动id", required = true, dataTypeClass = Long.class)
public ActivityInfoResDTO getDetail(@PathVariable("id") Long id) {
return activityService.queryById(id);
}
Service层
/**
* 查看优惠券详情
* @param id
* @return
*/
@Override
public ActivityInfoResDTO queryById(Long id) {
//获取获得
Activity activity = baseMapper.selectById(id);
//校验空
if(ObjectUtils.isNull(activity)){
return new ActivityInfoResDTO();
}
// 数据转换,并返回信息
ActivityInfoResDTO activityInfoResDTO = BeanUtils.toBean(activity, ActivityInfoResDTO.class);
//被领取的数量
Integer receiveNum = activity.getTotalNum()-activity.getStockNum();
activityInfoResDTO.setReceiveNum(receiveNum);
//核销数量
Integer writeOffNum = couponWriteOffService.countByActivityId(id);
activityInfoResDTO.setWriteOffNum(NumberUtils.null2Zero(writeOffNum));
return activityInfoResDTO;
}
测试
3.3.3 保存优惠券活动接口分析
修改优惠券活动信息后请求保存优惠券活动接口。
修改优惠券活动与新增优惠券活动调用同一个接口
3.3.4 保存优惠券活动接口分析
和新增一样
测试
3.4 查询领取记录
3.4.1 需求分析
进入优惠券活动查询界面,点击“领取记录”可查看用户领取优惠券的记录,如下图:
数据分析:
1)发放数量 = 新增优惠券时指定的发放总数
2)领取数量 = 领取该优惠券的数量,领取数量<=发放数量
3)使用数量 = 用户领取优惠券后使用数量,使用数量<=领取数量
4)发放率= 领取数量/发放数量100%;
5)使用率=使用数量/领取数量100%;
6)用户手机号:领取优惠券的用户手机号
7)领取时间:领取优惠券的时间
8)使用时间:使用优惠券的时间
9)优惠券状态:待使用,已使用
10)使用订单:使用优惠券的订单号
显示规则:
1)列表内数据倒序进行展示;
2)发放率、使用率最多显示两位小数;
3)单页最多显示5条,没有分页,超过5条出滚动条,无限向下滚;
3.4.2 接口分析
接口名称:根据活动ID查询优惠券领取记录
接口功能:根据活动ID查询优惠券领取记录
接口地址:GET/market/operation/coupon/page
请求数据类型 application/x-www-form-urlencoded
3.4.3 接口开发
Controller层
@RestController("operationCouponController")
@RequestMapping("/operation/coupon")
@Api(tags = "运营端-优惠券相关接口")
public class CouponController {
@Resource
private ICouponService couponService;
@GetMapping("/page")
@ApiOperation("根据活动id查询")
public PageResult<CouponInfoResDTO> queryForPage(CouponOperationPageQueryReqDTO couponOperationPageQueryReqDTO) {
return couponService.queryForPageOfOperation(couponOperationPageQueryReqDTO);
}
}
Service层开发
/**
* 运营端查询同一个活动的优惠券
* @param couponOperationPageQueryReqDTO
* @return
*/
@Override
public PageResult<CouponInfoResDTO> queryForPageOfOperation(CouponOperationPageQueryReqDTO couponOperationPageQueryReqDTO) {
// 1.数据校验
if (ObjectUtils.isNull(couponOperationPageQueryReqDTO.getActivityId())) {
throw new BadRequestException("请指定活动");
}
// 2.数据查询
// 分页 排序
Page<Coupon> couponQueryPage = PageUtils.parsePageQuery(couponOperationPageQueryReqDTO, Coupon.class);
// 查询条件
LambdaQueryWrapper<Coupon> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(Coupon::getActivityId, couponOperationPageQueryReqDTO.getActivityId());
// 查询数据
Page<Coupon> couponPage = baseMapper.selectPage(couponQueryPage, lambdaQueryWrapper);
// 3.数据转化,并返回
return PageUtils.toPage(couponPage, CouponInfoResDTO.class);
}
测试
3.5 撤销活动
3.5.1 需求分析
对于待生效及进行中的活动如果要进行终止可以执行撤销操作,执行后此活动将终止,用户已抢到还未使用的优惠券将作废。
点击【撤销】,出现【确认撤销】弹窗,如下:
撤销的活动可以选择发放状态:作废,进行查询
只允许对待生效及进行中的活动进行撤销。
3.5.2 接口分析
接口名称:撤销活动
接口功能:撤销一个优惠券活动,对于待生效及进行中的活动如果要进行终止可以执行撤销操作,执行后活动状态改为作废,用户已抢到还未使用的优惠券将作废。
接口地址:POST/market/operation/activity/revoke/{id}
请求数据类型 application/x-www-form-urlencoded
注意:本接口除了更新活动状态为作废,还需要将所有抢到本活动优惠券的状态为未使用的记录的状态更改为“已失效” 。
3.5.3 接口开发
Controller层
@PostMapping("/revoke/{id}")
@ApiOperation("活动撤销")
@ApiImplicitParam(name = "id", value = "活动id", required = true, dataTypeClass = Long.class)
public void revoke(@PathVariable("id") Long id) {
activityService.revoke(id);
}
Service层
/**
* 撤销优惠券
* @param id
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void revoke(Long id) {
// 1.活动作废
boolean update = lambdaUpdate()
.set(Activity::getStatus, ActivityStatusEnum.VOIDED.getStatus())
.eq(Activity::getId, id)
.in(Activity::getStatus, Arrays.asList(NO_DISTRIBUTE.getStatus(), DISTRIBUTING.getStatus()))
.update();
if(!update) {
throw new CommonException("撤销优惠券失败");
}
// 2.未使用优惠券作废
couponService.revoke(id);
}
/**
* 将Coupon数据库的这个id的优惠券变为已作废
* @param id
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void revoke(Long id) {
lambdaUpdate()
.set(Coupon::getStatus, CouponStatusEnum.VOIDED.getStatus())
.eq(Coupon::getActivityId, id)
.eq(Coupon::getStatus, CouponStatusEnum.NO_USE.getStatus())
.update();
}
测试
撤销一个
功能测试成功
3.6 自动变更活动状态
3.6.1 需求分析
状态变更的需求如下:
优惠券活动表的状态字段值包括:1:待生效,2:进行中,3:已失效 4:作废 几种,优惠券活动的初始状态是待生效,当到达优惠券发放时间状态将改为进行中,当到达结束时间状态改为已失效,当撤销活动后状态为作废。
状态变更不需要依赖人工操作,可由定时任务实现,每分钟更新一次状态:
1)对待生效的活动更新为进行中
到达发放开始时间状态改为“进行中”。
2)对待生效及进行中的活动更新为已失效
到达发放结束时间状态改为“已失效”
3.6.2 活动状态变更定时任务
需求分析
活动状态包括:1:待生效,2:进行中,3:已失效 4: 作废’
对于待生效的活动:到达发放开始时间状态改为“进行中”。
对于待生效及进行中的活动:到达发放结束时间状态改为“已失效”
使用xxl-job定义定时任务,每分钟执行一次。
定时任务代码
/**
* 活动状态修改,
* 1.活动进行中状态修改
* 2.活动已失效状态修改
* 1分钟一次
*/
@XxlJob("updateActivityStatus")
public void updateActivitySatus(){
log.info("定时修改活动状态...");
try {
activityService.updateStatus();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 更新活动状态,
* 1.已经进行中但是状态未修改的订单变为进行中
* 2.已经结束的任务变为已失效
*/
@Override
public void updateStatus() {
LocalDateTime now = DateUtils.now();
// 1.更新已经进行中的状态
lambdaUpdate()
.set(Activity::getStatus, ActivityStatusEnum.DISTRIBUTING.getStatus())//更新活动状态为进行中
.eq(Activity::getStatus, NO_DISTRIBUTE)//检索待生效的活动
.le(Activity::getDistributeStartTime, now)//活动开始时间小于等于当前时间
.gt(Activity::getDistributeEndTime,now)//活动结束时间大于当前时间
.update();
// 2.更新已经结束的
lambdaUpdate()
.set(Activity::getStatus, LOSE_EFFICACY.getStatus())//更新活动状态为已失效
.in(Activity::getStatus, Arrays.asList(DISTRIBUTING.getStatus(), NO_DISTRIBUTE.getStatus()))//检索待生效及进行中的活动
.lt(Activity::getDistributeEndTime, now)//活动结束时间小于当前时间
.update();
}
设置定时任务
测试
开启定时任务一次:
测试成功
3.6.3 已领取优惠券自动过期任务
需求分析
用户领取的优惠券如果到达有效期仍然没有使用自动改为“已失效”
使用xxl-job定义定时任务,每小时执行一次。
定时任务代码
/**
* 已领取优惠券自动过期任务
* 每小时执行一次
*/
@XxlJob("processExpireCoupon")
public void processExpireCoupon() {
log.info("已领取优惠券自动过期任务...");
try {
couponService.processExpireCoupon();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 过期优惠券处理
*/
@Override
public void processExpireCoupon() {
lambdaUpdate()
.set(Coupon::getStatus, 3)
.eq(Coupon::getStatus, 1)
.le(Coupon::getValidityTime, DateUtils.now())
.update();
}
设置定时任务
3.7 我的优惠券
3.7.1 需求分析
用户抢到优惠券后进入“我的”–>"我的优惠券"查询已抢到的优惠券,按抢券时间降序显示当前用户抢到的优惠券,
本查询为滚动查询,向上拖屏幕查询下一屏,一屏显示10条。
用户抢到优惠券有三个状态:
未使用:未过有效期的优惠券。
优惠券的有效期:从领取优惠券的时间加上优惠券的使用期限(“使用期限”在优惠券活动管理界面进行设置)。
已使用:已经在订单中使用的优惠券。
已过期:未使用且已过有效期的优惠券,已过期的优惠券将无法使用。
查询未使用的优惠券
查询已使用的优惠券:
查询已过期的优惠券:
3.7.2 接口分析
接口名称:我的优惠券列表
接口功能:用户查询自己领取的优惠券
接口地址:GET/market/consumer/coupon/my
请求数据类型 application/x-www-form-urlencoded
3.7.3 接口开发
Controller层
@RestController("consumerCouponController")
@RequestMapping("/consumer/coupon")
@Api(tags = "用户端-优惠券相关接口")
public class CouponController {
@Resource
private ICouponService couponService;
@GetMapping("/my")
@ApiOperation("我的优惠券列表")
@ApiImplicitParams({
@ApiImplicitParam(name = "lastId", value = "上一次查询最后一张优惠券id", required = false, dataTypeClass = Long.class),
@ApiImplicitParam(name = "status", value = "优惠券状态,1:未使用,2:已使用,3:已过期", required = true, dataTypeClass = Long.class)
})
public List<CouponInfoResDTO> queryMyCouponForPage(@RequestParam(value = "lastId", required = false) Long lastId,
@RequestParam(value = "status", required = true) Integer status) {
return couponService.queryForList(lastId, UserContext.currentUserId(), status);
}
}
Service层
/**
* 滚动查询用户优惠券列表
*
* @param lastId 上一批查询最后一条优惠券的id
* @param userId 查询用户的id
* @param status 优惠券状态
* @return 优惠券列表
*/
@Override
public List<CouponInfoResDTO> queryForList(Long lastId, Long userId, Integer status) {
//校验
if (status > 3 || status < 1) {
throw new BadRequestException("请求状态不存在");
}
//查询准备
LambdaQueryWrapper<Coupon> lambdaQueryWrapper = new LambdaQueryWrapper<Coupon>().eq(Coupon::getStatus, status)
.eq(Coupon::getUserId, userId)
.lt(ObjectUtils.isNotNull(lastId), Coupon::getId, lastId)
.select(Coupon::getId)
.orderByDesc(Coupon::getId)
.last("limit 10");
List<Coupon> couponIds = baseMapper.selectList(lambdaQueryWrapper);
//判空
if (CollUtils.isEmpty(couponIds)) {
return new ArrayList<>();
}
//获取数据且数据转换
List<Long> ids = couponIds.stream()
.map(Coupon::getId)
.collect(Collectors.toList());
//获取优惠券数据
List<Coupon> coupons = baseMapper.selectBatchIds(ids);
// 数据转换
return BeanUtils.copyToList(coupons, CouponInfoResDTO.class);
}