第五章
优学题库项目开发3
优就业.JAVA教研室
学习目标
- 掌握OSS云存储、账号开通、上传测试
- 掌握图片上传到OSS云存储
- 掌握开发广告内容前端维护功能
- 掌握开发用户管理前端维护功能
- 掌握微信小程序登录及验证调用接口
- 掌握开发微信小程序分类、题库、广告轮播图调用接口
- 了解接口文档生成方式
1、OSS云存储介绍,开通、上传测试
文件上传在系统中用的很频繁,所以我们需要将上传的文件进行存储,传统的将文件上传到本机已不适用分布式系统。自己搭建文件服务器有复杂性和维护成本。所以我们可以采用市面上成熟的文件存储服务,如阿里云的OSS对象存储服务。
1.1 登录阿里云开通OSS存储
产品->精选->对象存储OSS 立即开通 需要实名认证后才能开通
1.2、创建Bucket
点击创建Bucket按钮,弹出创建Bucket界面
特别强调这里的读写权限一定设置为“公共的”。
新建文件夹[pic,video],点击目录可以上传图片,并可以访问
1.3、accesskey申请
管理控制台,右上角,点击用户头像,弹出菜单:
选择:AccessKey管理,弹出提示,选择: 开始使用子用户 AccessKey
点击: 开始使用子用户AccessKey
出现创建用户界面:输入登录用户名、显示用户名,勾选 编程访问,点击 确定即可创建用户。
l 选择左边菜单的用户连接,进入用户列表
l 选择用户,点击左下角的 添加权限按钮,即可进入设置权限页面
点击确定,出现授权成功提示
l 选择用户,设置用户AccessKey
点击用户列表,中的用户名,进入用户设置界面
l 点击创建AccessKey,弹出Accesskey创建成功页面,复制并保存好相关秘钥
1.4、引入sdk
https://help.aliyun.com/document_detail/32009.html
SDK依赖:
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.8.0</version>
</dependency>
1.5、java编程方式上传文件到oss
package com.offcn.utils;
public class OSSTest {
/**
* OSS 使用步骤 阿里云
* 1)、引入SDK
* 2)、配置好相应的属性
*/
public static void main(String[] args)throws IOException {
// Endpoint以北京为例,其它Region请按实际情况填写。
String endpoint = "http://oss-cn-beijing.aliyuncs.com";
// 云账号AccessKey有所有API访问权限,建议遵循阿里云安全最佳实践,创建并使用RAM子账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建。
String accessKeyId = "LTAI4F***33p318BifWc";
String accessKeySecret = "epNR5QdCC44***bwW4FW8P3UEW";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 上传文件流。
InputStream inputStream = new FileInputStream(new File("D:\\007.jpg"));
//要存放的存储桶名称,存储文件名 ,文件输入流
ossClient.putObject("offcn20200330", "pic/008.jpg", inputStream);
// 关闭OSSClient。
ossClient.shutdown();
System.out.println("测试完成");
}
}
执行上传程序成功,查看oss服务器端
访问图片地址:
Bucket+域名 + 图片路径 + 文件名
https://offcn20200330.oss-cn-beijing.aliyuncs.com/pic/008.jpg
**
2、实现图片上传到oss云存储
2.1、上传原理
2.1.1、通过应用服务器中转上传
每个 OSS 的用户都会用到上传服务。Web 端常见的上传方法是用户在浏览器或 APP 端上传文件到应用服务器,应用服务器再把文件上传到 OSS。具体流程如下图所示。
和数据直传到 OSS 相比,以上方法有三个缺点:
上传慢:用户数据需先上传到应用服务器,之后再上传到OSS。网络传输时间比直传到OSS多一倍。如果用户数据不通过应用服务器中转,而是直传到OSS,速度将大大提升。而且OSS采用BGP带宽,能保证各地各运营商之间的传输速度。 扩展性差:如果后续用户多了,应用服务器会成为瓶颈。 费用高:需要准备多台应用服务器。由于OSS上传流量是免费的,如果数据直传到OSS,不通过应用服务器,可以节省应用服务器
2.1.2、直接javaScript客户端上传到oss
采用JavaScript客户端直接签名(参见JavaScript客户端签名直传)时,AccessKeyID和AcessKeySecret会暴露在前端页面,因此存在严重的安全隐患。因此,OSS提供了服务端签名后直传的方案。
服务器端获取签名,客户端直接上传到oss原理
服务端签名后直传的原理如下:
-
用户发送上传Policy请求到应用服务器。
-
应用服务器返回上传Policy和签名给用户。
-
用户直接上传数据到OSS。
参考官方文档:https://help.aliyun.com/document_detail/31926.htm
2.2、搭建获取oss签名微服务
2.2.1、创建新模块u-oss(标准springboot工程)
最终创建好模块
2.2.2、pom.xml引入依赖
<?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>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>u-oss</artifactId>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.0.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
2.2.3、配置文件application.yml
server:
port: 14000
spring:
application:
name: u-oss
cloud:
nacos:
discovery:
server-addr: localhost:8848
alicloud:
access-key: LTAI4G7***xizdRu
secret-key: aSHoxLMB&&&&GFSeb5nsLmJIYTwf
oss:
endpoint: oss-cn-beijing.aliyuncs.com
bucket: java0817-001
logging:
level:
com.offcn.oss: debug
14.2.4、编写获取oss服务器签名OssController
package com.offcn.oss.controller;
import com.aliyun.oss.OSS;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.MatchMode;
import com.aliyun.oss.model.PolicyConditions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
@RestController
@RequestMapping("/thirdparty/v1/admin/oss")
public class OssController {
@Autowired
OSS ossClient;
@Value("${spring.cloud.alicloud.access-key}")
private String accessId;
@Value("${spring.cloud.alicloud.oss.endpoint}")
private String endpoint;
@Value("${spring.cloud.alicloud.oss.bucket}")
private String bucket;
@RequestMapping("/getPolicy")
public Map<String, String> getPolicy() {
String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
String formatDate = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
String dir = formatDate + "/"; // 用户上传文件时指定的前缀。
Map<String, String> respMap = new LinkedHashMap<String, String>();
try {
long expireTime = 30;
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expiration = new Date(expireEndTime);
PolicyConditions policyConds = new PolicyConditions();
//PostObject请求最大可支持的文件大小为5 GB,即CONTENT_LENGTH_RANGE为5*1024*1024*1024。
policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
//获取授权策略
String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
byte[] binaryData = postPolicy.getBytes("utf-8");
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
//获取授权策略签名
String postSignature = ossClient.calculatePostSignature(postPolicy);
//封装accessid、签名等信息返回
respMap.put("accessid", accessId);
respMap.put("policy", encodedPolicy);
respMap.put("signature", postSignature);
respMap.put("dir", dir);
respMap.put("host", host);
respMap.put("expire", String.valueOf(expireEndTime / 1000));
} catch (Exception e) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
} finally {
ossClient.shutdown();
}
return respMap;
}
}
2.2.4、编写主启动类
@SpringBootApplication
@EnableDiscoveryClient
public class OssStartApplication {
public static void main(String[] args) {
SpringApplication.run(OssStartApplication.class,args);
}
}
2.2.5、启动服务,访问controller获取签名
访问地址:http://localhost:14000/thirdparty/v1/admin/oss/getPolicy
2.3、修改网关配置获取签名路由
打开java服务cloud-gateway,修改配置文件application.yml
增加到获取签名微服务的路由,注意配置顺序要在renren-fast 之前。
- id: oss-route # oss服务路由
uri: lb://u-oss
predicates:
- Path=/api/thirdparty/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
http://localhost:8888/api/thirdparty/v1/admin/oss/getPolicy
2.4、开发前端工程,配置上传
2.4.1、新建获取签名的js
在前端工程renren-fast-vue目录src/utils/ 新建获取签名js文件 policy.js
import http from '@/utils/httpRequest.js'
export function policy () {
return new Promise((resolve) => {
http({
url: http.adornUrl('/thirdparty/v1/admin/oss/getPolicy'),
method: 'get',
params: http.adornParams({})
}).then(({ data }) => {
resolve(data)
})
})
}
2.4.2、新建文件上传控件页面
在前端工程renren-fast-vue目录src/views/modules/common目录下 创建上传页面 singleUpload.vue
注意:**修改下面 action属性 ** 指向oss存储桶地址:http://java0817-001.oss-cn-guangzhou.aliyuncs.com!
<template>
<div>
<el-upload
action="http://java0817-001.oss-cn-guangzhou.aliyuncs.com"
:data="dataObj"
list-type="picture"
:multiple="false" :show-file-list="showFileList"
:auto-upload="true"
:file-list="fileList"
:before-upload="beforeUpload"
:on-remove="handleRemove"
:on-success="handleUploadSuccess"
:on-preview="handlePreview">
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过10MB</div>
</el-upload>
<el-dialog :visible.sync="dialogVisible">
<img width="100%" :src="fileList[0].url" alt="">
</el-dialog>
</div>
</template>
<script>
import {policy} from '@/utils/policy'
import { getUUID } from '@/utils'
export default {
name: 'singleUpload',
props: {
value: String
},
computed: {
imageUrl() {
return this.value;
},
imageName() {
if (this.value != null && this.value !== '') {
return this.value.substr(this.value.lastIndexOf("/") + 1);
} else {
return null;
}
},
fileList() {
return [{
name: this.imageName,
url: this.imageUrl
}]
},
showFileList: {
get: function () {
return this.value !== null && this.value !== ''&& this.value!==undefined;
},
set: function (newValue) {
}
}
},
data() {
return {
dataObj: {
policy: '',
signature: '',
key: '',
ossaccessKeyId: '',
dir: '',
host: '',
// callback:'',
},
dialogVisible: false
};
},
methods: {
emitInput(val) {
this.$emit('input', val)
},
handleRemove(file, fileList) {
this.emitInput('');
},
handlePreview(file) {
this.dialogVisible = true;
},
beforeUpload(file) {
let _self = this;
return new Promise((resolve, reject) => {
policy().then(response => {
// console.log(response)
_self.dataObj.policy = response.policy;
_self.dataObj.signature = response.signature;
_self.dataObj.ossaccessKeyId = response.accessid;
_self.dataObj.key = response.dir + getUUID()+'_${filename}';
_self.dataObj.dir = response.dir;
_self.dataObj.host = response.host;
resolve(true)
}).catch(err => {
console.log(err)
reject(false)
})
})
},
handleUploadSuccess(res, file) {
console.log("上传成功...")
this.showFileList = true;
this.fileList.pop();
this.fileList.push({name: file.name, url: this.dataObj.host + '/' + this.dataObj.key.replace("${filename}",file.name) });
this.emitInput(this.fileList[0].url);
}
}
}
</script>
<style>
</style>
2.4.3、修改题目分类新增页面,增加上传控件
在前端工程renren-fast-vue目录src/views/modules/question目录下 修改页面 type-add-or-update.vue
引入上传控件
import SingleUpload from "../common/singleUpload" // 引入单文件上传组件
注册上传组件:
export default {
components:{ SingleUpload }
},
修改分类logo输入框,改成上传按钮
<el-form-item label="类型logo路径" prop="logoUrl">
<single-upload v-model="dataForm.logoUrl"></single-upload>
</el-form-item>
2.4.4、测试上传
点击上传 按钮,选择要上传的图片
上传图片成功:
注意:oss服务器存储桶一定要设置允许跨域
配置跨域访问
- 登录OSS管理控制台。
- 单击Bucket列表,之后单击目标Bucket名称。
- 单击权限管理 > 跨域设置,在跨域设置区域单击设置。
- 单击创建规则,配置如下图所示。
3、开发广告内容前端维护管理
3.1、添加广告内容管理目录(一级菜单)
选择管理界面–系统管理–菜单管理,点击 新增按钮
3.2、添加广告轮播图管理菜单(二级菜单)
3.3、添加广告资讯管理菜单(二级菜单)
3.4、生成广告前端页面
用renren-generator自动生成前端代码
注意配置数据库连接文件application.yml,指向数据库 uxue_cms
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
#MySQL配置
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/uxue_cms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
修改模板生成属性配置文件generator.properties
mainPath=com.offcn
#\u5305\u540D
package=com.offcn
moduleName=context
#\u4F5C\u8005
author=sunny
#Email
email=hk109@126.com
#\u8868\u524D\u7F00(\u7C7B\u540D\u4E0D\u4F1A\u5305\u542B\u8868\u524D\u7F00)
tablePrefix=cms_
启动renren-generator
3.5、拷贝前端代码到 前端工程renren-fast-vue
拷贝\main\resources\src\views\modules\context目录到前端目录 renren-fast-vue\src\views\modules
3.6、配置网关转发
找到cloud-gateway,修改配置文件application.yml
- id: cms-route # 广告微服务路由
uri: lb://u-context
predicates:
- Path=/api/context/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
3.7、测试广告轮播图、广告资讯基本管理
刷新页面,点击菜单 广告内容管理—》广告轮播图
点击菜单 广告内容管理—》广告资讯
分别测试增删改
注意测试新增前,先把日期时间字段改成不是必须输入
修改banner-add-or-update.vue、news-add-or-update.vue
createTime: [
{ required: false, message: '创建时间不能为空', trigger: 'blur' }
],
updateTime: [
{ required: false, message: '更新时间不能为空', trigger: 'blur' }
]
3.8、修改广告轮播图广告配图上传
在前端工程renren-fast-vue目录src/views/modules/context目录下 修改页面 banner-add-or-update.vue
引入上传控件
import SingleUpload from "../common/singleUpload" // 引入单文件上传组件
注册上传组件:
export default {
components:{ SingleUpload }
}
修改图片路径输入框,改成上传按钮
<el-form-item label="图片路径" prop="imgUrl">
<single-upload v-model="dataForm.imgUrl"></single-upload>
</el-form-item>
实际运行效果:
3.9、修改广告资讯广告配图上传
在前端工程renren-fast-vue目录src/views/modules/context目录下 修改页面 news-add-or-update.vue
具体修改方式和广告轮播相同
4、开发用户管理前端维护管理
4.1、添加用户管理目录(一级菜单)
选择管理界面–系统管理–菜单管理,点击 新增按钮
4.2、添加用户列表管理菜单(二级菜单)
4.3、添加用户充值记录管理菜单(二级菜单)
4.4、生成用户服务相关前端页面
用renren-generator自动生成前端代码
注意配置数据库连接文件application.yml,指向数据库 uxue_cms
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
#MySQL配置
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/uxue_ums?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
修改模板生成属性配置文件generator.properties
mainPath=com.offcn
#\u5305\u540D
package=com.offcn
moduleName=member
#\u4F5C\u8005
author=sunny
#Email
email=hk109@126.com
#\u8868\u524D\u7F00(\u7C7B\u540D\u4E0D\u4F1A\u5305\u542B\u8868\u524D\u7F00)
tablePrefix=ums_
启动renren-generator
4.5、拷贝前端代码到 前端工程renren-fast-vue
拷贝\main\resources\src\views\modules\member目录到前端目录 renren-fast-vue\src\views\modules
4.6、配置网关转发
找到cloud-gateway,修改配置文件application.yml
spring:
cloud:
nacos:
discovery:
server-addr: http://localhost:8848
gateway:
routes:
- id: ums-route # 用户微服务路由
uri: lb://u-member
predicates:
- Path=/api/member/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
4.7、测试用户列表、充值记录基本管理
刷新页面,点击菜单用户管理—》用户列表
点击菜单用户管理—》用户充值记录
分别测试增删改
注意测试新增前,先把日期时间字段改成不是必须输入
修改member-add-or-update.vue、growthchangehistory-add-or-update.vue
createTime: [
{ required: false, message: '创建时间不能为空', trigger: 'blur' }
],
updateTime: [
{ required: false, message: '更新时间不能为空', trigger: 'blur' }
]
4.8.用户注册数量统计分析
4.8.1 编写java后端代码
打开java模块u-member ,编辑接口MemberService新增统计注册数量方法
public interface MemberService extends IService<MemberEntity> {
public List<Map<String, Object>> countByDateTime(String beginTime, String endTime);
}
编辑接口实现类MemberServiceImpl,编写统计注册数量实现方法
@Autowired
private MemberDao memberDao;
@Override
public List<Map<String, Object>> countByDateTime(String beginTime, String endTime) {
QueryWrapper<MemberEntity> queryWrapper = new QueryWrapper<MemberEntity>().select("DATE_FORMAT(create_time,'%Y-%m-%d') AS NAME ,COUNT(id) AS VALUE").between("create_time", beginTime+" 00:00:00", endTime+" 23:59:59").groupBy("NAME");
List<Map<String, Object>> mapList = memberDao.selectMaps(queryWrapper);
return mapList;
}
编辑MemberController,新增统计注册数量方法
//根据开始时间,结束时间,统计每日注册账号
@RequestMapping("countAccountCreate")
public R countAccountCreate(String beginTime,String endTime){
List<Map<String, Object>> mapList = memberService.countByDateTime(beginTime, endTime);
return R.ok().put("mapList",mapList);
}
4.8.2 编写前端代码
前端项目renren-fast-vue/src/views/modules/member目录 新增统计注册用户数量页面 echarts.vue
<template>
<div class="mod-demo-echarts">
<!--表单-->
<el-form :inline="true" class="demo-form-inline">
<el-form-item label="开始日期">
<el-date-picker
v-model="beginTime"
type="date"
placeholder="选择要统计的开始日期"
value-format="yyyy-MM-dd" />
</el-form-item>
<el-form-item label="结束日期">
<el-date-picker
v-model="endTime"
type="date"
placeholder="选择要统计的结束日期"
value-format="yyyy-MM-dd" />
</el-form-item>
<el-button
:disabled="btnDisabled"
type="primary"
@click="create()">生成</el-button>
</el-form>
<el-row :gutter="20">
<el-col :span="24">
<el-card>
<div id="J_chartBarBox" class="chart-box"></div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
import echarts from 'echarts'
export default {
data () {
return {
chartBar: null,
beginTime: '',
endTime: '',
mapList: [],
xData: [],
yData: [],
btnDisabled: false
}
},
mounted () {
},
activated () {
// 由于给echart添加了resize事件, 在组件激活时需要重新resize绘画一次, 否则出现空白bug
if (this.chartBar) {
this.chartBar.resize()
}
},
methods: {
create () {
this. getDataList ()
this.initChartBar()
},
// 获取数据列表
getDataList () {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/member/member/countAccountCreate'),
method: 'get',
params: this.$http.adornParams({
'beginTime': this.beginTime,
'endTime': this.endTime
})
}).then(({data}) => {
if (data && data.code === 0) {
this.mapList = data.mapList
this.xData = []
this.yData = []
for(let j in this.mapList){
this.xData.push(this.mapList[j].NAME)
this.yData.push(this.mapList[j].VALUE)
}
console.log(this.xData)
console.log(this.yData)
this.chartBar.resize()
// this.xData= this.mapList.map(obj => n.NAME);
// this.yData= this.mapList.map(obj => n.VALUE);
this.chartBar.setOption({
xAxis: [
{
type: 'category',
data: this.xData
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
name: '每日注册用户数',
type: 'line',
data: this.yData
}
]
})
} else {
this.mapList = []
}
})
},
// 柱状图
initChartBar () {
var option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: ['每日注册用户数']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: [
{
type: 'category',
data: this.xData
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
name: '每日注册用户数',
type: 'bar',
data: this.yData
}
]
}
this.chartBar = echarts.init(document.getElementById('J_chartBarBox'))
// this.chartBar.setOption(option)
window.addEventListener('resize', () => {
this.chartBar.resize()
})
}
}
}
</script>
<style lang="scss">
.mod-demo-echarts {
> .el-alert {
margin-bottom: 10px;
}
> .el-row {
margin-top: -10px;
margin-bottom: -10px;
.el-col {
padding-top: 10px;
padding-bottom: 10px;
}
}
.chart-box {
min-height: 400px;
}
}
</style>
4.8.3 配置用户注册统计菜单
新增菜单后,刷新,即可查看菜单
选中开始日期,结束日期,点击 生成按钮 出现统计折线图
5、开发微信小程序登录及验证调用接口
5.1.编写JWT工具类
5.1.1 打开模块u-common,编辑pom.xml
引入jwt依赖库
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
5.1.2 JWT工具类 JWTUtil
package com.offcn.common.utils;
import io.jsonwebtoken.*;
import java.util.Date;
public class JWTUtil {
private static final String SECRET_KEY = "u-member-offcn123"; //秘钥
public static final long TOKEN_EXPIRE_TIME = 5 * 60 * 1000; //token过期时间
public static final long REFRESH_TOKEN_EXPIRE_TIME = 10 * 60 * 1000; //refreshToken过期时间
private static final String ISSUER = "offcn"; //签发人
//生成签名
public static String generateToken(String username){
Date now = new Date();
return Jwts.builder()
.setIssuer(ISSUER) //签发人
.setIssuedAt(now)//签发时间
.setExpiration(new Date(now.getTime()+TOKEN_EXPIRE_TIME))//过期时间
.claim("username",username)//用户名
.signWith(SignatureAlgorithm.HS256,SECRET_KEY)//签名算法、秘钥
.compact();
}
//解析token
private static Claims parseToken(String token){
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token).getBody();
}
//验证签名
public static boolean verify(String token){
//尝试解析token
try {
Claims claims = parseToken(token);
return true;
} catch (Exception e) {
// e.printStackTrace();
System.out.println("签名验证失败");
return false;
}
}
//从token获取用户名
public static String getUsername(String token){
try {
Claims claims = parseToken(token);
String username = (String) claims.get("username");
return username;
} catch (Exception e) {
// e.printStackTrace();
System.out.println("解析令牌失败");
return "";
}
}
}
5.2.编写用户登录接口
打开模块u-member,MemberService增加登录接口
public interface MemberService extends IService<MemberEntity> {
public MemberEntity login(String username,String password);
}
打开模块u-member,MemberServiceImpl增加登录实现方法
@Override
public MemberEntity login(String username, String password) {
MemberEntity memberEntity = this.getOne(new QueryWrapper<MemberEntity>().eq("user_name", username).eq("PASSWORD", password));
return memberEntity;
}
5.3.编写用户登录、刷新token控制器
u-member添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在memberController
@Autowired
private StringRedisTemplate stringRedisTemplate;
//登录
@PostMapping("/login")
public R login(String username,String password){
MemberEntity memberEntity = memberService.login(username, password);
if(memberEntity!=null) {
String token = JWTUtil.generateToken(memberEntity.getUserName());
//生成refreshToken
String refreshToken = UUID.randomUUID().toString().replace("-", "");
stringRedisTemplate.opsForHash().put(refreshToken, "token", token);
stringRedisTemplate.opsForHash().put(refreshToken, "username", username);
//设置token的过期时间
stringRedisTemplate.expire(refreshToken, JWTUtil.REFRESH_TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS);
return R.ok().put("token",token).put("refreshToken",refreshToken);
}else {
return R.error("账号密码错误");
}
}
//刷新token
@PostMapping("/refreshtoken")
public R refreshToken(String refreshToken){
//根据refreshToken从redis读取用户名
String username= (String) stringRedisTemplate.boundHashOps(refreshToken).get("username");
if(StringUtils.isEmpty(username)){
return R.error("刷新token失败");
}
//根据用户名生成新的token
String token = JWTUtil.generateToken(username);
//更新token到redis
stringRedisTemplate.boundHashOps(refreshToken).put("token",token);
//设置token的过期时间
stringRedisTemplate.expire(refreshToken, JWTUtil.REFRESH_TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS);
return R.ok().put("token",token).put("refreshToken",refreshToken);
}
redis:
host: localhost
port: 6379
5.4.测试用户登录、刷新token
登录地址:http://localhost:10002/member/member/login
传递参数: username 用户账号 password 密码
刷新token地址:http://localhost:10002/member/member/refreshtoken
传递参数:refreshToken
返回结果:
5.5、配置网关路由转发
修改cloud-gateway模块,配置文件application.yml增加登录路由转发
- id: weixin-login-ums-route # 提供微信客户端调用的,用户微服务路由
uri: lb://u-member
predicates:
- Path=/member/member/login/**
- id: weixin-register-ums-route # 提供微信客户端调用的,用户微服务路由
uri: lb://u-member
predicates:
- Path=/member/member/save/**
测试网关访问地址:
http://localhost:8888/member/member/login?username=test&password=123
响应:
5.6、用户注册接口
http://localhost:8888/member/member/save
请求参数:
{
"miniOpenid": "1001",
"mpOpenid": "8877111",
"unionid": "test001",
"levelId": 1,
"userName": "test003",
"password": "456",
"nickname": "NickName",
"phone": "17562128765",
"email": "hk109@139.com",
"avatar": "1.jpg",
"gender": 0,
"city": "北京",
"birth": "1978-09-11",
"sourceType": 1,
"integration": 1
}
5.7、配置网关过滤器,拦截微信客户端请求验证token
5.7.1、修改pom.xml引入common依赖,排除不需要的mybatis-plus-boot-starter依赖
<dependency>
<groupId>com.offcn</groupId>
<artifactId>u-common</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<artifactId>mybatis-plus-boot-starter</artifactId>
<groupId>com.baomidou</groupId>
</exclusion>
</exclusions>
</dependency>
5.7.2、编写网关过滤器类
编写自定义过滤器实现类:InnerFilter
package com.offcn.filter;
import com.offcn.common.utils.JWTUtil;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public class InnerFilter implements GatewayFilter, Ordered {
private JwtCheckGatewayFilterFactory.Config config;
InnerFilter(JwtCheckGatewayFilterFactory.Config config){
this.config=config;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//尝试从请求头获取令牌
String token = request.getHeaders().getFirst("Authorization");
//判断令牌如果不为空
if(!StringUtils.isEmpty(token)){
//调用令牌工具类,校验令牌
boolean verify = JWTUtil.verify(token);
//判断令牌校验失败
if(!verify){
//令牌校验失败,结束转发
Mono mono = returnFail(response);
return response.writeWith(mono);
}else {
//令牌校验成功,放行
return chain.filter(exchange);
}
}else {
//令牌不存在,结束转发
Mono mono = returnFail(response);
return response.writeWith(mono);
}
}
//返回失败方法
private Mono returnFail(ServerHttpResponse response){
//获取响应对象头
HttpHeaders headers = response.getHeaders();
//设置响应头内容Content-Type 响应内容类型
headers.add("Content-Type","application/json; charset=UTF-8");
//禁止缓存
headers.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
//设置响应内容
String body="token验证失败";
DataBuffer dataBuffer = response.bufferFactory().wrap(body.getBytes());
//把内容写入到响应对象
return Mono.just(dataBuffer);
}
@Override
public int getOrder() {
return 1;
}
}
编写自定义网关过滤器
package com.offcn.filter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
public class JwtCheckGatewayFilterFactory extends AbstractGatewayFilterFactory<JwtCheckGatewayFilterFactory.Config> {
//要向父类传递配置类,要不然就会报类型转换错误
public JwtCheckGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return new InnerFilter(config);
}
//定义一个内部类,配置过滤器属性,必须是静态的
public static class Config{
}
}
5.7.3、配置网关过滤器
package com.offcn.config;
import com.offcn.filter.JwtCheckGatewayFilterFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
@Configuration
public class GatewayCorsConfiguration {
@Bean
public JwtCheckGatewayFilterFactory jwtCheckGatewayFilterFactory(){
return new JwtCheckGatewayFilterFactory();
}
}
5.7.4、修改网关配置
spring:
cloud:
nacos:
discovery:
server-addr: http://localhost:8848
gateway:
routes:
- id: weixin-login-ums-route # 提供微信客户端调用的,用户微服务路由
uri: lb://u-member
predicates:
- Path=/member/member/login/**
filters:
- RewritePath=/(?<segment>.*),/$\{segment}
- id: weixin-test-ums-route # 提供微信客户端调用的,用户微服务路由
uri: lb://u-member
predicates:
- Path=/member/member/refreshtoken/**
filters:
- RewritePath=/(?<segment>.*),/$\{segment}
- JwtCheck
刷新token接口,启用token校验过滤器,填上过滤器前缀就可以 JwtCheck
5.7.5、测试
访问登录接口地址:http://localhost:8888/member/member/login?username=test&password=123
不需要token验证。
访问刷新token接口地址:http://localhost:8888/member/member/refreshtoken?refreshToken=4257e35399db461ca0bfac3b6b3a61f9
在请求头不带参数,提示toekn验证失败
在请求头带上参数 Authorization
6、开发微信小程序分类、及题库、广告轮播图读取调用接口
6.1.获取题库分类接口
6.1.1、修改网关路由配置添加获取题库分类接口路由
修改application.yml
spring:
cloud:
nacos:
discovery:
server-addr: http://localhost:8848
gateway:
routes:
- id: weixin-question-route # 提供微信客户端调用的,题库微服务路由
uri: lb://u-question
predicates:
- Path=/question/**
filters:
- RewritePath=/(?<segment>.*),/$\{segment}
- JwtCheck
访问路径:http://localhost:8888/question/type/findall
6.2.获取题库数据接口
6.2.1.获取根据分类获取题库列表数据接口
访问路径:http://localhost:8888/question/question/list
可以传递参数:key 等于题库分类编号
http://localhost:8888/question/question/list?key=6
6.3.获取根据实体编号获取题目详情数据接口
http://localhost:8888/question/question/info/50
6.4.获取广告轮播图数据接口
6.4.1.获取广告轮播图列表数据接口
- id: weixin-context-route # 提供微信客户端调用的,用户微服务路由
uri: lb://u-context
predicates:
- Path=/context/**
filters:
- RewritePath=/(?<segment>.*),/$\{segment}
- JwtCheck
访问路径:http://localhost:8888/context/banner/list?page=1&limit=5
7、接口文档
7.1、用户注册
<a id=用户注册> </a>
基本信息
Path: /member/member/save
Method: POST
接口描述:
Request
Headers
参数名称 | 参数值 | 是否必须 | 示例 | 备注 |
---|---|---|---|---|
Content-Type | application/json | 是 |
Body
{
"miniOpenid": "1001", //小程序openid
"mpOpenid": "8877111", //服务号openid
"unionid": "test001", //微信unionid
"levelId": 1, //会员等级id 整数
"userName": "test003", //用户登录账号
"password": "456", //用户密码
"nickname": "NickName",//用户昵称
"phone": "17562128765", //用户手机号
"email": "hk109@139.com",//用户邮箱
"avatar": "1.jpg",//用户头像
"gender": 0, //性别 0 女 1男
"city": "北京", //用户地址
"birth": "1978-09-11",//用户生日
"sourceType": 1,//来源 1 微信 2 app
"integration": 1 //积分,初始化注册积分为1
}
Reponse
{
"msg": "success", //操作结果
"code": 0 //状态码 0 成功
}
7.2、用户登录
<a id=用户登录> </a>
基本信息
Path: /member/member/login
Method: GET
接口描述:
Request
Query
参数名称 | 是否必须 | 示例 | 备注 |
---|---|---|---|
username | 是 | test001 | 用户账号 |
password | 是 | 123 | 密码 |
Reponse
{
"msg": "success",//响应成功消息
"code": 0,//响应状态码 0 成功
//token ,注意后续访问 获取分类、题库数据接口 需要鉴权 请求头里面带 token
//请求头参数 Authorization token值
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJvZmZjbiIsImV4cCI6MTYwODY1MDMyMywiaWF0IjoxNjA4NjM4MzIzLCJ1c2VybmFtZSI6InRlc3QifQ.1JV-O3o7IXOuTnjdzZjHKm5FiIHrtviqy2CXEY4DZB4",
//token失效,刷新token使用
"refreshToken": "2bd89f56c6ba4befae70fc7399edda44"
}
7.3、刷新token
<a id=刷新token> </a>
基本信息
Path:/member/member/refreshtoken
Method: POST
接口描述:
Request
Headers
参数名称 | 参数值 | 是否必须 | 示例 | 备注 |
---|---|---|---|---|
Content-Type | application/x-www-form-urlencoded | 是 |
Query
参数名称 | 是否必须 | 示例 | 备注 |
---|---|---|---|
refreshToken | 是 | refreshToken 登录成功后有返回 |
Body
参数名称 | 参数类型 | 是否必须 | 示例 | 备注 |
---|---|---|---|---|
refreshToken | text | 是 | refreshToken登录成功后有返回 |
Reponse
{
"msg": "success",//消息
"code": 0,//状态码 0 成功
//最新的token
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJvZmZjbiIsImV4cCI6MTYwODY1MDY4MywiaWF0IjoxNjA4NjM4NjgzLCJ1c2VybmFtZSI6InRlc3QifQ.G_0WcKRnWAZcL4sI-9KO3H-Sj1wJGzNUAmxQeGzmTdQ",
//刷新token的值
"refreshToken": "9332b3bfd3ce46e590ae0720579b8bc1"
}
7.4、获取广告轮播图
<a id=获取广告轮播图> </a>
基本信息
Path: /context/banner/list
Method: GET
接口描述:
Request
Headers
参数名称 | 参数值 | 是否必须 | 示例 | 备注 |
---|---|---|---|---|
Authorization | 是 | 必须 | 登录获取到token |
Query
参数名称 | 是否必须 | 示例 | 备注 |
---|---|---|---|
page | 是 | 1 | 页码 |
limit | 是 | 5 | 轮播图数量 |
Reponse
{
"msg": "success",
"code": 0,
"page": {
"totalCount": 0,
"pageSize": 5,
"totalPage": 0,
"currPage": 1,
"list": [
{
"id": 1,
"imgUrl": "https://java0817-001.oss-cn-guangzhou.aliyuncs.com/2021-01-06/1533d9e1-d221-4c48-8804-118f32fdd173_6.png",
"title": "github",
"displayOrder": 1,
"enable": 1,
"renderType": 1,
"renderUrl": "http://www.ujiuye.com",
"delFlag": 0,
"createTime": "2021-01-06T04:30:29.000+00:00",
"updateTime": "2021-01-06T04:30:29.000+00:00"
},
{
"id": 2,
"imgUrl": "https://java0817-001.oss-cn-guangzhou.aliyuncs.com/2021-01-06/cb1fa277-1fce-4a1e-882d-a3c3cce551f2_7.png",
"title": "9",
"displayOrder": 2,
"enable": 1,
"renderType": 1,
"renderUrl": "http://www.baidu.com",
"delFlag": 0,
"createTime": "2021-01-06T04:38:04.000+00:00",
"updateTime": "2021-01-06T04:38:04.000+00:00"
},
{
"id": 3,
"imgUrl": "https://java0817-001.oss-cn-guangzhou.aliyuncs.com/2021-01-06/21cf3022-14f1-4a65-8889-acc196d84239_2.png",
"title": "222",
"displayOrder": 3,
"enable": 1,
"renderType": 1,
"renderUrl": "http://www.offcn.com",
"delFlag": 0,
"createTime": "2021-01-06T04:38:37.000+00:00",
"updateTime": "2021-01-06T04:38:37.000+00:00"
}
]
}
}
7.5、获取试题分类
<a id=获取试题分类> </a>
基本信息
Path: /api/question/type/findall
Method: GET
接口描述:
Request
Headers
参数名称 | 参数值 | 是否必须 | 示例 | 备注 |
---|---|---|---|---|
Authorization | 是 | 必须 | 登录成功获取到的token |
Reponse
{
"msg": "success",
"code": 0,
"data": [
{
"id": 6,
"type": "JAVA基础",
"comments": "JAVA基础",
"logoUrl": "https://java0817-001.oss-cn-guangzhou.aliyuncs.com/2020-12-18/0cdabddd-3f3b-4b49-8cd5-e008b3e6ad08_1.jpg",
"delFlag": 0,
"createTime": "2020-12-18T11:44:18.000+00:00",
"updateTime": "2020-12-18T11:44:18.000+00:00"
},
{
"id": 7,
"type": "多线程",
"comments": "多线程",
"logoUrl": "https://java0817-001.oss-cn-guangzhou.aliyuncs.com/2020-12-18/b4d68e14-7abe-4680-8645-e9d8584b6159_thread.png",
"delFlag": 0,
"createTime": "2020-12-18T11:48:11.000+00:00",
"updateTime": "2020-12-18T11:48:11.000+00:00"
},
{
"id": 8,
"type": "分布式架构",
"comments": "分布式架构",
"logoUrl": "https://java0817-001.oss-cn-guangzhou.aliyuncs.com/2020-12-18/ccc99163-69a9-44f4-8ee4-75fbdced3291_jichu.png",
"delFlag": 0,
"createTime": "2020-12-18T11:50:18.000+00:00",
"updateTime": "2020-12-18T11:50:18.000+00:00"
},
{
"id": 9,
"type": "业务逻辑",
"comments": "业务逻辑",
"logoUrl": "https://java0817-001.oss-cn-guangzhou.aliyuncs.com/2020-12-18/3478c4fe-80b6-412a-88f1-8788a09ff3c9_yewu.png",
"delFlag": 0,
"createTime": "2020-12-18T11:51:09.000+00:00",
"updateTime": "2020-12-18T11:51:09.000+00:00"
},
{
"id": 10,
"type": "智能运维",
"comments": "运维",
"logoUrl": "https://java0817-001.oss-cn-guangzhou.aliyuncs.com/2020-12-18/8ab19994-c80f-4dce-8f85-7d4be876f073_运维.png",
"delFlag": 0,
"createTime": "2020-12-18T11:51:45.000+00:00",
"updateTime": "2020-12-18T11:51:45.000+00:00"
}
]
}
7.6、读取指定分类对应的试题列表
<a id=读取指定分类对应的试题> </a>
基本信息
Path: /api/question/question/list
Method: GET
接口描述:
Request
Headers
参数名称 | 参数值 | 是否必须 | 示例 | 备注 |
---|---|---|---|---|
Authorization | 是 | 必须 | 登录成功获取到token |
Query
参数名称 | 是否必须 | 示例 | 备注 |
---|---|---|---|
page | 是 | 1 | 页码 |
limit | 是 | 10 | 每页显示记录数 |
key | 否 | 1 | 分类编号 |
Reponse
{
"msg": "success",
"code": 0,
"page": {
"totalCount": 8,
"pageSize": 10,
"totalPage": 1,
"currPage": 1,
"list": [
{
"id": 50,
"title": "请介绍一下SpringSecurity是做什么的?",
"answer": "SpringSecurity是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。",
"level": 3,
"displayOrder": 1,
"subTitle": "SpringSecurity",
"type": 6,
"enable": 1,
"delFlag": 0,
"createTime": "2020-12-18T11:54:36.000+00:00",
"updateTime": "2020-12-18T11:54:36.000+00:00"
},
{
"id": 51,
"title": "介绍一下商品图片的上传流程?",
"answer": "客户端发送请求到trackerServer 由trackerServer去查询可用StorageServer.返回给客户端.客户端找到StorageServer进行文件上传.StorageServer保存数据并且生产file_id.并将file_id(路径信息和文件名)返回给客户端.",
"level": 4,
"displayOrder": 2,
"subTitle": "图片的上传流程",
"type": 6,
"enable": 1,
"delFlag": 0,
"createTime": "2020-12-18T11:55:33.000+00:00",
"updateTime": "2020-12-18T11:55:33.000+00:00"
},
{
"id": 52,
"title": "什么是富文本编辑器?",
"answer": "富文本编辑器,Rich Text Editor, 简称 RTE, 它提供类似于 Microsoft Word 的编辑功能。常用的富文本编辑器:\nKindEditor http://kindeditor.net/\nUEditor http://ueditor.baidu.com/website/\nCKEditor http://ckeditor.com/",
"level": 2,
"displayOrder": 3,
"subTitle": "富文本编辑器",
"type": 6,
"enable": 1,
"delFlag": 0,
"createTime": "2020-12-18T11:56:10.000+00:00",
"updateTime": "2020-12-18T11:56:10.000+00:00"
},
{
"id": 53,
"title": "什么是逻辑删除?",
"answer": "逻辑删除:顾名思义,就是“假删除”。在企业开发中,一般做真实的物理删除情况较少,我们大多数都是通过一些标记字段,来表示该数据是否被删除,这样后期的统计、分析、研究数据都比较方便,便于积累大量的用户数据。",
"level": 4,
"displayOrder": 4,
"subTitle": "逻辑删除",
"type": 6,
"enable": 1,
"delFlag": 0,
"createTime": "2020-12-18T11:56:42.000+00:00",
"updateTime": "2020-12-18T11:56:42.000+00:00"
},
{
"id": 54,
"title": "简单说明redis存储的流程?",
"answer": "1.直接从reids中获取数据。\n2.判断获取的数据是否为空\n3.如果为空,则从数据库中获取数据,并将从数据库中获取的数据,存储到redis中,再返回。\n4.如果不为空,则直接将redis中获取的数据返回",
"level": 4,
"displayOrder": 5,
"subTitle": "redis存储",
"type": 6,
"enable": 1,
"delFlag": 0,
"createTime": "2020-12-18T11:57:15.000+00:00",
"updateTime": "2020-12-18T11:57:15.000+00:00"
},
{
"id": 55,
"title": "solr是什么?有什么优点?",
"answer": "基于Lucene的全文搜索服务器。同时对其进行了扩展,提供了比\nLucene更为丰富的查询语言,同时实现了可配置、可扩展并对查询性能进行了优化,并且提供了一个完善的功能管理界面,是一款非常优秀的全文搜索引擎",
"level": 2,
"displayOrder": 6,
"subTitle": "solr",
"type": 6,
"enable": 1,
"delFlag": 0,
"createTime": "2020-12-18T11:57:44.000+00:00",
"updateTime": "2020-12-18T11:57:44.000+00:00"
},
{
"id": 56,
"title": "activemq的作用以及原理",
"answer": "Activemq 的作用就是系统之间进行通信。 当然可以使用其他方式进行系统间通信, 如果使用 Activemq 的话可以对系统之间的调用进行解耦, 实现系统间的异步通信。 原理就是生产者生产消息, 把消息发送给activemq。 Activemq 接收到消息, 然后查看有多少个消费者, 然后把消息转发给消费者, 此过程中生产者无需参与。 消费者接收到消息后做相应的处理和生产者没有任何关系",
"level": 4,
"displayOrder": 7,
"subTitle": "activemq",
"type": 7,
"enable": 1,
"delFlag": 0,
"createTime": "2020-12-18T11:58:30.000+00:00",
"updateTime": "2020-12-18T11:58:30.000+00:00"
},
{
"id": 57,
"title": "什么是 Freemarker",
"answer": "FreeMarker 是一个用 Java 语言编写的模板引擎,它基于模板来生成文本输出。\nFreeMarker与 Web 容器无关,即在 Web 运行时,它并不知道 Servlet 或 HTTP。\n它不仅可以用作表现层的实现技术,而且还可以用于生成 XML,JSP 或 Java 等。",
"level": 3,
"displayOrder": 7,
"subTitle": " Freemarker",
"type": 7,
"enable": 1,
"delFlag": 0,
"createTime": "2020-12-18T11:59:01.000+00:00",
"updateTime": "2020-12-18T11:59:01.000+00:00"
}
]
}
}
7.7、获取试题详情
<a id=获取试题详情> </a>
基本信息
Path: /question/question/info/{id}
Method: GET
接口描述:
Request
路径参数
参数名称 | 示例 | 备注 |
---|---|---|
id | 1 | 题目id |
Reponse
{
"msg": "success",
"code": 0,
"question": {
"id": 50,
"title": "请介绍一下SpringSecurity是做什么的?",
"answer": "SpringSecurity是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。",
"level": 3,
"displayOrder": 1,
"subTitle": "SpringSecurity",
"type": 6,
"enable": 1,
"delFlag": 0,
"createTime": "2020-12-18 11:54:36",
"updateTime": "2020-12-18T11:54:36.000+00:00"
}
}
具体参见线上接口文档
https://www.25xt.com/html5css3/16782.html
https://mock.yonyoucloud.com/ 账号:hk109@126.com 密码:offcn123