├── 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 | 
62 |
63 |
64 |
65 | #### 商品详情页面 (未登录)
66 |
67 | 
68 |
69 | #### 点击秒杀之后
70 |
71 | 
72 |
73 | #### 再次秒杀
74 |
75 | 
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 | |
33 |
34 | |
35 |
36 | 剩余库存:
37 | |
38 |
39 |
40 |
41 | |
42 |
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 | |
50 |
51 |
52 |
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);
--------------------------------------------------------------------------------