Skip to main content

01小节:查询用户可用/不可用优惠券

作者:程序员马丁

在线博客:https://nageoffer.com

note

热门项目实战社群,收获国内众多知名公司面试青睐,近千名同学面试成功!助力你在校招或社招上拿个offer。

查询用户可用/不可用优惠券&计算折扣金额,元数据信息:

©版权所有 - 拿个offer-开源&项目实战星球专属学习项目,依据《中华人民共和国著作权法实施条例》《知识星球产权保护》,严禁未经本项目原作者明确书面授权擅自分享至 GitHub、Gitee 等任何开放平台。违者将面临法律追究。


内容摘要:本章节通过解析用户的订单信息,实现在订单结算时根据优惠券的不同类型(立减券、满减券、折扣券)进行分类和优惠金额计算。整个过程涉及到 Redis 数据获取、数据分区、消费规则解析和金额计算等多种操作。通过单元测试,我们验证了各种优惠券在不同订单金额和商品组合下的使用规则和优先级。

课程目录如下所示:

  • 业务背景
  • Git 分支
  • 查询用户优惠券&计算折扣金额
  • 功能测试
  • 文末总结

业务背景

我们在使用优惠券购物时,可以在订单结算页面查看自己的可用/不可用优惠券列表,可用的优惠券还会根据可扣减订单金额从大到小进行排序。

以下为美团外卖订单结算时优惠券可用/不可用列表:

Git 分支

先从 main 分支上查看,代码入口:CouponQueryController#listQueryCouponsBySync

查询用户优惠券&计算折扣金额

因为查询用户可用优惠券时,需要根据折扣金额进行排序,所以本质上在查询用户可用优惠券功能基础上,就把后者实现了。

1. 请求&返回参数

我们查询的时候,需要传入订单金额、商品集合以及店铺编号信息,因为我们优惠券服务这个接口不直接对用户,所以订单金额这些信息不需要额外验证。一般来说,会由聚合服务或者订单服务进行验证,然后路由到我们服务中。

响应参数里重点返回优惠券明细和优惠券减免金额。

代码如下所示:

@Data
@Schema(description = "查询用户优惠券请求参数")
public class QueryCouponsReqDTO {

/**
* 订单金额
*/
@Schema(description = "订单金额", required = true)
private BigDecimal orderAmount;

/**
* 店铺编号
*/
@Schema(description = "店铺编号", example = "1810714735922956666", required = true)
private String shopNumber;

/**
* 商品集合
*/
@Schema(description = "商品集合", required = true)
private List<QueryCouponGoodsReqDTO> goodsList;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "查询用户优惠券响应参数")
public class QueryCouponsRespDTO {

/**
* 可用优惠券列表
*/
@Schema(description = "可用优惠券列表")
private List<QueryCouponsDetailRespDTO> availableCouponList;

/**
* 不可用优惠券列表
*/
@Schema(description = "不可用优惠券列表")
private List<QueryCouponsDetailRespDTO> notAvailableCouponList;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "查询用户优惠券明细响应参数")
public class QueryCouponsDetailRespDTO {

/**
* 优惠券id
*/
@Schema(description = "优惠券id")
private String id;

/**
* 优惠对象 0:商品专属 1:全店通用
*/
@Schema(description = "优惠对象 0:商品专属 1:全店通用")
private Integer target;

/**
* 优惠商品编码
*/
@Schema(description = "优惠商品编码")
private String goods;

/**
* 优惠类型 0:立减券 1:满减券 2:折扣券
*/
@Schema(description = "优惠类型 0:立减券 1:满减券 2:折扣券")
private Integer type;

/**
* 消耗规则
*/
@Schema(description = "消耗规则")
private String consumeRule;

/**
* 优惠券金额
*/
@Schema(description = "优惠券金额")
private BigDecimal couponAmount;
}

2. 查询用户优惠券

首先我们需要将用户的优惠券列表从 Redis ZSet 缓存中查询出来,因为要从所有优惠券中查询可用优惠券并按照减免金额进行排序,所以我们直接 range 的范围是 0 到 -1。另外为了避免复杂计算,我们将现有优惠券按照全店通用以及指定商品可用拆分为两个分组,对这两个分组分别进行计算。

代码如下所示:

public QueryCouponsRespDTO listQueryUserCouponsBySync(QueryCouponsReqDTO requestParam) {
Set<String> rangeUserCoupons = stringRedisTemplate.opsForZSet().range(String.format(USER_COUPON_TEMPLATE_LIST_KEY, UserContext.getUserId()), 0, -1);

List<String> couponTemplateIds = rangeUserCoupons.stream()
.map(each -> StrUtil.split(each, "_").get(0))
.map(each -> redisDistributedProperties.getPrefix() + String.format(COUPON_TEMPLATE_KEY, each))
.toList();
List<Object> couponTemplateList = stringRedisTemplate.executePipelined((RedisCallback<String>) connection -> {
couponTemplateIds.forEach(each -> connection.hashCommands().hGetAll(each.getBytes()));
return null;
});

List<CouponTemplateQueryRespDTO> couponTemplateDTOList = JSON.parseArray(JSON.toJSONString(couponTemplateList), CouponTemplateQueryRespDTO.class);
Map<Boolean, List<CouponTemplateQueryRespDTO>> partitioned = couponTemplateDTOList.stream()
.collect(Collectors.partitioningBy(coupon -> StrUtil.isEmpty(coupon.getGoods())));

// 拆分后的两个列表
List<CouponTemplateQueryRespDTO> goodsEmptyList = partitioned.get(true); // goods 为空的列表
List<CouponTemplateQueryRespDTO> goodsNotEmptyList = partitioned.get(false); // goods 不为空的列表

// ......
}

我们解析下上述代码逻辑:

  1. 提取优惠券模板 ID:通过 StrUtil.split(each, "_").get(0) 操作,提取每个元素中的 templateId。假设 Redis 存储的格式是 templateId_couponId,则 templateId 为第一个元素。
  2. 构建 Redis Key:将提取的 templateId 转换为 Redis 中存储优惠券模板数据的完整 Key(例如 prefix_couponTemplate_{templateId})。
  3. 生成 Redis Key 列表:最终得到一个 List<String> 类型的 Redis Key 列表,准备从 Redis 中批量获取优惠券模板的详细信息。
  4. 使用 Redis Pipeline 批量获取数据:通过 stringRedisTemplate.executePipelined 方法,使用 Redis 管道(Pipeline)技术批量获取 Redis 中的优惠券模板信息。这样做能够减少与 Redis 服务器的网络交互次数,提高读取性能。
  5. 数据转换:通过 JSON.parseArray 方法,将 couponTemplateList 转换为 CouponTemplateQueryRespDTO 对象列表。
  6. goods 字段分区:使用 Collectors.partitioningBy 方法,将优惠券模板列表分为两部分:truegoods 字段为空的优惠券模板,表示该优惠券是平台券或店铺券(没有特定商品限制)。falsegoods 字段不为空的优惠券模板,表示该优惠券是商品专属券(只能用于特定商品)。

解锁付费内容,👉 戳