黑馬程序員-傳智健康項目(第四章)


傳智健康項目


第4章 預約管理-套餐管理

1. 圖片存儲方案

1.1 介紹

在實際開發中,我們會有很多處理不同功能的服務器。例如:

應用服務器:負責部署我們的應用

數據庫服務器:運行我們的數據庫

文件服務器:負責存儲用戶上傳文件的服務器

分服務器處理的目的是讓服務器各司其職,從而提高我們項目的運行效率。

常見的圖片存儲方案:

方案一:使用nginx搭建圖片服務器

方案二:使用開源的分布式文件存儲系統,例如Fastdfs、HDFS等

方案三:使用雲存儲,例如阿里雲、七牛雲等

1.2 七牛雲存儲

七牛雲(隸屬於上海七牛信息技術有限公司)是國內領先的以視覺智能和數據智能為核心的企業級雲計算服務商,同時也是國內知名智能視頻雲服務商,累計為 70 多萬家企業提供服務,覆蓋了國內80%網民。圍繞富媒體場景推出了對象存儲、融合 CDN 加速、容器雲、大數據平台、深度學習平台等產品、並提供一站式智能視頻雲解決方案。為各行業及應用提供可持續發展的智能視頻雲生態,幫助企業快速上雲,創造更廣闊的商業價值。

官網:https://www.qiniu.com/

通過七牛雲官網介紹我們可以知道其提供了多種服務,我們主要使用的是七牛雲提供的對象存儲服務來存儲圖片。

1.2.1 注冊、登錄

要使用七牛雲的服務,首先需要注冊成為會員。地址:https://portal.qiniu.com/signup

注冊完成后就可以使用剛剛注冊的郵箱和密碼登錄到七牛雲:

登錄成功后點擊頁面右上角管理控制台:

注意:登錄成功后還需要進行實名認證才能進行相關操作。

1.2.2 新建存儲空間

要進行圖片存儲,我們需要在七牛雲管理控制台新建存儲空間。點擊管理控制台首頁對象存儲下的立即添加按鈕,頁面跳轉到新建存儲空間頁面:

可以創建多個存儲空間,各個存儲空間是相互獨立的。

1.2.3 查看存儲空間信息

存儲空間創建后,會在左側的存儲空間列表菜單中展示創建的存儲空間名稱,點擊存儲空間名稱可以查看當前存儲空間的相關信息

1.2.4 開發者中心

可以通過七牛雲提供的開發者中心學習如何操作七牛雲服務,地址:https://developer.qiniu.com/

點擊對象存儲,跳轉到對象存儲開發頁面,地址:https://developer.qiniu.com/kodo

七牛雲提供了多種方式操作對象存儲服務,本項目采用Java SDK方式,地址:https://developer.qiniu.com/kodo/sdk/1239/java

使用Java SDK操作七牛雲需要導入如下maven坐標:

<dependency>
  <groupId>com.qiniu</groupId>
  <artifactId>qiniu-java-sdk</artifactId>
  <version>7.2.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完成圖片上傳和刪除,我們可以參考官方提供的例子。

//構造一個帶指定Zone對象的配置類
Configuration cfg = new Configuration(Zone.zone0());
//...其他參數參考類注釋
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
    }
}
//構造一個帶指定Zone對象的配置類
Configuration cfg = new Configuration(Zone.zone0());
//...其他參數參考類注釋

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 封裝工具類

為了方便操作七牛雲存儲服務,我們可以將官方提供的案例簡單改造成一個工具類,在我們的項目中直接使用此工具類來操作就可以:

package com.itheima.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;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

/**
 * 七牛雲工具類
 */
public class QiniuUtils {
    public  static String accessKey = "dulF9Wze9bxujtuRvu3yyYb9JX1Sp23jzd3tO708";
    public  static String secretKey = "vZkhW7iot3uWwcWz9vXfbaP4JepdWADFDHVLMZOe";
    public  static String bucket = "qiniutest";

    public static void upload2Qiniu(String filePath,String fileName){
        //構造一個帶指定Zone對象的配置類
        Configuration cfg = new Configuration(Zone.zone0());
        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
            }
        }
    }

    //上傳文件
    public static void upload2Qiniu(byte[] bytes, String fileName){
        //構造一個帶指定Zone對象的配置類
        Configuration cfg = new Configuration(Zone.zone0());
        //...其他參數參考類注釋
        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
            }
        }
    }

    //刪除文件
    public static void deleteFileFromQiniu(String fileName){
        //構造一個帶指定Zone對象的配置類
        Configuration cfg = new Configuration(Zone.zone0());
        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.do").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:

//查詢所有
@RequestMapping("/findAll")
public Result findAll(){
  List<CheckGroup> checkGroupList = checkGroupService.findAll();
  if(checkGroupList != null && checkGroupList.size() > 0){
    Result result = new Result(true, MessageConstant.QUERY_CHECKGROUP_SUCCESS);
    result.setData(checkGroupList);
    return result;
  }
  return new Result(false,MessageConstant.QUERY_CHECKGROUP_FAIL);
}

CheckGroupService:

List<CheckGroup> findAll();

CheckGroupServiceImpl:

public List<CheckGroup> findAll() {
  return checkGroupDao.findAll();
}

CheckGroupDao:

List<CheckGroup> findAll();

CheckGroupDao.xml:

<select id="findAll" resultType="com.itheima.pojo.CheckGroup">
  select * from t_checkgroup
</select>

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.do"
           :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)定義對應的鈎子函數:

//文件上傳成功后的鈎子,response為服務端返回的值,file為當前上傳的文件封裝成的js對象
handleAvatarSuccess(response, file) {
  this.imageUrl = "http://pqjroc654.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,接收上傳的文件

package com.itheima.controller;

import com.alibaba.dubbo.config.annotation.Reference;
import com.itheima.constant.MessageConstant;
import com.itheima.entity.PageResult;
import com.itheima.entity.QueryPageBean;
import com.itheima.entity.Result;
import com.itheima.pojo.CheckGroup;
import com.itheima.pojo.Setmeal;
import com.itheima.service.CheckGroupService;
import com.itheima.service.SetmealService;
import com.itheima.utils.QiniuUtils;
import org.springframework.web.bind.annotation.RequestBody;
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 javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.util.List;
import java.util.UUID;
/**
 * 套餐管理
 */
@RestController
@RequestMapping("/setmeal")
public class SetmealController {
    @Reference
    private SetmealService setmealService;
  
  	//圖片上傳
    @RequestMapping("/upload")
    public Result upload(@RequestParam("imgFile")MultipartFile imgFile){
        try{
            //獲取原始文件名
            String originalFilename = imgFile.getOriginalFilename();
            int lastIndexOf = originalFilename.lastIndexOf(".");
            //獲取文件后綴
            String suffix = originalFilename.substring(lastIndexOf - 1);
            //使用UUID隨機產生文件名稱,防止同名文件覆蓋
            String fileName = UUID.randomUUID().toString() + suffix;
            QiniuUtils.upload2Qiniu(imgFile.getBytes(),fileName);
            //圖片上傳成功
            Result result = new Result(true, MessageConstant.PIC_UPLOAD_SUCCESS);
            result.setData(fileName);
            return result;
        }catch (Exception e){
            e.printStackTrace();
            //圖片上傳失敗
            return new Result(false,MessageConstant.PIC_UPLOAD_FAIL);
        }
    }
}

注意:別忘了在spring配置文件中配置文件上傳組件

<!--文件上傳組件-->
<bean id="multipartResolver" 
      class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
  <property name="maxUploadSize" value="104857600" />
  <property name="maxInMemorySize" value="4096" />
  <property name="defaultEncoding" value="UTF-8"/>
</bean>

2.2.4 提交請求

當用戶點擊新增窗口中的確定按鈕時發送ajax請求將數據提交到后台進行數據庫操作。提交到后台的數據分為兩部分:套餐基本信息(對應的模型數據為formData)和檢查組id數組(對應的模型數據為checkgroupIds)。

為確定按鈕綁定單擊事件,對應的處理函數為handleAdd

<el-button type="primary" @click="handleAdd()">確定</el-button>

完善handleAdd方法

//添加
handleAdd () {
  axios.post("/setmeal/add.do?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中增加方法

//新增
@RequestMapping("/add")
public Result add(@RequestBody Setmeal setmeal, Integer[] checkgroupIds){
  try {
    setmealService.add(setmeal,checkgroupIds);
  }catch (Exception e){
    //新增套餐失敗
    return new Result(false,MessageConstant.ADD_SETMEAL_FAIL);
  }
  //新增套餐成功
  return new Result(true,MessageConstant.ADD_SETMEAL_SUCCESS);
}

2.3.2 服務接口

創建SetmealService接口並提供新增方法

package com.itheima.service;

import com.itheima.entity.PageResult;
import com.itheima.pojo.CheckGroup;
import com.itheima.pojo.Setmeal;
import java.util.List;
/**
 * 體檢套餐服務接口
 */
public interface SetmealService {
    public void add(Setmeal setmeal, Integer[] checkgroupIds);
}

2.3.3 服務實現類

創建SetmealServiceImpl服務實現類並實現新增方法

package com.itheima.service;

import com.alibaba.dubbo.config.annotation.Service;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.itheima.dao.SetmealDao;
import com.itheima.entity.PageResult;
import com.itheima.pojo.Setmeal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * 體檢套餐服務實現類
 */
@Service(interfaceClass = SetmealService.class)
@Transactional
public class SetmealServiceImpl implements SetmealService {
    @Autowired
    private SetmealDao setmealDao;

    //新增套餐
    public void add(Setmeal setmeal, Integer[] checkgroupIds) {
        setmealDao.add(setmeal);
        if(checkgroupIds != null && checkgroupIds.length > 0){
            //綁定套餐和檢查組的多對多關系
            setSetmealAndCheckGroup(setmeal.getId(),checkgroupIds);
        }
    }
    //綁定套餐和檢查組的多對多關系
    private void setSetmealAndCheckGroup(Integer id, Integer[] checkgroupIds) {
        for (Integer checkgroupId : checkgroupIds) {
            Map<String,Integer> map = new HashMap<>();
            map.put("setmeal_id",id);
            map.put("checkgroup_id",checkgroupId);
            setmealDao.setSetmealAndCheckGroup(map);
        }
    }
}

2.3.4 Dao接口

創建SetmealDao接口並提供相關方法

package com.itheima.dao;

import com.itheima.pojo.Setmeal;
import java.util.Map;

public interface SetmealDao {
    public void add(Setmeal setmeal);
    public void setSetmealAndCheckGroup(Map<String, Integer> map);
}

2.3.5 Mapper映射文件

創建SetmealDao.xml文件並定義相關SQL語句

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.itheima.dao.SetmealDao" >
    <!--新增-->
    <insert id="add" parameterType="com.itheima.pojo.Setmeal">
        <selectKey resultType="java.lang.Integer" order="AFTER" keyProperty="id">
            SELECT LAST_INSERT_ID()
        </selectKey>
        insert into t_setmeal
      		(code,name,sex,age,helpCode,price,remark,attention,img)
        		values 
      		(#{code},#{name},#{sex},#{age},#{helpCode},#{price},#{remark},#{attention},#{img})
    </insert>
    <!--綁定套餐和檢查組多對多關系-->
    <insert id="setSetmealAndCheckGroup" parameterType="hashmap">
        insert into t_setmeal_checkgroup
      		(setmeal_id,checkgroup_id) 
      			values
      		(#{setmeal_id},#{checkgroup_id})
    </insert>
</mapper>

2.4 完善文件上傳

前面我們已經完成了文件上傳,將圖片存儲在了七牛雲服務器中。但是這個過程存在一個問題,就是如果用戶只上傳了圖片而沒有最終保存套餐信息到我們的數據庫,這時我們上傳的圖片就變為了垃圾圖片。對於這些垃圾圖片我們需要定時清理來釋放磁盤空間。這就需要我們能夠區分出來哪些是垃圾圖片,哪些不是垃圾圖片。如何實現呢?

方案就是利用redis來保存圖片名稱,具體做法為:

1、當用戶上傳圖片后,將圖片名稱保存到redis的一個Set集合中,例如集合名稱為setmealPicResources

2、當用戶添加套餐后,將圖片名稱保存到redis的另一個Set集合中,例如集合名稱為setmealPicDbResources

3、計算setmealPicResources集合與setmealPicDbResources集合的差值,結果就是垃圾圖片的名稱集合,清理這些圖片即可

本小節我們先來完成前面2個環節,第3個環節(清理圖片環節)在后面會通過定時任務再實現。

實現步驟:

(1)在health_backend項目中提供Spring配置文件spring-redis.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" 
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
                         http://www.springframework.org/schema/beans/spring-beans.xsd
        				http://www.springframework.org/schema/mvc 
                         http://www.springframework.org/schema/mvc/spring-mvc.xsd
        				http://code.alibabatech.com/schema/dubbo 
                         http://code.alibabatech.com/schema/dubbo/dubbo.xsd
        				http://www.springframework.org/schema/context 
                         http://www.springframework.org/schema/context/spring-context.xsd">

	<!--Jedis連接池的相關配置-->
	<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
		<property name="maxTotal">
			<value>200</value>
		</property>
		<property name="maxIdle">
			<value>50</value>
		</property>
		<property name="testOnBorrow" value="true"/>
		<property name="testOnReturn" value="true"/>
	</bean>
	<bean id="jedisPool" class="redis.clients.jedis.JedisPool">
		<constructor-arg name="poolConfig" ref="jedisPoolConfig" />
		<constructor-arg name="host" value="127.0.0.1" />
		<constructor-arg name="port" value="6379" type="int" />
		<constructor-arg name="timeout" value="30000" type="int" />
	</bean>
</beans>

(2)在health_common工程中提供Redis常量類

package com.itheima.constant;

public class RedisConstant {
    //套餐圖片所有圖片名稱
    public static final String SETMEAL_PIC_RESOURCES = "setmealPicResources";
    //套餐圖片保存在數據庫中的圖片名稱
    public static final String SETMEAL_PIC_DB_RESOURCES = "setmealPicDbResources";
}

(3)完善SetmealController,在文件上傳成功后將圖片名稱保存到redis集合中

@Autowired
private JedisPool jedisPool;
//圖片上傳
@RequestMapping("/upload")
public Result upload(@RequestParam("imgFile")MultipartFile imgFile){
  try{
    //獲取原始文件名
    String originalFilename = imgFile.getOriginalFilename();
    int lastIndexOf = originalFilename.lastIndexOf(".");
    //獲取文件后綴
    String suffix = originalFilename.substring(lastIndexOf - 1);
    //使用UUID隨機產生文件名稱,防止同名文件覆蓋
    String fileName = UUID.randomUUID().toString() + suffix;
    QiniuUtils.upload2Qiniu(imgFile.getBytes(),fileName);
    //圖片上傳成功
    Result result = new Result(true, MessageConstant.PIC_UPLOAD_SUCCESS);
    result.setData(fileName);
    //將上傳圖片名稱存入Redis,基於Redis的Set集合存儲
    jedisPool.getResource().sadd(RedisConstant.SETMEAL_PIC_RESOURCES,fileName);
    return result;
  }catch (Exception e){
    e.printStackTrace();
    //圖片上傳失敗
    return new Result(false,MessageConstant.PIC_UPLOAD_FAIL);
  }
}

(4)在health_service_provider項目中提供Spring配置文件applicationContext-redis.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" 
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
                         http://www.springframework.org/schema/beans/spring-beans.xsd
        				http://www.springframework.org/schema/mvc 
                         http://www.springframework.org/schema/mvc/spring-mvc.xsd
        				http://code.alibabatech.com/schema/dubbo 
                         http://code.alibabatech.com/schema/dubbo/dubbo.xsd
        				http://www.springframework.org/schema/context 
                         http://www.springframework.org/schema/context/spring-context.xsd">

	<!--Jedis連接池的相關配置-->
	<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
		<property name="maxTotal">
			<value>200</value>
		</property>
		<property name="maxIdle">
			<value>50</value>
		</property>
		<property name="testOnBorrow" value="true"/>
		<property name="testOnReturn" value="true"/>
	</bean>
	<bean id="jedisPool" class="redis.clients.jedis.JedisPool">
		<constructor-arg name="poolConfig" ref="jedisPoolConfig" />
		<constructor-arg name="host" value="127.0.0.1" />
		<constructor-arg name="port" value="6379" type="int" />
		<constructor-arg name="timeout" value="30000" type="int" />
	</bean>
</beans>

(5)完善SetmealServiceImpl服務類,在保存完成套餐信息后將圖片名稱存儲到redis集合中

@Autowired
private JedisPool jedisPool;
//新增套餐
public void add(Setmeal setmeal, Integer[] checkgroupIds) {
  setmealDao.add(setmeal);
  if(checkgroupIds != null && checkgroupIds.length > 0){
    setSetmealAndCheckGroup(setmeal.getId(),checkgroupIds);
  }
  //將圖片名稱保存到Redis
  savePic2Redis(setmeal.getImg());
}
//將圖片名稱保存到Redis
private void savePic2Redis(String pic){
  jedisPool.getResource().sadd(RedisConstant.SETMEAL_PIC_DB_RESOURCES,pic);
}

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() {
  //分頁參數
  var param = {
    currentPage:this.pagination.currentPage,//頁碼
    pageSize:this.pagination.pageSize,//每頁顯示的記錄數
    queryString:this.pagination.queryString//查詢條件
  };
  //請求后台
  axios.post("/setmeal/findPage.do",param).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中增加分頁查詢方法

//分頁查詢
@RequestMapping("/findPage")
public PageResult findPage(@RequestBody QueryPageBean queryPageBean){
  PageResult pageResult = setmealService.pageQuery(
    queryPageBean.getCurrentPage(), 
    queryPageBean.getPageSize(), 
    queryPageBean.getQueryString()
  );
  return pageResult;
}

3.2.2 服務接口

在SetmealService服務接口中擴展分頁查詢方法

public PageResult pageQuery(Integer currentPage, Integer pageSize, String queryString);

3.2.3 服務實現類

在SetmealServiceImpl服務實現類中實現分頁查詢方法,基於Mybatis分頁助手插件實現分頁

public PageResult pageQuery(Integer currentPage, Integer pageSize, String queryString) {
  PageHelper.startPage(currentPage,pageSize);
  Page<CheckItem> page = checkGroupDao.selectByCondition(queryString);
  return new PageResult(page.getTotal(),page.getResult());
}

3.2.4 Dao接口

在SetmealDao接口中擴展分頁查詢方法

public Page<Setmeal> selectByCondition(String queryString);

3.2.5 Mapper映射文件

在SetmealDao.xml文件中增加SQL定義

<!--根據條件查詢-->
<select id="selectByCondition" parameterType="string" resultType="com.itheima.pojo.Setmeal">
  select * from t_setmeal
  <if test="value != null and value.length > 0">
    where code = #{value} or name = #{value} or helpCode = #{value}
  </if>
</select>

4. 定時任務組件Quartz

4.1 Quartz介紹

Quartz是Job scheduling(作業調度)領域的一個開源項目,Quartz既可以單獨使用也可以跟spring框架整合使用,在實際開發中一般會使用后者。使用Quartz可以開發一個或者多個定時任務,每個定時任務可以單獨指定執行的時間,例如每隔1小時執行一次、每個月第一天上午10點執行一次、每個月最后一天下午5點執行一次等。

官網:http://www.quartz-scheduler.org/

maven坐標:

<dependency>
  <groupId>org.quartz-scheduler</groupId>
  <artifactId>quartz</artifactId>
  <version>2.2.1</version>
</dependency>
<dependency>
  <groupId>org.quartz-scheduler</groupId>
  <artifactId>quartz-jobs</artifactId>
  <version>2.2.1</version>
</dependency>

4.2 Quartz入門案例

本案例基於Quartz和spring整合的方式使用。具體步驟:

(1)創建maven工程quartzdemo,導入Quartz和spring相關坐標,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">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.itheima</groupId>
    <artifactId>quartdemo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz-jobs</artifactId>
            <version>2.2.1</version>
        </dependency>
    </dependencies>
</project>

(2)自定義一個Job

package com.itheima.jobs;
/**
 * 自定義Job
 */
public class JobDemo {
    public void run(){
        System.out.println("job execute...");
    }
}

(3)提供Spring配置文件spring-jobs.xml,配置自定義Job、任務描述、觸發器、調度工廠等

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
						http://www.springframework.org/schema/beans/spring-beans.xsd
						http://www.springframework.org/schema/mvc
						http://www.springframework.org/schema/mvc/spring-mvc.xsd
						http://code.alibabatech.com/schema/dubbo
						http://code.alibabatech.com/schema/dubbo/dubbo.xsd
						http://www.springframework.org/schema/context
						http://www.springframework.org/schema/context/spring-context.xsd">
	<!-- 注冊自定義Job -->
    <bean id="jobDemo" class="com.itheima.jobs.JobDemo"></bean>
	<!-- 注冊JobDetail,作用是負責通過反射調用指定的Job -->
    <bean id="jobDetail" 
          class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
        <!-- 注入目標對象 -->
        <property name="targetObject" ref="jobDemo"/>
        <!-- 注入目標方法 -->
        <property name="targetMethod" value="run"/>
    </bean>
    <!-- 注冊一個觸發器,指定任務觸發的時間 -->
    <bean id="myTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
        <!-- 注入JobDetail -->
        <property name="jobDetail" ref="jobDetail"/>
        <!-- 指定觸發的時間,基於Cron表達式 -->
        <property name="cronExpression">
            <value>0/10 * * * * ?</value>
        </property>
    </bean>
    <!-- 注冊一個統一的調度工廠,通過這個調度工廠調度任務 -->
    <bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <!-- 注入多個觸發器 -->
        <property name="triggers">
            <list>
                <ref bean="myTrigger"/>
            </list>
        </property>
    </bean>
</beans>

(4)編寫main方法進行測試

package com.itheima.jobs.com.itheima.app;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class App {
    public static void main(String[] args) {
        new ClassPathXmlApplicationContext("spring-jobs.xml");
    }
}

執行上面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,打包方式為war,導入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>health_parent</artifactId>
        <groupId>com.itheima</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>health_jobs</artifactId>
    <packaging>war</packaging>
    <name>health_jobs Maven Webapp</name>
    <url>http://www.example.com</url>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.itheima</groupId>
            <artifactId>health_interface</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
        </dependency>
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz-jobs</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <configuration>
                    <!-- 指定端口 -->
                    <port>83</port>
                    <!-- 請求路徑 -->
                    <path>/</path>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

(2)配置web.xml

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
  <display-name>Archetype Created Web Application</display-name>
  <!-- 加載spring容器 -->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath*:applicationContext*.xml</param-value>
  </context-param>
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
</web-app>

(3)配置log4j.properties

### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.err
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

### direct messages to file mylog.log ###
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=c:\\mylog.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

### set log levels - for more verbose logging change 'info' to 'debug' ###

log4j.rootLogger=info, stdout

(4)配置applicationContext-redis.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" 
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
                         http://www.springframework.org/schema/beans/spring-beans.xsd
        				http://www.springframework.org/schema/mvc 
                          http://www.springframework.org/schema/mvc/spring-mvc.xsd
        				http://code.alibabatech.com/schema/dubbo 
                          http://code.alibabatech.com/schema/dubbo/dubbo.xsd
        				http://www.springframework.org/schema/context
                          http://www.springframework.org/schema/context/spring-context.xsd">

	<!--Jedis連接池的相關配置-->
	<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
		<property name="maxTotal">
			<value>200</value>
		</property>
		<property name="maxIdle">
			<value>50</value>
		</property>
		<property name="testOnBorrow" value="true"/>
		<property name="testOnReturn" value="true"/>
	</bean>
	<bean id="jedisPool" class="redis.clients.jedis.JedisPool">
		<constructor-arg name="poolConfig" ref="jedisPoolConfig" />
		<constructor-arg name="host" value="127.0.0.1" />
		<constructor-arg name="port" value="6379" type="int" />
		<constructor-arg name="timeout" value="30000" type="int" />
	</bean>
</beans>

(5)配置applicationContext-jobs.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" 
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
							http://www.springframework.org/schema/beans/spring-beans.xsd
							http://www.springframework.org/schema/mvc
							http://www.springframework.org/schema/mvc/spring-mvc.xsd
							http://code.alibabatech.com/schema/dubbo
							http://code.alibabatech.com/schema/dubbo/dubbo.xsd
							http://www.springframework.org/schema/context
							http://www.springframework.org/schema/context/spring-context.xsd">
	<context:annotation-config></context:annotation-config>
	<bean id="clearImgJob" class="com.itheima.jobs.ClearImgJob"></bean>
	<bean id="jobDetail" 
          class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
		<!-- 注入目標對象 -->
		<property name="targetObject" ref="clearImgJob"/>
		<!-- 注入目標方法 -->
		<property name="targetMethod" value="clearImg"/>
	</bean>
	<!-- 注冊一個觸發器,指定任務觸發的時間 -->
	<bean id="myTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
		<!-- 注入JobDetail -->
		<property name="jobDetail" ref="jobDetail"/>
		<!-- 指定觸發的時間,基於Cron表達式 -->
		<property name="cronExpression">
			<value>0 0 2 * * ?</value>
		</property>
	</bean>
	<!-- 注冊一個統一的調度工廠,通過這個調度工廠調度任務 -->
	<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
		<!-- 注入多個觸發器 -->
		<property name="triggers">
			<list>
				<ref bean="myTrigger"/>
			</list>
		</property>
	</bean>
</beans>

(6)創建ClearImgJob定時任務類

package com.itheima.jobs;

import com.itheima.constant.RedisConstant;
import com.itheima.utils.QiniuUtils;
import org.springframework.beans.factory.annotation.Autowired;
import redis.clients.jedis.JedisPool;
import java.util.Set;

/**
 * 自定義Job,實現定時清理垃圾圖片
 */
public class ClearImgJob {
    @Autowired
    private JedisPool jedisPool;
    public void clearImg(){
        //根據Redis中保存的兩個set集合進行差值計算,獲得垃圾圖片名稱集合
        Set<String> set = 
            jedisPool.getResource().sdiff(RedisConstant.SETMEAL_PIC_RESOURCES, 
                                          RedisConstant.SETMEAL_PIC_DB_RESOURCES);
        if(set != null){
            for (String picName : set) {
                //刪除七牛雲服務器上的圖片
                QiniuUtils.deleteFileFromQiniu(picName);
                //從Redis集合中刪除圖片名稱
                jedisPool.getResource().
                    srem(RedisConstant.SETMEAL_PIC_RESOURCES,picName);
            }
        }
    }
}

視頻地址:https://www.bilibili.com/video/BV1Bo4y117zV


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM