第4章 预约管理-套餐管理
1. 图片存储方案
1.1 介绍
在实际开发中,我们会有很多处理不同功能的服务器。例如:
应用服务器:负责部署我们的应用
数据库服务器:运行我们的数据库
文件服务器:负责存储用户上传文件的服务器
分服务器处理的目的是让服务器各司其职,从而提高我们项目的运行效率。
常见的图片存储方案:
方案一:使用nginx搭建图片服务器
方案二:使用开源的分布式文件存储系统,例如Fastdfs、HDFS等
方案三:使用云存储,例如阿里云、七牛云等
1.2 七牛云存储
七牛云(隶属于上海七牛信息技术有限公司)是国内领先的以视觉智能和数据智能为核心的企业级云计算服务商,同时也是国内知名智能视频云服务商,累计为 70 多万家企业提供服务,覆盖了国内80%网民。围绕富媒体场景推出了对象存储、融合 CDN 加速、容器云、大数据平台、深度学习平台等产品、并提供一站式智能视频云解决方案。为各行业及应用提供可持续发展的智能视频云生态,帮助企业快速上云,创造更广阔的商业价值。
通过七牛云官网介绍我们可以知道其提供了多种服务,我们主要使用的是七牛云提供的对象存储服务来存储图片。
1.2.1 注册、登录
要使用七牛云的服务,首先需要注册成为会员。
注册完成后就可以使用刚刚注册的邮箱和密码登录到七牛云:
登录成功后点击页面右上角管理控制台:
注意:登录成功后还需要进行实名认证才能进行相关操作。
1.2.2 新建存储空间
要进行图片存储,我们需要在七牛云管理控制台新建存储空间。点击管理控制台首页对象存储下的立即添加按钮,页面跳转到新建存储空间页面:
可以创建多个存储空间,各个存储空间是相互独立的。
1.2.3 查看存储空间信息
存储空间创建后,会在左侧的存储空间列表菜单中展示创建的存储空间名称,点击存储空间名称可以查看当前存储空间的相关信息
<img src="15.png" alt="15" style="zoom:80%;" />
1.2.4 开发者中心
可以通过七牛云提供的开发者中心学习如何操作七牛云服务,地址:https://developer.qiniu.com/
点击对象存储,跳转到对象存储开发页面,地址:https://developer.qiniu.com/kodo
七牛云提供了多种方式操作对象存储服务,本项目采用Java SDK方式,地址:https://developer.qiniu.com/kodo/sdk/1239/java
使用Java SDK操作七牛云需要在common工程导入如下maven坐标:
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>7.7.0</version>
</dependency>
1.2.5 鉴权
Java SDK的所有的功能,都需要合法的授权。授权凭证的签算需要七牛账号下的一对有效的Access Key和Secret Key,这对密钥可以在七牛云管理控制台的个人中心(https://portal.qiniu.com/user/key)获得,如下图:
1.2.6 Java SDK操作七牛云
本章节我们就需要使用七牛云提供的Java SDK完成图片上传和删除,我们可以参考官方提供的例子。
//构造一个带指定 Region 对象的配置类
Configuration cfg = new Configuration(Region.region0());
//...其他参数参考类注释
UploadManager uploadManager = new UploadManager(cfg);
//...生成上传凭证,然后准备上传
String accessKey = "your access key";
String secretKey = "your secret key";
String bucket = "your bucket name";
//如果是Windows情况下,格式是 D:\\qiniu\\test.png
String localFilePath = "/home/qiniu/test.png";
//默认不指定key的情况下,以文件内容的hash值作为文件名
String key = null;
Auth auth = Auth.create(accessKey, secretKey);
String upToken = auth.uploadToken(bucket);
try {
Response response = uploadManager.put(localFilePath, key, upToken);
//解析上传成功的结果
DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
System.out.println(putRet.key);
System.out.println(putRet.hash);
} catch (QiniuException ex) {
Response r = ex.response;
System.err.println(r.toString());
try {
System.err.println(r.bodyString());
} catch (QiniuException ex2) {
//ignore
}
}
//构造一个带指定 Region 对象的配置类
Configuration cfg = new Configuration(Region.region0());
//...其他参数参考类注释
String accessKey = "your access key";
String secretKey = "your secret key";
String bucket = "your bucket name";
String key = "your file key";
Auth auth = Auth.create(accessKey, secretKey);
BucketManager bucketManager = new BucketManager(auth, cfg);
try {
bucketManager.delete(bucket, key);
} catch (QiniuException ex) {
//如果遇到异常,说明删除失败
System.err.println(ex.code());
System.err.println(ex.response.toString());
}
1.2.7 封装工具类
为了方便操作七牛云存储服务,我们可以将官方提供的案例在common工程简单改造成一个工具类,在我们的项目中直接使用此工具类来操作就可以:
package cn.yunhe.utils;
import com.google.gson.Gson;
import com.qiniu.common.QiniuException;
import com.qiniu.common.Zone;
import com.qiniu.http.Response;
import com.qiniu.storage.BucketManager;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.util.Auth;
/**
* 七牛云对象存储工具类
*/
public class QiniuUtils {
//秘钥管理中的AK
public static String accessKey = "vWV-HFfKpaQW_W8n_iaaSrlrVk7GXiajICxLmkJS";
//秘钥管理中的SK
public static String secretKey = "cI_WoSd7z4HQa3lC8lAgIZLrDxd7gP-64jDM1TfW";
//空间名称
public static String bucket = "wjh-health";
/**
* 文件上传:根据文件的绝对路径上传
* @param filePath 上传文件的绝对路径
* @param fileName 自定义的上传到七牛云上的文件名
*/
public static void upload2Qiniu(String filePath,String fileName){
//构造一个带指定Zone对象的配置类
Configuration cfg = new Configuration(Region.region2());
UploadManager uploadManager = new UploadManager(cfg);
Auth auth = Auth.create(accessKey, secretKey);
String upToken = auth.uploadToken(bucket);
try {
Response response = uploadManager.put(filePath, fileName, upToken);
//解析上传成功的结果
DefaultPutRet putRet =
new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
} catch (QiniuException ex) {
Response r = ex.response;
try {
System.err.println(r.bodyString());
} catch (QiniuException ex2) {
//ignore
}
}
}
/**
* 文件上传:根据字节数组上传
* @param bytes 文件数组
* @param fileName 七牛云上的文件名
*/
public static void upload2Qiniu(byte[] bytes, String fileName){
//构造一个带指定Zone对象的配置类
Configuration cfg = new Configuration(Region.region2());
//...其他参数参考类注释
UploadManager uploadManager = new UploadManager(cfg);
//默认不指定key的情况下,以文件内容的hash值作为文件名
String key = fileName;
Auth auth = Auth.create(accessKey, secretKey);
String upToken = auth.uploadToken(bucket);
try {
Response response = uploadManager.put(bytes, key, upToken);
//解析上传成功的结果
DefaultPutRet putRet =
new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
System.out.println(putRet.key);
System.out.println(putRet.hash);
} catch (QiniuException ex) {
Response r = ex.response;
System.err.println(r.toString());
try {
System.err.println(r.bodyString());
} catch (QiniuException ex2) {
//ignore
}
}
}
/**
* 删除文件
* @param fileName
*/
public static void deleteFileFromQiniu(String fileName){
//构造一个带指定Zone对象的配置类
Configuration cfg = new Configuration(Region.region2());
String key = fileName;
Auth auth = Auth.create(accessKey, secretKey);
BucketManager bucketManager = new BucketManager(auth, cfg);
try {
bucketManager.delete(bucket, key);
} catch (QiniuException ex) {
//如果遇到异常,说明删除失败
System.err.println(ex.code());
System.err.println(ex.response.toString());
}
}
}
将此工具类放在health_common工程中,后续会使用到。
2. 新增套餐
2.1 需求分析
套餐其实就是检查组的集合,例如有一个套餐为“入职体检套餐”,这个体检套餐可以包括多个检查组:一般检查、血常规、尿常规、肝功三项等。所以在添加套餐时需要选择这个套餐包括的检查组。
套餐对应的实体类为Setmeal,对应的数据表为t_setmeal。套餐和检查组为多对多关系,所以需要中间表t_setmeal_checkgroup进行关联。
2.2 完善页面
套餐管理页面对应的是setmeal.html页面,根据产品设计的原型已经完成了页面基本结构的编写,现在需要完善页面动态效果。
2.2.1 弹出新增窗口
页面中已经提供了新增窗口,只是出于隐藏状态。只需要将控制展示状态的属性dialogFormVisible改为true接口显示出新增窗口。点击新建按钮时绑定的方法为handleCreate,所以在handleCreate方法中修改dialogFormVisible属性的值为true即可。同时为了增加用户体验度,需要每次点击新建按钮时清空表单输入项。
由于新增套餐时还需要选择此套餐包含的检查组,所以新增套餐窗口分为两部分信息:基本信息和检查组信息,如下图:
新建按钮绑定单击事件,对应的处理函数为handleCreate
<el-button type="primary" class="butT" @click="handleCreate()">新建</el-button>
// 重置表单
resetForm() {
this.formData = {};
this.activeName='first';
this.checkgroupIds = [];
this.imageUrl = null;
}
// 弹出添加窗口
handleCreate() {
this.dialogFormVisible = true;
this.resetForm();
}
2.2.2 动态展示检查组列表
现在虽然已经完成了新增窗口的弹出,但是在检查组信息标签页中需要动态展示所有的检查组信息列表数据,并且可以进行勾选。具体操作步骤如下:
(1)定义模型数据
tableData:[],//添加表单窗口中检查组列表数据
checkgroupIds:[],//添加表单窗口中检查组复选框对应id
(2)动态展示检查组列表数据,数据来源于上面定义的tableData模型数据
<table class="datatable">
<thead>
<tr>
<th>选择</th>
<th>项目编码</th>
<th>项目名称</th>
<th>项目说明</th>
</tr>
</thead>
<tbody>
<tr v-for="c in tableData">
<td>
<input :id="c.id" v-model="checkgroupIds" type="checkbox" :value="c.id">
</td>
<td><label :for="c.id">{{c.code}}</label></td>
<td><label :for="c.id">{{c.name}}</label></td>
<td><label :for="c.id">{{c.remark}}</label></td>
</tr>
</tbody>
</table>
(3)完善handleCreate方法,发送ajax请求查询所有检查组数据并将结果赋值给tableData模型数据用于页面表格展示
// 弹出添加窗口
handleCreate() {
this.dialogFormVisible = true;
this.resetForm();
axios.get("/checkgroup/findAll").then((res)=> {
if(res.data.flag){
this.tableData = res.data.data;
}else{
this.$message.error(res.data.message);
}
});
}
(4)分别在CheckGroupController、CheckGroupService、CheckGroupServiceImpl、CheckGroupDao、CheckGroupDao.xml中扩展方法查询所有检查组数据
CheckGroupController:
@ApiOperation("获取所有检查组")
@GetMapping("/findAll")
public Result findAll(){
List<CheckGroup> checkGroups = checkGroupService.findAll();
return new Result(true,MessageConstant.QUERY_CHECKGROUP_SUCCESS,checkGroups);
}
CheckGroupService:
List<CheckGroup> findAll()throws BusinessException;
CheckGroupServiceImpl:
@Override
public List<CheckGroup> findAll() throws BusinessException {
String key = RedisConstant.QUERY_CHECKGROUPS_KEY;
List<CheckGroup> checkGroups = null;
if (redisTemplate.hasKey(key)){
log.info("=============查询所有检查组findAll()走了缓存!");
checkGroups = (List<CheckGroup>) redisTemplate.opsForValue().get(key);
}else {
log.info("=============查询所有检查组findAll()走了数据库!");
checkGroups = checkGroupMapper.selectList(null);
redisTemplate.opsForValue().set(key,checkGroups);
}
return checkGroups;
}
2.2.3 图片上传并预览
此处使用的是ElementUI提供的上传组件el-upload,提供了多种不同的上传效果,上传成功后可以进行预览。
实现步骤:
(1)定义模型数据,用于后面上传文件的图片预览:
imageUrl:null,//模型数据,用于上传图片完成后图片预览
(2)定义上传组件:
<!--
el-upload:上传组件
action:上传的提交地址
auto-upload:选中文件后是否自动上传
name:上传文件的名称,服务端可以根据名称获得上传的文件对象
show-file-list:是否显示已上传文件列表
on-success:文件上传成功时的钩子
before-upload:上传文件之前的钩子
-->
<el-upload
class="avatar-uploader"
action="/setmeal/upload"
:auto-upload="autoUpload"
name="imgFile"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload">
<!--用于上传图片预览-->
<img v-if="imageUrl" :src="imageUrl" class="avatar">
<!--用于展示上传图标-->
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
(3)定义对应的钩子函数:
注意:这个url需要换成自己在七牛云的CND域名
//文件上传成功后的钩子,response为服务端返回的值,file为当前上传的文件封装成的js对象
handleAvatarSuccess(response, file) {
//url替换为自己的七牛云域名
this.imageUrl = "http://rdg5acyug.hn-bkt.clouddn.com/"+response.data;
this.$message({
message: response.message,
type: response.flag ? 'success' : 'error'
});
//设置模型数据(图片名称),后续提交ajax请求时会提交到后台最终保存到数据库
this.formData.img = response.data;
}
//上传文件之前的钩子
beforeAvatarUpload(file) {
const isJPG = file.type === 'image/jpeg';
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isJPG) {
this.$message.error('上传套餐图片只能是 JPG 格式!');
}
if (!isLt2M) {
this.$message.error('上传套餐图片大小不能超过 2MB!');
}
return isJPG && isLt2M;
}
(4)创建SetmealController,接收上传的文件
上传后将文件保存到redis的set集合中
package cn.yunhe.controller;
import cn.yunhe.constant.MessageConstant;
import cn.yunhe.entity.Result;
import cn.yunhe.exception.BusinessException;
import cn.yunhe.utils.QiniuUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.UUID;
@Api("套餐管理")
@RestController
@RequestMapping("/setmeal")
public class SetmealController {
@ApiOperation("上传文件到七牛云")
@PostMapping("/upload")
public Result upload(@RequestParam("imgFile")MultipartFile imgFile){
try {
//1.对图片进行重新命名
//1.1.获取原文件名
String originalFilename = imgFile.getOriginalFilename();
int i = originalFilename.lastIndexOf(".");
//1.2.获取后缀
String suffix = originalFilename.substring(i);
//1.3.重新命名
String newname = UUID.randomUUID().toString()+suffix;
//2.上传
QiniuUtils.upload2Qiniu(imgFile.getBytes(),newname);
return new Result(true,MessageConstant.UPLOAD_SUCCESS,newname);
} catch (IOException e) {
e.printStackTrace();
throw new BusinessException("文件上传失败");
}
}
}
2.2.4 提交请求
当用户点击新增窗口中的确定按钮时发送ajax请求将数据提交到后台进行数据库操作。提交到后台的数据分为两部分:套餐基本信息(对应的模型数据为formData)和检查组id数组(对应的模型数据为checkgroupIds)。
为确定按钮绑定单击事件,对应的处理函数为handleAdd
<el-button type="primary" @click="handleAdd()">确定</el-button>
完善handleAdd方法
//添加
handleAdd () {
axios.post("/setmeal/add?checkgroupIds=" + this.checkgroupIds,this.formData).
then((response)=> {
this.dialogFormVisible = false;
if(response.data.flag){
this.$message({
message: response.data.message,
type: 'success'
});
}else{
this.$message.error(response.data.message);
}
}).finally(()=> {
this.findPage();
});
}
2.3 后台代码
2.3.1 Controller
在SetmealController中增加方法
@ApiOperation("添加套餐")
@PostMapping("/add")
public Result add(@RequestBody Setmeal setmeal, Integer[] checkgroupIds){
setmealService.add(setmeal,checkgroupIds);
return new Result(true,MessageConstant.ADD_SETMEAL_SUCCESS);
}
2.3.2 服务接口
创建SetmealService接口并提供新增方法
package cn.yunhe.service;
import cn.yunhe.entity.PageResult;
import cn.yunhe.pojo.CheckGroup;
import cn.yunhe.pojo.Setmeal;
import java.util.List;
/**
* 体检套餐服务接口
*/
public interface SetmealService {
public void add(Setmeal setmeal, Integer[] checkgroupIds)throws BusinessException;
}
2.3.3 服务实现类
修改RedisConstant,添加如下静态常量:
/**查询所有套餐的key*/
public static final String QUERY_SETMEALS_KEY="SetMeals:allList";
/**套餐图片上传到七牛云的key*/
public static final String UPLOAD_SETMEAL_QNY_KEY="SetMeal:upload_qny";
/**套餐图片保存到数据库的key*/
public static final String UPLOAD_ETMEAL_DB_KEY="SetMeal:upload_db";
/**套餐垃圾图片*/
public static final String UPLOAD_ETMEALIMG_DIFF_KEY="SetMeal:img_diff";
/**用于缓存体检预约时发送的验证码*/
public static final String SENDTYPE_ORDER = "SMS:001";
/**用于缓存手机号快速登录时发送的验证码*/
public static final String SENDTYPE_LOGIN = "SMS:002";
/**用于缓存找回密码时发送的验证码*/
public static final String SENDTYPE_GETPWD = "SMS:003";
添加工具类RedisUtil:
package cn.yunhe.utils;
import org.springframework.data.redis.core.RedisTemplate;
public class RedisUtil {
/**
* 删除指定key的缓存
* @param redisTemplate
* @param key
*/
public static void delete(RedisTemplate redisTemplate,String key){
if (redisTemplate.hasKey(key)){
redisTemplate.delete(key);
}
}
}
创建SetmealServiceImpl服务实现类并实现新增方法:
添加套餐时验证套餐名称和套餐编码,添加成功后并将图片添加到redis的set集合中
package cn.yunhe.service;
import cn.yunhe.constant.RedisConstant;
import cn.yunhe.mapper.SetMealAndCheckGroupMapper;
import cn.yunhe.mapper.SetmealMapper;
import cn.yunhe.pojo.Setmeal;
import cn.yunhe.pojo.SetmealCheckgroup;
import cn.yunhe.utils.RedisUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.dubbo.config.annotation.Service;
import org.apache.ibatis.builder.BuilderException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.transaction.annotation.Transactional;
@Service
public class SetmealServiceImpl implements SetmealService {
@Autowired
private SetmealMapper setmealMapper;
@Autowired
private SetMealAndCheckGroupMapper setMealAndCheckGroupMapper;
@Autowired
private RedisTemplate redisTemplate;
String key = RedisConstant.QUERY_SETMEALS_KEY;
@Override
@Transactional
public void add(Setmeal setmeal, Integer[] checkGroupIds) {
//删除缓存
RedisUtil.delete(redisTemplate,key);
//基础数据校验
if (setmeal==null|| StringUtils.isEmpty(setmeal.getName())
||StringUtils.isEmpty(setmeal.getCode())){
throw new BuilderException("添加套餐时,必要数据为空");
}
//检查编码和名称是否重复
if (checkName(setmeal.getName())||checkCode(setmeal.getCode())){
throw new BuilderException("名称或编码已经存在");
}
//添加套餐和关系表
setmealMapper.insert(setmeal);
Integer id = setmeal.getId();
if (checkGroupIds!=null&&checkGroupIds.length>0){
for (Integer checkGroupId : checkGroupIds) {
SetmealCheckgroup setmealCheckgroup = new SetmealCheckgroup();
setmealCheckgroup.setCheckgroupId(checkGroupId);
setmealCheckgroup.setSetmealId(id);
setMealAndCheckGroupMapper.insert(setmealCheckgroup);
}
}
}
/**
* 检查名称是否存在
* @param name
*/
private boolean checkName(String name){
Integer count = setmealMapper.selectCount(
new LambdaQueryWrapper<Setmeal>().eq(Setmeal::getName, name)
);
return count>0;
}
/**
* 检查编码是否存在
* @param code
*/
private boolean checkCode(String code){
Integer count = setmealMapper.selectCount(
new LambdaQueryWrapper<Setmeal>().eq(Setmeal::getCode, code)
);
return count>0;
}
}
2.4 完善文件上传
前面我们已经完成了文件上传,将图片存储在了七牛云服务器中。但是这个过程存在一个问题,就是如果用户只上传了图片而没有最终保存套餐信息到我们的数据库,这时我们上传的图片就变为了垃圾图片。对于这些垃圾图片我们需要定时清理来释放磁盘空间。这就需要我们能够区分出来哪些是垃圾图片,哪些不是垃圾图片。如何实现呢?
方案就是利用redis来保存图片名称,具体做法为:
1、当用户上传图片后,将图片名称保存到redis的一个Set集合中,例如集合名称为SetMeal:upload_qny
2、当用户添加套餐后,将图片名称保存到redis的另一个Set集合中,例如集合名称为SetMeal:upload_db
3、计算SetMeal:upload_qny集合与SetMeal:upload_db集合的差值,结果就是垃圾图片的名称集合,清理这些图片即可
本小节我们先来完成前面2个环节,第3个环节(清理图片环节)在后面会通过定时任务再实现。
实现步骤:
(1)在health_backend项目中SetmealController注入RedisTemplate,修改upload方法
@ApiOperation("上传文件到七牛云")
@PostMapping("/upload")
public Result upload(@RequestParam("imgFile")MultipartFile imgFile){
try {
....省略
//2.上传
QiniuUtils.upload2Qiniu(imgFile.getBytes(),newname);
redisTemplate.opsForSet().add(RedisConstant.UPLOAD_SETMEAL_QNY_KEY,newname);
....省略
} catch (IOException e) {
e.printStackTrace();
throw new BusinessException("文件上传失败");
}
}
(2)完善SetmealServiceImpl服务类,在保存完成套餐信息后将图片名称存储到redis集合中
@Override
@Transactional
public void add(Setmeal setmeal, Integer[] checkGroupIds) {
....省略
//添加套餐和关系表
setmealMapper.insert(setmeal);
//将图片添加到redis缓存
redisTemplate.opsForSet().add(RedisConstant.UPLOAD_ETMEAL_DB_KEY,setmeal.getImg());
....省略
}
3. 体检套餐分页
3.1 完善页面
3.1.1 定义分页相关模型数据
pagination: {//分页相关模型数据
currentPage: 1,//当前页码
pageSize:10,//每页显示的记录数
total:0,//总记录数
queryString:null//查询条件
},
dataList: [],//当前页要展示的分页列表数据
3.1.2 定义分页方法
在页面中提供了findPage方法用于分页查询,为了能够在setmeal.html页面加载后直接可以展示分页数据,可以在VUE提供的钩子函数created中调用findPage方法
//钩子函数,VUE对象初始化完成后自动执行
created() {
this.findPage();
}
//分页查询
findPage() {
//请求后台
axios.post("/setmeal/findPage",this.pagination).then((response)=> {
//为模型数据赋值,基于VUE的双向绑定展示到页面
this.dataList = response.data.rows;
this.pagination.total = response.data.total;
});
}
3.1.3 完善分页方法执行时机
除了在created钩子函数中调用findPage方法查询分页数据之外,当用户点击查询按钮或者点击分页条中的页码时也需要调用findPage方法重新发起查询请求。
为查询按钮绑定单击事件,调用findPage方法
<el-button @click="findPage()" class="dalfBut">查询</el-button>
为分页条组件绑定current-change事件,此事件是分页条组件自己定义的事件,当页码改变时触发,对应的处理函数为handleCurrentChange
<el-pagination
class="pagiantion"
@current-change="handleCurrentChange"
:current-page="pagination.currentPage"
:page-size="pagination.pageSize"
layout="total, prev, pager, next, jumper"
:total="pagination.total">
</el-pagination>
定义handleCurrentChange方法
//切换页码
handleCurrentChange(currentPage) {
//currentPage为切换后的页码
this.pagination.currentPage = currentPage;
this.findPage();
}
3.2 后台代码
3.2.1 Controller
在SetmealController中增加分页查询方法
@ApiOperation("分页查询,可根据套餐助记码、名称、编码进行查询")
@PostMapping("/findPage")
public PageResult findPage(@RequestBody QueryPageBean queryPageBean){
PageResult pageResult = setmealService.findPage(queryPageBean);
return pageResult;
}
3.2.2 服务接口
在SetmealService服务接口中扩展分页查询方法
PageResult findPage(QueryPageBean queryPageBean)throws BusinessException;
3.2.3 服务实现类
在SetmealServiceImpl服务实现类中实现分页查询方法,基于Mybatis分页助手插件实现分页
@Override
public PageResult findPage(QueryPageBean queryPageBean) throws BusinessException{
Page<Setmeal> page = new Page<>(queryPageBean.getCurrentPage(),queryPageBean.getPageSize());
LambdaQueryWrapper<Setmeal> wrapper = null;
String qstr = queryPageBean.getQueryString();
if (!StringUtils.isEmpty(qstr)){
wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Setmeal::getCode,qstr).or()
.like(Setmeal::getName,qstr).or()
.eq(Setmeal::getHelpCode,qstr);
}
IPage<Setmeal> iPage = setmealMapper.selectPage(page, wrapper);
return new PageResult(iPage.getTotal(),iPage.getRecords());
}
4. 定时任务组件Quartz
4.1 Quartz介绍
Quartz是Job scheduling(作业调度)领域的一个开源项目,Quartz既可以单独使用也可以跟spring框架整合使用,在实际开发中一般会使用后者。使用Quartz可以开发一个或者多个定时任务,每个定时任务可以单独指定执行的时间,例如每隔1小时执行一次、每个月第一天上午10点执行一次、每个月最后一天下午5点执行一次等。
官网:http://www.quartz-scheduler.org/
maven坐标:版本和springboot版本一致
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
4.2 Quartz入门案例
本案例基于Quartz和spring整合的方式使用。具体步骤:
(1)创建maven工程quartzdemo,导入相关坐标,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>
<artifactId>yunhe_health</artifactId>
<groupId>cn.yunhe</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>quartzDemo</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
</dependencies>
</project>
(2)自定义启动类QuartzApplication
package cn.yunhe;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class QuartzApplication {
public static void main(String[] args) {
SpringApplication.run(QuartzApplication.class,args);
}
}
(3)自定义一个Job
package cn.yunhe.job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;
import java.io.Serializable;
/**
* 定时任务
*/
public class MyJob extends QuartzJobBean implements Serializable {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("定时任务开始......");
}
}
(4)定义定时任务配置类QuartzConfig
package cn.yunhe.config;
import cn.yunhe.job.MyJob;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean;
/**
* 定时任务配置类
*/
@Configuration
public class QuartzConfig {
/**注册定时任务*/
@Bean
public MyJob myJob(){
return new MyJob();
}
/**注册JobDetailFactoryBean:可以帮程序创建一个定时任务*/
@Bean
public JobDetailFactoryBean jobDetailFactoryBean(){
JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean();
//配置定时任务
jobDetailFactoryBean.setJobClass(MyJob.class);
//定时任务持久化
jobDetailFactoryBean.setDurability(true);
return jobDetailFactoryBean;
}
/**配置触发器*/
@Bean
public CronTriggerFactoryBean cronTriggerFactoryBean(){
CronTriggerFactoryBean cronTrigger = new CronTriggerFactoryBean();
cronTrigger.setJobDetail(jobDetailFactoryBean().getObject());
//配置时间规则
cronTrigger.setCronExpression("0/10 * * * * ?");
return cronTrigger;
}
}
(4)启动项目观察控制台。
执行上面main方法观察控制台,可以发现每隔10秒会输出一次,说明每隔10秒自定义Job被调用一次。
4.3 cron表达式
上面的入门案例中我们指定了一个表达式:0/10 * * * * ?
这种表达式称为cron表达式,通过cron表达式可以灵活的定义出符合要求的程序执行的时间。本小节我们就来学习一下cron表达式的使用方法。如下图:
cron表达式分为七个域,之间使用空格分隔。其中最后一个域(年)可以为空。每个域都有自己允许的值和一些特殊字符构成。使用这些特殊字符可以使我们定义的表达式更加灵活。
下面是对这些特殊字符的介绍:
逗号(,):指定一个值列表,例如使用在月域上1,4,5,7表示1月、4月、5月和7月
横杠(-):指定一个范围,例如在时域上3-6表示3点到6点(即3点、4点、5点、6点)
星号(*):表示这个域上包含所有合法的值。例如,在月份域上使用星号意味着每个月都会触发
斜线(/):表示递增,例如使用在秒域上0/15表示每15秒
问号(?):只能用在日和周域上,但是不能在这两个域上同时使用。表示不指定
井号(#):只能使用在周域上,用于指定月份中的第几周的哪一天,例如6#3,意思是某月的第三个周五 (6=星期五,3意味着月份中的第三周)
L:某域上允许的最后一个值。只能使用在日和周域上。当用在日域上,表示的是在月域上指定的月份的最后一天。用于周域上时,表示周的最后一天,就是星期六
W:W 字符代表着工作日 (星期一到星期五),只能用在日域上,它用来指定离指定日的最近的一个工作日
4.4 cron表达式在线生成器
前面介绍了cron表达式,但是自己编写表达式还是有一些困难的,我们可以借助一些cron表达式在线生成器来根据我们的需求生成表达式即可。
http://cron.qqe2.com/
5. 定时清理垃圾图片
前面我们已经完成了体检套餐的管理,在新增套餐时套餐的基本信息和图片是分两次提交到后台进行操作的。也就是用户首先将图片上传到七牛云服务器,然后再提交新增窗口中录入的其他信息。如果用户只是上传了图片而没有提交录入的其他信息,此时的图片就变为了垃圾图片,因为在数据库中并没有记录它的存在。此时我们要如何处理这些垃圾图片呢?
解决方案就是通过定时任务组件定时清理这些垃圾图片。为了能够区分出来哪些图片是垃圾图片,我们在文件上传成功后将图片保存到了一个redis集合中,当套餐数据插入到数据库后我们又将图片名称保存到了另一个redis集合中,通过计算这两个集合的差值就可以获得所有垃圾图片的名称。
本章节我们就会基于Quartz定时任务,通过计算redis两个集合的差值找出所有的垃圾图片,就可以将垃圾图片清理掉。
操作步骤:
(1)创建maven工程health_jobs,导入Quartz等相关坐标
<?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>yunhe_health</artifactId>
<groupId>cn.yunhe</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>health_jobs</artifactId>
<dependencies>
<dependency>
<groupId>cn.yunhe</groupId>
<artifactId>health_interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
</dependencies>
</project>
(2)定义启动类JobsApplication
package cn.yunhe;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class JobsApplication {
public static void main(String[] args) {
SpringApplication.run(JobsApplication.class,args);
}
}
(3)配置bootstrap.yml
server:
port: 9100
spring:
application:
name: health_jobs
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: 8a162108-2be1-4b51-a28c-b5d54d728ccf
config:
server-addr: 127.0.0.1:8848
name: redis
file-extension: yaml
group: COMMON_GROUP
namespace: 8a162108-2be1-4b51-a28c-b5d54d728ccf
(4)定义定时任务类ClearImgJob
package cn.yunhe.job;
import cn.yunhe.constant.RedisConstant;
import cn.yunhe.utils.QiniuUtils;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.scheduling.quartz.QuartzJobBean;
import java.io.Serializable;
import java.util.Set;
/**
* 定时任务,清理图片
*/
public class ClearImgJob extends QuartzJobBean implements Serializable {
@Autowired
private RedisTemplate redisTemplate;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
SetOperations opsForSet = redisTemplate.opsForSet();
//将垃圾图片放入到另一个set集合中
opsForSet.differenceAndStore(RedisConstant.UPLOAD_SETMEAL_QNY_KEY,
RedisConstant.UPLOAD_ETMEAL_DB_KEY,RedisConstant.UPLOAD_ETMEALIMG_DIFF_KEY);
// TODO 清除垃圾图片
Set<String> members = opsForSet.members(RedisConstant.UPLOAD_ETMEALIMG_DIFF_KEY);
System.out.println("=============>>>>members="+members);
for (String member : members) {
//在七牛云中删除此图片
QiniuUtils.deleteFileFromQiniu(member);
//删除差集中的元素
opsForSet.remove(RedisConstant.UPLOAD_ETMEALIMG_DIFF_KEY,member);
//删除七牛云缓存中的元素
opsForSet.remove(RedisConstant.UPLOAD_SETMEAL_QNY_KEY,member);
}
}
}
(5)定义定时任务配置类
package cn.yunhe.config;
import cn.yunhe.job.ClearImgJob;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
/**
* 定时任务配置类
*/
@Configuration
public class QuartzConfig {
@Bean
public ClearImgJob clearImgJob(){
return new ClearImgJob();
}
/**注册JobDetailFactoryBean*/
@Bean
public JobDetailFactoryBean jobDetailFactoryBean(){
JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean();
//配置定时任务
jobDetailFactoryBean.setJobClass(ClearImgJob.class);
//定时任务持久化
jobDetailFactoryBean.setDurability(true);
return jobDetailFactoryBean;
}
/**配置触发器*/
@Bean
public CronTriggerFactoryBean cronTriggerFactoryBean(){
CronTriggerFactoryBean cronTrigger = new CronTriggerFactoryBean();
cronTrigger.setJobDetail(jobDetailFactoryBean().getObject());
//配置时间规则
//cronTrigger.setCronExpression("0/20 * * * * ?");
cronTrigger.setCronExpression("0 0 0/2 * * ? ");
return cronTrigger;
}
}
6.编辑套餐
6.1 完善页面
6.1.1 添加编辑弹出层
<!-- 编辑标签弹层 -->
<div class="add-form">
<el-dialog title="编辑套餐" :visible.sync="dialogFormVisible4Edit">
<template>
<el-tabs v-model="activeName" type="card">
<el-tab-pane label="基本信息" name="first">
<el-form label-position="right" label-width="100px">
<el-row>
<el-col :span="12">
<el-form-item label="编码">
<el-input v-model="formData.code"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="名称">
<el-input v-model="formData.name"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="适用性别">
<el-select v-model="formData.sex">
<el-option label="不限" value="0"></el-option>
<el-option label="男" value="1"></el-option>
<el-option label="女" value="2"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="助记码">
<el-input v-model="formData.helpCode"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="套餐价格">
<el-input v-model="formData.price"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="适用年龄">
<el-input v-model="formData.age"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="上传图片">
<el-upload
class="avatar-uploader"
action="/setmeal/upload"
:auto-upload="autoUpload"
name="imgFile"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload">
<img v-if="imageUrl" :src="imageUrl" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="说明">
<el-input v-model="formData.remark" type="textarea"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="注意事项">
<el-input v-model="formData.attention" type="textarea"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-tab-pane>
<el-tab-pane label="检查组信息" name="second">
<div class="checkScrol">
<table class="datatable">
<thead>
<tr>
<th>选择</th>
<th>项目编码</th>
<th>项目名称</th>
<th>项目说明</th>
</tr>
</thead>
<tbody>
<tr v-for="c in tableData">
<td>
<input :id="c.id" v-model="checkgroupIds" type="checkbox" :value="c.id">
</td>
<td><label :for="c.id">{{c.code}}</label></td>
<td><label :for="c.id">{{c.name}}</label></td>
<td><label :for="c.id">{{c.remark}}</label></td>
</tr>
</tbody>
</table>
</div>
</el-tab-pane>
</el-tabs>
</template>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible4Edit = false">取消</el-button>
<el-button type="primary" @click="handleEdit()">确定</el-button>
</div>
</el-dialog>
</div>
6.1.2 修改vue
-
在data中添加dialogFormVisible4Edit控制窗口的显示和隐藏
dialogFormVisible4Edit: false, //控制编辑窗口显示/隐藏
-
在编辑标签中,绑定编辑事件
<el-button type="primary" size="mini" @click="handleUpdate(scope.row)">编辑</el-button>
-
编写handleUpdate(),imageUrl换成自己的七牛云域名
//弹出编辑窗口
handleUpdate(row){
//发送请求求出套餐详情进行回填
axios.get('/setmeal/findById/'+row.id).then((res)=>{
if (res.data.flag){
//弹出编辑窗口
this.dialogFormVisible4Edit=true;
//进行数据回填
this.formData=res.data.data;
this.imageUrl = "http://rdg5acyug.hn-bkt.clouddn.com/"+this.formData.img;
//3.发送axios请求,查询所有的检查组
axios.get("/checkgroup/findAll").then((res)=>{
if (res.data.flag){
this.tableData=res.data.data;
//4.再次发送axios请求,查询此套餐对应的检查组id集合
axios.get("/setmeal/findCheckGroupIdsBySetmealId/" + row.id).then((res)=> {
if(res.data.flag){
if(res.data.data==null){
this.checkgroupIds = []
}else{
this.checkgroupIds = res.data.data;
}
}else{
this.$message.error(res.data.message);
}
});
}else {
this.$message.error(res.data.message);
}
});
}else{
this.$message.error("获取数据失败,请刷新当前页面");
}
});
},
- 编写handleEdit()编辑套餐
//编辑套餐
handleEdit() {
//发送ajax请求,提交模型数据
axios.put("/setmeal/edit?checkgroupIds="+this.checkgroupIds,this.formData).
then((response)=> {
//隐藏编辑窗口
this.dialogFormVisible4Edit = false;
if(response.data.flag){
this.$message({
message: response.data.message,
type: 'success'
});
}else{
this.$message.error(response.data.message);
}
}).finally(()=> {
this.findPage();
});
},
6.2 后台代码
6.2.1 Controller
@ApiOperation("根据id获取套餐基本信息")
@GetMapping("/findById/{id}")
public Result findById(@PathVariable("id")Integer id){
Setmeal setmeal = setmealService.findById(id);
return new Result(true,MessageConstant.QUERY_SETMEAL_SUCCESS,setmeal);
}
@ApiOperation("查询套餐对应的检查组id集合")
@GetMapping("/findCheckGroupIdsBySetmealId/{id}")
public Result findCheckGroupIdsBySetmealId(@PathVariable("id")Integer id){
List<Integer> groupIds = setmealService.findCheckGroupIdsBySetmealId(id);
return new Result(true,"根据套餐ID获取检查组ID集合成功",groupIds);
}
@ApiOperation("编辑套餐")
@PutMapping("/edit")
public Result edit(Integer[] checkgroupIds,@RequestBody Setmeal setmeal){
setmealService.edit(checkgroupIds,setmeal);
return new Result(true,MessageConstant.EDIT_SETMEAL_SUCCESS);
}
6.2.2 服务接口
/**
* 根据id获取套餐基本信息
* @param id
*/
Setmeal findById(Integer id)throws BusinessException;
/**
* 根据套餐id获取对应的检查组id集合
* @param id 套餐id
* @return
*/
List<Integer> findCheckGroupIdsBySetmealId(Integer id)throws BusinessException;
void edit(Integer[] checkgroupIds, Setmeal setmeal)throws BusinessException;
6.2.3 服务实现类
@Override
public Setmeal findById(Integer id) throws BusinessException{
if (redisTemplate.hasKey(key)){
List<Setmeal> list = (List<Setmeal>) redisTemplate.opsForValue().get(key);
for (Setmeal setmeal : list) {
if (id.equals(setmeal.getId())){
return setmeal;
}
}
}
return setMealMapper.selectById(id);
}
@Override
public List<Integer> findCheckGroupIdsBySetmealId(Integer id) throws BusinessException{
List<SetmealCheckgroup> setmealCheckgroups = setMealAndCheckGroupMapper.selectList(
new LambdaQueryWrapper<SetmealCheckgroup>()
.eq(SetmealCheckgroup::getSetmealId, id)
);
List<Integer> list = null;
if (setmealCheckgroups!=null&&setmealCheckgroups.size()>0){
list = new ArrayList<>();
for (SetmealCheckgroup setmealCheckgroup : setmealCheckgroups) {
list.add(setmealCheckgroup.getCheckgroupId());
}
}
return list;
}
@Override
@Transactional
public void edit(Integer[] checkgroupIds, Setmeal setmeal) throws BusinessException {
//删除缓存
if (redisTemplate.hasKey(key)){
redisTemplate.delete(key);
}
//基本数据判断
if (setmeal==null||StringUtils.isEmpty(setmeal.getName())||
StringUtils.isEmpty(setmeal.getCode())){
throw new BusinessException("编辑套餐时,必要数据为空");
}
//获取原来的套餐,编辑之前的套餐
Setmeal origin = setMealMapper.selectById(setmeal.getId());
//获取原来的name和code
String originName = origin.getName();
String originCode = origin.getCode();
//获取现在的name和code
String name = setmeal.getName();
String code = setmeal.getCode();
//判断编码和name
if (!name.equals(originName)&&checkName(name)){
throw new BusinessException("套餐名称已存在");
}
if (!code.equals(originCode)&&checkCode(code)){
throw new BusinessException("套餐编码已存在");
}
//编辑套餐基本信息
setMealMapper.updateById(setmeal);
//删除套餐和检查组关系表
Integer setmealId = setmeal.getId();
setMealAndCheckGroupMapper.delete(
new LambdaQueryWrapper<SetmealCheckgroup>()
.eq(SetmealCheckgroup::getSetmealId,setmealId)
);
//重新添加关系表
if (checkgroupIds!=null&&checkgroupIds.length>0){
for (Integer checkgroupId : checkgroupIds) {
SetmealCheckgroup setmealCheckgroup = new SetmealCheckgroup();
setmealCheckgroup.setSetmealId(setmealId);
setmealCheckgroup.setCheckgroupId(checkgroupId);
setMealAndCheckGroupMapper.insert(setmealCheckgroup);
}
}
}
}
7.删除套餐
作业,自行完成!
标签:String,管理,预约,new,套餐,import,public,setmeal From: https://blog.51cto.com/teayear/7710798