优惠券系统简述

一.优惠券模板

模块优惠券模板是由运营人员根据一定的条件来设定的,优惠券必须有数量限制并且必须有优惠券码。

1.1模块功能

该功能就是创建优惠券模板并生成优惠券码,生成指定数量的优惠券码;放入到redis中,避免在用户获取优惠券的时候出现线程安全的问题。
图片.png

1.2优惠券模板ER图

图片.png

1.3优惠券模板数据库设计

图片.png

  1. DROP TABLE IF EXISTS `coupon_template`;
  2. CREATETABLE `coupon_template` (
  3. `id` int(11) NOTNULL AUTO_INCREMENT, `available` tinyint(1) UNSIGNED ZEROFILL NOTNULL COMMENT '是否可用', `expired` tinyint(1) NOTNULL COMMENT '是否过期', `name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOTNULL COMMENT '名字', `logo` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `intro` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '简介', `category` int(11) NOTNULL COMMENT '种类: 101-满减;102-折扣;103-立减', `scope` int(11) NOTNULL COMMENT '使用范围:1-单品;2-一类商品;3-全品', `product_line` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOTNULL COMMENT '使用的产品的id, 可以为数组', `coupon_count` int(11) NULL DEFAULT NULL COMMENT '优惠券发放数量', `create_time` datetime(0) NOTNULL COMMENT '创建时间', `user_id` int(11) NOTNULL COMMENT '创建![user_coupon_status](images/user_coupon_status.png)作用的人群:1-个人;2-全体;', `rule` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NOTNULL COMMENT '优惠券的规则', PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT= Dynamic;

二.优惠券分发模块

2.1模块功能

获取优惠券信息
图片.png

获取优惠券模板
图片.png
领取优惠券
图片.png
结算与核销
图片.png
用户获取可用优惠券
图片.png

用户领取优惠券
图片.png

系统派发优惠券
图片.png

2.2优惠券ER图

图片.png

2.3优惠券数据库设计

图片.png

DROP TABLE IF EXISTS coupon;CREATETABLE coupon  (  id int(11) NOTNULL AUTO_INCREMENT,  template_id int(11) NOTNULL COMMENT '优惠券模板ID',  user_id int(11) NOTNULL COMMENT '前端用户ID',  coupon_code varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOTNULL COMMENT '优惠券码',  assign_date datetime(0) NOTNULL COMMENT '优惠券分发时间',  status int(11) NOTNULL COMMENT '优惠券状态',  PRIMARY KEY (id) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT= Dynamic;

优惠券系统

一 . 优惠券模版模块

1.准备工作:首先打开数据库工具,导入\优惠券\sql目录下的文件,创建数据库

2.创建coupon父工程,并导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.qf</groupId>
    <artifactId>coupon</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>coupon-service</module>
        <module>coupon-gateway</module>
        <module>goods-service</module>
    </modules>
    <packaging>pom</packaging>

    <properties>
        <springcloud.alibaba.version>2.2.1.RELEASE</springcloud.alibaba.version>
        <springcloud.version>Hoxton.SR3</springcloud.version>
        <coupon>1.0-SNAPSHOT</coupon>
        <commons.pool2.vesion>2.7.0</commons.pool2.vesion>
        <HikariCP.version>2.4.13</HikariCP.version>
        <fastjson.version>1.2.4</fastjson.version>
        <mybatisplus.version>3.3.2</mybatisplus.version>
        <coupon.commons.version>1.0-SNAPSHOT</coupon.commons.version>
        <commons.text.version>1.8</commons.text.version>
        <goods.common.version>1.0-SNAPSHOT</goods.common.version>
        <forest.version>1.3.0</forest.version>
        <protostuff.version>1.7.2</protostuff.version>
        <hutools.version>5.3.10</hutools.version>
        <commons.collection4.version>4.4</commons.collection4.version>
    </properties>

    <repositories>
        <repository>
            <name>myRepoistory</name>
            <id>coupon-service</id>
            <url>http://maven.aliyun.com/nexus/content/groups/public</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutools.version}</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${springcloud.alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${springcloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

1.导入前端页面

图片.png

打开WebStorm,创建coupon目录,然后将\优惠券\前端页面\coupon目录下的资源拷贝过去即可

2.Goods-Service模块编写

打开优惠券创建页面,需要查询对应的某一类商品(对应数据库中的t_items表)

图片.png

2.1 创建goods-service工程以及goods-common,goods-info子工程

图片.png

2.2 编写goods-common工程

因为是公共模块,我们只需要编写一些公共使用的配置类即可

2.2.1 编写配置类

在goods-common工程中创建RedisPrefix类,用于设置items在redis中保存的key的名称

图片.png

代码如下:

package org.example.goods.constant;

public class RedisPrefix {

    public static class ItemKeyOrPrefix {
        public static final String ITEM_LIST = "items_list";
    }
}

2.3 编写goods-info工程

图片.png

2.3.1 准备工作

导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>goods-service</artifactId>
        <groupId>com.qf</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>goods-info</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.qf</groupId>
            <artifactId>goods-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>${commons.pool2.vesion}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP-java7</artifactId>
            <version>${HikariCP.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatisplus.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/io.protostuff/protostuff-core -->
    </dependencies>
</project>

创建application.yml

server:
  port: 8081

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  application:
    name: goods-info
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    url: jdbc:mysql://localhost:3306/coupon?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    hikari:
      connection-test-query: select 1
      minimum-idle: 5
      maximum-pool-size: 50
    password: root

mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: org/example/**/*.xml

创建启动类

package org.example.goods;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GoodsInfoApplication {
    public static void main(String[] args) {
        SpringApplication.run(GoodsInfoApplication.class, args);
    }
}

2.3.2 Pojo

编写Item实体类

package org.example.goods.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_items")
public class Item {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String name;
    private Integer no;
}

2.3.3 Mapper

编写ItemMapper

package org.example.goods.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.example.goods.entity.Item;

@Mapper
public interface ItemMapper extends BaseMapper<Item> {
}

2.3.4 Service

编写IItemService以及ItemServiceImpl

package org.example.goods.service;

import org.example.goods.entity.Item;
import java.util.List;

public interface IItemService {

    List<Item> getAll();
}
package org.example.goods.service.impl;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.example.goods.entity.Item;
import org.example.goods.mapper.ItemMapper;
import org.example.goods.service.IItemService;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class ItemServiceImpl implements IItemService {

    private ItemMapper itemMapper;

    public ItemServiceImpl(ItemMapper itemMapper) {
        this.itemMapper = itemMapper;
    }

    @Override
    public List<Item> getAll() {
        return itemMapper.selectList(Wrappers.emptyWrapper());
    }
}

2.3.5 Controller

编写ItemController

package org.example.goods.controller;

import com.alibaba.fastjson.JSONObject;
import org.example.goods.constant.RedisPrefix;
import org.example.goods.entity.Item;
import org.example.goods.service.IItemService;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/item")
@CrossOrigin("*")
public class ItemController {

    private IItemService itemService;
    private RedisTemplate redisTemplate;

    public ItemController(IItemService itemService, RedisTemplate redisTemplate) {
        this.itemService = itemService;
        this.redisTemplate = redisTemplate;
    }

    @GetMapping
    public List<Item> getAll() {
        Object items = redisTemplate.opsForValue()
                .get(RedisPrefix.ItemKeyOrPrefix.ITEM_LIST);//items_list
        if(null != items) {
            return JSONObject.parseArray(String.valueOf(items), Item.class);//类型转换
        }else {
            synchronized (RedisPrefix.ItemKeyOrPrefix.ITEM_LIST) {
                List<Item> itemList = itemService.getAll();
                // 比较少变化,故设置用不过期
                redisTemplate.opsForValue()
                        .set(RedisPrefix.ItemKeyOrPrefix.ITEM_LIST, JSONObject.toJSONString(itemList));

                return itemList;
            }
        }
    }
}

2.3.6 Config

设置redis序列化

package org.example.goods.config;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.nio.charset.Charset;

@Configuration
public class RedisConfig {

    //fastjson
    @Bean(name="redisTemplate")
    public RedisTemplate<String, Object> fastJsonRedisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        FastJson2JsonRedisSerializer fastJson2JsonRedisSerializer = new FastJson2JsonRedisSerializer(Object.class);
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(fastJson2JsonRedisSerializer);
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(fastJson2JsonRedisSerializer);
        template.setDefaultSerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }

    public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {

        public final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

        private Class<T> clazz;

        public FastJson2JsonRedisSerializer(Class<T> clazz) {
            super();
            this.clazz = clazz;
        }

        public byte[] serialize(T t) throws SerializationException {
            if (t == null) {
                return new byte[0];
            }
            return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
        }

        public T deserialize(byte[] bytes) throws SerializationException {
            if (bytes == null || bytes.length <= 0) {
                return null;
            }
            String str = new String(bytes, DEFAULT_CHARSET);

            return (T) JSON.parseObject(str, clazz);
        }

    }
}

2.3.6 Cache

编写ItemCache类设置缓存

package org.example.goods.cache;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.example.goods.constant.RedisPrefix;
import org.example.goods.entity.Item;
import org.example.goods.mapper.ItemMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.List;

@Component
public class ItemCache {

    private ItemMapper itemMapper;
    private RedisTemplate redisTemplate;

    public ItemCache(ItemMapper itemMapper, RedisTemplate redisTemplate) {
        this.itemMapper = itemMapper;
        this.redisTemplate = redisTemplate;
    }

    @PostConstruct
    public void initItem2Redis() {
        List<Item> itemList = itemMapper.selectList(Wrappers.emptyWrapper());
        // 比较少变化,故设置用不过期
        redisTemplate.opsForValue()
                .set(RedisPrefix.ItemKeyOrPrefix.ITEM_LIST, JSONObject.toJSONString(itemList));
    }
}

启动,访问 http://localhost:8081/item 测试即可!

3.Coupon-Service模块编写

3.1 编写coupon-common工程

因为是公共模块,我们只需要编写一些公共使用的配置类即可

图片.png

3.1.1代码编写

1.在pom.xml中导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>coupon-service</artifactId>
        <groupId>com.qf</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>coupon-common</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

</project>

2.RedisPrefix类的编写

package org.example.coupon.constant;

public class RedisPrefix {

    public static class CouponTemplatePrefix {
        public static final String COUPON_TEMPLATE_CODE_PREFIX = "coupon_template_code_";
    }
}

3.CouponCategory类的编写

package org.example.coupon.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.Objects;
import java.util.stream.Stream;

/**
 * 优惠券的种类
 */
@Getter
@AllArgsConstructor
public enum CouponCategory {

    MANJIAN("满减", 101), ZHEKOU("折扣", 102), LIJIAN("立减", 103);

    private String description;
    private Integer code;

    // 根据code返回数据
    public static CouponCategory of(Integer code) {
        Objects.requireNonNull(code); //判空

        // values() 获取所有的枚举对象
        return Stream.of(values())
                .filter(cc -> cc.code == code)
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException(code + " not exists"));
    }
}

4.CouponScope类的编写

package org.example.coupon.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.Objects;
import java.util.stream.Stream;

@Getter
@AllArgsConstructor
public enum CouponScope {

    SINGLE_GOODS("单品", 1),
    SERIAL_GOODS("一系列商品", 2),
    ALL_GOODS("全品类", 3);


    private String description;
    private Integer code;

    // 根据code返回数据
    public static CouponScope of(Integer code) {
        Objects.requireNonNull(code); //判空

        // values() 获取所有的枚举对象
        return Stream.of(values())
                .filter(cc -> cc.code == code)
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException(code + " not exists"));
    }
}

5.DistributeTarget类的编写

package org.example.coupon.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.Objects;
import java.util.stream.Stream;

/**
 * 优惠发放的人群
 */
@Getter
@AllArgsConstructor
public enum DistributeTarget {

    SINGLE("个人", 1),
    MULTI("全部", 2);

    private String description;
    private Integer code;

    // 根据code返回数据
    public static DistributeTarget of(Integer code) {
        Objects.requireNonNull(code); //判空

        // values() 获取所有的枚举对象
        return Stream.of(values())
                .filter(cc -> cc.code == code)
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException(code + " not exists"));
    }
}

6.PeriodType类的编写

package org.example.coupon.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.Objects;
import java.util.stream.Stream;

@Getter
@AllArgsConstructor
public enum PeriodType {

    REGULAR("固定时间(指定时间内有效)", 1),
    SHIFT("变动时间(指的是从领取之日开始计算)", 2);

    private String description;
    private Integer code;

    // 根据code返回数据
    public static PeriodType of(Integer code) {
        Objects.requireNonNull(code); //判空

        // values() 获取所有的枚举对象
        return Stream.of(values())
                .filter(cc -> cc.code == code)
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException(code + " not exists"));
    }
}

7.TemplateRequest类的编写

package org.example.coupon.vo;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class TemplateRequest {
    private String name;
    private String logo;
    private Integer category;   //优惠券的类型
    private BigDecimal base;   // 满减、折扣、立减需要达到的一定金额
    private BigDecimal favourable; // 减多少、折扣多少
    private Integer count;   //优惠券发放的数量
    private Integer target; // 优惠券发放的群体
    private Integer periodType; //失效的类型

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime begin;  // 如果优惠券失效的类型为 REGULAR, begin开始日期

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime end;  // 如果优惠券失效的类型为 REGULAR, end为结束日期

    private Integer gap; // 如果优惠券失效的类型为 SHIFT, gap指的是多长时间内有效

    private Integer scope; //优惠券作用的产品、产品线、全类目产品
    private String ids; //作用的产品或者产品线的对应的id.  10001,10003,
    private String intro;
    private Integer limitation; //每个用户限制领取的优惠券的数量

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime expireTime;  //获取截止日期(优惠券发放结束日期)
}

8.TemplateRule类的编写

package org.example.coupon.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.example.coupon.enums.CouponCategory;
import org.example.coupon.enums.PeriodType;

import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 优惠券的规则
 * http://www.woshipm.com/pd/1624774.html
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TemplateRule {

    private Discount discount;

    private Expiration expiration;

    private Usage usage;

    //每张优惠券限制领取的张数
    private Integer limitation;

    // 折扣规则
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Discount {
        // 优惠券的种类,对应着 CouponCategory中的code值
        private Integer category;

        // 满减、折扣、立减的金额限制,达到指定的金额才能使用
        private BigDecimal base;

        /**
         * 满减的时候,减去多少。
         * 折扣券的时候,折扣多少。
         */
        private BigDecimal favourable;

        // 自验证
        public boolean validate() {
            return null != CouponCategory.of(category)
                    && base.compareTo(BigDecimal.valueOf(0)) > 0
                    && favourable.compareTo(BigDecimal.valueOf(0)) > 0;
        }
    }

    // 时间限制规则
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Expiration {
        // 优惠券的有效类型,与 PeriodType 的code值对应
        private Integer period;

        // 如果优惠券的有效期方式为 “固定时间(指定时间内有效)”, begin与end才是有效的。
        private LocalDateTime begin;

        private LocalDateTime end;

        // 针对当 PeriodType.SHIFT 的时候是有效的。
        private Integer gap;

        public boolean validate() {
            return null != PeriodType.of(period)
                    && end.isAfter(begin)
                    && end.isAfter(LocalDateTime.now());
        }
    }

    // 使用规则
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Usage {
//        private String province;   //省份
//        private String city;  // 城市
//        private String level; //会员的级别

        // 使用的范围, 与 CouponScope的code值是对应
        private Integer scope;

        // 作用的产品或者产品线
        private String productLine;
    }
}

3.2 coupon-template工程

图片.png

3.2.1代码编写

1.创建启动类,编写application.yml配置文件,并导入依赖

package org.example.coupon;

import com.dtflys.forest.annotation.ForestScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@ForestScan("org.example.coupon.remoteservice")
public class CouponTemplateApplication {
    public static void main(String[] args) {
        SpringApplication.run(CouponTemplateApplication.class, args);
    }
}
server:
  port: 8082

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  application:
    name: coupon-template
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    url: jdbc:mysql://localhost:3306/coupon?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    hikari:
      connection-test-query: select 1
      minimum-idle: 5
      maximum-pool-size: 50
    password: root

pagehelper:
  reasonable: true

# 在调用远程的服务的时候使用
forest:
  # springboot中默认的使用http请求的组件是 apache httpComponent
  backend: okhttp3
  max-connections: 10

mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: org/example/**/*.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
    <artifactId>coupon-service</artifactId>
    <groupId>com.qf</groupId>
    <version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>coupon-template</artifactId>

    <dependencies>
        <!-- 基于Java语言的序列化库 -->
        <dependency>
            <groupId>io.protostuff</groupId>
            <artifactId>protostuff-core</artifactId>
            <version>${protostuff.version}</version>
        </dependency>
        <dependency>
            <groupId>io.protostuff</groupId>
            <artifactId>protostuff-runtime</artifactId>
            <version>${protostuff.version}</version>
        </dependency>
        <!-- HTTP客户端访问框架 -->
        <dependency>
            <groupId>com.dtflys.forest</groupId>
            <artifactId>spring-boot-starter-forest</artifactId>
            <version>${forest.version}</version>
            <exclusions>
                <exclusion>
                    <artifactId>commons-logging</artifactId>
                    <groupId>commons-logging</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>commons-io</artifactId>
                    <groupId>commons-io</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.qf</groupId>
            <artifactId>coupon-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>fastjson</artifactId>
                    <groupId>com.alibaba</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>jsr305</artifactId>
                    <groupId>com.google.code.findbugs</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>commons-io</artifactId>
                    <groupId>commons-io</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- 对象连接池管理类 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>${commons.pool2.vesion}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- 高性能的 JDBC 连接池组件 -->
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP-java7</artifactId>
            <version>${HikariCP.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatisplus.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
        <!-- 处理字符串的算法库 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-text</artifactId>
            <version>${commons.text.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>

</project>

3.2.2 Entity

CouponTemplate类的编写

package org.example.coupon.entity;

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.example.coupon.enums.CouponScope;
import org.example.coupon.enums.DistributeTarget;
import org.example.coupon.vo.TemplateRule;
import org.example.coupon.enums.CouponCategory;

import java.time.LocalDateTime;

@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonSerialize(using = org.example.coupon.serializer.CouponTemplateSerializer.class)
public class CouponTemplate {

    private Integer id;

    private Boolean available;
    private Boolean expired;

    private String name;
    private String logo;
    private String intro;

    private CouponCategory category;

    private CouponScope scope;
    private Integer count;
    private LocalDateTime createTime;
    private Integer userId;
    private String key;
    private DistributeTarget target;
    private TemplateRule rule;
    private LocalDateTime expireTime;
}

3.2.3 Mapper

CouponTemplateMapper类的编写

package org.example.coupon.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.example.coupon.entity.CouponTemplate;
import org.springframework.stereotype.Repository;

@Mapper
@Repository
public interface CouponTemplateMapper extends BaseMapper<CouponTemplate> {

    // 保存CouponTemplate
    void saveCouponTemplate(CouponTemplate couponTemplate);
}

3.2.4 Mapper.xml

在resources目录下创建文件夹:org/example/coupon/mapper,再创建CouponTemplateMapper.xml文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="org.example.coupon.mapper.CouponTemplateMapper">

    <resultMap id="couponTemplateResultMap" type="org.example.coupon.entity.CouponTemplate">
        <id column="id" property="id"></id>
        <result column="name" property="name"></result>
        <result column="available" property="available"></result>
        <result column="expired" property="expired"></result>
        <result column="logo" property="logo"></result>
        <result column="coupon_count" property="count"></result>
        <result column="create_time" property="createTime"></result>
        <result column="expire_time" property="expireTime"></result>
        <result column="template_key" property="key"></result>
        <result column="category" property="category" typeHandler="org.example.coupon.columnhandler.CategoryHandler"></result>
        <result column="scope" property="scope" typeHandler="org.example.coupon.columnhandler.ScopeHandler"></result>
        <result column="target" property="target" typeHandler="org.example.coupon.columnhandler.TargetHandler"></result>
        <result column="rule" property="rule" typeHandler="org.example.coupon.columnhandler.TemplateRuleHandler"></result>
    </resultMap>

    <sql id="coupontTemplateCommonSql">
        select id, name, available, expired, logo, intro, category, scope, coupon_count, create_time, expire_time, user_id, template_key, target, rule
            from coupon_template
    </sql>

    <select id="selectAvailableCouponTemplate" resultMap="couponTemplateResultMap">
        <include refid="coupontTemplateCommonSql"></include> where available = #{available}
    </select>

    <insert id="saveCouponTemplate" parameterType="org.example.coupon.entity.CouponTemplate">
        insert into coupon_template (id, name, available, expired, logo, intro, category, scope, coupon_count,
            create_time, expire_time, user_id, template_key, target, rule) values (
                #{id},
                #{name},
                #{available},
                #{expired},
                #{logo},
                #{intro},
                #{category, typeHandler=org.example.coupon.columnhandler.CategoryHandler},
                #{scope, typeHandler=org.example.coupon.columnhandler.ScopeHandler},
                #{count},
                #{createTime},
                #{expireTime},
                #{userId},
                #{key},
                #{target, typeHandler=org.example.coupon.columnhandler.TargetHandler},
                #{rule, typeHandler=org.example.coupon.columnhandler.TemplateRuleHandler}
            )

    </insert>
</mapper>

3.2.5 Service

ICouponTemplateService

package org.example.coupon.service;

import org.example.coupon.entity.CouponTemplate;
import org.example.coupon.vo.TemplateRequest;

public interface ICouponTemplateService {

    /**
     * 根据运营人员在前端页面输入的优惠券的信息,去构建 CouponTemplate
     * @param request
     * @return
     */
    CouponTemplate buildCouponTemplate(TemplateRequest request);
}

IAsyncCouponCode

package org.example.coupon.service;

import org.example.coupon.entity.CouponTemplate;

// 异步生成优惠券码
public interface IAsyncCouponCode {

    /**
     * 1.根据运营人员填写的优惠券数量异步生成对应数量的优惠券码
     * 2.将优惠券模板插入到数据库中。
     */
    void generateCouponCodes (CouponTemplate couponTemplate);
}

3.2.6 ServiceImpl

CouponTemplateServiceImpl

package org.example.coupon.service.impl;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.RandomUtil;
import lombok.extern.slf4j.Slf4j;
import org.example.coupon.entity.CouponTemplate;
import org.example.coupon.enums.CouponScope;
import org.example.coupon.enums.DistributeTarget;
import org.example.coupon.mapper.CouponTemplateMapper;
import org.example.coupon.remoteservice.LeafService;
import org.example.coupon.service.IAsyncCouponCode;
import org.example.coupon.service.ICouponTemplateService;
import org.example.coupon.vo.TemplateRequest;
import org.example.coupon.vo.TemplateRule;
import org.example.coupon.enums.CouponCategory;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;

@Slf4j
@Service
public class CouponTemplateServiceImpl implements ICouponTemplateService {

    private CouponTemplateMapper templateMapper;
    private IAsyncCouponCode asyncCouponCode;
    private LeafService leafService;

    private CouponTemplateServiceImpl(CouponTemplateMapper templateMapper,
                                      IAsyncCouponCode asyncCouponCode,
                                      LeafService leafService) {
        this.templateMapper = templateMapper;
        this.asyncCouponCode = asyncCouponCode;
        this.leafService = leafService;
    }


    @Override
    public CouponTemplate buildCouponTemplate(TemplateRequest request) {
        log.info("Current Thread: {}", Thread.currentThread().getName());
        CouponTemplate template = new CouponTemplate();

        LocalDateTime time = LocalDateTime.now();

        String leafTemplateId = leafService.getCouponTemplateId(); //获取leaf中 CouponTemplate 的主键
        log.info("leafTemplateId is {}", leafTemplateId);
        template.setId(Integer.parseInt(leafTemplateId));
        // 在实际工作中,刚生成的优惠券模板,肯定是不可用的状态,因为需要财务或其他相关部门审核
        template.setAvailable(true);

        template.setCategory(CouponCategory.of(request.getCategory())); //设置优惠券的种类
        template.setCount(request.getCount());
        template.setCreateTime(time);
        template.setExpired(false);
        template.setIntro(request.getIntro());
        template.setLogo(request.getLogo());
        template.setUserId(10000);
        template.setScope(CouponScope.of(request.getScope()));
        template.setName(request.getName());
        template.setTarget(DistributeTarget.of(request.getTarget()));
        template.setExpireTime(request.getExpireTime());

        /**
         *  3位种类 + 1位的作用范围 + 1位的作用目标 + YYYYMMDD + 7随机数
         */
        template.setKey("" + request.getCategory() + request.getScope()
                + request.getTarget() + DateUtil.format(time, "yyyyMMdd") + RandomUtil.randomString(7));

        TemplateRule rule = new TemplateRule();
        rule.setDiscount(new TemplateRule.Discount(request.getCategory(), request.getBase(), request.getFavourable()));
        rule.setLimitation(request.getLimitation());
        rule.setUsage(new TemplateRule.Usage(request.getScope(), request.getIds()));
        rule.setExpiration(new TemplateRule.Expiration(request.getPeriodType(), request.getBegin(), request.getEnd(), request.getGap()));

        template.setRule(rule);

        templateMapper.saveCouponTemplate(template);  //优惠券模板入库

        asyncCouponCode.generateCouponCodes(template);

        return template;
    }
}

AsyncCouponCodeImpl

package org.example.coupon.service.impl;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.RandomUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.StopWatch;
import org.apache.commons.text.RandomStringGenerator;
import org.example.coupon.constant.RedisPrefix;
import org.example.coupon.entity.CouponTemplate;
import org.example.coupon.service.IAsyncCouponCode;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import java.text.SimpleDateFormat;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class AsyncCouponCodeImpl implements IAsyncCouponCode {

    private RedisTemplate redisTemplate;

    public AsyncCouponCodeImpl(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

//    private RandomStringGenerator generator = new RandomStringGenerator.Builder()
//            .withinRange(new char[]{'a', 'z'}, new char[]{'A', 'Z'}, new char[]{'0','9'})
//            .build();

    @Async("getAsyncExecutor")
    @Override
    public void generateCouponCodes(CouponTemplate couponTemplate) {
        log.info("Current Thread: {}", Thread.currentThread().getName());

        //获取生成优惠券码的消耗时间
        StopWatch stopWatch = StopWatch.createStarted();//获取开始时间

        // 获取要生成的优惠券码的数量
        Integer count = couponTemplate.getCount();

        Integer couponTemplateId = couponTemplate.getId();  //优惠券主键

        Set<String> couponCodeSet = new HashSet<>();

        String couponCodePrefix = "" + couponTemplateId + couponTemplate.getCategory().getCode()
                + couponTemplate.getScope().getCode() + couponTemplate.getTarget().getCode()
                + DateUtil.format(new Date(), "yyyyMMdd");

        // 循环生成优惠券码, 有可能不够,因为 HashSet中的值不能重复
        for(int i = 0; i < count; i++) {
            String couponCode = couponCodePrefix + RandomUtil.randomString(10);
            couponCodeSet.add(couponCode);
        }

        while(couponCodeSet.size() < count) {
            String couponCode = couponCodePrefix + RandomUtil.randomString(10);
            couponCodeSet.add(couponCode);
        }

        //Assert断言
        Assert.isTrue(couponCodeSet.size() == count, "coupon code's number is invalid");

        // 将所有的优惠券码设置Redis中
        redisTemplate.opsForList().leftPushAll(RedisPrefix.CouponTemplatePrefix.COUPON_TEMPLATE_CODE_PREFIX + couponTemplateId, couponCodeSet);

        stopWatch.stop();//获取结束时间

        long time = stopWatch.getTime(TimeUnit.MILLISECONDS);

        log.info("coupon code generate finish. Cost time {}", time);//一共消耗了多长时间

        // 给运营人员发送短信或者邮件
    }
}

3.2.7 Controller

CouponTemplateController

package org.example.coupon.controller;

import lombok.extern.slf4j.Slf4j;
import org.example.coupon.entity.CouponTemplate;
import org.example.coupon.service.ICouponTemplateService;
import org.example.coupon.vo.TemplateRequest;
import org.springframework.web.bind.annotation.*;


@Slf4j
@RestController
@RequestMapping("/template")
@CrossOrigin("*")
public class CouponTemplateController {

    private ICouponTemplateService templateService;

    public CouponTemplateController(ICouponTemplateService templateService) {
        this.templateService = templateService;
    }

    /**
     * @param request
     * @return
     */
    @PostMapping
    public CouponTemplate buildCouponTemplate(@RequestBody TemplateRequest request) {
        log.info("Current Thread: {}", Thread.currentThread().getName());
        return templateService.buildCouponTemplate(request);
    }
}

3.2.8 Columnhandler

CategoryHandler

package org.example.coupon.columnhandler;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.example.coupon.enums.CouponCategory;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class CategoryHandler extends BaseTypeHandler {

    /**
     * java -> 数据库
     * i : 是该列对应的索引的位置
     * parameter: 是实体对象中的具体的属性,映射到CouponTemplate中指的是:CouponCategory
     */
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
        ps.setObject(i, ((CouponCategory)parameter).getCode());
    }

    /**
     * 将数据库的值 转换为 Java对应的对象:101(102, 103) -> CouponCategory
     * @param rs
     * @param columnName
     * @return
     * @throws SQLException
     */
    @Override
    public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return CouponCategory.of(rs.getInt(columnName));
    }

    @Override
    public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return CouponCategory.of(rs.getInt(columnIndex));
    }

    @Override
    public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return CouponCategory.of(cs.getInt(columnIndex));
    }
}

ScopeHandler

package org.example.coupon.columnhandler;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.example.coupon.enums.CouponScope;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class ScopeHandler extends BaseTypeHandler {

    /**
     * java -> 数据库
     * i : 是该列对应的索引的位置
     * parameter: 是实体对象中的具体的属性,映射到CouponTemplate中指的是: CouponScope
     */
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
        ps.setObject(i, ((CouponScope)parameter).getCode());
    }

    /**
     * 将数据库的值 转换为 Java对应的对象:101(102, 103) -> CouponCategory
     * @param rs
     * @param columnName
     * @return
     * @throws SQLException
     */
    @Override
    public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return CouponScope.of(rs.getInt(columnName));
    }

    @Override
    public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return CouponScope.of(rs.getInt(columnIndex));
    }

    @Override
    public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return CouponScope.of(cs.getInt(columnIndex));
    }
}

TargetHandler

package org.example.coupon.columnhandler;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.example.coupon.enums.DistributeTarget;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class TargetHandler extends BaseTypeHandler {

    /**
     * java -> 数据库
     * i : 是该列对应的索引的位置
     * parameter: 是实体对象中的具体的属性,映射到CouponTemplate中指的是: DistributeTarget
     */
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
        ps.setObject(i, ((DistributeTarget)parameter).getCode());
    }

    /**
     * 将数据库的值 转换为 Java对应的对象:101(102, 103) -> CouponCategory
     * @param rs
     * @param columnName
     * @return
     * @throws SQLException
     */
    @Override
    public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return DistributeTarget.of(rs.getInt(columnName));
    }

    @Override
    public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return DistributeTarget.of(rs.getInt(columnIndex));
    }

    @Override
    public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return DistributeTarget.of(cs.getInt(columnIndex));
    }
}

TemplateRuleHandler

package org.example.coupon.columnhandler;

import com.alibaba.fastjson.JSONObject;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.example.coupon.vo.TemplateRule;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class TemplateRuleHandler extends BaseTypeHandler {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, JSONObject.toJSONString(parameter));
    }

    @Override
    public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return JSONObject.parseObject(rs.getString(columnName), TemplateRule.class);
    }

    @Override
    public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return JSONObject.parseObject(rs.getString(columnIndex), TemplateRule.class);
    }

    @Override
    public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return JSONObject.parseObject(cs.getString(columnIndex), TemplateRule.class);
    }
}

3.2.9 Config

AyncExcutorPoolConfig

package org.example.coupon.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@EnableAsync  //开启异步执行
@Configuration
public class AyncExcutorPoolConfig implements AsyncConfigurer {

    @Bean
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        /**
         * IO密集型应用;
         * CPU密集型应用;
         */
        executor.setCorePoolSize(8);
        executor.setMaxPoolSize(16);
        executor.setWaitForTasksToCompleteOnShutdown(true);

        return executor;
    }
}

RedisConfig

package org.example.coupon.config;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.nio.charset.Charset;

@Configuration
public class RedisConfig {

    //fastjson
    @Bean(name="redisTemplate")
    public RedisTemplate<String, Object> fastJsonRedisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        FastJson2JsonRedisSerializer fastJson2JsonRedisSerializer = new FastJson2JsonRedisSerializer(Object.class);
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(fastJson2JsonRedisSerializer);
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(fastJson2JsonRedisSerializer);
        template.setDefaultSerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }

    public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {

        public final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

        private Class<T> clazz;

        public FastJson2JsonRedisSerializer(Class<T> clazz) {
            super();
            this.clazz = clazz;
        }

        public byte[] serialize(T t) throws SerializationException {
            if (t == null) {
                return new byte[0];
            }
            return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
        }

        public T deserialize(byte[] bytes) throws SerializationException {
            if (bytes == null || bytes.length <= 0) {
                return null;
            }
            String str = new String(bytes, DEFAULT_CHARSET);

            return (T) JSON.parseObject(str, clazz);
        }

    }
}

WebConfig

package org.example.coupon.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

//@Configuration
public class WebConfig {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

3.2.10 Leaf

Leaf:美团分布式ID生成服务开源

网址:https://tech.meituan.com/2019/03/07/open-source-project-leaf.html

Leaf项目已经在Github上开源:https://github.com/Meituan-Dianping/Leaf

package org.example.coupon.remoteservice;

import com.dtflys.forest.annotation.Request;
import org.springframework.stereotype.Service;

@Service
public interface LeafService {

    @Request(url = "http://localhost:8080/api/segment/get/coupontemplate")
    String getCouponTemplateId();
}

3.2.11 Serializer

CouponTemplateSerializer

package org.example.coupon.serializer;

import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.example.coupon.entity.CouponTemplate;
import org.example.coupon.enums.CouponCategory;

import java.io.IOException;

public class CouponTemplateSerializer extends JsonSerializer<CouponTemplate> {
    @Override
    public void serialize(CouponTemplate value, JsonGenerator generator, SerializerProvider serializers) throws IOException {
        /** 写一个对象的json数据,需要先 start, 然后再end
        generator.writeStartObject();
        generator.writeEndObject();
         */
        /**
         generator.writeStartArray();
         generator.writeEndArray();
         */
        generator.writeStartObject();

        generator.writeNumberField("id", value.getId());
        generator.writeBooleanField("available", value.getAvailable());
        generator.writeBooleanField("expired", value.getExpired());
        generator.writeStringField("name", value.getName());
        generator.writeStringField("logo", value.getLogo());
        generator.writeStringField("intro", value.getIntro());
        generator.writeStringField("scope", value.getScope().getDescription());
        generator.writeStringField("category", value.getCategory().getDescription());
        generator.writeNumberField("count", value.getCount());
        generator.writeStringField("createTime", DateUtil.format(value.getCreateTime(), "yyyy-MM-dd HH:mm:ss"));
        generator.writeNumberField("userId", value.getUserId());
        generator.writeStringField("key", value.getKey());
        generator.writeStringField("target", value.getTarget().getDescription());
        generator.writeStringField("rule", JSONObject.toJSONString(value.getRule()));
        generator.writeStringField("expireTime", DateUtil.format(value.getExpireTime(), "yyyy-MM-dd HH:mm:ss"));

        generator.writeEndObject();
    }
}

测试:启动goods-info,Leaf,coupon-template工程以及nacos,redis,提交前端 优惠券创建.html 页面即可

至此,优惠券模板功能实现完毕!

二 . 优惠券分发模块

1.获取可用优惠券功能实现

获取优惠券信息,可用,已用,过期三种优惠券的获取功能,涉及到缓存中的存储以及判断

1.1 在coupon-template工程中添加根据ids查询优惠券模版功能

1.1.1 coupon-common工程相关操作

修改RedisPrefix类

package org.example.coupon.constant;

public class RedisPrefix {

    public static class CouponTemplatePrefix {
        public static final String COUPON_TEMPLATE_CODE_PREFIX = "coupon_template_code_";
    }

    public static class CouponDistributionPrefix {
        // 用户可用优惠券的前缀
        public static final String USER_COUPON_AVAILABLE_PREFIX = "user_coupon_available_";

        // 用户的已经使用的优惠券前缀
        public static final String USER_COUPON_USED_PREFIX = "user_coupon_used_";

        // 用户的已经过期优惠券前缀
        public static final String USER_COUPON_EXPIRED_PREFIX = "user_coupon_expired_";
    }
}

创建CouponStatus枚举类

package org.example.coupon.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.Objects;
import java.util.stream.Stream;

@Getter
@AllArgsConstructor
@NoArgsConstructor
public enum CouponStatus {
    AVAILABLE("可用的优惠券", 1),
    USED("已经使用的优惠券", 2),
    EXPIRED("已过期的优惠券", 3);

    private String description;
    private Integer code;

    // 根据code返回数据
    public static CouponStatus of(Integer code) {
        Objects.requireNonNull(code); //判空

        // values() 获取所有的枚举对象
        return Stream.of(values())
                .filter(cc -> cc.code.equals(code))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException(code + " not exists"));
    }
}

创建CouponException类

package org.example.coupon.exception;

public class CouponException extends RuntimeException{

    public CouponException(String msg) {
        super(msg);
    }
}

创建CouponTemplateSDK类

package org.example.coupon.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

/**
 * 在优惠券模板模块和优惠券分发模块在数据的传递的时候使用的对象.
 * 在实际的开发过程中,模块与模块之间数据的传递不能直接传递 Entity 对象
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CouponTemplateSDK {
    private Integer id;
    private String name;
    private String logo;
    private String intro;
    private Integer category;
    private Integer scope;
    private LocalDateTime expiredTime;
    private String key;
    private Integer target;

    private TemplateRule rule;
}

1.1.2 coupon-template工程相关操作

Controller

package org.example.coupon.controller;

import lombok.extern.slf4j.Slf4j;
import org.example.coupon.entity.CouponTemplate;
import org.example.coupon.service.ICouponTemplateService;
import org.example.coupon.vo.CouponTemplateSDK;
import org.example.coupon.vo.TemplateRequest;
import org.springframework.web.bind.annotation.*;

import java.util.Map;


@Slf4j
@RestController
@RequestMapping("/template")
@CrossOrigin("*")
public class CouponTemplateController {

    private ICouponTemplateService templateService;

    public CouponTemplateController(ICouponTemplateService templateService) {
        this.templateService = templateService;
    }

    /**
     * @param request
     * @return
     */
    @PostMapping
    public CouponTemplate buildCouponTemplate(@RequestBody TemplateRequest request) {
        log.info("Current Thread: {}", Thread.currentThread().getName());
        return templateService.buildCouponTemplate(request);
    }


    /**
     * 根据id获取对应的优惠券模板
     * http://localhost:8082/template/ids?ids=3&ids=4&ids=67
     * @param ids
     * @return  key是优惠券模板的id, value是优惠券模板
     */
    @GetMapping("/ids")
    public Map<Integer, CouponTemplateSDK> findCouponTemplateSDK2Ids(Integer[] ids) {
        return templateService.findCouponTemplateSDK2Ids(ids);
    }
}

Service

package org.example.coupon.service;

import org.example.coupon.entity.CouponTemplate;
import org.example.coupon.vo.CouponTemplateSDK;
import org.example.coupon.vo.TemplateRequest;

import java.util.Map;

public interface ICouponTemplateService {

    /**
     * 根据运营人员在前端页面输入的优惠券的信息,去构建 CouponTemplate
     * @param request
     * @return
     */
    CouponTemplate buildCouponTemplate(TemplateRequest request);

    /**
     * 根据id获取对应的优惠券模板
     * @param ids
     * @return
     */
    Map<Integer, CouponTemplateSDK> findCouponTemplateSDK2Ids(Integer[] ids);
}

ServiceImpl

package org.example.coupon.service.impl;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.RandomUtil;
import lombok.extern.slf4j.Slf4j;
import org.example.coupon.entity.CouponTemplate;
import org.example.coupon.enums.CouponScope;
import org.example.coupon.enums.DistributeTarget;
import org.example.coupon.mapper.CouponTemplateMapper;
import org.example.coupon.remoteservice.LeafService;
import org.example.coupon.service.IAsyncCouponCode;
import org.example.coupon.service.ICouponTemplateService;
import org.example.coupon.vo.CouponTemplateSDK;
import org.example.coupon.vo.TemplateRequest;
import org.example.coupon.vo.TemplateRule;
import org.example.coupon.enums.CouponCategory;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Service
public class CouponTemplateServiceImpl implements ICouponTemplateService {

    private CouponTemplateMapper templateMapper;
    private IAsyncCouponCode asyncCouponCode;
    private LeafService leafService;

    private CouponTemplateServiceImpl(CouponTemplateMapper templateMapper,
                                      IAsyncCouponCode asyncCouponCode,
                                      LeafService leafService) {
        this.templateMapper = templateMapper;
        this.asyncCouponCode = asyncCouponCode;
        this.leafService = leafService;
    }


    @Override
    public CouponTemplate buildCouponTemplate(TemplateRequest request) {
        log.info("Current Thread: {}", Thread.currentThread().getName());
        CouponTemplate template = new CouponTemplate();

        LocalDateTime time = LocalDateTime.now();

        String leafTemplateId = leafService.getCouponTemplateId(); //获取leaf中 CouponTemplate 的主键
        log.info("leafTemplateId is {}", leafTemplateId);
        template.setId(Integer.parseInt(leafTemplateId));
        // 在实际工作中,刚生成的优惠券模板,肯定是不可用的状态,因为需要财务或其他相关部门审核
        template.setAvailable(true);

        template.setCategory(CouponCategory.of(request.getCategory())); //设置优惠券的种类
        template.setCount(request.getCount());
        template.setCreateTime(time);
        template.setExpired(false);
        template.setIntro(request.getIntro());
        template.setLogo(request.getLogo());
        template.setUserId(10000);
        template.setScope(CouponScope.of(request.getScope()));
        template.setName(request.getName());
        template.setTarget(DistributeTarget.of(request.getTarget()));
        template.setExpireTime(request.getExpireTime());

        /**
         *  3位种类 + 1位的作用范围 + 1位的作用目标 + YYYYMMDD + 7随机数
         */
        template.setKey("" + request.getCategory() + request.getScope()
                + request.getTarget() + DateUtil.format(time, "yyyyMMdd") + RandomUtil.randomString(7));

        TemplateRule rule = new TemplateRule();
        rule.setDiscount(new TemplateRule.Discount(request.getCategory(), request.getBase(), request.getFavourable()));
        rule.setLimitation(request.getLimitation());
        rule.setUsage(new TemplateRule.Usage(request.getScope(), request.getIds()));
        rule.setExpiration(new TemplateRule.Expiration(request.getPeriodType(), request.getBegin(), request.getEnd(), request.getGap()));

        template.setRule(rule);

        templateMapper.saveCouponTemplate(template);  //优惠券模板入库

        asyncCouponCode.generateCouponCodes(template);

        return template;
    }

    @Override
    public Map<Integer, CouponTemplateSDK> findCouponTemplateSDK2Ids(Integer[] ids) {
        if(log.isDebugEnabled()) {
            log.debug("Get coupon template by ids: {}", Arrays.asList(ids));  // 如果是数组:[@a76f xxxx
        }

        List<CouponTemplate> couponTemplateList = templateMapper.findCouponTemplatesByIds(ids);
        Map<Integer, CouponTemplateSDK> result = new HashMap<>();

        if(null != couponTemplateList && couponTemplateList.size() > 0) {
            couponTemplateList.forEach(ct -> {
                Integer templateId = ct.getId(); //获取优惠券模板的id

                /**
                 *     private Integer id;
                 *     private String name;
                 *     private String logo;
                 *     private String intro;
                 *     private Integer category;
                 *     private Integer scope;
                 *     private LocalDateTime expiredTime;
                 *     private String key;
                 *     private Integer target;
                 *
                 *     private TemplateRule rule;
                 */
                result.put(templateId, new CouponTemplateSDK(templateId, ct.getName(), ct.getLogo(), ct.getIntro(), ct.getCategory().getCode(),
                        ct.getScope().getCode(), ct.getExpireTime(), ct.getKey(), ct.getTarget().getCode(), ct.getRule()));
            });
        }

        return result;
    }
}

Mapper

package org.example.coupon.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.example.coupon.entity.CouponTemplate;
import org.springframework.stereotype.Repository;

import java.util.List;

@Mapper
@Repository
public interface CouponTemplateMapper extends BaseMapper<CouponTemplate> {

    // 保存CouponTemplate
    void saveCouponTemplate(CouponTemplate couponTemplate);

    List<CouponTemplate> findCouponTemplatesByIds(Integer[] ids);
}

Mapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="org.example.coupon.mapper.CouponTemplateMapper">

    <resultMap id="couponTemplateResultMap" type="org.example.coupon.entity.CouponTemplate">
        <id column="id" property="id"></id>
        <result column="name" property="name"></result>
        <result column="available" property="available"></result>
        <result column="expired" property="expired"></result>
        <result column="logo" property="logo"></result>
        <result column="coupon_count" property="count"></result>
        <result column="create_time" property="createTime"></result>
        <result column="expire_time" property="expireTime"></result>
        <result column="template_key" property="key"></result>
        <result column="category" property="category" typeHandler="org.example.coupon.columnhandler.CategoryHandler"></result>
        <result column="scope" property="scope" typeHandler="org.example.coupon.columnhandler.ScopeHandler"></result>
        <result column="target" property="target" typeHandler="org.example.coupon.columnhandler.TargetHandler"></result>
        <result column="rule" property="rule" typeHandler="org.example.coupon.columnhandler.TemplateRuleHandler"></result>
    </resultMap>

    <sql id="coupontTemplateCommonSql">
        select id, name, available, expired, logo, intro, category, scope, coupon_count, create_time, expire_time, user_id, template_key, target, rule
            from coupon_template
    </sql>

    <select id="selectAvailableCouponTemplate" resultMap="couponTemplateResultMap">
        <include refid="coupontTemplateCommonSql"></include> where available = #{available}
    </select>

    <select id="findCouponTemplatesByIds" resultMap="couponTemplateResultMap">
        <include refid="coupontTemplateCommonSql"></include> where id in
        <foreach collection="array" open="(" close=")" separator="," item="id">
            #{id}
        </foreach>
    </select>

    <insert id="saveCouponTemplate" parameterType="org.example.coupon.entity.CouponTemplate">
        insert into coupon_template (id, name, available, expired, logo, intro, category, scope, coupon_count,
            create_time, expire_time, user_id, template_key, target, rule) values (
                #{id},
                #{name},
                #{available},
                #{expired},
                #{logo},
                #{intro},
                #{category, typeHandler=org.example.coupon.columnhandler.CategoryHandler},
                #{scope, typeHandler=org.example.coupon.columnhandler.ScopeHandler},
                #{count},
                #{createTime},
                #{expireTime},
                #{userId},
                #{key},
                #{target, typeHandler=org.example.coupon.columnhandler.TargetHandler},
                #{rule, typeHandler=org.example.coupon.columnhandler.TemplateRuleHandler}
            )

    </insert>
</mapper>

启动项目进行测试:http://localhost:8082/template/ids?ids=22&ids=23&ids=24

1.2 创建coupon-distribution工程

1.2.1 代码编写

导入依赖,创建application.yml文件,编写启动类

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>coupon-service</artifactId>
        <groupId>com.qf</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>coupon-distribution</artifactId>

    <dependencies>
    <!-- 基于Java语言的序列化库 -->
    <dependency>
        <groupId>io.protostuff</groupId>
        <artifactId>protostuff-core</artifactId>
        <version>${protostuff.version}</version>
    </dependency>
    <dependency>
        <groupId>io.protostuff</groupId>
        <artifactId>protostuff-runtime</artifactId>
        <version>${protostuff.version}</version>
    </dependency>
    <!-- HTTP客户端访问框架 -->
    <dependency>
        <groupId>com.dtflys.forest</groupId>
        <artifactId>spring-boot-starter-forest</artifactId>
        <version>${forest.version}</version>
        <exclusions>
            <exclusion>
                <artifactId>commons-logging</artifactId>
                <groupId>commons-logging</groupId>
            </exclusion>
            <exclusion>
                <artifactId>commons-io</artifactId>
                <groupId>commons-io</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>com.qf</groupId>
        <artifactId>coupon-common</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        <exclusions>
            <exclusion>
                <artifactId>fastjson</artifactId>
                <groupId>com.alibaba</groupId>
            </exclusion>
            <exclusion>
                <artifactId>jsr305</artifactId>
                <groupId>com.google.code.findbugs</groupId>
            </exclusion>
            <exclusion>
                <artifactId>commons-io</artifactId>
                <groupId>commons-io</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- 对象连接池管理类 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
        <version>${commons.pool2.vesion}</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- 高性能的 JDBC 连接池组件 -->
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP-java7</artifactId>
        <version>${HikariCP.version}</version>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>${mybatisplus.version}</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>${fastjson.version}</version>
    </dependency>
    <!-- 处理字符串的算法库 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-text</artifactId>
        <version>${commons.text.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- java工具类 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutools.version}</version>
        </dependency>
        <!-- 集合工具类 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>${commons.collection4.version}</version>
        </dependency>
        <!-- 处理字符串的算法库 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-text</artifactId>
            <version>${commons.text.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

</dependencies>

</project>
server:
  port: 8083

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  application:
    name: coupon-distribution
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    url: jdbc:mysql://localhost:3306/coupon?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    hikari:
      connection-test-query: select 1
      minimum-idle: 5
      maximum-pool-size: 50
    password: root

# 在调用远程的服务的时候使用
forest:
  # springboot中默认的使用http请求的组件是 apache httpComponent
  backend: okhttp3
  max-connections: 10

mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: org/example/**/*.xml
package org.example.coupon;

import com.dtflys.forest.annotation.ForestScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@ForestScan("org.example.coupon.remoteservice")
@EnableFeignClients
public class CouponDistributionApplication {
    public static void main(String[] args) {
        SpringApplication.run(CouponDistributionApplication.class, args);
    }
}

columnhandler

package org.example.coupon.columnhandler;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.example.coupon.enums.CouponStatus;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class CouponStatusHandler extends BaseTypeHandler<CouponStatus> {
    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, CouponStatus couponStatus, JdbcType
            jdbcType) throws SQLException {
        preparedStatement.setInt(i, couponStatus.getCode());
    }

    @Override
    public CouponStatus getNullableResult(ResultSet resultSet, String s) throws SQLException {
        return CouponStatus.of(resultSet.getInt(s));
    }

    @Override
    public CouponStatus getNullableResult(ResultSet resultSet, int i) throws SQLException {
        return CouponStatus.of(resultSet.getInt(i));
    }

    @Override
    public CouponStatus getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
        return CouponStatus.of(callableStatement.getInt(i));
    }
}

TemplateClient

package org.example.coupon.feign;

import org.example.coupon.vo.CouponTemplateSDK;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;
import java.util.Map;

@Service
@FeignClient("coupon-template")
public interface TemplateClient {

    // http://localhost:8082/template/ids?ids=23&ids=5001&ids=6001
    @GetMapping("/template/ids")
    Map<Integer, CouponTemplateSDK> findCouponTemplateSDK2Ids(@RequestParam List<Integer> ids);
}

RedisConfig

package org.example.coupon.utils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.nio.charset.Charset;

@Configuration
public class RedisConfig {

    //fastjson
    @Bean(name="redisTemplate")
    public RedisTemplate<String, Object> fastJsonRedisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        FastJson2JsonRedisSerializer fastJson2JsonRedisSerializer = new FastJson2JsonRedisSerializer(Object.class);
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(fastJson2JsonRedisSerializer);
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(fastJson2JsonRedisSerializer);
        template.setDefaultSerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }

    public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {

        public final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

        private Class<T> clazz;

        public FastJson2JsonRedisSerializer(Class<T> clazz) {
            super();
            this.clazz = clazz;
        }

        public byte[] serialize(T t) throws SerializationException {
            if (t == null) {
                return new byte[0];
            }
            return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
        }

        public T deserialize(byte[] bytes) throws SerializationException {
            if (bytes == null || bytes.length <= 0) {
                return null;
            }
            String str = new String(bytes, DEFAULT_CHARSET);

            return (T) JSON.parseObject(str, clazz);
        }

    }
}

Entity

package org.example.coupon.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.example.coupon.enums.CouponStatus;
import org.example.coupon.vo.CouponTemplateSDK;
import java.time.LocalDateTime;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Coupon {
    private Integer id;
    private String couponCode;
    private Integer userId;
    private LocalDateTime assignDate;
    private Integer templateId;
    private CouponStatus status;

    private CouponTemplateSDK couponTemplateSDK;

    public static Coupon emptyCoupon() {
        Coupon coupon = new Coupon();
        coupon.setId(-1);//标识
        return coupon;
    }
}

CouponClassify

package org.example.coupon.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.example.coupon.entity.Coupon;
import org.example.coupon.enums.CouponStatus;
import org.example.coupon.enums.PeriodType;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

/**
 * 优惠券的分类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CouponClassify {
    private List<Coupon> available;
    private List<Coupon> used;
    private List<Coupon> expired;

    // 对传入的优惠券进行分类
    public static CouponClassify classify(List<Coupon> coupons) {
        int couponSize = coupons.size();
        List<Coupon> available = new ArrayList<>(couponSize);
        List<Coupon> used = new ArrayList<>(couponSize);
        List<Coupon> expired = new ArrayList<>(couponSize);

        // TODO 归类
        coupons.forEach(coupon -> {
            LocalDateTime assignDate = coupon.getAssignDate(); //优惠券的领取时间

            // 获取优惠券的过期的约束信息
            TemplateRule.Expiration expiration = coupon.getCouponTemplateSDK().getRule().getExpiration();

            boolean isExpired = false;

            // 如果过期策略为固定日期
            if(PeriodType.of(expiration.getPeriod()) == PeriodType.REGULAR) {
                // 如果当前日期在结束之后,表示优惠券过期了
                if (LocalDateTime.now().isAfter(expiration.getEnd())) {
                    isExpired = true;
                }
            }else if(PeriodType.of(expiration.getPeriod()) == PeriodType.SHIFT){
                // 如果当前日期在 用户领取之日 + 可使用的期限之后,表示过期了
                if(LocalDateTime.now().isAfter(assignDate.plusDays(expiration.getGap()))) {
                    isExpired = true;
                }
            }

            if(coupon.getStatus() == CouponStatus.USED) {
                used.add(coupon);
            }else if(coupon.getStatus() == CouponStatus.EXPIRED || isExpired) {
                expired.add(coupon);
            }else {
                available.add(coupon);
            }
        });

        return new CouponClassify(available, used, expired);
    }
}

Service

package org.example.coupon.service;

import org.example.coupon.entity.Coupon;

import java.util.List;

public interface IRedisService {

    /**
     * 根据状态查看优惠券的信息
     * @param userId
     * @param status
     * @return
     */
    List<Coupon> getCouponsByStatus(Integer userId, Integer status);

    /**
     * 设置空的优惠券信息到缓存中,目的是为了防止缓存的穿透以及缓存雪崩、缓存击穿
     * @param userId
     * @param coupon
     * @param status
     */
    void setEmptyCouponToCache(Integer userId, Integer status, Coupon coupon);

    /**
     * 尝试去获取优惠券码
     */
    String tryAcquireCouponCode(Integer templateId);

    /**
     * 将用户的优惠券设置redis中
     */
    void addCouponsToCache(Integer userId, Integer status, List<Coupon> coupons);
}
package org.example.coupon.service;

import org.example.coupon.entity.Coupon;

import java.util.List;

public interface ICouponService {

    /**
     * 根据优惠券状态来获取对应的优惠券
     */
    List<Coupon> getCouponsByStatus(Integer userId, Integer status);

}

ServiceImpl

package org.example.coupon.service.impl;

import cn.hutool.core.util.RandomUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.example.coupon.constant.RedisPrefix;
import org.example.coupon.entity.Coupon;
import org.example.coupon.enums.CouponStatus;
import org.example.coupon.service.IRedisService;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 用户的优惠券信息统一使用 hash来设置 redis:
 *  USER_COUPON_AVAILABLE_34  hash的key是优惠券的id, hash的值是优惠券具体信息
 */
@Slf4j
@Service
public class RedisServiceImpl implements IRedisService {

    private RedisTemplate redisTemplate;

    private RedisServiceImpl(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public List<Coupon> getCouponsByStatus(Integer userId, Integer status) {
        Objects.requireNonNull(userId, "用户信息不能为空");
        Objects.requireNonNull(status, "用户优惠券状态不能为空");

        //根据状态码返回优惠券在redis中存储的key值
        String redisKey = redisKeyPrefix(userId, status);
        // 获取对应的状态优惠券信息
        Object coupons = redisTemplate.opsForHash().values(redisKey);
        // 如果为空,防止缓存的穿透
        if(null == coupons) {
            // 给对应的状态的优惠券设置空的数据,目的防止缓存的穿透
            Coupon coupon = Coupon.emptyCoupon(); // 生成一个空的 Coupon
            setEmptyCouponToCache(userId, status, coupon);
            return Collections.emptyList();
        }

        List<Coupon> couponList = (List<Coupon>)coupons;

        return couponList;
    }

    /**
     * 设置空的 Coupon信息,目的是防止缓存穿透问题
     */
    @Override
    public void setEmptyCouponToCache(Integer userId, Integer status, Coupon coupon) {
        String redisKey = redisKeyPrefix(userId, status);
        // SessionCallback是Redis的管道操作, 大幅提升redis的效率
        SessionCallback<Object> sessionCallback = new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                operations.boundHashOps(redisKey).put("-1", Coupon.emptyCoupon());
                // 设置当前key在20-60分钟之后失效
                operations.expire(redisKey, randomExpireTime(20, 60), TimeUnit.SECONDS);
                return null;
            }
        };

        redisTemplate.executePipelined(sessionCallback);
    }

    @Override
    public String tryAcquireCouponCode(Integer templateId) {
        String couponCode = null;
        // 通过 redis的集合的 leftPop或者 rightPop 都能够防止超发的问题
        Object obj = redisTemplate.opsForList().leftPop(RedisPrefix.CouponTemplatePrefix.COUPON_TEMPLATE_CODE_PREFIX + templateId);
        if(null != obj) {
            couponCode = String.valueOf(obj);
        }
        return couponCode;
    }

    /**
     * 1.当新领取优惠券的时候,直接设置。
     * 2.如果是添加已使用优惠券,需要将之前对应的可使用的优惠券移除, 再添加到已使用;
     * 3.如果是添加已过期优惠券,需要将之前对应的可使用的优惠券移除,再添加到已过期。
     */
    @Override
    public void addCouponsToCache(Integer userId, Integer status, List<Coupon> coupons) {
        CouponStatus couponStatus = CouponStatus.of(status);
        switch (couponStatus) {
            case AVAILABLE:
                addAvailbleCouponToCache(userId, status, coupons);
                break;
            case USED:
                addUsedCouponToCache(userId, status, coupons);
                break;
            case EXPIRED:
                addExpiredCouponToCache(userId, status, coupons);
                break;
        }
    }

    /**
     * 添加可用状态的优惠券到缓存中
     * hash: key  k  v
     */
    private void addAvailbleCouponToCache(Integer userId, Integer status, List<Coupon> coupons) {
        log.info("Add available coupon to cache.");
        String redisKey = redisKeyPrefix(userId, status);

        Map<String, Coupon> ids2Coupon = new HashMap<>();

        coupons.stream().forEach(c -> ids2Coupon.put(c.getId().toString(), c));

        // SessionCallback是Redis的管道操作, 大幅提升redis的效率
        SessionCallback<Object> sessionCallback = new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                operations.boundHashOps(redisKey).putAll(ids2Coupon);
                operations.expire(redisKey, randomExpireTime(20, 60), TimeUnit.SECONDS);
                return null;
            }
        };

        redisTemplate.executePipelined(sessionCallback);
    }

    // 添加已使用的优惠券到缓存中,会影响可用优惠券
    private void addUsedCouponToCache(Integer userId, Integer status, List<Coupon> coupons) {
        // 获取之前可用优惠券
        List<Coupon> preAvailableCoupons = getCouponsByStatus(userId, CouponStatus.AVAILABLE.getCode());

        // 获取将要添加到已用优惠券 id
        List<Integer> willAddToUsedCacheCouponIds = coupons.stream().map(c -> c.getId()).collect(Collectors.toList());

        // 所有的可用优惠券的 id
        List<Integer> allAvailableCouponsIds = preAvailableCoupons.stream().map(c -> c.getId()).collect(Collectors.toList());

        String usedCouponRedisPrefix = redisKeyPrefix(userId, CouponStatus.USED.getCode());
        String availableCouponRedisPrefix = redisKeyPrefix(userId, CouponStatus.AVAILABLE.getCode());

        /**
         * 当要添加到已使用优惠券缓存的时候,那么先判断被添加的优惠是否在可用优惠券的缓存中。
         */
        if (CollectionUtils.isSubCollection(willAddToUsedCacheCouponIds, allAvailableCouponsIds)) {
            // 从可用优惠券中移除对应的优惠券。
            redisTemplate.boundHashOps(availableCouponRedisPrefix).delete(willAddToUsedCacheCouponIds.toArray());
        }

        Map<String, Coupon> willAdd2UsedCoupons = new HashMap<>();
        coupons.stream().forEach(c -> {
            willAdd2UsedCoupons.put(c.getId().toString(), c);
        });

        SessionCallback<Object> sessionCallback = new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                /**
                 * 1.将对应的优惠券添加到已使用优惠券中。
                 * 2.从可用优惠券中移除对应的优惠券。
                 * 3.设置一个随机的过期时间
                 */
                // 将对应的优惠券添加到已使用优惠券中。
                operations.boundHashOps(usedCouponRedisPrefix).putAll(willAdd2UsedCoupons);

                operations.boundHashOps(usedCouponRedisPrefix).expire(randomExpireTime(20, 60), TimeUnit.SECONDS);
                return null;
            }
        };

        redisTemplate.executePipelined(sessionCallback);
    }

    private void addExpiredCouponToCache(Integer userId, Integer status, List<Coupon> coupons) {
        // 获取之前可用优惠券
        List<Coupon> preAvailableCoupons = getCouponsByStatus(userId, CouponStatus.AVAILABLE.getCode());

        // 获取将要添加到已用优惠券 id
        List<Integer> willAddToUsedCacheCouponIds = coupons.stream().map(c -> c.getId()).collect(Collectors.toList());

        // 所有的优惠券的 id
        List<Integer> allAvailableCouponsIds = preAvailableCoupons.stream().map(c -> c.getId()).collect(Collectors.toList());

        String expiredCouponRedisPrefix = redisKeyPrefix(userId, CouponStatus.EXPIRED.getCode());
        String availableCouponRedisPrefix = redisKeyPrefix(userId, CouponStatus.AVAILABLE.getCode());

        // 判断要添加到已使用优惠券缓存的优惠券之前必须是可用的
        if (CollectionUtils.isSubCollection(willAddToUsedCacheCouponIds, allAvailableCouponsIds)) {
            // 从可用优惠券中移除对应的优惠券。
            redisTemplate.boundHashOps(availableCouponRedisPrefix).delete(willAddToUsedCacheCouponIds.toArray());
        }

        Map<String, Coupon> willAdd2Expiredoupons = new HashMap<>();
        coupons.stream().forEach(c -> {
            willAdd2Expiredoupons.put(c.getId().toString(), c);
        });

        SessionCallback<Object> sessionCallback = new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                /**
                 * 1.将对应的优惠券添加到过期优惠券中。
                 * 2.从可用优惠券中移除对应的优惠券。
                 * 3.设置一个随机的过期时间
                 */
                // 将对应的优惠券添加到已使用优惠券中。
                operations.boundHashOps(expiredCouponRedisPrefix).putAll(willAdd2Expiredoupons);

                operations.boundHashOps(expiredCouponRedisPrefix).expire(randomExpireTime(20, 60), TimeUnit.SECONDS);
                return null;
            }
        };

        redisTemplate.executePipelined(sessionCallback);
    }

    // 根据不同的优惠券的状态返回不同的前缀
    private String redisKeyPrefix(Integer userId, Integer status) {
        CouponStatus couponStatus = CouponStatus.of(status);
        String prefix = null;
        switch (couponStatus) {
            case AVAILABLE:
                prefix = RedisPrefix.CouponDistributionPrefix.USER_COUPON_AVAILABLE_PREFIX + userId;
                break;
            case USED:
                prefix = RedisPrefix.CouponDistributionPrefix.USER_COUPON_USED_PREFIX + userId;
                break;
            case EXPIRED:
                prefix = RedisPrefix.CouponDistributionPrefix.USER_COUPON_EXPIRED_PREFIX + userId;
                break;
        }
        return prefix;
    }

    /**
     * min 为最小的分分钟数
     * max 为最大的分钟数
     */
    public long randomExpireTime(int min, int max) {
        return RandomUtil.randomInt(min * 60, max * 60);
    }
}
package org.example.coupon.service.impl;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.example.coupon.entity.Coupon;
import org.example.coupon.enums.CouponStatus;
import org.example.coupon.exception.CouponException;
import org.example.coupon.feign.TemplateClient;
import org.example.coupon.mapper.CouponMapper;
import org.example.coupon.service.ICouponService;
import org.example.coupon.service.IRedisService;
import org.example.coupon.vo.CouponClassify;
import org.example.coupon.vo.CouponTemplateSDK;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;


/**
 * Code -> Inspections
 */
@Service
@Slf4j
public class CouponServiceImpl implements ICouponService {

    private IRedisService redisService;
    private CouponMapper couponMapper;
    private TemplateClient templateClient;

    public CouponServiceImpl(IRedisService redisService, CouponMapper couponMapper,
                             TemplateClient templateClient) {
        this.redisService = redisService;
        this.couponMapper = couponMapper;
        this.templateClient = templateClient;
    }

    /**
     * 根据状态获取对应的优惠券
     */
    @Override
    public List<Coupon> getCouponsByStatus(Integer userId, Integer status) {
        List<Coupon> cacheCoupon = redisService.getCouponsByStatus(userId, status);

        List<Coupon> preCoupons = null;

        // 如果缓存中为空
        if(CollectionUtils.isEmpty(cacheCoupon)) {
            log.info("Coupon in cache is null: {}, {}", userId, status);
            List<Coupon> dbCoupons = couponMapper.findCouponsByStatus(userId, status);
            // 如果数据库也为空
            if(CollectionUtils.isEmpty(dbCoupons)) {
                log.info("Coupon in db is null: {}, {}", userId, status);
                return cacheCoupon;
            }else {
                // 数据库有数据
                // 要获取优惠券模板的id
                List<Integer> ids = dbCoupons.stream().map(c -> c.getTemplateId()).collect(Collectors.toList());
                // 调用优惠券模板系统获取指定id的优惠券模板
                Map<Integer, CouponTemplateSDK> map = templateClient.findCouponTemplateSDK2Ids(ids);

                log.info("{} status coupons size is {}", status, dbCoupons.size());
                log.info("coupon template size is {}", map.size());

                if(dbCoupons.size() != map.size()) {
                    log.error("Coupon Template size is not correspond to coupon size: coupons's size is {}, template size's is {}",
                            dbCoupons.size(), map.size());
                    throw new CouponException("Size is not correspond.");
                }

                preCoupons = dbCoupons;

                dbCoupons.forEach(c -> c.setCouponTemplateSDK(map.get(c.getTemplateId())));

                // 将 优惠券设置到 缓存中
                redisService.addCouponsToCache(userId, status, dbCoupons); //将数据设置到缓存中
            }
        }else { // 缓存中不为空
            log.info("coupons is not null in cache: {}, {}", userId, status);
            preCoupons = cacheCoupon;
        }

        // 过滤掉id 为 -1 的数据
        preCoupons = preCoupons.stream().filter(c -> c.getId() != -1).collect(Collectors.toList());

        /**
         * 如果是查询可用状态,那么需要筛选出已经过期的优惠券,丢给rabbitmq异步处理
         */
        if(CouponStatus.AVAILABLE == CouponStatus.of(status)) {
            // 对优惠券重新分类
            CouponClassify couponClassify = CouponClassify.classify(preCoupons);
            // 分类之后,判断已经过期的优惠券是否为空
            if(CollectionUtils.isNotEmpty(couponClassify.getExpired())) {
                // TODO 丢给rabbitmq异步处理, 将对应的优惠券改为过期状态

            }
            return couponClassify.getAvailable(); //返回可用的优惠券
        }
        return preCoupons;
    }

}

Mapper

package org.example.coupon.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.example.coupon.entity.Coupon;
import org.springframework.stereotype.Repository;

import java.util.List;

@Mapper
@Repository
public interface CouponMapper extends BaseMapper<Coupon> {

    List<Coupon> findCouponsByStatus(@Param("userId") Integer userId, @Param("status") Integer status);
}

Mapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="org.example.coupon.mapper.CouponMapper">

    <resultMap id="couponResultMap" type="org.example.coupon.entity.Coupon">
        <id column="id" property="id"></id>
        <result column="user_id" property="userId"></result>
        <result column="template_id" property="templateId"></result>
        <result column="coupon_code" property="couponCode"></result>
        <result column="assign_date" property="assignDate"></result>
        <result column="status" property="status" typeHandler="org.example.coupon.columnhandler.CouponStatusHandler"></result>
    </resultMap>

    <sql id="couponCommonSql">
        select id, user_id, template_id, coupon_code, assign_date, status from coupon
    </sql>

    <select id="findCouponsByStatus" resultMap="couponResultMap">
        <include refid="couponCommonSql"></include>
        where user_id = #{userId} and status = #{status}
    </select>
</mapper>

编写测试类,进行测试即可

package org.example.coupon.test;

import org.example.coupon.CouponDistributionApplication;
import org.example.coupon.entity.Coupon;
import org.example.coupon.service.ICouponService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest(classes = CouponDistributionApplication.class)
public class CouponTest {

    @Autowired
    private ICouponService couponService;

    @Test
    public void getCoupons() {

        List<Coupon> list = couponService.getCouponsByStatus(1000, 1);
        list.forEach(c -> System.out.println(c.getCouponCode() + "##" + c.getId() + "##" + c.getTemplateId()));
        System.out.println(list);

    }
}