优惠券服务 (优惠券系统)
简介
优惠券系统基于springboot springcloud redis mysql mybatisplus Hytrix openfeign rabbitmq 等 技术的 微服务架构
分为三个服务 1.优惠券模板服务 2.用户和优惠券交互服务 3.核销服务 如果有错误欢迎指正、互相学习
前面主要准备工作
01.表创建
优惠券模板表 (用于储存系统管理员生成的优惠券种类 user_id为管理员id)
DROP TABLE IF EXISTS `coupon_template`;
CREATE TABLE `coupon_template` (
`id` int(64) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`available` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否是可用状态; true: 可用, false: 不可用',
`expired` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否过期; true: 是, false: 否',
`coupon_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '优惠券名称',
`coupon_desc` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '优惠券描述',
`category` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '优惠券分类(001满减券002立减券003折扣券)',
`product_line` int(11) NOT NULL DEFAULT 0 COMMENT '产品线()',
`coupon_count` int(11) NOT NULL DEFAULT 0 COMMENT '总数',
`create_time` datetime NOT NULL DEFAULT '0000-01-01 00:00:00' COMMENT '创建时间',
`user_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '创建用户',
`coupon_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '优惠券模板的编码',
`rule` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '优惠券规则: TemplateRule 的 json 表示',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1728069641 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '优惠券模板表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
优惠券表 用于储存用户领取的优惠券记录(user_id为用户id)
CREATE TABLE `coupon` (
`id` int(64) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`template_id` int(11) NOT NULL DEFAULT 0 COMMENT '关联优惠券模板的主键',
`user_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '领取用户',
`coupon_code` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '优惠券码',
`assign_time` datetime NOT NULL DEFAULT '0000-01-01 00:00:00' COMMENT '领取时间',
`status` int(11) NOT NULL DEFAULT 0 COMMENT '优惠券的状态',
`templateSDK` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'sdk',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_template_id`(`template_id`) USING BTREE,
INDEX `idx_user_id`(`user_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1728069638 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '优惠券(用户领取的记录)' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
02.主要公共类
0201错误状态码接口
public interface IErrorCode {
int getCode();
String getMessage();
}
0202错误状态码枚举类
//错误状态码
public enum CouponErrorCode implements IErrorCode{
PARAMETER_VALIDITY_FAILED(1000,"构建模板参数无效!"),
TEMPLATE_IS_NOT_EXIST(1001, "模板不存在 !"),
THE_TEMPLATE_NAME(1002,"存在同名模板!"),
NO_ACQUIRE_COUPON(5001,"无法从模板客户端获取模板!"),
COUPON_EXCEED_ASSIGN_LIMITATION(5002,"超出模板分配限制!"),
USER_COUPON_HAS_SOME_PROBLEM(5003,"用户优惠券有问题,不是优惠券的子集合!"),
NOT_SUPPORT_TEMPLATE_CATEGORY(5004,"不支持更多模板类别 !!!"),
CURCOUPONS_IS_NOT_EQUAL_CACHE(4003,"CurCoupons不等于缓存 !"),
KEY_EXIST(5005,"KEY存在 !"),
NOT_SUPPORT_TEMPLATE(5007,"长度大于不是2"),
NOT_EXIST_COUPONINFO(5007,"优惠卷信息不存在"),
CODE_NOT_EXIST(5006,"CODE不存在");
private int code;
private String message;
BusinessCode(int code,String message){
this.code = code;
this.message = message;
}
0203优惠券种类枚举类
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Objects;
import java.util.stream.Stream;
@AllArgsConstructor
@Getter
public enum CouponCategory {
ALLMINUS("满减卷","001"),
SETUPMINUS("立减卷","002"),
DISCOUNT("折扣卷","003");
/**
* 优惠卷描述
*/
private String description;
/**
* 优惠卷码
*/
private String code;
public static CouponCategory of(String code){
Objects.requireNonNull(code);
return Stream.of(values())
.filter(bean -> bean.code.equals(code))
.findAny()
.orElseThrow(() -> new IllegalArgumentException(code +" not exists !!!"));
}
}
0204结果状态码枚举类
import com.park.result.enums.IErrorCode;
import lombok.Getter;
@Getter
public enum ResultCode implements IErrorCode {
SUCCESS(200,"操作成功"),
FAILED(500,"操作失败"),
/**
* 参数错误:30001-39999
*/
PARAM_IS_INVALID(30001, "参数无效"),
PARAM_IS_BLANK(30002, "参数为空"),
PARAM_TYPE_BIND_ERROR(30003, "参数类型错误"),
/* 系统错误:40001-49999 */
SYSTEM_INNER_ERROR(40001, "系统繁忙,请稍后重试"),
/* 数据错误:50001-599999 */
RESULE_DATA_NONE(50001, "数据未找到"),
DATA_IS_WRONG(50002, "数据有误"),
DATA_ALREADY_EXISTED(50003, "数据已存在"),
/* 接口错误:60001-69999 */
INTERFACE_INNER_INVOKE_ERROR(60001, "内部系统接口调用异常"),
INTERFACE_OUTTER_INVOKE_ERROR(60002, "外部系统接口调用异常"),
INTERFACE_FORBID_VISIT(60003, "该接口禁止访问"),
INTERFACE_ADDRESS_INVALID(60004, "接口地址无效"),
INTERFACE_REQUEST_TIMEOUT(60005, "接口请求超时"),
INTERFACE_EXCEED_LOAD(60006, "接口负载过高"),
/* 权限错误:70001-79999 */
PERMISSION_NO_ACCESS(70001, "无访问权限");
private int code;
private String message;
ResultCode(int code, String message){
this.code = code;
this.message = message;
}
}
0205结果类
@ApiModel("统一返回结果")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> implements Serializable {
@ApiModelProperty("响应编码")
private int code;
@ApiModelProperty("响应信息")
private String message;
@ApiModelProperty("响应数据")
private T data;
private static <T> Result<T> common(int code, String message, T data) {
return new Result<>(code, message, data);
}
public static <T> Result<T> ok() {
return common(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), null);
}
public static <T> Result<T> ok(String message) {
return common(ResultCode.SUCCESS.getCode(),message , null);
}
public static <T> Result<T> ok(T data) {
return common(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
public static <T> Result<T> ok(String message,T data) {
return common(ResultCode.SUCCESS.getCode(), message, data);
}
public static <T> Result<T> fail() {
return common(ResultCode.FAILED.getCode(), ResultCode.FAILED.getMessage(), null);
}
public static <T> Result<T> fail(String message) {
return common(ResultCode.FAILED.getCode(), message, null);
}
public static <T> Result<T> fail(T data) {
return common(ResultCode.FAILED.getCode(), ResultCode.FAILED.getMessage(), data);
}
public static <T> Result<T> fail(String message, T data) {
return common(ResultCode.FAILED.getCode(), message, data);
}
public static <T> Result<T> fail(IErrorCode errorCode) {
return common(errorCode.getCode(), errorCode.getMessage(), null);
}
public static <T> Result<T> fail(int code, String message) {
return common(code, message, null);
}
public static <T> Result<T> fail(IErrorCode errorCode, T data) {
return common(errorCode.getCode(), errorCode.getMessage(), data);
}
}
0206消息队列常量
public class RabbitMq{
/**
*消息对列
*/
public static final String DISCOUNT_USER_COUPON_QUEUE = "discount_user_coupon_queue";
/**
* 消息交换机
*/
public static final String DISCOUNT_USER_COUPON_EXCHANGE = "discount_user_coupon_exchange";
/**
* 消息key
*/
public static final String DISCOUNT_USER_COUPON_KEY = "discount.user.coupon";
}
0207优惠券前缀常量
public class RedisPrefix{
/**
* 优惠卷key前缀
*/
public static final String COUPON_TEMPLATE = "discount_coupon_template_code_";
/**
* 用户当前所有可用优惠卷key前缀
*/
public static final String USER_COUPON_USABLE = "discount_user_coupon_usable_";
/**
* 用户当前所有已使用的优惠卷key前缀
*/
public static final String USER_COUPON_USED = "discount_user_coupon_used_";
/**
* 用户所有已过期优惠卷key前缀
*/
public static final String USER_COUPON_EXPIRED = "discount_user_coupon_expired_";
}
0208优惠券计算规则枚举
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
*
* 规则类枚举
* @author 涂航
*/
@Getter
@AllArgsConstructor
public enum RuleFlag {
//单类别优惠卷定义
ALLMINUS_RULE("满减卷的计算规则"),
DISCOUNT_RULE("折扣卷的规则"),
SETUPMINUS_RULE("立减卷的规则"),
//多类别优惠卷定义
ALLMINUS_DISCOUNT_RULE("满减卷 + 折扣卷的计算规则");
/**
* 规则描述
*/
private String description;
}
0209优惠券状态枚举
@Getter
@AllArgsConstructor
public enum CouponStatus {
USABLE("可用的", 1),
USED("已使用的", 2),
EXPIRED("过期的(未被使用的)", 3);
/**
* 优惠券状态描述信息
*/
private String description;
/**
* 优惠券状态编码
*/
private Integer code;
/**
* <h2>根据 code 获取到 CouponStatus</h2>
*/
public static CouponStatus of(Integer code) {
Objects.requireNonNull(code);
return Stream.of(values())
.filter(bean -> bean.code.equals(code))
.findAny()
.orElseThrow(
() -> new IllegalArgumentException(code + " not exists")
);
}
}
0210优惠券有效期类型枚举类
@Getter
@AllArgsConstructor
public enum PeriodType {
REGULAR("固定的(固定日期)", 1),
SHIFT("变动的(以领取之日开始计算)", 2);
/**
* 有效期描述
*/
private String description;
/**
* 有效期编码
*/
private Integer code;
public static PeriodType of(Integer code) {
Objects.requireNonNull(code);
return Stream.of(values())
.filter(bean -> bean.code.equals(code))
.findAny()
.orElseThrow(() -> new IllegalArgumentException(code + " not exists !!!"));
}
}
03.实体类 和实体类用到的类
0301优惠券实体类 (对应表)
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.park.model.vo.coupon.CouponTemplateSdk;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* 优惠卷实体
*
* @author 张震
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "Coupon优惠券对象", description = "Coupon表")
public class Coupon {
/**
* 主键id
*/
@TableId(value = "id",type = IdType.AUTO)
@ApiModelProperty("主键id")
private Integer id;
/**
* 模板id (逻辑外键)
*/
@ApiModelProperty("模板id (逻辑外键)")
private Integer templateId;
/**
* 领取用户
*/
@ApiModelProperty("领取用户")
private Integer userId;
/**
* 优惠卷码
*/
@ApiModelProperty("优惠卷码")
private String couponCode;
/**
* 领取时间
*/
@ApiModelProperty("领取时间")
private Date assignTime;
/**
* 优惠券状态
*/
@ApiModelProperty("优惠券状态")
private Integer status;
/**
* 用户优惠券对应的模板信息
*/
@ApiModelProperty("用户优惠券对应的模板信息")
private CouponTemplateSdk templateSDK;
/**
* <h2>返回一个无效的 Coupon 对象</h2>
*/
public static Coupon invalidCoupon() {
com.park.pojo.conpon.Coupon coupon = new com.park.pojo.conpon.Coupon();
coupon.setId(-1);
return coupon;
}
/**
* <h2>构造优惠券</h2>
*/
public Coupon(Integer templateId, Integer userId, String couponCode,
Integer status) {
this.templateId = templateId;
this.userId = userId;
this.couponCode = couponCode;
this.status = status;
}
}
0302优惠券模板实体类(对应表)
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 优惠卷模板实体类
*
* @author 张震
*/
@AllArgsConstructor
@NoArgsConstructor
@Data
@ApiModel(value = "CouponTemplate对象", description = "优惠卷模板")
public class CouponTemplate implements Serializable {
/**
* 设置主键
* 主键自增
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 是否可用状态
*/
private Boolean available;
/**
* 是否过期
*/
private Boolean expired;
/**
* 优惠卷名称
*/
private String couponName;
/**
* 优惠卷描述
*/
private String couponDesc;
/**
* 优惠卷分类
*/
private String category;
/**
* 产品线
*/
private Integer productLine;
/**
* 总数
*/
private Integer couponCount;
/**
* 创建时间 @UpdateTimestamp @CreatedBy
*/
private Date createTime;
/**
* 创建用户
*/
private Long userId;
/**
* 优惠卷编码
*/
private String couponKey;
/**
* 优惠卷规则
*/
private String rule;
public CouponTemplate(String name, String desc,
String category, Integer productLine, Integer count, Long userId,
TemplateRule rule) {
this.available = false;
this.expired = false;
this.couponName = name;
this.couponDesc = desc;
this.category = category;
this.productLine = productLine;
this.couponCount = count;
this.userId = userId;
this.rule = JSON.toJSONString(rule);
//优惠卷唯一编码 4位(的产品和类型)+8位(日期)+id(扩充为4位)
this.couponKey = productLine.toString() + category +
new SimpleDateFormat("yyyyMMdd").format(new Date());
}
}
0303优惠券规则对象
import com.park.pojo.conpon.constant.PeriodType;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
/**
* 优惠卷规则对象
* @author 张震
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TemplateRule {
/**
* 过期规则
*/
@ApiModelProperty("过期规则")
private Expiration expiration;
/**
* 折扣规则
*/
@ApiModelProperty("折扣规则")
private Discount discount;
/**
* 每个人最多领取多少张
*/
@ApiModelProperty("每个人最多领取多少张")
private Integer limitation;
/**
* 产品类型
*/
@ApiModelProperty("产品类型(什么产品可以用)")
private Integer goodsType;
/**
* 权重(可以和哪些优惠卷叠加使用) 同一类的优惠卷一定不能叠加 (是另一个优惠券的key值)
*/
@ApiModelProperty("权重(4位(的产品和类型)+8位(日期)+id(扩充为4位)(是另一个优惠券的couponkey值)")
private String weight;
public Boolean validate(){
//最简化效验
return expiration.validate() &&
discount.validate() &&
limitation>0 &&
StringUtils.isNotEmpty(weight);
}
/**
* 有效期规则
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Expiration{
/**
* 有效期规则
*/
@ApiModelProperty("对应该枚举的code 有效期规则code")
private Integer period;
/**
* 有效间隔 只对变动的有效期有效
*/
@ApiModelProperty("有效间隔 只对变动的有效期有效")
private Integer gap;
/**
* 优惠卷模板的失效日期,两类规则都有效
*/
@ApiModelProperty("优惠卷模板的失效日期,两类规则都有效")
private Long deadline;
boolean validate(){
//最简化效验
return null != PeriodType.of(period) && gap > 0 && deadline > 0 ;
}
}
/**
* 折扣
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Discount{
/**
* 额度,满减(20) 折扣(85)立减(10)
*/
@ApiModelProperty("额度,满减(20) 折扣(85)立减(10)")
private Integer quota;
/**
* 基准,需要满多少才可用
*/
@ApiModelProperty("基准,需要满多少才可用")
private Integer base;
boolean validate(){
return quota > 0 && base > 0;
}
}
}
0304请求优惠券模板类(用于管理员用户创建优惠券种类 和发券数量)
/**
* 优惠卷模板请求对象
*
* @author 张震
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TemplateRequest {
/**
* 优惠卷的名称
*/
private String couponName;
/**
* 优惠卷描述
*/
private String couponDesc;
/**
* 优惠卷分类
*/
private String category;
/**
* 优惠卷产品线
*/
private Integer productLine;
/**
* 总数
*/
private Integer couponCount;
/**
* 用户id
*/
private Integer userId;
/**
* 优惠卷规则
*/
private TemplateRule templateRule;
public Boolean validate() {
Boolean stringValid = StringUtils.isNotEmpty(couponName)
&& StringUtils.isNotEmpty(couponDesc);
Boolean enumValid = null != category
&& null != productLine;
Boolean numValid = couponCount > 0 && userId > 0;
return stringValid && enumValid && numValid && templateRule.validate();
}
}
0305微服务传递模板信息类
import com.park.pojo.conpon.TemplateRule;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.net.ServerSocket;
/**
*
* 微服务之间的优惠卷模板信息定义
* @author 张震
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "sdk模板", description = "CouponTemplate表")
public class CouponTemplateSdk {
/**
* 优惠卷模板主键
*/
@ApiModelProperty("优惠卷模板主键")
private Integer id;
/**
* 优惠卷模板名称
*/
@ApiModelProperty("优惠卷模板名称")
private String couponName;
/**
* 优惠卷模板描述
*/
@ApiModelProperty("优惠卷模板描述")
private String couponDesc;
/**
* 优惠卷分类
*/
@ApiModelProperty("优惠卷分类")
private String category;
/**
* 产品线
*/
@ApiModelProperty("产品线")
private Integer productLine;
/**
* 优惠卷模板编码
*/
@ApiModelProperty("优惠卷模板编码")
private String couponKey;
/**
* 优惠卷规则
*/
@ApiModelProperty("优惠卷规则")
private TemplateRule rule;
}
0306优惠券分类
import com.park.pojo.conpon.Coupon;
import com.park.pojo.conpon.constant.CouponStatus;
import com.park.pojo.conpon.constant.PeriodType;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.time.DateUtils;
import java.util.ArrayList;
import java.util.List;
/**
* 根据优惠卷状态,用户优惠卷的分类
* @author 张震
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CouponClassify {
/**
* 可使用的优惠卷
*/
private List<Coupon> usable;
/**
* 已使用的优惠卷
*/
private List<Coupon> used;
/**
* 已过期的优惠卷
*/
private List<Coupon> expired;
/**
* 对当前传入的优惠卷进行分类
* @param coupons {@link Coupon}
* @return
*/
public static CouponClassify classify(List<Coupon> coupons){
List<Coupon> usables = new ArrayList<>(coupons.size());
List<Coupon> useds = new ArrayList<>(coupons.size());
List<Coupon> expireds = new ArrayList<>(coupons.size());
coupons.forEach(c -> {
//判断优惠卷是否过期
boolean isTimeExpire;
Long curTime = System.currentTimeMillis();//获取当前时间
if(c.getTemplateSDK().getRule().getExpiration().getDeadline()
.equals(PeriodType.REGULAR.getCode())){//如果 过期种类是固定的
isTimeExpire = c.getTemplateSDK().getRule().getExpiration()
.getDeadline() <= curTime;//判断是否小于当前时间(秒)
}else {//如果过期种类是领取时候计算开始时间
isTimeExpire = DateUtils.addDays(c.getAssignTime()//addDays方法是从第一个参数起始 加上第二个的时间(天)返回的日期
,c.getTemplateSDK().getRule().getExpiration().getGap()).getTime() <= curTime;//判断是否小于当前时间(秒)
}
if(c.getStatus() == CouponStatus.USABLE.getCode()){//如果优惠券是可使用状态就放在可用集合
useds.add(c);
}if(c.getStatus() == CouponStatus.EXPIRED.getCode() || isTimeExpire){//如果优惠券是过期状态 或者 过期boolean为true 放在过期集合
expireds.add(c);
}else {
usables.add(c);
}
});
return new CouponClassify(usables,useds,expireds);
}
0307用户领取优惠券 参数dto
import com.park.model.vo.coupon.CouponTemplateSdk;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 领取优惠卷请求对象
* @author 张震
*/
@AllArgsConstructor
@NoArgsConstructor
@Data
@ApiModel(value = "用户获取优惠券模板Dto", description = "CouponDto")
public class AcquireTemplateRequestDto {
/**
* 用户id
*/
@ApiModelProperty("用户id")
private Integer userId;
/**
* 优惠卷模板信息
*/
@ApiModelProperty("优惠卷模板信息")
private CouponTemplateSdk couponTemplateSdk;
}
0308MQ传递对象
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CouponRabbitMessage {
/**
* 优惠卷状态
*/
private Integer status;
/**
* 优惠卷主键
*/
private List<Integer> ids;
}
0309商品信息 (对应项目的商品信息)
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GoodsInfo {
/**
* 商品类型 例如停车项目 1普通车费 2充值会员 (7天体验 30天 连续包月)3储值
*/
private Integer type;
/**
* 商品价格
*/
private Double price ;
/**
* 商品的数量
*/
private Integer count;
}
0310结算信息dto
import com.park.model.vo.coupon.CouponTemplateSdk;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SettlementInfo {
/**
* 用户id
*/
private Integer userId;
/**
* 优惠卷列表
*/
private List<CouponAndTemplateInfo> couponAndTemplateInfos;
/**
* 商品信息
*/
private List<GoodsInfo> goodsInfos;
/**
* 结果结算金额
*/
private Double cost;
/**
* 是否使结算生效,即核销 true 表示核销 false表示结算
*/
private Boolean employ;
/**
* 优惠卷和模板信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class CouponAndTemplateInfo {
/**
* 优惠卷id
*/
private Integer id;
/**
* 优惠卷对应的模板对象
*/
private CouponTemplateSdk templateSdk;
}
}
coupon-template-service 优惠券模板服务(管理员用户(测试用户)使用)
优惠券模板服务主要有四个接口分别是
1.根据请求模板 获取优惠券模板(管理员用户(测试用户)创建优惠券种类包括指定优惠券规则等)
2.根据优惠券模板id 查询模板详情(管理员用户(测试用户)查看优惠券模板)
3.查看所有可用状态 优惠券模板
4.获取 模板id集合 和 sdk的映射(管理员用户查看模板对应的 sdk模板(微服务传输模板))
1.根据请求模板 获取优惠券模板(对应创建模板service)
参数:RequestTemplate(请求模板) 返回值:CouponTemplate (优惠券模板)
包结构
依赖
server:
port: 9300
spring:
application:
name: park-coupon-template-service
datasource:
# 数据源
url: jdbc:mysql://116.62.148.64/park_system?serverTimezone=UTC&autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: 123456
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.jdbc.Driver
# hikari连接池
hikari:
maximum-pool-size: 8
minimum-idle: 4
idle-timeout: 30000
connection-timeout: 30000
max-lifetime: 45000
auto-commit: true
pool-name: CouponHikariCP
redis:
database: 0
host: 116.62.148.64
port: 6379
# password:
# 连接超时时间
timeout: 10000
password: 123456
#微服务注册中心
cloud:
nacos:
discovery:
server-addr: 116.62.148.64:8848
# ------------------------------------------------------------------------------------------------------
#监控
management:
endpoints:
web:
exposure:
include: "*"
#微服务信息监控
info:
app:
name: icoupon-template-service
groupId: com.th.coupon
artifactId: icoupon-template-service
version: 1.0-SNAPSHOT
# ------------------------------------------------------------------------------------------------------
#打印日志级别
logging:
level:
com.th.coupon: debug
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:/mapper/**/*.xml
实现思路:
校验
- 使用TemplateRequest 中的 validate() 方法进行参数校验 不通过抛异常
- 判断请求模板中的模板名字是否重复 重复 抛出异常
通过校验
- 使用方法(hutool的拷贝或者是自定义方法)将请求中数据放到CouponTemplate (优惠券模板)中并给模板设置创建时间
- 将模板保存到数据库中
- 异步调用获取优惠券码接口
- 返回优惠券模板
service 实现
/**
* 根据请求模板获取优惠券模板
* @author 张震
* @return
*/
@Override
public CouponTemplate getCouponTemplate(TemplateRequest templateRequest) {
//首先进行 请求模板参数校验
if (!templateRequest.validate()) {
throw new BusinessException(BusinessCode.PARAMETER_VALIDITY_FAILED);
}
//判断模板名称是否重复 如果重复 抛异常
if (ObjectUtils.isNotEmpty(getOne(Wrappers.lambdaQuery(CouponTemplate.class)
.eq(CouponTemplate::getCouponName, templateRequest.getCouponName())))) {
throw new BusinessException(BusinessCode.THE_TEMPLATE_NAME);
}
CouponTemplate couponTemplate = requestToTemplate(templateRequest);
//异步生成优惠卷码并且将 优惠券模板保存到redis 和 db中
couponTemplate.setCreateTime(new Date());
save(couponTemplate);
asyncService.asyncGetCouponCodeByTemplate(couponTemplate);
return couponTemplate;
}
异步线程池配置
/**
* 异步线程池配置
* @author 张震
*/
@Slf4j
@Configuration
@EnableAsync
public class AsyncPoolConfig implements AsyncConfigurer {
@Bean
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心线程数
executor.setCorePoolSize(10);
//最大线程数
executor.setMaxPoolSize(20);
//任务队列数量
executor.setQueueCapacity(20);
//超出核心线程范围外的线程最大空闲时间
executor.setKeepAliveSeconds(60);
//线程名
executor.setThreadNamePrefix("couponAsync_");
//关闭程序也要使,当线程任务执行完,设置等待60m
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
//线程池策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncExceptionHandler();
}
class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {
throwable.printStackTrace();
log.error("AsyncError:{},Method:{},Param:{}",
throwable.getMessage(),
method.getName(),
JSON.toJSON(objects));
//TODO 发送邮件以及短信等 进一步处理
}
}
}
异步调用生成优惠券码储存redis
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.base.Stopwatch;
import com.park.mapper.CouponTemplateMapper;
import com.park.pojo.conpon.CouponTemplate;
import com.park.result.RedisPrefix;
import com.park.service.IAsyncService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* @author :张震
* date:2023/8/16
*/
@Service
@Slf4j
public class AsyncServiceImpl extends ServiceImpl<CouponTemplateMapper, CouponTemplate> implements IAsyncService {
StringRedisTemplate stringRedisTemplate;
@Autowired
public AsyncServiceImpl(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 使用模板异步生成优惠卷码
*
* @param couponTemplate
*/
@Override
@Async
public void asyncGetCouponCodeByTemplate(CouponTemplate couponTemplate) {
//开启计时器
Stopwatch started = Stopwatch.createStarted();
//获取18位 优惠卷码 根据算法生成
Set<String> strings = buildCouponCode(couponTemplate);
//设置 redis 存储key 使用格式化 将 key前缀 和 优惠卷模板id 拼接成key
String key = String.format("%s%s", RedisPrefix.COUPON_TEMPLATE, couponTemplate.getId().toString());
//将优惠卷存储到redis list 结构里面
stringRedisTemplate.opsForList().rightPushAll(key, strings);
//存储完毕就将优惠卷 状态设置可用
update(Wrappers.lambdaUpdate(CouponTemplate.class).set(CouponTemplate::getAvailable, true).eq(CouponTemplate::getId, couponTemplate.getId()));
//停止计时器
started.stop();
log.info("按模板成本构建优惠券代码:{}ms", started.elapsed(TimeUnit.MILLISECONDS));
//couponTemplate.getId 优惠卷模板 可用
log.info("CouponTemplate({}) Is Available!", couponTemplate.getId());
}
/**
* 构造优惠卷码
* 优惠卷码 (对应于每一张优惠卷 18位)
* 前4位 :产品线+类型
* 中间6位 : 日期随机(200306)
* 后8位 : 0 - 9 随机数构成
*
* @param couponTemplate {@link CouponTemplate} 实体类
* @return {@link Set} 与CouponTemplate.count相同个数的集合
*/
private Set<String> buildCouponCode(CouponTemplate couponTemplate) {
Stopwatch watch = Stopwatch.createStarted();
Set<String> result = new HashSet<String>();
//前四位
String prefix4 = couponTemplate.getProductLine() + couponTemplate.getCategory();
//中间6位
String date = new SimpleDateFormat("yyMMdd").format(couponTemplate.getCreateTime());
for (int i = 0; i < result.size(); i++) {
result.add(prefix4 + buildCouponCodeSuffix14(date));
}
while (result.size() < couponTemplate.getCouponCount()) {
result.add(prefix4 + buildCouponCodeSuffix14(date));
}
assert result.size() == couponTemplate.getCouponCount();
watch.stop();
log.info("build Coupon Code Cost:{}ms", watch.elapsed(TimeUnit.MILLISECONDS));
return result;
}
/**
* 构造位符卷码后 14位
*
* @param date 创建优惠卷的日期
* @return {@link String} 14位优惠卷码
*/
private String buildCouponCodeSuffix14(String date) {
char[] bases = new char[]{'1', '2', '3', '4', '5', '6', '7', '8', '9'};
//中间6位
List<Character> collect = date.chars().mapToObj(e -> (char) e).collect(Collectors.toList());
//洗牌重排算法
Collections.shuffle(collect);
//前62位
String mid6 = collect.stream().map(Objects::toString).collect(Collectors.joining());
//后8位
String suffix8 = RandomStringUtils.random(1, bases) + RandomStringUtils.randomNumeric(7);
return mid6 + suffix8;
}
}
2.根据优惠券模板id 查询模板详情
参数:id(模板id) 返回值:CouponTemplate (优惠券模板)
实现思路 一个查询
/**
* 根据模板id 查询模板详情
*
* @param id
* @return
*/
@Override
public CouponTemplate getCouponTemplateById(Integer id) {
CouponTemplate byId = getById(id);
if (ObjectUtils.isEmpty(byId)) {
throw new BusinessException(BusinessCode.TEMPLATE_IS_NOT_EXIST);
}
return byId;
}
3.查看所有可用状态 优惠券模板
参数:无 返回值: List (优惠券模板集合)
实现思路 将可用状态的没有过期的优惠券 查询出 使用方法转换成CouponTemplateSdk对象 (for stream都可以 )
/**
* 获取所有可用状态优惠券模板
*
* @return
*/
@Override
public List<CouponTemplateSdk> getAllAvailableCouponTemplate() {
List<CouponTemplate> couponTemplateList = list
(Wrappers.lambdaQuery(CouponTemplate.class)
.eq(CouponTemplate::getAvailable, true)
.eq(CouponTemplate::getExpired, false));
List<CouponTemplateSdk> list = new ArrayList<>();
for (CouponTemplate couponTemplate : couponTemplateList) {
CouponTemplateSdk couponTemplateSdk = template2TemplateSDK(couponTemplate);
list.add(couponTemplateSdk);
}
return list;
}
/**
* 将CouponTemplate 转换为 CouponTemplateSDK
*/
private CouponTemplateSdk template2TemplateSDK(CouponTemplate template) {
return new CouponTemplateSdk
(template.getId(), template.getCouponName(),
template.getCouponDesc(), template.getCategory(),
template.getProductLine(), template.getCouponKey(),
JSONUtil.toBean(template.getRule(), TemplateRule.class));
}
4.获取 模板id集合 和 sdk的映射
参数:List ids (注意 这个是优惠券的id 集合 不是 模板的id集合)返回值: Map<Integer, CouponTemplateSdk>(优惠券模板集合)
实现思路:
- 用id集合查询数据库模板集合
- 模板id为key 模板sdk(CouponTemplateSdk)为值 将数据库的模板集合 映射到Map中( 过滤key存在)
- 返回sdk(CouponTemplateSdk)
/**
* 获取 模板id集合 和 sdk的映射
*
* @param ids
* @return
*/
@Override
public Map<Integer, CouponTemplateSdk> getKeyByIdsAndValuesBySdk(List<Integer> ids) {
//通过id 集合获取所有 优惠券模板
List<CouponTemplate> couponTemplateList = listByIds(ids);
Map<Integer, CouponTemplateSdk> couponTemplateSdkMap = new HashMap<>();
for (CouponTemplate couponTemplate : couponTemplateList) {
//将获取的模板 copy到 sdk里
CouponTemplateSdk couponTemplateSdk = template2TemplateSDK(couponTemplate);
if (ObjectUtils.isNotEmpty(couponTemplateSdkMap.put(couponTemplateSdk.getId(), couponTemplateSdk))) {
throw new BusinessException(BusinessCode.KEY_EXIST);
}
}
return couponTemplateSdkMap;
}
coupon-user-service 优惠券用户服务(和用户(普通用户交互))
优惠券用户服务主要有四个接口分别是
1.根据用户id和优惠券 状态查询优惠券集合(用户查看 可用 非可用 过期 优惠券)
2.查询当前用户可以领取的sdk模板(当用户查询自己可以领取什么券的时候使用)
3.用户领取优惠券
4.优惠卷核销(用户使用优惠券)
包结构
依赖
yml
server:
port: 9400
spring:
application:
name: park-coupon-user-service
datasource:
# 数据源
url: jdbc:mysql://116.62.148.64/park_system?serverTimezone=UTC&autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: 123456
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.jdbc.Driver
# hikari连接池
hikari:
maximum-pool-size: 8
minimum-idle: 4
idle-timeout: 30000
connection-timeout: 30000
max-lifetime: 45000
auto-commit: true
pool-name: CouponHikariCP
#redis
redis:
database: 0
host: 116.62.148.64
port: 6379
# password:
# 连接超时时间
timeout: 10000
password: 123456
#rabbitmq
rabbitmq:
host: 116.62.148.64
username: user
password: 123456
#微服务注册中心
cloud:
nacos:
discovery:
server-addr: 116.62.148.64:8848
logging:
level:
com.th.coupon: debug
feign:
hystrix:
enabled: false #开启熔断 hystrix 先关掉
client:
config:
default: #全局
readTimeout: 5000 #读取接口的数据响应信息
coupon-template-service: #单独服务的配置
readTimeout: 5000
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:/mapper/**/*.xml
1.根据用户id和优惠券 状态查询优惠券集合
参数:Integer userId, Integer status (优惠券状态对应 优惠券状态枚举code) 返回值:List(优惠券集合)
实现思路 :
第一步 : 我们根据 传进来的用户id 和优惠券的状态 去缓存中查询 用到了getCachedCoupons(方法)(并建立一个返回结果的List)
* 缓存中查询优惠券集合(getCachedCoupons())
* 1.传入状态 和userid 通过方法statusUserIdGetRedisKey()获取 key(下面有)
* 2.通过key 去缓存中查询优惠券值的集合 并遍历 将每一个值用过String.valueof方法转换成 string集合
* 3.如果 获取的集合是 null的 (缓存没有查到) 就保存 一个null的list 到 redis防止缓存穿透
(使用saveEmptyCouponListToCache()方法下面有)
* 4.遍历获取到的集合 通过alibaba 的json 工具将字符串解析成 优惠券对象 并add到 优惠券集合里 返回
上述getCachedCoupons()方法中用到的statusUserIdGetRedisKey()
/**
* 根据 status获取到对应的redis key
*
* @param status
* @param userId
* @return
*/
private String statusUserIdGetRedisKey(Integer status, Integer userId) {
String redisKey = null;
CouponStatus of = CouponStatus.of(status);
switch (of) {
case USABLE:
//RedisPrefix.USER_COUPON_USABLE = discount_coupon_template_code_
redisKey = String.format("%s%s", RedisPrefix.USER_COUPON_USABLE, userId);
break;
case USED:
redisKey = String.format("%s%s", RedisPrefix.USER_COUPON_USED, userId);
break;
case EXPIRED:
redisKey = String.format("%s%s", RedisPrefix.USER_COUPON_EXPIRED, userId);
break;
}
return redisKey;
}
上述getCachedCoupons()方法中用到的saveEmptyCouponListToCache() 主要是防止缓存穿透
/**
* 保存空list 到 redis 防止缓存穿透
*
* @param userId
* @param singletonList
*/
private void saveEmptyCouponListToCache(Integer userId, List<Integer> singletonList) {
Map<String, String> map = new HashMap<>();
//存储一个key为-1 的空集合
map.put("-1", JSON.toJSONString(Coupon.invalidCoupon()));
//使用SessionCallback进行批处理
SessionCallback<Object> objectSessionCallback = new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
singletonList.forEach(s -> {
//获取key
String redisKey = statusUserIdGetRedisKey(s, userId);
redisOperations.opsForHash().putAll(redisKey, map);
});
return null;
}
};
//直接redis 批处理 sessioncallback
stringRedisTemplate.executePipelined(objectSessionCallback);
}
第二步01(缓存查到了):
将其赋值给新List 并且进行 后续缓存处理(第三步)
第二步02(缓存没查到):
去数据库查询(根据用户id 和 优惠券状态)查询到优惠券集合 判断
—01数据库没查到(优惠券集合为null)
- 将数据库查到的空优惠券集合 直接return
—02数据库查到了
将数据库查到的优惠券集合中的id 传到新集合List 中
- 用这个id集合远程调用 模板服务 4 .获取 模板id集合 和 sdk的映射
- 遍历数据库查到的优惠券集合并用 每一个优惠券里的模板id TemplateId当作key 去映射里面取值 取到的sdk 设置到这个优惠券的模板sdk 属性 并赋值给 返回结果 list
- 使用redisService.addCouponToCache(userId, returnCouponList, status); 使用redis service 将用户id (传入参数)返回结果list 和 优惠券状态(传入参数)进行缓存处理
redisService.addCouponToCache(userId,List,status)方法
/**
* 将一个或多个优惠券 加入到缓存中
*
* @param userId
* @param returnCouponList
* @param status
* @return
*/
@Override
public Integer addCouponToCache(Integer userId, List<Coupon> returnCouponList, Integer status) {
//给一个 返回值默认值
Integer integer = -1;
//判断状态是什么
CouponStatus of = CouponStatus.of(status);
switch (of) {
//状态已使用的(用户使用过的)
case USED:
integer = addCouponToCacheToUSED(userId, returnCouponList);
break;
//状态为可用的刚加入的新优惠券
case USABLE:
integer = addCouponToCacheToUSABLE(userId, returnCouponList);
break;
//状态为过期的
case EXPIRED:
integer = addCouponToCacheToEXPIRED(userId, returnCouponList);
break;
}
return integer;
}
三种缓存处理需要用到的随机数方法
/**
* 获取随机的过期时间 解决缓存雪崩问题 : key在同一时间失效
*
* @param min 最小的小时数
* @param max 最大的小时数
* @return 返回 [min,max] 之间的随机秒数
*/
private Long getRandomExpirationTime(Integer min, Integer max) {
return RandomUtils.nextLong(min * 60 * 60, max * 60 * 60);
}
三种缓存处理需要用到的根据 status ,userid获取到对应的redis key方法
/**
* 根据 status获取到对应的redis key
*
* @param status
* @param userId
* @return
*/
private String statusUserIdGetRedisKey(Integer status, Integer userId) {
String redisKey = null;
CouponStatus of = CouponStatus.of(status);
switch (of) {
case USABLE:
redisKey = String.format("%s%s", RedisPrefix.USER_COUPON_USABLE, userId);
break;
case USED:
redisKey = String.format("%s%s", RedisPrefix.USER_COUPON_USED, userId);
break;
case EXPIRED:
redisKey = String.format("%s%s", RedisPrefix.USER_COUPON_EXPIRED, userId);
break;
}
return redisKey;
}
三种缓存处理需要用到的根据模板id 去缓存中拿去 优惠券码 (拿取存储在redislist结构最左边的优惠券码)
/**
* 根据模板id 去缓存中拿去 优惠券码
*
* @param id
* @return
*/
@Override
public String getCouponCodeByCache(Integer id) {
String temKey = String.format("%s%s", RedisPrefix.COUPON_TEMPLATE, id);
String couponCodeByLeft = stringRedisTemplate.opsForList().leftPop(temKey);
return couponCodeByLeft;
}
redisService.addCouponToCache(userId,List,status)方法 中对应的 优惠券状态为 USEABLE(可用)的缓存处理
/**
* 通过用户id 和 优惠券集合(一个或多个优惠券) 添加到缓存中(可用USABLE)
* * 1.如果优惠卷 状态是 USABLE (可用的) 就说明 是新增加的优惠卷 将优惠卷的id转成字符串 当key 将 自己转成json当values
* * 放到新的HashMap中
* * 2.将USABLE 的code码 和 userid 当key 将 这个新的HashMap 当 values 储存在 redis 里 并且设置过期时间 1-2 h 之内
* * 并返回 HashMap 的 size
*
* @param userId
* @param returnCouponList
* @return
*/
private Integer addCouponToCacheToUSABLE(Integer userId, List<Coupon> returnCouponList) {
//创建一个新的Map当values之后会保存到redis中 , 这个新map 的key 为id的string values 是这个优惠券集合 的json
Map<String, String> objectObjectHashMap = new HashMap<>();
for (Coupon coupon : returnCouponList) {
objectObjectHashMap.put(coupon.getId().toString(), JSON.toJSONString(coupon));
}
//通过方法获取key
String redisKey = statusUserIdGetRedisKey(CouponStatus.USABLE.getCode(), userId);
//将其储存redis
stringRedisTemplate.opsForHash().putAll(redisKey, objectObjectHashMap);
//设置过期时间(续签)
stringRedisTemplate.expire(redisKey, getRandomExpirationTime(1, 2), TimeUnit.SECONDS);
//返回map的大小
return objectObjectHashMap.size();
}
redisService.addCouponToCache(userId,List,status)方法 中对应的 优惠券状态为 USED(已使用)的缓存处理
/**
* 通过用户id 和 优惠券集合(一个或多个优惠券) 添加到缓存中
* 将以使用的优惠卷加入到Cache中
* 首先再拿到status 是USED 的 就代表了 是 用户使用当前的 优惠券
* 我们 的准备工作 :
* 第一步 获取 ---可以使用状态的优惠卷 ---缓存 key 通过 status2RedisKey()方法 传入 USABLE code 和 userid
* 第二步 获取----已经使用的优惠卷---- 缓存key 通过 status2RedisKey()方法 传入 USED code 和 userid
* 第三步 我们以传进来的 优惠券集合 的 长度来定义一个 存储----已经使用的优惠卷的信息 的 HashMap ----
* 第四步 我们 通过 getCachedCoupons() 传入 userId 和 USABLE code 来获取---当前可用的优惠券的 集合 List ----
* 通过 断言 assert 来 判断 当前可用的优惠券的集合 的 大小 是否 比 传入的 优惠券集合 list 大(防止传入的比可用的多)
* 准备工作完毕
* 1 . 我们 将马上要使用的 (传入的)优惠券集合 放到 已经使用的优惠卷 的Map集合里 id(string)为key 本身的 json 为 值
* 2.准备 两个list
* 01: 第一个 将从缓存中获取的当前可用状态的优惠券 的list 遍历 的 优惠券 id 存储的 list
* 02: 第二个 传进来 准备使用的优惠券集合 遍历的id 存储的list
* 之后 用集合工具!CollectionUtils.isSubCollection(paramIds, curUsableIds) 判断 准备使用的在不在 可用优惠卷 中
* 再准备一个 list 是 用完 准备使用的优惠券集合的 (马上要 再 可用中清空)id(String) 集合
* 3. new 一个 sessionCallable 重写 excute 方法 启动 批处理
* redisOperations.opsForHash().putAll ::直接以map集合的方式添加key对应的值 (map中key已经存在,覆盖替换 map中key不存在,新增)
* redisOperations.opsForHash().delete :: 看key 里面有有没有 map里 id 如果有 就将 前面的 删除
* 4.重置过期时间 redisOperations.expire(redisKeyForUsable, getRandomExpirationTime(1, 2), TimeUnit.SECONDS);
* 5. stringRedisTemplate.executePipelined(sessionCallback)) 提交批处理
* 6.返回 长度
*
* @param userId
* @param returnCouponList
* @return
*/
private Integer addCouponToCacheToUSED(Integer userId, List<Coupon> returnCouponList) {
//获取USABLE 状态 key
String redisKeyUSABLE = statusUserIdGetRedisKey(CouponStatus.USABLE.getCode(), userId);
//获取USED 状态 key
String redisKeyUSED = statusUserIdGetRedisKey(CouponStatus.USED.getCode(), userId);
//已使用优惠券信息的 map
Map<String, String> map = new HashMap<>(returnCouponList.size());
//获取当前可用的优惠券集合
List<Coupon> cachedCoupons = getCachedCoupons(userId, CouponStatus.USABLE.getCode());
//判断可用优惠券数量是否足够
if (!(cachedCoupons.size() > returnCouponList.size())) {
throw new BusinessException(BusinessCode.COUPON_EXCEED_ASSIGN_LIMITATION);
}
for (Coupon coupon : returnCouponList) {
map.put(coupon.getId().toString(), JSON.toJSONString(coupon));
}
//当前可用优惠券idlist集合
List<Integer> cachedCouponsidList = new ArrayList<>();
for (Coupon cachedCoupon : cachedCoupons) {
cachedCouponsidList.add(cachedCoupon.getId());
}
//将要使用优惠券idlist集合
List<Integer> returnCouponidList = new ArrayList<>();
for (Coupon coupon : returnCouponList) {
returnCouponidList.add(coupon.getId());
}
if (!CollectionUtils.isSubCollection(returnCouponidList, cachedCouponsidList)) {
throw new BusinessException(BusinessCode.COUPON_EXCEED_ASSIGN_LIMITATION);
}
//将要清除的id(string) 集合
List<String> isDeleteCouponidList = new ArrayList<>();
for (Integer returnCouponid : returnCouponidList) {
isDeleteCouponidList.add(returnCouponid.toString());
}
SessionCallback<Object> objectSessionCallback = new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
//存储已经使用的优惠券
redisOperations.opsForHash().putAll(redisKeyUSED, map);
//清除可用优惠券缓存key 中 已经用完的key 的优惠券(isDeleteCouponidList)
redisOperations.opsForHash().delete(redisKeyUSABLE, isDeleteCouponidList.toArray());
redisOperations.expire(redisKeyUSED, getRandomExpirationTime(1, 2), TimeUnit.SECONDS);
redisOperations.expire(redisKeyUSABLE, getRandomExpirationTime(1, 2), TimeUnit.SECONDS);
return null;
}
};
stringRedisTemplate.executePipelined(objectSessionCallback);
return map.size();
}
redisService.addCouponToCache(userId,List,status)方法 中对应的 优惠券状态为 EXPIRED(已过期)的缓存处理
整体流程 大概和 处理已使用状态缓存处理 相同
/**
* 通过用户id 和 优惠券集合(一个或多个优惠券) 添加到缓存中
*
* @param userId
* @param returnCouponList
* @return
*/
private Integer addCouponToCacheToEXPIRED(Integer userId, List<Coupon> returnCouponList) {
//先拿一个map 用来存储过期 优惠券values 存入缓存中
Map<String, String> expiredMap = new HashMap<>();
//获取可用状态优惠券 缓存key
String redisKeyUSABLE = statusUserIdGetRedisKey(CouponStatus.USABLE.getCode(), userId);
//获取过期状态优惠券 缓存key
String redisKeyEXPIRED = statusUserIdGetRedisKey(CouponStatus.EXPIRED.getCode(), userId);
//获取可用状态缓存优惠券 集合
List<Coupon> cachedCouponsUSABLE = getCachedCoupons(userId, CouponStatus.USABLE.getCode());
//判断可用集合 是否大于将用集合
if (!(cachedCouponsUSABLE.size() > returnCouponList.size())) {
throw new BusinessException(BusinessCode.COUPON_EXCEED_ASSIGN_LIMITATION);
}
for (Coupon coupon : returnCouponList) {
expiredMap.put(coupon.getId().toString(), JSON.toJSONString(coupon));
}
//创建一个可用状态优惠卷 id 的集合
List<Integer> useableCacheCouponidList = new ArrayList<>();
//遍历可用状态优惠券 获取id
for (Coupon coupon : cachedCouponsUSABLE) {
useableCacheCouponidList.add(coupon.getId());
}
//创建一个过期优惠卷id 集合
List<Integer> expiredCacheCouponidList = new ArrayList<>();
for (Coupon coupon : returnCouponList) {
expiredCacheCouponidList.add(coupon.getId());
}
//判断当前可用优惠券是否充足
if (!CollectionUtils.isSubCollection(expiredCacheCouponidList, useableCacheCouponidList)) {
throw new BusinessException(BusinessCode.COUPON_EXCEED_ASSIGN_LIMITATION);
}
//创建一个将要清除的id(string) 集合
List<String> cleanCacheCouponidList = new ArrayList<>();
for (Integer integer : expiredCacheCouponidList) {
cleanCacheCouponidList.add(integer.toString());
}
SessionCallback<Object> objectSessionCallback = new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
redisOperations.opsForHash().putAll(redisKeyEXPIRED, expiredMap);
redisOperations.opsForHash().delete(redisKeyUSABLE, cleanCacheCouponidList.toArray());
redisOperations.expire(redisKeyEXPIRED, getRandomExpirationTime(1, 2), TimeUnit.SECONDS);
redisOperations.expire(redisKeyUSABLE, getRandomExpirationTime(1, 2), TimeUnit.SECONDS);
return null;
}
};
stringRedisTemplate.executePipelined(objectSessionCallback);
return expiredMap.size();
}
第三步01:后续缓存处理(主要目的是为了在用户传入状态是可用状态时候的后续缓存处理)
-
将第二步02 (缓存没查到 但是数据库查到了并进行储存)的用户返回的list 进行剔除 id=-1 的处理 因为在Hytrix 熔断中我们给了保障熔断后的id 会设置成-1
-
我们使用优惠券分组对象(左侧大纲0306)将第二步02 的用户返回的list 进行 分类(具体看0306的分类规则)
-
分类之后我们将过期集合进行缓存处理 (具体看处理过期优惠券的方法)
-
之后将过期集合遍历获取到过期集合中的优惠券id加入到新集合中(过期优惠券id集合)
-
发送MQ 信息为 MQ传递对象(左侧大纲0308)对象的参数1 就是 优惠券状态枚举(左侧大纲0209)中的 EXPIRED 的code
参数2为过期优惠券id集合 MQ 的队列和交换机 在(左侧大纲0308)
第三步02:MQ处理01 发送的消息
- 获取MQ传递对象中的状态 根据 状态 选择对应的处理方法
- 通过MQ传递对象中的idlist 获取 优惠券集合 遍历修改每一个优惠券的状态 (根据传进来的状态)
- 保存优惠券集合到数据库
- 返回分组对象(左侧大纲0306) 的 可用状态优惠券集合
MQservice
**
* @author :张震
* date:2023/8/17
*/
@Service
public class IRabbitMQServiceImpl extends ServiceImpl<UserMapper, Coupon> implements IRabbitMQService {
/**
* 接收UserController 发送的消息
*
* @param couponRabbitMessage
*/
@Override
@RabbitListener(bindings = @QueueBinding(value = @Queue(name = DISCOUNT_USER_COUPON_QUEUE),
exchange = @Exchange(name = DISCOUNT_USER_COUPON_EXCHANGE, type = ExchangeTypes.TOPIC),
key = DISCOUNT_USER_COUPON_KEY
))
public void consumerCouponRabbitMessage(@Payload CouponRabbitMessage couponRabbitMessage) {
Optional<CouponRabbitMessage> couponRabbitMessageOptional = Optional.ofNullable(couponRabbitMessage);
if (couponRabbitMessageOptional.isPresent()) {
CouponRabbitMessage couponRabbitMessage1 = couponRabbitMessageOptional.get();
Integer status = couponRabbitMessage1.getStatus();
CouponStatus of = CouponStatus.of(status);
switch (of) {
case USABLE:
break;
case USED:
processUsedCoupons(couponRabbitMessage, status);
break;
case EXPIRED:
processExpiredCoupons(couponRabbitMessage, status);
break;
}
}
}
/**
* 修改优惠券状态 同步数据库
*
* @param couponRabbitMessage
* @param status
*/
private void processCouponsByStatus(CouponRabbitMessage couponRabbitMessage, Integer status) {
//通过id集合在数据库获取 优惠券集合
List<Integer> ids = couponRabbitMessage.getIds();
List<Coupon> couponList = listByIds(ids);
//判断集合是否为空
if (ObjectUtils.isEmpty(couponList)) {
return;
}
//修改每一个优惠券的状态
couponList.forEach(coupon -> {
coupon.setStatus(status);
});
//保存优惠券集合
saveBatch(couponList);
}
/**
* 状态是过期
*
* @param couponRabbitMessage
* @param status
*/
private void processExpiredCoupons(CouponRabbitMessage couponRabbitMessage, Integer status) {
//在这里可以进行通知用户操作
processCouponsByStatus(couponRabbitMessage, status);
}
/**
* 状态 是 已使用的
*
* @param couponRabbitMessage
* @param status
*/
private void processUsedCoupons(CouponRabbitMessage couponRabbitMessage, Integer status) {
//在这里可以进行通知用户操作
processCouponsByStatus(couponRabbitMessage, status);
}
}
第四步 返回第二步02的集合
有可能不懂的解释:
- 为什么过期和 使用过的 状态的 缓存处理啊要 将 储存在 redis 的 时候设置 1-2 小时的过期时间
因为要 避免同一时间大量key失效导致缓存雪崩
2.查询当前用户可以领取的sdk模板
参数:Integer userId, 返回值:List (优惠券模板Sdk集合)
实现思路:
- 远程调用所有可用状态优惠券模板Sdk api (模板服务3) 获取可用状态优惠卷模板Sdk集合
- 剔除 所有过期的 优惠券模板sdk
- 创建一个 Map<Integer, Pair<Integer, CouponTemplateSdk>> pairMap = new HashMap<>();
从优惠卷模板Sdk集合 中 遍历获取
每个sdk 模板的 id 当 pairMap 的 key
每个sdk模板 中的 rule中的li’mitation(优惠券规则中的闲置领取数量)当Pair的left
每个sdk模板当Pair 的right
pair 为 pairMap 的 value
-
调用本服务中的 1 方法 获取用户所有可用状态的优惠券集合 将其根据模板id 分组成Map
-
将pairMap 遍历 判断 用户可用的 是否在所有可用的中(用户有了这种模板的优惠券) 并且 看一下 用户可用的优惠券集合的数量 是否大于 限制领取的数量 (用户是否达到 这种模板优惠券的领取限制) 如果都符合直接 return (stream中的return 是退出本次循环) 如果符合 则说明 用户可以领取 这种模板对应的优惠券 就获取 pair 右侧的 模板sdk
放到 一个新集合中 List 返回
@Override public List<CouponTemplateSdk> getAvailableSdk(Integer userId) { //获取当前时间 long currentTimeMillis = System.currentTimeMillis(); //远程调用所有可用状态的模板 Result<List<CouponTemplateSdk>> re = couponTemplateApi.getAllAvailableCouponTemplate(); List<CouponTemplateSdk> allAvailableCouponTemplateList = re.getData(); //判断是否过期如果过期就剔除 List<CouponTemplateSdk> list = new ArrayList<>(); for (CouponTemplateSdk couponTemplateSdk : allAvailableCouponTemplateList) { if (couponTemplateSdk.getRule().getExpiration().getDeadline() > currentTimeMillis) { list.add(couponTemplateSdk); } } allAvailableCouponTemplateList = list; Map<Integer, Pair<Integer, CouponTemplateSdk>> pairMap = new HashMap<>(); allAvailableCouponTemplateList.forEach(a -> { pairMap.put(a.getId(), Pair.create(a.getRule().getLimitation(), a)); }); //获取用户可用优惠券集合 List<Coupon> couponListByUserIdAndStatus = getCouponListByUserIdAndStatus(userId, CouponStatus.USABLE.getCode()); //根据模板id 进行分组 Map<Integer, List<Coupon>> groupByComplateId = new HashMap<>(); for (Coupon listByUserIdAndStatus : couponListByUserIdAndStatus) { groupByComplateId.computeIfAbsent(listByUserIdAndStatus.getTemplateId(), key -> new ArrayList<>()).add(listByUserIdAndStatus); } List<CouponTemplateSdk> resultCouponTemplateSdkList = new ArrayList<>(); //将大map进行遍历 pairMap.forEach((k, v) -> { //可领取的张数 Integer limitationNum = v.getFirst(); CouponTemplateSdk couponTemplateSdkRight = v.getSecond(); if (groupByComplateId.containsKey(k) && groupByComplateId.get(k).size() >= limitationNum) { return; } resultCouponTemplateSdkList.add(couponTemplateSdkRight); }); return resultCouponTemplateSdkList; }
3.用户领取优惠券
参数:AcquireTemplateRequestDto(大纲0307) , 返回值:Coupon
实现思路:
-
远程调用模板服务的 4 获取 模板id 和sdk 的映射 map
-
根据dto 中的用户id 调用 本服务1方法 获取用户的可用状态的 优惠券 集合 并根据 这些 优惠券的模板id 进行分组
-
判断 根据dto中的sdk里的 模板id 去分组查看 是否 包含键 获取到为true
根据dto中的sdk里的 模板id去分组中 获取到 的优惠券集合中优惠券的数量是否达到了最大领取限制 达到为true
两项都为true 直接 抛出异常
-
调用 redisService 中的 模板id获取 缓存list 的优惠券码的方法 获取一个这种模板类型的优惠券码
-
new 一个优惠券 对象 设置id(dto 中)userid(dto中) 优惠券码(刚从redis拿的)状态码去优惠券状态枚举(0209)找到可用的code 设置 领取时间 为当前时间
-
保存这个 优惠券到数据库
-
调用redisService 中的缓存处理
@Override
public Coupon getCouponByUserIdAndSdk(AcquireTemplateRequestDto acquireTemplateRequestDto) {
//远程调用获取模板映射
Result<Map<Integer, CouponTemplateSdk>> mapResult = couponTemplateApi.getKeyByIdsAndValuesBySdk
(Collections.singletonList(acquireTemplateRequestDto.getCouponTemplateSdk().getId()));
Map<Integer, CouponTemplateSdk> couponTemplateSdkMap = mapResult.getData();
//判断是否拿到了模板
if (couponTemplateSdkMap.size() <= 0) {
throw new BusinessException(BusinessCode.NO_ACQUIRE_COUPON);
}
List<Coupon> couponListByUserIdAndStatus = getCouponListByUserIdAndStatus(acquireTemplateRequestDto.getUserId(),
CouponStatus.USABLE.getCode());
//根据这些优惠券中的模板id进行分组
Map<Integer, List<Coupon>> groupByTemIdList = couponListByUserIdAndStatus.stream().filter(s -> !ObjectUtils.isEmpty(s.getTemplateId())).collect(Collectors.groupingBy(Coupon::getTemplateId));
if (groupByTemIdList.containsKey(acquireTemplateRequestDto.getCouponTemplateSdk().getId()) //判断在用户可用状态的优惠券模板中是否是传进来的(请求的)模板
&& groupByTemIdList.get(acquireTemplateRequestDto.getCouponTemplateSdk().getId()).size() >= //并且用户可用优惠券 模板 是否已经达到最大领取限度
acquireTemplateRequestDto.getCouponTemplateSdk().getRule().getLimitation()) {
throw new BusinessException(BusinessCode.COUPON_EXCEED_ASSIGN_LIMITATION);
}
//根据模板id 去缓存 中 拿取list 中的优惠券码
String CouponCode = redisService.getCouponCodeByCache(acquireTemplateRequestDto.getCouponTemplateSdk().getId());
if (ObjectUtils.isEmpty(CouponCode)) {
throw new BusinessException(BusinessCode.CODE_NOT_EXIST);
}
Coupon coupon = new Coupon(acquireTemplateRequestDto.getCouponTemplateSdk().getId(),
acquireTemplateRequestDto.getUserId(),
CouponCode,
CouponStatus.USABLE.getCode());
coupon.setAssignTime(new Date());
save(coupon);
coupon.setTemplateSDK(acquireTemplateRequestDto.getCouponTemplateSdk());
redisService.addCouponToCache(acquireTemplateRequestDto.getUserId(), Collections.singletonList(coupon), CouponStatus.USABLE.getCode());
return coupon;
}
4.优惠卷核销
参数:SettlementInfo(大纲0310 结算信息对象) , 返回值:SettlementInfo(经过结算服务处理后的结算信息对象)
实现思路:
- 获取结算信息对象中的 优惠券信息结合
- 判断是否为null (看是不是使用了优惠券) 如果为null 直接按商品总价计算 SettlementInfo 对象中有商品信息 集合(对应0309)
如果不是null 就说明用了优惠券 先调用 本服务 的 1 方法 查询 用户可用 优惠券集合 根据优惠券的id 进行分组 为 Map
- 将SettlementInfo 中的优惠券信息 中的优惠券id 放进list
- 将 用户可用 优惠券集合 中的优惠券id 放进list
- 判断分组 的Map是否为空 (为空 true) 结算信息优惠卷id集合 是不是 用户可用优惠券id集合的子集(不是子集为true) 两项为true抛出异常
- 将SettlementInfo 中的优惠券信息 集合遍历 通过优惠券信息 中的优惠券id 去分组Map中 get 优惠卷 并将 其放入新集合中
- 远程调用 结算服务
- 如果结算服务返回的 employ(是否使结算生效,即核销 true 表示核销 false表示结算)为true 并且 结算服务返回的优惠券信息集合不是null 调用 redisService 缓存处理 加入USED(使用过的) list 中 并且发送MQ (对象为0308 code为USED list为优惠券id集合)
@Override
public SettlementInfo userSettlement(SettlementInfo settlementInfo) {
//首先获取dto的优惠券信息列表
List<SettlementInfo.CouponAndTemplateInfo> couponAndTemplateInfosList = settlementInfo.getCouponAndTemplateInfos();
//判断是否为null (是否使用了优惠券)
if (ObjectUtils.isEmpty(couponAndTemplateInfosList)) {
List<GoodsInfo> goodsInfos = settlementInfo.getGoodsInfos();
double aPrice = 0.0;
for (GoodsInfo goodsInfo : goodsInfos) {
Double price = goodsInfo.getPrice();
Integer count = goodsInfo.getCount();
double p = price * count;
aPrice += p;
}
Double allPrice = aPrice;
settlementInfo.setCost(retain2Decimals(allPrice, 2));
return settlementInfo;
}
//获取用户可用优惠券集合
List<Coupon> couponListByUserIdAndStatus =
getCouponListByUserIdAndStatus(settlementInfo.getUserId(), CouponStatus.USABLE.getCode());
Map<Integer, Coupon> couponMap = couponListByUserIdAndStatus.stream().collect(Collectors.toMap(Coupon::getId, Function.identity()));
List<Integer> collect = couponListByUserIdAndStatus.stream().map(Coupon::getId).collect(Collectors.toList());
for (Integer i : collect) {
log.debug("collect:{}",i);
}
List<Integer> collect1 = couponAndTemplateInfosList.stream().
map(SettlementInfo.CouponAndTemplateInfo::getId).collect(Collectors.toList());
for (Integer i : collect1) {
log.debug("collect:{}",i);
}
if (MapUtils.isEmpty(couponMap) ||
!CollectionUtils.isSubCollection(collect1,collect)) {
throw new BusinessException(BusinessCode.USER_COUPON_HAS_SOME_PROBLEM);
}
//根据传进的优惠券信息列表 获取 用户可用优惠券列表的list
List<Coupon> reCouponList = new ArrayList<>(couponAndTemplateInfosList.size());
for (SettlementInfo.CouponAndTemplateInfo couponAndTemplateInfo : couponAndTemplateInfosList) {
Coupon coupon = couponMap.get(couponAndTemplateInfo.getId());
reCouponList.add(coupon);
}
//远程调用结算服务
SettlementInfo data = settlementApi.userSettlements(settlementInfo).getData();
if (data.getEmploy() && ObjectUtils.isNotEmpty(data.getCouponAndTemplateInfos())) {
redisService.addCouponToCache(data.getUserId(), reCouponList, CouponStatus.USED.getCode());
rabbitTemplate.convertAndSend(RabbitMq.DISCOUNT_USER_COUPON_EXCHANGE,
RabbitMq.DISCOUNT_USER_COUPON_QUEUE,
new CouponRabbitMessage(CouponStatus.USED.getCode(),
reCouponList.stream().map(Coupon::getId).collect(Collectors.toList())));
}
return data;
}
coupon-user-service 优惠券结算服务(用户服务调用)
在说这个服务之前呢我们先想一下 怎么优雅的去 实现 单接口 多种类优惠券的核销
我们先写一个接口
@RestController
@RequestMapping("/settlement")
@Slf4j
public class SettlementController {
Executormanager executormanager;
@Autowired
public SettlementController(Executormanager executormanager) {
this.executormanager = executormanager;
}
@ApiOperation("用户服务远程调用结算优惠券")
@PostMapping("/userSettlements")
public Result<SettlementInfo> userSettlements(@RequestBody SettlementInfo settlementInfo) {
SettlementInfo settlementInfoResult = executormanager.userSettlements(settlementInfo);
return Result.ok(settlementInfoResult);
}
}
这个接口我们注入了一个 Executormanager 这个东西长这样
@Component
@Slf4j
public class Executormanager implements BeanPostProcessor {
public static Map<RuleFlag, RuleExecutor> executorIndex = new HashMap<>(RuleFlag.values().length);
public SettlementInfo userSettlements(SettlementInfo settlementInfo) {
SettlementInfo returns = null;
//判断是否是一张优惠卷
if (settlementInfo.getCouponAndTemplateInfos().size() == 1) {
//一张
List<String> collect = settlementInfo.getCouponAndTemplateInfos().stream().map(SettlementInfo.CouponAndTemplateInfo::getTemplateSdk).map(CouponTemplateSdk::getCategory).collect(Collectors.toList());
String couponType = collect.get(0);
CouponCategory couponCategory = CouponCategory.of(couponType);
switch (couponCategory) {
//满减
case ALLMINUS:
returns = executorIndex.get(RuleFlag.ALLMINUS_RULE).settlementReturn(settlementInfo);
break;
//折扣
case DISCOUNT:
returns = executorIndex.get(RuleFlag.DISCOUNT_RULE).settlementReturn(settlementInfo);
break;
//立减
case SETUPMINUS:
returns = executorIndex.get(RuleFlag.SETUPMINUS_RULE).settlementReturn(settlementInfo);
break;
}
} else {
//拿到两张优惠券类型集合
List<String> categorieList = settlementInfo.getCouponAndTemplateInfos().stream().map(SettlementInfo.CouponAndTemplateInfo::getTemplateSdk).map(CouponTemplateSdk::getCategory).collect(Collectors.toList());
if (categorieList.size() != 2) {
throw new BusinessException(BusinessCode.NOT_SUPPORT_TEMPLATE);
} else {
if (categorieList.contains(CouponCategory.ALLMINUS.getCode()) && categorieList.contains(CouponCategory.DISCOUNT.getCode())) {
returns = executorIndex.get(RuleFlag.ALLMINUS_DISCOUNT_RULE).settlementReturn(settlementInfo);
} else {
throw new BusinessException(BusinessCode.NOT_SUPPORT_TEMPLATE);
}
}
}
return returns;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (!(bean instanceof RuleExecutor)) {
return bean;
}
RuleExecutor executor = (RuleExecutor) bean;
RuleFlag ruleFlag = executor.ruleFlag();
if (executorIndex.containsKey(ruleFlag)) {
throw new BusinessException(BusinessCode.KEY_EXIST);
}
executorIndex.put(ruleFlag, executor);
return null;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
我们发现他实现了BeanPostProcessor 接口 重写了两个方法 postProcessBeforeInitialization() 和 postProcessAfterInitialization()
这是什么东西呢我们先看一下spring bean 的生命周期
是不是发现了spring中 在初始化之前 调用了postProcessBeforeInitialization()方法
而 这个类中是不是 有一个静态 map
public static Map<RuleFlag, RuleExecutor> executorIndex = new HashMap<>(RuleFlag.values().length);
没错 在这个类初始化之前呢 我们 已经通过postProcessBeforeInitialization()将这个Map 完成了映射了 对把 而这个Map 中的键值对的两个类
@Getter
@AllArgsConstructor
public enum RuleFlag {
//单类别优惠卷定义
ALLMINUS_RULE("满减卷的计算规则"),
DISCOUNT_RULE("折扣卷的规则"),
SETUPMINUS_RULE("立减卷的规则"),
//多类别优惠卷定义
ALLMINUS_DISCOUNT_RULE("满减卷 + 折扣卷的计算规则");
/**
* 规则描述
*/
private String description;
}
public interface RuleExecutor {
/**
* 规则标记
*
* @return
*/
RuleFlag ruleFlag();
/**
* 修订结算信息
*
* @param settlementInfo
* @return SettlementInfo (经过修改过的结算信息)
*/
SettlementInfo settlementReturn(SettlementInfo settlementInfo);
}
这样已经明了 这个静态map 的键 就是 定义了 优惠券 核销的 计算规则 值呢 是 这个 带有计算规则 和计算方法 的接口 我们在初始化之前已经准备好了 所以在 Executormanager 中我们可以直接 使用它
开始
-
我们首先呢 先将远程调用传进来的 settlementInfo 结算信息 中的优惠券信息 大小做判断 (看看用户用了几张优惠券)
-
这样就有两种情况了 一种呢 是 用户只使用了一张优惠券
-
另一种 是用户使用了满减和折扣 卷 (规定只能是这两组合)
-
在使用了一张优惠券的情况下 我们先 将结算信息中的商品信息集合中的 优惠券分类 找到 通过优惠券种类进行 选择那种结算方式
-
以第一种结算方式为例子我们看 他调用了 之前 准备好的map 通过计算规则 获取到 对应 RuleExecutor 接口中的
SettlementInfo settlementReturn(SettlementInfo settlementInfo); 方法
-
这样我们只需要 将不同情况(这里有四种) 的类实现 RuleExecutor 这个接口 就可以完成单接口 多种类优惠券的核销
这就是 IOC 和AOP 的结合使用
接下来是 这四种情况的 实现 我们把 重复代码 写一个抽象类 让这四种情况 去继承
多种类计算方法的抽象类
public abstract class AbstractExecutor {
/**
* 商品类型和优惠券要求类型配对
* * ps:1.这里实现单品类优惠卷的效验,多品类优惠卷重载此方法
* * 2.商品只需要有一个优惠卷要求的商品类型去匹配就可以
* *
* * 获取商品类型和 优惠券的我商品类型 进行交集判断 (判断交集是否为空)
* * intersection ()
* *
*
* @param settlementInfo
* @return
*/
protected boolean goodsCouponMatching(SettlementInfo settlementInfo) {
//传进来的产品类型
List<Integer> seGoodsTypeList = settlementInfo.getGoodsInfos().stream().map(GoodsInfo::getType).collect(Collectors.toList());
//优惠券规则里的产品类型
List<Integer> couponGoodsTypeList = settlementInfo.getCouponAndTemplateInfos().stream().map(SettlementInfo.CouponAndTemplateInfo::getTemplateSdk).map(CouponTemplateSdk::getRule).map(TemplateRule::getGoodsType).collect(Collectors.toList());
//判断这两个list 是否有交集
boolean notEmpty = CollectionUtils.isNotEmpty(CollectionUtils.intersection(seGoodsTypeList, couponGoodsTypeList));
return notEmpty;
}
/**
* 商品类型和优惠券要求类型配对失败 返回商品总价
*
* @param settlementInfo
* @return
*/
protected SettlementInfo goodsCouponNotMatching(SettlementInfo settlementInfo) {
if (!goodsCouponMatching(settlementInfo)) {
Double goodsPrice = getGoodsPrice(settlementInfo.getGoodsInfos());
settlementInfo.setCost(goodsPrice);
settlementInfo.setCouponAndTemplateInfos(Collections.emptyList());
}
return settlementInfo;
}
/**
* 获取商品总价
*
* @return
*/
protected Double getGoodsPrice(List<GoodsInfo> goodsInfos) {
double allPrice = 0.0;
for (GoodsInfo goodsInfo : goodsInfos) {
Integer count = goodsInfo.getCount();
Double price = goodsInfo.getPrice();
double p = count * price;
allPrice += p;
}
return allPrice;
}
/**
* 保留小数
*
* @return
*/
protected Double retain2Decimals(Double value, Integer place) {
return new BigDecimal(value).setScale(place, BigDecimal.ROUND_HALF_UP).doubleValue();
}
/**
* 返回最小支付费用
*
* @return
*/
protected Double minCost() {
return 0.1;
}
}
1.满减计算
@Component
public class AllminusExecutor extends AbstractExecutor implements RuleExecutor {
@Override
public RuleFlag ruleFlag() {
return RuleFlag.ALLMINUS_RULE;
}
/**
* 满减
* * 先把总价用保留小数计算
* * 之后校验优惠卷是否匹配商品 不为null直接返回 结算信息
* * 通过结算信息获取sdk 去规则字段 discount 获取 基本金额 和 减去金额(折扣 满减)
* * 和总价对比如果不满足基本金额 直接返回总价 set后(返回结算信息)
* * 之后计算 使用优惠券后的金额 (看是不是减去后的金额大于最小金额 如果不是 返回最小金额 整体方法在保留小数的方法中使用)
*
* @param settlementInfo
* @return
*/
@Override
public SettlementInfo settlementReturn(SettlementInfo settlementInfo) {
//传入的商品总价格
Double inPrice = retain2Decimals(getGoodsPrice(settlementInfo.getGoodsInfos()), 2);
if (!goodsCouponMatching(settlementInfo)) {
return settlementInfo;
}
//获取sdk
CouponTemplateSdk templateSdk = settlementInfo.getCouponAndTemplateInfos().get(0).getTemplateSdk();
//基本金额
Integer base = templateSdk.getRule().getDiscount().getBase();
//减少金额
Integer quota = templateSdk.getRule().getDiscount().getQuota();
if (inPrice < base) {
settlementInfo.setCouponAndTemplateInfos(Collections.emptyList());
settlementInfo.setCost(inPrice);
return settlementInfo;
}
settlementInfo.setCost(retain2Decimals((inPrice - quota), 2) > minCost() ? retain2Decimals((inPrice - quota), 2) : minCost());
return settlementInfo;
}
}
2.折扣计算
@Component
public class DiscountExecutor extends AbstractExecutor implements RuleExecutor {
@Override
public RuleFlag ruleFlag() {
return RuleFlag.DISCOUNT_RULE;
}
/**
* 折扣
*
* @param settlementInfo
* @return
*/
@Override
public SettlementInfo settlementReturn(SettlementInfo settlementInfo) {
Double inPrice = getGoodsPrice(settlementInfo.getGoodsInfos());
if (!goodsCouponMatching(settlementInfo)) {
return settlementInfo;
}
CouponTemplateSdk templateSdk = settlementInfo.getCouponAndTemplateInfos().get(0).getTemplateSdk();
Integer base = templateSdk.getRule().getDiscount().getBase();
Integer quota = templateSdk.getRule().getDiscount().getQuota();
if (inPrice < base) {
settlementInfo.setCouponAndTemplateInfos(Collections.emptyList());
settlementInfo.setCost(inPrice);
return settlementInfo;
}
settlementInfo.setCost(retain2Decimals((inPrice * quota * 1.0 * 0.1), 2) > minCost() ?
retain2Decimals((inPrice * quota * 1.0 * 0.1), 2) : minCost());
return settlementInfo;
}
}
3.立减计算
public class SetupminusEexcutor extends AbstractExecutor implements RuleExecutor {
@Override
public RuleFlag ruleFlag() {
return RuleFlag.SETUPMINUS_RULE;
}
/**
* 立减
*
* @param settlementInfo
* @return
*/
@Override
public SettlementInfo settlementReturn(SettlementInfo settlementInfo) {
Double inPrice = getGoodsPrice(settlementInfo.getGoodsInfos());
if (goodsCouponMatching(settlementInfo)) {
return settlementInfo;
}
CouponTemplateSdk templateSdk = settlementInfo.getCouponAndTemplateInfos().get(0).getTemplateSdk();
Integer base = templateSdk.getRule().getDiscount().getBase();
Integer quota = templateSdk.getRule().getDiscount().getQuota();
if (inPrice < base) {
settlementInfo.setCouponAndTemplateInfos(Collections.emptyList());
settlementInfo.setCost(inPrice);
return settlementInfo;
}
settlementInfo.setCost(retain2Decimals(inPrice - quota, 2) > minCost() ? retain2Decimals(inPrice - quota, 2) : minCost());
return settlementInfo;
}
}
4满减和折扣的组合计算
@Component
public class AllminusAndDiscountExecutor extends AbstractExecutor implements RuleExecutor {
@Override
public RuleFlag ruleFlag() {
return RuleFlag.ALLMINUS_DISCOUNT_RULE;
}
/**
* * 重写该方法 实现多类别优惠卷
* *
* * 重载这个方法 拿到结算信息的所有的商品类型 集合
* * 之后拿到 sdk 中所有 商品集合 判断 两个集合的差集 是否为nul substact()
*
* @param settlementInfo
* @return
*/
protected boolean goodsCouponMatching(SettlementInfo settlementInfo) {
List<Integer> goodsType = settlementInfo.getGoodsInfos().stream().map(GoodsInfo::getType).collect(Collectors.toList());
List<String> goodsSdkType = settlementInfo.getCouponAndTemplateInfos().stream().map(SettlementInfo.CouponAndTemplateInfo::getTemplateSdk).map(CouponTemplateSdk::getCategory).collect(Collectors.toList());
return CollectionUtils.isNotEmpty(CollectionUtils.subtract(goodsType, goodsSdkType));
}
/**
* 满减 和折扣同时
* * 比之前的 多家了一个步骤遍历 模板信息看啊看 里面的sdk 属于 那种 (先给个 默认值)
* * 判断一下这两个 模板信息 是否是 空 使用方法 判断权重 如果 false 就返回 结算信息 (设置 一下)
*
* @param settlementInfo
* @return
*/
@Override
public SettlementInfo settlementReturn(SettlementInfo settlementInfo) {
Double goodsPrice = getGoodsPrice(settlementInfo.getGoodsInfos());
if (!goodsCouponMatching(settlementInfo)) {
return settlementInfo;
}
SettlementInfo.CouponAndTemplateInfo allminus = null;
SettlementInfo.CouponAndTemplateInfo discount = null;
for (SettlementInfo.CouponAndTemplateInfo couponAndTemplateInfo : settlementInfo.getCouponAndTemplateInfos()) {
if (CouponCategory.ALLMINUS.getCode().equals(couponAndTemplateInfo.getTemplateSdk().getCategory())) {
allminus = couponAndTemplateInfo;
} else {
discount = couponAndTemplateInfo;
}
}
if (ObjectUtils.isEmpty(allminus) || ObjectUtils.isEmpty(discount)) {
throw new BusinessException(BusinessCode.NOT_EXIST_COUPONINFO);
}
if (!judgmentWeight(allminus, discount)) {
settlementInfo.setCost(goodsPrice);
settlementInfo.setCouponAndTemplateInfos(Collections.emptyList());
return settlementInfo;
}
List<SettlementInfo.CouponAndTemplateInfo> couponAndTemplateInfoList = new ArrayList<>();
Double allminusBase = Double.valueOf(allminus.getTemplateSdk().getRule().getDiscount().getBase());
Double allminusQuota = Double.valueOf(allminus.getTemplateSdk().getRule().getDiscount().getQuota());
Double rePrice = goodsPrice;
if (rePrice >= allminusBase) {
rePrice -= allminusQuota;
couponAndTemplateInfoList.add(allminus);
}
Double quotaDiscout = Double.valueOf(discount.getTemplateSdk().getRule().getDiscount().getQuota());
rePrice *= (quotaDiscout * 1.0) / 10;
settlementInfo.setCouponAndTemplateInfos(couponAndTemplateInfoList);
settlementInfo.setCost(retain2Decimals(rePrice, 2) > minCost() ? retain2Decimals(rePrice, 2) : minCost());
return settlementInfo;
}
/**
* 判断权重(如果两张优惠券要叠加使用 那么第二张的权重就是第一张的 优惠模板key)
*
* @param allminus
* @param discount
* @return
*/
public boolean judgmentWeight(SettlementInfo.CouponAndTemplateInfo allminus, SettlementInfo.CouponAndTemplateInfo discount) {
String allminusKey = allminus.getTemplateSdk().getCouponKey();
String discountKey = discount.getTemplateSdk().getCouponKey();
List<String> allminusList = new ArrayList<>();
allminusList.add(allminusKey);
allminusList.add(discount.getTemplateSdk().getRule().getWeight());
List<String> discounList = new ArrayList<>();
discounList.add(discountKey);
discounList.add(allminus.getTemplateSdk().getRule().getWeight());
return CollectionUtils.isSubCollection(Arrays.asList(allminusKey, discountKey), allminusList) || CollectionUtils.isSubCollection(Arrays.asList(allminusKey, discountKey), discounList);
}
}
ponAndTemplateInfo allminus = null;
SettlementInfo.CouponAndTemplateInfo discount = null;
for (SettlementInfo.CouponAndTemplateInfo couponAndTemplateInfo : settlementInfo.getCouponAndTemplateInfos()) {
if (CouponCategory.ALLMINUS.getCode().equals(couponAndTemplateInfo.getTemplateSdk().getCategory())) {
allminus = couponAndTemplateInfo;
} else {
discount = couponAndTemplateInfo;
}
}
if (ObjectUtils.isEmpty(allminus) || ObjectUtils.isEmpty(discount)) {
throw new BusinessException(BusinessCode.NOT_EXIST_COUPONINFO);
}
if (!judgmentWeight(allminus, discount)) {
settlementInfo.setCost(goodsPrice);
settlementInfo.setCouponAndTemplateInfos(Collections.emptyList());
return settlementInfo;
}
List<SettlementInfo.CouponAndTemplateInfo> couponAndTemplateInfoList = new ArrayList<>();
Double allminusBase = Double.valueOf(allminus.getTemplateSdk().getRule().getDiscount().getBase());
Double allminusQuota = Double.valueOf(allminus.getTemplateSdk().getRule().getDiscount().getQuota());
Double rePrice = goodsPrice;
if (rePrice >= allminusBase) {
rePrice -= allminusQuota;
couponAndTemplateInfoList.add(allminus);
}
Double quotaDiscout = Double.valueOf(discount.getTemplateSdk().getRule().getDiscount().getQuota());
rePrice *= (quotaDiscout * 1.0) / 10;
settlementInfo.setCouponAndTemplateInfos(couponAndTemplateInfoList);
settlementInfo.setCost(retain2Decimals(rePrice, 2) > minCost() ? retain2Decimals(rePrice, 2) : minCost());
return settlementInfo;
}
/**
* 判断权重(如果两张优惠券要叠加使用 那么第二张的权重就是第一张的 优惠模板key)
*
* @param allminus
* @param discount
* @return
*/
public boolean judgmentWeight(SettlementInfo.CouponAndTemplateInfo allminus, SettlementInfo.CouponAndTemplateInfo discount) {
String allminusKey = allminus.getTemplateSdk().getCouponKey();
String discountKey = discount.getTemplateSdk().getCouponKey();
List<String> allminusList = new ArrayList<>();
allminusList.add(allminusKey);
allminusList.add(discount.getTemplateSdk().getRule().getWeight());
List<String> discounList = new ArrayList<>();
discounList.add(discountKey);
discounList.add(allminus.getTemplateSdk().getRule().getWeight());
return CollectionUtils.isSubCollection(Arrays.asList(allminusKey, discountKey), allminusList) || CollectionUtils.isSubCollection(Arrays.asList(allminusKey, discountKey), discounList);
}
}
**整体来说 这四种 就是简单的计算 就不加赘述了**