优惠券服务(手把手教你写优惠券)

优惠券服务 (优惠券系统)

简介

优惠券系统基于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);

}

}


**整体来说 这四种 就是简单的计算  就不加赘述了**  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值