├── README.assets ├── 1.png ├── 2.png ├── 3.png └── 4.png ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ ├── maven-wrapper.properties │ └── MavenWrapperDownloader.java ├── src ├── main │ ├── resources │ │ ├── application.properties │ │ ├── static │ │ │ ├── lib │ │ │ │ ├── font │ │ │ │ │ └── icon │ │ │ │ │ │ ├── clock.png │ │ │ │ │ │ └── seckillbg.png │ │ │ │ ├── jquery.cookie.js │ │ │ │ ├── countdown.js │ │ │ │ └── bootstrap.min.js │ │ │ ├── css │ │ │ │ ├── public.css │ │ │ │ ├── seckill_item.css │ │ │ │ └── seckill.css │ │ │ └── js │ │ │ │ └── seckill_detail.js │ │ ├── templates │ │ │ ├── public │ │ │ │ ├── header.html │ │ │ │ └── footer.html │ │ │ ├── seckill.html │ │ │ └── seckill_detail.html │ │ └── mybatis │ │ │ └── mapper │ │ │ ├── SuccessKilledMapper.xml │ │ │ └── SeckillMapper.xml │ └── java │ │ └── com │ │ └── example │ │ └── demo3 │ │ ├── exception │ │ ├── SeckillException.java │ │ ├── SeckillCloseException.java │ │ └── RepeatKillException.java │ │ ├── Demo3Application.java │ │ ├── config │ │ ├── MyBatisConfig.java │ │ └── DruidConfig.java │ │ ├── controller │ │ ├── IndexController.java │ │ └── SeckillController.java │ │ ├── enums │ │ └── SeckillStatEnum.java │ │ ├── mapper │ │ ├── SuccessKilledMapper.java │ │ └── SeckillMapper.java │ │ ├── bean │ │ ├── SuccessKilled.java │ │ └── Seckill.java │ │ ├── dto │ │ ├── SeckillResult.java │ │ ├── SeckillExecution.java │ │ └── Exposer.java │ │ ├── service │ │ ├── SeckillService.java │ │ └── impl │ │ │ └── SeckillServiceImpl.java │ │ └── redis │ │ ├── JedisConfig.java │ │ └── RedisDao.java └── test │ └── java │ └── com │ └── example │ └── demo3 │ └── Demo3ApplicationTests.java ├── .gitignore ├── SQL ├── success_killed.sql ├── seckill.sql └── seckill-transaction.sql ├── README.md ├── pom.xml ├── mvnw.cmd └── mvnw /README.assets/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erdengk/SpringBoot-Seckill/HEAD/README.assets/1.png -------------------------------------------------------------------------------- /README.assets/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erdengk/SpringBoot-Seckill/HEAD/README.assets/2.png -------------------------------------------------------------------------------- /README.assets/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erdengk/SpringBoot-Seckill/HEAD/README.assets/3.png -------------------------------------------------------------------------------- /README.assets/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erdengk/SpringBoot-Seckill/HEAD/README.assets/4.png -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erdengk/SpringBoot-Seckill/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erdengk/SpringBoot-Seckill/HEAD/src/main/resources/application.properties -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip 2 | -------------------------------------------------------------------------------- /src/main/resources/static/lib/font/icon/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erdengk/SpringBoot-Seckill/HEAD/src/main/resources/static/lib/font/icon/clock.png -------------------------------------------------------------------------------- /src/main/resources/static/lib/font/icon/seckillbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erdengk/SpringBoot-Seckill/HEAD/src/main/resources/static/lib/font/icon/seckillbg.png -------------------------------------------------------------------------------- /src/main/java/com/example/demo3/exception/SeckillException.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3.exception; 2 | 3 | /** 4 | * 秒杀相关的异常 5 | * 6 | */ 7 | public class SeckillException extends RuntimeException { 8 | 9 | public SeckillException(String message) { 10 | super(message); 11 | } 12 | 13 | public SeckillException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo3/exception/SeckillCloseException.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3.exception; 2 | 3 | /** 4 | * 秒杀关闭异常 5 | 6 | */ 7 | public class SeckillCloseException extends SeckillException { 8 | 9 | public SeckillCloseException(String message) { 10 | super(message); 11 | } 12 | 13 | public SeckillCloseException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo3/exception/RepeatKillException.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3.exception; 2 | 3 | /** 4 | * 重复执行秒杀的异常(运行期异常) 5 | 6 | */ 7 | public class RepeatKillException extends SeckillException { 8 | 9 | public RepeatKillException(String message) { 10 | super(message); 11 | } 12 | 13 | public RepeatKillException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo3/Demo3Application.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3; 2 | 3 | import org.mybatis.spring.annotation.MapperScan; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | @SpringBootApplication 8 | @MapperScan("com.example.demo3.mapper") 9 | @MapperScan("com.example.demo3.redis") 10 | public class Demo3Application { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(Demo3Application.class, args); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/resources/templates/public/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |
9 | 16 |
17 | 18 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo3/config/MyBatisConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3.config; 2 | 3 | import org.apache.ibatis.session.Configuration; 4 | import org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer; 5 | import org.springframework.context.annotation.Bean; 6 | 7 | @org.springframework.context.annotation.Configuration 8 | public class MyBatisConfig { 9 | 10 | @Bean 11 | public ConfigurationCustomizer configurationCustomizer(){ 12 | return new ConfigurationCustomizer(){ 13 | 14 | @Override 15 | public void customize(Configuration configuration) { 16 | configuration.setMapUnderscoreToCamelCase(true); 17 | } 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/resources/templates/public/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo3/controller/IndexController.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3.controller; 2 | 3 | import com.example.demo3.bean.Seckill; 4 | import com.example.demo3.service.SeckillService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.ui.Model; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * @author : dk 14 | * @date : 2019/8/9 10:30 15 | * @description : 16 | */ 17 | @Controller 18 | public class IndexController { 19 | 20 | @Autowired 21 | private SeckillService seckillService; 22 | @RequestMapping("/") 23 | public String list(Model model) 24 | { 25 | List list = seckillService.findAll(); 26 | model.addAttribute("list", list); 27 | return "seckill"; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /SQL/success_killed.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat MySQL Data Transfer 3 | 4 | Source Server : localhost_3306 5 | Source Server Version : 50720 6 | Source Host : localhost:3306 7 | Source Database : spring 8 | 9 | Target Server Type : MYSQL 10 | Target Server Version : 50720 11 | File Encoding : 65001 12 | 13 | Date: 2019-08-10 09:17:21 14 | */ 15 | 16 | SET FOREIGN_KEY_CHECKS=0; 17 | 18 | -- ---------------------------- 19 | -- Table structure for success_killed 20 | -- ---------------------------- 21 | DROP TABLE IF EXISTS `success_killed`; 22 | CREATE TABLE `success_killed` ( 23 | `seckill_id` int(11) NOT NULL COMMENT '秒杀商品id', 24 | `user_phone` varchar(255) NOT NULL, 25 | `state` tinyint(4) DEFAULT '-1' COMMENT '-1:无效,0:成功,1:已付款', 26 | `create_time` datetime DEFAULT NULL, 27 | PRIMARY KEY (`seckill_id`,`user_phone`), 28 | KEY `idx_create_time` (`create_time`) 29 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 30 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo3/enums/SeckillStatEnum.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3.enums; 2 | 3 | /** 4 | * 枚举类 5 | */ 6 | public enum SeckillStatEnum { 7 | SUCCESS(1, "秒杀成功"), 8 | END(0, "秒杀结束"), 9 | REPEAT_KILL(-1,"重复秒杀"), 10 | INNER_ERROR(-2, "系统异常"), 11 | DATA_REWRITE(-3, "数据串改"); 12 | 13 | private int state; 14 | private String stateInfo; 15 | 16 | SeckillStatEnum(int state, String stateInfo) { 17 | this.state = state; 18 | this.stateInfo = stateInfo; 19 | } 20 | 21 | public int getState() { 22 | return state; 23 | } 24 | 25 | public String getStateInfo() { 26 | return stateInfo; 27 | } 28 | 29 | public static SeckillStatEnum stateOf(int index){ 30 | for (SeckillStatEnum state : values()){ 31 | if (state.getState() == index){ 32 | return state; 33 | } 34 | } 35 | return null; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo3/mapper/SuccessKilledMapper.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3.mapper; 2 | 3 | import com.example.demo3.bean.SuccessKilled; 4 | import org.apache.ibatis.annotations.Mapper; 5 | import org.apache.ibatis.annotations.Param; 6 | 7 | import java.util.Date; 8 | 9 | /** 10 | * @author : dk 11 | * @date : 2019/8/7 19:51 12 | * @description : 13 | */ 14 | @Mapper 15 | public interface SuccessKilledMapper { 16 | 17 | /** 18 | * 插入一条秒杀记录 19 | * @param seckillId 20 | * @param userPhone 21 | * @return 22 | */ 23 | int insertSuccessKilled(@Param("seckillId") Integer seckillId,@Param("userPhone") String userPhone,@Param("createTime") Date createTime); 24 | 25 | /** 26 | * 根据seckillId查询SuccessKilled对象,并携带Seckill对象 27 | * @param seckillId 28 | * @param userPhone 29 | * @return 30 | */ 31 | SuccessKilled queryByIdWithSeckill(@Param("seckillId") Integer seckillId, @Param("userPhone") String userPhone); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /SQL/seckill.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat MySQL Data Transfer 3 | 4 | Source Server : localhost_3306 5 | Source Server Version : 50720 6 | Source Host : localhost:3306 7 | Source Database : spring 8 | 9 | Target Server Type : MYSQL 10 | Target Server Version : 50720 11 | File Encoding : 65001 12 | 13 | Date: 2019-08-10 09:17:14 14 | */ 15 | 16 | SET FOREIGN_KEY_CHECKS=0; 17 | 18 | -- ---------------------------- 19 | -- Table structure for seckill 20 | -- ---------------------------- 21 | DROP TABLE IF EXISTS `seckill`; 22 | CREATE TABLE `seckill` ( 23 | `seckill_id` int(11) NOT NULL, 24 | `name` varchar(255) DEFAULT NULL, 25 | `number` int(11) DEFAULT NULL, 26 | `start_time` datetime DEFAULT NULL, 27 | `end_time` datetime DEFAULT NULL, 28 | `create_time` datetime DEFAULT NULL, 29 | PRIMARY KEY (`seckill_id`), 30 | KEY `idx_start_time` (`start_time`), 31 | KEY `idx_end_time` (`end_time`), 32 | KEY `idx_create_time` (`create_time`) 33 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 34 | -------------------------------------------------------------------------------- /src/main/resources/static/css/public.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | a { 7 | text-decoration: none; 8 | } 9 | 10 | #author { 11 | float: right; 12 | border-bottom: none; 13 | } 14 | 15 | #logo { 16 | float: left; 17 | border-bottom: none; 18 | color: #409EFF; 19 | font-weight: bold; 20 | font-size: 18px; 21 | } 22 | 23 | #author > img { 24 | border: 1px solid #eee; 25 | border-radius: 100%; 26 | box-sizing: border-box; 27 | height: 30px; 28 | margin: 0 8px 0 10px; 29 | padding: 2px; 30 | width: 30px; 31 | } 32 | 33 | #author .el-dropdown { 34 | display: inline-block; 35 | position: relative; 36 | color: #606266; 37 | font-size: 14px; 38 | } 39 | 40 | .footer { 41 | border-top: solid 1px #e6e6e6; 42 | margin-top: 50px; 43 | } 44 | 45 | .footer .footer-inner { 46 | margin-top: 10px; 47 | color: rgb(153, 153, 153); 48 | margin-bottom: 25px; 49 | } 50 | 51 | .footer .footer-inner .copyright { 52 | margin-bottom: 3px; 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo3/mapper/SeckillMapper.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3.mapper; 2 | 3 | import com.example.demo3.bean.Seckill; 4 | import org.apache.ibatis.annotations.Mapper; 5 | import org.apache.ibatis.annotations.Param; 6 | 7 | import java.util.Date; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | /** 12 | * @author : dk 13 | * @date : 2019/8/7 19:51 14 | * @description : 15 | */ 16 | @Mapper 17 | public interface SeckillMapper { 18 | /** 19 | * 根据id查看当前秒杀的商品 20 | * @param seckillId 21 | * @return 22 | */ 23 | Seckill queryById(@Param("seckillId") Integer seckillId); 24 | 25 | /** 26 | * 根据所给参数查询 秒杀的商品 27 | * @param offset 28 | * @param limit 29 | * @return 30 | */ 31 | List queryAll(@Param("offset")Integer offset , @Param("limit")Integer limit); 32 | 33 | 34 | /** 35 | * 返回所有商品列表 36 | * @return 37 | */ 38 | List findAll(); 39 | /** 40 | * 给当前商品的库存 -1 41 | * @param seckillId 42 | * @param killTime 43 | * @return 44 | */ 45 | int reduceNumber(@Param("seckillId") long seckillId, @Param("killTime") Date killTime); 46 | 47 | /** 48 | * 使用存储过程秒杀 49 | * @param objectMap 50 | */ 51 | void killByProcedure(Map objectMap); 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo3/bean/SuccessKilled.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3.bean; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import lombok.Data; 5 | import org.springframework.format.annotation.DateTimeFormat; 6 | 7 | @Data 8 | public class SuccessKilled { 9 | 10 | private Integer seckillId; 11 | private String userPhone; 12 | private Integer state; 13 | @DateTimeFormat(pattern ="yyyy-MM-dd HH:mm:ss") 14 | 15 | @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8") 16 | private java.util.Date createTime; 17 | 18 | private Seckill seckill; 19 | 20 | public Integer getSeckillId() { 21 | return seckillId; 22 | } 23 | 24 | public void setSeckillId(Integer seckillId) { 25 | this.seckillId = seckillId; 26 | } 27 | 28 | 29 | public String getUserPhone() { 30 | return userPhone; 31 | } 32 | 33 | public void setUserPhone(String userPhone) { 34 | this.userPhone = userPhone; 35 | } 36 | 37 | 38 | public Integer getState() { 39 | return state; 40 | } 41 | 42 | public void setState(Integer state) { 43 | this.state = state; 44 | } 45 | 46 | 47 | public java.util.Date getCreateTime() { 48 | return createTime; 49 | } 50 | 51 | public void setCreateTime(java.util.Date createTime) { 52 | this.createTime = createTime; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo3/dto/SeckillResult.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3.dto; 2 | 3 | /** 4 | * 封装JSON返回的结果格式 5 | */ 6 | public class SeckillResult { 7 | 8 | private boolean success; 9 | 10 | private T data; 11 | 12 | private String error; 13 | 14 | public SeckillResult(boolean success, T data) { 15 | this.success = success; 16 | this.data = data; 17 | } 18 | 19 | public SeckillResult(boolean success, String error) { 20 | this.success = success; 21 | this.error = error; 22 | } 23 | 24 | public boolean isSuccess() { 25 | return success; 26 | } 27 | 28 | public void setSuccess(boolean success) { 29 | this.success = success; 30 | } 31 | 32 | public T getData() { 33 | return data; 34 | } 35 | 36 | public void setData(T data) { 37 | this.data = data; 38 | } 39 | 40 | public String getError() { 41 | return error; 42 | } 43 | 44 | public void setError(String error) { 45 | this.error = error; 46 | } 47 | 48 | @Override 49 | public String toString() { 50 | return "SeckillResult{" + 51 | "success=" + success + 52 | ", data=" + data + 53 | ", error='" + error + '\'' + 54 | '}'; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/resources/mybatis/mapper/SuccessKilledMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | INSERT ignore INTO success_killed(seckill_id,user_phone,state,create_time) 10 | VALUES (#{seckillId},#{userPhone},0,#{createTime}) 11 | 12 | 13 | 34 | 35 | -------------------------------------------------------------------------------- /src/main/resources/mybatis/mapper/SeckillMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 16 | 17 | 20 | 21 | 29 | 30 | 31 | UPDATE seckill 32 | SET number = number-1 33 | WHERE seckill_id=#{seckillId} 34 | AND start_time #{killTime} 35 | AND end_time >= #{killTime} 36 | AND number > 0; 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /SQL/seckill-transaction.sql: -------------------------------------------------------------------------------- 1 | -- 秒杀执行存储过程 2 | 3 | DELIMITER $$ -- console ; 转换为 $$ 4 | -- 定义存储过程 5 | -- 参数: in 输入参数; out 输出参数 6 | -- row_count(): 返回上一条修改类型sql的影响行数 7 | -- 0:未修改数据; >0 表示修改的行数; <0 表示sql错误/未执行修改 8 | CREATE PROCEDURE `spring`.`execute_seckill`( IN v_seckill_id INT , IN v_phone VARCHAR(255) , IN v_kill_time TIMESTAMP , OUT r_result INT ) 9 | BEGIN 10 | DECLARE 11 | insert_count INT DEFAULT 0; 12 | START TRANSACTION; 13 | 14 | INSERT IGNORE INTO success_killed 15 | ( seckill_id, user_phone, create_time ) 16 | VALUES 17 | ( v_seckill_id, v_phone, v_kill_time ); 18 | SELECT 19 | ROW_COUNT( ) INTO insert_count; 20 | IF 21 | ( insert_count = 0 ) THEN 22 | ROLLBACK; 23 | 24 | SET r_result = - 1; 25 | 26 | ELSEIF ( insert_count < 0 ) THEN 27 | ROLLBACK; 28 | 29 | SET r_result = - 2; 30 | 31 | ELSE 32 | UPDATE seckill 33 | SET number = number - 1 34 | WHERE 35 | seckill_id = v_seckill_id 36 | AND end_time > v_kill_time 37 | AND start_time < v_kill_time AND number > 0; 38 | 39 | SELECT 40 | ROW_COUNT( ) INTO insert_count; 41 | IF 42 | ( insert_count = 0 ) THEN 43 | ROLLBACK; 44 | 45 | SET r_result = 0; 46 | 47 | ELSEIF ( insert_count < 0 ) THEN 48 | ROLLBACK; 49 | 50 | SET r_result = - 2; 51 | 52 | ELSE COMMIT; 53 | SET r_result = 1; 54 | 55 | END IF; 56 | 57 | END IF; 58 | 59 | END; 60 | $$ 61 | -- 存储过程定义结束 62 | 63 | 64 | 65 | set @r_result = -3; 66 | call execute_seckill(1, 11111111118, now(), @r_result); 67 | 68 | select @r_result; 69 | -- -- 获取结果 70 | 71 | -- 存储过程 72 | -- 1:存储过程优化:事务行级锁持有时间 73 | -- 2:不要过度依赖存储过程 74 | -- 3:简单的逻辑可以应用存储过程 75 | -- 4:QPS:一个秒杀单6000/qps-- 秒杀执行过程 76 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo3/service/SeckillService.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3.service; 2 | 3 | import com.example.demo3.bean.Seckill; 4 | import com.example.demo3.dto.Exposer; 5 | import com.example.demo3.dto.SeckillExecution; 6 | import com.example.demo3.exception.RepeatKillException; 7 | import com.example.demo3.exception.SeckillCloseException; 8 | import com.example.demo3.exception.SeckillException; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * @author : dk 14 | * @date : 2019/8/8 16:38 15 | * @description : 16 | */ 17 | 18 | 19 | public interface SeckillService { 20 | 21 | /** 22 | * 获取所有的秒杀商品列表 23 | * 24 | * @return 25 | */ 26 | List findAll(); 27 | 28 | /** 29 | * 获取某一条商品秒杀信息 30 | * 31 | * @param seckillId 32 | * @return 33 | */ 34 | Seckill getById(Integer seckillId); 35 | 36 | /** 37 | * 秒杀开始时输出暴露秒杀的地址 38 | * 否则输出系统时间和秒杀时间 39 | * 40 | * @param seckillId 41 | */ 42 | Exposer exportSeckillUrl(Integer seckillId); 43 | 44 | /** 45 | * 执行秒杀的操作 46 | * @param seckillId 47 | * 48 | * @param userPhone 49 | * @param md5 50 | * @return 51 | */ 52 | SeckillExecution executeSeckill(Integer seckillId, String userPhone, String md5) 53 | throws SeckillException, RepeatKillException, SeckillCloseException; 54 | 55 | 56 | /** 57 | * 执行秒杀的操作 by 存储过程 58 | * @param seckillId 59 | * 60 | * @param userPhone 61 | * @param md5 62 | * @return 63 | */ 64 | SeckillExecution executeSeckillProducedure(Integer seckillId, String userPhone, String md5); 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo3/redis/JedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3.redis; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import redis.clients.jedis.JedisPool; 9 | import redis.clients.jedis.JedisPoolConfig; 10 | 11 | @Configuration 12 | public class JedisConfig { 13 | private Logger logger = LoggerFactory.getLogger(JedisConfig.class); 14 | 15 | @Value("${spring.redis.host}") 16 | private String host; 17 | 18 | @Value("${spring.redis.port}") 19 | private int port; 20 | 21 | @Value("${spring.redis.timeout}") 22 | private int timeout; 23 | 24 | // @Value("${spring.redis.jedis.pool.max-active}") 25 | // private int maxActive; 26 | // 27 | // @Value("${spring.redis.jedis.pool.max-idle}") 28 | // private int maxIdle; 29 | // 30 | // @Value("${spring.redis.jedis.pool.min-idle}") 31 | // private int minIdle; 32 | // 33 | // @Value("${spring.redis.jedis.pool.max-wait}") 34 | // private long maxWaitMillis; 35 | 36 | @Bean 37 | public JedisPool redisPoolFactory(){ 38 | JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); 39 | // jedisPoolConfig.setMaxIdle(maxIdle); 40 | // jedisPoolConfig.setMaxWaitMillis(maxWaitMillis); 41 | // jedisPoolConfig.setMaxTotal(maxActive); 42 | // jedisPoolConfig.setMinIdle(minIdle); 43 | JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, null); 44 | 45 | logger.info("JedisPool注入成功"); 46 | logger.info("redis地址:" + host + ":" + port); 47 | return jedisPool; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo3/bean/Seckill.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3.bean; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import lombok.Data; 5 | import org.springframework.format.annotation.DateTimeFormat; 6 | 7 | @Data 8 | public class Seckill { 9 | 10 | private Integer seckillId; 11 | private String name; 12 | private Integer number; 13 | @DateTimeFormat(pattern ="yyyy-MM-dd HH:mm:ss") 14 | @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8") 15 | private java.util.Date startTime; 16 | 17 | @DateTimeFormat(pattern ="yyyy-MM-dd HH:mm:ss") 18 | @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8") 19 | private java.util.Date endTime; 20 | 21 | @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8") 22 | @DateTimeFormat(pattern ="yyyy-MM-dd HH:mm:ss") 23 | private java.util.Date createTime; 24 | 25 | 26 | public Integer getSeckillId() { 27 | return seckillId; 28 | } 29 | 30 | public void setSeckillId(Integer seckillId) { 31 | this.seckillId = seckillId; 32 | } 33 | 34 | 35 | public String getName() { 36 | return name; 37 | } 38 | 39 | public void setName(String name) { 40 | this.name = name; 41 | } 42 | 43 | 44 | public Integer getNumber() { 45 | return number; 46 | } 47 | 48 | public void setNumber(Integer number) { 49 | this.number = number; 50 | } 51 | 52 | 53 | public java.util.Date getStartTime() { 54 | return startTime; 55 | } 56 | 57 | public void setStartTime(java.util.Date startTime) { 58 | this.startTime = startTime; 59 | } 60 | 61 | 62 | public java.util.Date getEndTime() { 63 | return endTime; 64 | } 65 | 66 | public void setEndTime(java.util.Date endTime) { 67 | this.endTime = endTime; 68 | } 69 | 70 | 71 | public java.util.Date getCreateTime() { 72 | return createTime; 73 | } 74 | 75 | public void setCreateTime(java.util.Date createTime) { 76 | this.createTime = createTime; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## SpringBoot 秒杀项目 2 | 3 | ### **线上体验地址** 4 | [Spring Boot秒杀系统](http://106.15.38.234:8000/) 5 | 6 | ### **技术栈** 7 | 8 | - 后端: SpringBoot-2.x + Redis-4.x 9 | - 前端: Bootstrap + Jquery 10 | 11 | ### **测试环境** 12 | 13 | - IDEA + Maven 3.5.4 + Tomcat8 + JDK8 14 | 15 | ### 开发环境 16 | 17 | - 语言: JDK 1.8 18 | 19 | - IDE: IDEA 2019.2 20 | 21 | - 依赖管理: Maven 22 | 23 | - 数据库: Mysql 5.7 24 | 25 | ### **启动说明** 26 | 27 | - 启动前,请配置好 `application.properties` 中连接数据库的用户名和密码,以及Redis服务器的地址和端口信息。 28 | - 启动前,请创建数据库`seckill`,建表SQL语句放在:SQL文件夹中。具体的建表和建库语句请仔细看SQL文件。 29 | - 创建好数据库之后,请执行 SQL 文件夹中的 seckill-transaction.sql 中的存储过程,然后直接进行下面的操作。也可以将 `service.impl.SeckillServiceImpl` 中的144-174 行注释, 30 | 再将 `controller.SeckillController` 89 行开启注释,关闭90 行的注释,这样也可以正常运行项目。 31 | - 配置完成后,运行位于 `src\main\java\com\example\demo3`下的SpringbootSeckillApplication中的main方法,访问 `http://localhost:8080/seckill/list` 进行API测试。 32 | - 注意数据库的sql 数据需要自己添加。 33 | 34 | **仓库地址:**https://github.com/wannengdek/SpringBoot-Seckill 35 | **欢迎star、fork,给作者一些鼓励** https://github.com/wannengdek 36 | 37 | ### 开发过程 38 | 39 | 总体分为四个模块,具体开发过程请看我的博客: 40 | 41 | [Spring Boot秒杀系统(一)Dao 层](https://blog.csdn.net/qq_41852212/article/details/98884976) 42 | 43 | [Spring Boot秒杀系统(二)Service层](https://blog.csdn.net/qq_41852212/article/details/98954619) 44 | 45 | [Spring Boot秒杀系统(三)web 层](https://blog.csdn.net/qq_41852212/article/details/99111102) 46 | 47 | [Spring Boot秒杀系统(四)高并发优化](https://blog.csdn.net/qq_41852212/article/details/99111443) 48 | 49 | [Spring Boot秒杀系统(五)线上部署](https://blog.csdn.net/qq_41852212/article/details/99409105) 50 | 51 | [SpringBoot秒杀系统(六)项目总结](https://blog.csdn.net/qq_41852212/article/details/99475317) 52 | 53 | 54 | 55 | ### 项目预览 56 | 57 | #### 主页面 58 | 59 | 60 | 61 | ![1](README.assets/1.png) 62 | 63 | 64 | 65 | #### 商品详情页面 (未登录) 66 | 67 | ![2](README.assets/2.png) 68 | 69 | #### 点击秒杀之后 70 | 71 | ![3](README.assets/3.png) 72 | 73 | #### 再次秒杀 74 | 75 | ![4](README.assets/4.png) 76 | 77 | 没有错误数据产生,并且也提示了用户重复秒杀的问题。 78 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo3/config/DruidConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3.config; 2 | 3 | import com.alibaba.druid.pool.DruidDataSource; 4 | import com.alibaba.druid.support.http.StatViewServlet; 5 | import com.alibaba.druid.support.http.WebStatFilter; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 8 | import org.springframework.boot.web.servlet.ServletRegistrationBean; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | import javax.sql.DataSource; 13 | import java.util.Arrays; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | @Configuration 18 | public class DruidConfig { 19 | 20 | @ConfigurationProperties(prefix = "spring.datasource") 21 | @Bean 22 | public DataSource druid(){ 23 | return new DruidDataSource(); 24 | } 25 | 26 | //配置Druid的监控 27 | //1、配置一个管理后台的Servlet 28 | @Bean 29 | public ServletRegistrationBean statViewServlet(){ 30 | ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*"); 31 | Map initParams = new HashMap<>(); 32 | 33 | initParams.put("loginUsername","admin"); 34 | initParams.put("loginPassword","123456"); 35 | initParams.put("allow",""); 36 | //默认就是允许所有访问 37 | initParams.put("deny","192.168.15.21"); 38 | 39 | bean.setInitParameters(initParams); 40 | return bean; 41 | } 42 | 43 | 44 | //2、配置一个web监控的filter 45 | @Bean 46 | public FilterRegistrationBean webStatFilter(){ 47 | FilterRegistrationBean bean = new FilterRegistrationBean(); 48 | bean.setFilter(new WebStatFilter()); 49 | 50 | Map initParams = new HashMap<>(); 51 | initParams.put("exclusions","*.js,*.css,/druid/*"); 52 | 53 | bean.setInitParameters(initParams); 54 | 55 | bean.setUrlPatterns(Arrays.asList("/*")); 56 | 57 | return bean; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo3/dto/SeckillExecution.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3.dto; 2 | 3 | import com.example.demo3.bean.SuccessKilled; 4 | import com.example.demo3.enums.SeckillStatEnum; 5 | import lombok.Data; 6 | 7 | /** 8 | * 封装执行秒杀后的结果 9 | * 10 | */ 11 | 12 | @Data 13 | public class SeckillExecution { 14 | 15 | private Integer seckillId; 16 | 17 | //秒杀执行结果状态 18 | private int state; 19 | 20 | //状态表示 21 | private String stateInfo; 22 | 23 | //秒杀成功的订单对象 24 | private SuccessKilled successKilled; 25 | 26 | public SeckillExecution(Integer seckillId, SeckillStatEnum seckillStatEnum, String stateInfo, SuccessKilled successKilled) { 27 | this.seckillId = seckillId; 28 | this.state = seckillStatEnum.getState(); 29 | this.stateInfo = stateInfo; 30 | this.successKilled = successKilled; 31 | } 32 | 33 | public SeckillExecution(Integer seckillId, SeckillStatEnum seckillStatEnum, String stateInfo) { 34 | this.seckillId = seckillId; 35 | this.state = seckillStatEnum.getState(); 36 | this.stateInfo = stateInfo; 37 | } 38 | 39 | public SeckillExecution(Integer seckillId, SeckillStatEnum seckillStatEnum, SuccessKilled successKilled) { 40 | this.seckillId = seckillId; 41 | this.state = seckillStatEnum.getState(); 42 | this.successKilled = successKilled; 43 | } 44 | 45 | public SeckillExecution(Integer seckillId, SeckillStatEnum seckillStatEnum) { 46 | this.seckillId = seckillId; 47 | this.state = seckillStatEnum.getState(); 48 | } 49 | 50 | public Integer getSeckillId() { 51 | return seckillId; 52 | } 53 | 54 | public void setSeckillId(Integer seckillId) { 55 | this.seckillId = seckillId; 56 | } 57 | 58 | public int getState() { 59 | return state; 60 | } 61 | 62 | public void setState(int state) { 63 | this.state = state; 64 | } 65 | 66 | public String getStateInfo() { 67 | return stateInfo; 68 | } 69 | 70 | public void setStateInfo(String stateInfo) { 71 | this.stateInfo = stateInfo; 72 | } 73 | 74 | 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo3/dto/Exposer.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3.dto; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 暴露秒杀地址DTO 7 | */ 8 | @Data 9 | public class Exposer { 10 | 11 | //是否开启秒杀 12 | private boolean exposed; 13 | 14 | //加密措施,避免用户通过抓包拿到秒杀地址 15 | private String md5; 16 | 17 | //ID 18 | private Integer seckillId; 19 | 20 | //系统当前时间(毫秒) 21 | private long now; 22 | 23 | //秒杀开启时间 24 | private long start; 25 | 26 | //秒杀结束时间 27 | private long end; 28 | 29 | public Exposer(boolean exposed, String md5, Integer seckillId) { 30 | this.exposed = exposed; 31 | this.md5 = md5; 32 | this.seckillId = seckillId; 33 | } 34 | 35 | public Exposer(boolean exposed, Integer seckillId, long now, long start, long end) { 36 | this.exposed = exposed; 37 | this.seckillId = seckillId; 38 | this.now = now; 39 | this.start = start; 40 | this.end = end; 41 | } 42 | 43 | public Exposer(boolean exposed, Integer seckillId) { 44 | this.exposed = exposed; 45 | this.seckillId = seckillId; 46 | } 47 | 48 | public boolean isExposed() { 49 | return exposed; 50 | } 51 | 52 | public void setExposed(boolean exposed) { 53 | this.exposed = exposed; 54 | } 55 | 56 | public String getMd5() { 57 | return md5; 58 | } 59 | 60 | public void setMd5(String md5) { 61 | this.md5 = md5; 62 | } 63 | 64 | public Integer getSeckillId() { 65 | return seckillId; 66 | } 67 | 68 | public void setSeckillId(Integer seckillId) { 69 | this.seckillId = seckillId; 70 | } 71 | 72 | public long getNow() { 73 | return now; 74 | } 75 | 76 | public void setNow(long now) { 77 | this.now = now; 78 | } 79 | 80 | public long getStart() { 81 | return start; 82 | } 83 | 84 | public void setStart(long start) { 85 | this.start = start; 86 | } 87 | 88 | public long getEnd() { 89 | return end; 90 | } 91 | 92 | public void setEnd(long end) { 93 | this.end = end; 94 | } 95 | 96 | @Override 97 | public String toString() { 98 | return "Exposer{" + 99 | "exposed=" + exposed + 100 | ", md5='" + md5 + '\'' + 101 | ", seckillId=" + seckillId + 102 | ", now=" + now + 103 | ", start=" + start + 104 | ", end=" + end + 105 | '}'; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo3/redis/RedisDao.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3.redis; 2 | 3 | /** 4 | * @author : dk 5 | * @date : 2019/8/10 10:33 6 | * @description : 7 | */ 8 | 9 | import com.dyuproject.protostuff.LinkedBuffer; 10 | import com.dyuproject.protostuff.ProtostuffIOUtil; 11 | import com.dyuproject.protostuff.runtime.RuntimeSchema; 12 | import com.example.demo3.bean.Seckill; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.stereotype.Repository; 17 | import redis.clients.jedis.Jedis; 18 | import redis.clients.jedis.JedisPool; 19 | 20 | @Repository 21 | public class RedisDao { 22 | 23 | private final Logger logger = LoggerFactory.getLogger(this.getClass()); 24 | 25 | @Autowired 26 | JedisPool jedisPool; 27 | 28 | 29 | // public RedisDao(String host, int port) { 30 | // jedisPool = new JedisPool(host, port); 31 | // } 32 | 33 | private RuntimeSchema schema = RuntimeSchema.createFrom(Seckill.class); 34 | 35 | public Seckill getSeckill(Integer seckillId) { 36 | // Redis 操作逻辑 37 | try { 38 | // Jedis jedis = jedisPool.getResource(); 39 | 40 | Jedis jedis = jedisPool.getResource(); 41 | try { 42 | String key = "seckill:" + seckillId; 43 | // 并没有实现内部序列化操作,采用自定义序列化 44 | // get-》byte【】 -》 反序列化 -》 Object (seckill) 45 | // protostuff:pojo 46 | byte[] bytes = jedis.get(key.getBytes()); 47 | // 缓冲重获取到 48 | if (bytes != null) { 49 | //空对象 50 | Seckill seckill = schema.newMessage(); 51 | ProtostuffIOUtil.mergeFrom(bytes, seckill, schema); 52 | // seckill 被反序列化 53 | return seckill; 54 | } 55 | } finally { 56 | jedis.close(); 57 | } 58 | } catch (Exception e) { 59 | logger.error(e.getMessage(), e); 60 | } 61 | return null; 62 | } 63 | 64 | public String putSeckill(Seckill seckill) { 65 | // set Object(Seckill) -> 序列化 -> byte[] 66 | try { 67 | Jedis jedis = jedisPool.getResource(); 68 | try { 69 | String key = "seckill:" + seckill.getSeckillId(); 70 | byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema, 71 | LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE)); 72 | // 超时缓存 73 | int timeout = 60 * 60; 74 | // 1小时 75 | String result = jedis.setex(key.getBytes(), timeout, bytes); 76 | return result; 77 | } finally { 78 | jedis.close(); 79 | } 80 | } catch (Exception e) { 81 | logger.error(e.getMessage(), e); 82 | } 83 | return null; 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /src/main/resources/static/css/seckill_item.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | margin: 15px 0; 4 | font-family: "微软雅黑"; 5 | margin-left: 5%; 6 | } 7 | 8 | .itemInfo-wrap { 9 | font-family: "宋体"; 10 | width: 40%; 11 | margin-right: 24%; 12 | } 13 | 14 | .fr { 15 | float: right; 16 | } 17 | 18 | .sku-name h4 { 19 | font-weight: 700; 20 | font-size: 16px; 21 | color: #333; 22 | font-family: "微软雅黑"; 23 | } 24 | 25 | h4 { 26 | font-size: 14.04px; 27 | line-height: 21.06px; 28 | } 29 | 30 | .news { 31 | padding-left: 10px; 32 | line-height: 30px; 33 | font-weight: 700; 34 | color: #fff; 35 | background: url(../../lib/font/icon/seckillbg.png) repeat-x; 36 | } 37 | 38 | .news span img { 39 | margin-top: -5px; 40 | margin-right: 5px; 41 | } 42 | 43 | img { 44 | border: 0 none; 45 | vertical-align: middle; 46 | max-width: 100%; 47 | width: auto \9; 48 | height: auto; 49 | vertical-align: middle; 50 | border: 0; 51 | -ms-interpolation-mode: bicubic; 52 | border-style: none; 53 | } 54 | 55 | .news .overtime { 56 | float: right; 57 | padding-right: 15px; 58 | } 59 | 60 | .summary { 61 | background: #fee9eb; 62 | padding: 7px; 63 | } 64 | 65 | .summary-wrap { 66 | overflow: hidden; 67 | line-height: 28px; 68 | margin-top: 10px; 69 | } 70 | 71 | .title { 72 | margin-right: 15px; 73 | } 74 | 75 | .fl { 76 | float: left; 77 | } 78 | 79 | i { 80 | font-style: normal !important; 81 | } 82 | 83 | .summary-wrap .price { 84 | color: #c81623; 85 | } 86 | 87 | .summary-wrap .price i { 88 | font-size: 16px; 89 | } 90 | 91 | .summary-wrap .price b { 92 | font-size: 24px; 93 | font-weight: 700; 94 | } 95 | 96 | .summary-wrap .price span b { 97 | font-size: 12px; 98 | } 99 | 100 | .summary-wrap .price span { 101 | font-size: 12px; 102 | } 103 | 104 | .fr { 105 | float: right; 106 | } 107 | 108 | .clearfix { 109 | clear: both; 110 | } 111 | 112 | .summary-wrap { 113 | overflow: hidden; 114 | line-height: 28px; 115 | margin-top: 10px; 116 | } 117 | 118 | .preview-wrap { 119 | /*width: 25%;*/ 120 | /*height: 100%;*/ 121 | border: 1px solid red; 122 | border-style: dashed; 123 | } 124 | 125 | .fl { 126 | float: left; 127 | } 128 | 129 | .spec-preview { 130 | width: 100%; 131 | height: 100%; 132 | } 133 | 134 | .jqzoom { 135 | float: left; 136 | border: 0; 137 | position: relative; 138 | padding: 5px; 139 | cursor: pointer; 140 | margin: 0; 141 | display: block; 142 | } 143 | 144 | .jqzoom > img { 145 | width: 410px; 146 | max-width: none; 147 | border: 0 none; 148 | vertical-align: middle; 149 | max-width: 100%; 150 | width: auto \9; 151 | height: auto; 152 | vertical-align: middle; 153 | border: 0; 154 | -ms-interpolation-mode: bicubic; 155 | border-style: none; 156 | } 157 | 158 | -------------------------------------------------------------------------------- /src/main/resources/templates/seckill.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 秒杀商品列表页 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 |
18 |
19 |
20 | 26 |
27 |
28 |
29 |
30 | 31 | 32 | 35 | 38 | 42 | 50 | 51 | 52 |
33 | 34 | 36 | 剩余库存: 37 | 39 | 40 | 41 | 43 |
44 | 开始时间:[[${#dates.format(item.startTime, 'yyyy-MM-dd HH:mm:ss')}]] 45 |
46 |
47 | 结束时间:[[${#dates.format(item.endTime, 'yyyy-MM-dd HH:mm:ss')}]] 48 |
49 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | 61 | 62 | 63 |
64 | 65 |
66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/main/resources/static/css/seckill.css: -------------------------------------------------------------------------------- 1 | .el-dropdown-menu__item a { 2 | list-style: none; 3 | line-height: 36px; 4 | /*padding: 0 20px;*/ 5 | margin: 0; 6 | font-size: 14px; 7 | color: #606266; 8 | cursor: pointer; 9 | outline: 0; 10 | text-decoration: none; 11 | } 12 | 13 | #main { 14 | margin-top: 3%; 15 | margin-left: 3%; 16 | } 17 | 18 | #main > .el-container { 19 | width: 80%; 20 | height: 100%; 21 | } 22 | 23 | .show { 24 | /*width: 261px;*/ 25 | /*height: 357px;*/ 26 | /*display: inline-block;*/ 27 | /*margin: 2px 5px;*/ 28 | /*border: 1px solid #eee;*/ 29 | /*overflow: hidden;*/ 30 | 31 | display: inline-block !important; 32 | margin-left: 10px; 33 | margin-top: 10px; 34 | width: 287px; 35 | line-height: 26px; 36 | list-style-type: none; 37 | cursor: pointer; 38 | border: 1px solid transparent; 39 | border-color: transparent; 40 | } 41 | 42 | .show:hover { 43 | border-color: rgb(177, 25, 26); 44 | border-style: dashed; 45 | } 46 | 47 | .show .img > img { 48 | /*border-bottom: 1px solid #eee;*/ 49 | /*!*border-radius: 100%;*!*/ 50 | /*box-sizing: border-box;*/ 51 | /*height: 253px;*/ 52 | /*!*margin: 0 8px 0 10px;*!*/ 53 | /*padding: 2px;*/ 54 | width: 290px; 55 | 56 | max-width: 100%; 57 | width: auto \9; 58 | height: auto; 59 | vertical-align: middle; 60 | border: 0; 61 | -ms-interpolation-mode: bicubic; 62 | border-style: none; 63 | } 64 | 65 | .show .price { 66 | /*padding: 0 10px;*/ 67 | } 68 | 69 | .show .price > span { 70 | /*color: #F40;*/ 71 | /*font-weight: 800;*/ 72 | /*font-size: 23px;*/ 73 | 74 | font-size: 22px; 75 | color: #e60012; 76 | font-weight: bolder; 77 | } 78 | 79 | .show .count { 80 | font-size: 13px; 81 | 82 | /*padding: 0 10px;*/ 83 | display: inline-block; 84 | } 85 | 86 | .show .count .stock { 87 | /*font-size: 15px;*/ 88 | /*color: #F40;*/ 89 | /*font-weight: 600;*/ 90 | 91 | font-size: 16px; 92 | color: #e12228; 93 | font-weight: bolder; 94 | } 95 | 96 | .show .title { 97 | /*margin-top: 10px;*/ 98 | /*font-size: 10px;*/ 99 | /*padding: 0 5px;*/ 100 | 101 | /*padding: 0 10px;*/ 102 | font-size: 14px; 103 | color: #666; 104 | } 105 | 106 | .show .title > span { 107 | font-size: 14px; 108 | color: #666; 109 | } 110 | 111 | .time { 112 | margin-top: 20px; 113 | background: rgb(255, 0, 30); 114 | color: #fff; 115 | /*border-radius: 4px;*/ 116 | font-size: 12px; 117 | 118 | padding-left: 10px; 119 | line-height: 30px; 120 | font-weight: 700; 121 | } 122 | 123 | .time span img { 124 | margin-top: -5px; 125 | margin-right: 5px; 126 | border: 0 none; 127 | vertical-align: middle; 128 | } 129 | 130 | .time .overtime { 131 | float: right; 132 | padding-right: 15px; 133 | } 134 | 135 | .buy { 136 | padding-top: 15px; 137 | font-size: 20px; 138 | border: 0; 139 | border-radius: 0; 140 | /*background-color: #b1191a;*/ 141 | color: #fff; 142 | display: block; 143 | width: 100%; 144 | padding-left: 0; 145 | padding-right: 0; 146 | -webkit-box-sizing: border-box; 147 | -moz-box-sizing: border-box; 148 | box-sizing: border-box; 149 | margin-bottom: 0; 150 | font-size: 12px; 151 | line-height: 18px; 152 | text-align: center; 153 | vertical-align: middle; 154 | cursor: pointer; 155 | } 156 | -------------------------------------------------------------------------------- /src/main/resources/static/lib/jquery.cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Cookie Plugin v1.4.1 3 | * https://github.com/carhartl/jquery-cookie 4 | * 5 | * Copyright 2013 Klaus Hartl 6 | * Released under the MIT license 7 | */ 8 | (function (factory) { 9 | if (typeof define === 'function' && define.amd) { 10 | // AMD 11 | define(['jquery'], factory); 12 | } else if (typeof exports === 'object') { 13 | // CommonJS 14 | factory(require('jquery')); 15 | } else { 16 | // Browser globals 17 | factory(jQuery); 18 | } 19 | }(function ($) { 20 | 21 | var pluses = /\+/g; 22 | 23 | function encode(s) { 24 | return config.raw ? s : encodeURIComponent(s); 25 | } 26 | 27 | function decode(s) { 28 | return config.raw ? s : decodeURIComponent(s); 29 | } 30 | 31 | function stringifyCookieValue(value) { 32 | return encode(config.json ? JSON.stringify(value) : String(value)); 33 | } 34 | 35 | function parseCookieValue(s) { 36 | if (s.indexOf('"') === 0) { 37 | // This is a quoted cookie as according to RFC2068, unescape... 38 | s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); 39 | } 40 | 41 | try { 42 | // Replace server-side written pluses with spaces. 43 | // If we can't decode the cookie, ignore it, it's unusable. 44 | // If we can't parse the cookie, ignore it, it's unusable. 45 | s = decodeURIComponent(s.replace(pluses, ' ')); 46 | return config.json ? JSON.parse(s) : s; 47 | } catch(e) {} 48 | } 49 | 50 | function read(s, converter) { 51 | var value = config.raw ? s : parseCookieValue(s); 52 | return $.isFunction(converter) ? converter(value) : value; 53 | } 54 | 55 | var config = $.cookie = function (key, value, options) { 56 | 57 | // Write 58 | 59 | if (value !== undefined && !$.isFunction(value)) { 60 | options = $.extend({}, config.defaults, options); 61 | 62 | if (typeof options.expires === 'number') { 63 | var days = options.expires, t = options.expires = new Date(); 64 | t.setTime(+t + days * 864e+5); 65 | } 66 | 67 | return (document.cookie = [ 68 | encode(key), '=', stringifyCookieValue(value), 69 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 70 | options.path ? '; path=' + options.path : '', 71 | options.domain ? '; domain=' + options.domain : '', 72 | options.secure ? '; secure' : '' 73 | ].join('')); 74 | } 75 | 76 | // Read 77 | 78 | var result = key ? undefined : {}; 79 | 80 | // To prevent the for loop in the first place assign an empty array 81 | // in case there are no cookies at all. Also prevents odd result when 82 | // calling $.cookie(). 83 | var cookies = document.cookie ? document.cookie.split('; ') : []; 84 | 85 | for (var i = 0, l = cookies.length; i < l; i++) { 86 | var parts = cookies[i].split('='); 87 | var name = decode(parts.shift()); 88 | var cookie = parts.join('='); 89 | 90 | if (key && key === name) { 91 | // If second argument (value) is a function it's a converter... 92 | result = read(cookie, value); 93 | break; 94 | } 95 | 96 | // Prevent storing a cookie that we couldn't decode. 97 | if (!key && (cookie = read(cookie)) !== undefined) { 98 | result[name] = cookie; 99 | } 100 | } 101 | 102 | return result; 103 | }; 104 | 105 | config.defaults = {}; 106 | 107 | $.removeCookie = function (key, options) { 108 | if ($.cookie(key) === undefined) { 109 | return false; 110 | } 111 | 112 | // Must not alter options, thus extending a fresh object... 113 | $.cookie(key, '', $.extend({}, options, { expires: -1 })); 114 | return !$.cookie(key); 115 | }; 116 | 117 | })); 118 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.1.7.RELEASE 9 | 10 | 11 | com.example 12 | demo3 13 | 0.0.1 14 | seckill 15 | Spring Boot Seckill 16 | 17 | 18 | 1.8 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-data-redis 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-jdbc 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-web 33 | 34 | 35 | org.mybatis.spring.boot 36 | mybatis-spring-boot-starter 37 | 2.1.0 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-thymeleaf 42 | 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-devtools 48 | runtime 49 | true 50 | 51 | 52 | mysql 53 | mysql-connector-java 54 | runtime 55 | 56 | 57 | org.projectlombok 58 | lombok 59 | true 60 | 61 | 62 | org.springframework.boot 63 | spring-boot-starter-test 64 | test 65 | 66 | 67 | 68 | 69 | com.alibaba 70 | druid 71 | 1.1.8 72 | 73 | 74 | 75 | org.springframework.boot 76 | spring-boot-starter-data-redis 77 | 78 | 79 | 80 | redis.clients 81 | jedis 82 | 83 | 84 | 85 | com.dyuproject.protostuff 86 | protostuff-core 87 | 1.0.8 88 | 89 | 90 | com.dyuproject.protostuff 91 | protostuff-runtime 92 | 1.0.8 93 | 94 | 95 | 96 | org.apache.commons 97 | commons-collections4 98 | 4.2 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | org.springframework.boot 108 | spring-boot-maven-plugin 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /src/main/resources/templates/seckill_detail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 秒杀详情页 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 |
21 |
22 |
23 | 27 |
28 |
29 |
30 |
31 | 剩余库存: 32 |
33 | 开始时间:[[${#dates.format(seckill.startTime, 'yyyy-MM-dd HH:mm:ss')}]] 34 |
35 |
36 | 结束时间:[[${#dates.format(seckill.endTime, 'yyyy-MM-dd HH:mm:ss')}]] 37 |
38 |
39 |

40 | 41 |
42 |
43 | 44 |

45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | 54 | 55 | 56 | 82 | 83 | 84 |
85 | 86 |
87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 117 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | import java.io.File; 21 | import java.io.FileInputStream; 22 | import java.io.FileOutputStream; 23 | import java.io.IOException; 24 | import java.net.URL; 25 | import java.nio.channels.Channels; 26 | import java.nio.channels.ReadableByteChannel; 27 | import java.util.Properties; 28 | 29 | public class MavenWrapperDownloader { 30 | 31 | /** 32 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 33 | */ 34 | private static final String DEFAULT_DOWNLOAD_URL = 35 | "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; 36 | 37 | /** 38 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 39 | * use instead of the default one. 40 | */ 41 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 42 | ".mvn/wrapper/maven-wrapper.properties"; 43 | 44 | /** 45 | * Path where the maven-wrapper.jar will be saved to. 46 | */ 47 | private static final String MAVEN_WRAPPER_JAR_PATH = 48 | ".mvn/wrapper/maven-wrapper.jar"; 49 | 50 | /** 51 | * Name of the property which should be used to override the default download url for the wrapper. 52 | */ 53 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 54 | 55 | public static void main(String args[]) { 56 | System.out.println("- Downloader started"); 57 | File baseDirectory = new File(args[0]); 58 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 59 | 60 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 61 | // wrapperUrl parameter. 62 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 63 | String url = DEFAULT_DOWNLOAD_URL; 64 | if (mavenWrapperPropertyFile.exists()) { 65 | FileInputStream mavenWrapperPropertyFileInputStream = null; 66 | try { 67 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 68 | Properties mavenWrapperProperties = new Properties(); 69 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 70 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 71 | } catch (IOException e) { 72 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 73 | } finally { 74 | try { 75 | if (mavenWrapperPropertyFileInputStream != null) { 76 | mavenWrapperPropertyFileInputStream.close(); 77 | } 78 | } catch (IOException e) { 79 | // Ignore ... 80 | } 81 | } 82 | } 83 | System.out.println("- Downloading from: : " + url); 84 | 85 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 86 | if (!outputFile.getParentFile().exists()) { 87 | if (!outputFile.getParentFile().mkdirs()) { 88 | System.out.println( 89 | "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 90 | } 91 | } 92 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 93 | try { 94 | downloadFileFromURL(url, outputFile); 95 | System.out.println("Done"); 96 | System.exit(0); 97 | } catch (Throwable e) { 98 | System.out.println("- Error downloading"); 99 | e.printStackTrace(); 100 | System.exit(1); 101 | } 102 | } 103 | 104 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 105 | URL website = new URL(urlString); 106 | ReadableByteChannel rbc; 107 | rbc = Channels.newChannel(website.openStream()); 108 | FileOutputStream fos = new FileOutputStream(destination); 109 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 110 | fos.close(); 111 | rbc.close(); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo3/controller/SeckillController.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3.controller; 2 | 3 | import com.example.demo3.bean.Seckill; 4 | import com.example.demo3.dto.Exposer; 5 | import com.example.demo3.dto.SeckillExecution; 6 | import com.example.demo3.dto.SeckillResult; 7 | import com.example.demo3.enums.SeckillStatEnum; 8 | import com.example.demo3.exception.RepeatKillException; 9 | import com.example.demo3.exception.SeckillCloseException; 10 | import com.example.demo3.exception.SeckillException; 11 | import com.example.demo3.service.SeckillService; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.stereotype.Controller; 16 | import org.springframework.ui.Model; 17 | import org.springframework.web.bind.annotation.*; 18 | 19 | import java.util.Date; 20 | import java.util.List; 21 | 22 | /** 23 | * @author : dk 24 | * @date : 2019/8/9 10:55 25 | * @description : 秒杀的相关控制器 26 | */ 27 | 28 | @Controller 29 | @RequestMapping("/seckill") 30 | public class SeckillController { 31 | 32 | @Autowired 33 | private SeckillService seckillService; 34 | 35 | private final Logger logger = LoggerFactory.getLogger(this.getClass()); 36 | 37 | 38 | @RequestMapping(value = "/list" ,method = RequestMethod.GET) 39 | public String list(Model model) 40 | { 41 | List list = seckillService.findAll(); 42 | model.addAttribute("list", list); 43 | return "seckill"; 44 | } 45 | 46 | @RequestMapping(value = "/{seckillId}/detail",method = RequestMethod.GET) 47 | public String detail(@PathVariable("seckillId") Integer seckillId, Model model) { 48 | if (seckillId == null) { 49 | return "seckill"; 50 | } 51 | Seckill seckill = seckillService.getById(seckillId); 52 | if (seckill == null) { 53 | return "seckill"; 54 | } 55 | model.addAttribute("seckill", seckill); 56 | return "seckill_detail"; 57 | } 58 | 59 | @ResponseBody 60 | @RequestMapping(value = "/{seckillId}/exposer", 61 | method = RequestMethod.POST, 62 | produces = {"application/json;charset=UTF-8"} 63 | ) 64 | public SeckillResult exposer(@PathVariable("seckillId") Integer seckillId) { 65 | SeckillResult result; 66 | try { 67 | Exposer exposer = seckillService.exportSeckillUrl(seckillId); 68 | result = new SeckillResult(true, exposer); 69 | } catch (Exception e) { 70 | System.out.println(e.getMessage()); 71 | logger.error(e.getMessage(), e); 72 | result = new SeckillResult(false, e.getMessage()); 73 | } 74 | return result; 75 | } 76 | 77 | @RequestMapping(value = "/{seckillId}/{md5}/execution", 78 | method = RequestMethod.POST, 79 | produces = {"application/json;charset=UTF-8"}) 80 | @ResponseBody 81 | public SeckillResult execute(@PathVariable("seckillId") Integer seckillId, 82 | @PathVariable("md5") String md5, 83 | @CookieValue(value = "killPhone", required = false) String userPhone) { 84 | //System.out.println("-------------"+userPhone); 85 | if (userPhone == null) { 86 | return new SeckillResult(false, "未注册"); 87 | } 88 | try { 89 | SeckillExecution seckillExecution = seckillService.executeSeckillProducedure(seckillId,userPhone, md5); 90 | // SeckillExecution seckillExecution = seckillService.executeSeckill(seckillId,userPhone, md5); 91 | System.out.println("seckillExecution"+seckillExecution); 92 | return new SeckillResult(true, seckillExecution); 93 | } catch (RepeatKillException e) { 94 | SeckillExecution seckillExecution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL); 95 | System.out.println("seckillExecution"+seckillExecution); 96 | return new SeckillResult(true, seckillExecution); 97 | } catch (SeckillCloseException e) { 98 | SeckillExecution seckillExecution = new SeckillExecution(seckillId, SeckillStatEnum.END); 99 | System.out.println("seckillExecution"+seckillExecution); 100 | return new SeckillResult(true, seckillExecution); 101 | } catch (SeckillException e) { 102 | SeckillExecution seckillExecution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR); 103 | System.out.println("seckillExecution"+seckillExecution); 104 | return new SeckillResult(true, seckillExecution); 105 | } 106 | } 107 | 108 | /** 109 | * 获取系统时间 110 | * @return 111 | */ 112 | @ResponseBody 113 | @GetMapping(value = "/time/now") 114 | public SeckillResult time() { 115 | Date now = new Date(); 116 | return new SeckillResult(true, now.getTime()); 117 | } 118 | 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/main/resources/static/js/seckill_detail.js: -------------------------------------------------------------------------------- 1 | // JavaScript模块化 2 | var seckill = { 3 | //封装秒杀相关的ajax的url地址 4 | URL: { 5 | now: function () { 6 | return '/seckill/time/now'; 7 | }, 8 | exposer: function(seckillId){ 9 | return '/seckill/' + seckillId + '/exposer'; 10 | }, 11 | execution : function(seckillId, md5){ 12 | return '/seckill/' + seckillId + '/' + md5 + '/execution'; 13 | } 14 | }, 15 | //验证手机号 16 | validatePhone: function (phone) { 17 | if (phone && phone.length == 11 && !isNaN(phone)) { 18 | return true; 19 | } else { 20 | return false; 21 | } 22 | }, 23 | //处理秒杀逻辑 24 | handleSeckill: function(seckillId, node){ 25 | //获取秒杀地址,控制显示逻辑,执行秒杀 26 | node.hide().html(''); 27 | $.post(seckill.URL.exposer(seckillId), {}, function(result){ 28 | //在回调函数中执行交互流程 29 | if (result && result['success']){ 30 | var exposer = result['data']; 31 | if (exposer['exposed']){ 32 | //开启秒杀 33 | var md5 = exposer['md5']; 34 | var killUrl = seckill.URL.execution(seckillId, md5); 35 | console.log('killUrl:' + killUrl); 36 | console.log('exposer:' + exposer); 37 | //one: 绑定一次点击事件 38 | $('#killBtn').one('click', function(){ 39 | //执行秒杀的操作 40 | //1. 先禁用按钮 41 | $(this).addClass('disabled'); 42 | //2. 发送秒杀请求,执行秒杀 43 | $.post(killUrl,function(result){ 44 | if (result && result['success']){ 45 | console.log(' if (result && result'); 46 | var killResult = result['data']; 47 | var stateInfo = killResult['stateInfo']; 48 | //3. 显示秒杀结果 49 | node.html('' + stateInfo + ''); 50 | } 51 | }) 52 | }); 53 | node.show(); 54 | } else{ 55 | //未开启秒杀,避免用户得到的时间有偏差 56 | var now = exposer['now']; 57 | var start = exposer['start']; 58 | var end = exposer['end']; 59 | seckill.countdown(seckillId, now, start, end); 60 | } 61 | } else{ 62 | console.log('result:' + result); 63 | } 64 | }); 65 | }, 66 | //计时 67 | countdown: function (seckillId, nowTime, startTime, endTime) { 68 | var seckillBox = $('#seckill-box'); 69 | var seckillTimeSpan = $('#seckill-time-span'); 70 | //时间判断 71 | if (nowTime > endTime){ 72 | //秒杀结束 73 | seckillTimeSpan.html('秒杀结束'); 74 | seckillBox.hide(); 75 | }else if(nowTime < startTime){ 76 | //说明秒杀未开始,计时事件绑定 77 | var killTime = new Date(startTime + 1000); 78 | seckillTimeSpan.countdown(killTime, function(event){ 79 | //时间格式 80 | var format = event.strftime('秒杀开始倒计时:%D天 %H时 %M分 %S秒'); 81 | seckillTimeSpan.html(format); 82 | //时间完成后回调事件 83 | }).on('finish.countdown', function(){ 84 | //获取秒杀地址,控制实现逻辑,执行秒杀 85 | seckill.handleSeckill(seckillId, seckillBox ); 86 | }); 87 | }else{ 88 | //秒杀开始 89 | seckill.handleSeckill(seckillId, seckillBox); 90 | //计时 91 | var killEndTime = new Date(endTime + 1000); 92 | seckillTimeSpan.countdown(killEndTime, function(event){ 93 | //时间格式 94 | var format = event.strftime('距离秒杀结束: %D天 %H时 %M分 %S秒'); 95 | seckillTimeSpan.html(format); 96 | }); 97 | } 98 | }, 99 | //详情页秒杀逻辑 100 | detail: { 101 | //详情页初始化 102 | init: function (params) { 103 | //1、进行手机验证 104 | //在cookie中查询用户 105 | var killPhone = $.cookie('killPhone'); 106 | //验证手机号 107 | if (!seckill.validatePhone(killPhone)) { 108 | //绑定phone 109 | var killPhoneModal = $('#killPhoneModal'); 110 | //控制输出 111 | killPhoneModal.modal({ 112 | show: true, 113 | backdrop: 'static', //禁止位置关闭 114 | keyboard: false //关闭键盘事件 115 | }); 116 | $("#killPhoneBtn").click(function () { 117 | var inputPhone = $('#killPhoneKey').val(); 118 | 119 | if (seckill.validatePhone(inputPhone)) { 120 | //将手机号写入cookie 121 | $.cookie('killPhone', inputPhone, {expires: 7, path: '/seckill/'}); 122 | //刷新页面 123 | window.location.reload(); 124 | } else { 125 | $("#killPhoneMessage").hide().html('手机号错误!').show(300); 126 | } 127 | }); 128 | } 129 | //已经登录 130 | //计时交互 131 | var startTime = params['startTime']; 132 | var endTime = params['endTime']; 133 | var seckillId = params['seckillId']; 134 | 135 | $.get(seckill.URL.now(), {}, function (result) { 136 | if (result && result['success']) { 137 | var nowTime = result['data']; 138 | //时间判断 139 | seckill.countdown(seckillId, nowTime, startTime, endTime); 140 | } else { 141 | console.log('result:' + result); 142 | } 143 | }); 144 | } 145 | } 146 | }; -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" 124 | FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( 125 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | echo Found %WRAPPER_JAR% 132 | ) else ( 133 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 134 | echo Downloading from: %DOWNLOAD_URL% 135 | powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" 136 | echo Finished downloading %WRAPPER_JAR% 137 | ) 138 | @REM End of extension 139 | 140 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 141 | if ERRORLEVEL 1 goto error 142 | goto end 143 | 144 | :error 145 | set ERROR_CODE=1 146 | 147 | :end 148 | @endlocal & set ERROR_CODE=%ERROR_CODE% 149 | 150 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 151 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 152 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 153 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 154 | :skipRcPost 155 | 156 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 157 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 158 | 159 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 160 | 161 | exit /B %ERROR_CODE% 162 | -------------------------------------------------------------------------------- /src/test/java/com/example/demo3/Demo3ApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3; 2 | 3 | import com.example.demo3.bean.Seckill; 4 | import com.example.demo3.bean.SuccessKilled; 5 | import com.example.demo3.dto.Exposer; 6 | import com.example.demo3.dto.SeckillExecution; 7 | import com.example.demo3.exception.RepeatKillException; 8 | import com.example.demo3.exception.SeckillCloseException; 9 | import com.example.demo3.mapper.SeckillMapper; 10 | import com.example.demo3.mapper.SuccessKilledMapper; 11 | import com.example.demo3.redis.RedisDao; 12 | import com.example.demo3.service.SeckillService; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.test.context.SpringBootTest; 17 | import org.springframework.test.context.junit4.SpringRunner; 18 | 19 | import java.util.Calendar; 20 | import java.util.Date; 21 | import java.util.List; 22 | 23 | @RunWith(SpringRunner.class) 24 | @SpringBootTest 25 | public class Demo3ApplicationTests { 26 | 27 | 28 | @Autowired 29 | SeckillMapper seckillMapper; 30 | 31 | @Test 32 | public void reduceNumber() { 33 | Date date = new Date(); 34 | int up = seckillMapper.reduceNumber(1,date); 35 | System.out.println(up); 36 | } 37 | 38 | @Test 39 | public void queryById() { 40 | seckillMapper.queryById(1); 41 | System.out.println(1); 42 | } 43 | @Test 44 | public void queryAll() { 45 | List seckills = seckillMapper.queryAll(1,10); 46 | for (int i = 0; i seckills = seckillService.getSeckillList(); 89 | // for (int i = 0; i < seckills.size(); i++) { 90 | // System.out.println(seckills.get(i).toString()); 91 | // } 92 | // } 93 | 94 | @Test 95 | public void getById() { 96 | Seckill seckill = seckillService.getById(1); 97 | System.out.println(seckill.toString()); 98 | } 99 | 100 | @Test 101 | public void exportSeckillUrl() { 102 | Exposer exposer = seckillService.exportSeckillUrl(1); 103 | //测试时 要注意数据库中的时间 104 | // INSERT INTO `spring`.`seckill` 105 | // (`seckill_id`, `name`, `number`, `start_time`, `end_time`, `create_time`) 106 | // VALUES 107 | // ('1', '100秒杀ipad', '100', '2019-08-07 16:27:13', '2019-08-10 16:27:16', '2019-08-06 16:27:20'); 108 | 109 | System.out.println(exposer.toString()); 110 | // Exposer{exposed=true, md5='05fd17ce7b3fb01e5c9fb08e4f7004c8', seckillId=1, now=0, start=0, end=0} 111 | } 112 | 113 | @Test 114 | public void executeSeckill() { 115 | String md5 = "05fd17ce7b3222fb01e5c9fb08e4f7004c8"; 116 | SeckillExecution seckillExecution = seckillService.executeSeckill(1,"15256466666",md5); 117 | System.out.println(seckillExecution.toString()); 118 | //再次使用相同的手机号去秒杀的时候会报错 com.example.demo3.exception.RepeatKillException: seckill repeated 119 | } 120 | 121 | 122 | 123 | @Test 124 | public void testSeckillLogic() throws Exception { 125 | Exposer exposer = seckillService.exportSeckillUrl(1); 126 | if (exposer.isExposed()) { 127 | Integer id = exposer.getSeckillId(); 128 | String md5 = exposer.getMd5(); 129 | try { 130 | SeckillExecution seckillExecution = seckillService.executeSeckill(2,"1221111222", md5); 131 | System.out.println("秒杀开启"); 132 | } catch (SeckillCloseException e) { 133 | System.out.println(e.getMessage()); 134 | } catch (RepeatKillException e1) { 135 | System.out.println(e1.getMessage()); 136 | } 137 | } else { 138 | //秒杀未开启 139 | System.out.println("秒杀未开启"); 140 | } 141 | } 142 | 143 | @Autowired 144 | RedisDao redisDao; 145 | 146 | private Integer id =1; 147 | 148 | @Test 149 | public void Seckill() { 150 | // get and put 151 | Seckill seckill = redisDao.getSeckill(id); 152 | if (seckill == null) 153 | { 154 | seckill = seckillMapper.queryById(id); 155 | if(seckill != null) 156 | { 157 | String result = redisDao.putSeckill(seckill); 158 | System.out.println(result); 159 | seckill = redisDao.getSeckill(id); 160 | System.out.println(seckill); 161 | } 162 | } 163 | } 164 | 165 | @Test 166 | public void executeSeckillProducedure() { 167 | int seckillId = 1; 168 | String phone = "15596520256" ; 169 | Exposer exposer = seckillService.exportSeckillUrl(seckillId); 170 | if (exposer.isExposed()) 171 | { 172 | String md5 = exposer.getMd5(); 173 | SeckillExecution seckillExecution = seckillService.executeSeckillProducedure(seckillId, phone, md5); 174 | System.out.println(seckillExecution.getStateInfo()); 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo3/service/impl/SeckillServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.example.demo3.service.impl; 2 | 3 | import com.example.demo3.bean.Seckill; 4 | import com.example.demo3.bean.SuccessKilled; 5 | import com.example.demo3.dto.Exposer; 6 | import com.example.demo3.dto.SeckillExecution; 7 | import com.example.demo3.enums.SeckillStatEnum; 8 | import com.example.demo3.exception.RepeatKillException; 9 | import com.example.demo3.exception.SeckillCloseException; 10 | import com.example.demo3.exception.SeckillException; 11 | import com.example.demo3.mapper.SeckillMapper; 12 | import com.example.demo3.mapper.SuccessKilledMapper; 13 | import com.example.demo3.redis.RedisDao; 14 | import com.example.demo3.service.SeckillService; 15 | import org.apache.commons.collections4.MapUtils; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.stereotype.Service; 20 | import org.springframework.transaction.annotation.Transactional; 21 | import org.springframework.util.DigestUtils; 22 | 23 | import java.util.*; 24 | 25 | import static com.example.demo3.enums.SeckillStatEnum.SUCCESS; 26 | 27 | /** 28 | * @author : dk 29 | * @date : 2019/8/8 16:28 30 | * @description : 31 | */ 32 | @Service 33 | public class SeckillServiceImpl implements SeckillService { 34 | 35 | private Logger logger = LoggerFactory.getLogger(this.getClass()); 36 | 37 | //设置盐值字符串,随便定义,用于混淆MD5值 38 | private final String salt = "sjajahjgnm00982jrfm;sd"; 39 | 40 | //生成MD5值 41 | private String getMD5(Integer seckillId) { 42 | String base = seckillId + "/" + salt; 43 | String md5 = DigestUtils.md5DigestAsHex(base.getBytes()); 44 | //生成md5 45 | return md5; 46 | } 47 | 48 | 49 | @Autowired 50 | SeckillMapper seckillMapper; 51 | @Autowired 52 | SuccessKilledMapper successKilledMapper; 53 | 54 | @Autowired 55 | RedisDao redisDao; 56 | 57 | @Override 58 | public List findAll() { 59 | return seckillMapper.findAll(); 60 | } 61 | 62 | @Override 63 | public Seckill getById(Integer seckillId) { 64 | return seckillMapper.queryById(seckillId); 65 | } 66 | 67 | @Override 68 | //优化暴露接口 69 | public Exposer exportSeckillUrl(Integer seckillId) { 70 | //优化点 : 缓存优化 超时的基础上维护统一性 71 | // 1 访问 redis 72 | Seckill seckill = redisDao.getSeckill(seckillId); 73 | if(seckill == null) 74 | { 75 | //2 访问数据库 76 | seckill = seckillMapper.queryById(seckillId); 77 | if (seckill == null) 78 | { 79 | return new Exposer(false,seckillId); 80 | } 81 | else 82 | { 83 | // 3 放入 redis 84 | redisDao.putSeckill(seckill); 85 | } 86 | } 87 | Date startTieme = seckill.getStartTime(); 88 | Date endTime = seckill.getEndTime(); 89 | Date nowTime = new Date(); 90 | if(nowTime.getTime() < startTieme.getTime() 91 | || nowTime.getTime() >endTime.getTime()) 92 | //第一个判断条件是 秒杀未开始 第二个是秒杀已结束 93 | { 94 | return new Exposer(false,seckillId,nowTime.getTime(),startTieme.getTime(),endTime.getTime()); 95 | } 96 | String md5 = getMD5(seckillId); 97 | return new Exposer(true,md5,seckillId); 98 | } 99 | /** 100 | * 使用注解式事务方法的有优点:开发团队达成了一致约定,明确标注事务方法的编程风格 101 | * 使用事务控制需要注意: 102 | * 1.保证事务方法的执行时间尽可能短,不要穿插其他网络操作PRC/HTTP请求(可以将这些请求剥离出来) 103 | * 2.不是所有的方法都需要事务控制,如只有一条修改的操作、只读操作等是不需要进行事务控制的 104 | * 105 | * Spring默认只对运行期异常进行事务的回滚操作,对于编译异常Spring是不进行回滚的,所以对于需要进行事务控制的方法尽可能将可能抛出的异常都转换成运行期异常 106 | */ 107 | @Override 108 | @Transactional 109 | public SeckillExecution executeSeckill(Integer seckillId, String userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException { 110 | if (md5 == null || !md5.equals(getMD5(seckillId))) { 111 | throw new SeckillException("seckill data rewrite"); 112 | } 113 | System.out.println("d"); 114 | //执行秒杀逻辑:1.减库存;2.储存秒杀订单 115 | Date nowTime = new Date(); 116 | 117 | 118 | try { 119 | int insertCount = successKilledMapper.insertSuccessKilled(seckillId, userPhone,nowTime); 120 | if (insertCount <= 0) { 121 | //重复秒杀 122 | // throw new RepeatKillException("seckill repeated"); 123 | return new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL,"重复秒杀"); 124 | } else { 125 | // 减库存、热点商品的竞争 126 | int updateCount = seckillMapper.reduceNumber(seckillId, nowTime); 127 | if (updateCount <= 0) { 128 | //没有更新记录,秒杀结束,rollback 129 | throw new SeckillCloseException("seckill is closed"); 130 | } else { 131 | //秒杀成功 commit 132 | SuccessKilled successKilled = successKilledMapper.queryByIdWithSeckill(seckillId, userPhone); 133 | return new SeckillExecution(seckillId, SUCCESS,"秒杀成功",successKilled); 134 | } 135 | } 136 | } catch (SeckillCloseException e) { 137 | throw e; 138 | } catch (RepeatKillException e) { 139 | throw e; 140 | } catch (Exception e) { 141 | logger.error(e.getMessage(), e); 142 | //所有编译期异常,转换为运行期异常 143 | throw new SeckillException("seckill inner error:" + e.getMessage()); 144 | } 145 | } 146 | 147 | @Override 148 | public SeckillExecution executeSeckillProducedure(Integer seckillId, String userPhone, String md5) { 149 | if (md5 == null || !md5.equals(getMD5(seckillId))) 150 | { 151 | return new SeckillExecution(seckillId,SeckillStatEnum.DATA_REWRITE,"数据串改"); 152 | } 153 | // Date killTime = new Date(); 154 | Date killTime = Calendar.getInstance().getTime(); 155 | Map map = new HashMap<>(); 156 | map.put("seckillId",seckillId); 157 | map.put(("phone"),userPhone); 158 | map.put("killTime",killTime); 159 | map.put("result",null); 160 | // 执行存储过程,result 被赋值 161 | try { 162 | seckillMapper.killByProcedure(map); 163 | int result = MapUtils.getInteger(map,"result",-2); 164 | if (result == 1) 165 | { 166 | SuccessKilled successKilled = successKilledMapper.queryByIdWithSeckill(seckillId,userPhone); 167 | return new SeckillExecution(seckillId, SUCCESS,"秒杀成功",successKilled); 168 | } 169 | else 170 | { 171 | return new SeckillExecution(seckillId,SeckillStatEnum.stateOf(result),SeckillStatEnum.stateOf(result).getStateInfo()); 172 | } 173 | }catch (Exception e){ 174 | logger.error(e.getMessage(),e); 175 | return new SeckillExecution(seckillId,SeckillStatEnum.INNER_ERROR,"系统错误"); 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | ########################################################################################## 204 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 205 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 206 | ########################################################################################## 207 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 208 | if [ "$MVNW_VERBOSE" = true ]; then 209 | echo "Found .mvn/wrapper/maven-wrapper.jar" 210 | fi 211 | else 212 | if [ "$MVNW_VERBOSE" = true ]; then 213 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 214 | fi 215 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" 216 | while IFS="=" read key value; do 217 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 218 | esac 219 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 220 | if [ "$MVNW_VERBOSE" = true ]; then 221 | echo "Downloading from: $jarUrl" 222 | fi 223 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 224 | 225 | if command -v wget > /dev/null; then 226 | if [ "$MVNW_VERBOSE" = true ]; then 227 | echo "Found wget ... using wget" 228 | fi 229 | wget "$jarUrl" -O "$wrapperJarPath" 230 | elif command -v curl > /dev/null; then 231 | if [ "$MVNW_VERBOSE" = true ]; then 232 | echo "Found curl ... using curl" 233 | fi 234 | curl -o "$wrapperJarPath" "$jarUrl" 235 | else 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Falling back to using Java to download" 238 | fi 239 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 240 | if [ -e "$javaClass" ]; then 241 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 242 | if [ "$MVNW_VERBOSE" = true ]; then 243 | echo " - Compiling MavenWrapperDownloader.java ..." 244 | fi 245 | # Compiling the Java class 246 | ("$JAVA_HOME/bin/javac" "$javaClass") 247 | fi 248 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 249 | # Running the downloader 250 | if [ "$MVNW_VERBOSE" = true ]; then 251 | echo " - Running MavenWrapperDownloader.java ..." 252 | fi 253 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 254 | fi 255 | fi 256 | fi 257 | fi 258 | ########################################################################################## 259 | # End of extension 260 | ########################################################################################## 261 | 262 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 263 | if [ "$MVNW_VERBOSE" = true ]; then 264 | echo $MAVEN_PROJECTBASEDIR 265 | fi 266 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 267 | 268 | # For Cygwin, switch paths to Windows format before running java 269 | if $cygwin; then 270 | [ -n "$M2_HOME" ] && 271 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 272 | [ -n "$JAVA_HOME" ] && 273 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 274 | [ -n "$CLASSPATH" ] && 275 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 276 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 277 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 278 | fi 279 | 280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 281 | 282 | exec "$JAVACMD" \ 283 | $MAVEN_OPTS \ 284 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 285 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 286 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 287 | -------------------------------------------------------------------------------- /src/main/resources/static/lib/countdown.js: -------------------------------------------------------------------------------- 1 | // AMD support (Thanks to @FagnerMartinsBrack) 2 | ;(function(factory) { 3 | 'use strict'; 4 | 5 | if (typeof define === 'function' && define.amd) { 6 | define(['jquery'], factory); 7 | } else { 8 | factory(jQuery); 9 | } 10 | })(function($){ 11 | 'use strict'; 12 | 13 | var instances = [], 14 | matchers = [], 15 | defaultOptions = { 16 | precision: 100, // 0.1 seconds, used to update the DOM 17 | elapse: false, 18 | defer: false 19 | }; 20 | // Miliseconds 21 | matchers.push(/^[0-9]*$/.source); 22 | // Month/Day/Year [hours:minutes:seconds] 23 | matchers.push(/([0-9]{1,2}\/){2}[0-9]{4}( [0-9]{1,2}(:[0-9]{2}){2})?/ 24 | .source); 25 | // Year/Day/Month [hours:minutes:seconds] and 26 | // Year-Day-Month [hours:minutes:seconds] 27 | matchers.push(/[0-9]{4}([\/\-][0-9]{1,2}){2}( [0-9]{1,2}(:[0-9]{2}){2})?/ 28 | .source); 29 | // Cast the matchers to a regular expression object 30 | matchers = new RegExp(matchers.join('|')); 31 | // Parse a Date formatted has String to a native object 32 | function parseDateString(dateString) { 33 | // Pass through when a native object is sent 34 | if(dateString instanceof Date) { 35 | return dateString; 36 | } 37 | // Caste string to date object 38 | if(String(dateString).match(matchers)) { 39 | // If looks like a milisecond value cast to number before 40 | // final casting (Thanks to @msigley) 41 | if(String(dateString).match(/^[0-9]*$/)) { 42 | dateString = Number(dateString); 43 | } 44 | // Replace dashes to slashes 45 | if(String(dateString).match(/\-/)) { 46 | dateString = String(dateString).replace(/\-/g, '/'); 47 | } 48 | return new Date(dateString); 49 | } else { 50 | throw new Error('Couldn\'t cast `' + dateString + 51 | '` to a date object.'); 52 | } 53 | } 54 | // Map to convert from a directive to offset object property 55 | var DIRECTIVE_KEY_MAP = { 56 | 'Y': 'years', 57 | 'm': 'months', 58 | 'n': 'daysToMonth', 59 | 'd': 'daysToWeek', 60 | 'w': 'weeks', 61 | 'W': 'weeksToMonth', 62 | 'H': 'hours', 63 | 'M': 'minutes', 64 | 'S': 'seconds', 65 | 'D': 'totalDays', 66 | 'I': 'totalHours', 67 | 'N': 'totalMinutes', 68 | 'T': 'totalSeconds' 69 | }; 70 | // Returns an escaped regexp from the string 71 | function escapedRegExp(str) { 72 | var sanitize = str.toString().replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); 73 | return new RegExp(sanitize); 74 | } 75 | // Time string formatter 76 | function strftime(offsetObject) { 77 | return function(format) { 78 | var directives = format.match(/%(-|!)?[A-Z]{1}(:[^;]+;)?/gi); 79 | if(directives) { 80 | for(var i = 0, len = directives.length; i < len; ++i) { 81 | var directive = directives[i] 82 | .match(/%(-|!)?([a-zA-Z]{1})(:[^;]+;)?/), 83 | regexp = escapedRegExp(directive[0]), 84 | modifier = directive[1] || '', 85 | plural = directive[3] || '', 86 | value = null; 87 | // Get the key 88 | directive = directive[2]; 89 | // Swap shot-versions directives 90 | if(DIRECTIVE_KEY_MAP.hasOwnProperty(directive)) { 91 | value = DIRECTIVE_KEY_MAP[directive]; 92 | value = Number(offsetObject[value]); 93 | } 94 | if(value !== null) { 95 | // Pluralize 96 | if(modifier === '!') { 97 | value = pluralize(plural, value); 98 | } 99 | // Add zero-padding 100 | if(modifier === '') { 101 | if(value < 10) { 102 | value = '0' + value.toString(); 103 | } 104 | } 105 | // Replace the directive 106 | format = format.replace(regexp, value.toString()); 107 | } 108 | } 109 | } 110 | format = format.replace(/%%/, '%'); 111 | return format; 112 | }; 113 | } 114 | // Pluralize 115 | function pluralize(format, count) { 116 | var plural = 's', singular = ''; 117 | if(format) { 118 | format = format.replace(/(:|;|\s)/gi, '').split(/\,/); 119 | if(format.length === 1) { 120 | plural = format[0]; 121 | } else { 122 | singular = format[0]; 123 | plural = format[1]; 124 | } 125 | } 126 | // Fix #187 127 | if(Math.abs(count) > 1) { 128 | return plural; 129 | } else { 130 | return singular; 131 | } 132 | } 133 | // The Final Countdown 134 | var Countdown = function(el, finalDate, options) { 135 | this.el = el; 136 | this.$el = $(el); 137 | this.interval = null; 138 | this.offset = {}; 139 | this.options = $.extend({}, defaultOptions); 140 | // console.log(this.options); 141 | // This helper variable is necessary to mimick the previous check for an 142 | // event listener on this.$el. Because of the event loop there might not 143 | // be a registered event listener during the first tick. In order to work 144 | // as expected a second tick is necessary, so that the events can be fired 145 | // and handled properly. 146 | this.firstTick = true; 147 | // Register this instance 148 | this.instanceNumber = instances.length; 149 | instances.push(this); 150 | // Save the reference 151 | this.$el.data('countdown-instance', this.instanceNumber); 152 | // Handle options or callback 153 | if (options) { 154 | // Register the callbacks when supplied 155 | if(typeof options === 'function') { 156 | this.$el.on('update.countdown', options); 157 | this.$el.on('stoped.countdown', options); 158 | this.$el.on('finish.countdown', options); 159 | } else { 160 | this.options = $.extend({}, defaultOptions, options); 161 | } 162 | } 163 | // Set the final date and start 164 | this.setFinalDate(finalDate); 165 | // Starts the countdown automatically unless it's defered, 166 | // Issue #198 167 | if (this.options.defer === false) { 168 | this.start(); 169 | } 170 | }; 171 | $.extend(Countdown.prototype, { 172 | start: function() { 173 | if(this.interval !== null) { 174 | clearInterval(this.interval); 175 | } 176 | var self = this; 177 | this.update(); 178 | this.interval = setInterval(function() { 179 | self.update.call(self); 180 | }, this.options.precision); 181 | }, 182 | stop: function() { 183 | clearInterval(this.interval); 184 | this.interval = null; 185 | this.dispatchEvent('stoped'); 186 | }, 187 | toggle: function() { 188 | if (this.interval) { 189 | this.stop(); 190 | } else { 191 | this.start(); 192 | } 193 | }, 194 | pause: function() { 195 | this.stop(); 196 | }, 197 | resume: function() { 198 | this.start(); 199 | }, 200 | remove: function() { 201 | this.stop.call(this); 202 | instances[this.instanceNumber] = null; 203 | // Reset the countdown instance under data attr (Thanks to @assiotis) 204 | delete this.$el.data().countdownInstance; 205 | }, 206 | setFinalDate: function(value) { 207 | this.finalDate = parseDateString(value); // Cast the given date 208 | }, 209 | update: function() { 210 | // Stop if dom is not in the html (Thanks to @dleavitt) 211 | if(this.$el.closest('html').length === 0) { 212 | this.remove(); 213 | return; 214 | } 215 | var now = new Date(), 216 | newTotalSecsLeft; 217 | // Create an offset date object 218 | newTotalSecsLeft = this.finalDate.getTime() - now.getTime(); // Millisecs 219 | // Calculate the remaining time 220 | newTotalSecsLeft = Math.ceil(newTotalSecsLeft / 1000); // Secs 221 | // If is not have to elapse set the finish 222 | newTotalSecsLeft = !this.options.elapse && newTotalSecsLeft < 0 ? 0 : 223 | Math.abs(newTotalSecsLeft); 224 | // Do not proceed to calculation if the seconds have not changed or 225 | // during the first tick 226 | if (this.totalSecsLeft === newTotalSecsLeft || this.firstTick) { 227 | this.firstTick = false; 228 | return; 229 | } else { 230 | this.totalSecsLeft = newTotalSecsLeft; 231 | } 232 | // Check if the countdown has elapsed 233 | this.elapsed = (now >= this.finalDate); 234 | // Calculate the offsets 235 | this.offset = { 236 | seconds : this.totalSecsLeft % 60, 237 | minutes : Math.floor(this.totalSecsLeft / 60) % 60, 238 | hours : Math.floor(this.totalSecsLeft / 60 / 60) % 24, 239 | days : Math.floor(this.totalSecsLeft / 60 / 60 / 24) % 7, 240 | daysToWeek : Math.floor(this.totalSecsLeft / 60 / 60 / 24) % 7, 241 | daysToMonth : Math.floor(this.totalSecsLeft / 60 / 60 / 24 % 30.4368), 242 | weeks : Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 7), 243 | weeksToMonth: Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 7) % 4, 244 | months : Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 30.4368), 245 | years : Math.abs(this.finalDate.getFullYear()-now.getFullYear()), 246 | totalDays : Math.floor(this.totalSecsLeft / 60 / 60 / 24), 247 | totalHours : Math.floor(this.totalSecsLeft / 60 / 60), 248 | totalMinutes: Math.floor(this.totalSecsLeft / 60), 249 | totalSeconds: this.totalSecsLeft 250 | }; 251 | // Dispatch an event 252 | if(!this.options.elapse && this.totalSecsLeft === 0) { 253 | this.stop(); 254 | this.dispatchEvent('finish'); 255 | } else { 256 | this.dispatchEvent('update'); 257 | } 258 | }, 259 | dispatchEvent: function(eventName) { 260 | var event = $.Event(eventName + '.countdown'); 261 | event.finalDate = this.finalDate; 262 | event.elapsed = this.elapsed; 263 | event.offset = $.extend({}, this.offset); 264 | event.strftime = strftime(this.offset); 265 | this.$el.trigger(event); 266 | } 267 | }); 268 | // Register the jQuery selector actions 269 | $.fn.countdown = function() { 270 | var argumentsArray = Array.prototype.slice.call(arguments, 0); 271 | return this.each(function() { 272 | // If no data was set, jQuery.data returns undefined 273 | var instanceNumber = $(this).data('countdown-instance'); 274 | // Verify if we already have a countdown for this node ... 275 | // Fix issue #22 (Thanks to @romanbsd) 276 | if (instanceNumber !== undefined) { 277 | var instance = instances[instanceNumber], 278 | method = argumentsArray[0]; 279 | // If method exists in the prototype execute 280 | if(Countdown.prototype.hasOwnProperty(method)) { 281 | instance[method].apply(instance, argumentsArray.slice(1)); 282 | // If method look like a date try to set a new final date 283 | } else if(String(method).match(/^[$A-Z_][0-9A-Z_$]*$/i) === null) { 284 | instance.setFinalDate.call(instance, method); 285 | // Allow plugin to restart after finished 286 | // Fix issue #38 (thanks to @yaoazhen) 287 | instance.start(); 288 | } else { 289 | $.error('Method %s does not exist on jQuery.countdown' 290 | .replace(/\%s/gi, method)); 291 | } 292 | } else { 293 | // ... if not we create an instance 294 | new Countdown(this, argumentsArray[0], argumentsArray[1]); 295 | } 296 | }); 297 | }; 298 | }); -------------------------------------------------------------------------------- /src/main/resources/static/lib/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2016 Twitter, Inc. 4 | * Licensed under the MIT license 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>3)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){if(a(b.target).is(this))return b.handleObj.handler.apply(this,arguments)}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.7",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a("#"===f?[]:f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.7",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c).prop(c,!0)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c).prop(c,!1))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target).closest(".btn");b.call(d,"toggle"),a(c.target).is('input[type="radio"], input[type="checkbox"]')||(c.preventDefault(),d.is("input,button")?d.trigger("focus"):d.find("input:visible,button:visible").first().trigger("focus"))}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.7",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));if(!(a>this.$items.length-1||a<0))return this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){if(!this.sliding)return this.slide("next")},c.prototype.prev=function(){if(!this.sliding)return this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.7",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger(a.Event("hidden.bs.dropdown",f)))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.7",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger(a.Event("shown.bs.dropdown",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},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!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);if(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),!c.isInStateTrue())return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null,a.$element=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;!e&&/destroy|hide/.test(b)||(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.7",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.7",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.7",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return e=a-d&&"bottom"},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); --------------------------------------------------------------------------------