傳智健康---十天項目總結

第一天:

基本內容如下:

從gitee拉取對應的基礎代碼。做好配置相關工作。測試頁面是否可以正常打開。

無問題
需要學習的內容:spring security

了解到這個框架的基礎作用大概是:管理請求路徑,管理用戶權限,用于登陸安全的保護。

第二天:

分配任務:我需要實現的是預約功能相關的代碼開發。需要分析表結構,書寫接口文檔。然后開發對應的代碼。

基本內容如下

1.理解五張表的關系:

套餐表 ,套餐體驗組關聯表 ,體驗組表 ,體驗組體驗項關聯表? ,體驗項目表

三張表,兩張中間表。互為多對多關系。

2.編寫接口文檔:

問題:請求路徑等不明確,首次寫接口文檔不熟練。返回內容請求內容等不明確。。。

解決方式:學習看懂前端代碼,通過查看pages目錄下的對應頁面內容找到對應的請求路徑,設計了接口文檔。

3.編寫檢查項表的crud,分頁查詢。

基礎crud,問題不大。wu

小問題在于:不熟悉代碼結構。沒有仔細查看返回值pageresult類。導致分頁查詢出現問題。

sql中我們直接對queryString做模糊查詢:

<select id="findPage" resultType="com.itheima.pojo.vo.CheckItemVO" >select id,code,name,sex,age,remark from t_checkitem<where><if test="queryString!=null and queryString!=''">and (code like concat('%',#{queryString},'%') or name like concat('%',#{queryString},'%'))</if></where>
</select>

?和之前手打的蒼穹外賣代碼對比查看了一下,發現這樣寫其實挺妙的:

在傳智健康中,我們定義了下面這樣一個類用于輔助實現分頁查詢。

/*** 封裝查詢條件*/
public class QueryPageBean implements Serializable{private Integer currentPage;//頁碼private Integer pageSize;//每頁記錄數private String queryString;//查詢條件

對應的其實是蒼穹外賣中各種分頁查詢的DTO:

package com.sky.dto;import lombok.Data;import java.io.Serializable;@Data
public class DishPageQueryDTO implements Serializable {private int page;private int pageSize;private String name;//分類idprivate Integer categoryId;//狀態 0表示禁用 1表示啟用private Integer status;}

這里相當于可以少寫很多DTO,而將查詢條件直接寫在了SQL腳本當中。

<if test="queryString!=null and queryString!=''">and (code like concat('%',#{queryString},'%') or name like concat('%',#{queryString},'%'))</if>

省略了很多代碼。學到了。

需要學習:前后端聯調相關知識。

第三天:

基本內容如下:

1.學習前后端聯調:前后端聯調:關鍵步驟、必要性與有效實踐-CSDN博客

總結:查看數據能否正常返回,統一請求方式請求路徑。

2.完成套餐相關和檢查組相關的基礎代碼。

注意點:需要實現阿里云oss的圖片上傳功能。

基礎配置:

package com.itheima.config;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@Data
@Component
@ConfigurationProperties(prefix = "health.alioss")
public class AliOssProperties {private String endpoint;private String accessKeyId;private String accessKeySecret;private String bucketName;}
package com.itheima.config;import com.itheima.common.utils.AliOssUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** 配置類,用于創建AliOssUtil對象*/
@Slf4j
@Configuration
public class OssConfiguration {@Bean@ConditionalOnMissingBeanpublic AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){log.info("開始創建阿里云文件上傳工具類對象:{}",aliOssProperties);return new AliOssUtil(aliOssProperties.getEndpoint(),aliOssProperties.getAccessKeyId(),aliOssProperties.getAccessKeySecret(),aliOssProperties.getBucketName());}
}

以及你的pom依賴,和yml文件當中的四項配置。

還有這個工具類:

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;import java.io.ByteArrayInputStream;@Data
@Slf4j
@AllArgsConstructor
public class AliOssUtil {private String endpoint;private String accessKeyId;private String accessKeySecret;private String bucketName;/*** 文件上傳** @param bytes* @param objectName* @return*/public String upload(byte[] bytes, String objectName) {// 創建OSSClient實例。OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);try {// 創建PutObject請求。ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));} catch (OSSException oe) {log.error("Caught an OSSException, which means your request made it to OSS, "+ "but was rejected with an error response for some reason.");log.error("Error Message:{}", oe.getErrorMessage());log.error("Error Code:{}",oe.getErrorCode());log.error("Request ID:{}",oe.getRequestId());log.error("Host ID:{}", oe.getHostId());} catch (ClientException ce) {log.error("Caught an ClientException, which means the client encountered "+ "a serious internal problem while trying to communicate with OSS, "+ "such as not being able to access the network.");log.error("Error Message:{}",ce.getMessage());} finally {if (ossClient != null) {ossClient.shutdown();}}//文件訪問路徑規則 https://BucketName.Endpoint/ObjectNameStringBuilder stringBuilder = new StringBuilder("https://");stringBuilder.append(bucketName).append(".").append(endpoint).append("/").append(objectName);log.info("文件上傳到:{}", stringBuilder.toString());return stringBuilder.toString();}public void delete(String objectName) {OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId,accessKeySecret);try {// 刪除文件或目錄。如果要刪除目錄,目錄必須為空。ossClient.deleteObject(bucketName, objectName);} catch (OSSException oe) {System.out.println("Caught an OSSException, which means your request made it to OSS, "+ "but was rejected with an error response for some reason.");System.out.println("Error Message:" + oe.getErrorMessage());System.out.println("Error Code:" + oe.getErrorCode());System.out.println("Request ID:" + oe.getRequestId());System.out.println("Host ID:" + oe.getHostId());} catch (ClientException ce) {System.out.println("Caught an ClientException, which means the client encountered "+ "a serious internal problem while trying to communicate with OSS, "+ "such as not being able to access the network.");System.out.println("Error Message:" + ce.getMessage());} finally {if (ossClient != null) {ossClient.shutdown();}}}
}

注意到里面相比蒼穹外賣還運用到了文件刪除的功能。

文件上傳核心代碼如下:

/*** 上傳文件* @param imgFile* @return*/@PostMapping("/upload")public Result upload(MultipartFile imgFile){log.info("上傳文件中...");//獲的原始文件的名字String originalFilename = imgFile.getOriginalFilename();//截取原始文件名-獲的擴展名String exctension = originalFilename.substring(originalFilename.lastIndexOf("."));//使用uuid構造新的文件名String newFileName = UUID.randomUUID()+ exctension;//使用阿里云工具類上傳文件try {String filePath = aliOssUtil.upload(imgFile.getBytes(), newFileName);log.info("文件上傳成功:{}",filePath);//將圖片緩存在Redis中redisTemplate.opsForSet().add(RedisConstant.SETMEAL_PIC_RESOURCES,filePath);return new Result(true,MessageConstant.PIC_UPLOAD_SUCCESS,filePath);} catch (IOException e) {log.error("文件上傳失敗{}",e.getMessage());}return new Result(false,MessageConstant.PIC_UPLOAD_FAIL);}
}

其余crud很基礎,不必細講。

需要學習的內容:redis?Redis中文網

第四天:

基本內容如下:

1.與前端工程師完成代碼的聯調,發現問題,在上傳圖片的操作過程中,點擊圖片上傳的按鈕時圖片就已經上傳成功回顯了。但是此時如果取消編輯或者新增,圖片依然存在阿里云服務器當中。會出現垃圾圖片的現象。

2.探討解決方式:

redis里面的set應該是能對比差集的,文件上傳的時候將地址保存到redis里面的一個set集合,保存數據庫成功的圖片插入到redis里面的一個set集和里面。然后再用定時任務去觸發對比,對比這兩個集合就能比較出差集了,然后去阿里云OSS里面刪除差集的內容就好了。

3.編碼解決問題:

參考文檔:

spring boot中使用redisTemplate:如何在Spring Boot中使用RedisTemplate_springboot redistemplate 使用-CSDN博客


spring boot中使用Spring Task:SpringBoot使用@Scheduled注解實現定時任務_springboot scheduled注解-CSDN博客

解決思路如下:

1.在文件上傳成功后將圖片保存到了一個redis集合中

 @Overridepublic void add(List<Integer> checkGroupIds, Setmeal setmeal) {//增加套餐SetmealMapper.add(setmeal);redisTemplate.opsForSet().add(RedisConstant.SETMEAL_PIC_DB_RESOURCES,setmeal.getImg());//添加套餐和檢查組的關聯關系if(checkGroupIds!=null&&checkGroupIds.size()>0){for (Integer checkGroupId : checkGroupIds) {SetmealMapper.addCheckGroupAndSetmeal(setmeal.getId(),checkGroupId);}}}/*** 編輯套餐* @param checkGroupIds* @param setmeal*/@Transactional@Overridepublic void edit(List<Integer> checkGroupIds, Setmeal setmeal) {//修改套餐SetmealMapper.update(setmeal);redisTemplate.opsForSet().add(RedisConstant.SETMEAL_PIC_DB_RESOURCES,setmeal.getImg());log.info("圖片添加到redis緩存");//修改套餐和檢查組的關聯關系if(checkGroupIds!=null&&checkGroupIds.size()>0){//刪除套餐和檢查組的關聯關系SetmealMapper.deleteCheckGroupAndSetmeal(setmeal.getId());//添加套餐和檢查組的關聯關系for (Integer checkGroupId : checkGroupIds) {SetmealMapper.addCheckGroupAndSetmeal(setmeal.getId(),checkGroupId);}}}

可以看到在添加和修改套餐時我們調用redisTemplate類,將新添加的image圖片添加到了redis當中的SETMEAL_PIC_DB_RESOURCES中。
2. 當套餐數據插入到數據庫后我們又將圖片名稱保存到另一個redis集合中

/*** 上傳文件* @param imgFile* @return*/@PostMapping("/upload")public Result upload(MultipartFile imgFile){log.info("上傳文件中...");//獲的原始文件的名字String originalFilename = imgFile.getOriginalFilename();//截取原始文件名-獲的擴展名String exctension = originalFilename.substring(originalFilename.lastIndexOf("."));//使用uuid構造新的文件名String newFileName = UUID.randomUUID()+ exctension;//使用阿里云工具類上傳文件try {String filePath = aliOssUtil.upload(imgFile.getBytes(), newFileName);log.info("文件上傳成功:{}",filePath);//將圖片緩存在Redis中redisTemplate.opsForSet().add(RedisConstant.SETMEAL_PIC_RESOURCES,filePath);return new Result(true,MessageConstant.PIC_UPLOAD_SUCCESS,filePath);} catch (IOException e) {log.error("文件上傳失敗{}",e.getMessage());}return new Result(false,MessageConstant.PIC_UPLOAD_FAIL);}
}

可以看到在上傳成功到數據庫后我們將圖片緩存到了redis的SETMEAL_PIC_RESOURCES中

3. 通過計算這兩個集合的差值就可以獲得所有垃圾圖片的名稱。

這一步可以寫在定時任務類當中
4.使用SpringTask定時任務,定時計算redis兩個集合的差值就是所有的垃圾圖片

package com.itheima.common;import com.itheima.common.constant.RedisConstant;
import com.itheima.common.utils.AliOssUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;import java.util.Set;@Slf4j
@Componentpublic class GarbageImageCleaner {@Autowiredprivate RedisTemplate<String,String> redisTemplate;@Autowiredprivate AliOssUtil aliOssUtil;@Scheduled(cron = "0/10 * *  * * ? ")public void cleanGarbageImages() {log.info("垃圾圖片清理任務執行了");// 獲取Redis中所有圖片的完整路徑Set<String> ossPaths = redisTemplate.opsForSet().members(RedisConstant.SETMEAL_PIC_RESOURCES);// 獲取數據庫中使用的純文件名集合Set<String> dbFileNames = redisTemplate.opsForSet().members(RedisConstant.SETMEAL_PIC_DB_RESOURCES);if (ossPaths != null && !ossPaths.isEmpty()) {for (String ossPath : ossPaths) {// 從完整路徑中提取文件名String fileName = ossPath.substring(ossPath.lastIndexOf("/") + 1);// 檢查該文件名是否在數據庫使用的集合中if (!dbFileNames.contains(fileName)) {// 文件名不在數據庫中,屬于垃圾圖片log.info("發現垃圾圖片: {}", ossPath);try {String[] split = ossPath.split("/");// 嘗試刪除OSS中的圖片aliOssUtil.delete(split[3]);log.info("成功刪除OSS圖片: {}", ossPath);// 從Redis中移除該路徑redisTemplate.opsForSet().remove(RedisConstant.SETMEAL_PIC_RESOURCES, ossPath);log.info("已從緩存中移除: {}", ossPath);} catch (Exception e) {log.error("刪除OSS圖片失敗: {}", ossPath, e);}}}}}}

可以看到這里用兩個String類型的set集合分別存儲了兩個不同常量當中所存儲的img路徑。分別是存入數據庫的和觸發過文件上傳的。

然后遍歷其中一個集合,依次對集合中圖片的完整路徑做截取,來和另一個集合比對。

如過出現了不一致的圖片路徑,則判斷為垃圾圖片,調用阿里云工具類當中的delete方法刪除,再調用remove方法移除緩存。

最后通過? @Scheduled(cron = "0/10 * * ?* * ? ")注解設置定時條件。我設置的是定時十秒鐘執行一次。

這樣就實現了垃圾圖片刪除的操作。

需要學習知識:批量導入

第五天:

基本內容如下:

分析預約管理的功能:功能很簡單,就三個,一個模板下載、一個導入預約設置、還有一個設置的每天的預約人數。

excel導入在蒼穹外賣中使用過,是用阿帕奇下的POI來實現的。資料如下。

spring boot使用POI實現導入導出:Apache POI Excel 導入、導出簡單使用_apache poi導入excel-CSDN博客

所以這次想試試新玩意,基于POI進一步實現的阿里云旗下的easyexcel

EasyExcel讀取Excel數據(含多種方式)_easyexcel讀取excel內容-CSDN博客

上文資料中主要實現了三步操作:

1.引入依賴 2.實現對excel的寫入 3.實現對excel內容的讀取。

在我分析的預約管理功能當中,并不需要實現書寫excel的過程,導入現成的excel表格實現讀取和修改數據庫就可以了。

任務如下

1. 編寫預約設置的模板。

基礎代碼已經有了,放在resource文件當中。
2. 編寫模板下載接口,下載項目resource目錄下的excel模板。

(建議學習一下請求頭請求頭,響應頭,content-type等相關屬性的作用,參考鏈接如下
MDN:Properly Configuring Server MIME Types - 學習 Web 開發 | MDN

菜鳥教程:HTTP content-type | 菜鳥教程)

 @GetMapping("/download")public ResponseEntity<Resource> downloadFile(String filename) {// 記錄下載文件的日志信息log.info("下載模板文件:{}",filename);// 通過服務方法獲取文件資源Resource resource = orderSettingService.downloadFile(filename);// 檢查獲取的文件資源是否為空if (resource != null) {try {// 如果文件資源存在,構建HTTP響應,設置響應頭、內容長度、內容類型,并將文件作為響應體返回return ResponseEntity.ok().header("Content-Disposition", "attachment;filename=" + filename).contentLength(resource.contentLength()).contentType(MediaType.APPLICATION_OCTET_STREAM).body(resource);//以application開頭的媒體格式類型:////application/xhtml+xml :XHTML格式//application/xml: XML數據格式//application/atom+xml :Atom XML聚合格式//application/json: JSON數據格式//application/pdf:pdf格式//application/msword : Word文檔格式//application/octet-stream : 二進制流數據(如常見的文件下載)} catch (IOException e) {// 捕獲IO異常,記錄文件下載失敗的錯誤信息log.error("文件下載失敗{}",e.getMessage());}}// 如果文件資源為空或下載失敗,返回404未找到的HTTP響應return ResponseEntity.notFound().build();}
 /*** 下載文件** @param filename*/@Overridepublic Resource downloadFile(String filename) {// 構建文件路徑String filePath = "bxg-health-backend\\src\\main\\resources\\templates\\" + filename;File file = new File(filePath);// 檢查文件是否存在if (!file.exists()) {log.error("文件不存在:{}", filePath);}// 創建文件資源對象Resource resource = new FileSystemResource(file);return resource;}

代碼解析:

我調用download方法將架構中template包下的文件名為filename的包返回給controller層,

這里之所以用filename是發現前端代碼中是提供了固定的文件名稱的。我就把這個文件名稱傳下來用于文件獲取。其實挺好的,需要更改不同的文件只需要修改前端代碼就好了。只要文件都在template包下。

然后對文件構建了http響應。來讓用戶可以從網頁獲取我服務端的指定文件。

3. 編寫excel上傳接口,上傳excel文件,并將excel數據保存到MySQL

/*** 上傳文件設置預約人數* @param excelFile* @return*/@PostMapping("/upload")public Result uploadFile(MultipartFile excelFile){log.info("上傳文件中...");orderSettingService.uploadFileAndUpdate(excelFile);return new Result(true,MessageConstant.UPLOAD_SUCCESS);}/*** 上傳文件并更新預約設置*/@Overridepublic void uploadFileAndUpdate(MultipartFile excelFile) {try {// 使用EasyExcel讀取文件EasyExcel.read(excelFile.getInputStream(), null,new OrderSettingExcelListener(this)).sheet().doRead();} catch (IOException e) {throw new RuntimeException("上傳文件讀取失敗", e);}}
package com.itheima.common;import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.itheima.pojo.OrderSetting;
import com.itheima.service.OrderSettingService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;/*** Excel讀取監聽器,用于處理預約設置數據*/
public class OrderSettingExcelListener extends AnalysisEventListener<Map<Integer, String>> {private static final Logger log = LoggerFactory.getLogger(OrderSettingExcelListener.class);private static final int BATCH_SIZE = 20; // 每批處理的數據量private final OrderSettingService orderSettingService;private List<OrderSetting> orderSettings = new ArrayList<>();private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");public OrderSettingExcelListener(OrderSettingService orderSettingService) {this.orderSettingService = orderSettingService;}/*** 逐行讀取Excel內容*/@Overridepublic void invoke(Map<Integer, String> data, AnalysisContext context) {// 跳過表頭行if (context.readRowHolder().getRowIndex() == 0) {return;}try {// 解析Excel行數據String orderDateStr = data.get(0);String numberStr = data.get(1);if (orderDateStr == null || numberStr == null) {log.warn("無效的Excel行數據,跳過: {}", data);return;}// 轉換日期格式Date orderDate = dateFormat.parse(orderDateStr);int number = Integer.parseInt(numberStr);// 創建OrderSetting對象OrderSetting orderSetting = new OrderSetting();orderSetting.setOrderDate(orderDate);orderSetting.setNumber(number);orderSettings.add(orderSetting);// 達到批處理數量時執行入庫if (orderSettings.size() >= BATCH_SIZE) {saveData();}} catch (ParseException e) {log.error("日期解析錯誤: {}", data, e);} catch (NumberFormatException e) {log.error("數量格式錯誤: {}", data, e);} catch (Exception e) {log.error("處理Excel行時發生未知錯誤: {}", data, e);}}/*** 所有數據解析完成后調用*/@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {// 處理剩余數據saveData();log.info("Excel文件解析完成,共處理 {} 條數據", orderSettings.size());}/*** 保存數據到數據庫*/private void saveData() {if (!orderSettings.isEmpty()) {try {orderSettingService.addBatch(orderSettings);log.info("批量插入 {} 條預約設置數據", orderSettings.size());orderSettings.clear(); // 清空集合} catch (Exception e) {log.error("批量插入預約設置數據失敗", e);// 可添加失敗重試邏輯或事務回滾邏輯}}}
}

代碼邏輯:

創建文件上傳的接口,在實現類中調用監聽器來實現讀取和保存。監聽器中方法是仿照其他人書寫的easyexcel監聽器來寫的。作用分別是逐行讀取,整理數據內容并保存數據。

/*** 批量添加預約設置*/@Overridepublic void addBatch(List<OrderSetting> orderSettings) {if (orderSettings == null || orderSettings.isEmpty()) {return;}// 優化批量插入性能// 這里采用分批處理,避免一次性處理過多數據導致內存問題// 先檢查日期是否已存在Map<String, Object> paramMap = new HashMap<>();List<Date> dates = new ArrayList<>();for (OrderSetting os : orderSettings) {dates.add(os.getOrderDate());}paramMap.put("dates", dates);// 查詢已存在的日期List<Date> existDates = orderSettingMapper.findOrderDateByDates(paramMap);// 分離需要插入和更新的數據List<OrderSetting> insertList = new ArrayList<>();List<OrderSetting> updateList = new ArrayList<>();for (OrderSetting os : orderSettings) {if (existDates.contains(os.getOrderDate())) {updateList.add(os);} else {insertList.add(os);}}// 執行批量插入和更新if (!insertList.isEmpty()) {orderSettingMapper.addBatch(insertList);}if (!updateList.isEmpty()) {orderSettingMapper.updateBatch(updateList);}}

在添加數據的方法中我做了數據判斷,根據時間?將數據分為存在相同時間的數據和新數據。

存在相同時間的數據執行更新操作,新數據執行插入操作。防止同一時間存在多個數據導致的bug。

    <!-- 批量添加預約設置 --><insert id="addBatch">INSERT INTO t_ordersetting (orderDate, number, reservations)VALUES<foreach collection="list" item="item" separator=",">(#{item.orderDate}, #{item.number}, 0)</foreach></insert><!--  批量更新預約設置<update id="updateBatch"><foreach collection="list" item="item" separator=";">UPDATE t_ordersettingSET number = #{item.number}WHERE orderDate = #{item.orderDate}</foreach></update>--><!--MyBatis 會自動為每個 item 執行一次 UPDATE,雖然性能略差,但兼容性和安全性更高。--><!--<update id="updateBatch"><foreach collection="list" item="item">UPDATE t_ordersettingSET number = #{item.number}WHERE orderDate = #{item.orderDate}</foreach></update>--><!--究極解決方法,,,,,不太懂--><update id="updateBatch">UPDATE t_ordersetting<trim prefix="SET number = CASE orderDate" suffix="END"><foreach collection="list" item="item">WHEN #{item.orderDate} THEN #{item.number}</foreach></trim>WHERE orderDate IN<foreach collection="list" item="item" open="(" separator="," close=")">#{item.orderDate}</foreach></update>
</mapper>

?由于我的操作,我就需要自己寫批量新增的sql,自己沒寫明白,ai教的。


4. 編寫設置每天可預約人數功能接口

    /**** 編輯預約設置* @param orderSettingDTO* @return*/@PostMapping("/editNumberByOrderDate")public Result editNumberByOrderDate(@RequestBody OrderSettingDTO orderSettingDTO) {log.info("編輯預約設置:{}", orderSettingDTO);try {orderSettingService.editNumberByOrderDate(orderSettingDTO);} catch (Exception e) {return new Result(false, MessageConstant.ORDERSETTING_FAIL);}return new Result(true, MessageConstant.ORDERSETTING_SUCCESS);}/*** 根據預約日期修改預約人數** @param orderSettingDTO*/@Overridepublic void editNumberByOrderDate(OrderSettingDTO orderSettingDTO) {OrderSetting orderSetting = new OrderSetting();BeanUtils.copyProperties(orderSettingDTO, orderSetting);//把orderSetting中的日期格式化成yyyy-MM-ddSimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");String formattedDate = sdf.format(orderSetting.getOrderDate());try {//sdf.parse(formattedDate)//把格式化后的日期字符串(格式為yyyy-MM-dd)轉回java.util.Date對象。//new java.sql.Date(...)//借助時間戳創建java.sql.Date對象。這里使用的java.sql.Date是專門為 SQL 日期字段設計的,它只包含年月日信息,不包含時分秒。orderSetting.setOrderDate(new java.sql.Date(sdf.parse(formattedDate).getTime()));} catch (ParseException e) {log.info("日期格式化失敗");}orderSettingMapper.editNumberByOrderDate(orderSetting);}

這段代碼中修改時間的那段感覺有些多余,不過留著也不出錯。

因為接口文檔中的時間就是yyyy-mm-dd的格式,其次我在dto中也規定了時間格式

  @DateTimeFormat(pattern = "yyyy-MM-dd")private Date orderDate;//預約設置日期


5. 編寫預約設置列表查詢接口

    /**** @param month* @return*/@GetMapping("/getOrderSettingByMonth")public Result getOrderSettingByMonth(String month) {log.info("根據月份獲取預約設置數據:{}", month);if (month.split("-").length == 2 && month.split("-")[1].length() == 1) {month = month.split("-")[0] + "-0" + month.split("-")[1];}List<OrderSettingVO> orderLists = orderSettingService.getOrderSettingByMonth(month);return new Result(true, MessageConstant.GET_ORDERSETTING_SUCCESS, orderLists);}/*** 根據月份獲取預約設置數據** @param month* @return*/@Overridepublic List<OrderSettingVO> getOrderSettingByMonth(String month) {log.info("根據月份獲取預約設置數據:{}", month);List<OrderSettingVO> orderLists = orderSettingMapper.getOrderSettingByMonth(month);return orderLists;}/*** 根據月份查詢預約設置* @param month* @return*/@Select("select day(orderDate) as date,number,reservations from t_ordersetting where DATE_FORMAT(orderDate, '%Y-%m') = #{month}")List<OrderSettingVO> getOrderSettingByMonth(String month);/*** 根據月份查詢預約設置* @param month* @return*/@Select("select day(orderDate) as date,number,reservations from t_ordersetting where DATE_FORMAT(orderDate, '%Y-%m') = #{month}")List<OrderSettingVO> getOrderSettingByMonth(String month);

由于接口文檔中接收到的數據是個月份,就根據這個月份做了一系列的時間判斷,搞得人頭大,還好管用。

這個方法接收一個形如?yyyy-MM?的月份參數(例如:2023-6?或?2023-06),然后:

  1. 格式檢查:驗證月份是否為?yyyy-M?格式(即月份部分只有 1 位數字)。
  2. 格式轉換:將?yyyy-M?格式轉換為?yyyy-MM?格式(補零)。
  3. 業務處理:調用?orderSettingService.getOrderSettingByMonth()?獲取該月的預約設置數據。
  1. 分割字符串:使用?-?分割月份參數,例如:
    • 2023-6?→?["2023", "6"]
    • 2023-06?→?["2023", "06"]
  2. 條件判斷
    • month.split("-").length == 2:確保參數是?yyyy-M?或?yyyy-MM?格式。
    • month.split("-")[1].length() == 1:檢查月份部分是否只有 1 位數字(如?6)。
  3. 格式轉換
    • 如果是?2023-6,則轉換為?2023-06
    • 如果已經是?2023-06,則保持不變。

最后的sql語法解析:

  • 字段選擇
    • day(orderDate) as date:提取日期中的 "日" 部分(如 1-31),并重命名為date
    • number:可預約的最大數量。
    • reservations:已預約的數量。
  • 表名t_ordersetting(存儲預約設置的表)。
  • 條件過濾
    • DATE_FORMAT(orderDate, '%Y-%m') = #{month}:將日期格式化為YYYY-MM格式后,與傳入的month參數進行比較。例如:2023-06

這樣預約管理的代碼就算搞定了。

需要學習的知識:阿里云短信?

第六天:

基本內容如下:

1. 編寫套餐列表查詢,不需要分頁。

【注意】:頁面原型有根據性別查詢,實戰中不用。直接查詢所有列表即可

2. 根據套餐id查詢套餐詳情接口。

【注意】:在套餐詳情頁面需要展示當前套餐的信息(包括圖片、套餐名稱、套餐介紹、適用性別、適用年齡)、此套餐包含的檢查組信息、檢查組包含的檢查項信息等。)

難點:多表聯查:?MyBatis學習之多表操作_mybatis多表-CSDN博客

代碼實現如下:

這一段就是移動端的代碼開發了。


@RestController
@RequestMapping("/setmeal")
@Slf4j
public class SetmealController {@Autowiredprivate SetmealService setmealService;/*** 查詢所有套餐* @return*/@RequestMapping("/getSetmeal")public Result findAll(){log.info("查詢所有套餐");List<Setmeal> setmeals =setmealService.findAll();return new Result(true, MessageConstant.QUERY_SETMEAL_SUCCESS,setmeals);}/*** 根據套餐id查詢套餐詳情* @param id* @return*/@PostMapping("/findById")public Result findById(Integer id){log.info("查詢套餐詳情");Setmeal setmeal = setmealService.findById(id);return new Result(true,MessageConstant.QUERY_SETMEAL_SUCCESS,setmeal);}
}

    <!--    配置映射關系--><resultMap id="SetmealResultMap" type="com.itheima.pojo.Setmeal"><id property="id" column="setmeal_id"/><result property="name" column="setmeal_name"/><result property="code" column="setmeal_code"/><result property="helpCode" column="setmeal_helpCode"/><result property="sex" column="setmeal_sex"/><result property="age" column="setmeal_age"/><result property="price" column="setmeal_price"/><result property="remark" column="setmeal_remark"/><result property="attention" column="setmeal_attention"/><result property="img" column="setmeal_img"/><collection property="checkGroups" ofType="com.itheima.pojo.CheckGroup" resultMap="CheckGroupResultMap"></collection></resultMap><resultMap id="CheckGroupResultMap" type="com.itheima.pojo.CheckGroup"><id property="id" column="checkgroup_id"/><result property="code" column="checkgroup_code"/><result property="name" column="checkgroup_name"/><result property="helpCode" column="checkgroup_helpCode"/><result property="sex" column="checkgroup_sex"/><result property="remark" column="checkgroup_remark"/><result property="attention" column="checkgroup_attention"/><collection property="checkItems" ofType="com.itheima.pojo.CheckItem" resultMap="CheckItemResultMap"></collection></resultMap><resultMap id="CheckItemResultMap" type="com.itheima.pojo.CheckItem"><id property="id" column="checkitem_id"/><result property="code" column="checkitem_code"/><result property="name" column="checkitem_name"/><result property="sex" column="checkitem_sex"/><result property="age" column="checkitem_age"/><result property="price" column="checkitem_price"/><result property="type" column="checkitem_type"/><result property="remark" column="checkitem_remark"/><result property="attention" column="checkitem_attention"/></resultMap><select id="findById" resultMap="SetmealResultMap">select sm.id AS setmeal_id,sm.name AS setmeal_name,sm.code AS setmeal_code,sm.helpCode AS setmeal_helpCode,sm.sex AS setmeal_sex,sm.age AS setmeal_age,sm.price AS setmeal_price,sm.remark AS setmeal_remark,sm.attention AS setmeal_attention,sm.img AS setmeal_img,cg.id AS checkgroup_id,cg.code AS checkgroup_code,cg.name AS checkgroup_name,cg.helpCode AS checkgroup_helpCode,cg.sex AS checkgroup_sex,cg.remark AS checkgroup_remark,cg.attention AS checkgroup_attention,ci.id AS checkitem_id,ci.code AS checkitem_code,ci.name AS checkitem_name,ci.sex AS checkitem_sex,ci.age AS checkitem_age,ci.price AS checkitem_price,ci.type AS checkitem_type,ci.remark AS checkitem_remark,ci.attention AS checkitem_attentionFROM t_setmeal smLEFT JOINt_setmeal_checkgroup smcg ON sm.id = smcg.setmeal_idLEFT JOINt_checkgroup cg ON smcg.checkgroup_id = cg.idLEFT JOINt_checkgroup_checkitem cgci ON cg.id = cgci.checkgroup_idLEFT JOINt_checkitem ci ON cgci.checkitem_id = ci.idWHERE sm.id = #{id};</select>

這一段是給我寫吐了,大體代碼思路是三張表分別指定查詢字段和別名。然后用collection標簽關聯。套餐關聯檢查組,檢查組關聯檢查項。然后查詢字段中:se是套餐表別名,cg是檢查組表別名,ci是檢查項表別名。再用五個表(三個表+兩個中間表)左外連連連,實現的多表聯查。

除了這個查詢惡心其他到沒啥問題。

3.編寫用戶預約功能。

思路如下:

1. 驗證碼保存到redis,保存5分鐘,超時自動刪除。

首先,阿里云短信需要公司資質,俺沒這個條件。就隨機生成隨機數當驗證碼存儲redis替代啦。

?隨機生成驗證碼如下:


/*** 隨機生成驗證碼工具類*/
public class ValidateCodeUtils {public static final Integer CODE_NUMBER = 4;/*** 隨機生成驗證碼* @param length 長度為4位或者6位* @return*/public static Integer generateValidateCode(int length){Integer code =null;if(length == 4){code = new Random().nextInt(9999);//生成隨機數,最大為9999if(code < 1000){code = code + 1000;//保證隨機數為4位數字}}else if(length == 6){code = new Random().nextInt(999999);//生成隨機數,最大為999999if(code < 100000){code = code + 100000;//保證隨機數為6位數字}}else{throw new RuntimeException("只能生成4位或6位數字驗證碼");}return code;}/*** 隨機生成指定長度字符串驗證碼* @param length 長度* @return*/public static String generateValidateCode4String(int length){Random rdm = new Random();String hash1 = Integer.toHexString(rdm.nextInt());String capstr = hash1.substring(0, length);return capstr;}
}

實現類中將驗證碼存入redis中,并且配置有效時間。

 /*** 發送驗證碼* @param telephone* @return*/@Overridepublic String getCode(String telephone) {//引用工具類中定義好的常量作為length,來生成對應位數的驗證碼String code = ValidateCodeUtils.generateValidateCode4String(ValidateCodeUtils.CODE_NUMBER);log.info("{}的驗證碼為:{}",telephone,code);//將驗證碼通過 redisTemplate.opsForValue().set() 存入 Redis,并設置過期時間為 5 分鐘。redisTemplate.opsForValue().set(telephone, code,5, TimeUnit.MINUTES);return code;}

2.?校驗用戶輸入的驗證碼是否正確。

3. 預約日期的預約人數沒有設置的話不能預約。

4.?預約日期是否已經約滿,如果已經約滿則無法預約。

5. 不能重復預約(同一個用戶在同一天預約了同一個套餐)。

6.?當前用戶不是會員,需要自動完成注冊。?

7.?更新已預約人數

上述功能判斷就寫在一起咯,發現常量類中還有很多沒用到但是可以用的常量,所以這里干脆用上:

/*** 提交預約* @return*/@RequestMapping("/submitOrder")public Result submitOrder(@RequestBody OrderDTO orderDTO) {log.info("提交預約:{}", orderDTO);Integer submitInfo=orderService.submitOrder(orderDTO);if (submitInfo == -1) {return new Result(false, MessageConstant.SELECTED_DATE_CANNOT_ORDER);}else if (submitInfo == -2){return new Result(false, MessageConstant.ORDER_FULL);}else if (submitInfo == -3){return new Result(false, MessageConstant.NOT_MEMBER);}else if (submitInfo == -4){return new Result(false, MessageConstant.HAS_ORDERED);}else if (submitInfo == -5){return new Result(false, MessageConstant.VALIDATECODE_ERROR);}else if (submitInfo > 0){return new Result(true, MessageConstant.ORDER_SUCCESS, submitInfo);}return new Result(false, MessageConstant.QUERY_ORDER_FAIL);}
@Autowiredprivate OrderSettingMapper orderSettingMapper;@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate MemberMapper memberMapper;@Overridepublic Integer submitOrder(OrderDTO orderDTO) {//拷貝dto屬性到pojo類中Order order = new Order();BeanUtils.copyProperties(orderDTO, order);Member member = new Member();BeanUtils.copyProperties(orderDTO, member);//因為手機號碼變量名不一樣,重新賦值member.setPhoneNumber(orderDTO.getTelephone());String redisValidateCode = redisTemplate.opsForValue().get(orderDTO.getTelephone());if (redisValidateCode != null && Objects.equals(redisValidateCode,orderDTO.getValidateCode())) {//驗證碼正確//查詢是否設置預約人數Integer number = orderSettingMapper.getOrderSettingByOrderDate(order.getOrderDate());if (number == null || number == 0) {return -1;}//判斷是否已經約滿Integer reservationsByOrderDate = orderSettingMapper.getReservationsByOrderDate(order.getOrderDate());if (number <= reservationsByOrderDate) {return -2;}//判斷是否是會員Integer memberId = memberMapper.getRemberIdByUserPhoneNumber(member.getPhoneNumber());if (memberId == null) {return -3;}//不能重復預約(同一個用戶在同一天預約了同一個套餐)Order orderInfo = orderMapper.findByMemberIdAndOrderDateAndSetmealId(memberId, order.getOrderDate(), order.getSetmealId());//判斷是否已經預約if (orderInfo != null) {return -4;}//預約成功,添加預約信息order.setOrderStatus(Order.ORDERSTATUS_NO);order.setOrderType(Order.ORDERTYPE_WEIXIN);order.setMemberId(memberId);orderMapper.add(order);//設置預約人數reservationsByOrderDate=reservationsByOrderDate+1;orderSettingMapper.setReservationsByOrderDate(reservationsByOrderDate,order.getOrderDate());return order.getId();} else {//驗證碼錯誤return -5;}}

還有個根據id查詢預約數據,沒什么好講的就不打出來了。

    //查詢是否設置了預約人數@Select("select number from t_ordersetting where DATE_FORMAT(orderDate, '%Y-%m-%d') = DATE_FORMAT(#{orderDate}, '%Y-%m-%d')")Integer getOrderSettingByOrderDate(Date orderDate);//查詢預約人數@Select("select reservations from t_ordersetting where DATE_FORMAT(orderDate, '%Y-%m-%d') = DATE_FORMAT(#{orderDate}, '%Y-%m-%d')")Integer getReservationsByOrderDate(Date orderDate);//預約人數+1@Update("update t_ordersetting set reservations = #{reservationsByOrderDate} where DATE_FORMAT(orderDate, '%Y-%m-%d') = DATE_FORMAT(#{orderDate}, '%Y-%m-%d')")void setReservationsByOrderDate(Integer reservationsByOrderDate, Date orderDate);

功能到此完成!

需要學習知識:強化springsecurity

第七天:

基本內容如下:

1.實現用戶端的手機號驗證碼登錄功能:

代碼實現如下:

    /*** 登錄* @param telephone* @param validateCode* @return*/@PostMapping("/login")public Result login(String telephone, String validateCode) {log.info("{}輸入的驗證碼為:{}",telephone,validateCode);String loginInfo = memberService.login(telephone, validateCode);return new Result(true, loginInfo);}
   /*** 實現登錄功能* @param telephone* @param validateCode*/public String login(String telephone, String validateCode) {//獲取redis中的驗證碼String redisValidateCode = redisTemplate.opsForValue().get(telephone);if (redisValidateCode != null && Objects.equals(redisValidateCode, validateCode)) {//驗證碼正確//判斷是否是會員Integer memberId = memberMapper.getRemberIdByUserPhoneNumber(telephone);if (memberId == null) {//不是會員,添加會員LocalDate registerTime = LocalDate.now();memberMapper.add(registerTime, telephone);}} else {//驗證碼錯誤return MessageConstant.VALIDATECODE_ERROR;}return MessageConstant.LOGIN_SUCCESS;}}

用的還是自己瞎搞的隨機生成驗證碼。讓登錄接口也調用那個方法做驗證碼判斷。多加了一個點是判斷是否是會員,如過不是會員則自動添加會員。如果有這個點的話我昨日寫的判斷是否預約成功的代碼就要改了。不需要判斷是否是會員了。

2.完成管理端用戶登陸和授權的功能。

@Service
public class SpringSecurityUserService implements UserDetailsService {//查找服務,實現查詢數據庫@Autowiredprivate UserService userService;//根據用戶名查詢數據庫中用戶信息@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 從數據庫中查詢用戶User user = userService.loadUserByUsername(username);if (user == null) {throw new UsernameNotFoundException("用戶不存在!");}// 查詢用戶的角色List<Role> roles = userService.loadRolesByUserId(user.getId());// 把角色轉換成數組//String[] roleCodes = roles.stream().map(Role::getKeyword).collect(Collectors.toList()).toArray(new String[]{});List<String> keywordList = new ArrayList<>();for (Role role : roles) {keywordList.add(role.getKeyword());}String[] roleCodes = keywordList.toArray(new String[0]);List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(roleCodes);// 設置用戶的權限user.setAuthorities(authorityList);return new CustomUserDetails(user);}
}
/*** Security配置類*/@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {/*** http請求處理方法** @param http* @throws Exception*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin()// 開啟表單認證.loginPage("/backend/login.html")// 自定義登錄頁面.loginProcessingUrl("/login")// 登錄處理Url.usernameParameter("username").passwordParameter("password")// 修改自定義表單name值..defaultSuccessUrl("/backend/pages/main.html")// 登錄成功后跳轉路徑.and().authorizeRequests().antMatchers("/backend/login.html").permitAll().anyRequest().authenticated(); //所有請求都需要登錄認證才能訪問;// 關閉csrf防護http.csrf().disable();// 允許iframe加載頁面http.headers().frameOptions().sameOrigin();}@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/backend/css/**", "/backend/img/**","/backend/js/**", "/backend/plugins/**", "/favicon.ico");}@Autowiredprivate SpringSecurityUserService userService;@Beanprotected PasswordEncoder myPasswordEncoder(){return new BCryptPasswordEncoder();}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userService).passwordEncoder(myPasswordEncoder());}
}

?代碼解析拆解如下:

1. 創建SpringSecurityUserService類實現UserDetailsService


@Service
public class SpringSecurityUserService implements UserDetailsService {//查找服務,實現查詢數據庫@Autowiredprivate UserService userService;


2. 重寫 public UserDetails loadUserByUsername(String username)方法

 @Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {


3. 調用用戶服務獲取用戶信息

   // 從數據庫中查詢用戶User user = userService.loadUserByUsername(username);if (user == null) {throw new UsernameNotFoundException("用戶不存在!");}


4. 查詢當前用戶對應的角色和權限

  // 查詢用戶的角色List<Role> roles = userService.loadRolesByUserId(user.getId());// 把角色轉換成數組//String[] roleCodes = roles.stream().map(Role::getKeyword).collect(Collectors.toList()).toArray(new String[]{});List<String> keywordList = new ArrayList<>();for (Role role : roles) {keywordList.add(role.getKeyword());}String[] roleCodes = keywordList.toArray(new String[0]);List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(roleCodes);// 設置用戶的權限user.setAuthorities(authorityList);

5. 封裝當前用戶的角色和權限

return new CustomUserDetails(user);


6. 返回帶用戶名,密碼,權限列表的user對象User(username,user.getPassword(),list)

根據前端需求定義了這個類:


public class CustomUserDetails implements UserDetails {private User user;public User getUser() {return user;}public CustomUserDetails(User user) {this.user = user;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return user.getAuthorities();}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUsername();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}

代碼開發完成

需要學習知識:學習一下Echarts

第八天:

基本內容如下:?

1.分析報表模塊的相關功能,并開始設計相關接口。

折線圖

餅圖

?

各種統計數據。。。。。。。暈

?2. 編寫會員數量折線圖接口

1. 分析會員數量折線圖原型圖,確定數據格式

/*** 會員數量統計* @return*/@GetMapping("/getMemberReport")public Result getMemberReport() {log.info("會員數量統計...");MemberReportVO memberReportVO = reportService.getMemberReport();return new Result(true, MessageConstant.GET_MEMBER_NUMBER_REPORT_SUCCESS, memberReportVO);}/*** 會員數量統計* @return*/public MemberReportVO getMemberReport() {List<LocalDate> lastDays = new ArrayList<>();// 獲取當前日期LocalDate currentDate = LocalDate.now();for (int i = 0; i < 12; i++) {// 獲取當前月份的最后一天LocalDate lastDayOfMonth = currentDate.with(TemporalAdjusters.lastDayOfMonth());lastDays.add(lastDayOfMonth);// 往前推一個月currentDate = currentDate.minusMonths(1);}//統計每月的會員數量List<Integer> memberCounts = new ArrayList<>();for (LocalDate lastDay : lastDays) {// 獲取當前月份的最后一天LocalDate lastDayOfMonth = lastDay.with(TemporalAdjusters.lastDayOfMonth());// 獲取當前月份的第一天LocalDate firstDayOfMonth = lastDay.with(TemporalAdjusters.firstDayOfMonth());// 獲取當前月份的會員數量int memberCount = 0;try {memberCount = reportMapper.getMemberCountByDate(firstDayOfMonth, lastDayOfMonth);} catch (Exception e) {e.printStackTrace();}// 獲取當前月份的會員數量memberCounts.add(memberCount);}//轉行成數組String[] monthsArray = lastDays.stream().map(LocalDate::toString).toArray(String[]::new);String[] memberCountsArray = memberCounts.stream().map(String::valueOf).toArray(String[]::new);return MemberReportVO.builder().months(monthsArray).memberCounts(memberCountsArray).build();}

這里面的vo全是自己爬前端代碼爬出來的。。自己設計要暈

3. 分析套餐預約占比餅形原型圖,確定數據格式

4. 編寫套餐預約占比餅形圖接口

   /*** 套餐占比統計* @return*/@GetMapping("/getSetmealReport")public Result getSetmealReport(){log.info("套餐占比統計...");SetmealReportVO  setmealReportVO = reportService.getSetmealReport();return new Result(true, MessageConstant.GET_SETMEAL_COUNT_REPORT_SUCCESS, setmealReportVO);}/*** 套餐數量統計* @return*/@Overridepublic SetmealReportVO getSetmealReport() {//獲取套餐namesList<String> setmealNames = reportMapper.getSetmealNames();List<SetmealCount> setmealCountsList = new ArrayList<>();for (String setmealName : setmealNames) {//獲取套餐的valueInteger setmealCount = reportMapper.getSetmealCountByName(setmealName);//封裝成SetmealCount對象SetmealCount setmealCounts = SetmealCount.builder().name(setmealName).value(setmealCount).build();//添加到集合中setmealCountsList.add(setmealCounts);}//轉換為數組String[] setmealNamesArray = setmealNames.toArray(new String[0]);SetmealCount[] setmealCountsArray = setmealCountsList.toArray(new SetmealCount[0]);return SetmealReportVO.builder().setmealNames(setmealNamesArray).setmealCounts(setmealCountsArray).build();}

2.運營數據統計接口和數據導出功能。


1. 分析運營數據統計原型圖
2. 編寫運營數據統計接口

  /*** 獲取運營數據* @return*/@GetMapping("/getBusinessReportData")public Result getBusinessReportData(){log.info("獲取運營數據...");BusinessReportVO businessReportVO = reportService.getBusinessReportData();return new Result(true, MessageConstant.GET_BUSINESS_REPORT_SUCCESS, businessReportVO);}/*** 運營數據統計* @return*/@Overridepublic BusinessReportVO getBusinessReportData() {//計算今日時間范圍LocalDate currentDate = LocalDate.now();LocalDateTime dayOfBeginTime = LocalDateTime.of(currentDate, LocalTime.MIN);LocalDateTime dayOfEndTime = LocalDateTime.of(currentDate, LocalTime.MAX);//計算本周時間范圍currentDate = currentDate.minusDays(currentDate.getDayOfWeek().getValue() - 1);LocalDate firstDayOfMonth = currentDate.with(TemporalAdjusters.firstDayOfMonth());LocalDateTime firstMonthsOfBeginTime = LocalDateTime.of(firstDayOfMonth, LocalTime.MIN);LocalDate lastDayOfMonth = currentDate.with(TemporalAdjusters.lastDayOfMonth());LocalDateTime lastMonthsOfEndTime = LocalDateTime.of(lastDayOfMonth, LocalTime.MAX);//計算本月時間范圍LocalDate firstDayOfWeek = currentDate.minusDays(currentDate.getDayOfWeek().getValue() - 1);LocalDateTime firstweeksOfBeginTime = LocalDateTime.of(firstDayOfWeek, LocalTime.MIN);LocalDate lastDayOfWeek = currentDate.plusDays(7 - currentDate.getDayOfWeek().getValue());LocalDateTime lastweeksOfEndTime = LocalDateTime.of(lastDayOfWeek, LocalTime.MAX);// 查詢今日訪問量Integer todayVisitsNumber = reportMapper.getVisitsNumberByDate(dayOfBeginTime, dayOfEndTime);// 查詢今日新增會員數Integer todayNewMember = reportMapper.getNewMemberCountByDate(dayOfBeginTime,dayOfEndTime);// 查詢本周訪問量Integer thisWeekVisitsNumber = reportMapper.getVisitsNumberByDate(firstweeksOfBeginTime,lastweeksOfEndTime);// 查詢本月新增會員數Integer thisMonthNewMember = reportMapper.getNewMemberCountByDate(firstMonthsOfBeginTime, lastMonthsOfEndTime);// 查詢本周新增會員數Integer thisWeekNewMember = reportMapper.getNewMemberCountByDate(firstweeksOfBeginTime, lastweeksOfEndTime);// 查詢總會員數Integer totalMember = reportMapper.getTotalMemberCount();// 查詢本月訂單數Integer thisMonthOrderNumber = reportMapper.getOrderNumberByDate(firstMonthsOfBeginTime, lastMonthsOfEndTime);// 查詢本月訪問量Integer thisMonthVisitsNumber = reportMapper.getVisitsNumberByDate(firstMonthsOfBeginTime, lastMonthsOfEndTime);// 查詢今日訂單數Integer todayOrderNumber = reportMapper.getOrderNumberByDate(dayOfBeginTime, dayOfEndTime);// 查詢本周訂單數Integer thisWeekOrderNumber = reportMapper.getOrderNumberByDate(firstweeksOfBeginTime, lastweeksOfEndTime);// 查詢熱門套餐List<HotSetmeal> hotSetmealList = reportMapper.getHotSetmeal();// 獲取所有的套餐總數Integer totalSetmealCount = reportMapper.getTotalSetmealCount();//計算每一種套餐的占比并賦值給proportionfor (HotSetmeal hotSetmeal : hotSetmealList) {// 計算占比,保留四位小數double proportion = (double) hotSetmeal.getSetmeal_count() / totalSetmealCount;// 格式化保留四位小數DecimalFormat df = new DecimalFormat("#0.0000");hotSetmeal.setProportion(Double.parseDouble(df.format(proportion)));}HotSetmeal[] hotSetmealArray = hotSetmealList.toArray(new HotSetmeal[0]);return BusinessReportVO.builder().todayVisitsNumber(todayVisitsNumber).reportDate(currentDate).todayNewMember(todayNewMember).thisWeekVisitsNumber(thisWeekVisitsNumber).hotSetmeal(hotSetmealArray).thisMonthNewMember(thisMonthNewMember).thisWeekNewMember(thisWeekNewMember).totalMember(totalMember).thisMonthOrderNumber(thisMonthOrderNumber).thisMonthVisitsNumber(thisMonthVisitsNumber).todayOrderNumber(todayOrderNumber).thisWeekOrderNumber(thisWeekOrderNumber).build();}


3. 跟據張楠提供的模板編寫運營數據統計導出功能

 /*** 導出PDF運營數據* @return*///TODO @PostMapping("/exportBusinessReport4PDF")@PostMapping("/exportBusinessReport4PDF")public Result exportBusinessReport4PDF(){log.info("導出PDF運營數據...");reportService.exportBusinessReport4PDF();return new Result(true, MessageConstant.GET_BUSINESS_REPORT_SUCCESS);}@Overridepublic void exportBusinessReport4PDF() {BusinessReportVO businessReportVo = getBusinessReportData();List<Map<String, Object>> dataList = new ArrayList<>();dataList.add(new HashMap<String, Object>() {{put("reportDate", businessReportVo.getReportDate());put("todayNewMember", businessReportVo.getTodayNewMember());put("totalMember", businessReportVo.getTotalMember());put("thisWeekNewMember", businessReportVo.getThisWeekNewMember());put("thisMonthNewMember", businessReportVo.getThisMonthNewMember());put("todayVisitsNumber", businessReportVo.getTodayVisitsNumber());put("thisWeekVisitsNumber", businessReportVo.getThisWeekVisitsNumber());put("thisMonthVisitsNumber", businessReportVo.getThisMonthVisitsNumber());put("todayOrderNumber", businessReportVo.getTodayOrderNumber());put("thisWeekOrderNumber", businessReportVo.getThisWeekOrderNumber());put("thisMonthOrderNumber", businessReportVo.getThisMonthOrderNumber());put("hotSetmeal", businessReportVo.getHotSetmeal());}});// 臨時Excel文件路徑String excelFilePath = "E:\\temp_report.xlsx";// 最終PDF文件路徑String pdfFilePath = "E:\\report_template.pdf";File templateFile = new File("bxg-health-backend\\src\\main\\resources\\templates\\report_template.xlsx");if (!templateFile.exists()) {throw new IllegalArgumentException("模板文件不存在: " + templateFile);}try {// 1. 先生成Excel文件try (ExcelWriter excelWriter = EasyExcel.write(excelFilePath).withTemplate(templateFile).build()) {WriteSheet writeSheet = EasyExcel.writerSheet(0).build();excelWriter.fill(dataList, writeSheet);}// 2. 將Excel轉換為PDFconvertExcelToPdf(excelFilePath, pdfFilePath);// 3. 刪除臨時Excel文件new File(excelFilePath).delete();} catch (Exception e) {log.error("導出PDF失敗", e);throw new RuntimeException("導出PDF失敗", e);}}/*** 將Excel文件轉換為PDF文件*/private void convertExcelToPdf(String excelPath, String pdfPath) throws Exception {// 加載Excel文件XSSFWorkbook workbook = new XSSFWorkbook(new FileInputStream(excelPath));// 創建PDF文檔PdfDocument pdfDoc = new PdfDocument(new PdfWriter(pdfPath));Document document = new Document(pdfDoc);// 遍歷Excel的每個sheetfor (int sheetIndex = 0; sheetIndex < workbook.getNumberOfSheets(); sheetIndex++) {XSSFSheet sheet = workbook.getSheetAt(sheetIndex);// 創建PDF表格int columnCount = getColumnCount(sheet);Table table = new Table(columnCount);// 遍歷Excel的每一行for (int rowIndex = 0; rowIndex <= sheet.getLastRowNum(); rowIndex++) {XSSFRow row = sheet.getRow(rowIndex);if (row == null) continue;// 遍歷Excel的每一列for (int colIndex = 0; colIndex < columnCount; colIndex++) {XSSFCell cell = row.getCell(colIndex);String cellValue = cell != null ? getCellValueAsString(cell) : "";// 添加單元格到PDF表格table.addCell(new Cell().add(new Paragraph(cellValue)));}}// 將表格添加到文檔document.add(table);// 如果不是最后一個sheet,添加分頁符if (sheetIndex < workbook.getNumberOfSheets() - 1) {document.add(new AreaBreak(AreaBreakType.NEXT_PAGE));}}// 關閉文檔document.close();workbook.close();}/*** 獲取Excel表格的列數*/private int getColumnCount(XSSFSheet sheet) {int maxColumnCount = 0;for (int i = 0; i <= sheet.getLastRowNum(); i++) {XSSFRow row = sheet.getRow(i);if (row != null && row.getLastCellNum() > maxColumnCount) {maxColumnCount = row.getLastCellNum();}}return maxColumnCount;}/*** 獲取單元格的值并轉換為字符串*/private String getCellValueAsString(XSSFCell cell) {switch (cell.getCellType()) {case STRING:return cell.getStringCellValue();case NUMERIC:if (DateUtil.isCellDateFormatted(cell)) {return cell.getDateCellValue().toString();} else {return String.valueOf(cell.getNumericCellValue());}case BOOLEAN:return String.valueOf(cell.getBooleanCellValue());case FORMULA:return cell.getCellFormula();default:return "";}}
}

?上圖的導入pdf是我自己用itextpdf搞的,原理就是先生成excel后轉成pdf

  /*** 導出運營數據統計* @return*/@Overridepublic void exportBusinessReport() {BusinessReportVO businessReportVo = getBusinessReportData();List<Map<String, Object>> dataList = new ArrayList<>();dataList.add(new HashMap<String, Object>() {{put("reportDate", businessReportVo.getReportDate());put("todayNewMember", businessReportVo.getTodayNewMember());put("totalMember", businessReportVo.getTotalMember());put("thisWeekNewMember", businessReportVo.getThisWeekNewMember());put("thisMonthNewMember", businessReportVo.getThisMonthNewMember());put("todayVisitsNumber", businessReportVo.getTodayVisitsNumber());put("thisWeekVisitsNumber", businessReportVo.getThisWeekVisitsNumber());put("thisMonthVisitsNumber", businessReportVo.getThisMonthVisitsNumber());put("todayOrderNumber", businessReportVo.getTodayOrderNumber());put("thisWeekOrderNumber", businessReportVo.getThisWeekOrderNumber());put("thisMonthOrderNumber", businessReportVo.getThisMonthOrderNumber());put("hotSetmeal", businessReportVo.getHotSetmeal());}});int sheetNo = 0;File templateFile = new File("bxg-health-backend\\src\\main\\resources\\templates\\report_template.xlsx");String outputFilePath = "E:\\report_template.xlsx";if (!templateFile.exists()) {throw new IllegalArgumentException("模板文件不存在: " + templateFile);}try (ExcelWriter excelWriter = EasyExcel.write(outputFilePath).withTemplate(templateFile).build()) {WriteSheet writeSheet = EasyExcel.writerSheet(sheetNo).build();excelWriter.fill(dataList, writeSheet);}}

代碼開發完成。

需要學習知識:
學習一下,并思考如何提升頁面的加載速度呢?

參考地址 :http://www.freemarker.net/

參考地址 :Freemarker學習指南-CSDN博客

第九天:

基本內容如下:

?用FreeMarker生成套餐列表和套餐詳情頁

1.修改套餐列表頁面為FreeMarker模板
2.修改套餐詳情頁面為FreeMarker模板
3. 編寫添加套餐的時候使用模板生成頁面的接口

(1)新增套餐,同時關聯檢查組

(2)完成數據庫操作后需要將圖片名稱保存到redis

(3) 新增套餐后需要重新生成靜態頁面

(4)新增套餐后需要重新生成靜態頁面

(5)生成套餐列表靜態頁面

(6)生成套餐詳情靜態頁面(多個)

框架中提供好了兩個模板,參考文檔準備好yml內的配置類,然后代碼如下補足:

//新增套餐,同時關聯檢查組public void add(Setmeal setmeal, Integer[] checkgroupIds) {//新增套餐,插入t_setmeal表,設置idSetmealMapper.add(setmeal);Integer setmealId = setmeal.getId();this.setSetmealAndCheckGroup(setmealId,checkgroupIds);//完成數據庫操作后需要將圖片名稱保存到redisString replaced = setmeal.getImg().replace("https://sky-incast-jiaxingyu.oss-cn-beijing.aliyuncs.com/", "");redisTemplate.opsForSet().add(RedisConstant.SETMEAL_PIC_DB_RESOURCES,replaced);//當添加套餐后需要重新生成靜態頁面(套餐列表頁面、套餐詳情頁面)generateMobileStaticHtml();}//生成當前方法所需的靜態頁面public void generateMobileStaticHtml(){//在生成靜態頁面之前需要查詢數據List<Setmeal> list =SetmealMapper.findAll();//需要生成套餐列表靜態頁面generateMobileSetmealListHtml(list);//需要生成套餐詳情靜態頁面generateMobileSetmealDetailHtml(list);}//生成套餐列表靜態頁面public void generateMobileSetmealListHtml(List<Setmeal> list){Map map = new HashMap();//為模板提供數據,用于生成靜態頁面map.put("setmealList",list);generteHtml("mobile_setmeal.ftl","m_setmeal.html",map);}//通用的方法,用于生成靜態頁面public void generteHtml(String templateName,String htmlPageName,Map map){Configuration configuration = freeMarkerConfigurer.getConfiguration();//獲得配置對象Writer out = null;try {Template template = configuration.getTemplate(templateName);//構造輸出流out = new FileWriter(new File(outPutPath + "/" + htmlPageName));//輸出文件template.process(map,out);out.close();} catch (Exception e) {e.printStackTrace();}}//生成套餐詳情靜態頁面(可能有多個)public void generateMobileSetmealDetailHtml(List<Setmeal> list) {for (Setmeal setmeal : list) {Map map = new HashMap();map.put("setmeal", SetmealMapper.findById4Detail(setmeal.getId()));generteHtml("mobile_setmeal_detail.ftl", "setmeal_detail_" + setmeal.getId() + ".html", map);}}//設置套餐和檢查組多對多關聯關系public void setSetmealAndCheckGroup(Integer setmealId,Integer[] checkgroupIds){if(checkgroupIds != null && checkgroupIds.length > 0){for (Integer checkgroupId : checkgroupIds) {Map<String,Integer> map = new HashMap<>();map.put("setmealId",setmealId);map.put("checkgroupId",checkgroupId);SetmealMapper.setSetmealAndCheckGroup(map);}}}

2.學習如何上線一個項目

部署常用的方式參考如下:如何在服務端部署SpringBoot項目_服務器部署springboot項目-CSDN博客

需要學習的知識:

如何寫項目總結:如何寫Java開發項目總結_java基礎項目總結從哪里寫-CSDN博客

參考案例:https://juejin.cn/post/6984755776543784968

第十天:

基本內容如下:

編寫項目總結:

完結撒花!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/84792.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/84792.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/84792.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

AbMole| Angiotensin II human(M6240;血管緊張素Ⅱ)

Angiotensin II&#xff08;血管緊張素II&#xff09;是一種生物活性肽和血管收縮劑。Angiotensin II作為腎素-血管緊張素-醛固酮系統&#xff08;RAAS&#xff09;的關鍵活性成分&#xff0c;在動物模型中&#xff0c;它通過調節于血管平滑肌細胞上的血管緊張素II受體&#xf…

【C/C++】gmock vs mockcpp

文章目錄 gmock vs mockcpp1 基本介紹2 語法風格與使用方式gmock 特點&#xff08;基于接口 Mock&#xff09;&#xff1a;mockcpp 特點&#xff08;基于重寫/攔截原函數&#xff09;&#xff1a; 3 對比總結4 實際使用建議 gmock vs mockcpp gmock 和 mockcpp 是 C 中常用的兩…

自己的服務器被 DDOS跟CC攻擊了怎么處理,如何抵御攻擊?

今天后臺突然彈出警報&#xff1a;服務器帶寬瞬間跑滿&#xff0c;CPU 占用率飆到 100%。刷新頁面時&#xff0c;首頁加載像卡帶般斷斷續續&#xff0c;圖片剛顯示半張就卡住&#xff0c;點擊任何按鈕都沒反應。登錄服務器一看&#xff0c;訪問日志里密密麻麻全是陌生 IP 的高頻…

icg真的只能用latch不能用Flip-flop嗎

soc設計中常用latch來做時鐘門控&#xff0c;它的rtl描述如下&#xff1a; input EN; input clk; input TE; output E_clk;always (*) beginif (clk1d0)E_latch EN | TE; endassign E_clk E_latch & clk;實際soc實現會把上面代碼中latch和與操作換成專用CLKLANQ的libcel…

基于python大數據的nba球員可視化分析系統

博主介紹&#xff1a;java高級開發&#xff0c;從事互聯網行業六年&#xff0c;熟悉各種主流語言&#xff0c;精通java、python、php、爬蟲、web開發&#xff0c;已經做了六年的畢業設計程序開發&#xff0c;開發過上千套畢業設計程序&#xff0c;沒有什么華麗的語言&#xff0…

從0開始學習R語言--Day22--km曲線

KM曲線 在分析疾病的死亡率時&#xff0c;我們往往會糾結于怎樣在邏輯架構中去考慮未死亡的人群&#xff0c;以及想研究兩種藥物的表現效果&#xff0c;但病人的指標表現都不明顯&#xff0c;作用于其他指標且很難量化。 而KM曲線可以很好地反映人群在時間序列上的生存率&…

SpringBoot ?@ControllerAdvice 處理異常

應用中的異常&#xff0c;有兩件事要考慮&#xff0c;怎么處理這個異常&#xff0c;怎么把異常可讀性高地返回給前端用戶 1.怎么把異常可讀性高的返回給前端用戶或API的消費者 自定義錯誤代碼和錯誤內容 2.怎么處理異常 比如遇到某個異常時需要發郵件通知IT團隊 Controlle…

爬百度圖片如何解決{“antiFlag“:1,“message“:“Forbid spider access“}

在學習深度學習的卷積神經算法時&#xff0c;需要貓和狗的訓練數據集。這時想到在百度網上爬取貓和狗的圖片。 在爬取狗狗圖片的時候&#xff0c;我抓包分析了下獲取這個url1 “https://image.baidu.com/search/index?tnbaiduimage&ipnr&ct201326592&cl2&lm&…

QWebEngine

Qt自帶的QWebEngine 不支持播放MP4, 需要手動編譯QWebEngine模塊 不支持播放mp4 // mainwindow.cpp , m_webEngine(new MyWebEngine(this)) void MainWindow::init() { //關閉系統代理&#xff0c;提高速度,采用release會更快QNetworkProxyFactory::setUseSystemConfigurati…

Rust 學習筆記1

Basic基礎 actix_web基礎 #[get("/favicon")] 獲取靜態圖片 #[get("/welcome")] 簡單的歡迎 #`/user/{name}/` basic.rs源碼 源碼 use std::{convert::Infallible, io};use actix_files::{Files, NamedFile}; use actix_session::{storage::Cooki…

3GPP協議PDF下載

https://www.tech-invite.com/3m38/tinv-3gpp-38.html 可以進入3GPP官網界面&#xff0c;也可以進入PDF下載界面 PDF加載比較慢

高性能服務器程序框架知識梳理

服務器編程框架 服務器程序種類有很多&#xff0c;但是基本框架都一樣&#xff0c;核心不同點在于邏輯處理單元。基本框架包含&#xff1a;I/O處理單元、邏輯單元、網絡存儲單元以及請求隊列。 I/O處理單元&#xff08;主線程&#xff09;&#xff1a;服務器用來管理客戶連接…

【AI】從0開始玩轉混元3D?模型,如何讓一張靜態實物圖片一鍵轉為3D實物圖,大模型都表示服了,超級簡單易上手,快來試試!

HAI 與 NVIDIA &#xff0c;為開發者提供一鍵部署及生圖的能力&#xff0c;讓開發者體驗3D 模型的同時&#xff0c;也了解云的便利性。 混元3D 2.0是騰訊推出的尖端3D?成模型&#xff0c;能夠創建帶有?分辨率紋理貼圖的?保真3D資產 參賽報名&#xff1a;https://marketing.c…

電路圖識圖基礎知識-電動機的保護電路保護方式(二十六)

電動機保護電路是確保電動機安全、可靠運行的關鍵技術之一。在工業和日常生活中&#xff0c;電動機被廣泛應用于各種設備中&#xff0c;其安全運行對于保障生產效率和人身安全至關重要。本文將詳細介紹電動機保護電路的重要性、保護方式以及具體的電路分析&#xff0c;以期為電…

【Pandas】pandas DataFrame droplevel

Pandas2.2 DataFrame Reshaping sorting transposing 方法描述DataFrame.droplevel(level[, axis])用于**從 DataFrame 的索引&#xff08;行或列&#xff09;中刪除指定層級&#xff08;level&#xff09;**的方法 pandas.DataFrame.droplevel() pandas.DataFrame.droplev…

Delivering Arbitrary-Modal Semantic Segmentation(CVPR2023)任意模態語義分割論文閱讀

文章目錄 文章研究思路創建了DeLiVER任意模態分割基準數據集統計信息4種模態25個語義類 提出了任意跨模態分割模型CMNeXt自查詢中心&#xff08;Self-Query Hub&#xff0c;SQ-Hub&#xff09;并行池化混合器&#xff08;Parallel Pooling Mixer&#xff0c;PPX&#xff09; 實…

進程控制

一. 進程創建 1.fork的概念與使用 在 Linux 中 fork 可以在一個進程中創建一個新的進程。這個新進程稱為子進程&#xff0c;原進程為父進程。使用前需要包含頭文件 #include <unistd.h> 。在調用 fork 函數時&#xff0c;子進程與父進程會共享數據和代碼&#xff0c;此…

造輪子系列:從0到1打造生產級HTTP客戶端,優雅封裝OkHttp/HttpClient,支持異步、重試與文件操作

TechZhi HTTP Client Starter 源碼特性快速開始1. 添加依賴2. 配置3. 使用 主要功能支持的HTTP方法文件操作功能高級功能配置示例 API使用示例基本請求自定義請求異步請求文件操作示例錯誤處理 構建和測試依賴說明 本文將介紹一款本人開發的高性能Spring Boot HTTP客戶端Starte…

Java過濾器的基本概念

概述 Java 過濾器是 Java EE (Jakarta EE) 中的一種組件&#xff0c;用于在請求到達 Servlet 或 JSP 之前對其進行預處理&#xff0c;或者在響應返回客戶端之前對其進行后處理。過濾器主要應用于以下場景&#xff1a; 請求參數過濾和轉換字符編碼處理身份驗證和授權日志記錄壓…

gbase8s數據庫獲取jdbc/odbc協議的幾種方式

PROTOCOLTRACEFILED:\sqltrace1.log;PROTOCOLTRACE5 jdbc 參數 &#xff0c;明文協議&#xff0c;并發時 會錯亂&#xff0c;適合單線程調試 SQLIDEBUGC:\Users\lenovo\Desktop\sqlidebug.log1 jdbc參數&#xff0c;密文協議&#xff0c;需使用解密工具解析&#xff0c;解析…