1.股票相关概念

【1】什么是股票?

1
2
3
4
股票是股份公司发给股东证明其所入股份的一种有价证券(有效凭证),它可以作为买卖对象和抵押品,是资金市场中主要的信用工具之一;
举例:我和朋友开公司,一人出10万块钱,那怎么证明公司里的20万里有我的10万呢?
最传统的办法就是发行股票,也就是盖章签字的纸质凭证。
每人出了10万,那我们就发行200股,这样每个人就分得100股,股票就是证明你是公司股东且占有公司200股里面100 股的一个凭证。

【2】股票的分类

1
2
3
4
5
6
7
8
9
10
11
12
A股:★★★
即人民币普通股票,是由中国境内注册公司发行,在境内上市,以人民币标明面值,供境内机构、组织或个人(2013年4月1日起,境内港澳台居民可开立A股账户)以人民币认购和交易的普通股股票。
英文字母A没有实际意义,只是用来区分人民币普通股票和人民币特种股票。
A股中的股票分类:
绩优股:绩优股就是业绩优良公司的股票;
垃圾股:与绩优股相对应,垃圾股指的是业绩较差的公司的股票;
蓝筹股:指在其所属行业内占有重要支配性地位业绩优良成交活跃、红利优厚的大公司股票;
##########################################################################################
B股:也称为人民币特种股票。是指那些在中国大陆注册、在中国大陆上市的特种股票。以人民币标明面值,只能以外币认购和交易;部分股票也开放港元交易;
H股:也称为国企股,是指国有企业在香港 (Hong Kong) 上市的股票;
N股:是指那些在中国大陆注册、在纽约(New York)上市的外资股;
SCA股:是指那些主要生产或者经营等核心业务在中国大陆、而企业的注册地在新加坡(Singapore)或者其他国家和地区,但是在新加坡交易所上市挂牌的企业股票;

【3】股票核心参数介绍

image-20211227171630088

​ 开盘价:

1
2
3
4
开盘价:是指证券交易所在每个交易日开市后的第一笔股票买卖成交的价格;
中国股市的开盘价格是由市场集合竞价所产生的。
在A股市场中,股票的开盘价是由集合竞价的9点15分至9点25分买卖双方的撮合,股票价格在有效价格范围内选取成交量最大的价位所产生的,也是证券交易所每个交易日开市后的第一笔每股买卖成交价格。如果集合竞价的时间内股票没有买卖或没有成交,则股票前一日的收盘价作为当日股票的开盘价。
一般状况:开盘参考价=前一买卖日收盘价。

​ 收盘价:

1
2
收盘价又称收市价,是指某只股票在证券交易所每个交易日里的【最后一笔买卖成交价格】;
如果某只股票当日没有成交,则采用最近一次的成交价作为收盘价;

​ 涨跌值:

1
2
3
当前股票的价格与前一天的收盘价比价,来反应股票的实时涨跌情况;
计算公式为:最新价格-前收盘价格
注意:一般在交易台上用“+”或“-”号表示,单位是元,正值为涨,负值为跌,否则为持平;

​ 涨跌幅(涨幅):

1
2
3
涨跌幅是对涨跌值的描绘;
计算公式为:股票涨跌幅计算公式:(最新成交价-前收盘价)÷前收盘价×100%
也就是(涨跌值)÷前收盘价×100%

​ 涨停与跌停:

1
2
3
4
5
6
7
8
涨停与跌停就是一种对股市涨跌幅的限制;
证券交易所为了抑制过度投机行为,防止市场出现过分的暴涨暴跌,强制约定在每天的交易中股票的涨跌幅必须在规定的区间内上下波动。如果股票价格上升到该限制幅度的最高限价为涨停板,而下跌至该限制幅度的最低限度为跌停板。
1、涨停指的是价格涨到一定幅度,价格会被限制,不再变动,但是可以继续交易。
2、跌停指的是价格跌到一定幅度,价格会被限制,不在变动,但是可以继续交易。
3、在中国A股市场,均设有涨幅和跌幅的限制,他们都是【涨跌幅10%】的限制,即所谓的涨停和跌停。
注意事项:
涨停和跌停适用中国所有A股,但是对于第一次上市交易股票当天是没有涨停限制的,第二天才有限制(打新);
股票交易采用T+1机制,也就是当日买进,那么最快第二个交易日才可卖出;

​ 振幅:

1
2
股票振幅是指股票开盘后的当日最高价和最低价之间的差的绝对值与昨日收盘价的百分比,它在一定程度上表现股票的活跃程度。
计算公式为:(当日最高价-当日最低价)÷前收盘价 ×100%

​ 成交量:

1
2
成交量指当天成交的股票总手数(1手=100股);
计算则是由交易所直接进行计算;

​ 成交金额:

1
2
3
4
5
6
股票成交额是指某一特定时期内,成交某种股票的金额,其单位以人民币"元"计算;
成交金额就是每股价格乘交易的手数,再乘上100。
例如:投资者以每股10元的价格买入50手,10X50X100=50000,此时的5万就是成交额。
注意:
成交总金额又称总额,总额是指当天开盘以来总成交金额,单位以人民币"元"计算。
简单的说,这个总金额是反应当日有多少资金进行了成交操作。

股票编码:

1
2
3
4
5
6
7
8
9
10
1、创业板 创业板的代码是300打头的股票代码;
2、沪市A股 沪市A股的代码是以600、601或603打头;★★★
3、沪市B股 沪市B股的代码是以900打头;
4、深市A股 深市A股的代码是以000打头;★★★
5、中小板 中小板的代码是002打头;
6、深圳B股 深圳B股的代码是以200打头;
7、新股申购 沪市新股申购的代码是以730打头 深市新股申购的代码与深市股票买卖代码一样;
8、配股代码 沪市以700打头,深市以080打头 权证,沪市是580打头 深市是031打头。
9、 400开头的股票是三板市场股票。
注意:国内上市公司可选择在上海证券交易所(上证)、深证证券交易所(深证)挂牌上市,中小型企业可选择北交所上市;

K线:

1
2
K线图(Candlestick Charts)又称蜡烛图、日本线、【阴阳线】、棒线等,常用说法是“K线”,起源于日本十八世纪德川幕府时代(1603~1867年)的米市交易,用来计算米价每天的涨跌。
它是以每个交易日(或每个分析周期,甚至是月年)的开盘价、最高价、最低价、和收盘价绘制而成,结构上可分为上影线、下影线及中间实体三部分。

K线图

示例:

image-20220106112807335

小结:

1
2
3
4
5
6
1.什么是股票?
证明股东持有公司份额的有价证券,可以被交易
2.什么是A股?
在中国国内,国内证券交易所上市,且以必须人民币结算,必须国内组织,个人机构购买
3.股票核心参数:开盘价、收盘价、涨跌、涨幅、振幅、涨停|跌停、成交量、成交金额等;
4.K线图:分时K线图、日k线图、周k线图等

2.2大盘与板块概念介绍

【1】什么是大盘指数

1
2
3
1.大盘指数就是大家为了了解上市公司及股票市场的涨跌情况和市场的发展、健康程度,将上市公司所有股票或特定股票,经过专业计算得出的一个指数。然后交易所给它取了个名:上证综合指数和深证成份股指数。
说明:
股市中之所以要用到指数,就是因为股票市场中有太多的股票,我们不可能一次性将所有股票的行情走势都了解清楚,所以想要把握【大盘走势】,我们必须依靠指数!这也是为什么指数被称为股市晴雨表的原因了;

image-20211227162215974

image-20211227171436655

image-20220107211411341

【2】什么是板块指数

1
2
3
4
股票板块是指具有某种特定属性的股票组成的群体,比如:股票根据行业划分为19个板块,涵盖了农业、林业、牧业、渔业、采矿业、制造业、电力、热力、燃气及水生产和供应业、建筑业等行业。
这些股票因为有某一共同特征而被人为地归类在一起,而这一特征往往是被所谓股市庄家用来进行炒作的题材。
注意:
根据定义板块的方式,主要分为:地域板块、行业板块、概念板块等;

image-20211227172525047

2.软件开发流程

image-20210725231106363

需求分析——》设计—-》开发——》测试——-》发布|部署|运维

1). 第1阶段: 需求分析

完成产品原型、需求规格说明书|| 需求说明书的编写。

产品原型:一般是通过网页(html)的形式展示业务功能,。比如包含页面的布局是什么样子的,点击某个菜单,打开什么页面,点击某个按钮,出现什么效果,都可以通过产品原型看到。

需求规格说明书, 一般来说就是使用 Word 文档来描述当前项目有哪些功能,每一项功能的需求及业务流程是什么样的,都会在文档中描述。

2). 第2阶段: 设计

设计的内容包含 产品设计、UI界面设计、概要设计、详细设计、数据库设计。

在设计阶段,会出具相关的UI界面、及相关的设计文档。比如数据库设计,需要设计当前项目中涉及到哪些数据库,每一个数据库里面包含哪些表,这些表结构之间的关系是什么样的,表结构中包含哪些字段,字段类型都会在文档中描述清楚。

在该阶段查出:UI界面(纯静态的html页面)、概要设计文档(涵盖笼统的核心模块)、详细设计(具体功能的设计,包含接口设计产出:接口文档说明书)、数据库设计(设计业务关联的表、表与表之间的关系、表中的字段、字段的类型、索引等等)

3). 第3阶段: 编码

项目管理人员或者项目组长|架构师任务分发;

编写项目代码、并完成单元测试;

作为软件开发工程师,我们主要的工作就是在该阶段, 对分配给我们的模块功能,进行编码实现。编码实现完毕后,进行单元测试,单元测试通过后再进入到下一阶段;

4). 第4阶段: 测试

在该阶段中主要由测试人员, 对部署在测试环境的项目进行功能测试, 并出具测试报告。

5). 第5阶段: 上线运维

4个9999 保证全年99.9999%;

在项目上线之前, 会由运维人员准备服务器上的软件环境安装、配置, 配置完毕后, 再将我们开发好的项目,部署在服务器上运行。

沙箱环境|准生产环境:最大程度模拟生产环境,又不是生产环境;

注意:

我们作为软件开发工程师, 我们主要的任务是在编码阶段, 但是在一些小的项目组当中, 也会涉及到数据库的设计、测试等方面的工作。

小结

软件的开发流程:

1
市场调研||需求分析----设计----编码-----测试-----上线运维----后期维护

4.2 软件开发角色分工

学习了软件开发的流程之后, 我们还有必要了解一下在整个软件开发过程中涉及到的岗位角色,以及各个角色的职责分工。

image-20210725234015404

岗位/角色 职责/分工
项目经理 对整个项目负责,任务分配、把控进度
产品经理 进行需求调研,输出需求调研文档、产品原型等、需求文档
UI设计师 根据产品原型输出界面效果图
架构师 项目整体架构设计、技术选型等
开发工程师 功能代码实现
测试工程师 编写测试用例,输出测试报告
运维工程师 软件环境搭建、项目上线

上述我们讲解的角色分工, 是在一个项目组中比较标准的角色分工, 但是在实际的项目中, 有一些项目组由于人员配置紧张, 可能并没有专门的架构师或测试人员, 这个时候可能需要有项目经理或者程序员兼任。

4.3 软件开发环境

image-20210725234046008

在我们日常的软件开发中,会涉及到软件开发中的三套环境, 那么这三套环境分别是: 开发环境、测试环境、生产环境。 接下来,我们分别介绍一下这三套环境的作用和特点。

1). 开发环境(development)

我们作为软件开发人员,在开发阶段使用的环境,就是开发环境,一般外部用户无法访问。

比如,我们在开发中使用的MySQL数据库和其他的一些常用软件,我们可以安装在本地, 也可以安装在一台专门的服务器中, 这些应用软件仅仅在软件开发过程中使用, 项目测试、上线时,我们不会使用这套环境了,这个环境就是开发环境。

2). 测试环境(testing)

当软件开发工程师,将项目的功能模块开发完毕,并且单元测试通过后,就需要将项目部署到测试服务器上,让测试人员对项目进行测试。那这台测试服务器就是专门给测试人员使用的环境, 也就是测试环境,用于项目测试,一般外部用户无法访问。

3). 生产环境(production)

当项目开发完毕,并且由测试人员测试通过之后,就可以上线项目,将项目部署到线上环境,并正式对外提供服务,这个线上环境也称之为生产环境。

拓展知识:

准生产环境: 对于有的公司来说,项目功能开发好, 并测试通过以后,并不是直接就上生产环境。为了保证我们开发的项目在上线之后能够完全满足要求,就需要把项目部署在真实的环境中, 测试一下是否完全符合要求啊,这时候就诞生了准生产环境,你可以把他当做生产环境的克隆体,准生产环境的服务器配置, 安装的应用软件(JDK、Tomcat、数据库、中间件 …) 的版本都一样,这种环境也称为 “仿真环境”。

​ ps.由于项目的性质和类型不同,有的项目可能不需要这个环境

Mybatis逆向工程

我们可借助mybatisX工具生成基础代码,步骤如下:

第一步:通过idea自带的database组件连接数据库:

image-20211229213310376

第二步:配置pojo实体类选项

image-20211229213519751

第三步:配置mapper接口和xml选项:

image-20211229214204663

前后端分离跨域问题

【1】前后端跨域配置

在stock_front_admin\src\settings.js文件下配置跨域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
devServer: {
port: 8080,
host: '127.0.0.1',// 开发host
open:true,// 是否启动时打开浏览器
disableHostCheck: true, // 映射外网时,需要设置为true
/**
* 域名,他将会基于 window.location来链接服务器,需要使用public配置
* dev-server被代理到nginx中配置的 itheima.com
*/
public: "127.0.0.1:8080",//itheima.com
publicPath:'/',
compress:true,
overlay: {// 是否在浏览器全屏显示错误与警告
warnings: false,
errors:true
},
proxy: {// 跨域请求配置
"/api": {
secure: false,// 关闭安全检测,默认请求 https
//target: "http://192.168.188.131:8081",
target: "http://localhost:8081",
changeOrigin: true,
// pathRewrite: {"^/api" : ""},
}
},
},

密码加密

使用spring Security中的PasswordEncoder进行加密

配置BCryptPasswordEncoder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

/**
* @author by itheima
* @Date 2021/12/30
* @Description 定义公共配置类
*/
@Configuration
public class CommonConfig {
/**
* 密码加密器
* BCryptPasswordEncoder方法采用SHA-256对密码进行加密
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}

用户登录流程

使用工具类生成验证码,放入到Redis中,设置过期时间为1分钟判断用户是否存在,如果存在则进行密码比对,

使用passwordEncoder.matches进行比对

验证码登录

传统单体架构session实现验证码流程和弊端

单体架构session实现验证码流程:

1646038563833

当前我们的项目采用前后端分离的技术架构,因为前后端请求存在跨域问题,会导致请求无法携带和服务器对应的cookie,导致session失效,且后续服务端也会做集群方案部署,整体来看使用session方案带来的扩展和维护成本是比较高的!

2)验证码逻辑分析

我们可使用分布式缓存redis模拟session机制,实现验证码的生成和校验功能,核心流程如下:

image-20220106200236049

思考:存储redis中验证码的key又是什么呢?

模拟sessionId ,我们可以借助工具类生成全局唯一ID;

yml Redis配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
# 配置缓存
redis:
host: 192.168.188.130
port: 6379
database: 0 #Redis数据库索引(默认为0)
lettuce:
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 8 # 连接池中的最大空闲连接
min-idle: 1 # 连接池中的最小空闲连接
timeout: PT10S # 连接超时时间(毫秒)

大盘指数展示分析

获取国内大盘ID集合===> 获取最近有效交易时间—-> 调用mapper查询

SQL分析思路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
从设计角度看,大盘价格流水表和大盘实时流水表没有必然的练习,对于价格流水表仅仅记录当天的开盘价和前一个交易日的收盘价,也就是一个交易日仅产生一条数据,而大盘的实时流水则会产生N条数据,所以我们采取先查询大盘实时流水主表信息(将数据压扁),然后再关联价格日流水表进行查询。
# 步骤1:先查询指定时间点下大盘主表对应的数据
select * from stock_market_index_info as smi
where smi.mark_Id in ('s_sh000001','s_sz399001') and
smi.cur_time='20211226105600';
#步骤2:将步骤1的结果作为一张表与log_price流水表左外连接查询,获取开盘和前收盘价格
# 为什么左外?因为内连接只查询都存在的数据:
select tmp.trade_account as tradeAmt,tmp.mark_Id as code,tmp.mark_name as name,
date_format(tmp.cur_time,'%Y%m%d%H%i') as curDate,tmp.trade_volume as tradeVol,
tmp.updown_rate as upDown,tmp.current_price as tradePrice,sml.open_price as openPrice,
sml.pre_close_price preClosePrice
from
(
select * from stock_market_index_info as smi
where smi.mark_Id in ('s_sh000001','s_sz399001') and
smi.cur_time='20211226105600'
) as tmp left join stock_market_log_price as sml on
sml.market_code=tmp.mark_id and
date_format(sml.cur_date,'%Y%m%d')=date_format(tmp.cur_time,'%Y%m%d');

T日涨跌停统计功能实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!--
sql思路:
步骤1.统计出每分钟对应的涨幅数值
步骤2.在步骤1的基础上,根据时间分组,查询每分钟内涨停/跌停的数量
-->
<select id="upDownCount" resultType="java.util.Map">
select
tmp.time as time,
count(*) as count
from
(
select
date_format(sri.cur_time,'%Y%m%d%H%i') as time,
(sri.cur_price-sri.pre_close_price)/sri.pre_close_price as upDown
from stock_rt_info as sri
where sri.cur_time between #{openDate} and #{avlDate} having
<if test="flag==1">
upDown >=0.1
</if>
<if test="flag==0">
upDown &lt;=-0.1
</if>
) as
tmp
group by tmp.time
order by tmp.time asc
</select>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 功能描述:沪深两市涨跌停分时行情数据查询,查询T日每分钟的涨跌停数据(T:当前股票交易日)
* 查询每分钟的涨停和跌停的数据的同级;
* 如果不在股票的交易日内,那么就统计最近的股票交易下的数据
* map:
* upList:涨停数据统计
* downList:跌停数据统计
* @return
*/
@Override
public R<Map> upDownCount() {
//1.获取股票最近的有效交易日期,精确到秒
DateTime curDateTime = DateTimeUtil.getLastDate4Stock(DateTime.now());
//当前最近有效期
Date curTime = curDateTime.toDate();
//开盘日期
Date openTime = DateTimeUtil.getOpenDate(curDateTime).toDate();
//TODO mock_data 后续数据实时获取时,注释掉
String curTimeStr="20220106142500";
//对应开盘日期 mock_data
String openTimeStr="20220106092500";
curTime = DateTime.parse(curTimeStr, DateTimeFormat.forPattern("yyyyMMddHHmmss")).toDate();
openTime = DateTime.parse(openTimeStr, DateTimeFormat.forPattern("yyyyMMddHHmmss")).toDate();
//2.统计涨停的数据 约定:1代表涨停 0:跌停
List<Map> upCount= stockRtInfoMapper.upDownCount(curTime,openTime,1);
//3.统计跌停的数据
List<Map> downCount= stockRtInfoMapper.upDownCount(curTime,openTime,0);
//4.组装数据到map
HashMap<String, List<Map>> info = new HashMap<>();
info.put("upList",upCount);
info.put("downList",downCount);
//5.响应
return R.ok(info);
}

EasyExcel入门

导出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class TestEasyExcel {

public List<User> init(){
//组装数据
ArrayList<User> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
User user = new User();
user.setAddress("上海"+i);
user.setUserName("张三"+i);
user.setBirthday(new Date());
user.setAge(10+i);
users.add(user);
}
return users;
}
/**
* 直接导出后,表头名称默认是实体类中的属性名称
*/
@Test
public void test02(){
List<User> users = init();
//不做任何注解处理时,表头名称与实体类属性名称一致
EasyExcel.write("C:\\Users\\46035\\Desktop\\ex\\用户.xls",User.class).sheet("用户信息").doWrite(users);
}
}

自定义表头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
/**
* 通过注解自定义表头名称 注解添加排序规则,值越大 越靠近右边
*/
public class User implements Serializable {
@ExcelProperty(value = {"用户名"},index = 1)
private String userName;
@ExcelProperty(value = {"年龄"},index = 2)
private Integer age;
@ExcelProperty(value = {"地址"} ,index = 4)
private String address;
@ExcelProperty(value = {"生日"},index = 3)
private Date birthday;
}

导出指定页数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 将指定页的股票数据导出到excel表下
* @param response
* @param page 当前页
* @param pageSize 每页大小
*/
@Override
public void stockExport(HttpServletResponse response, Integer page, Integer pageSize) {
try {
//1.设置响应数据的类型:excel
response.setContentType("application/vnd.ms-excel");
//2.设置响应数据的编码格式
response.setCharacterEncoding("utf-8");
//3.设置默认的文件名称
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("stockRt", "UTF-8");
//设置默认文件名称
response.setHeader("content-disposition", "attachment;filename=" + fileName + ".xlsx");
//4.分页查询股票数据
PageHelper.startPage(page,pageSize);
List<StockUpdownDomain> infos = this.stockRtInfoMapper.stockAll();
Gson gson = new Gson();
List<StockExcelDomain> excelDomains = infos.stream().map(info -> {
StockExcelDomain domain=new StockExcelDomain();
BeanUtils.copyProperties(info,domain);
return domain;
}).collect(Collectors.toList());
//5.导出
EasyExcel.write(response.getOutputStream(),StockExcelDomain.class).sheet("股票数据").doWrite(excelDomains);
} catch (IOException e) {
log.info("股票excel数据导出异常,当前页:{},每页大小:{},异常信息:{}",page,pageSize,e.getMessage());
}
}

个股分时涨跌幅度统计功能

接口分析

1
2
3
4
5
6
功能描述:统计当前时间下(精确到分钟),股票在各个涨跌区间的数量
服务路径:/api/quot/stock/updown
服务方法:GET
请求频率:每分钟
请求参数:无
注意事项:如果当前不在股票有效时间内,则以前一个有效股票交易日作为查询时间点;

SQL分析步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 步骤1:统计当前时间下,每只股票的涨幅值
select
date_format( sri.cur_time, '%Y%m%d%H%i' ) as curTime,
( sri.cur_price - sri.pre_close_price )/ sri.pre_close_price as rate
from
stock_rt_info as sri
where
sri.cur_time = '20220106095500'
# 步骤2:将步骤1的查询结果中数据转换为区间范围集合
select
CASE
WHEN tmp.rate > 0.07 THEN '>7%'
WHEN tmp.rate > 0.05 AND tmp.rate <= 0.07 THEN '5~7%'
WHEN tmp.rate > 0.03 AND tmp.rate <= 0.05 THEN '3~5%'
WHEN tmp.rate > 0 AND tmp.rate <= 0.03 THEN '0~3%'
WHEN tmp.rate > -0.03 AND tmp.rate <= 0 THEN '-3~0%'
WHEN tmp.rate > -0.05 AND tmp.rate <= -0.03 THEN '-5~-3%'
WHEN tmp.rate > -0.07 AND tmp.rate <= -0.05 THEN '-7~-5%'
ELSE '<-7%'
END 'title'
from
(
select
date_format(sri.cur_time,'%Y%m%d%H%i') as curTime,
(sri.cur_price-sri.pre_close_price)/sri.pre_close_price as rate
from stock_rt_info as sri
where sri.cur_time='20220106095500'
)as tmp
# 步骤3:根据区间分组,统计各个区间数据量
select
tmp2.title,
count(*) as count
from
(select
CASE
WHEN tmp.rate > 0.07 THEN '>7%'
WHEN tmp.rate > 0.05 AND tmp.rate <= 0.07 THEN '5~7%'
WHEN tmp.rate > 0.03 AND tmp.rate <= 0.05 THEN '3~5%'
WHEN tmp.rate > 0 AND tmp.rate <= 0.03 THEN '0~3%'
WHEN tmp.rate > -0.03 AND tmp.rate <= 0 THEN '-3~0%'
WHEN tmp.rate > -0.05 AND tmp.rate <= -0.03 THEN '-5~-3%'
WHEN tmp.rate > -0.07 AND tmp.rate <= -0.05 THEN '-7~-5%'
ELSE '<-7%'
END 'title'
from
(select
date_format(sri.cur_time,'%Y%m%d%H%i') as curTime,
(sri.cur_price-sri.pre_close_price)/sri.pre_close_price as rate
from stock_rt_info as sri
where sri.cur_time='20220106095500')
as tmp)
as tmp2 group by tmp2.title

个股分时K线行情功能

接口分析

1
2
3
4
5
6
7
功能描述:查询个股的分时行情数据,也就是统计指定股票T日每分钟的交易数据;
服务路径:/api/quot/stock/screen/time-sharing
服务方法:GET
请求参数:code(股票code)
请求频率:每分钟
响应数据字段:日期 交易量 交易金额 最低价 最高价 前收盘价 公司名称 开盘价 股票code 当前价
注意点:如果当前日期不在有效时间内,则以最近的一个股票交易时间作为查询时间点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<select id="stockScreenTimeSharing" resultType="com.itheima.stock.common.domain.Stock4MinuteDomain">
select
date_format(sri.cur_time,'%Y%m%d%H%i') as date,
sri.trade_amount as tradeAmt,
sri.stock_code as code,
sri.min_price lowPrice,
sri.pre_close_price as preClosePrice,
sri.stock_name as name,
sri.max_price as highPrice,
sri.open_price as openPrice,
sri.trade_volume as tradeVol,
sri.cur_price as tradePrice
from stock_rt_info as sri
where sri.stock_code=#{stockCode}
and sri.cur_time between #{startDate} and #{endtDate}
</select>

个股日K线

1
2
3
4
5
6
功能描述:个股日K数据查询 ,可以根据时间区间查询数日的K线数据
默认查询历史20天的数据;
服务路径:/api/quot/stock/screen/dkline
服务方法:GET
请求参数:code(股票编码)
请求频率:每分钟

个股日K线详情功能SQL分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
说明:因为在股票流水中,开盘价、最高价、最低价、当前价等信息在每条记录中都会记录,所以我们更加关注的是每天的收盘价格,业务要求如果当前没有收盘,则以最新价格作为收盘价,所以该业务就可以转化成查询每天最大交易时间对应的信息;
步骤1:查询指定股票在指定日期范围内每天的最大时间,说白了就是以天分组,求每天最大时间
select
max( sri.cur_time ) as closeDate
from
stock_rt_info as sri
where
sri.stock_code ='600021'
and sri.cur_time between '20220101093000' and '20220106142500'
group by
date_format( sri.cur_time, '%Y%m%d' )
步骤2:以步骤1查询结果作为条件,同统计指定时间点下,股票的数据信息
select
date_format(sri2.cur_time,'%Y%m%d'),
sri2.trade_amount as tradeAmt,
sri2.stock_code as code,
sri2.min_price as lowPrice,
sri2.stock_name as name,
sri2.max_price as highPrice,
sri2.open_price as openPrice,
sri2.trade_volume as tradeVol,
sri2.cur_price as closePrice,
sri2.pre_close_price as preClosePrice
from
stock_rt_info as sri2
where sri2.stock_code='600021' and sri2.cur_time in (
select
max( sri.cur_time ) as closeDate
from
stock_rt_info as sri
where
sri.stock_code ='600021'
and sri.cur_time between '20220101093000' and '20220106142500'
group by
date_format( sri.cur_time, '%Y%m%d' )
)

RestTemplate远程调用

请求头携带参数访问外部接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 请求头设置参数,访问指定接口
*/
@Test
public void test03(){
String url="http://localhost:6666/account/getHeader";
//设置请求头参数
HttpHeaders headers = new HttpHeaders();
headers.add("userName","zhangsan");
//请求头填充到请求对象下
HttpEntity<Map> entry = new HttpEntity<>(headers);
//发送请求
ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.GET, entry, String.class);
String result = responseEntity.getBody();
System.out.println(result);
}

POST请求模拟form表单访问外部接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* post模拟form表单提交数据
*/
@Test
public void test04(){
String url="http://localhost:6666/account/addAccount";
//设置请求头,指定请求数据方式
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type","application/x-www-form-urlencoded");
//组装模拟form表单提交数据
LinkedMultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
map.add("id","10");
map.add("userName","itheima");
map.add("address","shanghai");
HttpEntity<LinkedMultiValueMap<String, Object>> httpEntity = new HttpEntity<>(map, headers);
/*
参数1:请求url地址
参数2:请求方式 POST
参数3:请求体对象,携带了请求头和请求体相关的参数
参数4:响应数据类型
*/
ResponseEntity<Account> exchange = restTemplate.exchange(url, HttpMethod.POST, httpEntity, Account.class);
Account body = exchange.getBody();
System.out.println(body);
}

POST请求发送JSON数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    /**
* post发送json数据
*/
@Test
public void test05(){
String url="http://localhost:6666/account/updateAccount";
//设置请求头的请求参数类型
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type","application/json; charset=utf-8");
//组装json格式数据
// HashMap<String, String> reqMap = new HashMap<>();
// reqMap.put("id","1");
// reqMap.put("userName","zhangsan");
// reqMap.put("address","上海");
// String jsonReqData = new Gson().toJson(reqMap);
String jsonReq="{\"address\":\"上海\",\"id\":\"1\",\"userName\":\"zhangsan\"}";
//构建请求对象
HttpEntity<String> httpEntity = new HttpEntity<>(jsonReq, headers);
/*
发送数据
参数1:请求url地址
参数2:请求方式
参数3:请求体对象,携带了请求头和请求体相关的参数
参数4:响应数据类型
*/
ResponseEntity<Account> responseEntity = restTemplate.exchange(url, HttpMethod.POST, httpEntity, Account.class);
//或者
// Account account=restTemplate.postForObject(url,httpEntity,Account.class);
Account body = responseEntity.getBody();
System.out.println(body);
}

股票数据拉取

1)股票常量数据配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 配置股票相关的参数
stock:
inner: # 国内大盘ID
- s_sh000001 # 上证ID
- s_sz399001 # 深证ID
outer: # 外盘ID
- int_dji # 道琼斯
- int_nasdaq # 纳斯达克
- int_hangseng # 恒生
- int_nikkei # 日经指数
- b_TWSE # 台湾加权
- b_FSSTI # 新加坡
marketUrl: http://hq.sinajs.cn/list=
blockUrl: http://vip.stock.finance.sina.com.cn/q/view/newSinaHy.php

接口实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@Service("stockTimerTaskService")
@Slf4j
public class StockTimerTaskServiceImpl implements StockTimerTaskService {

@Autowired
private RestTemplate restTemplate;

@Autowired
private StockInfoConfig stockInfoConfig;

@Autowired
private IdWorker idWorker;

@Override
public void getInnerMarketInfo() {
//组装动态url
//http://hq.sinajs.cn/list=s_sh000001,s_sz399001
String innerUrl=stockInfoConfig.getMarketUrl()+String.join(",",stockInfoConfig.getInner());
//设置请求头信息
HttpHeaders headers = new HttpHeaders();
headers.add("Referer","https://finance.sina.com.cn/stock/");
headers.add("User-Agent","Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36");
//发起请求
String result = restTemplate.postForObject(innerUrl, new HttpEntity<>(headers), String.class);
// String result = restTemplate.getForObject(innerUrl, String.class);
String reg="var hq_str_(.+)=\"(.+)\";";
Pattern pattern = Pattern.compile(reg);
Matcher matcher = pattern.matcher(result);
//收集大盘封装后后的对象
ArrayList<StockMarketIndexInfo> list = new ArrayList<>();
while (matcher.find()) {
//获取大盘的id
String marketCode = matcher.group(1);
System.out.println(marketCode);
//其它信息
String other = matcher.group(2);
String[] others = other.split(",");
//大盘名称
String marketName=others[0];
//当前点
BigDecimal curPoint=new BigDecimal(others[1]);
//当前价格
BigDecimal curPrice=new BigDecimal(others[2]);
//涨跌率
BigDecimal upDownRate=new BigDecimal(others[3]);
//成交量
Long tradeAmount=Long.valueOf(others[4]);
//成交金额
Long tradeVol=Long.valueOf(others[5]);
//当前日期
Date now = DateTimeUtil.getDateTimeWithoutSecond(DateTime.now()).toDate();
//封装对象
StockMarketIndexInfo stockMarketIndexInfo = StockMarketIndexInfo.builder().id(idWorker.nextId()+"")
.markName(marketName)
.tradeVolume(tradeVol)
.tradeAccount(tradeAmount)
.updownRate(upDownRate)
.curTime(now)
.curPoint(curPoint)
.currentPrice(curPrice)
.markId(marketCode)
.build();
list.add(stockMarketIndexInfo);
};
log.info("集合长度:{},内容:{}",list.size(),list);
//批量插入
if (CollectionUtils.isEmpty(list)) {
log.info("");
return;
}
String curTime = DateTime.now().toString(DateTimeFormat.forPattern("yyyyMMddHHmmss"));
log.info("采集的大盘数据:{},当前时间:{}",list,curTime);
//TODO 后续完成批量插入功能
}
}

板块实时数据批量插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 获取板块实时数据
* http://vip.stock.finance.sina.com.cn/q/view/newSinaHy.php
*/
@Override
public void getStockSectorRtIndex() {
//发送板块数据请求
String result = restTemplate.getForObject(stockInfoConfig.getBlockUrl(), String.class);
//响应结果转板块集合数据
List<StockBlockRtInfo> infos = parserStockInfoUtil.parse4StockBlock(result);
log.info("板块数据量:{}",infos.size());
//数据分片保存到数据库下 行业板块类目大概50个,可每小时查询一次即可
Lists.partition(infos,20).forEach(list->{
//20个一组,批量插入
stockBlockRtInfoMapper.insertBatch(list);
});
}

XXLJob在项目中的使用

1)导入xxljob工程

image-20220102153303653

2)配置数据库

image-20220115095650494

1.初始化SQL脚本

将xxljob提供的初始化SQL脚本导入数据库:

image-20220102153736369

整体如下:

image-20220102153808757

注意:

如果表xxl_job_registry导入过程报Specified key was too long; max key length is 767 bytes错误,则尝试将联合主键关联的varchar改小一些即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE `xxl_job_registry` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`registry_group` varchar(50) NOT NULL,
`registry_key` varchar(255) NOT NULL,
`registry_value` varchar(255) NOT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `i_g_k_v` (`registry_group`,`registry_key`,`registry_value`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
> 1071 - Specified key was too long; max key length is 767 bytes
> 时间: 0s
解决:将varchar修改为100即可;
使用的字符集为(utf8mb4),这个指每个字符最大的字节数为4,所以很明显 4*255 > 767
2.配置数据库环境

image-20220102154202964

3.配置任务注册中心

image-20220102154349558

认证注册中心端口保证与admin端口一致即可;

3)启动任务调度中心

运行xxl-job-admin工程main方法启动:

image-20220102160615377

访问管理界面:http://localhost:8082/xxl-job-admin/

登录用户名:admin 密码:123456

image-20220102160725139

当前我们重点关注:执行器管理和任务管理;

4)配置执行器工程

在xxl-job-executor-sample-springboot工程修改端口号和任务注册中心端口号:

1
2
3
4
5
6
# web port
server.port=8083
### xxl-job admin address list, such as "http://address" or "http://address01,http://address02"
xxl.job.admin.addresses=http://127.0.0.1:8082/xxl-job-admin
# 当前任务执行器启动时,会独立一个端口9999用于与任务管理中心交互,改端口也可改动
xxl.job.executor.port=9999

该工程默认导入了xxl-core核心包:

1
2
3
4
5
6
<!-- xxl-job-core -->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>${project.parent.version}</version>
</dependency>

默认已经配置好xxl-job相关支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.xxl.job.executor.core.config;
@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);

@Value("${xxl.job.admin.addresses}")
private String adminAddresses;

@Value("${xxl.job.accessToken}")
private String accessToken;

@Value("${xxl.job.executor.appname}")
private String appname;

@Value("${xxl.job.executor.address}")
private String address;

@Value("${xxl.job.executor.ip}")
private String ip;

@Value("${xxl.job.executor.port}")
private int port;

@Value("${xxl.job.executor.logpath}")
private String logPath;

@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
}

定义可执行的任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.xxl.job.executor.service.jobhandler;
@Component
public class SampleXxlJob {
private static Logger logger = LoggerFactory.getLogger(SampleXxlJob.class);


/**
* 1、简单任务示例(Bean模式)
*/
@XxlJob("demoJobHandler")
public void demoJobHandler() throws Exception {
//todo 打印时间
System.out.println("hello xxljob.....");
}

//.....省略......

/**
* 5、生命周期任务示例:任务初始化与销毁时,支持自定义相关逻辑;
*/
@XxlJob(value = "demoJobHandler2", init = "init", destroy = "destroy")
public void demoJobHandler2() throws Exception {
XxlJobHelper.log("XXL-JOB, Hello World.");
}
public void init(){
logger.info("init");
}
public void destroy(){
logger.info("destory");
}
}

@XxlJob中的value值就是定时任务的一个标识,注解作用的方法,就是定时任务的逻辑,在该类下,我们可以注入自定义的job服务,然后通过注解作用的方法被调用执行;

5)配置并启动任务执行器

image-20220102162510380

接下来,我们将xxl-job-executor-sample-springboot工程下的demoJobHandler任务,可视化配置,并启动:

image-20220102163055618

接下来,输入JobHanler,输入的名称保证与@xxljob注解下的value值一致即可:

image-20220102163222124

启动任务查看执行效果:

image-20220102163302810

image-20220102163455786

当然,我们也可以随时停止正在被执行的任务:

股票和板块数据异步采集

配置线程池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package com.itheima.stock.config;

import com.itheima.stock.common.domain.TaskThreadPoolInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

/**
* @author by itheima
* @Date 2021/12/13
* @Description
*/
@Configuration
@EnableConfigurationProperties(TaskThreadPoolInfo.class)
@Slf4j
public class TaskExecutePool {
private TaskThreadPoolInfo info;

public TaskExecutePool(TaskThreadPoolInfo info) {
this.info = info;
}

/**
* 定义任务执行器
* @return
*/
@Bean(name = "threadPoolTaskExecutor",destroyMethod = "shutdown")
public ThreadPoolTaskExecutor threadPoolTaskExecutor(){
//构建线程池对象
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
//核心线程数:核心线程数(获取硬件):线程池创建时候初始化的线程数
taskExecutor.setCorePoolSize(info.getCorePoolSize());
//最大线程数:只有在缓冲队列满了之后才会申请超过核心线程数的线程
taskExecutor.setMaxPoolSize(info.getMaxPoolSize());
//缓冲队列:用来缓冲执行任务的队列
taskExecutor.setQueueCapacity(info.getQueueCapacity());
//允许线程的空闲时间:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
taskExecutor.setKeepAliveSeconds(info.getKeepAliveSeconds());
//线程名称前缀
taskExecutor.setThreadNamePrefix("StockThread-");
//设置拒绝策略
taskExecutor.setRejectedExecutionHandler(rejectedExecutionHandler());
//参数初始化
taskExecutor.initialize();
return taskExecutor;
}

/**
* 自定义线程拒绝策略
* @return
*/
@Bean
public RejectedExecutionHandler rejectedExecutionHandler(){
RejectedExecutionHandler errorHandler = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable runnable, ThreadPoolExecutor executor) {
//TODO 可自定义Runable实现类,传入参数,做到不同任务,不同处理
log.info("股票任务出现异常:发送邮件");
}
};
return errorHandler;
}
}

板块数据异步采集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 获取板块实时数据
* http://vip.stock.finance.sina.com.cn/q/view/newSinaHy.php
*/
@Override
public void getStockSectorRtIndex() {
//发送板块数据请求
String result = restTemplate.getForObject(stockInfoConfig.getBlockUrl(), String.class);
//响应结果转板块集合数据
List<StockBlockRtInfo> infos = parserStockInfoUtil.parse4StockBlock(result);
log.info("板块数据量:{}",infos.size());
//数据分片保存到数据库下 行业板块类目大概50个,可每小时查询一次即可
Lists.partition(infos,20).forEach(list->{
threadPoolTaskExecutor.execute(()->{
//20个一组,批量插入
stockBlockRtInfoMapper.insertBatch(list);
});
});
}

线程池高级

1)回顾线程池工作流程

image-20220107152235483

说明:

1
2
3
4
5
6
7
1 当一个任务通过submit或者execute方法提交到线程池的时候,如果当前池中线程数(包括闲置线程)小于coolPoolSize,则创建一个新的线程执行该任务;
2 如果当前线程池中线程数已经达到coolPoolSize,则将任务放入等待队列;
3 如果任务队列已满,则任务无法入队列,此时如果当前线程池中线程数小于maxPoolSize,则创建一个临时线程(非核心线程)执行该任务。
4 如果当前池中线程数已经等于maxPoolSize,此时无法执行该任务,根据拒绝执行策略处理。

注意:
当池中线程数大于coolPoolSize,超过keepAliveTime时间的闲置线程会被回收掉。回收的是非核心线程,核心线程一般是不会回收的。如果设置allowCoreThreadTimeOut(true),则核心线程在闲置keepAliveTime时间后也会被回收。

2)线程池拒绝策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
【1】什么时候会触发线程池的拒绝策略?
1.当我们调用 shutdown 等方法关闭线程池后,如果再向线程池内提交任务,就会遭到拒绝;
2.线程池没有空闲线程(线程达到最大线程数且都在执行任务)并且队列已经满;★★★

【2】拒绝策略类型有哪些?
线程池为我们提供了4种拒绝策略:

AbortPolicy
这种拒绝策略在拒绝任务时,会直接抛出一个类型为 RejectedExecutionException 的 RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略(默认)。

DiscardPolicy
当有新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。(不负责任)

DiscardOldestPolicy
丢弃任务队列中的头结点,通常是存活时间最长的任务,它也存在一定的数据丢失风险。

CallerRunsPolicy
第四种拒绝策略是 ,相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。
任务线程满了后,改策略可将执行的人为交换给主线程执行,这个过程相当于一个正反馈,此时如果主线程能处理,则处理,如果也不能处理,也就以为这当前服务不能接收新的任务了;
主线程处理任务期间,可以为线程池腾出时间,如果此时有新的空闲线程,那么继续协助主线程处理任务;

自定义拒绝策略
实现RejectedExecutionHandler接口来实现自己的拒绝策略;★★★

JWT集成项目

Jwt组成

头部(Header)(非敏感)

1
2
3
4
5
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
{"typ":"JWT","alg":"HS256"}

在头部指明了签名算法是HS256算法。 我们进行BASE64编码https://www.qqxiuzi.cn/bianma/base64.htm/,编码后的字符串如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

载荷(playload)(非敏感数据)

1
2
3
4
5
6
7
载荷就是存放有效信息的地方。该部分的信息是可以自定义的

定义一个payload:
{"sub":"1234567890","name":"John Doe","admin":true}

然后将其进行base64编码,得到Jwt的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG7CoERvZSIsImFkbWluIjp0cnVlfQ==

签证(signature)

1
2
3
4
5
6
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
签名算法( header (base64后的).payload (base64后的) . secret )

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret秘钥组合加密,然后就构成了jwt的第三部分。

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

JWT入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>

3.1 生成令牌

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testGenerate(){
String compact = Jwts.builder()
.setId(UUID.randomUUID().toString())//设置唯一标识
.setSubject("JRZS") //设置主题
.claim("name", "nineclock") //自定义信息
.claim("age", 88) //自定义信息
.setExpiration(new Date()) //设置过期时间
.setIssuedAt(new Date()) //令牌签发时间
.signWith(SignatureAlgorithm.HS256, "itheima")
.compact();//签名算法, 秘钥

System.out.println(compact);
}

3.2 校验令牌

1
2
3
4
5
6
@Test
public void testVerify(){
String jwt = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI5MzljNjU4MC0yMTQyLTRlOWEtYjcxOC0yNzlmNzRhODVmNDMiLCJzdWIiOiJOSU5FQ0xPQ0siLCJuYW1lIjoibmluZWNsb2NrIiwiYWdlIjo4OCwiaWF0IjoxNjE3MDMxMjUxfQ.J-4kjEgyn-Gkh0ZuivUCevrzDXt0K9bAyF76rn1BfUs";
Claims claims = Jwts.parser().setSigningKey("itheima").parseClaimsJws(jwt).getBody();
System.out.println(claims);
}