Merge branch 'master' of github.com:FleyX/technology-note
This commit is contained in:
commit
532f82d7fd
@ -1,17 +1,15 @@
|
|||||||
---
|
---
|
||||||
id: '2019-03-21-10-38'
|
id: "2019-03-21-10-38"
|
||||||
date: '2019/03/21 10:38:00'
|
date: "2019/03/21 10:38:00"
|
||||||
title: 'springboot超级详细的日志配置(基于logback)'
|
title: "springboot超级详细的日志配置(基于logback)"
|
||||||
tags: ['java', 'springboot', 'logback', 'log']
|
tags: ["java", "springboot", "logback", "log"]
|
||||||
categories:
|
categories:
|
||||||
- 'java'
|
- "java"
|
||||||
- 'spring boot学习'
|
- "spring boot学习"
|
||||||
---
|
---
|
||||||
|
|
||||||
![嘿](https://raw.githubusercontent.com/FleyX/files/master/blogImg/20190320135049.png)
|
![嘿](https://raw.githubusercontent.com/FleyX/files/master/blogImg/20190320135049.png)
|
||||||
|
|
||||||
**本文所用代码:**[github](https://github.com/FleyX/demo-project/tree/master/spring-boot/log-demo)
|
|
||||||
|
|
||||||
# 前言
|
# 前言
|
||||||
|
|
||||||
  java web 下有好几种日志框架,比如:logback,log4j,log4j2(slj4f 并不是一种日志框架,它相当于定义了规范,实现了这个规范的日志框架就能够用 slj4f 调用)。其中性能最高的应该使 logback 了,而且 springboot 默认使用的也是 logback 日志,所以本篇将会详细的讲解 logback 的日志配置方案。
|
  java web 下有好几种日志框架,比如:logback,log4j,log4j2(slj4f 并不是一种日志框架,它相当于定义了规范,实现了这个规范的日志框架就能够用 slj4f 调用)。其中性能最高的应该使 logback 了,而且 springboot 默认使用的也是 logback 日志,所以本篇将会详细的讲解 logback 的日志配置方案。
|
||||||
@ -263,5 +261,4 @@ appender 也是子节点之一,将会在后面说明。
|
|||||||
|
|
||||||
  日志打印可以很复杂, 这里只是说明了常用的 logback 配置,代码详见:[github](https://github.com/FleyX/demo-project/tree/master/spring-boot/log-demo)
|
  日志打印可以很复杂, 这里只是说明了常用的 logback 配置,代码详见:[github](https://github.com/FleyX/demo-project/tree/master/spring-boot/log-demo)
|
||||||
|
|
||||||
|
**本文原创发布于:**[http://www.tapme.top/blog/detail/2019-03-21-10-38](http://www.tapme.top/blog/detail/2019-03-20-10-38)
|
||||||
**本文原创发布于:**[http://www.tapme.top/blog/detail/2019-03-21-10-38](http://www.tapme.top/blog/detail/2019-03-20-10-38)
|
|
||||||
|
@ -2,16 +2,12 @@
|
|||||||
id: "2018-09-01-10-38"
|
id: "2018-09-01-10-38"
|
||||||
date: "2018/09/01 10:38:00"
|
date: "2018/09/01 10:38:00"
|
||||||
title: "springboot整合Mybatis(xml和注解)"
|
title: "springboot整合Mybatis(xml和注解)"
|
||||||
tag: ["java", "spring","springboot","mysql","mybatis","xml","注解"]
|
tag: ["java", "spring", "springboot", "mysql", "mybatis", "xml", "注解"]
|
||||||
categories:
|
categories:
|
||||||
- "java"
|
- "java"
|
||||||
- "spring boot学习"
|
- "spring boot学习"
|
||||||
---
|
---
|
||||||
|
|
||||||
## 写在前面
|
|
||||||
|
|
||||||
  项目源代码在 github,地址为:[https://github.com/FleyX/demo-project/tree/master/mybatis-test](https://github.com/FleyX/demo-project/tree/master/mybatis-test),有需要的自取。
|
|
||||||
|
|
||||||
  刚毕业的第一份工作是 java 开发,项目中需要用到 mybatis,特此记录学习过程,这只是一个简单 demo,mybatis 用法很多不可能全部写出来,有更复杂的需求建议查看 mybatis 的官方中文文档,[点击跳转](http://www.mybatis.org/mybatis-3/zh/index.html)。下面时项目环境/版本。
|
  刚毕业的第一份工作是 java 开发,项目中需要用到 mybatis,特此记录学习过程,这只是一个简单 demo,mybatis 用法很多不可能全部写出来,有更复杂的需求建议查看 mybatis 的官方中文文档,[点击跳转](http://www.mybatis.org/mybatis-3/zh/index.html)。下面时项目环境/版本。
|
||||||
|
|
||||||
- 开发工具:IDEA
|
- 开发工具:IDEA
|
||||||
@ -366,3 +362,7 @@ public interface UserDao {
|
|||||||
```
|
```
|
||||||
|
|
||||||
然后重新启动项目测试,测试结果跟上面完全一样。
|
然后重新启动项目测试,测试结果跟上面完全一样。
|
||||||
|
|
||||||
|
**本文原创发布于:**[https://www.tapme.top/blog/detail/2018-09-01-10-38](https://www.tapme.top/blog/detail/2018-09-01-10-38)
|
||||||
|
|
||||||
|
**源码地址:**[https://github.com/FleyX/demo-project/tree/master/mybatis-test](https://github.com/FleyX/demo-project/tree/master/mybatis-test).
|
||||||
|
@ -1,19 +1,15 @@
|
|||||||
---
|
---
|
||||||
id: '2019-02-22-14-59'
|
id: "2019-02-22-14-59"
|
||||||
date: '2019/02/22 14:59:00'
|
date: "2019/02/22 14:59:00"
|
||||||
title: 'springboot整合redis'
|
title: "springboot整合redis"
|
||||||
tag: ['java', 'spring-boot', 'redis', 'nosql', '缓存']
|
tag: ["java", "spring-boot", "redis", "nosql", "缓存"]
|
||||||
categories:
|
categories:
|
||||||
- 'java'
|
- "java"
|
||||||
- 'spring boot学习'
|
- "spring boot学习"
|
||||||
---
|
---
|
||||||
|
|
||||||
![薄暮传说](https://raw.githubusercontent.com/FleyX/files/master/blogImg/20190311141056.png)
|
![薄暮传说](https://raw.githubusercontent.com/FleyX/files/master/blogImg/20190311141056.png)
|
||||||
|
|
||||||
**本篇原创发布于:**[springboot 整合 redis](http://www.tapme.top/blog/detail/2019-02-22-14-59)
|
|
||||||
|
|
||||||
**项目源码:**:[github](https://github.com/FleyX/demo-project/tree/master/1.SSO%E5%8D%95%E7%82%B9%E7%99%BB%E5%BD%95/sso)
|
|
||||||
|
|
||||||
**特别说明**:本文针对的是新版 spring boot 2.1.3,其 spring data 依赖为 spring-boot-starter-data-redis,且其默认连接池为 lettuce
|
**特别说明**:本文针对的是新版 spring boot 2.1.3,其 spring data 依赖为 spring-boot-starter-data-redis,且其默认连接池为 lettuce
|
||||||
|
|
||||||
  redis 作为一个高性能的内存数据库,如果不会用就太落伍了,之前在 node.js 中用过 redis,本篇记录如何将 redis 集成到 spring boot 中。提供 redis 操作类,和注解使用 redis 两种方式。主要内容如下:
|
  redis 作为一个高性能的内存数据库,如果不会用就太落伍了,之前在 node.js 中用过 redis,本篇记录如何将 redis 集成到 spring boot 中。提供 redis 操作类,和注解使用 redis 两种方式。主要内容如下:
|
||||||
@ -29,13 +25,13 @@ categories:
|
|||||||
|
|
||||||
```yml
|
```yml
|
||||||
# docker-compose.yml
|
# docker-compose.yml
|
||||||
version: '2'
|
version: "2"
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
container_name: redis
|
container_name: redis
|
||||||
image: redis:3.2.10
|
image: redis:3.2.10
|
||||||
ports:
|
ports:
|
||||||
- '6379:6379'
|
- "6379:6379"
|
||||||
```
|
```
|
||||||
|
|
||||||
  然后在`docker-compose.yml`所在目录使用`docker-compose up -d`命令,启动 redis。
|
  然后在`docker-compose.yml`所在目录使用`docker-compose up -d`命令,启动 redis。
|
||||||
@ -283,3 +279,7 @@ public class RedisService {
|
|||||||
  手动控制就相当于 mybatis 的手写 sql 语句,需要调用`redisTemplate`中的各种方法来进行缓存查询,缓存更新,缓存删除等操作。
|
  手动控制就相当于 mybatis 的手写 sql 语句,需要调用`redisTemplate`中的各种方法来进行缓存查询,缓存更新,缓存删除等操作。
|
||||||
|
|
||||||
  使用方法参见 util/RedisUtil 中的方法。`redisTemplate`基本可以实现所有的 redis 操作。
|
  使用方法参见 util/RedisUtil 中的方法。`redisTemplate`基本可以实现所有的 redis 操作。
|
||||||
|
|
||||||
|
**本篇原创发布于:**[springboot 整合 redis](https://www.tapme.top/blog/detail/2019-02-22-14-59)
|
||||||
|
|
||||||
|
**项目源码:**:[github](https://github.com/FleyX/demo-project/tree/master/1.SSO%E5%8D%95%E7%82%B9%E7%99%BB%E5%BD%95/sso)
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
---
|
---
|
||||||
id: "2018-09-05-10-38"
|
id: "2018-09-05-10-38"
|
||||||
date: "2018/09/05 10:38:00"
|
date: "2018/09/05 10:38:00"
|
||||||
title: "springboot整合ActiveMQ(1)"
|
title: "springboot整合ActiveMQ1(基本使用)"
|
||||||
tags: ["java", "spring","springboot","消息队列","activeMQ"]
|
tags: ["java", "spring","springboot","消息队列","activeMQ"]
|
||||||
categories:
|
categories:
|
||||||
- "java"
|
- "java"
|
||||||
- "spring boot学习"
|
- "spring boot学习"
|
||||||
---
|
---
|
||||||
|
|
||||||
**说明:acitveMQ 版本为:5.9.1,springboot 版本为 2.0.3,项目地址:[点击跳转](https://github.com/FleyX/demo-project/tree/master/jms_demo)**<br/>
|
- 基本使用,[https://www.tapme.top/blog/detail/2018-09-05-10-38](https://www.tapme.top/blog/detail/2018-09-05-10-38)
|
||||||
|
- 主备模式,[https://www.tapme.top/blog/detail/2018-09-06-10-38](https://www.tapme.top/blog/detail/2018-09-06-10-38)
|
||||||
|
|
||||||
|
**说明**:acitveMQ 版本为:5.9.1,springboot 版本为 2.0.3
|
||||||
|
|
||||||
|
|
||||||
## 一. 下载安装(windows)
|
## 一. 下载安装(windows)
|
||||||
|
|
||||||
@ -141,3 +145,7 @@ public class Comsumer {
|
|||||||
![运行结果](https://raw.githubusercontent.com/FleyX/files/master/blogImg/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/20190107101808.png)
|
![运行结果](https://raw.githubusercontent.com/FleyX/files/master/blogImg/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/20190107101808.png)
|
||||||
|
|
||||||
每个消息队列有两个消费者,队列进入了三个消息,出了三个消息,说明消息都被消费掉了,如果注释掉消费者代码,再次运行,然后发送消息就会发现 MessagesEnqueued 数量大于 MessagesDequeued,然后再让消费者上线会立即消费掉队列中的消息。
|
每个消息队列有两个消费者,队列进入了三个消息,出了三个消息,说明消息都被消费掉了,如果注释掉消费者代码,再次运行,然后发送消息就会发现 MessagesEnqueued 数量大于 MessagesDequeued,然后再让消费者上线会立即消费掉队列中的消息。
|
||||||
|
|
||||||
|
**本文原创发布于:**[https://www.tapme.top/blog/detail/2018-09-05-10-38](https://www.tapme.top/blog/detail/2018-09-05-10-38)
|
||||||
|
|
||||||
|
源码:[https://github.com/FleyX/demo-project/tree/master/jms_demo](https://github.com/FleyX/demo-project/tree/master/jms_demo)
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
---
|
---
|
||||||
id: "2018-09-06-10-38"
|
id: "2018-09-06-10-38"
|
||||||
date: "2018/09/06 10:38:00"
|
date: "2018/09/06 10:38:00"
|
||||||
title: "springboot整合ActiveMQ(2)"
|
title: "springboot整合ActiveMQ2(主备模式)"
|
||||||
tags: ["java", "spring","springboot","消息队列","activeMQ"]
|
tags: ["java", "spring","springboot","消息队列","activeMQ"]
|
||||||
categories:
|
categories:
|
||||||
- "java"
|
- "java"
|
||||||
- "spring boot学习"
|
- "spring boot学习"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
- 基本使用,[https://www.tapme.top/blog/detail/2018-09-05-10-38](https://www.tapme.top/blog/detail/2018-09-05-10-38)
|
||||||
|
- 主备模式,[https://www.tapme.top/blog/detail/2018-09-06-10-38](https://www.tapme.top/blog/detail/2018-09-06-10-38)
|
||||||
|
|
||||||
  单个 MQ 节点总是不可靠的,一旦该节点出现故障,MQ 服务就不可用了,势必会产生较大的损失。这里记录 activeMQ 如何开启主从备份,一旦 master(主节点故障),slave(从节点)立即提供服务,实现原理是运行多个 MQ 使用同一个持久化数据源,这里以 jdbc 数据源为例。同一时间只有一个节点(节点 A)能够抢到数据库的表锁,其他节点进入阻塞状态,一旦 A 发生错误崩溃,其他节点就会重新获取表锁,获取到锁的节点成为 master,其他节点为 slave,如果节点 A 重新启动,也将成为 slave。
|
  单个 MQ 节点总是不可靠的,一旦该节点出现故障,MQ 服务就不可用了,势必会产生较大的损失。这里记录 activeMQ 如何开启主从备份,一旦 master(主节点故障),slave(从节点)立即提供服务,实现原理是运行多个 MQ 使用同一个持久化数据源,这里以 jdbc 数据源为例。同一时间只有一个节点(节点 A)能够抢到数据库的表锁,其他节点进入阻塞状态,一旦 A 发生错误崩溃,其他节点就会重新获取表锁,获取到锁的节点成为 master,其他节点为 slave,如果节点 A 重新启动,也将成为 slave。
|
||||||
|
|
||||||
  主从备份解决了单节点故障的问题,但是同一时间提供服务的只有一个 master,显然是不能面对数据量的增长,所以需要一种横向拓展的集群方式来解决面临的问题。
|
  主从备份解决了单节点故障的问题,但是同一时间提供服务的只有一个 master,显然是不能面对数据量的增长,所以需要一种横向拓展的集群方式来解决面临的问题。
|
||||||
@ -161,3 +165,5 @@ brokerA 和 brokerB 都要设置该配置,以连上对方。
|
|||||||
```
|
```
|
||||||
|
|
||||||
证明负载均衡成功。
|
证明负载均衡成功。
|
||||||
|
|
||||||
|
本文原创发布于:[https://www.tapme.top/blog/detail/2018-09-06-10-38](https://www.tapme.top/blog/detail/2018-09-06-10-38)
|
||||||
|
@ -2,15 +2,15 @@
|
|||||||
id: "2018-09-10-10-38"
|
id: "2018-09-10-10-38"
|
||||||
date: "2018/09/10 10:38:00"
|
date: "2018/09/10 10:38:00"
|
||||||
title: "springboot配置读写分离(Mybatis)"
|
title: "springboot配置读写分离(Mybatis)"
|
||||||
tags: ["java", "spring","springboot","mysql","主从备份","读写分离"]
|
tags: ["java", "spring", "springboot", "mysql", "主从备份", "读写分离"]
|
||||||
categories:
|
categories:
|
||||||
- "java"
|
- "java"
|
||||||
- "spring boot学习"
|
- "spring boot学习"
|
||||||
---
|
---
|
||||||
|
|
||||||
  近日工作任务较轻,有空学习学习技术,遂来研究如果实现读写分离。这里用博客记录下过程,一方面可备日后查看,同时也能分享给大家(网上的资料真的大都是抄来抄去,,还不带格式的,看的真心难受)。
|
近日工作任务较轻,有空学习学习技术,遂来研究如果实现读写分离。这里用博客记录下过程,一方面可备日后查看,同时也能分享给大家(网上的资料真的大都是抄来抄去,,还不带格式的,看的真心难受)。
|
||||||
|
|
||||||
[完整代码](https://github.com/FleyX/demo-project/tree/master/dxfl)
|
完整代码:[https://github.com/FleyX/demo-project/tree/master/dxfl](https://github.com/FleyX/demo-project/tree/master/dxfl)
|
||||||
|
|
||||||
## 1、背景
|
## 1、背景
|
||||||
|
|
||||||
@ -312,7 +312,7 @@ public class ReadOnlyInterceptor implements Ordered {
|
|||||||
|
|
||||||
#### b、方法名式
|
#### b、方法名式
|
||||||
|
|
||||||
  这种方法不许要注解,但是需要事务名称按一定规则编写,然后通过切面来设置数据库类别,比如`setXXX`设置为写、`getXXX`设置为读,代码我就不写了,应该都知道怎么写。
|
  这种方法不许要注解,但是需要service中方法名称按一定规则编写,然后通过切面来设置数据库类别,比如`setXXX`设置为写、`getXXX`设置为读,代码我就不写了,应该都知道怎么写。
|
||||||
|
|
||||||
## 4、测试
|
## 4、测试
|
||||||
|
|
||||||
@ -320,4 +320,6 @@ public class ReadOnlyInterceptor implements Ordered {
|
|||||||
|
|
||||||
![测试结果](https://raw.githubusercontent.com/FleyX/files/master/blogImg/%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/20190107102056.png)
|
![测试结果](https://raw.githubusercontent.com/FleyX/files/master/blogImg/%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/20190107102056.png)
|
||||||
|
|
||||||
  断断续续写了好几天终于是写完了,,,如果有帮助到你,,欢迎 star 哦,,这里是完整代码地址:[点击跳转](https://github.com/FleyX/demo-project/tree/master/dxfl)
|
读写分离只是数据库扩展的一个临时解决办法,并不能一劳永逸,随着负载进一步增大,只有一个库用于写入肯定是不够的,而且单表的数据库是有上限的,mysql 最多千万级别的数据能保持较好的查询性能。最终还是会变成--**分库分表**架构的。分库分表可以看看这一篇:[https://www.tapme.top/blog/detail/2019-03-20-10-38](https://www.tapme.top/blog/detail/2019-03-20-10-38)
|
||||||
|
|
||||||
|
**本文原创发布于:**[www.tapme.top/blog/detail/2018-09-10-10-38](www.tapme.top/blog/detail/2018-09-10-10-38)
|
||||||
|
@ -8,7 +8,6 @@ categories:
|
|||||||
- 'springCloud实战'
|
- 'springCloud实战'
|
||||||
---
|
---
|
||||||
|
|
||||||
**本篇原创发布于:**[FleyX 的个人博客](http://tapme.top/blog/detail/2019-02-28-11-33)
|
|
||||||
|
|
||||||
  写完最后一篇特意去看了看第一篇是什么时候写的———2018/11/19,到现在三个月多一点,总的来说这三个月通过《Spring 微服务实战》这本书,算是对微服务进行了一次扫盲学习。
|
  写完最后一篇特意去看了看第一篇是什么时候写的———2018/11/19,到现在三个月多一点,总的来说这三个月通过《Spring 微服务实战》这本书,算是对微服务进行了一次扫盲学习。
|
||||||
|
|
||||||
@ -29,4 +28,4 @@ categories:
|
|||||||
|
|
||||||
_2019,Fighting!_
|
_2019,Fighting!_
|
||||||
|
|
||||||
**本篇原创发布于:**[FleyX 的个人博客](http://tapme.top/blog/detail/2019-02-28-11-33)
|
**本篇原创发布于:**[FleyX 的个人博客](https://www.tapme.top/blog/detail/2019-02-28-11-33)
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
id: "2018-11-19-15-57-00"
|
id: "2018-11-19-15-57-00"
|
||||||
date: "2018/11/19 15:57:00"
|
date: "2018/11/19 15:57:00"
|
||||||
title: "springCloud学习1(集中式配置管理)"
|
title: "springCloud学习1(集中式配置管理)"
|
||||||
tags: ["spring-boot", "spring-cloud-config","git"]
|
tags: ["spring-boot", "spring-cloud-config", "git"]
|
||||||
categories:
|
categories:
|
||||||
- "java"
|
- "java"
|
||||||
- "springCloud实战"
|
- "springCloud实战"
|
||||||
---
|
---
|
||||||
|
|
||||||
  本篇项目代码存放于:[点击跳转](https://github.com/FleyX/demo-project/tree/master/springcloud/spring-cloud-config%E9%85%8D%E7%BD%AE%E4%B8%AD%E5%BF%83)
|
**springcloud 总集:**[https://www.tapme.top/blog/detail/2019-02-28-11-33](https://www.tapme.top/blog/detail/2019-02-28-11-33)
|
||||||
|
|
||||||
## 一、前言
|
## 一、前言
|
||||||
|
|
||||||
@ -288,3 +288,5 @@ spring:
|
|||||||
|
|
||||||
  本篇只是用到了 spring-cloud-config 这个来进行配置集中管理,并没有涉及到微服务,在下一篇将开始微服务的学习。
|
  本篇只是用到了 spring-cloud-config 这个来进行配置集中管理,并没有涉及到微服务,在下一篇将开始微服务的学习。
|
||||||
  本篇两个项目代码存放于:[点击跳转](https://github.com/FleyX/demo-project/tree/master/springcloud/spring-cloud-config%E9%85%8D%E7%BD%AE%E4%B8%AD%E5%BF%83)
|
  本篇两个项目代码存放于:[点击跳转](https://github.com/FleyX/demo-project/tree/master/springcloud/spring-cloud-config%E9%85%8D%E7%BD%AE%E4%B8%AD%E5%BF%83)
|
||||||
|
|
||||||
|
本文原创发布于:[www.tapme.top/blog/detail/2018-11-19-15-57-00](www.tapme.top/blog/detail/2018-11-19-15-57-00)
|
||||||
|
@ -1,16 +1,22 @@
|
|||||||
---
|
---
|
||||||
id: "2018-10-01-13-58"
|
id: "2018-10-01-13-58"
|
||||||
date: "2018/10/01 13:58"
|
date: "2018/10/01 13:58"
|
||||||
title: "node,vue开发教学管理系统"
|
title: "如何用node,vue开发一个管理系统"
|
||||||
tags: ["node", "vue","element-ui","axios","koa","redis","mysql","jwt"]
|
tags: ["node", "vue", "element-ui", "axios", "koa", "redis", "mysql", "jwt"]
|
||||||
categories:
|
categories:
|
||||||
- "node"
|
- "node"
|
||||||
- "项目"
|
- "项目"
|
||||||
---
|
---
|
||||||
|
|
||||||
  毕业才刚刚两个多月而已,现在想想大学生活是已经是那么的遥不可及,感觉已经过了好久好久,社会了两个月才明白学校的好啊。。。额,扯远了,自从毕业开始就想找个时间写下毕设的记录总结,结果找了好久好久到今天才开始动笔。
|
  毕业良久,把当初的毕业设计发出来,给大家参考参考如何用 vue+node 构建一个功能较为完善的系统。
|
||||||
|
|
||||||
  我的毕业设计题目是:教学辅助系统的设计与实现,,是不是很俗。。。至于为啥是这个题目呢,完全是被导师坑了。。。。。
|
  毕设题目是:教学辅助系统的设计与实现,,至于为啥是这个题目呢,因为导师觉得学校的系统不好用,想让学生做一个好用的出来。虽然这个系统没有难以实现的功能,但是细节太多了,学生端、管理端加起来有几十个页面,还要写对应的接口,然后还得做代码自动判题和判重。然后就变成了现在这个样子,虽然功能都有,但是存在不少细节问题。
|
||||||
|
|
||||||
|
但是系统的架构实现大家还是可以参考参考的。
|
||||||
|
|
||||||
|
代码已开源:[https://github.com/FleyX/teach_system](https://github.com/FleyX/teach_system)
|
||||||
|
|
||||||
|
demo 地址:[ali.tapme.top:8008](http://ali.tapme.top:8008) 123456/123456
|
||||||
|
|
||||||
## 1、需求分析
|
## 1、需求分析
|
||||||
|
|
||||||
@ -37,7 +43,7 @@ categories:
|
|||||||
- 在线答疑,给学生解答
|
- 在线答疑,给学生解答
|
||||||
- 统计分析,包含测试统计和课程统计
|
- 统计分析,包含测试统计和课程统计
|
||||||
|
|
||||||
洋洋洒洒需求列了一大堆,后面才发现是给自己挖坑,,答辩老师一看这类的题目就不感兴趣了,不论你做的咋样(况且我的演讲能力真的很一般),最后累死累活写了一大堆功能也没太高的分,,不过倒是让我的系统设计能力和代码能力有了不少的提高。
|
洋洋洒洒需求列了一大堆,后面才发现是给自己挖坑,,答辩老师一看这类的题目就不感兴趣了,不论你做的咋样(况且我的演讲能力真的很一般),最后累死累活写了一大堆功能也没太高的分,,不过让我的系统设计能力和代码能力有了不少的提高。
|
||||||
|
|
||||||
<!-- more -->
|
<!-- more -->
|
||||||
|
|
||||||
@ -78,10 +84,10 @@ KOA 作为一个 web 框架其实它本身并没有提供路由功能,需要
|
|||||||
  KOA 作为一个 web 框架其实它本身并没有提供路由功能,需要配合使用 koa-router 来实现路由,koa-router 以类似下面这样的风格来进行路由:
|
  KOA 作为一个 web 框架其实它本身并没有提供路由功能,需要配合使用 koa-router 来实现路由,koa-router 以类似下面这样的风格来进行路由:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const app = require('koa');
|
const app = require("koa");
|
||||||
const router = require('koa-router');
|
const router = require("koa-router");
|
||||||
router.get('/hello', koa => {
|
router.get("/hello", koa => {
|
||||||
koa.response = 'hello';
|
koa.response = "hello";
|
||||||
});
|
});
|
||||||
app.use(router.routes());
|
app.use(router.routes());
|
||||||
```
|
```
|
||||||
@ -89,27 +95,27 @@ app.use(router.routes());
|
|||||||
显然这样在项目中是很不方便的,如果每个路由都要手动进行挂载,很难将每个文件中的路由都挂载到一个 router 中。因此在参考网上的实现后,我写了一个方法在启动时自动扫描某个文件夹下所有的路由文件并挂载到 router 中,代码如下:
|
显然这样在项目中是很不方便的,如果每个路由都要手动进行挂载,很难将每个文件中的路由都挂载到一个 router 中。因此在参考网上的实现后,我写了一个方法在启动时自动扫描某个文件夹下所有的路由文件并挂载到 router 中,代码如下:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
const koaBody = require('koa-body');
|
const koaBody = require("koa-body");
|
||||||
const config = require('../config/config.js');
|
const config = require("../config/config.js");
|
||||||
|
|
||||||
function addMapping(router, filePath) {
|
function addMapping(router, filePath) {
|
||||||
let mapping = require(filePath);
|
let mapping = require(filePath);
|
||||||
for (let url in mapping) {
|
for (let url in mapping) {
|
||||||
if (url.startsWith('GET ')) {
|
if (url.startsWith("GET ")) {
|
||||||
let temp = url.substring(4);
|
let temp = url.substring(4);
|
||||||
router.get(temp, mapping[url]);
|
router.get(temp, mapping[url]);
|
||||||
console.log(`----GET:${temp}`);
|
console.log(`----GET:${temp}`);
|
||||||
} else if (url.startsWith('POST ')) {
|
} else if (url.startsWith("POST ")) {
|
||||||
let temp = url.substring(5);
|
let temp = url.substring(5);
|
||||||
router.post(temp, mapping[url]);
|
router.post(temp, mapping[url]);
|
||||||
console.log(`----POST:${temp}`);
|
console.log(`----POST:${temp}`);
|
||||||
} else if (url.startsWith('PUT ')) {
|
} else if (url.startsWith("PUT ")) {
|
||||||
let temp = url.substring(4);
|
let temp = url.substring(4);
|
||||||
router.put(temp, mapping[url]);
|
router.put(temp, mapping[url]);
|
||||||
console.log(`----PUT:${temp}`);
|
console.log(`----PUT:${temp}`);
|
||||||
} else if (url.startsWith('DELETE ')) {
|
} else if (url.startsWith("DELETE ")) {
|
||||||
let temp = url.substring(7);
|
let temp = url.substring(7);
|
||||||
router.delete(temp, mapping[url]);
|
router.delete(temp, mapping[url]);
|
||||||
console.log(`----DELETE: ${temp}`);
|
console.log(`----DELETE: ${temp}`);
|
||||||
@ -127,8 +133,8 @@ function addControllers(router, filePath) {
|
|||||||
if (state.isDirectory()) {
|
if (state.isDirectory()) {
|
||||||
addControllers(router, temp);
|
addControllers(router, temp);
|
||||||
} else {
|
} else {
|
||||||
if (!temp.endsWith('Helper.js')) {
|
if (!temp.endsWith("Helper.js")) {
|
||||||
console.log('\n--开始处理: ' + element + '路由');
|
console.log("\n--开始处理: " + element + "路由");
|
||||||
addMapping(router, temp);
|
addMapping(router, temp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,21 +159,18 @@ app.use(RouterMW(router,path.join(config.rootPath, 'api')));
|
|||||||
然后路由文件以下面的形式编写:
|
然后路由文件以下面的形式编写:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const knowledgePointDao = require('../dao/knowledgePointDao.js');
|
const knowledgePointDao = require("../dao/knowledgePointDao.js");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 返回某门课的全部知识点,按章节分类
|
* 返回某门课的全部知识点,按章节分类
|
||||||
*/
|
*/
|
||||||
exports['GET /course/:c_id/knowledge_point'] = async (ctx, next) => {
|
exports["GET /course/:c_id/knowledge_point"] = async (ctx, next) => {
|
||||||
let res = await knowledgePointDao.getPontsOrderBySection(ctx.params.c_id);
|
let res = await knowledgePointDao.getPontsOrderBySection(ctx.params.c_id);
|
||||||
ctx.onSuccess(res);
|
ctx.onSuccess(res);
|
||||||
};
|
};
|
||||||
|
|
||||||
//返回某位学生知识点答题情况
|
//返回某位学生知识点答题情况
|
||||||
exports['GET /user/:u_id/course/:c_id/knowledge_point/condition'] = async (
|
exports["GET /user/:u_id/course/:c_id/knowledge_point/condition"] = async (ctx, next) => {
|
||||||
ctx,
|
|
||||||
next
|
|
||||||
) => {
|
|
||||||
let { u_id, c_id } = ctx.params;
|
let { u_id, c_id } = ctx.params;
|
||||||
let res = await knowledgePointDao.getStudentCondition(u_id, c_id);
|
let res = await knowledgePointDao.getStudentCondition(u_id, c_id);
|
||||||
ctx.onSuccess(res);
|
ctx.onSuccess(res);
|
||||||
@ -189,15 +192,13 @@ let token = jwt.sign(
|
|||||||
},
|
},
|
||||||
str,
|
str,
|
||||||
{
|
{
|
||||||
expiresIn: isRememberMe
|
expiresIn: isRememberMe ? config.longTokenExpiration : config.shortTokenExpiration
|
||||||
? config.longTokenExpiration
|
|
||||||
: config.shortTokenExpiration
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
//token-盐值存入redis,如想让该token过期,redis中清楚该token键值对即可
|
//token-盐值存入redis,如想让该token过期,redis中清楚该token键值对即可
|
||||||
await RedisHelper.setString(token, str, 30 * 24 * 60 * 60);
|
await RedisHelper.setString(token, str, 30 * 24 * 60 * 60);
|
||||||
res.code = 1;
|
res.code = 1;
|
||||||
res.info = '登录成功';
|
res.info = "登录成功";
|
||||||
res.data = {
|
res.data = {
|
||||||
u_type: userInfo.u_type,
|
u_type: userInfo.u_type,
|
||||||
u_id: userInfo.u_id,
|
u_id: userInfo.u_id,
|
||||||
@ -216,7 +217,7 @@ res.data = {
|
|||||||
*/
|
*/
|
||||||
let verify = async ctx => {
|
let verify = async ctx => {
|
||||||
let token = ctx.headers.authorization;
|
let token = ctx.headers.authorization;
|
||||||
if (typeof token != 'string') {
|
if (typeof token != "string") {
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
let yan = await redisHelper.getString(token);
|
let yan = await redisHelper.getString(token);
|
||||||
@ -248,38 +249,27 @@ let verify = async ctx => {
|
|||||||
);
|
);
|
||||||
// await redisHelper.deleteKey(token);
|
// await redisHelper.deleteKey(token);
|
||||||
await redisHelper.setString(newToken, newYan, config.shortTokenExpiration);
|
await redisHelper.setString(newToken, newYan, config.shortTokenExpiration);
|
||||||
ctx.response.set('new-token', newToken);
|
ctx.response.set("new-token", newToken);
|
||||||
ctx.response.set('Access-Control-Expose-Headers', 'new-token');
|
ctx.response.set("Access-Control-Expose-Headers", "new-token");
|
||||||
}
|
}
|
||||||
//获取用户信息
|
//获取用户信息
|
||||||
let userInfoKey = data.u_id + '_userInfo';
|
let userInfoKey = data.u_id + "_userInfo";
|
||||||
let userInfo = await redisHelper.getString(userInfoKey);
|
let userInfo = await redisHelper.getString(userInfoKey);
|
||||||
if (userInfo == null || Object.keys(userInfo).length != 3) {
|
if (userInfo == null || Object.keys(userInfo).length != 3) {
|
||||||
userInfo = await mysqlHelper.first(
|
userInfo = await mysqlHelper.first(`select u_id,u_type,j_id from user where u_id=?`, data.u_id);
|
||||||
`select u_id,u_type,j_id from user where u_id=?`,
|
await redisHelper.setString(userInfoKey, JSON.stringify(userInfo), 24 * 60 * 60);
|
||||||
data.u_id
|
|
||||||
);
|
|
||||||
await redisHelper.setString(
|
|
||||||
userInfoKey,
|
|
||||||
JSON.stringify(userInfo),
|
|
||||||
24 * 60 * 60
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
userInfo = JSON.parse(userInfo);
|
userInfo = JSON.parse(userInfo);
|
||||||
}
|
}
|
||||||
ctx.userInfo = userInfo;
|
ctx.userInfo = userInfo;
|
||||||
//更新用户上次访问时间
|
//更新用户上次访问时间
|
||||||
mysqlHelper.execute(
|
mysqlHelper.execute(`update user set last_login_time=? where u_id=?`, Date.now(), userInfo.u_id);
|
||||||
`update user set last_login_time=? where u_id=?`,
|
|
||||||
Date.now(),
|
|
||||||
userInfo.u_id
|
|
||||||
);
|
|
||||||
//管理员拥有全部权限
|
//管理员拥有全部权限
|
||||||
if (userInfo.u_type == 0) {
|
if (userInfo.u_type == 0) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
//获取该用户类型权限
|
//获取该用户类型权限
|
||||||
let authKey = userInfo.j_id + '_authority';
|
let authKey = userInfo.j_id + "_authority";
|
||||||
let urls = await redisHelper.getObject(authKey);
|
let urls = await redisHelper.getObject(authKey);
|
||||||
// let urls = null;
|
// let urls = null;
|
||||||
if (urls == null) {
|
if (urls == null) {
|
||||||
@ -297,11 +287,7 @@ let verify = async ctx => {
|
|||||||
urls = temp;
|
urls = temp;
|
||||||
}
|
}
|
||||||
//判断是否拥有权限
|
//判断是否拥有权限
|
||||||
if (
|
if (urls.hasOwnProperty(ctx._matchedRoute.replace(config.url_prefix, "") + ctx.method)) {
|
||||||
urls.hasOwnProperty(
|
|
||||||
ctx._matchedRoute.replace(config.url_prefix, '') + ctx.method
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return 1;
|
return 1;
|
||||||
} else {
|
} else {
|
||||||
return 3;
|
return 3;
|
||||||
@ -325,25 +311,25 @@ let verify = async ctx => {
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
export default new Router({
|
export default new Router({
|
||||||
mode: 'history',
|
mode: "history",
|
||||||
base: '/app/',
|
base: "/app/",
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: "",
|
||||||
name: 'indexPage',
|
name: "indexPage",
|
||||||
component: IndexPage
|
component: IndexPage
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/about',
|
path: "/about",
|
||||||
name: 'about',
|
name: "about",
|
||||||
component: About
|
component: About
|
||||||
},
|
},
|
||||||
Admin,
|
Admin,
|
||||||
Client,
|
Client,
|
||||||
Public,
|
Public,
|
||||||
{
|
{
|
||||||
path: '*',
|
path: "*",
|
||||||
name: 'NotFound',
|
name: "NotFound",
|
||||||
component: NotFound
|
component: NotFound
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -406,13 +392,13 @@ export default {
|
|||||||
```javascript
|
```javascript
|
||||||
request = (url, method, params, form, isFormData, type) => {
|
request = (url, method, params, form, isFormData, type) => {
|
||||||
let token;
|
let token;
|
||||||
if (type == 'admin') token = getToken();
|
if (type == "admin") token = getToken();
|
||||||
else token = getClientToken();
|
else token = getClientToken();
|
||||||
let headers = {
|
let headers = {
|
||||||
Authorization: token
|
Authorization: token
|
||||||
};
|
};
|
||||||
if (isFormData) {
|
if (isFormData) {
|
||||||
headers['Content-Type'] = 'multipart/form-data';
|
headers["Content-Type"] = "multipart/form-data";
|
||||||
}
|
}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
axios({
|
axios({
|
||||||
@ -427,25 +413,25 @@ request = (url, method, params, form, isFormData, type) => {
|
|||||||
resolve(res.data);
|
resolve(res.data);
|
||||||
//检查是否有更新token
|
//检查是否有更新token
|
||||||
// console.log(res);
|
// console.log(res);
|
||||||
if (res.headers['new-token'] != undefined) {
|
if (res.headers["new-token"] != undefined) {
|
||||||
console.log('set new token');
|
console.log("set new token");
|
||||||
if (vm.$route.path.startsWith('/admin')) {
|
if (vm.$route.path.startsWith("/admin")) {
|
||||||
localStorage.setItem('token', res.headers['new-token']);
|
localStorage.setItem("token", res.headers["new-token"]);
|
||||||
window.token = undefined;
|
window.token = undefined;
|
||||||
} else if (vm.$route.path.startsWith('/client')) {
|
} else if (vm.$route.path.startsWith("/client")) {
|
||||||
localStorage.setItem('clientToken', res.headers['new-token']);
|
localStorage.setItem("clientToken", res.headers["new-token"]);
|
||||||
window.clientToken = undefined;
|
window.clientToken = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
reject(err);
|
reject(err);
|
||||||
if (err.code == 'ECONNABORTED') {
|
if (err.code == "ECONNABORTED") {
|
||||||
alertNotify('错误', '请求超时', 'error');
|
alertNotify("错误", "请求超时", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (err.message == 'Network Error') {
|
if (err.message == "Network Error") {
|
||||||
alertNotify('错误', '无法连接服务器', 'error');
|
alertNotify("错误", "无法连接服务器", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (err.response != undefined) {
|
if (err.response != undefined) {
|
||||||
@ -456,49 +442,33 @@ request = (url, method, params, form, isFormData, type) => {
|
|||||||
}
|
}
|
||||||
//使用该变量表示是否已经弹窗提示了,避免大量未登录弹窗堆积。
|
//使用该变量表示是否已经弹窗提示了,避免大量未登录弹窗堆积。
|
||||||
window.isGoToLogin = true;
|
window.isGoToLogin = true;
|
||||||
vm.$alert(err.response.data, '警告', {
|
vm.$alert(err.response.data, "警告", {
|
||||||
type: 'warning',
|
type: "warning",
|
||||||
showClose: false
|
showClose: false
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
window.isGoToLogin = false;
|
window.isGoToLogin = false;
|
||||||
if (vm.$route.path.startsWith('/admin/')) {
|
if (vm.$route.path.startsWith("/admin/")) {
|
||||||
clearInfo();
|
clearInfo();
|
||||||
vm.$router.replace('/public/admin_login');
|
vm.$router.replace("/public/admin_login");
|
||||||
} else {
|
} else {
|
||||||
clearClientInfo();
|
clearClientInfo();
|
||||||
vm.$router.replace('/public/client_login');
|
vm.$router.replace("/public/client_login");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 403:
|
case 403:
|
||||||
alertNotify(
|
alertNotify("Error:403", "拒绝执行:" + err.response.data, "error");
|
||||||
'Error:403',
|
|
||||||
'拒绝执行:' + err.response.data,
|
|
||||||
'error'
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case 404:
|
case 404:
|
||||||
alertNotify(
|
alertNotify("Error:404", "找不到资源:" + url.substr(0, url.indexOf("?")), "error");
|
||||||
'Error:404',
|
|
||||||
'找不到资源:' + url.substr(0, url.indexOf('?')),
|
|
||||||
'error'
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case 400:
|
case 400:
|
||||||
alertNotify(
|
alertNotify("Error:400", "请求参数错误:" + err.response.data, "error");
|
||||||
'Error:400',
|
|
||||||
'请求参数错误:' + err.response.data,
|
|
||||||
'error'
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case 500:
|
case 500:
|
||||||
alertNotify(
|
alertNotify("Error:500", "服务器内部错误:" + err.response.data, "error");
|
||||||
'Error:500',
|
|
||||||
'服务器内部错误:' + err.response.data,
|
|
||||||
'error'
|
|
||||||
);
|
|
||||||
default:
|
default:
|
||||||
console.log('存在错误未处理:' + err);
|
console.log("存在错误未处理:" + err);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
@ -508,4 +478,10 @@ request = (url, method, params, form, isFormData, type) => {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
  到这里就算是简单介绍完了,,想要更加深入了解的可以去 github 查看源代码,地址如下:[https://github.com/FleyX/teach_system,](https://github.com/FleyX/teach_system)记得 star 哦!
|
## 结束
|
||||||
|
|
||||||
|
目前这个系统已知存在如下问题:
|
||||||
|
|
||||||
|
- 未控制代码判题速度,如果大量判题同时进行会导致判题核心崩溃.应该使用消息队列处理。
|
||||||
|
|
||||||
|
本文原创发布于:[www.tapme.top/blog/detail/2018-10-01-13-58](https://www.tapme.top/blog/detail/2018-10-01-13-58)
|
||||||
|
@ -77,8 +77,65 @@ categories:
|
|||||||
|
|
||||||
最后打开要调试的文件,按`F5`就能断点调试啦.
|
最后打开要调试的文件,按`F5`就能断点调试啦.
|
||||||
|
|
||||||
注意要选择第二个调试配置,也就是`Launch currently open script`
|
注意要选择第一个调试配置,也就是`Listen for XDebug`.
|
||||||
|
|
||||||
![](https://raw.githubusercontent.com/FleyX/files/master/blogImg/20190523092530.png)
|
**注意并不是访问 xdebug 的端口来调试,而是需要访问 web 服务器的端口。并将 web 服务器的目录设置为当前开发的目录。(这个问题浪费了半天的时间)**
|
||||||
|
|
||||||
|
# 配置 rest 服务
|
||||||
|
|
||||||
|
php 不同于其他的后端语言,并不能监听某个端口,然后对请求做出响应。因此如果想用 php 搭建 rest 服务,就需要配置访问任意路径都有通过同一个 php 文件来处理请求。下面介绍几种配置方法:
|
||||||
|
|
||||||
|
## nginx 配置
|
||||||
|
|
||||||
|
```properties
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name example.com;
|
||||||
|
index index.php;
|
||||||
|
error_log /path/to/example.error.log;
|
||||||
|
access_log /path/to/example.access.log;
|
||||||
|
root /path/to/public;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri /index.php$is_args$args;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php {
|
||||||
|
try_files $uri =404;
|
||||||
|
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||||||
|
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## apache 配置
|
||||||
|
|
||||||
|
确保.htaccess 文件和 index.php 在同一个目录中。然后在.htaccess 文件加入如下:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^ index.php [QSA,L]
|
||||||
|
```
|
||||||
|
|
||||||
|
此配置需要 Apache 启用 mod_rewrite 模块,并开启`AllowOverride All`配置。
|
||||||
|
|
||||||
|
## php 内置服务器
|
||||||
|
|
||||||
|
php 从 5.4.0 开始就自带一个内置服务器了,用于开发调试用,不能用于线上模式。通过如下命令启动:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php -S localhost:8888 -t public
|
||||||
|
```
|
||||||
|
|
||||||
|
- -S 指定端口
|
||||||
|
- -t 指定根目录
|
||||||
|
|
||||||
|
详细可查看官方文档:[点击跳转](https://www.php.net/manual/zh/features.commandline.webserver.php)
|
||||||
|
|
||||||
**本文原创发布于:**[https://www.tapme.top/blog/detail/20190521](https://www.tapme.top/blog/detail/20190521)
|
**本文原创发布于:**[https://www.tapme.top/blog/detail/20190521](https://www.tapme.top/blog/detail/20190521)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user