程序员社区项目
开发模式 前后端分离,后端负责所有的设计、接口的定义,后端先行,前端协同,通过接口文档,采用apifox的文档进行对接。
敏捷开发,版本上线迭代,需求分析->功能设计->详细设计->编码实现。
开发工具 后端:IDEA
前端:VSCode
项目管理:giteazycode
包依赖管理:Maven3.6.0
数据库:Mysql5.7
数据库连接池和监控库:Druid
框架:Springboot 2.4.2
数据库图形化:Navicat
接口管理工具:APIPost7
Redis桌面工具:RedisDesktop
表建模:PDManager
原型设计:axure8
原型组件库: antdesign
代码生成器:easycode(idea的plugin市场)
一些插件:mybatis(类->dao->数据库),easycode(由数据库表生成相应代码), preconditions(参数校验)
node.js
架构设计 传统项目 [SpringMVC框架(详解)-CSDN博客](https://blog.csdn.net/H20031011/article/details/131511482?ops_request_misc=%7B%22request%5Fid%22%3A%22172145077916800222810035%22%2C%22scm%22%3A%2220140713.130102334..%22%7D&request_id=172145077916800222810035&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-2-131511482-null-null.142^v100^pc_search_result_base8&utm_term=spring mvc架构&spm=1018.2226.3001.4187)
一般的mvc:model,view,controller
SpringMVC:controller(view+controller),service(业务逻辑),dao(数据库)
现有的架构 DDD架构-CSDN博客
[浅谈架构设计:MVC架构与DDD架构【开发实践】_ddd架构和mvc架构区别-CSDN博客](https://blog.csdn.net/qq_40656637/article/details/137344153?ops_request_misc=%7B%22request%5Fid%22%3A%22172145093416800184184571%22%2C%22scm%22%3A%2220140713.130102334.pc%5Fall.%22%7D&request_id=172145093416800184184571&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-1-137344153-null-null.142^v100^pc_search_result_base8&utm_term=spring mvc架构和ddd架构&spm=1018.2226.3001.4187)
ddd 架构
用户接口层(User Interface ):负责向用户显示信息和解释用户指令(是DDD架构中的表现层)(表现层是视图层的超集,概念有所区别,知道最上层就是表现层即可)
应用层(Application):很“薄”的一层,理论上不应该有业务规则或逻辑,主要面向用例和流程相关的操作。在应用层协调多个服务和领域对象完成服务的组合和编排,协作完成业务操作。此外,应用层也是微服务之间交互的通道,它可以调用其它微服务的应用服务,完成微服务之间服务的组合和编排
领域层(Domain):是实现企业核心业务逻辑,通过各种校验手段保证业务的正确性。领域层主要体现领域模型的业务能力,它用来表达业务概念、业务状态和业务规则。领域层包含聚合根、实体、值对象、领域服务等领域模型中的领域对象
基础层(Infrastructure):贯穿所有层,为其它层提供通用的技术和基础服务,包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等
个人理解:将service层拆分为了应用层和领域层。其中应用层关注于用例和流程,不涉及业务规则或逻辑,通过组合和编排下层的领域层来完成业务操作。而领域层用于封装具体的业务规则或逻辑,拆分出来的领域层不再和具体流程关联,实现了高内聚和低耦合,还提高了领域层的可复用性。用户接口层和基础层则为原来的视图层和dao层的扩展,新增了部分职责功能。
例子:
电子商务领域 :
实体:用户、产品、订单、支付记录。
聚合:购物车、订单详情。
领域服务:订单处理、库存管理、用户认证。
交通物流领域 :
实体:司机、车辆、货物、运输任务。
聚合:运输订单、车队管理。
领域服务:路径规划、货物追踪、调度优化。
API(对外接口层) :这一层负责定义对外提供的服务接口,通常用于与客户端或其他服务进行交互。
Controller :在传统的MVC架构中,控制器用于处理用户的请求。在这里,它用于接收API层的请求,并将请求转换为应用层可以理解的格式。
DTO(Data Transfer Object) :
代表数据传输对象的意思
是一种设计模式之间传输数据的软件应用系统,数据传输目标往往是数据访问对象从数据库中检索数据
数据传输对象与数据交互对象或数据访问对象之间的差异是一个以不具任何行为除了存储和检索的数据(访问和存取器)
简而言之,就是接口之间传递的数据封装
表里面有十几个字段:id,name,gender(M/F),age……
页面需要展示三个字段:name,gender(男/女),age
DTO由此产生,一是能提高数据传输的速度(减少了传输字段),二能隐藏后端表结构。
BO(Business Object) :
代表业务对象的意思,Bo就是把业务逻辑封装为一个对象(注意是逻辑,业务逻辑),这个对象可以包括一个或多个其它的对象。通过调用Dao方法,结合Po或Vo进行业务操作。
形象描述为一个对象的形为和动作,当然也有涉及到基它对象的一些形为和动作。比如处理一个人的业务逻辑,该人会睡觉,吃饭,工作,上班等等行为,还有可能和别人发关系的行为,处理这样的业务逻辑时,我们就可以针对BO去处理。
再比如投保人是一个PO,被保险人是一个PO,险种信息也是一个PO等等,他们组合起来就是一张保单的BO。
PO/DO: Persistent Object / Data Object,持久对象 / 数据对象。
它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应PO的一个(或若干个)属性。
VO: View Object, 视图模型,展示层对象 :
对应页面显示(web页面/移动端H5/Native视图)的数据对象。
Application层(应用层) :这一层包含应用服务,它们协调领域对象来完成业务逻辑。它还包含一些业务逻辑的转换逻辑,如DTO到BO的转换。
Interceptor :拦截器,用于在请求处理过程中进行一些前置或后置处理,例如日志记录、权限验证等。
Application-MQ(消费者)/ Application-Job :这指的是应用层中处理消息队列消息的组件,或者定时任务的处理。
Domain层(领域层) :这是DDD中的核心层,包含业务逻辑和领域模型。领域层专注于业务规则和业务实体。
Service :领域服务,执行领域逻辑但不自然属于任何实体或值对象的操作。
Entity/PO(Persistent Object) :持久化对象,通常与数据库存储相关,代表数据库中的记录。
Mapper :数据访问对象,用于将领域对象映射到数据库表。
Infra层(基础设施层) :提供技术实现,如数据库访问、消息传递、外部服务调用等。
RPC :远程过程调用,用于服务之间的通信。
MG(生产者) :指的是消息生成者,负责生成并发送消息到消息队列。
Starter(启动层) :指的是服务启动时需要自动执行的代码或配置。
Aggressive(聚合层) :聚合层,将多个领域对象聚合成一个更大的业务实体。
Config :配置层,用于存储和访问配置信息。
Dict(字典) :指的是数据字典,用于存储一些固定的数据或映射关系。
Common(公共层) :包含整个应用中多个地方会用到的通用代码或工具。
Enums :枚举,用于定义一组命名的常量。
Utils :工具类,提供一些通用的辅助功能。
req->dto->do->bo->entity->po
项目结构
后端项目目录(backend)
asyncTool : 包含异步处理工具或库,用于处理异步任务。
doc : 存放项目文档,如API文档、技术规范等。
jc-club-auth : 认证服务,负责用户认证和授权。
jc-club-circle : 可能与社区圈子或用户组相关功能。
jc-club-common-starter : 通用启动器或工具类,提供项目通用功能。
jc-club-gateway : 网关服务,负责请求路由、负载均衡等。
jc-club-gen : 代码生成工具,可能用于快速生成项目代码。
jc-club-interview : 面试相关功能,可能包含面试题库或模拟面试。
jc-club-oss : 对象存储服务,用于管理文件存储。
jc-club-practice : 实践项目或示例代码。
jc-club-subject : 主题或课程相关功能,可能用于教育或培训。
jc-club-wx : 微信相关功能,可能包含微信公众号接口或小程序支持。
技术选型 [Spring和Spring Boot之间的区别(小结)_spring和springboot的区别-CSDN博客](https://blog.csdn.net/mengxin_chen/article/details/116240326?ops_request_misc=%7B%22request%5Fid%22%3A%22172145220016800227442776%22%2C%22scm%22%3A%2220140713.130102334..%22%7D&request_id=172145220016800227442776&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-116240326-null-null.142^v100^pc_search_result_base8&utm_term=spring和spring boot区别&spm=1018.2226.3001.4187)
服务器中间件 服务器采用的京东云 centos
Docker安装 1 2 3 4 5 6 7 yum install -y yum-utils device-mapper-persistent-data lvm2 yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo yum install docker-ce docker-ce-cli containerd.io -y systemctl start docker systemctl enable docker docker version docker images
Docker安装mysql 1 2 3 4 5 6 docker pull mysql:5.7 docker images mkdir -p /home/service/mysql/data mkdir -p /home/service/mysql/conf cd /home/service/mysql/conf touch my.cnf
将以下内容粘入:
1 2 3 4 5 6 7 8 9 [mysqld] user=mysql character-set-server=utf8 default_authentication_plugin=mysql_native_password default-time_zone = '+8:00' [client] default-character-set=utf8 [mysql] default-character-set=utf8
1 2 3 4 5 6 7 docker run -p 3306:3306 --name mysql -v /home/service/mysql/logs:/logs -v /home/service/mysql/data:/mysql_data -e MYSQL_ROOT_PASSWORD=Wing1Q2W#E -d mysql:5.7 docker exec -it mysql bash mysql -uroot -p CREATE USER 'admin'@'%' IDENTIFIED BY 'Wing1Q2W#E'; GRANT ALL ON *.* TO 'admin'@'%'; flush privileges;
docker ps 查看启动状态。
navicat直接连接即可,云服务器需要开启防火墙。
Maven配置国内源 maven一定要放到Jenkins的数据挂载目录内,这样容器才能读到。参考开发工具选型里面的maaven包。
在maven的conf的setting的mirrors里面进行配置,配置后,Jenkins下载包会非常的快。
alimaven
aliyun maven
http://maven.aliyun.com/nexus/content/groups/public/
central
Docker安装Jenkins 机器上要有 jdk,服务器可以执行如下命令安装
1 yum install -y java-1.8.0-openjdk.x86_64
jenkins开始
1 2 3 4 docker search jenkins docker pull jenkins/jenkins:2.414.2 docker run -d -u root -p 8080:8080 -p 50000:50000 -v /var/jenkins_home:/var/jenkins_home -v /etc/localtime:/etc/localtime --name jenkins jenkins/jenkins:2.414.2 docker start jenkins
这样就是启动成功了。然后通过8080端口进行访问。访问的过程会很慢等待一下。服务器内存最好大点,内存小的容易启动不起来。
通过log来看一下密码:
1 docker logs 67166b666c76
访问之后,输入上面的密码。
点击继续后,选择 按照推荐安装插件。然后继续等待。
界面如下:
新建任务
上面输入任务名称,下面选择构建自由风格
选择源码管理,配置maven,注意:maven一定要放到Jenkins的数据挂载目录内,这样容器才能读到。
配置ssh服务器
设置密码即可。
配置ssh分发
配置shell脚本
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 cp /var/jenkins_home/workspace/programmer-club-subject/programmer-club-subject/programmer-club-starter/target/programmer-club-starter.jar /var/jenkins_home/jar/ # !/bin/bash APP_NAME=programmer-club-starter.jar LOG_NAME=programmer-club-starter.log pid=`ps -ef | grep $APP_NAME | grep -v grep|awk '{print $2}'` function is_exist(){ pid=`ps -ef | grep $APP_NAME | grep -v grep|awk '{print $2}'` if [ -z ${pid} ]; then String="notExist" echo $String else String="exist" echo $String fi } str=$(is_exist) if [ ${str} = "exist" ]; then echo " 检测到已经启动的程序,pid 是 ${pid} " kill -9 $pid else echo " 程序没有启动了 " echo "${APP_NAME} is not running" fi str=$(is_exist) if [ ${str} = "exist" ]; then echo "${APP_NAME} 已经启动了. pid=${pid} ." else source /etc/profile BUILD_ID=dontKillMe nohup java -Xms300m -Xmx300m -jar /var/jenkins_home/jar/$APP_NAME >$LOG_NAME 2>&1 & echo "程序已重新启动..." fi
yum安装JDK 1 yum install -y java-1.8.0-openjdk.x86_64
Docker安装minio,搭建自己的oss服务器
1 2 3 4 5 6 7 8 9 10 docker pull minio/minio docker run -p 9000:9000 -p 9090:9090 \ --name minio \ -d --restart=always \ -e "MINIO_ACCESS_KEY=minioadmin" \ -e "MINIO_SECRET_KEY=minioadmin" \ -v /mydata/minio/data:/data \ minio/minio server \ /data --console-address ":9090" -address ":9000"
启动后,访问机器ip+9090,进入minio的界面,输入用户名或密码后可以访问。
Docker安装miniomc突破7天限制 1 2 3 4 5 6 7 8 9 10 11 12 13 docker pull minio/mc docker run -it --entrypoint=/bin/sh minio/mc mc config host add <ALIAS> <YOUR-S3-ENDPOINT> <YOUR-ACCESS-KEY> <YOUR-SECRET-KEY> [--api API-SIGNATURE] mc config host add minio http://xxx.xx.xx.xxx:9000 GrVCPXySKgGoJiGgXmtv 0xlqSI9GXvnBOtp0GwUj5OshKNBk9JgwoexotbVV mc ls minio mc anonymous mc anonymous set download minio/jichi
Docker查看运行容器启动命令 安装一个小工具 get_command_4_run_container
1 2 3 docker pull cucker/get_command_4_run_container # 以nacos为例子 docker run --rm -v /var/run/docker.sock:/var/run/docker.sock cucker/get_command_4_run_container nacos
看到如下的启动命令
1 2 3 4 5 6 7 8 9 10 11 12 docker run -d \ --name nacos \ --privileged \ --cgroupns host \ --env JVM_XMX=256m \ --env MODE=standalone \ --env JVM_XMS=256m \ -p 8848:8848/tcp \ -p 9848:9848/tcp \ --restart=always \ -w /home/nacos \ nacos/nacos-server
Docker安装nacos 1 2 docker search nacos docker pull nacos/nacos-server
镜像拉完之后,启动脚本
1 2 3 4 5 6 7 8 9 10 11 12 docker run -d \ --name nacos \ --privileged \ --cgroupns host \ --env JVM_XMX=256m \ --env MODE=standalone \ --env JVM_XMS=256m \ -p 8848:8848/tcp \ -p 9848:9848/tcp \ --restart=always \ -w /home/nacos \ nacos/nacos-server
云服务器不要忘记打开防火墙端口。
访问 ip 地址+8848 /nacos 即可进入控制台
nacos 的文档:https://nacos.io/zh-cn/docs/what-is-nacos.html
nacos 的架构原理:https://developer.aliyun.com/ebook/36?spm=a2c6h.20345107.ebook-index.18.152c2984fsi5ST
Docker安装Redis 1 2 docker search redis docker pull redis
拉下镜像之后,点击下面地址选择自己需要的 redis 版本的配置文件
https://redis.io/docs/management/config/
提前在服务器建立 /data/redis 文件夹,touch 文件redis.conf,也可以上面的直接复制
redis.conf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected-mode yes port 6379 tcp-backlog 511 timeout 0 tcp-keepalive 300 daemonize no pidfile /var/run/redis_6379.pid loglevel notice logfile "" databases 16 always-show-logo no set-proc-title yes proc-title-template "{title} {listen-addr} {server-mode}" locale-collate "" ...
1 2 启动命令 docker run -p 6379:6379 --name redis -v /data/redis/redis.conf:/etc/redis/redis.conf -v /data/redis/data:/data -d redis redis-server /etc/redis/redis.conf --appendonly yes
-p 6379:6379:把容器内的6379端口映射到宿主机6379端口
-v /data/redis/redis.conf:/etc/redis/redis.conf:把宿主机配置好的redis.conf放到容器内的这个位置中
-v /data/redis/data:/data:把redis持久化的数据在宿主机内显示,做数据备份
redis-server /etc/redis/redis.conf:这个是关键配置,让redis不是无配置启动,而是按照这个redis.conf的配置启动
–appendonly yes:redis启动后数据持久化
工具:Redis Desktop Manager
IDEA连接redis可以直接下载 plugin 的 redis 插件
Docker安装es 1 2 3 4 5 6 7 yum install -y yum-utils device-mapper-persistent-data lvm2 docker search elasticsearch docker pull elasticsearch:7.3.1 docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e ES_JAVA_OPTS="-Xms1024m -Xmx1024m" 3d3aa92f641f
启动成功之后,访问http://xxx.xx.xx.xxx:9200/
看到这个就证明成功了!
插件:es-head
docker安装xxl-job 1 2 3 4 5 6 7 8 9 10 11 12 13 docker search xxl-job docker pull xuxueli/xxl-job-admin:2.4.0 docker run -d \ -p 8088:8088\ -v /tool/xxl-job/logs:/data/applogs \ -v /tool/xxl-job/application.properties:/xxl-job/xxl-job-admin/src/main/resources/application.properties \ -e PARAMS="--server.port=8088\ --spring.datasource.url=jdbc:mysql://xxx.xx.xx.xxx:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai \ --spring.datasource.username=root \ --spring.datasource.password=Wing1Q2W#E" \ --name xxl-job-admin \ xuxueli/xxl-job-admin:2.4.0
rocketmq安装 官网地址:https://rocketmq.apache.org/
安装包上传到 linux 的/soft 文件夹,没有此文件夹,先创建,不过在 es 的时候已经创建过了。
1 2 3 4 5 6 7 8 9 10 yum install unzip 可以解压zip包的依赖 unzip rocketmq-all-4.8.0-bin-release.zip cd rocketmq-all-4.8.0-bin-release cd bin vim runserver.sh 将其中的xmx,xms等进行修改256m,弄小一点,让服务器用 vim runbroker.sh 同理修改其中的xmx,xms等进行修改256m,弄小一点,让服务器用 nohup sh mqnamesrv & tail -f ~/logs/rocketmqlogs/namesrv.log
启动broker
1 2 3 export NAMESRV_ADDR=localhost:9876 nohup sh mqbroker -n localhost:9876 & tail -f ~/logs/rocketmqlogs/broker.log
发送消息
1 2 sh tools.sh org.apache.rocketmq.example.quickstart.Producer sh tools.sh org.apache.rocketmq.example.quickstart.Consumer
如果发送消息报错,建立文件夹
1 2 3 4 cd ~/store mkdir commitlog cd commitlog mkdir consumequeue
关闭
1 2 sh bin/mqshutdown broker sh bin/mqshutdown namesrv
安装控制台
更改端口和配置文件。
1 nohup java -Xms300m -Xmx300m -jar rocketmq-console.jar > console.log &
第一部分 数据库表 数据库表建模JSON 刷题模块
SQL
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 DROP TABLE IF EXISTS `subject_radio`;CREATE TABLE `subject_radio`( `id` bigint (20 ) NOT NULL AUTO_INCREMENT COMMENT '主键' , `subject_id` bigint (20 ) DEFAULT NULL COMMENT '题目id' , `option_type` tinyint(4 ) DEFAULT NULL COMMENT 'a,b,c,d' , `option_content` varchar (128 ) DEFAULT NULL COMMENT '选项内容' , `is_correct` tinyint(2 ) DEFAULT NULL COMMENT '是否正确' , `created_by` varchar (32 ) DEFAULT NULL COMMENT '创建人' , `created_time` datetime DEFAULT NULL COMMENT '创建时间' , `update_by` varchar (32 ) DEFAULT NULL COMMENT '修改人' , `update_time` datetime DEFAULT NULL COMMENT '修改时间' , `is_deleted` int (11 ) DEFAULT '0' , PRIMARY KEY (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8 COMMENT= '单选题信息表' ; DROP TABLE IF EXISTS `subject_multiple`;CREATE TABLE `subject_multiple`( `id` bigint (20 ) NOT NULL AUTO_INCREMENT COMMENT '主键' , `subject_id` bigint (20 ) DEFAULT NULL COMMENT '题目id' , `option_type` bigint (4 ) DEFAULT NULL COMMENT '选项类型' , `option_content` varchar (64 ) DEFAULT NULL COMMENT '选项内容' , `is_correct` tinyint(2 ) DEFAULT NULL COMMENT '是否正确' , `created_by` varchar (32 ) DEFAULT NULL COMMENT '创建人' , `created_time` datetime DEFAULT NULL COMMENT '创建时间' , `update_by` varchar (32 ) DEFAULT NULL COMMENT '更新人' , `update_time` datetime DEFAULT NULL COMMENT '更新时间' , `is_deleted` int (11 ) DEFAULT '0' , PRIMARY KEY (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8 COMMENT= '多选题信息表' ; DROP TABLE IF EXISTS `subject_mapping`;CREATE TABLE `subject_mapping`( `id` bigint (20 ) NOT NULL AUTO_INCREMENT COMMENT '主键' , `subject_id` bigint (20 ) DEFAULT NULL COMMENT '题目id' , `category_id` bigint (20 ) DEFAULT NULL COMMENT '分类id' , `label_id` bigint (20 ) DEFAULT NULL COMMENT '标签id' , `created_by` varchar (32 ) DEFAULT NULL COMMENT '创建人' , `created_time` datetime DEFAULT NULL COMMENT '创建时间' , `update_by` varchar (32 ) DEFAULT NULL COMMENT '修改人' , `update_time` datetime DEFAULT NULL COMMENT '修改时间' , `is_deleted` int (11 ) DEFAULT '0' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 536 DEFAULT CHARSET= utf8 COMMENT= '题目分类关系表' ; DROP TABLE IF EXISTS `subject_liked`;CREATE TABLE `subject_liked`( `id` bigint (20 ) NOT NULL AUTO_INCREMENT COMMENT '主键' , `subject_id` bigint (20 ) DEFAULT NULL COMMENT '题目id' , `like_user_id` varchar (32 ) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '点赞人id' , `status` int (11 ) DEFAULT NULL COMMENT '点赞状态 1点赞 0不点赞' , `created_by` varchar (32 ) CHARACTER SET utf8 DEFAULT NULL COMMENT '创建人' , `created_time` datetime DEFAULT NULL COMMENT '创建时间' , `update_by` varchar (32 ) CHARACTER SET utf8 DEFAULT NULL COMMENT '修改人' , `update_time` datetime DEFAULT NULL COMMENT '修改时间' , `is_deleted` int (11 ) DEFAULT '0' , PRIMARY KEY (`id`), UNIQUE KEY `uniq_like` (`subject_id`,`like_user_id`) USING BTREE COMMENT '点赞唯一索引' ) ENGINE= InnoDB DEFAULT CHARSET= utf8mb4 COLLATE = utf8mb4_bin COMMENT= '题目点赞表' ; DROP TABLE IF EXISTS `subject_label`;CREATE TABLE `subject_label`( `id` bigint (20 ) NOT NULL AUTO_INCREMENT COMMENT '主键' , `label_name` varchar (32 ) DEFAULT NULL COMMENT '标签分类' , `sort_num` int (11 ) DEFAULT NULL COMMENT '排序' , `category_id` varchar (50 ) DEFAULT NULL , `created_by` varchar (32 ) DEFAULT NULL COMMENT '创建人' , `created_time` datetime DEFAULT NULL COMMENT '创建时间' , `update_by` varchar (32 ) DEFAULT NULL COMMENT '更新人' , `update_time` datetime DEFAULT NULL COMMENT '更新时间' , `is_deleted` int (11 ) DEFAULT '0' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 64 DEFAULT CHARSET= utf8 COMMENT= '题目标签表' ; DROP TABLE IF EXISTS `subject_judge`;CREATE TABLE `subject_judge`( `id` bigint (20 ) NOT NULL AUTO_INCREMENT COMMENT '主键' , `subject_id` bigint (20 ) DEFAULT NULL COMMENT '题目id' , `is_correct` tinyint(2 ) DEFAULT NULL COMMENT '是否正确' , `created_by` varchar (32 ) DEFAULT NULL COMMENT '创建人' , `created_time` datetime DEFAULT NULL COMMENT '创建时间' , `update_by` varchar (32 ) DEFAULT NULL COMMENT '更新人' , `update_time` datetime DEFAULT NULL COMMENT '更新时间' , `is_deleted` int (11 ) DEFAULT '0' , PRIMARY KEY (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8 COMMENT= '判断题' ; DROP TABLE IF EXISTS `subject_info`;CREATE TABLE `subject_info`( `id` bigint (20 ) NOT NULL AUTO_INCREMENT COMMENT '主键' , `subject_name` varchar (128 ) DEFAULT NULL COMMENT '题目名称' , `subject_difficult` tinyint(4 ) DEFAULT NULL COMMENT '题目难度' , `settle_name` varchar (32 ) DEFAULT NULL COMMENT '出题人名' , `subject_type` tinyint(4 ) DEFAULT NULL COMMENT '题目类型 1单选 2多选 3判断 4简答' , `subject_score` tinyint(4 ) DEFAULT NULL COMMENT '题目分数' , `subject_parse` varchar (512 ) DEFAULT NULL COMMENT '题目解析' , `created_by` varchar (32 ) DEFAULT NULL COMMENT '创建人' , `created_time` datetime DEFAULT NULL COMMENT '创建时间' , `update_by` varchar (32 ) DEFAULT NULL COMMENT '修改人' , `update_time` datetime DEFAULT NULL COMMENT '修改时间' , `is_deleted` int (11 ) DEFAULT '0' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 327 DEFAULT CHARSET= utf8 COMMENT= '题目信息表' ; DROP TABLE IF EXISTS `subject_category`;CREATE TABLE `subject_category`( `id` bigint (20 ) NOT NULL AUTO_INCREMENT COMMENT '主键' , `category_name` varchar (16 ) DEFAULT NULL COMMENT '分类名称' , `category_type` tinyint(2 ) DEFAULT NULL COMMENT '分类类型' , `image_url` varchar (64 ) DEFAULT NULL COMMENT '图标连接' , `parent_id` bigint (20 ) DEFAULT NULL COMMENT '父级id' , `created_by` varchar (32 ) DEFAULT NULL COMMENT '创建人' , `created_time` datetime DEFAULT NULL COMMENT '创建时间' , `update_by` varchar (32 ) DEFAULT NULL COMMENT '更新人' , `update_time` datetime DEFAULT NULL COMMENT '更新时间' , `is_deleted` tinyint(1 ) DEFAULT '0' COMMENT '是否删除 0: 未删除 1: 已删除' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 12 DEFAULT CHARSET= utf8 COMMENT= '题目分类' ; DROP TABLE IF EXISTS `subject_brief`;CREATE TABLE `subject_brief`( `id` bigint (20 ) NOT NULL AUTO_INCREMENT COMMENT '主键' , `subject_id` int (20 ) DEFAULT NULL COMMENT '题目id' , `subject_answer` text COMMENT '题目答案' , `created_by` varchar (32 ) DEFAULT NULL COMMENT '创建人' , `created_time` datetime DEFAULT NULL COMMENT '创建时间' , `update_by` varchar (32 ) DEFAULT NULL COMMENT '更新人' , `update_time` datetime DEFAULT NULL COMMENT '更新时间' , `is_deleted` int (11 ) DEFAULT '0' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 280 DEFAULT CHARSET= utf8 COMMENT= '简答题' ;
刷题模块数据模型
鉴权模块
1.刷题模块(微服务模块)
starter类是用于放置整个项目的启动类的
产品功能模块
研发功能模块拆分
原型设计 axrue+antdesign的组件库
刷题首页
题目详情
分类模块
分类的概念是面试题的大类。其中我们有两种概念:
一种是岗位分类,例如后端,前端,测试。
一种是岗位下细分的分类,比如后端下细分,框架,并发,集合等等。
新增分类 正常的业务逻辑,保证新增后,可以正常的插入数据库即可。
修改分类 crud
删除分类 crud
首页的分类 可以扩展做成做成缓存,不易变的数据,直接从redis查缓存。
缓存预热这种,启动项目之后,扔进去。
目前做成串行化的,二期可以优化,由前端先查询岗位大类,然后再根据大类查询小类。
标签详细设计 标签的概念是分类下的细分。标签是通用性的,独立的个体,与分类不进行强耦合,和题目相关。标签和分类是公用的,多个分类可以对应同一个标签。
新增标签 crud 直接看代码
修改标签 crud
删除标签 crud
标签查询 根据分类去查询标签,要通过题目信息的关联表来进行查询。详细看代码
以上功能涉及到 subject_label 表
题目模块
题目分为单选,多选,判断,简单,四种数据类型,在设计数据的时候,拆分成了题目的主表和其他对应的表来做。
新增题目 注意:采取工厂+策略的模式去做扩展,现在有四种题型,未来无论加多少种,都可以不用动主流程。
后期会结合es 做题目的查重。为搜索做准备。
修改题目 crud
删除题目 要注意删除主表的同时,也把其他的细分的数据表进行同步的处理。
题目列表 难度不大,就是个简单的分页的查询,分类、标签,难度这些其实都是入参的场景。
查标签,难度啊,出题人啊,等等,这些就直接查,不做join。
题目的详情 也做一下工厂+策略吧
此功能涉及如下数据表
刷题模块代码的实现(jc-club-subject) application层的SubjectController(应用层初探&SpringMVC集成) 在jc-club-subject
中的jc-club-application-controller
中的controller
模块下面建立SubjectController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.jingdianjichi.subject.application.controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping("/subject") public class SubjectController { @GetMapping("/test") public String test () { return "hello world" ; } }
这里给出的测试代码如上。
在jc-club-subject
中的jc-club-starter
包中有SubjectApplication.java
作为启动类,并且需要修改pom.xml
中的内容,让启动类能够访问到jc-club-application-controller
中的代码内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 //jc-club-starter的pom.xml,在starter中关于启动的配置是写在resources中的application.yml文件中的,包裹数据库连接,redis等等 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <version > 2.4.2</version > <exclusions > <exclusion > <artifactId > spring-boot-starter-logging</artifactId > <groupId > org.springframework.boot</groupId > </exclusion > </exclusions > </dependency > <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-application-controller</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > </dependencies >
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 package com.jingdianjichi.subject;import org.mybatis.spring.annotation.MapperScan;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.openfeign.EnableFeignClients;import org.springframework.context.annotation.ComponentScan;@SpringBootApplication @ComponentScan("com.jingdianjichi") @MapperScan("com.jingdianjichi.**.mapper") @EnableFeignClients(basePackages = "com.jingdianjichi") public class SubjectApplication { public static void main (String[] args) { SpringApplication.run(SubjectApplication.class); } }
记得maven install
mysql,druid,mybatis集成(infrastructure层) 彻底搞懂MyBaits中#{}和${}的区别_mybatis #{}-CSDN博客
在jc-club-subject
中的jc-club-infra
包中
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 //pom.xml <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-jdbc</artifactId > <version > 2.4.2</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > <version > 1.1.22</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.22</version > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.4.0</version > </dependency > </dependencies >
做subject_category 这个模块
用IDEA自带是数据库工具去连上MySQL
联上数据库后,右键category表,然后Eazycode,选择目录,放在jc-club-infra
包下的basic目录中,template
选择mapper.xml.vm, dao.java.vm, entity.java.vm, service.java.vm, serviceImpl.java.vm
(下面两张图是项目结束后完整的截图,这里就涉及到了mybatis)
https://blog.csdn.net/quest101/article/details/105624322?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522172154711316800226546866%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=172154711316800226546866&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-1-105624322-null-null.142
[mybatis-plus关于@Mapper、@Repository、@MapperScan、xml文件的相关问题_mybatisplus repository-CSDN博客](https://blog.csdn.net/qq_41428418/article/details/132575881?ops_request_misc=&request_id=&biz_id=102&utm_term=mapperscan resulttype=&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-1-132575881.142^v100^pc_search_result_base8&spm=1018.2226.3001.4187)
继续在jc-club-starter
模块的pom.xml
中引入当前的jc-club-infra
模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 //jc-club-starter的pom.xml <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <version > 2.4.2</version > <exclusions > <exclusion > <artifactId > spring-boot-starter-logging</artifactId > <groupId > org.springframework.boot</groupId > </exclusion > </exclusions > </dependency > <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-application-controller</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-infra</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > </dependencies >
在jc-club-subject
中的jc-club-starter
中的resources
中的application.yml
中,定义了服务器和数据源的配置:
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 server: port:3000 spring: datasource: username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/jc-club?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=true type: com.alibaba.druid.pool.DruidDataSource druid: initial-size: 20 min-idle: 20 max-active: 100 max-wait: 60000 stat-view-servlet: enabled: true url-pattern: /druid/* login-username: admin login-password: 123456 filter: stat: enabled: true slow-sql-millis: 2000 log-slow-sql: true wall: enabled: true
server
: 定义了服务器的配置。
port
: 指定了服务器运行的端口号,这里是 3000
。
spring
: 包含了Spring框架的配置。
datasource
: 定义了数据源的配置,用于数据库连接。
username
: 数据库的用户名,这里是 root
。
password
: 数据库的密码,这里是 123456
。
driver-class-name
: MySQL数据库驱动的类名。
url
: 数据库的连接URL,包括数据库地址、端口、数据库名以及一些连接参数。这里的URL表明连接到本地的MySQL服务器上的 jc-club
数据库,并且指定了时区、字符编码和SSL的使用。
type
: 指定了数据源的类型,这里使用的是阿里巴巴的Druid连接池。
druid
: Druid连接池的特定配置。
initial-size
: 连接池的初始大小,这里是 20
。
min-idle
: 连接池中最小的空闲连接数,这里是 20
。
max-active
: 连接池中最大的活动连接数,这里是 100
。
max-wait
: 连接池中获取连接的最大等待时间(毫秒),这里是 60000
毫秒(即60秒)。
stat-view-servlet
: 用于Druid的监控页面。
enabled
: 是否启用监控页面,这里是 true
。
url-pattern
: 监控页面的URL模式。
login-username
: 监控页面的登录用户名。
login-password
: 监控页面的登录密码。
filter
: 定义了Druid的过滤器配置。
stat
: 用于统计的过滤器。
enabled
: 是否启用统计过滤器,这里是 true
。
slow-sql-millis
: 执行时间超过多少毫秒的SQL被认为是慢查询,这里是 2000
毫秒。
log-slow-sql
: 是否记录慢查询的日志,这里是 true
。
wall
: 用于防火墙的过滤器,用于防止SQL注入。
enabled
: 是否启用防火墙过滤器,这里是 true
。
在jc-club-subject
中的jc-club-application-controller
中的pom.xml
中引入jc-club-infra
模块
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 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <parent > <artifactId > jc-club-application</artifactId > <groupId > com.jingdianjichi</groupId > <version > 1.0-SNAPSHOT</version > </parent > <modelVersion > 4.0.0</modelVersion > <artifactId > jc-club-application-controller</artifactId > <properties > <maven.compiler.source > 1.8</maven.compiler.source > <maven.compiler.target > 1.8</maven.compiler.target > <maven.compiler.compilerVersion > 1.8</maven.compiler.compilerVersion > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <project.reporting.outputEncoding > UTF-8</project.reporting.outputEncoding > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <version > 2.4.2</version > </dependency > <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-infra</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > </dependencies > </project >
在jc-club-subject
中的jc-club-application-controller
中的SubjectController.java
中新增一段对数据库的测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.jingdianjichi.subject.application.controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping("/subject") public class SubjectController { @GetMapping("/test") public String test () { SubjectCategory subjectCategory = subjectCategoryService.queryById(1L ); return subjectCategory; } }
测试成功
基于druid配置文件加密(infra中的工具类) 在jc-club-subject
中的jc-club-infra/basic
中新建一个utils
包,建一个用于数据库加密的DruidEncrypUtil.java
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 package com.jingdianjichi.subject.infra.basic.utils;import com.alibaba.druid.filter.config.ConfigTools;import java.security.NoSuchAlgorithmException;import java.security.NoSuchProviderException;public class DruidEncryptUtil { private static String publicKey; private static String privateKey; static { try { String[] keyPair = ConfigTools.genKeyPair(512 ); privateKey = keyPair[0 ]; System.out.println("privateKey:" + privateKey); publicKey = keyPair[1 ]; System.out.println("publicKey:" + publicKey); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (NoSuchProviderException e) { e.printStackTrace(); } } public static String encrypt (String plainText) throws Exception { String encrypt = ConfigTools.encrypt(privateKey, plainText); System.out.println("encrypt:" + encrypt); return encrypt; } public static String decrypt (String encryptText) throws Exception { String decrypt = ConfigTools.decrypt(publicKey, encryptText); System.out.println("decrypt:" + decrypt); return decrypt; } public static void main (String[] args) throws Exception { String encrypt = encrypt("123456" ); System.out.println("encrypt:" + encrypt); } }
生成的公私钥和加密的密码
在jc-club-subject
中的jc-club-starter
中修改application.yml
,在password、config和publicKey
处进行修改:
config
: 配置过滤器,这里启用了配置过滤器。
connectionProperties
: 连接属性,这里配置了解密配置,config.decrypt=true
表示开启解密功能,config.decrypt.key=${publicKey}
表示使用配置的公钥属性进行解密。
publicKey
: 定义了一个公钥,用于与 connectionProperties
中的 config.decrypt.key
配合,进行数据库密码的解密。
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 server: port:3000 spring: datasource: username: root password: Me2Tw8jJlEU2C3ghYkBPPfauoyYKXOnb7iTsOHbISHU/mC1ol9OUvU3O9klxv1o5UEv49mErTSawnrw4zsG+5g== driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/jc-club?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=true type: com.alibaba.druid.pool.DruidDataSource druid: initial-size: 20 min-idle: 20 max-active: 100 max-wait: 60000 connectionProperties: config.decrypt=true;config.decrypt.key=${publicKey}; stat-view-servlet: enabled: true url-pattern: /druid/* login-username: admin login-password: 123456 filter: stat: enabled: true slow-sql-millis: 2000 log-slow-sql: true wall: enabled: true config: enabled: true publicKey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAIKZoTIyh/UEThK6nHOmxVlsSYM6o5qTle39c8NilMUjIln2P3bll86R0asiMLU2p2S81RRfARjIO1im8dNBvS8CAwEAAQ==
现在数据库可以通过加密后的密码连上了
分层架构业务开发 所有接口如图所示:
题目分类(SubjectCategoryController.java) 这部分内容中,SujectCategoryController
作为对题目类型涉及到get、post增删改查的入口,其中包含都在jc-club-application-controller
中的SubjectCategoryServiceImpl
和SubjectCategoryDomainServiceImpl
。该controller通过jc-club-application-convert
组件中SubjectCategoryDTOConverter
的将DTO->BO/BO->DTO,再利用SubjectCategoryDomainServiceImpl
组件的add方法,其中涉及到jc-club-domain-convert
组件中的SubjectCategoryConverter
将BO->Category,在通过SubjectCategoryServiceImpl
调用infra
层的SubjectCategoryConverter
将Category类转化为DAO。
MapStruct转换器常见问题及其使用方式_@inheritinverseconfiguration-CSDN博客
这里的convert的实现是利用@Mapper注解到converter类的
MapStruct使用详解-CSDN博客
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import org.mapstruct.Mapper;@Mapper public interface SubjectCategoryDTOConverter { SubjectCategoryDTOConverter INSTANCE = Mappers.getMapper(SubjectCategoryDTOConverter.class); List<SubjectCategoryDTO> convertBoToCategoryDTOList (List<SubjectCategoryBO> subjectCategoryDTO) ; SubjectCategoryBO convertDtoToCategoryBO (SubjectCategoryDTO subjectCategoryDTO) ; SubjectCategoryDTO convertBoToCategoryDTO (SubjectCategoryBO subjectCategoryBO) ; }
新增分类(POST: /subject/category/add
)
请求body
:
1 2 3 4 5 6 { "categoryName" : "后端" , "categoryType" : 1 , "parentId" : 0 , "imgUrl" : "http://image/123" }
响应成功示例:
1 2 3 4 5 6 7 { "code" : 200 , "message" : "新增成功" , "data" : true , "reqUuid" : "123123123.123123.123123" , "success" : true }
新增jc-club-common
包:
Lombok
是一个Java库,它通过注解的方式提供了一系列可以简化Java代码的工具,比如自动生成getter、setter、toString等方法。
MapStruct
是一个代码生成器,用于将Java方法的输入参数映射到输出参数,通常用于DTO(数据传输对象)和Entity(实体)之间的映射。
DTO数据传输对象详解_dto撖寡情-CSDN博客
mapstruct-processor
,是MapStruct的注解处理器部分,用于在编译时生成映射代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 //pom.xml 注意lombok放在mapstruct前面是为了能够正常的拿到数据。如果项目中使用了lombok,那么需要在编译器指定他们的执行顺序,因为mapstrut底层是靠set/get赋值的,所以需要lombok先编译。 <dependencies > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.16</version > </dependency > <dependency > <groupId > org.mapstruct</groupId > <artifactId > mapstruct</artifactId > <version > 1.4.2.Final</version > </dependency > <dependency > <groupId > org.mapstruct</groupId > <artifactId > mapstruct-processor</artifactId > <version > 1.4.2.Final</version > </dependency > </dependencies >
jc-club-infra
中的entity
下的SubjectCategory.java
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 package com.jingdianjichi.subject.infra.basic.entity;import lombok.Data;import java.util.Date;import java.io.Serializable;@Data public class SubjectCategory implements Serializable { private Long id; private String categoryName; private Integer categoryType; private String imageUrl; private Long parentId; private String createdBy; private Date createdTime; private String updateBy; private Date updateTime; private Integer isDeleted; }
jc-club-infra
中的mapper
下的SubjectCategoryDao.java
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 package com.jingdianjichi.subject.infra.basic.mapper;import com.jingdianjichi.subject.infra.basic.entity.SubjectCategory;import org.apache.ibatis.annotations.Param;import java.util.List;public interface SubjectCategoryDao { SubjectCategory queryById (Long id) ; long count (SubjectCategory subjectCategory) ; int insert (SubjectCategory subjectCategory) ; int insertBatch (@Param("entities") List<SubjectCategory> entities) ; int insertOrUpdateBatch (@Param("entities") List<SubjectCategory> entities) ; int update (SubjectCategory subjectCategory) ; int deleteById (Long id) ; List<SubjectCategory> queryCategory (SubjectCategory subjectCategory) ; Integer querySubjectCount (Long id) ; }
jc-club-infra
中的service
下的SubjectCategoryServiceImpl.java
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 package com.jingdianjichi.subject.infra.basic.service.impl;import com.alibaba.fastjson.JSON;import com.jingdianjichi.subject.infra.basic.entity.SubjectCategory;import com.jingdianjichi.subject.infra.basic.mapper.SubjectCategoryDao;import com.jingdianjichi.subject.infra.basic.service.SubjectCategoryService;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Service;import javax.annotation.Resource;import java.util.List;@Service("subjectCategoryService") @Slf4j public class SubjectCategoryServiceImpl implements SubjectCategoryService { @Resource private SubjectCategoryDao subjectCategoryDao; @Override public SubjectCategory insert (SubjectCategory subjectCategory) { if (log.isInfoEnabled()){ log.info("SubjectCategoryController.add.subjectCategory:{}" , JSON.toJSONString(subjectCategory)); } this .subjectCategoryDao.insert(subjectCategory); return subjectCategory; } @Override public SubjectCategory queryById (Long id) { return this .subjectCategoryDao.queryById(id); } @Override public int update (SubjectCategory subjectCategory) { return this .subjectCategoryDao.update(subjectCategory); } @Override public boolean deleteById (Long id) { return this .subjectCategoryDao.deleteById(id) > 0 ; } @Override public List<SubjectCategory> queryCategory (SubjectCategory subjectCategory) { return this .subjectCategoryDao.queryCategory(subjectCategory); } @Override public Integer querySubjectCount (Long id) { return this .subjectCategoryDao.querySubjectCount(id); } }
jc-club-domain
中的pom.xml
引入infra
模块,便于jc-club-domain
中的service/impl
下的SubjectCategoryDomainServiceImpl.java
来引用
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 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <parent > <artifactId > jc-club-subject</artifactId > <groupId > com.jingdianjichi</groupId > <version > 1.0-SNAPSHOT</version > </parent > <modelVersion > 4.0.0</modelVersion > <artifactId > jc-club-domain</artifactId > <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <configuration > <source > 8</source > <target > 8</target > </configuration > </plugin > </plugins > </build > <dependencies > <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-infra</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > </dependencies > </project >
jc-club-domain
中的entity
下的SubjectCategoryBO.java
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 package com.jingdianjichi.subject.domain.entity;import lombok.Data;import java.io.Serializable;import java.util.Date;import java.util.List;@Data public class SubjectCategoryBO implements Serializable { private Long id; private String categoryName; private Integer categoryType; private String imageUrl; private Long parentId; private Integer count; private List<SubjectLabelBO> labelBOList; }
jc-club-domain
中的service
下的SubjectCategoryDomainService.java
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 package com.jingdianjichi.subject.domain.service;import com.jingdianjichi.subject.domain.entity.SubjectCategoryBO;import java.util.List;public interface SubjectCategoryDomainService { void add (SubjectCategoryBO subjectCategoryBO) ; List<SubjectCategoryBO> queryCategory (SubjectCategoryBO subjectCategoryBO) ; Boolean update (SubjectCategoryBO subjectCategoryBO) ; Boolean delete (SubjectCategoryBO subjectCategoryBO) ; List<SubjectCategoryBO> queryCategoryAndLabel (SubjectCategoryBO subjectCategoryBO) ; }
怎么将BO转为Category呢?这里需要在jc-club-domain
中的convert
包设置SubjectCategoryConverter.java
,用到mapstruct。
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 package com.jingdianjichi.subject.domain.convert;import com.jingdianjichi.subject.domain.entity.SubjectCategoryBO;import com.jingdianjichi.subject.infra.basic.entity.SubjectCategory;import org.mapstruct.Mapper;import org.mapstruct.factory.Mappers;import java.util.List;@Mapper public interface SubjectCategoryConverter { SubjectCategoryConverter INSTANCE = Mappers.getMapper(SubjectCategoryConverter.class); SubjectCategory convertBoToCategory (SubjectCategoryBO subjectCategoryBO) ; List<SubjectCategoryBO> convertBoToCategory (List<SubjectCategory> categoryList) ; } package com.jingdianjichi.subject.domain.service.impl;@Service public class SubjectCategoryDomainServiceImpl implements SubjectCategoryDomainService { @Resource private SubjectCategoryService subjectCategoryService; public void add (SubjectCategoryBO subjectCategoryBO) { SubjectCategory subjectCategory = SubjectCategoryConverter.INSTANCE .convertBoToCategory(subjectCategoryBO); subjectCategory.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); subjectCategoryService.insert(subjectCategory); } }
在jc-club-subject
中的jc-club-application-controller
的pom.xml
中新增jc-club-domain
,目的是在创建SubjectCategoryController
时能正常引入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 //pom.xml <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <version > 2.4.2</version > </dependency > <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-infra</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-domain</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > </dependencies >
在jc-club-subject
中的jc-club-application-controller
的SubjectCategoryController.java
。这里会涉及到在jc-club-application-dto
模块下的从BO到DTO的转换。
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 package com.jingdianjichi.subject.application.dto;import lombok.Data;import java.io.Serializable;import java.util.List;@Data public class SubjectCategoryDTO implements Serializable { private Long id; private String categoryName; private Integer categoryType; private String imageUrl; private Long parentId; private Integer count; private List<SubjectLabelDTO> labelDTOList; } package com.jingdianjichi.subject.application.convert;import com.jingdianjichi.subject.application.dto.SubjectCategoryDTO;import com.jingdianjichi.subject.domain.entity.SubjectCategoryBO;import com.jingdianjichi.subject.infra.basic.entity.SubjectCategory;import org.mapstruct.Mapper;import org.mapstruct.factory.Mappers;import java.util.List;@Mapper public interface SubjectCategoryDTOConverter { SubjectCategoryDTOConverter INSTANCE = Mappers.getMapper(SubjectCategoryDTOConverter.class); List<SubjectCategoryDTO> convertBoToCategoryDTOList (List<SubjectCategoryBO> subjectCategoryDTO) ; SubjectCategoryBO convertDtoToCategoryBO (SubjectCategoryDTO subjectCategoryDTO) ; SubjectCategoryDTO convertBoToCategoryDTO (SubjectCategoryBO subjectCategoryBO) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @RestController @RequestMapping("/subject/category") @Slf4j public class SubjectCategoryController { @Resource private SubjectCategoryDomainService subjectCategoryDomainService; @GetMapping("/add") public Result<Boolean> add (@RequestBody SubjectCategoryDTO subjectCategoryDTO) { try { SubjectCategoryBO subjectCategoryBO=SubjectCategoryDTOConverter.INSTANCE.convertDtoToBO(subjectCategoryDTO); subjectDomainService.add(subjectCategoryBO); return Result.ok(true ); }catch (Exception e){ return Result.fail(false ); } } }
由于会涉及到结果返回:
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 package com.jingdianjichi.subject.common.entity;import com.jingdianjichi.subject.common.enums.ResultCodeEnum;import lombok.Data;@Data public class Result <T> { private Boolean success; private Integer code; private String message; private T data; public static Result ok () { Result result = new Result (); result.setSuccess(true ); result.setCode(ResultCodeEnum.SUCCESS.getCode()); result.setMessage(ResultCodeEnum.SUCCESS.getDesc()); return result; } public static <T> Result ok (T data) { Result result = new Result (); result.setSuccess(true ); result.setCode(ResultCodeEnum.SUCCESS.getCode()); result.setMessage(ResultCodeEnum.SUCCESS.getDesc()); result.setData(data); return result; } public static Result fail () { Result result = new Result (); result.setSuccess(false ); result.setCode(ResultCodeEnum.FAIL.getCode()); result.setMessage(ResultCodeEnum.FAIL.getDesc()); return result; } public static <T> Result fail (T data) { Result result = new Result (); result.setSuccess(false ); result.setCode(ResultCodeEnum.FAIL.getCode()); result.setMessage(ResultCodeEnum.FAIL.getDesc()); result.setData(data); return result; } } package com.jingdianjichi.subject.common.enums;import lombok.Getter;@Getter public enum ResultCodeEnum { SUCCESS(200 ,"成功" ), FAIL(500 ,"失败" ); public int code; public String desc; ResultCodeEnum(int code,String desc){ this .code = code; this .desc = desc; } public static ResultCodeEnum getByCode (int codeVal) { for (ResultCodeEnum resultCodeEnum : ResultCodeEnum.values()){ if (resultCodeEnum.code == codeVal){ return resultCodeEnum; } } return null ; } }
增加日志lo4j2和fastjson在jc-club-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 //pom.xml <dependencies > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.16</version > </dependency > <dependency > <groupId > org.mapstruct</groupId > <artifactId > mapstruct</artifactId > <version > 1.4.2.Final</version > </dependency > <dependency > <groupId > org.mapstruct</groupId > <artifactId > mapstruct-processor</artifactId > <version > 1.4.2.Final</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-log4j2</artifactId > <version > 2.4.2</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.24</version > </dependency > </dependencies >
对应的SubjectCategoryController.java
和SubjectCategoryDomainServiceImpl.java
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 @RestController @RequestMapping("/subject/category") @Slf4j public class SubjectCategoryController { @Resource private SubjectCategoryDomainService subjectCategoryDomainService; @GetMapping("/add") public Result<Boolean> add (@RequestBody SubjectCategoryDTO subjectCategoryDTO) { try { if (log.isInfoEnabled()) { log.info("SubjectCategoryController.add.dto:{}" , JSON.toJSONString(subjectCategoryDTO)); } SubjectCategoryBO subjectCategoryBO=SubjectCategoryDTOConverter.INSTANCE.convertDtoToBO(subjectCategoryDTO); subjectDomainService.add(subjectCategoryBO); return Result.ok(true ); }catch (Exception e){ return Result.fail(false ); } } } package com.jingdianjichi.subject.domain.service.impl;@Service @Slf4j public class SubjectCategoryDomainServiceImpl implements SubjectCategoryDomainService { @Resource private SubjectCategoryService subjectCategoryService; public void add (SubjectCategoryBO subjectCategoryBO) { if (log.isInfoEnabled()) { log.info("SubjectCategoryController.add.bo:{}" , JSON.toJSONString(subjectCategoryBO)); } SubjectCategory subjectCategory = SubjectCategoryConverter.INSTANCE .convertBoToCategory(subjectCategoryBO); subjectCategory.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); subjectCategoryService.insert(subjectCategory); } }
preconditions参数校验 :
回到SubjectCategoryController.java
,进行参数校验,在pom.xml
中添加guava
(注意参数校验放在controller)
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 //jc-club-common/pom.xml <dependencies > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.16</version > </dependency > <dependency > <groupId > org.mapstruct</groupId > <artifactId > mapstruct</artifactId > <version > 1.4.2.Final</version > </dependency > <dependency > <groupId > org.mapstruct</groupId > <artifactId > mapstruct-processor</artifactId > <version > 1.4.2.Final</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-log4j2</artifactId > <version > 2.4.2</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.24</version > </dependency > <dependency > <groupId > com.google.guava</groupId > <artifactId > guava</artifactId > <version > 19.0</version > </dependency > </dependencies >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @PostMapping("/add") public Result<Boolean> add (@RequestBody SubjectCategoryDTO subjectCategoryDTO) { try { if (log.isInfoEnabled()) { log.info("SubjectCategoryController.add.dto:{}" , JSON.toJSONString(subjectCategoryDTO)); } Preconditions.checkNotNull(subjectCategoryDTO.getCategoryType(), "分类类型不能为空" ); Preconditions.checkArgument(!StringUtils.isBlank(subjectCategoryDTO.getCategoryName()), "分类名称不能为空" ); Preconditions.checkNotNull(subjectCategoryDTO.getParentId(), "分类父级id不能为空" ); SubjectCategoryBO subjectCategoryBO = SubjectCategoryDTOConverter.INSTANCE.convertDtoToCategoryBO(subjectCategoryDTO); subjectCategoryDomainService.add(subjectCategoryBO); return Result.ok(true ); } catch (Exception e) { log.error("SubjectCategoryController.add.error:{}" , e.getMessage(), e); return Result.fail("新增分类失败" ); } }
刷题模块接口定义
新增分类
更新分类
查询分类
查询大类下分类
查询分类及标签(二期优化)
删除分类
分类->标签
基本的->二次优化
数据库->缓存优化
新增分类:POST:/subject/category/add
请求体:
1 2 3 4 5 6 { "categoryName" : "缓存" , "parentId" : 0 , "categoryType" : 1 , "imageUrl" : "https://image/category.icon" }
响应成功示例:
1 2 3 4 5 6 { "success" : true , "code" : 200 , "message" : "成功" , "data" : true }
查询分类:POST:/subject/category/queryPrimaryCategory
请求体:
响应示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { "success" : true , "code" : 200 , "message" : "成功" , "data" : [ { "id" : 1 , "categoryName" : "后端" , "categoryType" : 1 , "imageUrl" : "https://image/category.icon" , "parentId" : 0 , "count" : 65 } ] }
查询大类下分类:POST:/subject/category/queryCategoryByPrimary
请求体:
1 2 3 4 { "parentId" : 1 , "categoryType" : 2 }
响应示例:
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 { "success" : true , "code" : 200 , "message" : "成功" , "data" : [ { "id" : 4 , "categoryName" : "框架" , "categoryType" : 2 , "imageUrl" : "http://image/123" , "parentId" : 1 } , { "id" : 5 , "categoryName" : "并发" , "categoryType" : 2 , "imageUrl" : "http://image/123" , "parentId" : 1 } , { "id" : 6 , "categoryName" : "jvm" , "categoryType" : 2 , "imageUrl" : "http://image/123" , "parentId" : 1 } ] }
查询分类及标签(二期优化)
请求体:
响应示例:
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 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 { "success" : true , "code" : 200 , "message" : "成功" , "data" : [ { "id" : 2 , "categoryName" : "缓存" , "categoryType" : 2 , "imageUrl" : "https://image/category.icon" , "parentId" : 1 , "labelDTOList" : [ { "id" : 1 , "categoryId" : 1 , "labelName" : "Redis" , "sortNum" : 1 } , { "id" : 8 , "categoryId" : 1 , "labelName" : "集群" , "sortNum" : 1 } , { "id" : 23 , "categoryId" : 1 , "labelName" : "实际应用" , "sortNum" : 1 } , { "id" : 34 , "categoryId" : 1 , "labelName" : "多线程" , "sortNum" : 1 } , { "id" : 44 , "categoryId" : 1 , "labelName" : "数据一致性" , "sortNum" : 1 } , { "id" : 46 , "categoryId" : 1 , "labelName" : "分布式" , "sortNum" : 1 } , { "id" : 47 , "categoryId" : 1 , "labelName" : "持久化" , "sortNum" : 1 } , { "id" : 49 , "categoryId" : 1 , "labelName" : "事务" , "sortNum" : 1 } ] } , { "id" : 3 , "categoryName" : "数据库" , "categoryType" : 2 , "imageUrl" : "https://image/category.icon" , "parentId" : 1 , "labelDTOList" : [ { "id" : 2 , "categoryId" : 1 , "labelName" : "进程" , "sortNum" : 1 } , { "id" : 4 , "categoryId" : 1 , "labelName" : "Mysql" , "sortNum" : 1 } , { "id" : 16 , "categoryId" : 1 , "labelName" : "索引" , "sortNum" : 1 } , { "id" : 23 , "categoryId" : 1 , "labelName" : "实际应用" , "sortNum" : 1 } , { "id" : 33 , "categoryId" : 1 , "labelName" : "存储引擎" , "sortNum" : 1 } , { "id" : 44 , "categoryId" : 1 , "labelName" : "数据一致性" , "sortNum" : 1 } , { "id" : 49 , "categoryId" : 1 , "labelName" : "事务" , "sortNum" : 1 } ] } , { "id" : 4 , "categoryName" : "JavaSE" , "categoryType" : 2 , "imageUrl" : "https://image/category.icon" , "parentId" : 1 , "labelDTOList" : [ { "id" : 15 , "categoryId" : 1 , "labelName" : "基础" , "sortNum" : 1 } ] } , { "id" : 5 , "categoryName" : "框架" , "categoryType" : 2 , "imageUrl" : "https://image/category.icon" , "parentId" : 1 , "labelDTOList" : [ { "id" : 38 , "categoryId" : 1 , "labelName" : "Spring" , "sortNum" : 1 } , { "id" : 62 , "categoryId" : 1 , "labelName" : "SpringBoot" , "sortNum" : 1 } ] } , { "id" : 6 , "categoryName" : "消息队列" , "categoryType" : 2 , "imageUrl" : "https://image/category.icon" , "parentId" : 1 , "labelDTOList" : [ { "id" : 15 , "categoryId" : 1 , "labelName" : "基础" , "sortNum" : 1 } , { "id" : 23 , "categoryId" : 1 , "labelName" : "实际应用" , "sortNum" : 1 } ] } , { "id" : 7 , "categoryName" : "代码管理工具" , "categoryType" : 2 , "imageUrl" : "https://image/category.icon" , "parentId" : 1 , "labelDTOList" : [ { "id" : 25 , "categoryId" : 1 , "labelName" : "Git" , "sortNum" : 1 } ] } , { "id" : 9 , "categoryName" : "网络" , "categoryType" : 2 , "imageUrl" : "https://image/category.icon" , "parentId" : 1 , "labelDTOList" : [ { "id" : 15 , "categoryId" : 1 , "labelName" : "基础" , "sortNum" : 1 } ] } , { "id" : 10 , "categoryName" : "操作系统" , "categoryType" : 2 , "imageUrl" : "https://image/category.icon" , "parentId" : 1 , "labelDTOList" : [ { "id" : 15 , "categoryId" : 1 , "labelName" : "基础" , "sortNum" : 1 } ] } , { "id" : 11 , "categoryName" : "最佳实践" , "categoryType" : 2 , "imageUrl" : "https://image/category.icon" , "parentId" : 1 , "labelDTOList" : [ { "id" : 23 , "categoryId" : 1 , "labelName" : "实际应用" , "sortNum" : 1 } , { "id" : 53 , "categoryId" : 1 , "labelName" : "Jvm" , "sortNum" : 1 } ] } ] }
这里优化后的其实是把大分类->小分类->标签全都查完了,是用的多线程去实现的
数据查完之后,再通过遍历然后利用某些规则,前端就能很轻松的查到了
删除分类
请求体:
响应示例:
1 2 3 4 5 6 { "success" : true , "code" : 200 , "message" : "成功" , "data" : true }
更新分类
请求体:
1 2 3 4 5 6 7 { "id" : 3 , "categoryName" : "Spring" , "categoryType" : 2 , "parentId" : 3 , "imageUrl" : "http://image/123" }
响应示例:
1 2 3 4 5 6 { "success" : true , "code" : 200 , "message" : "成功" , "data" : true }
题目列表及详情接口定义
涉及到难度,创建时间,题目,点赞收藏评论,创建人
分页查询
查询题目列表(POST:/subject/category/querySubjectList)
请求体:
1 2 3 4 5 6 7 { "pageIndex" : 1 , "pageSize" : 10 , "labelId" : 2 , "categoryId" : 1 , "difficulty" : 1 }
响应示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "code" : 200 , "message" : "查询成功" , "data" : { "total" : 100 , "totalPage" : 20 , "pageList" : [ { "subjectName" : "SpringBoot的自动装配原理是什么?" , "subjectId" : 1 , "difficulty" : 1 , "labelNames" : [ "并发" , "集合" ] } ] } , "reqUuid" :"1231231223" , "success" :true }
分类接口开发 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 @PostMapping("/queryPrimaryCategory") public Result<List<SubjectCategoryDTO>> queryPrimaryCategory (@RequestBody SubjectCategoryDTO subjectCategoryDTO) { try { SubjectCategoryBO subjectCategoryBO = SubjectCategoryDTOConverter.INSTANCE. convertDtoToCategoryBO(subjectCategoryDTO); List<SubjectCategoryBO> subjectCategoryBOList = subjectCategoryDomainService.queryCategory(subjectCategoryBO); List<SubjectCategoryDTO> subjectCategoryDTOList = SubjectCategoryDTOConverter.INSTANCE. convertBoToCategoryDTOList(subjectCategoryBOList); return Result.ok(subjectCategoryDTOList); } catch (Exception e) { log.error("SubjectCategoryController.queryPrimaryCategory.error:{}" , e.getMessage(), e); return Result.fail("查询失败" ); } } public interface SubjectCategoryDomainService { void add (SubjectCategoryBO subjectCategoryBO) ; List<SubjectCategoryBO> queryCategory (SubjectCategoryBO subjectCategoryBO) ; } @Override public List<SubjectCategoryBO> queryCategory (SubjectCategoryBO subjectCategoryBO) { SubjectCategory subjectCategory = SubjectCategoryConverter.INSTANCE .convertBoToCategory(subjectCategoryBO); subjectCategory.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); List<SubjectCategory> subjectCategoryList = subjectCategoryService.queryCategory(subjectCategory); List<SubjectCategoryBO> boList = SubjectCategoryConverter.INSTANCE .convertBoToCategory(subjectCategoryList); if (log.isInfoEnabled()) { log.info("SubjectCategoryController.queryPrimaryCategory.boList:{}" , JSON.toJSONString(boList)); } boList.forEach(bo -> { Integer subjectCount = subjectCategoryService.querySubjectCount(bo.getId()); bo.setCount(subjectCount); }); return boList; }
controller:用converter DTO->BO
domainservice: converter bo->category,用service带着category去查,用Dao去查(dao的接口+dao.xml,mapperscan能扫描到)数据库,返回List
controller:用converter bolist->dtolist
返回dtolist,用Result封装一下,查出来了
关于为什么能够用Result返回用postman可以查到,其实用了@RestController标识:[SpringBoot以及集成组件注解大全详解(一)——lomback && JPA_@componentscan是复合注解吗-CSDN博客](https://blog.csdn.net/qq_42133100/article/details/89084518?ops_request_misc=%7B%22request%5Fid%22%3A%22172154818816800186530805%22%2C%22scm%22%3A%2220140713.130102334.pc%5Fall.%22%7D&request_id=172154818816800186530805&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-1-89084518-null-null.142^v100^pc_search_result_base8&utm_term=lomback中@data RestController&spm=1018.2226.3001.4187)
[【MyBatis】Dao接口和Dao.xml文件如何建立连接-CSDN博客](https://blog.csdn.net/weixin_45156425/article/details/120956070?ops_request_misc=%7B%22request%5Fid%22%3A%22172154533316800207082850%22%2C%22scm%22%3A%2220140713.130102334.pc%5Fall.%22%7D&request_id=172154533316800207082850&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-1-120956070-null-null.142^v100^pc_search_result_base8&utm_term=mybatis和dao xml&spm=1018.2226.3001.4187)
根据大类查询二级分类
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 public interface SubjectCategoryDomainService { void add (SubjectCategoryBO subjectCategoryBO) ; List<SubjectCategoryBO> queryCategory (SubjectCategoryBO subjectCategoryBO) ; } @PostMapping("/queryCategoryByPrimary") public Result<List<SubjectCategoryDTO>> queryCategoryByPrimary (@RequestBody SubjectCategoryDTO subjectCategoryDTO) { try { if (log.isInfoEnabled()) { log.info("SubjectCategoryController.queryCategoryByPrimary.dto:{}" , JSON.toJSONString(subjectCategoryDTO)); } Preconditions.checkNotNull(subjectCategoryDTO.getParentId(), "分类id不能为空" ); SubjectCategoryBO subjectCategoryBO = SubjectCategoryDTOConverter.INSTANCE. convertDtoToCategoryBO(subjectCategoryDTO); List<SubjectCategoryBO> subjectCategoryBOList = subjectCategoryDomainService.queryCategory(subjectCategoryBO); List<SubjectCategoryDTO> subjectCategoryDTOList = SubjectCategoryDTOConverter.INSTANCE. convertBoToCategoryDTOList(subjectCategoryBOList); return Result.ok(subjectCategoryDTOList); } catch (Exception e) { log.error("SubjectCategoryController.queryPrimaryCategory.error:{}" , e.getMessage(), e); return Result.fail("查询失败" ); } }
更新分类
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 @PostMapping("/update") public Result<Boolean> update (@RequestBody SubjectCategoryDTO subjectCategoryDTO) { try { if (log.isInfoEnabled()) { log.info("SubjectCategoryController.update.dto:{}" , JSON.toJSONString(subjectCategoryDTO)); } SubjectCategoryBO subjectCategoryBO = SubjectCategoryDTOConverter.INSTANCE. convertDtoToCategoryBO(subjectCategoryDTO); Boolean result = subjectCategoryDomainService.update(subjectCategoryBO); return Result.ok(result); } catch (Exception e) { log.error("SubjectCategoryController.update.error:{}" , e.getMessage(), e); return Result.fail("更新分类失败" ); } } Boolean update (SubjectCategoryBO subjectCategoryBO) ; @Override public Boolean update (SubjectCategoryBO subjectCategoryBO) { SubjectCategory subjectCategory = SubjectCategoryConverter.INSTANCE .convertBoToCategory(subjectCategoryBO); int count = subjectCategoryService.update(subjectCategory); return count > 0 ; } int update (SubjectCategory subjectCategory) ;@Override public int update (SubjectCategory subjectCategory) { return this .subjectCategoryDao.update(subjectCategory); } int update (SubjectCategory subjectCategory) ;
删除分类
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 @PostMapping("/delete") public Result<Boolean> delete (@RequestBody SubjectCategoryDTO subjectCategoryDTO) { try { if (log.isInfoEnabled()) { log.info("SubjectCategoryController.delete.dto:{}" , JSON.toJSONString(subjectCategoryDTO)); } SubjectCategoryBO subjectCategoryBO = SubjectCategoryDTOConverter.INSTANCE. convertDtoToCategoryBO(subjectCategoryDTO); Boolean result = subjectCategoryDomainService.delete(subjectCategoryBO); return Result.ok(result); } catch (Exception e) { log.error("SubjectCategoryController.delete.error:{}" , e.getMessage(), e); return Result.fail("删除分类失败" ); } } Boolean delete (SubjectCategoryBO subjectCategoryBO) ; @Override public Boolean delete (SubjectCategoryBO subjectCategoryBO) { SubjectCategory subjectCategory = SubjectCategoryConverter.INSTANCE .convertBoToCategory(subjectCategoryBO); subjectCategory.setIsDeleted(IsDeletedFlagEnum.DELETED.getCode()); int count = subjectCategoryService.update(subjectCategory); return count > 0 ; } @Getter public enum IsDeletedFlagEnum { DELETED(1 ,"已删除" ), UN_DELETED(0 ,"未删除" ); public int code; public String desc; IsDeletedFlagEnum(int code, String desc){ this .code = code; this .desc = desc; } public static IsDeletedFlagEnum getByCode (int codeVal) { for (IsDeletedFlagEnum resultCodeEnum : IsDeletedFlagEnum.values()){ if (resultCodeEnum.code == codeVal){ return resultCodeEnum; } } return null ; } } boolean deleteById (Long id) ;@Override public boolean deleteById (Long id) { return this .subjectCategoryDao.deleteById(id) > 0 ; } int deleteById (Long id) ;
题目标签接口定义
新增标签
请求体:
1 2 3 4 5 { "labelName" : "SpringMVC" , "categoryId" : 1 , "sortNum" : 1 }
响应示例:
1 2 3 4 5 6 { "success" : true , "code" : 200 , "message" : "成功" , "data" : true }
更新标签
请求体:
1 2 3 4 5 { "id" : 1 , "labelName" : "Spring" , "sortNum" : 10 }
响应示例:
1 2 3 4 5 6 { "success" : true , "code" : 200 , "message" : "成功" , "data" : true }
删除标签
请求体:
响应示例:
1 2 3 4 5 6 { "success" : true , "code" : 200 , "message" : "成功" , "data" : true }
根据分类查询标签
请求体:
响应示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { "success" : true , "code" : 200 , "message" : "成功" , "data" : [ { "id" : 2 , "categoryId" : 4 , "labelName" : "SpringBoot" , "sortNum" : 0 } , { "id" : 3 , "categoryId" : 4 , "labelName" : "SpringMVC" , "sortNum" : 1 } ] }
标签基础模块开发 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 public class SubjectLabelController { @Resource private SubjectLabelDomainService subjectLabelDomainService; @PostMapping("/add") public Result<Boolean> add (@RequestBody SubjectLabelDTO subjectLabelDTO) { try { if (log.isInfoEnabled()) { log.info("SubjectLabelController.add.dto:{}" , JSON.toJSONString(subjectLabelDTO)); } Preconditions.checkArgument(!StringUtils.isBlank(subjectLabelDTO.getLabelName()), "标签名称不能为空" ); SubjectLabelBO subjectLabelBO = SubjectLabelDTOConverter.INSTANCE.convertDtoToLabelBO(subjectLabelDTO); Boolean result = subjectLabelDomainService.add(subjectLabelBO); return Result.ok(result); } catch (Exception e) { log.error("SubjectLabelController.add.error:{}" , e.getMessage(), e); return Result.fail("新增标签失败" ); } } @PostMapping("/update") public Result<Boolean> update (@RequestBody SubjectLabelDTO subjectLabelDTO) { try { if (log.isInfoEnabled()) { log.info("SubjectLabelController.update.dto:{}" , JSON.toJSONString(subjectLabelDTO)); } Preconditions.checkNotNull(subjectLabelDTO.getId(), "标签id不能为空" ); SubjectLabelBO subjectLabelBO = SubjectLabelDTOConverter.INSTANCE.convertDtoToLabelBO(subjectLabelDTO); Boolean result = subjectLabelDomainService.update(subjectLabelBO); return Result.ok(result); } catch (Exception e) { log.error("SubjectLabelController.update.error:{}" , e.getMessage(), e); return Result.fail("更新标签失败" ); } } @PostMapping("/delete") public Result<Boolean> delete (@RequestBody SubjectLabelDTO subjectLabelDTO) { try { if (log.isInfoEnabled()) { log.info("SubjectLabelController.delete.dto:{}" , JSON.toJSONString(subjectLabelDTO)); } Preconditions.checkNotNull(subjectLabelDTO.getId(), "标签id不能为空" ); SubjectLabelBO subjectLabelBO = SubjectLabelDTOConverter.INSTANCE.convertDtoToLabelBO(subjectLabelDTO); Boolean result = subjectLabelDomainService.delete(subjectLabelBO); return Result.ok(result); } catch (Exception e) { log.error("SubjectLabelController.delete.error:{}" , e.getMessage(), e); return Result.fail("删除标签失败" ); } } @PostMapping("/queryLabelByCategoryId") public Result<List<SubjectLabelDTO>> queryLabelByCategoryId (@RequestBody SubjectLabelDTO subjectLabelDTO) { try { if (log.isInfoEnabled()) { log.info("SubjectLabelController.queryLabelByCategoryId.dto:{}" , JSON.toJSONString(subjectLabelDTO)); } Preconditions.checkNotNull(subjectLabelDTO.getCategoryId(), "分类id不能为空" ); SubjectLabelBO subjectLabelBO = SubjectLabelDTOConverter.INSTANCE.convertDtoToLabelBO(subjectLabelDTO); List<SubjectLabelBO> resultList = subjectLabelDomainService.queryLabelByCategoryId(subjectLabelBO); List<SubjectLabelDTO> subjectLabelDTOS = SubjectLabelDTOConverter.INSTANCE.convertBOToLabelDTOList(resultList); return Result.ok(subjectLabelDTOS); } catch (Exception e) { log.error("SubjectLabelController.queryLabelByCategoryId.error:{}" , e.getMessage(), e); return Result.fail("查询分类下标签失败" ); } } }
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 @Mapper public interface SubjectLabelDTOConverter { SubjectLabelDTOConverter INSTANCE = Mappers.getMapper(SubjectLabelDTOConverter.class); SubjectLabelBO convertDtoToLabelBO (SubjectLabelDTO subjectLabelDTO) ; List<SubjectLabelDTO> convertBOToLabelDTOList (List<SubjectLabelBO> boList) ; } @Mapper public interface SubjectLabelConverter { SubjectLabelConverter INSTANCE = Mappers.getMapper(SubjectLabelConverter.class); SubjectLabel convertBoToLabel (SubjectLabelBO subjectLabelBO) ; List<SubjectLabelBO> convertLabelToBoList (List<SubjectLabel> subjectLabelList) ; } @Data public class SubjectLabelDTO implements Serializable { private Long id; private Long categoryId; private String labelName; private Integer sortNum; } @Data public class SubjectLabelBO implements Serializable { private Long id; private String labelName; private Integer sortNum; private Long categoryId; }
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 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 public interface SubjectLabelDomainService { Boolean add (SubjectLabelBO subjectLabelBO) ; Boolean update (SubjectLabelBO subjectLabelBO) ; Boolean delete (SubjectLabelBO subjectLabelBO) ; List<SubjectLabelBO> queryLabelByCategoryId (SubjectLabelBO subjectLabelBO) ; } @Service @Slf4j public class SubjectLabelDomainServiceImpl implements SubjectLabelDomainService { @Resource private SubjectLabelService subjectLabelService; @Resource private SubjectMappingService subjectMappingService; @Resource private SubjectCategoryService subjectCategoryService; @Override public Boolean add (SubjectLabelBO subjectLabelBO) { if (log.isInfoEnabled()) { log.info("SubjectLabelDomainServiceImpl.add.bo:{}" , JSON.toJSONString(subjectLabelBO)); } SubjectLabel subjectLabel = SubjectLabelConverter.INSTANCE .convertBoToLabel(subjectLabelBO); subjectLabel.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); int count = subjectLabelService.insert(subjectLabel); return count > 0 ; } @Override public Boolean update (SubjectLabelBO subjectLabelBO) { if (log.isInfoEnabled()) { log.info("SubjectLabelDomainServiceImpl.update.bo:{}" , JSON.toJSONString(subjectLabelBO)); } SubjectLabel subjectLabel = SubjectLabelConverter.INSTANCE .convertBoToLabel(subjectLabelBO); int count = subjectLabelService.update(subjectLabel); return count > 0 ; } @Override public Boolean delete (SubjectLabelBO subjectLabelBO) { if (log.isInfoEnabled()) { log.info("SubjectLabelDomainServiceImpl.update.bo:{}" , JSON.toJSONString(subjectLabelBO)); } SubjectLabel subjectLabel = SubjectLabelConverter.INSTANCE .convertBoToLabel(subjectLabelBO); subjectLabel.setIsDeleted(IsDeletedFlagEnum.DELETED.getCode()); int count = subjectLabelService.update(subjectLabel); return count > 0 ; } @Override public List<SubjectLabelBO> queryLabelByCategoryId (SubjectLabelBO subjectLabelBO) { SubjectCategory subjectCategory = subjectCategoryService.queryById(subjectLabelBO.getCategoryId()); if (CategoryTypeEnum.PRIMARY.getCode() == subjectCategory.getCategoryType()){ SubjectLabel subjectLabel = new SubjectLabel (); subjectLabel.setCategoryId(subjectLabelBO.getCategoryId()); List<SubjectLabel> labelList = subjectLabelService.queryByCondition(subjectLabel); List<SubjectLabelBO> labelResultList = SubjectLabelConverter.INSTANCE.convertLabelToBoList(labelList); return labelResultList; } Long categoryId = subjectLabelBO.getCategoryId(); SubjectMapping subjectMapping = new SubjectMapping (); subjectMapping.setCategoryId(categoryId); subjectMapping.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); List<SubjectMapping> mappingList = subjectMappingService.queryLabelId(subjectMapping); if (CollectionUtils.isEmpty(mappingList)) { return Collections.emptyList(); } List<Long> labelIdList = mappingList.stream().map(SubjectMapping::getLabelId).collect(Collectors.toList()); List<SubjectLabel> labelList = subjectLabelService.batchQueryById(labelIdList); List<SubjectLabelBO> boList = new LinkedList <>(); labelList.forEach(label -> { SubjectLabelBO bo = new SubjectLabelBO (); bo.setId(label.getId()); bo.setLabelName(label.getLabelName()); bo.setCategoryId(categoryId); bo.setSortNum(label.getSortNum()); boList.add(bo); }); return boList; } } public interface SubjectMappingService { SubjectMapping queryById (int id) ; SubjectMapping insert (SubjectMapping subjectMapping) ; int update (SubjectMapping subjectMapping) ; boolean deleteById (int id) ; List<SubjectMapping> queryLabelId (SubjectMapping subjectMapping) ; void batchInsert (List<SubjectMapping> mappingList) ; } @Service("subjectMappingService") public class SubjectMappingServiceImpl implements SubjectMappingService { @Resource private SubjectMappingDao subjectMappingDao; @Override public SubjectMapping queryById (int id) { return this .subjectMappingDao.queryById(id); } @Override public SubjectMapping insert (SubjectMapping subjectMapping) { this .subjectMappingDao.insert(subjectMapping); return subjectMapping; } @Override public int update (SubjectMapping subjectMapping) { return this .subjectMappingDao.update(subjectMapping); } @Override public boolean deleteById (int id) { return this .subjectMappingDao.deleteById(id) > 0 ; } @Override public List<SubjectMapping> queryLabelId (SubjectMapping subjectMapping) { return this .subjectMappingDao.queryDistinctLabelId(subjectMapping); } @Override public void batchInsert (List<SubjectMapping> mappingList) { this .subjectMappingDao.insertBatch(mappingList); } }
标签业务改动 将一级分类和标签联系起来
SubjectLabelDao.xml
domain层也跟着改
题目模块接口定义
新增单选题目
请求体:
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 { "subjectName" : "SpringBoot自动装配原理是什么?" , "subjectDifficult" : 1 , "subjectType" : 1 , "subjectScore" : 2 , "subjectParse" : "题目解析" , "categoryIds" : [ 4 , 5 ] , "labelIds" : [ 2 , 3 ] , "optionList" : [ { "optionType" : 1 , "optionContent" : "自动的" , "isCorrect" : 1 } , { "optionType" : 2 , "optionContent" : "其实是用配置文件" , "isCorrect" : 0 } ] }
响应成功示例:
1 2 3 4 5 6 { "success" : true , "code" : 200 , "message" : "成功" , "data" : true }
新增多选题目
请求体:
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 { "subjectName" : "SpringBoot自动装配原理是什么?" , "subjectDifficult" : 1 , "subjectType" : 2 , "subjectScore" : 2 , "subjectParse" : "题目解析" , "categoryIds" : [ 4 , 5 ], "labelIds" : [ 2 , 3 ], "optionList" : [ { "optionType" : 1 , "optionContent" : "自动的" , "isCorrect" : 1 }, { "optionType" : 2 , "optionContent" : "其实是用配置文件" , "isCorrect" : 1 } ] }
响应成功示例:
1 2 3 4 5 6 { "success" : true , "code" : 200 , "message" : "成功" , "data" : true }
新增判断题目
请求体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { "subjectName" : "SpringBoot自动装配原理是什么?" , "subjectDifficult" : 1 , "subjectType" : 3 , "subjectScore" : 2 , "subjectParse" : "题目解析" , "categoryIds" : [ 4 , 5 ], "labelIds" : [ 2 , 3 ], "optionList" : [ { "isCorrect" : 1 } ] }
响应成功示例:
1 2 3 4 5 6 { "success" : true , "code" : 200 , "message" : "成功" , "data" : true }
新增简答题目
请求体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "subjectName" : "Mysql是个什么东西?" , "subjectDifficult" : 1 , "subjectType" : 4 , "subjectScore" : 2 , "subjectParse" : "题目解析2" , "subjectAnswer" : "Mysql是个数据库" , "categoryIds" : [ 5 ], "labelIds" : [ 11 ] }
响应示例:
1 2 3 4 5 6 { "success" : true , "code" : 200 , "message" : "成功" , "data" : true }
查询题目列表
请求体:
1 2 3 4 5 6 7 { "pageNo" : 2 , "pageSize" : 10 , "labelId" : 1 , "categoryId" : 2 , "subjectDifficult" : 1 }
响应示例:
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 { "success" : true , "code" : 200 , "message" : "成功" , "data" : { "pageNo" : 1 , "pageSize" : 20 , "total" : 1 , "totalPages" : 1 , "result" : [ { "pageNo" : 1 , "pageSize" : 20 , "id" : 9 , "subjectName" : "SpringBoot自动装配原理是什么?" , "subjectDifficult" : 1 , "subjectType" : 4 , "subjectScore" : 2 , "subjectParse" : "题目解析" } ], "start" : 1 , "end" : 20 } }
查询题目列表
请求体:
1 2 3 4 5 6 7 { "pageNo" : 2 , "pageSize" : 10 , "labelId" : 1 , "categoryId" : 2 , "subjectDifficult" : 1 }
响应示例:
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 { "success" : true , "code" : 200 , "message" : "成功" , "data" : { "pageNo" : 1 , "pageSize" : 20 , "total" : 1 , "totalPages" : 1 , "result" : [ { "pageNo" : 1 , "pageSize" : 20 , "id" : 9 , "subjectName" : "SpringBoot自动装配原理是什么?" , "subjectDifficult" : 1 , "subjectType" : 4 , "subjectScore" : 2 , "subjectParse" : "题目解析" } ], "start" : 1 , "end" : 20 } }
查询题目详情
请求体:
响应体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { "success" : true , "code" : 200 , "message" : "成功" , "data" : { "pageNo" : 1 , "pageSize" : 20 , "id" : 9 , "subjectName" : "SpringBoot自动装配原理是什么?" , "subjectDifficult" : 1 , "subjectType" : 4 , "subjectScore" : 2 , "subjectParse" : "题目解析" , "subjectAnswer" : "题目答案" , "labelName" : [ "SpringBoot" , "SpringMVC" ] } }
题目模块接口开发 题目模块接口开发是程序员社区项目中用于管理题目信息的核心部分。它涉及一系列接口,用于实现题目的新增、查询、更新和删除等操作。这些接口包括但不限于:
新增单选、多选、判断和简答题目的接口,允许用户提交题目名称、难度、类型、分数、解析、所属分类和标签等信息。
查询题目列表的接口,支持分页查询,并可根据分类、标签、难度等条件进行筛选。
查询题目详情的接口,通过题目ID获取详细信息,包括题目类型、内容、选项、正确答案和解析等。
代码层面,题目模块接口开发使用了Spring Boot框架来构建RESTful API,并采用了DTO(Data Transfer Object)和BO(Business Object)模式来分离数据传输和业务逻辑。通过MapStruct库实现了DTO与BO之间的自动转换,以简化代码并减少重复性工作。此外,服务层(Service)和数据访问层(DAO)的分离确保了业务逻辑的清晰和数据访问的高效性。
在实现上,题目模块接口利用了Lombok库来自动生成getter、setter等方法,以及使用Spring框架的注解如@RestController
和@RequestMapping
来简化路由配置。日志记录采用SLF4J与Log4j2,确保了日志的灵活性和可配置性。此外,通过使用Spring的事务管理@Transactional
,保证了数据操作的原子性和一致性。
整体而言,题目模块接口开发为程序员社区项目提供了一个稳定、可扩展且易于维护的后端服务,以支持题目内容的管理与展示。
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 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 @Data public class SubjectInfoBO extends PageInfo implements Serializable { private Long id; private String subjectName; private Integer subjectDifficult; private String settleName; private Integer subjectType; private Integer subjectScore; private String subjectParse; private String subjectAnswer; private List<Integer> categoryIds; private List<Integer> labelIds; private List<String> labelName; private List<SubjectAnswerBO> optionList; private Long categoryId; private Long labelId; private String keyWord; private String createUser; private String createUserAvatar; private Integer subjectCount; private Boolean liked; private Integer likedCount; private Long nextSubjectId; private Long lastSubjectId; } @Data public class PageResult <T> implements Serializable { private Integer pageNo = 1 ; private Integer pageSize = 20 ; private Integer total = 0 ; private Integer totalPages = 0 ; private List<T> result = Collections.emptyList(); private Integer start = 1 ; private Integer end = 0 ; public void setRecords (List<T> result) { this .result = result; if (result != null && result.size() > 0 ) { setTotal(result.size()); } } public void setTotal (Integer total) { this .total = total; if (this .pageSize > 0 ) { this .totalPages = (total / this .pageSize) + (total % this .pageSize == 0 ? 0 : 1 ); } else { this .totalPages = 0 ; } this .start = (this .pageSize > 0 ? (this .pageNo - 1 ) * this .pageSize : 0 ) + 1 ; this .end = (this .start - 1 + this .pageSize * (this .pageNo > 0 ? 1 : 0 )); } public void setPageSize (Integer pageSize) { this .pageSize = pageSize; } public void setPageNo (Integer pageNo) { this .pageNo = pageNo; } } @Data public class PageInfo implements Serializable { private Integer pageNo = 1 ; private Integer pageSize = 20 ; public Integer getPageNo () { if (pageNo == null || pageNo < 1 ) { return 1 ; } return pageNo; } public Integer getPageSize () { if (pageSize == null || pageSize < 1 || pageSize > Integer.MAX_VALUE) { return 20 ; } return pageSize; } } @Data public class SubjectAnswerBO implements Serializable { private Integer optionType; private String optionContent; private Integer isCorrect; } @Data public class SubjectAnswerDTO implements Serializable { private Integer optionType; private String optionContent; private Integer isCorrect; } @Data public class SubjectOptionBO implements Serializable { private String subjectAnswer; private List<SubjectAnswerBO> optionList; }
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 @Data public class SubjectInfo implements Serializable { private static final long serialVersionUID = -71318372165220898L ; private Long id; private String subjectName; private Integer subjectDifficult; private String settleName; private Integer subjectType; private Integer subjectScore; private String subjectParse; private String createdBy; private Date createdTime; private String updateBy; private Date updateTime; private Integer isDeleted; private Integer subjectCount; }
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 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 @Data public class SubjectInfoDTO extends PageInfo implements Serializable { private Long id; private String subjectName; private Integer subjectDifficult; private String settleName; private Integer subjectType; private Integer subjectScore; private String subjectParse; private String subjectAnswer; private List<Integer> categoryIds; private List<Integer> labelIds; private List<SubjectAnswerDTO> optionList; private List<String> labelName; private Long categoryId; private Long labelId; private String keyWord; private String createUser; private String createUserAvatar; private Integer subjectCount; private Boolean liked; private Integer likedCount; private Long nextSubjectId; private Long lastSubjectId; } public interface SubjectTypeHandler { SubjectInfoTypeEnum getHandlerType () ; void add (SubjectInfoBO subjectInfoBO) ; SubjectOptionBO query (int subjectId) ; } @Component public class BriefTypeHandler implements SubjectTypeHandler { @Resource private SubjectBriefService subjectBriefService; @Override public SubjectInfoTypeEnum getHandlerType () { return SubjectInfoTypeEnum.BRIEF; } @Override public void add (SubjectInfoBO subjectInfoBO) { SubjectBrief subjectBrief = BriefSubjectConverter.INSTANCE.convertBoToEntity(subjectInfoBO); subjectBrief.setSubjectId(subjectInfoBO.getId().intValue()); subjectBrief.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); subjectBriefService.insert(subjectBrief); } @Override public SubjectOptionBO query (int subjectId) { SubjectBrief subjectBrief = new SubjectBrief (); subjectBrief.setSubjectId(subjectId); SubjectBrief result = subjectBriefService.queryByCondition(subjectBrief); SubjectOptionBO subjectOptionBO = new SubjectOptionBO (); subjectOptionBO.setSubjectAnswer(result.getSubjectAnswer()); return subjectOptionBO; } } @Component public class JudgeTypeHandler implements SubjectTypeHandler { @Resource private SubjectJudgeService subjectJudgeService; @Override public SubjectInfoTypeEnum getHandlerType () { return SubjectInfoTypeEnum.JUDGE; } @Override public void add (SubjectInfoBO subjectInfoBO) { SubjectJudge subjectJudge = new SubjectJudge (); SubjectAnswerBO subjectAnswerBO = subjectInfoBO.getOptionList().get(0 ); subjectJudge.setSubjectId(subjectInfoBO.getId()); subjectJudge.setIsCorrect(subjectAnswerBO.getIsCorrect()); subjectJudge.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); subjectJudgeService.insert(subjectJudge); } @Override public SubjectOptionBO query (int subjectId) { SubjectJudge subjectJudge = new SubjectJudge (); subjectJudge.setSubjectId(Long.valueOf(subjectId)); List<SubjectJudge> result = subjectJudgeService.queryByCondition(subjectJudge); List<SubjectAnswerBO> subjectAnswerBOList = JudgeSubjectConverter.INSTANCE.convertEntityToBoList(result); SubjectOptionBO subjectOptionBO = new SubjectOptionBO (); subjectOptionBO.setOptionList(subjectAnswerBOList); return subjectOptionBO; } } @Component public class MultipleTypeHandler implements SubjectTypeHandler { @Resource private SubjectMultipleService subjectMultipleService; @Override public SubjectInfoTypeEnum getHandlerType () { return SubjectInfoTypeEnum.MULTIPLE; } @Override public void add (SubjectInfoBO subjectInfoBO) { List<SubjectMultiple> subjectMultipleList = new LinkedList <>(); subjectInfoBO.getOptionList().forEach(option -> { SubjectMultiple subjectMultiple = MultipleSubjectConverter.INSTANCE.convertBoToEntity(option); subjectMultiple.setSubjectId(subjectInfoBO.getId()); subjectMultiple.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); subjectMultipleList.add(subjectMultiple); }); subjectMultipleService.batchInsert(subjectMultipleList); } @Override public SubjectOptionBO query (int subjectId) { SubjectMultiple subjectMultiple = new SubjectMultiple (); subjectMultiple.setSubjectId(Long.valueOf(subjectId)); List<SubjectMultiple> result = subjectMultipleService.queryByCondition(subjectMultiple); List<SubjectAnswerBO> subjectAnswerBOList = MultipleSubjectConverter.INSTANCE.convertEntityToBoList(result); SubjectOptionBO subjectOptionBO = new SubjectOptionBO (); subjectOptionBO.setOptionList(subjectAnswerBOList); return subjectOptionBO; } } @Component public class RadioTypeHandler implements SubjectTypeHandler { @Resource private SubjectRadioService subjectRadioService; @Override public SubjectInfoTypeEnum getHandlerType () { return SubjectInfoTypeEnum.RADIO; } @Override public void add (SubjectInfoBO subjectInfoBO) { List<SubjectRadio> subjectRadioList = new LinkedList <>(); subjectInfoBO.getOptionList().forEach(option -> { SubjectRadio subjectRadio = RadioSubjectConverter.INSTANCE.convertBoToEntity(option); subjectRadio.setSubjectId(subjectInfoBO.getId()); subjectRadio.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); subjectRadioList.add(subjectRadio); }); subjectRadioService.batchInsert(subjectRadioList); } @Override public SubjectOptionBO query (int subjectId) { SubjectRadio subjectRadio = new SubjectRadio (); subjectRadio.setSubjectId(Long.valueOf(subjectId)); List<SubjectRadio> result = subjectRadioService.queryByCondition(subjectRadio); List<SubjectAnswerBO> subjectAnswerBOList = RadioSubjectConverter.INSTANCE.convertEntityToBoList(result); SubjectOptionBO subjectOptionBO = new SubjectOptionBO (); subjectOptionBO.setOptionList(subjectAnswerBOList); return subjectOptionBO; } }
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 @RestController @Slf4j @RequestMapping("/subject") public class SubjectController { @Resource private SubjectInfoDomainService subjectInfoDomainService; @Resource private RocketMQTemplate rocketMQTemplate; @PostMapping("/add") public Result<Boolean> add (@RequestBody SubjectInfoDTO subjectInfoDTO) { try { if (log.isInfoEnabled()) { log.info("SubjectController.add.dto:{}" , JSON.toJSONString(subjectInfoDTO)); } Preconditions.checkArgument(!StringUtils.isBlank(subjectInfoDTO.getSubjectName()), "题目名称不能为空" ); Preconditions.checkNotNull(subjectInfoDTO.getSubjectDifficult(), "题目难度不能为空" ); Preconditions.checkNotNull(subjectInfoDTO.getSubjectType(), "题目类型不能为空" ); Preconditions.checkNotNull(subjectInfoDTO.getSubjectScore(), "题目分数不能为空" ); Preconditions.checkArgument(!CollectionUtils.isEmpty(subjectInfoDTO.getCategoryIds()) , "分类id不能为空" ); Preconditions.checkArgument(!CollectionUtils.isEmpty(subjectInfoDTO.getLabelIds()) , "标签id不能为空" ); SubjectInfoBO subjectInfoBO = SubjectInfoDTOConverter.INSTANCE.convertDTOToBO(subjectInfoDTO); List<SubjectAnswerBO> subjectAnswerBOS = SubjectAnswerDTOConverter.INSTANCE.convertListDTOToBO(subjectInfoDTO.getOptionList()); subjectInfoBO.setOptionList(subjectAnswerBOS); subjectInfoDomainService.add(subjectInfoBO); return Result.ok(true ); } catch (Exception e) { log.error("SubjectCategoryController.add.error:{}" , e.getMessage(), e); return Result.fail("新增题目失败" ); } } @PostMapping("/getSubjectPage") public Result<PageResult<SubjectInfoDTO>> getSubjectPage (@RequestBody SubjectInfoDTO subjectInfoDTO) { try { if (log.isInfoEnabled()) { log.info("SubjectController.getSubjectPage.dto:{}" , JSON.toJSONString(subjectInfoDTO)); } Preconditions.checkNotNull(subjectInfoDTO.getCategoryId(), "分类id不能为空" ); Preconditions.checkNotNull(subjectInfoDTO.getLabelId(), "标签id不能为空" ); SubjectInfoBO subjectInfoBO = SubjectInfoDTOConverter.INSTANCE.convertDTOToBO(subjectInfoDTO); subjectInfoBO.setPageNo(subjectInfoDTO.getPageNo()); subjectInfoBO.setPageSize(subjectInfoDTO.getPageSize()); PageResult<SubjectInfoBO> boPageResult = subjectInfoDomainService.getSubjectPage(subjectInfoBO); return Result.ok(boPageResult); } catch (Exception e) { log.error("SubjectCategoryController.add.error:{}" , e.getMessage(), e); return Result.fail("分页查询题目失败" ); } } @PostMapping("/querySubjectInfo") public Result<SubjectInfoDTO> querySubjectInfo (@RequestBody SubjectInfoDTO subjectInfoDTO) { try { if (log.isInfoEnabled()) { log.info("SubjectController.querySubjectInfo.dto:{}" , JSON.toJSONString(subjectInfoDTO)); } Preconditions.checkNotNull(subjectInfoDTO.getId(), "题目id不能为空" ); SubjectInfoBO subjectInfoBO = SubjectInfoDTOConverter.INSTANCE.convertDTOToBO(subjectInfoDTO); SubjectInfoBO boResult = subjectInfoDomainService.querySubjectInfo(subjectInfoBO); SubjectInfoDTO dto = SubjectInfoDTOConverter.INSTANCE.convertBOToDTO(boResult); return Result.ok(dto); } catch (Exception e) { log.error("SubjectCategoryController.add.error:{}" , e.getMessage(), e); return Result.fail("查询题目详情失败" ); } } @PostMapping("/getSubjectPageBySearch") public Result<PageResult<SubjectInfoEs>> getSubjectPageBySearch (@RequestBody SubjectInfoDTO subjectInfoDTO) { try { if (log.isInfoEnabled()) { log.info("SubjectController.getSubjectPageBySearch.dto:{}" , JSON.toJSONString(subjectInfoDTO)); } Preconditions.checkArgument(StringUtils.isNotBlank(subjectInfoDTO.getKeyWord()), "关键词不能为空" ); SubjectInfoBO subjectInfoBO = SubjectInfoDTOConverter.INSTANCE.convertDTOToBO(subjectInfoDTO); subjectInfoBO.setPageNo(subjectInfoDTO.getPageNo()); subjectInfoBO.setPageSize(subjectInfoDTO.getPageSize()); PageResult<SubjectInfoEs> boPageResult = subjectInfoDomainService.getSubjectPageBySearch(subjectInfoBO); return Result.ok(boPageResult); } catch (Exception e) { log.error("SubjectCategoryController.getSubjectPageBySearch.error:{}" , e.getMessage(), e); return Result.fail("全文检索失败" ); } } @PostMapping("/getContributeList") public Result<List<SubjectInfoDTO>> getContributeList () { try { List<SubjectInfoBO> boList = subjectInfoDomainService.getContributeList(); List<SubjectInfoDTO> dtoList = SubjectInfoDTOConverter.INSTANCE.convertBOToDTOList(boList); return Result.ok(dtoList); } catch (Exception e) { log.error("SubjectCategoryController.getContributeList.error:{}" , e.getMessage(), e); return Result.fail("获取贡献榜失败" ); } } @PostMapping("/pushMessage") public Result<Boolean> pushMessage (@Param("id") int id) { rocketMQTemplate.convertAndSend("test-topic" , "早上好" + id); return Result.ok(true ); } }
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 @Mapper public interface SubjectInfoDTOConverter { SubjectInfoDTOConverter INSTANCE = Mappers.getMapper(SubjectInfoDTOConverter.class); SubjectInfoBO convertDTOToBO (SubjectInfoDTO subjectInfoDTO) ; SubjectInfoDTO convertBOToDTO (SubjectInfoBO subjectInfoBO) ; List<SubjectInfoDTO> convertBOToDTOList (List<SubjectInfoBO> subjectInfoBO) ; } @Mapper public interface SubjectAnswerDTOConverter { SubjectAnswerDTOConverter INSTANCE = Mappers.getMapper(SubjectAnswerDTOConverter.class); SubjectAnswerBO convertDTOToBO (SubjectAnswerDTO subjectAnswerDTO) ; List<SubjectAnswerBO> convertListDTOToBO (List<SubjectAnswerDTO> dtoList) ; } @Mapper public interface SubjectInfoConverter { SubjectInfoConverter INSTANCE = Mappers.getMapper(SubjectInfoConverter.class); SubjectInfo convertBoToInfo (SubjectInfoBO subjectInfoBO) ; SubjectInfoBO convertOptionToBo (SubjectOptionBO subjectOptionBO) ; SubjectInfoBO convertOptionAndInfoToBo (SubjectOptionBO subjectOptionBO,SubjectInfo subjectInfo) ; List<SubjectInfoBO> convertListInfoToBO (List<SubjectInfo> subjectInfoList) ; }
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 public interface SubjectInfoDomainService { void add (SubjectInfoBO subjectInfoBO) ; PageResult<SubjectInfoBO> getSubjectPage (SubjectInfoBO subjectInfoBO) ; SubjectInfoBO querySubjectInfo (SubjectInfoBO subjectInfoBO) ; PageResult<SubjectInfoEs> getSubjectPageBySearch (SubjectInfoBO subjectInfoBO) ; List<SubjectInfoBO> getContributeList () ; }
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 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 @Service @Slf4j public class SubjectInfoDomainServiceImpl implements SubjectInfoDomainService { @Resource private SubjectInfoService subjectInfoService; @Resource private SubjectMappingService subjectMappingService; @Resource private SubjectLabelService subjectLabelService; @Resource private SubjectTypeHandlerFactory subjectTypeHandlerFactory; @Resource private SubjectEsService subjectEsService; @Resource private SubjectLikedDomainService subjectLikedDomainService; @Resource private UserRpc userRpc; @Resource private RedisUtil redisUtil; private static final String RANK_KEY = "subject_rank" ; @Override @Transactional(rollbackFor = Exception.class) public void add (SubjectInfoBO subjectInfoBO) { if (log.isInfoEnabled()) { log.info("SubjectInfoDomainServiceImpl.add.bo:{}" , JSON.toJSONString(subjectInfoBO)); } SubjectInfo subjectInfo = SubjectInfoConverter.INSTANCE.convertBoToInfo(subjectInfoBO); subjectInfo.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); subjectInfoService.insert(subjectInfo); SubjectTypeHandler handler = subjectTypeHandlerFactory.getHandler(subjectInfo.getSubjectType()); subjectInfoBO.setId(subjectInfo.getId()); handler.add(subjectInfoBO); List<Integer> categoryIds = subjectInfoBO.getCategoryIds(); List<Integer> labelIds = subjectInfoBO.getLabelIds(); List<SubjectMapping> mappingList = new LinkedList <>(); categoryIds.forEach(categoryId -> { labelIds.forEach(labelId -> { SubjectMapping subjectMapping = new SubjectMapping (); subjectMapping.setSubjectId(subjectInfo.getId()); subjectMapping.setCategoryId(Long.valueOf(categoryId)); subjectMapping.setLabelId(Long.valueOf(labelId)); subjectMapping.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); mappingList.add(subjectMapping); }); }); subjectMappingService.batchInsert(mappingList); SubjectInfoEs subjectInfoEs = new SubjectInfoEs (); subjectInfoEs.setDocId(new IdWorkerUtil (1 , 1 , 1 ).nextId()); subjectInfoEs.setSubjectId(subjectInfo.getId()); subjectInfoEs.setSubjectAnswer(subjectInfoBO.getSubjectAnswer()); subjectInfoEs.setCreateTime(new Date ().getTime()); subjectInfoEs.setCreateUser("Roger" ); subjectInfoEs.setSubjectName(subjectInfo.getSubjectName()); subjectInfoEs.setSubjectType(subjectInfo.getSubjectType()); subjectEsService.insert(subjectInfoEs); redisUtil.addScore(RANK_KEY, LoginUtil.getLoginId(), 1 ); } @Override public PageResult<SubjectInfoBO> getSubjectPage (SubjectInfoBO subjectInfoBO) { PageResult<SubjectInfoBO> pageResult = new PageResult <>(); pageResult.setPageNo(subjectInfoBO.getPageNo()); pageResult.setPageSize(subjectInfoBO.getPageSize()); int start = (subjectInfoBO.getPageNo() - 1 ) * subjectInfoBO.getPageSize(); SubjectInfo subjectInfo = SubjectInfoConverter.INSTANCE.convertBoToInfo(subjectInfoBO); int count = subjectInfoService.countByCondition(subjectInfo, subjectInfoBO.getCategoryId() , subjectInfoBO.getLabelId()); if (count == 0 ) { return pageResult; } List<SubjectInfo> subjectInfoList = subjectInfoService.queryPage(subjectInfo, subjectInfoBO.getCategoryId() , subjectInfoBO.getLabelId(), start, subjectInfoBO.getPageSize()); List<SubjectInfoBO> subjectInfoBOS = SubjectInfoConverter.INSTANCE.convertListInfoToBO(subjectInfoList); subjectInfoBOS.forEach(info -> { SubjectMapping subjectMapping = new SubjectMapping (); subjectMapping.setSubjectId(info.getId()); List<SubjectMapping> mappingList = subjectMappingService.queryLabelId(subjectMapping); List<Long> labelIds = mappingList.stream().map(SubjectMapping::getLabelId).collect(Collectors.toList()); List<SubjectLabel> labelList = subjectLabelService.batchQueryById(labelIds); List<String> labelNames = labelList.stream().map(SubjectLabel::getLabelName).collect(Collectors.toList()); info.setLabelName(labelNames); }); pageResult.setRecords(subjectInfoBOS); pageResult.setTotal(count); return pageResult; } @Override public SubjectInfoBO querySubjectInfo (SubjectInfoBO subjectInfoBO) { SubjectInfo subjectInfo = subjectInfoService.queryById(subjectInfoBO.getId()); SubjectTypeHandler handler = subjectTypeHandlerFactory.getHandler(subjectInfo.getSubjectType()); SubjectOptionBO optionBO = handler.query(subjectInfo.getId().intValue()); SubjectInfoBO bo = SubjectInfoConverter.INSTANCE.convertOptionAndInfoToBo(optionBO, subjectInfo); SubjectMapping subjectMapping = new SubjectMapping (); subjectMapping.setSubjectId(subjectInfo.getId()); subjectMapping.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); List<SubjectMapping> mappingList = subjectMappingService.queryLabelId(subjectMapping); List<Long> labelIdList = mappingList.stream().map(SubjectMapping::getLabelId).collect(Collectors.toList()); List<SubjectLabel> labelList = subjectLabelService.batchQueryById(labelIdList); List<String> labelNameList = labelList.stream().map(SubjectLabel::getLabelName).collect(Collectors.toList()); bo.setLabelName(labelNameList); bo.setLiked(subjectLikedDomainService.isLiked(subjectInfoBO.getId().toString(), LoginUtil.getLoginId())); bo.setLikedCount(subjectLikedDomainService.getLikedCount(subjectInfoBO.getId().toString())); assembleSubjectCursor(subjectInfoBO, bo); return bo; } private void assembleSubjectCursor (SubjectInfoBO subjectInfoBO, SubjectInfoBO bo) { Long categoryId = subjectInfoBO.getCategoryId(); Long labelId = subjectInfoBO.getLabelId(); Long subjectId = subjectInfoBO.getId(); if (Objects.isNull(categoryId) || Objects.isNull(labelId)) { return ; } Long nextSubjectId = subjectInfoService.querySubjectIdCursor(subjectId, categoryId, labelId, 1 ); bo.setNextSubjectId(nextSubjectId); Long lastSubjectId = subjectInfoService.querySubjectIdCursor(subjectId, categoryId, labelId, 0 ); bo.setLastSubjectId(lastSubjectId); } @Override public PageResult<SubjectInfoEs> getSubjectPageBySearch (SubjectInfoBO subjectInfoBO) { SubjectInfoEs subjectInfoEs = new SubjectInfoEs (); subjectInfoEs.setPageNo(subjectInfoBO.getPageNo()); subjectInfoEs.setPageSize(subjectInfoBO.getPageSize()); subjectInfoEs.setKeyWord(subjectInfoBO.getKeyWord()); return subjectEsService.querySubjectList(subjectInfoEs); } @Override public List<SubjectInfoBO> getContributeList () { Set<ZSetOperations.TypedTuple<String>> typedTuples = redisUtil.rankWithScore(RANK_KEY, 0 , 5 ); if (log.isInfoEnabled()) { log.info("getContributeList.typedTuples:{}" , JSON.toJSONString(typedTuples)); } if (CollectionUtils.isEmpty(typedTuples)) { return Collections.emptyList(); } List<SubjectInfoBO> boList = new LinkedList <>(); typedTuples.forEach((rank -> { SubjectInfoBO subjectInfoBO = new SubjectInfoBO (); subjectInfoBO.setSubjectCount(rank.getScore().intValue()); UserInfo userInfo = userRpc.getUserInfo(rank.getValue()); subjectInfoBO.setCreateUser(userInfo.getNickName()); subjectInfoBO.setCreateUserAvatar(userInfo.getAvatar()); boList.add(subjectInfoBO); })); return boList; } }
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 @Service("subjectInfoService") public class SubjectInfoServiceImpl implements SubjectInfoService { @Resource private SubjectInfoDao subjectInfoDao; @Override public SubjectInfo queryById (Long id) { return this .subjectInfoDao.queryById(id); } @Override public SubjectInfo insert (SubjectInfo subjectInfo) { this .subjectInfoDao.insert(subjectInfo); return subjectInfo; } @Override public SubjectInfo update (SubjectInfo subjectInfo) { this .subjectInfoDao.update(subjectInfo); return this .queryById(subjectInfo.getId()); } @Override public boolean deleteById (Long id) { return this .subjectInfoDao.deleteById(id) > 0 ; } @Override public int countByCondition (SubjectInfo subjectInfo, Long categoryId, Long labelId) { return this .subjectInfoDao.countByCondition(subjectInfo, categoryId, labelId); } @Override public List<SubjectInfo> queryPage (SubjectInfo subjectInfo, Long categoryId, Long labelId, int start, Integer pageSize) { return this .subjectInfoDao.queryPage(subjectInfo, categoryId, labelId, start, pageSize); } @Override public List<SubjectInfo> getContributeCount () { return this .subjectInfoDao.getContributeCount(); } @Override public Long querySubjectIdCursor (Long subjectId, Long categoryId, Long labelId, int cursor) { return this .subjectInfoDao.querySubjectIdCursor(subjectId, categoryId, labelId, cursor); } }
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 public interface SubjectInfoService { SubjectInfo queryById (Long id) ; SubjectInfo insert (SubjectInfo subjectInfo) ; SubjectInfo update (SubjectInfo subjectInfo) ; boolean deleteById (Long id) ; int countByCondition (SubjectInfo subjectInfo, Long categoryId, Long labelId) ; List<SubjectInfo> queryPage (SubjectInfo subjectInfo, Long categoryId, Long labelId, int start, Integer pageSize) ; List<SubjectInfo> getContributeCount () ; Long querySubjectIdCursor (Long subjectId, Long categoryId, Long labelId, int cursor) ; }
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 public interface SubjectInfoDao { SubjectInfo queryById (Long id) ; List<SubjectInfo> queryAllByLimit (SubjectInfo subjectInfo) ; long count (SubjectInfo subjectInfo) ; int insert (SubjectInfo subjectInfo) ; int insertBatch (@Param("entities") List<SubjectInfo> entities) ; int insertOrUpdateBatch (@Param("entities") List<SubjectInfo> entities) ; int update (SubjectInfo subjectInfo) ; int deleteById (Long id) ; int countByCondition (@Param("subjectInfo") SubjectInfo subjectInfo, @Param("categoryId") Long categoryId, @Param("labelId") Long labelId) ; List<SubjectInfo> queryPage (@Param("subjectInfo") SubjectInfo subjectInfo, @Param("categoryId") Long categoryId, @Param("labelId") Long labelId, @Param("start") int start, @Param("pageSize") Integer pageSize) ; List<SubjectInfo> getContributeCount () ; Long querySubjectIdCursor (@Param("subjectId") Long subjectId, @Param("categoryId") Long categoryId, @Param("labelId") Long labelId, @Param("cursor") int cursor) ;}
JacksonConverter(对于返回值的处理)
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 package com.jingdianjichi.subject.application.config;import com.fasterxml.jackson.annotation.JsonInclude;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.databind.SerializationFeature;import com.jingdianjichi.subject.application.interceptor.LoginInterceptor;import org.springframework.context.annotation.Configuration;import org.springframework.http.converter.HttpMessageConverter;import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;import java.util.List;@Configuration public class GlobalConfig extends WebMvcConfigurationSupport { @Override protected void configureMessageConverters (List<HttpMessageConverter<?>> converters) { super .configureMessageConverters(converters); converters.add(mappingJackson2HttpMessageConverter()); } @Override protected void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor ()) .addPathPatterns("/**" ); } private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter () { ObjectMapper objectMapper = new ObjectMapper (); objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false ); objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); return new MappingJackson2HttpMessageConverter (objectMapper); } }
SQL拦截器自动翻译(mybatis提供的)
SqlStatementInterceptor 主要作用是监控MyBatis的SQL执行时间,并根据不同的执行时间记录不同级别的日志
MybatisPlusAllSqlLog这个类实现了InnerInterceptor
接口,它是MyBatis-Plus框架提供的一个内部拦截器接口,用于拦截SQL的执行。这个类有两个主要的重写方法:
beforeQuery
: 在查询执行前调用,记录SQL信息。
beforeUpdate
: 在更新执行前调用,记录SQL信息。
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 package com.jingdianjichi.subject.infra.config;import org.apache.ibatis.cache.CacheKey;import org.apache.ibatis.executor.Executor;import org.apache.ibatis.mapping.BoundSql;import org.apache.ibatis.mapping.MappedStatement;import org.apache.ibatis.plugin.*;import org.apache.ibatis.session.ResultHandler;import org.apache.ibatis.session.RowBounds;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.util.Properties;@Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})}) public class SqlStatementInterceptor implements Interceptor { public static final Logger log = LoggerFactory.getLogger("sys-sql" ); @Override public Object intercept (Invocation invocation) throws Throwable { long startTime = System.currentTimeMillis(); try { return invocation.proceed(); } finally { long timeConsuming = System.currentTimeMillis() - startTime; log.info("执行SQL:{}ms" , timeConsuming); if (timeConsuming > 999 && timeConsuming < 5000 ) { log.info("执行SQL大于1s:{}ms" , timeConsuming); } else if (timeConsuming >= 5000 && timeConsuming < 10000 ) { log.info("执行SQL大于5s:{}ms" , timeConsuming); } else if (timeConsuming >= 10000 ) { log.info("执行SQL大于10s:{}ms" , timeConsuming); } } } @Override public Object plugin (Object target) { return Plugin.wrap(target, this ); } @Override public void setProperties (Properties properties) { } } public class MybatisPlusAllSqlLog implements InnerInterceptor { public static final Logger log = LoggerFactory.getLogger("sys-sql" ); @Override public void beforeQuery (Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { logInfo(boundSql, ms, parameter); } @Override public void beforeUpdate (Executor executor, MappedStatement ms, Object parameter) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); logInfo(boundSql, ms, parameter); } private static void logInfo (BoundSql boundSql, MappedStatement ms, Object parameter) { try { log.info("parameter = " + parameter); String sqlId = ms.getId(); log.info("sqlId = " + sqlId); Configuration configuration = ms.getConfiguration(); String sql = getSql(configuration, boundSql, sqlId); log.info("完整的sql:{}" , sql); } catch (Exception e) { log.error("异常:{}" , e.getLocalizedMessage(), e); } } public static String getSql (Configuration configuration, BoundSql boundSql, String sqlId) { return sqlId + ":" + showSql(configuration, boundSql); } public static String showSql (Configuration configuration, BoundSql boundSql) { Object parameterObject = boundSql.getParameterObject(); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); String sql = boundSql.getSql().replaceAll("[\\s]+" , " " ); if (!CollectionUtils.isEmpty(parameterMappings) && parameterObject != null ) { TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { sql = sql.replaceFirst("\\?" , Matcher.quoteReplacement(getParameterValue(parameterObject))); } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); for (ParameterMapping parameterMapping : parameterMappings) { String propertyName = parameterMapping.getProperty(); if (metaObject.hasGetter(propertyName)) { Object obj = metaObject.getValue(propertyName); sql = sql.replaceFirst("\\?" , Matcher.quoteReplacement(getParameterValue(obj))); } else if (boundSql.hasAdditionalParameter(propertyName)) { Object obj = boundSql.getAdditionalParameter(propertyName); sql = sql.replaceFirst("\\?" , Matcher.quoteReplacement(getParameterValue(obj))); } else { sql = sql.replaceFirst("\\?" , "缺失" ); } } } } return sql; } private static String getParameterValue (Object obj) { String value; if (obj instanceof String) { value = "'" + obj.toString() + "'" ; } else if (obj instanceof Date) { DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA); value = "'" + formatter.format(new Date ()) + "'" ; } else { if (obj != null ) { value = obj.toString(); } else { value = "" ; } } return value; } }
部署 传统部署形式 在starter模块的pom.xml中
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 //配置的版本等内容 <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > <java.version > 1.8</java.version > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <project.reporting.outputEncoding > UTF-8</project.reporting.outputEncoding > <spring-boot.version > 2.4.2</spring-boot.version > <spring-cloud-alibaba.version > 2021.1</spring-cloud-alibaba.version > <spring-cloud.version > 2020.0.6</spring-cloud.version > </properties > .... //打包的名字 <build > <finalName > ${project.artifactId}</finalName > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > <version > 2.3.0.RELEASE</version > <executions > <execution > <goals > <goal > repackage</goal > </goals > </execution > </executions > </plugin > </plugins > </build >
打包好Jar包,复制到服务器上,java -jar运行即可
但是这样还是比较复杂
CI/CD jenkins自动打包集成和部署 1 2 3 4 docker search jenkins docker pull jenkins/jenkins:lts docker run -d -u root -p 8080:8080 -p 50000:50000 -v /var/jenkins_home:/var/jenkins_home -v /etc/localtime:/etc/localtime --name jenkins jenkins/jenkins:lts docker ps -a
然后配置一些密码,maven,拉一些包,配置pom.xml,配置路径,就可以打包好,写一个shell脚本然后运行就行了
[shell脚本语言(超全超详细)-CSDN博客](https://blog.csdn.net/weixin_43288201/article/details/105643692?ops_request_misc=&request_id=&biz_id=102&utm_term=shell 脚本 ^&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-5-105643692.142^v100^pc_search_result_base8&spm=1018.2226.3001.4187)
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 cp /var/jenkins_home/workspace/programmer-club-subject/programmer-club-subject/programmer-club-starter/target/programmer-club-starter.jar /var/jenkins_home/jar/ # !/bin/bash APP_NAME=programmer-club-starter.jar LOG_NAME=programmer-club-starter.log pid=`ps -ef | grep $APP_NAME | grep -v grep|awk '{print $2}'` function is_exist(){ pid=`ps -ef | grep $APP_NAME | grep -v grep|awk '{print $2}'` if [ -z ${pid} ]; then String="notExist" echo $String else String="exist" echo $String fi } str=$(is_exist) if [ ${str} = "exist" ]; then echo " 检测到已经启动的程序,pid 是 ${pid} " kill -9 $pid else echo " 程序没有启动了 " echo "${APP_NAME} is not running" fi str=$(is_exist) if [ ${str} = "exist" ]; then echo "${APP_NAME} 已经启动了. pid=${pid} ." else source /etc/profile BUILD_ID=dontKillMe nohup java -Xms300m -Xmx300m -jar /var/jenkins_home/jar/$APP_NAME >$LOG_NAME 2>&1 & echo "程序已重新启动..." fi
这个脚本中获取PID的命令:
ps -ef
:这个命令列出了当前系统上所有正在运行的进程。-e
选项表示显示所有进程,-f
选项表示显示完整格式。
grep $APP_NAME
:grep
命令用于搜索包含指定文本的行。在这里,它搜索包含APP_NAME
变量值(即programmer-club-starter.jar
)的行。
grep -v grep
:这个命令用于排除包含grep
本身的行,-v
选项表示显示不包含匹配文本的行。
awk '{print $2}'
:awk
是一个强大的文本处理工具。在这里,它用于打印每行的第二个字段,即进程ID(PID)。在Unix/Linux系统中,ps -ef
命令的输出中,PID通常位于第二列。
2. OSS模块(jc-club-oss)
OSS模块设计
注意:考虑 oss 的扩展性和切换性。
目前对接的 minio,要考虑,如果作为公共的 oss 服务,如何切换到其他的阿里云 oss 或者对接京东云的 oss。作为基础的 oss 服务,切换等等动作,不应该要求业务方进行改造,以及对切换有感知。
minio模块开发 controller->service->adaper(阿里/minio,适配器模式,不用工厂+策略的是因为工厂+策略的传入的参数之类的差不多,所以用适配器)->具体操作
minio安装部署及使用-CSDN博客
pom.xml
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 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <exclusions > <exclusion > <artifactId > spring-boot-starter-logging</artifactId > <groupId > org.springframework.boot</groupId > </exclusion > </exclusions > </dependency > <dependency > <groupId > io.minio</groupId > <artifactId > minio</artifactId > <version > 8.2.0</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.16</version > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-config</artifactId > //spingcloud的nacos </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-log4j2</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-bootstrap</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency > </dependencies >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 //application.yml server: port: 4000 minio: url: http://117.72.14.166:9000 accessKey: minioadmin secretKey: minioadmin storage: service: type: minio /* - `server.port: 4000 `:这行配置指定了应用程序运行的端口号为4000。 - `minio`部分:配置了MinIO服务的相关属性。 - `url`:MinIO服务的URL地址,这里是`http://117.72.14.166:9000`。 - `accessKey`和`secretKey`:访问MinIO服务的密钥和私钥,这里都设置为`minioadmin`。 - `storage.service.type.minio`:指定存储服务的类型为MinIO。 */
Miniocofig.java
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 package com.jingdianjichi.oss.config;import io.minio.MinioClient;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configuration public class MinioConfig { @Value("${minio.url}") private String url; @Value("${minio.accessKey}") private String accessKey; @Value("${minio.secretKey}") private String secretKey; @Bean public MinioClient getMinioClient () { return MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build(); } }
MinioUtil.java
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 package com.jingdianjichi.oss.util;import com.jingdianjichi.oss.entity.FileInfo;import io.minio.*;import io.minio.errors.*;import io.minio.http.Method;import io.minio.messages.Bucket;import io.minio.messages.Item;import org.springframework.stereotype.Component;import javax.annotation.Resource;import java.io.IOException;import java.io.InputStream;import java.security.InvalidKeyException;import java.security.NoSuchAlgorithmException;import java.util.LinkedList;import java.util.List;import java.util.stream.Collectors;@Component public class MinioUtil { @Resource private MinioClient minioClient; public void createBucket (String bucket) throws Exception { boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build()); if (!exists) { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build()); } } public void uploadFile (InputStream inputStream, String bucket, String objectName) throws Exception { minioClient.putObject(PutObjectArgs.builder().bucket(bucket).object(objectName) .stream(inputStream, -1 , 5242889L ).build()); } public List<String> getAllBucket () throws Exception { List<Bucket> buckets = minioClient.listBuckets(); return buckets.stream().map(Bucket::name).collect(Collectors.toList()); } public List<FileInfo> getAllFile (String bucket) throws Exception { Iterable<Result<Item>> results = minioClient.listObjects( ListObjectsArgs.builder().bucket(bucket).build()); List<FileInfo> fileInfoList = new LinkedList <>(); for (Result<Item> result : results) { FileInfo fileInfo = new FileInfo (); Item item = result.get(); fileInfo.setFileName(item.objectName()); fileInfo.setDirectoryFlag(item.isDir()); fileInfo.setEtag(item.etag()); fileInfoList.add(fileInfo); } return fileInfoList; } public InputStream downLoad (String bucket, String objectName) throws Exception { return minioClient.getObject( GetObjectArgs.builder().bucket(bucket).object(objectName).build() ); } public void deleteBucket (String bucket) throws Exception { minioClient.removeBucket( RemoveBucketArgs.builder().bucket(bucket).build() ); } public void deleteObject (String bucket, String objectName) throws Exception { minioClient.removeObject( RemoveObjectArgs.builder().bucket(bucket).object(objectName).build() ); } public String getPreviewFileUrl (String bucketName, String objectName) throws Exception{ GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder() .method(Method.GET) .bucket(bucketName).object(objectName).build(); return minioClient.getPresignedObjectUrl(args); } }
FileInfo.java
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 public class FileInfo { private String fileName; private Boolean directoryFlag; private String etag; public String getFileName () { return fileName; } public void setFileName (String fileName) { this .fileName = fileName; } public Boolean getDirectoryFlag () { return directoryFlag; } public void setDirectoryFlag (Boolean directoryFlag) { this .directoryFlag = directoryFlag; } public String getEtag () { return etag; } public void setEtag (String etag) { this .etag = etag; } }
FileController.java
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 @RestController public class FileController { @Resource private FileService fileService; @RequestMapping("/testGetAllBuckets") public String testGetAllBuckets () throws Exception { List<String> allBucket = fileService.getAllBucket(); return allBucket.get(0 ); } @RequestMapping("/getUrl") public String getUrl (String bucketName, String objectName) throws Exception { return fileService.getUrl(bucketName, objectName); } @RequestMapping("/upload") public Result upload (MultipartFile uploadFile, String bucket, String objectName) throws Exception { String url = fileService.uploadFile(uploadFile, bucket, objectName); return Result.ok(url); } }
fileService.java
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 @Service public class FileService { private final StorageAdapter storageAdapter; public FileService (StorageAdapter storageAdapter) { this .storageAdapter = storageAdapter; } public List<String> getAllBucket () { return storageAdapter.getAllBucket(); } public String getUrl (String bucketName,String objectName) { return storageAdapter.getUrl(bucketName,objectName); } public String uploadFile (MultipartFile uploadFile, String bucket, String objectName) { storageAdapter.uploadFile(uploadFile,bucket,objectName); objectName = objectName + "/" + uploadFile.getOriginalFilename(); return storageAdapter.getUrl(bucket, objectName); } }
StorageConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Configuration @RefreshScope public class StorageConfig { @Value("${storage.service.type}") private String storageType; @Bean @RefreshScope public StorageAdapter storageService () { if ("minio" .equals(storageType)) { return new MinioStorageAdapter (); } else if ("aliyun" .equals(storageType)) { return new AliStorageAdapter (); } else { throw new IllegalArgumentException ("未找到对应的文件存储处理器" ); } } }
StorageAdapter.java
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 public interface StorageAdapter { void createBucket (String bucket) ; void uploadFile (MultipartFile uploadFile, String bucket, String objectName) ; List<String> getAllBucket () ; List<FileInfo> getAllFile (String bucket) ; InputStream downLoad (String bucket, String objectName) ; void deleteBucket (String bucket) ; void deleteObject (String bucket, String objectName) ; String getUrl (String bucket, String objectName) ; }
MinioStorageAdapter.java
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 package com.jingdianjichi.oss.adapter;import com.jingdianjichi.oss.entity.FileInfo;import com.jingdianjichi.oss.util.MinioUtil;import lombok.SneakyThrows;import org.springframework.beans.factory.annotation.Value;import org.springframework.web.multipart.MultipartFile;import javax.annotation.Resource;import java.io.InputStream;import java.util.List;public class MinioStorageAdapter implements StorageAdapter { @Resource private MinioUtil minioUtil; @Value("${minio.url}") private String url; @Override @SneakyThrows public void createBucket (String bucket) { minioUtil.createBucket(bucket); } @Override @SneakyThrows public void uploadFile (MultipartFile uploadFile, String bucket, String objectName) { minioUtil.createBucket(bucket); if (objectName != null ) { minioUtil.uploadFile(uploadFile.getInputStream(), bucket, objectName + "/" + uploadFile.getOriginalFilename()); } else { minioUtil.uploadFile(uploadFile.getInputStream(), bucket, uploadFile.getOriginalFilename()); } } @Override @SneakyThrows public List<String> getAllBucket () { return minioUtil.getAllBucket(); } @Override @SneakyThrows public List<FileInfo> getAllFile (String bucket) { return minioUtil.getAllFile(bucket); } @Override @SneakyThrows public InputStream downLoad (String bucket, String objectName) { return minioUtil.downLoad(bucket, objectName); } @Override @SneakyThrows public void deleteBucket (String bucket) { minioUtil.deleteBucket(bucket); } @Override @SneakyThrows public void deleteObject (String bucket, String objectName) { minioUtil.deleteObject(bucket, objectName); } @Override @SneakyThrows public String getUrl (String bucket, String objectName) { return url + "/" + bucket + "/" + objectName; } }
这里相当于做了个适配器,这里用的是minio的,如果要用阿里的服务就要切到另一套了
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 package com.jingdianjichi.oss.adapter;import com.jingdianjichi.oss.entity.FileInfo;import org.springframework.web.multipart.MultipartFile;import java.io.InputStream;import java.util.LinkedList;import java.util.List;public class AliStorageAdapter implements StorageAdapter { @Override public void createBucket (String bucket) { } @Override public void uploadFile (MultipartFile uploadFile, String bucket, String objectName) { } @Override public List<String> getAllBucket () { List<String> bucketNameList = new LinkedList <>(); bucketNameList.add("aliyun" ); return bucketNameList; } @Override public List<FileInfo> getAllFile (String bucket) { return null ; } @Override public InputStream downLoad (String bucket, String objectName) { return null ; } @Override public void deleteBucket (String bucket) { } @Override public void deleteObject (String bucket, String objectName) { } @Override public String getUrl (String bucket, String objectName) { return null ; } }
OSS模块配合nacos实现动态切换 nacos 作为配置中心,可以实现动态配置,适用于比如动态数据源切换,动态切换 oss。
这里要配合RefreshScope
注解去使用 ,实现动态刷新(minio->ali)。
一文带你理解@RefreshScope注解实现动态刷新原理-CSDN博客
集成naocs动态配置 配置
jc-club-oss/pom.xml
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 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <exclusions > <exclusion > <artifactId > spring-boot-starter-logging</artifactId > <groupId > org.springframework.boot</groupId > </exclusion > </exclusions > </dependency > <dependency > <groupId > io.minio</groupId > <artifactId > minio</artifactId > <version > 8.2.0</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.16</version > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-config</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-log4j2</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-bootstrap</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency > </dependencies >
jc-club-oss/src/main/java/com/jingdianjichi/oss/controller/FileController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class FileController { @Resource private FileService fileService; @NacosValue(value = "${storage.service.type}", autoRefreshed = true) private String storageType; @RequestMapping("/testGetAllBuckets") public String testGetAllBuckets () throws Exception { List<String> allBucket = fileService.getAllBucket(); return allBucket.get(0 ); } @RequestMapping("/testNacos") public String testNacos () throws Exception { return storageType; } }
jc-club-oss/src/main/resources/bootstrap.yml
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 spring: application: name: jc-club-oss-dev profiles: active: dev cloud: nacos: config: server-addr: 117.72 .14 .166 :8848 prefix: ${spring.application.name} group: DEFAULT_GROUP namespace: file-extension: yaml discovery: enabled: true server-addr: 117.72 .14 .166 :8848 /* Spring 应用名称: application.name: jc-club-oss-dev:定义了Spring应用的名称为jc-club-oss-dev。 激活的配置文件: profiles.active: dev:指定了激活的配置文件为dev,这意味着在启动时会加载application-dev.yaml或application-dev.properties等配置文件。 Nacos 配置: cloud.nacos.config.server-addr: 117.72 .14 .166 :8848:定义了Nacos配置服务器的地址,这里是117.72.14.166:8848。 cloud.nacos.config.prefix: ${spring.application.name}:定义了配置的前缀,这里使用了${spring.application.name},即应用名称作为前缀。 cloud.nacos.config.group: DEFAULT_GROUP:定义了配置的分组,这里是DEFAULT_GROUP。 cloud.nacos.config.namespace:这里没有指定具体的命名空间,通常用于区分不同环境的配置。 cloud.nacos.config.file-extension: yaml:指定了配置文件的扩展名,这里是yaml。 Nacos 服务发现: cloud.nacos.discovery.enabled: true :启用了Nacos的服务发现功能。 cloud.nacos.discovery.server-addr: 117.72 .14 .166 :8848:定义了Nacos服务发现服务器的地址,与配置服务器地址一致。 配置的作用 配置管理:通过Nacos配置中心,应用可以动态地读取和更新配置,而不需要重启应用。这对于微服务架构中的配置管理非常有用。 服务发现:通过Nacos服务注册中心,应用可以注册自身服务并发现其他服务,从而实现服务间的调用。 使用场景 在微服务架构中,使用Nacos进行配置管理和服务发现可以提高系统的可维护性和可扩展性。 动态配置更新:应用可以根据Nacos配置中心的配置变化自动更新其配置,而不需要人工干预。*/
通过在nacos界面修改:
1 2 3 storage: service: type: minio/aliyun
即可通过**@Configuration**注解的Storage.java
中的@Bean、@RefreshScope
注解的storageService方法会创建适配器的对应的StorageAdapter,拿到minio/阿里云的oss服务。
@Configuration注解使用详解【记录】-CSDN博客
Spring 常用注解@Configuration,@Bean,@Component,@Service,@Controller,@Repository,@Entity的区分与学习_@entity @repository 区别-CSDN博客
3. 登录鉴权模块
技术选型 Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证 、权限认证 、单点登录 、OAuth2.0 、分布式Session会话 、微服务网关鉴权 等一系列权限相关问题。
Sa-Token 旨在以简单、优雅的方式完成系统的权限认证部分,以登录认证为例,你只需要:
无需实现任何接口,无需创建任何配置文件,只需要这一句静态代码的调用,便可以完成会话登录认证。
如果一个接口需要登录后才能访问,我们只需调用以下代码:
在 Sa-Token 中,大多数功能都可以一行代码解决:
踢人下线:
1 2 StpUtil.kickout(10077 );复制到剪贴板错误复制成功
权限认证:
1 2 3 4 5 6 @SaCheckPermission("user:add") public String insert (SysUser user) { return "用户增加" ; }
路由拦截鉴权:
1 2 3 4 5 6 7 8 9 10 registry.addInterceptor(new SaInterceptor (handler -> { SaRouter.match("/user/**" , r -> StpUtil.checkPermission("user" )); SaRouter.match("/admin/**" , r -> StpUtil.checkPermission("admin" )); SaRouter.match("/goods/**" , r -> StpUtil.checkPermission("goods" )); SaRouter.match("/orders/**" , r -> StpUtil.checkPermission("orders" )); SaRouter.match("/notice/**" , r -> StpUtil.checkPermission("notice" )); })).addPathPatterns("/**" ); 9
当你受够 Shiro、SpringSecurity 等框架的三拜九叩之后,你就会明白,相对于这些传统老牌框架,Sa-Token 的 API 设计是多么的简单、优雅!
Sa-Token 功能一览
Sa-Token 目前主要五大功能模块:登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。
登录认证 —— 单端登录、多端登录、同端互斥登录、七天内免登录。
权限认证 —— 权限认证、角色认证、会话二级认证。
踢人下线 —— 根据账号id踢人下线、根据Token值踢人下线。
注解式鉴权 —— 优雅的将鉴权与业务代码分离。
路由拦截式鉴权 —— 根据路由拦截鉴权,可适配 restful 模式。
Session会话 —— 全端共享Session,单端独享Session,自定义Session,方便的存取值。
持久层扩展 —— 可集成 Redis,重启数据不丢失。
前后台分离 —— APP、小程序等不支持 Cookie 的终端也可以轻松鉴权。
Token风格定制 —— 内置六种 Token 风格,还可:自定义 Token 生成策略。
记住我模式 —— 适配 [记住我] 模式,重启浏览器免验证。
二级认证 —— 在已登录的基础上再次认证,保证安全性。
模拟他人账号 —— 实时操作任意用户状态数据。
临时身份切换 —— 将会话身份临时切换为其它账号。
同端互斥登录 —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录。
账号封禁 —— 登录封禁、按照业务分类封禁、按照处罚阶梯封禁。
密码加密 —— 提供基础加密算法,可快速 MD5、SHA1、SHA256、AES 加密。
会话查询 —— 提供方便灵活的会话查询接口。
Http Basic认证 —— 一行代码接入 Http Basic、Digest 认证。
全局侦听器 —— 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作。
全局过滤器 —— 方便的处理跨域,全局设置安全响应头等操作。
多账号体系认证 —— 一个系统多套账号分开鉴权(比如商城的 User 表和 Admin 表)
单点登录 —— 内置三种单点登录模式:同域、跨域、同Redis、跨Redis、前后端分离等架构都可以搞定。
单点注销 —— 任意子系统内发起注销,即可全端下线。
OAuth2.0认证 —— 轻松搭建 OAuth2.0 服务,支持openid模式 。
分布式会话 —— 提供共享数据中心分布式会话方案。
微服务网关鉴权 —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证。
RPC调用鉴权 —— 网关转发鉴权,RPC调用鉴权,让服务调用不再裸奔
临时Token认证 —— 解决短时间的 Token 授权问题。
独立Redis —— 将权限缓存与业务缓存分离。
Quick快速登录认证 —— 为项目零代码注入一个登录页面。
标签方言 —— 提供 Thymeleaf 标签方言集成包,提供 beetl 集成示例。
jwt集成 —— 提供三种模式的 jwt 集成方案,提供 token 扩展参数能力。
RPC调用状态传递 —— 提供 dubbo、grpc 等集成包,在RPC调用时登录状态不丢失。
参数签名 —— 提供跨系统API调用签名校验模块,防参数篡改,防请求重放。
自动续签 —— 提供两种Token过期策略,灵活搭配使用,还可自动续签。
开箱即用 —— 提供SpringMVC、WebFlux、Solon 等常见框架集成包,开箱即用。
最新技术栈 —— 适配最新技术栈:支持 SpringBoot 3.x,jdk 17。
功能结构图:
鉴权设计-RBAC模型 RBAC 模型(role-based access control)
非常成熟的安全的模型概念,基于角色帮助我们把授权和用户的访问控制来做结合。
User(用户)用户就是指我们的系统使用者。
PerMission(权限)用户我们对系统的操作,访问哪些东西,可以操作写入操作等等。实际的例子,比如新增题目。
Role(角色)我们去把一组的权限,去做集合,就得到了角色。
核心思想其实就是把角色和权限做关联,实现整体的一个灵活访问,提高我们的系统的安全性和管理型。基于这个模型,我们的开发速度还有粒度的粗细也都是十分好控制的。
优点:
灵活,安全,简化管理。
三种RBAC模型:
RBAC-0 模型
用户和角色是一个多对多的关系,角色和权限也是一个多对多关系。
RBAC-1 模型
多了一个继承的概念。
比如一个业务部门,经理,主管,营业员。主管的权限肯定不能大于经理,营业员不能大于主管。
子角色的范围一定会小于父角色。
RBAC-2 模型
角色互斥,基数约束,先决条件等等。
角色互斥:同一个用户,不能被分配到复制的角色,比如说,你是一个采购,那你就不能分配销售。
基数约束:一个角色分配的用户数量是有限的。比如有一个公司的架构师,最多只能有三个。
先决条件:你想获得架构师的角色,那你必然得先是一个资深工程师的角色。
权限 :
他的含义其实是非常广泛的,可以是菜单,页面,字段,数据。
用户组:
平台的用户基数非常大,角色也非常的多,如果说我给每个用户都操作一下角色,就非常的麻烦。
抽象一层组的概念,把同类的用户,放在一起,直接拥有相同的权限。
非常有益于减少工作量,一些管理方面也非常合适。用户组抽象到实际中,其实就是部门啊,科室啊。
鉴权数据模型设计
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 DROP TABLE IF EXISTS `auth_permission`;CREATE TABLE `auth_permission`( `id` bigint (20 ) NOT NULL AUTO_INCREMENT, `name` varchar (64 ) DEFAULT NULL COMMENT '权限名称' , `parent_id` bigint (20 ) DEFAULT NULL COMMENT '父id' , `type` tinyint(4 ) DEFAULT NULL COMMENT '权限类型 0菜单 1操作' , `menu_url` varchar (255 ) DEFAULT NULL COMMENT '菜单路由' , `status` tinyint(2 ) DEFAULT NULL COMMENT '状态 0启用 1禁用' , `show ` tinyint(2 ) DEFAULT NULL COMMENT '展示状态 0展示 1隐藏' , `icon` varchar (128 ) DEFAULT NULL COMMENT '图标' , `permission_key` varchar (64 ) DEFAULT NULL COMMENT '权限唯一标识' , `created_by` varchar (32 ) DEFAULT NULL COMMENT '创建人' , `created_time` datetime DEFAULT NULL COMMENT '创建时间' , `update_by` varchar (32 ) DEFAULT NULL COMMENT '更新人' , `update_time` datetime DEFAULT NULL COMMENT '更新时间' , `is_deleted` int (11 ) DEFAULT '0' COMMENT '是否被删除 0为删除 1已删除' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 2 DEFAULT CHARSET= utf8; INSERT INTO `auth_permission`VALUES ('1' , '新增题目' , '0' , '1' , 'ladiwd/www' , '0' , '0' , 'http://1.png' , 'subject:add1' , 'oYA4HtwGJEsLio6pGrhx5Hzv9XD0' , '2024-02-28 03:20:38' , '' , '2023-11-12 16:17:12' , '0' ); DROP TABLE IF EXISTS `auth_role`;CREATE TABLE `auth_role`( `id` bigint (20 ) NOT NULL AUTO_INCREMENT, `role_name` varchar (32 ) DEFAULT NULL COMMENT '角色名称' , `role_key` varchar (64 ) DEFAULT NULL COMMENT '角色唯一标识' , `created_by` varchar (32 ) DEFAULT NULL COMMENT '创建人' , `created_time` datetime DEFAULT NULL COMMENT '创建时间' , `update_by` varchar (32 ) DEFAULT NULL COMMENT '更新人' , `update_time` datetime DEFAULT NULL COMMENT '更新时间' , `is_deleted` int (11 ) DEFAULT '0' COMMENT '是否被删除 0未删除 1已删除' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 3 DEFAULT CHARSET= utf8; INSERT INTO `auth_role`VALUES ('1' , '管理员' , 'admin_user' , 'oYA4HtwGJEsLio6pGrhx5Hzv9XD0' , '2024-02-28 03:20:44' , '' , '2023-11-12 16:16:07' , '0' ); INSERT INTO `auth_role`VALUES ('2' , '普通用户' , 'normal_user' , 'oYA4HtwGJEsLio6pGrhx5Hzv9XD0' , '2024-02-28 03:20:44' , '' , '2023-11-12 16:16:10' , '0' ); DROP TABLE IF EXISTS `auth_role_permission`;CREATE TABLE `auth_role_permission`( `id` bigint (20 ) NOT NULL AUTO_INCREMENT, `role_id` bigint (20 ) DEFAULT NULL COMMENT '角色id' , `permission_id` bigint (20 ) DEFAULT NULL COMMENT '权限id' , `created_by` varchar (32 ) DEFAULT NULL COMMENT '创建人' , `created_time` datetime DEFAULT NULL COMMENT '创建时间' , `update_by` varchar (32 ) DEFAULT NULL COMMENT '更新人' , `update_time` datetime DEFAULT NULL COMMENT '更新时间' , `is_deleted` int (11 ) DEFAULT '0' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 2 DEFAULT CHARSET= utf8 COMMENT= '角色权限关联表' ; DROP TABLE IF EXISTS `auth_user`;CREATE TABLE `auth_user`( `id` bigint (20 ) NOT NULL AUTO_INCREMENT COMMENT '主键' , `user_name` varchar (32 ) DEFAULT NULL COMMENT '用户名称/账号' , `nick_name` varchar (32 ) DEFAULT NULL COMMENT '昵称' , `email` varchar (32 ) DEFAULT NULL COMMENT '邮箱' , `phone` varchar (32 ) DEFAULT NULL COMMENT '手机号' , `password` varchar (64 ) DEFAULT NULL COMMENT '密码' , `sex` tinyint(2 ) DEFAULT NULL COMMENT '性别' , `avatar` varchar (255 ) DEFAULT NULL COMMENT '头像' , `status` tinyint(2 ) DEFAULT NULL COMMENT '状态 0启用 1禁用' , `introduce` varchar (255 ) DEFAULT NULL COMMENT '个人介绍' , `ext_json` varchar (255 ) DEFAULT NULL COMMENT '特殊字段' , `created_by` varchar (32 ) DEFAULT NULL COMMENT '创建人' , `created_time` datetime DEFAULT NULL COMMENT '创建时间' , `update_by` varchar (32 ) DEFAULT NULL COMMENT '更新人' , `update_time` datetime DEFAULT NULL COMMENT '更新时间' , `is_deleted` int (11 ) DEFAULT '0' COMMENT '是否被删除 0未删除 1已删除' , PRIMARY KEY (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8 COMMENT= '用户信息表' ; DROP TABLE IF EXISTS `auth_user_role`;CREATE TABLE `auth_user_role`( `id` bigint (20 ) NOT NULL AUTO_INCREMENT COMMENT '主键' , `user_id` bigint (20 ) DEFAULT NULL COMMENT '用户id' , `role_id` bigint (20 ) DEFAULT NULL COMMENT '角色id' , `created_by` varchar (32 ) DEFAULT NULL COMMENT '创建人' , `created_time` datetime DEFAULT NULL COMMENT '创建时间' , `update_by` varchar (32 ) DEFAULT NULL COMMENT '更新人' , `update_time` datetime DEFAULT NULL COMMENT '更新时间' , `is_deleted` int (11 ) DEFAULT '0' , PRIMARY KEY (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8 COMMENT= '用户角色表' ;
鉴权架构设计 jc-club-auth :这个服务承载了我们所有的基础数据源。他不管鉴权,只管数据相关的持久化操作以及业务操作 ,提供出各种各样的权限相关的接口。
nacos: 将 auth 服务以及 subject 服务都注册到上面。内部进行调用,不对外暴露。通过 nacos 实现我们的服务发现。
gateway(网关) :网关层会对外提供服务 ,内部实现路由,鉴权 。整体我们采取 token 的方式来与前端进行交互。由网关来决定当前用户是否可以操作到后面的业务逻辑。
鉴权、路由等处理都由网关来做。
鉴权功能设计
用户基础模块
新增用户
修改用户
删除用户
用户启用
用户禁用
用户密码加密
角色基础模块
权限基础模块
新增权限
修改权限
删除权限
权限禁用与启用
权限的展示与隐藏
权限与角色关联
登录注册模块
短信的方式,通过向手机号发送验证码,来实现用户的验证并登录(考虑的成本是短信的费用)
邮箱的注册登录。
用户注册的时候,留一个邮箱,我们往邮箱里通过邮箱服务器发送一个链接,用户点击之后,实现一个激活,激活成功之后就完成了注册。(0 成本,坏处这种发送的邮件很容易进垃圾箱)
个人公众号模式(个人开发者无公司的,比较适合使用,0 成本)
用户登录的时候,弹出我们的这个公众号的码。扫码后,用户输入我们提示的验证码。可以随机比如说 nadbuge,通过我们的公众号对接的回调。能拿到一定的信息,用户的 openId。进而记录用户的信息
企业的服务号(必须要有营业执照,自己玩的不上线的话,也可以用测试号)
好处就是不仅打通了各种回调,而且还能拿到用户的信息。
传统的 pc 形式,都是登录之后,写入 cookie。前端再次请求的时候,带着 cookie 一个身份识别就可以完成认证。
坏处是什么?小程序呀,app 呀,其实是没有 cookie 这个概念的。
单点登录(SSO)详解——超详细-CSDN博客
为了更好的扩展,我们就直接选择 token的模式 。token 放入 header 来实现用户身份的识别与鉴权。
发现风险用户,可以通过后台直接把用户踢掉,禁止其再访问,token 也可以直接置为失效的形式。
如果说我们选择了 token,然后不做 token 的保存,服务重启呀,分布式微服务啊,数据是无法共享并且会产生丢失问题,所以用 redis 来存储一些信息,实现共享。
当我们去勾选记住我的时候,下次登录就自动实现了。
前后端分离,没有 token 的时候,必然会产生无法实现的问题,我们就选择在前端的 localstorage 来做。
网关统一鉴权
校验权限,校验用户的角色等等的东西,就放在网关里面统一去做。
不放在网关,导致每个微服务,全要引入的鉴权的框架,不断的去写重复的代码。
数据的权限获取产生问题:
网关直接对接数据库,实现查询。
redis 中获取数据,获取不到的时候还是要像第一种一样去数据库里查。
redis 中获取缓存,没有的话,从 auth 服务里面获取相关的信息。
直接从 redis 读取。
auth模块配置 jc-club-auth-application/pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependencies > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > //用于 Spring Cloud 项目中,通过 Feign 客户端简化 HTTP 客户端的调用。 <version > 3.0.7</version > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-loadbalancer</artifactId > //用于 Spring Cloud 项目中,提供客户端负载均衡功能。 <version > 3.0.6</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > //自动生成 getter、setter、构造函数等。 <version > 1.18.16</version > </dependency > </dependencies >
jc-club-auth-application-controller/pom.xml
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 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <version > 2.4.2</version > </dependency > <dependency > <groupId > cn.dev33</groupId > <artifactId > sa-token-spring-boot-starter</artifactId > <version > 1.37.0</version > </dependency > <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-auth-domain</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-auth-api</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > </dependencies >
jc-club-auth-starter/pom.xml
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 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <version > 2.4.2</version > <exclusions > <exclusion > <artifactId > spring-boot-starter-logging</artifactId > <groupId > org.springframework.boot</groupId > </exclusion > </exclusions > </dependency > <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-auth-infra</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-auth-application-controller</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-config</artifactId > //nacos </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-bootstrap</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency > </dependencies >
jc-club-auth-domain/pom.xml
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 <dependencies > <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-auth-infra</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-auth-common</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > <dependency > <groupId > com.fasterxml.jackson.core</groupId > <artifactId > jackson-core</artifactId > <version > 2.12.7</version > </dependency > <dependency > <groupId > com.fasterxml.jackson.core</groupId > <artifactId > jackson-databind</artifactId > <version > 2.12.7</version > </dependency > <dependency > <groupId > com.google.code.gson</groupId > <artifactId > gson</artifactId > <version > 2.8.6</version > </dependency > </dependencies >
jc-club-auth-infra/pom.xml
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 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-jdbc</artifactId > <version > 2.4.2</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > <version > 1.1.22</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.22</version > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.4.0</version > </dependency > <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-auth-common</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > </dependencies >
登录(详情在用户模块) 注意openid就是username
Sa-token
前后端分离 (sa-token.cc)
在auth模块中的UserController.java
中实现
jc-club-auth: UserController
1 2 3 4 5 6 7 8 9 10 @RequestMapping("doLogin") public Result<SaTokenInfo> doLogin (@RequestParam("validCode") String validCode) { try { Preconditions.checkArgument(!StringUtils.isBlank(validCode), "验证码不能为空!" ); return Result.ok(authUserDomainService.doLogin(validCode)); } catch (Exception e) { log.error("UserController.doLogin.error:{}" , e.getMessage(), e); return Result.fail("用户登录失败" ); } }
jc-club-auth:AuthUserDomainServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Override public SaTokenInfo doLogin (String validCode) { String loginKey = redisUtil.buildKey(LOGIN_PREFIX, validCode); String openId = redisUtil.get(loginKey); if (StringUtils.isBlank(openId)) { return null ; } AuthUserBO authUserBO = new AuthUserBO (); authUserBO.setUserName(openId); this .register(authUserBO); StpUtil.login(openId); SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); return tokenInfo; }
微服务注册到nacos 阿里云脚手架用于组件/版本的选择兼容,非常方便: start.aliyun.com
oss服务->nacos
pom.xml
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 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > //构建 Web 应用程序所需的 Spring MVC 和 Tomcat。 <exclusions > <exclusion > <artifactId > spring-boot-starter-logging</artifactId > //不会包含默认的日志框架 <groupId > org.springframework.boot</groupId > </exclusion > </exclusions > </dependency > <dependency > <groupId > io.minio</groupId > //用于与 Minio 对象存储服务进行交互的客户端库。 <artifactId > minio</artifactId > <version > 8.2.0</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > // <artifactId > lombok</artifactId > //减少样板代码,自动生成 getter、setter 等。 <version > 1.18.16</version > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-config</artifactId > //用于集成 Nacos 配置中心,提供配置管理功能。 </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-log4j2</artifactId > //替换默认的日志框架,使用 Log4j2 作为日志记录器。 </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-bootstrap</artifactId > //用于 Spring Cloud 应用的引导类,帮助应用启动时加载配置(bootstrap.yaml)。 </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > //用于集成 Nacos 服务发现,提供服务注册与发现功能。 </dependency > </dependencies >
这里要用到bootstrap.yaml
去在nacos中注册:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 spring: application: name: jc-club-oss-dev profiles: active: dev cloud: nacos: config: server-addr: 117.72 .14 .166 :8848 prefix: ${spring.application.name} group: DEFAULT_GROUP namespace: file-extension: yaml discovery: enabled: true server-addr: 117.72 .14 .166 :8848 //部署有nacos的云服务器的地址
启动程序就注册上去了
(gateway->nacos)Spring Cloud Gateway搭建及路由配置 pom.xml
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 <dependencies > <dependency > <groupId > io.minio</groupId > <artifactId > minio</artifactId > <version > 8.2.0</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.16</version > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-config</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-bootstrap</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-gateway</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-loadbalancer</artifactId > </dependency > </dependencies >
bootstrap.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 spring: application: name: jc-club-gateway-dev profiles: active: dev cloud: nacos: config: server-addr: 117.72 .14 .166 :8848 prefix: ${spring.application.name} group: DEFAULT_GROUP namespace: file-extension: yaml discovery: enabled: true server-addr: 117.72 .14 .166 :8848
application.yaml
,用于路由转发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 server: port: 5000 spring: cloud: gateway: routes: - id: oss uri: lb://jc-club-oss-dev //使用 lb:// 前缀表示这是一个负载均衡器的调用。jc-club-oss-dev 是服务名,Spring Cloud Gateway 将通过服务发现找到对应的实例。 predicates: - Path=/oss/** //路由匹配条件 filters: - StripPrefix=1 //过滤器用于从请求路径中去除前缀。 - id: auth uri: lb://jc-club-auth-dev predicates: - Path=/auth/** filters: - StripPrefix=1 - id: subject uri: lb://jc-club-subject-dev predicates: - Path=/subject/** filters: - StripPrefix=1
运行及注册到nacos中,并且可以通过网关进行路由转发到对应的微服务上。
鉴权+刷题->nacos auth:
starter/pom.xml
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 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <version > 2.4.2</version > <exclusions > <exclusion > <artifactId > spring-boot-starter-logging</artifactId > <groupId > org.springframework.boot</groupId > </exclusion > </exclusions > </dependency > <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-auth-infra</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-auth-application-controller</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-config</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-bootstrap</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency > </dependencies >
bootstrap.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 spring: application: name: jc-club-gateway-dev profiles: active: dev cloud: nacos: config: server-addr: 117.72 .14 .166 :8848 prefix: ${spring.application.name} group: DEFAULT_GROUP namespace: file-extension: yaml discovery: enabled: true server-addr: 117.72 .14 .166 :8848
subject:
starter/pom.xml
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 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <version > 2.4.2</version > <exclusions > <exclusion > <artifactId > spring-boot-starter-logging</artifactId > <groupId > org.springframework.boot</groupId > </exclusion > </exclusions > </dependency > <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-application-controller</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-application-mq</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-infra</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-config</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-bootstrap</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency > </dependencies >
bootstrap.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 spring: application: name: jc-club-subject-dev profiles: active: dev cloud: nacos: config: server-addr: 117.72 .14 .166 :8848 prefix: ${spring.application.name} group: DEFAULT_GROUP namespace: file-extension: yaml discovery: enabled: true server-addr: 117.72 .14 .166 :8848
Sa-Token集成Redis 集成 Redis (sa-token.cc)
redis.io/docs/management/config
gateway网关(sa-token)基于redis实现分布式会话鉴权 gateway 集成 redis 及 refactor 鉴权
pom文件写入
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 <dependencies > <dependency > <groupId > cn.dev33</groupId > <artifactId > sa-token-reactor-spring-boot-starter</artifactId > <version > 1.37.0</version > </dependency > <dependency > <groupId > cn.dev33</groupId > <artifactId > sa-token-redis-jackson</artifactId > <version > 1.37.0</version > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-pool2</artifactId > </dependency > <dependency > <groupId > com.google.code.gson</groupId > <artifactId > gson</artifactId > <version > 2.8.6</version > </dependency > </dependencies >
gateway的applicaiton.yaml
,修改网关配置文件
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 server: port: 5000 spring: cloud: gateway: routes: - id: oss uri: lb://jc-club-oss-dev predicates: - Path=/oss/** filters: - StripPrefix=1 - id: auth uri: lb://jc-club-auth-dev predicates: - Path=/auth/** filters: - StripPrefix=1 - id: subject uri: lb://jc-club-subject-dev predicates: - Path=/subject/** filters: - StripPrefix=1 - id: practice uri: lb://jc-club-practice-dev predicates: - Path=/practice/** filters: - StripPrefix=1 - id: circle uri: lb://jc-club-circle predicates: - Path=/circle/** filters: - StripPrefix=1 - id: interview uri: lb://jc-club-interview predicates: - Path=/interview/** filters: - StripPrefix=1 redis: database: 1 host: 117.72 .14 .166 port: 6379 password: jichi1234 timeout: 2s lettuce: pool: max-active: 200 max-wait: -1ms max-idle: 10 min-idle: 0 sa-token: token-name: satoken timeout: 2592000 active-timeout: -1 is-concurrent: true is-share: true token-style: random-32 is-log: true token-prefix: jichi
jc-club-auth-starter/application.yaml
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 server: port: 3011 spring: datasource: username: root password: qvQP7MBvSkbyGzLzlRaPp9swmOmkqdVkVgBNPQF7pMlImathGYopQcWR2CuZMZAkL1xrDHwut9Hbr2TZ4qmr2Q== driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://117.72.14.166:3306/jc-club?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false type: com.alibaba.druid.pool.DruidDataSource druid: initial-size: 20 min-idle: 20 connectionProperties: config.decrypt=true;config.decrypt.key=${publicKey}; max-active: 100 max-wait: 60000 stat-view-servlet: enabled: true url-pattern: /druid/* login-username: admin login-password: 123456 filter: stat: enabled: true slow-sql-millis: 2000 log-slow-sql: true wall: enabled: true config: enabled: true redis: database: 1 host: 117.72 .14 .166 port: 6379 password: jichi1234 timeout: 2s lettuce: pool: max-active: 200 max-wait: -1ms max-idle: 10 min-idle: 0 publicKey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAMJzo9TiSuOGAMR2Zma25lWdtR1oxq6RcZYnWE9vcYLNKxUOkBlvSfMrbS25KtlJi+hIzikfCoyTDB0VI5gB3Q8CAwEAAQ== logging: config: classpath:log4j2-spring.xml sa-token: token-name: satoken timeout: 2592000 active-timeout: -1 is-concurrent: true is-share: true token-style: random-32 is-log: true token-prefix: jichi
网关自定义权限接口扩展
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Component public class StpInterfaceImpl implements StpInterface { @Resource private RedisUtil redisUtil; private String authPermissionPrefix = "auth.permission" ; private String authRolePrefix = "auth.role" ; @Override public List<String> getPermissionList (Object loginId, String loginType) { return getAuth(loginId.toString(), authPermissionPrefix); } @Override public List<String> getRoleList (Object loginId, String loginType) { return getAuth(loginId.toString(), authRolePrefix); } }
jc-club-gateway:SaTokenConfigure
,satoken配置。
这个配置类的主要作用是:
拦截所有请求。
根据请求的路径,执行不同的权限校验逻辑,确保只有具有相应权限的用户才能访问特定的资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Configuration public class SaTokenConfigure { @Bean public SaReactorFilter getSaReactorFilter () { return new SaReactorFilter () .addInclude("/**" ) .setAuth(obj -> { System.out.println("-------- 前端访问path:" + SaHolder.getRequest().getRequestPath()); SaRouter.match("/auth/**" , "/auth/user/doLogin" , r -> StpUtil.checkRole("admin" )); SaRouter.match("/oss/**" , r -> StpUtil.checkLogin()); SaRouter.match("/subject/subject/add" , r -> StpUtil.checkPermission("subject:add" )); SaRouter.match("/subject/**" , r -> StpUtil.checkLogin()); }) ; } }
网关全局异常处理(sa-token mono) Sa-token 提供的示例,适用于单体项目的全局异常捕获。我们选择了微服务架构,则就要变为通过网关来进行全局异常的处理,我们希望,权限发生异常的时候,可以统一做 401 的返回,前端进行跳转登录。
GatewayExceptionHandler.java
这个全局异常处理器的主要作用是:
捕获并处理网关中的异常。
根据异常类型返回不同的状态码和消息。
将异常信息序列化为 JSON 格式,并设置正确的 Content-Type
发送回客户端。
深入学习ErrorWebExceptionHandler-CSDN博客
Flux、Mono、Reactor 实战(史上最全)_reactor mono-CSDN博客
[响应式编程二Mono,Flux简单介绍_flux mono-CSDN博客](https://blog.csdn.net/lsdstone/article/details/134983206?ops_request_misc=&request_id=&biz_id=102&utm_term=mono java响应式&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-2-134983206.142^v100^pc_search_result_base8&spm=1018.2226.3001.4187)
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 @Component public class GatewayExceptionHandler implements ErrorWebExceptionHandler { private ObjectMapper objectMapper = new ObjectMapper (); @Override public Mono<Void> handle (ServerWebExchange serverWebExchange, Throwable throwable) { ServerHttpRequest request = serverWebExchange.getRequest(); ServerHttpResponse response = serverWebExchange.getResponse(); Integer code = 200 ; String message = "" ; if (throwable instanceof SaTokenException) { code = 401 ; message = "用户无权限" ; throwable.printStackTrace(); } else { code = 500 ; message = "系统繁忙" ; throwable.printStackTrace(); } Result result = Result.fail(code, message); response.getHeaders().setContentType(MediaType.APPLICATION_JSON); return response.writeWith(Mono.fromSupplier(() -> { DataBufferFactory dataBufferFactory = response.bufferFactory(); byte [] bytes = null ; try { bytes = objectMapper.writeValueAsBytes(result); } catch (JsonProcessingException e) { e.printStackTrace(); } return dataBufferFactory.wrap(bytes); })); } }
ResultCodeEnum.java
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 @Getter public enum ResultCodeEnum { SUCCESS(200 ,"成功" ), FAIL(500 ,"失败" ); public int code; public String desc; ResultCodeEnum(int code, String desc){ this .code = code; this .desc = desc; } public static ResultCodeEnum getByCode (int codeVal) { for (ResultCodeEnum resultCodeEnum : ResultCodeEnum.values()){ if (resultCodeEnum.code == codeVal){ return resultCodeEnum; } } return null ; } }
Result.java
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 @Data public class Result <T> { private Boolean success; private Integer code; private String message; private T data; public static Result ok () { Result result = new Result (); result.setSuccess(true ); result.setCode(ResultCodeEnum.SUCCESS.getCode()); result.setMessage(ResultCodeEnum.SUCCESS.getDesc()); return result; } public static <T> Result ok (T data) { Result result = new Result (); result.setSuccess(true ); result.setCode(ResultCodeEnum.SUCCESS.getCode()); result.setMessage(ResultCodeEnum.SUCCESS.getDesc()); result.setData(data); return result; } public static Result fail () { Result result = new Result (); result.setSuccess(false ); result.setCode(ResultCodeEnum.FAIL.getCode()); result.setMessage(ResultCodeEnum.FAIL.getDesc()); return result; } public static <T> Result fail (T data) { Result result = new Result (); result.setSuccess(false ); result.setCode(ResultCodeEnum.FAIL.getCode()); result.setMessage(ResultCodeEnum.FAIL.getDesc()); result.setData(data); return result; } public static Result fail (Integer code,String message) { Result result = new Result (); result.setSuccess(false ); result.setCode(code); result.setMessage(message); return result; } }
gateway实现redis权限数据拉取(gateway) RedisTemplate->RedisConfig(重写序列化,@Bean创建RedisTemplate bean)->RedisUtil(封装对redis的操作,具体是用redistemplate来操作的)
为什么重写redistemplate?
这里不重新他的一个序列化会造成一个乱码的问题,重写了RedisTemplate:
objectMapper->Jackson2jsonRedisSerializer->redisTemplate,注意@Bean注入
网关读取权限内容有三种形式。
1、网关层直接与数据库交互
2、网关层与 redis 进行交互
3、网关层与 redis 进行交互,redis 没有,则通过 feign 调用 auth 服务获取。
选择第二种形式,完全信任缓存,同时引出数据库与缓存数据一致性的方案。
pom.xml
1 2 3 4 5 <dependency > <groupId > com.google.code.gson</groupId > <artifactId > gson</artifactId > <version > 2.8.6</version > </dependency >
自定义获取权限改为从 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 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 package com.jingdianjichi.club.gateway.auth;import cn.dev33.satoken.stp.StpInterface;import com.alibaba.cloud.commons.lang.StringUtils;import com.google.gson.Gson;import com.google.gson.reflect.TypeToken;import com.jingdianjichi.club.gateway.entity.AuthPermission;import com.jingdianjichi.club.gateway.entity.AuthRole;import com.jingdianjichi.club.gateway.redis.RedisUtil;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Component;import javax.annotation.Resource;import java.util.Collections;import java.util.LinkedList;import java.util.List;import java.util.stream.Collectors;@Component public class StpInterfaceImpl implements StpInterface { @Resource private RedisUtil redisUtil; private String authPermissionPrefix = "auth.permission" ; private String authRolePrefix = "auth.role" ; @Override public List<String> getPermissionList (Object loginId, String loginType) { return getAuth(loginId.toString(), authPermissionPrefix); } @Override public List<String> getRoleList (Object loginId, String loginType) { return getAuth(loginId.toString(), authRolePrefix); } private List<String> getAuth (String loginId, String prefix) { String authKey = redisUtil.buildKey(prefix, loginId.toString()); String authValue = redisUtil.get(authKey); if (StringUtils.isBlank(authValue)) { return Collections.emptyList(); } List<String> authList = new LinkedList <>(); if (authRolePrefix.equals(prefix)) { List<AuthRole> roleList = new Gson ().fromJson(authValue, new TypeToken <List<AuthRole>>() { }.getType()); authList = roleList.stream().map(AuthRole::getRoleKey).collect(Collectors.toList()); } else if (authPermissionPrefix.equals(prefix)) { List<AuthPermission> permissionList = new Gson ().fromJson(authValue, new TypeToken <List<AuthPermission>>() { }.getType()); authList = permissionList.stream().map(AuthPermission::getPermissionKey).collect(Collectors.toList()); } return authList; } }
RedisTemplate 重写优化:原生 redis 的 template 的序列化器会产生乱码问题,重写改为 jackson。
Spring date-redis中RedisTemplate的Jackson序列化设置_redistemplate序列化时date带有类型信息-CSDN博客
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 package com.jingdianjichi.club.gateway.config;import com.fasterxml.jackson.annotation.JsonAutoDetect;import com.fasterxml.jackson.annotation.JsonTypeInfo;import com.fasterxml.jackson.annotation.PropertyAccessor;import com.fasterxml.jackson.databind.DeserializationFeature;import com.fasterxml.jackson.databind.ObjectMapper;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.RedisSerializer;import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration public class RedisConfig { @Bean public RedisTemplate<String,Object> redisTemplate (RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String,Object> redisTemplate = new RedisTemplate <>(); RedisSerializer<String> redisSerializer = new StringRedisSerializer (); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(redisSerializer); redisTemplate.setHashKeySerializer(redisSerializer); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer()); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer()); return redisTemplate; } private Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer () { Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer <>(Object.class); ObjectMapper objectMapper = new ObjectMapper (); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false ); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); jsonRedisSerializer.setObjectMapper(objectMapper); return jsonRedisSerializer; } }
RedisUtil 的封装
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 package com.jingdianjichi.club.gateway.config;import lombok.extern.slf4j.Slf4j;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Component;import javax.annotation.Resource;import java.util.Set;import java.util.concurrent.TimeUnit;import java.util.stream.Collectors;import java.util.stream.Stream;@Component @Slf4j public class RedisUtil { @Resource private RedisTemplate redisTemplate; private static final String CACHE_KEY_SEPARATOR = "." ; public String buildKey (String... strObjs) { return Stream.of(strObjs).collect(Collectors.joining(CACHE_KEY_SEPARATOR)); } public boolean exist (String key) { return redisTemplate.hasKey(key); } public boolean del (String key) { return redisTemplate.delete(key); } public void set (String key, String value) { redisTemplate.opsForValue().set(key, value); } public boolean setNx (String key, String value, Long time, TimeUnit timeUnit) { return redisTemplate.opsForValue().setIfAbsent(key, value, time, timeUnit); } public String get (String key) { return (String) redisTemplate.opsForValue().get(key); } public Boolean zAdd (String key, String value, Long score) { return redisTemplate.opsForZSet().add(key, value, Double.valueOf(String.valueOf(score))); } public Long countZset (String key) { return redisTemplate.opsForZSet().size(key); } public Set<String> rangeZset (String key, long start, long end) { return redisTemplate.opsForZSet().range(key, start, end); } public Long removeZset (String key, Object value) { return redisTemplate.opsForZSet().remove(key, value); } public void removeZsetList (String key, Set<String> value) { value.stream().forEach((val) -> redisTemplate.opsForZSet().remove(key, val)); } public Double score (String key, Object value) { return redisTemplate.opsForZSet().score(key, value); } public Set<String> rangeByScore (String key, long start, long end) { return redisTemplate.opsForZSet().rangeByScore(key, Double.valueOf(String.valueOf(start)), Double.valueOf(String.valueOf(end))); } public Object addScore (String key, Object obj, double score) { return redisTemplate.opsForZSet().incrementScore(key, obj, score); } public Object rank (String key, Object obj) { return redisTemplate.opsForZSet().rank(key, obj); } }
用户模块开发(auth_user) 依然有从DTO->BO->entity的converter,此部分忽略掉描述
controller包 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 148 149 150 151 152 153 154 155 @RestController @RequestMapping("/user/") @Slf4j public class UserController { @Resource private AuthUserDomainService authUserDomainService; @RequestMapping("register") public Result<Boolean> register (@RequestBody AuthUserDTO authUserDTO) { try { if (log.isInfoEnabled()) { log.info("UserController.register.dto:{}" , JSON.toJSONString(authUserDTO)); } checkUserInfo(authUserDTO); AuthUserBO authUserBO = AuthUserDTOConverter.INSTANCE.convertDTOToBO(authUserDTO); return Result.ok(authUserDomainService.register(authUserBO)); } catch (Exception e) { log.error("UserController.register.error:{}" , e.getMessage(), e); return Result.fail("注册用户失败" ); } } @RequestMapping("update") public Result<Boolean> update (@RequestBody AuthUserDTO authUserDTO) { try { if (log.isInfoEnabled()) { log.info("UserController.update.dto:{}" , JSON.toJSONString(authUserDTO)); } checkUserInfo(authUserDTO); AuthUserBO authUserBO = AuthUserDTOConverter.INSTANCE.convertDTOToBO(authUserDTO); return Result.ok(authUserDomainService.update(authUserBO)); } catch (Exception e) { log.error("UserController.update.error:{}" , e.getMessage(), e); return Result.fail("更新用户信息失败" ); } } @RequestMapping("getUserInfo") public Result<AuthUserDTO> getUserInfo (@RequestBody AuthUserDTO authUserDTO) { try { if (log.isInfoEnabled()) { log.info("UserController.getUserInfo.dto:{}" , JSON.toJSONString(authUserDTO)); } Preconditions.checkArgument(!StringUtils.isBlank(authUserDTO.getUserName()), "用户名不能为空" ); AuthUserBO authUserBO = AuthUserDTOConverter.INSTANCE.convertDTOToBO(authUserDTO); AuthUserBO userInfo = authUserDomainService.getUserInfo(authUserBO); return Result.ok(AuthUserDTOConverter.INSTANCE.convertBOToDTO(userInfo)); } catch (Exception e) { log.error("UserController.update.error:{}" , e.getMessage(), e); return Result.fail("更新用户信息失败" ); } } @RequestMapping("listByIds") public Result<List<AuthUserDTO>> listUserInfoByIds (@RequestBody List<String> userNameList) { try { if (log.isInfoEnabled()) { log.info("UserController.listUserInfoByIds.dto:{}" , JSON.toJSONString(userNameList)); } Preconditions.checkArgument(!CollectionUtils.isEmpty(userNameList), "id集合不能为空" ); List<AuthUserBO> userInfos = authUserDomainService.listUserInfoByIds(userNameList); return Result.ok(AuthUserDTOConverter.INSTANCE.convertBOToDTO(userInfos)); } catch (Exception e) { log.error("UserController.listUserInfoByIds.error:{}" , e.getMessage(), e); return Result.fail("批量获取用户信息失败" ); } } @RequestMapping("logOut") public Result logOut (@RequestParam String userName) { try { log.info("UserController.logOut.userName:{}" , userName); Preconditions.checkArgument(!StringUtils.isBlank(userName), "用户名不能为空" ); StpUtil.logout(userName); return Result.ok(); } catch (Exception e) { log.error("UserController.logOut.error:{}" , e.getMessage(), e); return Result.fail("用户登出失败" ); } } @RequestMapping("delete") public Result<Boolean> delete (@RequestBody AuthUserDTO authUserDTO) { try { if (log.isInfoEnabled()) { log.info("UserController.delete.dto:{}" , JSON.toJSONString(authUserDTO)); } AuthUserBO authUserBO = AuthUserDTOConverter.INSTANCE.convertDTOToBO(authUserDTO); return Result.ok(authUserDomainService.update(authUserBO)); } catch (Exception e) { log.error("UserController.update.error:{}" , e.getMessage(), e); return Result.fail("删除用户信息失败" ); } } private void checkUserInfo (@RequestBody AuthUserDTO authUserDTO) { Preconditions.checkArgument(!StringUtils.isBlank(authUserDTO.getUserName()), "用户名不能为空" ); } @RequestMapping("changeStatus") public Result<Boolean> changeStatus (@RequestBody AuthUserDTO authUserDTO) { try { if (log.isInfoEnabled()) { log.info("UserController.changeStatus.dto:{}" , JSON.toJSONString(authUserDTO)); } Preconditions.checkNotNull(authUserDTO.getStatus(), "用户状态不能为空" ); AuthUserBO authUserBO = AuthUserDTOConverter.INSTANCE.convertDTOToBO(authUserDTO); return Result.ok(authUserDomainService.update(authUserBO)); } catch (Exception e) { log.error("UserController.changeStatus.error:{}" , e.getMessage(), e); return Result.fail("启用/禁用用户信息失败" ); } } @RequestMapping("doLogin") public Result<SaTokenInfo> doLogin (@RequestParam("validCode") String validCode) { try { Preconditions.checkArgument(!StringUtils.isBlank(validCode), "验证码不能为空!" ); return Result.ok(authUserDomainService.doLogin(validCode)); } catch (Exception e) { log.error("UserController.doLogin.error:{}" , e.getMessage(), e); return Result.fail("用户登录失败" ); } } @RequestMapping("isLogin") public String isLogin () { return "当前会话是否登录:" + StpUtil.isLogin(); } }
domain包 AuthUserDomainServiceImpl.java(用户注册、更新、删除、登录和信息查询,redis)
资源注入
@Resource
:用于自动注入Spring管理的Bean,如各种服务(Service
)和RedisUtil
。
成员变量
定义了用于构建Redis键的前缀、盐值(salt
)、登录验证码前缀(LOGIN_PREFIX
)。
注册方法
@Override
:覆盖接口中定义的方法。
@SneakyThrows
:使用Lombok注解来隐藏抛出的异常。
@Transactional
:声明事务支持,指定异常回滚。
public Boolean register(AuthUserBO authUserBO)
:注册用户的方法。
注册逻辑
检查用户是否存在。
密码加密存储。(md5+salt)
设置默认头像和昵称。
插入用户数据到数据库。
建立用户与角色的关联。
将角色和权限信息存储到Redis。
更新和删除方法
public Boolean update(AuthUserBO authUserBO)
:更新用户信息的方法。
public Boolean delete(AuthUserBO authUserBO)
:逻辑删除用户的方法,同时更新Redis中的缓存。
登录和获取用户信息方法
public SaTokenInfo doLogin(String validCode)
:处理用户登录的方法,使用Sa-Token
进行认证。
public AuthUserBO getUserInfo(AuthUserBO authUserBO)
:根据用户名获取用户信息的方法。
批量获取用户信息方法
public List<AuthUserBO> listUserInfoByIds(List<String> userNameList)
:根据用户ID列表批量获取用户信息的方法。
事务和异常处理
注册、更新和删除方法使用@Transactional
注解,确保操作的原子性。
使用@SneakyThrows
来处理可能抛出的异常,避免显式声明异常。
日志记录
缓存操作
使用RedisUtil
进行Redis的读写操作,如存储用户的角色和权限信息。
密码安全
使用SaSecureUtil.md5BySalt
方法对用户密码进行MD5加盐加密。
默认资源
服务交互
通过调用AuthUserService
、AuthUserRoleService
等的方法,实现业务逻辑。
这个AuthUserDomainServiceImpl
类通过实现AuthUserDomainService
接口,提供了用户注册、更新、删除、登录和信息查询等服务,同时与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 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 @Service @Slf4j public class AuthUserDomainServiceImpl implements AuthUserDomainService { @Resource private AuthUserService authUserService; @Resource private AuthUserRoleService authUserRoleService; @Resource private AuthPermissionService authPermissionService; @Resource private AuthRolePermissionService authRolePermissionService; @Resource private AuthRoleService authRoleService; private String salt = "chicken" ; @Resource private RedisUtil redisUtil; private String authPermissionPrefix = "auth.permission" ; private String authRolePrefix = "auth.role" ; private static final String LOGIN_PREFIX = "loginCode" ; @Override @SneakyThrows @Transactional(rollbackFor = Exception.class) public Boolean register (AuthUserBO authUserBO) { AuthUser existAuthUser = new AuthUser (); existAuthUser.setUserName(authUserBO.getUserName()); List<AuthUser> existUser = authUserService.queryByCondition(existAuthUser); if (existUser.size() > 0 ) { return true ; } AuthUser authUser = AuthUserBOConverter.INSTANCE.convertBOToEntity(authUserBO); if (StringUtils.isNotBlank(authUser.getPassword())) { authUser.setPassword(SaSecureUtil.md5BySalt(authUser.getPassword(), salt)); } if (StringUtils.isBlank(authUser.getAvatar())) { authUser.setAvatar("http://117.72.10.84:9000/user/icon/微信图片_20231203153718(1).png" ); } if (StringUtils.isBlank(authUser.getNickName())) { authUser.setNickName("lzrj" ); } authUser.setStatus(AuthUserStatusEnum.OPEN.getCode()); authUser.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); Integer count = authUserService.insert(authUser); AuthRole authRole = new AuthRole (); authRole.setRoleKey(AuthConstant.NORMAL_USER); AuthRole roleResult = authRoleService.queryByCondition(authRole); Long roleId = roleResult.getId(); Long userId = authUser.getId(); AuthUserRole authUserRole = new AuthUserRole (); authUserRole.setUserId(userId); authUserRole.setRoleId(roleId); authUserRole.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); authUserRoleService.insert(authUserRole); String roleKey = redisUtil.buildKey(authRolePrefix, authUser.getUserName()); List<AuthRole> roleList = new LinkedList <>(); roleList.add(authRole); redisUtil.set(roleKey, new Gson ().toJson(roleList)); AuthRolePermission authRolePermission = new AuthRolePermission (); authRolePermission.setRoleId(roleId); List<AuthRolePermission> rolePermissionList = authRolePermissionService. queryByCondition(authRolePermission); List<Long> permissionIdList = rolePermissionList.stream() .map(AuthRolePermission::getPermissionId).collect(Collectors.toList()); List<AuthPermission> permissionList = authPermissionService.queryByRoleList(permissionIdList); String permissionKey = redisUtil.buildKey(authPermissionPrefix, authUser.getUserName()); redisUtil.set(permissionKey, new Gson ().toJson(permissionList)); return count > 0 ; } @Override public Boolean update (AuthUserBO authUserBO) { AuthUser authUser = AuthUserBOConverter.INSTANCE.convertBOToEntity(authUserBO); Integer count = authUserService.updateByUserName(authUser); return count > 0 ; } @Override public Boolean delete (AuthUserBO authUserBO) { AuthUser authUser = new AuthUser (); authUser.setId(authUserBO.getId()); authUser.setIsDeleted(IsDeletedFlagEnum.DELETED.getCode()); Integer count = authUserService.update(authUser); return count > 0 ; } @Override public SaTokenInfo doLogin (String validCode) { String loginKey = redisUtil.buildKey(LOGIN_PREFIX, validCode); String openId = redisUtil.get(loginKey); if (StringUtils.isBlank(openId)) { return null ; } AuthUserBO authUserBO = new AuthUserBO (); authUserBO.setUserName(openId); this .register(authUserBO); StpUtil.login(openId); SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); return tokenInfo; } @Override public AuthUserBO getUserInfo (AuthUserBO authUserBO) { AuthUser authUser = new AuthUser (); authUser.setUserName(authUserBO.getUserName()); List<AuthUser> userList = authUserService.queryByCondition(authUser); if (CollectionUtils.isEmpty(userList)) { return new AuthUserBO (); } AuthUser user = userList.get(0 ); return AuthUserBOConverter.INSTANCE.convertEntityToBO(user); } @Override public List<AuthUserBO> listUserInfoByIds (List<String> userNameList) { List<AuthUser> userList = authUserService.listUserInfoByIds(userNameList); if (CollectionUtils.isEmpty(userList)) { return Collections.emptyList(); } return AuthUserBOConverter.INSTANCE.convertEntityToBO(userList); } }
infra层
对这五张表的增删查改
常见加密&密码加密 前言
数据库如果说存储明文的密码是非常的危险的,一旦被攻击啊,或者数据泄漏,用户的信息疯狂的暴露出去,黑客什么都能干,这是非常不行,所以我们要做加密,让黑客即使拿到了密码信息, 也不知道原始的密码,就登录不成功。
加密的方式
摘要加密
md5,sha1,sha256
摘要主要就是哈希值,通过我们的散列的算法。摘要的概念主要是验证完整性和唯一性,不管我们的密码是多长啊,或者多复杂的啊,得到的值都是固定长度。
摘要加密有一定的风险。123456 用 md5 加密。他其实是固定的,大家也可以到一些网站有反解密。
对称加密
我们约定了一个密钥。这个密钥一定要好好保存,不能泄漏,一旦泄漏就可以进行想你想的解密了。
加密的过程:密码+密钥 生成
解密的过程:密文+密钥 反解
密钥一定一定要做好其中的保存。
常见的对称加密的算法:AES,DES,3DESC,SM4
非对称加密
一个公钥,一个私钥。
公钥去加密,私钥去解密。
私钥去加密,公钥去解密。
常见的算法:RSA,ECC,国密的 SM2
算法的时性能上,差一点,加密的数量没有对称加密快。
加盐?是做饭吗?
摘要算法比如 md5,光加密 123456,结果都是一样的,如果是破解的库里正好有这个 md5 就很容易知道逆向是 123456。来一手加盐。盐是随机的字符串,他来与原密码进行一波二次加密。这样获取到的很难破解出来。如果不加盐,简单密码很容易撞库的。
AuthUserDomainServiceImpl.java
,satoken:md5+salt,这里的salt是一个字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Override @SneakyThrows @Transactional(rollbackFor = Exception.class) public Boolean register (AuthUserBO authUserBO) { AuthUser existAuthUser = new AuthUser (); existAuthUser.setUserName(authUserBO.getUserName()); List<AuthUser> existUser = authUserService.queryByCondition(existAuthUser); if (existUser.size() > 0 ) { return true ; } AuthUser authUser = AuthUserBOConverter.INSTANCE.convertBOToEntity(authUserBO); if (StringUtils.isNotBlank(authUser.getPassword())) { authUser.setPassword(SaSecureUtil.md5BySalt(authUser.getPassword(), salt)); } if (StringUtils.isBlank(authUser.getAvatar())) { authUser.setAvatar("http://117.72.10.84:9000/user/icon/微信图片_20231203153718(1).png" ); } ...... }
角色模块开发(auth_role) controller RolePermissionController.java
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 package com.jingdianjichi.auth.application.controller;import com.alibaba.fastjson.JSON;import com.google.common.base.Preconditions;import com.jingdianjichi.auth.application.convert.AuthRolePermissionDTOConverter;import com.jingdianjichi.auth.application.dto.AuthRolePermissionDTO;import com.jingdianjichi.auth.domain.entity.AuthRolePermissionBO;import com.jingdianjichi.auth.domain.service.AuthRolePermissionDomainService;import com.jingdianjichi.auth.entity.Result;import lombok.extern.slf4j.Slf4j;import org.springframework.util.CollectionUtils;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;@RestController @RequestMapping("/rolePermission/") @Slf4j public class RolePermissionController { @Resource private AuthRolePermissionDomainService authRolePermissionDomainService; @RequestMapping("add") public Result<Boolean> add (@RequestBody AuthRolePermissionDTO authRolePermissionDTO) { try { if (log.isInfoEnabled()) { log.info("RolePermissionController.add.dto:{}" , JSON.toJSONString(authRolePermissionDTO)); } Preconditions.checkArgument(!CollectionUtils.isEmpty(authRolePermissionDTO.getPermissionIdList()),"权限关联不能为空" ); Preconditions.checkNotNull(authRolePermissionDTO.getRoleId(),"角色不能为空!" ); AuthRolePermissionBO rolePermissionBO = AuthRolePermissionDTOConverter.INSTANCE.convertDTOToBO(authRolePermissionDTO); return Result.ok(authRolePermissionDomainService.add(rolePermissionBO)); } catch (Exception e) { log.error("PermissionController.add.error:{}" , e.getMessage(), e); return Result.fail("新增角色权限失败" ); } } }
RoleController.java
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 package com.jingdianjichi.auth.application.controller;import com.alibaba.fastjson.JSON;import com.google.common.base.Preconditions;import com.jingdianjichi.auth.application.convert.AuthRoleDTOConverter;import com.jingdianjichi.auth.application.dto.AuthRoleDTO;import com.jingdianjichi.auth.domain.entity.AuthRoleBO;import com.jingdianjichi.auth.domain.service.AuthRoleDomainService;import com.jingdianjichi.auth.entity.Result;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang3.StringUtils;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;@RestController @RequestMapping("/role/") @Slf4j public class RoleController { @Resource private AuthRoleDomainService authRoleDomainService; @RequestMapping("add") public Result<Boolean> add (@RequestBody AuthRoleDTO authRoleDTO) { try { if (log.isInfoEnabled()) { log.info("RoleController.add.dto:{}" , JSON.toJSONString(authRoleDTO)); } Preconditions.checkArgument(!StringUtils.isBlank(authRoleDTO.getRoleKey()), "角色key不能为空" ); Preconditions.checkArgument(!StringUtils.isBlank(authRoleDTO.getRoleName()), "角色名称不能为空" ); AuthRoleBO authRoleBO = AuthRoleDTOConverter.INSTANCE.convertDTOToBO(authRoleDTO); return Result.ok(authRoleDomainService.add(authRoleBO)); } catch (Exception e) { log.error("UserController.register.error:{}" , e.getMessage(), e); return Result.fail("新增角色失败" ); } } @RequestMapping("update") public Result<Boolean> update (@RequestBody AuthRoleDTO authRoleDTO) { try { if (log.isInfoEnabled()) { log.info("RoleController.update.dto:{}" , JSON.toJSONString(authRoleDTO)); } Preconditions.checkNotNull(authRoleDTO.getId(), "角色id不能为空" ); AuthRoleBO authRoleBO = AuthRoleDTOConverter.INSTANCE.convertDTOToBO(authRoleDTO); return Result.ok(authRoleDomainService.update(authRoleBO)); } catch (Exception e) { log.error("RoleController.update.error:{}" , e.getMessage(), e); return Result.fail("更新角色信息失败" ); } } @RequestMapping("delete") public Result<Boolean> delete (@RequestBody AuthRoleDTO authRoleDTO) { try { if (log.isInfoEnabled()) { log.info("RoleController.delete.dto:{}" , JSON.toJSONString(authRoleDTO)); } AuthRoleBO authRoleBO = AuthRoleDTOConverter.INSTANCE.convertDTOToBO(authRoleDTO); return Result.ok(authRoleDomainService.delete(authRoleBO)); } catch (Exception e) { log.error("RoleController.delete.error:{}" , e.getMessage(), e); return Result.fail("删除角色信息失败" ); } } }
PermissionController.java
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 package com.jingdianjichi.auth.application.controller;import com.alibaba.fastjson.JSON;import com.google.common.base.Preconditions;import com.jingdianjichi.auth.application.convert.AuthPermissionDTOConverter;import com.jingdianjichi.auth.application.dto.AuthPermissionDTO;import com.jingdianjichi.auth.domain.entity.AuthPermissionBO;import com.jingdianjichi.auth.domain.service.AuthPermissionDomainService;import com.jingdianjichi.auth.entity.Result;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang3.StringUtils;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;@RestController @RequestMapping("/permission/") @Slf4j public class PermissionController { @Resource private AuthPermissionDomainService authPermissionDomainService; @RequestMapping("add") public Result<Boolean> add (@RequestBody AuthPermissionDTO authPermissionDTO) { try { if (log.isInfoEnabled()) { log.info("PermissionController.add.dto:{}" , JSON.toJSONString(authPermissionDTO)); } Preconditions.checkArgument(!StringUtils.isBlank(authPermissionDTO.getName()), "权限名称不能为空" ); Preconditions.checkNotNull(authPermissionDTO.getParentId(), "权限父id不能为空" ); AuthPermissionBO permissionBO = AuthPermissionDTOConverter.INSTANCE.convertDTOToBO(authPermissionDTO); return Result.ok(authPermissionDomainService.add(permissionBO)); } catch (Exception e) { log.error("PermissionController.add.error:{}" , e.getMessage(), e); return Result.fail("新增权限失败" ); } } @RequestMapping("update") public Result<Boolean> update (@RequestBody AuthPermissionDTO authPermissionDTO) { try { if (log.isInfoEnabled()) { log.info("PermissionController.update.dto:{}" , JSON.toJSONString(authPermissionDTO)); } Preconditions.checkNotNull(authPermissionDTO.getId(), "权限id不能为空" ); AuthPermissionBO permissionBO = AuthPermissionDTOConverter.INSTANCE.convertDTOToBO(authPermissionDTO); return Result.ok(authPermissionDomainService.update(permissionBO)); } catch (Exception e) { log.error("PermissionController.update.error:{}" , e.getMessage(), e); return Result.fail("更新权限信息失败" ); } } @RequestMapping("delete") public Result<Boolean> delete (@RequestBody AuthPermissionDTO authPermissionDTO) { try { if (log.isInfoEnabled()) { log.info("PermissionController.delete.dto:{}" , JSON.toJSONString(authPermissionDTO)); } Preconditions.checkNotNull(authPermissionDTO.getId(), "权限id不能为空" ); AuthPermissionBO permissionBO = AuthPermissionDTOConverter.INSTANCE.convertDTOToBO(authPermissionDTO); return Result.ok(authPermissionDomainService.delete(permissionBO)); } catch (Exception e) { log.error("PermissionController.delete.error:{}" , e.getMessage(), e); return Result.fail("删除权限信息失败" ); } } @RequestMapping("getPermission") public Result<Boolean> getPermission (String userName) { try { log.info("PermissionController.getPermission.userName:{}" ,userName); Preconditions.checkArgument(!StringUtils.isBlank(userName), "用户id不能为空" ); return Result.ok(authPermissionDomainService.getPermission(userName)); } catch (Exception e) { log.error("PermissionController.getPermission.error:{}" , e.getMessage(), e); return Result.fail("查询用户权限信息失败" ); } } }
domain AuthPermissionDomainServiceImpl.java
(权限)
业务方法实现
@Override
:覆盖接口中定义的方法。
public Boolean add(AuthPermissionBO authPermissionBO)
:添加权限的方法,将业务对象(BO)转换为实体对象(Entity),设置未删除标志,并插入数据库。
public Boolean update(AuthPermissionBO authPermissionBO)
:更新权限的方法,将BO转换为Entity,并更新数据库。
public Boolean delete(AuthPermissionBO authPermissionBO)
:逻辑删除权限的方法,更新删除标志。
权限获取方法
public List<String> getPermission(String userName)
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 :根据用户名获取权限列表的方法。 - 使用`RedisUtil`构建权限的Redis键。 - 从Redis中获取权限的JSON字符串。 - 如果Redis中没有数据,则返回空列表。 - 使用Gson反序列化JSON字符串为`AuthPermission`列表。 - 从权限列表中提取权限键(Permission Key)。 3. 日志记录 - `log`变量用于记录日志信息。 4. 缓存操作 - 使用`RedisUtil`进行Redis的读写操作,如获取用户权限信息。 5. 数据转换 - 使用`AuthPermissionBOConverter`将业务对象(BO)转换为数据库实体(Entity)。 6. 逻辑删除 - 设置`IsDeletedFlagEnum.UN_DELETED.getCode()`和`IsDeletedFlagEnum.DELETED.getCode()`来标记记录的删除状态。 这个`AuthPermissionDomainServiceImpl`类通过实现`AuthPermissionDomainService`接口,提供了权限的增删改以及根据用户名获取权限列表的服务。它利用了Redis缓存来提高获取权限列表的性能,并采用了逻辑删除的方式来管理权限数据。通过这种方式,应用程序可以灵活地进行权限控制和验证。 ```java @Service @Slf4j public class AuthPermissionDomainServiceImpl implements AuthPermissionDomainService { @Resource private AuthPermissionService authPermissionService; @Resource private RedisUtil redisUtil; private String authPermissionPrefix = "auth.permission"; @Override public Boolean add(AuthPermissionBO authPermissionBO) { AuthPermission authPermission = AuthPermissionBOConverter.INSTANCE.convertBOToEntity(authPermissionBO); authPermission.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); Integer count = authPermissionService.insert(authPermission); return count > 0; } @Override public Boolean update(AuthPermissionBO authPermissionBO) { AuthPermission authPermission = AuthPermissionBOConverter.INSTANCE.convertBOToEntity(authPermissionBO); Integer count = authPermissionService.update(authPermission); return count > 0; } @Override public Boolean delete(AuthPermissionBO authPermissionBO) { AuthPermission authPermission = new AuthPermission(); authPermission.setId(authPermissionBO.getId()); authPermission.setIsDeleted(IsDeletedFlagEnum.DELETED.getCode()); Integer count = authPermissionService.update(authPermission); return count > 0; } @Override public List<String> getPermission(String userName) { String permissionKey = redisUtil.buildKey(authPermissionPrefix, userName); String permissionValue = redisUtil.get(permissionKey); if (StringUtils.isBlank(permissionValue)) { return Collections.emptyList(); } List<AuthPermission> permissionList = new Gson().fromJson(permissionValue, new TypeToken<List<AuthPermission>>() { }.getType()); List<String> authList = permissionList.stream().map(AuthPermission::getPermissionKey).collect(Collectors.toList()); return authList; } }
AuthRoleDomainServiceImpl
(角色)
业务方法实现@Override:覆盖接口中定义的方法。
添加角色方法public Boolean add(AuthRoleBO authRoleBO):
添加角色的方法。
使用 AuthRoleBOConverter 将业务对象(BO)转换为实体对象(Entity)。
设置角色未删除标志。
调用 authRoleService 的 insert 方法将实体插入数据库。
返回操作影响的行数是否大于0。
更新角色方法public Boolean update(AuthRoleBO authRoleBO):
更新角色的方法。
类似于添加方法,但调用 update 方法更新数据库中的实体。
删除角色方法public Boolean delete(AuthRoleBO authRoleBO):
逻辑删除角色的方法。
创建一个新的 AuthRole 实体,设置ID和逻辑删除标志。
调用 authRoleService 的 update 方法更新数据库中的实体。8.
日志记录log
逻辑删除设置
IsDeletedFlagEnum.UN_DELETED.getCode() 和 IsDeletedFlagEnum.DELETED.getCode() 来标记记录的删除状态。
这个 AuthRoleDomainServiceImpl 类通过实现 AuthRoleDomainService 接口,提供了角色的增删改服务。它利用了逻辑删除的方式来管理角色数据,通过这种方式,应用程序可以灵活地进行角色管理和权限分配。注意,代码中没有显示具体的日志输出语句,但 @Slf4j 注解会在类中添加日志变量,可以在方法中使用 log 进行日志记录。此外,@SneakyThrows 注解未在此代码片段中使用,如果存在,它通常用于隐藏方法抛出的异常。
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 @Service @Slf4j public class AuthRoleDomainServiceImpl implements AuthRoleDomainService { @Resource private AuthRoleService authRoleService; @Override public Boolean add (AuthRoleBO authRoleBO) { AuthRole authRole = AuthRoleBOConverter.INSTANCE.convertBOToEntity(authRoleBO); authRole.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); Integer count = authRoleService.insert(authRole); return count > 0 ; } @Override public Boolean update (AuthRoleBO authRoleBO) { AuthRole authRole = AuthRoleBOConverter.INSTANCE.convertBOToEntity(authRoleBO); Integer count = authRoleService.update(authRole); return count > 0 ; } @Override public Boolean delete (AuthRoleBO authRoleBO) { AuthRole authRole = new AuthRole (); authRole.setId(authRoleBO.getId()); authRole.setIsDeleted(IsDeletedFlagEnum.DELETED.getCode()); Integer count = authRoleService.update(authRole); return count > 0 ; } }
AuthRolePermissionDomainServiceImpl.java
(角色权限关联)
添加角色权限关联方法
infra层
用户角色关联(user_role,这里主要是infra层的东西) controller(UserController的注册模块) 其实就是UserController,在进行register等相关操作时会进行与默认角色的关联
domain AuthUserRoleDomainServiceImpl.java
用户存在性检查 :首先检查要注册的用户是否已存在。如果存在,则返回true
。
用户BO转换 :将AuthUserBO
(业务对象)转换为AuthUser
实体。
密码加密 :如果用户密码不为空,则使用MD5加盐的方式加密密码。
默认头像和昵称 :如果用户没有提供头像或昵称,则设置默认值。
用户状态设置 :设置用户状态为开启(AuthUserStatusEnum.OPEN
)和未删除(IsDeletedFlagEnum.UN_DELETED
)。
用户插入数据库 :将用户实体插入数据库,并检查插入操作是否成功。
角色关联 :为新用户分配一个默认角色(普通用户),并将角色与用户关联。
Redis缓存角色信息 :使用Redis缓存用户的角色信息,以便快速检索。
权限查询与缓存 :查询角色拥有的权限,并将权限信息缓存到Redis。
事务管理 :使用@Transactional
注解确保方法在出现异常时可以回滚。(也可以用TransactionnalTemplate)
异常处理 :使用@SneakyThrows
注解来重新抛出检查型异常。
返回结果 :如果用户插入成功,则返回true
。
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 @Override @SneakyThrows @Transactional(rollbackFor = Exception.class) public Boolean register (AuthUserBO authUserBO) { AuthUser existAuthUser = new AuthUser (); existAuthUser.setUserName(authUserBO.getUserName()); List<AuthUser> existUser = authUserService.queryByCondition(existAuthUser); if (existUser.size() > 0 ) { return true ; } AuthUser authUser = AuthUserBOConverter.INSTANCE.convertBOToEntity(authUserBO); if (StringUtils.isNotBlank(authUser.getPassword())) { authUser.setPassword(SaSecureUtil.md5BySalt(authUser.getPassword(), salt)); } if (StringUtils.isBlank(authUser.getAvatar())) { authUser.setAvatar("http://117.72.10.84:9000/user/icon/微信图片_20231203153718(1).png" ); } if (StringUtils.isBlank(authUser.getNickName())) { authUser.setNickName("lzrj" ); } authUser.setStatus(AuthUserStatusEnum.OPEN.getCode()); authUser.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); Integer count = authUserService.insert(authUser); AuthRole authRole = new AuthRole (); authRole.setRoleKey(AuthConstant.NORMAL_USER); AuthRole roleResult = authRoleService.queryByCondition(authRole); Long roleId = roleResult.getId(); Long userId = authUser.getId(); AuthUserRole authUserRole = new AuthUserRole (); authUserRole.setUserId(userId); authUserRole.setRoleId(roleId); authUserRole.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); authUserRoleService.insert(authUserRole); String roleKey = redisUtil.buildKey(authRolePrefix, authUser.getUserName()); List<AuthRole> roleList = new LinkedList <>(); roleList.add(authRole); redisUtil.set(roleKey, new Gson ().toJson(roleList)); AuthRolePermission authRolePermission = new AuthRolePermission (); authRolePermission.setRoleId(roleId); List<AuthRolePermission> rolePermissionList = authRolePermissionService. queryByCondition(authRolePermission); List<Long> permissionIdList = rolePermissionList.stream() .map(AuthRolePermission::getPermissionId).collect(Collectors.toList()); List<AuthPermission> permissionList = authPermissionService.queryByRoleList(permissionIdList); String permissionKey = redisUtil.buildKey(authPermissionPrefix, authUser.getUserName()); redisUtil.set(permissionKey, new Gson ().toJson(permissionList)); return count > 0 ; }
为什么这里遇到@Transactional注解:Spring——事务注解@Transactional【建议收藏】-CSDN博客
@Transactional
注解在 Java 应用程序中,尤其是在使用 Spring 框架时,是一个非常重要的特性。这个注解通常用于声明方法在执行时应该被视为一个事务的边界。以下是使用 @Transactional
注解的一些主要原因:
确保数据一致性 :在涉及数据库操作的方法中,@Transactional
确保方法执行过程中的所有数据库操作要么全部成功,要么在遇到异常时全部撤销,以保持数据的一致性。
简化代码 :使用 @Transactional
注解可以避免在每个数据库操作后手动管理事务的开始和提交,简化了代码。
声明式事务管理 :Spring 支持声明式事务管理,@Transactional
注解就是这一概念的实现之一,它允许将事务管理逻辑从业务逻辑代码中分离出来。
回滚策略 :通过 @Transactional
注解,可以定义哪些异常会导致事务回滚。在您提供的代码中,rollbackFor = Exception.class
表示如果抛出任何类型的异常,事务都会回滚。
支持嵌套事务 :当一个事务方法调用另一个带有 @Transactional
注解的方法时,Spring 会处理这些方法之间的事务嵌套。
提高性能 :Spring 事务管理器可以针对不同的事务策略进行优化,比如懒加载事务、使用适当的隔离级别等,以提高应用程序性能。
可伸缩性 :随着应用程序的扩展,@Transactional
注解可以很容易地应用于新的方法或类,而不需要对现有代码进行大量修改。
在您的代码示例中,@Transactional
注解应用于注册用户的方法上,这意味着从检查用户是否存在到用户信息写入数据库、角色和权限信息缓存到 Redis 的整个过程被视为一个单一的事务。如果在这个过程的任何地方发生异常,整个操作将回滚,以确保用户信息和相关的角色、权限设置要么完全应用,要么完全不应用,避免数据不一致的问题。
auth-domain
1 2 3 4 5 6 7 8 9 10 package com.jingdianjichi.auth.domain.constants;public class AuthConstant { public static final String NORMAL_USER = "normal_user" ; }
infra层
接下来回到gateway模块,在用户登录时做好与redis的交互就可以打通了。
权限模块开发(auth_permission) controller层 PermissionController.java
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 @RestController @RequestMapping("/permission/") @Slf4j public class PermissionController { @Resource private AuthPermissionDomainService authPermissionDomainService; @RequestMapping("add") public Result<Boolean> add (@RequestBody AuthPermissionDTO authPermissionDTO) { try { if (log.isInfoEnabled()) { log.info("PermissionController.add.dto:{}" , JSON.toJSONString(authPermissionDTO)); } Preconditions.checkArgument(!StringUtils.isBlank(authPermissionDTO.getName()), "权限名称不能为空" ); Preconditions.checkNotNull(authPermissionDTO.getParentId(), "权限父id不能为空" ); AuthPermissionBO permissionBO = AuthPermissionDTOConverter.INSTANCE.convertDTOToBO(authPermissionDTO); return Result.ok(authPermissionDomainService.add(permissionBO)); } catch (Exception e) { log.error("PermissionController.add.error:{}" , e.getMessage(), e); return Result.fail("新增权限失败" ); } } @RequestMapping("update") public Result<Boolean> update (@RequestBody AuthPermissionDTO authPermissionDTO) { try { if (log.isInfoEnabled()) { log.info("PermissionController.update.dto:{}" , JSON.toJSONString(authPermissionDTO)); } Preconditions.checkNotNull(authPermissionDTO.getId(), "权限id不能为空" ); AuthPermissionBO permissionBO = AuthPermissionDTOConverter.INSTANCE.convertDTOToBO(authPermissionDTO); return Result.ok(authPermissionDomainService.update(permissionBO)); } catch (Exception e) { log.error("PermissionController.update.error:{}" , e.getMessage(), e); return Result.fail("更新权限信息失败" ); } } @RequestMapping("delete") public Result<Boolean> delete (@RequestBody AuthPermissionDTO authPermissionDTO) { try { if (log.isInfoEnabled()) { log.info("PermissionController.delete.dto:{}" , JSON.toJSONString(authPermissionDTO)); } Preconditions.checkNotNull(authPermissionDTO.getId(), "权限id不能为空" ); AuthPermissionBO permissionBO = AuthPermissionDTOConverter.INSTANCE.convertDTOToBO(authPermissionDTO); return Result.ok(authPermissionDomainService.delete(permissionBO)); } catch (Exception e) { log.error("PermissionController.delete.error:{}" , e.getMessage(), e); return Result.fail("删除权限信息失败" ); } } @RequestMapping("getPermission") public Result<Boolean> getPermission (String userName) { try { log.info("PermissionController.getPermission.userName:{}" ,userName); Preconditions.checkArgument(!StringUtils.isBlank(userName), "用户id不能为空" ); return Result.ok(authPermissionDomainService.getPermission(userName)); } catch (Exception e) { log.error("PermissionController.getPermission.error:{}" , e.getMessage(), e); return Result.fail("查询用户权限信息失败" ); } } }
AuthPermissionDTO.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Data public class AuthPermissionDTO implements Serializable { private Long id; private String name; private Long parentId; private Integer type; private String menuUrl; private Integer status; private Integer show; private String icon; private String permissionKey; }
AuthPermissionDTOConverter.java
1 2 3 4 5 6 7 8 @Mapper public interface AuthPermissionDTOConverter { AuthPermissionDTOConverter INSTANCE = Mappers.getMapper(AuthPermissionDTOConverter.class); AuthPermissionBO convertDTOToBO (AuthPermissionDTO authPermissionDTO) ; }
domain层 AuthPermissionBOConverter.java
1 2 3 4 5 6 7 8 @Mapper public interface AuthPermissionBOConverter { AuthPermissionBOConverter INSTANCE = Mappers.getMapper(AuthPermissionBOConverter.class); AuthPermission convertBOToEntity (AuthPermissionBO authPermissionBO) ; }
AuthPermissionBO.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Data public class AuthPermissionBO implements Serializable { private Long id; private String name; private Long parentId; private Integer type; private String menuUrl; private Integer status; private Integer show; private String icon; private String permissionKey; }
AuthPermissionService.java
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 @Service @Slf4j public class AuthPermissionDomainServiceImpl implements AuthPermissionDomainService { @Resource private AuthPermissionService authPermissionService; @Resource private RedisUtil redisUtil; private String authPermissionPrefix = "auth.permission" ; @Override public Boolean add (AuthPermissionBO authPermissionBO) { AuthPermission authPermission = AuthPermissionBOConverter.INSTANCE.convertBOToEntity(authPermissionBO); authPermission.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); Integer count = authPermissionService.insert(authPermission); return count > 0 ; } @Override public Boolean update (AuthPermissionBO authPermissionBO) { AuthPermission authPermission = AuthPermissionBOConverter.INSTANCE.convertBOToEntity(authPermissionBO); Integer count = authPermissionService.update(authPermission); return count > 0 ; } @Override public Boolean delete (AuthPermissionBO authPermissionBO) { AuthPermission authPermission = new AuthPermission (); authPermission.setId(authPermissionBO.getId()); authPermission.setIsDeleted(IsDeletedFlagEnum.DELETED.getCode()); Integer count = authPermissionService.update(authPermission); return count > 0 ; } @Override public List<String> getPermission (String userName) { String permissionKey = redisUtil.buildKey(authPermissionPrefix, userName); String permissionValue = redisUtil.get(permissionKey); if (StringUtils.isBlank(permissionValue)) { return Collections.emptyList(); } List<AuthPermission> permissionList = new Gson ().fromJson(permissionValue, new TypeToken <List<AuthPermission>>() { }.getType()); List<String> authList = permissionList.stream().map(AuthPermission::getPermissionKey).collect(Collectors.toList()); return authList; } }
infra层 AuthPermission.java
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 148 149 150 151 152 153 public class AuthPermission implements Serializable { private static final long serialVersionUID = -56518358607843924L ; private Long id; private String name; private Long parentId; private Integer type; private String menuUrl; private Integer status; private Integer show; private String icon; private String permissionKey; private String createdBy; private Date createdTime; private String updateBy; private Date updateTime; private Integer isDeleted; public Long getId () { return id; } public void setId (Long id) { this .id = id; } public String getName () { return name; } public void setName (String name) { this .name = name; } public Long getParentId () { return parentId; } public void setParentId (Long parentId) { this .parentId = parentId; } public Integer getType () { return type; } public void setType (Integer type) { this .type = type; } public String getMenuUrl () { return menuUrl; } public void setMenuUrl (String menuUrl) { this .menuUrl = menuUrl; } public Integer getStatus () { return status; } public void setStatus (Integer status) { this .status = status; } public Integer getShow () { return show; } public void setShow (Integer show) { this .show = show; } public String getIcon () { return icon; } public void setIcon (String icon) { this .icon = icon; } public String getPermissionKey () { return permissionKey; } public void setPermissionKey (String permissionKey) { this .permissionKey = permissionKey; } public String getCreatedBy () { return createdBy; } public void setCreatedBy (String createdBy) { this .createdBy = createdBy; } public Date getCreatedTime () { return createdTime; } public void setCreatedTime (Date createdTime) { this .createdTime = createdTime; } public String getUpdateBy () { return updateBy; } public void setUpdateBy (String updateBy) { this .updateBy = updateBy; } public Date getUpdateTime () { return updateTime; } public void setUpdateTime (Date updateTime) { this .updateTime = updateTime; } public Integer getIsDeleted () { return isDeleted; } public void setIsDeleted (Integer isDeleted) { this .isDeleted = isDeleted; } }
AuthPermissionDao.xml
type
, show
:这些列名使用了反引号,因为 type
和 show
是 MySQL 的保留字。在 SQL 中,保留字是具有特定意义的关键字,如果用作列名或表名,需要用反引号括起来,以避免语法错误。
1 2 3 4 5 <insert id ="insert" keyProperty ="id" useGeneratedKeys ="true" > insert into auth_permission(name, parent_id, `type`, menu_url, status, `show`, icon, permission_key, created_by, created_time, update_by, update_time, is_deleted) values (#{name}, #{parentId}, #{type}, #{menuUrl}, #{status}, #{show}, #{icon}, #{permissionKey}, #{createdBy}, #{createdTime}, #{updateBy}, #{updateTime}, #{isDeleted}) </insert >
AuthPermissionServiceImpl.java
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 @Service("authPermissionService") public class AuthPermissionServiceImpl implements AuthPermissionService { @Resource private AuthPermissionDao authPermissionDao; @Override public AuthPermission queryById (Long id) { return this .authPermissionDao.queryById(id); } @Override public int insert (AuthPermission authPermission) { return this .authPermissionDao.insert(authPermission); } @Override public int update (AuthPermission authPermission) { return this .authPermissionDao.update(authPermission); } @Override public boolean deleteById (Long id) { return this .authPermissionDao.deleteById(id) > 0 ; } @Override public List<AuthPermission> queryByRoleList (List<Long> roleIdList) { return this .authPermissionDao.queryByRoleList(roleIdList); } }
角色权限关联开发(auth_role_permission) controller RolePermissionController
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 @RestController @RequestMapping("/rolePermission/") @Slf4j public class RolePermissionController { @Resource private AuthRolePermissionDomainService authRolePermissionDomainService; @RequestMapping("add") public Result<Boolean> add (@RequestBody AuthRolePermissionDTO authRolePermissionDTO) { try { if (log.isInfoEnabled()) { log.info("RolePermissionController.add.dto:{}" , JSON.toJSONString(authRolePermissionDTO)); } Preconditions.checkArgument(!CollectionUtils.isEmpty(authRolePermissionDTO.getPermissionIdList()),"权限关联不能为空" ); Preconditions.checkNotNull(authRolePermissionDTO.getRoleId(),"角色不能为空!" ); AuthRolePermissionBO rolePermissionBO = AuthRolePermissionDTOConverter.INSTANCE.convertDTOToBO(authRolePermissionDTO); return Result.ok(authRolePermissionDomainService.add(rolePermissionBO)); } catch (Exception e) { log.error("PermissionController.add.error:{}" , e.getMessage(), e); return Result.fail("新增角色权限失败" ); } } }
AuthRolePermissionDTO.java
1 2 3 4 5 6 7 8 9 10 11 12 @Data public class AuthRolePermissionDTO implements Serializable { private static final long serialVersionUID = 459343371709166261L ; private Long id; private Long roleId; private Long permissionId; private List<Long> permissionIdList; }
domain层 AuthRolePermissionDomainServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Service @Slf4j public class AuthRolePermissionDomainServiceImpl implements AuthRolePermissionDomainService { @Resource private AuthRolePermissionService authRolePermissionService; @Override public Boolean add (AuthRolePermissionBO authRolePermissionBO) { List<AuthRolePermission> rolePermissionList = new LinkedList <>(); Long roleId = authRolePermissionBO.getRoleId(); authRolePermissionBO.getPermissionIdList().forEach(permissionId -> { AuthRolePermission authRolePermission = new AuthRolePermission (); authRolePermission.setRoleId(roleId); authRolePermission.setPermissionId(permissionId); authRolePermission.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); rolePermissionList.add(authRolePermission); }); int count = authRolePermissionService.batchInsert(rolePermissionList); return count > 0 ; } }
AuthRolePermissionBO.java
1 2 3 4 5 6 7 8 9 10 11 12 @Data public class AuthRolePermissionBO implements Serializable { private static final long serialVersionUID = 459343371709166261L ; private Long id; private Long roleId; private Long permissionId; private List<Long> permissionIdList; }
infra层 AuthRolePermissionServiceImpl.java
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 @Service("authRolePermissionService") public class AuthRolePermissionServiceImpl implements AuthRolePermissionService { @Resource private AuthRolePermissionDao authRolePermissionDao; @Override public AuthRolePermission queryById (Long id) { return this .authRolePermissionDao.queryById(id); } @Override public AuthRolePermission insert (AuthRolePermission authRolePermission) { this .authRolePermissionDao.insert(authRolePermission); return authRolePermission; } @Override public int batchInsert (List<AuthRolePermission> authRolePermissionList) { return this .authRolePermissionDao.insertBatch(authRolePermissionList); } @Override public AuthRolePermission update (AuthRolePermission authRolePermission) { this .authRolePermissionDao.update(authRolePermission); return this .queryById(authRolePermission.getId()); } @Override public boolean deleteById (Long id) { return this .authRolePermissionDao.deleteById(id) > 0 ; } @Override public List<AuthRolePermission> queryByCondition (AuthRolePermission authRolePermission) { return this .authRolePermissionDao.queryAllByLimit(authRolePermission); } }
AuthRolePermission.java
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 public class AuthRolePermission implements Serializable { private static final long serialVersionUID = 459343371709166261L ; private Long id; private Long roleId; private Long permissionId; private String createdBy; private Date createdTime; private String updateBy; private Date updateTime; private Integer isDeleted; public Long getId () { return id; } public void setId (Long id) { this .id = id; } public Long getRoleId () { return roleId; } public void setRoleId (Long roleId) { this .roleId = roleId; } public Long getPermissionId () { return permissionId; } public void setPermissionId (Long permissionId) { this .permissionId = permissionId; } public String getCreatedBy () { return createdBy; } public void setCreatedBy (String createdBy) { this .createdBy = createdBy; } public Date getCreatedTime () { return createdTime; } public void setCreatedTime (Date createdTime) { this .createdTime = createdTime; } public String getUpdateBy () { return updateBy; } public void setUpdateBy (String updateBy) { this .updateBy = updateBy; } public Date getUpdateTime () { return updateTime; } public void setUpdateTime (Date updateTime) { this .updateTime = updateTime; } public Integer getIsDeleted () { return isDeleted; } public void setIsDeleted (Integer isDeleted) { this .isDeleted = isDeleted; } }
缓存与数据一致性问题(延迟双删)
根据以上的流程没有问题,但是当数据变更的时候,如何把缓存变到最新,使我们下面要讨论的问题。
更新了数据库,再更新缓存
假设数据库更新成功,缓存更新失败,在缓存失效和过期的时候,读取到的都是老数据缓存。
更新缓存,更新数据库
缓存更新成功了,数据库更新失败,是不是读取的缓存的都是错误的。
以上两种,全都不推荐。
先删除缓存,再更新数据库
有一定的使用量。即使数据库更新失败。缓存也可以会刷。
存在的问题是什么?
高并发情况下!!
比如说有两个线程,一个是 A 线程,一个是 B 线程。
A 线程把数据删了,正在更新数据库,这个时候 B 线程来了,发现缓存没了,又查数据,又放入缓存。缓存里面存的就一直是老数据了。
延迟双删: :star:
扩展思路
消息队列补偿
删除失败的缓存,作为消息打入 mq,mq 消费者进行监听,再次进行重试刷缓存。
canal
监听数据库的变化,做一个公共服务,专门来对接缓存刷新。优点业务解耦,业务太多冗余代码复杂度。
网关与auth微服务缓存打通 主要跟注册有关
AuthUserDomainServiceImpl.java
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 @Override @SneakyThrows @Transactional(rollbackFor = Exception.class) public Boolean register (AuthUserBO authUserBO) { AuthUser existAuthUser = new AuthUser (); existAuthUser.setUserName(authUserBO.getUserName()); List<AuthUser> existUser = authUserService.queryByCondition(existAuthUser); if (existUser.size() > 0 ) { return true ; } AuthUser authUser = AuthUserBOConverter.INSTANCE.convertBOToEntity(authUserBO); if (StringUtils.isNotBlank(authUser.getPassword())) { authUser.setPassword(SaSecureUtil.md5BySalt(authUser.getPassword(), salt)); } if (StringUtils.isBlank(authUser.getAvatar())) { authUser.setAvatar("http://117.72.10.84:9000/user/icon/微信图片_20231203153718(1).png" ); } if (StringUtils.isBlank(authUser.getNickName())) { authUser.setNickName("别名" ); } authUser.setStatus(AuthUserStatusEnum.OPEN.getCode()); authUser.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); Integer count = authUserService.insert(authUser); AuthRole authRole = new AuthRole (); authRole.setRoleKey(AuthConstant.NORMAL_USER); AuthRole roleResult = authRoleService.queryByCondition(authRole); Long roleId = roleResult.getId(); Long userId = authUser.getId(); AuthUserRole authUserRole = new AuthUserRole (); authUserRole.setUserId(userId); authUserRole.setRoleId(roleId); authUserRole.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); authUserRoleService.insert(authUserRole); String roleKey = redisUtil.buildKey(authRolePrefix, authUser.getUserName()); List<AuthRole> roleList = new LinkedList <>(); roleList.add(authRole); redisUtil.set(roleKey, new Gson ().toJson(roleList)); AuthRolePermission authRolePermission = new AuthRolePermission (); authRolePermission.setRoleId(roleId); List<AuthRolePermission> rolePermissionList = authRolePermissionService. queryByCondition(authRolePermission); List<Long> permissionIdList = rolePermissionList.stream() .map(AuthRolePermission::getPermissionId).collect(Collectors.toList()); List<AuthPermission> permissionList = authPermissionService.queryByRoleList(permissionIdList); String permissionKey = redisUtil.buildKey(authPermissionPrefix, authUser.getUserName()); redisUtil.set(permissionKey, new Gson ().toJson(permissionList)); return count > 0 ; }
AuthPermissionDao.java
1 List<AuthPermission> queryByRoleList (@Param("list") List<Long> roleIdList) ;
AuthPermissionDao.xml
1 2 3 4 5 6 7 <select id ="queryByRoleList" resultMap ="AuthPermissionMap" > select * from auth_permission where id in <foreach open ="(" close =")" collection ="list" item ="id" separator ="," > #{id} </foreach > </select >
[mybatis中resultMap的理解_result map-CSDN博客](https://blog.csdn.net/u012843873/article/details/80198185?ops_request_misc=%7B%22request%5Fid%22%3A%22172259150816800178584486%22%2C%22scm%22%3A%2220140713.130102334..%22%7D&request_id=172259150816800178584486&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~baidu_landing_v2~default-1-80198185-null-null.142^v100^pc_search_result_base8&utm_term=mybatic resultmap&spm=1018.2226.3001.4187)
回到网关层
StpInterfaceImpl.java
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 @Component public class StpInterfaceImpl implements StpInterface { @Resource private RedisUtil redisUtil; private String authPermissionPrefix = "auth.permission" ; private String authRolePrefix = "auth.role" ; @Override public List<String> getPermissionList (Object loginId, String loginType) { return getAuth(loginId.toString(), authPermissionPrefix); } @Override public List<String> getRoleList (Object loginId, String loginType) { return getAuth(loginId.toString(), authRolePrefix); } private List<String> getAuth (String loginId, String prefix) { String authKey = redisUtil.buildKey(prefix, loginId.toString()); String authValue = redisUtil.get(authKey); if (StringUtils.isBlank(authValue)) { return Collections.emptyList(); } List<String> authList = new LinkedList <>(); if (authRolePrefix.equals(prefix)) { List<AuthRole> roleList = new Gson ().fromJson(authValue, new TypeToken <List<AuthRole>>() { }.getType()); authList = roleList.stream().map(AuthRole::getRoleKey).collect(Collectors.toList()); } else if (authPermissionPrefix.equals(prefix)) { List<AuthPermission> permissionList = new Gson ().fromJson(authValue, new TypeToken <List<AuthPermission>>() { }.getType()); authList = permissionList.stream().map(AuthPermission::getPermissionKey).collect(Collectors.toList()); } return authList; } }
在登录后就会拿到token,在redis中存放有k v,保存有登录状态。在网关层会有拦截器根据路由进行对应的拦截,验证是否登录/有权限。
拿到token之后,就可以通过isLogin去验证是否登录了(在请求header中带有isLogin)
权限认证 (sa-token.cc)
登录开发(微信公众号,测试号) 全流程:扫码微信->微信发送消息到服务器,校验签名确保来自微信(get)->服务器再把消息进行包装, 通过前缀+验证码(一个随机数)作为redis key,fromUserName(openId)作为value存到redis,并把包含验证码的消息发送到微信->用户验证码(微信),从redis拿到openId(即用户名)->用satoken用openId进行登录,然后利用satoken返回一个token,后续登录就会带上这个token
这里用的是测试号,并不会生成真正的验证码到手机上,只是为了开发接口。
登录注册模块
注册用户与验证
短信的方式,通过向手机号发送验证码,来实现用户的验证并登录(考虑的成本是短信的费用)
邮箱的注册登录。
用户注册的时候,留一个邮箱,我们往邮箱里通过邮箱服务器发送一个链接,用户点击之后,实现一个激活,激活成功之后就完成了注册。(0 成本,坏处这种发送的邮件很容易进垃圾箱)
个人公众号模式(个人开发者无公司的,比较适合使用,0 成本)
用户登录的时候,弹出我们的这个公众号的码。扫码后,用户输入我们提示的验证码。可以随机比如说 nadbuge,通过我们的公众号对接的回调。能拿到一定的信息,用户的 openId。进而记录用户的信息
企业的服务号(必须要有营业执照,自己玩的不上线的话,也可以用测试号)
好处就是不仅打通了各种回调,而且还能拿到用户的信息。
登录功能
传统的 pc 形式,都是登录之后,写入 cookie。前端再次请求的时候,带着 cookie 一个身份识别就可以完成认证。
坏处是什么?小程序呀,app 呀,其实是没有 cookie 这个概念的。
单点登录(SSO)详解——超详细-CSDN博客
为了更好的扩展,我们就直接选择 token的模式 。token 放入 header 来实现用户身份的识别与鉴权。
踢人下线
发现风险用户,可以通过后台直接把用户踢掉,禁止其再访问,token 也可以直接置为失效的形式。
集成 redis (保存token)
如果说我们选择了 token,然后不做 token 的保存,服务重启呀,分布式微服务啊,数据是无法共享并且会产生丢失问题,所以用 redis 来存储一些信息,实现共享。
自定义我们的 token 风格和前缀
比如正常的 token 可能是 uuid,我们可以选择其他形式。
然后就是 token 的前端的传递,也可以去定义前缀,固定前缀才生效。
记住我
当我们去勾选记住我的时候,下次登录就自动实现了。
前后端分离,没有 token 的时候,必然会产生无法实现的问题,我们就选择在前端的 localstorage 来做。
登录流程 整体采取个人号的登录模式,选取某信号的 openId 作为用户的唯一标识!
整体流程:
用户扫公众号码。然后发一条消息:验证码。
通过 api 回复一个随机的验证码。存入 redis
key: 前缀+验证码
value: fromUsername(也就是openId)
用户在验证码框输入之后,点击登录,进入我们的注册模块,同时关联角色和权限。就实现了网关的统一鉴权。
用户就可以进行操作,用户可以根据个人的 openId 来维护个人信息。
用户登录成功之后,返回 token,前端的所有请求都带着 token 就可以访问了。
服务设计
开一个新的服务,叫我们的 jc-club-wechat。专门用于对接微信的 api 和微信的消息的回调。
通过 nacos 注册中心来调用我们的 auth 服务,来实现用户的注册。
另一种扩展方案,wechat 和 auth 不直接交互。
通过 mq 来做。wechat 接收回调后,反向发出 mq。自身的 auth 来订阅 mq 进行消费。
公众号开发文档 里面的例子只有python的没有java的
测试号地址:https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index
公众号开发文档:https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html
回调消息接入指南:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html
接收公众号消息体文档:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html
公众号验签开发
用户向公众号发送消息时,公众号方收到的消息发送者是一个OpenID,是使用用户微信号加密后的结果,每个用户对每个公众号有一个唯一的OpenID。
填写服务器配置:
登录微信公众平台官网后,在公众平台官网的开发-基本设置页面,勾选协议成为开发者,点击“修改配置”按钮,填写服务器地址(URL)、Token和EncodingAESKey,其中URL是开发者用来接收微信消息和事件的接口URL。Token可由开发者可以任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性)。EncodingAESKey由开发者手动填写或随机生成,将用作消息体加解密密钥。
同时,开发者可选择消息加解密方式:明文模式、兼容模式和安全模式。模式的选择与服务器配置在提交后都会立即生效,请开发者谨慎填写及选择。加解密方式的默认状态为明文模式,选择兼容模式和安全模式需要提前配置好相关加解密代码,详情请参考消息体签名及加解密部分的文档 。
验证消息的确来自微信服务器
开发者提交信息后,微信服务器将发送GET请求到填写的服务器地址URL上,GET请求携带参数如下表所示:
CallBackController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 @GetMapping("callback") public String callback (@RequestParam("signature") String signature, @RequestParam("timestamp") String timestamp, @RequestParam("nonce") String nonce, @RequestParam("echostr") String echostr) { log.info("get验签请求参数:signature:{},timestamp:{},nonce:{},echostr:{}" , signature, timestamp, nonce, echostr); String shaStr = SHA1.getSHA1(token, timestamp, nonce, "" ); if (signature.equals(shaStr)) { return echostr; } return "unknown" ; }
这里signature是微信根据传入的token、timestamp、nonce根据sha1计算生成的,是微信回调来的。
内网穿透(natapp) 不同电脑下载地址:https://natapp.cn/#download
内网穿透使用指南:
https://natapp.cn/article/natapp_newbie
windows 启动方式:
进到目录下,运行exe
1 start natapp -authtoken=xxxx
配置内网穿透:
会生成一个公网的分配的地址,从本地地址->公网地址,实现穿透
监听用户行为&自动回复消息(消息事件监听+策略模式实现解耦) controller 解释关键点
**MessageUtil.parseXml
**:这是一个自定义方法,用于将 XML 格式的字符串解析成 Map<String, String>
。
**WxChatMsgHandler
和 wxChatMsgFactory
**:WxChatMsgHandler
是一个处理微信消息的接口或类,wxChatMsgFactory
是一个工厂类,用于根据消息类型获取对应的消息处理器。
**msgTypeKey
**:这个字符串键值用于标识不同的消息类型,例如 “text”、”image”、”event.subscribe” 等。
主要逻辑流程
接收并记录微信发送的消息。
将 XML 格式的消息解析成 Map
。
提取消息类型和事件类型。
构建消息类型键值,并根据键值从工厂中获取对应的消息处理器。
使用消息处理器处理消息,返回处理结果。
CallBackController.java
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 private static final String token = "adwidhaidwoaid" ;@Resource private WxChatMsgFactory wxChatMsgFactory;@PostMapping(value = "callback", produces = "application/xml;charset=UTF-8") public String callback ( @RequestBody String requestBody, @RequestParam("signature") String signature, @RequestParam("timestamp") String timestamp, @RequestParam("nonce") String nonce, @RequestParam(value = "msg_signature", required = false) String msgSignature) { log.info("接收到微信消息:requestBody:{}" , requestBody); Map<String, String> messageMap = MessageUtil.parseXml(requestBody); String msgType = messageMap.get("MsgType" ); String event = messageMap.get("Event" ) == null ? "" : messageMap.get("Event" ); log.info("msgType:{},event:{}" , msgType, event); StringBuilder sb = new StringBuilder (); sb.append(msgType); if (!StringUtils.isEmpty(event)) { sb.append("." ); sb.append(event); } String msgTypeKey = sb.toString(); WxChatMsgHandler wxChatMsgHandler = wxChatMsgFactory.getHandlerByMsgType(msgTypeKey); if (Objects.isNull(wxChatMsgHandler)) { return "unknown" ; } String replyContent = wxChatMsgHandler.dealMsg(messageMap); log.info("replyContent:{}" , replyContent); return replyContent; }
Utils MessageUtil.java
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 class MessageUtil { public static Map<String, String> parseXml (final String msg) { Map<String, String> map = new HashMap <String, String>(); try (InputStream inputStream = new ByteArrayInputStream (msg.getBytes(StandardCharsets.UTF_8.name()))) { SAXReader reader = new SAXReader (); Document document = reader.read(inputStream); Element root = document.getRootElement(); List<Element> elementList = root.elements(); for (Element e : elementList) { map.put(e.getName(), e.getText()); } } catch (Exception e) { e.printStackTrace(); } return map; } }
handler WxChatMsgHandler.java
有两个类继承它:
1 2 3 4 5 6 7 public interface WxChatMsgHandler { WxChatMsgTypeEnum getMsgType () ; String dealMsg (Map<String, String> messageMap) ; }
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 package com.jingdianjichi.wx.handler;import com.jingdianjichi.wx.redis.RedisUtil;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;import javax.annotation.Resource;import java.util.Map;import java.util.Random;import java.util.concurrent.TimeUnit;@Component @Slf4j public class ReceiveTextMsgHandler implements WxChatMsgHandler { private static final String KEY_WORD = "验证码" ; private static final String LOGIN_PREFIX = "loginCode" ; @Resource private RedisUtil redisUtil; @Override public WxChatMsgTypeEnum getMsgType () { return WxChatMsgTypeEnum.TEXT_MSG; } @Override public String dealMsg (Map<String, String> messageMap) { log.info("接收到文本消息事件" ); String content = messageMap.get("Content" ); if (!KEY_WORD.equals(content)) { return "" ; } String fromUserName = messageMap.get("FromUserName" ); String toUserName = messageMap.get("ToUserName" ); Random random = new Random (); int num = random.nextInt(1000 ); String numKey = redisUtil.buildKey(LOGIN_PREFIX, String.valueOf(num)); redisUtil.setNx(numKey, fromUserName, 5L , TimeUnit.MINUTES); String numContent = "您当前的验证码是:" + num + "! 5分钟内有效" ; String replyContent = "<xml>\n" + " <ToUserName><![CDATA[" + fromUserName + "]]></ToUserName>\n" + " <FromUserName><![CDATA[" + toUserName + "]]></FromUserName>\n" + " <CreateTime>12345678</CreateTime>\n" + " <MsgType><![CDATA[text]]></MsgType>\n" + " <Content><![CDATA[" + numContent + "]]></Content>\n" + "</xml>" ; return replyContent; } }
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 package com.jingdianjichi.wx.handler;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;import java.util.Map;@Component @Slf4j public class SubscribeMsgHandler implements WxChatMsgHandler { @Override public WxChatMsgTypeEnum getMsgType () { return WxChatMsgTypeEnum.SUBSCRIBE; } @Override public String dealMsg (Map<String, String> messageMap) { log.info("触发用户关注事件!" ); String fromUserName = messageMap.get("FromUserName" ); String toUserName = messageMap.get("ToUserName" ); String subscribeContent = "感谢您的关注" ; String content = "<xml>\n" + " <ToUserName><![CDATA[" + fromUserName + "]]></ToUserName>\n" + " <FromUserName><![CDATA[" + toUserName + "]]></FromUserName>\n" + " <CreateTime>12345678</CreateTime>\n" + " <MsgType><![CDATA[text]]></MsgType>\n" + " <Content><![CDATA[" + subscribeContent + "]]></Content>\n" + "</xml>" ; return content; } }
redis 这里也用到了RedisUtil.java
,和gateway模块的一致,所以后续思路可以把这部分提取出来当成common模块的。
ReceiveTextMsgHandler.java
1 2 3 String numKey = redisUtil.buildKey(LOGIN_PREFIX, String.valueOf(num)); redisUtil.setNx(numKey, fromUserName, 5L , TimeUnit.MINUTES); String numContent = "您当前的验证码是:" + num + "! 5分钟内有效" ;
公众号登录验证码逻辑 UserController.java
1 2 3 4 5 6 7 8 9 10 @RequestMapping("doLogin") public Result<SaTokenInfo> doLogin (@RequestParam("validCode") String validCode) { try { Preconditions.checkArgument(!StringUtils.isBlank(validCode), "验证码不能为空!" ); return Result.ok(authUserDomainService.doLogin(validCode)); } catch (Exception e) { log.error("UserController.doLogin.error:{}" , e.getMessage(), e); return Result.fail("用户登录失败" ); } }
AuthUserDomainServiceImpl.java
通过前缀+验证码->从redis拿到openId(即用户名),用satoken用openId进行登录,然后利用satoken返回一个token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Override public SaTokenInfo doLogin (String validCode) { String loginKey = redisUtil.buildKey(LOGIN_PREFIX, validCode); String openId = redisUtil.get(loginKey); if (StringUtils.isBlank(openId)) { return null ; } AuthUserBO authUserBO = new AuthUserBO (); authUserBO.setUserName(openId); this .register(authUserBO); StpUtil.login(openId); SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); return tokenInfo; }
第二部分 前后端部署 前端:
下好node.js,
下载好依赖。
后端:
如果服务器没那么大空间,Jenkins跑起来有点困难,可以就配置好相关配置后install打好jar包,扔到服务器上:
细节优化 1、分类和标签的性能优化,一次性查询出来,组装成树结构
2、去出题的按钮的权限交互。用户登录成功后,返回给前端,当前用户的相关权限。前端存到本地 localstorage 里面,进行按钮级别的前端交互。对于一些敏感的写操作,后端也应该提供一些权限接口
3、退出功能,token 失效的功能
4、个人信息页面的查询功能。
5、上传头像的功能。
6、每次注册的时候,相同的 openId 要做校验。
1. 避免重复注册 AuthUserDomainServiceImpl.java
注意这里调了register的接口:登录—>注册(如果已经注册了就返回,没有注册就注册)->返回satoken提供了的tokenInfo。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Override public SaTokenInfo doLogin (String validCode) { String loginKey = redisUtil.buildKey(LOGIN_PREFIX, validCode); String openId = redisUtil.get(loginKey); if (StringUtils.isBlank(openId)) { return null ; } AuthUserBO authUserBO = new AuthUserBO (); authUserBO.setUserName(openId); this .register(authUserBO); StpUtil.login(openId); SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); return tokenInfo; }
AuthUserDomainServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 @Override @SneakyThrows @Transactional(rollbackFor = Exception.class) public Boolean register (AuthUserBO authUserBO) { AuthUser existAuthUser = new AuthUser (); existAuthUser.setUserName(authUserBO.getUserName()); List<AuthUser> existUser = authUserService.queryByCondition(existAuthUser); if (existUser.size() > 0 ) { return true ; } ... }
2.个人信息查询 UserController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RequestMapping("getUserInfo") public Result<AuthUserDTO> getUserInfo (@RequestBody AuthUserDTO authUserDTO) { try { if (log.isInfoEnabled()) { log.info("UserController.getUserInfo.dto:{}" , JSON.toJSONString(authUserDTO)); } Preconditions.checkArgument(!StringUtils.isBlank(authUserDTO.getUserName()), "用户名不能为空" ); AuthUserBO authUserBO = AuthUserDTOConverter.INSTANCE.convertDTOToBO(authUserDTO); AuthUserBO userInfo = authUserDomainService.getUserInfo(authUserBO); return Result.ok(AuthUserDTOConverter.INSTANCE.convertBOToDTO(userInfo)); } catch (Exception e) { log.error("UserController.update.error:{}" , e.getMessage(), e); return Result.fail("更新用户信息失败" ); } }
authUserDomainServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 @Override public AuthUserBO getUserInfo (AuthUserBO authUserBO) { AuthUser authUser = new AuthUser (); authUser.setUserName(authUserBO.getUserName()); List<AuthUser> userList = authUserService.queryByCondition(authUser); if (CollectionUtils.isEmpty(userList)) { return new AuthUserBO (); } AuthUser user = userList.get(0 ); return AuthUserBOConverter.INSTANCE.convertEntityToBO(user); }
authUserServiceImpl.java
1 2 3 4 @Override public List<AuthUser> queryByCondition (AuthUser authUser) { return this .authUserDao.queryAllByLimit(authUser); }
3.用户退出 UserController.java
1 2 3 4 5 6 7 8 9 10 11 12 @RequestMapping("logOut") public Result logOut (@RequestParam String userName) { try { log.info("UserController.logOut.userName:{}" , userName); Preconditions.checkArgument(!StringUtils.isBlank(userName), "用户名不能为空" ); StpUtil.logout(userName); return Result.ok(); } catch (Exception e) { log.error("UserController.logOut.error:{}" , e.getMessage(), e); return Result.fail("用户登出失败" ); } }
4. 用户上传头像 oss
模块下的FileController.java
接口定义 :/upload
是一个 HTTP 请求映射,用于接收文件上传请求。
参数 :MultipartFile uploadFile
是上传的文件,String bucket
是桶的名称,String objectName
是对象名称(文件路径)。
调用服务 :调用 fileService.uploadFile
方法上传文件,并获取文件的 URL。
返回结果 :使用 Result.ok(url)
返回上传后的文件 URL。
1 2 3 4 5 @RequestMapping("/upload") public Result upload (MultipartFile uploadFile, String bucket, String objectName) throws Exception { String url = fileService.uploadFile(uploadFile, bucket, objectName); return Result.ok(url); }
fileService.java
调用适配器 :调用 storageAdapter.uploadFile
方法,将文件上传到指定的桶和对象名称。
更新对象名称 :将 objectName
更新为 objectName + "/" + uploadFile.getOriginalFilename()
,这一步是为了构建文件的完整路径。
获取 URL :调用 storageAdapter.getUrl
方法,获取文件的访问 URL 并返回。
1 2 3 4 5 public String uploadFile (MultipartFile uploadFile, String bucket, String objectName) { storageAdapter.uploadFile(uploadFile,bucket,objectName); objectName = objectName + "/" + uploadFile.getOriginalFilename(); return storageAdapter.getUrl(bucket, objectName); }
MinioStorageAdapter.java
创建桶 :调用 minioUtil.createBucket(bucket)
方法,确保桶存在。如果桶不存在则创建。
上传文件 :根据 objectName
是否为空,决定文件上传的路径。如果 objectName
不为空,则将文件上传到 objectName + "/" + uploadFile.getOriginalFilename()
。如果为空,则上传到 uploadFile.getOriginalFilename()
。
获取 URL :构建并返回文件的 URL。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override @SneakyThrows public void uploadFile (MultipartFile uploadFile, String bucket, String objectName) { minioUtil.createBucket(bucket); if (objectName != null ) { minioUtil.uploadFile(uploadFile.getInputStream(), bucket, objectName + "/" + uploadFile.getOriginalFilename()); } else { minioUtil.uploadFile(uploadFile.getInputStream(), bucket, uploadFile.getOriginalFilename()); } } @Override @SneakyThrows public String getUrl (String bucket, String objectName) { return url + "/" + bucket + "/" + objectName; }
5.分类题目数量更新 SubjectCategoryController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @PostMapping("/queryPrimaryCategory") public Result<List<SubjectCategoryDTO>> queryPrimaryCategory (@RequestBody SubjectCategoryDTO subjectCategoryDTO) { try { SubjectCategoryBO subjectCategoryBO = SubjectCategoryDTOConverter.INSTANCE. convertDtoToCategoryBO(subjectCategoryDTO); List<SubjectCategoryBO> subjectCategoryBOList = subjectCategoryDomainService.queryCategory(subjectCategoryBO); List<SubjectCategoryDTO> subjectCategoryDTOList = SubjectCategoryDTOConverter.INSTANCE. convertBoToCategoryDTOList(subjectCategoryBOList); return Result.ok(subjectCategoryDTOList); } catch (Exception e) { log.error("SubjectCategoryController.queryPrimaryCategory.error:{}" , e.getMessage(), e); return Result.fail("查询失败" ); } }
SubjectCategoryDomainServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Override public List<SubjectCategoryBO> queryCategory (SubjectCategoryBO subjectCategoryBO) { SubjectCategory subjectCategory = SubjectCategoryConverter.INSTANCE .convertBoToCategory(subjectCategoryBO); subjectCategory.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); List<SubjectCategory> subjectCategoryList = subjectCategoryService.queryCategory(subjectCategory); List<SubjectCategoryBO> boList = SubjectCategoryConverter.INSTANCE .convertBoToCategory(subjectCategoryList); if (log.isInfoEnabled()) { log.info("SubjectCategoryController.queryPrimaryCategory.boList:{}" , JSON.toJSONString(boList)); } boList.forEach(bo -> { Integer subjectCount = subjectCategoryService.querySubjectCount(bo.getId()); bo.setCount(subjectCount); }); return boList; }
SubjectCategoryServiceImpl.java
1 2 3 4 5 6 7 8 9 @Override public List<SubjectCategory> queryCategory (SubjectCategory subjectCategory) { return this .subjectCategoryDao.queryCategory(subjectCategory); } @Override public Integer querySubjectCount (Long id) { return this .subjectCategoryDao.querySubjectCount(id); }
SubjectCategoryDao.xml
1 2 3 4 5 6 7 <select id ="querySubjectCount" resultType ="java.lang.Integer" > select count(distinct subject_id) from subject_mapping a, subject_label b where a.label_id = b.id and b.category_id = #{id} </select >
6. 分类标签性能优化:star: SubjectCategoryDomainServiceImpl.java
核心是使用CompletableFuture
和Java流来并行处理任务:
对每个category
,启动一个异步任务来获取标签列表,并将任务结果以CompletableFuture
的形式收集到列表中。
遍历completableFutureList
,获取每个任务的结果,并将非空结果合并到map
中。 【Java 8 新特性】Java CompletableFuture supplyAsync()详解_completablefuture.supplyasync-CSDN博客
通过这种方式,可以并行处理多个任务,提高了程序的效率。
SubjectCategoryController.java
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 @PostMapping("/queryCategoryAndLabel") public Result<List<SubjectCategoryDTO>> queryCategoryAndLabel (@RequestBody SubjectCategoryDTO subjectCategoryDTO) { try { if (log.isInfoEnabled()) { log.info("SubjectCategoryController.queryCategoryAndLabel.dto:{}" , JSON.toJSONString(subjectCategoryDTO)); } Preconditions.checkNotNull(subjectCategoryDTO.getId(), "分类id不能为空" ); SubjectCategoryBO subjectCategoryBO = SubjectCategoryDTOConverter.INSTANCE. convertDtoToCategoryBO(subjectCategoryDTO); List<SubjectCategoryBO> subjectCategoryBOList = subjectCategoryDomainService.queryCategoryAndLabel(subjectCategoryBO); List<SubjectCategoryDTO> dtoList = new LinkedList <>(); subjectCategoryBOList.forEach(bo -> { SubjectCategoryDTO dto = SubjectCategoryDTOConverter.INSTANCE.convertBoToCategoryDTO(bo); List<SubjectLabelDTO> labelDTOList = SubjectLabelDTOConverter.INSTANCE.convertBOToLabelDTOList(bo.getLabelBOList()); dto.setLabelDTOList(labelDTOList); dtoList.add(dto); }); return Result.ok(dtoList); } catch (Exception e) { log.error("SubjectCategoryController.queryPrimaryCategory.error:{}" , e.getMessage(), e); return Result.fail("查询失败" ); } }
自定义线程工厂:
CustomNameThreadFactory.java
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 import java.util.concurrent.ThreadFactory;import java.util.concurrent.atomic.AtomicInteger;import org.apache.commons.lang3.StringUtils;public class CustomNameThreadFactory implements ThreadFactory { private static final AtomicInteger poolNumber = new AtomicInteger (1 ); private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger (1 ); private final String namePrefix; CustomNameThreadFactory(String name) { SecurityManager s = System.getSecurityManager(); group = (s != null ) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); if (StringUtils.isBlank(name)) { name = "pool" ; } namePrefix = name + "-" + poolNumber.getAndIncrement() + "-thread-" ; } @Override public Thread newThread (Runnable r) { Thread t = new Thread (group, r, namePrefix + threadNumber.getAndIncrement(), 0 ); if (t.isDaemon()){ t.setDaemon(false ); } if (t.getPriority() != Thread.NORM_PRIORITY){ t.setPriority(Thread.NORM_PRIORITY); } return t; } }
SubjectCategoryDomainServiceImpl.java
创建并初始化SubjectCategory
对象 :
设置ParentId
为categoryId
。
设置IsDeleted
为未删除状态。
查询类别列表 :
调用subjectCategoryService.queryCategory
方法查询类别列表。
日志记录 :
转换类别列表 :
使用SubjectCategoryConverter
将SubjectCategory
列表转换为SubjectCategoryBO
列表。
并行处理标签列表 :
为每个类别创建一个异步任务,调用getLabelBOList
方法获取标签列表。
使用CompletableFuture
并行处理这些任务。
收集并合并结果 :
等待所有异步任务完成,并将结果合并到一个Map<Long, List<SubjectLabelBO>>
中。
设置标签列表 :
为每个SubjectCategoryBO
对象设置对应的标签列表。
返回结果 :
返回包含标签列表的SubjectCategoryBO
列表。
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 @Resource private ThreadPoolExecutor labelThreadPool;@SneakyThrows @Override public List<SubjectCategoryBO> queryCategoryAndLabel (SubjectCategoryBO subjectCategoryBO) { Long id = subjectCategoryBO.getId(); String cacheKey = "categoryAndLabel." + subjectCategoryBO.getId(); List<SubjectCategoryBO> subjectCategoryBOS = cacheUtil.getResult(cacheKey, SubjectCategoryBO.class, (key) -> getSubjectCategoryBOS(id)); return subjectCategoryBOS; } private List<SubjectCategoryBO> getSubjectCategoryBOS (Long categoryId) { SubjectCategory subjectCategory = new SubjectCategory (); subjectCategory.setParentId(categoryId); subjectCategory.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); List<SubjectCategory> subjectCategoryList = subjectCategoryService.queryCategory(subjectCategory); if (log.isInfoEnabled()) { log.info("SubjectCategoryController.queryCategoryAndLabel.subjectCategoryList:{}" , JSON.toJSONString(subjectCategoryList)); } List<SubjectCategoryBO> categoryBOList = SubjectCategoryConverter.INSTANCE.convertBoToCategory(subjectCategoryList); Map<Long, List<SubjectLabelBO>> map = new HashMap <>(); List<CompletableFuture<Map<Long, List<SubjectLabelBO>>>> completableFutureList = categoryBOList.stream().map(category -> CompletableFuture.supplyAsync(() -> getLabelBOList(category), labelThreadPool).collect(Collectors.toList()); completableFutureList.forEach(future -> { try { Map<Long, List<SubjectLabelBO>> resultMap = future.get(); if (!MapUtils.isEmpty(resultMap)) { map.putAll(resultMap); } } catch (Exception e) { e.printStackTrace(); } }); categoryBOList.forEach(categoryBO -> { if (!CollectionUtils.isEmpty(map.get(categoryBO.getId()))) { categoryBO.setLabelBOList(map.get(categoryBO.getId())); } }); return categoryBOList; } private Map<Long, List<SubjectLabelBO>> getLabelBOList (SubjectCategoryBO category) { if (log.isInfoEnabled()) { log.info("getLabelBOList:{}" , JSON.toJSONString(category)); } Map<Long, List<SubjectLabelBO>> labelMap = new HashMap <>(); SubjectMapping subjectMapping = new SubjectMapping (); subjectMapping.setCategoryId(category.getId()); List<SubjectMapping> mappingList = subjectMappingService.queryLabelId(subjectMapping); if (CollectionUtils.isEmpty(mappingList)) { return null ; } List<Long> labelIdList = mappingList.stream().map(SubjectMapping::getLabelId).collect(Collectors.toList()); List<SubjectLabel> labelList = subjectLabelService.batchQueryById(labelIdList); List<SubjectLabelBO> labelBOList = new LinkedList <>(); labelList.forEach(label -> { SubjectLabelBO subjectLabelBO = new SubjectLabelBO (); subjectLabelBO.setId(label.getId()); subjectLabelBO.setLabelName(label.getLabelName()); subjectLabelBO.setCategoryId(label.getCategoryId()); subjectLabelBO.setSortNum(label.getSortNum()); labelBOList.add(subjectLabelBO); }); labelMap.put(category.getId(), labelBOList); return labelMap; }
这段代码的核心是使用CompletableFuture
和Java流来并行处理任务:
对每个category
,启动一个异步任务来获取标签列表,并将任务结果以CompletableFuture
的形式收集到列表中。
遍历completableFutureList
,获取每个任务的结果,并将非空结果合并到map
中。 【Java 8 新特性】Java CompletableFuture supplyAsync()详解_completablefuture.supplyasync-CSDN博客
通过这种方式,可以并行处理多个任务,提高了程序的效率。
7. 用户权限获取 AuthPermissionDomainServiceImpl.java
将存储在Redis中的权限数据转换为一个易于使用的权限键列表,以便在应用程序中进行权限检查。如果Redis中没有找到用户的权限数据,则返回一个空列表。
1 2 3 4 5 6 7 8 9 10 11 12 13 @Override public List<String> getPermission (String userName) { String permissionKey = redisUtil.buildKey(authPermissionPrefix, userName); String permissionValue = redisUtil.get(permissionKey); if (StringUtils.isBlank(permissionValue)) { return Collections.emptyList(); } List<AuthPermission> permissionList = new Gson ().fromJson(permissionValue, new TypeToken <List<AuthPermission>>() { }.getType()); List<String> authList = permissionList.stream().map(AuthPermission::getPermissionKey).collect(Collectors.toList()); return authList; }
8. 利用minio/mc突破图片7天权限
有两个问题:
minio上传头像只能保存7天
生成的url很长,需要简化
1 2 3 4 5 6 7 8 9 10 11 12 13 docker pull minio/mc docker run -it --entrypoint=/bin/sh minio/mc mc config host add <ALIAS> <YOUR-S3-ENDPOINT> <YOUR-ACCESS-KEY> <YOUR-SECRET-KEY> [--api API-SIGNATURE] mc config host add minio http://xxx.xx.xx.xxx:9000 GrVCPXySKgGoJiGgXmtv 0xlqSI9GXvnBOtp0GwUj5OshKNBk9JgwoexotbVV mc ls minio mc anonymous //可以设置什么 mc anonymous set download minio/jichi
url: bucket+name
FileController.java
MinioStorageAdapter.java
FileController.java
MinioStorageAdapter.java
功能规划 搜索功能(完成) 全文检索,技术选型 es。
安装 es。
xxl-job 定时任务,去做一个数据同步,全量数据导入
es 全文检索,做高亮
点赞(完成) 自己点赞过的,这里肯定要有一个点赞过的 icon 的一个标识
后面的数量,意味着这道题目被多少个人点过赞。
如何去防刷点赞。疯狂的点赞,取消点赞。前端配合防抖,后端的点赞数量放到 redis 里面。数据库的持久化,可以通过定时任务来定时的刷新同步。
我的点赞(完成) 展示,我们当前当过赞的所有的数据,来进行一波展示。
收藏(完成)
我的收藏(完成)
纠错(完成) 纠错当用户发现题目有问题,错误的话,就可以通过这个方式,来进行反馈。
快速刷题(完成)
在这个位置去加一个上一题,下一题。
贡献榜(完成) 按照我的周维度,月维度,来做数据的存储。zset。和 redis 做大量的交互。
feign 的微服务间调用(完成) 会涉及到微服务之间的逻辑调用。这个就用 feign 了。
打通用户上下文(完成) 配合 threadlocal,基于 token 来实现用户信息的上下文传递。
二级缓存的使用(完成) 点赞里面。
用户上下文打通 链路流程:
详细设计:
Loginfilter
(实现Globalfilter接口
,通过filter拿到token,解析出loginId ,然后传到后面的过滤链中)
->LoginInterceptor
(实现HandlerInterceptor
,检验loginId 是否存在且非空,如果存在,将其保存到自定义的线程局部变量上下文LoginContextHolder
中,通过InheritableThreadLocal
来实现)
以上都不拦截doLogin操作
LoginFilter.java
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 @Component @Slf4j public class LoginFilter implements GlobalFilter { @Override @SneakyThrows public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); ServerHttpRequest.Builder mutate = request.mutate(); String url = request.getURI().getPath(); log.info("LoginFilter.filter.url:{}" , url); if (url.equals("/user/doLogin" )) { return chain.filter(exchange); } SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); log.info("LoginFilter.filter.url:{}" , new Gson ().toJson(tokenInfo)); String loginId = (String) tokenInfo.getLoginId(); mutate.header("loginId" , loginId); return chain.filter(exchange.mutate().request(mutate.build()).build()); } }
通过filter拿到token,解析出loginId,然后传到后面的过滤链中。
基于threadLocal实现上下文传递 mvc的全局处理:GlobalConfig.java
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 @Configuration public class GlobalConfig extends WebMvcConfigurationSupport { @Override protected void configureMessageConverters (List<HttpMessageConverter<?>> converters) { super .configureMessageConverters(converters); converters.add(mappingJackson2HttpMessageConverter()); } @Override protected void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor ()) .addPathPatterns("/**" ) .excludePathPatterns("/user/doLogin" ); } private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter () { ObjectMapper objectMapper = new ObjectMapper (); objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false ); objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); return new MappingJackson2HttpMessageConverter (objectMapper); } }
登录拦截器:LoginInterceptor.java
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 public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String loginId = request.getHeader("loginId" ); if (StringUtils.isNotBlank(loginId)) { LoginContextHolder.set("loginId" , loginId); } return true ; } @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { LoginContextHolder.remove(); } }
登录上下文对象: LoginContextHolder
InheritableThreadLocal详解-CSDN博客
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 public class LoginContextHolder { private static final InheritableThreadLocal<Map<String, Object>> THREAD_LOCAL = new InheritableThreadLocal <>(); public static void set (String key, Object val) { Map<String, Object> map = getThreadLocalMap(); map.put(key, val); } public static Object get (String key) { Map<String, Object> threadLocalMap = getThreadLocalMap(); return threadLocalMap.get(key); } public static String getLoginId () { return (String) getThreadLocalMap().get("loginId" ); } public static void remove () { THREAD_LOCAL.remove(); } public static Map<String, Object> getThreadLocalMap () { Map<String, Object> map = THREAD_LOCAL.get(); if (Objects.isNull(map)) { map = new ConcurrentHashMap <>(); THREAD_LOCAL.set(map); } return map; } }
之后就可以根据这个上下文,封装一下,去拿loginId
1 2 3 4 5 6 7 8 public class LoginUtil { public static String getLoginId () { return LoginContextHolder.getLoginId(); } }
微服务之间的Feign调用 微服务之间的调用
openfeign 是 spring cloud 搞出来的一个升级版,netflix 的 feign 这个不维护了。
openfeign 他就是声明式的 webservice 的客户端,使用 feign,编写调用更加的简单,主要打上注解就可以进行一个调用。
1 String responese = service.hello();
一行代码直接搞定。
feign 就帮助我们把 http 的调用编的非常的容易和方便,他整体的实现就是利用了 resttemplate 对 http 的一个封装。
feign 通过注解的方式配置之后,就可以完成接口的自动绑定,那我们调用 feign 的时候就像调接口一样,内置负载。内部封装了 ribbon。
实操:
首先,微服务之间要暴露出提供给其他服务的接口。在这里以auth
包中的接口暴露给subject
包的接口为例。
auth:
jc-club-auth-api
包中的UserFeignService
接口,这里注意@FeignClient
注解后面的"jc-club-auth-dev"
实则是在starter
包中对应的bootstrap.yaml
中的服务名称(这个东西是注册到nacos上面的)
1 2 3 4 5 6 7 8 9 10 11 @FeignClient("jc-club-auth-dev") public interface UserFeignService { @RequestMapping("/user/getUserInfo") Result<AuthUserDTO> getUserInfo (@RequestBody AuthUserDTO authUserDTO) ; @RequestMapping("/user/listByIds") Result<List<AuthUserDTO>> listUserInfoByIds (@RequestBody List<String> userNameList) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Override public AuthUserBO getUserInfo (AuthUserBO authUserBO) { AuthUser authUser = new AuthUser (); authUser.setUserName(authUserBO.getUserName()); List<AuthUser> userList = authUserService.queryByCondition(authUser); if (CollectionUtils.isEmpty(userList)) { return new AuthUserBO (); } AuthUser user = userList.get(0 ); return AuthUserBOConverter.INSTANCE.convertEntityToBO(user); } @Override public List<AuthUserBO> listUserInfoByIds (List<String> userNameList) { List<AuthUser> userList = authUserService.listUserInfoByIds(userNameList); if (CollectionUtils.isEmpty(userList)) { return Collections.emptyList(); } return AuthUserBOConverter.INSTANCE.convertEntityToBO(userList); }
在jc-club-auth-application-controller
包中的pom.xml
文件中,添加对该api包的依赖
1 2 3 4 5 <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-auth-api</artifactId > <version > 1.0-SNAPSHOT</version > </dependency >
subject:
在jc-club-subject
的infra
包中的pom.xml
中添加依赖
1 2 3 4 5 <dependency > <groupId > com.jingdianjichi</groupId > <artifactId > jc-club-auth-api</artifactId > <version > 1.0-SNAPSHOT</version > </dependency >
在jc-club-subject
的infra
包中添加UserRpc.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Component public class UserRpc { @Resource private UserFeignService userFeignService; public UserInfo getUserInfo (String userName) { AuthUserDTO authUserDTO = new AuthUserDTO (); authUserDTO.setUserName(userName); Result<AuthUserDTO> result = userFeignService.getUserInfo(authUserDTO); UserInfo userInfo = new UserInfo (); if (!result.getSuccess()) { return userInfo; } AuthUserDTO data = result.getData(); userInfo.setUserName(data.getUserName()); userInfo.setNickName(data.getNickName()); userInfo.setAvatar(data.getAvatar()); return userInfo; } }
进行测试,能拿到userInfo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestController @RequestMapping("/subject/category") @Slf4j public class TestFeignController { @Resource private UserRpc userRpc; @GetMapping("testFeign") public void testFeign () { UserInfo userInfo = userRpc.getUserInfo("lzrj" ); log.info("testFeign.userInfo:{}" , userInfo); } }
openFeign
拦截器实现用户上下文打通
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 import feign.RequestInterceptor;import feign.RequestTemplate;import org.springframework.stereotype.Component;import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;import org.apache.commons.lang3.StringUtils;import java.util.Objects;@Component public class FeignRequestInterceptor implements RequestInterceptor { @Override public void apply (RequestTemplate requestTemplate) { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); if (Objects.nonNull(request)) { String loginId = request.getHeader("loginId" ); if (StringUtils.isNotBlank(loginId)) { requestTemplate.header("loginId" , loginId); } } } }
1 2 3 4 5 6 7 8 9 @Configuration public class FeignConfiguration { @Bean public RequestInterceptor requestInterceptor () { return new FeignRequestInterceptor (); } }
通过FeignRequestInterceptor
在调用微服务之前,就可以把相关请求的loginId封装到请求中,进行跨微服务的loginId的传递:
guava本地缓存(已经升级成Caffiene) 泛型+函数式编程
guava:
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 import com.google.common.cache.Cache;import com.google.common.cache.CacheBuilder;import com.google.common.collect.Lists;import com.google.gson.JsonObject;import org.apache.commons.collections4.CollectionUtils;import org.apache.commons.lang3.StringUtils;import java.util.List;import java.util.Map;import java.util.concurrent.TimeUnit;import org.springframework.stereotype.Component;@Component public class CacheUtil <K, V> { private Cache<String, String> localCache = CacheBuilder.newBuilder() .maximumSize(5000 ) .expireAfterWrite(10 , TimeUnit.SECONDS) .build(); public List<V> getResult (String cacheKey, Class<V> clazz, Function<String, List<V>> function) { List<V> resultList = new ArrayList <>(); String content = localCache.getIfPresent(cacheKey); if (StringUtils.isNotBlank(content)) { resultList = JSON.parseArray(content, clazz); } else { resultList = function.apply(cacheKey); if (!CollectionUtils.isEmpty(resultList)) { localCache.put(cacheKey, JSON.toJSONString(resultList)); } } return resultList; } public Map<K, V> getMapResult (String cacheKey, Class<V> clazz, Function<String, Map<K, V>> function) { return new HashMap <>(); } }
在这用到了queryCategoryAndLabel
:
1 2 3 4 5 6 7 8 9 10 @SneakyThrows @Override public List<SubjectCategoryBO> queryCategoryAndLabel (SubjectCategoryBO subjectCategoryBO) { Long id = subjectCategoryBO.getId(); String cacheKey = "categoryAndLabel." + subjectCategoryBO.getId(); List<SubjectCategoryBO> subjectCategoryBOS = cacheUtil.getResult(cacheKey, SubjectCategoryBO.class, (key) -> getSubjectCategoryBOS(id)); return subjectCategoryBOS; }
全文检索功能 ElasticSearch从入门到精通,史上最全(持续更新,未完待续,每天一点点)_elasticsearch从入门到精通,史上最全-CSDN博客
功能设计
技术选型:elasticsearch。
目的是,网站现在整体的题目预计会到好几百,方便快速的搜索到自己想看的内容。
实现形式:
实际操作 [通过HTTP的方式操作ES-CSDN博客](https://blog.csdn.net/sss294438204/article/details/122884953?ops_request_misc=%7B%22request%5Fid%22%3A%22172355571716800211536069%22%2C%22scm%22%3A%2220140713.130102334..%22%7D&request_id=172355571716800211536069&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-2-122884953-null-null.142^v100^pc_search_result_base8&utm_term=es http&spm=1018.2226.3001.4187)
接口:
es分词问题 原生的不太行,用这个
1 https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.3.1/elasticsearch-analysis-ik-7.3.1.zip
解压后,上传到服务器 的 ik 文件夹下面:
1 2 3 4 5 6 7 8 9 10 11 12 mkdir /soft/ik 进入容器内部 docker exec -it elasticsearch /bin/bash cd plugins mkdir ik 回到外部 docker cp /soft/ik/. 73438a827b55:/usr/share/elasticsearch/plugins/ik 重启es docker restart 73438a827b55
编码: 试手 subject-infra
层pom.xml
1 2 3 4 5 6 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-elasticsearch</artifactId > <version > 2.4.2</version > </dependency >
TestFeignController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @GetMapping("testCreateIndex") public void testCreateIndex () { subjectEsService.createIndex(); } @GetMapping("addDocs") public void addDocs () { subjectEsService.addDoc(); } @GetMapping("find") public void find () { subjectEsService.find(); } @GetMapping("search") public void search () { subjectEsService.search(); }
实体类:SubjectInfoEs.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Data @AllArgsConstructor @NoArgsConstructor @Document(indexName = "subject_index", createIndex = false) public class SubjectInfoEs { @Field(type = FieldType.Long) @Id private Long id; @Field(type = FieldType.Text, analyzer = "ik_smart") private String subjectName; @Field(type = FieldType.Text, analyzer = "ik_smart") private String subjectAnswer; @Field(type = FieldType.Keyword) private String createUser; @Field(type = FieldType.Date, index = false) private Date createTime; }
SubjectEsRepository.java
,继承自ElasticsearchRepository
。这个接口是Spring Data Elasticsearch的一部分,用于提供对Elasticsearch的访问和操作。
1 2 3 @Component public interface SubjectEsRepository extends ElasticsearchRepository <SubjectInfoEs, Long> {}
SubjectEsService.java
1 2 3 4 5 6 7 8 9 10 11 public interface SubjectEsService { void createIndex () ; void addDoc () ; void find () ; void search () ; }
SubjectEsServiceImpl.java
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 @Service @Slf4j public class SubjectEsServiceImpl implements SubjectEsService { @Resource private ElasticsearchRestTemplate elasticsearchRestTemplate; @Resource private SubjectEsRepository subjectEsRepository; @Override public void createIndex () { IndexOperations indexOperations = elasticsearchRestTemplate.indexOps(SubjectInfoEs.class); indexOperations.create(); Document mapping = indexOperations.createMapping(SubjectInfoEs.class); indexOperations.putMapping(mapping); } @Override public void addDoc () { List<SubjectInfoEs> list = new ArrayList <>(); list.add(new SubjectInfoEs (1L ,"redis是什么" ,"redis是一个缓存" ,"鸡翅" ,new Date ())); list.add(new SubjectInfoEs (2L ,"mysql是什么" ,"mysql是数据库" ,"鸡翅" ,new Date ())); subjectEsRepository.saveAll(list); } @Override public void find () { Iterable<SubjectInfoEs> all = subjectEsRepository.findAll(); for (SubjectInfoEs subjectInfoEs : all){ log.info("subjectInfoEs:{}" ,JSON.toJSONString(subjectInfoEs)); } } @Override public void search () { NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder () .withQuery(QueryBuilders.matchQuery("subjectName" ,"redis" )) .build(); SearchHits<SubjectInfoEs> search = elasticsearchRestTemplate. search(nativeSearchQuery, SubjectInfoEs.class); List<SearchHit<SubjectInfoEs>> searchHits = search.getSearchHits(); log.info("searchHits:{}" , JSON.toJSONString(searchHits)); } }
subject-starter
中的application.yaml
1 2 3 elasticsearch: rest: uris:http://172.72.14.166:9200
编码:自定义封装es集群连接统一管理 希望的一个目的
有自己的封装好的工具
集群,索引等等都要兼容的配置的概念
不想用 data 的这种方式,不够扩展
配置类:读取配置文件自定义的属性,支持集群,节点等等一些信息
@Configuration + @ConfigurationProperties + @Data(必须提供set方法)
@Configuration + @Value
集群类:集群的名称、集群的节点
索引类:集群名称、索引名称
封装的请求类:查询条件、查询字段、页数、条数、快照、快照缓存时间、排序字段、排序类型、高亮
封装的返回类:文档id(保证唯一)、所有跟restClient交互的封装成一个Map
自定义工具类:目的就是为了提供一个RestHighLevelClient,在原生client的基础上封装一些好用的api
整体基于 es 的原生的 client 来去做。
subject-infra
层pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <dependency > <groupId > org.elasticsearch</groupId > <artifactId > elasticsearch</artifactId > <version > 7.5.2</version > </dependency > <dependency > <groupId > org.elasticsearch.client</groupId > <artifactId > elasticsearch-rest-client</artifactId > <version > 7.5.2</version > </dependency > <dependency > <groupId > org.elasticsearch.client</groupId > <artifactId > elasticsearch-rest-high-level-client</artifactId > <version > 7.5.2</version > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-collections4</artifactId > <version > 4.4</version > </dependency >
es集群类EsClusterConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Data public class EsClusterConfig implements Serializable { private String name; private String nodes; }
EsConfigProperties.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Component @ConfigurationProperties(prefix = "es.cluster") public class EsConfigProperties { private List<EsClusterConfig> esConfigs = new ArrayList <>(); public List<EsClusterConfig> getEsConfigs () { return esConfigs; } public void setEsConfigs (List<EsClusterConfig> esConfigs) { this .esConfigs = esConfigs; } }
subject-starter
中的application.yaml
1 2 3 4 5 es: cluster: esConfigs[0]: name: 73438a827b55 nodes: 117.72 .14 .166 :9200
EsIndexInfo.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Data public class EsIndexInfo implements Serializable { private String clusterName; private String indexName; }
EsSearchRequest.java
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 @Data public class EsSearchRequest { private BoolQueryBuilder bq; private String[] fields; private int from; private int size; private Boolean needScroll; private Long minutes; private String sortName; private SortOrder sortOrder; private HighlightBuilder highlightBuilder; }
EsSourceData.java
1 2 3 4 5 6 7 8 @Data public class EsSourceData implements Serializable { private String docId; private Map<String, Object> data; }
EsRestClient.java
这个类封装了与Elasticsearch集群交互的常见操作
类定义和日志记录 :使用@Component
注解表明这是一个Spring组件,使用@Slf4j
来引入日志记录功能。
客户端映射 :clientMap
是一个静态的HashMap
,用于存储不同Elasticsearch集群的RestHighLevelClient
实例。
配置属性注入 :通过@Resource
注解注入EsConfigProperties
,这是一个配置属性类,用于获取Elasticsearch集群的配置信息。
请求选项 :定义了一个静态的RequestOptions
对象COMMON_OPTIONS
,用于后续的请求。
初始化方法 :initialize
方法在组件初始化时被调用,用于根据配置创建和初始化RestHighLevelClient
实例。
创建客户端方法 :initRestClient
是一个私有方法,用于根据给定的集群配置创建RestHighLevelClient
实例。
获取客户端方法 :getClient
是一个静态方法,用于根据集群名称获取对应的RestHighLevelClient
实例。
文档操作 :类中定义了一系列的静态方法,用于执行Elasticsearch中的文档操作,如插入(insertDoc
)、更新(updateDoc
)、批量更新(batchUpdateDoc
)、删除(delete
和deleteDoc
)、检查文档是否存在(isExistDocById
)、获取文档(getDocById
)等。
搜索功能 :searchWithTermQuery
方法用于执行基于布尔查询构建器的搜索请求,并支持高亮显示、排序和滚动(scroll)。
批量插入 :batchInsertDoc
方法用于批量插入文档。
更新查询 :updateByQuery
方法允许执行基于查询的更新操作。
分词功能 :getAnalyze
方法提供了一个分词功能,可以对输入的文本进行分词。
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 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 @Component @Slf4j public class EsRestClient { public static Map<String, RestHighLevelClient> clientMap = new HashMap <>(); @Resource private EsConfigProperties esConfigProperties; private static final RequestOptions COMMON_OPTIONS; static { RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder(); COMMON_OPTIONS = builder.build(); } @PostConstruct public void initialize () { List<EsClusterConfig> esConfigs = esConfigProperties.getEsConfigs(); for (EsClusterConfig esConfig : esConfigs) { log.info("initialize.config.name:{},node:{}" , esConfig.getName(), esConfig.getNodes()); RestHighLevelClient restHighLevelClient = initRestClient(esConfig); if (restHighLevelClient != null ) { clientMap.put(esConfig.getName(), restHighLevelClient); } else { log.error("config.name:{},node:{}.initError" , esConfig.getName(), esConfig.getNodes()); } } } private RestHighLevelClient initRestClient (EsClusterConfig esClusterConfig) { String[] ipPortArr = esClusterConfig.getNodes().split("," ); List<HttpHost> httpHostList = new ArrayList <>(ipPortArr.length); for (String ipPort : ipPortArr) { String[] ipPortInfo = ipPort.split(":" ); if (ipPortInfo.length == 2 ) { HttpHost httpHost = new HttpHost (ipPortInfo[0 ], NumberUtils.toInt(ipPortInfo[1 ])); httpHostList.add(httpHost); } } HttpHost[] httpHosts = new HttpHost [httpHostList.size()]; httpHostList.toArray(httpHosts); RestClientBuilder builder = RestClient.builder(httpHosts); RestHighLevelClient restHighLevelClient = new RestHighLevelClient (builder); return restHighLevelClient; } private static RestHighLevelClient getClient (String clusterName) { return clientMap.get(clusterName); } public static boolean insertDoc (EsIndexInfo esIndexInfo, EsSourceData esSourceData) { try { IndexRequest indexRequest = new IndexRequest (esIndexInfo.getIndexName()); indexRequest.source(esSourceData.getData()); indexRequest.id(esSourceData.getDocId()); getClient(esIndexInfo.getClusterName()).index(indexRequest, COMMON_OPTIONS); return true ; } catch (Exception e) { log.error("insertDoc.exception:{}" , e.getMessage(), e); } return false ; } public static boolean updateDoc (EsIndexInfo esIndexInfo, EsSourceData esSourceData) { try { UpdateRequest updateRequest = new UpdateRequest (); updateRequest.index(esIndexInfo.getIndexName()); updateRequest.id(esSourceData.getDocId()); updateRequest.doc(esSourceData.getData()); getClient(esIndexInfo.getClusterName()).update(updateRequest, COMMON_OPTIONS); return true ; } catch (Exception e) { log.error("updateDoc.exception:{}" , e.getMessage(), e); } return false ; } public static boolean batchUpdateDoc (EsIndexInfo esIndexInfo, List<EsSourceData> esSourceDataList) { try { boolean flag = false ; BulkRequest bulkRequest = new BulkRequest (); for (EsSourceData esSourceData : esSourceDataList) { String docId = esSourceData.getDocId(); if (StringUtils.isNotBlank(docId)) { UpdateRequest updateRequest = new UpdateRequest (); updateRequest.index(esIndexInfo.getIndexName()); updateRequest.id(esSourceData.getDocId()); updateRequest.doc(esSourceData.getData()); bulkRequest.add(updateRequest); flag = true ; } } if (flag) { BulkResponse bulk = getClient(esIndexInfo.getClusterName()).bulk(bulkRequest, COMMON_OPTIONS); if (bulk.hasFailures()) { return false ; } } return true ; } catch (Exception e) { log.error("batchUpdateDoc.exception:{}" , e.getMessage(), e); } return false ; } public static boolean delete (EsIndexInfo esIndexInfo) { try { DeleteByQueryRequest deleteByQueryRequest = new DeleteByQueryRequest (esIndexInfo.getIndexName()); deleteByQueryRequest.setQuery(QueryBuilders.matchAllQuery()); BulkByScrollResponse response = getClient(esIndexInfo.getClusterName()).deleteByQuery( deleteByQueryRequest, COMMON_OPTIONS ); long deleted = response.getDeleted(); log.info("deleted.size:{}" , deleted); return true ; } catch (Exception e) { log.error("delete.exception:{}" , e.getMessage(), e); } return false ; } public static boolean deleteDoc (EsIndexInfo esIndexInfo, String docId) { try { DeleteRequest deleteRequest = new DeleteRequest (esIndexInfo.getIndexName()); deleteRequest.id(docId); DeleteResponse response = getClient(esIndexInfo.getClusterName()).delete(deleteRequest, COMMON_OPTIONS); log.info("deleteDoc.response:{}" , JSON.toJSONString(response)); return true ; } catch (Exception e) { log.error("deleteDoc.exception:{}" , e.getMessage(), e); } return false ; } public static boolean isExistDocById (EsIndexInfo esIndexInfo, String docId) { try { GetRequest getRequest = new GetRequest (esIndexInfo.getIndexName()); getRequest.id(docId); return getClient(esIndexInfo.getClusterName()).exists(getRequest, COMMON_OPTIONS); } catch (Exception e) { log.error("isExistDocById.exception:{}" , e.getMessage(), e); } return false ; } public static Map<String, Object> getDocById (EsIndexInfo esIndexInfo, String docId) { try { GetRequest getRequest = new GetRequest (esIndexInfo.getIndexName()); getRequest.id(docId); GetResponse response = getClient(esIndexInfo.getClusterName()).get(getRequest, COMMON_OPTIONS); Map<String, Object> source = response.getSource(); return source; } catch (Exception e) { log.error("isExistDocById.exception:{}" , e.getMessage(), e); } return null ; } public static Map<String, Object> getDocById (EsIndexInfo esIndexInfo, String docId, String[] fields) { try { GetRequest getRequest = new GetRequest (esIndexInfo.getIndexName()); getRequest.id(docId); FetchSourceContext fetchSourceContext = new FetchSourceContext (true , fields, null ); getRequest.fetchSourceContext(fetchSourceContext); GetResponse response = getClient(esIndexInfo.getClusterName()).get(getRequest, COMMON_OPTIONS); Map<String, Object> source = response.getSource(); return source; } catch (Exception e) { log.error("isExistDocById.exception:{}" , e.getMessage(), e); } return null ; } public static SearchResponse searchWithTermQuery (EsIndexInfo esIndexInfo, EsSearchRequest esSearchRequest) { try { BoolQueryBuilder bq = esSearchRequest.getBq(); String[] fields = esSearchRequest.getFields(); int from = esSearchRequest.getFrom(); int size = esSearchRequest.getSize(); Long minutes = esSearchRequest.getMinutes(); Boolean needScroll = esSearchRequest.getNeedScroll(); String sortName = esSearchRequest.getSortName(); SortOrder sortOrder = esSearchRequest.getSortOrder(); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder (); searchSourceBuilder.query(bq); searchSourceBuilder.fetchSource(fields, null ).from(from).size(size); if (Objects.nonNull(esSearchRequest.getHighlightBuilder())) { searchSourceBuilder.highlighter(esSearchRequest.getHighlightBuilder()); } if (StringUtils.isNotBlank(sortName)) { searchSourceBuilder.sort(sortName); } searchSourceBuilder.sort(new ScoreSortBuilder ().order(SortOrder.DESC)); SearchRequest searchRequest = new SearchRequest (); searchRequest.searchType(SearchType.DEFAULT); searchRequest.indices(esIndexInfo.getIndexName()); searchRequest.source(searchSourceBuilder); if (needScroll) { Scroll scroll = new Scroll (TimeValue.timeValueMinutes(minutes)); searchRequest.scroll(scroll); } SearchResponse search = getClient(esIndexInfo.getClusterName()).search(searchRequest, COMMON_OPTIONS); return search; } catch (Exception e) { log.error("searchWithTermQuery.exception:{}" , e.getMessage(), e); } return null ; } public static boolean batchInsertDoc (EsIndexInfo esIndexInfo, List<EsSourceData> esSourceDataList) { if (log.isInfoEnabled()) { log.info("批量新增ES:" + esSourceDataList.size()); log.info("indexName:" + esIndexInfo.getIndexName()); } try { boolean flag = false ; BulkRequest bulkRequest = new BulkRequest (); for (EsSourceData source : esSourceDataList) { String docId = source.getDocId(); if (StringUtils.isNotBlank(docId)) { IndexRequest indexRequest = new IndexRequest (esIndexInfo.getIndexName()); indexRequest.id(docId); indexRequest.source(source.getData()); bulkRequest.add(indexRequest); flag = true ; } } if (flag) { BulkResponse response = getClient(esIndexInfo.getClusterName()).bulk(bulkRequest, COMMON_OPTIONS); if (response.hasFailures()) { return false ; } } } catch (Exception e) { log.error("batchInsertDoc.error" , e); } return true ; } public static boolean updateByQuery (EsIndexInfo esIndexInfo, QueryBuilder queryBuilder, Script script, int batchSize) { if (log.isInfoEnabled()) { log.info("updateByQuery.indexName:" + esIndexInfo.getIndexName()); } try { UpdateByQueryRequest updateByQueryRequest = new UpdateByQueryRequest (esIndexInfo.getIndexName()); updateByQueryRequest.setQuery(queryBuilder); updateByQueryRequest.setScript(script); updateByQueryRequest.setBatchSize(batchSize); updateByQueryRequest.setAbortOnVersionConflict(false ); BulkByScrollResponse response = getClient(esIndexInfo.getClusterName()).updateByQuery(updateByQueryRequest, RequestOptions.DEFAULT); List<BulkItemResponse.Failure> failures = response.getBulkFailures(); } catch (Exception e) { log.error("updateByQuery.error" , e); } return true ; } public static List<String> getAnalyze (EsIndexInfo esIndexInfo, String text) throws Exception { List<String> list = new ArrayList <String>(); Request request = new Request ("GET" , "_analyze" ); JSONObject entity = new JSONObject (); entity.put("analyzer" , "ik_smart" ); entity.put("text" , text); request.setJsonEntity(entity.toJSONString()); Response response = getClient(esIndexInfo.getClusterName()).getLowLevelClient().performRequest(request); JSONObject tokens = JSONObject.parseObject(EntityUtils.toString(response.getEntity())); JSONArray arrays = tokens.getJSONArray("tokens" ); for (int i = 0 ; i < arrays.size(); i++) { JSONObject obj = JSON.parseObject(arrays.getString(i)); list.add(obj.getString("token" )); } return list; } }
新增题目同步到es+带高亮的网站全文搜索 subject-infra
的SubjectInfoEs.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Data public class SubjectInfoEs extends PageInfo implements Serializable { private Long subjectId; private Long docId; private String subjectName; private String subjectAnswer; private String createUser; private Long createTime; private Integer subjectType; private String keyWord; private BigDecimal score; }
subject-infra
的EsSubjectFields.java
负责与Elasticsearch(ES)的交互,包括数据的插入和查询。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class EsSubjectFields { public static final String DOC_ID = "doc_id" ; public static final String SUBJECT_ID = "subject_id" ; public static final String SUBJECT_NAME = "subject_name" ; public static final String SUBJECT_ANSWER = "subject_answer" ; public static final String SUBJECT_TYPE = "subject_type" ; public static final String CREATE_USER = "create_user" ; public static final String CREATE_TIME = "create_time" ; public static final String[] FIELD_QUERY = { SUBJECT_ID, SUBJECT_NAME, SUBJECT_ANSWER, SUBJECT_TYPE, DOC_ID, CREATE_USER, CREATE_TIME }; }
subject-infra
的SubjectEsService.java
1 2 3 4 5 6 7 public interface SubjectEsService { boolean insert (SubjectInfoEs subjectInfoEs) ; PageResult<SubjectInfoEs> querySubjectList (SubjectInfoEs subjectInfoEs) ; }
SubjectEsServiceImpl.java
插入方法 :insert
方法实现了将SubjectInfoEs
对象转换为ES的文档并插入到ES中。它首先调用convert2EsSourceData
方法将SubjectInfoEs
对象转换为ES的源数据格式,然后使用EsRestClient
的insertDoc
方法执行插入操作。
转换方法 :convert2EsSourceData
是一个私有方法,用于将SubjectInfoEs
对象的属性转换为一个Map,这个Map将作为ES文档的数据部分。
查询方法 :querySubjectList
方法实现了分页查询ES中的数据。它首先创建一个EsSearchRequest
查询请求,然后使用EsRestClient
的searchWithTermQuery
方法执行查询,并将结果转换为PageResult<SubjectInfoEs>
对象。
结果转换方法 :convertResult
是一个私有方法,用于将ES查询结果的SearchHit
转换为SubjectInfoEs
对象。它还处理了高亮显示查询关键字的功能。
查询构建方法 :createSearchListQuery
是一个私有方法,用于构建查询请求。它使用BoolQueryBuilder
来构建查询条件,并设置了高亮显示的配置。
获取ES索引信息方法 :getEsIndexInfo
是一个私有方法,用于获取ES的索引信息,包括集群名称和索引名称。
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 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 @Service @Slf4j public class SubjectEsServiceImpl implements SubjectEsService { @Override public boolean insert (SubjectInfoEs subjectInfoEs) { EsSourceData esSourceData = new EsSourceData (); Map<String, Object> data = convert2EsSourceData(subjectInfoEs); esSourceData.setDocId(subjectInfoEs.getDocId().toString()); esSourceData.setData(data); return EsRestClient.insertDoc(getEsIndexInfo(), esSourceData); } private Map<String, Object> convert2EsSourceData (SubjectInfoEs subjectInfoEs) { Map<String, Object> data = new HashMap <>(); data.put(EsSubjectFields.SUBJECT_ID, subjectInfoEs.getSubjectId()); data.put(EsSubjectFields.DOC_ID, subjectInfoEs.getDocId()); data.put(EsSubjectFields.SUBJECT_NAME, subjectInfoEs.getSubjectName()); data.put(EsSubjectFields.SUBJECT_ANSWER, subjectInfoEs.getSubjectAnswer()); data.put(EsSubjectFields.SUBJECT_TYPE, subjectInfoEs.getSubjectType()); data.put(EsSubjectFields.CREATE_USER, subjectInfoEs.getCreateUser()); data.put(EsSubjectFields.CREATE_TIME, subjectInfoEs.getCreateTime()); return data; } @Override public PageResult<SubjectInfoEs> querySubjectList (SubjectInfoEs req) { PageResult<SubjectInfoEs> pageResult = new PageResult <>(); EsSearchRequest esSearchRequest = createSearchListQuery(req); SearchResponse searchResponse = EsRestClient.searchWithTermQuery(getEsIndexInfo(), esSearchRequest); List<SubjectInfoEs> subjectInfoEsList = new LinkedList <>(); SearchHits searchHits = searchResponse.getHits(); if (searchHits == null || searchHits.getHits() == null ) { pageResult.setPageNo(req.getPageNo()); pageResult.setPageSize(req.getPageSize()); pageResult.setRecords(subjectInfoEsList); pageResult.setTotal(0 ); return pageResult; } SearchHit[] hits = searchHits.getHits(); for (SearchHit hit : hits) { SubjectInfoEs subjectInfoEs = convertResult(hit); if (Objects.nonNull(subjectInfoEs)) { subjectInfoEsList.add(subjectInfoEs); } } pageResult.setPageNo(req.getPageNo()); pageResult.setPageSize(req.getPageSize()); pageResult.setRecords(subjectInfoEsList); pageResult.setTotal(Long.valueOf(searchHits.getTotalHits().value).intValue()); return pageResult; } private SubjectInfoEs convertResult (SearchHit hit) { Map<String, Object> sourceAsMap = hit.getSourceAsMap(); if (CollectionUtils.isEmpty(sourceAsMap)) { return null ; } SubjectInfoEs result = new SubjectInfoEs (); result.setSubjectId(MapUtils.getLong(sourceAsMap, EsSubjectFields.SUBJECT_ID)); result.setSubjectName(MapUtils.getString(sourceAsMap, EsSubjectFields.SUBJECT_NAME)); result.setSubjectAnswer(MapUtils.getString(sourceAsMap, EsSubjectFields.SUBJECT_ANSWER)); result.setDocId(MapUtils.getLong(sourceAsMap, EsSubjectFields.DOC_ID)); result.setSubjectType(MapUtils.getInteger(sourceAsMap, EsSubjectFields.SUBJECT_TYPE)); result.setScore(new BigDecimal (String.valueOf(hit.getScore())) .multiply(new BigDecimal ("100.00" ).setScale(2 , RoundingMode.HALF_UP))); Map<String, HighlightField> highlightFields = hit.getHighlightFields(); HighlightField subjectNameField = highlightFields.get(EsSubjectFields.SUBJECT_NAME); if (Objects.nonNull(subjectNameField)) { Text[] fragments = subjectNameField.getFragments(); StringBuilder subjectNameBuilder = new StringBuilder (); for (Text fragment : fragments) { subjectNameBuilder.append(fragment); } result.setSubjectName(subjectNameBuilder.toString()); } HighlightField subjectAnswerField = highlightFields.get(EsSubjectFields.SUBJECT_ANSWER); if (Objects.nonNull(subjectAnswerField)) { Text[] fragments = subjectAnswerField.getFragments(); StringBuilder subjectAnswerBuilder = new StringBuilder (); for (Text fragment : fragments) { subjectAnswerBuilder.append(fragment); } result.setSubjectAnswer(subjectAnswerBuilder.toString()); } return result; } private EsSearchRequest createSearchListQuery (SubjectInfoEs req) { EsSearchRequest esSearchRequest = new EsSearchRequest (); BoolQueryBuilder bq = new BoolQueryBuilder (); MatchQueryBuilder subjectNameQueryBuilder = QueryBuilders.matchQuery(EsSubjectFields.SUBJECT_NAME, req.getKeyWord()); bq.should(subjectNameQueryBuilder); subjectNameQueryBuilder.boost(2 ); MatchQueryBuilder subjectAnswerQueryBuilder = QueryBuilders.matchQuery(EsSubjectFields.SUBJECT_ANSWER, req.getKeyWord()); bq.should(subjectAnswerQueryBuilder); MatchQueryBuilder subjectTypeQueryBuilder = QueryBuilders.matchQuery(EsSubjectFields.SUBJECT_TYPE, SubjectInfoTypeEnum.BRIEF.getCode()); bq.must(subjectTypeQueryBuilder); bq.minimumShouldMatch(1 ); HighlightBuilder highlightBuilder = new HighlightBuilder ().field("*" ).requireFieldMatch(false ); highlightBuilder.preTags("<span style = \"color:red\">" ); highlightBuilder.postTags("</span>" ); esSearchRequest.setBq(bq); esSearchRequest.setHighlightBuilder(highlightBuilder); esSearchRequest.setFields(EsSubjectFields.FIELD_QUERY); esSearchRequest.setFrom((req.getPageNo() - 1 ) * req.getPageSize()); esSearchRequest.setSize(req.getPageSize()); esSearchRequest.setNeedScroll(false ); return esSearchRequest; } private EsIndexInfo getEsIndexInfo () { EsIndexInfo esIndexInfo = new EsIndexInfo (); esIndexInfo.setClusterName("73438a827b55" ); esIndexInfo.setIndexName("subject_index" ); return esIndexInfo; } }
SubjectInfoDomainServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override @Transactional(rollbackFor = Exception.class) public void add (SubjectInfoBO subjectInfoBO) { ...... SubjectInfoEs subjectInfoEs = new SubjectInfoEs (); subjectInfoEs.setDocId(new IdWorkerUtil (1 , 1 , 1 ).nextId()); subjectInfoEs.setSubjectId(subjectInfo.getId()); subjectInfoEs.setSubjectAnswer(subjectInfoBO.getSubjectAnswer()); subjectInfoEs.setCreateTime(new Date ().getTime()); subjectInfoEs.setCreateUser("lzrj" ); subjectInfoEs.setSubjectName(subjectInfo.getSubjectName()); subjectInfoEs.setSubjectType(subjectInfo.getSubjectType()); subjectEsService.insert(subjectInfoEs); }
SubjectController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @PostMapping("/getSubjectPageBySearch") public Result<PageResult<SubjectInfoEs>> getSubjectPageBySearch (@RequestBody SubjectInfoDTO subjectInfoDTO) { try { if (log.isInfoEnabled()) { log.info("SubjectController.getSubjectPageBySearch.dto:{}" , JSON.toJSONString(subjectInfoDTO)); } Preconditions.checkArgument(StringUtils.isNotBlank(subjectInfoDTO.getKeyWord()), "关键词不能为空" ); SubjectInfoBO subjectInfoBO = SubjectInfoDTOConverter.INSTANCE.convertDTOToBO(subjectInfoDTO); subjectInfoBO.setPageNo(subjectInfoDTO.getPageNo()); subjectInfoBO.setPageSize(subjectInfoDTO.getPageSize()); PageResult<SubjectInfoEs> boPageResult = subjectInfoDomainService.getSubjectPageBySearch(subjectInfoBO); return Result.ok(boPageResult); } catch (Exception e) { log.error("SubjectCategoryController.getSubjectPageBySearch.error:{}" , e.getMessage(), e); return Result.fail("全文检索失败" ); } }
SubjectInfoDomainServiceImpl.java
1 2 3 4 5 6 7 8 @Override public PageResult<SubjectInfoEs> getSubjectPageBySearch (SubjectInfoBO subjectInfoBO) { SubjectInfoEs subjectInfoEs = new SubjectInfoEs (); subjectInfoEs.setPageNo(subjectInfoBO.getPageNo()); subjectInfoEs.setPageSize(subjectInfoBO.getPageSize()); subjectInfoEs.setKeyWord(subjectInfoBO.getKeyWord()); return subjectEsService.querySubjectList(subjectInfoEs); }
手写Mybatis拦截器自动填充数据(方法级别拦截器) LoginContextHolder
和LoginUtil
都放在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 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 @Component @Slf4j @Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) }) public class MybatisInterceptor implements Interceptor { @Override public Object intercept (Invocation invocation) throws Throwable { MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0 ]; SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType(); Object parameter = invocation.getArgs()[1 ]; if (parameter == null ) { return invocation.proceed(); } String loginId = LoginUtil.getLoginId(); if (StringUtils.isBlank(loginId)) { return invocation.proceed(); } if (SqlCommandType.INSERT == sqlCommandType || SqlCommandType.UPDATE == sqlCommandType) { replaceEntityProperty(parameter, loginId, sqlCommandType); } return invocation.proceed(); } private void replaceEntityProperty (Object parameter, String loginId, SqlCommandType sqlCommandType) { if (parameter instanceof Map) { replaceMap((Map) parameter, loginId, sqlCommandType); } else { replace(parameter, loginId, sqlCommandType); } } private void replaceMap (Map parameter, String loginId, SqlCommandType sqlCommandType) { for (Object val : parameter.values()) { replace(val, loginId, sqlCommandType); } } private void replace (Object parameter, String loginId, SqlCommandType sqlCommandType) { if (SqlCommandType.INSERT == sqlCommandType) { dealInsert(parameter, loginId); } else { dealUpdate(parameter, loginId); } } private void dealUpdate (Object parameter, String loginId) { Field[] fields = getAllFields(parameter); for (Field field : fields) { try { field.setAccessible(true ); Object o = field.get(parameter); if (Objects.nonNull(o)) { field.setAccessible(false ); continue ; } if ("updateBy" .equals(field.getName())) { field.set(parameter, loginId); } else if ("updateTime" .equals(field.getName())) { field.set(parameter, new Date ()); } else { } field.setAccessible(false ); } catch (Exception e) { log.error("dealUpdate.error:{}" , e.getMessage(), e); } } } private void dealInsert (Object parameter, String loginId) { Field[] fields = getAllFields(parameter); for (Field field : fields) { try { field.setAccessible(true ); Object o = field.get(parameter); if (Objects.nonNull(o)) { field.setAccessible(false ); continue ; } if ("isDeleted" .equals(field.getName())) { field.set(parameter, 0 ); } else if ("createdBy" .equals(field.getName())) { field.set(parameter, loginId); } else if ("createdTime" .equals(field.getName())) { field.set(parameter, new Date ()); } else { } field.setAccessible(false ); } catch (Exception e) { log.error("dealInsert.error:{}" , e.getMessage(), e); } } } private Field[] getAllFields(Object object) { Class<?> clazz = object.getClass(); List<Field> fieldList = new ArrayList <>(); while (clazz != null ) { fieldList.addAll(new ArrayList <>(Arrays.asList(clazz.getDeclaredFields()))); clazz = clazz.getSuperclass(); } Field[] fields = new Field [fieldList.size()]; fieldList.toArray(fields); return fields; } @Override public Object plugin (Object target) { return Plugin.wrap(target, this ); } @Override public void setProperties (Properties properties) { } }
题目排行榜功能设计 排行榜一般来说实时的,非实时的。
实时的方案
数据库统计
现在数据库里面的 createby 字段。用户的标识是唯一的,那我直接通过 group by 的形式统计 count。
select count(1),create_by from subject_info group by create_by limit 0,5;
数据量比较小,并发也比较小。这种方案是 ok 的。保证可以走到索引,返回速度快,不要产生慢 sql。
在数据库层面加一层缓存,接受一定的延时性。
redis 的 sorted set
有序集合,不允许重复的成员,然后每一个 key 都会包含一个 score 分数的概念。redis 根据分数可以帮助我们做从小到大,和从大到小的一个处理。
有序集合的 key 不可重复,score 重复。
它通过我们的一个哈希表来实现的,添加,删除,查找,复杂度 o(1) ,最大数量是 2的32 次方-1。
1 2 3 4 5 6 7 zadd zrange zincrby zscore
这种的好处在于,完全不用和数据库做任何的交互,纯纯的通过缓存来做,速度非常快,要避免一些大 key 的问题。
非实时
定时任务 xxl-job
统计数据库的数据形式,帮助我们统计完成后,直接写入缓存。缓存的外部的交互展示。
传统数据库实现排行榜 SubjectController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @PostMapping("/getContributeList") public Result<List<SubjectInfoDTO>> getContributeList () { try { List<SubjectInfoBO> boList = subjectInfoDomainService.getContributeList(); List<SubjectInfoDTO> dtoList = SubjectInfoDTOConverter.INSTANCE.convertBOToDTOList(boList); return Result.ok(dtoList); } catch (Exception e) { log.error("SubjectCategoryController.getContributeList.error:{}" , e.getMessage(), e); return Result.fail("获取贡献榜失败" ); } }
SubjectDomainInfoServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Override public List<SubjectInfoBO> getContributeList () { List<SubjectInfo> subjectInfoList = subjectInfoService.getContributeCount(); if (CollectionUtils.isEmpty(subjectInfoList)) { return Collections.emptyList(); } List<SubjectInfoBO> boList = new LinkedList <>(); subjectInfoList.forEach((subjectInfo -> { SubjectInfoBO subjectInfoBO = new SubjectInfoBO (); subjectInfoBO.setSubjectCount(subjectInfo.getSubjectCount()); UserInfo userInfo = userRpc.getUserInfo(subjectInfo.getCreatedBy()); subjectInfoBO.setCreateUser(userInfo.getNickName()); subjectInfoBO.setCreateUserAvatar(userInfo.getAvatar()); boList.add(subjectInfoBO); })); return boList; }
UserRpc.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Component public class UserRpc { @Resource private UserFeignService userFeignService; public UserInfo getUserInfo (String userName) { AuthUserDTO authUserDTO = new AuthUserDTO (); authUserDTO.setUserName(userName); Result<AuthUserDTO> result = userFeignService.getUserInfo(authUserDTO); UserInfo userInfo = new UserInfo (); if (!result.getSuccess()) { return userInfo; } AuthUserDTO data = result.getData(); userInfo.setUserName(data.getUserName()); userInfo.setNickName(data.getNickName()); userInfo.setAvatar(data.getAvatar()); return userInfo; } }
subjectInfoServiceImpl.java
1 2 3 4 @Override public List<SubjectInfo> getContributeCount () { return this .subjectInfoDao.getContributeCount(); }
SubjectInfoDao.xml
1 2 3 4 5 6 7 8 9 <select id ="getContributeCount" resultType ="com.jingdianjichi.subject.infra.basic.entity.SubjectInfo" > select count(1) as subjectCount, created_by as createdBy from subject_info where is_deleted = 0 and created_by is not null group by created_by limit 0,5 </select >
基于redis的zset实现排行榜 subject-domain/pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > <version > 2.4.2</version > </dependency > <dependency > <groupId > com.fasterxml.jackson.core</groupId > <artifactId > jackson-core</artifactId > <version > 2.12.7</version > </dependency > <dependency > <groupId > com.fasterxml.jackson.core</groupId > <artifactId > jackson-databind</artifactId > <version > 2.12.7</version > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-pool2</artifactId > <version > 2.9.0</version > </dependency >
subject-doamin/RedisUtil.java
1 2 3 4 public Set<ZSetOperations.TypedTuple<String>> rankWithScore(String key, long start, long end) { Set<ZSetOperations.TypedTuple<String>> set = redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end); return set; }
redis——Zset有序集合之reverseRangeWithScore函数使用_reverserangewithscores-CSDN博客
SubjectInfoDomainServiceImpl.java
排行榜接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private static final String RANK_KEY = "subject_rank" ;@Override public List<SubjectInfoBO> getContributeList () { Set<ZSetOperations.TypedTuple<String>> typedTuples = redisUtil.rankWithScore(RANK_KEY, 0 , 5 ); if (log.isInfoEnabled()) { log.info("getContributeList.typedTuples:{}" , JSON.toJSONString(typedTuples)); } if (CollectionUtils.isEmpty(typedTuples)) { return Collections.emptyList(); } List<SubjectInfoBO> boList = new LinkedList <>(); typedTuples.forEach((rank -> { SubjectInfoBO subjectInfoBO = new SubjectInfoBO (); subjectInfoBO.setSubjectCount(rank.getScore().intValue()); UserInfo userInfo = userRpc.getUserInfo(rank.getValue()); subjectInfoBO.setCreateUser(userInfo.getNickName()); subjectInfoBO.setCreateUserAvatar(userInfo.getAvatar()); boList.add(subjectInfoBO); })); return boList; }
点赞和收藏 点赞和收藏功能设计 点赞与收藏的逻辑是非常一样的,我们这里就选取点赞功能来给大家做开发。
按照我们的程序员 club 的设计,点赞业务其实涉及几个方面:
我们肯定要知道一个题目被多少人点过赞
还要知道,每个人他点赞了哪些题目。
点赞的业务特性:频繁。用户一多,时时刻刻都在进行点赞啊,收藏啊等等处理,如果说我们采取传统的数据库的模式啊,这个交互量是非常大的,很难去抗住这个并发问题,所以我们采取 redis 的方式来做。
查询的数据交互,我们可以和 redis 直接来做,持久化的数据,通过数据库查询即可,这个数据如何去同步到数据库,我们就采取的定时任务 xxl-job 定期来刷数据。
记录的时候三个关键信息,点赞的人,被点赞的题目,点赞的状态。
我们最终的数据结构就是 hash,string 类型。
hash,存到一个键里面,键里是一个 map,他又分为 hashkey 和 hashval。
谁点赞了哪个题目+状态:hashkey,subjectId:userId,val 就存的是点赞的状态 1 是点赞 0 是不点赞。
点赞数量:string 类型 key subjectId,val 即使我们的题目被点赞的数量。
有没有点过赞,key存在说明点过(并非记录状态):key为string 类型, subjectId:userId。
表结构:
新增点赞
直接操作 redis
存 hash,存数量,存点赞的人与题目的 key。
取消点赞
上面的反逻辑,数量会-1,hash 里面的状态会更新,点赞人与题目关联的 key 会被删除
查询当前题目被点赞的数量
直接与 redis 交互,读题目的被点赞数量的 key
查询当前题目被当前用户是否点过赞
直接查 redis 就可以了。
我的点赞
直接查数据库做分页逻辑的展示。
点赞功能开发 RedisUtil.java
1 2 3 4 public void putHash (String key, String hashKey, Object hashVal) { redisTemplate.opsForHash().put(key, hashKey, hashVal); }
SubjectLikedDomainServiceImpl.java
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 private static final String SUBJECT_LIKED_KEY = "subject.liked" ;private static final String SUBJECT_LIKED_COUNT_KEY = "subject.liked.count" ;private static final String SUBJECT_LIKED_DETAIL_KEY = "subject.liked.detail" ;public void add (SubjectLikedBO subjectLikedBO) { Long subjectId = subjectLikedBO.getSubjectId(); String likeUserId = subjectLikedBO.getLikeUserId(); Integer status = subjectLikedBO.getStatus(); String hashKey = buildSubjectLikedKey(subjectId.toString(), likeUserId); redisUtil.putHash(SUBJECT_LIKED_KEY, hashKey, status); String detailKey = SUBJECT_LIKED_DETAIL_KEY + "." + subjectId + "." + likeUserId; String countKey = SUBJECT_LIKED_COUNT_KEY + "." + subjectId; if (SubjectLikedStatusEnum.LIKED.getCode() == status) { redisUtil.increment(countKey, 1 ); redisUtil.set(detailKey, "1" ); } else { Integer count = redisUtil.getInt(countKey); if (Objects.isNull(count) || count <= 0 ) { return ; } redisUtil.increment(countKey, -1 ); redisUtil.del(detailKey); } }
题目详情增加点赞数据 SubjectLikedDomainServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public Boolean isLiked (String subjectId, String userId) { String detailKey = SUBJECT_LIKED_DETAIL_KEY + "." + subjectId + "." + userId; return redisUtil.exist(detailKey); } @Override public Integer getLikedCount (String subjectId) { String countKey = SUBJECT_LIKED_COUNT_KEY + "." + subjectId; Integer count = redisUtil.getInt(countKey); if (Objects.isNull(count) || count <= 0 ) { return 0 ; } return redisUtil.getInt(countKey); }
SubjectInfoDomainServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Override public SubjectInfoBO querySubjectInfo (SubjectInfoBO subjectInfoBO) { SubjectInfo subjectInfo = subjectInfoService.queryById(subjectInfoBO.getId()); SubjectTypeHandler handler = subjectTypeHandlerFactory.getHandler(subjectInfo.getSubjectType()); SubjectOptionBO optionBO = handler.query(subjectInfo.getId().intValue()); SubjectInfoBO bo = SubjectInfoConverter.INSTANCE.convertOptionAndInfoToBo(optionBO, subjectInfo); SubjectMapping subjectMapping = new SubjectMapping (); subjectMapping.setSubjectId(subjectInfo.getId()); subjectMapping.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); List<SubjectMapping> mappingList = subjectMappingService.queryLabelId(subjectMapping); List<Long> labelIdList = mappingList.stream().map(SubjectMapping::getLabelId).collect(Collectors.toList()); List<SubjectLabel> labelList = subjectLabelService.batchQueryById(labelIdList); List<String> labelNameList = labelList.stream().map(SubjectLabel::getLabelName).collect(Collectors.toList()); bo.setLabelName(labelNameList); bo.setLiked(subjectLikedDomainService.isLiked(subjectInfoBO.getId().toString(), LoginUtil.getLoginId())); bo.setLikedCount(subjectLikedDomainService.getLikedCount(subjectInfoBO.getId().toString())); assembleSubjectCursor(subjectInfoBO, bo); return bo; }
xxljob定时任务完成 XXL-JOB 是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
分布式任务调度平台XXL-JOB (xuxueli.com)
特性
1、简单:支持通过 Web 页面对任务进行 CRUD 操作,操作简单,一分钟上手;
2、动态:支持动态修改任务状态、启动 / 停止任务,以及终止运行中任务,即时生效;
3、调度中心 HA(中心式):调度采用中心式设计,“调度中心” 自研调度组件并支持集群部署,可保证调度中心 HA;
4、执行器 HA(分布式):任务分布式执行,任务 “执行器” 支持集群部署,可保证任务执行 HA;
5、注册中心:执行器会周期性自动注册任务,调度中心将会自动发现注册的任务并触发执行。同时,也支持手动录入执行器地址;
6、弹性扩容缩容:一旦有新执行器机器上线或者下线,下次调度时将会重新分配任务;
7、路由策略:执行器集群部署时提供丰富的路由策略,包括:第一个、最后一个、轮询、随机、一致性 HASH、最不经常使用、最近最久未使用、故障转移、忙碌转移等;
8、故障转移:任务路由策略选择 “故障转移” 情况下,如果执行器集群中某一台机器故障,将会自动 Failover 切换到一台正常的执行器发送调度请求。
9、阻塞处理策略:调度过于密集执行器来不及处理时的处理策略,策略包括:单机串行(默认)、丢弃后续调度、覆盖之前调度;
10、任务超时控制:支持自定义任务超时时间,任务运行超时将会主动中断任务;
11、任务失败重试:支持自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试;其中分片任务支持分片粒度的失败重试;
12、任务失败告警;默认提供邮件方式失败告警,同时预留扩展接口,可方便的扩展短信、钉钉等告警方式;
13、分片广播任务:执行器集群部署时,任务路由策略选择 “分片广播” 情况下,一次任务调度将会广播触发集群中所有执行器执行一次任务,可根据分片参数开发分片任务;
14、动态分片:分片广播任务以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。
15、事件触发:除了 “Cron 方式” 和 “任务依赖方式” 触发任务执行之外,支持基于事件的触发任务方式。调度中心提供触发任务单次执行的 API 服务,可根据业务事件灵活触发。
16、任务进度监控:支持实时监控任务进度;
17、Rolling 实时日志:支持在线查看调度结果,并且支持以 Rolling 方式实时查看执行器输出的完整的执行日志;
18、GLUE:提供 Web IDE,支持在线开发任务逻辑代码,动态发布,实时编译生效,省略部署上线的过程。支持 30 个版本的历史版本回溯。
19、脚本任务:支持以 GLUE 模式开发和运行脚本任务,包括 Shell、Python、NodeJS、PHP、PowerShell 等类型脚本;
20、命令行任务:原生提供通用命令行任务 Handler(Bean 任务,”CommandJobHandler”);业务方只需要提供命令行即可;
21、任务依赖:支持配置子任务依赖,当父任务执行结束且执行成功后将会主动触发一次子任务的执行,多个子任务用逗号分隔;
22、一致性:“调度中心” 通过 DB 锁保证集群分布式调度的一致性,一次任务调度只会触发一次执行;
23、自定义任务参数:支持在线配置调度任务入参,即时生效;
24、调度线程池:调度系统多线程触发调度运行,确保调度精确执行,不被堵塞;
25、数据加密:调度中心和执行器之间的通讯进行数据加密,提升调度信息安全性;
26、邮件报警:任务失败时支持邮件报警,支持配置多邮件地址群发报警邮件;
27、推送 maven 中央仓库:将会把最新稳定版推送到 maven 中央仓库,方便用户接入和使用;
28、运行报表:支持实时查看运行数据,如任务数量、调度次数、执行器数量等;以及调度报表,如调度日期分布图,调度成功分布图等;
29、全异步:任务调度流程全异步化设计实现,如异步调度、异步运行、异步回调等,有效对密集调度进行流量削峰,理论上支持任意时长任务的运行;
30、跨语言:调度中心与执行器提供语言无关的 RESTful API 服务,第三方任意语言可据此对接调度中心或者实现执行器。除此之外,还提供了 “多任务模式” 和 “httpJobHandler” 等其他跨语言方案;
31、国际化:调度中心支持国际化设置,提供中文、英文两种可选语言,默认为中文;
32、容器化:提供官方 docker 镜像,并实时更新推送 dockerhub,进一步实现产品开箱即用;
33、线程池隔离:调度线程池进行隔离拆分,慢任务自动降级进入 “Slow” 线程池,避免耗尽调度线程,提高系统稳定性;
34、用户管理:支持在线管理系统用户,存在管理员、普通用户两种角色;
35、权限控制:执行器维度进行权限控制,管理员拥有全量权限,普通用户需要分配执行器权限后才允许相关操作;
其实把东西配置好就行
subject-starter/application.yaml
1 2 3 4 5 6 7 8 9 10 11 12 xxl: job: admin: addresses: http://127.0.0.1:8080/xxl-job-admin accessToken: default_token executor: appname: jc-club-subjcet address: ip: 127.0 .0 .1 port: 9999 logpath: /data/applogs/xxl-job/jobhandler logretentiondays: 30
XxlJobConfig.java
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 @Value("${xxl.job.admin.addresses}") private String adminAddresses;@Value("${xxl.job.accessToken}") private String accessToken;@Value("${xxl.job.executor.appname}") private String appname;@Value("${xxl.job.executor.address}") private String address;@Value("${xxl.job.executor.ip}") private String ip;@Value("${xxl.job.executor.port}") private int port;@Value("${xxl.job.executor.logpath}") private String logPath;@Value("${xxl.job.executor.logretentiondays}") private int logRetentionDays;@Bean public XxlJobSpringExecutor xxlJobExecutor () { logger.info(">>>>>>>>>>> xxl-job config init." ); XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor (); xxlJobSpringExecutor.setAdminAddresses(adminAddresses); xxlJobSpringExecutor.setAppname(appname); xxlJobSpringExecutor.setAddress(address); xxlJobSpringExecutor.setIp(ip); xxlJobSpringExecutor.setPort(port); xxlJobSpringExecutor.setAccessToken(accessToken); xxlJobSpringExecutor.setLogPath(logPath); xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays); return xxlJobSpringExecutor; }
SyncLikedJob.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Component @Slf4j public class SyncLikedJob { @Resource private SubjectLikedDomainService subjectLikedDomainService; @XxlJob("syncLikedJobHandler") public void syncLikedJobHandler () throws Exception { XxlJobHelper.log("syncLikedJobHandler.start" ); try { subjectLikedDomainService.syncLiked(); } catch (Exception e) { XxlJobHelper.log("syncLikedJobHandler.error" + e.getMessage()); } } }
SubjectLikedDomainServiceImpl.java
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 @Override public void syncLiked () { Map<Object, Object> subjectLikedMap = redisUtil.getHashAndDelete(SUBJECT_LIKED_KEY); if (log.isInfoEnabled()) { log.info("syncLiked.subjectLikedMap:{}" , JSON.toJSONString(subjectLikedMap)); } if (MapUtils.isEmpty(subjectLikedMap)) { return ; } List<SubjectLiked> subjectLikedList = new LinkedList <>(); subjectLikedMap.forEach((key, val) -> { SubjectLiked subjectLiked = new SubjectLiked (); String[] keyArr = key.toString().split(":" ); String subjectId = keyArr[0 ]; String likedUser = keyArr[1 ]; subjectLiked.setSubjectId(Long.valueOf(subjectId)); subjectLiked.setLikeUserId(likedUser); subjectLiked.setStatus(Integer.valueOf(val.toString())); subjectLiked.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); subjectLikedList.add(subjectLiked); }); subjectLikedService.batchInsertOrUpdate(subjectLikedList); }
SubjectLikedServiceImpl.java
1 2 3 4 @Override public void batchInsertOrUpdate (List<SubjectLiked> subjectLikedList) { this .subjectLikedDao.batchInsertOrUpdate(subjectLikedList); }
RedisUtil.java
定义Map :创建一个新的HashMap
实例,用于存储从Redis中获取的键值对。
使用Cursor遍历哈希表 :通过调用redisTemplate.opsForHash().scan(key, ScanOptions.NONE)
获取一个Cursor
,它可以用来遍历Redis哈希表中的所有条目。ScanOptions.NONE
表示不使用任何扫描选项。
遍历Cursor :使用while
循环遍历Cursor
,直到没有更多的条目。
获取条目 :在循环内部,使用cursor.next()
获取当前的条目,它是一个Map.Entry
对象。
提取键和值 :从Map.Entry
对象中提取键(hashKey
)和值(value
)。
将键值对放入Map :使用map.put(hashKey, value)
将提取的键和值放入之前创建的Map中。
删除哈希表中的条目 :使用redisTemplate.opsForHash().delete(key, hashKey)
从Redis的哈希表中删除当前遍历到的键值对。
返回Map :遍历完成后,返回包含所有键值对的Map。
1 2 3 4 5 6 7 8 9 10 11 12 public Map<Object, Object> getHashAndDelete (String key) { Map<Object, Object> map = new HashMap <>(); Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(key, ScanOptions.NONE); while (cursor.hasNext()) { Map.Entry<Object, Object> entry = cursor.next(); Object hashKey = entry.getKey(); Object value = entry.getValue(); map.put(hashKey, value); redisTemplate.opsForHash().delete(key, hashKey); } return map; }
我的点赞功能开发 SubjectLikedController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @PostMapping("/getSubjectLikedPage") public Result<PageResult<SubjectLikedDTO>> getSubjectLikedPage (@RequestBody SubjectLikedDTO subjectLikedDTO) { try { if (log.isInfoEnabled()) { log.info("SubjectController.getSubjectLikedPage.dto:{}" , JSON.toJSONString(subjectLikedDTO)); } SubjectLikedBO subjectLikedBO = SubjectLikedDTOConverter.INSTANCE.convertDTOToBO(subjectLikedDTO); subjectLikedBO.setPageNo(subjectLikedDTO.getPageNo()); subjectLikedBO.setPageSize(subjectLikedDTO.getPageSize()); PageResult<SubjectLikedBO> boPageResult = subjectLikedDomainService.getSubjectLikedPage(subjectLikedBO); return Result.ok(boPageResult); } catch (Exception e) { log.error("SubjectCategoryController.getSubjectLikedPage.error:{}" , e.getMessage(), e); return Result.fail("分页查询我的点赞失败" ); } }
SubjectLikedDomainServiceImpl.java
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 @Override public PageResult<SubjectLikedBO> getSubjectLikedPage (SubjectLikedBO subjectLikedBO) { PageResult<SubjectLikedBO> pageResult = new PageResult <>(); pageResult.setPageNo(subjectLikedBO.getPageNo()); pageResult.setPageSize(subjectLikedBO.getPageSize()); int start = (subjectLikedBO.getPageNo() - 1 ) * subjectLikedBO.getPageSize(); SubjectLiked subjectLiked = SubjectLikedBOConverter.INSTANCE.convertBOToEntity(subjectLikedBO); subjectLiked.setLikeUserId(LoginUtil.getLoginId()); int count = subjectLikedService.countByCondition(subjectLiked); if (count == 0 ) { return pageResult; } List<SubjectLiked> subjectLikedList = subjectLikedService.queryPage(subjectLiked, start, subjectLikedBO.getPageSize()); List<SubjectLikedBO> subjectInfoBOS = SubjectLikedBOConverter.INSTANCE.convertListInfoToBO(subjectLikedList); subjectInfoBOS.forEach(info -> { SubjectInfo subjectInfo = subjectInfoService.queryById(info.getSubjectId()); info.setSubjectName(subjectInfo.getSubjectName()); }); pageResult.setRecords(subjectInfoBOS); pageResult.setTotal(count); return pageResult; }
SubjectLikedServiceImpl.java
1 2 3 4 5 6 7 8 9 @Override public int countByCondition (SubjectLiked subjectLiked) { return this .subjectLikedDao.countByCondition(subjectLiked); } @Override public List<SubjectLiked> queryPage (SubjectLiked subjectLiked, int start, Integer pageSize) { return this .subjectLikedDao.queryPage(subjectLiked, start, pageSize); }
快速刷题功能开发 SubjectInfoDTO.java
新增字段
1 2 3 4 5 6 7 8 9 private Long nextSubjectId;private Long lastSubjectId;
SubjectInfoDomainService.java
这段代码的主要逻辑是:
根据题目ID查询题目实体。
根据题目类型获取相应的处理器,并使用它查询题目选项信息。
将题目选项和题目信息转换为业务对象BO。
查询题目的标签ID列表,并批量查询标签列表。
提取标签名称并设置到业务对象BO中。
设置是否已点赞的状态和点赞数量。
组装题目的上下文信息,包括上一个和下一个题目的ID。
返回封装好的业务对象BO。
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 @Override public SubjectInfoBO querySubjectInfo (SubjectInfoBO subjectInfoBO) { SubjectInfo subjectInfo = subjectInfoService.queryById(subjectInfoBO.getId()); SubjectTypeHandler handler = subjectTypeHandlerFactory.getHandler(subjectInfo.getSubjectType()); SubjectOptionBO optionBO = handler.query(subjectInfo.getId().intValue()); SubjectInfoBO bo = SubjectInfoConverter.INSTANCE.convertOptionAndInfoToBo(optionBO, subjectInfo); SubjectMapping subjectMapping = new SubjectMapping (); subjectMapping.setSubjectId(subjectInfo.getId()); subjectMapping.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); List<SubjectMapping> mappingList = subjectMappingService.queryLabelId(subjectMapping); List<Long> labelIdList = mappingList.stream().map(SubjectMapping::getLabelId).collect(Collectors.toList()); List<SubjectLabel> labelList = subjectLabelService.batchQueryById(labelIdList); List<String> labelNameList = labelList.stream().map(SubjectLabel::getLabelName).collect(Collectors.toList()); bo.setLabelName(labelNameList); bo.setLiked(subjectLikedDomainService.isLiked(subjectInfoBO.getId().toString(), LoginUtil.getLoginId())); bo.setLikedCount(subjectLikedDomainService.getLikedCount(subjectInfoBO.getId().toString())); assembleSubjectCursor(subjectInfoBO, bo); return bo; } private void assembleSubjectCursor (SubjectInfoBO subjectInfoBO, SubjectInfoBO bo) { Long categoryId = subjectInfoBO.getCategoryId(); Long labelId = subjectInfoBO.getLabelId(); Long subjectId = subjectInfoBO.getId(); if (Objects.isNull(categoryId) || Objects.isNull(labelId)) { return ; } Long nextSubjectId = subjectInfoService.querySubjectIdCursor(subjectId, categoryId, labelId, 1 ); bo.setNextSubjectId(nextSubjectId); Long lastSubjectId = subjectInfoService.querySubjectIdCursor(subjectId, categoryId, labelId, 0 ); bo.setLastSubjectId(lastSubjectId); }
SubjectInfoDao.xml
querySubjectIdCursor
对应的sql语句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <select id ="querySubjectIdCursor" resultType ="java.lang.Long" > select a.id from subject_info a, subject_mapping b where a.id = b.subject_id and b.category_id = #{categoryId} and b.label_id = #{labelId} <if test ="cursor !=null and cursor == 1" > and a.id > #{subjectId} </if > <if test ="cursor !=null and cursor == 0" > and a.id < #{subjectId} </if > limit 0,1 </select >