Skip to content

系统设计

此部分针对整个项目的系统设计,包含整个系统的架构设计,数据库表设计;系统交互时序;缓存体系设计

建模

ER图

数据表

奖品表(card_product)

字段类型备注
idint(10) unsigned
namevarchar(255)奖品名称
picvarchar(255)图片
infovarchar(1000)简介
pricedecimal(10,2)市场价

活动表(card_game)

字段类型备注
idint(10) unsigned
titlevarchar(255)活动主题
picvarchar(255)活动宣传图
infovarchar(1000)活动简介
starttimedatetime开始时间
endtimedatetime结束时间
typetinyint(2)类型(1=概率类,2=随机类)
statustinyint(1)状态(0=新建,1=已加载)

会员表(card_user)

字段类型备注
idint(11) unsigned
unamevarchar(20)用户名
passwdvarchar(50)密码
realnamevarchar(10)姓名
idcardvarchar(18)身份证号
phonevarchar(15)手机号码
levelsmallint(6)等级
createtimedatetime注册时间
updatetimedatetime更新时间

策略表(card_game_rules)

字段类型备注
idint(11) unsigned
gameidint(11) unsigned活动id
userlevelsmallint(6)会员等级
enter_timessmallint(6)可抽奖次数(0为不限)
goal_timessmallint(6)最大中奖次数(0为不限)

中奖纪录(card_user_hit)

字段类型备注
idint(10) unsigned
gameidint(10) unsigned活动id
useridint(10) unsigned用户
productidint(10) unsigned奖品
hittimedatetime中奖时间

奖品活动关联关系(card_game_product)

字段类型备注
idint(10) unsigned
gameidint(11) unsigned活动id
productidint(11) unsigned奖品id
amountsmallint(6)数量

视图

中奖信息(view_card_user_hit)

字段类型备注
idint(10) unsigned
titlevarchar(255)活动主题
typevarchar(100)类型(值)
unamevarchar(20)用户名
realnamevarchar(10)姓名
idcardvarchar(18)身份证号
phonevarchar(15)手机号码
levelvarchar(100)会员等级(值)
namevarchar(255)奖品名称
pricedecimal(10,2)市场价
gameidint(10) unsigned活动
useridint(10) unsigned用户
productidint(10) unsigned奖品
hittimedatetime中奖时间

奖品数统计(view_game_curinfo)

字段类型备注
idint(10) unsigned
titlevarchar(255)活动主题
typevarchar(100)类型(值)
starttimedatetime开始时间
endtimedatetime结束时间
typevarchar(100)类型(值)
totaldecimal(27,0)
hitbigint(21)

概要设计

业务系统架构拓扑图

软件系统架构拓扑图

设计原则

动静分离

  • 静态文件分离,nginx直接响应,不要再绕后台应用机器

微服务化

  • 将模块细粒度拆分,微服务化
  • 借助 docker swarm 的容器管理功能,实现不同服务的副本部署,滚动更新
  • 在本项目中,api 模块就部署了3份,以适应前端的高并发

负载均衡

  • 多个实例之间通过 nginx 做负载均衡,提升并发性能
  • 本项目模块均部署在1台节点。生产环境涉及多台机器,用通过 upstream 配置实现。

异步消息

  • 中奖后,中奖人及奖品信息要持久化到数据库。引入 rabbitmq,将抽奖操作与数据库操作异步隔离。
  • 抽奖中奖后,只需要将中奖信息放入 rabbitmq,并立即返回中奖信息给前端用户。
  • 后端 msg 模块消费 rabbitmq 消息,缓慢处理。

缓存预热

  • 每隔 1 分钟扫描一次活动表,查询未来 1 分钟内将要开始的活动。
  • 将扫到的活动加载进 redis,包括活动详细信息,中奖策略信息,奖品信息,抽奖令牌。
  • 活动正式开始后,基于 redis 数据做查询,不必再与数据库打交道。

项目交互序列图

缓存体系

缓存体系概览图:

缓存结构设计:

  • 活动开始前 1 分钟扫描将要开始的活动
  • 将活动信息加载进 redis
  • 将活动策略信息加载进 redis
  • 按活动奖品信息,生成对应个数的时间戳做令牌,从小到大排好序,右侧入队
  • 以令牌为 key,对应的奖品为 value,建立映射关系,为中奖后获取奖品做准备
  • 抽奖开始时,从令牌队列左侧获取令牌
  • 如果令牌小于当前时间,说明中奖,找到令牌对应的奖品,抽走
  • 如果令牌大于当前时间,说明未中奖,从左侧将令牌还回队列

活动基本信息

k-v形式存储,以活动 id 为 key,活动对象为 value,永不超时。示例代码如下:

java
redisUtil.set(RedisKeys.INFO + game.getId(), game, ‐1);

活动策略信息

hash类型存储,以 活动 id 为 key,用户等级为 hashkey,策略值为 value。示例代码如下:

java
redisUtil.hset(RedisKeys.MAXGOAL + game.getId(), r.getUserlevel() + "", r.getGoalTimes());
redisUtil.hset(RedisKeys.MAXENTER + game.getId(), r.getUserlevel() + "", r.getEnterTimes());

抽奖令牌桶

使用双端队列,即 list 类型存储,以 活动 id 为 key,在活动时间段内,随机生成时间戳做令牌,有多少个奖品就生成多少个令牌。令牌即奖品发放的时间点。从小到大排序后从右侧入队。示例代码如下:

java
redisUtil.rightPushAll(RedisKeys.TOKENS + game.getId(), tokenList);

奖品映射信息

k-v形式存储 , 以 活动id_令牌 为 key,奖品信息为 value,会员获取到令牌后,如果令牌有效,则用令牌 token 值,来这里获取奖品详细信息。示例代码如下:

java
redisUtil.set(RedisKeys.TOKEN + game.getId() + "_" + token, productMap.get(cgp.getProductid()), expire);

令牌设计技巧

假设活动时间间隔太短,奖品数量太多。那么极有可能产生的时间戳发生重复。

解决技巧:额外再附加一个随机因子。将 (时间戳 * 1000 + 3位随机数)作为令牌。抽奖时,将抽中的令牌/1000,还原真实的时间戳。示例代码如下:

java
// 活动持续时间(ms)
long duration = end ‐ start;
long rnd = start + new Random().nextInt((int) duration);
// 为什么乘1000,再额外加一个随机数呢? ‐ 防止时间段奖品多时重复
long token = rnd * 1000 + new Random().nextInt(999);

中奖计数

k-v形式存储,以 活动id_用户id 作为 key,中奖数为 value,利用 redis 原子性,中奖后使用 incr 命令增加计数。示例代码如下:

java
redisUtil.incr(RedisKeys.USERHIT + gameid + "_" + user.getId(), 1);

中奖逻辑判断

抽奖时,从令牌桶左侧出队和当前时间比较,如果令牌时间戳小于等于当前时间,令牌有效,表示中奖。大于当前时间,则令牌无效,将令牌还回,从左侧压入队列。

管理后台框架选型

此管理后台直接借助开源zcurd开发平台,完成后台基本的增删改查。

zcurd 项目地址:https://gitee.com/515097842/zcurd

前台工程各模块介绍

【frontend】工程是接口微服务源码,项目基于springcloud构建微服务体系

聚合工程(frontend)

此工程是所有项目共用的依赖及版本定义。继承自 springboot 2.1.7.RELEASE,依赖 spring cloud Greenwich.SR2。以下是部分依赖片段:

xml
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.7.RELEASE</version>
</parent>

....

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Greenwich.SR2</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
        ....
    </dependencies>
</dependencyManagement>

公共模块(commons)

分页、密码、统一结果dto等工具类,mybatis生成的实体,mapper,redis及rabbit的配置bean

注册中心(eureka)

cloud 官方推荐的微服务注册中心,所有的服务模块都在 eureka 中注册。

调度模块(job)

集成当当网开源的 elastic-job,活动的调度扫描预热等定时任务在该模块中。

消息模块(msg)

RabbitMQ 消息消费端,用户中奖后将中奖信息发送到 RabbitMQ,msg模块将消费消息,将中奖信息写入db。

接口模块(api)

提供给前端页面的 rest 接口模块,如:抽奖接口,活动查询接口,中奖信息接口等

微服务框架集成

集成 zookeeper 配置中心

spring cloud 配置中心使用 zookeeper,以 api 工程为例

引入 zookeeper 配置中心依赖

此依赖已经在父级聚合工程中引入:

xml
<!-- 配置中心 zookeeper -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zookeeper-config</artifactId>
</dependency>

创建 bootstrap.properties 配置文件

在 api 项目的 resources 目录下创建 bootstrap.properties 配置文件(此配置文件的优先最高)。此文件核心的配置内容是 zookeeper 配置中心的连接地址。具体配置内容如下:

properties
## 配置应用名称
spring.application.name=api
## 配置zookeeper地址
spring.cloud.zookeeper.connect-string=192.168.12.132:2181
spring.cloud.zookeeper.config.root=prize
spring.profiles.active=dev

配置查找的优先级

使用 zookeeper 配置中心的加载优先级是:

本地配置 ->  `模块,环境` 节点 -> `模块` 节点 -> `application,环境` 公共节点 -> `application` 公共节点

以 api 工程为例,会先加载本地配置文件 bootstrap.properties,再根据当前配置的 profiles.active 属性,加载 api,dev 节点,再加载 api 节点,再加载公共的 application,dev,最后加载公共的 application 节点

6.2.集成 Redis

引入 Spring Data Redis 依赖

本项目使用 Spring Data Redis,因为可能会有多个模块用到缓存,所以在 commons 工程引入 redis 客户端的依赖,其他工程需要时再依赖 commons 模块即可:

xml
<!-- Spring Data Redis 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <!--<scope>require</scope>-->
</dependency>

Redis 相关配置

在 zookeeper 配置中心中修改 Redis 的配置。此部分为公共配置,所以在 /prize/application 节点中定义。如开发时有不一样的配置,在application,dev节点下设置相同配置即可覆盖

properties
/prize/application=spring.redis.database=0
/prize/application=spring.redis.host=172.17.0.1
/prize/application=spring.redis.password=
/prize/application=spring.redis.pool.max-active=3
/prize/application=spring.redis.pool.max-idle=10
/prize/application=spring.redis.pool.max-wait=1000
/prize/application=spring.redis.pool.min-idle=1
/prize/application=spring.redis.port=6379
/prize/application=spring.redis.timeout=3000

集成 elastic-job 定时任务

引入 elastic-job 依赖

定时调度只有 job 模块会使用,所只在 job 模块工程中引入 elastic-job 的依赖

注意:curator版本会报冲突,排除后,手动引入版本

xml
<properties>
    ....
    <job.version>2.1.5</job.version>
    <curator.version>2.10.0</curator.version>
</properties>

<dependencies>
    <!-- elastic-job 框架核心jar包 -->
    <dependency>
        <groupId>com.dangdang</groupId>
        <artifactId>elastic-job-lite-core</artifactId>
        <version>${job.version}</version>
        <exclusions>
            <exclusion>
                <artifactId>curator-client</artifactId>
                <groupId>org.apache.curator</groupId>
            </exclusion>
            <exclusion>
                <artifactId>curator-framework</artifactId>
                <groupId>org.apache.curator</groupId>
            </exclusion>
            <exclusion>
                <artifactId>curator-recipes</artifactId>
                <groupId>org.apache.curator</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    <!-- 手动引入 curator 依赖,指定不会冲突的版本 -->
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-framework</artifactId>
        <version>${curator.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-client</artifactId>
        <version>${curator.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-recipes</artifactId>
        <version>${curator.version}</version>
    </dependency>

    <dependency>
        <groupId>com.dangdang</groupId>
        <artifactId>elastic-job-lite-spring</artifactId>
        <version>${job.version}</version>
    </dependency>

    <!--添加数据相关的驱动主要是为了记录任务相关的一些数据,日志-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

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

    <dependency>
        <groupId>com.moon.prize</groupId>
        <artifactId>commons</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

elastic-job 相关配置

以下是 elastic-job 相关配置,保存到 txt 文档,通过 zkui 导入到 zookeeper 即可。

properties
/prize/job,dev=elaticjob.zookeeper.session-timeout-milliseconds=5000
/prize/job,dev=spring.datasource.druid.joblog.driver-class-name=com.mysql.jdbc.Driver
/prize/job,dev=spring.datasource.druid.joblog.password=root
/prize/job,dev=spring.datasource.druid.joblog.url=jdbc:mysql://192.168.12.132:3306/event_log
/prize/job,dev=spring.datasource.druid.joblog.username=root
/prize/job,dev=spring.jpa.database=mysql
/prize/job,dev=spring.jpa.hibernate.ddl-auto=update
/prize/job,dev=spring.jpa.show-sql=true
/prize/job=elaticjob.zookeeper.session-timeout-milliseconds=5000
/prize/job=spring.datasource.druid.joblog.driver-class-name=com.mysql.jdbc.Driver
/prize/job=spring.datasource.druid.joblog.password=root
/prize/job=spring.datasource.druid.joblog.url=jdbc:mysql://172.17.0.1:3306/event_log
/prize/job=spring.datasource.druid.joblog.username=root
/prize/job=spring.jpa.database=mysql
/prize/job=spring.jpa.hibernate.ddl-auto=update
/prize/job=spring.jpa.show-sql=true

配置与使用

  • 创建 elastic-job 的自定义注解 ElasticSimpleJob,用于设置任务的相关内容
java
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface ElasticSimpleJob {
    // cron 表达式
    @AliasFor("cron")
    String value() default "";

    @AliasFor("value")
    String cron() default "";
    // 定时任务的名称
    String jobName() default "";
    // 分片的数量,即在分布式环境中分成多份同时执行
    int shardingTotalCount() default 1;
    // 指定不同分片所获取到的传递参数
    String shardingItemParameters() default "";
    // 执行任务所传递的参数
    String jobParameter() default "";
    // 定义数据源,值是数据源实例在 spring 容器中的名称
    String dataSource() default "";

    String description() default "";

    boolean disabled() default false;

    boolean overwrite() default true;
}
  • 创建 elastic-job 的配置类,在此类中主要是初始化 elastic-job 的任务调度对象 JobEventRdbConfiguration 并调用其 init 初始化方法,这里所需要的参数都是从上面自定义的 @ElasticSimpleJob 注解中获取
java
@Configuration
public class ElasticJobAutoConfiguration {

    @Value("${spring.cloud.zookeeper.connect-string}")
    private String serverList;

    @Value("${spring.application.name}")
    private String namespace;

    @Value("${elaticjob.zookeeper.session-timeout-milliseconds}")
    private int sessionTimeoutMilliseconds;

    @Autowired
    private ApplicationContext applicationContext;

    // 在配置类加载时执行,对 elastic-job 的一些配置
    @PostConstruct
    public void initElasticJob() {
        // 获取 zookeeper 中的配置
        ZookeeperConfiguration config = new ZookeeperConfiguration(serverList, namespace);
        config.setSessionTimeoutMilliseconds(sessionTimeoutMilliseconds);
        // 初始化 ZookeeperRegistryCenter,主要是用于 elastic-job 分片与调试
        ZookeeperRegistryCenter regCenter = new ZookeeperRegistryCenter(config);
        regCenter.init();
        // 获取 spring 容器中所有 SimpleJob 接口的实例(实现类)
        Map<String, SimpleJob> map = applicationContext.getBeansOfType(SimpleJob.class);
        for (Map.Entry<String, SimpleJob> entry : map.entrySet()) {
            SimpleJob simpleJob = entry.getValue();
            // 获取 @ElasticSimpleJob 注解(自定义的注解)
            ElasticSimpleJob elasticSimpleJobAnnotation = simpleJob.getClass().getAnnotation(ElasticSimpleJob.class);
            // 获取注解中的 cron 表达式、分片、任务名称、任务的参数等,用于创建 SimpleJobConfiguration 配置对象
            String cron = StringUtils.defaultIfBlank(elasticSimpleJobAnnotation.cron(), elasticSimpleJobAnnotation.value());
            SimpleJobConfiguration simpleJobConfiguration = new SimpleJobConfiguration(
                    JobCoreConfiguration.newBuilder(simpleJob.getClass().getName(), cron, elasticSimpleJobAnnotation.shardingTotalCount())
                            .shardingItemParameters(elasticSimpleJobAnnotation.shardingItemParameters())
                            .build(),
                    simpleJob.getClass().getCanonicalName()
            );
            LiteJobConfiguration liteJobConfiguration = LiteJobConfiguration.newBuilder(simpleJobConfiguration).overwrite(true).build();

            String dataSourceRef = elasticSimpleJobAnnotation.dataSource();
            if (StringUtils.isNotBlank(dataSourceRef)) {

                if (!applicationContext.containsBean(dataSourceRef)) {
                    throw new RuntimeException("not exist datasource [" + dataSourceRef + "] !");
                }

                DataSource dataSource = (DataSource) applicationContext.getBean(dataSourceRef);
                JobEventRdbConfiguration jobEventRdbConfiguration = new JobEventRdbConfiguration(dataSource);
                // 创建 SpringJobScheduler 任务对象,并调用初始化方法
                SpringJobScheduler jobScheduler = new SpringJobScheduler(simpleJob, regCenter, liteJobConfiguration, jobEventRdbConfiguration);
                jobScheduler.init();
            } else {
                SpringJobScheduler jobScheduler = new SpringJobScheduler(simpleJob, regCenter, liteJobConfiguration);
                jobScheduler.init();
            }
        }
    }

    /**
     * 设置活动监听,前提是已经设置好了监听,见下一个目录
     *
     * @return
     */
    @Bean
    public ElasticJobListener elasticJobListener() {
        return new ElasticJobListener(100, 100);
    }
}
  • 创建 com.dangdang.ddframe.job.api.simple.SimpleJob 的实现类,在此类的 execute 方法中编写定时任务的业务逻辑
java
/**
 * 分片调度测试
 * ribbon调度测试
 * 启动多个实例,会发现不同分片运行到不同实例上
 * 关闭后,分片被重新调度到存活的机器
 * 证明elastic-job是分布式的,可分片的。
 */
@ElasticSimpleJob(cron = "0/3 * * * * ?",
        jobName = "firstJob",
        shardingTotalCount = 2,
        jobParameter = "测试参数",
        shardingItemParameters = "0=A,1=B",
        dataSource = "datasource")
@Component
public class MyJob implements SimpleJob {

    @Override
    public void execute(ShardingContext shardingContext) {
        System.out.println(String.format("jobtest------>Thread ID: %s, 任务总片数: %s, " +
                        "当前分片项: %s,当前参数: %s," +
                        "当前任务名称: %s,当前任务参数: %s,"+
                        "当前任务的id: %s"
                ,
                // 获取当前线程的id
                Thread.currentThread().getId(),
                // 获取任务总片数
                shardingContext.getShardingTotalCount(),
                // 获取当前分片项
                shardingContext.getShardingItem(),
                // 获取当前的参数
                shardingContext.getShardingParameter(),
                // 获取当前的任务名称
                shardingContext.getJobName(),
                // 获取当前任务参数
                shardingContext.getJobParameter(),
                // 获取任务的id
                shardingContext.getTaskId()
        ));
    }
}

注:其中 @ElasticSimpleJob 是自定义的注解

集成 RabbitMQ

引入 RabbitMQ 依赖

因为可能会有多个需要发送与消费消息,所以在 commons 工程引入 RabbitMQ 的依赖,其他工程需要时再依赖 commons 模块即可:

xml
<!-- rabbitmq -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

RabbitMQ 相关配置

以下是 RabbitMQ 相关配置,保存到 txt 文档,通过 zkui 导入到 zookeeper 即可。

properties
/prize/application,dev=spring.rabbitmq.host=192.168.12.132
/prize/application,dev=spring.rabbitmq.password=guest
/prize/application,dev=spring.rabbitmq.port=5672
/prize/application,dev=spring.rabbitmq.publisher-confirms=true
/prize/application,dev=spring.rabbitmq.username=guest
/prize/application,dev=spring.rabbitmq.virtual-host=/
/prize/application=spring.rabbitmq.host=172.17.0.1
/prize/application=spring.rabbitmq.password=guest
/prize/application=spring.rabbitmq.port=5672
/prize/application=spring.rabbitmq.publisher-confirms=true
/prize/application=spring.rabbitmq.username=guest
/prize/application=spring.rabbitmq.virtual-host=/

集成 MySQL 数据源 druid

引入数据源相关依赖

在父级聚合项目中 <dependencyManagement> 进行版本控制,各自模块自行依赖即可:

xml
<!-- 数据源 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.31</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
</dependency>

数据源相关配置

properties
/prize/application=spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
/prize/application=spring.datasource.driver-class-name=com.mysql.jdbc.Driver
/prize/application=spring.datasource.filters=stat,wall,log4j
/prize/application=spring.datasource.initialSize=5
/prize/application=spring.datasource.maxActive=20
/prize/application=spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
/prize/application=spring.datasource.maxWait=60000
/prize/application=spring.datasource.minEvictableIdleTimeMillis=300000
/prize/application=spring.datasource.minIdle=5
/prize/application=spring.datasource.password=root
/prize/application=spring.datasource.poolPreparedStatements=true
/prize/application=spring.datasource.testOnBorrow=false
/prize/application=spring.datasource.testOnReturn=false
/prize/application=spring.datasource.testWhileIdle=true
/prize/application=spring.datasource.timeBetweenEvictionRunsMillis=60000
/prize/application=spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
/prize/application=spring.datasource.url=jdbc:mysql://172.17.0.1/prize?useUnicode=true&characterEncoding=utf-8
/prize/application=spring.datasource.useGlobalDataSourceStat=true
/prize/application=spring.datasource.username=root
/prize/application=spring.datasource.validationQuery=SELECT 'x'
/prize/job,dev=spring.datasource.druid.joblog.driver-class-name=com.mysql.jdbc.Driver
/prize/job,dev=spring.datasource.druid.joblog.password=root
/prize/job,dev=spring.datasource.druid.joblog.url=jdbc:mysql://192.168.12.132:3306/event_log
/prize/job,dev=spring.datasource.druid.joblog.username=root
/prize/job=spring.datasource.druid.joblog.driver-class-name=com.mysql.jdbc.Driver
/prize/job=spring.datasource.druid.joblog.password=root
/prize/job=spring.datasource.druid.joblog.url=jdbc:mysql://192.168.12.132:3306/event_log
/prize/job=spring.datasource.druid.joblog.username=root

集成 mybatis

引入 mybatis 依赖

在父级聚合项目中 <dependencyManagement> 进行版本控制,各自模块自行依赖即可:

xml
<!-- mybatis -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.2</version>
</dependency>

mybatis 相关配置

通过 zkui 服务直接修改配置即可

properties
/prize/application,dev=mybatis.mapper-locations=classpath:mapper/*.xml
/prize/application=mybatis.mapper-locations=classpath:mapper/*.xml

集成 redis session

引入 session 依赖

commons 工程引入 spring-session-data-redis 客户端的依赖,其他工程需要时再依赖 commons 模块即可:

xml
<!-- redis 处理 session -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

相关配置

通过 zkui 服务直接修改配置即可

properties
/prize/application,dev=spring.session.store-type=redis
/prize/application=spring.session.store-type=redis

项目相关工具框架

zkui

zkui 是 zookeeper 的一个管理工具,具有登录界面,及zk中节点和值的 crud 操作入口,支持导入导出功能。

配置文件

在【frontend】目录中的 config.cfg 文件,是 zkui 的项目配置文件

properties
#Server Port
serverPort=9090
#Comma seperated list of all the zookeeper servers
zkServer=192.168.12.132:2181
#Http path of the repository. Ignore if you dont intent to upload files from repository.
scmRepo=http://myserver.com/@rev1=
#Path appended to the repo url. Ignore if you dont intent to upload files from repository.
scmRepoPath=//appconfig.txt
#if set to true then userSet is used for authentication, else ldap authentication is used.
ldapAuth=false
ldapDomain=mycompany,mydomain
#ldap authentication url. Ignore if using file based authentication.
ldapUrl=ldap://<ldap_host>:<ldap_port>/dc=mycom,dc=com
#Specific roles for ldap authenticated users. Ignore if using file based authentication.
ldapRoleSet={"users": [{ "username":"domain\\user1" , "role": "ADMIN" }]}
userSet = {"users": [{ "username":"admin" , "password":"admin","role": "ADMIN" },{ "username":"appconfig" , "password":"appconfig","role": "USER" }]}
#Set to prod in production and dev in local. Setting to dev will clear history each time.
env=prod
jdbcClass=org.h2.Driver
jdbcUrl=jdbc:h2:zkui
jdbcUser=root
jdbcPwd=manager
#If you want to use mysql db to store history then comment the h2 db section.
#jdbcClass=com.mysql.jdbc.Driver
#jdbcUrl=jdbc:mysql://localhost:3306/zkui
#jdbcUser=root
#jdbcPwd=manager
loginMessage=Please login using admin/manager or appconfig/appconfig.
#session timeout 5 mins/300 secs.
sessionTimeout=60000
#Default 5 seconds to keep short lived zk sessions. If you have large data then the read will take more than 30 seconds so increase this accordingly. 
#A bigger zkSessionTimeout means the connection will be held longer and resource consumption will be high.
zkSessionTimeout=50
#Block PWD exposure over rest call.
blockPwdOverRest=false
#ignore rest of the props below if https=false.
https=false
keystoreFile=/home/user/keystore.jks
keystorePwd=password
keystoreManagerPwd=password
# The default ACL to use for all creation of nodes. If left blank, then all nodes will be universally accessible
# Permissions are based on single character flags: c (Create), r (read), w (write), d (delete), a (admin), * (all)
# For example defaultAcl={"acls": [{"scheme":"ip", "id":"192.168.1.192", "perms":"*"}, {"scheme":"ip", id":"192.168.1.0/24", "perms":"r"}]
defaultAcl=
# Set X-Forwarded-For to true if zkui is behind a proxy
X-Forwarded-For=false

启动项目

修改 config.cfg 配置文件后,可以使用以下命令来更新 zkui.jar 中的配置文件。

bash
jar uvf zkui.jar config.cfg

启动

bash
java -jar zkui.jar

启动后的访问地址:http://localhost:9090/

导入 zookeeper 数据

项目的 \prize-event-project\document\zk\zkui.txt 文件是微服务配置项。启动 zkui 后,使用 admin/admin登陆,通过 zkui 导入到 zookeeper 作为配置

mybatis-generator 代码生成器

mybatis-generator 项目是 mybatis 的代码生成工程

项目依赖与插件

工程的 pom 的依赖与插件配置

xml
<dependencies>
    <dependency>
        <groupId>org.mybatis.generator</groupId>
        <artifactId>mybatis-generator-core</artifactId>
        <version>1.3.7</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-maven-plugin</artifactId>
            <version>1.3.7</version>
            <dependencies>
                <dependency>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                    <version>5.1.31</version>
                </dependency>
                <dependency>
                    <groupId>com.moon.prize</groupId>
                    <artifactId>coder</artifactId>
                    <version>0.0.1-SNAPSHOT</version>
                </dependency>
            </dependencies>
            <configuration>
                <overwrite>true</overwrite>
                <outputDirectory>${project.build.directory}</outputDirectory>
                <verbose>true</verbose>
                <!-- 指定哪些表需要生成代码,% 代表全部 -->
                <tableNames>%</tableNames>
            </configuration>
        </plugin>
    </plugins>
</build>

配置表映射关系

修改项目 resources 目录下的配置文件 generatorConfig.xml,配置数据库表与生成代码的映射关系。

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <context id="MysqlContext" targetRuntime="MyBatis3" defaultModelType="flat">
        <property name="beginningDelimiter" value="`"/>
        <property name="endingDelimiter" value="`"/>

        <!-- 指定生成的java文件的编码,没有直接生成到项目时中文可能会乱码 -->
        <property name="javaFileEncoding" value="UTF-8"/>

        <!-- 生成的pojo,将implements Serializable -->
        <plugin type="org.mybatis.generator.plugins.SerializablePlugin"></plugin>
        <!-- 这里的type里写的是你的实现类的类全路径 -->
        <!--<commentGenerator type="com.MyCommentGenerator">
            <property name="suppressDate" value="true"/>
        </commentGenerator>-->

        <commentGenerator>
            <property name="suppressDate" value="true"/>
        </commentGenerator>

        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://192.168.12.132/prize?tinyInt1isBit=false"
                        userId="root"
                        password="123456">
        </jdbcConnection>

        <javaTypeResolver type="com.JavaTypeResolverDefaultImpl">
        </javaTypeResolver>

        <!-- entity 包路径 -->
        <javaModelGenerator targetPackage="com.moon.prize.commons.db.entity" targetProject="MAVEN">
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>

        <!-- xml 路径-->
        <sqlMapGenerator targetPackage="com.moon.prize.commons.db.xml.mapper" targetProject="MAVEN"/>

        <!-- dao路径 -->
        <!--<javaClientGenerator type="XMLMAPPER" targetPackage="com.moon.dao1"  targetProject="MAVEN"/>-->
        <!--<javaClientGenerator type="ANNOTATEDMAPPER" targetPackage="com.moon.dao2"  targetProject="MAVEN"/>-->
        <javaClientGenerator type="MIXEDMAPPER" targetPackage="com.moon.prize.commons.db.mapper" targetProject="MAVEN"/>

        <table tableName="%">
            <generatedKey column="id" sqlStatement="Mysql" identity="true"/>
        </table>
    </context>
</generatorConfiguration>

tinyint 会映射为 bit,习惯上使用 Integer 更方便。所以定义一个 type 映射类 JavaTypeResolverDefaultImpl.java 并配置到上面的 generatorConfig.xml 中

java
public class JavaTypeResolverDefaultImpl implements JavaTypeResolver {
    ....
    public JavaTypeResolverDefaultImpl() {
        ....
        this.typeMap.put(-6, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("TINYINT", new FullyQualifiedJavaType(Integer.class.getName())));
        ....
    }
    ....
}

PageHelper 分页插件

项目依赖

在聚合项目中引入依赖并进行版本控制

xml
<!-- mybatis 分页插件 -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>4.1.6</version>
</dependency>

分页实体

在前台项目中的 commons 模块定义一个分页实体类:

java
@ApiModel("分页信息")
public class PageBean<T> {
    @ApiModelProperty(value = "当前页,1开始")
    private Integer currentPage = 1;
    @ApiModelProperty(value = "每页条数,默认10")
    private Integer pageSize = 10;
    @ApiModelProperty(value = "总条数")
    private Long totalNum;
    @ApiModelProperty(value = "是否有下一页")
    private Integer isMore;
    @ApiModelProperty(value = "总页数")
    private Integer totalPage;
    @ApiModelProperty(value = "开始索引")
    private Integer startIndex;
    @ApiModelProperty(value = "本页数据")
    private List<T> items;

    public PageBean() {
        super();
    }

    public PageBean(Integer currentPage, Integer pageSize, Long totalNum, List<T> data) {
        super();
        this.currentPage = currentPage;
        this.pageSize = pageSize;
        this.totalNum = totalNum;
        this.totalPage = Math.toIntExact((this.totalNum + this.pageSize - 1) / this.pageSize);
        this.startIndex = (this.currentPage - 1) * this.pageSize;
        this.isMore = this.currentPage >= this.totalPage ? 0 : 1;
        this.items = data;
    }
    ....
}

使用示例

在项目代码中使用:

java
// 在查询之前,调用startPage,设置页码及条数
PageHelper.startPage(curpage, limit);
List<ViewCardUserHit> all = hitMapper.selectByExample(example);

查询的 sql 会被自动拼接 limit

返回的数据体:

json
{
    "code": 1,
    "msg": "成功",
    "data": {
        "currentPage": 1,
        "pageSize": 2,
        "totalNum": 5,
        "isMore": 1,
        "totalPage": 3,
        "startIndex": 0,
        "items": [
            ....
        ]
    }
}

swagger2 文档生成框架

项目依赖

在前台工程 frontend 聚合工程中定义并版本控制

xml
<!-- swagger2增强,官方ui太low , 访问地址: /doc.html -->
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>swagger-bootstrap-ui</artifactId>
    <version>1.8.8</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>

框架配置

在前台工程中的 api 模块创建 swagger2 的配置类(暂时只是api模块使用

java
@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Value("${spring.application.name}")
    private String appName;

    @Bean
    public Docket docket() {
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
                .select()
                // 当前包路径
                .apis(RequestHandlerSelectors.basePackage("com.moon.prize.api.action"))
                .paths(PathSelectors.any()).build();

    }

    // 构建api文档的详细信息函数
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                // 页面标题
                .title("抽奖系统前端" + appName)
                // 创建人
                .contact(new Contact("Moon", null, "moonkira@moon.com"))
                // 版本号
                .version("1.0")
                // 描述
                .description("提供给前端页面调用的相关接口")
                .build();
    }
}

使用效果

I know where dry desert ends, green grass grows · MooN