今日目标
1. 完善基于前后端分用户验证码登录功能;
2. 理解验证码生成流程,并使用postman测试;
3. 理解并实现国内大盘数据展示功能;
4. 理解并实现国内板块数据展示功能;
5. 理解后端接口调试和前后端联调的概念;
1.验证码登录功能
1.1 验证码功能分析
1)前后端分离架构的session问题
单体架构session实现验证码流程:
![](https://i-blog.csdnimg.cn/blog_migrate/09e0d123d66c702adebd010aa8c8de95.png)
解释:
如果前端后端都在一个工程下 统一部暑 首先访问验证码接口 将生成校验码保存在后台session下
每个session都有一个sessionid 也就是说验证码在后台保存了一份 然后将sessionid放到cookie中
响应给前端 把验证码也给前端 这样子在前端我们能够拿到验证码 也可以拿到sessionid,这样子的话 当我们输入验证码,点击登录后 就可以把sessionid和验证码带回去给后台 因为cookie来自于服务器,不存在跨域问题问题 所以服务端接收到请求之后 就可以从cookie中解析到sessionid sessionid获取到之后 根据K:V值就可以获取验证码 然后在与后台根据sessionid获取保存的验证码进行对比
![](https://i-blog.csdnimg.cn/blog_migrate/7c2713f9a5cf9349f79b694a9622d99b.png)
但是当前我们的项目存在跨域问题 cookie不同源 前端的cookie是无法发送到服务器后端 所以后端拿不到前端的sessionid
当前我们的项目采用前后端分离的技术架构,因为前后端请求存在跨域问题,会导致请求无法携带和服务器对应的cookie,导致session失效,且后续服务端也会做集群方案部署,整体来看使用session方案带来的扩展和维护成本是比较高的!
2)验证码逻辑分析
我们可使用分布式缓存redis模拟session机制,实现验证码的生成和校验功能,核心流程如下:
![](https://i-blog.csdnimg.cn/blog_migrate/0cd8f4140b36207b2a20555521683f60.png)
解释:
第一次请求:
刚开始时 第一次 页面一加载 接口主动访问, 访问地址是api/captcha
![](https://i-blog.csdnimg.cn/blog_migrate/0a801ca907a6b300b0905d71c441b174.png)
请求经过代理之后 就来到了服务器后端 后端先生成验证码 然后把验证码保存在redis下 这样子的话 哪怕到时做集群 大家都访问的是共同的服务 都能做到共享 也就是由原来要放到session中的缓存数据 现在把他们放在公共的redis下(公共的缓存区域)
然后在把后台生成的验证码响应给前端 前端就能够得到后台随机生成的验证码 这样用户就可以存入这个验证码 之后 我们在点击登录 就可以发起了第二次请求
第二次请求
携带用户输入的验证码登录 地址是/api/login/ 这样就把前端用户输入的验证码转入后台 后台在从redis获取到验证码和前端传入的验证码进行比较 如果不是 就是销毁 如果是 就说明验证成功
思考:存储redis中验证码的key又是什么呢?
模拟sessionId ,我们可以借助工具类生成全局唯一ID;
![](https://i-blog.csdnimg.cn/blog_migrate/a545f475c83c1514f57f929928855580.png)
3)验证码生成接口说明
请求路径:/api/captcha
请求参数:无
响应数据格式:
{
"code": 1,
"data": {
"code": "5411", //响应的验证码
"rkey": "1479063316897845248" //保存在redis中验证码对应的key,模拟sessioinId
}
}
1.2.redis环境集成
前后端分离后,后台session无法共享使用,所以我们可以把验证码数据存入redis中,所以接下来,backend项目中先引 入redis的依赖:
<!--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>
<!--apache工具包,提供验证随机码工具类-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
![](https://i-blog.csdnimg.cn/blog_migrate/894f18236eba7ef5c79e21ac5f3cae6c.png)
yml配置redis:
spring:
# 配置缓存
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 # 连接超时时间(毫秒)
备注:因为我这里面部暑在本地 所以 host连接的是本地 如果在部暑到linux 那么就要写linux的ip
![](https://i-blog.csdnimg.cn/blog_migrate/89f5004a9ffe7e256fbd9412be4d9418.png)
添加RedisConfig redis Key序列化配置文件
package com.itheima.stock.config;
import org.springframework.cache.annotation.CachingConfigurerSupport;
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.StringRedisSerializer;
/**
* Redis配置类
*/
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
//默认的Key序列化器为:JdkSerializationRedisSerializer
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
注意:这个配置文件和视频里面的可能不行 因为我没有视频里面的配置文件
![](https://i-blog.csdnimg.cn/blog_migrate/cc4018bb3a33ee10aa0e471da732c370.png)
测试redis基础环境:
package com.itheima.stock;
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;
@SpringBootTest
public class TestRedisDemo {
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Test
public void test01(){
//存入值
redisTemplate.opsForValue().set("myname","zhangsan");
//获取值
String myname = redisTemplate.opsForValue().get("myname");
System.out.println(myname);
}
}
我的
![](https://i-blog.csdnimg.cn/blog_migrate/da6405c5150e91418ec846f5afa2d631.png)
视频的
![](https://i-blog.csdnimg.cn/blog_migrate/57cd5b5eafca8626064e3ec2d68c3e4f.png)
清除所有的Key:FLUSHALL
清除所有key
![](https://i-blog.csdnimg.cn/blog_migrate/54907dce6bea504e3ff4b22ce4e9d379.png)
测试结果
![](https://i-blog.csdnimg.cn/blog_migrate/66c417f921ae7ab9d3a5e90b8165bfcb.png)
获取到k
![](https://i-blog.csdnimg.cn/blog_migrate/054dfc63dda7c90fa0d80f700aa1943d.png)
1.3 验证码功能实现
1)配置id生成器
导入id生成器工具类:
![](https://i-blog.csdnimg.cn/blog_migrate/28b17f4139c0eb4edf947bd9a70762a6.png)
![](https://i-blog.csdnimg.cn/blog_migrate/c681d248362d1dc8710b2ed59e3fbfb2.png)
在CommonConfig类里面配置id
package com.itheima.stock.config;
import com.itheima.stock.utils.IdWorker;
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;
/*定义公共配置类*/
@Configuration
public class CommonConfig {
/**
* 配置基于雪花算法生成全局唯一id
* 参与运算的参数:时间戳+机房id+机器id+序列号
* 保证id唯一
* 配置id生成器bean
* @return
*/
@Bean
public IdWorker idWorker(){
//指定当前为1号机房的2号机器生成
return new IdWorker(2L,1L);
}
/**
* 密码加密器 定义密码加密器和解密器 bean
* BCryptPasswordEncoder方法采用SHA-256对密码进行加密
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
![](https://i-blog.csdnimg.cn/blog_migrate/50b33b51f8bf370499f81f7f729cb708.png)
2)定义web接口
在UserController接口定义访问方法:
视频
![](https://i-blog.csdnimg.cn/blog_migrate/e26aab8b4128c7c319bf0e54c80d7d1b.png)
我的
@RestController
@RequestMapping("/api")
public class UserController {
/* @GetMapping("/test")
public String getName(){
return "itheima";
}*/
@Autowired
private UserService userService;
/**
* 用户登录功能实现
* @param vo
* @return
*/
@PostMapping("/login")
public R<LoginRespVo> login(@RequestBody LoginReqVo vo){
return userService.login(vo);
}
/**
* 生成登录校验码
* 生成验证码
* map结构:
* code: xxx,
* rkey: xxx
* @return
*/
@GetMapping("/captcha")
public R<Map> generateCaptcha(){
return this.userService.generateCaptcha();
}
}
![](https://i-blog.csdnimg.cn/blog_migrate/2a81896e6eba723568a7e2f7487c91bb.png)
3)定义生成验证码服务
在UserService服务接口:
在UserController类中的generateCaptcha中Alt+回车跳转到UserService接口
/*定义用户服务接口*/
public interface UserService {
/*用户登录功能*/
R<LoginRespVo> login(LoginReqVo vo);
/*生成登录校验码*/
R<Map> generateCaptcha();
}
![](https://i-blog.csdnimg.cn/blog_migrate/241b116b342f231c00c36a2393c30ca2.png)
![](https://i-blog.csdnimg.cn/blog_migrate/5acb2a4b32f14aaa14803078d5dc14ba.png)
方法实现:
![](https://i-blog.csdnimg.cn/blog_migrate/ed83cf10345d69cd9698a3841a5a9911.png)
![](https://i-blog.csdnimg.cn/blog_migrate/2f2079c4716941fc72cf326869285ec7.png)
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired
private IdWorker idWorker;
@Autowired
private RedisTemplate redisTemplate;
/*
* 生成登录验证码
* */
@Override
public R<Map> generateCaptcha() {
//1.生成4位数字验证码
String checkCode = RandomStringUtils.randomNumeric(4);
//2.获取全局唯一id
//生成一个类似sessionid的id作为key,然后校验码作为value保存在redis下
String rkey=String.valueOf(idWorker.nextId());
//验证码存入redis中,并设置有效期1分钟
redisTemplate.opsForValue().set(rkey,checkCode,60, TimeUnit.SECONDS);
//3.组装数据 组装响应的map对象
HashMap<String, String> map = new HashMap<>();
map.put("rkey",rkey);
map.put("code",checkCode);
return R.ok(map);
}
}
理解代码:
第一步 生成一个随机码 调用第三方工具类 RandomStringUtils
第二步 用雪花算法保证全局唯一 生成一个Id
第三步 让sessionid/rkey做为key checkCode 验证码作为value 保存在redis下面
第四步 然后对应的rkey/ sessionid和验证码checkCode发给前台
![](https://i-blog.csdnimg.cn/blog_migrate/6e17a88d0192928f4a25554cb0ff9fd0.png)
![](https://i-blog.csdnimg.cn/blog_migrate/41d43970121105765dcd2664c3832b8a.png)
打断点
![](https://i-blog.csdnimg.cn/blog_migrate/3936d42b2c0d43af4c9589ba3df36848.png)
![](https://i-blog.csdnimg.cn/blog_migrate/b8595eabcd35f3843ab7d23f1f2ce6a7.png)
打开Postman
![](https://i-blog.csdnimg.cn/blog_migrate/773fd5a65c96bfab6778d0ecddf430d2.png)
![](https://i-blog.csdnimg.cn/blog_migrate/d96813123206301656816392c56e4c29.png)
为了测试 我们把存活时间修改成300秒 5分钟 正常生活中是一分钟
![](https://i-blog.csdnimg.cn/blog_migrate/c8803c34c2e8f5d562a133ff1e9ff487.png)
F8
![](https://i-blog.csdnimg.cn/blog_migrate/4d237420ce16f4db9800bfb66af86f92.png)
F8
![](https://i-blog.csdnimg.cn/blog_migrate/2f0cc978bf7c3d7011e43a54b97fbec5.png)
接下来我们把保存到redis下面
F8
![](https://i-blog.csdnimg.cn/blog_migrate/e72ac742d95c25d96df0d2405ad3e273.png)
![](https://i-blog.csdnimg.cn/blog_migrate/3ac3852fdc8b6f71d52cc7735d78b49e.png)
3次F8
![](https://i-blog.csdnimg.cn/blog_migrate/2cc0454ebb249284db2f4264ca5e2a6a.png)
F9
![](https://i-blog.csdnimg.cn/blog_migrate/d2d0a192e587432fa7d1d01cade5279f.png)
![](https://i-blog.csdnimg.cn/blog_migrate/e7e93e95f8f155ef133cd1af33b4c892.png)
![](https://i-blog.csdnimg.cn/blog_migrate/1b604e2c26f0f218769ca12663a7114a.png)
接下去 我们联调一下 注意打开这个前端浏览器 时 要去启动前端 如何启动看第一天
![](https://i-blog.csdnimg.cn/blog_migrate/b6f4c8636264a4c1f335693601b9e7fe.png)
刷新
![](https://i-blog.csdnimg.cn/blog_migrate/508a58043a2dada355f5865e2b5af65e.png)
刷新
![](https://i-blog.csdnimg.cn/blog_migrate/e969a49a4ffe05b5c5802f78cbf3a3a3.png)
![](https://i-blog.csdnimg.cn/blog_migrate/83e9558b0224a73c0e896680acf6e443.png)
F8
![](https://i-blog.csdnimg.cn/blog_migrate/3c2aaac28c0ff218e27dacb9cbe5b433.png)
![](https://i-blog.csdnimg.cn/blog_migrate/d6d97f3d636112912a72c8f595cbc6d6.png)
![](https://i-blog.csdnimg.cn/blog_migrate/afe83daaaf3c652fe9399c7d05dcd5fa.png)
![](https://i-blog.csdnimg.cn/blog_migrate/ea7c6c0968fe4121257a7dc51622098c.png)
接下来 我们来看一下前端代码 理解 一下rkey放在哪里
页面只要一加载 访问验证码的动作会初步触发 而这个是在vue的钩子函数下触发的
![](https://i-blog.csdnimg.cn/blog_migrate/1fb30925e74c894caadf53b541b3d38b.png)
![](https://i-blog.csdnimg.cn/blog_migrate/a6b87a97ab26e25fc1ee6a624c36720c.png)
为了使用方便 我们用idea打开前端代码
![](https://i-blog.csdnimg.cn/blog_migrate/f9a3d3d06524d65d9efe457223cd860b.png)
进入captcha()方法里面
![](https://i-blog.csdnimg.cn/blog_migrate/460e13a2e3b660d2c51c4d71aad6e795.png)
![](https://i-blog.csdnimg.cn/blog_migrate/5cbaec004dc14ce6db608f37e23a7622.png)
在后端只要拿到from 就可以拿到rkey
![](https://i-blog.csdnimg.cn/blog_migrate/dcbd8825d9a0bcdf17b912e6d58c9a6e.png)
1.4 完善验证码登录功能
看下前端代码
一点击登录 loginSubmit('form')就可以触发这个事件的完成
![](https://i-blog.csdnimg.cn/blog_migrate/21774504cfde942c413e4df71ccac087.png)
![](https://i-blog.csdnimg.cn/blog_migrate/11b4eaed3fdb96cc84c799af16613f19.png)
会传进来一个名称loginSubmit(name)
![](https://i-blog.csdnimg.cn/blog_migrate/02565a514e61129bd1157e2bf45d7472.png)
根据这个名称获取from对象 form对象下有username password code
![](https://i-blog.csdnimg.cn/blog_migrate/5b8325dd1ff5df7e812e59bd9e42389d.png)
在推送的时候应该要有rkey这个属性 但是我们之前封装的vo并没有rkey这个属性
![](https://i-blog.csdnimg.cn/blog_migrate/264c1bda1a6dd8551d07e37a7a6b6b50.png)
所以我们要添加一个rkey这个属性
![](https://i-blog.csdnimg.cn/blog_migrate/291acb23952cf2b5e682f2c689aa4ca6.png)
1)完善登录请求VO
LoginReqVo添加rkey属性:
package com.itheima.stock.vo.req;
import lombok.Data;
/*用户登录请求vo*/
@Data
public class LoginReqVo {
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 前端发送的验证码
*/
private String code;
/**
* 保存redis随机码的key
* 前端发送的sessionid
*/
private String rkey;
}
以后前端只要一登录 rkey就会带过去 带到后台的login中 @RequestBody一反序列化 LoginReqVo下 就能获取元素
![](https://i-blog.csdnimg.cn/blog_migrate/7c1ef614a481e5a643f6429fac6a9151.png)
![](https://i-blog.csdnimg.cn/blog_migrate/710ba351b327f21562be28c2c2bf78ff.png)
![](https://i-blog.csdnimg.cn/blog_migrate/1798c2bc001cc89b9da8b7a9165e3f7e.png)
获取到元素之后 把对象vo放到服务层进行逻辑处理
![](https://i-blog.csdnimg.cn/blog_migrate/9ee42f8dba17f5485e7780052374494e.png)
![](https://i-blog.csdnimg.cn/blog_migrate/1577f004ab5afd589869fc7ddad626c2.png)
![](https://i-blog.csdnimg.cn/blog_migrate/d382a1f642ffbfccd3b082cf8d2a24ed.png)
2)完善登录验证码逻辑
添加校验码校验功能:
第一种写法
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private IdWorker idWorker;
@Autowired
private RedisTemplate redisTemplate;
@Override
public R<LoginRespVo> login(LoginReqVo vo) {
//1.判断vo是否存在 或者用户名是否存在或都密码是否存在
if (vo == null || Strings.isNullOrEmpty(vo.getUsername()) || Strings.isNullOrEmpty(vo.getPassword())
|| Strings.isNullOrEmpty(vo.getCode()) || Strings.isNullOrEmpty(vo.getRkey())) {
//快速淘汰校验码,合理利用redis的内存空间
//redis清除key
redisTemplate.delete(vo.getRkey());
return R.error(ResponseCode.DATA_ERROR.getMessage());
}
//校验验证码
//获取redis中rey对应的验证码
//获取redis中rkey对应的code验证码
String redisCode = (String) redisTemplate.opsForValue().get(vo.getRkey());
//比对
if (redisCode==null || !redisCode.equals(vo.getCode())) {
return R.error(ResponseCode.DATA_ERROR.getMessage());
}
//快速淘汰校验码,合理利用redis的内存空间
//redis清除key
redisTemplate.delete(vo.getRkey());
//2. 根据用户名判断用户是否存在
SysUser userInfo = sysUserMapper.findUserInfoByUserName(vo.getUsername());
if (userInfo == null) {
return R.error(ResponseCode.DATA_ERROR.getMessage());
}
//3.判断密码,不匹配
if (!passwordEncoder.matches(vo.getPassword(), userInfo.getPassword())) {
return R.error(ResponseCode.SYSTEM_PASSWORD_ERROR.getMessage());
}
//4.属性赋值 两个类之间属性名称一致
LoginRespVo respVo = new LoginRespVo();
BeanUtils.copyProperties(userInfo,respVo);
return R.ok(respVo);
}
/*
* 生成登录验证码
* */
@Override
public R<Map> generateCaptcha() {
//1.生成4位数字验证码
String checkCode = RandomStringUtils.randomNumeric(4);
//2.获取全局唯一id
//生成一个类似sessionid的id作为key,然后校验码作为value保存在redis下
String rkey=String.valueOf(idWorker.nextId());
//验证码存入redis中,并设置有效期1分钟
redisTemplate.opsForValue().set(rkey,checkCode,300, TimeUnit.SECONDS);
//3.组装数据 组装响应的map对象
HashMap<String, String> map = new HashMap<>();
map.put("rkey",rkey);
map.put("code",checkCode);
return R.ok(map);
}
}
![](https://i-blog.csdnimg.cn/blog_migrate/9052707366da464791f4a65b8675a593.png)
第二种写法
/**
* 用户登录服务实现
* @param vo
* @return
*/
@Override
public R<LoginRespVo> login(LoginReqVo vo) {
if (vo==null || Strings.isNullOrEmpty(vo.getUsername())
|| Strings.isNullOrEmpty(vo.getPassword()) || Strings.isNullOrEmpty(vo.getRkey())){
return R.error(ResponseCode.DATA_ERROR.getMessage());
}
//验证码校验
//获取redis中rkey对应的code验证码
String rCode = (String) redisTemplate.opsForValue().get(vo.getRkey());
//校验
if (Strings.isNullOrEmpty(rCode) || !rCode.equals(vo.getCode())) {
return R.error(ResponseCode.DATA_ERROR.getMessage());
}
//redis清除key
redisTemplate.delete(vo.getRkey());
//根据用户名查询用户信息
SysUser user=this.sysUserMapper.findByUserName(vo.getUsername());
//判断用户是否存在,存在则密码校验比对
if (user==null || !passwordEncoder.matches(vo.getPassword(),user.getPassword())){
return R.error(ResponseCode.SYSTEM_PASSWORD_ERROR.getMessage());
}
//组装登录成功数据
LoginRespVo respVo = new LoginRespVo();
BeanUtils.copyProperties(user,respVo);
return R.ok(respVo);
}
打断点
![](https://i-blog.csdnimg.cn/blog_migrate/85d7c7c198e44f5ea7f412c3dae2b709.png)
![](https://i-blog.csdnimg.cn/blog_migrate/dc1b06734bbdf9b64022f517de08dbe0.png)
![](https://i-blog.csdnimg.cn/blog_migrate/b388ddc43633de260794054f85479f11.png)
![](https://i-blog.csdnimg.cn/blog_migrate/339cd1aef3bcd102bffd012ddc9b0a35.png)
![](https://i-blog.csdnimg.cn/blog_migrate/524e4845421f0e47f6a34aebffde3849.png)
![](https://i-blog.csdnimg.cn/blog_migrate/bec8dbbbda851de586e590d6d6740e7f.png)
![](https://i-blog.csdnimg.cn/blog_migrate/8a18ec84e432b046b1e419cc5d79bed9.png)
![](https://i-blog.csdnimg.cn/blog_migrate/296464de2851c92b3f594061c9eb4ae6.png)
2次F8
![](https://i-blog.csdnimg.cn/blog_migrate/2beb099da3ab95959f24fcb8edf4ff27.png)
F8
![](https://i-blog.csdnimg.cn/blog_migrate/2b9d35627935efc011aa575842f631ad.png)
发现是个null
先把他放行 F9
![](https://i-blog.csdnimg.cn/blog_migrate/67d34db8dc64d3deef097464fa23eb01.png)
可能原因是时间太快了(过期了) 所以为了方便演示 我们把时间调大点 我这里面直接修改成30分钟
![](https://i-blog.csdnimg.cn/blog_migrate/9927822823cd10678d1663063f74c668.png)
重启
![](https://i-blog.csdnimg.cn/blog_migrate/03da0849d37125ff31f7e4114d3a8b62.png)
![](https://i-blog.csdnimg.cn/blog_migrate/50be5857fc4ad851fd53e383640ff281.png)
![](https://i-blog.csdnimg.cn/blog_migrate/f5d386a24d13af44260ec74f38401cc6.png)
![](https://i-blog.csdnimg.cn/blog_migrate/5c4cdfd46c09f3eca1faf18a87bd4057.png)
![](https://i-blog.csdnimg.cn/blog_migrate/d898a8e0135c01c01c9342f38ca26393.png)
2次F8
![](https://i-blog.csdnimg.cn/blog_migrate/c2da0cf99d694c89f87a1e908cf7de1b.png)
F8
![](https://i-blog.csdnimg.cn/blog_migrate/eea9673278deb5eeb9c874365984dba9.png)
3次F8
![](https://i-blog.csdnimg.cn/blog_migrate/199e95b55a120356ddc90628dd8a2b23.png)
![](https://i-blog.csdnimg.cn/blog_migrate/d6653100817dc7037662f9e9cae25e8e.png)
![](https://i-blog.csdnimg.cn/blog_migrate/cf0a4db65dd92e3967337cc781308db4.png)
5次F8后放行
![](https://i-blog.csdnimg.cn/blog_migrate/ee910f8f1772747b6b1527cd24a65197.png)
3)登录测试联调
页面登录效果:
![](https://i-blog.csdnimg.cn/blog_migrate/a7002339d0aa06738e6c76fd7b390d25.png)
2.国内大盘指数功能
2.1国内大盘指数业务分析
1)页面原型效果
大盘展示需要的字段:
![](https://i-blog.csdnimg.cn/blog_migrate/4e371cda990edb4d4abe371b500c651b.png)
国内大盘数据包含:
大盘代码、名称、前收盘价、开盘价、最新价、涨幅、总金额、总手、当前日期
2)相关表结构分析
大盘指数包含国内和国外的大盘数据,目前我们先完成国内大盘信数据的展示功能;
股票大盘数据详情表(stock_market_index_info)设计如下:
![](https://i-blog.csdnimg.cn/blog_migrate/7f934fee35b917e3c0119d061e490b79.png)
相关的开盘与收盘流水表(stock_market_log_price)设计如下:
![](https://i-blog.csdnimg.cn/blog_migrate/ca965c16960a38f91b108ad932891e76.png)
开盘和收盘流水表与股票大盘数据详情表通过market_code进行业务关联,同时一个大盘每天只产生一条开盘与收盘流水数据,该数据,后期通过定时任务统计获取;
显然后去上述数据,需要大盘表和价格流水表的联合查询;
3)国内大盘数据注意事项
1.如果当前时间没有最新的大盘数据,则显示最近有效的大盘数据信息;
比如:今天是周1上午九点,则显示上周五收盘时的大盘数据信息;☹☹☹
2.当前大盘的数据采集频率为一分钟一次;
工程直接导入日期工具类:今日指数\day02\资料\date工具类\DateTimeUtil.java
![](https://i-blog.csdnimg.cn/blog_migrate/b2893e473a965c4012d2d60156b290a0.png)
![](https://i-blog.csdnimg.cn/blog_migrate/11fb9cecd0064472d66fbe834389f1d1.png)
![](https://i-blog.csdnimg.cn/blog_migrate/4a2f038e21565c1731a599529769fe4e.png)
打开类结构路径 Alt+7
![](https://i-blog.csdnimg.cn/blog_migrate/6ad23550b336c8ee57c1fe6e2046b98f.png)
![](https://i-blog.csdnimg.cn/blog_migrate/a1cc587397b499751a22d3ccdf5be020.png)
![](https://i-blog.csdnimg.cn/blog_migrate/e35bdca902cf964cc120f39ad22b5fbb.png)
![](https://i-blog.csdnimg.cn/blog_migrate/b7c130444e969325462f2cd4b3949537.png)
如果出现以下情况 说明 没有编译
![](https://i-blog.csdnimg.cn/blog_migrate/41eba89017339ef7264e8d5822c34b0e.png)
在这里面看看有没有编译
![](https://i-blog.csdnimg.cn/blog_migrate/c5f0a6500c559509a40b7ecbbd146f37.png)
查看以下内容 发现并没有Util
![](https://i-blog.csdnimg.cn/blog_migrate/84d796a3b54d6d6b26d7c6378434e6af.png)
![](https://i-blog.csdnimg.cn/blog_migrate/250789050a4a9ff65b92dd0678b2a8eb.png)
![](https://i-blog.csdnimg.cn/blog_migrate/784d8d6851dd882ba7d7fc2450f8de75.png)
![](https://i-blog.csdnimg.cn/blog_migrate/7f59eb65c1029b45e82f0bf484aa388e.png)
![](https://i-blog.csdnimg.cn/blog_migrate/71bcc6744126a4c4b7edbb58dc46663e.png)
先判断是否在工作日
![](https://i-blog.csdnimg.cn/blog_migrate/ff7059cfbb3adec5a077a53d76399b58.png)
![](https://i-blog.csdnimg.cn/blog_migrate/7c2a4ec58f533c10dd38cd0ec100015b.png)
![](https://i-blog.csdnimg.cn/blog_migrate/2806d1382bccede4f1c12b3e84222d1d.png)
F8
![](https://i-blog.csdnimg.cn/blog_migrate/47690e8638beeba81f9bf5575a9284ab.png)
Shift+F8
![](https://i-blog.csdnimg.cn/blog_migrate/eeecf9d938146e50fd9aa820cf03286b.png)
![](https://i-blog.csdnimg.cn/blog_migrate/67ffdf75b1f306c91cda1cb50a0eb725.png)
F8
![](https://i-blog.csdnimg.cn/blog_migrate/f53dccd91973ee11c02ddfce76efa644.png)
![](https://i-blog.csdnimg.cn/blog_migrate/bc12347653ce99ccdd6839c021c1b47c.png)
![](https://i-blog.csdnimg.cn/blog_migrate/004f0d12a4d7479195809ae38789fdb7.png)
可以用Debug慢慢调理(先放着)
jode和工具类使用参考:day02\资料\date工具类\TestJodeDate.java
![](https://i-blog.csdnimg.cn/blog_migrate/44a5e52a34affa3e02f6d3b0b39434a4.png)
![](https://i-blog.csdnimg.cn/blog_migrate/754753c1bdcbdec99a4a3c93c9be95e1.png)
![](https://i-blog.csdnimg.cn/blog_migrate/68f07b28a10c584b7427930defce82d0.png)
![](https://i-blog.csdnimg.cn/blog_migrate/656fff5942c8fd876f662f5a6143c790.png)
获取joda提供的date
![](https://i-blog.csdnimg.cn/blog_migrate/601b3b21731ce3f47d01b2a23d04f32e.png)
获取java提供的date
理论上上可以直接
![](https://i-blog.csdnimg.cn/blog_migrate/c4376440cb00d794c40421aa2248ecc0.png)
![](https://i-blog.csdnimg.cn/blog_migrate/e9c3232fb800a3a9b9084d2822a6463d.png)
或是用
![](https://i-blog.csdnimg.cn/blog_migrate/02292d61df507ee558da4449820f831b.png)
主要学习以下的
第一个Test
DateTime.now().withDate(2022, 1, 9); 2022年1月9号不变 时分秒为当前系统的时间 因为时分秒没有设置
![](https://i-blog.csdnimg.cn/blog_migrate/38edd9ff50056de52a0944d83bf937eb.png)
DateTime.now().withDate(2022, 1, 10).withHourOfDay(9).withMinuteOfHour(0);
2022年1月10号9点02发不变 秒发生变化 为系统时间
![](https://i-blog.csdnimg.cn/blog_migrate/c5c569a669604a38403e7abb43825d4c.png)
![](https://i-blog.csdnimg.cn/blog_migrate/b775c0e980fc1aa2db0190ff84a4c33d.png)
第二个Test
//获取jode下的当前时间
DateTime now = DateTime.now();
//日期后退指定的时间 当前的时期向后加一天
DateTime plusDay = now.plusDays(1);
System.out.println(plusDay);
![](https://i-blog.csdnimg.cn/blog_migrate/71a69b097a5ceea6e8f739e4d31e0b6e.png)
//前推指定的时间
DateTime now = DateTime.now();
DateTime preDate = now.minusDays(5);
System.out.println(preDate);
![](https://i-blog.csdnimg.cn/blog_migrate/8edd27d61a3bfa7bb0dbeb5e9d68709d.png)
4)国内大盘指数接口说明
请求路径:/api/quot/index/all
请求方式:GET
参数:无
响应数据格式:
{
"code": 1,
"data": [
{
"tradeAmt": 235158296,//交易量
"preClosePrice": 78.9,//前收盘价格
"code": "s_sz399001",//大盘编码
"name": "深证成指",//大盘名称
"curDate": "202112261056",// 当前日期
"openPrice": 79.2,//开盘价
"tradeVol": 32434490,//交易金额
"upDown": -0.89,//涨幅
"tradePrice": -131.52//当前价格
},
{
"tradeAmt": 1627113,
"code": "s_sh000001",
"name": "上证指数",
"curDate": "202112261056",
"tradeVol": 21549808,
"upDown": -0.56,
"tradePrice": -20.26
}
]
}
![](https://i-blog.csdnimg.cn/blog_migrate/1009859f94af10efc143600e83801789.png)
cur_time(当前时间)----curDate(当前日期)
mark_name(指数名称)---
cur_point(当前点数)---
current_price(当前价格)---preClosePrice(前收盘价) openPrice(开盘价)
updown_rate(涨跌率)--
trade_account(成交量(多少手))--tradeAmt(交易量) tradeVol(交易金额)
trade_volume(成交额(万元))
5)DO封装
注意:直接导入day02\资料\domain\InnerMarketDomain.java
![](https://i-blog.csdnimg.cn/blog_migrate/ca5a3936f8c401b82392610a9ff10a48.png)
![](https://i-blog.csdnimg.cn/blog_migrate/625b0febe1ad1d8866d310c0c8235abb.png)
![](https://i-blog.csdnimg.cn/blog_migrate/0b8960adf7a51c2764c390374d0fd9d7.png)
2.2 国内大盘指数功能实现
先分析sql
![](https://i-blog.csdnimg.cn/blog_migrate/a921e2fcbfb45ce905da7f6b1b10edc6.png)
# 获取最新的国内大盘的数据信息 上证 深证---》s_sh000001、s_sz399001
# 最新:最近最新交易产生的数据 -- 2022-01-03 11:15:00
# 大盘流水表与大盘价格日统计表没有必然的联系(两张独立的表)
# 1.先查询主表信息
select
smi.trade_account as tradeAmt,
smi.mark_Id as code,
smi.mark_name as name,
date_format(smi.cur_time,'%Y%m%d%H%i') as curDate,
smi.trade_volume as tradeVol,
smi.updown_rate as upDown,
smi.current_price as tradePrice
from stock_market_index_info as smi
where smi.mark_Id in ('s_sh000001','s_sz399001')
and smi.cur_time='2022-01-03 11:15:00';
# 2.然后获取主表信息数据后(数据被压缩)关联日统计表查询
select
tmp.*,
sml.open_price as openPrice,
sml.pre_close_price as preClosePrice
from (select
smi.trade_account as tradeAmt,
smi.mark_Id as code,
smi.mark_name as name,
date_format(smi.cur_time,'%Y%m%d%H%i') as curDate,
smi.trade_volume as tradeVol,
smi.updown_rate as upDown,
smi.current_price as tradePrice
from stock_market_index_info as smi
where smi.mark_Id in ('s_sh000001','s_sz399001')
and smi.cur_time='2022-01-03 11:15:00') as tmp
left join stock_market_log_price as sml
on tmp.code=sml.market_code
and date_format(sml.cur_date,'%Y%m%d')=date_format('2022-01-03 11:15:00','%Y%m%d')
测试数据
![](https://i-blog.csdnimg.cn/blog_migrate/497d88612c32e3e407b0e8c497369c57.png)
1)常量数据封装
将大盘或外盘的常量数据配置在yml下:
`# 配置股票相关的参数
stock:
inner: # A股
- s_sh000001 # 上证ID
- s_sz399001 # 深证ID
outer: # 外盘
- int_dji # 道琼斯
- int_nasdaq # 纳斯达克
- int_hangseng # 恒生
- int_nikkei # 日经指数
- b_TWSE # 台湾加权
- b_FSSTI # 新加坡
![](https://i-blog.csdnimg.cn/blog_migrate/db1ca2f52105ebfc24c792ca8b193035.png)
实体类封装:
@ConfigurationProperties(prefix = "stock")
@Data
public class StockInfoConfig {
//a股大盘ID集合
private List<String> inner;
//外盘ID集合
private List<String> outer;
}
![](https://i-blog.csdnimg.cn/blog_migrate/c01c48b7958a4cd378b45aee90dcefb9.png)
在main启动类上开启实体类配置:
@SpringBootApplication
//@MapperScan("com.itheima.stock.mapper")
@EnableConfigurationProperties(StockInfoConfig.class) //开启配置初始化 加入IOC容器中
public class StockApp {
public static void main(String[] args) {
SpringApplication.run(StockApp.class, args);
}
}
![](https://i-blog.csdnimg.cn/blog_migrate/a665448ff12d8c763bc8d43f89c48234.png)
2)定义国内大盘web接口
@RestController
@RequestMapping("/api/quot")
public class StockController {
@Autowired
private StockService stockService;
//其它省略.....
/**
* 获取国内最新大盘指数
* @return
*/
@GetMapping("/index/all")
public R<List<InnerMarketDomain>> innerIndexAll(){
return stockService.innerIndexAll();
}
}
3)定义国内大盘数据服务
服务接口:
public interface StockService {
//其它省略......
/**
* 获取国内大盘的实时数据
* @return
*/
R<List<InnerMarketDomain>> innerIndexAll();
}
服务接口实现:
`@Service("stockService")
public class StockServiceImpl implements StockService {
@Autowired
private StockBusinessMapper stockBusinessMapper;
@Autowired
private StockMarketIndexInfoMapper stockMarketIndexInfoMapper;
@Autowired
private StockInfoConfig stockInfoConfig;
@Override
public List<StockBusiness> getAllStockBusiness() {
return stockBusinessMapper.findAll();
}
/**
* 获取国内大盘的实时数据
* @return
*/
@Override
public R<List<InnerMarketDomain>> innerIndexAll() {
//1.获取国内大盘的id集合
List<String> innerIds = stockInfoConfig.getInner();
//2.获取最近最新的股票有效交易日
Date lDate = DateTimeUtil.getLastDate4Stock(DateTime.now()).toDate();
//mock数据
String mockDate="20211226105600";//TODO后续大盘数据实时拉去,将该行注释掉 传入的日期秒必须为0
lDate = DateTime.parse(mockDate, DateTimeFormat.forPattern("yyyyMMddHHmmss")).toDate();
//3.调用mapper查询指定日期下对应的国内大盘数据
List<InnerMarketDomain> maps=stockMarketIndexInfoMapper.selectByIdsAndDate(innerIds,lDate);
//组装响应的额数据
if (CollectionUtils.isEmpty(maps)) {
return R.error(ResponseCode.NO_RESPONSE_DATA.getMessage());
}
return R.ok(maps);
}
}
4)定义mapper接口方法和xml
mapper下定义接口方法和xml:
/**
* 根据注定的id集合和日期查询大盘数据
* @param ids 大盘id集合
* @param lastDate 对应日期
* @return
*/
List<InnerMarketDomain> selectByIdsAndDate(@Param("ids") List<String> ids, @Param("lastDate") Date lastDate);
XML绑定SQL分析:
SQL分析思路:
从设计角度看,大盘价格流水表和大盘实时流水表没有必然的练习,对于价格流水表仅仅记录当天的开盘价和前一个交易日的收盘价,也就是一个交易日仅产生一条数据,而大盘的实时流水则会产生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');
定义mapper接口绑定SQL:
<select id="selectByIdsAndDate" resultType="com.itheima.stock.common.domain.InnerMarketDomain">
SELECT
tmp.mark_Id AS code,
tmp.mark_name AS name,
sml.pre_close_price AS preClosePrice,
sml.open_price AS openPrice,
tmp.current_price AS tradePrice,
tmp.updown_rate AS upDown,
tmp.trade_account AS tradeAmt,
tmp.trade_volume AS tradeVol,
DATE_FORMAT( tmp.cur_time, '%Y%m%d') AS curDate
FROM
(
SELECT * FROM stock_market_index_info AS smi
WHERE smi.cur_time =#{lastDate}
AND smi.mark_Id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
) AS tmp
LEFT JOIN stock_market_log_price AS sml ON tmp.mark_Id=sml.market_code
AND DATE_FORMAT( sml.cur_date, '%Y%m%d' )= DATE_FORMAT(#{lastDate},'%Y%m%d' )
</select>
5)接口测试
postman:http://127.0.0.1:8080/api/quot/index/all
![](https://i-blog.csdnimg.cn/blog_migrate/a6e41731404d76a1c3959ab41cb2946f.png)
页面最终显示效果:
![](https://i-blog.csdnimg.cn/blog_migrate/6b46fe644ee122472b87f0bea690c613.png)
3.板块指数功能实现
3.1 国内板块指数业务分析
1)功能原型效果
![](https://i-blog.csdnimg.cn/blog_migrate/c7b69286f7aa5cb223295bc7f8e160dc.png)
2)板块表数据分析
stock_block_rt_info板块表分析:
![](https://i-blog.csdnimg.cn/blog_migrate/e38a8c467ca3a1b85e5a57a53199b9a4.png)
板块表涵盖了业务所需的所有字段数据。
3)国内板块接口说明
需求说明: 沪深两市板块分时行情数据查询,以交易时间和交易总金额降序查询,取前10条数据
请求URL: /api/quot/sector/all
请求方式: GET
请求参数: 无
接口响应数据格式:
{
"code": 1,
"data": [
{
"companyNum": 247,//公司数量
"tradeAmt": 5065110316,//交易量
"code": "new_dzxx",//板块编码
"avgPrice": 14.571,//平均价格
"name": "电子信息",//板块名称
"curDate": "20211230",//当前日期
"tradeVol": 60511659145,//交易总金额
"updownRate": 0.196//涨幅
},
{
"companyNum": 155,
"tradeAmt": 4281655990,
"code": "new_swzz",
"avgPrice": 22.346,
"name": "生物制药",
"curDate": "20211230",
"tradeVol": 52026876373,
"updownRate": -0.068
}
]
}
4)DO封装
package com.itheima.stock.pojo;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 股票板块详情信息表
* @TableName stock_block_rt_info
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class StockBlockRtInfo implements Serializable {
/**
* 板块主键ID(业务无关)
*/
private String id;
/**
* 表示,如:new_blhy-玻璃行业
*/
private String label;
/**
* 板块名称
*/
private String blockName;
/**
* 公司数量
*/
private Integer companyNum;
/**
* 平均价格
*/
private BigDecimal avgPrice;
/**
* 涨跌幅
*/
private BigDecimal updownRate;
/**
* 交易量
*/
private Long tradeAmount;
/**
* 交易金额
*/
private BigDecimal tradeVolume;
/**
* 当前日期(精确到秒)
*/
private Date curTime;
private static final long serialVersionUID = 1L;
}
3.2 国内板块指数功能实现
1)定义板块web访问接口方法
/**
*需求说明: 沪深两市板块分时行情数据查询,以交易时间和交易总金额降序查询,取前10条数据
* @return
*/
@GetMapping("/sector/all")
public R<List<StockBlockRtInfo>> sectorAll(){
return stockService.sectorAllLimit();
}
2)定义服务方法和实现
服务接口方法:
/**
*需求说明: 沪深两市板块分时行情数据查询,以交易时间和交易总金额降序查询,取前10条数据
* @return
*/
R<List<StockBlockRtInfo>> sectorAllLimit();
方法实现:
//注入mapper接口
@Autowired
private StockBlockRtInfoMapper stockBlockRtInfoMapper;
/**
*需求说明: 沪深两市板块分时行情数据查询,以交易时间和交易总金额降序查询,取前10条数据
* @return
*/
@Override
public R<List<StockBlockRtInfo>> sectorAllLimit() {
//1.调用mapper接口获取数据 TODO 优化 避免全表查询 根据时间范围查询,提高查询效率
List<StockBlockRtInfo> infos=stockBlockRtInfoMapper.sectorAllLimit();
//2.组装数据
if (CollectionUtils.isEmpty(infos)) {
return R.error(ResponseCode.NO_RESPONSE_DATA.getMessage());
}
return R.ok(infos);
}
3)定义mapper方法与xml
mapper接口方法:
/**
* 沪深两市板块分时行情数据查询,以交易时间和交易总金额降序查询,取前10条数据
* @return
*/
List<StockBlockRtInfo> sectorAllLimit();
定义mapper接口xml:
<select id="sectorAllLimit" resultType="com.itheima.stock.common.domain.StockBlockDomain">
select
sbr.company_num as companyNum,
sbr.trade_amount as tradeAmt,
sbr.label as code,
sbr.avg_price as avgPrice,
sbr.block_name as name,
date_format(sbr.cur_time,'%Y%m%d') as curDate,
sbr.trade_volume as tradeVol,
sbr.updown_rate as updownRate
from stock_block_rt_info as sbr
order by sbr.cur_time desc,sbr.trade_volume desc
limit 10
</select>
4) web接口测试
postman:http://127.0.0.1:8080/api/quot/sector/all
![](https://i-blog.csdnimg.cn/blog_migrate/1073ffd9b0e99e4181da8c0deb7ee38f.png)
前端页面效果:
![](https://i-blog.csdnimg.cn/blog_migrate/53fc36a706c35d90287cfb5f561685c7.png)