本 markdown 文档记录该项目学到的关键性知识,以及遇到的问题
vue, nuxt 框架
- common 大模块:
- service 共用的 utils
- runtimeException
- Jwt 加密包, MD5加密包
- 数据库配置,swagger 配置等
- service 大模块,下面放置许多 service 小模块
- canal 模块,用于增量同步数据库
- 后台课程管理,方便管理员在 web 端作增删改查
- 前台页面展示,这是用户平常看到的部分
- nacos discovery
- Feign
- Hystrix
- gateway
---root
------common
------------common_utils
------------service_base
------service
------------acl
------------edu
------------cms
------------msm
------------order
------------oss
------------statistic
------------ucenter
------------vod
------infrastructure
------------api_gateway
在 root 的 pom.xml 中用 <dependencyManagement>
管理所有引入包的版本,但这里的 <dependency>
标签实际上并没有进行引入。实际的引入可在 service 根目录或其子模块下进行。
这样做的好处是,所有模块都用同样版本的包。并且要更换版本时,只需更改一处,而不用更改所有地方。
点击课程购买
- 生成订单接口
- 根据订单 id 查询订单信息
- 生成微信支付二维码
- 查询订单支付状态接口
开发目录不要放到 onedrive 下
开发目录不要放到 onedrive 下
开发目录不要放到 onedrive 下
- 起初 idea 总是遇到莫名其妙的问题,最后发现是 onedrive 同步的缘故,可能锁定了某些文件,导致 maven 管理频频出问题。
- 如果文件夹包含的文件数量过多,onedrive 直接复制会失效。这个 bug 很隐秘,你根本不知道为什么复制不了文件夹。解决方案是把文件夹打包成 rar,然后只复制这一个文件。
没有小蓝点,就意味着没有被 spring 管理。有时候 idea 或 maven 自身的故障会莫名其妙发生该问题。在idea 右侧添加该子模块的文件夹,即可加入 maven
报错:
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 即可
问题: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>
在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信息,对方服务器也就无法拦截了。
问题:
服务在 windows 上已经 standalone mode 启动,并且也在 bootsrap.yml 中进行了配置,但报错 server is down.
解决:
没改任何其他地方,把 nacos 部署到 linux 上,可以正常使用
尽量不要使用 windows 部署服务!!!
这问题搞了我几小时……心态非常崩溃,百度 google 遍了也无法解决
查阅后端代码,发现是代码调用错了,比如本来是 getPrice 的地方,由于批量复制没有修改,还是与前面的 getSubjectId 一致
问题:
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>
问题:
问题出现在 service_order 这个模块。pom 文件已经引入了 wxpay ,并且语法检查 import 也没有灰或红色下划线,而是正常的。但是点 run 运行程序却报错找不到这个包。而 maven clean, compile, install 都是正常无报错。
解决:
- 浪费了非常非常多的时间……前后加起来一天了……算是我遇到最难解决的 bug。
- 这是 idea 2020 版本的问题。除了恢复出厂设置,其他所有办法都试过了,没有用。
问题:
feign.FeignException$NotFound: status 404 reading UcenterClient#countRegister(String)
解决:
一开始以为 Feign 的代码写的有问题,找了很久,发信是 statistic 模块 nacso 没注册上。但是为什么没注册上呢,每个模块的配置都是一样的,别的都注册上了。报的是找不到 config 的错误,我把 config 注释掉重启,又加上重启,就好了。原因猜测是缓存问题。
问题:
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 资源文件夹,因此没有检测到数据库配置。
问题:
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 大部分的启动参数。即可启动成功
- 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>
-
阿里云网站开通短信服务:
-
申请模板
-
申请签名
-
-
验证码由自己生成,阿里云只负责发送
-
在 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 中的验证码对比
- 用 MP 代码生成器生成 controller, mapper, entity, service 结构,每次只需要修改名称即可。
- 用 MP 的 wrapper 控制查询条件
- MybatisPlus 配置类:在该微服务模块下,新建一个Config.java,里面用来配置 MP 的各种插件
- 分页插件
PaginationInterceptor
- 逻辑删除
ISqlInjector
- 在描述数据库表的 entity 文件夹下,给对应表对应字段配置逻辑删除
@TableLogic
- 在描述数据库表的 entity 文件夹下,给对应表对应字段配置逻辑删除
- 分页插件
主键注解
表名注解
常用属性:
- fill 自动填充策略
- exist 是否为数据库表字段
-
如果涉及多表联合查询,那么对于单个表的 baseMapper, someService,是无法完成联查需求的。必须在 mapper 文件夹 interface 添加函数,然后在 xml 中写该函数的 sql 语句
-
mapper 语句默认不会被加入 baseMapper,我们必须要在 pom.xml 中加入,并且在 application.properties 中配置
- 用 @Autowire 自动注入一个该数据库表的 service,不妨命名为 someService
- new 一个 wrapper,调用 wrapper.eq 把特定值放进去
- 用一个 List<> 容器保存 someService.list(wrapper) 返回的数据
这里的 someService.list 实质上调用的是 baseMapper.selectList(wrapper) 方法。如果我们是编写当前表的 serviceImpl 文件,查当前表中的数据,通常直接用 baseMapper。但如果在当前 service 中查询其他表的数据,那么必须自动注入 otherService
- 新建一个 Page 类,在构造函数传入 page, limit
- 用 wrapper 设置规则
- 调用 baseMapper 完成分页查询
- 此时 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();
把需要互相调用的微服务模块,在注册中心中进行注册,注册之后,实现互相调用
在一个 interface 中,打上注解:@Component
, @FeignClient(some-module)
。意为该微服务模块通过 SpringCloud Feign 调用
- 统一前端端口,把不同的微服务代理到不同的端口
- 解决 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);
-
用 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");
- 定义:在某一个服务器登陆后,其他模块所部署的服务器不用重复登陆
-
在一个服务器登陆,记录在 session 中,然后复制到所有其他服务器
-
默认 30 分钟过期
-
现在服务器数量较多,deprecated
- key : 生成唯一值
- value: 存用户数据
- 把 redis 里生成的 key 值放到 cookie 里面
- 获取 cookie 值,到 redis 查询,根据 key 查询,若查询到数据就是已登陆
token 定义:按照一定规则生成字符串,字符串可以包含用户信息
- 在项目某个模块登陆后,用 jwt 生成 token 串,然后返回这个 token 串
- 通过 cookie 返回
- 通过地址栏返回 (redirect 一个地址,用 "?" 在最后拼接参数)
- 再去访问其他项目模块,访问时在地址栏带着 token。服务器根据该字符串获取用户信息,如果可以获取到,就是已登陆
- 前端设置 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;
}
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);
}
- 一个是 login_cookie,里面包含 id 和 nickname 的 token
- 一个是 member_cookie,里面存了用户数据库表的所有信息
退出登陆时,前端也要取消这两个 cookie。代码中是用 cookie.set 放一个空字符串
JWT 是官方生成 token 的规则
需要引入 jwt 的 maven 依赖
JWT 生成的 token 包含三部分:
- jwt 头
- 有效载荷
- 签名哈希
- 用户名密码复制: 适用于一个公司内部的多个系统
- 通用开发者 key : 适用于合作商或者授信的不同业务部门之间
- 方法令牌: 接近OAuth2 方式,需要考虑如何管理令牌、颁发令牌、吊销令牌,需要统一的协议,因此就有了OAuth2协议。类似 token
微信登录是采用的 OAuth2 方式,流程与 OAuth2 一致
有三个角色:
- 客户应用
- 授权服务器
- 资源服务器
流程:
-
授权服务器负责生成Access Token, 并将Access Token 颁发给客户应用
-
客户应用带上Access Token 去访问用户数据
-
资源服务器负责从请求里取出 AccessToken,校验 Access Token 是否具有访问用户的权限,如果有则返回客户数据。
- 注册开发者资质
- 申请网站应用名称
- 需要域名地址
# 微信开放平台 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
然后取出两个关键变量:
- access_token : 访问凭证
- openid:每个微信唯一标识
不使用 cookie 的原因是,cookie 不能跨域访问
- httpclient
- json 转换工具
- fastjson
- gson
- jackson
wxpay-sdk 导入不了,搁置
- 启动类添加注解
@EnableScheduling
- 创建定时任务类,用注解
@Scheduled
,在这个类里面使用表达式设置什么时候去执行- cron 表达式:设置执行规则 。用工具在线生成
- 创建一个每天凌晨的定时任务,把前一天数据进行查询,并添加到数据库
采取服务调用获取其他数据库表的统计数据,这样耦合度高,效率相对较低。
我们采取另一种实现方式,实时同步数据库表。例如我们要统计每天注册与登录人数,我们只需把其他数据库的会员表同步到统计库中,实现本地统计就可以了,这样效率更高,耦合度更低。
Canal就是一个很好的数据库同步工具
把自己伪装成MySQL slave,模拟MySQL slave的交互协议向MySQL Mater发送 dump协议,MySQL mater收到canal发送过来的dump请求,开始推送binary log给canal,然后canal解析binary log,再发送到存储目的地,比如MySQL,Kafka,Elastic Search等等。
权限管理包含三个功能模块:
- 菜单管理
- 菜单列表
- 菜单增删改
- 角色管理
- 增删改查
- 为角色分配菜单
- 用户管理
- 增删改查
- 为用户分配角色
- 客户端作普通的 ajax 请求,不利于搜索引擎 SEO,用 NUXT 可解决这个问题
![image-20201025154246064](D:\oneDrive_personal\OneDrive - mail.scut.edu.cn\Java_Project\guli\assets\技术详情\image-20201025154246064.png)
- 用 nuxt 直接发 axios 请求会有 CROS 问题。在本项目中用 nginx 代理解决