Skip to content

Latest commit

 

History

History
913 lines (458 loc) · 23.6 KB

技术详情.md

File metadata and controls

913 lines (458 loc) · 23.6 KB

本 markdown 文档记录该项目学到的关键性知识,以及遇到的问题

整体架构

前端

vue, nuxt 框架

后端

maven 管理依赖

用微服务的方式分开多个模块

  • common 大模块:
    • service 共用的 utils
    • runtimeException
    • Jwt 加密包, MD5加密包
    • 数据库配置,swagger 配置等
  • service 大模块,下面放置许多 service 小模块
  • canal 模块,用于增量同步数据库

讲师和课程管理分为两部分

  • 后台课程管理,方便管理员在 web 端作增删改查
  • 前台页面展示,这是用户平常看到的部分

SpringCloud

  • nacos discovery
  • Feign
  • Hystrix
  • gateway

各模块功能

模块结构

---root

------common

------------common_utils

------------service_base

------service

------------acl

------------edu

------------cms

------------msm

------------order

------------oss

------------statistic

------------ucenter

------------vod

------infrastructure

------------api_gateway

maven 管理

​ 在 root 的 pom.xml 中用 <dependencyManagement> 管理所有引入包的版本,但这里的 <dependency> 标签实际上并没有进行引入。实际的引入可在 service 根目录或其子模块下进行。

​ 这样做的好处是,所有模块都用同样版本的包。并且要更换版本时,只需更改一处,而不用更改所有地方。

订单模块 service-order

image-20201102161128599

点击课程购买

  1. 生成订单接口
  2. 根据订单 id 查询订单信息
  3. 生成微信支付二维码
  4. 查询订单支付状态接口

权限管理模块

遇到的 Bugs

onedrive 引发的问题

开发目录不要放到 onedrive 下

开发目录不要放到 onedrive 下

开发目录不要放到 onedrive 下

  1. 起初 idea 总是遇到莫名其妙的问题,最后发现是 onedrive 同步的缘故,可能锁定了某些文件,导致 maven 管理频频出问题。
  2. 如果文件夹包含的文件数量过多,onedrive 直接复制会失效。这个 bug 很隐秘,你根本不知道为什么复制不了文件夹。解决方案是把文件夹打包成 rar,然后只复制这一个文件。

maven 子模块没有小蓝点

没有小蓝点,就意味着没有被 spring 管理。有时候 idea 或 maven 自身的故障会莫名其妙发生该问题。在idea 右侧添加该子模块的文件夹,即可加入 maven

课程首页幻灯片 banner 存不进 redis

报错

MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk

查看日志信息

6097:C 27 Oct 21:58:22.027 # Failed opening the RDB file root (in server root dir /etc/cron.d) for saving: Permission denied 24811:M 27 Oct 21:58:22.127 # Background saving error

  • 显然,是权限不足所致。
  • 修改该文件权限为 644 即可

Jwt 生成 token 报错

问题:Caused by: java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter

原因

  • JAXB API 是 java EE 的API,因此在java SE 9.0 中不再包含这个 Jar 包。
  • java 9 中引入了模块的概念,默认情况下,Java SE中将不再包含java EE 的Jar包,而在 java 6/7 / 8 时关于这个API 都是捆绑在一起的

解决方法

(1)将jdk版本降级,使用java8

(2)手动加入相关依赖

<dependency>
        <groupId>javax.xml.bind</groupId>
        <artifactId>jaxb-api</artifactId>
        <version>2.3.0</version>
    </dependency>
    <dependency>
        <groupId>com.sun.xml.bind</groupId>
        <artifactId>jaxb-impl</artifactId>
        <version>2.3.0</version>
    </dependency>
    <dependency>
        <groupId>com.sun.xml.bind</groupId>
        <artifactId>jaxb-core</artifactId>
        <version>2.3.0</version>
    </dependency>
    <dependency>
        <groupId>javax.activation</groupId>
        <artifactId>activation</artifactId>
        <version>1.1.1</version>
    </dependency>

图片 403 挂了,但是单独点开图片,并敲 enter 可以显示

在HTML代码的head中添加一句,如果用到 nuxt ,在 nuxt.config.js 添加

  head: {
    meta: [
      {name: 'referrer', content:'no-referrer'}
  }
  • http请求体的header中有一个referrer字段,用来表示发起http请求的源地址信息,这个referrer信息是可以省略但是不可修改的,就是说你只能设置是否带上这个referrer信息,不能定制referrer里面的值。
  • 服务器端在拿到这个referrer值后就可以进行相关的处理,比如图片资源,可以通过referrer值判断请求是否来自本站,若不是则返回403或者重定向返回其他信息,从而实现图片的防盗链。上面出现403就是因为,请求的是别人服务器上的资源,但把自己的referrer信息带过去了,被对方服务器拦截返回了403。
  • 在前端可以通过meta来设置referrer policy(来源策略),具体可以设置哪些值以及对应的结果参考这里。所以针对上面的403情况的解决方法,就是把referrer设置成no-referrer,这样发送请求不会带上referrer信息,对方服务器也就无法拦截了。

Nacos 注册不上服务

问题

​ 服务在 windows 上已经 standalone mode 启动,并且也在 bootsrap.yml 中进行了配置,但报错 server is down.

解决

​ 没改任何其他地方,把 nacos 部署到 linux 上,可以正常使用

​ 尽量不要使用 windows 部署服务!!!

​ 这问题搞了我几小时……心态非常崩溃,百度 google 遍了也无法解决

课程按某些变量排序没反应

查阅后端代码,发现是代码调用错了,比如本来是 getPrice 的地方,由于批量复制没有修改,还是与前面的 getSubjectId 一致

通用工具模块 service_base 无法打包,提示找不到主类

问题

Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.2.1.RELEASE:repackage (repackage) on project service_base: Execution repackage of goal org.springframework.boot:spring-boot-maven-plugin:2.2.1.RELEASE:repackage failed: Unable to find main class

解决

这是在 common 模块下的子模块,当然是没有 main class 的。但是它却要求你要有 main class,原因是 pom 文件没有配置好。在根目录下的 pom 不能配置 springboot-maven-plugin,而是要把它配置到 service 子模块中。这样 common 模块就不要求 main class 了。具体配置如下

            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <classifier>execute</classifier>
                </configuration>
            </plugin>

运行程序报错找不到 wxpay-sdk

问题

​ 问题出现在 service_order 这个模块。pom 文件已经引入了 wxpay ,并且语法检查 import 也没有灰或红色下划线,而是正常的。但是点 run 运行程序却报错找不到这个包。而 maven clean, compile, install 都是正常无报错。

解决

  • 浪费了非常非常多的时间……前后加起来一天了……算是我遇到最难解决的 bug。
  • 这是 idea 2020 版本的问题。除了恢复出厂设置,其他所有办法都试过了,没有用。

Feign 调用失败 404

问题

feign.FeignException$NotFound: status 404 reading UcenterClient#countRegister(String)

解决

一开始以为 Feign 的代码写的有问题,找了很久,发信是 statistic 模块 nacso 没注册上。但是为什么没注册上呢,每个模块的配置都是一样的,别的都注册上了。报的是找不到 config 的错误,我把 config 注释掉重启,又加上重启,就好了。原因猜测是缓存问题。

sqlSessionFactory

问题

Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required

解决

极大可能是 invalidate cache and restart 时,idea 还原了配置。而默认情况下,resource 文件夹没有 marked as 资源文件夹,因此没有检测到数据库配置。

canal 启动失败

问题

idea 显示 connection refuse。

Linux 服务器端查看 canal 日志,Java HotSpot(TM) 64-Bit Server VM warning: Ignoring option PermSize; support was removed in 8.0 Java HotSpot(TM) 64-Bit Server VM warning: Ignoring option MaxPermSize; support was removed in 8.0 Unrecognized VM option 'UseConcMarkSweepGC' Error: Could not create the Java Virtual Machine. Error: A fatal exception has occurred. Program will exit.

解决

  • 尝试用 docker,但又遇到内存不足的错误
  • 发现启动参数是由运行的 sh 文件设置的,那么我们根本不需要用 docker。
  • 手动更改 ${canal_home}/bin/startup.sh 中 JAVA 的目录,以及删除 java 大部分的启动参数。即可启动成功

Swagger

  • SwaggerConfig 放在 common.service_base 子模块
  • 在 web 端用来模拟数据传输,作调试
  • 在对应端口输入 /swagger-ui.html

统一数据返回接口

  • 在 service_base.common_utils 模块中设立返回类 R,然后每次在其他接口 return 时,都用这个返回类封装。
  • 如返回 R.ok(), R.error(),就返回了预先规定好的状态码
  • 用 hashmap

阿里云服务

引入相关依赖

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
        </dependency>

阿里云短信

  1. 阿里云网站开通短信服务:

    • 申请模板

    • 申请签名

  2. 验证码由自己生成,阿里云只负责发送

  3. 在 serviceImpl 中实现发送函数。不用手敲,复制阿里云 doc 的代码,改相关参数

    • phone
    • 签名
    • 模板 code
    • 验证码数据
    @GetMapping("send/{phone}")
    public R sendMsm(@PathVariable String phone){
        String code = redisTemplate.opsForValue().get(phone);
        if (!StringUtils.isEmpty(code)){
            return R.ok();
        }
        code = RandomUtil.getFourBitRandom();   // 用自己定义的规则生成验证码
        Map<String, Object> param = new HashMap<>();
        param.put("code", code);
        boolean isSend = msmService.send(param, phone);   // 用阿里云服务发送到手机
        if (isSend) {  // 5 分钟内不再重复发送
            redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES);
            return R.ok();
        }
        return R.error().message("send fail");
    }

由于阿里云开通最便宜的短信服务也要 180RMB,我就没开通了……swagger 测试无 bug,只是没交钱发不出去。服务器代码改用 "1234" 来模拟验证码。

阿里云视频

Redis的使用

缓存网站首页高频访问收据

如,幻灯片,课程,讲师。

  • 提高访问速度,减轻数据库压力

保存阿里云短信验证码

  • 服务器生成验证码时,把它存到 redis
  • 把用户提交的注册表单中的验证码,与 redis 中的验证码对比

MyBatisPlus

  • 用 MP 代码生成器生成 controller, mapper, entity, service 结构,每次只需要修改名称即可。
  • 用 MP 的 wrapper 控制查询条件
  • MybatisPlus 配置类:在该微服务模块下,新建一个Config.java,里面用来配置 MP 的各种插件
    • 分页插件 PaginationInterceptor
    • 逻辑删除 ISqlInjector
      • 在描述数据库表的 entity 文件夹下,给对应表对应字段配置逻辑删除 @TableLogic

常用注解:

@TableId

主键注解

@TableName

表名注解

@TableField

常用属性:

  • fill 自动填充策略
  • exist 是否为数据库表字段

联合查询

  • 如果涉及多表联合查询,那么对于单个表的 baseMapper, someService,是无法完成联查需求的。必须在 mapper 文件夹 interface 添加函数,然后在 xml 中写该函数的 sql 语句

  • mapper 语句默认不会被加入 baseMapper,我们必须要在 pom.xml 中加入,并且在 application.properties 中配置

查询特定值的数据

  1. 用 @Autowire 自动注入一个该数据库表的 service,不妨命名为 someService
  2. new 一个 wrapper,调用 wrapper.eq 把特定值放进去
  3. 用一个 List<> 容器保存 someService.list(wrapper) 返回的数据

这里的 someService.list 实质上调用的是 baseMapper.selectList(wrapper) 方法。如果我们是编写当前表的 serviceImpl 文件,查当前表中的数据,通常直接用 baseMapper。但如果在当前 service 中查询其他表的数据,那么必须自动注入 otherService

分页查询

  1. 新建一个 Page 类,在构造函数传入 page, limit
  2. 用 wrapper 设置规则
  3. 调用 baseMapper 完成分页查询
  4. 此时 pageParam 对象字段信息就是已经完成分页查询的。如, records 字段把内容取出。此外还有 getCurrent,hasNext 等字段,分别包含不同的信息
Page<EduTeacher> pageParam = new Page<>(page, limit);
QueryWrapper<EduTeacher> wrapper = new QueryWrapper<>();
wrapper.orderByDesc("id");
baseMapper.selectPage(pageParam, wrapper);
List<EduTeacher> records = pageParam.getRecords();

微服务组件

Nacos注册中心

把需要互相调用的微服务模块,在注册中心中进行注册,注册之后,实现互相调用

Feign 远程调用

在一个 interface 中,打上注解:@Component, @FeignClient(some-module) 。意为该微服务模块通过 SpringCloud Feign 调用

Hystrix 熔断器

GateWay 服务网关

Nginx反向代理

  • 统一前端端口,把不同的微服务代理到不同的端口
  • 解决 nuxt 框架的跨域问题:把 nxut 代理到 nginx 9500 端口的根目录。即把根目录转发到 3000 端口

用户注册

  • 用阿里云短信服务提供验证码,把验证码存到 redis
  • 用 MD5 加密密码,数据库存的是加密后的字符串
  • 检查数据库是否已存在相同用户名
        Integer count = baseMapper.selectCount(wrapper);
        if (count > 0) {
            throw new GuliException(20001, "注册失败");
        }
  • 检查一切无误后,用 baseMapper 插入数据库
        UcenterMember ucenterMember = new UcenterMember();
        ucenterMember.setMobile(mobile);
        ucenterMember.setNickname(nickname);
        ucenterMember.setPassword(MD5.encrypt(password));
        baseMapper.insert(ucenterMember);

Cookie 是怎么使用的

  • 用 axios.interceptor 拦截器,每次发请求前都把 cookie 放到 header 里面

  • 后端接口传入 HttpServletRequest request,调用 request.getHeader("token");即可取出前端发送的 token

  • 用 Jwt 包取出这个 token 的信息,即可查询到用户 id

            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
            Claims claims = claimsJws.getBody();
    		String memberId = (String)claims.get("id");

Cookie 不能跨域传输,如果要跨域传输 token,只能通过 url

单点登录(SSO)

  • 定义:在某一个服务器登陆后,其他模块所部署的服务器不用重复登陆

session 广播机制实现

  • 在一个服务器登陆,记录在 session 中,然后复制到所有其他服务器

  • 默认 30 分钟过期

  • 现在服务器数量较多,deprecated

cookie + redis 实现

1. 在项目中任何一个模块进行登陆后,把数据放到两个地方

redis

  • key : 生成唯一值
  • value: 存用户数据

cookie

  • 把 redis 里生成的 key 值放到 cookie 里面

2. 访问项目中其他模块,发送请求带着 cookie 进行发送

  • 获取 cookie 值,到 redis 查询,根据 key 查询,若查询到数据就是已登陆

token 实现

token 定义:按照一定规则生成字符串,字符串可以包含用户信息

  1. 在项目某个模块登陆后,用 jwt 生成 token 串,然后返回这个 token 串
    • 通过 cookie 返回
    • 通过地址栏返回 (redirect 一个地址,用 "?" 在最后拼接参数)
  2. 再去访问其他项目模块,访问时在地址栏带着 token。服务器根据该字符串获取用户信息,如果可以获取到,就是已登陆
  3. 前端设置 cookie,访问同级域名时,带上 cookie 发送
    @PostMapping("login")
    public R loginUser(@RequestBody UcenterMember member) {
        //member对象封装手机号和密码
        //调用service方法实现登录
        //返回token值,使用jwt生成
        String token = memberService.login(member);
        return R.ok().data("token",token);
    }

    @Override
    public String login(UcenterMember member) {
        //获取登录手机号和密码
        String mobile = member.getMobile();
        String password = member.getPassword();
        
        //手机号和密码非空判断
        if(StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)) {
            throw new GuliException(20001,"登录失败");
        }
        //判断手机号是否正确
        QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
        wrapper.eq("mobile",mobile);
        UcenterMember mobileMember = baseMapper.selectOne(wrapper);
        //判断查询对象是否为空
        if(mobileMember == null) {//没有这个手机号
            throw new GuliException(20001,"登录失败");
        }
        //判断密码
        //因为存储到数据库密码肯定加密的
        //把输入的密码进行加密,再和数据库密码进行比较
        //加密方式 MD5
        if(!MD5.encrypt(password).equals(mobileMember.getPassword())) {
            throw new GuliException(20001,"登录失败");
        }
        //判断用户是否禁用
        if(mobileMember.getIsDisabled()) {
            throw new GuliException(20001,"登录失败");
        }
        //登录成功
        //生成token字符串,使用jwt工具类
        String jwtToken = JwtUtils.getJwtToken(mobileMember.getId(), mobileMember.getNickname());
        return jwtToken;
    }

根据 token 获取用户信息

login 时,我们用 id 和 nickName 生成了 jwt,因此使用 id 来查询数据库得到 member

    @GetMapping("getMemberInfo")
    public R getMemberInfo(HttpServletRequest request) {
        //调用jwt工具类的方法。根据request对象获取头信息,返回用户id
        String memberId = JwtUtils.getMemberIdByJwtToken(request);
        //查询数据库根据用户id获取用户信息
        UcenterMember member = memberService.getById(memberId);
        return R.ok().data("userInfo",member);
    }

前端存了两个 cookie:

  • 一个是 login_cookie,里面包含 id 和 nickname 的 token
  • 一个是 member_cookie,里面存了用户数据库表的所有信息

退出登陆时,前端也要取消这两个 cookie。代码中是用 cookie.set 放一个空字符串

JWT

JWT 是官方生成 token 的规则

需要引入 jwt 的 maven 依赖

JWT 生成的 token 包含三部分:

  • jwt 头
  • 有效载荷
  • 签名哈希

微信登陆

开放系统授权

  • 用户名密码复制: 适用于一个公司内部的多个系统
  • 通用开发者 key : 适用于合作商或者授信的不同业务部门之间
  • 方法令牌: 接近OAuth2 方式,需要考虑如何管理令牌、颁发令牌、吊销令牌,需要统一的协议,因此就有了OAuth2协议。类似 token

微信登录是采用的 OAuth2 方式,流程与 OAuth2 一致

OAuth2的流程

有三个角色:

  • 客户应用
  • 授权服务器
  • 资源服务器

流程:

  1. 授权服务器负责生成Access Token, 并将Access Token 颁发给客户应用

  2. 客户应用带上Access Token 去访问用户数据

  3. 资源服务器负责从请求里取出 AccessToken,校验 Access Token 是否具有访问用户的权限,如果有则返回客户数据。

微信登陆流程:

1. 微信登陆准备

  1. 注册开发者资质
  2. 申请网站应用名称
  3. 需要域名地址

2. 生成微信扫描二维码

# 微信开放平台 appid
wx.open.app_id=wxed9954c01bb89b47
# 微信开放平台 appsecret
wx.open.app_secret=a7482517235173ddb4083788de60b90e
# 微信开放平台 重定向url
wx.open.redirect_url=http://guli.shop/api/ucenter/wx/callback
  • 直接请求微信提供的固定地址,向地址拼接参数
  • app_id, app_secret 注册资质后获得
  • redirect_url 为扫码确认后重定向的 url

3. 微信扫码得 code 和 state,通过 callback 后缀的 get 请求服务器

4. 后端用这个 code 和 app_id, app_secret 请求授权服务器

然后取出两个关键变量:

  • access_token : 访问凭证
  • openid:每个微信唯一标识

5. 拿着上一步的两个值,请求资源服务器,即可获得微信用户 ID,头像等信息

6. 后端把 openid 作为唯一性标识,将用户数据插入到数据库

7. 登陆完成,后端把用户信息通过 url + jwtToken 的方式返回到首页面

不使用 cookie 的原因是,cookie 不能跨域访问

流程图

image-20201029162058638

用到的技术点:

  • httpclient
  • json 转换工具
    • fastjson
    • gson
    • jackson

image-20201029205718665

微信支付

wxpay-sdk 导入不了,搁置

定时任务

  1. 启动类添加注解 @EnableScheduling
  2. 创建定时任务类,用注解 @Scheduled,在这个类里面使用表达式设置什么时候去执行
    • cron 表达式:设置执行规则 。用工具在线生成
  3. 创建一个每天凌晨的定时任务,把前一天数据进行查询,并添加到数据库

Cannal 数据同步工具

应用场景:

​ 采取服务调用获取其他数据库表的统计数据,这样耦合度高,效率相对较低。

​ 我们采取另一种实现方式,实时同步数据库表。例如我们要统计每天注册与登录人数,我们只需把其他数据库的会员表同步到统计库中,实现本地统计就可以了,这样效率更高,耦合度更低。

​ Canal就是一个很好的数据库同步工具

工作原理:

把自己伪装成MySQL slave,模拟MySQL slave的交互协议向MySQL Mater发送 dump协议,MySQL mater收到canal发送过来的dump请求,开始推送binary log给canal,然后canal解析binary log,再发送到存储目的地,比如MySQL,Kafka,Elastic Search等等。

image-20201104204353686

权限管理

权限管理包含三个功能模块:

  • 菜单管理
    • 菜单列表
    • 菜单增删改
  • 角色管理
    • 增删改查
    • 为角色分配菜单
  • 用户管理
    • 增删改查
    • 为用户分配角色

前端相关

NUXT服务器渲染

  • 客户端作普通的 ajax 请求,不利于搜索引擎 SEO,用 NUXT 可解决这个问题

![image-20201025154246064](D:\oneDrive_personal\OneDrive - mail.scut.edu.cn\Java_Project\guli\assets\技术详情\image-20201025154246064.png)

  • 用 nuxt 直接发 axios 请求会有 CROS 问题。在本项目中用 nginx 代理解决

登陆的前端流程

image-20201029135402586