项目介绍及环境搭建和登录功能
1.项目介绍
1.1项目概述
- 今日指数是基于股票实时交易产生的数据分析产品,旨在为特定用户和机构提供定制化的股票数据分析和展示服务;
- 项目的核心功能以数据分析和展示为主,功能涵盖了A股大盘实时指数展示、涨幅榜、个股涨跌、个股秒级行情、实时日K线行情等;
1.2股票介绍
股票是股份证书的简称,是股份公司为筹集资金而发行给股东的一种有价证券,股东可凭此取得红利和买卖抵押,是资金市场中主要的信用工具之一;
股票的分类
- A股-人民币普通股票
- 由中国境内注册公司发行,在境内上市,以人民币标明面值,供境内机构、组织或个人以人民币认购和交易的普通股股票;
- B股-人民币特种股票
- 即公司在中国大陆注册和上市,但只能以外币认购和交易;
- 即公司在中国大陆注册和上市,但只能以外币认购和交易;
- 开盘价
- 又称开市价,是证券交易所在每个交易日开市后的第一笔股票买卖成交的价格;
- 开盘价是在9点15分至9点25分买卖双方的竞价撮合产生(了解);
- 开盘价一般会参考前一个股票交易日收盘价;
- 收盘价
- 又称收市价,是指股票在每个交易日里最后一笔买卖成交价格;
- 昨收:上一个交易日的收盘价格;
- 当前价
- 当前股票实时的最新成交价格;
- 涨跌值
- 涨跌值=最新价格-前收盘价格 ;
- 股票涨跌值主要用于反应股票的涨跌情况,单位是元(A股);
- 一般用“+”或“-”号表示,正值为涨,负值为跌,否则为持平;
- 涨跌幅度(涨幅)
- 股票涨幅=(最新成交价-前收盘价)÷ 前收盘价×100%
- 涨停与跌停
- 股市涨跌停的机制与生活中电路过载保护思想一致,在股票市场中为了防止股价过分的暴涨暴跌,同时抑制过度投机行为,证券交易所给股价的涨跌做了相关限制;
- 在A股市场中,股价的涨跌幅度范围:-10%~+10%;
- 打新-对于新上市股票第一天交易中股价涨幅不设限,第二天才会有限制(了解);
- 振幅
- 股票振幅=(当日最高价-当日最低价)÷ 前收盘价 ×100%;
- 股票振幅在一定程度上反应了股票的活跃程度;
- 成交量
- 成交量指当天成交的股票总手数(1手=100股);
- 成交金额
- 股票成交金额是成交量和成交价格的累加,由证券交易锁计算得出;
- 示例投资者以每股10元的价格买入50手,那么此时成交金额为:10X50X100=5w;
- 股票编码
- 每个上市公司的股票都一个唯一的编码,通过这个编码就可定义具体股票;
- 沪市A股的代码是以600、601或603打头(6打头);
- 深市A股 深市A股的代码是以000打头(0打头);
- 其它:创业板股票代码以300打头,沪市B股代码以900打头,深圳B股代码以200打头等等;
1.3个股K线图介绍
K线图源于日本,早期主要用于米价涨跌情况统计,后来发展到股市金融领域;
k线图又分为日K线、周K线、月K线等,信息主要包含股票的开盘、收盘、最高、最低等价格信息;
备注:
分时图:统计当天每分钟的交易数据(当前价格、均价、涨跌、涨幅、成交量和成交金额等)
日K线图:统计每天交易数据(最高、最低、开盘、收盘、涨跌、涨幅等)
周K线图:统计每周交易数据(最高、最低、开盘、收盘、涨跌、涨幅等)
月K线图:统计每月交易数据(最高、最低、开盘、收盘、涨跌、涨幅等)
- 小结
- 什么是股票?
证明股东持有公司股份的有价证券;
说白了正证明投资人投资的一种凭证; - 什么是A股?
国内企业,国内证券交易所上市,且必须以人民币结算,且必须国内的组织、个人、机构购买; - 股票核心参数?
开盘价、收盘价、涨跌值、涨幅、振幅、涨停|跌停、成交量、成交金额等; - K线图核心参数及分类?
参数:开盘、收盘、最高、最低等;
分类:日k线图、周k线图、月K图等;
1.4盘与板块概念介绍
股市的大盘指数是由证券交易所经过一系列专业计算得出的一个反应股市行情健康状态的指数。
大盘指数反应了整体的市场行情,不能反应具体某个行业,而板块指数可以更加细粒度的反应具体某个行业股市的活跃程度;
根据定义板块的方式主要分为:地域板块、行业板块、概念板块等;
2.项目架构
2.1技术选型
- 前端技术
名称 | 技术 | 场景 |
---|---|---|
基本骨架 | vue-cli+vue+element+axios | 前端核心技术架构 |
报表 | echartsjs | 股票数据报表展示,比如折线图、柱状图、K线图等 |
前端支持 | node webpack | 脚手架支持 |
- 后端技术栈
名称 | 技术 | 场景 |
---|---|---|
基础框架 | SpringBoot、Mybatis、SpringMVC | 项目基础骨架 |
安全框架 | SpringSecurity+Jwt+Token | 认证与授权 |
缓存 | Redis+SpringCache | 数据缓存 |
excel表格导出 | EasyExcel | 导出股票相关热点数据 |
小组件 | Jode-Time 、hutool 、Guava 、HttpClient / RestTemplate 、线程池 | 生产力工具 |
定时任务 | Xxljob | 定时采集股票数据 |
分库分表 | Sharding-JDBC | 股票数据分库分表方案落地 |
前端资源部署 | Nginx+Linux | 前端静态资源部署; 代理访问后端功能接口; |
2.2核心业务介绍
- 股票采集系统
- 核心功能是周期性采集股票数据,并刷入数据库;
- 借助xxljob提供完善的任务监控机制;
- 国内指数服务
- 主要统计国内大盘实时数据信息;
- 板块分析服务
- 主要统计国内各大行业板块行情数据信息;
- 涨幅榜展示功能
- 根据个股涨幅排序,提供热点股票数据展示;
- 涨停跌停数展示功能
- 统计股票涨停跌停的数量;
- 成交量对比展示功能
- 综合对比真个股市成交量的变化趋势;
- 个股详情展示功能
- 包含分时行情、日k线、周K线图、个股描述服务等
- 报表导出服务
- 根据涨幅排序导出热点股票数据信息;
- 其它
- 用户信息管理;
- 角色管理;
- 权限管理等
3.项目搭建
3.1数据库搭建
- 大盘和板块相关表
- 个股相关表
- 权限相关表
- 日志表
注意事项
1) 表与表之间的关系尽量通过业务逻辑维护,而不是通过使用数据库外键约束,原因如下:
1.性能问题:外键约束会使约束的表之间做级联检查,导致数据库性能降低;
2.并发问题:外键约束的表在事务中需要获取级联表的锁,才能进行写操作,这更容易造成死锁问题;(数据库自身存在死锁检查,当放生死锁时,会自动终端另一方的事务)
3.扩展性问题:数据分库分表时,加大了拆分的难度;
2)docker容器中的数据库注意时区问题;
docker run —restart=always -p 3306:3306 —name mysql -v /tmp/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=root -e TZ=Asia/Shanghai -d mysql:5.7.25 —character-set-server=utf8mb4 —collation-server=utf8mb4_unicode_ci —default-time_zone=’+8:00’
模拟公共的开发环境;
- 提前构建好数据库:stock_db,并设置编码格式为:utf8;
- 导入 day01\资料\stock.sql到数据库中, 自动构建库和表;
3.2项目结构
3.2.1stock_parent父工程
1 |
|
3.2.2stock_common公共工程
1 |
|
使用mybatisx逆向生成mybatis代码
)
3.2.3stock_backend主业务工程
1 |
|
1 | # web定义 |
启动类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package com.itheima.stock;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author by itheima
* @Date 2021/12/29
* @Description 定义main启动类
*/
public class StockApp {
public static void main(String[] args) {
SpringApplication.run(StockApp.class,args);
}
}
4.前端搭建
4.1导入前端代码
导入stock_front_admin资料 遇到 getaddrinfo ENOENT raw.githubusercontent.com
npm install
npm run dev
登陆界面:没有验证码 遇到的问题
解决:找到hosts文件
host文件位置: C:\Windows\System32\drivers\etc1
185.199.108.133 raw.githubusercontent.com
4.2跨域问题
浏览器跨域问题:
- 只要请求的协议、端口、ip域名不一致,浏览器就拒绝访问对应的资源;
- 浏览器的同源策略:访问安全问题;
在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
26devServer: {
port: 80,
host: '127.0.0.1',// 开发host
open:true,// 是否启动时打开浏览器
disableHostCheck: true, // 映射外网时,需要设置为true
/**
* 域名,他将会基于 window.location来链接服务器,需要使用public配置
* dev-server被代理到nginx中配置的 itheima.com
*/
public: "127.0.0.1:80",//itheima.com
publicPath:'/',
compress:true,
overlay: {// 是否在浏览器全屏显示错误与警告
warnings: false,
errors:true
},
proxy: {// 跨域请求配置
"/api": {
secure: false,// 关闭安全检测,默认请求 https
//target: "http://192.168.188.131:8091",
target: "http://localhost:8091",
changeOrigin: true,
// pathRewrite: {"^/api" : ""},
}
},
},
4.3统一返回类型
统一格式1
2
3
4
5{
code: 1 //状态码
msg: "" //提示信息
data: //任意响应数据
}
封装统一类型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
76
77
78
79
80
81
82
83package com.itheima.vo.resq;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.itheima.pojo.vo.ResponseCode;
import java.io.Serializable;
/**
* 返回数据类
* @JsonInclude 保证序列化json的时候,如果是null的对象,key也会消失
* @param <T>
*/
public class R<T> implements Serializable {
private static final long serialVersionUID = 7735505903525411467L;
// 成功值,默认为1
private static final int SUCCESS_CODE = 1;
// 失败值,默认为0
private static final int ERROR_CODE = 0;
//状态码
private int code;
//消息
private String msg;
//返回数据
private T data;
//私有的构造函数,为了不让外界new对象
private R(int code){
this.code = code;
}
private R(int code, T data){
this.code = code;
this.data = data;
}
private R(int code, String msg){
this.code = code;
this.msg = msg;
}
private R(int code, String msg, T data){
this.code = code;
this.msg = msg;
this.data = data;
}
//静态方法,外界调用生成新的对象
public static <T> R<T> ok(){
return new R<T>(SUCCESS_CODE,"success");
}
public static <T> R<T> ok(String msg){
return new R<T>(SUCCESS_CODE,msg);
}
public static <T> R<T> ok(T data){
return new R<T>(SUCCESS_CODE,data);
}
public static <T> R<T> ok(String msg, T data){
return new R<T>(SUCCESS_CODE,msg,data);
}
public static <T> R<T> error(){
return new R<T>(ERROR_CODE,"error");
}
public static <T> R<T> error(String msg){
return new R<T>(ERROR_CODE,msg);
}
public static <T> R<T> error(int code, String msg){
return new R<T>(code,msg);
}
public static <T> R<T> error(ResponseCode res){
return new R<T>(res.getCode(),res.getMessage());
}
public int getCode(){
return code;
}
public String getMsg(){
return msg;
}
public T getData(){
return data;
}
}
枚举类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
42package com.itheima.vo.resq;
/**
* @author by itheima
* @Date 2021/12/21
* @Description
*/
public enum ResponseCode{
ERROR(0,"操作失败"),
SUCCESS(1,"操作成功"),
DATA_ERROR(0,"参数异常"),
NO_RESPONSE_DATA(0,"无响应数据"),
CHECK_CODE_NOT_EMPTY(0,"验证码不能为空"),
CHECK_CODE_ERROR(0,"验证码错误"),
USERNAME_OR_PASSWORD_ERROR(0,"用户名或密码错误"),
ACCOUNT_EXISTS_ERROR(0,"该账号已存在"),
ACCOUNT_NOT_EXISTS(0,"该账号不存在"),
TOKEN_ERROR(2,"用户未登录,请先登录"),
NOT_PERMISSION(3,"没有权限访问该资源"),
ANONMOUSE_NOT_PERMISSION(0,"匿名用户没有权限访问"),
INVALID_TOKEN(0,"无效的票据"),
OPERATION_MENU_PERMISSION_CATALOG_ERROR(0,"操作后的菜单类型是目录,所属菜单必须为默认顶级菜单或者目录"),
OPERATION_MENU_PERMISSION_MENU_ERROR(0,"操作后的菜单类型是菜单,所属菜单必须为目录类型"),
OPERATION_MENU_PERMISSION_BTN_ERROR(0,"操作后的菜单类型是按钮,所属菜单必须为菜单类型"),
OPERATION_MENU_PERMISSION_URL_CODE_NULL(0,"菜单权限的按钮标识不能为空"),
ROLE_PERMISSION_RELATION(0, "该菜单权限存在子集关联,不允许删除");
private int code;
private String message;
ResponseCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
5.用户登录
只完成账号,密码的校验。
后期在完善验证码校验
5.0密码匹配器
- 导入依赖
1
2
3
4
5<!--密码加密和校验工具包-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency> - 创建配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24package com.itheima.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author by itheima
* @Date 2021/12/30
* @Description 定义公共配置类
*/
public class CommonConfig {
/**
* 密码加密器
* BCryptPasswordEncoder方法采用SHA-256对密码进行加密
* @return
*/
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
} - 测试
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
32package com.itheima;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
public class TestPasswordEncoder {
private PasswordEncoder passwordEncoder;
//测试密码加密
public void test01(){
String pwd = "123456";
String encodePwd = passwordEncoder.encode(pwd);
System.out.println(encodePwd);
// $2a$10$DzYivF8fCFkbGcIafbwsfurifAmNMW26XwNIKLHecllZBNGgIWcJy
}
//测试密码匹配
public void text02(){
String pwd = "123456";
String encodePwd = "$2a$10$DzYivF8fCFkbGcIafbwsfurifAmNMW26XwNIKLHecllZBNGgIWcJy";
boolean isSuccess = passwordEncoder.matches(pwd, encodePwd);
System.out.println(isSuccess?"密码匹配成功":"密码匹配失败");
}
}5.1需求分析
页面原型
sys_user表5.2接口定义
问题:数据库id为Long类型,但是响应参数返回的类型为String1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18请求接口:/api/login
请求方式:POST
请求数据示例:
{
username:'zhangsan',//用户名
password:'666',//密码
code:'1234' //校验码
}
响应数据:
{
"code": 1,//成功1 失败0
"data": { //data为响应的具体数据,不同的接口,data数据格式可能会不同
"id":"1237365636208922624",
"username":"zhangsan",
"nickName":"xiaozhang",
"phone":"1886702304"
}
}
原因:
这是因为数字大于16位时响应给前端时会丢失精度
这就需要后端在序列化时,把Long类型转换位String类型
解决:在创建响应实体类时添加注解1
2
3
4
5
6/**
- 用户ID
- 将Long类型数字进行json格式转化时,转成String格式类型
*/
private Long id;5.3接口实现
- 封装请求vo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24package com.itheima.vo.req;
import lombok.Data;
/**
* @author by itheima
* @Date 2021/12/30
* @Description 登录请求vo
*/
public class LoginReqVo {
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 验证码
*/
private String code;
} - 封装响应vo
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
39package com.itheima.vo.resq;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author by itheima
* @Date 2021/12/24
* @Description 登录后响应前端的vo
*/
public class LoginRespVo {
/**
* 用户ID
* 将Long类型数字进行json格式转化时,转成String格式类型
*/
private Long id;
/**
* 电话
*/
private String phone;
/**
* 用户名
*/
private String username;
/**
* 昵称
*/
private String nickName;
} - UserController
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
32package com.itheima.stock.controller;
import com.itheima.stock.service.UserService;
import com.itheima.stock.vo.req.LoginReqVo;
import com.itheima.stock.vo.resp.LoginRespVo;
import com.itheima.stock.vo.resp.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @author by itheima
* @Date 2021/12/29
* @Description 定义用户访问层
*/
public class UserController {
private UserService userService;
/**
* 用户登录功能实现
* @param vo
* @return
*/
public R<LoginRespVo> login({ LoginReqVo vo)
R<LoginRespVo> r= this.userService.login(vo);
return r;
}
} - UserService
1
R<LoginRespVo> login(LoginReqVo vo);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22//用户登录
public R<LoginRespVo> login(LoginReqVo vo) {
//1.判断参数是否合法
if (vo == null || StringUtils.isBlank(vo.getUsername())
|| StringUtils.isBlank(vo.getPassword()) || StringUtils.isBlank(vo.getCode())) {
return R.error(ResponseCode.DATA_ERROR);
}
//2.根据用户名查找用户信息,获取密码密文
SysUser dbUser = sysUserMapper.findByUserName(vo.getUsername());
if (dbUser == null){
return R.error(ResponseCode.ACCOUNT_NOT_EXISTS);
}
//3.调用密码匹配器,对比密码
if (!passwordEncoder.matches(vo.getPassword(), dbUser.getPassword())) {
return R.error(ResponseCode.USERNAME_OR_PASSWORD_ERROR);
}
//4.封装响应
LoginRespVo respVo = new LoginRespVo();
BeanUtils.copyProperties(dbUser,respVo);
return R.ok(respVo);
} - SysUserMapper
1
2
3
4
5
6/**
* 根据用户名称查询用户信息
* @param userName 用户名称
* @return
*/
SysUser findByUserName(; String userName)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<sql id="Base_Column_List">
id,username,password,
phone,real_name,nick_name,
email,status,sex,
deleted,create_id,update_id,
create_where,create_time,update_time
</sql>
<!--根据用户名称查询用户信息-->
<select id="findByUserName" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from sys_user
where username= #{name}
</select>6.验证码登录
6.1需求分析
我们可使用分布式缓存redis模拟session机制,实现验证码的生成和校验功能,核心流程如下:6.2接口定义
功能描述:验证码生成功能
请求路径:/api/captcha
请求参数:无
请求方式:get
响应数据格式:1
2
3
4
5
6
7{
"code": 1,
"data": {
"imageData": "iVBORw0KGgoAAAANSUh...省略...AAAPoAAAAoCAYAAADX=", //base64格式图片
"sessionId": "1479063316897845248" //保存在redis中验证码对应的key,模拟sessioinId
}
}
6.3集成redis
- 导入依赖
1
2
3
4
5
6
7
8
9
10<!--redis场景依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redis创建连接池,默认不会创建连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency> - 配置application-cache.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13spring:
# 配置缓存
redis:
host: 127.0.0.1
port: 6379
database: 0 #Redis数据库索引(默认为0)
lettuce:
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 8 # 连接池中的最大空闲连接
min-idle: 1 # 连接池中的最小空闲连接
timeout: PT10S # 连接超时时间 - 在application.yaml激活
1
2
3spring:
profiles:
active: cache #激活其他配置文件 - 自定义RedisTemplate序列化
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
39package com.itheima.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
public class RedisCacheConfig {
/**
* 配置redisTemplate bean,自定义数据的序列化的方式
* @param redisConnectionFactory 连接redis的工厂,底层有场景依赖启动时,自动加载
* @return
* jdk序列化方式问题:
* 1.阅读体验差
* 2.序列化后内容体积比较大,占用过多内存
*/
public RedisTemplate redisTemplate({ RedisConnectionFactory redisConnectionFactory)
//1.构建RedisTemplate模板对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//2.为不同的数据结构设置不同的序列化方案
//设置key序列化方式
template.setKeySerializer(new StringRedisSerializer());
//设置value序列化方式
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
//设置hash中field字段序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
//设置hash中value的序列化方式
template.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
//5.初始化参数设置
template.afterPropertiesSet();
return template;
}
} - 测试redis环境
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
27package com.itheima;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
/**
* @author by itheima
* @Date 2021/12/30
* @Description
*/
public class TestRedis {
private RedisTemplate<String,String> redisTemplate;
public void test01(){
//存入值
redisTemplate.opsForValue().set("myname","zhangsan");
//获取值
String myname = redisTemplate.opsForValue().get("myname");
System.out.println(myname);
}
}
6.4SessionID唯一性解决方案
- 雪花算法
雪花算法是Twitter公司内部为分布式环境下生成唯一ID的一种算法解决方案,底层会帮助我们生成一个64位(比特位)的long类型的Id; - 在stock_common导入雪花算法工具类
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
76
77
78
79
80
81
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148package com.itheima.utils;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;
/**
* 分布式自增长ID实现,底层基于Twitter的Snowflake
* 64位ID (42(时间戳)+5(机房ID)+5(机器ID)+12(序列号-同毫秒内重复累加))
* @author itheima
*/
public class IdWorker {
// 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
private final static long twepoch = 1288834974657L;
// 机器标识位数
private final static long workerIdBits = 5L;
// 数据中心标识位数
private final static long datacenterIdBits = 5L;
// 机器ID最大值
private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 数据中心ID最大值
private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// 毫秒内自增位
private final static long sequenceBits = 12L;
// 机器ID偏左移12位
private final static long workerIdShift = sequenceBits;
// 数据中心ID左移17位
private final static long datacenterIdShift = sequenceBits + workerIdBits;
// 时间毫秒左移22位
private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
/* 上次生产id时间戳 */
private static long lastTimestamp = -1L;
//同毫秒并发控制
private long sequence = 0L;
//机器ID
private final long workerId;
//机房ID
private final long datacenterId;
public IdWorker(){
this.datacenterId = getDatacenterId(maxDatacenterId);
this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
}
/**
* @param workerId
* 工作机器ID
* @param datacenterId
* 序列号
*/
public IdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
/**
* 获取下一个ID
*
* @return
*/
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
if (lastTimestamp == timestamp) {
// 当前毫秒内,则+1
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒内计数满了,则等待下一秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
// ID偏移组合生成最终的ID,并返回ID
long nextId = ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift) | sequence;
return nextId;
}
private long tilNextMillis(final long lastTimestamp) {
long timestamp = this.timeGen();
while (timestamp <= lastTimestamp) {
timestamp = this.timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
/**
* <p>
* 获取 maxWorkerId
* </p>
*/
protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
StringBuffer mpid = new StringBuffer();
mpid.append(datacenterId);
String name = ManagementFactory.getRuntimeMXBean().getName();
if (!name.isEmpty()) {
/*
* GET jvmPid
*/
mpid.append(name.split("@")[0]);
}
/*
* MAC + PID 的 hashcode 获取16个低位
*/
return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
}
/**
* <p>
* 数据标识id部分
* </p>
*/
protected static long getDatacenterId(long maxDatacenterId) {
long id = 0L;
try {
InetAddress ip = InetAddress.getLocalHost();
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
if (network == null) {
id = 1L;
} else {
byte[] mac = network.getHardwareAddress();
id = ((0x000000FF & (long) mac[mac.length - 1])
| (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
id = id % (maxDatacenterId + 1);
}
} catch (Exception e) {
System.out.println(" getDatacenterId: " + e.getMessage());
}
return id;
}
} - 在stock_backend工程配置ID生成器bean对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CommonConfig {
/**
* 基于雪花算法生成id的唯一值
* 配置id生成器bean
* @return
*/
public IdWorker idWorker(){
//基于运维人员对机房和机器的编号规划自行约定
//(机器ID,机房ID)
return new IdWorker(1l,2l);
}
}
6.5接口实现
- 在stock_backend工程引入图片验证码生成工具包
1
2
3
4
5<!--hutool万能工具包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency> - UserController
1
2
3
4
5//生成图片验证码
public R<Map> getCaptchaCode(){
return userService.getCaptchaCode();
} - UserService
1
2//生成图片验证码
R<Map> getCaptchaCode();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
private IdWorker idWorker;
private RedisTemplate redisTemplate;
//生成验证码图片
public R<Map> getCaptchaCode() {
//1.生成图片验证码
//(图片宽度,图片高度,图片中包含的验证码的长度,干扰线的数量)
LineCaptcha captcha = CaptchaUtil.createLineCaptcha(250, 40, 4, 5);
//设置背景图片
captcha.setBackground(Color.LIGHT_GRAY);
//2.获取校验码
String checkCode = captcha.getCode();
//3.获取经过base64编码处理的图片数据
String imageBase64 = captcha.getImageBase64();
//4.生成sessionId 转化为String避免前端精度丢失
String sessionId = String.valueOf(idWorker.nextId());
log.info("当前生成的图片校验码:{},会话id:{}",checkCode,sessionId);
//5.将sessinId作为Key,校验码作为value保存到redis中
redisTemplate.opsForValue().set("CK" + sessionId,checkCode,5, TimeUnit.MINUTES);
//6.封装响应
HashMap<String,String> data = new HashMap<>();
data.put("imageData",imageBase64);
data.put("sessionId",sessionId);
return R.ok(data);
}
7.完善登录功能
- 定义常量类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package com.itheima.constant;
/**
* @author by itheima
* @Description 常量类信息封装
*/
public class StockConstant {
/**
* 定义校验码的前缀
*/
public static final String CHECK_PREFIX="CK:";
/**
* http请求头携带Token信息key
*/
public static final String TOKEN_HEADER = "authorization";
/**
* 缓存股票相关信息的cacheNames命名前缀
*/
public static final String STOCK="stock";
} - LoginReqVo添加rdeis的key属性
1
2
3
4
5
6
7
8
9
public class LoginReqVo {
//.....
/**
* 保存redis随机码的key,也就是sessionId
*/
private String sessionId;
} - userService
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//用户登录
public R<LoginRespVo> login(LoginReqVo vo) {
//1.判断参数是否合法
if (vo == null || StringUtils.isBlank(vo.getUsername()) || StringUtils.isBlank(vo.getPassword())) {
return R.error(ResponseCode.DATA_ERROR);
}
//判断输入验证码是否存在
if(StringUtils.isBlank(vo.getCode()) || StringUtils.isBlank(vo.getSessionId())){
return R.error(ResponseCode.CHECK_CODE_NOT_EMPTY);
}
//判断redis中保存的验证码与输入的验证码是否相同(忽略大小写)
String redisCode = (String) redisTemplate.opsForValue().get(StockConstant.CHECK_PREFIX + vo.getSessionId());
if (StringUtils.isBlank(redisCode)) {
return R.error(ResponseCode.CHECK_CODE_TIMEOUT);
}
if (!redisCode.equalsIgnoreCase(vo.getCode())) {
return R.error(ResponseCode.CHECK_CODE_ERROR);
}
//2.根据用户名查找用户信息,获取密码密文
SysUser dbUser = sysUserMapper.findByUserName(vo.getUsername());
if (dbUser == null){
return R.error(ResponseCode.ACCOUNT_NOT_EXISTS);
}
//3.调用密码匹配器,对比密码
if (!passwordEncoder.matches(vo.getPassword(), dbUser.getPassword())) {
return R.error(ResponseCode.USERNAME_OR_PASSWORD_ERROR);
}
//4.封装响应
LoginRespVo respVo = new LoginRespVo();
BeanUtils.copyProperties(dbUser,respVo);
return R.ok(respVo);
}