项目
博客
文档
归档
资源链接
关于我
项目
博客
文档
归档
资源链接
关于我
19| 优惠券微服务业务-库存扣减数据库乐观锁
2024-08-08
·
·
原创
·
·
本文共 1,415个字,预计阅读需要 5分钟。
### 优惠券业务介绍 拉新业务衍生出的玩法:砍价,拼团,裂变,优惠券。 电商优惠券逻辑: - 获取方式维度: 被动:`新⼈优惠券,⽆⻔槛现⾦劵` ,...其他 第⼀次登录注册某平台,登录成功后进入到⾸个⻚⾯,弹出新⼈红包或者某固定位置领取新⼈红包,前端领取位置及细节不做详细讲解,根据实际业务场景⽽定。 主动:`领取优惠券,满减劵`, ...其他 促进商品订单成交,⽽设置的优惠券,提⾼下单⽀付率 - 使⽤⻔槛维度 - ⽆⻔槛现⾦劵 - 满减劵 - 满减折扣卷 - 运费抵扣券 - 兑换券 - 店铺劵 - 单品劵 - ... 按照多维度设计,就需要⽤到规则引擎,这类⼀般是⼤⼚的营销中台设计的,复杂且容易出问题。 - 优惠券常⻅`属性` - `类型`:⽆⻔槛、满减、单品、折扣等每⼈领劵次数 - `发券总量` - 优惠券开始`时间`和结束时间 ### 优惠券微服务及数据库和表 优惠券业务需求: - 新⽤户注册-发放后端配置的新⼈优惠券 - ⽤户可以主动领取优惠券 - 下单可以选择对应的优惠券抵扣 - ⽀持满减优惠券-⽆⻔槛优惠券两种 - 多种元数据配置 - 类型:⽆⻔槛、满减等 - 每⼈领劵次数限制 - 发券总量控制优惠券开始时间和结束时间 - 优惠券状态配置 **核心知识:高并发下扣减劵库存,超发,单人超领取** **原⽣分布式锁+redisson框架分布锁使⽤: 分布式锁+最佳实践** 建立优惠券服务数据库: **p_nla_coupon** 优惠券表: ```sql CREATE TABLE `coupon` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `category` varchar(11) DEFAULT NULL COMMENT '优惠卷类型[NEW_USER注册赠券,TASK任务卷,PROMOTION促销劵]', `publish` varchar(11) DEFAULT NULL COMMENT '发布状态, PUBLISH发布,DRAFT草稿,OFFLINE下线', `coupon_img` varchar(524) DEFAULT NULL COMMENT '优惠券图片', `coupon_title` varchar(128) DEFAULT NULL COMMENT '优惠券标题', `price` decimal(16,2) DEFAULT NULL COMMENT '抵扣价格', `user_limit` int(11) DEFAULT NULL COMMENT '每人限制张数', `start_time` datetime DEFAULT NULL COMMENT '优惠券开始有效时间', `end_time` datetime DEFAULT NULL COMMENT '优惠券失效时间', `publish_count` int(11) DEFAULT NULL COMMENT '优惠券总量', `stock` int(11) DEFAULT '0' COMMENT '库存', `create_time` datetime DEFAULT NULL, `condition_price` decimal(16,2) DEFAULT NULL COMMENT '满多少才可以使用', `version` bigint(20) DEFAULT NULL COMMENT '版本', --当使用乐观锁的方式时 PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COMMENT='优惠券表'; ``` 优惠券领劵记录表: ```sql CREATE TABLE `coupon_record` ( `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT, `coupon_id` bigint(11) DEFAULT NULL COMMENT '优惠券id', `create_time` datetime DEFAULT NULL COMMENT '创建时间获得时间', `use_state` varchar(32) DEFAULT NULL COMMENT '使用状态: 可用NEW,已使用USED,过期EXPIRED;', `user_id` bigint(11) DEFAULT NULL COMMENT '用户id', `user_name` varchar(128) DEFAULT NULL COMMENT '用户昵称', `coupon_title` varchar(128) DEFAULT NULL COMMENT '优惠券标题', `start_time` datetime DEFAULT NULL COMMENT '开始时间', `end_time` datetime DEFAULT NULL COMMENT '结束时间', `order_id` bigint(11) DEFAULT NULL COMMENT '订单id', `price` decimal(16,2) DEFAULT NULL COMMENT '抵扣价格', `condition_price` decimal(16,2) DEFAULT NULL COMMENT '满多少才可以使用', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠券领劵记录'; ``` 添加数据: ```sql INSERT INTO `coupon` (`id`, `category`, `publish`, `coupon_img`, `coupon_title`, `price`, `user_limit`, `start_time`, `end_time`, `publish_count`, `stock`, `create_time`, `condition_price`) VALUES (1, 'NEW_USER', 'PUBLISH', 'https://file.xdclass.net/video/2020/alibabacloud/zt-alibabacloud.png', '永久有效-新人注册-0元满减-5元抵扣劵-限领取2张-不可叠加使用', 5.00, 2, '2000-01-01 00:00:00', '2099-01-29 00:00:00',100000000, 99999991, '2020-12-26 16:33:02',0.00), (2, 'PROMOTION', 'PUBLISH','https://file.xdclass.net/video/2020/alibabacloud/zt-alibabacloud.png', '有效中-21年1月到25年1月-20元满减-5元抵扣劵-限领取2张-不可叠加使用', 5.00,2, '2000-01-29 00:00:00', '2025-01-29 00:00:00', 10, 3, '2020-12-26 16:33:03',20.00), (3, 'PROMOTION', 'PUBLISH','https://file.xdclass.net/video/2020/alibabacloud/zt-alibabacloud.png', '过期-20年8月到20年9月-商 品id3-6元抵扣劵-限领取1张-可叠加使用', 6.00, 1,'2020-08-01 00:00:00', '2020-09-29 00:00:00',100, 100, '2020-12-26 16:33:03', 0.00), (4, 'PROMOTION', 'PUBLISH','https://file.xdclass.net/video/2020/alibabacloud/zt-alibabacloud.png', '有效中-20年8月到21年9月-商品id1-8.8元抵扣劵-限领取2张-不可叠加使用', 8.80,2, '2020-08-01 00:00:00', '2021-09-29 00:00:00', 100, 96, '2020-12-26 16:33:03', 0.00), (5, 'PROMOTION', 'PUBLISH','https://file.xdclass.net/video/2020/alibabacloud/zt-alibabacloud.png', '有效中-20年8月到21年9月-商品id2-9.9元抵扣劵-限领取2张-可叠加使用', 8.80, 2,'2020-08-01 00:00:00', '2021-09-29 00:00:00',100, 96, '2020-12-26 16:33:03', 0.00); ``` ### Mybatis-plus 分页插件配置 公共添加分页插件配置`MybatisPlusInterceptor`: ```java @Configuration public class MybatisPlusPageConfig { // 旧版本配置 // @Bean // public PaginationInterceptorpaginationInterceptor() { // return new PaginationInterceptor(); // } /** * 新的分⻚插件,⼀缓和⼆缓遵循mybatis的规则, * 需要设置 * MybatisConfiguration#useDeprecatedExecutor = * false 避免缓存出现问题 */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } } ``` SwaggerConfiguration中配置优惠券服务接口文档配置 ```java /** * 对优惠劵服务端的接口文档 */ @Bean public Docket couponApiDoc() { return new Docket(DocumentationType.OAS_30) // 版本3.0 .groupName("优惠劵端接口文档") .pathMapping("/") //定义是否开启Swagger,false是关闭,可以通过变量去控制,线上关闭 .enable(true) //配置文档的元信息 .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("cn.nla")) //正则匹配请求路径,并分配到当前项目组 .paths(PathSelectors.ant("/cop/**")) .build() // 新版SwaggerUI3.0 .globalRequestParameters(globalRequestParameters()) .globalResponses(HttpMethod.GET, getGlobalResponseMessage()) .globalResponses(HttpMethod.POST, getGlobalResponseMessage()); } ``` **构建优惠券服务模块nla-coupon-service同时配置参数** contoller层开发 ```java @Api(tags ="优惠券控制器") @RestController @RequestMapping("/cop/coupon/v1") public class CouponController { @Resource private CouponService couponService; @ApiOperation("分页查询优惠券") @GetMapping("page_coupon") public JsonData pageCouponList( @ApiParam(value = "当前页") @RequestParam(value = "page", defaultValue = "1") int page, @ApiParam(value = "每页显示多少条") @RequestParam(value = "size", defaultValue = "10") int size) { return JsonData.buildSuccess(couponService.pageCouponActivity(page,size)); ``` 添加优惠卷相关字典 ```java /** * 优惠券状态 */ public enum CouponPublishEnum { /** * 线上 */ PUBLISH, /** * 草稿 */ DRAFT, /** * 下线 */ OFFLINE } public enum CouponCategoryEnum { /** * 新人注册 */ NEW_USER, /** * 活动任务 */ TASK, /** * 促销劵 */ PROMOTION } ``` 定义优惠卷返回对象 ```java @Data public class CouponVO { /** * id */ private Long id; /** * 优惠卷类型[NEW_USER注册赠券,TASK任务卷,PROMOTION促销劵] */ private String category; /** * 优惠券图片 */ @JsonProperty("coupon_img") private String couponImg; /** * 优惠券标题 */ @JsonProperty("coupon_title") private String couponTitle; /** * 抵扣价格 */ private BigDecimal price; /** * 每人限制张数 */ @JsonProperty("user_limit") private Integer userLimit; /** * 优惠券开始有效时间 */ @JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss",locale = "zh",timezone = "GMT+8") @JsonProperty("start_time") private Date startTime; /** * 优惠券失效时间 */ @JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss",locale = "zh",timezone = "GMT+8") @JsonProperty("end_time") private Date endTime; /** * 优惠券总量 */ @JsonProperty("publish_count") private Integer publishCount; /** * 库存 */ private Integer stock; /** * 满多少才可以使用 */ @JsonProperty("condition_price") private BigDecimal conditionPrice; } ``` service分页开发`Page,IPage`: ```java @Service public class CouponServiceImpl extends ServiceImpl
implements CouponService { @Override public Map
pageCouponActivity(int page, int size) { Page
pageInfo = new Page<>(page,size); IPage
couponDOIPage = baseMapper.selectPage(pageInfo, Wrappers.
lambdaQuery().eq(CouponEntity::getPublish, CouponPublishEnum.PUBLISH) .eq(CouponEntity::getCategory, CouponCategoryEnum.PROMOTION) .orderByDesc(CouponEntity::getCreateTime)); Map
pageMap = new HashMap<>(3); //总条数 pageMap.put("total_record", couponDOIPage.getTotal()); //总页数 pageMap.put("total_page",couponDOIPage.getPages()); pageMap.put("current_data",couponDOIPage.getRecords().stream().map(this::beanProcess).collect(Collectors.toList())); return pageMap; } private CouponVO beanProcess(CouponEntity entity) { CouponVO couponVO = new CouponVO(); BeanUtils.copyProperties(entity,couponVO); return couponVO; } } ``` 注意:分页插件配置在公共模块,而优惠券服务是引入公共依赖,所以要添加次路径的扫描:否则不会生效 ```java @SpringBootApplication(scanBasePackages = "cn.nla.*") ``` ### 用户领劵核心业务逻辑 - 高并发下扣减劵库存,采用乐观锁 contoller层 ```java /** * 领取优惠券 */ @ApiOperation("领取优惠券") @GetMapping("/add/promotion/{coupon_id}") public JsonData addPromotionCoupon(@ApiParam(value = "优惠券id", required = true) @PathVariable("coupon_id") long couponId) { return couponService.addCoupon(couponId, CouponCategoryEnum.PROMOTION); } ``` 领劵接口业务逻辑: 1、获取优惠券是否存在 2、校验优惠券是否可以领取:时间、库存、超过限制 3、扣减库存 4、保存领劵记录 - 优惠券是否可以领取校验: ```java /** * 校验是否可以领取 */ private void checkCoupon(CouponEntity coupon, Long userId) { if (coupon == null) { throw new BizException(BizCodeEnum.COUPON_NO_EXITS); } //库存是否足够 if (coupon.getStock() <= 0) { throw new BizException(BizCodeEnum.COUPON_NO_STOCK); } //判断是否是否发布状态 if (!coupon.getPublish().equals(CouponPublishEnum.PUBLISH.name())) { throw new BizException(BizCodeEnum.COUPON_GET_FAIL); } //是否在领取时间范围 long time = CommonUtil.getCurrentTimestamp(); long start = coupon.getStartTime().getTime(); long end = coupon.getEndTime().getTime(); if (time < start || time > end) { throw new BizException(BizCodeEnum.COUPON_OUT_OF_TIME); } //用户是否超过限制 int recordNum = couponRecordMapper.selectCount( Wrappers.
lambdaQuery().eq(CouponRecordEntity::getCouponId, coupon.getId()) .eq(CouponRecordEntity::getUserId, userId)); if (recordNum >= coupon.getUserLimit()) { throw new BizException(BizCodeEnum.COUPON_OUT_OF_LIMIT); } } ``` `mapper.xml` ```xml
update coupon set stock=stock-1 where id = #{couponId}
update coupon set stock=stock-1,version=version+1 where id = #{couponId} and stock>0 and version=#{oldVersion}
``` 对应的mapper ```java /** * 扣减存储 */ int reduceStock(@Param("couponId")long couponId); /** * 扣减存储 * @param couponId 优惠卷id * @param oldVersion 版本号 */ int reduceStockOpt(@Param("couponId") long couponId, @Param("oldVersion")long oldVersion); ``` 领劵接口:`库存扣减:⾼并发下扣减劵库存,采⽤乐观锁,当前stock做版本号,延伸多种防⽌超卖的问题,⼀次只能领取1张` ```java @Override public JsonData addCoupon(Long couponId, CouponCategoryEnum category) { LoginUser loginUser = LoginInterceptor.threadLocal.get(); CouponEntity coupon = baseMapper.selectOne( Wrappers.
lambdaQuery().eq(CouponEntity::getId, couponId) .eq(CouponEntity::getCategory, category.name())); //优惠券是否可以领取 this.checkCoupon(coupon, loginUser.getId()); //构建领劵记录 CouponRecordEntity couponRecord = new CouponRecordEntity(); BeanUtils.copyProperties(coupon, couponRecord); couponRecord.setCreateTime(new Date()); couponRecord.setUseState(CouponStateEnum.NEW.name()); couponRecord.setUserId(loginUser.getId()); couponRecord.setUserName(loginUser.getName()); couponRecord.setCouponId(couponId); couponRecord.setId(null); //扣减库存 // int rows = baseMapper.reduceStock(couponId); //⾼并发下扣减劵库存,采⽤乐观锁,当前stock做版本号,延伸多种防⽌超卖的问题,⼀次只能领取1张 // 数据库添加字段: version INT DEFAULT 1, 根据当前的版本号更新 int rows = baseMapper.reduceStockOpt(couponId, coupon.getVersion()); if (rows == 1) { //库存扣减成功才保存 couponRecordMapper.insert(couponRecord); } else { log.warn("发放优惠券失败:{},⽤ 户:{}", coupon, loginUser); throw new BizException(BizCodeEnum.COUPON_NO_STOCK); } return JsonData.buildSuccess(); } ```