第一天:
基本内容如下:
从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
),然后:
- 格式检查:验证月份是否为
yyyy-M
格式(即月份部分只有 1 位数字)。 - 格式转换:将
yyyy-M
格式转换为yyyy-MM
格式(补零)。 - 业务处理:调用
orderSettingService.getOrderSettingByMonth()
获取该月的预约设置数据。
- 分割字符串:使用
-
分割月份参数,例如:2023-6
→["2023", "6"]
2023-06
→["2023", "06"]
- 条件判断:
month.split("-").length == 2
:确保参数是yyyy-M
或yyyy-MM
格式。month.split("-")[1].length() == 1
:检查月份部分是否只有 1 位数字(如6
)。
- 格式转换:
- 如果是
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
第十天:
基本内容如下:
编写项目总结:
完结撒花!