Repository: Grootzz/dis-seckill Branch: master Commit: 5864da23307f Files: 115 Total size: 517.8 KB Directory structure: gitextract_4juf6qor/ ├── .gitignore ├── README.md ├── dis-seckill-cache/ │ ├── pom.xml │ └── src/ │ └── main/ │ ├── java/ │ │ └── com/ │ │ └── seckill/ │ │ └── dis/ │ │ └── cache/ │ │ ├── CacheApplication.java │ │ ├── config/ │ │ │ ├── RedisConfig.java │ │ │ └── RedisPoolFactory.java │ │ └── service/ │ │ ├── RedisLockImpl.java │ │ └── RedisServiceImpl.java │ └── resources/ │ └── application.properties ├── dis-seckill-common/ │ ├── pom.xml │ ├── schema/ │ │ └── seckill.sql │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── seckill/ │ └── dis/ │ └── common/ │ ├── api/ │ │ ├── cache/ │ │ │ ├── DLockApi.java │ │ │ ├── RedisServiceApi.java │ │ │ └── vo/ │ │ │ ├── AccessKeyPrefix.java │ │ │ ├── BaseKeyPrefix.java │ │ │ ├── GoodsKeyPrefix.java │ │ │ ├── KeyPrefix.java │ │ │ ├── OrderKeyPrefix.java │ │ │ ├── SkKeyPrefix.java │ │ │ ├── SkUserKeyPrefix.java │ │ │ └── UserKey.java │ │ ├── goods/ │ │ │ ├── GoodsServiceApi.java │ │ │ └── vo/ │ │ │ ├── GoodsDetailVo.java │ │ │ └── GoodsVo.java │ │ ├── mq/ │ │ │ ├── MqProviderApi.java │ │ │ └── vo/ │ │ │ └── SkMessage.java │ │ ├── order/ │ │ │ ├── OrderServiceApi.java │ │ │ └── vo/ │ │ │ └── OrderDetailVo.java │ │ ├── seckill/ │ │ │ ├── SeckillServiceApi.java │ │ │ └── vo/ │ │ │ └── VerifyCodeVo.java │ │ └── user/ │ │ ├── UserServiceApi.java │ │ └── vo/ │ │ ├── LoginVo.java │ │ ├── RegisterVo.java │ │ ├── UserInfoVo.java │ │ └── UserVo.java │ ├── domain/ │ │ ├── Goods.java │ │ ├── OrderInfo.java │ │ ├── SeckillGoods.java │ │ ├── SeckillOrder.java │ │ └── SeckillUser.java │ ├── exception/ │ │ ├── GlobalException.java │ │ └── GlobalExceptionHandler.java │ ├── result/ │ │ ├── CodeMsg.java │ │ └── Result.java │ ├── util/ │ │ ├── DBUtil.java │ │ ├── JsonUtil.java │ │ ├── MD5Util.java │ │ ├── UUIDUtil.java │ │ ├── ValidatorUtil.java │ │ └── VerifyCodeUtil.java │ └── validator/ │ ├── IsMobile.java │ └── IsMobileValidator.java ├── dis-seckill-gateway/ │ ├── pom.xml │ └── src/ │ └── main/ │ ├── java/ │ │ └── com/ │ │ └── seckill/ │ │ └── dis/ │ │ └── gateway/ │ │ ├── DisSeckillServletInitializer.java │ │ ├── GatewayApplication.java │ │ ├── config/ │ │ │ ├── WebConfig.java │ │ │ ├── access/ │ │ │ │ ├── AccessInterceptor.java │ │ │ │ ├── AccessLimit.java │ │ │ │ └── UserContext.java │ │ │ └── resolver/ │ │ │ └── UserArgumentResolver.java │ │ ├── exception/ │ │ │ ├── GlobalException.java │ │ │ └── GlobalExceptionHandler.java │ │ ├── goods/ │ │ │ └── GoodsController.java │ │ ├── order/ │ │ │ └── OrderController.java │ │ ├── seckill/ │ │ │ └── SeckillController.java │ │ └── user/ │ │ └── UserController.java │ └── resources/ │ ├── application.properties │ ├── static/ │ │ ├── bootstrap/ │ │ │ ├── css/ │ │ │ │ ├── bootstrap-theme.css │ │ │ │ └── bootstrap.css │ │ │ └── js/ │ │ │ ├── bootstrap.js │ │ │ └── npm.js │ │ ├── goods_detail.htm │ │ ├── js/ │ │ │ └── common.js │ │ ├── layer/ │ │ │ ├── layer.js │ │ │ ├── mobile/ │ │ │ │ ├── layer.js │ │ │ │ └── need/ │ │ │ │ └── layer.css │ │ │ └── skin/ │ │ │ └── default/ │ │ │ └── layer.css │ │ └── order_detail.htm │ └── templates/ │ ├── goods_detail.html │ ├── goods_list.html │ ├── login.html │ ├── order_detail.html │ └── register.html ├── dis-seckill-goods/ │ ├── pom.xml │ └── src/ │ └── main/ │ ├── java/ │ │ └── com/ │ │ └── seckill/ │ │ └── dis/ │ │ └── goods/ │ │ ├── GoodsApplication.java │ │ ├── persistence/ │ │ │ └── GoodsMapper.java │ │ └── service/ │ │ ├── GoodsServiceImpl.java │ │ └── SeckillServiceImpl.java │ └── resources/ │ └── application.properties ├── dis-seckill-mq/ │ ├── pom.xml │ └── src/ │ └── main/ │ ├── java/ │ │ └── com/ │ │ └── seckill/ │ │ └── dis/ │ │ └── mq/ │ │ ├── MqApplication.java │ │ ├── config/ │ │ │ └── MQConfig.java │ │ ├── receiver/ │ │ │ └── MqConsumer.java │ │ └── service/ │ │ └── MqProviderImpl.java │ └── resources/ │ └── application.properties ├── dis-seckill-order/ │ ├── pom.xml │ └── src/ │ └── main/ │ ├── java/ │ │ └── com/ │ │ └── seckill/ │ │ └── dis/ │ │ └── order/ │ │ ├── OrderApplication.java │ │ ├── persistence/ │ │ │ └── OrderMapper.java │ │ └── service/ │ │ └── OrderServiceImpl.java │ └── resources/ │ └── application.properties ├── dis-seckill-user/ │ ├── pom.xml │ └── src/ │ └── main/ │ ├── java/ │ │ └── com/ │ │ └── seckill/ │ │ └── dis/ │ │ └── user/ │ │ ├── UserApplication.java │ │ ├── domain/ │ │ │ └── SeckillUser.java │ │ ├── persistence/ │ │ │ ├── SeckillUserMapper.java │ │ │ └── SeckillUserMapper.xml │ │ ├── service/ │ │ │ └── UserServiceImpl.java │ │ └── util/ │ │ └── UserUtil.java │ └── resources/ │ └── application.properties ├── doc/ │ ├── HandlerInterceptor的使用.md │ ├── README.md │ ├── Redis中存储的数据.md │ ├── 使用分布式锁解决恶意用户重复注册问题.md │ ├── 前后端交互接口定义.md │ └── 前后端交互接口逻辑实现.md └── pom.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Created by .ignore support plugin (hsz.mobi) ### Example user template template ### Example user template # IntelliJ project files .idea *.iml out gen### Java template # Compiled class file *.class # Log file *.log # BlueJ files *.ctxt # Mobile Tools for Java (J2ME) .mtj.tmp/ # Package Files # *.jar *.war *.nar *.ear *.zip *.tar.gz *.rar # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* .idea/ dis-seckill-common/dis-seckill-common.iml dis-seckill-common/src/test/ dis-seckill-common/target/ dis-seckill-gateway/dis-seckill-gateway.iml dis-seckill-gateway/src/test/ dis-seckill-gateway/target/ dis-seckill-goods/dis-seckill-goods.iml dis-seckill-goods/src/test/ dis-seckill-goods/target/ dis-seckill-order/dis-seckill-order.iml dis-seckill-order/src/test/ dis-seckill-order/target/ dis-seckill-user/dis-seckill-user.iml dis-seckill-user/src/test/ dis-seckill-user/target/ dis-seckill.iml ================================================ FILE: README.md ================================================ # 分布式高并发商品秒杀系统设计 - [介绍](#介绍) - [快速启动](#快速启动) - [系统架构](#系统架构) - [模块介绍](#模块介绍) - [Q&A](#Q&A) - [TODO LIST](#TODO) - [参考](#参考) ## 介绍 本项目为另一个项目[seckill](https://github.com/Grootzz/seckill)的分布式改进版本,dis-seckill意为:distributed seckill,即分布式秒杀系统。 商品秒杀与其他业务最大的区别在于: - 低廉价格; - 大幅推广; - 瞬时售空; - 一般是定时上架; - 时间短、瞬时并发量高、网络的流量也会瞬间变大。 除了具有以上特点,秒杀商品还需要完成正常的电子商务逻辑,即:(1)查询商品;(2)创建订单;(3)扣减库存;(4)更新订单;(5)付款;(6)卖家发货。 本项目正是基于上述业务特点进行设计的,在项目中引入诸多优化手段,使系统可以从容应对秒杀场景下的业务处理。 另外,项目[seckill](https://github.com/Grootzz/seckill)为单体应用,在大并发情形下处理能力有限,所以本项目对其进行分布式改造,对职责进行划分,降低单体应用的业务耦合性。 ## 快速启动 - 构建工具 apache-maven-3.6.1 - 开发环境 JDK 1.8、Mysql 8.0.12、SpringBoot 2.1.5、zookeeper 3.4.10、dubbo 2.7.1、redis 5.0.5、rabbitmq 3.7.15 在安装之前,需要安装好上述构建工具和开发环境,推荐在linux下安装上述开发环境。 **第一步**;完成数据库的初始化,使用`./dis-seckill-common/schema/seckill.sql`初始化数据库。 **第二步**;如果安装了git,则可以采用下面的方式快速启动; ```properties git clone git@github.com:Grootzz/dis-seckill.git mvn clean package ``` 启动缓存服务: ```properties java -jar dis-seckill-cache/target/dis-seckill-cache-0.0.1-SNAPSHOT.jar ``` 启动用户服务: ```properties java -jar dis-seckill-user/target/dis-seckill-user-0.0.1-SNAPSHOT.jar ``` 启动订单服务: ```properties java -jar dis-seckill-order/target/dis-seckill-order-0.0.1-SNAPSHOT.jar ``` 启动商品服务: ```properties java -jar dis-seckill-goods/target/dis-seckill-goods-0.0.1-SNAPSHOT.jar ``` 启动消息队列服务: ```properties java -jar dis-seckill-mq/target/dis-seckill-mq-0.0.1-SNAPSHOT.jar ``` 启动网关服务: ```properties java -jar dis-seckill-gateway/target/dis-seckill-gateway-0.0.1-SNAPSHOT.jar ``` > 注:启动服务时最好按上面的顺序启动。 如果将项目导入IDE中进行构建,则分别按上面的顺序启动服务即可。 **第三步**;访问项目入口地址 初始用户手机号码:18342390420,密码:000000 ## 系统架构 ![系统架构](doc/assets/SYSTEM_ARCHITECTURE.png) - 注册中心使用zookeeper; - 缓存采用redis; - 消息队列采用RabbitMQ; - 用户请求全部交由Gateway模块处理; - Gateway模块使用RPC的方式调用其他模块提供的服务完成业务处理。 ## 模块介绍 - dis-seckill-common:通用模块 - dis-seckill-user:用户模块 - dis-seckill-goods:商品模块 - dis-seckill-order:订单模块 - dis-seckill-gateway:网关模块 - dis-seckill-cache:缓存模块 - dis-seckill-mq:消息队列模块 ## Q&A - [前后端交互接口定义](https://github.com/Grootzz/dis-seckill/blob/master/doc/前后端交互接口定义.md) - [前后端交互接口逻辑实现](https://github.com/Grootzz/dis-seckill/blob/master/doc/前后端交互接口逻辑实现.md) - [Redis中存储的数据](https://github.com/Grootzz/dis-seckill/blob/master/doc/Redis中存储的数据.md) - [使用分布式锁解决恶意用户重复注册问题](https://github.com/Grootzz/dis-seckill/blob/master/doc/使用分布式锁解决恶意用户重复注册问题.md) - ...... ## TODO - [ ] 引入JWT简化权限验证; - [x] 完成用户注册功能; - [x] 引入分布式锁保证更改密码接口用户注册接口的幂等性,防止用户恶意访问; - [ ] 服务模块横向扩展; - [ ] 服务调用的负载均衡与服务降级; - [ ] gateway模块横向扩展,降低单个应用的压力; - [ ] Nginx水平扩展; - [ ] 接口压测; - [ ] ...... ## 参考 - - - ================================================ FILE: dis-seckill-cache/pom.xml ================================================ dis-seckill com.seckill.dis 0.0.1-SNAPSHOT 4.0.0 dis-seckill-cache dis-seckill-cache 分布式缓存服务模块 org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import org.apache.dubbo dubbo-dependencies-bom ${dubbo.version} pom import org.apache.dubbo dubbo ${dubbo.version} org.springframework spring javax.servlet servlet-api log4j log4j com.seckill.dis dis-seckill-common 1.0.0 org.springframework.boot spring-boot-configuration-processor org.apache.dubbo dubbo-spring-boot-starter ${dubbo.version} org.apache.dubbo dubbo org.apache.dubbo dubbo-dependencies-zookeeper ${dubbo.version} org.slf4j slf4j-log4j12 pom redis.clients jedis org.springframework.boot spring-boot-maven-plugin ================================================ FILE: dis-seckill-cache/src/main/java/com/seckill/dis/cache/CacheApplication.java ================================================ package com.seckill.dis.cache; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 缓存服务 */ @SpringBootApplication public class CacheApplication { public static void main(String[] args) { SpringApplication.run(CacheApplication.class, args); } } ================================================ FILE: dis-seckill-cache/src/main/java/com/seckill/dis/cache/config/RedisConfig.java ================================================ package com.seckill.dis.cache.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "redis") public class RedisConfig { private String host; private int port; private int timeout;//秒 // private String password; private int poolMaxTotal; private int poolMaxIdle; private int poolMaxWait;//秒 public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public int getTimeout() { return timeout; } public void setTimeout(int timeout) { this.timeout = timeout; } public int getPoolMaxTotal() { return poolMaxTotal; } public void setPoolMaxTotal(int poolMaxTotal) { this.poolMaxTotal = poolMaxTotal; } public int getPoolMaxIdle() { return poolMaxIdle; } public void setPoolMaxIdle(int poolMaxIdle) { this.poolMaxIdle = poolMaxIdle; } public int getPoolMaxWait() { return poolMaxWait; } public void setPoolMaxWait(int poolMaxWait) { this.poolMaxWait = poolMaxWait; } } ================================================ FILE: dis-seckill-cache/src/main/java/com/seckill/dis/cache/config/RedisPoolFactory.java ================================================ package com.seckill.dis.cache.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; /** * jedis连接池 */ @Component public class RedisPoolFactory { @Autowired RedisConfig redisConfig; @Bean public JedisPool JedisPoolFactory() { // jedis连接池 JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxIdle(redisConfig.getPoolMaxIdle()); poolConfig.setMaxTotal(redisConfig.getPoolMaxTotal()); poolConfig.setMaxWaitMillis(redisConfig.getPoolMaxWait() * 1000); JedisPool pool = new JedisPool(poolConfig, redisConfig.getHost(), redisConfig.getPort(), redisConfig.getTimeout()); return pool; } } ================================================ FILE: dis-seckill-cache/src/main/java/com/seckill/dis/cache/service/RedisLockImpl.java ================================================ package com.seckill.dis.cache.service; import com.seckill.dis.common.api.cache.DLockApi; import org.apache.dubbo.config.annotation.Service; import org.springframework.beans.factory.annotation.Autowired; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import java.util.Collections; /** * Redis分布式锁 * * @author noodle */ @Service(interfaceClass = DLockApi.class) public class RedisLockImpl implements DLockApi { /** * 通过连接池对象可以获得对redis的连接 */ @Autowired private JedisPool jedisPool; private final String LOCK_SUCCESS = "OK"; private final String SET_IF_NOT_EXIST = "NX"; private final String SET_WITH_EXPIRE_TIME = "PX"; private final Long RELEASE_SUCCESS = 1L; /** * 获取锁 * * @param lockKey 锁 * @param uniqueValue 能够唯一标识请求的值,以此保证锁的加解锁是同一个客户端 * @param expireTime 过期时间, 单位:milliseconds * @return */ public boolean lock(String lockKey, String uniqueValue, int expireTime) { Jedis jedis = null; try { jedis = jedisPool.getResource(); // 获取锁 String result = jedis.set(lockKey, uniqueValue, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } finally { if (jedis != null) jedis.close(); } } /** * 使用Lua脚本保证解锁的原子性 * * @param lockKey 锁 * @param uniqueValue 能够唯一标识请求的值,以此保证锁的加解锁是同一个客户端 * @return */ public boolean unlock(String lockKey, String uniqueValue) { Jedis jedis = null; try { jedis = jedisPool.getResource(); // 使用Lua脚本保证操作的原子性 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " + "return redis.call('del', KEYS[1]) " + "else return 0 " + "end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(uniqueValue)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; } finally { if (jedis != null) jedis.close(); } } } ================================================ FILE: dis-seckill-cache/src/main/java/com/seckill/dis/cache/service/RedisServiceImpl.java ================================================ package com.seckill.dis.cache.service; import com.alibaba.fastjson.JSON; import com.seckill.dis.common.api.cache.RedisServiceApi; import com.seckill.dis.common.api.cache.vo.KeyPrefix; import org.apache.dubbo.config.annotation.Service; import org.springframework.beans.factory.annotation.Autowired; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; /** * redis服务实现 * * @author noodle */ @Service(interfaceClass = RedisServiceApi.class) public class RedisServiceImpl implements RedisServiceApi { /** * 通过连接池对象可以获得对redis的连接 */ @Autowired JedisPool jedisPool; @Override public T get(KeyPrefix prefix, String key, Class clazz) { Jedis jedis = null;// redis连接 try { jedis = jedisPool.getResource(); // 生成真正的存储于redis中的key String realKey = prefix.getPrefix() + key; // 通过key获取存储于redis中的对象(这个对象是以json格式存储的,所以是字符串) String strValue = jedis.get(realKey); // 将json字符串转换为对应的对象 T objValue = stringToBean(strValue, clazz); return objValue; } finally { // 归还redis连接到连接池 returnToPool(jedis); } } @Override public boolean set(KeyPrefix prefix, String key, T value) { Jedis jedis = null; try { jedis = jedisPool.getResource(); // 将对象转换为json字符串 String strValue = beanToString(value); if (strValue == null || strValue.length() <= 0) return false; // 生成实际存储于redis中的key String realKey = prefix.getPrefix() + key; // 获取key的过期时间 int expires = prefix.expireSeconds(); if (expires <= 0) { // 设置key的过期时间为redis默认值(由redis的缓存策略控制) jedis.set(realKey, strValue); } else { // 设置key的过期时间 jedis.setex(realKey, expires, strValue); } return true; } finally { returnToPool(jedis); } } @Override public boolean exists(KeyPrefix keyPrefix, String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); String realKey = keyPrefix.getPrefix() + key; return jedis.exists(realKey); } finally { returnToPool(jedis); } } @Override public long incr(KeyPrefix keyPrefix, String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); String realKey = keyPrefix.getPrefix() + key; return jedis.incr(realKey); } finally { returnToPool(jedis); } } @Override public long decr(KeyPrefix keyPrefix, String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); String realKey = keyPrefix.getPrefix() + key; return jedis.decr(realKey); } finally { returnToPool(jedis); } } @Override public boolean delete(KeyPrefix prefix, String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); String realKey = prefix.getPrefix() + key; Long del = jedis.del(realKey); return del > 0; } finally { returnToPool(jedis); } } /** * 将对象转换为对应的json字符串 * * @param value 对象 * @param 对象的类型 * @return 对象对应的json字符串 */ public static String beanToString(T value) { if (value == null) return null; Class clazz = value.getClass(); /*首先对基本类型处理*/ if (clazz == int.class || clazz == Integer.class) return "" + value; else if (clazz == long.class || clazz == Long.class) return "" + value; else if (clazz == String.class) return (String) value; /*然后对Object类型的对象处理*/ else return JSON.toJSONString(value); } /** * 根据传入的class参数,将json字符串转换为对应类型的对象 * * @param strValue json字符串 * @param clazz 类型 * @param 类型参数 * @return json字符串对应的对象 */ public static T stringToBean(String strValue, Class clazz) { if ((strValue == null) || (strValue.length() <= 0) || (clazz == null)) return null; // int or Integer if ((clazz == int.class) || (clazz == Integer.class)) return (T) Integer.valueOf(strValue); // long or Long else if ((clazz == long.class) || (clazz == Long.class)) return (T) Long.valueOf(strValue); // String else if (clazz == String.class) return (T) strValue; // 对象类型 else return JSON.toJavaObject(JSON.parseObject(strValue), clazz); } /** * 将redis连接对象归还到redis连接池 * * @param jedis */ private void returnToPool(Jedis jedis) { if (jedis != null) jedis.close(); } } ================================================ FILE: dis-seckill-cache/src/main/resources/application.properties ================================================ # -------------------------------- # spring #--------------------------------- spring.application.name=dis-seckill-cache # -------------------------------- # web #--------------------------------- server.port=8085 #--------------------------------- # Dubbo Application #--------------------------------- # Base packages to scan Dubbo Component: @org.apache.dubbo.config.annotation.Service dubbo.scan.base-packages=com.seckill.dis.cache.service # The default value of dubbo.application.name is ${spring.application.name} dubbo.application.name=${spring.application.name} # Dubbo Protocol dubbo.protocol.name=dubbo dubbo.protocol.port=12348 dubbo.registry.check=true ## Dubbo Registry embedded.zookeeper.port=2181 dubbo.registry.address=zookeeper://127.0.0.1:${embedded.zookeeper.port} #--------------------------------- # redis #--------------------------------- #redis.host=192.168.216.128 redis.host=127.0.0.1 redis.port=6379 redis.timeout=1000 # redis.password=123456 redis.poolMaxTotal=1000 redis.poolMaxIdle=500 redis.poolMaxWait=500 #--------------------------------- # ־ #--------------------------------- logging.level.root=info logging.level.com.seckill.dis=debug logging.path=logs/ logging.file=dis-seckill.log ================================================ FILE: dis-seckill-common/pom.xml ================================================ dis-seckill com.seckill.dis 0.0.1-SNAPSHOT 4.0.0 dis-seckill-common ${top-version} jar dis-seckill-common 通用模块 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-web com.alibaba fastjson org.apache.dubbo dubbo-spring-boot-starter ${dubbo.version} org.apache.dubbo dubbo org.apache.dubbo dubbo-dependencies-zookeeper ${dubbo.version} org.slf4j slf4j-log4j12 pom org.apache.commons commons-lang3 commons-codec commons-codec junit junit ================================================ FILE: dis-seckill-common/schema/seckill.sql ================================================ # ------------------------------------------------------------------------ # # 创建秒杀场景下的表, 包含以下几个表: # # 1. 秒杀用户表 seckill_user # 2. 商品表 goods # 3. 参与秒杀的商品表 seckill_goods # 4. 秒杀订单表 seckill_order # 5. 订单表 order_info # # ------------------------------------------------------------------------ CREATE DATABASE seckill; USE seckill; # ************************************************************************ # 秒杀用户表 DROP TABLE IF EXISTS `seckill_user`; CREATE TABLE `seckill_user` ( UUID INT AUTO_INCREMENT COMMENT '主键编号,用户id', phone BIGINT(20) NOT NULL COMMENT '手机号码', nickname VARCHAR(255) NOT NULL COMMENT '昵称', password VARCHAR(32) DEFAULT NULL COMMENT 'MD5(MD5(password明文+固定salt) + salt)', salt VARCHAR(10) DEFAULT NULL COMMENT '盐值', head VARCHAR(128) DEFAULT NULL COMMENT '头像,云存储的id', register_date DATETIME DEFAULT NULL COMMENT '注册时间', last_login_date DATETIME DEFAULT NULL COMMENT '上次登录时间', login_count INT(11) DEFAULT 0 COMMENT '登录次数', PRIMARY KEY (UUID) ) ENGINE = InnoDB AUTO_INCREMENT = 2 DEFAULT CHARSET = utf8mb4 COMMENT '秒杀用户表'; # 插入一条记录(未经过MD5的密码为000000, 两次MD5后的密码为记录中的密码,两次MD5的salt一样) INSERT INTO seckill_user (phone, nickname, password, salt) VALUES (18342390420, 'Noodle', '5e7b3a9754c2777f96174d4ccb980d23', '1a2b3c4d'); # ************************************************************************ # 创建商品表 DROP TABLE IF EXISTS `goods`; CREATE TABLE `goods` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID', `goods_name` VARCHAR(16) DEFAULT NULL COMMENT '商品名称', `goods_title` VARCHAR(64) DEFAULT NULL COMMENT '商品标题', `goods_img` VARCHAR(64) DEFAULT NULL COMMENT '商品的图片', `goods_detail` LONGTEXT DEFAULT NULL COMMENT '商品的详情介绍', `goods_price` DECIMAL(10, 2) DEFAULT '0.00' COMMENT '商品单价', `goods_stock` INT(11) DEFAULT '0' COMMENT '商品库存,-1表示没有限制', PRIMARY KEY (`id`) ) ENGINE = InnoDB AUTO_INCREMENT = 3 DEFAULT CHARSET = utf8mb4; # 在商品表中添加4条记录 INSERT INTO `goods` VALUES (1, 'iphoneX', 'Apple iPhone X (A1865) 64GB 银色 移动联通电信4G手机', '/img/iphonex.png','Apple iPhone X (A1865) 64GB 银色 移动联通电信4G手机', 8765.00, 10000), (2, '华为Meta9', '华为 Mate 9 4GB+32GB版 月光银 移动联通电信4G手机 双卡双待', '/img/meta10.png','华为 Mate 9 4GB+32GB版 月光银 移动联通电信4G手机 双卡双待', 3212.00, -1), (3, 'iphone8', 'Apple iPhone 8 (A1865) 64GB 银色 移动联通电信4G手机', '/img/iphone8.png','Apple iPhone 8 (A1865) 64GB 银色 移动联通电信4G手机', 5589.00, 10000), (4, '小米6', '小米6 4GB+32GB版 月光银 移动联通电信4G手机 双卡双待', '/img/mi6.png', '小米6 4GB+32GB版 月光银 移动联通电信4G手机 双卡双待', 3212.00, 10000); # ************************************************************************ # 秒杀商品表 DROP TABLE IF EXISTS `seckill_goods`; CREATE TABLE `seckill_goods` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '秒杀的商品表', `goods_id` BIGINT(20) DEFAULT NULL COMMENT '商品Id', `seckill_price` DECIMAL(10, 2) DEFAULT '0.00' COMMENT '秒杀价', `stock_count` INT(11) DEFAULT NULL COMMENT '库存数量', `start_date` DATETIME DEFAULT NULL COMMENT '秒杀开始时间', `end_date` DATETIME DEFAULT NULL COMMENT '秒杀结束时间', PRIMARY KEY (`id`) ) ENGINE = InnoDB AUTO_INCREMENT = 5 DEFAULT CHARSET = utf8mb4; # 插入两条记录 INSERT INTO `seckill_goods` VALUES (1, 1, 0.01, 9, '2017-12-04 21:51:23', '2017-12-31 21:51:27'), (2, 2, 0.01, 9, '2017-12-04 21:40:14', '2017-12-31 14:00:24'), (3, 3, 0.01, 9, '2017-12-04 21:40:14', '2017-12-31 14:00:24'), (4, 4, 0.01, 9, '2017-12-04 21:40:14', '2017-12-31 14:00:24'); # ************************************************************************ # 秒杀订单表 DROP TABLE IF EXISTS `seckill_order`; CREATE TABLE `seckill_order` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT, `user_id` BIGINT(20) DEFAULT NULL COMMENT '用户ID', `order_id` BIGINT(20) DEFAULT NULL COMMENT '订单ID', `goods_id` BIGINT(20) DEFAULT NULL COMMENT '商品ID', PRIMARY KEY (`id`), UNIQUE KEY `u_uid_gid` (`user_id`, `goods_id`) USING BTREE COMMENT '定义联合索引' ) ENGINE = InnoDB AUTO_INCREMENT = 1551 DEFAULT CHARSET = utf8mb4; # ************************************************************************ # 订单信息表 DROP TABLE IF EXISTS `order_info`; CREATE TABLE `order_info` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT, `user_id` BIGINT(20) DEFAULT NULL COMMENT '用户ID', `goods_id` BIGINT(20) DEFAULT NULL COMMENT '商品ID', `delivery_addr_id` BIGINT(20) DEFAULT NULL COMMENT '收获地址ID', `goods_name` VARCHAR(16) DEFAULT NULL COMMENT '冗余过来的商品名称', `goods_count` INT(11) DEFAULT '0' COMMENT '商品数量', `goods_price` DECIMAL(10, 2) DEFAULT '0.00' COMMENT '商品单价', `order_channel` TINYINT(4) DEFAULT '0' COMMENT '1-pc,2-android,3-ios', `status` TINYINT(4) DEFAULT '0' COMMENT '订单状态,0新建未支付,1已支付,2已发货,3已收货,4已退款,5已完成', `create_date` DATETIME DEFAULT NULL COMMENT '订单的创建时间', `pay_date` DATETIME DEFAULT NULL COMMENT '支付时间', PRIMARY KEY (`id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1565 DEFAULT CHARSET = utf8mb4; ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/cache/DLockApi.java ================================================ package com.seckill.dis.common.api.cache; /** * 分布式锁接口 * * @author noodle */ public interface DLockApi { /** * 获取锁 * * @param lockKey 锁 * @param uniqueValue 能够唯一标识请求的值,以此保证锁的加解锁是同一个客户端 * @param expireTime 过期时间, 单位:milliseconds * @return */ boolean lock(String lockKey, String uniqueValue, int expireTime); /** * 使用Lua脚本保证解锁的原子性 * * @param lockKey 锁 * @param uniqueValue 能够唯一标识请求的值,以此保证锁的加解锁是同一个客户端 * @return */ boolean unlock(String lockKey, String uniqueValue); } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/cache/RedisServiceApi.java ================================================ package com.seckill.dis.common.api.cache; import com.seckill.dis.common.api.cache.vo.KeyPrefix; /** * redis 服务接口 * * @author noodle */ public interface RedisServiceApi { /** * redis 的get操作,通过key获取存储在redis中的对象 * * @param prefix key的前缀 * @param key 业务层传入的key * @param clazz 存储在redis中的对象类型(redis中是以字符串存储的) * @param 指定对象对应的类型 * @return 存储于redis中的对象 */ T get(KeyPrefix prefix, String key, Class clazz); /** * redis的set操作 * * @param prefix 键的前缀 * @param key 键 * @param value 值 * @return 操作成功为true,否则为true */ boolean set(KeyPrefix prefix, String key, T value); /** * 判断key是否存在于redis中 * * @param keyPrefix key的前缀 * @param key * @return */ boolean exists(KeyPrefix keyPrefix, String key); /** * 自增 * * @param keyPrefix * @param key * @return */ long incr(KeyPrefix keyPrefix, String key); /** * 自减 * * @param keyPrefix * @param key * @return */ long decr(KeyPrefix keyPrefix, String key); /** * 删除缓存中的用户数据 * * @param prefix * @param key * @return */ boolean delete(KeyPrefix prefix, String key); } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/cache/vo/AccessKeyPrefix.java ================================================ package com.seckill.dis.common.api.cache.vo; import java.io.Serializable; /** * 访问次数的key前缀 * * @author noodle */ public class AccessKeyPrefix extends BaseKeyPrefix implements Serializable{ public AccessKeyPrefix(String prefix) { super(prefix); } public AccessKeyPrefix(int expireSeconds, String prefix) { super(expireSeconds, prefix); } // 可灵活设置过期时间 public static AccessKeyPrefix withExpire(int expireSeconds) { return new AccessKeyPrefix(expireSeconds, "access"); } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/cache/vo/BaseKeyPrefix.java ================================================ package com.seckill.dis.common.api.cache.vo; /** * 模板方法的基本类 * * @author noodle */ public abstract class BaseKeyPrefix implements KeyPrefix { /** * 过期时间 */ int expireSeconds; /** * 前缀 */ String prefix; /** * 默认过期时间为0,即不过期,过期时间受到redis的缓存策略影响 * * @param prefix 前缀 */ public BaseKeyPrefix(String prefix) { this(0, prefix); } public BaseKeyPrefix(int expireSeconds, String prefix) { this.expireSeconds = expireSeconds; this.prefix = prefix; } /** * 默认0代表永不过期 * * @return */ @Override public int expireSeconds() { return expireSeconds; } /** * 前缀为模板类的实现类的类名 * * @return */ @Override public String getPrefix() { String simpleName = getClass().getSimpleName(); return simpleName + ":" + prefix; } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/cache/vo/GoodsKeyPrefix.java ================================================ package com.seckill.dis.common.api.cache.vo; import java.io.Serializable; /** * redis中,用于商品信息的key * * @author noodle */ public class GoodsKeyPrefix extends BaseKeyPrefix implements Serializable { public GoodsKeyPrefix(int expireSeconds, String prefix) { super(expireSeconds, prefix); } // 缓存在redis中的商品列表页面的key的前缀 public static GoodsKeyPrefix goodsListKeyPrefix = new GoodsKeyPrefix(60, "goodsList"); public static GoodsKeyPrefix GOODS_LIST_HTML = new GoodsKeyPrefix(60, "goodsListHtml"); // 缓存在redis中的商品详情页面的key的前缀 public static GoodsKeyPrefix goodsDetailKeyPrefix = new GoodsKeyPrefix(60, "goodsDetail"); // 缓存在redis中的商品库存的前缀(缓存过期时间为永久) public static GoodsKeyPrefix seckillGoodsStockPrefix = new GoodsKeyPrefix(0, "goodsStock"); /** * 缓存在redis中的商品库存的前缀(缓存过期时间为永久) */ public static GoodsKeyPrefix GOODS_STOCK = new GoodsKeyPrefix(0, "goodsStock"); } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/cache/vo/KeyPrefix.java ================================================ package com.seckill.dis.common.api.cache.vo; /** * redis 键的前缀 * 之所以在key前面设置一个前缀,是因为如果出现设置相同的key情形,可以通过前缀加以区分 * * @author noodle */ public interface KeyPrefix { /** * key的过期时间 * * @return 过期时间 */ int expireSeconds(); /** * key的前缀 * * @return 前缀 */ String getPrefix(); } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/cache/vo/OrderKeyPrefix.java ================================================ package com.seckill.dis.common.api.cache.vo; import java.io.Serializable; /** * 存储订单的key前缀 * * @author noodle */ public class OrderKeyPrefix extends BaseKeyPrefix implements Serializable { public OrderKeyPrefix(int expireSeconds, String prefix) { super(expireSeconds, prefix); } public OrderKeyPrefix(String prefix) { super(prefix); } /** * 秒杀订单信息的前缀 */ public static OrderKeyPrefix getSeckillOrderByUidGid = new OrderKeyPrefix("getSeckillOrderByUidGid"); public static OrderKeyPrefix SK_ORDER = new OrderKeyPrefix("SK_ORDER"); } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/cache/vo/SkKeyPrefix.java ================================================ package com.seckill.dis.common.api.cache.vo; import java.io.Serializable; /** * 判断秒杀状态的key前缀 */ public class SkKeyPrefix extends BaseKeyPrefix implements Serializable { public SkKeyPrefix(String prefix) { super(prefix); } public SkKeyPrefix(int expireSeconds, String prefix) { super(expireSeconds, prefix); } public static SkKeyPrefix isGoodsOver = new SkKeyPrefix("isGoodsOver"); /** * 库存为0的商品的前缀 */ public static SkKeyPrefix GOODS_SK_OVER = new SkKeyPrefix("goodsSkOver"); /** * 秒杀接口随机地址 */ public static SkKeyPrefix skPath = new SkKeyPrefix(60, "skPath"); public static SkKeyPrefix SK_PATH = new SkKeyPrefix(60, "skPath"); // 验证码5分钟有效 public static SkKeyPrefix skVerifyCode = new SkKeyPrefix(300, "skVerifyCode"); /** * 验证码5分钟有效 */ public static SkKeyPrefix VERIFY_RESULT = new SkKeyPrefix(300, "verifyResult"); } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/cache/vo/SkUserKeyPrefix.java ================================================ package com.seckill.dis.common.api.cache.vo; import java.io.Serializable; /** * 秒杀用户信息的key前缀 */ public class SkUserKeyPrefix extends BaseKeyPrefix implements Serializable { public static final int TOKEN_EXPIRE = 30*60;// 缓存有效时间为30min public SkUserKeyPrefix(int expireSeconds, String prefix) { super(expireSeconds, prefix); } public static SkUserKeyPrefix token = new SkUserKeyPrefix(TOKEN_EXPIRE, "token"); /** * 用户cookie */ public static SkUserKeyPrefix TOKEN = new SkUserKeyPrefix(TOKEN_EXPIRE, "token"); /** * 用于存储用户对象到redis的key前缀 */ public static SkUserKeyPrefix getSeckillUserById = new SkUserKeyPrefix(0, "id"); public static SkUserKeyPrefix SK_USER_PHONE = new SkUserKeyPrefix(0, "id"); } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/cache/vo/UserKey.java ================================================ package com.seckill.dis.common.api.cache.vo; import java.io.Serializable; /** * redis中,用于管理用户表的key */ public class UserKey extends BaseKeyPrefix implements Serializable { public UserKey(String prefix) { super(prefix); } public static UserKey getById = new UserKey("id"); public static UserKey getByName = new UserKey("name"); } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/goods/GoodsServiceApi.java ================================================ package com.seckill.dis.common.api.goods; import com.seckill.dis.common.api.goods.vo.GoodsVo; import java.util.List; /** * 商品服务接口 * * @author noodle */ public interface GoodsServiceApi { /** * 获取商品列表 * * @return */ List listGoodsVo(); /** * 通过商品的id查出商品的所有信息(包含该商品的秒杀信息) * * @param goodsId * @return */ GoodsVo getGoodsVoByGoodsId(long goodsId); /** * 通过商品的id查出商品的所有信息(包含该商品的秒杀信息) * * @param goodsId * @return */ GoodsVo getGoodsVoByGoodsId(Long goodsId); /** * order表减库存 * * @param goods */ boolean reduceStock(GoodsVo goods); } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/goods/vo/GoodsDetailVo.java ================================================ package com.seckill.dis.common.api.goods.vo; import com.seckill.dis.common.api.user.vo.UserVo; import java.io.Serializable; /** * 商品详情 * 用于将数据传递给客户端 * * @author noodle */ public class GoodsDetailVo implements Serializable { private int seckillStatus = 0; private int remainSeconds = 0; private GoodsVo goods; private UserVo user; @Override public String toString() { return "GoodsDetailVo{" + "seckillStatus=" + seckillStatus + ", remainSeconds=" + remainSeconds + ", goods=" + goods + ", user=" + user + '}'; } public int getSeckillStatus() { return seckillStatus; } public void setSeckillStatus(int seckillStatus) { this.seckillStatus = seckillStatus; } public int getRemainSeconds() { return remainSeconds; } public void setRemainSeconds(int remainSeconds) { this.remainSeconds = remainSeconds; } public GoodsVo getGoods() { return goods; } public void setGoods(GoodsVo goods) { this.goods = goods; } public UserVo getUser() { return user; } public void setUser(UserVo user) { this.user = user; } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/goods/vo/GoodsVo.java ================================================ package com.seckill.dis.common.api.goods.vo; import com.seckill.dis.common.domain.Goods; import java.io.Serializable; import java.util.Date; /** * 商品信息(并且包含商品的秒杀信息) * 商品信息和商品的秒杀信息是存储在两个表中的(goods 和 seckill_goods) * 继承 Goods 便具有了 goods 表的信息,再额外添加上 seckill_goods 的信息即可 * * @author noodle */ public class GoodsVo extends Goods implements Serializable { /*只包含了部分 seckill_goods 表的信息*/ private Double seckillPrice; private Integer stockCount; private Date startDate; private Date endDate; public Double getSeckillPrice() { return seckillPrice; } public void setSeckillPrice(Double seckillPrice) { this.seckillPrice = seckillPrice; } public Integer getStockCount() { return stockCount; } public void setStockCount(Integer stockCount) { this.stockCount = stockCount; } public Date getStartDate() { return startDate; } public void setStartDate(Date startDate) { this.startDate = startDate; } public Date getEndDate() { return endDate; } public void setEndDate(Date endDate) { this.endDate = endDate; } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/mq/MqProviderApi.java ================================================ package com.seckill.dis.common.api.mq; import com.seckill.dis.common.api.mq.vo.SkMessage; /** * 消息队列服务 * * @author noodle */ public interface MqProviderApi { /** * 将用户秒杀信息投递到MQ中(使用direct模式的exchange) * * @param message */ void sendSkMessage(SkMessage message); } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/mq/vo/SkMessage.java ================================================ package com.seckill.dis.common.api.mq.vo; import com.seckill.dis.common.api.user.vo.UserVo; import java.io.Serializable; /** * 在MQ中传递的秒杀信息 * 包含参与秒杀的用户和商品的id * * @author noodle */ public class SkMessage implements Serializable{ private UserVo user; private long goodsId; public UserVo getUser() { return user; } public void setUser(UserVo user) { this.user = user; } public long getGoodsId() { return goodsId; } public void setGoodsId(long goodsId) { this.goodsId = goodsId; } @Override public String toString() { return "SeckillMessage{" + "user=" + user + ", goodsId=" + goodsId + '}'; } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/order/OrderServiceApi.java ================================================ package com.seckill.dis.common.api.order; import com.seckill.dis.common.api.goods.vo.GoodsVo; import com.seckill.dis.common.api.user.vo.UserVo; import com.seckill.dis.common.domain.OrderInfo; import com.seckill.dis.common.domain.SeckillOrder; /** * 订单服务接口 * * @author noodle */ public interface OrderServiceApi { /** * 通过订单id获取订单 * * @param orderId * @return */ OrderInfo getOrderById(long orderId); /** * 通过用户id与商品id从订单列表中获取订单信息,这个地方用到了唯一索引(unique index!!!!!) * * @param userId * @param goodsId * @return 秒杀订单信息 */ SeckillOrder getSeckillOrderByUserIdAndGoodsId(long userId, long goodsId); /** * 创建订单 * * @param user * @param goods * @return */ OrderInfo createOrder(UserVo user, GoodsVo goods); } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/order/vo/OrderDetailVo.java ================================================ package com.seckill.dis.common.api.order.vo; import com.seckill.dis.common.api.goods.vo.GoodsVo; import com.seckill.dis.common.api.user.vo.UserVo; import com.seckill.dis.common.domain.OrderInfo; /** * 订单详情,包含订单信息和商品信息 *

* 用于将数据传递给客户端 * * @author noodle */ public class OrderDetailVo { /** * 用户信息 */ private UserVo user; /** * 商品信息 */ private GoodsVo goods; /** * 订单信息 */ private OrderInfo order; public UserVo getUser() { return user; } public void setUser(UserVo user) { this.user = user; } public GoodsVo getGoods() { return goods; } public void setGoods(GoodsVo goods) { this.goods = goods; } public OrderInfo getOrder() { return order; } public void setOrder(OrderInfo order) { this.order = order; } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/seckill/SeckillServiceApi.java ================================================ package com.seckill.dis.common.api.seckill; import com.seckill.dis.common.api.goods.vo.GoodsVo; import com.seckill.dis.common.api.user.vo.UserVo; import com.seckill.dis.common.domain.OrderInfo; /** * 秒杀服务接口 * * @author noodle */ public interface SeckillServiceApi { /** * 创建验证码 * * @param user * @param goodsId * @return */ String createVerifyCode(UserVo user, long goodsId); /** * 执行秒杀操作,包含以下两步: * 1. 从goods表中减库存 * 2. 将生成的订单写入miaosha_order表中 * * @param user 秒杀商品的用户 * @param goods 所秒杀的商品 * @return 生成的订单信息 */ OrderInfo seckill(UserVo user, GoodsVo goods); /** * 获取秒杀结果 * * @param userId * @param goodsId * @return */ long getSeckillResult(Long userId, long goodsId); } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/seckill/vo/VerifyCodeVo.java ================================================ package com.seckill.dis.common.api.seckill.vo; import java.awt.image.BufferedImage; import java.io.Serializable; /** * 验证码图片及计算结果 * * @author noodle */ public class VerifyCodeVo implements Serializable { /** * 验证码图片 */ private BufferedImage image; /** * 验证码计算结果 */ private int expResult; public VerifyCodeVo() { } public VerifyCodeVo(BufferedImage image, int expResult) { this.image = image; this.expResult = expResult; } public BufferedImage getImage() { return image; } public void setImage(BufferedImage image) { this.image = image; } public int getExpResult() { return expResult; } public void setExpResult(int expResult) { this.expResult = expResult; } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/user/UserServiceApi.java ================================================ package com.seckill.dis.common.api.user; import com.seckill.dis.common.api.user.vo.LoginVo; import com.seckill.dis.common.api.user.vo.RegisterVo; import com.seckill.dis.common.api.user.vo.UserInfoVo; import com.seckill.dis.common.api.user.vo.UserVo; import com.seckill.dis.common.result.CodeMsg; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; /** * 用于用户交互api * * @author noodle */ public interface UserServiceApi { String COOKIE_NAME_TOKEN = "token"; /** * 登录 * 返回用户id;用户登录成功后,将用户id从后台传到前台,通过JWT的形式传到客户端, * 用户再次访问的时候,就携带JWT到服务端,服务端用一个缓存(如redis)将JWT缓存起来, * 并设置有效期,这样,用户不用每次访问都到数据库中查询用户id * * @param username * @param password * @return 用户id */ int login(String username, String password); /** * 注册 * * @param userModel * @return */ CodeMsg register(RegisterVo userModel); /** * 检查用户名是否存在 * * @param username * @return */ boolean checkUsername(String username); /** * 获取用户信息 * * @param uuid * @return */ UserInfoVo getUserInfo(int uuid); /** * 更新用户信息 * * @param userInfoVo * @return */ UserInfoVo updateUserInfo(UserInfoVo userInfoVo); /** * 登录 * * @param loginVo * @return */ String login(@Valid LoginVo loginVo); /** * 根据phone获取用户 * * @param phone * @return */ UserVo getUserByPhone(long phone); } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/user/vo/LoginVo.java ================================================ package com.seckill.dis.common.api.user.vo; import com.seckill.dis.common.validator.IsMobile; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.NotNull; import java.io.Serializable; /** * 用于接收客户端请求中的表单数据 * 使用JSR303完成参数校验 * * @author noodle */ public class LoginVo implements Serializable{ /** * 通过注解的方式校验手机号(JSR303) */ @NotNull @IsMobile// 自定义的注解完成手机号的校验 private String mobile; /** * 通过注解的方式校验密码(JSR303) */ @NotNull @Length(min = 32)// 长度最小为32 private String password; public String getMobile() { return mobile; } public void setMobile(String mobile) { this.mobile = mobile; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return "LoginVo{" + "mobile='" + mobile + '\'' + ", password='" + password + '\'' + '}'; } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/user/vo/RegisterVo.java ================================================ package com.seckill.dis.common.api.user.vo; import javax.validation.constraints.NotNull; import java.io.Serializable; /** * 注册参数 * * @author noodle */ public class RegisterVo implements Serializable { @NotNull private Long phone; @NotNull private String nickname; private String head; @NotNull private String password; public Long getPhone() { return phone; } public void setPhone(Long phone) { this.phone = phone; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public String getHead() { return head; } public void setHead(String head) { this.head = head; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return "RegisterVo{" + "phone=" + phone + ", nickname='" + nickname + '\'' + ", head='" + head + '\'' + ", password='" + password + '\'' + '}'; } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/user/vo/UserInfoVo.java ================================================ package com.seckill.dis.common.api.user.vo; import java.io.Serializable; /** * 用户信息 *

* 注:因为需要通过网络传输此model,所以需要序列化 * * @author noodle */ public class UserInfoVo implements Serializable { private Integer uuid; private String username; private String nickname; private String email; private String phone; private int sex; private String birthday; private String lifeState; private String biography; private String address; private String headAddress; private long beginTime; // 创建时间 private long updateTime;// 更新时间 public Integer getUuid() { return uuid; } public void setUuid(Integer uuid) { this.uuid = uuid; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public int getSex() { return sex; } public void setSex(int sex) { this.sex = sex; } public String getBirthday() { return birthday; } public void setBirthday(String birthday) { this.birthday = birthday; } public String getLifeState() { return lifeState; } public void setLifeState(String lifeState) { this.lifeState = lifeState; } public String getBiography() { return biography; } public void setBiography(String biography) { this.biography = biography; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getHeadAddress() { return headAddress; } public void setHeadAddress(String headAddress) { this.headAddress = headAddress; } public long getBeginTime() { return beginTime; } public void setBeginTime(long beginTime) { this.beginTime = beginTime; } public long getUpdateTime() { return updateTime; } public void setUpdateTime(long updateTime) { this.updateTime = updateTime; } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/api/user/vo/UserVo.java ================================================ package com.seckill.dis.common.api.user.vo; import java.io.Serializable; import java.util.Date; /** * 用户信息 *

* 注:因为需要通过网络传输此model,所以需要序列化 * * @author noodle */ public class UserVo implements Serializable { private Long uuid; private Long phone; private String nickname; private String password; private String salt; private String head; private Date registerDate; private Date lastLoginDate; private Integer loginCount; @Override public String toString() { return "UserVo{" + "uuid=" + uuid + ", phone=" + phone + ", nickname='" + nickname + '\'' + ", password='" + password + '\'' + ", salt='" + salt + '\'' + ", head='" + head + '\'' + ", registerDate=" + registerDate + ", lastLoginDate=" + lastLoginDate + ", loginCount=" + loginCount + '}'; } public Long getUuid() { return uuid; } public void setUuid(Long uuid) { this.uuid = uuid; } public Long getPhone() { return phone; } public void setPhone(Long phone) { this.phone = phone; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getSalt() { return salt; } public void setSalt(String salt) { this.salt = salt; } public String getHead() { return head; } public void setHead(String head) { this.head = head; } public Date getRegisterDate() { return registerDate; } public void setRegisterDate(Date registerDate) { this.registerDate = registerDate; } public Date getLastLoginDate() { return lastLoginDate; } public void setLastLoginDate(Date lastLoginDate) { this.lastLoginDate = lastLoginDate; } public Integer getLoginCount() { return loginCount; } public void setLoginCount(Integer loginCount) { this.loginCount = loginCount; } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/domain/Goods.java ================================================ package com.seckill.dis.common.domain; /** * goods 表 * * @author noodle */ public class Goods { private Long id; private String goodsName; private String goodsTitle; private String goodsImg; private String goodsDetail; private Double goodsPrice; private Long goodsStock; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getGoodsName() { return goodsName; } public void setGoodsName(String goodsName) { this.goodsName = goodsName; } public String getGoodsTitle() { return goodsTitle; } public void setGoodsTitle(String goodsTitle) { this.goodsTitle = goodsTitle; } public String getGoodsImg() { return goodsImg; } public void setGoodsImg(String goodsImg) { this.goodsImg = goodsImg; } public String getGoodsDetail() { return goodsDetail; } public void setGoodsDetail(String goodsDetail) { this.goodsDetail = goodsDetail; } public Double getGoodsPrice() { return goodsPrice; } public void setGoodsPrice(Double goodsPrice) { this.goodsPrice = goodsPrice; } public Long getGoodsStock() { return goodsStock; } public void setGoodsStock(Long goodsStock) { this.goodsStock = goodsStock; } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/domain/OrderInfo.java ================================================ package com.seckill.dis.common.domain; import java.io.Serializable; import java.util.Date; /** * order_info 订单信息 * * @author noodle */ public class OrderInfo implements Serializable{ private Long id; private Long userId; private Long goodsId; private Long deliveryAddrId; private String goodsName; private Integer goodsCount; private Double goodsPrice; private Integer orderChannel; private Integer status; private Date createDate; private Date payDate; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public Long getGoodsId() { return goodsId; } public void setGoodsId(Long goodsId) { this.goodsId = goodsId; } public Long getDeliveryAddrId() { return deliveryAddrId; } public void setDeliveryAddrId(Long deliveryAddrId) { this.deliveryAddrId = deliveryAddrId; } public String getGoodsName() { return goodsName; } public void setGoodsName(String goodsName) { this.goodsName = goodsName; } public Integer getGoodsCount() { return goodsCount; } public void setGoodsCount(Integer goodsCount) { this.goodsCount = goodsCount; } public Double getGoodsPrice() { return goodsPrice; } public void setGoodsPrice(Double goodsPrice) { this.goodsPrice = goodsPrice; } public Integer getOrderChannel() { return orderChannel; } public void setOrderChannel(Integer orderChannel) { this.orderChannel = orderChannel; } public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; } public Date getCreateDate() { return createDate; } public void setCreateDate(Date createDate) { this.createDate = createDate; } public Date getPayDate() { return payDate; } public void setPayDate(Date payDate) { this.payDate = payDate; } @Override public String toString() { return "OrderInfo{" + "id=" + id + ", userId=" + userId + ", goodsId=" + goodsId + ", deliveryAddrId=" + deliveryAddrId + ", goodsName='" + goodsName + '\'' + ", goodsCount=" + goodsCount + ", goodsPrice=" + goodsPrice + ", orderChannel=" + orderChannel + ", status=" + status + ", createDate=" + createDate + ", payDate=" + payDate + '}'; } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/domain/SeckillGoods.java ================================================ package com.seckill.dis.common.domain; import java.io.Serializable; import java.util.Date; /** * seckill_goods * * @author noodle */ public class SeckillGoods implements Serializable{ private Long id; private Long goodsId; private Double seckillPrice; private Integer stockCount; private Date startDate; private Date endDate; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Long getGoodsId() { return goodsId; } public void setGoodsId(Long goodsId) { this.goodsId = goodsId; } public Double getSeckillPrice() { return seckillPrice; } public void setSeckillPrice(Double seckillPrice) { this.seckillPrice = seckillPrice; } public Integer getStockCount() { return stockCount; } public void setStockCount(Integer stockCount) { this.stockCount = stockCount; } public Date getStartDate() { return startDate; } public void setStartDate(Date startDate) { this.startDate = startDate; } public Date getEndDate() { return endDate; } public void setEndDate(Date endDate) { this.endDate = endDate; } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/domain/SeckillOrder.java ================================================ package com.seckill.dis.common.domain; import java.io.Serializable; /** * seckill_order 表 * * @author noodle */ public class SeckillOrder implements Serializable{ private Long id; private Long userId; private Long orderId; private Long goodsId; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public Long getOrderId() { return orderId; } public void setOrderId(Long orderId) { this.orderId = orderId; } public Long getGoodsId() { return goodsId; } public void setGoodsId(Long goodsId) { this.goodsId = goodsId; } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/domain/SeckillUser.java ================================================ package com.seckill.dis.common.domain; import java.io.Serializable; import java.util.Date; /** * 秒杀用户信息 * * @author noodle */ public class SeckillUser implements Serializable{ private Long uuid; private Long phone; private String nickname; private String password; private String salt; private String head; private Date registerDate; private Date lastLoginDate; private Integer loginCount; public Long getUuid() { return uuid; } public void setUuid(Long uuid) { this.uuid = uuid; } public Long getPhone() { return phone; } public void setPhone(Long phone) { this.phone = phone; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getSalt() { return salt; } public void setSalt(String salt) { this.salt = salt; } public String getHead() { return head; } public void setHead(String head) { this.head = head; } public Date getRegisterDate() { return registerDate; } public void setRegisterDate(Date registerDate) { this.registerDate = registerDate; } public Date getLastLoginDate() { return lastLoginDate; } public void setLastLoginDate(Date lastLoginDate) { this.lastLoginDate = lastLoginDate; } public Integer getLoginCount() { return loginCount; } public void setLoginCount(Integer loginCount) { this.loginCount = loginCount; } @Override public String toString() { return "SeckillUser{" + "uuid=" + uuid + ", phone=" + phone + ", nickname='" + nickname + '\'' + ", password='" + password + '\'' + ", salt='" + salt + '\'' + ", head='" + head + '\'' + ", registerDate=" + registerDate + ", lastLoginDate=" + lastLoginDate + ", loginCount=" + loginCount + '}'; } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/exception/GlobalException.java ================================================ package com.seckill.dis.common.exception; import com.seckill.dis.common.result.CodeMsg; /** * 全局异常处理器 * * @author noodle */ public class GlobalException extends RuntimeException { private CodeMsg codeMsg; /** * 使用构造器接收CodeMsg * * @param codeMsg */ public GlobalException(CodeMsg codeMsg) { this.codeMsg = codeMsg; } public CodeMsg getCodeMsg() { return codeMsg; } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/exception/GlobalExceptionHandler.java ================================================ package com.seckill.dis.common.exception; import com.seckill.dis.common.result.CodeMsg; import com.seckill.dis.common.result.Result; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.validation.BindException; import org.springframework.validation.ObjectError; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import java.util.List; /** * 全局异常处理器(底层使用方法拦截的方式完成,和AOP一样) * 在异常发生时,将会调用这里面的方法给客户端一个响应 * * @author noodle */ @ControllerAdvice // 通过Advice可知,这个处理器实际上是一个切面 @ResponseBody public class GlobalExceptionHandler { private static Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); /** * 异常处理 * * @param request 绑定了出现异常的请求信息 * @param e 该请求所产生的异常 * @return 向客户端返回的结果(这里为json数据) */ @ExceptionHandler(value = Exception.class)// 这个注解用指定这个方法对何种异常处理(这里默认所有异常都用这个方法处理) public Result exceptionHandler(HttpServletRequest request, Exception e) { logger.info("出现异常"); e.printStackTrace();// 打印原始的异常信息,方便调试 // 如果所拦截的异常是自定义的全局异常,这按自定义异常的处理方式处理,否则按默认方式处理 if (e instanceof GlobalException) { logger.debug("common 模块中的异常"); GlobalException exception = (GlobalException) e; return Result.error(exception.getCodeMsg());// 向客户端返回异常信息 } else if (e instanceof BindException) { BindException bindException = (BindException) e; List errors = bindException.getAllErrors(); ObjectError error = errors.get(0);// 这里只获取了第一个错误对象 String message = error.getDefaultMessage();// 获取其中的信息 return Result.error(CodeMsg.BIND_ERROR.fillArgs(message));// 将错误信息动态地拼接到已定义的部分信息上 } else { return Result.error(CodeMsg.SERVER_ERROR); } } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/result/CodeMsg.java ================================================ package com.seckill.dis.common.result; import java.io.Serializable; /** * 响应结果状态码 * * @author noodle */ public class CodeMsg implements Serializable { private int code; private String msg; /** * 通用异常 */ public static CodeMsg SUCCESS = new CodeMsg(0, "success"); public static CodeMsg SERVER_ERROR = new CodeMsg(500100, "服务端异常"); public static CodeMsg BIND_ERROR = new CodeMsg(500101, "参数校验异常:%s"); public static CodeMsg REQUEST_ILLEGAL = new CodeMsg(500102, "请求非法"); public static CodeMsg VERITF_FAIL = new CodeMsg(500103, "校验失败,请重新输入表达式结果或刷新校验码重新输入"); public static CodeMsg ACCESS_LIMIT_REACHED = new CodeMsg(500104, "访问太频繁!"); /** * 用户模块 5002XX */ public static CodeMsg SESSION_ERROR = new CodeMsg(500210, "Session不存在或者已经失效,请返回登录!"); public static CodeMsg PASSWORD_EMPTY = new CodeMsg(500211, "登录密码不能为空"); public static CodeMsg MOBILE_EMPTY = new CodeMsg(500212, "手机号不能为空"); public static CodeMsg MOBILE_ERROR = new CodeMsg(500213, "手机号格式错误"); public static CodeMsg MOBILE_NOT_EXIST = new CodeMsg(500214, "手机号不存在"); public static CodeMsg PASSWORD_ERROR = new CodeMsg(500215, "密码错误"); public static CodeMsg USER_EXIST = new CodeMsg(500216, "用户已经存在,无需重复注册"); public static CodeMsg REGISTER_SUCCESS = new CodeMsg(500217, "注册成功"); public static CodeMsg REGISTER_FAIL = new CodeMsg(500218, "注册异常"); public static CodeMsg FILL_REGISTER_INFO = new CodeMsg(500219, "请填写注册信息"); public static CodeMsg WAIT_REGISTER_DONE = new CodeMsg(500220, "等待注册完成"); //登录模块 5002XX //商品模块 5003XX //订单模块 5004XX public static CodeMsg ORDER_NOT_EXIST = new CodeMsg(500400, "订单不存在"); /** * 秒杀模块 5005XX */ public static CodeMsg SECKILL_OVER = new CodeMsg(500500, "商品已经秒杀完毕"); public static CodeMsg REPEATE_SECKILL = new CodeMsg(500501, "不能重复秒杀"); public static CodeMsg SECKILL_FAIL = new CodeMsg(500502, "秒杀失败"); public static CodeMsg SECKILL_PARM_ILLEGAL = new CodeMsg(500503, "秒杀请求参数异常:%s"); /** * 构造器定义为private是为了防止controller直接new * * @param code * @param msg */ public CodeMsg(int code, String msg) { this.code = code; this.msg = msg; } /** * 动态地填充msg字段 * * @param args * @return */ public CodeMsg fillArgs(Object... args) { int code = this.code; String message = String.format(this.msg, args);// 将arg格式化到msg中,组合成一个message return new CodeMsg(code, message); } public int getCode() { return code; } public String getMsg() { return msg; } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/result/Result.java ================================================ package com.seckill.dis.common.result; import java.io.Serializable; /** * 用户接口返回结果 * * @param 数据实体类型 * @author noodle */ public class Result implements Serializable { /** * 状态码 */ private int code; /** * 状态短语 */ private String msg; /** * 数据实体 */ private T data; /** * 定义为private是为了在防止在controller中直接new * * @param data */ private Result(T data) { this.code = 0; this.msg = "success"; this.data = data; } private Result(CodeMsg codeMsg) { if (codeMsg == null) return; this.code = codeMsg.getCode(); this.msg = codeMsg.getMsg(); } /** * 只有get没有set,是为了防止在controller使用set对结果修改,从而达到一个更好的封装效果 * * @return */ public int getCode() { return code; } /** * 业务处理成功返回结果,直接返回业务数据 * * @param data * @return */ public static Result success(T data) { return new Result(data); } /** * 业务处理信息 * * @param serverError * @param * @return */ public static Result info(CodeMsg serverError) { return new Result(serverError); } /** * 业务处理成功 * * @param serverError * @param * @return */ public static Result success(CodeMsg serverError) { return new Result(serverError); } /** * 业务处理失败 * * @param serverError * @param * @return */ public static Result error(CodeMsg serverError) { return new Result(serverError); } public String getMsg() { return msg; } public T getData() { return data; } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/util/DBUtil.java ================================================ package com.seckill.dis.common.util; import java.io.IOException; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; /** * 数据库连接工具 * * @author noodle */ public class DBUtil { private static Properties properties; // 从属性文件中加载数据库配置信息 static { try { InputStream resource = DBUtil.class.getClassLoader().getResourceAsStream("application.properties"); properties = new Properties(); properties.load(resource); } catch (IOException e) { e.printStackTrace(); } } /** * 获取数据库连接对象 * * @return * @throws ClassNotFoundException * @throws SQLException */ public static Connection getConn() throws ClassNotFoundException, SQLException { String url = properties.getProperty("spring.datasource.url"); String username = properties.getProperty("spring.datasource.username"); String password = properties.getProperty("spring.datasource.password"); String driver = properties.getProperty("spring.datasource.driver-class-name"); Class.forName(driver);// 加载驱动 return DriverManager.getConnection(url, username, password); } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/util/JsonUtil.java ================================================ package com.seckill.dis.common.util; import com.alibaba.fastjson.JSON; /** * json 工具类 * * @author noodle */ public class JsonUtil { /** * 根据传入的class参数,将json字符串转换为对应类型的对象 * * @param strValue json字符串 * @param clazz 类型 * @param 类型参数 * @return json字符串对应的对象 */ public static T stringToBean(String strValue, Class clazz) { if ((strValue == null) || (strValue.length() <= 0) || (clazz == null)) return null; // int or Integer if ((clazz == int.class) || (clazz == Integer.class)) return (T) Integer.valueOf(strValue); // long or Long else if ((clazz == long.class) || (clazz == Long.class)) return (T) Long.valueOf(strValue); // String else if (clazz == String.class) return (T) strValue; // 对象类型 else return JSON.toJavaObject(JSON.parseObject(strValue), clazz); } /** * 将对象转换为对应的json字符串 * * @param value 对象 * @param 对象的类型 * @return 对象对应的json字符串 */ public static String beanToString(T value) { if (value == null) return null; Class clazz = value.getClass(); /*首先对基本类型处理*/ if (clazz == int.class || clazz == Integer.class) return "" + value; else if (clazz == long.class || clazz == Long.class) return "" + value; else if (clazz == String.class) return (String) value; /*然后对Object类型的对象处理*/ else return JSON.toJSONString(value); } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/util/MD5Util.java ================================================ package com.seckill.dis.common.util; import org.apache.commons.codec.digest.DigestUtils; import org.junit.Test; /** * MD5工具类 * * @author noodle */ public class MD5Util { /** * 获取输入字符串的MD5 * * @param src * @return */ public static String md5(String src) { return DigestUtils.md5Hex(src); } // MD5的第一次加盐的salt值,客户端和服务端一致 public static final String SALT = "1a2b3c4d"; /** * 对客户端输入的密码加盐(第一次加盐),得到的MD5值为表单中传输的值 *

* 在密码的传输和存储中,总共经历了两次MD5和两次salt *

* 第一次:客户输入到表单中的密码会经过一次MD5和加盐,即;pwd_md_1st = MD5(用户明文密码)+ salt_1st。 * 其中,pwd_md_1st 是客户端真正接收到的密码。salt_1st在客户端和服务端都是一样的 *

* 第二次:对客户端传递到服务器的 pwd_md_1st 再一次MD5和加盐,即;pwd_md_2nd = MD5(MD5和加盐)+ salt_2nd。 * 其中,salt_2nd是存储在服务器端的,每个用户都有自己的salt_2nd,所以在使用salt_2nd时需要从数据库中查出 *

* 最终存储在数据库中的用户密码实际为 pwd_md_2nd。 * * @param inputPassword 用户输入的密码 * @return Calculates the MD5 digest and returns the value as a 32 character hex string. */ public static String inputPassToFormPass(String inputPassword) { // 加盐规则(自定义) String str = "" + SALT.charAt(0) + SALT.charAt(2) + inputPassword + SALT.charAt(5) + SALT.charAt(4); // Calculates the MD5 digest and returns the value as a 32 character hex string. return md5(str); } /** * 对表单中的密码md5加盐,加盐后的md5为存储在数据库中的密码md5 * * @param formPassword 表单中填充的明文密码 * @param saltDb 这里的salt是在数据库查出来的,而并非第一次加盐的盐值 * @return */ public static String formPassToDbPass(String formPassword, String saltDb) { String str = "" + saltDb.charAt(0) + saltDb.charAt(2) + formPassword + saltDb.charAt(5) + saltDb.charAt(4); return md5(str); } /** * 两次MD5:储存在数据库中的密码的md5 = MD5(MD5(input_password) + SALT) + db_salt * * @param inputPassword 用户输入密码 * @param saltDb 数据库中用户的salt * @return */ public static String inputPassToDbPass(String inputPassword, String saltDb) { String formPass = inputPassToFormPass(inputPassword); String dbPass = formPassToDbPass(formPass, saltDb); return dbPass; } /** * 测试 */ @Test public void TestMD5() { System.out.println(inputPassToFormPass("000000")); System.out.println(inputPassToDbPass("000000", SALT)); } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/util/UUIDUtil.java ================================================ package com.seckill.dis.common.util; import java.util.UUID; /** * UUID工具类用于生成session * * @author noodle */ public class UUIDUtil { public static String uuid() { return UUID.randomUUID().toString().replace("-", ""); } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/util/ValidatorUtil.java ================================================ package com.seckill.dis.common.util; import org.apache.commons.lang3.StringUtils; import java.util.regex.Pattern; /** * 手机号码格式校验工具 * * @noodle */ public class ValidatorUtil { // 手机号正则 private static final Pattern mobilePattern = Pattern.compile("1\\d{10}"); /** * 正则表达式:验证手机号 */ public static final String REGEX_MOBILE = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$";; /** * 校验手机号 * * @param mobile * @return 校验通过返回true,否则返回false */ public static boolean isMobile(String mobile) { if (StringUtils.isEmpty(mobile)) return false; return Pattern.matches(REGEX_MOBILE, mobile); } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/util/VerifyCodeUtil.java ================================================ package com.seckill.dis.common.util; import com.seckill.dis.common.api.seckill.vo.VerifyCodeVo; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import java.awt.*; import java.awt.image.BufferedImage; import java.util.Random; public class VerifyCodeUtil { /** * 用于生成验证码中的运算符 */ private static char[] ops = new char[]{'+', '-', '*'}; /** * 创建验证码 * @return */ public static VerifyCodeVo createVerifyCode() { // 验证码的宽高 int width = 80; int height = 32; //create the image BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics g = image.getGraphics(); // set the background color g.setColor(new Color(0xDCDCDC)); g.fillRect(0, 0, width, height); // draw the border g.setColor(Color.black); g.drawRect(0, 0, width - 1, height - 1); // create a random instance to generate the codes Random rdm = new Random(); // make some confusion for (int i = 0; i < 50; i++) { int x = rdm.nextInt(width); int y = rdm.nextInt(height); g.drawOval(x, y, 0, 0); } // generate a random code String verifyCode = generateVerifyCode(rdm); g.setColor(new Color(0, 100, 0)); g.setFont(new Font("Candara", Font.BOLD, 24)); g.drawString(verifyCode, 8, 24); g.dispose(); // 计算表达式值,并把把验证码值存到redis中 int expResult = calc(verifyCode); //输出图片和结果 return new VerifyCodeVo(image, expResult); } /** * 使用 ScriptEngine 计算验证码中的数学表达式的值 * * @param exp * @return */ private static int calc(String exp) { try { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("JavaScript"); return (Integer) engine.eval(exp);// 表达式计算 } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 生成验证码,只含有+/-/* *

* 随机生成三个数字,然后生成表达式 * * @param rdm * @return 验证码中的数学表达式 */ private static String generateVerifyCode(Random rdm) { int num1 = rdm.nextInt(10); int num2 = rdm.nextInt(10); int num3 = rdm.nextInt(10); char op1 = ops[rdm.nextInt(3)]; char op2 = ops[rdm.nextInt(3)]; String exp = "" + num1 + op1 + num2 + op2 + num3; return exp; } } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/validator/IsMobile.java ================================================ package com.seckill.dis.common.validator; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * 手机号码的校验注解 * * @author */ @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {IsMobileValidator.class}) // 这个注解的参数指定用于校验工作的是哪个类 public @interface IsMobile { /** * 默认手机号码不可为空 * * @return */ boolean required() default true; /** * 如果校验不通过时的提示信息 * * @return */ String message() default "手机号码格式有误!"; Class[] groups() default {}; Class[] payload() default {}; } ================================================ FILE: dis-seckill-common/src/main/java/com/seckill/dis/common/validator/IsMobileValidator.java ================================================ package com.seckill.dis.common.validator; import com.seckill.dis.common.util.ValidatorUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; /** * 真正用户手机号码检验的工具,会被注解@isMobile所使用 * 这个类需要实现javax.validation.ConstraintValidator,否则不能被@Constraint参数使用 * * @author noodle */ public class IsMobileValidator implements ConstraintValidator { private static Logger logger = LoggerFactory.getLogger(IsMobileValidator.class); /** * 用于获取检验字段是否可以为空 */ private boolean required = false; /** * 用于获取注解 * * @param constraintAnnotation */ @Override public void initialize(IsMobile constraintAnnotation) { required = constraintAnnotation.required(); } /** * 用于检验字段是否合法 * * @param value 待校验的字段 * @param context * @return 字段检验结果 */ @Override public boolean isValid(String value, ConstraintValidatorContext context) { logger.info("是否需要校验参数:" + required); // 如果所检验字段可以为空 if (required) { return ValidatorUtil.isMobile(value); } else { return StringUtils.isEmpty(value) || ValidatorUtil.isMobile(value); } } } ================================================ FILE: dis-seckill-gateway/pom.xml ================================================ dis-seckill com.seckill.dis 0.0.1-SNAPSHOT 4.0.0 dis-seckill-gateway jar org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import org.apache.dubbo dubbo-dependencies-bom ${dubbo.version} pom import org.apache.dubbo dubbo ${dubbo.version} org.springframework spring javax.servlet servlet-api log4j log4j com.seckill.dis dis-seckill-common 1.0.0 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-configuration-processor org.springframework.boot spring-boot-starter-thymeleaf com.alibaba fastjson org.apache.dubbo dubbo-spring-boot-starter ${dubbo.version} org.apache.dubbo dubbo org.apache.dubbo dubbo-dependencies-zookeeper ${dubbo.version} org.slf4j slf4j-log4j12 pom redis.clients jedis org.springframework.boot spring-boot-starter-amqp org.springframework.boot spring-boot-maven-plugin ================================================ FILE: dis-seckill-gateway/src/main/java/com/seckill/dis/gateway/DisSeckillServletInitializer.java ================================================ package com.seckill.dis.gateway; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; /** * web app 程序启动器 * * @author noodle * @date 2019/7/21 11:26 */ public class DisSeckillServletInitializer extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(GatewayApplication.class); } } ================================================ FILE: dis-seckill-gateway/src/main/java/com/seckill/dis/gateway/GatewayApplication.java ================================================ package com.seckill.dis.gateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.servlet.config.annotation.EnableWebMvc; /** * 网关服务 * * @author noodle */ @SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } } ================================================ FILE: dis-seckill-gateway/src/main/java/com/seckill/dis/gateway/config/WebConfig.java ================================================ package com.seckill.dis.gateway.config; import com.seckill.dis.gateway.config.access.AccessInterceptor; import com.seckill.dis.gateway.config.resolver.UserArgumentResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.List; /** * 自定义web配置 * * @author noodle */ @Configuration public class WebConfig implements WebMvcConfigurer { private static Logger logger = LoggerFactory.getLogger(WebConfig.class); @Autowired UserArgumentResolver userArgumentResolver; @Autowired AccessInterceptor accessInterceptor; /** * 添加自定义的参数解析器到MVC配置中 * * @param argumentResolvers */ @Override public void addArgumentResolvers(List argumentResolvers) { logger.info("添加自定义的参数解析器"); // 添加自定义的参数解析器 argumentResolvers.add(userArgumentResolver); } /** * 添加自定义方法拦截器到MVC配置中 * * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { logger.info("添加请求拦截器"); registry.addInterceptor(accessInterceptor); } } ================================================ FILE: dis-seckill-gateway/src/main/java/com/seckill/dis/gateway/config/access/AccessInterceptor.java ================================================ package com.seckill.dis.gateway.config.access; import com.alibaba.fastjson.JSON; import com.seckill.dis.common.api.cache.RedisServiceApi; import com.seckill.dis.common.api.cache.vo.AccessKeyPrefix; import com.seckill.dis.common.api.cache.vo.SkUserKeyPrefix; import com.seckill.dis.common.api.user.UserServiceApi; import com.seckill.dis.common.api.user.vo.UserVo; import com.seckill.dis.common.result.CodeMsg; import com.seckill.dis.common.result.Result; import org.apache.commons.lang3.StringUtils; import org.apache.dubbo.config.annotation.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.OutputStream; /** * 用户访问拦截器 * * @author noodle */ @Service public class AccessInterceptor extends HandlerInterceptorAdapter { private static Logger logger = LoggerFactory.getLogger(AccessInterceptor.class); @Reference(interfaceClass = UserServiceApi.class) UserServiceApi userService; @Reference(interfaceClass = RedisServiceApi.class) RedisServiceApi redisService; /** * 目标方法执行前的处理 *

* 查询访问次数,进行防刷请求拦截 * 在 AccessLimit#seconds() 时间内频繁访问会有次数限制 * * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { logger.info(request.getRequestURL() + " 拦截请求"); // 指明拦截的是方法 if (handler instanceof HandlerMethod) { logger.info("HandlerMethod: " + ((HandlerMethod) handler).getMethod().getName()); // 获取用户对象 UserVo user = this.getUser(request, response); // 保存用户到ThreadLocal,这样,同一个线程访问的是同一个用户 UserContext.setUser(user); // 获取标注了 @AccessLimit 的方法,没有注解,则直接返回 HandlerMethod hm = (HandlerMethod) handler; AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class); // 如果没有添加@AccessLimit注解,直接放行(true) if (accessLimit == null) return true; // 获取注解的元素值 int seconds = accessLimit.seconds(); int maxCount = accessLimit.maxAccessCount(); boolean needLogin = accessLimit.needLogin(); String key = request.getRequestURI(); if (needLogin) { if (user == null) { this.render(response, CodeMsg.SESSION_ERROR); return false; } key += "_" + user.getPhone(); } else { //do nothing } // 设置缓存过期时间 AccessKeyPrefix accessKeyPrefix = AccessKeyPrefix.withExpire(seconds); // 在redis中存储的访问次数的key为请求的URI Integer count = redisService.get(accessKeyPrefix, key, Integer.class); // 第一次重复点击 秒杀按钮 if (count == null) { redisService.set(accessKeyPrefix, key, 1); // 点击次数未达最大值 } else if (count < maxCount) { redisService.incr(accessKeyPrefix, key); // 点击次数已满 } else { this.render(response, CodeMsg.ACCESS_LIMIT_REACHED); return false; } } // 不是方法直接放行 return true; } /** * 渲染返回信息 * 以 json 格式返回 * * @param response * @param cm * @throws Exception */ private void render(HttpServletResponse response, CodeMsg cm) throws Exception { response.setContentType("application/json;charset=UTF-8"); OutputStream out = response.getOutputStream(); String str = JSON.toJSONString(Result.error(cm)); out.write(str.getBytes("UTF-8")); out.flush(); out.close(); } /** * 和 UserArgumentResolver 功能类似,用于解析拦截的请求,获取 UserVo 对象 * * @param request * @param response * @return UserVo 对象 */ private UserVo getUser(HttpServletRequest request, HttpServletResponse response) { logger.info(request.getRequestURL() + " 获取 UserVo 对象"); // 从请求中获取token String paramToken = request.getParameter(UserServiceApi.COOKIE_NAME_TOKEN); String cookieToken = getCookieValue(request, UserServiceApi.COOKIE_NAME_TOKEN); if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) { return null; } String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken; if (StringUtils.isEmpty(token)) { return null; } UserVo userVo = redisService.get(SkUserKeyPrefix.TOKEN, token, UserVo.class); // 在有效期内从redis获取到key之后,需要将key重新设置一下,从而达到延长有效期的效果 if (userVo != null) { addCookie(response, token, userVo); } return userVo; } /** * 从众多的cookie中找出指定cookiName的cookie * * @param request * @param cookieName * @return cookiName对应的value */ private String getCookieValue(HttpServletRequest request, String cookieName) { Cookie[] cookies = request.getCookies(); if (cookies == null || cookies.length == 0) return null; for (Cookie cookie : cookies) { if (cookie.getName().equals(cookieName)) { return cookie.getValue(); } } return null; } /** * 将cookie存入redis,并将cookie写入到请求的响应中 * * @param response * @param token * @param user */ private void addCookie(HttpServletResponse response, String token, UserVo user) { redisService.set(SkUserKeyPrefix.TOKEN, token, user); Cookie cookie = new Cookie(UserServiceApi.COOKIE_NAME_TOKEN, token); // 客户端cookie的有限期和缓存中的cookie有效期一致 cookie.setMaxAge(SkUserKeyPrefix.TOKEN.expireSeconds()); cookie.setPath("/"); response.addCookie(cookie); } } ================================================ FILE: dis-seckill-gateway/src/main/java/com/seckill/dis/gateway/config/access/AccessLimit.java ================================================ package com.seckill.dis.gateway.config.access; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * 用户访问拦截的注解 * 主要用于防止刷功能的实现 * * @author noodle */ @Retention(RUNTIME) @Target(METHOD) public @interface AccessLimit { /** * 两次请求的最大有效时间间隔,即视两次请求为同一状态的时间间隔 * * @return */ int seconds(); /** * 最大请求次数 * * @return */ int maxAccessCount(); /** * 是否需要重新登录 * * @return */ boolean needLogin() default true; } ================================================ FILE: dis-seckill-gateway/src/main/java/com/seckill/dis/gateway/config/access/UserContext.java ================================================ package com.seckill.dis.gateway.config.access; import com.seckill.dis.common.api.user.vo.UserVo; /** * 用于保存用户 * 使用ThreadLocal保存用户,因为ThreadLocal是线程安全的,使用ThreadLocal可以保存当前线程持有的对象 * 每个用户的请求对应一个线程,所以使用ThreadLocal以线程为键保存用户是合适的 * * @author noodle */ public class UserContext { // 保存用户的容器 private static ThreadLocal userHolder = new ThreadLocal<>(); public static void setUser(UserVo user) { userHolder.set(user); } public static UserVo getUser() { return userHolder.get(); } } ================================================ FILE: dis-seckill-gateway/src/main/java/com/seckill/dis/gateway/config/resolver/UserArgumentResolver.java ================================================ package com.seckill.dis.gateway.config.resolver; import com.seckill.dis.common.api.cache.RedisServiceApi; import com.seckill.dis.common.api.cache.vo.SkUserKeyPrefix; import com.seckill.dis.common.api.user.UserServiceApi; import com.seckill.dis.common.api.user.vo.UserVo; import org.apache.commons.lang3.StringUtils; import org.apache.dubbo.config.annotation.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Service; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 解析请求,并将请求的参数设置到方法参数中 * * @author noodle */ @Service public class UserArgumentResolver implements HandlerMethodArgumentResolver { private static Logger logger = LoggerFactory.getLogger(UserArgumentResolver.class); /** * 由于需要将一个cookie对应的用户存入第三方缓存中,这里用redis,所以需要引入redis service */ @Reference(interfaceClass = RedisServiceApi.class) RedisServiceApi redisService; /** * 当请求参数为 UserVo 时,使用这个解析器处理 * 客户端的请求到达某个 Controller 的方法时,判断这个方法的参数是否为 UserVo, * 如果是,则这个 UserVo 参数对象通过下面的 resolveArgument() 方法获取, * 然后,该 Controller 方法继续往下执行时所看到的 UserVo 对象就是在这里的 resolveArgument() 方法处理过的对象 * * @param methodParameter * @return */ @Override public boolean supportsParameter(MethodParameter methodParameter) { logger.info("supportsParameter"); Class parameterType = methodParameter.getParameterType(); return parameterType == UserVo.class; } /** * 从分布式 session 中获取 UserVo 对象 * * @param methodParameter * @param modelAndViewContainer * @param nativeWebRequest * @param webDataBinderFactory * @return * @throws Exception */ @Override public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception { // 获取请求和响应对象 HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class); HttpServletResponse response = nativeWebRequest.getNativeResponse(HttpServletResponse.class); logger.info(request.getRequestURL() + " resolveArgument"); // 从请求对象中获取token(token可能有两种方式从客户端返回,1:通过url的参数,2:通过set-Cookie字段) String paramToken = request.getParameter(UserServiceApi.COOKIE_NAME_TOKEN); String cookieToken = getCookieValue(request, UserServiceApi.COOKIE_NAME_TOKEN); if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) { return null; } // 判断是哪种方式返回的token,并由该种方式获取token(cookie) String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken; if (StringUtils.isEmpty(token)) { return null; } // 通过token就可以在redis中查出该token对应的用户对象 UserVo userVo = redisService.get(SkUserKeyPrefix.TOKEN, token, UserVo.class); logger.info("获取userVo:" + userVo.toString()); // 在有效期内从redis获取到key之后,需要将key重新设置一下,从而达到延长有效期的效果 if (userVo != null) { addCookie(response, token, userVo); } return userVo; } /** * 根据cookie名获取相应的cookie值 * * @param request * @param cookieName * @return */ private String getCookieValue(HttpServletRequest request, String cookieName) { logger.info("getCookieValue"); Cookie[] cookies = request.getCookies(); // null判断,否则并发时会发生异常 if (cookies == null || cookies.length == 0) { logger.info("cookies is null"); return null; } for (Cookie cookie : cookies) { if (cookie.getName().equals(cookieName)) { return cookie.getValue(); } } return null; } /** * 将cookie存入redis,并将cookie写入到请求的响应中 * * @param response * @param token * @param user */ private void addCookie(HttpServletResponse response, String token, UserVo user) { redisService.set(SkUserKeyPrefix.TOKEN, token, user); Cookie cookie = new Cookie(UserServiceApi.COOKIE_NAME_TOKEN, token); cookie.setMaxAge(SkUserKeyPrefix.TOKEN.expireSeconds()); cookie.setPath("/"); response.addCookie(cookie); } } ================================================ FILE: dis-seckill-gateway/src/main/java/com/seckill/dis/gateway/exception/GlobalException.java ================================================ package com.seckill.dis.gateway.exception; import com.seckill.dis.common.result.CodeMsg; /** * 全局异常处理器 * * @author noodle */ public class GlobalException extends RuntimeException { private CodeMsg codeMsg; /** * 使用构造器接收CodeMsg * * @param codeMsg */ public GlobalException(CodeMsg codeMsg) { this.codeMsg = codeMsg; } public CodeMsg getCodeMsg() { return codeMsg; } } ================================================ FILE: dis-seckill-gateway/src/main/java/com/seckill/dis/gateway/exception/GlobalExceptionHandler.java ================================================ package com.seckill.dis.gateway.exception; import com.seckill.dis.common.result.CodeMsg; import com.seckill.dis.common.result.Result; import com.seckill.dis.gateway.user.UserController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.validation.BindException; import org.springframework.validation.ObjectError; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import java.util.List; /** * 全局异常处理器(底层使用方法拦截的方式完成,和AOP一样) * 在异常发生时,将会调用这里面的方法给客户端一个响应 * * @author noodle */ @ControllerAdvice // 通过Advice可知,这个处理器实际上是一个切面 @ResponseBody public class GlobalExceptionHandler { private static Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); /** * 异常处理 * * @param request 绑定了出现异常的请求信息 * @param e 该请求所产生的异常 * @return 向客户端返回的结果(这里为json数据) */ @ExceptionHandler(value = Exception.class)// 这个注解用指定这个方法对何种异常处理(这里默认所有异常都用这个方法处理) public Result exceptionHandler(HttpServletRequest request, Exception e) { logger.info("出现异常"); e.printStackTrace();// 打印原始的异常信息,方便调试 // 如果所拦截的异常是自定义的全局异常,这按自定义异常的处理方式处理,否则按默认方式处理 if (e instanceof GlobalException) { GlobalException exception = (GlobalException) e; // 向客户端返回异常信息 return Result.error(exception.getCodeMsg()); } else if (e instanceof BindException) { BindException bindException = (BindException) e; List errors = bindException.getAllErrors(); // 这里只获取了第一个错误对象 ObjectError error = errors.get(0); // 获取其中的信息 String message = error.getDefaultMessage(); // 将错误信息动态地拼接到已定义的部分信息上 return Result.error(CodeMsg.BIND_ERROR.fillArgs(message)); } else { return Result.error(CodeMsg.SERVER_ERROR); } } } ================================================ FILE: dis-seckill-gateway/src/main/java/com/seckill/dis/gateway/goods/GoodsController.java ================================================ package com.seckill.dis.gateway.goods; import com.seckill.dis.common.api.cache.RedisServiceApi; import com.seckill.dis.common.api.cache.vo.GoodsKeyPrefix; import com.seckill.dis.common.api.goods.GoodsServiceApi; import com.seckill.dis.common.api.goods.vo.GoodsDetailVo; import com.seckill.dis.common.api.goods.vo.GoodsVo; import com.seckill.dis.common.api.user.vo.UserVo; import com.seckill.dis.common.result.Result; import com.seckill.dis.gateway.user.UserController; import org.apache.commons.lang3.StringUtils; import org.apache.dubbo.config.annotation.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.thymeleaf.context.WebContext; import org.thymeleaf.spring5.view.ThymeleafViewResolver; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.List; /** * 商品模块接口 * * @author noodle */ @Controller @RequestMapping("/goods/") public class GoodsController { private static Logger logger = LoggerFactory.getLogger(UserController.class); @Reference(interfaceClass = RedisServiceApi.class) RedisServiceApi redisService; @Reference(interfaceClass = GoodsServiceApi.class) GoodsServiceApi goodsService; /** * 因为在redis缓存中不存页面缓存时需要手动渲染,所以注入一个视图解析器,自定义渲染 */ @Autowired ThymeleafViewResolver thymeleafViewResolver; /** * 获取 SKUser 对象,并将其传递到页面解析器 * 从数据库中获取商品信息(包含秒杀信息) *

* QPS: 1267, 用户数目5000,每个用户发起10次请求,共5000*10次请求 *

* 页面级缓存实现;从redis中取页面,如果没有则需要手动渲染页面,并且将渲染的页面存储在redis中供下一次访问时获取 * * @param request * @param response * @param model 响应的资源文件 * @param user 通过自定义参数解析器UserArgumentResolver解析的 SKUser 对象 * @return */ @RequestMapping(value = "goodsList", produces = "text/html")// produces表明:这个请求会返回text/html媒体类型的数据 @ResponseBody public String goodsList(HttpServletRequest request, HttpServletResponse response, Model model, UserVo user) { logger.info("获取商品列表"); // 1. 从redis缓存中取html String html = redisService.get(GoodsKeyPrefix.GOODS_LIST_HTML, "", String.class); if (!StringUtils.isEmpty(html)) return html; // 2. 如果redis中不存在该缓存,则需要手动渲染 // 查询商品列表,用于手动渲染时将商品数据填充到页面 List goodsVoList = goodsService.listGoodsVo(); model.addAttribute("goodsList", goodsVoList); model.addAttribute("user", user); // 3. 渲染html WebContext webContext = new WebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap()); // (第一个参数为渲染的html文件名,第二个为web上下文:里面封装了web应用的上下文) html = thymeleafViewResolver.getTemplateEngine().process("goods_list", webContext); if (!StringUtils.isEmpty(html)) // 如果html文件不为空,则将页面缓存在redis中 redisService.set(GoodsKeyPrefix.GOODS_LIST_HTML, "", html); return html; } /** * 处理商品详情页(页面静态化处理, 直接将数据返回给客户端,交给客户端处理) *

* URL级缓存实现;从redis中取商品详情页面,如果没有则需要手动渲染页面,并且将渲染的页面存储在redis中供下一次访问时获取 * 实际上URL级缓存和页面级缓存是一样的,只不过URL级缓存会根据url的参数从redis中取不同的数据 * * @param user * @param goodsId * @return */ @RequestMapping(value = "getDetails/{goodsId}") @ResponseBody public Result getDetails(UserVo user, @PathVariable("goodsId") long goodsId) { logger.info("获取商品详情"); // 通过商品id在数据库查询 GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId); // 获取商品的秒杀开始与结束的时间 long startDate = goods.getStartDate().getTime(); long endDate = goods.getEndDate().getTime(); long now = System.currentTimeMillis(); // 秒杀状态; 0: 秒杀未开始,1: 秒杀进行中,2: 秒杀已结束 int skStatus = 0; // 秒杀剩余时间 int remainSeconds = 0; if (now < startDate) { // 秒杀未开始 skStatus = 0; remainSeconds = (int) ((startDate - now) / 1000); } else if (now > endDate) { // 秒杀已结束 skStatus = 2; remainSeconds = -1; } else { // 秒杀进行中 skStatus = 1; remainSeconds = 0; } // 服务端封装商品数据直接传递给客户端,而不用渲染页面 GoodsDetailVo goodsDetailVo = new GoodsDetailVo(); goodsDetailVo.setGoods(goods); goodsDetailVo.setUser(user); goodsDetailVo.setRemainSeconds(remainSeconds); goodsDetailVo.setSeckillStatus(skStatus); return Result.success(goodsDetailVo); } } ================================================ FILE: dis-seckill-gateway/src/main/java/com/seckill/dis/gateway/order/OrderController.java ================================================ package com.seckill.dis.gateway.order; import com.seckill.dis.common.api.goods.GoodsServiceApi; import com.seckill.dis.common.api.goods.vo.GoodsVo; import com.seckill.dis.common.api.order.OrderServiceApi; import com.seckill.dis.common.api.order.vo.OrderDetailVo; import com.seckill.dis.common.api.user.vo.UserVo; import com.seckill.dis.common.domain.OrderInfo; import com.seckill.dis.common.result.CodeMsg; import com.seckill.dis.common.result.Result; import org.apache.dubbo.config.annotation.Reference; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; /** * 订单服务接口 * * @author noodle */ @Controller @RequestMapping("/order/") public class OrderController { @Reference(interfaceClass = OrderServiceApi.class) OrderServiceApi orderService; @Reference(interfaceClass = GoodsServiceApi.class) GoodsServiceApi goodsService; /** * 获取订单详情 * * @param model * @param user * @param orderId * @return */ @RequestMapping("detail") @ResponseBody public Result orderInfo(Model model, UserVo user, @RequestParam("orderId") long orderId) { if (user == null) { return Result.error(CodeMsg.SESSION_ERROR); } // 获取订单信息 OrderInfo order = orderService.getOrderById(orderId); if (order == null) { return Result.error(CodeMsg.ORDER_NOT_EXIST); } // 如果订单存在,则根据订单信息获取商品信息 long goodsId = order.getGoodsId(); GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId); OrderDetailVo vo = new OrderDetailVo(); vo.setUser(user);// 设置用户信息 vo.setOrder(order); // 设置订单信息 vo.setGoods(goods); // 设置商品信息 return Result.success(vo); } } ================================================ FILE: dis-seckill-gateway/src/main/java/com/seckill/dis/gateway/seckill/SeckillController.java ================================================ package com.seckill.dis.gateway.seckill; import com.seckill.dis.common.api.cache.RedisServiceApi; import com.seckill.dis.common.api.cache.vo.GoodsKeyPrefix; import com.seckill.dis.common.api.cache.vo.OrderKeyPrefix; import com.seckill.dis.common.api.cache.vo.SkKeyPrefix; import com.seckill.dis.common.api.goods.GoodsServiceApi; import com.seckill.dis.common.api.goods.vo.GoodsVo; import com.seckill.dis.common.api.mq.MqProviderApi; import com.seckill.dis.common.api.mq.vo.SkMessage; import com.seckill.dis.common.api.order.OrderServiceApi; import com.seckill.dis.common.api.seckill.SeckillServiceApi; import com.seckill.dis.common.api.seckill.vo.VerifyCodeVo; import com.seckill.dis.common.api.user.vo.UserVo; import com.seckill.dis.common.domain.SeckillOrder; import com.seckill.dis.common.result.CodeMsg; import com.seckill.dis.common.result.Result; import com.seckill.dis.common.util.MD5Util; import com.seckill.dis.common.util.UUIDUtil; import com.seckill.dis.common.util.VerifyCodeUtil; import com.seckill.dis.gateway.config.access.AccessLimit; import org.apache.dubbo.config.annotation.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import javax.imageio.ImageIO; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 秒杀接口 * * @author noodle */ @Controller @RequestMapping("/seckill/") public class SeckillController implements InitializingBean { private static Logger logger = LoggerFactory.getLogger(SeckillController.class); @Reference(interfaceClass = RedisServiceApi.class) RedisServiceApi redisService; @Reference(interfaceClass = GoodsServiceApi.class) GoodsServiceApi goodsService; @Reference(interfaceClass = SeckillServiceApi.class) SeckillServiceApi seckillService; @Reference(interfaceClass = OrderServiceApi.class) OrderServiceApi orderService; @Reference(interfaceClass = MqProviderApi.class) MqProviderApi sender; /** * 用于内存标记,标记库存是否为空,从而减少对redis的访问 */ private Map localOverMap = new HashMap<>(); /** * 获取秒杀接口地址 * 1. 每一次点击秒杀,都会生成一个随机的秒杀地址返回给客户端 * 2. 对秒杀的次数做限制(通过自定义拦截器注解完成) * * @param model * @param user * @param goodsId 秒杀的商品id * @param verifyCode 验证码 * @return 被隐藏的秒杀接口路径 */ @AccessLimit(seconds = 5, maxAccessCount = 5, needLogin = true) @RequestMapping(value = "path", method = RequestMethod.GET) @ResponseBody public Result getSeckillPath(Model model, UserVo user, @RequestParam("goodsId") long goodsId, @RequestParam(value = "verifyCode", defaultValue = "0") int verifyCode) { /** 在执行下面的逻辑之前,会先对path请求进行拦截处理(@AccessLimit, AccessInterceptor),防止访问次数过于频繁,对服务器造成过大的压力 */ model.addAttribute("user", user); if (goodsId <= 0) { return Result.error(CodeMsg.SECKILL_PARM_ILLEGAL.fillArgs("商品id小于0")); } // 校验验证码 boolean check = this.checkVerifyCode(user, goodsId, verifyCode); if (!check) return Result.error(CodeMsg.VERITF_FAIL);// 检验不通过,请求非法 // 检验通过,获取秒杀路径 String path = this.createSkPath(user, goodsId); // 向客户端回传随机生成的秒杀地址 return Result.success(path); } /** * 秒杀逻辑(页面静态化分离,不需要直接将页面返回给客户端,而是返回客户端需要的页面动态数据,返回数据时json格式) *

* QPS:1306 * 5000 * 10 *

* GET/POST的@RequestMapping是有区别的 *

* 通过随机的path,客户端隐藏秒杀接口 *

* 优化: 不同于每次都去数据库中读取秒杀订单信息,而是在第一次生成秒杀订单成功后, * 将订单存储在redis中,再次读取订单信息的时候就直接从redis中读取 * * @param model * @param user * @param goodsId * @param path 隐藏的秒杀地址,为客户端回传的path,最初也是有服务端产生的 * @return 订单详情或错误码 */ // {path}为客户端回传的path,最初也是有服务端产生的 @RequestMapping(value = "{path}/doSeckill", method = RequestMethod.POST) @ResponseBody public Result doSeckill(Model model, UserVo user, @RequestParam("goodsId") long goodsId, @PathVariable("path") String path) { model.addAttribute("user", user); // 验证path是否正确 boolean check = this.checkPath(user, goodsId, path); if (!check) return Result.error(CodeMsg.REQUEST_ILLEGAL);// 请求非法 // 通过内存标记,减少对redis的访问,秒杀未结束才继续访问redis Boolean over = localOverMap.get(goodsId); if (over) return Result.error(CodeMsg.SECKILL_OVER); // 预减库存,同时在库存为0时标记该商品已经结束秒杀 Long stock = redisService.decr(GoodsKeyPrefix.GOODS_STOCK, "" + goodsId); if (stock < 0) { localOverMap.put(goodsId, true);// 秒杀结束。标记该商品已经秒杀结束 return Result.error(CodeMsg.SECKILL_OVER); } // 判断是否重复秒杀 // 从redis中取缓存,减少数据库的访问 SeckillOrder order = redisService.get(OrderKeyPrefix.SK_ORDER, ":" + user.getUuid() + "_" + goodsId, SeckillOrder.class); // 如果缓存中不存该数据,则从数据库中取 if (order == null) { order = orderService.getSeckillOrderByUserIdAndGoodsId(user.getUuid(), goodsId); } if (order != null) { return Result.error(CodeMsg.REPEATE_SECKILL); } // 商品有库存且用户为秒杀商品,则将秒杀请求放入MQ SkMessage message = new SkMessage(); message.setUser(user); message.setGoodsId(goodsId); // 放入MQ(对秒杀请求异步处理,直接返回) sender.sendSkMessage(message); // 排队中 return Result.success(0); } /** * 用于返回用户秒杀的结果 * * @param model * @param user * @param goodsId * @return orderId:成功, -1:秒杀失败, 0: 排队中 */ @RequestMapping(value = "result", method = RequestMethod.GET) @ResponseBody public Result getSeckillResult(Model model, UserVo user, @RequestParam("goodsId") long goodsId) { model.addAttribute("user", user); long result = seckillService.getSeckillResult(user.getUuid(), goodsId); return Result.success(result); } /** * goods_detail.htm: $("#verifyCodeImg").attr("src", "/seckill/verifyCode?goodsId=" + $("#goodsId").val()); * 使用HttpServletResponse的输出流返回客户端异步获取的验证码(异步获取的代码如上所示) * * @param response * @param user * @param goodsId * @return */ @RequestMapping(value = "verifyCode", method = RequestMethod.GET) @ResponseBody public Result getVerifyCode(HttpServletResponse response, UserVo user, @RequestParam("goodsId") long goodsId) { logger.info("获取验证码"); if (user == null || goodsId <= 0) { return Result.error(CodeMsg.SESSION_ERROR); } // 刷新验证码的时候置缓存中的随机地址无效 String path = redisService.get(SkKeyPrefix.SK_PATH, "" + user.getUuid() + "_" + goodsId, String.class); if (path != null) redisService.delete(SkKeyPrefix.SK_PATH, "" + user.getUuid() + "_" + goodsId); // 创建验证码 try { // String verifyCodeJsonString = seckillService.createVerifyCode(user, goodsId); VerifyCodeVo verifyCode = VerifyCodeUtil.createVerifyCode(); // 验证码结果预先存到redis中 redisService.set(SkKeyPrefix.VERIFY_RESULT, user.getUuid() + "_" + goodsId, verifyCode.getExpResult()); ServletOutputStream out = response.getOutputStream(); // 将图片写入到resp对象中 ImageIO.write(verifyCode.getImage(), "JPEG", out); out.close(); out.flush(); return null; } catch (Exception e) { e.printStackTrace(); return Result.error(CodeMsg.SECKILL_FAIL); } } /** * 检验检验码的计算结果 * * @param user * @param goodsId * @param verifyCode * @return */ private boolean checkVerifyCode(UserVo user, long goodsId, int verifyCode) { if (user == null || goodsId <= 0) { return false; } // 从redis中获取验证码计算结果 Integer oldCode = redisService.get(SkKeyPrefix.VERIFY_RESULT, user.getUuid() + "_" + goodsId, Integer.class); if (oldCode == null || oldCode - verifyCode != 0) {// !!!!!! return false; } // 如果校验成功,则说明校验码过期,删除校验码缓存,防止重复提交同一个验证码 redisService.delete(SkKeyPrefix.VERIFY_RESULT, user.getUuid() + "_" + goodsId); return true; } /** * 创建秒杀地址, 并将其存储在redis中 * * @param user * @param goodsId * @return */ public String createSkPath(UserVo user, long goodsId) { if (user == null || goodsId <= 0) { return null; } // 随机生成秒杀地址 String path = MD5Util.md5(UUIDUtil.uuid() + "123456"); // 将随机生成的秒杀地址存储在redis中(保证不同的用户和不同商品的秒杀地址是不一样的) redisService.set(SkKeyPrefix.SK_PATH, "" + user.getUuid() + "_" + goodsId, path); return path; } /** * 验证路径是否正确 * * @param user * @param goodsId * @param path * @return */ public boolean checkPath(UserVo user, long goodsId, String path) { if (user == null || path == null) return false; // 从redis中读取出秒杀的path变量是否为本次秒杀操作执行前写入redis中的path String oldPath = redisService.get(SkKeyPrefix.SK_PATH, "" + user.getUuid() + "_" + goodsId, String.class); return path.equals(oldPath); } /** * 服务器程序启动的时候加载商品列表信息 */ @Override public void afterPropertiesSet() { List goods = goodsService.listGoodsVo(); if (goods == null) { return; } // 将商品的库存信息存储在redis中 for (GoodsVo good : goods) { redisService.set(GoodsKeyPrefix.GOODS_STOCK, "" + good.getId(), good.getStockCount()); // 在系统启动时,标记库存不为空 localOverMap.put(good.getId(), false); } } } ================================================ FILE: dis-seckill-gateway/src/main/java/com/seckill/dis/gateway/user/UserController.java ================================================ package com.seckill.dis.gateway.user; import com.seckill.dis.common.api.cache.vo.SkUserKeyPrefix; import com.seckill.dis.common.api.user.UserServiceApi; import com.seckill.dis.common.api.user.vo.LoginVo; import com.seckill.dis.common.api.user.vo.RegisterVo; import com.seckill.dis.common.api.user.vo.UserVo; import com.seckill.dis.common.result.CodeMsg; import com.seckill.dis.common.result.Result; import com.seckill.dis.common.util.MD5Util; import com.seckill.dis.gateway.exception.GlobalException; import org.apache.dubbo.config.annotation.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; /** * 用户接口 * * @author noodle */ @Controller @RequestMapping("/user/") public class UserController { /** * 日志记录:Logger是由slf4j接口规范创建的,对象有具体的实现类创建 */ private static Logger logger = LoggerFactory.getLogger(UserController.class); @Reference(interfaceClass = UserServiceApi.class) UserServiceApi userService; /** * 由于需要将一个cookie对应的用户存入第三方缓存中,这里用redis,所以需要引入redis serice */ // @Reference(interfaceClass = RedisServiceApi.class) // RedisServiceApi redisService; /** * 首页 * * @return */ @RequestMapping(value = "index", method = RequestMethod.GET) public String index() { logger.info("首页接口"); return "login";// login页面 } /** * 用户登录接口 * * @param response 响应 * @param loginVo 用户登录请求的表单数据(将表单数据封装为了一个Vo:Value Object) * 注解@Valid用于校验表单参数,校验成功才会继续执行业务逻辑,否则, * 请求参数校验不成功抛出异常 * @return */ @RequestMapping(value = "login", method = RequestMethod.POST) @ResponseBody public Result login(HttpServletResponse response, @Valid LoginVo loginVo) { String token = userService.login(loginVo); logger.info("token: " + token); // 将token写入cookie中, 然后传给客户端(一个cookie对应一个用户,这里将这个cookie的用户信息写入redis中) Cookie cookie = new Cookie(UserServiceApi.COOKIE_NAME_TOKEN, token); cookie.setMaxAge(SkUserKeyPrefix.TOKEN.expireSeconds());// 保持与redis中的session一致 cookie.setPath("/"); response.addCookie(cookie); // 返回登陆成功 return Result.success(true); } /** * 注册跳转 * * @return */ @RequestMapping(value = "doRegister", method = RequestMethod.GET) public String doRegister() { logger.info("doRegister()"); return "register"; } /** * 注册接口 * * @param registerVo * @return */ @RequestMapping(value = "register", method = RequestMethod.POST) @ResponseBody public Result register(RegisterVo registerVo) { logger.info("RegisterVo = " + registerVo); if (registerVo == null) { throw new GlobalException(CodeMsg.FILL_REGISTER_INFO); } CodeMsg codeMsg = userService.register(registerVo); return Result.info(codeMsg); } } ================================================ FILE: dis-seckill-gateway/src/main/resources/application.properties ================================================ #--------------------------------- # web #--------------------------------- server.port=8082 #--------------------------------- # thymeleaf ģ #--------------------------------- spring.thymeleaf.enabled=true spring.thymeleaf.cache=false spring.thymeleaf.servlet.content-type=text/html spring.thymeleaf.encoding=UTF-8 spring.thymeleaf.mode=HTML5 spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html #--------------------------------- #ҳ澲̬static #--------------------------------- spring.resources.add-mappings=true spring.resources.cache.period=3600s spring.resources.chain.cache=true spring.resources.chain.enabled=true spring.resources.chain.compressed=true spring.resources.chain.html-application-cache=true spring.resources.static-locations=classpath:/static/ #--------------------------------- # dubbo #--------------------------------- # Spring boot application spring.application.name=dis-seckill-gateway # Base packages to scan Dubbo Component: @org.apache.dubbo.config.annotation.Service #dubbo.scan.base-packages=com.example.bootdubboprovider # Dubbo Application ## The default value of dubbo.application.name is ${spring.application.name} ## dubbo.application.name=${spring.application.name} # Dubbo Protocol #dubbo.protocol.name=dubbo #dubbo.protocol.port=12345 ## Dubbo Registry embedded.zookeeper.port=2181 dubbo.registry.address=zookeeper://127.0.0.1:${embedded.zookeeper.port} ## DemoService version #demo.service.version=1.0.0 ================================================ FILE: dis-seckill-gateway/src/main/resources/static/bootstrap/css/bootstrap-theme.css ================================================ /*! * Bootstrap v3.3.7 (http://getbootstrap.com) * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ .btn-default, .btn-primary, .btn-success, .btn-info, .btn-warning, .btn-danger { text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); } .btn-default:active, .btn-primary:active, .btn-success:active, .btn-info:active, .btn-warning:active, .btn-danger:active, .btn-default.active, .btn-primary.active, .btn-success.active, .btn-info.active, .btn-warning.active, .btn-danger.active { -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); } .btn-default.disabled, .btn-primary.disabled, .btn-success.disabled, .btn-info.disabled, .btn-warning.disabled, .btn-danger.disabled, .btn-default[disabled], .btn-primary[disabled], .btn-success[disabled], .btn-info[disabled], .btn-warning[disabled], .btn-danger[disabled], fieldset[disabled] .btn-default, fieldset[disabled] .btn-primary, fieldset[disabled] .btn-success, fieldset[disabled] .btn-info, fieldset[disabled] .btn-warning, fieldset[disabled] .btn-danger { -webkit-box-shadow: none; box-shadow: none; } .btn-default .badge, .btn-primary .badge, .btn-success .badge, .btn-info .badge, .btn-warning .badge, .btn-danger .badge { text-shadow: none; } .btn:active, .btn.active { background-image: none; } .btn-default { text-shadow: 0 1px 0 #fff; background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); background-repeat: repeat-x; border-color: #dbdbdb; border-color: #ccc; } .btn-default:hover, .btn-default:focus { background-color: #e0e0e0; background-position: 0 -15px; } .btn-default:active, .btn-default.active { background-color: #e0e0e0; border-color: #dbdbdb; } .btn-default.disabled, .btn-default[disabled], fieldset[disabled] .btn-default, .btn-default.disabled:hover, .btn-default[disabled]:hover, fieldset[disabled] .btn-default:hover, .btn-default.disabled:focus, .btn-default[disabled]:focus, fieldset[disabled] .btn-default:focus, .btn-default.disabled.focus, .btn-default[disabled].focus, fieldset[disabled] .btn-default.focus, .btn-default.disabled:active, .btn-default[disabled]:active, fieldset[disabled] .btn-default:active, .btn-default.disabled.active, .btn-default[disabled].active, fieldset[disabled] .btn-default.active { background-color: #e0e0e0; background-image: none; } .btn-primary { background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%); background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88)); background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); background-repeat: repeat-x; border-color: #245580; } .btn-primary:hover, .btn-primary:focus { background-color: #265a88; background-position: 0 -15px; } .btn-primary:active, .btn-primary.active { background-color: #265a88; border-color: #245580; } .btn-primary.disabled, .btn-primary[disabled], fieldset[disabled] .btn-primary, .btn-primary.disabled:hover, .btn-primary[disabled]:hover, fieldset[disabled] .btn-primary:hover, .btn-primary.disabled:focus, .btn-primary[disabled]:focus, fieldset[disabled] .btn-primary:focus, .btn-primary.disabled.focus, .btn-primary[disabled].focus, fieldset[disabled] .btn-primary.focus, .btn-primary.disabled:active, .btn-primary[disabled]:active, fieldset[disabled] .btn-primary:active, .btn-primary.disabled.active, .btn-primary[disabled].active, fieldset[disabled] .btn-primary.active { background-color: #265a88; background-image: none; } .btn-success { background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); background-repeat: repeat-x; border-color: #3e8f3e; } .btn-success:hover, .btn-success:focus { background-color: #419641; background-position: 0 -15px; } .btn-success:active, .btn-success.active { background-color: #419641; border-color: #3e8f3e; } .btn-success.disabled, .btn-success[disabled], fieldset[disabled] .btn-success, .btn-success.disabled:hover, .btn-success[disabled]:hover, fieldset[disabled] .btn-success:hover, .btn-success.disabled:focus, .btn-success[disabled]:focus, fieldset[disabled] .btn-success:focus, .btn-success.disabled.focus, .btn-success[disabled].focus, fieldset[disabled] .btn-success.focus, .btn-success.disabled:active, .btn-success[disabled]:active, fieldset[disabled] .btn-success:active, .btn-success.disabled.active, .btn-success[disabled].active, fieldset[disabled] .btn-success.active { background-color: #419641; background-image: none; } .btn-info { background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); background-repeat: repeat-x; border-color: #28a4c9; } .btn-info:hover, .btn-info:focus { background-color: #2aabd2; background-position: 0 -15px; } .btn-info:active, .btn-info.active { background-color: #2aabd2; border-color: #28a4c9; } .btn-info.disabled, .btn-info[disabled], fieldset[disabled] .btn-info, .btn-info.disabled:hover, .btn-info[disabled]:hover, fieldset[disabled] .btn-info:hover, .btn-info.disabled:focus, .btn-info[disabled]:focus, fieldset[disabled] .btn-info:focus, .btn-info.disabled.focus, .btn-info[disabled].focus, fieldset[disabled] .btn-info.focus, .btn-info.disabled:active, .btn-info[disabled]:active, fieldset[disabled] .btn-info:active, .btn-info.disabled.active, .btn-info[disabled].active, fieldset[disabled] .btn-info.active { background-color: #2aabd2; background-image: none; } .btn-warning { background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); background-repeat: repeat-x; border-color: #e38d13; } .btn-warning:hover, .btn-warning:focus { background-color: #eb9316; background-position: 0 -15px; } .btn-warning:active, .btn-warning.active { background-color: #eb9316; border-color: #e38d13; } .btn-warning.disabled, .btn-warning[disabled], fieldset[disabled] .btn-warning, .btn-warning.disabled:hover, .btn-warning[disabled]:hover, fieldset[disabled] .btn-warning:hover, .btn-warning.disabled:focus, .btn-warning[disabled]:focus, fieldset[disabled] .btn-warning:focus, .btn-warning.disabled.focus, .btn-warning[disabled].focus, fieldset[disabled] .btn-warning.focus, .btn-warning.disabled:active, .btn-warning[disabled]:active, fieldset[disabled] .btn-warning:active, .btn-warning.disabled.active, .btn-warning[disabled].active, fieldset[disabled] .btn-warning.active { background-color: #eb9316; background-image: none; } .btn-danger { background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); background-repeat: repeat-x; border-color: #b92c28; } .btn-danger:hover, .btn-danger:focus { background-color: #c12e2a; background-position: 0 -15px; } .btn-danger:active, .btn-danger.active { background-color: #c12e2a; border-color: #b92c28; } .btn-danger.disabled, .btn-danger[disabled], fieldset[disabled] .btn-danger, .btn-danger.disabled:hover, .btn-danger[disabled]:hover, fieldset[disabled] .btn-danger:hover, .btn-danger.disabled:focus, .btn-danger[disabled]:focus, fieldset[disabled] .btn-danger:focus, .btn-danger.disabled.focus, .btn-danger[disabled].focus, fieldset[disabled] .btn-danger.focus, .btn-danger.disabled:active, .btn-danger[disabled]:active, fieldset[disabled] .btn-danger:active, .btn-danger.disabled.active, .btn-danger[disabled].active, fieldset[disabled] .btn-danger.active { background-color: #c12e2a; background-image: none; } .thumbnail, .img-thumbnail { -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); box-shadow: 0 1px 2px rgba(0, 0, 0, .075); } .dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus { background-color: #e8e8e8; background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); background-repeat: repeat-x; } .dropdown-menu > .active > a, .dropdown-menu > .active > a:hover, .dropdown-menu > .active > a:focus { background-color: #2e6da4; background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); background-repeat: repeat-x; } .navbar-default { background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8)); background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); background-repeat: repeat-x; border-radius: 4px; -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); } .navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .active > a { background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2)); background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0); background-repeat: repeat-x; -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); } .navbar-brand, .navbar-nav > li > a { text-shadow: 0 1px 0 rgba(255, 255, 255, .25); } .navbar-inverse { background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); background-repeat: repeat-x; border-radius: 4px; } .navbar-inverse .navbar-nav > .open > a, .navbar-inverse .navbar-nav > .active > a { background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%); background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f)); background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0); background-repeat: repeat-x; -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); } .navbar-inverse .navbar-brand, .navbar-inverse .navbar-nav > li > a { text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); } .navbar-static-top, .navbar-fixed-top, .navbar-fixed-bottom { border-radius: 0; } @media (max-width: 767px) { .navbar .navbar-nav .open .dropdown-menu > .active > a, .navbar .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar .navbar-nav .open .dropdown-menu > .active > a:focus { color: #fff; background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); background-repeat: repeat-x; } } .alert { text-shadow: 0 1px 0 rgba(255, 255, 255, .2); -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); } .alert-success { background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); background-repeat: repeat-x; border-color: #b2dba1; } .alert-info { background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); background-repeat: repeat-x; border-color: #9acfea; } .alert-warning { background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); background-repeat: repeat-x; border-color: #f5e79e; } .alert-danger { background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); background-repeat: repeat-x; border-color: #dca7a7; } .progress { background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); background-repeat: repeat-x; } .progress-bar { background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%); background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090)); background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0); background-repeat: repeat-x; } .progress-bar-success { background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); background-repeat: repeat-x; } .progress-bar-info { background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); background-repeat: repeat-x; } .progress-bar-warning { background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); background-repeat: repeat-x; } .progress-bar-danger { background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); background-repeat: repeat-x; } .progress-bar-striped { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); } .list-group { border-radius: 4px; -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); box-shadow: 0 1px 2px rgba(0, 0, 0, .075); } .list-group-item.active, .list-group-item.active:hover, .list-group-item.active:focus { text-shadow: 0 -1px 0 #286090; background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%); background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a)); background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0); background-repeat: repeat-x; border-color: #2b669a; } .list-group-item.active .badge, .list-group-item.active:hover .badge, .list-group-item.active:focus .badge { text-shadow: none; } .panel { -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); box-shadow: 0 1px 2px rgba(0, 0, 0, .05); } .panel-default > .panel-heading { background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); background-repeat: repeat-x; } .panel-primary > .panel-heading { background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); background-repeat: repeat-x; } .panel-success > .panel-heading { background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); background-repeat: repeat-x; } .panel-info > .panel-heading { background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); background-repeat: repeat-x; } .panel-warning > .panel-heading { background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); background-repeat: repeat-x; } .panel-danger > .panel-heading { background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); background-repeat: repeat-x; } .well { background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); background-repeat: repeat-x; border-color: #dcdcdc; -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); } /*# sourceMappingURL=bootstrap-theme.css.map */ ================================================ FILE: dis-seckill-gateway/src/main/resources/static/bootstrap/css/bootstrap.css ================================================ /*! * Bootstrap v3.3.7 (http://getbootstrap.com) * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ html { font-family: sans-serif; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } body { margin: 0; } article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary { display: block; } audio, canvas, progress, video { display: inline-block; vertical-align: baseline; } audio:not([controls]) { display: none; height: 0; } [hidden], template { display: none; } a { background-color: transparent; } a:active, a:hover { outline: 0; } abbr[title] { border-bottom: 1px dotted; } b, strong { font-weight: bold; } dfn { font-style: italic; } h1 { margin: .67em 0; font-size: 2em; } mark { color: #000; background: #ff0; } small { font-size: 80%; } sub, sup { position: relative; font-size: 75%; line-height: 0; vertical-align: baseline; } sup { top: -.5em; } sub { bottom: -.25em; } img { border: 0; } svg:not(:root) { overflow: hidden; } figure { margin: 1em 40px; } hr { height: 0; -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box; } pre { overflow: auto; } code, kbd, pre, samp { font-family: monospace, monospace; font-size: 1em; } button, input, optgroup, select, textarea { margin: 0; font: inherit; color: inherit; } button { overflow: visible; } button, select { text-transform: none; } button, html input[type="button"], input[type="reset"], input[type="submit"] { -webkit-appearance: button; cursor: pointer; } button[disabled], html input[disabled] { cursor: default; } button::-moz-focus-inner, input::-moz-focus-inner { padding: 0; border: 0; } input { line-height: normal; } input[type="checkbox"], input[type="radio"] { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; padding: 0; } input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button { height: auto; } input[type="search"] { -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box; -webkit-appearance: textfield; } input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } fieldset { padding: .35em .625em .75em; margin: 0 2px; border: 1px solid #c0c0c0; } legend { padding: 0; border: 0; } textarea { overflow: auto; } optgroup { font-weight: bold; } table { border-spacing: 0; border-collapse: collapse; } td, th { padding: 0; } /*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ @media print { *, *:before, *:after { color: #000 !important; text-shadow: none !important; background: transparent !important; -webkit-box-shadow: none !important; box-shadow: none !important; } a, a:visited { text-decoration: underline; } a[href]:after { content: " (" attr(href) ")"; } abbr[title]:after { content: " (" attr(title) ")"; } a[href^="#"]:after, a[href^="javascript:"]:after { content: ""; } pre, blockquote { border: 1px solid #999; page-break-inside: avoid; } thead { display: table-header-group; } tr, img { page-break-inside: avoid; } img { max-width: 100% !important; } p, h2, h3 { orphans: 3; widows: 3; } h2, h3 { page-break-after: avoid; } .navbar { display: none; } .btn > .caret, .dropup > .btn > .caret { border-top-color: #000 !important; } .label { border: 1px solid #000; } .table { border-collapse: collapse !important; } .table td, .table th { background-color: #fff !important; } .table-bordered th, .table-bordered td { border: 1px solid #ddd !important; } } @font-face { font-family: 'Glyphicons Halflings'; src: url('../fonts/glyphicons-halflings-regular.eot'); src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); } .glyphicon { position: relative; top: 1px; display: inline-block; font-family: 'Glyphicons Halflings'; font-style: normal; font-weight: normal; line-height: 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .glyphicon-asterisk:before { content: "\002a"; } .glyphicon-plus:before { content: "\002b"; } .glyphicon-euro:before, .glyphicon-eur:before { content: "\20ac"; } .glyphicon-minus:before { content: "\2212"; } .glyphicon-cloud:before { content: "\2601"; } .glyphicon-envelope:before { content: "\2709"; } .glyphicon-pencil:before { content: "\270f"; } .glyphicon-glass:before { content: "\e001"; } .glyphicon-music:before { content: "\e002"; } .glyphicon-search:before { content: "\e003"; } .glyphicon-heart:before { content: "\e005"; } .glyphicon-star:before { content: "\e006"; } .glyphicon-star-empty:before { content: "\e007"; } .glyphicon-user:before { content: "\e008"; } .glyphicon-film:before { content: "\e009"; } .glyphicon-th-large:before { content: "\e010"; } .glyphicon-th:before { content: "\e011"; } .glyphicon-th-list:before { content: "\e012"; } .glyphicon-ok:before { content: "\e013"; } .glyphicon-remove:before { content: "\e014"; } .glyphicon-zoom-in:before { content: "\e015"; } .glyphicon-zoom-out:before { content: "\e016"; } .glyphicon-off:before { content: "\e017"; } .glyphicon-signal:before { content: "\e018"; } .glyphicon-cog:before { content: "\e019"; } .glyphicon-trash:before { content: "\e020"; } .glyphicon-home:before { content: "\e021"; } .glyphicon-file:before { content: "\e022"; } .glyphicon-time:before { content: "\e023"; } .glyphicon-road:before { content: "\e024"; } .glyphicon-download-alt:before { content: "\e025"; } .glyphicon-download:before { content: "\e026"; } .glyphicon-upload:before { content: "\e027"; } .glyphicon-inbox:before { content: "\e028"; } .glyphicon-play-circle:before { content: "\e029"; } .glyphicon-repeat:before { content: "\e030"; } .glyphicon-refresh:before { content: "\e031"; } .glyphicon-list-alt:before { content: "\e032"; } .glyphicon-lock:before { content: "\e033"; } .glyphicon-flag:before { content: "\e034"; } .glyphicon-headphones:before { content: "\e035"; } .glyphicon-volume-off:before { content: "\e036"; } .glyphicon-volume-down:before { content: "\e037"; } .glyphicon-volume-up:before { content: "\e038"; } .glyphicon-qrcode:before { content: "\e039"; } .glyphicon-barcode:before { content: "\e040"; } .glyphicon-tag:before { content: "\e041"; } .glyphicon-tags:before { content: "\e042"; } .glyphicon-book:before { content: "\e043"; } .glyphicon-bookmark:before { content: "\e044"; } .glyphicon-print:before { content: "\e045"; } .glyphicon-camera:before { content: "\e046"; } .glyphicon-font:before { content: "\e047"; } .glyphicon-bold:before { content: "\e048"; } .glyphicon-italic:before { content: "\e049"; } .glyphicon-text-height:before { content: "\e050"; } .glyphicon-text-width:before { content: "\e051"; } .glyphicon-align-left:before { content: "\e052"; } .glyphicon-align-center:before { content: "\e053"; } .glyphicon-align-right:before { content: "\e054"; } .glyphicon-align-justify:before { content: "\e055"; } .glyphicon-list:before { content: "\e056"; } .glyphicon-indent-left:before { content: "\e057"; } .glyphicon-indent-right:before { content: "\e058"; } .glyphicon-facetime-video:before { content: "\e059"; } .glyphicon-picture:before { content: "\e060"; } .glyphicon-map-marker:before { content: "\e062"; } .glyphicon-adjust:before { content: "\e063"; } .glyphicon-tint:before { content: "\e064"; } .glyphicon-edit:before { content: "\e065"; } .glyphicon-share:before { content: "\e066"; } .glyphicon-check:before { content: "\e067"; } .glyphicon-move:before { content: "\e068"; } .glyphicon-step-backward:before { content: "\e069"; } .glyphicon-fast-backward:before { content: "\e070"; } .glyphicon-backward:before { content: "\e071"; } .glyphicon-play:before { content: "\e072"; } .glyphicon-pause:before { content: "\e073"; } .glyphicon-stop:before { content: "\e074"; } .glyphicon-forward:before { content: "\e075"; } .glyphicon-fast-forward:before { content: "\e076"; } .glyphicon-step-forward:before { content: "\e077"; } .glyphicon-eject:before { content: "\e078"; } .glyphicon-chevron-left:before { content: "\e079"; } .glyphicon-chevron-right:before { content: "\e080"; } .glyphicon-plus-sign:before { content: "\e081"; } .glyphicon-minus-sign:before { content: "\e082"; } .glyphicon-remove-sign:before { content: "\e083"; } .glyphicon-ok-sign:before { content: "\e084"; } .glyphicon-question-sign:before { content: "\e085"; } .glyphicon-info-sign:before { content: "\e086"; } .glyphicon-screenshot:before { content: "\e087"; } .glyphicon-remove-circle:before { content: "\e088"; } .glyphicon-ok-circle:before { content: "\e089"; } .glyphicon-ban-circle:before { content: "\e090"; } .glyphicon-arrow-left:before { content: "\e091"; } .glyphicon-arrow-right:before { content: "\e092"; } .glyphicon-arrow-up:before { content: "\e093"; } .glyphicon-arrow-down:before { content: "\e094"; } .glyphicon-share-alt:before { content: "\e095"; } .glyphicon-resize-full:before { content: "\e096"; } .glyphicon-resize-small:before { content: "\e097"; } .glyphicon-exclamation-sign:before { content: "\e101"; } .glyphicon-gift:before { content: "\e102"; } .glyphicon-leaf:before { content: "\e103"; } .glyphicon-fire:before { content: "\e104"; } .glyphicon-eye-open:before { content: "\e105"; } .glyphicon-eye-close:before { content: "\e106"; } .glyphicon-warning-sign:before { content: "\e107"; } .glyphicon-plane:before { content: "\e108"; } .glyphicon-calendar:before { content: "\e109"; } .glyphicon-random:before { content: "\e110"; } .glyphicon-comment:before { content: "\e111"; } .glyphicon-magnet:before { content: "\e112"; } .glyphicon-chevron-up:before { content: "\e113"; } .glyphicon-chevron-down:before { content: "\e114"; } .glyphicon-retweet:before { content: "\e115"; } .glyphicon-shopping-cart:before { content: "\e116"; } .glyphicon-folder-close:before { content: "\e117"; } .glyphicon-folder-open:before { content: "\e118"; } .glyphicon-resize-vertical:before { content: "\e119"; } .glyphicon-resize-horizontal:before { content: "\e120"; } .glyphicon-hdd:before { content: "\e121"; } .glyphicon-bullhorn:before { content: "\e122"; } .glyphicon-bell:before { content: "\e123"; } .glyphicon-certificate:before { content: "\e124"; } .glyphicon-thumbs-up:before { content: "\e125"; } .glyphicon-thumbs-down:before { content: "\e126"; } .glyphicon-hand-right:before { content: "\e127"; } .glyphicon-hand-left:before { content: "\e128"; } .glyphicon-hand-up:before { content: "\e129"; } .glyphicon-hand-down:before { content: "\e130"; } .glyphicon-circle-arrow-right:before { content: "\e131"; } .glyphicon-circle-arrow-left:before { content: "\e132"; } .glyphicon-circle-arrow-up:before { content: "\e133"; } .glyphicon-circle-arrow-down:before { content: "\e134"; } .glyphicon-globe:before { content: "\e135"; } .glyphicon-wrench:before { content: "\e136"; } .glyphicon-tasks:before { content: "\e137"; } .glyphicon-filter:before { content: "\e138"; } .glyphicon-briefcase:before { content: "\e139"; } .glyphicon-fullscreen:before { content: "\e140"; } .glyphicon-dashboard:before { content: "\e141"; } .glyphicon-paperclip:before { content: "\e142"; } .glyphicon-heart-empty:before { content: "\e143"; } .glyphicon-link:before { content: "\e144"; } .glyphicon-phone:before { content: "\e145"; } .glyphicon-pushpin:before { content: "\e146"; } .glyphicon-usd:before { content: "\e148"; } .glyphicon-gbp:before { content: "\e149"; } .glyphicon-sort:before { content: "\e150"; } .glyphicon-sort-by-alphabet:before { content: "\e151"; } .glyphicon-sort-by-alphabet-alt:before { content: "\e152"; } .glyphicon-sort-by-order:before { content: "\e153"; } .glyphicon-sort-by-order-alt:before { content: "\e154"; } .glyphicon-sort-by-attributes:before { content: "\e155"; } .glyphicon-sort-by-attributes-alt:before { content: "\e156"; } .glyphicon-unchecked:before { content: "\e157"; } .glyphicon-expand:before { content: "\e158"; } .glyphicon-collapse-down:before { content: "\e159"; } .glyphicon-collapse-up:before { content: "\e160"; } .glyphicon-log-in:before { content: "\e161"; } .glyphicon-flash:before { content: "\e162"; } .glyphicon-log-out:before { content: "\e163"; } .glyphicon-new-window:before { content: "\e164"; } .glyphicon-record:before { content: "\e165"; } .glyphicon-save:before { content: "\e166"; } .glyphicon-open:before { content: "\e167"; } .glyphicon-saved:before { content: "\e168"; } .glyphicon-import:before { content: "\e169"; } .glyphicon-export:before { content: "\e170"; } .glyphicon-send:before { content: "\e171"; } .glyphicon-floppy-disk:before { content: "\e172"; } .glyphicon-floppy-saved:before { content: "\e173"; } .glyphicon-floppy-remove:before { content: "\e174"; } .glyphicon-floppy-save:before { content: "\e175"; } .glyphicon-floppy-open:before { content: "\e176"; } .glyphicon-credit-card:before { content: "\e177"; } .glyphicon-transfer:before { content: "\e178"; } .glyphicon-cutlery:before { content: "\e179"; } .glyphicon-header:before { content: "\e180"; } .glyphicon-compressed:before { content: "\e181"; } .glyphicon-earphone:before { content: "\e182"; } .glyphicon-phone-alt:before { content: "\e183"; } .glyphicon-tower:before { content: "\e184"; } .glyphicon-stats:before { content: "\e185"; } .glyphicon-sd-video:before { content: "\e186"; } .glyphicon-hd-video:before { content: "\e187"; } .glyphicon-subtitles:before { content: "\e188"; } .glyphicon-sound-stereo:before { content: "\e189"; } .glyphicon-sound-dolby:before { content: "\e190"; } .glyphicon-sound-5-1:before { content: "\e191"; } .glyphicon-sound-6-1:before { content: "\e192"; } .glyphicon-sound-7-1:before { content: "\e193"; } .glyphicon-copyright-mark:before { content: "\e194"; } .glyphicon-registration-mark:before { content: "\e195"; } .glyphicon-cloud-download:before { content: "\e197"; } .glyphicon-cloud-upload:before { content: "\e198"; } .glyphicon-tree-conifer:before { content: "\e199"; } .glyphicon-tree-deciduous:before { content: "\e200"; } .glyphicon-cd:before { content: "\e201"; } .glyphicon-save-file:before { content: "\e202"; } .glyphicon-open-file:before { content: "\e203"; } .glyphicon-level-up:before { content: "\e204"; } .glyphicon-copy:before { content: "\e205"; } .glyphicon-paste:before { content: "\e206"; } .glyphicon-alert:before { content: "\e209"; } .glyphicon-equalizer:before { content: "\e210"; } .glyphicon-king:before { content: "\e211"; } .glyphicon-queen:before { content: "\e212"; } .glyphicon-pawn:before { content: "\e213"; } .glyphicon-bishop:before { content: "\e214"; } .glyphicon-knight:before { content: "\e215"; } .glyphicon-baby-formula:before { content: "\e216"; } .glyphicon-tent:before { content: "\26fa"; } .glyphicon-blackboard:before { content: "\e218"; } .glyphicon-bed:before { content: "\e219"; } .glyphicon-apple:before { content: "\f8ff"; } .glyphicon-erase:before { content: "\e221"; } .glyphicon-hourglass:before { content: "\231b"; } .glyphicon-lamp:before { content: "\e223"; } .glyphicon-duplicate:before { content: "\e224"; } .glyphicon-piggy-bank:before { content: "\e225"; } .glyphicon-scissors:before { content: "\e226"; } .glyphicon-bitcoin:before { content: "\e227"; } .glyphicon-btc:before { content: "\e227"; } .glyphicon-xbt:before { content: "\e227"; } .glyphicon-yen:before { content: "\00a5"; } .glyphicon-jpy:before { content: "\00a5"; } .glyphicon-ruble:before { content: "\20bd"; } .glyphicon-rub:before { content: "\20bd"; } .glyphicon-scale:before { content: "\e230"; } .glyphicon-ice-lolly:before { content: "\e231"; } .glyphicon-ice-lolly-tasted:before { content: "\e232"; } .glyphicon-education:before { content: "\e233"; } .glyphicon-option-horizontal:before { content: "\e234"; } .glyphicon-option-vertical:before { content: "\e235"; } .glyphicon-menu-hamburger:before { content: "\e236"; } .glyphicon-modal-window:before { content: "\e237"; } .glyphicon-oil:before { content: "\e238"; } .glyphicon-grain:before { content: "\e239"; } .glyphicon-sunglasses:before { content: "\e240"; } .glyphicon-text-size:before { content: "\e241"; } .glyphicon-text-color:before { content: "\e242"; } .glyphicon-text-background:before { content: "\e243"; } .glyphicon-object-align-top:before { content: "\e244"; } .glyphicon-object-align-bottom:before { content: "\e245"; } .glyphicon-object-align-horizontal:before { content: "\e246"; } .glyphicon-object-align-left:before { content: "\e247"; } .glyphicon-object-align-vertical:before { content: "\e248"; } .glyphicon-object-align-right:before { content: "\e249"; } .glyphicon-triangle-right:before { content: "\e250"; } .glyphicon-triangle-left:before { content: "\e251"; } .glyphicon-triangle-bottom:before { content: "\e252"; } .glyphicon-triangle-top:before { content: "\e253"; } .glyphicon-console:before { content: "\e254"; } .glyphicon-superscript:before { content: "\e255"; } .glyphicon-subscript:before { content: "\e256"; } .glyphicon-menu-left:before { content: "\e257"; } .glyphicon-menu-right:before { content: "\e258"; } .glyphicon-menu-down:before { content: "\e259"; } .glyphicon-menu-up:before { content: "\e260"; } * { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } *:before, *:after { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } html { font-size: 10px; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } body { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.42857143; color: #333; background-color: #fff; } input, button, select, textarea { font-family: inherit; font-size: inherit; line-height: inherit; } a { color: #337ab7; text-decoration: none; } a:hover, a:focus { color: #23527c; text-decoration: underline; } a:focus { outline: 5px auto -webkit-focus-ring-color; outline-offset: -2px; } figure { margin: 0; } img { vertical-align: middle; } .img-responsive, .thumbnail > img, .thumbnail a > img, .carousel-inner > .item > img, .carousel-inner > .item > a > img { display: block; max-width: 100%; height: auto; } .img-rounded { border-radius: 6px; } .img-thumbnail { display: inline-block; max-width: 100%; height: auto; padding: 4px; line-height: 1.42857143; background-color: #fff; border: 1px solid #ddd; border-radius: 4px; -webkit-transition: all .2s ease-in-out; -o-transition: all .2s ease-in-out; transition: all .2s ease-in-out; } .img-circle { border-radius: 50%; } hr { margin-top: 20px; margin-bottom: 20px; border: 0; border-top: 1px solid #eee; } .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); border: 0; } .sr-only-focusable:active, .sr-only-focusable:focus { position: static; width: auto; height: auto; margin: 0; overflow: visible; clip: auto; } [role="button"] { cursor: pointer; } h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { font-family: inherit; font-weight: 500; line-height: 1.1; color: inherit; } h1 small, h2 small, h3 small, h4 small, h5 small, h6 small, .h1 small, .h2 small, .h3 small, .h4 small, .h5 small, .h6 small, h1 .small, h2 .small, h3 .small, h4 .small, h5 .small, h6 .small, .h1 .small, .h2 .small, .h3 .small, .h4 .small, .h5 .small, .h6 .small { font-weight: normal; line-height: 1; color: #777; } h1, .h1, h2, .h2, h3, .h3 { margin-top: 20px; margin-bottom: 10px; } h1 small, .h1 small, h2 small, .h2 small, h3 small, .h3 small, h1 .small, .h1 .small, h2 .small, .h2 .small, h3 .small, .h3 .small { font-size: 65%; } h4, .h4, h5, .h5, h6, .h6 { margin-top: 10px; margin-bottom: 10px; } h4 small, .h4 small, h5 small, .h5 small, h6 small, .h6 small, h4 .small, .h4 .small, h5 .small, .h5 .small, h6 .small, .h6 .small { font-size: 75%; } h1, .h1 { font-size: 36px; } h2, .h2 { font-size: 30px; } h3, .h3 { font-size: 24px; } h4, .h4 { font-size: 18px; } h5, .h5 { font-size: 14px; } h6, .h6 { font-size: 12px; } p { margin: 0 0 10px; } .lead { margin-bottom: 20px; font-size: 16px; font-weight: 300; line-height: 1.4; } @media (min-width: 768px) { .lead { font-size: 21px; } } small, .small { font-size: 85%; } mark, .mark { padding: .2em; background-color: #fcf8e3; } .text-left { text-align: left; } .text-right { text-align: right; } .text-center { text-align: center; } .text-justify { text-align: justify; } .text-nowrap { white-space: nowrap; } .text-lowercase { text-transform: lowercase; } .text-uppercase { text-transform: uppercase; } .text-capitalize { text-transform: capitalize; } .text-muted { color: #777; } .text-primary { color: #337ab7; } a.text-primary:hover, a.text-primary:focus { color: #286090; } .text-success { color: #3c763d; } a.text-success:hover, a.text-success:focus { color: #2b542c; } .text-info { color: #31708f; } a.text-info:hover, a.text-info:focus { color: #245269; } .text-warning { color: #8a6d3b; } a.text-warning:hover, a.text-warning:focus { color: #66512c; } .text-danger { color: #a94442; } a.text-danger:hover, a.text-danger:focus { color: #843534; } .bg-primary { color: #fff; background-color: #337ab7; } a.bg-primary:hover, a.bg-primary:focus { background-color: #286090; } .bg-success { background-color: #dff0d8; } a.bg-success:hover, a.bg-success:focus { background-color: #c1e2b3; } .bg-info { background-color: #d9edf7; } a.bg-info:hover, a.bg-info:focus { background-color: #afd9ee; } .bg-warning { background-color: #fcf8e3; } a.bg-warning:hover, a.bg-warning:focus { background-color: #f7ecb5; } .bg-danger { background-color: #f2dede; } a.bg-danger:hover, a.bg-danger:focus { background-color: #e4b9b9; } .page-header { padding-bottom: 9px; margin: 40px 0 20px; border-bottom: 1px solid #eee; } ul, ol { margin-top: 0; margin-bottom: 10px; } ul ul, ol ul, ul ol, ol ol { margin-bottom: 0; } .list-unstyled { padding-left: 0; list-style: none; } .list-inline { padding-left: 0; margin-left: -5px; list-style: none; } .list-inline > li { display: inline-block; padding-right: 5px; padding-left: 5px; } dl { margin-top: 0; margin-bottom: 20px; } dt, dd { line-height: 1.42857143; } dt { font-weight: bold; } dd { margin-left: 0; } @media (min-width: 768px) { .dl-horizontal dt { float: left; width: 160px; overflow: hidden; clear: left; text-align: right; text-overflow: ellipsis; white-space: nowrap; } .dl-horizontal dd { margin-left: 180px; } } abbr[title], abbr[data-original-title] { cursor: help; border-bottom: 1px dotted #777; } .initialism { font-size: 90%; text-transform: uppercase; } blockquote { padding: 10px 20px; margin: 0 0 20px; font-size: 17.5px; border-left: 5px solid #eee; } blockquote p:last-child, blockquote ul:last-child, blockquote ol:last-child { margin-bottom: 0; } blockquote footer, blockquote small, blockquote .small { display: block; font-size: 80%; line-height: 1.42857143; color: #777; } blockquote footer:before, blockquote small:before, blockquote .small:before { content: '\2014 \00A0'; } .blockquote-reverse, blockquote.pull-right { padding-right: 15px; padding-left: 0; text-align: right; border-right: 5px solid #eee; border-left: 0; } .blockquote-reverse footer:before, blockquote.pull-right footer:before, .blockquote-reverse small:before, blockquote.pull-right small:before, .blockquote-reverse .small:before, blockquote.pull-right .small:before { content: ''; } .blockquote-reverse footer:after, blockquote.pull-right footer:after, .blockquote-reverse small:after, blockquote.pull-right small:after, .blockquote-reverse .small:after, blockquote.pull-right .small:after { content: '\00A0 \2014'; } address { margin-bottom: 20px; font-style: normal; line-height: 1.42857143; } code, kbd, pre, samp { font-family: Menlo, Monaco, Consolas, "Courier New", monospace; } code { padding: 2px 4px; font-size: 90%; color: #c7254e; background-color: #f9f2f4; border-radius: 4px; } kbd { padding: 2px 4px; font-size: 90%; color: #fff; background-color: #333; border-radius: 3px; -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); } kbd kbd { padding: 0; font-size: 100%; font-weight: bold; -webkit-box-shadow: none; box-shadow: none; } pre { display: block; padding: 9.5px; margin: 0 0 10px; font-size: 13px; line-height: 1.42857143; color: #333; word-break: break-all; word-wrap: break-word; background-color: #f5f5f5; border: 1px solid #ccc; border-radius: 4px; } pre code { padding: 0; font-size: inherit; color: inherit; white-space: pre-wrap; background-color: transparent; border-radius: 0; } .pre-scrollable { max-height: 340px; overflow-y: scroll; } .container { padding-right: 15px; padding-left: 15px; margin-right: auto; margin-left: auto; } @media (min-width: 768px) { .container { width: 750px; } } @media (min-width: 992px) { .container { width: 970px; } } @media (min-width: 1200px) { .container { width: 1170px; } } .container-fluid { padding-right: 15px; padding-left: 15px; margin-right: auto; margin-left: auto; } .row { margin-right: -15px; margin-left: -15px; } .col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { position: relative; min-height: 1px; padding-right: 15px; padding-left: 15px; } .col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { float: left; } .col-xs-12 { width: 100%; } .col-xs-11 { width: 91.66666667%; } .col-xs-10 { width: 83.33333333%; } .col-xs-9 { width: 75%; } .col-xs-8 { width: 66.66666667%; } .col-xs-7 { width: 58.33333333%; } .col-xs-6 { width: 50%; } .col-xs-5 { width: 41.66666667%; } .col-xs-4 { width: 33.33333333%; } .col-xs-3 { width: 25%; } .col-xs-2 { width: 16.66666667%; } .col-xs-1 { width: 8.33333333%; } .col-xs-pull-12 { right: 100%; } .col-xs-pull-11 { right: 91.66666667%; } .col-xs-pull-10 { right: 83.33333333%; } .col-xs-pull-9 { right: 75%; } .col-xs-pull-8 { right: 66.66666667%; } .col-xs-pull-7 { right: 58.33333333%; } .col-xs-pull-6 { right: 50%; } .col-xs-pull-5 { right: 41.66666667%; } .col-xs-pull-4 { right: 33.33333333%; } .col-xs-pull-3 { right: 25%; } .col-xs-pull-2 { right: 16.66666667%; } .col-xs-pull-1 { right: 8.33333333%; } .col-xs-pull-0 { right: auto; } .col-xs-push-12 { left: 100%; } .col-xs-push-11 { left: 91.66666667%; } .col-xs-push-10 { left: 83.33333333%; } .col-xs-push-9 { left: 75%; } .col-xs-push-8 { left: 66.66666667%; } .col-xs-push-7 { left: 58.33333333%; } .col-xs-push-6 { left: 50%; } .col-xs-push-5 { left: 41.66666667%; } .col-xs-push-4 { left: 33.33333333%; } .col-xs-push-3 { left: 25%; } .col-xs-push-2 { left: 16.66666667%; } .col-xs-push-1 { left: 8.33333333%; } .col-xs-push-0 { left: auto; } .col-xs-offset-12 { margin-left: 100%; } .col-xs-offset-11 { margin-left: 91.66666667%; } .col-xs-offset-10 { margin-left: 83.33333333%; } .col-xs-offset-9 { margin-left: 75%; } .col-xs-offset-8 { margin-left: 66.66666667%; } .col-xs-offset-7 { margin-left: 58.33333333%; } .col-xs-offset-6 { margin-left: 50%; } .col-xs-offset-5 { margin-left: 41.66666667%; } .col-xs-offset-4 { margin-left: 33.33333333%; } .col-xs-offset-3 { margin-left: 25%; } .col-xs-offset-2 { margin-left: 16.66666667%; } .col-xs-offset-1 { margin-left: 8.33333333%; } .col-xs-offset-0 { margin-left: 0; } @media (min-width: 768px) { .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { float: left; } .col-sm-12 { width: 100%; } .col-sm-11 { width: 91.66666667%; } .col-sm-10 { width: 83.33333333%; } .col-sm-9 { width: 75%; } .col-sm-8 { width: 66.66666667%; } .col-sm-7 { width: 58.33333333%; } .col-sm-6 { width: 50%; } .col-sm-5 { width: 41.66666667%; } .col-sm-4 { width: 33.33333333%; } .col-sm-3 { width: 25%; } .col-sm-2 { width: 16.66666667%; } .col-sm-1 { width: 8.33333333%; } .col-sm-pull-12 { right: 100%; } .col-sm-pull-11 { right: 91.66666667%; } .col-sm-pull-10 { right: 83.33333333%; } .col-sm-pull-9 { right: 75%; } .col-sm-pull-8 { right: 66.66666667%; } .col-sm-pull-7 { right: 58.33333333%; } .col-sm-pull-6 { right: 50%; } .col-sm-pull-5 { right: 41.66666667%; } .col-sm-pull-4 { right: 33.33333333%; } .col-sm-pull-3 { right: 25%; } .col-sm-pull-2 { right: 16.66666667%; } .col-sm-pull-1 { right: 8.33333333%; } .col-sm-pull-0 { right: auto; } .col-sm-push-12 { left: 100%; } .col-sm-push-11 { left: 91.66666667%; } .col-sm-push-10 { left: 83.33333333%; } .col-sm-push-9 { left: 75%; } .col-sm-push-8 { left: 66.66666667%; } .col-sm-push-7 { left: 58.33333333%; } .col-sm-push-6 { left: 50%; } .col-sm-push-5 { left: 41.66666667%; } .col-sm-push-4 { left: 33.33333333%; } .col-sm-push-3 { left: 25%; } .col-sm-push-2 { left: 16.66666667%; } .col-sm-push-1 { left: 8.33333333%; } .col-sm-push-0 { left: auto; } .col-sm-offset-12 { margin-left: 100%; } .col-sm-offset-11 { margin-left: 91.66666667%; } .col-sm-offset-10 { margin-left: 83.33333333%; } .col-sm-offset-9 { margin-left: 75%; } .col-sm-offset-8 { margin-left: 66.66666667%; } .col-sm-offset-7 { margin-left: 58.33333333%; } .col-sm-offset-6 { margin-left: 50%; } .col-sm-offset-5 { margin-left: 41.66666667%; } .col-sm-offset-4 { margin-left: 33.33333333%; } .col-sm-offset-3 { margin-left: 25%; } .col-sm-offset-2 { margin-left: 16.66666667%; } .col-sm-offset-1 { margin-left: 8.33333333%; } .col-sm-offset-0 { margin-left: 0; } } @media (min-width: 992px) { .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { float: left; } .col-md-12 { width: 100%; } .col-md-11 { width: 91.66666667%; } .col-md-10 { width: 83.33333333%; } .col-md-9 { width: 75%; } .col-md-8 { width: 66.66666667%; } .col-md-7 { width: 58.33333333%; } .col-md-6 { width: 50%; } .col-md-5 { width: 41.66666667%; } .col-md-4 { width: 33.33333333%; } .col-md-3 { width: 25%; } .col-md-2 { width: 16.66666667%; } .col-md-1 { width: 8.33333333%; } .col-md-pull-12 { right: 100%; } .col-md-pull-11 { right: 91.66666667%; } .col-md-pull-10 { right: 83.33333333%; } .col-md-pull-9 { right: 75%; } .col-md-pull-8 { right: 66.66666667%; } .col-md-pull-7 { right: 58.33333333%; } .col-md-pull-6 { right: 50%; } .col-md-pull-5 { right: 41.66666667%; } .col-md-pull-4 { right: 33.33333333%; } .col-md-pull-3 { right: 25%; } .col-md-pull-2 { right: 16.66666667%; } .col-md-pull-1 { right: 8.33333333%; } .col-md-pull-0 { right: auto; } .col-md-push-12 { left: 100%; } .col-md-push-11 { left: 91.66666667%; } .col-md-push-10 { left: 83.33333333%; } .col-md-push-9 { left: 75%; } .col-md-push-8 { left: 66.66666667%; } .col-md-push-7 { left: 58.33333333%; } .col-md-push-6 { left: 50%; } .col-md-push-5 { left: 41.66666667%; } .col-md-push-4 { left: 33.33333333%; } .col-md-push-3 { left: 25%; } .col-md-push-2 { left: 16.66666667%; } .col-md-push-1 { left: 8.33333333%; } .col-md-push-0 { left: auto; } .col-md-offset-12 { margin-left: 100%; } .col-md-offset-11 { margin-left: 91.66666667%; } .col-md-offset-10 { margin-left: 83.33333333%; } .col-md-offset-9 { margin-left: 75%; } .col-md-offset-8 { margin-left: 66.66666667%; } .col-md-offset-7 { margin-left: 58.33333333%; } .col-md-offset-6 { margin-left: 50%; } .col-md-offset-5 { margin-left: 41.66666667%; } .col-md-offset-4 { margin-left: 33.33333333%; } .col-md-offset-3 { margin-left: 25%; } .col-md-offset-2 { margin-left: 16.66666667%; } .col-md-offset-1 { margin-left: 8.33333333%; } .col-md-offset-0 { margin-left: 0; } } @media (min-width: 1200px) { .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { float: left; } .col-lg-12 { width: 100%; } .col-lg-11 { width: 91.66666667%; } .col-lg-10 { width: 83.33333333%; } .col-lg-9 { width: 75%; } .col-lg-8 { width: 66.66666667%; } .col-lg-7 { width: 58.33333333%; } .col-lg-6 { width: 50%; } .col-lg-5 { width: 41.66666667%; } .col-lg-4 { width: 33.33333333%; } .col-lg-3 { width: 25%; } .col-lg-2 { width: 16.66666667%; } .col-lg-1 { width: 8.33333333%; } .col-lg-pull-12 { right: 100%; } .col-lg-pull-11 { right: 91.66666667%; } .col-lg-pull-10 { right: 83.33333333%; } .col-lg-pull-9 { right: 75%; } .col-lg-pull-8 { right: 66.66666667%; } .col-lg-pull-7 { right: 58.33333333%; } .col-lg-pull-6 { right: 50%; } .col-lg-pull-5 { right: 41.66666667%; } .col-lg-pull-4 { right: 33.33333333%; } .col-lg-pull-3 { right: 25%; } .col-lg-pull-2 { right: 16.66666667%; } .col-lg-pull-1 { right: 8.33333333%; } .col-lg-pull-0 { right: auto; } .col-lg-push-12 { left: 100%; } .col-lg-push-11 { left: 91.66666667%; } .col-lg-push-10 { left: 83.33333333%; } .col-lg-push-9 { left: 75%; } .col-lg-push-8 { left: 66.66666667%; } .col-lg-push-7 { left: 58.33333333%; } .col-lg-push-6 { left: 50%; } .col-lg-push-5 { left: 41.66666667%; } .col-lg-push-4 { left: 33.33333333%; } .col-lg-push-3 { left: 25%; } .col-lg-push-2 { left: 16.66666667%; } .col-lg-push-1 { left: 8.33333333%; } .col-lg-push-0 { left: auto; } .col-lg-offset-12 { margin-left: 100%; } .col-lg-offset-11 { margin-left: 91.66666667%; } .col-lg-offset-10 { margin-left: 83.33333333%; } .col-lg-offset-9 { margin-left: 75%; } .col-lg-offset-8 { margin-left: 66.66666667%; } .col-lg-offset-7 { margin-left: 58.33333333%; } .col-lg-offset-6 { margin-left: 50%; } .col-lg-offset-5 { margin-left: 41.66666667%; } .col-lg-offset-4 { margin-left: 33.33333333%; } .col-lg-offset-3 { margin-left: 25%; } .col-lg-offset-2 { margin-left: 16.66666667%; } .col-lg-offset-1 { margin-left: 8.33333333%; } .col-lg-offset-0 { margin-left: 0; } } table { background-color: transparent; } caption { padding-top: 8px; padding-bottom: 8px; color: #777; text-align: left; } th { text-align: left; } .table { width: 100%; max-width: 100%; margin-bottom: 20px; } .table > thead > tr > th, .table > tbody > tr > th, .table > tfoot > tr > th, .table > thead > tr > td, .table > tbody > tr > td, .table > tfoot > tr > td { padding: 8px; line-height: 1.42857143; vertical-align: top; border-top: 1px solid #ddd; } .table > thead > tr > th { vertical-align: bottom; border-bottom: 2px solid #ddd; } .table > caption + thead > tr:first-child > th, .table > colgroup + thead > tr:first-child > th, .table > thead:first-child > tr:first-child > th, .table > caption + thead > tr:first-child > td, .table > colgroup + thead > tr:first-child > td, .table > thead:first-child > tr:first-child > td { border-top: 0; } .table > tbody + tbody { border-top: 2px solid #ddd; } .table .table { background-color: #fff; } .table-condensed > thead > tr > th, .table-condensed > tbody > tr > th, .table-condensed > tfoot > tr > th, .table-condensed > thead > tr > td, .table-condensed > tbody > tr > td, .table-condensed > tfoot > tr > td { padding: 5px; } .table-bordered { border: 1px solid #ddd; } .table-bordered > thead > tr > th, .table-bordered > tbody > tr > th, .table-bordered > tfoot > tr > th, .table-bordered > thead > tr > td, .table-bordered > tbody > tr > td, .table-bordered > tfoot > tr > td { border: 1px solid #ddd; } .table-bordered > thead > tr > th, .table-bordered > thead > tr > td { border-bottom-width: 2px; } .table-striped > tbody > tr:nth-of-type(odd) { background-color: #f9f9f9; } .table-hover > tbody > tr:hover { background-color: #f5f5f5; } table col[class*="col-"] { position: static; display: table-column; float: none; } table td[class*="col-"], table th[class*="col-"] { position: static; display: table-cell; float: none; } .table > thead > tr > td.active, .table > tbody > tr > td.active, .table > tfoot > tr > td.active, .table > thead > tr > th.active, .table > tbody > tr > th.active, .table > tfoot > tr > th.active, .table > thead > tr.active > td, .table > tbody > tr.active > td, .table > tfoot > tr.active > td, .table > thead > tr.active > th, .table > tbody > tr.active > th, .table > tfoot > tr.active > th { background-color: #f5f5f5; } .table-hover > tbody > tr > td.active:hover, .table-hover > tbody > tr > th.active:hover, .table-hover > tbody > tr.active:hover > td, .table-hover > tbody > tr:hover > .active, .table-hover > tbody > tr.active:hover > th { background-color: #e8e8e8; } .table > thead > tr > td.success, .table > tbody > tr > td.success, .table > tfoot > tr > td.success, .table > thead > tr > th.success, .table > tbody > tr > th.success, .table > tfoot > tr > th.success, .table > thead > tr.success > td, .table > tbody > tr.success > td, .table > tfoot > tr.success > td, .table > thead > tr.success > th, .table > tbody > tr.success > th, .table > tfoot > tr.success > th { background-color: #dff0d8; } .table-hover > tbody > tr > td.success:hover, .table-hover > tbody > tr > th.success:hover, .table-hover > tbody > tr.success:hover > td, .table-hover > tbody > tr:hover > .success, .table-hover > tbody > tr.success:hover > th { background-color: #d0e9c6; } .table > thead > tr > td.info, .table > tbody > tr > td.info, .table > tfoot > tr > td.info, .table > thead > tr > th.info, .table > tbody > tr > th.info, .table > tfoot > tr > th.info, .table > thead > tr.info > td, .table > tbody > tr.info > td, .table > tfoot > tr.info > td, .table > thead > tr.info > th, .table > tbody > tr.info > th, .table > tfoot > tr.info > th { background-color: #d9edf7; } .table-hover > tbody > tr > td.info:hover, .table-hover > tbody > tr > th.info:hover, .table-hover > tbody > tr.info:hover > td, .table-hover > tbody > tr:hover > .info, .table-hover > tbody > tr.info:hover > th { background-color: #c4e3f3; } .table > thead > tr > td.warning, .table > tbody > tr > td.warning, .table > tfoot > tr > td.warning, .table > thead > tr > th.warning, .table > tbody > tr > th.warning, .table > tfoot > tr > th.warning, .table > thead > tr.warning > td, .table > tbody > tr.warning > td, .table > tfoot > tr.warning > td, .table > thead > tr.warning > th, .table > tbody > tr.warning > th, .table > tfoot > tr.warning > th { background-color: #fcf8e3; } .table-hover > tbody > tr > td.warning:hover, .table-hover > tbody > tr > th.warning:hover, .table-hover > tbody > tr.warning:hover > td, .table-hover > tbody > tr:hover > .warning, .table-hover > tbody > tr.warning:hover > th { background-color: #faf2cc; } .table > thead > tr > td.danger, .table > tbody > tr > td.danger, .table > tfoot > tr > td.danger, .table > thead > tr > th.danger, .table > tbody > tr > th.danger, .table > tfoot > tr > th.danger, .table > thead > tr.danger > td, .table > tbody > tr.danger > td, .table > tfoot > tr.danger > td, .table > thead > tr.danger > th, .table > tbody > tr.danger > th, .table > tfoot > tr.danger > th { background-color: #f2dede; } .table-hover > tbody > tr > td.danger:hover, .table-hover > tbody > tr > th.danger:hover, .table-hover > tbody > tr.danger:hover > td, .table-hover > tbody > tr:hover > .danger, .table-hover > tbody > tr.danger:hover > th { background-color: #ebcccc; } .table-responsive { min-height: .01%; overflow-x: auto; } @media screen and (max-width: 767px) { .table-responsive { width: 100%; margin-bottom: 15px; overflow-y: hidden; -ms-overflow-style: -ms-autohiding-scrollbar; border: 1px solid #ddd; } .table-responsive > .table { margin-bottom: 0; } .table-responsive > .table > thead > tr > th, .table-responsive > .table > tbody > tr > th, .table-responsive > .table > tfoot > tr > th, .table-responsive > .table > thead > tr > td, .table-responsive > .table > tbody > tr > td, .table-responsive > .table > tfoot > tr > td { white-space: nowrap; } .table-responsive > .table-bordered { border: 0; } .table-responsive > .table-bordered > thead > tr > th:first-child, .table-responsive > .table-bordered > tbody > tr > th:first-child, .table-responsive > .table-bordered > tfoot > tr > th:first-child, .table-responsive > .table-bordered > thead > tr > td:first-child, .table-responsive > .table-bordered > tbody > tr > td:first-child, .table-responsive > .table-bordered > tfoot > tr > td:first-child { border-left: 0; } .table-responsive > .table-bordered > thead > tr > th:last-child, .table-responsive > .table-bordered > tbody > tr > th:last-child, .table-responsive > .table-bordered > tfoot > tr > th:last-child, .table-responsive > .table-bordered > thead > tr > td:last-child, .table-responsive > .table-bordered > tbody > tr > td:last-child, .table-responsive > .table-bordered > tfoot > tr > td:last-child { border-right: 0; } .table-responsive > .table-bordered > tbody > tr:last-child > th, .table-responsive > .table-bordered > tfoot > tr:last-child > th, .table-responsive > .table-bordered > tbody > tr:last-child > td, .table-responsive > .table-bordered > tfoot > tr:last-child > td { border-bottom: 0; } } fieldset { min-width: 0; padding: 0; margin: 0; border: 0; } legend { display: block; width: 100%; padding: 0; margin-bottom: 20px; font-size: 21px; line-height: inherit; color: #333; border: 0; border-bottom: 1px solid #e5e5e5; } label { display: inline-block; max-width: 100%; margin-bottom: 5px; font-weight: bold; } input[type="search"] { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } input[type="radio"], input[type="checkbox"] { margin: 4px 0 0; margin-top: 1px \9; line-height: normal; } input[type="file"] { display: block; } input[type="range"] { display: block; width: 100%; } select[multiple], select[size] { height: auto; } input[type="file"]:focus, input[type="radio"]:focus, input[type="checkbox"]:focus { outline: 5px auto -webkit-focus-ring-color; outline-offset: -2px; } output { display: block; padding-top: 7px; font-size: 14px; line-height: 1.42857143; color: #555; } .form-control { display: block; width: 100%; height: 34px; padding: 6px 12px; font-size: 14px; line-height: 1.42857143; color: #555; background-color: #fff; background-image: none; border: 1px solid #ccc; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; } .form-control:focus { border-color: #66afe9; outline: 0; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); } .form-control::-moz-placeholder { color: #999; opacity: 1; } .form-control:-ms-input-placeholder { color: #999; } .form-control::-webkit-input-placeholder { color: #999; } .form-control::-ms-expand { background-color: transparent; border: 0; } .form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control { background-color: #eee; opacity: 1; } .form-control[disabled], fieldset[disabled] .form-control { cursor: not-allowed; } textarea.form-control { height: auto; } input[type="search"] { -webkit-appearance: none; } @media screen and (-webkit-min-device-pixel-ratio: 0) { input[type="date"].form-control, input[type="time"].form-control, input[type="datetime-local"].form-control, input[type="month"].form-control { line-height: 34px; } input[type="date"].input-sm, input[type="time"].input-sm, input[type="datetime-local"].input-sm, input[type="month"].input-sm, .input-group-sm input[type="date"], .input-group-sm input[type="time"], .input-group-sm input[type="datetime-local"], .input-group-sm input[type="month"] { line-height: 30px; } input[type="date"].input-lg, input[type="time"].input-lg, input[type="datetime-local"].input-lg, input[type="month"].input-lg, .input-group-lg input[type="date"], .input-group-lg input[type="time"], .input-group-lg input[type="datetime-local"], .input-group-lg input[type="month"] { line-height: 46px; } } .form-group { margin-bottom: 15px; } .radio, .checkbox { position: relative; display: block; margin-top: 10px; margin-bottom: 10px; } .radio label, .checkbox label { min-height: 20px; padding-left: 20px; margin-bottom: 0; font-weight: normal; cursor: pointer; } .radio input[type="radio"], .radio-inline input[type="radio"], .checkbox input[type="checkbox"], .checkbox-inline input[type="checkbox"] { position: absolute; margin-top: 4px \9; margin-left: -20px; } .radio + .radio, .checkbox + .checkbox { margin-top: -5px; } .radio-inline, .checkbox-inline { position: relative; display: inline-block; padding-left: 20px; margin-bottom: 0; font-weight: normal; vertical-align: middle; cursor: pointer; } .radio-inline + .radio-inline, .checkbox-inline + .checkbox-inline { margin-top: 0; margin-left: 10px; } input[type="radio"][disabled], input[type="checkbox"][disabled], input[type="radio"].disabled, input[type="checkbox"].disabled, fieldset[disabled] input[type="radio"], fieldset[disabled] input[type="checkbox"] { cursor: not-allowed; } .radio-inline.disabled, .checkbox-inline.disabled, fieldset[disabled] .radio-inline, fieldset[disabled] .checkbox-inline { cursor: not-allowed; } .radio.disabled label, .checkbox.disabled label, fieldset[disabled] .radio label, fieldset[disabled] .checkbox label { cursor: not-allowed; } .form-control-static { min-height: 34px; padding-top: 7px; padding-bottom: 7px; margin-bottom: 0; } .form-control-static.input-lg, .form-control-static.input-sm { padding-right: 0; padding-left: 0; } .input-sm { height: 30px; padding: 5px 10px; font-size: 12px; line-height: 1.5; border-radius: 3px; } select.input-sm { height: 30px; line-height: 30px; } textarea.input-sm, select[multiple].input-sm { height: auto; } .form-group-sm .form-control { height: 30px; padding: 5px 10px; font-size: 12px; line-height: 1.5; border-radius: 3px; } .form-group-sm select.form-control { height: 30px; line-height: 30px; } .form-group-sm textarea.form-control, .form-group-sm select[multiple].form-control { height: auto; } .form-group-sm .form-control-static { height: 30px; min-height: 32px; padding: 6px 10px; font-size: 12px; line-height: 1.5; } .input-lg { height: 46px; padding: 10px 16px; font-size: 18px; line-height: 1.3333333; border-radius: 6px; } select.input-lg { height: 46px; line-height: 46px; } textarea.input-lg, select[multiple].input-lg { height: auto; } .form-group-lg .form-control { height: 46px; padding: 10px 16px; font-size: 18px; line-height: 1.3333333; border-radius: 6px; } .form-group-lg select.form-control { height: 46px; line-height: 46px; } .form-group-lg textarea.form-control, .form-group-lg select[multiple].form-control { height: auto; } .form-group-lg .form-control-static { height: 46px; min-height: 38px; padding: 11px 16px; font-size: 18px; line-height: 1.3333333; } .has-feedback { position: relative; } .has-feedback .form-control { padding-right: 42.5px; } .form-control-feedback { position: absolute; top: 0; right: 0; z-index: 2; display: block; width: 34px; height: 34px; line-height: 34px; text-align: center; pointer-events: none; } .input-lg + .form-control-feedback, .input-group-lg + .form-control-feedback, .form-group-lg .form-control + .form-control-feedback { width: 46px; height: 46px; line-height: 46px; } .input-sm + .form-control-feedback, .input-group-sm + .form-control-feedback, .form-group-sm .form-control + .form-control-feedback { width: 30px; height: 30px; line-height: 30px; } .has-success .help-block, .has-success .control-label, .has-success .radio, .has-success .checkbox, .has-success .radio-inline, .has-success .checkbox-inline, .has-success.radio label, .has-success.checkbox label, .has-success.radio-inline label, .has-success.checkbox-inline label { color: #3c763d; } .has-success .form-control { border-color: #3c763d; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); } .has-success .form-control:focus { border-color: #2b542c; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; } .has-success .input-group-addon { color: #3c763d; background-color: #dff0d8; border-color: #3c763d; } .has-success .form-control-feedback { color: #3c763d; } .has-warning .help-block, .has-warning .control-label, .has-warning .radio, .has-warning .checkbox, .has-warning .radio-inline, .has-warning .checkbox-inline, .has-warning.radio label, .has-warning.checkbox label, .has-warning.radio-inline label, .has-warning.checkbox-inline label { color: #8a6d3b; } .has-warning .form-control { border-color: #8a6d3b; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); } .has-warning .form-control:focus { border-color: #66512c; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; } .has-warning .input-group-addon { color: #8a6d3b; background-color: #fcf8e3; border-color: #8a6d3b; } .has-warning .form-control-feedback { color: #8a6d3b; } .has-error .help-block, .has-error .control-label, .has-error .radio, .has-error .checkbox, .has-error .radio-inline, .has-error .checkbox-inline, .has-error.radio label, .has-error.checkbox label, .has-error.radio-inline label, .has-error.checkbox-inline label { color: #a94442; } .has-error .form-control { border-color: #a94442; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); } .has-error .form-control:focus { border-color: #843534; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; } .has-error .input-group-addon { color: #a94442; background-color: #f2dede; border-color: #a94442; } .has-error .form-control-feedback { color: #a94442; } .has-feedback label ~ .form-control-feedback { top: 25px; } .has-feedback label.sr-only ~ .form-control-feedback { top: 0; } .help-block { display: block; margin-top: 5px; margin-bottom: 10px; color: #737373; } @media (min-width: 768px) { .form-inline .form-group { display: inline-block; margin-bottom: 0; vertical-align: middle; } .form-inline .form-control { display: inline-block; width: auto; vertical-align: middle; } .form-inline .form-control-static { display: inline-block; } .form-inline .input-group { display: inline-table; vertical-align: middle; } .form-inline .input-group .input-group-addon, .form-inline .input-group .input-group-btn, .form-inline .input-group .form-control { width: auto; } .form-inline .input-group > .form-control { width: 100%; } .form-inline .control-label { margin-bottom: 0; vertical-align: middle; } .form-inline .radio, .form-inline .checkbox { display: inline-block; margin-top: 0; margin-bottom: 0; vertical-align: middle; } .form-inline .radio label, .form-inline .checkbox label { padding-left: 0; } .form-inline .radio input[type="radio"], .form-inline .checkbox input[type="checkbox"] { position: relative; margin-left: 0; } .form-inline .has-feedback .form-control-feedback { top: 0; } } .form-horizontal .radio, .form-horizontal .checkbox, .form-horizontal .radio-inline, .form-horizontal .checkbox-inline { padding-top: 7px; margin-top: 0; margin-bottom: 0; } .form-horizontal .radio, .form-horizontal .checkbox { min-height: 27px; } .form-horizontal .form-group { margin-right: -15px; margin-left: -15px; } @media (min-width: 768px) { .form-horizontal .control-label { padding-top: 7px; margin-bottom: 0; text-align: right; } } .form-horizontal .has-feedback .form-control-feedback { right: 15px; } @media (min-width: 768px) { .form-horizontal .form-group-lg .control-label { padding-top: 11px; font-size: 18px; } } @media (min-width: 768px) { .form-horizontal .form-group-sm .control-label { padding-top: 6px; font-size: 12px; } } .btn { display: inline-block; padding: 6px 12px; margin-bottom: 0; font-size: 14px; font-weight: normal; line-height: 1.42857143; text-align: center; white-space: nowrap; vertical-align: middle; -ms-touch-action: manipulation; touch-action: manipulation; cursor: pointer; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; background-image: none; border: 1px solid transparent; border-radius: 4px; } .btn:focus, .btn:active:focus, .btn.active:focus, .btn.focus, .btn:active.focus, .btn.active.focus { outline: 5px auto -webkit-focus-ring-color; outline-offset: -2px; } .btn:hover, .btn:focus, .btn.focus { color: #333; text-decoration: none; } .btn:active, .btn.active { background-image: none; outline: 0; -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); } .btn.disabled, .btn[disabled], fieldset[disabled] .btn { cursor: not-allowed; filter: alpha(opacity=65); -webkit-box-shadow: none; box-shadow: none; opacity: .65; } a.btn.disabled, fieldset[disabled] a.btn { pointer-events: none; } .btn-default { color: #333; background-color: #fff; border-color: #ccc; } .btn-default:focus, .btn-default.focus { color: #333; background-color: #e6e6e6; border-color: #8c8c8c; } .btn-default:hover { color: #333; background-color: #e6e6e6; border-color: #adadad; } .btn-default:active, .btn-default.active, .open > .dropdown-toggle.btn-default { color: #333; background-color: #e6e6e6; border-color: #adadad; } .btn-default:active:hover, .btn-default.active:hover, .open > .dropdown-toggle.btn-default:hover, .btn-default:active:focus, .btn-default.active:focus, .open > .dropdown-toggle.btn-default:focus, .btn-default:active.focus, .btn-default.active.focus, .open > .dropdown-toggle.btn-default.focus { color: #333; background-color: #d4d4d4; border-color: #8c8c8c; } .btn-default:active, .btn-default.active, .open > .dropdown-toggle.btn-default { background-image: none; } .btn-default.disabled:hover, .btn-default[disabled]:hover, fieldset[disabled] .btn-default:hover, .btn-default.disabled:focus, .btn-default[disabled]:focus, fieldset[disabled] .btn-default:focus, .btn-default.disabled.focus, .btn-default[disabled].focus, fieldset[disabled] .btn-default.focus { background-color: #fff; border-color: #ccc; } .btn-default .badge { color: #fff; background-color: #333; } .btn-primary { color: #fff; background-color: #337ab7; border-color: #2e6da4; } .btn-primary:focus, .btn-primary.focus { color: #fff; background-color: #286090; border-color: #122b40; } .btn-primary:hover { color: #fff; background-color: #286090; border-color: #204d74; } .btn-primary:active, .btn-primary.active, .open > .dropdown-toggle.btn-primary { color: #fff; background-color: #286090; border-color: #204d74; } .btn-primary:active:hover, .btn-primary.active:hover, .open > .dropdown-toggle.btn-primary:hover, .btn-primary:active:focus, .btn-primary.active:focus, .open > .dropdown-toggle.btn-primary:focus, .btn-primary:active.focus, .btn-primary.active.focus, .open > .dropdown-toggle.btn-primary.focus { color: #fff; background-color: #204d74; border-color: #122b40; } .btn-primary:active, .btn-primary.active, .open > .dropdown-toggle.btn-primary { background-image: none; } .btn-primary.disabled:hover, .btn-primary[disabled]:hover, fieldset[disabled] .btn-primary:hover, .btn-primary.disabled:focus, .btn-primary[disabled]:focus, fieldset[disabled] .btn-primary:focus, .btn-primary.disabled.focus, .btn-primary[disabled].focus, fieldset[disabled] .btn-primary.focus { background-color: #337ab7; border-color: #2e6da4; } .btn-primary .badge { color: #337ab7; background-color: #fff; } .btn-success { color: #fff; background-color: #5cb85c; border-color: #4cae4c; } .btn-success:focus, .btn-success.focus { color: #fff; background-color: #449d44; border-color: #255625; } .btn-success:hover { color: #fff; background-color: #449d44; border-color: #398439; } .btn-success:active, .btn-success.active, .open > .dropdown-toggle.btn-success { color: #fff; background-color: #449d44; border-color: #398439; } .btn-success:active:hover, .btn-success.active:hover, .open > .dropdown-toggle.btn-success:hover, .btn-success:active:focus, .btn-success.active:focus, .open > .dropdown-toggle.btn-success:focus, .btn-success:active.focus, .btn-success.active.focus, .open > .dropdown-toggle.btn-success.focus { color: #fff; background-color: #398439; border-color: #255625; } .btn-success:active, .btn-success.active, .open > .dropdown-toggle.btn-success { background-image: none; } .btn-success.disabled:hover, .btn-success[disabled]:hover, fieldset[disabled] .btn-success:hover, .btn-success.disabled:focus, .btn-success[disabled]:focus, fieldset[disabled] .btn-success:focus, .btn-success.disabled.focus, .btn-success[disabled].focus, fieldset[disabled] .btn-success.focus { background-color: #5cb85c; border-color: #4cae4c; } .btn-success .badge { color: #5cb85c; background-color: #fff; } .btn-info { color: #fff; background-color: #5bc0de; border-color: #46b8da; } .btn-info:focus, .btn-info.focus { color: #fff; background-color: #31b0d5; border-color: #1b6d85; } .btn-info:hover { color: #fff; background-color: #31b0d5; border-color: #269abc; } .btn-info:active, .btn-info.active, .open > .dropdown-toggle.btn-info { color: #fff; background-color: #31b0d5; border-color: #269abc; } .btn-info:active:hover, .btn-info.active:hover, .open > .dropdown-toggle.btn-info:hover, .btn-info:active:focus, .btn-info.active:focus, .open > .dropdown-toggle.btn-info:focus, .btn-info:active.focus, .btn-info.active.focus, .open > .dropdown-toggle.btn-info.focus { color: #fff; background-color: #269abc; border-color: #1b6d85; } .btn-info:active, .btn-info.active, .open > .dropdown-toggle.btn-info { background-image: none; } .btn-info.disabled:hover, .btn-info[disabled]:hover, fieldset[disabled] .btn-info:hover, .btn-info.disabled:focus, .btn-info[disabled]:focus, fieldset[disabled] .btn-info:focus, .btn-info.disabled.focus, .btn-info[disabled].focus, fieldset[disabled] .btn-info.focus { background-color: #5bc0de; border-color: #46b8da; } .btn-info .badge { color: #5bc0de; background-color: #fff; } .btn-warning { color: #fff; background-color: #f0ad4e; border-color: #eea236; } .btn-warning:focus, .btn-warning.focus { color: #fff; background-color: #ec971f; border-color: #985f0d; } .btn-warning:hover { color: #fff; background-color: #ec971f; border-color: #d58512; } .btn-warning:active, .btn-warning.active, .open > .dropdown-toggle.btn-warning { color: #fff; background-color: #ec971f; border-color: #d58512; } .btn-warning:active:hover, .btn-warning.active:hover, .open > .dropdown-toggle.btn-warning:hover, .btn-warning:active:focus, .btn-warning.active:focus, .open > .dropdown-toggle.btn-warning:focus, .btn-warning:active.focus, .btn-warning.active.focus, .open > .dropdown-toggle.btn-warning.focus { color: #fff; background-color: #d58512; border-color: #985f0d; } .btn-warning:active, .btn-warning.active, .open > .dropdown-toggle.btn-warning { background-image: none; } .btn-warning.disabled:hover, .btn-warning[disabled]:hover, fieldset[disabled] .btn-warning:hover, .btn-warning.disabled:focus, .btn-warning[disabled]:focus, fieldset[disabled] .btn-warning:focus, .btn-warning.disabled.focus, .btn-warning[disabled].focus, fieldset[disabled] .btn-warning.focus { background-color: #f0ad4e; border-color: #eea236; } .btn-warning .badge { color: #f0ad4e; background-color: #fff; } .btn-danger { color: #fff; background-color: #d9534f; border-color: #d43f3a; } .btn-danger:focus, .btn-danger.focus { color: #fff; background-color: #c9302c; border-color: #761c19; } .btn-danger:hover { color: #fff; background-color: #c9302c; border-color: #ac2925; } .btn-danger:active, .btn-danger.active, .open > .dropdown-toggle.btn-danger { color: #fff; background-color: #c9302c; border-color: #ac2925; } .btn-danger:active:hover, .btn-danger.active:hover, .open > .dropdown-toggle.btn-danger:hover, .btn-danger:active:focus, .btn-danger.active:focus, .open > .dropdown-toggle.btn-danger:focus, .btn-danger:active.focus, .btn-danger.active.focus, .open > .dropdown-toggle.btn-danger.focus { color: #fff; background-color: #ac2925; border-color: #761c19; } .btn-danger:active, .btn-danger.active, .open > .dropdown-toggle.btn-danger { background-image: none; } .btn-danger.disabled:hover, .btn-danger[disabled]:hover, fieldset[disabled] .btn-danger:hover, .btn-danger.disabled:focus, .btn-danger[disabled]:focus, fieldset[disabled] .btn-danger:focus, .btn-danger.disabled.focus, .btn-danger[disabled].focus, fieldset[disabled] .btn-danger.focus { background-color: #d9534f; border-color: #d43f3a; } .btn-danger .badge { color: #d9534f; background-color: #fff; } .btn-link { font-weight: normal; color: #337ab7; border-radius: 0; } .btn-link, .btn-link:active, .btn-link.active, .btn-link[disabled], fieldset[disabled] .btn-link { background-color: transparent; -webkit-box-shadow: none; box-shadow: none; } .btn-link, .btn-link:hover, .btn-link:focus, .btn-link:active { border-color: transparent; } .btn-link:hover, .btn-link:focus { color: #23527c; text-decoration: underline; background-color: transparent; } .btn-link[disabled]:hover, fieldset[disabled] .btn-link:hover, .btn-link[disabled]:focus, fieldset[disabled] .btn-link:focus { color: #777; text-decoration: none; } .btn-lg, .btn-group-lg > .btn { padding: 10px 16px; font-size: 18px; line-height: 1.3333333; border-radius: 6px; } .btn-sm, .btn-group-sm > .btn { padding: 5px 10px; font-size: 12px; line-height: 1.5; border-radius: 3px; } .btn-xs, .btn-group-xs > .btn { padding: 1px 5px; font-size: 12px; line-height: 1.5; border-radius: 3px; } .btn-block { display: block; width: 100%; } .btn-block + .btn-block { margin-top: 5px; } input[type="submit"].btn-block, input[type="reset"].btn-block, input[type="button"].btn-block { width: 100%; } .fade { opacity: 0; -webkit-transition: opacity .15s linear; -o-transition: opacity .15s linear; transition: opacity .15s linear; } .fade.in { opacity: 1; } .collapse { display: none; } .collapse.in { display: block; } tr.collapse.in { display: table-row; } tbody.collapse.in { display: table-row-group; } .collapsing { position: relative; height: 0; overflow: hidden; -webkit-transition-timing-function: ease; -o-transition-timing-function: ease; transition-timing-function: ease; -webkit-transition-duration: .35s; -o-transition-duration: .35s; transition-duration: .35s; -webkit-transition-property: height, visibility; -o-transition-property: height, visibility; transition-property: height, visibility; } .caret { display: inline-block; width: 0; height: 0; margin-left: 2px; vertical-align: middle; border-top: 4px dashed; border-top: 4px solid \9; border-right: 4px solid transparent; border-left: 4px solid transparent; } .dropup, .dropdown { position: relative; } .dropdown-toggle:focus { outline: 0; } .dropdown-menu { position: absolute; top: 100%; left: 0; z-index: 1000; display: none; float: left; min-width: 160px; padding: 5px 0; margin: 2px 0 0; font-size: 14px; text-align: left; list-style: none; background-color: #fff; -webkit-background-clip: padding-box; background-clip: padding-box; border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, .15); border-radius: 4px; -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175); box-shadow: 0 6px 12px rgba(0, 0, 0, .175); } .dropdown-menu.pull-right { right: 0; left: auto; } .dropdown-menu .divider { height: 1px; margin: 9px 0; overflow: hidden; background-color: #e5e5e5; } .dropdown-menu > li > a { display: block; padding: 3px 20px; clear: both; font-weight: normal; line-height: 1.42857143; color: #333; white-space: nowrap; } .dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus { color: #262626; text-decoration: none; background-color: #f5f5f5; } .dropdown-menu > .active > a, .dropdown-menu > .active > a:hover, .dropdown-menu > .active > a:focus { color: #fff; text-decoration: none; background-color: #337ab7; outline: 0; } .dropdown-menu > .disabled > a, .dropdown-menu > .disabled > a:hover, .dropdown-menu > .disabled > a:focus { color: #777; } .dropdown-menu > .disabled > a:hover, .dropdown-menu > .disabled > a:focus { text-decoration: none; cursor: not-allowed; background-color: transparent; background-image: none; filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); } .open > .dropdown-menu { display: block; } .open > a { outline: 0; } .dropdown-menu-right { right: 0; left: auto; } .dropdown-menu-left { right: auto; left: 0; } .dropdown-header { display: block; padding: 3px 20px; font-size: 12px; line-height: 1.42857143; color: #777; white-space: nowrap; } .dropdown-backdrop { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 990; } .pull-right > .dropdown-menu { right: 0; left: auto; } .dropup .caret, .navbar-fixed-bottom .dropdown .caret { content: ""; border-top: 0; border-bottom: 4px dashed; border-bottom: 4px solid \9; } .dropup .dropdown-menu, .navbar-fixed-bottom .dropdown .dropdown-menu { top: auto; bottom: 100%; margin-bottom: 2px; } @media (min-width: 768px) { .navbar-right .dropdown-menu { right: 0; left: auto; } .navbar-right .dropdown-menu-left { right: auto; left: 0; } } .btn-group, .btn-group-vertical { position: relative; display: inline-block; vertical-align: middle; } .btn-group > .btn, .btn-group-vertical > .btn { position: relative; float: left; } .btn-group > .btn:hover, .btn-group-vertical > .btn:hover, .btn-group > .btn:focus, .btn-group-vertical > .btn:focus, .btn-group > .btn:active, .btn-group-vertical > .btn:active, .btn-group > .btn.active, .btn-group-vertical > .btn.active { z-index: 2; } .btn-group .btn + .btn, .btn-group .btn + .btn-group, .btn-group .btn-group + .btn, .btn-group .btn-group + .btn-group { margin-left: -1px; } .btn-toolbar { margin-left: -5px; } .btn-toolbar .btn, .btn-toolbar .btn-group, .btn-toolbar .input-group { float: left; } .btn-toolbar > .btn, .btn-toolbar > .btn-group, .btn-toolbar > .input-group { margin-left: 5px; } .btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { border-radius: 0; } .btn-group > .btn:first-child { margin-left: 0; } .btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { border-top-right-radius: 0; border-bottom-right-radius: 0; } .btn-group > .btn:last-child:not(:first-child), .btn-group > .dropdown-toggle:not(:first-child) { border-top-left-radius: 0; border-bottom-left-radius: 0; } .btn-group > .btn-group { float: left; } .btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { border-radius: 0; } .btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, .btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { border-top-right-radius: 0; border-bottom-right-radius: 0; } .btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { border-top-left-radius: 0; border-bottom-left-radius: 0; } .btn-group .dropdown-toggle:active, .btn-group.open .dropdown-toggle { outline: 0; } .btn-group > .btn + .dropdown-toggle { padding-right: 8px; padding-left: 8px; } .btn-group > .btn-lg + .dropdown-toggle { padding-right: 12px; padding-left: 12px; } .btn-group.open .dropdown-toggle { -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); } .btn-group.open .dropdown-toggle.btn-link { -webkit-box-shadow: none; box-shadow: none; } .btn .caret { margin-left: 0; } .btn-lg .caret { border-width: 5px 5px 0; border-bottom-width: 0; } .dropup .btn-lg .caret { border-width: 0 5px 5px; } .btn-group-vertical > .btn, .btn-group-vertical > .btn-group, .btn-group-vertical > .btn-group > .btn { display: block; float: none; width: 100%; max-width: 100%; } .btn-group-vertical > .btn-group > .btn { float: none; } .btn-group-vertical > .btn + .btn, .btn-group-vertical > .btn + .btn-group, .btn-group-vertical > .btn-group + .btn, .btn-group-vertical > .btn-group + .btn-group { margin-top: -1px; margin-left: 0; } .btn-group-vertical > .btn:not(:first-child):not(:last-child) { border-radius: 0; } .btn-group-vertical > .btn:first-child:not(:last-child) { border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .btn-group-vertical > .btn:last-child:not(:first-child) { border-top-left-radius: 0; border-top-right-radius: 0; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; } .btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { border-radius: 0; } .btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, .btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { border-top-left-radius: 0; border-top-right-radius: 0; } .btn-group-justified { display: table; width: 100%; table-layout: fixed; border-collapse: separate; } .btn-group-justified > .btn, .btn-group-justified > .btn-group { display: table-cell; float: none; width: 1%; } .btn-group-justified > .btn-group .btn { width: 100%; } .btn-group-justified > .btn-group .dropdown-menu { left: auto; } [data-toggle="buttons"] > .btn input[type="radio"], [data-toggle="buttons"] > .btn-group > .btn input[type="radio"], [data-toggle="buttons"] > .btn input[type="checkbox"], [data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] { position: absolute; clip: rect(0, 0, 0, 0); pointer-events: none; } .input-group { position: relative; display: table; border-collapse: separate; } .input-group[class*="col-"] { float: none; padding-right: 0; padding-left: 0; } .input-group .form-control { position: relative; z-index: 2; float: left; width: 100%; margin-bottom: 0; } .input-group .form-control:focus { z-index: 3; } .input-group-lg > .form-control, .input-group-lg > .input-group-addon, .input-group-lg > .input-group-btn > .btn { height: 46px; padding: 10px 16px; font-size: 18px; line-height: 1.3333333; border-radius: 6px; } select.input-group-lg > .form-control, select.input-group-lg > .input-group-addon, select.input-group-lg > .input-group-btn > .btn { height: 46px; line-height: 46px; } textarea.input-group-lg > .form-control, textarea.input-group-lg > .input-group-addon, textarea.input-group-lg > .input-group-btn > .btn, select[multiple].input-group-lg > .form-control, select[multiple].input-group-lg > .input-group-addon, select[multiple].input-group-lg > .input-group-btn > .btn { height: auto; } .input-group-sm > .form-control, .input-group-sm > .input-group-addon, .input-group-sm > .input-group-btn > .btn { height: 30px; padding: 5px 10px; font-size: 12px; line-height: 1.5; border-radius: 3px; } select.input-group-sm > .form-control, select.input-group-sm > .input-group-addon, select.input-group-sm > .input-group-btn > .btn { height: 30px; line-height: 30px; } textarea.input-group-sm > .form-control, textarea.input-group-sm > .input-group-addon, textarea.input-group-sm > .input-group-btn > .btn, select[multiple].input-group-sm > .form-control, select[multiple].input-group-sm > .input-group-addon, select[multiple].input-group-sm > .input-group-btn > .btn { height: auto; } .input-group-addon, .input-group-btn, .input-group .form-control { display: table-cell; } .input-group-addon:not(:first-child):not(:last-child), .input-group-btn:not(:first-child):not(:last-child), .input-group .form-control:not(:first-child):not(:last-child) { border-radius: 0; } .input-group-addon, .input-group-btn { width: 1%; white-space: nowrap; vertical-align: middle; } .input-group-addon { padding: 6px 12px; font-size: 14px; font-weight: normal; line-height: 1; color: #555; text-align: center; background-color: #eee; border: 1px solid #ccc; border-radius: 4px; } .input-group-addon.input-sm { padding: 5px 10px; font-size: 12px; border-radius: 3px; } .input-group-addon.input-lg { padding: 10px 16px; font-size: 18px; border-radius: 6px; } .input-group-addon input[type="radio"], .input-group-addon input[type="checkbox"] { margin-top: 0; } .input-group .form-control:first-child, .input-group-addon:first-child, .input-group-btn:first-child > .btn, .input-group-btn:first-child > .btn-group > .btn, .input-group-btn:first-child > .dropdown-toggle, .input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), .input-group-btn:last-child > .btn-group:not(:last-child) > .btn { border-top-right-radius: 0; border-bottom-right-radius: 0; } .input-group-addon:first-child { border-right: 0; } .input-group .form-control:last-child, .input-group-addon:last-child, .input-group-btn:last-child > .btn, .input-group-btn:last-child > .btn-group > .btn, .input-group-btn:last-child > .dropdown-toggle, .input-group-btn:first-child > .btn:not(:first-child), .input-group-btn:first-child > .btn-group:not(:first-child) > .btn { border-top-left-radius: 0; border-bottom-left-radius: 0; } .input-group-addon:last-child { border-left: 0; } .input-group-btn { position: relative; font-size: 0; white-space: nowrap; } .input-group-btn > .btn { position: relative; } .input-group-btn > .btn + .btn { margin-left: -1px; } .input-group-btn > .btn:hover, .input-group-btn > .btn:focus, .input-group-btn > .btn:active { z-index: 2; } .input-group-btn:first-child > .btn, .input-group-btn:first-child > .btn-group { margin-right: -1px; } .input-group-btn:last-child > .btn, .input-group-btn:last-child > .btn-group { z-index: 2; margin-left: -1px; } .nav { padding-left: 0; margin-bottom: 0; list-style: none; } .nav > li { position: relative; display: block; } .nav > li > a { position: relative; display: block; padding: 10px 15px; } .nav > li > a:hover, .nav > li > a:focus { text-decoration: none; background-color: #eee; } .nav > li.disabled > a { color: #777; } .nav > li.disabled > a:hover, .nav > li.disabled > a:focus { color: #777; text-decoration: none; cursor: not-allowed; background-color: transparent; } .nav .open > a, .nav .open > a:hover, .nav .open > a:focus { background-color: #eee; border-color: #337ab7; } .nav .nav-divider { height: 1px; margin: 9px 0; overflow: hidden; background-color: #e5e5e5; } .nav > li > a > img { max-width: none; } .nav-tabs { border-bottom: 1px solid #ddd; } .nav-tabs > li { float: left; margin-bottom: -1px; } .nav-tabs > li > a { margin-right: 2px; line-height: 1.42857143; border: 1px solid transparent; border-radius: 4px 4px 0 0; } .nav-tabs > li > a:hover { border-color: #eee #eee #ddd; } .nav-tabs > li.active > a, .nav-tabs > li.active > a:hover, .nav-tabs > li.active > a:focus { color: #555; cursor: default; background-color: #fff; border: 1px solid #ddd; border-bottom-color: transparent; } .nav-tabs.nav-justified { width: 100%; border-bottom: 0; } .nav-tabs.nav-justified > li { float: none; } .nav-tabs.nav-justified > li > a { margin-bottom: 5px; text-align: center; } .nav-tabs.nav-justified > .dropdown .dropdown-menu { top: auto; left: auto; } @media (min-width: 768px) { .nav-tabs.nav-justified > li { display: table-cell; width: 1%; } .nav-tabs.nav-justified > li > a { margin-bottom: 0; } } .nav-tabs.nav-justified > li > a { margin-right: 0; border-radius: 4px; } .nav-tabs.nav-justified > .active > a, .nav-tabs.nav-justified > .active > a:hover, .nav-tabs.nav-justified > .active > a:focus { border: 1px solid #ddd; } @media (min-width: 768px) { .nav-tabs.nav-justified > li > a { border-bottom: 1px solid #ddd; border-radius: 4px 4px 0 0; } .nav-tabs.nav-justified > .active > a, .nav-tabs.nav-justified > .active > a:hover, .nav-tabs.nav-justified > .active > a:focus { border-bottom-color: #fff; } } .nav-pills > li { float: left; } .nav-pills > li > a { border-radius: 4px; } .nav-pills > li + li { margin-left: 2px; } .nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus { color: #fff; background-color: #337ab7; } .nav-stacked > li { float: none; } .nav-stacked > li + li { margin-top: 2px; margin-left: 0; } .nav-justified { width: 100%; } .nav-justified > li { float: none; } .nav-justified > li > a { margin-bottom: 5px; text-align: center; } .nav-justified > .dropdown .dropdown-menu { top: auto; left: auto; } @media (min-width: 768px) { .nav-justified > li { display: table-cell; width: 1%; } .nav-justified > li > a { margin-bottom: 0; } } .nav-tabs-justified { border-bottom: 0; } .nav-tabs-justified > li > a { margin-right: 0; border-radius: 4px; } .nav-tabs-justified > .active > a, .nav-tabs-justified > .active > a:hover, .nav-tabs-justified > .active > a:focus { border: 1px solid #ddd; } @media (min-width: 768px) { .nav-tabs-justified > li > a { border-bottom: 1px solid #ddd; border-radius: 4px 4px 0 0; } .nav-tabs-justified > .active > a, .nav-tabs-justified > .active > a:hover, .nav-tabs-justified > .active > a:focus { border-bottom-color: #fff; } } .tab-content > .tab-pane { display: none; } .tab-content > .active { display: block; } .nav-tabs .dropdown-menu { margin-top: -1px; border-top-left-radius: 0; border-top-right-radius: 0; } .navbar { position: relative; min-height: 50px; margin-bottom: 20px; border: 1px solid transparent; } @media (min-width: 768px) { .navbar { border-radius: 4px; } } @media (min-width: 768px) { .navbar-header { float: left; } } .navbar-collapse { padding-right: 15px; padding-left: 15px; overflow-x: visible; -webkit-overflow-scrolling: touch; border-top: 1px solid transparent; -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); } .navbar-collapse.in { overflow-y: auto; } @media (min-width: 768px) { .navbar-collapse { width: auto; border-top: 0; -webkit-box-shadow: none; box-shadow: none; } .navbar-collapse.collapse { display: block !important; height: auto !important; padding-bottom: 0; overflow: visible !important; } .navbar-collapse.in { overflow-y: visible; } .navbar-fixed-top .navbar-collapse, .navbar-static-top .navbar-collapse, .navbar-fixed-bottom .navbar-collapse { padding-right: 0; padding-left: 0; } } .navbar-fixed-top .navbar-collapse, .navbar-fixed-bottom .navbar-collapse { max-height: 340px; } @media (max-device-width: 480px) and (orientation: landscape) { .navbar-fixed-top .navbar-collapse, .navbar-fixed-bottom .navbar-collapse { max-height: 200px; } } .container > .navbar-header, .container-fluid > .navbar-header, .container > .navbar-collapse, .container-fluid > .navbar-collapse { margin-right: -15px; margin-left: -15px; } @media (min-width: 768px) { .container > .navbar-header, .container-fluid > .navbar-header, .container > .navbar-collapse, .container-fluid > .navbar-collapse { margin-right: 0; margin-left: 0; } } .navbar-static-top { z-index: 1000; border-width: 0 0 1px; } @media (min-width: 768px) { .navbar-static-top { border-radius: 0; } } .navbar-fixed-top, .navbar-fixed-bottom { position: fixed; right: 0; left: 0; z-index: 1030; } @media (min-width: 768px) { .navbar-fixed-top, .navbar-fixed-bottom { border-radius: 0; } } .navbar-fixed-top { top: 0; border-width: 0 0 1px; } .navbar-fixed-bottom { bottom: 0; margin-bottom: 0; border-width: 1px 0 0; } .navbar-brand { float: left; height: 50px; padding: 15px 15px; font-size: 18px; line-height: 20px; } .navbar-brand:hover, .navbar-brand:focus { text-decoration: none; } .navbar-brand > img { display: block; } @media (min-width: 768px) { .navbar > .container .navbar-brand, .navbar > .container-fluid .navbar-brand { margin-left: -15px; } } .navbar-toggle { position: relative; float: right; padding: 9px 10px; margin-top: 8px; margin-right: 15px; margin-bottom: 8px; background-color: transparent; background-image: none; border: 1px solid transparent; border-radius: 4px; } .navbar-toggle:focus { outline: 0; } .navbar-toggle .icon-bar { display: block; width: 22px; height: 2px; border-radius: 1px; } .navbar-toggle .icon-bar + .icon-bar { margin-top: 4px; } @media (min-width: 768px) { .navbar-toggle { display: none; } } .navbar-nav { margin: 7.5px -15px; } .navbar-nav > li > a { padding-top: 10px; padding-bottom: 10px; line-height: 20px; } @media (max-width: 767px) { .navbar-nav .open .dropdown-menu { position: static; float: none; width: auto; margin-top: 0; background-color: transparent; border: 0; -webkit-box-shadow: none; box-shadow: none; } .navbar-nav .open .dropdown-menu > li > a, .navbar-nav .open .dropdown-menu .dropdown-header { padding: 5px 15px 5px 25px; } .navbar-nav .open .dropdown-menu > li > a { line-height: 20px; } .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-nav .open .dropdown-menu > li > a:focus { background-image: none; } } @media (min-width: 768px) { .navbar-nav { float: left; margin: 0; } .navbar-nav > li { float: left; } .navbar-nav > li > a { padding-top: 15px; padding-bottom: 15px; } } .navbar-form { padding: 10px 15px; margin-top: 8px; margin-right: -15px; margin-bottom: 8px; margin-left: -15px; border-top: 1px solid transparent; border-bottom: 1px solid transparent; -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); } @media (min-width: 768px) { .navbar-form .form-group { display: inline-block; margin-bottom: 0; vertical-align: middle; } .navbar-form .form-control { display: inline-block; width: auto; vertical-align: middle; } .navbar-form .form-control-static { display: inline-block; } .navbar-form .input-group { display: inline-table; vertical-align: middle; } .navbar-form .input-group .input-group-addon, .navbar-form .input-group .input-group-btn, .navbar-form .input-group .form-control { width: auto; } .navbar-form .input-group > .form-control { width: 100%; } .navbar-form .control-label { margin-bottom: 0; vertical-align: middle; } .navbar-form .radio, .navbar-form .checkbox { display: inline-block; margin-top: 0; margin-bottom: 0; vertical-align: middle; } .navbar-form .radio label, .navbar-form .checkbox label { padding-left: 0; } .navbar-form .radio input[type="radio"], .navbar-form .checkbox input[type="checkbox"] { position: relative; margin-left: 0; } .navbar-form .has-feedback .form-control-feedback { top: 0; } } @media (max-width: 767px) { .navbar-form .form-group { margin-bottom: 5px; } .navbar-form .form-group:last-child { margin-bottom: 0; } } @media (min-width: 768px) { .navbar-form { width: auto; padding-top: 0; padding-bottom: 0; margin-right: 0; margin-left: 0; border: 0; -webkit-box-shadow: none; box-shadow: none; } } .navbar-nav > li > .dropdown-menu { margin-top: 0; border-top-left-radius: 0; border-top-right-radius: 0; } .navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { margin-bottom: 0; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .navbar-btn { margin-top: 8px; margin-bottom: 8px; } .navbar-btn.btn-sm { margin-top: 10px; margin-bottom: 10px; } .navbar-btn.btn-xs { margin-top: 14px; margin-bottom: 14px; } .navbar-text { margin-top: 15px; margin-bottom: 15px; } @media (min-width: 768px) { .navbar-text { float: left; margin-right: 15px; margin-left: 15px; } } @media (min-width: 768px) { .navbar-left { float: left !important; } .navbar-right { float: right !important; margin-right: -15px; } .navbar-right ~ .navbar-right { margin-right: 0; } } .navbar-default { background-color: #f8f8f8; border-color: #e7e7e7; } .navbar-default .navbar-brand { color: #777; } .navbar-default .navbar-brand:hover, .navbar-default .navbar-brand:focus { color: #5e5e5e; background-color: transparent; } .navbar-default .navbar-text { color: #777; } .navbar-default .navbar-nav > li > a { color: #777; } .navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus { color: #333; background-color: transparent; } .navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:focus { color: #555; background-color: #e7e7e7; } .navbar-default .navbar-nav > .disabled > a, .navbar-default .navbar-nav > .disabled > a:hover, .navbar-default .navbar-nav > .disabled > a:focus { color: #ccc; background-color: transparent; } .navbar-default .navbar-toggle { border-color: #ddd; } .navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus { background-color: #ddd; } .navbar-default .navbar-toggle .icon-bar { background-color: #888; } .navbar-default .navbar-collapse, .navbar-default .navbar-form { border-color: #e7e7e7; } .navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .open > a:hover, .navbar-default .navbar-nav > .open > a:focus { color: #555; background-color: #e7e7e7; } @media (max-width: 767px) { .navbar-default .navbar-nav .open .dropdown-menu > li > a { color: #777; } .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { color: #333; background-color: transparent; } .navbar-default .navbar-nav .open .dropdown-menu > .active > a, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { color: #555; background-color: #e7e7e7; } .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { color: #ccc; background-color: transparent; } } .navbar-default .navbar-link { color: #777; } .navbar-default .navbar-link:hover { color: #333; } .navbar-default .btn-link { color: #777; } .navbar-default .btn-link:hover, .navbar-default .btn-link:focus { color: #333; } .navbar-default .btn-link[disabled]:hover, fieldset[disabled] .navbar-default .btn-link:hover, .navbar-default .btn-link[disabled]:focus, fieldset[disabled] .navbar-default .btn-link:focus { color: #ccc; } .navbar-inverse { background-color: #222; border-color: #080808; } .navbar-inverse .navbar-brand { color: #9d9d9d; } .navbar-inverse .navbar-brand:hover, .navbar-inverse .navbar-brand:focus { color: #fff; background-color: transparent; } .navbar-inverse .navbar-text { color: #9d9d9d; } .navbar-inverse .navbar-nav > li > a { color: #9d9d9d; } .navbar-inverse .navbar-nav > li > a:hover, .navbar-inverse .navbar-nav > li > a:focus { color: #fff; background-color: transparent; } .navbar-inverse .navbar-nav > .active > a, .navbar-inverse .navbar-nav > .active > a:hover, .navbar-inverse .navbar-nav > .active > a:focus { color: #fff; background-color: #080808; } .navbar-inverse .navbar-nav > .disabled > a, .navbar-inverse .navbar-nav > .disabled > a:hover, .navbar-inverse .navbar-nav > .disabled > a:focus { color: #444; background-color: transparent; } .navbar-inverse .navbar-toggle { border-color: #333; } .navbar-inverse .navbar-toggle:hover, .navbar-inverse .navbar-toggle:focus { background-color: #333; } .navbar-inverse .navbar-toggle .icon-bar { background-color: #fff; } .navbar-inverse .navbar-collapse, .navbar-inverse .navbar-form { border-color: #101010; } .navbar-inverse .navbar-nav > .open > a, .navbar-inverse .navbar-nav > .open > a:hover, .navbar-inverse .navbar-nav > .open > a:focus { color: #fff; background-color: #080808; } @media (max-width: 767px) { .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { border-color: #080808; } .navbar-inverse .navbar-nav .open .dropdown-menu .divider { background-color: #080808; } .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { color: #9d9d9d; } .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { color: #fff; background-color: transparent; } .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { color: #fff; background-color: #080808; } .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { color: #444; background-color: transparent; } } .navbar-inverse .navbar-link { color: #9d9d9d; } .navbar-inverse .navbar-link:hover { color: #fff; } .navbar-inverse .btn-link { color: #9d9d9d; } .navbar-inverse .btn-link:hover, .navbar-inverse .btn-link:focus { color: #fff; } .navbar-inverse .btn-link[disabled]:hover, fieldset[disabled] .navbar-inverse .btn-link:hover, .navbar-inverse .btn-link[disabled]:focus, fieldset[disabled] .navbar-inverse .btn-link:focus { color: #444; } .breadcrumb { padding: 8px 15px; margin-bottom: 20px; list-style: none; background-color: #f5f5f5; border-radius: 4px; } .breadcrumb > li { display: inline-block; } .breadcrumb > li + li:before { padding: 0 5px; color: #ccc; content: "/\00a0"; } .breadcrumb > .active { color: #777; } .pagination { display: inline-block; padding-left: 0; margin: 20px 0; border-radius: 4px; } .pagination > li { display: inline; } .pagination > li > a, .pagination > li > span { position: relative; float: left; padding: 6px 12px; margin-left: -1px; line-height: 1.42857143; color: #337ab7; text-decoration: none; background-color: #fff; border: 1px solid #ddd; } .pagination > li:first-child > a, .pagination > li:first-child > span { margin-left: 0; border-top-left-radius: 4px; border-bottom-left-radius: 4px; } .pagination > li:last-child > a, .pagination > li:last-child > span { border-top-right-radius: 4px; border-bottom-right-radius: 4px; } .pagination > li > a:hover, .pagination > li > span:hover, .pagination > li > a:focus, .pagination > li > span:focus { z-index: 2; color: #23527c; background-color: #eee; border-color: #ddd; } .pagination > .active > a, .pagination > .active > span, .pagination > .active > a:hover, .pagination > .active > span:hover, .pagination > .active > a:focus, .pagination > .active > span:focus { z-index: 3; color: #fff; cursor: default; background-color: #337ab7; border-color: #337ab7; } .pagination > .disabled > span, .pagination > .disabled > span:hover, .pagination > .disabled > span:focus, .pagination > .disabled > a, .pagination > .disabled > a:hover, .pagination > .disabled > a:focus { color: #777; cursor: not-allowed; background-color: #fff; border-color: #ddd; } .pagination-lg > li > a, .pagination-lg > li > span { padding: 10px 16px; font-size: 18px; line-height: 1.3333333; } .pagination-lg > li:first-child > a, .pagination-lg > li:first-child > span { border-top-left-radius: 6px; border-bottom-left-radius: 6px; } .pagination-lg > li:last-child > a, .pagination-lg > li:last-child > span { border-top-right-radius: 6px; border-bottom-right-radius: 6px; } .pagination-sm > li > a, .pagination-sm > li > span { padding: 5px 10px; font-size: 12px; line-height: 1.5; } .pagination-sm > li:first-child > a, .pagination-sm > li:first-child > span { border-top-left-radius: 3px; border-bottom-left-radius: 3px; } .pagination-sm > li:last-child > a, .pagination-sm > li:last-child > span { border-top-right-radius: 3px; border-bottom-right-radius: 3px; } .pager { padding-left: 0; margin: 20px 0; text-align: center; list-style: none; } .pager li { display: inline; } .pager li > a, .pager li > span { display: inline-block; padding: 5px 14px; background-color: #fff; border: 1px solid #ddd; border-radius: 15px; } .pager li > a:hover, .pager li > a:focus { text-decoration: none; background-color: #eee; } .pager .next > a, .pager .next > span { float: right; } .pager .previous > a, .pager .previous > span { float: left; } .pager .disabled > a, .pager .disabled > a:hover, .pager .disabled > a:focus, .pager .disabled > span { color: #777; cursor: not-allowed; background-color: #fff; } .label { display: inline; padding: .2em .6em .3em; font-size: 75%; font-weight: bold; line-height: 1; color: #fff; text-align: center; white-space: nowrap; vertical-align: baseline; border-radius: .25em; } a.label:hover, a.label:focus { color: #fff; text-decoration: none; cursor: pointer; } .label:empty { display: none; } .btn .label { position: relative; top: -1px; } .label-default { background-color: #777; } .label-default[href]:hover, .label-default[href]:focus { background-color: #5e5e5e; } .label-primary { background-color: #337ab7; } .label-primary[href]:hover, .label-primary[href]:focus { background-color: #286090; } .label-success { background-color: #5cb85c; } .label-success[href]:hover, .label-success[href]:focus { background-color: #449d44; } .label-info { background-color: #5bc0de; } .label-info[href]:hover, .label-info[href]:focus { background-color: #31b0d5; } .label-warning { background-color: #f0ad4e; } .label-warning[href]:hover, .label-warning[href]:focus { background-color: #ec971f; } .label-danger { background-color: #d9534f; } .label-danger[href]:hover, .label-danger[href]:focus { background-color: #c9302c; } .badge { display: inline-block; min-width: 10px; padding: 3px 7px; font-size: 12px; font-weight: bold; line-height: 1; color: #fff; text-align: center; white-space: nowrap; vertical-align: middle; background-color: #777; border-radius: 10px; } .badge:empty { display: none; } .btn .badge { position: relative; top: -1px; } .btn-xs .badge, .btn-group-xs > .btn .badge { top: 0; padding: 1px 5px; } a.badge:hover, a.badge:focus { color: #fff; text-decoration: none; cursor: pointer; } .list-group-item.active > .badge, .nav-pills > .active > a > .badge { color: #337ab7; background-color: #fff; } .list-group-item > .badge { float: right; } .list-group-item > .badge + .badge { margin-right: 5px; } .nav-pills > li > a > .badge { margin-left: 3px; } .jumbotron { padding-top: 30px; padding-bottom: 30px; margin-bottom: 30px; color: inherit; background-color: #eee; } .jumbotron h1, .jumbotron .h1 { color: inherit; } .jumbotron p { margin-bottom: 15px; font-size: 21px; font-weight: 200; } .jumbotron > hr { border-top-color: #d5d5d5; } .container .jumbotron, .container-fluid .jumbotron { padding-right: 15px; padding-left: 15px; border-radius: 6px; } .jumbotron .container { max-width: 100%; } @media screen and (min-width: 768px) { .jumbotron { padding-top: 48px; padding-bottom: 48px; } .container .jumbotron, .container-fluid .jumbotron { padding-right: 60px; padding-left: 60px; } .jumbotron h1, .jumbotron .h1 { font-size: 63px; } } .thumbnail { display: block; padding: 4px; margin-bottom: 20px; line-height: 1.42857143; background-color: #fff; border: 1px solid #ddd; border-radius: 4px; -webkit-transition: border .2s ease-in-out; -o-transition: border .2s ease-in-out; transition: border .2s ease-in-out; } .thumbnail > img, .thumbnail a > img { margin-right: auto; margin-left: auto; } a.thumbnail:hover, a.thumbnail:focus, a.thumbnail.active { border-color: #337ab7; } .thumbnail .caption { padding: 9px; color: #333; } .alert { padding: 15px; margin-bottom: 20px; border: 1px solid transparent; border-radius: 4px; } .alert h4 { margin-top: 0; color: inherit; } .alert .alert-link { font-weight: bold; } .alert > p, .alert > ul { margin-bottom: 0; } .alert > p + p { margin-top: 5px; } .alert-dismissable, .alert-dismissible { padding-right: 35px; } .alert-dismissable .close, .alert-dismissible .close { position: relative; top: -2px; right: -21px; color: inherit; } .alert-success { color: #3c763d; background-color: #dff0d8; border-color: #d6e9c6; } .alert-success hr { border-top-color: #c9e2b3; } .alert-success .alert-link { color: #2b542c; } .alert-info { color: #31708f; background-color: #d9edf7; border-color: #bce8f1; } .alert-info hr { border-top-color: #a6e1ec; } .alert-info .alert-link { color: #245269; } .alert-warning { color: #8a6d3b; background-color: #fcf8e3; border-color: #faebcc; } .alert-warning hr { border-top-color: #f7e1b5; } .alert-warning .alert-link { color: #66512c; } .alert-danger { color: #a94442; background-color: #f2dede; border-color: #ebccd1; } .alert-danger hr { border-top-color: #e4b9c0; } .alert-danger .alert-link { color: #843534; } @-webkit-keyframes progress-bar-stripes { from { background-position: 40px 0; } to { background-position: 0 0; } } @-o-keyframes progress-bar-stripes { from { background-position: 40px 0; } to { background-position: 0 0; } } @keyframes progress-bar-stripes { from { background-position: 40px 0; } to { background-position: 0 0; } } .progress { height: 20px; margin-bottom: 20px; overflow: hidden; background-color: #f5f5f5; border-radius: 4px; -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); } .progress-bar { float: left; width: 0; height: 100%; font-size: 12px; line-height: 20px; color: #fff; text-align: center; background-color: #337ab7; -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); -webkit-transition: width .6s ease; -o-transition: width .6s ease; transition: width .6s ease; } .progress-striped .progress-bar, .progress-bar-striped { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); -webkit-background-size: 40px 40px; background-size: 40px 40px; } .progress.active .progress-bar, .progress-bar.active { -webkit-animation: progress-bar-stripes 2s linear infinite; -o-animation: progress-bar-stripes 2s linear infinite; animation: progress-bar-stripes 2s linear infinite; } .progress-bar-success { background-color: #5cb85c; } .progress-striped .progress-bar-success { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); } .progress-bar-info { background-color: #5bc0de; } .progress-striped .progress-bar-info { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); } .progress-bar-warning { background-color: #f0ad4e; } .progress-striped .progress-bar-warning { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); } .progress-bar-danger { background-color: #d9534f; } .progress-striped .progress-bar-danger { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); } .media { margin-top: 15px; } .media:first-child { margin-top: 0; } .media, .media-body { overflow: hidden; zoom: 1; } .media-body { width: 10000px; } .media-object { display: block; } .media-object.img-thumbnail { max-width: none; } .media-right, .media > .pull-right { padding-left: 10px; } .media-left, .media > .pull-left { padding-right: 10px; } .media-left, .media-right, .media-body { display: table-cell; vertical-align: top; } .media-middle { vertical-align: middle; } .media-bottom { vertical-align: bottom; } .media-heading { margin-top: 0; margin-bottom: 5px; } .media-list { padding-left: 0; list-style: none; } .list-group { padding-left: 0; margin-bottom: 20px; } .list-group-item { position: relative; display: block; padding: 10px 15px; margin-bottom: -1px; background-color: #fff; border: 1px solid #ddd; } .list-group-item:first-child { border-top-left-radius: 4px; border-top-right-radius: 4px; } .list-group-item:last-child { margin-bottom: 0; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; } a.list-group-item, button.list-group-item { color: #555; } a.list-group-item .list-group-item-heading, button.list-group-item .list-group-item-heading { color: #333; } a.list-group-item:hover, button.list-group-item:hover, a.list-group-item:focus, button.list-group-item:focus { color: #555; text-decoration: none; background-color: #f5f5f5; } button.list-group-item { width: 100%; text-align: left; } .list-group-item.disabled, .list-group-item.disabled:hover, .list-group-item.disabled:focus { color: #777; cursor: not-allowed; background-color: #eee; } .list-group-item.disabled .list-group-item-heading, .list-group-item.disabled:hover .list-group-item-heading, .list-group-item.disabled:focus .list-group-item-heading { color: inherit; } .list-group-item.disabled .list-group-item-text, .list-group-item.disabled:hover .list-group-item-text, .list-group-item.disabled:focus .list-group-item-text { color: #777; } .list-group-item.active, .list-group-item.active:hover, .list-group-item.active:focus { z-index: 2; color: #fff; background-color: #337ab7; border-color: #337ab7; } .list-group-item.active .list-group-item-heading, .list-group-item.active:hover .list-group-item-heading, .list-group-item.active:focus .list-group-item-heading, .list-group-item.active .list-group-item-heading > small, .list-group-item.active:hover .list-group-item-heading > small, .list-group-item.active:focus .list-group-item-heading > small, .list-group-item.active .list-group-item-heading > .small, .list-group-item.active:hover .list-group-item-heading > .small, .list-group-item.active:focus .list-group-item-heading > .small { color: inherit; } .list-group-item.active .list-group-item-text, .list-group-item.active:hover .list-group-item-text, .list-group-item.active:focus .list-group-item-text { color: #c7ddef; } .list-group-item-success { color: #3c763d; background-color: #dff0d8; } a.list-group-item-success, button.list-group-item-success { color: #3c763d; } a.list-group-item-success .list-group-item-heading, button.list-group-item-success .list-group-item-heading { color: inherit; } a.list-group-item-success:hover, button.list-group-item-success:hover, a.list-group-item-success:focus, button.list-group-item-success:focus { color: #3c763d; background-color: #d0e9c6; } a.list-group-item-success.active, button.list-group-item-success.active, a.list-group-item-success.active:hover, button.list-group-item-success.active:hover, a.list-group-item-success.active:focus, button.list-group-item-success.active:focus { color: #fff; background-color: #3c763d; border-color: #3c763d; } .list-group-item-info { color: #31708f; background-color: #d9edf7; } a.list-group-item-info, button.list-group-item-info { color: #31708f; } a.list-group-item-info .list-group-item-heading, button.list-group-item-info .list-group-item-heading { color: inherit; } a.list-group-item-info:hover, button.list-group-item-info:hover, a.list-group-item-info:focus, button.list-group-item-info:focus { color: #31708f; background-color: #c4e3f3; } a.list-group-item-info.active, button.list-group-item-info.active, a.list-group-item-info.active:hover, button.list-group-item-info.active:hover, a.list-group-item-info.active:focus, button.list-group-item-info.active:focus { color: #fff; background-color: #31708f; border-color: #31708f; } .list-group-item-warning { color: #8a6d3b; background-color: #fcf8e3; } a.list-group-item-warning, button.list-group-item-warning { color: #8a6d3b; } a.list-group-item-warning .list-group-item-heading, button.list-group-item-warning .list-group-item-heading { color: inherit; } a.list-group-item-warning:hover, button.list-group-item-warning:hover, a.list-group-item-warning:focus, button.list-group-item-warning:focus { color: #8a6d3b; background-color: #faf2cc; } a.list-group-item-warning.active, button.list-group-item-warning.active, a.list-group-item-warning.active:hover, button.list-group-item-warning.active:hover, a.list-group-item-warning.active:focus, button.list-group-item-warning.active:focus { color: #fff; background-color: #8a6d3b; border-color: #8a6d3b; } .list-group-item-danger { color: #a94442; background-color: #f2dede; } a.list-group-item-danger, button.list-group-item-danger { color: #a94442; } a.list-group-item-danger .list-group-item-heading, button.list-group-item-danger .list-group-item-heading { color: inherit; } a.list-group-item-danger:hover, button.list-group-item-danger:hover, a.list-group-item-danger:focus, button.list-group-item-danger:focus { color: #a94442; background-color: #ebcccc; } a.list-group-item-danger.active, button.list-group-item-danger.active, a.list-group-item-danger.active:hover, button.list-group-item-danger.active:hover, a.list-group-item-danger.active:focus, button.list-group-item-danger.active:focus { color: #fff; background-color: #a94442; border-color: #a94442; } .list-group-item-heading { margin-top: 0; margin-bottom: 5px; } .list-group-item-text { margin-bottom: 0; line-height: 1.3; } .panel { margin-bottom: 20px; background-color: #fff; border: 1px solid transparent; border-radius: 4px; -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05); box-shadow: 0 1px 1px rgba(0, 0, 0, .05); } .panel-body { padding: 15px; } .panel-heading { padding: 10px 15px; border-bottom: 1px solid transparent; border-top-left-radius: 3px; border-top-right-radius: 3px; } .panel-heading > .dropdown .dropdown-toggle { color: inherit; } .panel-title { margin-top: 0; margin-bottom: 0; font-size: 16px; color: inherit; } .panel-title > a, .panel-title > small, .panel-title > .small, .panel-title > small > a, .panel-title > .small > a { color: inherit; } .panel-footer { padding: 10px 15px; background-color: #f5f5f5; border-top: 1px solid #ddd; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; } .panel > .list-group, .panel > .panel-collapse > .list-group { margin-bottom: 0; } .panel > .list-group .list-group-item, .panel > .panel-collapse > .list-group .list-group-item { border-width: 1px 0; border-radius: 0; } .panel > .list-group:first-child .list-group-item:first-child, .panel > .panel-collapse > .list-group:first-child .list-group-item:first-child { border-top: 0; border-top-left-radius: 3px; border-top-right-radius: 3px; } .panel > .list-group:last-child .list-group-item:last-child, .panel > .panel-collapse > .list-group:last-child .list-group-item:last-child { border-bottom: 0; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; } .panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child { border-top-left-radius: 0; border-top-right-radius: 0; } .panel-heading + .list-group .list-group-item:first-child { border-top-width: 0; } .list-group + .panel-footer { border-top-width: 0; } .panel > .table, .panel > .table-responsive > .table, .panel > .panel-collapse > .table { margin-bottom: 0; } .panel > .table caption, .panel > .table-responsive > .table caption, .panel > .panel-collapse > .table caption { padding-right: 15px; padding-left: 15px; } .panel > .table:first-child, .panel > .table-responsive:first-child > .table:first-child { border-top-left-radius: 3px; border-top-right-radius: 3px; } .panel > .table:first-child > thead:first-child > tr:first-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child, .panel > .table:first-child > tbody:first-child > tr:first-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child { border-top-left-radius: 3px; border-top-right-radius: 3px; } .panel > .table:first-child > thead:first-child > tr:first-child td:first-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, .panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, .panel > .table:first-child > thead:first-child > tr:first-child th:first-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, .panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { border-top-left-radius: 3px; } .panel > .table:first-child > thead:first-child > tr:first-child td:last-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, .panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, .panel > .table:first-child > thead:first-child > tr:first-child th:last-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, .panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { border-top-right-radius: 3px; } .panel > .table:last-child, .panel > .table-responsive:last-child > .table:last-child { border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; } .panel > .table:last-child > tbody:last-child > tr:last-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child, .panel > .table:last-child > tfoot:last-child > tr:last-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child { border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; } .panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, .panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, .panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, .panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { border-bottom-left-radius: 3px; } .panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, .panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, .panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, .panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child { border-bottom-right-radius: 3px; } .panel > .panel-body + .table, .panel > .panel-body + .table-responsive, .panel > .table + .panel-body, .panel > .table-responsive + .panel-body { border-top: 1px solid #ddd; } .panel > .table > tbody:first-child > tr:first-child th, .panel > .table > tbody:first-child > tr:first-child td { border-top: 0; } .panel > .table-bordered, .panel > .table-responsive > .table-bordered { border: 0; } .panel > .table-bordered > thead > tr > th:first-child, .panel > .table-responsive > .table-bordered > thead > tr > th:first-child, .panel > .table-bordered > tbody > tr > th:first-child, .panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, .panel > .table-bordered > tfoot > tr > th:first-child, .panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, .panel > .table-bordered > thead > tr > td:first-child, .panel > .table-responsive > .table-bordered > thead > tr > td:first-child, .panel > .table-bordered > tbody > tr > td:first-child, .panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, .panel > .table-bordered > tfoot > tr > td:first-child, .panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { border-left: 0; } .panel > .table-bordered > thead > tr > th:last-child, .panel > .table-responsive > .table-bordered > thead > tr > th:last-child, .panel > .table-bordered > tbody > tr > th:last-child, .panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, .panel > .table-bordered > tfoot > tr > th:last-child, .panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, .panel > .table-bordered > thead > tr > td:last-child, .panel > .table-responsive > .table-bordered > thead > tr > td:last-child, .panel > .table-bordered > tbody > tr > td:last-child, .panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, .panel > .table-bordered > tfoot > tr > td:last-child, .panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { border-right: 0; } .panel > .table-bordered > thead > tr:first-child > td, .panel > .table-responsive > .table-bordered > thead > tr:first-child > td, .panel > .table-bordered > tbody > tr:first-child > td, .panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, .panel > .table-bordered > thead > tr:first-child > th, .panel > .table-responsive > .table-bordered > thead > tr:first-child > th, .panel > .table-bordered > tbody > tr:first-child > th, .panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { border-bottom: 0; } .panel > .table-bordered > tbody > tr:last-child > td, .panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, .panel > .table-bordered > tfoot > tr:last-child > td, .panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, .panel > .table-bordered > tbody > tr:last-child > th, .panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, .panel > .table-bordered > tfoot > tr:last-child > th, .panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { border-bottom: 0; } .panel > .table-responsive { margin-bottom: 0; border: 0; } .panel-group { margin-bottom: 20px; } .panel-group .panel { margin-bottom: 0; border-radius: 4px; } .panel-group .panel + .panel { margin-top: 5px; } .panel-group .panel-heading { border-bottom: 0; } .panel-group .panel-heading + .panel-collapse > .panel-body, .panel-group .panel-heading + .panel-collapse > .list-group { border-top: 1px solid #ddd; } .panel-group .panel-footer { border-top: 0; } .panel-group .panel-footer + .panel-collapse .panel-body { border-bottom: 1px solid #ddd; } .panel-default { border-color: #ddd; } .panel-default > .panel-heading { color: #333; background-color: #f5f5f5; border-color: #ddd; } .panel-default > .panel-heading + .panel-collapse > .panel-body { border-top-color: #ddd; } .panel-default > .panel-heading .badge { color: #f5f5f5; background-color: #333; } .panel-default > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #ddd; } .panel-primary { border-color: #337ab7; } .panel-primary > .panel-heading { color: #fff; background-color: #337ab7; border-color: #337ab7; } .panel-primary > .panel-heading + .panel-collapse > .panel-body { border-top-color: #337ab7; } .panel-primary > .panel-heading .badge { color: #337ab7; background-color: #fff; } .panel-primary > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #337ab7; } .panel-success { border-color: #d6e9c6; } .panel-success > .panel-heading { color: #3c763d; background-color: #dff0d8; border-color: #d6e9c6; } .panel-success > .panel-heading + .panel-collapse > .panel-body { border-top-color: #d6e9c6; } .panel-success > .panel-heading .badge { color: #dff0d8; background-color: #3c763d; } .panel-success > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #d6e9c6; } .panel-info { border-color: #bce8f1; } .panel-info > .panel-heading { color: #31708f; background-color: #d9edf7; border-color: #bce8f1; } .panel-info > .panel-heading + .panel-collapse > .panel-body { border-top-color: #bce8f1; } .panel-info > .panel-heading .badge { color: #d9edf7; background-color: #31708f; } .panel-info > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #bce8f1; } .panel-warning { border-color: #faebcc; } .panel-warning > .panel-heading { color: #8a6d3b; background-color: #fcf8e3; border-color: #faebcc; } .panel-warning > .panel-heading + .panel-collapse > .panel-body { border-top-color: #faebcc; } .panel-warning > .panel-heading .badge { color: #fcf8e3; background-color: #8a6d3b; } .panel-warning > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #faebcc; } .panel-danger { border-color: #ebccd1; } .panel-danger > .panel-heading { color: #a94442; background-color: #f2dede; border-color: #ebccd1; } .panel-danger > .panel-heading + .panel-collapse > .panel-body { border-top-color: #ebccd1; } .panel-danger > .panel-heading .badge { color: #f2dede; background-color: #a94442; } .panel-danger > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #ebccd1; } .embed-responsive { position: relative; display: block; height: 0; padding: 0; overflow: hidden; } .embed-responsive .embed-responsive-item, .embed-responsive iframe, .embed-responsive embed, .embed-responsive object, .embed-responsive video { position: absolute; top: 0; bottom: 0; left: 0; width: 100%; height: 100%; border: 0; } .embed-responsive-16by9 { padding-bottom: 56.25%; } .embed-responsive-4by3 { padding-bottom: 75%; } .well { min-height: 20px; padding: 19px; margin-bottom: 20px; background-color: #f5f5f5; border: 1px solid #e3e3e3; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); } .well blockquote { border-color: #ddd; border-color: rgba(0, 0, 0, .15); } .well-lg { padding: 24px; border-radius: 6px; } .well-sm { padding: 9px; border-radius: 3px; } .close { float: right; font-size: 21px; font-weight: bold; line-height: 1; color: #000; text-shadow: 0 1px 0 #fff; filter: alpha(opacity=20); opacity: .2; } .close:hover, .close:focus { color: #000; text-decoration: none; cursor: pointer; filter: alpha(opacity=50); opacity: .5; } button.close { -webkit-appearance: none; padding: 0; cursor: pointer; background: transparent; border: 0; } .modal-open { overflow: hidden; } .modal { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 1050; display: none; overflow: hidden; -webkit-overflow-scrolling: touch; outline: 0; } .modal.fade .modal-dialog { -webkit-transition: -webkit-transform .3s ease-out; -o-transition: -o-transform .3s ease-out; transition: transform .3s ease-out; -webkit-transform: translate(0, -25%); -ms-transform: translate(0, -25%); -o-transform: translate(0, -25%); transform: translate(0, -25%); } .modal.in .modal-dialog { -webkit-transform: translate(0, 0); -ms-transform: translate(0, 0); -o-transform: translate(0, 0); transform: translate(0, 0); } .modal-open .modal { overflow-x: hidden; overflow-y: auto; } .modal-dialog { position: relative; width: auto; margin: 10px; } .modal-content { position: relative; background-color: #fff; -webkit-background-clip: padding-box; background-clip: padding-box; border: 1px solid #999; border: 1px solid rgba(0, 0, 0, .2); border-radius: 6px; outline: 0; -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5); box-shadow: 0 3px 9px rgba(0, 0, 0, .5); } .modal-backdrop { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 1040; background-color: #000; } .modal-backdrop.fade { filter: alpha(opacity=0); opacity: 0; } .modal-backdrop.in { filter: alpha(opacity=50); opacity: .5; } .modal-header { padding: 15px; border-bottom: 1px solid #e5e5e5; } .modal-header .close { margin-top: -2px; } .modal-title { margin: 0; line-height: 1.42857143; } .modal-body { position: relative; padding: 15px; } .modal-footer { padding: 15px; text-align: right; border-top: 1px solid #e5e5e5; } .modal-footer .btn + .btn { margin-bottom: 0; margin-left: 5px; } .modal-footer .btn-group .btn + .btn { margin-left: -1px; } .modal-footer .btn-block + .btn-block { margin-left: 0; } .modal-scrollbar-measure { position: absolute; top: -9999px; width: 50px; height: 50px; overflow: scroll; } @media (min-width: 768px) { .modal-dialog { width: 600px; margin: 30px auto; } .modal-content { -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5); box-shadow: 0 5px 15px rgba(0, 0, 0, .5); } .modal-sm { width: 300px; } } @media (min-width: 992px) { .modal-lg { width: 900px; } } .tooltip { position: absolute; z-index: 1070; display: block; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 12px; font-style: normal; font-weight: normal; line-height: 1.42857143; text-align: left; text-align: start; text-decoration: none; text-shadow: none; text-transform: none; letter-spacing: normal; word-break: normal; word-spacing: normal; word-wrap: normal; white-space: normal; filter: alpha(opacity=0); opacity: 0; line-break: auto; } .tooltip.in { filter: alpha(opacity=90); opacity: .9; } .tooltip.top { padding: 5px 0; margin-top: -3px; } .tooltip.right { padding: 0 5px; margin-left: 3px; } .tooltip.bottom { padding: 5px 0; margin-top: 3px; } .tooltip.left { padding: 0 5px; margin-left: -3px; } .tooltip-inner { max-width: 200px; padding: 3px 8px; color: #fff; text-align: center; background-color: #000; border-radius: 4px; } .tooltip-arrow { position: absolute; width: 0; height: 0; border-color: transparent; border-style: solid; } .tooltip.top .tooltip-arrow { bottom: 0; left: 50%; margin-left: -5px; border-width: 5px 5px 0; border-top-color: #000; } .tooltip.top-left .tooltip-arrow { right: 5px; bottom: 0; margin-bottom: -5px; border-width: 5px 5px 0; border-top-color: #000; } .tooltip.top-right .tooltip-arrow { bottom: 0; left: 5px; margin-bottom: -5px; border-width: 5px 5px 0; border-top-color: #000; } .tooltip.right .tooltip-arrow { top: 50%; left: 0; margin-top: -5px; border-width: 5px 5px 5px 0; border-right-color: #000; } .tooltip.left .tooltip-arrow { top: 50%; right: 0; margin-top: -5px; border-width: 5px 0 5px 5px; border-left-color: #000; } .tooltip.bottom .tooltip-arrow { top: 0; left: 50%; margin-left: -5px; border-width: 0 5px 5px; border-bottom-color: #000; } .tooltip.bottom-left .tooltip-arrow { top: 0; right: 5px; margin-top: -5px; border-width: 0 5px 5px; border-bottom-color: #000; } .tooltip.bottom-right .tooltip-arrow { top: 0; left: 5px; margin-top: -5px; border-width: 0 5px 5px; border-bottom-color: #000; } .popover { position: absolute; top: 0; left: 0; z-index: 1060; display: none; max-width: 276px; padding: 1px; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; font-style: normal; font-weight: normal; line-height: 1.42857143; text-align: left; text-align: start; text-decoration: none; text-shadow: none; text-transform: none; letter-spacing: normal; word-break: normal; word-spacing: normal; word-wrap: normal; white-space: normal; background-color: #fff; -webkit-background-clip: padding-box; background-clip: padding-box; border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, .2); border-radius: 6px; -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); box-shadow: 0 5px 10px rgba(0, 0, 0, .2); line-break: auto; } .popover.top { margin-top: -10px; } .popover.right { margin-left: 10px; } .popover.bottom { margin-top: 10px; } .popover.left { margin-left: -10px; } .popover-title { padding: 8px 14px; margin: 0; font-size: 14px; background-color: #f7f7f7; border-bottom: 1px solid #ebebeb; border-radius: 5px 5px 0 0; } .popover-content { padding: 9px 14px; } .popover > .arrow, .popover > .arrow:after { position: absolute; display: block; width: 0; height: 0; border-color: transparent; border-style: solid; } .popover > .arrow { border-width: 11px; } .popover > .arrow:after { content: ""; border-width: 10px; } .popover.top > .arrow { bottom: -11px; left: 50%; margin-left: -11px; border-top-color: #999; border-top-color: rgba(0, 0, 0, .25); border-bottom-width: 0; } .popover.top > .arrow:after { bottom: 1px; margin-left: -10px; content: " "; border-top-color: #fff; border-bottom-width: 0; } .popover.right > .arrow { top: 50%; left: -11px; margin-top: -11px; border-right-color: #999; border-right-color: rgba(0, 0, 0, .25); border-left-width: 0; } .popover.right > .arrow:after { bottom: -10px; left: 1px; content: " "; border-right-color: #fff; border-left-width: 0; } .popover.bottom > .arrow { top: -11px; left: 50%; margin-left: -11px; border-top-width: 0; border-bottom-color: #999; border-bottom-color: rgba(0, 0, 0, .25); } .popover.bottom > .arrow:after { top: 1px; margin-left: -10px; content: " "; border-top-width: 0; border-bottom-color: #fff; } .popover.left > .arrow { top: 50%; right: -11px; margin-top: -11px; border-right-width: 0; border-left-color: #999; border-left-color: rgba(0, 0, 0, .25); } .popover.left > .arrow:after { right: 1px; bottom: -10px; content: " "; border-right-width: 0; border-left-color: #fff; } .carousel { position: relative; } .carousel-inner { position: relative; width: 100%; overflow: hidden; } .carousel-inner > .item { position: relative; display: none; -webkit-transition: .6s ease-in-out left; -o-transition: .6s ease-in-out left; transition: .6s ease-in-out left; } .carousel-inner > .item > img, .carousel-inner > .item > a > img { line-height: 1; } @media all and (transform-3d), (-webkit-transform-3d) { .carousel-inner > .item { -webkit-transition: -webkit-transform .6s ease-in-out; -o-transition: -o-transform .6s ease-in-out; transition: transform .6s ease-in-out; -webkit-backface-visibility: hidden; backface-visibility: hidden; -webkit-perspective: 1000px; perspective: 1000px; } .carousel-inner > .item.next, .carousel-inner > .item.active.right { left: 0; -webkit-transform: translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0); } .carousel-inner > .item.prev, .carousel-inner > .item.active.left { left: 0; -webkit-transform: translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0); } .carousel-inner > .item.next.left, .carousel-inner > .item.prev.right, .carousel-inner > .item.active { left: 0; -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } } .carousel-inner > .active, .carousel-inner > .next, .carousel-inner > .prev { display: block; } .carousel-inner > .active { left: 0; } .carousel-inner > .next, .carousel-inner > .prev { position: absolute; top: 0; width: 100%; } .carousel-inner > .next { left: 100%; } .carousel-inner > .prev { left: -100%; } .carousel-inner > .next.left, .carousel-inner > .prev.right { left: 0; } .carousel-inner > .active.left { left: -100%; } .carousel-inner > .active.right { left: 100%; } .carousel-control { position: absolute; top: 0; bottom: 0; left: 0; width: 15%; font-size: 20px; color: #fff; text-align: center; text-shadow: 0 1px 2px rgba(0, 0, 0, .6); background-color: rgba(0, 0, 0, 0); filter: alpha(opacity=50); opacity: .5; } .carousel-control.left { background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); background-image: -o-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .5)), to(rgba(0, 0, 0, .0001))); background-image: linear-gradient(to right, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); background-repeat: repeat-x; } .carousel-control.right { right: 0; left: auto; background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); background-image: -o-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .0001)), to(rgba(0, 0, 0, .5))); background-image: linear-gradient(to right, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); background-repeat: repeat-x; } .carousel-control:hover, .carousel-control:focus { color: #fff; text-decoration: none; filter: alpha(opacity=90); outline: 0; opacity: .9; } .carousel-control .icon-prev, .carousel-control .icon-next, .carousel-control .glyphicon-chevron-left, .carousel-control .glyphicon-chevron-right { position: absolute; top: 50%; z-index: 5; display: inline-block; margin-top: -10px; } .carousel-control .icon-prev, .carousel-control .glyphicon-chevron-left { left: 50%; margin-left: -10px; } .carousel-control .icon-next, .carousel-control .glyphicon-chevron-right { right: 50%; margin-right: -10px; } .carousel-control .icon-prev, .carousel-control .icon-next { width: 20px; height: 20px; font-family: serif; line-height: 1; } .carousel-control .icon-prev:before { content: '\2039'; } .carousel-control .icon-next:before { content: '\203a'; } .carousel-indicators { position: absolute; bottom: 10px; left: 50%; z-index: 15; width: 60%; padding-left: 0; margin-left: -30%; text-align: center; list-style: none; } .carousel-indicators li { display: inline-block; width: 10px; height: 10px; margin: 1px; text-indent: -999px; cursor: pointer; background-color: #000 \9; background-color: rgba(0, 0, 0, 0); border: 1px solid #fff; border-radius: 10px; } .carousel-indicators .active { width: 12px; height: 12px; margin: 0; background-color: #fff; } .carousel-caption { position: absolute; right: 15%; bottom: 20px; left: 15%; z-index: 10; padding-top: 20px; padding-bottom: 20px; color: #fff; text-align: center; text-shadow: 0 1px 2px rgba(0, 0, 0, .6); } .carousel-caption .btn { text-shadow: none; } @media screen and (min-width: 768px) { .carousel-control .glyphicon-chevron-left, .carousel-control .glyphicon-chevron-right, .carousel-control .icon-prev, .carousel-control .icon-next { width: 30px; height: 30px; margin-top: -10px; font-size: 30px; } .carousel-control .glyphicon-chevron-left, .carousel-control .icon-prev { margin-left: -10px; } .carousel-control .glyphicon-chevron-right, .carousel-control .icon-next { margin-right: -10px; } .carousel-caption { right: 20%; left: 20%; padding-bottom: 30px; } .carousel-indicators { bottom: 20px; } } .clearfix:before, .clearfix:after, .dl-horizontal dd:before, .dl-horizontal dd:after, .container:before, .container:after, .container-fluid:before, .container-fluid:after, .row:before, .row:after, .form-horizontal .form-group:before, .form-horizontal .form-group:after, .btn-toolbar:before, .btn-toolbar:after, .btn-group-vertical > .btn-group:before, .btn-group-vertical > .btn-group:after, .nav:before, .nav:after, .navbar:before, .navbar:after, .navbar-header:before, .navbar-header:after, .navbar-collapse:before, .navbar-collapse:after, .pager:before, .pager:after, .panel-body:before, .panel-body:after, .modal-header:before, .modal-header:after, .modal-footer:before, .modal-footer:after { display: table; content: " "; } .clearfix:after, .dl-horizontal dd:after, .container:after, .container-fluid:after, .row:after, .form-horizontal .form-group:after, .btn-toolbar:after, .btn-group-vertical > .btn-group:after, .nav:after, .navbar:after, .navbar-header:after, .navbar-collapse:after, .pager:after, .panel-body:after, .modal-header:after, .modal-footer:after { clear: both; } .center-block { display: block; margin-right: auto; margin-left: auto; } .pull-right { float: right !important; } .pull-left { float: left !important; } .hide { display: none !important; } .show { display: block !important; } .invisible { visibility: hidden; } .text-hide { font: 0/0 a; color: transparent; text-shadow: none; background-color: transparent; border: 0; } .hidden { display: none !important; } .affix { position: fixed; } @-ms-viewport { width: device-width; } .visible-xs, .visible-sm, .visible-md, .visible-lg { display: none !important; } .visible-xs-block, .visible-xs-inline, .visible-xs-inline-block, .visible-sm-block, .visible-sm-inline, .visible-sm-inline-block, .visible-md-block, .visible-md-inline, .visible-md-inline-block, .visible-lg-block, .visible-lg-inline, .visible-lg-inline-block { display: none !important; } @media (max-width: 767px) { .visible-xs { display: block !important; } table.visible-xs { display: table !important; } tr.visible-xs { display: table-row !important; } th.visible-xs, td.visible-xs { display: table-cell !important; } } @media (max-width: 767px) { .visible-xs-block { display: block !important; } } @media (max-width: 767px) { .visible-xs-inline { display: inline !important; } } @media (max-width: 767px) { .visible-xs-inline-block { display: inline-block !important; } } @media (min-width: 768px) and (max-width: 991px) { .visible-sm { display: block !important; } table.visible-sm { display: table !important; } tr.visible-sm { display: table-row !important; } th.visible-sm, td.visible-sm { display: table-cell !important; } } @media (min-width: 768px) and (max-width: 991px) { .visible-sm-block { display: block !important; } } @media (min-width: 768px) and (max-width: 991px) { .visible-sm-inline { display: inline !important; } } @media (min-width: 768px) and (max-width: 991px) { .visible-sm-inline-block { display: inline-block !important; } } @media (min-width: 992px) and (max-width: 1199px) { .visible-md { display: block !important; } table.visible-md { display: table !important; } tr.visible-md { display: table-row !important; } th.visible-md, td.visible-md { display: table-cell !important; } } @media (min-width: 992px) and (max-width: 1199px) { .visible-md-block { display: block !important; } } @media (min-width: 992px) and (max-width: 1199px) { .visible-md-inline { display: inline !important; } } @media (min-width: 992px) and (max-width: 1199px) { .visible-md-inline-block { display: inline-block !important; } } @media (min-width: 1200px) { .visible-lg { display: block !important; } table.visible-lg { display: table !important; } tr.visible-lg { display: table-row !important; } th.visible-lg, td.visible-lg { display: table-cell !important; } } @media (min-width: 1200px) { .visible-lg-block { display: block !important; } } @media (min-width: 1200px) { .visible-lg-inline { display: inline !important; } } @media (min-width: 1200px) { .visible-lg-inline-block { display: inline-block !important; } } @media (max-width: 767px) { .hidden-xs { display: none !important; } } @media (min-width: 768px) and (max-width: 991px) { .hidden-sm { display: none !important; } } @media (min-width: 992px) and (max-width: 1199px) { .hidden-md { display: none !important; } } @media (min-width: 1200px) { .hidden-lg { display: none !important; } } .visible-print { display: none !important; } @media print { .visible-print { display: block !important; } table.visible-print { display: table !important; } tr.visible-print { display: table-row !important; } th.visible-print, td.visible-print { display: table-cell !important; } } .visible-print-block { display: none !important; } @media print { .visible-print-block { display: block !important; } } .visible-print-inline { display: none !important; } @media print { .visible-print-inline { display: inline !important; } } .visible-print-inline-block { display: none !important; } @media print { .visible-print-inline-block { display: inline-block !important; } } @media print { .hidden-print { display: none !important; } } /*# sourceMappingURL=bootstrap.css.map */ ================================================ FILE: dis-seckill-gateway/src/main/resources/static/bootstrap/js/bootstrap.js ================================================ /*! * Bootstrap v3.3.7 (http://getbootstrap.com) * Copyright 2011-2016 Twitter, Inc. * Licensed under the MIT license */ if (typeof jQuery === 'undefined') { throw new Error('Bootstrap\'s JavaScript requires jQuery') } +function ($) { 'use strict'; var version = $.fn.jquery.split(' ')[0].split('.') if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1) || (version[0] > 3)) { throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4') } }(jQuery); /* ======================================================================== * Bootstrap: transition.js v3.3.7 * http://getbootstrap.com/javascript/#transitions * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) // ============================================================ function transitionEnd() { var el = document.createElement('bootstrap') var transEndEventNames = { WebkitTransition : 'webkitTransitionEnd', MozTransition : 'transitionend', OTransition : 'oTransitionEnd otransitionend', transition : 'transitionend' } for (var name in transEndEventNames) { if (el.style[name] !== undefined) { return { end: transEndEventNames[name] } } } return false // explicit for ie8 ( ._.) } // http://blog.alexmaccaw.com/css-transitions $.fn.emulateTransitionEnd = function (duration) { var called = false var $el = this $(this).one('bsTransitionEnd', function () { called = true }) var callback = function () { if (!called) $($el).trigger($.support.transition.end) } setTimeout(callback, duration) return this } $(function () { $.support.transition = transitionEnd() if (!$.support.transition) return $.event.special.bsTransitionEnd = { bindType: $.support.transition.end, delegateType: $.support.transition.end, handle: function (e) { if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) } } }) }(jQuery); /* ======================================================================== * Bootstrap: alert.js v3.3.7 * http://getbootstrap.com/javascript/#alerts * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // ALERT CLASS DEFINITION // ====================== var dismiss = '[data-dismiss="alert"]' var Alert = function (el) { $(el).on('click', dismiss, this.close) } Alert.VERSION = '3.3.7' Alert.TRANSITION_DURATION = 150 Alert.prototype.close = function (e) { var $this = $(this) var selector = $this.attr('data-target') if (!selector) { selector = $this.attr('href') selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } var $parent = $(selector === '#' ? [] : selector) if (e) e.preventDefault() if (!$parent.length) { $parent = $this.closest('.alert') } $parent.trigger(e = $.Event('close.bs.alert')) if (e.isDefaultPrevented()) return $parent.removeClass('in') function removeElement() { // detach from parent, fire event then clean up data $parent.detach().trigger('closed.bs.alert').remove() } $.support.transition && $parent.hasClass('fade') ? $parent .one('bsTransitionEnd', removeElement) .emulateTransitionEnd(Alert.TRANSITION_DURATION) : removeElement() } // ALERT PLUGIN DEFINITION // ======================= function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.alert') if (!data) $this.data('bs.alert', (data = new Alert(this))) if (typeof option == 'string') data[option].call($this) }) } var old = $.fn.alert $.fn.alert = Plugin $.fn.alert.Constructor = Alert // ALERT NO CONFLICT // ================= $.fn.alert.noConflict = function () { $.fn.alert = old return this } // ALERT DATA-API // ============== $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) }(jQuery); /* ======================================================================== * Bootstrap: button.js v3.3.7 * http://getbootstrap.com/javascript/#buttons * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // BUTTON PUBLIC CLASS DEFINITION // ============================== var Button = function (element, options) { this.$element = $(element) this.options = $.extend({}, Button.DEFAULTS, options) this.isLoading = false } Button.VERSION = '3.3.7' Button.DEFAULTS = { loadingText: 'loading...' } Button.prototype.setState = function (state) { var d = 'disabled' var $el = this.$element var val = $el.is('input') ? 'val' : 'html' var data = $el.data() state += 'Text' if (data.resetText == null) $el.data('resetText', $el[val]()) // push to event loop to allow forms to submit setTimeout($.proxy(function () { $el[val](data[state] == null ? this.options[state] : data[state]) if (state == 'loadingText') { this.isLoading = true $el.addClass(d).attr(d, d).prop(d, true) } else if (this.isLoading) { this.isLoading = false $el.removeClass(d).removeAttr(d).prop(d, false) } }, this), 0) } Button.prototype.toggle = function () { var changed = true var $parent = this.$element.closest('[data-toggle="buttons"]') if ($parent.length) { var $input = this.$element.find('input') if ($input.prop('type') == 'radio') { if ($input.prop('checked')) changed = false $parent.find('.active').removeClass('active') this.$element.addClass('active') } else if ($input.prop('type') == 'checkbox') { if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false this.$element.toggleClass('active') } $input.prop('checked', this.$element.hasClass('active')) if (changed) $input.trigger('change') } else { this.$element.attr('aria-pressed', !this.$element.hasClass('active')) this.$element.toggleClass('active') } } // BUTTON PLUGIN DEFINITION // ======================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.button') var options = typeof option == 'object' && option if (!data) $this.data('bs.button', (data = new Button(this, options))) if (option == 'toggle') data.toggle() else if (option) data.setState(option) }) } var old = $.fn.button $.fn.button = Plugin $.fn.button.Constructor = Button // BUTTON NO CONFLICT // ================== $.fn.button.noConflict = function () { $.fn.button = old return this } // BUTTON DATA-API // =============== $(document) .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { var $btn = $(e.target).closest('.btn') Plugin.call($btn, 'toggle') if (!($(e.target).is('input[type="radio"], input[type="checkbox"]'))) { // Prevent double click on radios, and the double selections (so cancellation) on checkboxes e.preventDefault() // The target component still receive the focus if ($btn.is('input,button')) $btn.trigger('focus') else $btn.find('input:visible,button:visible').first().trigger('focus') } }) .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type)) }) }(jQuery); /* ======================================================================== * Bootstrap: carousel.js v3.3.7 * http://getbootstrap.com/javascript/#carousel * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // CAROUSEL CLASS DEFINITION // ========================= var Carousel = function (element, options) { this.$element = $(element) this.$indicators = this.$element.find('.carousel-indicators') this.options = options this.paused = null this.sliding = null this.interval = null this.$active = null this.$items = null this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) } Carousel.VERSION = '3.3.7' Carousel.TRANSITION_DURATION = 600 Carousel.DEFAULTS = { interval: 5000, pause: 'hover', wrap: true, keyboard: true } Carousel.prototype.keydown = function (e) { if (/input|textarea/i.test(e.target.tagName)) return switch (e.which) { case 37: this.prev(); break case 39: this.next(); break default: return } e.preventDefault() } Carousel.prototype.cycle = function (e) { e || (this.paused = false) this.interval && clearInterval(this.interval) this.options.interval && !this.paused && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) return this } Carousel.prototype.getItemIndex = function (item) { this.$items = item.parent().children('.item') return this.$items.index(item || this.$active) } Carousel.prototype.getItemForDirection = function (direction, active) { var activeIndex = this.getItemIndex(active) var willWrap = (direction == 'prev' && activeIndex === 0) || (direction == 'next' && activeIndex == (this.$items.length - 1)) if (willWrap && !this.options.wrap) return active var delta = direction == 'prev' ? -1 : 1 var itemIndex = (activeIndex + delta) % this.$items.length return this.$items.eq(itemIndex) } Carousel.prototype.to = function (pos) { var that = this var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active')) if (pos > (this.$items.length - 1) || pos < 0) return if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid" if (activeIndex == pos) return this.pause().cycle() return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos)) } Carousel.prototype.pause = function (e) { e || (this.paused = true) if (this.$element.find('.next, .prev').length && $.support.transition) { this.$element.trigger($.support.transition.end) this.cycle(true) } this.interval = clearInterval(this.interval) return this } Carousel.prototype.next = function () { if (this.sliding) return return this.slide('next') } Carousel.prototype.prev = function () { if (this.sliding) return return this.slide('prev') } Carousel.prototype.slide = function (type, next) { var $active = this.$element.find('.item.active') var $next = next || this.getItemForDirection(type, $active) var isCycling = this.interval var direction = type == 'next' ? 'left' : 'right' var that = this if ($next.hasClass('active')) return (this.sliding = false) var relatedTarget = $next[0] var slideEvent = $.Event('slide.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) this.$element.trigger(slideEvent) if (slideEvent.isDefaultPrevented()) return this.sliding = true isCycling && this.pause() if (this.$indicators.length) { this.$indicators.find('.active').removeClass('active') var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) $nextIndicator && $nextIndicator.addClass('active') } var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid" if ($.support.transition && this.$element.hasClass('slide')) { $next.addClass(type) $next[0].offsetWidth // force reflow $active.addClass(direction) $next.addClass(direction) $active .one('bsTransitionEnd', function () { $next.removeClass([type, direction].join(' ')).addClass('active') $active.removeClass(['active', direction].join(' ')) that.sliding = false setTimeout(function () { that.$element.trigger(slidEvent) }, 0) }) .emulateTransitionEnd(Carousel.TRANSITION_DURATION) } else { $active.removeClass('active') $next.addClass('active') this.sliding = false this.$element.trigger(slidEvent) } isCycling && this.cycle() return this } // CAROUSEL PLUGIN DEFINITION // ========================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.carousel') var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) var action = typeof option == 'string' ? option : options.slide if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) if (typeof option == 'number') data.to(option) else if (action) data[action]() else if (options.interval) data.pause().cycle() }) } var old = $.fn.carousel $.fn.carousel = Plugin $.fn.carousel.Constructor = Carousel // CAROUSEL NO CONFLICT // ==================== $.fn.carousel.noConflict = function () { $.fn.carousel = old return this } // CAROUSEL DATA-API // ================= var clickHandler = function (e) { var href var $this = $(this) var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 if (!$target.hasClass('carousel')) return var options = $.extend({}, $target.data(), $this.data()) var slideIndex = $this.attr('data-slide-to') if (slideIndex) options.interval = false Plugin.call($target, options) if (slideIndex) { $target.data('bs.carousel').to(slideIndex) } e.preventDefault() } $(document) .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler) $(window).on('load', function () { $('[data-ride="carousel"]').each(function () { var $carousel = $(this) Plugin.call($carousel, $carousel.data()) }) }) }(jQuery); /* ======================================================================== * Bootstrap: collapse.js v3.3.7 * http://getbootstrap.com/javascript/#collapse * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ /* jshint latedef: false */ +function ($) { 'use strict'; // COLLAPSE PUBLIC CLASS DEFINITION // ================================ var Collapse = function (element, options) { this.$element = $(element) this.options = $.extend({}, Collapse.DEFAULTS, options) this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + '[data-toggle="collapse"][data-target="#' + element.id + '"]') this.transitioning = null if (this.options.parent) { this.$parent = this.getParent() } else { this.addAriaAndCollapsedClass(this.$element, this.$trigger) } if (this.options.toggle) this.toggle() } Collapse.VERSION = '3.3.7' Collapse.TRANSITION_DURATION = 350 Collapse.DEFAULTS = { toggle: true } Collapse.prototype.dimension = function () { var hasWidth = this.$element.hasClass('width') return hasWidth ? 'width' : 'height' } Collapse.prototype.show = function () { if (this.transitioning || this.$element.hasClass('in')) return var activesData var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing') if (actives && actives.length) { activesData = actives.data('bs.collapse') if (activesData && activesData.transitioning) return } var startEvent = $.Event('show.bs.collapse') this.$element.trigger(startEvent) if (startEvent.isDefaultPrevented()) return if (actives && actives.length) { Plugin.call(actives, 'hide') activesData || actives.data('bs.collapse', null) } var dimension = this.dimension() this.$element .removeClass('collapse') .addClass('collapsing')[dimension](0) .attr('aria-expanded', true) this.$trigger .removeClass('collapsed') .attr('aria-expanded', true) this.transitioning = 1 var complete = function () { this.$element .removeClass('collapsing') .addClass('collapse in')[dimension]('') this.transitioning = 0 this.$element .trigger('shown.bs.collapse') } if (!$.support.transition) return complete.call(this) var scrollSize = $.camelCase(['scroll', dimension].join('-')) this.$element .one('bsTransitionEnd', $.proxy(complete, this)) .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]) } Collapse.prototype.hide = function () { if (this.transitioning || !this.$element.hasClass('in')) return var startEvent = $.Event('hide.bs.collapse') this.$element.trigger(startEvent) if (startEvent.isDefaultPrevented()) return var dimension = this.dimension() this.$element[dimension](this.$element[dimension]())[0].offsetHeight this.$element .addClass('collapsing') .removeClass('collapse in') .attr('aria-expanded', false) this.$trigger .addClass('collapsed') .attr('aria-expanded', false) this.transitioning = 1 var complete = function () { this.transitioning = 0 this.$element .removeClass('collapsing') .addClass('collapse') .trigger('hidden.bs.collapse') } if (!$.support.transition) return complete.call(this) this.$element [dimension](0) .one('bsTransitionEnd', $.proxy(complete, this)) .emulateTransitionEnd(Collapse.TRANSITION_DURATION) } Collapse.prototype.toggle = function () { this[this.$element.hasClass('in') ? 'hide' : 'show']() } Collapse.prototype.getParent = function () { return $(this.options.parent) .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') .each($.proxy(function (i, element) { var $element = $(element) this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) }, this)) .end() } Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { var isOpen = $element.hasClass('in') $element.attr('aria-expanded', isOpen) $trigger .toggleClass('collapsed', !isOpen) .attr('aria-expanded', isOpen) } function getTargetFromTrigger($trigger) { var href var target = $trigger.attr('data-target') || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 return $(target) } // COLLAPSE PLUGIN DEFINITION // ========================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.collapse') var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) if (typeof option == 'string') data[option]() }) } var old = $.fn.collapse $.fn.collapse = Plugin $.fn.collapse.Constructor = Collapse // COLLAPSE NO CONFLICT // ==================== $.fn.collapse.noConflict = function () { $.fn.collapse = old return this } // COLLAPSE DATA-API // ================= $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { var $this = $(this) if (!$this.attr('data-target')) e.preventDefault() var $target = getTargetFromTrigger($this) var data = $target.data('bs.collapse') var option = data ? 'toggle' : $this.data() Plugin.call($target, option) }) }(jQuery); /* ======================================================================== * Bootstrap: dropdown.js v3.3.7 * http://getbootstrap.com/javascript/#dropdowns * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // DROPDOWN CLASS DEFINITION // ========================= var backdrop = '.dropdown-backdrop' var toggle = '[data-toggle="dropdown"]' var Dropdown = function (element) { $(element).on('click.bs.dropdown', this.toggle) } Dropdown.VERSION = '3.3.7' function getParent($this) { var selector = $this.attr('data-target') if (!selector) { selector = $this.attr('href') selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } var $parent = selector && $(selector) return $parent && $parent.length ? $parent : $this.parent() } function clearMenus(e) { if (e && e.which === 3) return $(backdrop).remove() $(toggle).each(function () { var $this = $(this) var $parent = getParent($this) var relatedTarget = { relatedTarget: this } if (!$parent.hasClass('open')) return if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) if (e.isDefaultPrevented()) return $this.attr('aria-expanded', 'false') $parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget)) }) } Dropdown.prototype.toggle = function (e) { var $this = $(this) if ($this.is('.disabled, :disabled')) return var $parent = getParent($this) var isActive = $parent.hasClass('open') clearMenus() if (!isActive) { if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { // if mobile we use a backdrop because click events don't delegate $(document.createElement('div')) .addClass('dropdown-backdrop') .insertAfter($(this)) .on('click', clearMenus) } var relatedTarget = { relatedTarget: this } $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) if (e.isDefaultPrevented()) return $this .trigger('focus') .attr('aria-expanded', 'true') $parent .toggleClass('open') .trigger($.Event('shown.bs.dropdown', relatedTarget)) } return false } Dropdown.prototype.keydown = function (e) { if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return var $this = $(this) e.preventDefault() e.stopPropagation() if ($this.is('.disabled, :disabled')) return var $parent = getParent($this) var isActive = $parent.hasClass('open') if (!isActive && e.which != 27 || isActive && e.which == 27) { if (e.which == 27) $parent.find(toggle).trigger('focus') return $this.trigger('click') } var desc = ' li:not(.disabled):visible a' var $items = $parent.find('.dropdown-menu' + desc) if (!$items.length) return var index = $items.index(e.target) if (e.which == 38 && index > 0) index-- // up if (e.which == 40 && index < $items.length - 1) index++ // down if (!~index) index = 0 $items.eq(index).trigger('focus') } // DROPDOWN PLUGIN DEFINITION // ========================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.dropdown') if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) if (typeof option == 'string') data[option].call($this) }) } var old = $.fn.dropdown $.fn.dropdown = Plugin $.fn.dropdown.Constructor = Dropdown // DROPDOWN NO CONFLICT // ==================== $.fn.dropdown.noConflict = function () { $.fn.dropdown = old return this } // APPLY TO STANDARD DROPDOWN ELEMENTS // =================================== $(document) .on('click.bs.dropdown.data-api', clearMenus) .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown) }(jQuery); /* ======================================================================== * Bootstrap: modal.js v3.3.7 * http://getbootstrap.com/javascript/#modals * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // MODAL CLASS DEFINITION // ====================== var Modal = function (element, options) { this.options = options this.$body = $(document.body) this.$element = $(element) this.$dialog = this.$element.find('.modal-dialog') this.$backdrop = null this.isShown = null this.originalBodyPad = null this.scrollbarWidth = 0 this.ignoreBackdropClick = false if (this.options.remote) { this.$element .find('.modal-content') .load(this.options.remote, $.proxy(function () { this.$element.trigger('loaded.bs.modal') }, this)) } } Modal.VERSION = '3.3.7' Modal.TRANSITION_DURATION = 300 Modal.BACKDROP_TRANSITION_DURATION = 150 Modal.DEFAULTS = { backdrop: true, keyboard: true, show: true } Modal.prototype.toggle = function (_relatedTarget) { return this.isShown ? this.hide() : this.show(_relatedTarget) } Modal.prototype.show = function (_relatedTarget) { var that = this var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) this.$element.trigger(e) if (this.isShown || e.isDefaultPrevented()) return this.isShown = true this.checkScrollbar() this.setScrollbar() this.$body.addClass('modal-open') this.escape() this.resize() this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) this.$dialog.on('mousedown.dismiss.bs.modal', function () { that.$element.one('mouseup.dismiss.bs.modal', function (e) { if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true }) }) this.backdrop(function () { var transition = $.support.transition && that.$element.hasClass('fade') if (!that.$element.parent().length) { that.$element.appendTo(that.$body) // don't move modals dom position } that.$element .show() .scrollTop(0) that.adjustDialog() if (transition) { that.$element[0].offsetWidth // force reflow } that.$element.addClass('in') that.enforceFocus() var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) transition ? that.$dialog // wait for modal to slide in .one('bsTransitionEnd', function () { that.$element.trigger('focus').trigger(e) }) .emulateTransitionEnd(Modal.TRANSITION_DURATION) : that.$element.trigger('focus').trigger(e) }) } Modal.prototype.hide = function (e) { if (e) e.preventDefault() e = $.Event('hide.bs.modal') this.$element.trigger(e) if (!this.isShown || e.isDefaultPrevented()) return this.isShown = false this.escape() this.resize() $(document).off('focusin.bs.modal') this.$element .removeClass('in') .off('click.dismiss.bs.modal') .off('mouseup.dismiss.bs.modal') this.$dialog.off('mousedown.dismiss.bs.modal') $.support.transition && this.$element.hasClass('fade') ? this.$element .one('bsTransitionEnd', $.proxy(this.hideModal, this)) .emulateTransitionEnd(Modal.TRANSITION_DURATION) : this.hideModal() } Modal.prototype.enforceFocus = function () { $(document) .off('focusin.bs.modal') // guard against infinite focus loop .on('focusin.bs.modal', $.proxy(function (e) { if (document !== e.target && this.$element[0] !== e.target && !this.$element.has(e.target).length) { this.$element.trigger('focus') } }, this)) } Modal.prototype.escape = function () { if (this.isShown && this.options.keyboard) { this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) { e.which == 27 && this.hide() }, this)) } else if (!this.isShown) { this.$element.off('keydown.dismiss.bs.modal') } } Modal.prototype.resize = function () { if (this.isShown) { $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this)) } else { $(window).off('resize.bs.modal') } } Modal.prototype.hideModal = function () { var that = this this.$element.hide() this.backdrop(function () { that.$body.removeClass('modal-open') that.resetAdjustments() that.resetScrollbar() that.$element.trigger('hidden.bs.modal') }) } Modal.prototype.removeBackdrop = function () { this.$backdrop && this.$backdrop.remove() this.$backdrop = null } Modal.prototype.backdrop = function (callback) { var that = this var animate = this.$element.hasClass('fade') ? 'fade' : '' if (this.isShown && this.options.backdrop) { var doAnimate = $.support.transition && animate this.$backdrop = $(document.createElement('div')) .addClass('modal-backdrop ' + animate) .appendTo(this.$body) this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { if (this.ignoreBackdropClick) { this.ignoreBackdropClick = false return } if (e.target !== e.currentTarget) return this.options.backdrop == 'static' ? this.$element[0].focus() : this.hide() }, this)) if (doAnimate) this.$backdrop[0].offsetWidth // force reflow this.$backdrop.addClass('in') if (!callback) return doAnimate ? this.$backdrop .one('bsTransitionEnd', callback) .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : callback() } else if (!this.isShown && this.$backdrop) { this.$backdrop.removeClass('in') var callbackRemove = function () { that.removeBackdrop() callback && callback() } $.support.transition && this.$element.hasClass('fade') ? this.$backdrop .one('bsTransitionEnd', callbackRemove) .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : callbackRemove() } else if (callback) { callback() } } // these following methods are used to handle overflowing modals Modal.prototype.handleUpdate = function () { this.adjustDialog() } Modal.prototype.adjustDialog = function () { var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight this.$element.css({ paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' }) } Modal.prototype.resetAdjustments = function () { this.$element.css({ paddingLeft: '', paddingRight: '' }) } Modal.prototype.checkScrollbar = function () { var fullWindowWidth = window.innerWidth if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8 var documentElementRect = document.documentElement.getBoundingClientRect() fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left) } this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth this.scrollbarWidth = this.measureScrollbar() } Modal.prototype.setScrollbar = function () { var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10) this.originalBodyPad = document.body.style.paddingRight || '' if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth) } Modal.prototype.resetScrollbar = function () { this.$body.css('padding-right', this.originalBodyPad) } Modal.prototype.measureScrollbar = function () { // thx walsh var scrollDiv = document.createElement('div') scrollDiv.className = 'modal-scrollbar-measure' this.$body.append(scrollDiv) var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth this.$body[0].removeChild(scrollDiv) return scrollbarWidth } // MODAL PLUGIN DEFINITION // ======================= function Plugin(option, _relatedTarget) { return this.each(function () { var $this = $(this) var data = $this.data('bs.modal') var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) if (!data) $this.data('bs.modal', (data = new Modal(this, options))) if (typeof option == 'string') data[option](_relatedTarget) else if (options.show) data.show(_relatedTarget) }) } var old = $.fn.modal $.fn.modal = Plugin $.fn.modal.Constructor = Modal // MODAL NO CONFLICT // ================= $.fn.modal.noConflict = function () { $.fn.modal = old return this } // MODAL DATA-API // ============== $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { var $this = $(this) var href = $this.attr('href') var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) if ($this.is('a')) e.preventDefault() $target.one('show.bs.modal', function (showEvent) { if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown $target.one('hidden.bs.modal', function () { $this.is(':visible') && $this.trigger('focus') }) }) Plugin.call($target, option, this) }) }(jQuery); /* ======================================================================== * Bootstrap: tooltip.js v3.3.7 * http://getbootstrap.com/javascript/#tooltip * Inspired by the original jQuery.tipsy by Jason Frame * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // TOOLTIP PUBLIC CLASS DEFINITION // =============================== var Tooltip = function (element, options) { this.type = null this.options = null this.enabled = null this.timeout = null this.hoverState = null this.$element = null this.inState = null this.init('tooltip', element, options) } Tooltip.VERSION = '3.3.7' Tooltip.TRANSITION_DURATION = 150 Tooltip.DEFAULTS = { animation: true, placement: 'top', selector: false, template: '

', trigger: 'hover focus', title: '', delay: 0, html: false, container: false, viewport: { selector: 'body', padding: 0 } } Tooltip.prototype.init = function (type, element, options) { this.enabled = true this.type = type this.$element = $(element) this.options = this.getOptions(options) this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport)) this.inState = { click: false, hover: false, focus: false } if (this.$element[0] instanceof document.constructor && !this.options.selector) { throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!') } var triggers = this.options.trigger.split(' ') for (var i = triggers.length; i--;) { var trigger = triggers[i] if (trigger == 'click') { this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) } else if (trigger != 'manual') { var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) } } this.options.selector ? (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : this.fixTitle() } Tooltip.prototype.getDefaults = function () { return Tooltip.DEFAULTS } Tooltip.prototype.getOptions = function (options) { options = $.extend({}, this.getDefaults(), this.$element.data(), options) if (options.delay && typeof options.delay == 'number') { options.delay = { show: options.delay, hide: options.delay } } return options } Tooltip.prototype.getDelegateOptions = function () { var options = {} var defaults = this.getDefaults() this._options && $.each(this._options, function (key, value) { if (defaults[key] != value) options[key] = value }) return options } Tooltip.prototype.enter = function (obj) { var self = obj instanceof this.constructor ? obj : $(obj.currentTarget).data('bs.' + this.type) if (!self) { self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) $(obj.currentTarget).data('bs.' + this.type, self) } if (obj instanceof $.Event) { self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true } if (self.tip().hasClass('in') || self.hoverState == 'in') { self.hoverState = 'in' return } clearTimeout(self.timeout) self.hoverState = 'in' if (!self.options.delay || !self.options.delay.show) return self.show() self.timeout = setTimeout(function () { if (self.hoverState == 'in') self.show() }, self.options.delay.show) } Tooltip.prototype.isInStateTrue = function () { for (var key in this.inState) { if (this.inState[key]) return true } return false } Tooltip.prototype.leave = function (obj) { var self = obj instanceof this.constructor ? obj : $(obj.currentTarget).data('bs.' + this.type) if (!self) { self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) $(obj.currentTarget).data('bs.' + this.type, self) } if (obj instanceof $.Event) { self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false } if (self.isInStateTrue()) return clearTimeout(self.timeout) self.hoverState = 'out' if (!self.options.delay || !self.options.delay.hide) return self.hide() self.timeout = setTimeout(function () { if (self.hoverState == 'out') self.hide() }, self.options.delay.hide) } Tooltip.prototype.show = function () { var e = $.Event('show.bs.' + this.type) if (this.hasContent() && this.enabled) { this.$element.trigger(e) var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]) if (e.isDefaultPrevented() || !inDom) return var that = this var $tip = this.tip() var tipId = this.getUID(this.type) this.setContent() $tip.attr('id', tipId) this.$element.attr('aria-describedby', tipId) if (this.options.animation) $tip.addClass('fade') var placement = typeof this.options.placement == 'function' ? this.options.placement.call(this, $tip[0], this.$element[0]) : this.options.placement var autoToken = /\s?auto?\s?/i var autoPlace = autoToken.test(placement) if (autoPlace) placement = placement.replace(autoToken, '') || 'top' $tip .detach() .css({ top: 0, left: 0, display: 'block' }) .addClass(placement) .data('bs.' + this.type, this) this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) this.$element.trigger('inserted.bs.' + this.type) var pos = this.getPosition() var actualWidth = $tip[0].offsetWidth var actualHeight = $tip[0].offsetHeight if (autoPlace) { var orgPlacement = placement var viewportDim = this.getPosition(this.$viewport) placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' : placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' : placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' : placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' : placement $tip .removeClass(orgPlacement) .addClass(placement) } var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) this.applyPlacement(calculatedOffset, placement) var complete = function () { var prevHoverState = that.hoverState that.$element.trigger('shown.bs.' + that.type) that.hoverState = null if (prevHoverState == 'out') that.leave(that) } $.support.transition && this.$tip.hasClass('fade') ? $tip .one('bsTransitionEnd', complete) .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : complete() } } Tooltip.prototype.applyPlacement = function (offset, placement) { var $tip = this.tip() var width = $tip[0].offsetWidth var height = $tip[0].offsetHeight // manually read margins because getBoundingClientRect includes difference var marginTop = parseInt($tip.css('margin-top'), 10) var marginLeft = parseInt($tip.css('margin-left'), 10) // we must check for NaN for ie 8/9 if (isNaN(marginTop)) marginTop = 0 if (isNaN(marginLeft)) marginLeft = 0 offset.top += marginTop offset.left += marginLeft // $.fn.offset doesn't round pixel values // so we use setOffset directly with our own function B-0 $.offset.setOffset($tip[0], $.extend({ using: function (props) { $tip.css({ top: Math.round(props.top), left: Math.round(props.left) }) } }, offset), 0) $tip.addClass('in') // check to see if placing tip in new offset caused the tip to resize itself var actualWidth = $tip[0].offsetWidth var actualHeight = $tip[0].offsetHeight if (placement == 'top' && actualHeight != height) { offset.top = offset.top + height - actualHeight } var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) if (delta.left) offset.left += delta.left else offset.top += delta.top var isVertical = /top|bottom/.test(placement) var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight' $tip.offset(offset) this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical) } Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) { this.arrow() .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%') .css(isVertical ? 'top' : 'left', '') } Tooltip.prototype.setContent = function () { var $tip = this.tip() var title = this.getTitle() $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) $tip.removeClass('fade in top bottom left right') } Tooltip.prototype.hide = function (callback) { var that = this var $tip = $(this.$tip) var e = $.Event('hide.bs.' + this.type) function complete() { if (that.hoverState != 'in') $tip.detach() if (that.$element) { // TODO: Check whether guarding this code with this `if` is really necessary. that.$element .removeAttr('aria-describedby') .trigger('hidden.bs.' + that.type) } callback && callback() } this.$element.trigger(e) if (e.isDefaultPrevented()) return $tip.removeClass('in') $.support.transition && $tip.hasClass('fade') ? $tip .one('bsTransitionEnd', complete) .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : complete() this.hoverState = null return this } Tooltip.prototype.fixTitle = function () { var $e = this.$element if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') { $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') } } Tooltip.prototype.hasContent = function () { return this.getTitle() } Tooltip.prototype.getPosition = function ($element) { $element = $element || this.$element var el = $element[0] var isBody = el.tagName == 'BODY' var elRect = el.getBoundingClientRect() if (elRect.width == null) { // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093 elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top }) } var isSvg = window.SVGElement && el instanceof window.SVGElement // Avoid using $.offset() on SVGs since it gives incorrect results in jQuery 3. // See https://github.com/twbs/bootstrap/issues/20280 var elOffset = isBody ? { top: 0, left: 0 } : (isSvg ? null : $element.offset()) var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() } var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null return $.extend({}, elRect, scroll, outerDims, elOffset) } Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } } Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) { var delta = { top: 0, left: 0 } if (!this.$viewport) return delta var viewportPadding = this.options.viewport && this.options.viewport.padding || 0 var viewportDimensions = this.getPosition(this.$viewport) if (/right|left/.test(placement)) { var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight if (topEdgeOffset < viewportDimensions.top) { // top overflow delta.top = viewportDimensions.top - topEdgeOffset } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset } } else { var leftEdgeOffset = pos.left - viewportPadding var rightEdgeOffset = pos.left + viewportPadding + actualWidth if (leftEdgeOffset < viewportDimensions.left) { // left overflow delta.left = viewportDimensions.left - leftEdgeOffset } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset } } return delta } Tooltip.prototype.getTitle = function () { var title var $e = this.$element var o = this.options title = $e.attr('data-original-title') || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) return title } Tooltip.prototype.getUID = function (prefix) { do prefix += ~~(Math.random() * 1000000) while (document.getElementById(prefix)) return prefix } Tooltip.prototype.tip = function () { if (!this.$tip) { this.$tip = $(this.options.template) if (this.$tip.length != 1) { throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!') } } return this.$tip } Tooltip.prototype.arrow = function () { return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')) } Tooltip.prototype.enable = function () { this.enabled = true } Tooltip.prototype.disable = function () { this.enabled = false } Tooltip.prototype.toggleEnabled = function () { this.enabled = !this.enabled } Tooltip.prototype.toggle = function (e) { var self = this if (e) { self = $(e.currentTarget).data('bs.' + this.type) if (!self) { self = new this.constructor(e.currentTarget, this.getDelegateOptions()) $(e.currentTarget).data('bs.' + this.type, self) } } if (e) { self.inState.click = !self.inState.click if (self.isInStateTrue()) self.enter(self) else self.leave(self) } else { self.tip().hasClass('in') ? self.leave(self) : self.enter(self) } } Tooltip.prototype.destroy = function () { var that = this clearTimeout(this.timeout) this.hide(function () { that.$element.off('.' + that.type).removeData('bs.' + that.type) if (that.$tip) { that.$tip.detach() } that.$tip = null that.$arrow = null that.$viewport = null that.$element = null }) } // TOOLTIP PLUGIN DEFINITION // ========================= function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.tooltip') var options = typeof option == 'object' && option if (!data && /destroy|hide/.test(option)) return if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) if (typeof option == 'string') data[option]() }) } var old = $.fn.tooltip $.fn.tooltip = Plugin $.fn.tooltip.Constructor = Tooltip // TOOLTIP NO CONFLICT // =================== $.fn.tooltip.noConflict = function () { $.fn.tooltip = old return this } }(jQuery); /* ======================================================================== * Bootstrap: popover.js v3.3.7 * http://getbootstrap.com/javascript/#popovers * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // POPOVER PUBLIC CLASS DEFINITION // =============================== var Popover = function (element, options) { this.init('popover', element, options) } if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') Popover.VERSION = '3.3.7' Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { placement: 'right', trigger: 'click', content: '', template: '' }) // NOTE: POPOVER EXTENDS tooltip.js // ================================ Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) Popover.prototype.constructor = Popover Popover.prototype.getDefaults = function () { return Popover.DEFAULTS } Popover.prototype.setContent = function () { var $tip = this.tip() var title = this.getTitle() var content = this.getContent() $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) $tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' ](content) $tip.removeClass('fade top bottom left right in') // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do // this manually by checking the contents. if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() } Popover.prototype.hasContent = function () { return this.getTitle() || this.getContent() } Popover.prototype.getContent = function () { var $e = this.$element var o = this.options return $e.attr('data-content') || (typeof o.content == 'function' ? o.content.call($e[0]) : o.content) } Popover.prototype.arrow = function () { return (this.$arrow = this.$arrow || this.tip().find('.arrow')) } // POPOVER PLUGIN DEFINITION // ========================= function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.popover') var options = typeof option == 'object' && option if (!data && /destroy|hide/.test(option)) return if (!data) $this.data('bs.popover', (data = new Popover(this, options))) if (typeof option == 'string') data[option]() }) } var old = $.fn.popover $.fn.popover = Plugin $.fn.popover.Constructor = Popover // POPOVER NO CONFLICT // =================== $.fn.popover.noConflict = function () { $.fn.popover = old return this } }(jQuery); /* ======================================================================== * Bootstrap: scrollspy.js v3.3.7 * http://getbootstrap.com/javascript/#scrollspy * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // SCROLLSPY CLASS DEFINITION // ========================== function ScrollSpy(element, options) { this.$body = $(document.body) this.$scrollElement = $(element).is(document.body) ? $(window) : $(element) this.options = $.extend({}, ScrollSpy.DEFAULTS, options) this.selector = (this.options.target || '') + ' .nav li > a' this.offsets = [] this.targets = [] this.activeTarget = null this.scrollHeight = 0 this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this)) this.refresh() this.process() } ScrollSpy.VERSION = '3.3.7' ScrollSpy.DEFAULTS = { offset: 10 } ScrollSpy.prototype.getScrollHeight = function () { return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight) } ScrollSpy.prototype.refresh = function () { var that = this var offsetMethod = 'offset' var offsetBase = 0 this.offsets = [] this.targets = [] this.scrollHeight = this.getScrollHeight() if (!$.isWindow(this.$scrollElement[0])) { offsetMethod = 'position' offsetBase = this.$scrollElement.scrollTop() } this.$body .find(this.selector) .map(function () { var $el = $(this) var href = $el.data('target') || $el.attr('href') var $href = /^#./.test(href) && $(href) return ($href && $href.length && $href.is(':visible') && [[$href[offsetMethod]().top + offsetBase, href]]) || null }) .sort(function (a, b) { return a[0] - b[0] }) .each(function () { that.offsets.push(this[0]) that.targets.push(this[1]) }) } ScrollSpy.prototype.process = function () { var scrollTop = this.$scrollElement.scrollTop() + this.options.offset var scrollHeight = this.getScrollHeight() var maxScroll = this.options.offset + scrollHeight - this.$scrollElement.height() var offsets = this.offsets var targets = this.targets var activeTarget = this.activeTarget var i if (this.scrollHeight != scrollHeight) { this.refresh() } if (scrollTop >= maxScroll) { return activeTarget != (i = targets[targets.length - 1]) && this.activate(i) } if (activeTarget && scrollTop < offsets[0]) { this.activeTarget = null return this.clear() } for (i = offsets.length; i--;) { activeTarget != targets[i] && scrollTop >= offsets[i] && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1]) && this.activate(targets[i]) } } ScrollSpy.prototype.activate = function (target) { this.activeTarget = target this.clear() var selector = this.selector + '[data-target="' + target + '"],' + this.selector + '[href="' + target + '"]' var active = $(selector) .parents('li') .addClass('active') if (active.parent('.dropdown-menu').length) { active = active .closest('li.dropdown') .addClass('active') } active.trigger('activate.bs.scrollspy') } ScrollSpy.prototype.clear = function () { $(this.selector) .parentsUntil(this.options.target, '.active') .removeClass('active') } // SCROLLSPY PLUGIN DEFINITION // =========================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.scrollspy') var options = typeof option == 'object' && option if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) if (typeof option == 'string') data[option]() }) } var old = $.fn.scrollspy $.fn.scrollspy = Plugin $.fn.scrollspy.Constructor = ScrollSpy // SCROLLSPY NO CONFLICT // ===================== $.fn.scrollspy.noConflict = function () { $.fn.scrollspy = old return this } // SCROLLSPY DATA-API // ================== $(window).on('load.bs.scrollspy.data-api', function () { $('[data-spy="scroll"]').each(function () { var $spy = $(this) Plugin.call($spy, $spy.data()) }) }) }(jQuery); /* ======================================================================== * Bootstrap: tab.js v3.3.7 * http://getbootstrap.com/javascript/#tabs * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // TAB CLASS DEFINITION // ==================== var Tab = function (element) { // jscs:disable requireDollarBeforejQueryAssignment this.element = $(element) // jscs:enable requireDollarBeforejQueryAssignment } Tab.VERSION = '3.3.7' Tab.TRANSITION_DURATION = 150 Tab.prototype.show = function () { var $this = this.element var $ul = $this.closest('ul:not(.dropdown-menu)') var selector = $this.data('target') if (!selector) { selector = $this.attr('href') selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } if ($this.parent('li').hasClass('active')) return var $previous = $ul.find('.active:last a') var hideEvent = $.Event('hide.bs.tab', { relatedTarget: $this[0] }) var showEvent = $.Event('show.bs.tab', { relatedTarget: $previous[0] }) $previous.trigger(hideEvent) $this.trigger(showEvent) if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return var $target = $(selector) this.activate($this.closest('li'), $ul) this.activate($target, $target.parent(), function () { $previous.trigger({ type: 'hidden.bs.tab', relatedTarget: $this[0] }) $this.trigger({ type: 'shown.bs.tab', relatedTarget: $previous[0] }) }) } Tab.prototype.activate = function (element, container, callback) { var $active = container.find('> .active') var transition = callback && $.support.transition && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length) function next() { $active .removeClass('active') .find('> .dropdown-menu > .active') .removeClass('active') .end() .find('[data-toggle="tab"]') .attr('aria-expanded', false) element .addClass('active') .find('[data-toggle="tab"]') .attr('aria-expanded', true) if (transition) { element[0].offsetWidth // reflow for transition element.addClass('in') } else { element.removeClass('fade') } if (element.parent('.dropdown-menu').length) { element .closest('li.dropdown') .addClass('active') .end() .find('[data-toggle="tab"]') .attr('aria-expanded', true) } callback && callback() } $active.length && transition ? $active .one('bsTransitionEnd', next) .emulateTransitionEnd(Tab.TRANSITION_DURATION) : next() $active.removeClass('in') } // TAB PLUGIN DEFINITION // ===================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.tab') if (!data) $this.data('bs.tab', (data = new Tab(this))) if (typeof option == 'string') data[option]() }) } var old = $.fn.tab $.fn.tab = Plugin $.fn.tab.Constructor = Tab // TAB NO CONFLICT // =============== $.fn.tab.noConflict = function () { $.fn.tab = old return this } // TAB DATA-API // ============ var clickHandler = function (e) { e.preventDefault() Plugin.call($(this), 'show') } $(document) .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler) .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler) }(jQuery); /* ======================================================================== * Bootstrap: affix.js v3.3.7 * http://getbootstrap.com/javascript/#affix * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // AFFIX CLASS DEFINITION // ====================== var Affix = function (element, options) { this.options = $.extend({}, Affix.DEFAULTS, options) this.$target = $(this.options.target) .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) this.$element = $(element) this.affixed = null this.unpin = null this.pinnedOffset = null this.checkPosition() } Affix.VERSION = '3.3.7' Affix.RESET = 'affix affix-top affix-bottom' Affix.DEFAULTS = { offset: 0, target: window } Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) { var scrollTop = this.$target.scrollTop() var position = this.$element.offset() var targetHeight = this.$target.height() if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false if (this.affixed == 'bottom') { if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom' return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom' } var initializing = this.affixed == null var colliderTop = initializing ? scrollTop : position.top var colliderHeight = initializing ? targetHeight : height if (offsetTop != null && scrollTop <= offsetTop) return 'top' if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom' return false } Affix.prototype.getPinnedOffset = function () { if (this.pinnedOffset) return this.pinnedOffset this.$element.removeClass(Affix.RESET).addClass('affix') var scrollTop = this.$target.scrollTop() var position = this.$element.offset() return (this.pinnedOffset = position.top - scrollTop) } Affix.prototype.checkPositionWithEventLoop = function () { setTimeout($.proxy(this.checkPosition, this), 1) } Affix.prototype.checkPosition = function () { if (!this.$element.is(':visible')) return var height = this.$element.height() var offset = this.options.offset var offsetTop = offset.top var offsetBottom = offset.bottom var scrollHeight = Math.max($(document).height(), $(document.body).height()) if (typeof offset != 'object') offsetBottom = offsetTop = offset if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element) var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom) if (this.affixed != affix) { if (this.unpin != null) this.$element.css('top', '') var affixType = 'affix' + (affix ? '-' + affix : '') var e = $.Event(affixType + '.bs.affix') this.$element.trigger(e) if (e.isDefaultPrevented()) return this.affixed = affix this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null this.$element .removeClass(Affix.RESET) .addClass(affixType) .trigger(affixType.replace('affix', 'affixed') + '.bs.affix') } if (affix == 'bottom') { this.$element.offset({ top: scrollHeight - height - offsetBottom }) } } // AFFIX PLUGIN DEFINITION // ======================= function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.affix') var options = typeof option == 'object' && option if (!data) $this.data('bs.affix', (data = new Affix(this, options))) if (typeof option == 'string') data[option]() }) } var old = $.fn.affix $.fn.affix = Plugin $.fn.affix.Constructor = Affix // AFFIX NO CONFLICT // ================= $.fn.affix.noConflict = function () { $.fn.affix = old return this } // AFFIX DATA-API // ============== $(window).on('load', function () { $('[data-spy="affix"]').each(function () { var $spy = $(this) var data = $spy.data() data.offset = data.offset || {} if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom if (data.offsetTop != null) data.offset.top = data.offsetTop Plugin.call($spy, data) }) }) }(jQuery); ================================================ FILE: dis-seckill-gateway/src/main/resources/static/bootstrap/js/npm.js ================================================ // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. require('../../js/transition.js') require('../../js/alert.js') require('../../js/button.js') require('../../js/carousel.js') require('../../js/collapse.js') require('../../js/dropdown.js') require('../../js/modal.js') require('../../js/tooltip.js') require('../../js/popover.js') require('../../js/scrollspy.js') require('../../js/tab.js') require('../../js/affix.js') ================================================ FILE: dis-seckill-gateway/src/main/resources/static/goods_detail.htm ================================================ 商品详情 商品详情

秒杀商品详情

您还没有登录,请登陆后再操作

商品名称
商品图片
秒杀开始时间
商品原价
秒杀价
库存数量
================================================ FILE: dis-seckill-gateway/src/main/resources/static/js/common.js ================================================ /** * 展示loading弹框 * @returns {*} */ function g_showLoading() { var idx = layer.msg('处理中...', {icon: 16, shade: [0.5, '#f5f5f5'], scrollbar: false, offset: '0px', time: 100000}); return idx; } // salt 与服务器端的第一次salt是相同 var g_passsword_salt = "1a2b3c4d"; // 获取url参数 function g_getQueryString(name) { var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); var r = window.location.search.substr(1).match(reg); if (r != null) return unescape(r[2]); return null; }; // 设定时间格式化函数,使用new Date().format("yyyy-MM-dd-hh-mm-ss"); Date.prototype.format = function (format) { var args = { "M+": this.getMonth() + 1, "d+": this.getDate(), "h+": this.getHours(), "m+": this.getMinutes(), "s+": this.getSeconds(), }; if (/(y+)/.test(format)) format = format.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)); for (var i in args) { var n = args[i]; if (new RegExp("(" + i + ")").test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? n : ("00" + n).substr(("" + n).length)); } return format; }; ================================================ FILE: dis-seckill-gateway/src/main/resources/static/layer/layer.js ================================================ /*! layer-v3.0.3 Web弹层组件 MIT License http://layer.layui.com/ By 贤心 */ ;!function(e,t){"use strict";var i,n,a=e.layui&&layui.define,o={getPath:function(){var e=document.scripts,t=e[e.length-1],i=t.src;if(!t.getAttribute("merge"))return i.substring(0,i.lastIndexOf("/")+1)}(),config:{},end:{},minIndex:0,minLeft:[],btn:["确定","取消"],type:["dialog","page","iframe","loading","tips"]},r={v:"3.0.3",ie:function(){var t=navigator.userAgent.toLowerCase();return!!(e.ActiveXObject||"ActiveXObject"in e)&&((t.match(/msie\s(\d+)/)||[])[1]||"11")}(),index:e.layer&&e.layer.v?1e5:0,path:o.getPath,config:function(e,t){return e=e||{},r.cache=o.config=i.extend({},o.config,e),r.path=o.config.path||r.path,"string"==typeof e.extend&&(e.extend=[e.extend]),o.config.path&&r.ready(),e.extend?(a?layui.addcss("modules/layer/"+e.extend):r.link("skin/"+e.extend),this):this},link:function(t,n,a){if(r.path){var o=i("head")[0],s=document.createElement("link");"string"==typeof n&&(a=n);var l=(a||t).replace(/\.|\//g,""),f="layuicss-"+l,c=0;s.rel="stylesheet",s.href=r.path+t,s.id=f,i("#"+f)[0]||o.appendChild(s),"function"==typeof n&&!function u(){return++c>80?e.console&&console.error("layer.css: Invalid"):void(1989===parseInt(i("#"+f).css("width"))?n():setTimeout(u,100))}()}},ready:function(e){var t="skinlayercss",i="303";return a?layui.addcss("modules/layer/default/layer.css?v="+r.v+i,e,t):r.link("skin/default/layer.css?v="+r.v+i,e,t),this},alert:function(e,t,n){var a="function"==typeof t;return a&&(n=t),r.open(i.extend({content:e,yes:n},a?{}:t))},confirm:function(e,t,n,a){var s="function"==typeof t;return s&&(a=n,n=t),r.open(i.extend({content:e,btn:o.btn,yes:n,btn2:a},s?{}:t))},msg:function(e,n,a){var s="function"==typeof n,f=o.config.skin,c=(f?f+" "+f+"-msg":"")||"layui-layer-msg",u=l.anim.length-1;return s&&(a=n),r.open(i.extend({content:e,time:3e3,shade:!1,skin:c,title:!1,closeBtn:!1,btn:!1,resize:!1,end:a},s&&!o.config.skin?{skin:c+" layui-layer-hui",anim:u}:function(){return n=n||{},(n.icon===-1||n.icon===t&&!o.config.skin)&&(n.skin=c+" "+(n.skin||"layui-layer-hui")),n}()))},load:function(e,t){return r.open(i.extend({type:3,icon:e||0,resize:!1,shade:.01},t))},tips:function(e,t,n){return r.open(i.extend({type:4,content:[e,t],closeBtn:!1,time:3e3,shade:!1,resize:!1,fixed:!1,maxWidth:210},n))}},s=function(e){var t=this;t.index=++r.index,t.config=i.extend({},t.config,o.config,e),document.body?t.creat():setTimeout(function(){t.creat()},30)};s.pt=s.prototype;var l=["layui-layer",".layui-layer-title",".layui-layer-main",".layui-layer-dialog","layui-layer-iframe","layui-layer-content","layui-layer-btn","layui-layer-close"];l.anim=["layer-anim","layer-anim-01","layer-anim-02","layer-anim-03","layer-anim-04","layer-anim-05","layer-anim-06"],s.pt.config={type:0,shade:.3,fixed:!0,move:l[1],title:"信息",offset:"auto",area:"auto",closeBtn:1,time:0,zIndex:19891014,maxWidth:360,anim:0,isOutAnim:!0,icon:-1,moveType:1,resize:!0,scrollbar:!0,tips:2},s.pt.vessel=function(e,t){var n=this,a=n.index,r=n.config,s=r.zIndex+a,f="object"==typeof r.title,c=r.maxmin&&(1===r.type||2===r.type),u=r.title?'
'+(f?r.title[0]:r.title)+"
":"";return r.zIndex=s,t([r.shade?'
':"",'
'+(e&&2!=r.type?"":u)+'
'+(0==r.type&&r.icon!==-1?'':"")+(1==r.type&&e?"":r.content||"")+'
'+function(){var e=c?'':"";return r.closeBtn&&(e+=''),e}()+""+(r.btn?function(){var e="";"string"==typeof r.btn&&(r.btn=[r.btn]);for(var t=0,i=r.btn.length;t'+r.btn[t]+"";return'
'+e+"
"}():"")+(r.resize?'':"")+"
"],u,i('
')),n},s.pt.creat=function(){var e=this,t=e.config,a=e.index,s=t.content,f="object"==typeof s,c=i("body");if(!t.id||!i("#"+t.id)[0]){switch("string"==typeof t.area&&(t.area="auto"===t.area?["",""]:[t.area,""]),t.shift&&(t.anim=t.shift),6==r.ie&&(t.fixed=!1),t.type){case 0:t.btn="btn"in t?t.btn:o.btn[0],r.closeAll("dialog");break;case 2:var s=t.content=f?t.content:[t.content,"auto"];t.content='';break;case 3:delete t.title,delete t.closeBtn,t.icon===-1&&0===t.icon,r.closeAll("loading");break;case 4:f||(t.content=[t.content,"body"]),t.follow=t.content[1],t.content=t.content[0]+'',delete t.title,t.tips="object"==typeof t.tips?t.tips:[t.tips,!0],t.tipsMore||r.closeAll("tips")}e.vessel(f,function(n,r,u){c.append(n[0]),f?function(){2==t.type||4==t.type?function(){i("body").append(n[1])}():function(){s.parents("."+l[0])[0]||(s.data("display",s.css("display")).show().addClass("layui-layer-wrap").wrap(n[1]),i("#"+l[0]+a).find("."+l[5]).before(r))}()}():c.append(n[1]),i(".layui-layer-move")[0]||c.append(o.moveElem=u),e.layero=i("#"+l[0]+a),t.scrollbar||l.html.css("overflow","hidden").attr("layer-full",a)}).auto(a),2==t.type&&6==r.ie&&e.layero.find("iframe").attr("src",s[0]),4==t.type?e.tips():e.offset(),t.fixed&&n.on("resize",function(){e.offset(),(/^\d+%$/.test(t.area[0])||/^\d+%$/.test(t.area[1]))&&e.auto(a),4==t.type&&e.tips()}),t.time<=0||setTimeout(function(){r.close(e.index)},t.time),e.move().callback(),l.anim[t.anim]&&e.layero.addClass(l.anim[t.anim]),t.isOutAnim&&e.layero.data("isOutAnim",!0)}},s.pt.auto=function(e){function t(e){e=s.find(e),e.height(f[1]-c-u-2*(0|parseFloat(e.css("padding-top"))))}var a=this,o=a.config,s=i("#"+l[0]+e);""===o.area[0]&&o.maxWidth>0&&(r.ie&&r.ie<8&&o.btn&&s.width(s.innerWidth()),s.outerWidth()>o.maxWidth&&s.width(o.maxWidth));var f=[s.innerWidth(),s.innerHeight()],c=s.find(l[1]).outerHeight()||0,u=s.find("."+l[6]).outerHeight()||0;switch(o.type){case 2:t("iframe");break;default:""===o.area[1]?o.fixed&&f[1]>=n.height()&&(f[1]=n.height(),t("."+l[5])):t("."+l[5])}return a},s.pt.offset=function(){var e=this,t=e.config,i=e.layero,a=[i.outerWidth(),i.outerHeight()],o="object"==typeof t.offset;e.offsetTop=(n.height()-a[1])/2,e.offsetLeft=(n.width()-a[0])/2,o?(e.offsetTop=t.offset[0],e.offsetLeft=t.offset[1]||e.offsetLeft):"auto"!==t.offset&&("t"===t.offset?e.offsetTop=0:"r"===t.offset?e.offsetLeft=n.width()-a[0]:"b"===t.offset?e.offsetTop=n.height()-a[1]:"l"===t.offset?e.offsetLeft=0:"lt"===t.offset?(e.offsetTop=0,e.offsetLeft=0):"lb"===t.offset?(e.offsetTop=n.height()-a[1],e.offsetLeft=0):"rt"===t.offset?(e.offsetTop=0,e.offsetLeft=n.width()-a[0]):"rb"===t.offset?(e.offsetTop=n.height()-a[1],e.offsetLeft=n.width()-a[0]):e.offsetTop=t.offset),t.fixed||(e.offsetTop=/%$/.test(e.offsetTop)?n.height()*parseFloat(e.offsetTop)/100:parseFloat(e.offsetTop),e.offsetLeft=/%$/.test(e.offsetLeft)?n.width()*parseFloat(e.offsetLeft)/100:parseFloat(e.offsetLeft),e.offsetTop+=n.scrollTop(),e.offsetLeft+=n.scrollLeft()),i.attr("minLeft")&&(e.offsetTop=n.height()-(i.find(l[1]).outerHeight()||0),e.offsetLeft=i.css("left")),i.css({top:e.offsetTop,left:e.offsetLeft})},s.pt.tips=function(){var e=this,t=e.config,a=e.layero,o=[a.outerWidth(),a.outerHeight()],r=i(t.follow);r[0]||(r=i("body"));var s={width:r.outerWidth(),height:r.outerHeight(),top:r.offset().top,left:r.offset().left},f=a.find(".layui-layer-TipsG"),c=t.tips[0];t.tips[1]||f.remove(),s.autoLeft=function(){s.left+o[0]-n.width()>0?(s.tipLeft=s.left+s.width-o[0],f.css({right:12,left:"auto"})):s.tipLeft=s.left},s.where=[function(){s.autoLeft(),s.tipTop=s.top-o[1]-10,f.removeClass("layui-layer-TipsB").addClass("layui-layer-TipsT").css("border-right-color",t.tips[1])},function(){s.tipLeft=s.left+s.width+10,s.tipTop=s.top,f.removeClass("layui-layer-TipsL").addClass("layui-layer-TipsR").css("border-bottom-color",t.tips[1])},function(){s.autoLeft(),s.tipTop=s.top+s.height+10,f.removeClass("layui-layer-TipsT").addClass("layui-layer-TipsB").css("border-right-color",t.tips[1])},function(){s.tipLeft=s.left-o[0]-10,s.tipTop=s.top,f.removeClass("layui-layer-TipsR").addClass("layui-layer-TipsL").css("border-bottom-color",t.tips[1])}],s.where[c-1](),1===c?s.top-(n.scrollTop()+o[1]+16)<0&&s.where[2]():2===c?n.width()-(s.left+s.width+o[0]+16)>0||s.where[3]():3===c?s.top-n.scrollTop()+s.height+o[1]+16-n.height()>0&&s.where[0]():4===c&&o[0]+16-s.left>0&&s.where[1](),a.find("."+l[5]).css({"background-color":t.tips[1],"padding-right":t.closeBtn?"30px":""}),a.css({left:s.tipLeft-(t.fixed?n.scrollLeft():0),top:s.tipTop-(t.fixed?n.scrollTop():0)})},s.pt.move=function(){var e=this,t=e.config,a=i(document),s=e.layero,l=s.find(t.move),f=s.find(".layui-layer-resize"),c={};return t.move&&l.css("cursor","move"),l.on("mousedown",function(e){e.preventDefault(),t.move&&(c.moveStart=!0,c.offset=[e.clientX-parseFloat(s.css("left")),e.clientY-parseFloat(s.css("top"))],o.moveElem.css("cursor","move").show())}),f.on("mousedown",function(e){e.preventDefault(),c.resizeStart=!0,c.offset=[e.clientX,e.clientY],c.area=[s.outerWidth(),s.outerHeight()],o.moveElem.css("cursor","se-resize").show()}),a.on("mousemove",function(i){if(c.moveStart){var a=i.clientX-c.offset[0],o=i.clientY-c.offset[1],l="fixed"===s.css("position");if(i.preventDefault(),c.stX=l?0:n.scrollLeft(),c.stY=l?0:n.scrollTop(),!t.moveOut){var f=n.width()-s.outerWidth()+c.stX,u=n.height()-s.outerHeight()+c.stY;af&&(a=f),ou&&(o=u)}s.css({left:a,top:o})}if(t.resize&&c.resizeStart){var a=i.clientX-c.offset[0],o=i.clientY-c.offset[1];i.preventDefault(),r.style(e.index,{width:c.area[0]+a,height:c.area[1]+o}),c.isResize=!0,t.resizing&&t.resizing(s)}}).on("mouseup",function(e){c.moveStart&&(delete c.moveStart,o.moveElem.hide(),t.moveEnd&&t.moveEnd(s)),c.resizeStart&&(delete c.resizeStart,o.moveElem.hide())}),e},s.pt.callback=function(){function e(){var e=a.cancel&&a.cancel(t.index,n);e===!1||r.close(t.index)}var t=this,n=t.layero,a=t.config;t.openLayer(),a.success&&(2==a.type?n.find("iframe").on("load",function(){a.success(n,t.index)}):a.success(n,t.index)),6==r.ie&&t.IE6(n),n.find("."+l[6]).children("a").on("click",function(){var e=i(this).index();if(0===e)a.yes?a.yes(t.index,n):a.btn1?a.btn1(t.index,n):r.close(t.index);else{var o=a["btn"+(e+1)]&&a["btn"+(e+1)](t.index,n);o===!1||r.close(t.index)}}),n.find("."+l[7]).on("click",e),a.shadeClose&&i("#layui-layer-shade"+t.index).on("click",function(){r.close(t.index)}),n.find(".layui-layer-min").on("click",function(){var e=a.min&&a.min(n);e===!1||r.min(t.index,a)}),n.find(".layui-layer-max").on("click",function(){i(this).hasClass("layui-layer-maxmin")?(r.restore(t.index),a.restore&&a.restore(n)):(r.full(t.index,a),setTimeout(function(){a.full&&a.full(n)},100))}),a.end&&(o.end[t.index]=a.end)},o.reselect=function(){i.each(i("select"),function(e,t){var n=i(this);n.parents("."+l[0])[0]||1==n.attr("layer")&&i("."+l[0]).length<1&&n.removeAttr("layer").show(),n=null})},s.pt.IE6=function(e){i("select").each(function(e,t){var n=i(this);n.parents("."+l[0])[0]||"none"===n.css("display")||n.attr({layer:"1"}).hide(),n=null})},s.pt.openLayer=function(){var e=this;r.zIndex=e.config.zIndex,r.setTop=function(e){var t=function(){r.zIndex++,e.css("z-index",r.zIndex+1)};return r.zIndex=parseInt(e[0].style.zIndex),e.on("mousedown",t),r.zIndex}},o.record=function(e){var t=[e.width(),e.height(),e.position().top,e.position().left+parseFloat(e.css("margin-left"))];e.find(".layui-layer-max").addClass("layui-layer-maxmin"),e.attr({area:t})},o.rescollbar=function(e){l.html.attr("layer-full")==e&&(l.html[0].style.removeProperty?l.html[0].style.removeProperty("overflow"):l.html[0].style.removeAttribute("overflow"),l.html.removeAttr("layer-full"))},e.layer=r,r.getChildFrame=function(e,t){return t=t||i("."+l[4]).attr("times"),i("#"+l[0]+t).find("iframe").contents().find(e)},r.getFrameIndex=function(e){return i("#"+e).parents("."+l[4]).attr("times")},r.iframeAuto=function(e){if(e){var t=r.getChildFrame("html",e).outerHeight(),n=i("#"+l[0]+e),a=n.find(l[1]).outerHeight()||0,o=n.find("."+l[6]).outerHeight()||0;n.css({height:t+a+o}),n.find("iframe").css({height:t})}},r.iframeSrc=function(e,t){i("#"+l[0]+e).find("iframe").attr("src",t)},r.style=function(e,t,n){var a=i("#"+l[0]+e),r=a.find(".layui-layer-content"),s=a.attr("type"),f=a.find(l[1]).outerHeight()||0,c=a.find("."+l[6]).outerHeight()||0;a.attr("minLeft");s!==o.type[3]&&s!==o.type[4]&&(n||(parseFloat(t.width)<=260&&(t.width=260),parseFloat(t.height)-f-c<=64&&(t.height=64+f+c)),a.css(t),c=a.find("."+l[6]).outerHeight(),s===o.type[2]?a.find("iframe").css({height:parseFloat(t.height)-f-c}):r.css({height:parseFloat(t.height)-f-c-parseFloat(r.css("padding-top"))-parseFloat(r.css("padding-bottom"))}))},r.min=function(e,t){var a=i("#"+l[0]+e),s=a.find(l[1]).outerHeight()||0,f=a.attr("minLeft")||181*o.minIndex+"px",c=a.css("position");o.record(a),o.minLeft[0]&&(f=o.minLeft[0],o.minLeft.shift()),a.attr("position",c),r.style(e,{width:180,height:s,left:f,top:n.height()-s,position:"fixed",overflow:"hidden"},!0),a.find(".layui-layer-min").hide(),"page"===a.attr("type")&&a.find(l[4]).hide(),o.rescollbar(e),a.attr("minLeft")||o.minIndex++,a.attr("minLeft",f)},r.restore=function(e){var t=i("#"+l[0]+e),n=t.attr("area").split(",");t.attr("type");r.style(e,{width:parseFloat(n[0]),height:parseFloat(n[1]),top:parseFloat(n[2]),left:parseFloat(n[3]),position:t.attr("position"),overflow:"visible"},!0),t.find(".layui-layer-max").removeClass("layui-layer-maxmin"),t.find(".layui-layer-min").show(),"page"===t.attr("type")&&t.find(l[4]).show(),o.rescollbar(e)},r.full=function(e){var t,a=i("#"+l[0]+e);o.record(a),l.html.attr("layer-full")||l.html.css("overflow","hidden").attr("layer-full",e),clearTimeout(t),t=setTimeout(function(){var t="fixed"===a.css("position");r.style(e,{top:t?0:n.scrollTop(),left:t?0:n.scrollLeft(),width:n.width(),height:n.height()},!0),a.find(".layui-layer-min").hide()},100)},r.title=function(e,t){var n=i("#"+l[0]+(t||r.index)).find(l[1]);n.html(e)},r.close=function(e){var t=i("#"+l[0]+e),n=t.attr("type"),a="layer-anim-close";if(t[0]){var s="layui-layer-wrap",f=function(){if(n===o.type[1]&&"object"===t.attr("conType")){t.children(":not(."+l[5]+")").remove();for(var a=t.find("."+s),r=0;r<2;r++)a.unwrap();a.css("display",a.data("display")).removeClass(s)}else{if(n===o.type[2])try{var f=i("#"+l[4]+e)[0];f.contentWindow.document.write(""),f.contentWindow.close(),t.find("."+l[5])[0].removeChild(f)}catch(c){}t[0].innerHTML="",t.remove()}"function"==typeof o.end[e]&&o.end[e](),delete o.end[e]};t.data("isOutAnim")&&t.addClass(a),i("#layui-layer-moves, #layui-layer-shade"+e).remove(),6==r.ie&&o.reselect(),o.rescollbar(e),t.attr("minLeft")&&(o.minIndex--,o.minLeft.push(t.attr("minLeft"))),r.ie&&r.ie<10||!t.data("isOutAnim")?f():setTimeout(function(){f()},200)}},r.closeAll=function(e){i.each(i("."+l[0]),function(){var t=i(this),n=e?t.attr("type")===e:1;n&&r.close(t.attr("times")),n=null})};var f=r.cache||{},c=function(e){return f.skin?" "+f.skin+" "+f.skin+"-"+e:""};r.prompt=function(e,t){var a="";if(e=e||{},"function"==typeof e&&(t=e),e.area){var o=e.area;a='style="width: '+o[0]+"; height: "+o[1]+';"',delete e.area}var s,l=2==e.formType?'":function(){return''}(),f=e.success;return delete e.success,r.open(i.extend({type:1,btn:["确定","取消"],content:l,skin:"layui-layer-prompt"+c("prompt"),maxWidth:n.width(),success:function(e){s=e.find(".layui-layer-input"),s.focus(),"function"==typeof f&&f(e)},resize:!1,yes:function(i){var n=s.val();""===n?s.focus():n.length>(e.maxlength||500)?r.tips("最多输入"+(e.maxlength||500)+"个字数",s,{tips:1}):t&&t(n,i,s)}},e))},r.tab=function(e){e=e||{};var t=e.tab||{},n=e.success;return delete e.success,r.open(i.extend({type:1,skin:"layui-layer-tab"+c("tab"),resize:!1,title:function(){var e=t.length,i=1,n="";if(e>0)for(n=''+t[0].title+"";i"+t[i].title+"";return n}(),content:'
    '+function(){var e=t.length,i=1,n="";if(e>0)for(n='
  • '+(t[0].content||"no content")+"
  • ";i'+(t[i].content||"no content")+"";return n}()+"
",success:function(t){var a=t.find(".layui-layer-title").children(),o=t.find(".layui-layer-tabmain").children();a.on("mousedown",function(t){t.stopPropagation?t.stopPropagation():t.cancelBubble=!0;var n=i(this),a=n.index();n.addClass("layui-layer-tabnow").siblings().removeClass("layui-layer-tabnow"),o.eq(a).show().siblings().hide(),"function"==typeof e.change&&e.change(a)}),"function"==typeof n&&n(t)}},e))},r.photos=function(t,n,a){function o(e,t,i){var n=new Image;return n.src=e,n.complete?t(n):(n.onload=function(){n.onload=null,t(n)},void(n.onerror=function(e){n.onerror=null,i(e)}))}var s={};if(t=t||{},t.photos){var l=t.photos.constructor===Object,f=l?t.photos:{},u=f.data||[],d=f.start||0;s.imgIndex=(0|d)+1,t.img=t.img||"img";var y=t.success;if(delete t.success,l){if(0===u.length)return r.msg("没有图片")}else{var p=i(t.photos),h=function(){u=[],p.find(t.img).each(function(e){var t=i(this);t.attr("layer-index",e),u.push({alt:t.attr("alt"),pid:t.attr("layer-pid"),src:t.attr("layer-src")||t.attr("src"),thumb:t.attr("src")})})};if(h(),0===u.length)return;if(n||p.on("click",t.img,function(){var e=i(this),n=e.attr("layer-index");r.photos(i.extend(t,{photos:{start:n,data:u,tab:t.tab},full:t.full}),!0),h()}),!n)return}s.imgprev=function(e){s.imgIndex--,s.imgIndex<1&&(s.imgIndex=u.length),s.tabimg(e)},s.imgnext=function(e,t){s.imgIndex++,s.imgIndex>u.length&&(s.imgIndex=1,t)||s.tabimg(e)},s.keyup=function(e){if(!s.end){var t=e.keyCode;e.preventDefault(),37===t?s.imgprev(!0):39===t?s.imgnext(!0):27===t&&r.close(s.index)}},s.tabimg=function(e){if(!(u.length<=1))return f.start=s.imgIndex-1,r.close(s.index),r.photos(t,!0,e)},s.event=function(){s.bigimg.hover(function(){s.imgsee.show()},function(){s.imgsee.hide()}),s.bigimg.find(".layui-layer-imgprev").on("click",function(e){e.preventDefault(),s.imgprev()}),s.bigimg.find(".layui-layer-imgnext").on("click",function(e){e.preventDefault(),s.imgnext()}),i(document).on("keyup",s.keyup)},s.loadi=r.load(1,{shade:!("shade"in t)&&.9,scrollbar:!1}),o(u[d].src,function(n){r.close(s.loadi),s.index=r.open(i.extend({type:1,id:"layui-layer-photos",area:function(){var a=[n.width,n.height],o=[i(e).width()-100,i(e).height()-100];if(!t.full&&(a[0]>o[0]||a[1]>o[1])){var r=[a[0]/o[0],a[1]/o[1]];r[0]>r[1]?(a[0]=a[0]/r[0],a[1]=a[1]/r[0]):r[0]'+(u[d].alt||
'+(u.length>1?'':"")+'
'+(u[d].alt||"")+""+s.imgIndex+"/"+u.length+"
",success:function(e,i){s.bigimg=e.find(".layui-layer-phimg"),s.imgsee=e.find(".layui-layer-imguide,.layui-layer-imgbar"),s.event(e),t.tab&&t.tab(u[d],e),"function"==typeof y&&y(e)},end:function(){s.end=!0,i(document).off("keyup",s.keyup)}},t))},function(){r.close(s.loadi),r.msg("当前图片地址异常
是否继续查看下一张?",{time:3e4,btn:["下一张","不看了"],yes:function(){u.length>1&&s.imgnext(!0,!0)}})})}},o.run=function(t){i=t,n=i(e),l.html=i("html"),r.open=function(e){var t=new s(e);return t.index}},e.layui&&layui.define?(r.ready(),layui.define("jquery",function(t){r.path=layui.cache.dir,o.run(layui.jquery),e.layer=r,t("layer",r)})):"function"==typeof define&&define.amd?define(["jquery"],function(){return o.run(e.jQuery),r}):function(){o.run(e.jQuery),r.ready()}()}(window); ================================================ FILE: dis-seckill-gateway/src/main/resources/static/layer/mobile/layer.js ================================================ /*! layer mobile-v2.0.0 Web弹层组件 MIT License http://layer.layui.com/mobile By 贤心 */ ;!function(e){"use strict";var t=document,n="querySelectorAll",i="getElementsByClassName",a=function(e){return t[n](e)},s={type:0,shade:!0,shadeClose:!0,fixed:!0,anim:"scale"},l={extend:function(e){var t=JSON.parse(JSON.stringify(s));for(var n in e)t[n]=e[n];return t},timer:{},end:{}};l.touch=function(e,t){e.addEventListener("click",function(e){t.call(this,e)},!1)};var r=0,o=["layui-m-layer"],c=function(e){var t=this;t.config=l.extend(e),t.view()};c.prototype.view=function(){var e=this,n=e.config,s=t.createElement("div");e.id=s.id=o[0]+r,s.setAttribute("class",o[0]+" "+o[0]+(n.type||0)),s.setAttribute("index",r);var l=function(){var e="object"==typeof n.title;return n.title?'

'+(e?n.title[0]:n.title)+"

":""}(),c=function(){"string"==typeof n.btn&&(n.btn=[n.btn]);var e,t=(n.btn||[]).length;return 0!==t&&n.btn?(e=''+n.btn[0]+"",2===t&&(e=''+n.btn[1]+""+e),'
'+e+"
"):""}();if(n.fixed||(n.top=n.hasOwnProperty("top")?n.top:100,n.style=n.style||"",n.style+=" top:"+(t.body.scrollTop+n.top)+"px"),2===n.type&&(n.content='

'+(n.content||"")+"

"),n.skin&&(n.anim="up"),"msg"===n.skin&&(n.shade=!1),s.innerHTML=(n.shade?"
':"")+'
"+l+'
'+n.content+"
"+c+"
",!n.type||2===n.type){var d=t[i](o[0]+n.type),y=d.length;y>=1&&layer.close(d[0].getAttribute("index"))}document.body.appendChild(s);var u=e.elem=a("#"+e.id)[0];n.success&&n.success(u),e.index=r++,e.action(n,u)},c.prototype.action=function(e,t){var n=this;e.time&&(l.timer[n.index]=setTimeout(function(){layer.close(n.index)},1e3*e.time));var a=function(){var t=this.getAttribute("type");0==t?(e.no&&e.no(),layer.close(n.index)):e.yes?e.yes(n.index):layer.close(n.index)};if(e.btn)for(var s=t[i]("layui-m-layerbtn")[0].children,r=s.length,o=0;odiv{line-height:22px;padding-top:7px;margin-bottom:20px;font-size:14px}.layui-m-layerbtn{display:box;display:-moz-box;display:-webkit-box;width:100%;height:50px;line-height:50px;font-size:0;border-top:1px solid #D0D0D0;background-color:#F2F2F2}.layui-m-layerbtn span{display:block;-moz-box-flex:1;box-flex:1;-webkit-box-flex:1;font-size:14px;cursor:pointer}.layui-m-layerbtn span[yes]{color:#40AFFE}.layui-m-layerbtn span[no]{border-right:1px solid #D0D0D0;border-radius:0 0 0 5px}.layui-m-layerbtn span:active{background-color:#F6F6F6}.layui-m-layerend{position:absolute;right:7px;top:10px;width:30px;height:30px;border:0;font-weight:400;background:0 0;cursor:pointer;-webkit-appearance:none;font-size:30px}.layui-m-layerend::after,.layui-m-layerend::before{position:absolute;left:5px;top:15px;content:'';width:18px;height:1px;background-color:#999;transform:rotate(45deg);-webkit-transform:rotate(45deg);border-radius:3px}.layui-m-layerend::after{transform:rotate(-45deg);-webkit-transform:rotate(-45deg)}body .layui-m-layer .layui-m-layer-footer{position:fixed;width:95%;max-width:100%;margin:0 auto;left:0;right:0;bottom:10px;background:0 0}.layui-m-layer-footer .layui-m-layercont{padding:20px;border-radius:5px 5px 0 0;background-color:rgba(255,255,255,.8)}.layui-m-layer-footer .layui-m-layerbtn{display:block;height:auto;background:0 0;border-top:none}.layui-m-layer-footer .layui-m-layerbtn span{background-color:rgba(255,255,255,.8)}.layui-m-layer-footer .layui-m-layerbtn span[no]{color:#FD482C;border-top:1px solid #c2c2c2;border-radius:0 0 5px 5px}.layui-m-layer-footer .layui-m-layerbtn span[yes]{margin-top:10px;border-radius:5px}body .layui-m-layer .layui-m-layer-msg{width:auto;max-width:90%;margin:0 auto;bottom:-150px;background-color:rgba(0,0,0,.7);color:#fff}.layui-m-layer-msg .layui-m-layercont{padding:10px 20px} ================================================ FILE: dis-seckill-gateway/src/main/resources/static/layer/skin/default/layer.css ================================================ .layui-layer-imgbar,.layui-layer-imgtit a,.layui-layer-tab .layui-layer-title span,.layui-layer-title{text-overflow:ellipsis;white-space:nowrap}*html{background-image:url(about:blank);background-attachment:fixed}html #layuicss-skinlayercss{display:none;position:absolute;width:1989px}.layui-layer,.layui-layer-shade{position:fixed;_position:absolute;pointer-events:auto}.layui-layer-shade{top:0;left:0;width:100%;height:100%;_height:expression(document.body.offsetHeight+"px")}.layui-layer{-webkit-overflow-scrolling:touch;top:150px;left:0;margin:0;padding:0;background-color:#fff;-webkit-background-clip:content;box-shadow:1px 1px 50px rgba(0,0,0,.3)}.layui-layer-close{position:absolute}.layui-layer-content{position:relative}.layui-layer-border{border:1px solid #B2B2B2;border:1px solid rgba(0,0,0,.1);box-shadow:1px 1px 5px rgba(0,0,0,.2)}.layui-layer-load{background:url(loading-1.gif) center center no-repeat #eee}.layui-layer-ico{background:url(icon.png) no-repeat}.layui-layer-btn a,.layui-layer-dialog .layui-layer-ico,.layui-layer-setwin a{display:inline-block;*display:inline;*zoom:1;vertical-align:top}.layui-layer-move{display:none;position:fixed;*position:absolute;left:0;top:0;width:100%;height:100%;cursor:move;opacity:0;filter:alpha(opacity=0);background-color:#fff;z-index:2147483647}.layui-layer-resize{position:absolute;width:15px;height:15px;right:0;bottom:0;cursor:se-resize}.layui-layer{border-radius:2px;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.3s;animation-duration:.3s}@-webkit-keyframes layer-bounceIn{0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes layer-bounceIn{0%{opacity:0;-webkit-transform:scale(.5);-ms-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}.layer-anim{-webkit-animation-name:layer-bounceIn;animation-name:layer-bounceIn}@-webkit-keyframes layer-zoomInDown{0%{opacity:0;-webkit-transform:scale(.1) translateY(-2000px);transform:scale(.1) translateY(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateY(60px);transform:scale(.475) translateY(60px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}@keyframes layer-zoomInDown{0%{opacity:0;-webkit-transform:scale(.1) translateY(-2000px);-ms-transform:scale(.1) translateY(-2000px);transform:scale(.1) translateY(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateY(60px);-ms-transform:scale(.475) translateY(60px);transform:scale(.475) translateY(60px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}.layer-anim-01{-webkit-animation-name:layer-zoomInDown;animation-name:layer-zoomInDown}@-webkit-keyframes layer-fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes layer-fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);-ms-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);-ms-transform:translateY(0);transform:translateY(0)}}.layer-anim-02{-webkit-animation-name:layer-fadeInUpBig;animation-name:layer-fadeInUpBig}@-webkit-keyframes layer-zoomInLeft{0%{opacity:0;-webkit-transform:scale(.1) translateX(-2000px);transform:scale(.1) translateX(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateX(48px);transform:scale(.475) translateX(48px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}@keyframes layer-zoomInLeft{0%{opacity:0;-webkit-transform:scale(.1) translateX(-2000px);-ms-transform:scale(.1) translateX(-2000px);transform:scale(.1) translateX(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateX(48px);-ms-transform:scale(.475) translateX(48px);transform:scale(.475) translateX(48px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}.layer-anim-03{-webkit-animation-name:layer-zoomInLeft;animation-name:layer-zoomInLeft}@-webkit-keyframes layer-rollIn{0%{opacity:0;-webkit-transform:translateX(-100%) rotate(-120deg);transform:translateX(-100%) rotate(-120deg)}100%{opacity:1;-webkit-transform:translateX(0) rotate(0);transform:translateX(0) rotate(0)}}@keyframes layer-rollIn{0%{opacity:0;-webkit-transform:translateX(-100%) rotate(-120deg);-ms-transform:translateX(-100%) rotate(-120deg);transform:translateX(-100%) rotate(-120deg)}100%{opacity:1;-webkit-transform:translateX(0) rotate(0);-ms-transform:translateX(0) rotate(0);transform:translateX(0) rotate(0)}}.layer-anim-04{-webkit-animation-name:layer-rollIn;animation-name:layer-rollIn}@keyframes layer-fadeIn{0%{opacity:0}100%{opacity:1}}.layer-anim-05{-webkit-animation-name:layer-fadeIn;animation-name:layer-fadeIn}@-webkit-keyframes layer-shake{0%,100%{-webkit-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);transform:translateX(10px)}}@keyframes layer-shake{0%,100%{-webkit-transform:translateX(0);-ms-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);-ms-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);-ms-transform:translateX(10px);transform:translateX(10px)}}.layer-anim-06{-webkit-animation-name:layer-shake;animation-name:layer-shake}@-webkit-keyframes fadeIn{0%{opacity:0}100%{opacity:1}}.layui-layer-title{padding:0 80px 0 20px;height:42px;line-height:42px;border-bottom:1px solid #eee;font-size:14px;color:#333;overflow:hidden;background-color:#F8F8F8;border-radius:2px 2px 0 0}.layui-layer-setwin{position:absolute;right:15px;*right:0;top:15px;font-size:0;line-height:initial}.layui-layer-setwin a{position:relative;width:16px;height:16px;margin-left:10px;font-size:12px;_overflow:hidden}.layui-layer-setwin .layui-layer-min cite{position:absolute;width:14px;height:2px;left:0;top:50%;margin-top:-1px;background-color:#2E2D3C;cursor:pointer;_overflow:hidden}.layui-layer-setwin .layui-layer-min:hover cite{background-color:#2D93CA}.layui-layer-setwin .layui-layer-max{background-position:-32px -40px}.layui-layer-setwin .layui-layer-max:hover{background-position:-16px -40px}.layui-layer-setwin .layui-layer-maxmin{background-position:-65px -40px}.layui-layer-setwin .layui-layer-maxmin:hover{background-position:-49px -40px}.layui-layer-setwin .layui-layer-close1{background-position:1px -40px;cursor:pointer}.layui-layer-setwin .layui-layer-close1:hover{opacity:.7}.layui-layer-setwin .layui-layer-close2{position:absolute;right:-28px;top:-28px;width:30px;height:30px;margin-left:0;background-position:-149px -31px;*right:-18px;_display:none}.layui-layer-setwin .layui-layer-close2:hover{background-position:-180px -31px}.layui-layer-btn{text-align:right;padding:0 10px 12px;pointer-events:auto;user-select:none;-webkit-user-select:none}.layui-layer-btn a{height:28px;line-height:28px;margin:6px 6px 0;padding:0 15px;border:1px solid #dedede;background-color:#f1f1f1;color:#333;border-radius:2px;font-weight:400;cursor:pointer;text-decoration:none}.layui-layer-btn a:hover{opacity:.9;text-decoration:none}.layui-layer-btn a:active{opacity:.8}.layui-layer-btn .layui-layer-btn0{border-color:#4898d5;background-color:#2e8ded;color:#fff}.layui-layer-btn-l{text-align:left}.layui-layer-btn-c{text-align:center}.layui-layer-dialog{min-width:260px}.layui-layer-dialog .layui-layer-content{position:relative;padding:20px;line-height:24px;word-break:break-all;overflow:hidden;font-size:14px;overflow-x:hidden;overflow-y:auto}.layui-layer-dialog .layui-layer-content .layui-layer-ico{position:absolute;top:16px;left:15px;_left:-40px;width:30px;height:30px}.layui-layer-ico1{background-position:-30px 0}.layui-layer-ico2{background-position:-60px 0}.layui-layer-ico3{background-position:-90px 0}.layui-layer-ico4{background-position:-120px 0}.layui-layer-ico5{background-position:-150px 0}.layui-layer-ico6{background-position:-180px 0}.layui-layer-rim{border:6px solid #8D8D8D;border:6px solid rgba(0,0,0,.3);border-radius:5px;box-shadow:none}.layui-layer-msg{min-width:180px;border:1px solid #D3D4D3;box-shadow:none}.layui-layer-hui{min-width:100px;background-color:#000;filter:alpha(opacity=60);background-color:rgba(0,0,0,.6);color:#fff;border:none}.layui-layer-hui .layui-layer-content{padding:12px 25px;text-align:center}.layui-layer-dialog .layui-layer-padding{padding:20px 20px 20px 55px;text-align:left}.layui-layer-page .layui-layer-content{position:relative;overflow:auto}.layui-layer-iframe .layui-layer-btn,.layui-layer-page .layui-layer-btn{padding-top:10px}.layui-layer-nobg{background:0 0}.layui-layer-iframe iframe{display:block;width:100%}.layui-layer-loading{border-radius:100%;background:0 0;box-shadow:none;border:none}.layui-layer-loading .layui-layer-content{width:60px;height:24px;background:url(loading-0.gif) no-repeat}.layui-layer-loading .layui-layer-loading1{width:37px;height:37px;background:url(loading-1.gif) no-repeat}.layui-layer-ico16,.layui-layer-loading .layui-layer-loading2{width:32px;height:32px;background:url(loading-2.gif) no-repeat}.layui-layer-tips{background:0 0;box-shadow:none;border:none}.layui-layer-tips .layui-layer-content{position:relative;line-height:22px;min-width:12px;padding:5px 10px;font-size:12px;_float:left;border-radius:2px;box-shadow:1px 1px 3px rgba(0,0,0,.2);background-color:#000;color:#fff}.layui-layer-tips .layui-layer-close{right:-2px;top:-1px}.layui-layer-tips i.layui-layer-TipsG{position:absolute;width:0;height:0;border-width:8px;border-color:transparent;border-style:dashed;*overflow:hidden}.layui-layer-tips i.layui-layer-TipsB,.layui-layer-tips i.layui-layer-TipsT{left:5px;border-right-style:solid;border-right-color:#000}.layui-layer-tips i.layui-layer-TipsT{bottom:-8px}.layui-layer-tips i.layui-layer-TipsB{top:-8px}.layui-layer-tips i.layui-layer-TipsL,.layui-layer-tips i.layui-layer-TipsR{top:1px;border-bottom-style:solid;border-bottom-color:#000}.layui-layer-tips i.layui-layer-TipsR{left:-8px}.layui-layer-tips i.layui-layer-TipsL{right:-8px}.layui-layer-lan[type=dialog]{min-width:280px}.layui-layer-lan .layui-layer-title{background:#4476A7;color:#fff;border:none}.layui-layer-lan .layui-layer-btn{padding:5px 10px 10px;text-align:right;border-top:1px solid #E9E7E7}.layui-layer-lan .layui-layer-btn a{background:#BBB5B5;border:none}.layui-layer-lan .layui-layer-btn .layui-layer-btn1{background:#C9C5C5}.layui-layer-molv .layui-layer-title{background:#009f95;color:#fff;border:none}.layui-layer-molv .layui-layer-btn a{background:#009f95}.layui-layer-molv .layui-layer-btn .layui-layer-btn1{background:#92B8B1}.layui-layer-iconext{background:url(icon-ext.png) no-repeat}.layui-layer-prompt .layui-layer-input{display:block;width:220px;height:30px;margin:0 auto;line-height:30px;padding:0 5px;border:1px solid #ccc;box-shadow:1px 1px 5px rgba(0,0,0,.1) inset;color:#333}.layui-layer-prompt textarea.layui-layer-input{width:300px;height:100px;line-height:20px}.layui-layer-prompt .layui-layer-content{padding:20px}.layui-layer-prompt .layui-layer-btn{padding-top:0}.layui-layer-tab{box-shadow:1px 1px 50px rgba(0,0,0,.4)}.layui-layer-tab .layui-layer-title{padding-left:0;border-bottom:1px solid #ccc;background-color:#eee;overflow:visible}.layui-layer-tab .layui-layer-title span{position:relative;float:left;min-width:80px;max-width:260px;padding:0 20px;text-align:center;cursor:default;overflow:hidden}.layui-layer-tab .layui-layer-title span.layui-layer-tabnow{height:43px;border-left:1px solid #ccc;border-right:1px solid #ccc;background-color:#fff;z-index:10}.layui-layer-tab .layui-layer-title span:first-child{border-left:none}.layui-layer-tabmain{line-height:24px;clear:both}.layui-layer-tabmain .layui-layer-tabli{display:none}.layui-layer-tabmain .layui-layer-tabli.xubox_tab_layer{display:block}.xubox_tabclose{position:absolute;right:10px;top:5px;cursor:pointer}.layui-layer-photos{-webkit-animation-duration:.8s;animation-duration:.8s}.layui-layer-photos .layui-layer-content{overflow:hidden;text-align:center}.layui-layer-photos .layui-layer-phimg img{position:relative;width:100%;display:inline-block;*display:inline;*zoom:1;vertical-align:top}.layui-layer-imgbar,.layui-layer-imguide{display:none}.layui-layer-imgnext,.layui-layer-imgprev{position:absolute;top:50%;width:27px;_width:44px;height:44px;margin-top:-22px;outline:0;blr:expression(this.onFocus=this.blur())}.layui-layer-imgprev{left:10px;background-position:-5px -5px;_background-position:-70px -5px}.layui-layer-imgprev:hover{background-position:-33px -5px;_background-position:-120px -5px}.layui-layer-imgnext{right:10px;_right:8px;background-position:-5px -50px;_background-position:-70px -50px}.layui-layer-imgnext:hover{background-position:-33px -50px;_background-position:-120px -50px}.layui-layer-imgbar{position:absolute;left:0;bottom:0;width:100%;height:32px;line-height:32px;background-color:rgba(0,0,0,.8);background-color:#000\9;filter:Alpha(opacity=80);color:#fff;overflow:hidden;font-size:0}.layui-layer-imgtit *{display:inline-block;*display:inline;*zoom:1;vertical-align:top;font-size:12px}.layui-layer-imgtit a{max-width:65%;overflow:hidden;color:#fff}.layui-layer-imgtit a:hover{color:#fff;text-decoration:underline}.layui-layer-imgtit em{padding-left:10px;font-style:normal}@-webkit-keyframes layer-bounceOut{100%{opacity:0;-webkit-transform:scale(.7);transform:scale(.7)}30%{-webkit-transform:scale(1.05);transform:scale(1.05)}0%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes layer-bounceOut{100%{opacity:0;-webkit-transform:scale(.7);-ms-transform:scale(.7);transform:scale(.7)}30%{-webkit-transform:scale(1.05);-ms-transform:scale(1.05);transform:scale(1.05)}0%{-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}.layer-anim-close{-webkit-animation-name:layer-bounceOut;animation-name:layer-bounceOut;-webkit-animation-duration:.2s;animation-duration:.2s}@media screen and (max-width:1100px){.layui-layer-iframe{overflow-y:auto;-webkit-overflow-scrolling:touch}} ================================================ FILE: dis-seckill-gateway/src/main/resources/static/order_detail.htm ================================================ 订单详情

秒杀订单详情

商品名称
商品图片
订单价格
下单时间
订单状态
收货人
联系方式
收货地址 北京市昌平区回龙观龙博一区
================================================ FILE: dis-seckill-gateway/src/main/resources/templates/goods_detail.html ================================================ 商品详情
秒杀商品详情
您还没有登录,请登陆后再操作
没有收货地址的提示……
商品名称
商品图片
秒杀开始时间 秒杀倒计时: 秒杀进行中 秒杀已结束
商品原价
秒杀价
库存数量
================================================ FILE: dis-seckill-gateway/src/main/resources/templates/goods_list.html ================================================ 商品列表

秒杀商品列表

商品名称 商品图片 商品原价 秒杀价 库存数量 详情
================================================ FILE: dis-seckill-gateway/src/main/resources/templates/login.html ================================================ 登录

用户登录

================================================ FILE: dis-seckill-gateway/src/main/resources/templates/order_detail.html ================================================ 订单详情
秒杀订单详情
商品名称
商品图片
订单价格
下单时间
订单状态 未支付 待发货 已发货 已收货 已退款 已完成
收货人
收货地址 北京市昌平区回龙观龙博一区
================================================ FILE: dis-seckill-gateway/src/main/resources/templates/register.html ================================================ 注册

用户注册

================================================ FILE: dis-seckill-goods/pom.xml ================================================ dis-seckill com.seckill.dis 0.0.1-SNAPSHOT 4.0.0 dis-seckill-goods dis-seckill-goods 商品模块 org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import org.apache.dubbo dubbo-dependencies-bom ${dubbo.version} pom import org.apache.dubbo dubbo ${dubbo.version} org.springframework spring javax.servlet servlet-api log4j log4j com.seckill.dis dis-seckill-common org.mybatis.spring.boot mybatis-spring-boot-starter org.springframework.boot spring-boot-configuration-processor true mysql mysql-connector-java com.alibaba druid org.apache.dubbo dubbo-spring-boot-starter ${dubbo.version} org.apache.dubbo dubbo org.apache.dubbo dubbo-dependencies-zookeeper ${dubbo.version} org.slf4j slf4j-log4j12 pom redis.clients jedis org.springframework.boot spring-boot-maven-plugin ================================================ FILE: dis-seckill-goods/src/main/java/com/seckill/dis/goods/GoodsApplication.java ================================================ package com.seckill.dis.goods; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 商品模块 * * @author noodle */ @SpringBootApplication public class GoodsApplication { public static void main(String[] args) { SpringApplication.run(GoodsApplication.class, args); } } ================================================ FILE: dis-seckill-goods/src/main/java/com/seckill/dis/goods/persistence/GoodsMapper.java ================================================ package com.seckill.dis.goods.persistence; import com.seckill.dis.common.api.goods.vo.GoodsVo; import com.seckill.dis.common.domain.SeckillGoods; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; import java.util.List; /** * goods 表的数据库访问层 */ @Mapper public interface GoodsMapper { /** * 查出商品信息(包含该商品的秒杀信息) * 利用左外连接(LEFT JOIN...ON...)的方式查 * * @return */ @Select("SELECT g.*, mg.stock_count, mg.start_date, mg.end_date, mg.seckill_price FROM seckill_goods mg LEFT JOIN goods g ON mg.goods_id=g.id") List listGoodsVo(); /** * 通过商品的id查出商品的所有信息(包含该商品的秒杀信息) * * @param goodsId * @return */ @Select("SELECT g.*, mg.stock_count, mg.start_date, mg.end_date, mg.seckill_price FROM seckill_goods mg LEFT JOIN goods g ON mg.goods_id=g.id where g.id = #{goodsId}") GoodsVo getGoodsVoByGoodsId(@Param("goodsId") Long goodsId); /** * 减少 seckill_order 中的库存 *

* 增加库存判断 stock_count>0, 一次使得数据库不存在卖超问题 * * @param seckillGoods */ @Update("UPDATE seckill_goods SET stock_count = stock_count-1 WHERE goods_id=#{goodsId} AND stock_count > 0") int reduceStack(SeckillGoods seckillGoods); } ================================================ FILE: dis-seckill-goods/src/main/java/com/seckill/dis/goods/service/GoodsServiceImpl.java ================================================ package com.seckill.dis.goods.service; import com.seckill.dis.common.api.goods.GoodsServiceApi; import com.seckill.dis.common.api.goods.vo.GoodsVo; import com.seckill.dis.common.domain.SeckillGoods; import com.seckill.dis.goods.persistence.GoodsMapper; import org.apache.dubbo.config.annotation.Service; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; /** * 商品服务接口实现 * * @author noodle */ @Service(interfaceClass = GoodsServiceApi.class) public class GoodsServiceImpl implements GoodsServiceApi { @Autowired GoodsMapper goodsMapper; @Override public List listGoodsVo() { return goodsMapper.listGoodsVo(); } @Override public GoodsVo getGoodsVoByGoodsId(long goodsId) { return goodsMapper.getGoodsVoByGoodsId(goodsId); } /** * 通过商品的id查出商品的所有信息(包含该商品的秒杀信息) * * @param goodsId * @return */ @Override public GoodsVo getGoodsVoByGoodsId(Long goodsId) { return goodsMapper.getGoodsVoByGoodsId(goodsId); } /** * 减库存 * * @param goods * @return */ @Override public boolean reduceStock(GoodsVo goods) { SeckillGoods seckillGoods = new SeckillGoods(); // 秒杀商品的id和商品的id是一样的 seckillGoods.setGoodsId(goods.getId()); int ret = goodsMapper.reduceStack(seckillGoods); return ret > 0; } } ================================================ FILE: dis-seckill-goods/src/main/java/com/seckill/dis/goods/service/SeckillServiceImpl.java ================================================ package com.seckill.dis.goods.service; import com.alibaba.fastjson.JSONObject; import com.seckill.dis.common.api.cache.RedisServiceApi; import com.seckill.dis.common.api.cache.vo.GoodsKeyPrefix; import com.seckill.dis.common.api.cache.vo.SkKeyPrefix; import com.seckill.dis.common.api.goods.GoodsServiceApi; import com.seckill.dis.common.api.goods.vo.GoodsVo; import com.seckill.dis.common.api.order.OrderServiceApi; import com.seckill.dis.common.api.seckill.SeckillServiceApi; import com.seckill.dis.common.api.seckill.vo.VerifyCodeVo; import com.seckill.dis.common.api.user.vo.UserVo; import com.seckill.dis.common.domain.OrderInfo; import com.seckill.dis.common.domain.SeckillOrder; import org.apache.dubbo.config.annotation.Reference; import org.apache.dubbo.config.annotation.Service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import java.awt.*; import java.awt.image.BufferedImage; import java.util.Random; /** * 秒杀服务接口实现 * * @author noodle */ @Service(interfaceClass = SeckillServiceApi.class) public class SeckillServiceImpl implements SeckillServiceApi { @Autowired GoodsServiceApi goodsService; @Reference(interfaceClass = OrderServiceApi.class) OrderServiceApi orderService; @Reference(interfaceClass = RedisServiceApi.class) RedisServiceApi redisService; /** * 用于生成验证码中的运算符 */ private char[] ops = new char[]{'+', '-', '*'}; /** * 减库存,生成订单,实现秒杀操作核心业务 * 秒杀操作由两步构成,不可分割,为一个事务 * * @param user 秒杀商品的用户 * @param goods 所秒杀的商品 * @return */ @Transactional @Override public OrderInfo seckill(UserVo user, GoodsVo goods) { // 1. 减库存 boolean success = goodsService.reduceStock(goods); if (!success) { setGoodsOver(goods.getId()); return null; } // 2. 生成订单;向 order_info 表和 seckill_order 表中写入订单信息 OrderInfo order = orderService.createOrder(user, goods); // 3. 更新缓存中的库存信息 GoodsVo good = goodsService.getGoodsVoByGoodsId(goods.getId()); redisService.set(GoodsKeyPrefix.GOODS_STOCK, "" + good.getId(), good.getStockCount()); return order; } /** * 设置秒杀商品数量为0 * * @param goodsId */ private void setGoodsOver(long goodsId) { redisService.set(SkKeyPrefix.GOODS_SK_OVER, "" + goodsId, true); } private boolean getGoodsOver(long goodsId) { return redisService.exists(SkKeyPrefix.GOODS_SK_OVER, "" + goodsId); } /** * 获取秒杀结果 * * @param userId * @param goodsId * @return */ public long getSeckillResult(Long userId, long goodsId) { SeckillOrder order = orderService.getSeckillOrderByUserIdAndGoodsId(userId, goodsId); if (order != null) {//秒杀成功 return order.getOrderId(); } else { boolean isOver = getGoodsOver(goodsId); if (isOver) { return -1; } else { return 0; } } } /** * 创建验证码 * * @param user * @param goodsId * @return */ @Override public String createVerifyCode(UserVo user, long goodsId) { if (user == null || goodsId <= 0) { return null; } // 验证码的宽高 int width = 80; int height = 32; //create the image BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics g = image.getGraphics(); // set the background color g.setColor(new Color(0xDCDCDC)); g.fillRect(0, 0, width, height); // draw the border g.setColor(Color.black); g.drawRect(0, 0, width - 1, height - 1); // create a random instance to generate the codes Random rdm = new Random(); // make some confusion for (int i = 0; i < 50; i++) { int x = rdm.nextInt(width); int y = rdm.nextInt(height); g.drawOval(x, y, 0, 0); } // generate a random code String verifyCode = generateVerifyCode(rdm); g.setColor(new Color(0, 100, 0)); g.setFont(new Font("Candara", Font.BOLD, 24)); g.drawString(verifyCode, 8, 24); g.dispose(); // 计算表达式值,并把把验证码值存到redis中 int expResult = calc(verifyCode); //输出图片和结果 VerifyCodeVo verifyCodeVo = new VerifyCodeVo(image, expResult); String verifyCodeVoJson = JSONObject.toJSONString(verifyCodeVo); return verifyCodeVoJson; } /** * 使用ScriptEngine计算验证码中的数学表达式的值 * * @param exp * @return */ private int calc(String exp) { try { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("JavaScript"); return (Integer) engine.eval(exp);// 表达式计算 } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 生成验证码,只含有+/-/* *

* 随机生成三个数字,然后生成表达式 * * @param rdm * @return 验证码中的数学表达式 */ private String generateVerifyCode(Random rdm) { int num1 = rdm.nextInt(10); int num2 = rdm.nextInt(10); int num3 = rdm.nextInt(10); char op1 = ops[rdm.nextInt(3)]; char op2 = ops[rdm.nextInt(3)]; String exp = "" + num1 + op1 + num2 + op2 + num3; return exp; } } ================================================ FILE: dis-seckill-goods/src/main/resources/application.properties ================================================ # -------------------------------- # spring #--------------------------------- spring.application.name=dis-seckill-goods # -------------------------------- # web #--------------------------------- server.port=8083 # -------------------------------- # druid Դ #--------------------------------- # linuxϵmysql #spring.datasource.url=jdbc:mysql://192.168.216.128:3306/seckill?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8 #spring.datasource.username=linuxmysql #spring.datasource.password=000 # windowsϵmysql spring.datasource.url=jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8 spring.datasource.username=root spring.datasource.password=000 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.filters=stat spring.datasource.maxActive=1000 spring.datasource.initialSize=100 spring.datasource.maxWait=60000 spring.datasource.minIdle=500 spring.datasource.timeBetweenEvictionRunsMillis=60000 spring.datasource.minEvictableIdleTimeMillis=300000 spring.datasource.validationQuery=select 'x' spring.datasource.testWhileIdle=true spring.datasource.testOnBorrow=false spring.datasource.testOnReturn=false spring.datasource.poolPreparedStatements=true spring.datasource.maxOpenPreparedStatements=20 #--------------------------------- # mybatis #--------------------------------- mybatis.type-aliases-package=com.seckill.dis.common.domain #mybatis.type-handlers-package=com.example.typehandler mybatis.configuration.map-underscore-to-camel-case=true mybatis.configuration.default-fetch-size=100 mybatis.configuration.default-statement-timeout=3000 mybatis.mapper-locations=classpath*:com.seckill.dis.goods.persistence/*.xml #--------------------------------- # Dubbo Application #--------------------------------- # Base packages to scan Dubbo Component: @org.apache.dubbo.config.annotation.Service dubbo.scan.base-packages=com.seckill.dis.goods.service # The default value of dubbo.application.name is ${spring.application.name} dubbo.application.name=${spring.application.name} # Dubbo Protocol dubbo.protocol.name=dubbo dubbo.protocol.port=12346 dubbo.registry.check=true ## Dubbo Registry embedded.zookeeper.port=2181 dubbo.registry.address=zookeeper://127.0.0.1:${embedded.zookeeper.port} #--------------------------------- # redis #--------------------------------- ##redis.host=192.168.216.128 #redis.host=127.0.0.1 #redis.port=6379 #redis.timeout=100 ## redis.password=123456 #redis.poolMaxTotal=1000 #redis.poolMaxIdle=500 #redis.poolMaxWait=500 #--------------------------------- # ־ #--------------------------------- logging.level.root=info logging.level.com.seckill.dis=debug logging.path=logs/ logging.file=dis-seckill.log ================================================ FILE: dis-seckill-mq/pom.xml ================================================ dis-seckill com.seckill.dis 0.0.1-SNAPSHOT 4.0.0 dis-seckill-mq dis-seckill-mq 消息队列模块 org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import org.apache.dubbo dubbo-dependencies-bom ${dubbo.version} pom import org.apache.dubbo dubbo ${dubbo.version} org.springframework spring javax.servlet servlet-api log4j log4j com.seckill.dis dis-seckill-common org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-configuration-processor com.alibaba fastjson org.apache.dubbo dubbo-spring-boot-starter ${dubbo.version} org.apache.dubbo dubbo org.apache.dubbo dubbo-dependencies-zookeeper ${dubbo.version} org.slf4j slf4j-log4j12 pom org.springframework.boot spring-boot-starter-amqp org.springframework.boot spring-boot-maven-plugin ================================================ FILE: dis-seckill-mq/src/main/java/com/seckill/dis/mq/MqApplication.java ================================================ package com.seckill.dis.mq; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 消息队列服务 * * @author noodle */ @SpringBootApplication public class MqApplication { public static void main(String[] args) { SpringApplication.run(MqApplication.class, args); } } ================================================ FILE: dis-seckill-mq/src/main/java/com/seckill/dis/mq/config/MQConfig.java ================================================ package com.seckill.dis.mq.config; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; /** * 通过配置文件获取消息队列 * * @author noodle */ @Configuration public class MQConfig { /** * 消息队列名 */ public static final String SECKILL_QUEUE = "seckill.queue"; /** * 秒杀 routing key, 生产者沿着 routingKey 将消息投递到 exchange 中 */ public static final String SK_ROUTING_KEY = "routing.sk"; /** * Direct模式 交换机exchange * 生成用于秒杀的queue * * @return */ @Bean public Queue seckillQueue() { return new Queue(SECKILL_QUEUE, true); } /** * 实例化 RabbitTemplate * * @param connectionFactory * @return */ @Bean @Scope("prototype") public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { RabbitTemplate template = new RabbitTemplate(connectionFactory); return template; } } ================================================ FILE: dis-seckill-mq/src/main/java/com/seckill/dis/mq/receiver/MqConsumer.java ================================================ package com.seckill.dis.mq.receiver; import com.seckill.dis.common.api.cache.RedisServiceApi; import com.seckill.dis.common.api.cache.vo.GoodsKeyPrefix; import com.seckill.dis.common.api.cache.vo.OrderKeyPrefix; import com.seckill.dis.common.api.goods.GoodsServiceApi; import com.seckill.dis.common.api.goods.vo.GoodsVo; import com.seckill.dis.common.api.mq.vo.SkMessage; import com.seckill.dis.common.api.order.OrderServiceApi; import com.seckill.dis.common.api.seckill.SeckillServiceApi; import com.seckill.dis.common.api.user.vo.UserVo; import com.seckill.dis.common.domain.SeckillOrder; import com.seckill.dis.mq.config.MQConfig; import org.apache.dubbo.config.annotation.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Service; /** * MQ消息接收者, 消费者 * 消费者绑定在队列监听,既可以接收到队列中的消息 * * @author noodle */ @Service public class MqConsumer { private static Logger logger = LoggerFactory.getLogger(MqConsumer.class); @Reference(interfaceClass = GoodsServiceApi.class) GoodsServiceApi goodsService; @Reference(interfaceClass = OrderServiceApi.class) OrderServiceApi orderService; @Reference(interfaceClass = SeckillServiceApi.class) SeckillServiceApi seckillService; @Reference(interfaceClass = RedisServiceApi.class) RedisServiceApi redisService; /** * 处理收到的秒杀成功信息(核心业务实现) * * @param message */ @RabbitListener(queues = MQConfig.SECKILL_QUEUE) public void receiveSkInfo(SkMessage message) { logger.info("MQ receive a message: " + message); // 获取秒杀用户信息与商品id UserVo user = message.getUser(); long goodsId = message.getGoodsId(); // 获取商品的库存 GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId); Integer stockCount = goods.getStockCount(); if (stockCount <= 0) { return; } // 判断是否已经秒杀到了(保证秒杀接口幂等性) SeckillOrder order = this.getSkOrderByUserIdAndGoodsId(user.getUuid(), goodsId); if (order != null) { return; } // 1.减库存 2.写入订单 3.写入秒杀订单 seckillService.seckill(user, goods); } /** * 通过用户id与商品id从订单列表中获取订单信息,这个地方用了唯一索引(unique index!!!!!) *

* 优化,不同每次都去数据库中读取秒杀订单信息,而是在第一次生成秒杀订单成功后, * 将订单存储在redis中,再次读取订单信息的时候就直接从redis中读取 * * @param userId * @param goodsId * @return 秒杀订单信息 */ private SeckillOrder getSkOrderByUserIdAndGoodsId(Long userId, long goodsId) { // 从redis中取缓存,减少数据库的访问 SeckillOrder seckillOrder = redisService.get(OrderKeyPrefix.SK_ORDER, ":" + userId + "_" + goodsId, SeckillOrder.class); if (seckillOrder != null) { return seckillOrder; } return orderService.getSeckillOrderByUserIdAndGoodsId(userId, goodsId); } } ================================================ FILE: dis-seckill-mq/src/main/java/com/seckill/dis/mq/service/MqProviderImpl.java ================================================ package com.seckill.dis.mq.service; import com.seckill.dis.common.api.mq.MqProviderApi; import com.seckill.dis.common.api.mq.vo.SkMessage; import com.seckill.dis.mq.config.MQConfig; import org.apache.dubbo.config.annotation.Service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import java.util.UUID; /** * 消息队列服务化(消息生产者) * * @author noodle */ @Service(interfaceClass = MqProviderApi.class) public class MqProviderImpl implements MqProviderApi, RabbitTemplate.ConfirmCallback { private static Logger logger = LoggerFactory.getLogger(MqProviderImpl.class); private RabbitTemplate rabbitTemplate; @Autowired public MqProviderImpl(RabbitTemplate rabbitTemplate) { this.rabbitTemplate = rabbitTemplate; // 设置 ack 回调 rabbitTemplate.setConfirmCallback(this); } @Override public void sendSkMessage(SkMessage message) { logger.info("MQ send message: " + message); // 秒杀消息关联的数据 CorrelationData skCorrData = new CorrelationData(UUID.randomUUID().toString()); // 第一个参数为消息队列名(此处也为routingKey),第二个参数为发送的消息 rabbitTemplate.convertAndSend(MQConfig.SECKILL_QUEUE, message, skCorrData); } /** * MQ ack 机制 * TODO 完善验证机制,确保消息能够被消费,且不影响消息吞吐量 */ @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { logger.info("SkMessage UUID: " + correlationData.getId()); if (ack) { logger.info("SkMessage 消息消费成功!"); } else { System.out.println("SkMessage 消息消费失败!"); } if (cause != null) { logger.info("CallBackConfirm Cause: " + cause); } } } ================================================ FILE: dis-seckill-mq/src/main/resources/application.properties ================================================ # -------------------------------- # spring #--------------------------------- spring.application.name=dis-seckill-mq # -------------------------------- # web #--------------------------------- server.port=8086 #--------------------------------- #--------------------------------- # rabbitmq #--------------------------------- #spring.rabbitmq.host=192.168.216.128 spring.rabbitmq.host=127.0.0.1 spring.rabbitmq.port=5672 #spring.rabbitmq.username=anon #spring.rabbitmq.password=000 spring.rabbitmq.username=guest spring.rabbitmq.password=guest spring.rabbitmq.virtual-host=/ # spring.rabbitmq.listener.simple.concurrency=10 spring.rabbitmq.listener.simple.max-concurrency=10 # ÿδӶȡϢprefetchQueueÿη͸ÿߵϢΪ1queueÿλᷢ1ϢconsumerȵյconsumerackqueueŻϢconsumer spring.rabbitmq.listener.simple.prefetch=1 # Ĭ spring.rabbitmq.listener.simple.auto-startup=true # ʧܺ󣬽Ϣѹ spring.rabbitmq.listener.simple.default-requeue-rejected=true # ʧ spring.rabbitmq.template.retry.enabled=true spring.rabbitmq.template.retry.initial-interval=1s spring.rabbitmq.template.retry.max-attempts=3 spring.rabbitmq.template.retry.max-interval=10s spring.rabbitmq.template.retry.multiplier=1.0 #Ϣ͵ȷϻ, Ƿȷϻص #ûбϢյ߷͵Ϣ޷յȷϳɹĻصϢ spring.rabbitmq.publisher-confirms=true #--------------------------------- # Dubbo Application #--------------------------------- # Base packages to scan Dubbo Component: @org.apache.dubbo.config.annotation.Service dubbo.scan.base-packages=com.seckill.dis.mq.service # The default value of dubbo.application.name is ${spring.application.name} dubbo.application.name=${spring.application.name} # Dubbo Protocol dubbo.protocol.name=dubbo dubbo.protocol.port=12349 dubbo.registry.check=true ## Dubbo Registry embedded.zookeeper.port=2181 dubbo.registry.address=zookeeper://127.0.0.1:${embedded.zookeeper.port} #--------------------------------- # ־ #--------------------------------- logging.level.root=info logging.level.com.seckill.dis=debug logging.path=logs/ logging.file=dis-seckill.log ================================================ FILE: dis-seckill-order/pom.xml ================================================ dis-seckill com.seckill.dis 0.0.1-SNAPSHOT 4.0.0 dis-seckill-order dis-seckill-order 订单模块 org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import org.apache.dubbo dubbo-dependencies-bom ${dubbo.version} pom import org.apache.dubbo dubbo ${dubbo.version} org.springframework spring javax.servlet servlet-api log4j log4j com.seckill.dis dis-seckill-common org.mybatis.spring.boot mybatis-spring-boot-starter org.springframework.boot spring-boot-configuration-processor mysql mysql-connector-java com.alibaba druid org.apache.dubbo dubbo-spring-boot-starter ${dubbo.version} org.apache.dubbo dubbo org.apache.dubbo dubbo-dependencies-zookeeper ${dubbo.version} org.slf4j slf4j-log4j12 pom redis.clients jedis org.springframework.boot spring-boot-maven-plugin ================================================ FILE: dis-seckill-order/src/main/java/com/seckill/dis/order/OrderApplication.java ================================================ package com.seckill.dis.order; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 订单服务 * * @author noodle */ @SpringBootApplication public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } } ================================================ FILE: dis-seckill-order/src/main/java/com/seckill/dis/order/persistence/OrderMapper.java ================================================ package com.seckill.dis.order.persistence; import com.seckill.dis.common.domain.OrderInfo; import com.seckill.dis.common.domain.SeckillOrder; import org.apache.ibatis.annotations.*; /** * seckill_order 表数据访问层 * * @author noodle */ @Mapper public interface OrderMapper { /** * 通过用户id与商品id从订单列表中获取订单信息 * * @param userId 用户id * @param goodsId 商品id * @return 秒杀订单信息 */ @Select("SELECT * FROM seckill_order WHERE user_id=#{userId} AND goods_id=#{goodsId}") SeckillOrder getSeckillOrderByUserIdAndGoodsId(@Param("userId") long userId, @Param("goodsId") long goodsId); /** * 将订单信息插入 order_info 表中 * * @param orderInfo 订单信息 * @return 插入成功的订单信息id */ @Insert("INSERT INTO order_info (user_id, goods_id, goods_name, goods_count, goods_price, order_channel, status, create_date)" + "VALUES (#{userId}, #{goodsId}, #{goodsName}, #{goodsCount}, #{goodsPrice}, #{orderChannel}, #{status}, #{createDate})") // 查询出插入订单信息的表id,并返回 @SelectKey(keyColumn = "id", keyProperty = "id", resultType = long.class, before = false, statement = "SELECT last_insert_id()") long insert(OrderInfo orderInfo); /** * 将秒杀订单信息插入到 seckill_order 中 * * @param seckillOrder 秒杀订单 */ @Insert("INSERT INTO seckill_order(user_id, order_id, goods_id) VALUES (#{userId}, #{orderId}, #{goodsId})") void insertSeckillOrder(SeckillOrder seckillOrder); /** * 获取订单信息 * * @param orderId * @return */ @Select("select * from order_info where id = #{orderId}") OrderInfo getOrderById(@Param("orderId") long orderId); } ================================================ FILE: dis-seckill-order/src/main/java/com/seckill/dis/order/service/OrderServiceImpl.java ================================================ package com.seckill.dis.order.service; import com.seckill.dis.common.api.cache.RedisServiceApi; import com.seckill.dis.common.api.cache.vo.OrderKeyPrefix; import com.seckill.dis.common.api.goods.vo.GoodsVo; import com.seckill.dis.common.api.order.OrderServiceApi; import com.seckill.dis.common.api.user.vo.UserVo; import com.seckill.dis.common.domain.OrderInfo; import com.seckill.dis.common.domain.SeckillOrder; import com.seckill.dis.order.persistence.OrderMapper; import org.apache.dubbo.config.annotation.Reference; import org.apache.dubbo.config.annotation.Service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import java.util.Date; /** * 订单服务实现 * * @author noodle */ @Service(interfaceClass = OrderServiceApi.class) public class OrderServiceImpl implements OrderServiceApi { private static Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class); @Autowired OrderMapper orderMapper; @Reference(interfaceClass = RedisServiceApi.class) RedisServiceApi redisService; @Override public OrderInfo getOrderById(long orderId) { return orderMapper.getOrderById(orderId); } @Override public SeckillOrder getSeckillOrderByUserIdAndGoodsId(long userId, long goodsId) { return orderMapper.getSeckillOrderByUserIdAndGoodsId(userId, goodsId); } /** * 创建订单 *

* 首先向数据库中写入数据,然后将数据写到缓存中,这样可以保证缓存和数据库中的数据的一致 * 1. 向 order_info 中插入订单详细信息 * 2. 向 seckill_order 中插入订单概要 * 两个操作需要构成一个数据库事务 * * @param user * @param goods * @return */ @Transactional @Override public OrderInfo createOrder(UserVo user, GoodsVo goods) { OrderInfo orderInfo = new OrderInfo(); SeckillOrder seckillOrder = new SeckillOrder(); orderInfo.setCreateDate(new Date()); orderInfo.setDeliveryAddrId(0L); orderInfo.setGoodsCount(1);// 订单中商品的数量 orderInfo.setGoodsId(goods.getId()); orderInfo.setGoodsName(goods.getGoodsName()); orderInfo.setGoodsPrice(goods.getSeckillPrice());// 秒杀价格 orderInfo.setOrderChannel(1); orderInfo.setStatus(0); orderInfo.setUserId(user.getUuid()); // 将订单信息插入 order_info 表中 long orderId = orderMapper.insert(orderInfo); logger.debug("将订单信息插入 order_info 表中: 记录为" + orderId); seckillOrder.setGoodsId(goods.getId()); seckillOrder.setOrderId(orderInfo.getId()); seckillOrder.setUserId(user.getUuid()); // 将秒杀订单插入 seckill_order 表中 orderMapper.insertSeckillOrder(seckillOrder); logger.debug("将秒杀订单插入 seckill_order 表中"); // 将秒杀订单概要信息存储于redis中 redisService.set(OrderKeyPrefix.SK_ORDER, ":" + user.getUuid() + "_" + goods.getId(), seckillOrder); return orderInfo; } } ================================================ FILE: dis-seckill-order/src/main/resources/application.properties ================================================ # -------------------------------- # spring #--------------------------------- spring.application.name=dis-seckill-order # -------------------------------- # web #--------------------------------- server.port=8084 # -------------------------------- # druid Դ #--------------------------------- # linuxϵmysql #spring.datasource.url=jdbc:mysql://192.168.216.128:3306/seckill?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8 #spring.datasource.username=linuxmysql #spring.datasource.password=000 # windowsϵmysql spring.datasource.url=jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8 spring.datasource.username=root spring.datasource.password=000 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.filters=stat spring.datasource.maxActive=1000 spring.datasource.initialSize=100 spring.datasource.maxWait=60000 spring.datasource.minIdle=500 spring.datasource.timeBetweenEvictionRunsMillis=60000 spring.datasource.minEvictableIdleTimeMillis=300000 spring.datasource.validationQuery=select 'x' spring.datasource.testWhileIdle=true spring.datasource.testOnBorrow=false spring.datasource.testOnReturn=false spring.datasource.poolPreparedStatements=true spring.datasource.maxOpenPreparedStatements=20 #--------------------------------- # mybatis #--------------------------------- mybatis.type-aliases-package=com.seckill.dis.common.domain #mybatis.type-handlers-package=com.example.typehandler mybatis.configuration.map-underscore-to-camel-case=true mybatis.configuration.default-fetch-size=100 mybatis.configuration.default-statement-timeout=3000 mybatis.mapper-locations=classpath*:com.seckill.dis.order.persistence/*.xml #--------------------------------- # Dubbo Application #--------------------------------- # Base packages to scan Dubbo Component: @org.apache.dubbo.config.annotation.Service dubbo.scan.base-packages=com.seckill.dis.order.service # The default value of dubbo.application.name is ${spring.application.name} dubbo.application.name=${spring.application.name} # Dubbo Protocol dubbo.protocol.name=dubbo dubbo.protocol.port=12347 dubbo.registry.check=true ## Dubbo Registry embedded.zookeeper.port=2181 dubbo.registry.address=zookeeper://127.0.0.1:${embedded.zookeeper.port} #--------------------------------- # redis #--------------------------------- ##redis.host=192.168.216.128 #redis.host=127.0.0.1 #redis.port=6379 #redis.timeout=100 ## redis.password=123456 #redis.poolMaxTotal=1000 #redis.poolMaxIdle=500 #redis.poolMaxWait=500 #--------------------------------- # ־ #--------------------------------- logging.level.root=info logging.level.com.seckill.dis=debug logging.path=logs/ logging.file=dis-seckill.log ================================================ FILE: dis-seckill-user/pom.xml ================================================ dis-seckill com.seckill.dis 0.0.1-SNAPSHOT 4.0.0 dis-seckill-user dis-seckill-user 用户模块 org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import org.apache.dubbo dubbo-dependencies-bom ${dubbo.version} pom import org.apache.dubbo dubbo ${dubbo.version} org.springframework spring javax.servlet servlet-api log4j log4j com.seckill.dis dis-seckill-common org.mybatis.spring.boot mybatis-spring-boot-starter org.springframework.boot spring-boot-configuration-processor mysql mysql-connector-java com.alibaba druid org.apache.dubbo dubbo-spring-boot-starter ${dubbo.version} org.apache.dubbo dubbo org.apache.dubbo dubbo-dependencies-zookeeper ${dubbo.version} org.slf4j slf4j-log4j12 pom redis.clients jedis org.springframework.boot spring-boot-maven-plugin ================================================ FILE: dis-seckill-user/src/main/java/com/seckill/dis/user/UserApplication.java ================================================ package com.seckill.dis.user; import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; @EnableDubbo @SpringBootApplication public class UserApplication { public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(UserApplication.class, args); } } ================================================ FILE: dis-seckill-user/src/main/java/com/seckill/dis/user/domain/SeckillUser.java ================================================ package com.seckill.dis.user.domain; import java.io.Serializable; import java.util.Date; /** * 秒杀用户信息 * * @author noodle */ public class SeckillUser implements Serializable{ private Long uuid; private Long phone; private String nickname; private String password; private String salt; private String head; private Date registerDate; private Date lastLoginDate; private Integer loginCount; public Long getUuid() { return uuid; } public void setUuid(Long uuid) { this.uuid = uuid; } public Long getPhone() { return phone; } public void setPhone(Long phone) { this.phone = phone; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getSalt() { return salt; } public void setSalt(String salt) { this.salt = salt; } public String getHead() { return head; } public void setHead(String head) { this.head = head; } public Date getRegisterDate() { return registerDate; } public void setRegisterDate(Date registerDate) { this.registerDate = registerDate; } public Date getLastLoginDate() { return lastLoginDate; } public void setLastLoginDate(Date lastLoginDate) { this.lastLoginDate = lastLoginDate; } public Integer getLoginCount() { return loginCount; } public void setLoginCount(Integer loginCount) { this.loginCount = loginCount; } @Override public String toString() { return "SeckillUser{" + "uuid=" + uuid + ", phone=" + phone + ", nickname='" + nickname + '\'' + ", password='" + password + '\'' + ", salt='" + salt + '\'' + ", head='" + head + '\'' + ", registerDate=" + registerDate + ", lastLoginDate=" + lastLoginDate + ", loginCount=" + loginCount + '}'; } } ================================================ FILE: dis-seckill-user/src/main/java/com/seckill/dis/user/persistence/SeckillUserMapper.java ================================================ package com.seckill.dis.user.persistence; import com.seckill.dis.user.domain.SeckillUser; import org.apache.ibatis.annotations.*; /** * seckill_user表交互接口 * * @author noodle */ @Mapper public interface SeckillUserMapper { /** * 通过 phone 查询用户信息 * * @param phone * @return */ SeckillUser getUserByPhone(@Param("phone") Long phone); /** * 更新用户信息 * * @param updatedUser */ @Update("UPDATE seckill_user SET password=#{password} WHERE id=#{id}") void updatePassword(SeckillUser updatedUser); /** * 插入一条用户信息到数据库中 * * @param seckillUser * @return */ long insertUser(SeckillUser seckillUser); /** * 查询电话号码 * * @param phone * @return */ long findPhone(long phone); } ================================================ FILE: dis-seckill-user/src/main/java/com/seckill/dis/user/persistence/SeckillUserMapper.xml ================================================ INSERT INTO seckill_user (phone, nickname, password, salt, head, register_date, last_login_date, login_count) VALUES (#{phone}, #{nickname}, #{password}, #{salt}, #{head}, #{registerDate}, #{lastLoginDate}, #{loginCount}) SELECT last_insert_id() ================================================ FILE: dis-seckill-user/src/main/java/com/seckill/dis/user/service/UserServiceImpl.java ================================================ package com.seckill.dis.user.service; import com.seckill.dis.common.api.cache.DLockApi; import com.seckill.dis.common.api.cache.RedisServiceApi; import com.seckill.dis.common.api.cache.vo.SkUserKeyPrefix; import com.seckill.dis.common.api.user.UserServiceApi; import com.seckill.dis.common.api.user.vo.LoginVo; import com.seckill.dis.common.api.user.vo.RegisterVo; import com.seckill.dis.common.api.user.vo.UserInfoVo; import com.seckill.dis.common.api.user.vo.UserVo; import com.seckill.dis.common.exception.GlobalException; import com.seckill.dis.common.result.CodeMsg; import com.seckill.dis.common.util.MD5Util; import com.seckill.dis.common.util.UUIDUtil; import com.seckill.dis.user.domain.SeckillUser; import com.seckill.dis.user.persistence.SeckillUserMapper; import org.apache.dubbo.config.annotation.Reference; import org.apache.dubbo.config.annotation.Service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import javax.validation.Valid; import java.util.Date; @Service(interfaceClass = UserServiceApi.class) public class UserServiceImpl implements UserServiceApi { private static Logger logger = LoggerFactory.getLogger(UserServiceImpl.class); @Autowired private SeckillUserMapper userMapper; /** * 由于需要将一个cookie对应的用户存入第三方缓存中,这里用redis,所以需要引入redis serice */ @Reference(interfaceClass = RedisServiceApi.class) private RedisServiceApi redisService; @Reference(interfaceClass = DLockApi.class) private DLockApi dLock; @Override public int login(String username, String password) { return 45; } /** * 注册用户 * * @param userModel 用户vo * @return 状态码 */ @Override public CodeMsg register(RegisterVo userModel) { // 加锁 String uniqueValue = UUIDUtil.uuid() + "-" + Thread.currentThread().getId(); String lockKey = "redis-lock" + userModel.getPhone(); boolean lock = dLock.lock(lockKey, uniqueValue, 60 * 1000); if (!lock) return CodeMsg.WAIT_REGISTER_DONE; logger.debug("注册接口加锁成功"); // 检查用户是否注册 SeckillUser user = this.getSeckillUserByPhone(userModel.getPhone()); // 用户已经注册 if (user != null) { dLock.unlock(lockKey, uniqueValue); return CodeMsg.USER_EXIST; } // 生成skuser对象 SeckillUser newUser = new SeckillUser(); newUser.setPhone(userModel.getPhone()); newUser.setNickname(userModel.getNickname()); newUser.setHead(userModel.getHead()); newUser.setSalt(MD5Util.SALT); String dbPass = MD5Util.formPassToDbPass(userModel.getPassword(), MD5Util.SALT); newUser.setPassword(dbPass); Date date = new Date(System.currentTimeMillis()); newUser.setRegisterDate(date); // 写入数据库 long id = userMapper.insertUser(newUser); boolean unlock = dLock.unlock(lockKey, uniqueValue); if (!unlock) return CodeMsg.REGISTER_FAIL; logger.debug("注册接口解锁成功"); // 用户注册成功 if (id > 0) return CodeMsg.SUCCESS; // 用户注册失败 return CodeMsg.REGISTER_FAIL; } @Override public boolean checkUsername(String username) { return false; } @Override public UserInfoVo getUserInfo(int uuid) { return null; } @Override public UserInfoVo updateUserInfo(UserInfoVo userInfoVo) { return null; } /** * 用户登录, 要么处理成功返回true,否则会抛出全局异常 * 抛出的异常信息会被全局异常接收,全局异常会将异常信息传递到全局异常处理器 * * @param loginVo 封装了客户端请求传递过来的数据(即账号密码) * (使用post方式,请求参数放在了请求体中,这个参数就是获取请求体中的数据) * @return 用户token */ @Override public String login(@Valid LoginVo loginVo) { logger.info(loginVo.toString()); // 获取用户提交的手机号码和密码 String mobile = loginVo.getMobile(); String password = loginVo.getPassword(); // 判断手机号是否存在(首先从缓存中取,再从数据库取) SeckillUser user = this.getSeckillUserByPhone(Long.parseLong(mobile)); // 缓存中、数据库中都不存在该用户信息,直接返回 if (user == null) throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST); logger.info("用户:" + user.toString()); // 判断手机号对应的密码是否一致 String dbPassword = user.getPassword(); String dbSalt = user.getSalt(); String calcPass = MD5Util.formPassToDbPass(password, dbSalt); if (!calcPass.equals(dbPassword)) throw new GlobalException(CodeMsg.PASSWORD_ERROR); // 执行到这里表明登录成功,更新用户cookie // 生成cookie String token = UUIDUtil.uuid(); // 每次访问都会生成一个新的session存储于redis和反馈给客户端,一个session对应存储一个user对象 redisService.set(SkUserKeyPrefix.TOKEN, token, user); return token; } @Override public UserVo getUserByPhone(long phone) { UserVo userVo = new UserVo(); SeckillUser user = userMapper.getUserByPhone(phone); userVo.setUuid(user.getUuid()); userVo.setSalt(user.getSalt()); userVo.setRegisterDate(user.getRegisterDate()); userVo.setPhone(user.getPhone()); userVo.setPassword(user.getPassword()); userVo.setNickname(user.getNickname()); userVo.setLoginCount(user.getLoginCount()); userVo.setLastLoginDate(user.getLastLoginDate()); userVo.setHead(user.getHead()); return userVo; } /** * 根据 phone 查询秒杀用户信息 *

* 对象级缓存 * 从缓存中查询 SeckillUser 对象,如果 SeckillUser 在缓存中存在,则直接返回,否则从数据库返回 * * @param phone 用户手机号码 * @return SeckillUser */ private SeckillUser getSeckillUserByPhone(long phone) { // 1. 从redis中获取用户数据缓存 SeckillUser user = redisService.get(SkUserKeyPrefix.SK_USER_PHONE, "_" + phone, SeckillUser.class); if (user != null) return user; // 2. 如果缓存中没有用户数据,则从数据库中查询数据并将数据写入缓存 // 先从数据库中取出数据 user = userMapper.getUserByPhone(phone); // 然后将数据返回并将数据缓存在redis中 if (user != null) redisService.set(SkUserKeyPrefix.SK_USER_PHONE, "_" + phone, user); return user; } } ================================================ FILE: dis-seckill-user/src/main/java/com/seckill/dis/user/util/UserUtil.java ================================================ package com.seckill.dis.user.util; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.seckill.dis.common.util.DBUtil; import com.seckill.dis.common.util.MD5Util; import com.seckill.dis.user.domain.SeckillUser; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * 生成用户数据,用于压测 * * @author noodle */ public class UserUtil { static String PASSWORD = "000000"; public static void createUser(int count) throws IOException { List users = new ArrayList<>(count); // 生成用户信息 generateMiaoshaUserList(count, users); // 将用户信息插入数据库,以便在后面模拟用户登录时可以找到该用户,从而可以生成token返会给客户端,然后保存到文件中用于压测 // 首次生成数据库信息的时候需要调用这个方法,非首次需要注释掉 // insertSeckillUserToDB(users); // 模拟用户登录,生成token System.out.println("start to login..."); String urlString = "http://localhost:8080/login/create_token"; File file = new File("E:\\JavaProject\\Web\\seckill\\tokens.txt"); if (file.exists()) { file.delete(); } RandomAccessFile accessFile = new RandomAccessFile(file, "rw"); file.createNewFile(); accessFile.seek(0); for (int i = 0; i < users.size(); i++) { // 模拟用户登录 SeckillUser user = users.get(i); URL url = new URL(urlString); HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(); httpURLConnection.setRequestMethod("POST"); httpURLConnection.setDoOutput(true); OutputStream out = httpURLConnection.getOutputStream(); String params = "mobile=" + user.getPhone() + "&password=" + MD5Util.inputPassToFormPass(PASSWORD); out.write(params.getBytes()); out.flush(); // 生成token InputStream inputStream = httpURLConnection.getInputStream(); ByteArrayOutputStream bout = new ByteArrayOutputStream(); byte buff[] = new byte[1024]; int len = 0; while ((len = inputStream.read(buff)) >= 0) { bout.write(buff, 0, len); } inputStream.close(); bout.close(); String response = new String(bout.toByteArray()); JSONObject jo = JSON.parseObject(response); String token = jo.getString("data");// data为edu.uestc.controller.result.Result中的字段 System.out.println("create token : " + user.getPhone()); // 将token写入文件中,用于压测时模拟客户端登录时发送的token String row = user.getPhone() + "," + token; accessFile.seek(accessFile.length()); accessFile.write(row.getBytes()); accessFile.write("\r\n".getBytes()); System.out.println("write to file : " + user.getPhone()); } accessFile.close(); System.out.println("write token to file done!"); } /** * 生成用户信息 * * @param count 生成的用户数量 * @param users 用于存储用户的list */ private static void generateMiaoshaUserList(int count, List users) { for (int i = 0; i < count; i++) { SeckillUser user = new SeckillUser(); user.setPhone(19800000000L + i);// 模拟11位的手机号码 user.setLoginCount(1); user.setNickname("user-" + i); user.setRegisterDate(new Date()); user.setSalt("1a2b3c4d"); user.setPassword(MD5Util.inputPassToDbPass(PASSWORD, user.getSalt())); users.add(user); } } /** * 将用户信息插入数据库中 * * @param users 待插入的用户信息 * @throws ClassNotFoundException * @throws SQLException */ private static void insertSeckillUserToDB(List users) throws ClassNotFoundException, SQLException { System.out.println("start create user..."); Connection conn = DBUtil.getConn(); String sql = "INSERT INTO miaosha_user(login_count, nickname, register_date, salt, password, id)VALUES(?,?,?,?,?,?)"; PreparedStatement pstmt = conn.prepareStatement(sql); for (int i = 0; i < users.size(); i++) { SeckillUser user = users.get(i); pstmt.setInt(1, user.getLoginCount()); pstmt.setString(2, user.getNickname()); pstmt.setTimestamp(3, new Timestamp(user.getRegisterDate().getTime())); pstmt.setString(4, user.getSalt()); pstmt.setString(5, user.getPassword()); pstmt.setLong(6, user.getPhone()); pstmt.addBatch(); } pstmt.executeBatch(); pstmt.close(); conn.close(); System.out.println("insert to db done!"); } // public static void main(String[] args) throws IOException { // createUser(5000); // } } ================================================ FILE: dis-seckill-user/src/main/resources/application.properties ================================================ # -------------------------------- # spring #--------------------------------- spring.application.name=dis-seckill-user # -------------------------------- # web #--------------------------------- server.port=8081 # -------------------------------- # druid Դ #--------------------------------- # linuxϵmysql #spring.datasource.url=jdbc:mysql://192.168.216.128:3306/seckill?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8 #spring.datasource.username=linuxmysql #spring.datasource.password=000 # windowsϵmysql spring.datasource.url=jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8 spring.datasource.username=root spring.datasource.password=000 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.filters=stat spring.datasource.maxActive=1000 spring.datasource.initialSize=100 spring.datasource.maxWait=60000 spring.datasource.minIdle=500 spring.datasource.timeBetweenEvictionRunsMillis=60000 spring.datasource.minEvictableIdleTimeMillis=300000 spring.datasource.validationQuery=select 'x' spring.datasource.testWhileIdle=true spring.datasource.testOnBorrow=false spring.datasource.testOnReturn=false spring.datasource.poolPreparedStatements=true spring.datasource.maxOpenPreparedStatements=20 #--------------------------------- # mybatis #--------------------------------- mybatis.type-aliases-package=com.seckill.dis.user.domain #mybatis.type-handlers-package=com.example.typehandler mybatis.configuration.map-underscore-to-camel-case=true mybatis.configuration.default-fetch-size=100 mybatis.configuration.default-statement-timeout=3000 mybatis.mapper-locations=classpath*:com.seckill.dis.user.persistence/*.xml #--------------------------------- # Dubbo Application #--------------------------------- # Base packages to scan Dubbo Component: @org.apache.dubbo.config.annotation.Service dubbo.scan.base-packages=com.seckill.dis.user.service # The default value of dubbo.application.name is ${spring.application.name} dubbo.application.name=${spring.application.name} # Dubbo Protocol dubbo.protocol.name=dubbo dubbo.protocol.port=12345 dubbo.registry.check=true ## Dubbo Registry embedded.zookeeper.port=2181 dubbo.registry.address=zookeeper://127.0.0.1:${embedded.zookeeper.port} #--------------------------------- # redis #--------------------------------- ##redis.host=192.168.216.128 #redis.host=127.0.0.1 #redis.port=6379 #redis.timeout=100 ## redis.password=123456 #redis.poolMaxTotal=1000 #redis.poolMaxIdle=500 #redis.poolMaxWait=500 #--------------------------------- # ־ #--------------------------------- logging.level.root=info logging.level.com.seckill.dis=debug logging.path=logs/ logging.file=dis-seckill.log ================================================ FILE: doc/HandlerInterceptor的使用.md ================================================ ## 简介 SpringMVC的处理器拦截器,类似于Servlet开发中的过滤器Filter,用于对请求进行拦截和处理。 ## 常见应用场景 1、**权限检查**:如检测请求是否具有登录权限,如果没有直接返回到登陆页面。 2、**性能监控**:用请求处理前和请求处理后的时间差计算整个请求响应完成所消耗的时间。 3、**日志记录**:可以记录请求信息的日志,以便进行信息监控、信息统计等。 ## 使用 1、实现WebMvcConfigurer接口并使用@Configuration完成mvc配置,通过addInterceptors()方法注册拦截器 ```java @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired AccessInterceptor accessInterceptor; /** * 添加自定义的参数解析器到MVC配置中 * * @param argumentResolvers */ @Override public void addArgumentResolvers(List argumentResolvers) { // 添加自定义的参数解析器到MVC配置中 } /** * 添加自定义方法拦截器到MVC配置中 * * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(accessInterceptor); } } ``` 2、实现接口并继承方法(可以同时包含多个实现类) ```java @Service public class AccessInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { } default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception { } public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { } } ``` 本项目使用拦截器作为权限验证和限流组件。具体实现见源码。 ================================================ FILE: doc/README.md ================================================ # 分布式高并发商品秒杀系统设计 - [介绍](#介绍) - [快速启动](#快速启动) - [系统架构](#系统架构) - [模块介绍](#模块介绍) - [TODO LIST](#TODO) ## 介绍 本项目为另一个项目[seckill](https://github.com/Grootzz/seckill)的分布式改进版本,dis-seckill意为:distributed seckill,即分布式秒杀系统。 商品秒杀与其他业务最大的区别在于: - 低廉价格; - 大幅推广; - 瞬时售空; - 一般是定时上架; - 时间短、瞬时并发量高、网络的流量也会瞬间变大。 除了具有以上特点,秒杀商品还需要完成正常的电子商务逻辑,即:(1)查询商品;(2)创建订单;(3)扣减库存;(4)更新订单;(5)付款;(6)卖家发货。 本项目正是基于上述业务特点进行设计的,在项目中引入诸多优化手段,使系统可以从容应对秒杀场景下的业务处理。 另外,项目[seckill](https://github.com/Grootzz/seckill)为单体应用,在大并发情形下处理能力有限,所以本项目对其进行分布式改造,对职责进行划分,降低单体应用的业务耦合性。 ## 快速启动 - 构建工具 apache-maven-3.6.1 - 开发环境 JDK 1.8、Mysql 8.0.12、SpringBoot 2.1.5、zookeeper 3.4.10、dubbo 2.7.1、redis 5.0.5、rabbitmq 3.7.15 在安装之前,需要安装好上述构建工具和开发环境,推荐在linux下安装上述开发环境。 **第一步**;完成数据库的初始化,使用`./dis-seckill-common/schema/seckill.sql`初始化数据库。 **第二步**;如果安装了git,则可以采用下面的方式快速启动; ```properties git clone git@github.com:Grootzz/dis-seckill.git mvn clean package ``` 启动用户服务: ```properties jar dis-seckill-user/target/dis-seckill-user-0.0.1-SNAPSHOT.jar ``` 启动商品服务: ```properties jar dis-seckill-goods/target/dis-seckill-goods-0.0.1-SNAPSHOT.jar ``` 启动订单服务: ```properties jar dis-seckill-order/target/dis-seckill-order-0.0.1-SNAPSHOT.jar ``` 启动缓存服务: ```properties jar dis-seckill-cache/target/dis-seckill-cache-0.0.1-SNAPSHOT.jar ``` 启动消息队列服务: ```properties jar dis-seckill-mq/target/dis-seckill-mq-0.0.1-SNAPSHOT.jar ``` 启动网关服务: ```properties jar dis-seckill-gateway/target/dis-seckill-gateway-0.0.1-SNAPSHOT.jar ``` 如果将项目导入IDE中进行构建,则分别按上面的顺序启动服务即可。 **第三步**;访问项目入口地址 初始用户手机号码:18342390420,密码,:00000 ## 系统架构 ![系统架构](assets/SYSTEM_ARCHITECTURE.png) - 注册中心使用zookeeper; - 缓存采用redis; - 详细队列采用RabbitMq; - 用户请求全部交由Gateway模块处理; - Gateway模块使用RPC的方式调用其他模块提供的服务完成业务处理。 ## 模块介绍 - dis-seckill-common:通用模块 - dis-seckill-user:用户模块 - dis-seckill-goods:商品模块 - dis-seckill-order:订单模块 - dis-seckill-gateway:网关模块 - dis-seckill-cache:缓存模块 - dis-seckill-mq:消息队列模块 ## TODO - [ ] 引入JWT简化权限验证; - [ ] 完成用户注册功能; - [ ] 服务模块横向扩展; - [ ] 服务调用的负载均衡与服务降级; - [ ] gateway模块横向扩展,降低单个应用的压力; - [ ] Nginx水平扩展; - [ ] 接口压测; - [ ] ...... ================================================ FILE: doc/Redis中存储的数据.md ================================================ # Redis中存储的数据 ```properties # 1. redis中缓存通过用户手机号码获取的用户信息 key: SkUserKeyPrefix:id_{phone} value: {SeckillUser} expire: 0 # 2. redis中通过缓存的token获取用户信息 key: SkUserKeyPrefix:token_{token} value: {SeckillUser} expire: 30min # 3. redis中存储的商品列表页面 key: GoodsKeyPrefix:goodsListHtml value: {html} expire: 1min # 4. redis中存储验证码结果 key: SkKeyPrefix:verifyResult_{uuid}_{goodsId} value: {verifyResult} expire: 5min # 5. redis中存储随机秒杀地址 key: SkKeyPrefix:skPath_{uuid}_{goodsId} value: {path} expire: 1min # 6. redis中存储用户一段时间内的访问次数 key: AccessKeyPrefix:access_{URI}_{phone} value: {count} expire: {@AccessLimit#seconds} # 7. redis中存储的随机秒杀地址 key: SkKeyPrefix:skPath_{userId}_{goodsId} value: {path} expire: 1 min # 8. redis中存储的在系统加载时从db读取的商品库存数量 key: GoodsKeyPrefix:goodsStock_{goodsId} value: {stock} expire: 0 # 9. redis中存储的订单信息 key: OrderKeyPrefix:SK_ORDER:{userId}_{goodsId} value: {SeckillOrder} expire: 0 ``` ================================================ FILE: doc/使用分布式锁解决恶意用户重复注册问题.md ================================================ # 使用分布式锁解决恶意用户重复注册问题 [TOC] ## 问题分析 考虑用户模块下的用户服务实现(`com.seckill.dis.user.service.UserServiceImpl`): ```java @Override public CodeMsg register(RegisterVo userModel) { // 检查用户是否注册 SeckillUser user = this.getSeckillUserByPhone(userModel.getPhone()); // 用户已经注册 if (user != null) { return CodeMsg.USER_EXIST; } // 生成skuser对象 ... // 写入数据库 long id = userMapper.insertUser(newUser); // 用户注册成功 if (id > 0) return CodeMsg.SUCCESS; // 用户注册失败 return CodeMsg.REGISTER_FAIL; } ``` 显然,这段代码存在线程安全问题,在恶意用户使用注册接口重复写入相同数据的时候,会导致用户数据表中存在重复的数据,这是我们所不希望的。 **实际上,上述问题可以归结为:一次请求和多次请求对资源本身具有同样的结果,也就是说,相同条件下的任意多次执行对资源本身所产生的影响均与一次执行的影响相同,即保证接口的幂等性**。这就是我们需要解决的问题。 ## 解决思路 上述问题的解决可以从几个方面入手: 1. 数据库:使用乐观锁、悲观锁或唯一索引; 1. 服务端:加锁。 不管从哪个方面入手,本质的手段都是加锁。 - 使用悲观锁比较简单,使用在`insert`加上`on duplicate key update `即可,这样可以以去重复的方式加锁访问。 - 使用索引也比较简单,对`phone`字段加唯一索引即可。 - 使用乐观锁需要在表上加上一个表示版本的字段。 但是,利用数据库实现锁,会造成大量的请求落在数据库上,并阻塞等待执行,这个过程是需要消耗数据库资源的,使得真正的业务请求得不到处理,在秒杀大并发情形下,可能会导致数据库宕机,数据我们不使用数据库来实现锁,而是在服务端实现锁。 如果只有一个用户服务模块实例,则采用JUC下的`ReentrantLock`重入锁实现即可,或者直接对方法加上`synchronized`,这在单个实例下式没有问题的,也就是单进程的情况。 如果用户服务模块存在多个实例,也就是以集群的方式部署,那么就涉及进程之间的锁问题,`synchronized`和`ReentrantLock`这种单进程的锁只对落到本服务模块的请求有效,而对多进程无效,依旧会有线程安全问题。 这个时候,分布式锁就派上用场了。 ## Redis 分布式锁 分布式锁一般有三种实现方式: 1. 数据库乐观锁; 1. 基于Redis的分布式锁; 1. 基于ZooKeeper的分布式锁。 本文将介绍第二种方式,基于Redis实现分布式锁。 首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件: 1. 互斥性。在任意时刻,只有一个客户端能持有锁。 1. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。 1. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。 1. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。 ### 加锁 ```java public boolean lock(String lockKey, String uniqueValue, int expireTime) { Jedis jedis = null; try { jedis = jedisPool.getResource(); // 获取锁 String result = jedis.set(lockKey, uniqueValue, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } finally { if (jedis != null) jedis.close(); } } ``` 可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参: - 第一个为key,我们使用key来当锁,因为key是唯一的。 - 第二个为value,我们传的是uniqueValue,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为uniqueValue,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。 - 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作; - 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。 - 第五个为time,与第四个参数相呼应,代表key的过期时间。 总的来说,执行上面的set()方法就只会导致两种结果: 1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。 1. 已有锁存在,不做任何操作。 我们的加锁代码满足可靠性里描述的三个条件。 首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。 其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。 最后,因为我们将value赋值为uniqueValue,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。 ### 解锁代码 ```java public boolean unlock(String lockKey, String uniqueValue) { Jedis jedis = null; try { jedis = jedisPool.getResource(); // 使用Lua脚本保证操作的原子性 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " + "return redis.call('del', KEYS[1]) " + "else return 0 " + "end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(uniqueValue)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; } finally { if (jedis != null) jedis.close(); } } ``` 我们写了一个简单的Lua脚本代码,首先获取锁对应的value值,检查是否与uniqueValue相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。 如果不用lua,而直接用get和del,则有以下几步: 1. `get lockKey` 1. 判断 1. `del lockKey` 可见,这三步不是原子的,也就会在`del`的时候导致误删。考虑下面的情形: 如果客户端A执行`get lockKey`,执行完后,判断是客户端A,那么接下来就是准备删除操作了`del lockKey`,而如果在`del lockKey`之前`lockKey`失效,客户端B此时获取锁,问题出现了。 客户端继续执行`del lockKey,lockKey`被删除,而此时的`lockKey`却是客户端B的锁,锁以就造成了锁的误删。这个时候如果再出现客户端C获取锁,那就会造成客户端C和客户端B同时访问一个资源,违反了分布式锁的互斥性。因此,分步骤实现分布式锁是不可行的,但是如果这三个步骤是原子的,那就没问题了。而Lua正好解决了这个问题。 ## 加锁后的实现 ```java public CodeMsg register(RegisterVo userModel) { // 加锁 String uniqueValue = UUIDUtil.uuid() + "-" + Thread.currentThread().getId(); String lockKey = "redis-lock" + userModel.getPhone(); boolean lock = dLock.lock("redis-lock", uniqueValue, 60 * 1000); if (!lock) return CodeMsg.WAIT_REGISTER_DONE; logger.debug("注册接口加锁成功"); // 检查用户是否注册 SeckillUser user = this.getSeckillUserByPhone(userModel.getPhone()); // 用户已经注册 if (user != null) { return CodeMsg.USER_EXIST; } // 生成skuser对象 ... // 写入数据库 long id = userMapper.insertUser(newUser); boolean unlock = dLock.unlock(lockKey, uniqueValue); if (!unlock) return CodeMsg.REGISTER_FAIL; logger.debug("注册接口解锁成功"); // 用户注册成功 if (id > 0) return CodeMsg.SUCCESS; // 用户注册失败 return CodeMsg.REGISTER_FAIL; } ``` 在访问数据库之前加分布式锁,在完成业务之后释放分布式锁。值得注意的是,分布式锁需要标识用户以防止其他用户无法获取到锁。 ================================================ FILE: doc/前后端交互接口定义.md ================================================ # 前后端交互接口 [TOC] ## 用户模块接口 ### 首页 - 请求地址:/user/index - 请求方式:get - 返回响应:login.html ### 登录接口 - 请求地址:/user/login - 请求方式:post - 请求参数: | 请求字段 | 字段含义 | 是否必填 | | -------- | ---------- | -------- | | mobile | 用户手机号 | 必填 | | password | 密码 | 必填 | ## 商品模块接口 ### 商品列表接口 - 请求地址:/goods/goodsList - 请求方式:get - 返回响应:goods_list.htm **工作过程** 1. 客户端发出获取商品列表信息的异步请求; 1. 服务端从缓存中获取商品列表`goods_list.htm`,如果存在,则直接返回给客户端,如果不存在,则进入3; 1. 服务端从商品模块中查询商品列表信息,然后使用`ThymeleafViewResolver`渲染引擎手动渲染商品列表页面,将渲染后的页面数据存储在缓存中,并返回商品列表页面给客户端。 **好处** 上述工作过程对`/goods/goodsList`接口实现了页面级缓存;从redis中取页面,如果没有则需要手动渲染页面,并且将渲染的页面存储在redis中供下一次访问时获取。通过引入缓存,避免每次从数据库中取数据后使用模板渲染html文件,从而省去服务器程序的渲染html文件的不必要工作和的减轻数据库访问压力。 ### 商品详情接口 - 请求地址:/goods/getDetails/{goodsId} - 请求方式:get - 请求参数: | 请求字段 | 字段含义 | 是否必填 | | -------- | -------- | -------- | | goodsId | 商品id | 必填 | - 返回响应: ```json { "code": 0, "msg": "success", "data": { "seckillStatus": 1, "remainSeconds": 0, "goods": { "id": 1, "goodsName": "iphoneX", "goodsTitle": "Apple iPhone X (A1865) 64GB 银色 移动联通电信4G手机", "goodsImg": "/img/iphonex.png", "goodsDetail": "Apple iPhone X (A1865) 64GB 银色 移动联通电信4G手机", "goodsPrice": 8765, "goodsStock": 10000, "seckillPrice": 0.01, "stockCount": 9, "startDate": "2017-12-04T13:51:23.000+0000", "endDate": "2019-12-31T13:51:27.000+0000" }, "user": { "uuid": 2, "phone": 18342390420, "nickname": "Noodle", "password": "5e7b3a9754c2777f96174d4ccb980d23", "salt": "1a2b3c4d", "head": null, "registerDate": null, "lastLoginDate": null, "loginCount": 0 } } } ``` **工作过程** 1. 客户端发出获取商品详情信息的请求; 1. 服务端通过商品id从商品模块中读取商品详细信息,计算秒杀状态,将秒杀状态和剩余时间以及商品详细信息一并返回给客户端。 ## 秒杀模块接口 ### 获取验证码接口 - 接口地址:/seckill/verifyCode?goodsId={} - 请求方式:get - 请求参数: | 请求字段 | 字段含义 | 是否必填 | | -------- | -------- | -------- | | goodsId | 商品id | 必填 | - 返回响应:验证码图片 **工作过程** 客户端发出获取验证请求,服务端生成验证码图片,并通过response对象返回,通过,将验证码结果存储于缓存中。 ### 获取验证码接口 - 接口地址:/seckill/path?goodsId={}&verifyCode={} - 请求方式:get - 请求参数: | 请求字段 | 字段含义 | 是否必填 | | ---------- | ---------- | -------- | | goodsId | 商品id | 必填 | | verifyCode | 验证码结果 | 必填 | - 返回响应:验证码图片 **工作过程** 1. 客户端获取验证码结果,并将商品id和验证码结果作为请求参数传递到服务端; 2. 服务端获取用户id和商品id,从缓存中查询该用户和该商品下的验证码结果; 2. 比较客户端传递到服务端的验证码结果和缓存中查询出来的验证码结果,如果校验失败,返回非法请求,通过则进入4; 2. 创建随机秒杀地址(MD5随机字符串),并将其存储在缓存中,其key为用户id和商品id的组合,已确定秒杀地址的唯一性。 ### 获取秒杀随机地址接口 - 接口地址:/seckill/{path}/doSeckill - 请求方式:post - 请求参数: | 请求字段 | 字段含义 | 是否必填 | | -------- | ------------ | -------- | | path | 随机秒杀地址 | 必填 | | goodsId | 商品id | 必填 | - 返回响应:秒杀状态 **工作过程** 1. 以用户id和商品id的组合为键从缓存中读取随机地址,如果随机地址和客户端传入的随机地址不一致,则返回非法请求,如果一致,则进入2; 1. 判断内存中商品是否已秒杀结束,如果结束,则返回秒杀结束标志; 1. 如果判断出秒杀商品未结束则从缓存中预减库存,并在内存中标记商品是否秒杀结束; 1. 从缓存中读取秒杀订单信息,判断是否重复秒杀,如果重复秒杀,则返回重复秒杀提示,如果是第一次秒杀,则进入5; 1. 第4步中可能存在缓存数据已经无效的情形,所以,需要充数据库中读取秒杀订单信息,如果存在记录,则返回重复秒杀提示,如果没有记录,则进入6; 1. 将秒杀请求发送到消息队列中,返回排队提示。客户端继续轮询以获取秒杀结果; 1. 消息队列减库存、下订单、写入秒杀订单。 ### 获取秒杀结果接口 - 接口地址:/seckill/result - 请求方式:GET - 请求参数: | 请求字段 | 字段含义 | 是否必填 | | -------- | -------- | -------- | | goodsId | 商品id | 必填 | - 返回响应:订单信息 **工作过程** 从订单列表获取秒杀结果,返回给客户端订单信息。 ================================================ FILE: doc/前后端交互接口逻辑实现.md ================================================ # 前后端交互接口逻辑实现 [TOC] ## 用户登录逻辑 请求url:`/user/login` 1. 通过请求参数解析器获取用户请求参数,获取用户名和用户密码,判断用户是否存在(首先从缓存中获取该用户是否存在,如果用户存在,则从缓存中获取,如果不存在,则从db中获取,并将获取得到的用户信息存储到缓存中,以便下次获取用户信息时直接从缓存中获取);如果用户不存在,则向客户端发送用户不存在的消息,如果用户存在,进入2; 1. 检验用户输入密码是否正确,如果用户输入密码不正确,则返回用户密码不正确的消息给客户端,否则,如果用户密码校验正确,进入3; 1. 生成token,并存储在缓存中,这样,下次访问的时候,就可以从缓存中查询token,直接通过token校验用户是否合法,从而防止重复登录,token存储到缓存中后,进入4; 1. 生成cookie对象,包装token,然后将cookie对象通过response返回给客户端,cookie有效期和缓存中的token有效期一致; 1. 用户在cookie有效期内发出资源请求时,服务端从url或者set-cookie字段获取token信息,通过token从缓存中获取用户信息,如果获取的用户信息存在,则表明是同一个用户在访问,否则,如果用户不存在,则表名token不正确,请求非法; 1. 从缓存中获取用户信息存在时,需要重新在缓存中设置一下该token,以达到记录最新访问时间,根据过期时间延长cookie有效期的目的。 **登录过程总结:** 对需要进行用户鉴权的访问,在controller层的方法上添加LoginVo参数,这样,用户请求都会使用自定义的参数解析器处理LoginVo,在处理的时候完成鉴权工作。 为了方式用户每次请求都从db中获取用户信息,在第一次登录成功的时候将用户信息从数据库中查询出来并缓存在redis中,并将其过期时间设置为0,即永久有效,那么,在往后该用户访问并需要获取用户信息的时候,就可以直接从缓存中获取用户信息,而不用反复从db中获取,从而减少不必要的请求落到db上。本身用户数据就属于不会经常变动却需要经常读取的数据,放在缓存中再合适不过了,不过,在用户数据更改的时候,需要考虑缓存和数据库的数据一致性。 用户数据通过两种方式缓存在redis中,一种是以phone为key,另一种是以token为key,缓存的数据都是用户信息,唯一不一样的是,它们的过期时间不同。token具有时效性,因此过期时间设置得比较短,为30min。而另一份以phone为key的用户信息永久保留在redis中,用户减少对db的访问。所以,两份数据的意义是不一样的,以token为key的用户信息缓存用户鉴权,而以phone为key的用户信息缓存用于查询。 redis中缓存通过用户手机号码获取的用户信息: ```properties key: SkUserKeyPrefix:id_{phone} value: {SeckillUser} expire: 0 ``` redis中通过缓存的token获取用户信息: ```properties key: SkUserKeyPrefix:token_{token} value: {SeckillUser} expire: 30min ``` ## 商品列表请求逻辑 商品列表请求url:`goods/goodsList` 1. 首先从缓存中查询商品列表页面的html文件,如果存在,则直接返回给客户端,如果不存在,进入2; 1. 从数据库中查询所有商品信息,然后通过thymeleafViewResolver渲染页面商品列表页面,通过将列表页面存储到缓存中,以便下次访问商品列表页面时直接从缓存中获取; **商品列表请求逻辑总结:** 商品列表请求逻辑相对简单,只是在处理的时候,将商品列表模板渲染过程从自动变为手动,之所以这样做是希望在redis中缓存该页面,如果自动渲染,那么该页面会在每次客户端发出请求时都渲染一次,列表页的数据实际上除了库存信息以外,其他信息都是不变的,因此,可以将其缓存起来。如果要缓存页面信息到redis中,必须获取该页面,显然,自动渲染时没法得到页面的,所以手动显示地渲染,得到列表页面,并缓存。 值得注意的一点是,redis在缓存列表html时,因为列表页面的库存信息实际上会变化,如果redis中缓存页面过期时间设置过长,则会造成db和缓存的数据不一致,所以,缓存时间的设置是一个关键,过期就需要从db中获取,本项目将缓存过期施加设置为1min,也就是说,在1min内,用户看到的数据和db的数据不会过于不一致,但实际上还是会有一定的不一致。当然,这个过期时间越小越好,但是这就会造成对db的频繁访问,造成db压力过大,影响核心业务,所以,需要在过期时间和db访问压力之间做一个权衡。 通过上述可知,为了库存数据更加实时,db查询商品列表信息越小,需要进一步优化。(目前思路:将商品信息也预存到redis中,但是这样有可能会出现存储空间不足的情况) redis中存储的商品列表页面为: ```properties key: GoodsKeyPrefix:goodsListHtml value: {html} expire: 1min ``` ## 商品详情请求逻辑 商品详情请求url:`goods/getDetails/{goodsId}` 1. 从db中获取获取商品详情; 1. 计算商品秒杀状态以及秒杀剩余时间; 1. 封装商品秒杀状态、秒杀剩余时间和秒杀商品详细信息到vo,并返回给客户端,由客户端获取该vo的json数据并渲染。 **商品详情请求逻辑总结** 因为商品详细信息中的库存、秒杀状态在详情页面的时候需要近乎实时的获取,这样可以给用户一个更好的体验。所以,需要每次都从db中获取该商品的详细信息。 秒杀商品和商品是分别使用两个表存储的,这样做的目的在于:商品列表包含了商品的详细信息,秒杀商品存储的信息为和秒杀有关的信息,如果使用同一个表存储商品的所有信息(包含秒杀信息),那么,在向表写入数据的时候,就会造成过多的请求阻塞地获取锁,而实际上,秒杀业务下,写入操作多为和秒杀有关的字段,如果将这些字段分离处理,商品表主要用于读,而秒杀商品列表用于秒杀业务,这样就可以提高数据库的吞吐量。 **改进**:是否可以将秒杀商品表的关键信息预存到redis中? ## 获取验证码图片逻辑 获取验证码url:`/seckill/verifyCode` 1. 服务端收到请求,生成验证码,并通过ScriptEngine引擎计算验证码结果,然后将验证码结果存储于缓存中; 2. 将验证码图片以`JPEG`格式返回给客户端; redis中存储验证码结果: ```properties key: SkKeyPrefix:verifyResult_{uuid}_{goodsId} value: {verifyResult} expire: 5min ``` ## 获取秒杀接口地址逻辑 获取秒杀接口地址请求url:`/seckill/path` 1. 根据用户输入的验证码值和goodsId从缓存中获取验证码结果,校验验证码是够正确,如果校验失败,则返回用户重新输入提示,如果校验成功,则创建随机秒杀地址; 1. 使用UUID工具生成随机秒杀地址,并将随机秒杀地址存储于缓存中; 1. 返回给客户端随机秒杀地址。 **获取秒杀接口地址总结:** 之所以引入随机秒杀地址,原因如下: 如果秒杀接口的地址为静态地址,那么用户可以轻易的使用接口地址完成进行恶意秒杀,这样会使得参与秒杀的用户参与度不搞,达不到业务目的,引入随机地址则可以很好的规避这个问题。 秒杀地址的生成在验证码校验之后,这就是说,一定需要在验证码输入正确的情况下才能获取到随机秒杀地址。 除了上述的通过验证码保护秒杀请求地址外,还引入了接口防刷机制防止用户过于频繁的提交请求。 redis中存储的随机秒杀地址用户秒杀请求时的地址校验。 redis中存储随机秒杀地址: ```properties key: SkKeyPrefix:skPath_{uuid}_{goodsId} value: {path} expire: 1min ``` ## 秒杀接口防刷机制 1. 服务器拦截用户请求,判断请求处理器上是否有@accessLimit注解,如果没有,直接放行,如果有,则进入2; 1. 获取注解参数,包括最大访问次数、时间间隔; 1. 对于第一次访问有@accessLimit注解的方法,将随机url地址存储到redis中,并设置过期时间为注解上的时间间隔。 1. 对第二次请求,如果redis中统计的请求次数没有达到最大值,则自增,如果达到最大值,则向用户发出频繁请求响应。 **总结** ```java @AccessLimit(seconds = 5, maxAccessCount = 5, needLogin = true) ``` 有上述注解的方法将会被拦截,其含义为:在5s内,最大访问次数为5次。 redis中存储用户一段时间内的访问次数: ```properties key: AccessKeyPrefix:access_{URI}_{phone} value: {count} expire: {@AccessLimit#seconds} ``` ## 秒杀请求处理逻辑 秒杀请求url:`/seckill/{path}/doSeckill` 1. 根据userId和goodsId从redis中读取{path},校验随机秒杀接口地址是否一致,如果不一致,则说明客户端发送的秒杀请求非法,随机秒杀地址被客户端更改。如果一致,则进入2; 1. 系统在启动的时候,已经从数据库中加载所有秒杀商品的库存信息,标记库存的有无到本地内存(HashMap)中和记录具体商品的库存到redis中,所以,这一步在内存标记中判断是否该商品还有库存,如果没有,直接驳回请求,如果有,则进入3; 1. redis中在最开始系统启动时记录了商品的库存信息,所以,可以通过redis预减库存,而不需要在这个时候到db中减库存,如果库存预减到不大于0,表明之前的请求已经将商品库存消耗完成,此时,在内存中标记该商品已经完成秒杀。如果预减库存成功,则将请求放行到4; 1. 根据useId和goodsId从redis中查询秒杀订单信息,如果存在,则说明,该用户已经完成该商品的秒杀,直接驳回请求,否则,放行请求到5; 1. 根据useId和goodsId从数据库中获取订单信息,如果存在,则直接驳回请求,否则放行请求到6; 1. 生成秒杀请求消息,放入队列中,将秒杀请求交由队列处理。 **秒杀请求处理逻辑总结:** 一句话,使用内存标记和缓存将秒杀请求拦截在db上游,防止大并发下的秒杀请求落到db。 为什么有了内存标记,预减库存,订单缓存的情形下,还要在缓存中订单不存在的情况下从db读取订单信息,而在队列中又有从db读取订单信息拦截请求的操作? **这个问题是由大并发导致的**,实际上,内存标记,可以阻挡一部分请求,然后通过redis预减库存,也只能拦截一部分请求。考虑一种情形: 假设用户以极快的速度同时发出两次请求,两次请求有相同的userId和goodsId,前一次请求完成秒杀时,后一次请求正好从db中读取订单信息,那么可见,后一次请求可以读到完成秒杀的订单,这样就可以将该用户请求拦截下来,不用发送到消息队列中处理,减轻消息队列的负载。 另一种情形,后一次请求没有从数据库中读取到该用户的订单信息,也就是执行时间稍微超前于前一次请求写入订单的时机,那么实际上后一次请求也是无效请求,会发送到消息队列处理,再看消息队列的消费者,消费者也会先从缓存再从db中读取该请求的秒杀订单信息,这样就可以将这个无效请求拦截下来,不用落到db上,达到减轻db负载的压力。 这就是为什么会**两次**从redis中读取订单信息,在redis中订单信息无效时从db读订单信息;一次发生在秒杀请求发送到消息队列之前,一次发生在发送到消息队列之后(即真正做秒杀之前)。目的即使为了阻挡无效的请求落到db上。 redis中存储的随机秒杀地址为: ```properties key: SkKeyPrefix:skPath_{userId}_{goodsId} value: {path} expire: 1 min ``` redis中存储的在系统加载时从db读取的商品库存信息: ```properties key: GoodsKeyPrefix:goodsStock_{goodsId} value: {stock} expire: 0 ``` ## 消息队列处理秒杀请求的过程(秒杀业务的核心) 1. 消息队列收到秒杀消息(SkMessage[user, goodsId])后,通过goodsId从DB中查询该商品的库存信息,如果库存不大于0,则直接返回,反之,则表明该商品还有库存,进入2; 1. 通过userId和goodsId从redis中查询秒杀订单信息,如果查询结果不为空,则说明该用户已经对该商品进行过秒杀,直接返回;反之,可能因为缓存有效期的问题,使得缓存中的秒杀订单信息无效,进入3; 1. 根据userId和goodsId从DB中获取秒杀订单信息,如果秒杀订单信息不为空,则说明该用户已经完成该商品的秒杀,直接将秒杀请求驳回,如果查询的秒杀订单信息为空。则说明该用户为对该商品进行过秒杀,进入4; 1. 这一步为秒杀业务逻辑的关键,分为三步:从商品表中减该商品的库存,生成订单信息写入秒杀订单表和订单表中; 1. 首先,减库存。在该商品库存不为零的时候,返回更新记得记录id,大于0则表明更新成,即减库成功;反之,库存为0,减库存失败,在redis中标记该商品已没有库存。 ```mysql UPDATE seckill_goods SET stock_count = stock_count-1 WHERE goods_id=#{goodsId} AND stock_count > 0 ``` 1. 如果5中减库存成功,则创建订单,将订单写入秒杀订单表和订单信息表中,并且,将生成的订单信息在db写操作完成后存储到redis中,这样,下次同一用户对同一商品发起秒杀请求时,直接使用redis中的数据就可以判断其完成了秒杀,而不用再从db中读数据判断该用户是否对该商品已经完成了秒杀; 1. 需要注意的是,秒杀动作的关键三步:减库存,生成订单记录插入订单信息表和秒杀订单表构成事务,需要使用Spring的@Transactional注解处理事务。 **消息队列处理秒杀请求的过程总结:** 在秒杀请求中,我们使用大量的缓存将秒杀请求阻挡在db外,真正落入db的请求应该尽可能的小,这样可以防止秒杀请求直接透穿DB,从而减轻db压力。 实际上,秒杀商品有限,库存也有限,如果将秒杀请求直接落到db,是非常不合理的,考虑一种情形,某件秒杀商品的库存为100,在秒杀开始的时候,瞬间的秒杀请求并发量为5W,可以想象,数据库是无法承担如此高的并大量的,另外,5w个秒杀请求,实际只有100个秒杀请求有效,多出来的请求只会无端对数据库造成访问压力,而对业务毫不相关。 消息队列使用redis来拦截秒杀请求,redis中缓存何种数据是非常重要的。消息队列处理秒杀请求时只会从缓存中读/写订单信息,写缓存发生在db写订单完成后,读缓存发生在对db写之前。写redis发生在db之后,可以保证缓存和db中的数据的一致性,读redis发生在写db之前,可以用来阻挡无用请求,减轻db压力。这个地方,并没有做到缓存于dB的强一致性,只能保证最终一致性。 redis中存储的订单信息为: ```properties key: OrderKeyPrefix:SK_ORDER:{userId}_{goodsId} value: {SeckillOrder} expire: 0 ``` ================================================ FILE: pom.xml ================================================ 4.0.0 pom com.seckill.dis dis-seckill 0.0.1-SNAPSHOT dis-seckill 分布式商品秒杀系统 dis-seckill-user dis-seckill-common dis-seckill-gateway dis-seckill-goods dis-seckill-order dis-seckill-cache dis-seckill-mq org.springframework.boot spring-boot-starter-parent 2.1.5.RELEASE 2.1.5.RELEASE 1.8 2.0.1 8.0.28 1.0.5 2.7.1 1.2.38 1.0.0 com.seckill.dis dis-seckill-common ${top-version} org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import org.springframework.boot spring-boot-starter ${spring-boot.version} org.springframework.boot spring-boot-starter-test ${spring-boot.version} test org.springframework.boot spring-boot-starter-web ${spring-boot.version} org.springframework.boot spring-boot-configuration-processor ${spring-boot.version} true org.mybatis.spring.boot mybatis-spring-boot-starter ${mybatis.version} org.apache.dubbo dubbo-dependencies-bom ${dubbo.version} pom import org.apache.dubbo dubbo-spring-boot-starter ${dubbo.version} org.apache.dubbo dubbo ${dubbo.version} org.springframework spring javax.servlet servlet-api log4j log4j org.apache.dubbo dubbo-dependencies-zookeeper ${dubbo.version} org.slf4j slf4j-log4j12 pom mysql mysql-connector-java ${mysql.vsersion} com.alibaba druid ${druid.version} com.alibaba fastjson ${fastjson.version} org.apache.commons commons-lang3 ${commons-lang3.version} junit junit ${junit.version} org.apache.maven.plugins maven-compiler-plugin 3.1 ${java.version} ${java.version} org.springframework.boot spring-boot-maven-plugin src/main/resources true src/main/java **/*.xml