目录
复盘开发时遇到的一些问题
前言
就把自己遇到的,认为比较重要的提出来讲一讲
后端
- 修改两个SpringBoot项目各自的路径
因为这个涉及到映射容器内的日志到宿主机上,配合loki使用,而两个项目的日志路径,原本是有问题的,路径没有做区分,甚至还有undefined目录,所以如何改路径需要好好的斟酌。
- 计算工作时长
涉及到计算工作时长,排除午休和周末,暂时还没有要求排出节假日,如果要求的话,可能到时候需要将节假日维护进数据库。
大致思路是遍历开始时间和结束时间的每一天,遇到周六加两天,遇到周日加一天,将期间的工作时长(排除掉午休)累加起来,得到期间的工作时长。具体实现如下:
package com.workplat.administrate.util;
import com.workplat.administrate.constant.NumberConstants;
import java.time.*;
import java.util.Date;
/**
* @author stupi
*/
public class WorkTimeCalculateUtil {
// 上班时间
private static final LocalTime WORKING_START_TIME = LocalTime.of(9, 00);
// 下班时间
private static final LocalTime WORKING_END_TIME = LocalTime.of(17, 00);
// 午休开始时间
private static final LocalTime NOON_BREAK_START_TIME = LocalTime.of(11, 30);
// 午休结束时间
private static final LocalTime NOON_BREAK_END_TIME = LocalTime.of(13, 0);
public static Double getWorkTime(LocalDateTime startDateTime, LocalDateTime endDateTime, Boolean isSkip) {
Asserts.testFail(startDateTime.compareTo(endDateTime) > 0,"时间参数错误");
int diff = 0;
while (true) {
// TODO: 调休日期处理
// 是否需要跳过周六周日
if(isSkip){
if (startDateTime.getDayOfWeek() == DayOfWeek.SATURDAY) {
startDateTime = LocalDateTime.of(startDateTime.toLocalDate(), WORKING_START_TIME).plusDays(2);
}
if (startDateTime.getDayOfWeek() == DayOfWeek.SUNDAY) {
startDateTime = LocalDateTime.of(startDateTime.toLocalDate(), WORKING_START_TIME).plusDays(1);
}
if (endDateTime.getDayOfWeek() == DayOfWeek.SATURDAY) {
endDateTime = LocalDateTime.of(endDateTime.toLocalDate(), WORKING_START_TIME).plusDays(2);
}
if (endDateTime.getDayOfWeek() == DayOfWeek.SUNDAY) {
endDateTime = LocalDateTime.of(endDateTime.toLocalDate(), WORKING_START_TIME).plusDays(1);
}
}
// 跨天处理
if (startDateTime.getDayOfYear() == endDateTime.getDayOfYear()) {
int diffSecond = getDiffSecond(startDateTime, endDateTime);
diff = diffSecond + diff;
break;
}
int diffSecond = getDiffSecond(startDateTime, LocalDateTime.of(startDateTime.toLocalDate(), WORKING_END_TIME));
diff = diffSecond + diff;
startDateTime = LocalDateTime.of(startDateTime.toLocalDate(), WORKING_START_TIME).plusDays(1);
}
//总的小时数
double hours = Double.valueOf(diff) / 3600;
//保留1位小数
double finalHours = (Math.floor(hours * 10)) / 10;
int intPart = (int) finalHours;
double decimalPart = finalHours - intPart;
if(decimalPart >= NumberConstants.ZERO_POINT_FIVE){
finalHours = intPart + NumberConstants.ZERO_POINT_FIVE;
}else {
finalHours = intPart;
}
System.out.println("finalHours: " + finalHours);
return finalHours;
}
private static int getDiffSecond(LocalDateTime startDateTime, LocalDateTime endDateTime) {
LocalTime startTime = startDateTime.toLocalTime();
LocalTime endTime = endDateTime.toLocalTime();
// diff单位:秒
int diff = 0;
// 开始时间切移
if (startTime.isBefore(WORKING_START_TIME)) {
startTime = WORKING_START_TIME;
} else if (startTime.isAfter(NOON_BREAK_START_TIME) && startTime.isBefore(NOON_BREAK_END_TIME)) {
startTime = NOON_BREAK_START_TIME;
} else if (startTime.isAfter(WORKING_END_TIME)) {
startTime = WORKING_END_TIME;
}
// 结束时间切移
if (endTime.isBefore(WORKING_START_TIME)) {
endTime = WORKING_START_TIME;
} else if (endTime.isAfter(NOON_BREAK_START_TIME) && endTime.isBefore(NOON_BREAK_END_TIME)) {
endTime = NOON_BREAK_START_TIME;
} else if (endTime.isAfter(WORKING_END_TIME)) {
endTime = WORKING_END_TIME;
}
// 午休时间判断处理
if (startTime.compareTo(NOON_BREAK_START_TIME) <= 0 && endTime.compareTo(NOON_BREAK_END_TIME) >= 0) {
diff = diff + 60 * 60 + 60 * 60 / 2;
}
diff = endTime.toSecondOfDay() - startTime.toSecondOfDay() - diff;
return diff;
}
public static LocalDateTime DateToLocalDateTime(Date date){
Instant instant = date.toInstant();
ZoneId zoneId = ZoneId.systemDefault();
LocalDateTime localDateTime = instant.atZone(zoneId).toLocalDateTime();
return localDateTime;
}
public static void main(String[] args) {
WorkTimeCalculateUtil.getWorkTime(LocalDateTime.of(2022, 10, 23, 20, 30, 0),
LocalDateTime.of(2022, 10, 27, 13, 50, 0),Boolean.TRUE);
}
}
- mybatis中的if标签的条件不生效
原因:mybatis会自动将本是String类型而填充的是数字识别为Integer;对于单引号的内容会认为是字符(前者理解好像有一丢丢不对,因为交换双单引号对前者也用)
解决方法:前者将数字的引号去掉即可;后者要么将双引号和单引号的位置换一下,要么像这样'Y'.toString()
转换一下再比较
参考文章:
https://blog.csdn.net/qq_31594647/article/details/89429265
https://www.yht7.com/news/124481
https://blog.csdn.net/weixin_45237517/article/details/101530117 - mybatis-plus的逻辑删除
@TableLogic,该注解对应mybatis-plus的删除方法会改为逻辑删除 - EasyExcel
封装EasyExcel的公共导出方法;写一个Handler设置表格的自动宽度,大概就是要根据单元格的字符长度来设置单元格的宽度;写一些Converter将实体字段从标识转换为对应的文字含义。大致代码如下:
公共方法:
package com.workplat.administrate.util;
import com.alibaba.excel.EasyExcel;
import com.workplat.administrate.entity.SysConvenienceUser;
import com.workplat.administrate.excel.handler.AutoColumnWidthHandler;
import org.springframework.http.MediaType;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
/**
* @author stupi
*/
public class EasyExcelUtil {
public static void export(HttpServletResponse response, Class clazz, String sheetName, String fileName, List dataList){
//生成EXCEL
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
response.setCharacterEncoding("utf-8");
//设置文件名
String fileNameURL = "";
try {
fileNameURL = URLEncoder.encode(fileName+".xlsx", "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
response.setHeader("Content-Disposition", "attachment;filename="+fileNameURL+";"+"filename*=utf-8''"+fileNameURL);
try {
EasyExcel.write(response.getOutputStream(), clazz).registerWriteHandler(new AutoColumnWidthHandler()).sheet(sheetName).doWrite(dataList);
} catch (IOException e) {
e.printStackTrace();
}
}
}
自动宽度
package com.workplat.administrate.excel.handler;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.CellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.style.column.AbstractColumnWidthStyleStrategy;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.springframework.util.CollectionUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author stupi
*/
public class AutoColumnWidthHandler extends AbstractColumnWidthStyleStrategy {
private Map<Integer, Map<Integer, Integer>> CACHE = new HashMap<>();
@Override
protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
boolean needSetWidth = isHead || !CollectionUtils.isEmpty(cellDataList);
if (needSetWidth) {
Map<Integer, Integer> maxColumnWidthMap = CACHE.get(writeSheetHolder.getSheetNo());
if (maxColumnWidthMap == null) {
maxColumnWidthMap = new HashMap<>();
CACHE.put(writeSheetHolder.getSheetNo(), maxColumnWidthMap);
}
Integer columnWidth = this.dataLength(cellDataList, cell, isHead);
if (columnWidth >= 0) {
if (columnWidth > 255) {
columnWidth = 255;
}
Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex());
if (maxColumnWidth == null || columnWidth > maxColumnWidth) {
maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
writeSheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), columnWidth * 256);
}
}
}
}
private Integer dataLength(List<WriteCellData<?>> cellDataList, Cell cell, Boolean isHead) {
if (isHead) {
return cell.getStringCellValue().getBytes().length;
} else {
CellData cellData = cellDataList.get(0);
CellDataTypeEnum type = cellData.getType();
if (type == null) {
return -1;
} else {
switch (type) {
case STRING:
return cellData.getStringValue().getBytes().length + 1;
case BOOLEAN:
return cellData.getBooleanValue().toString().getBytes().length;
case NUMBER:
return cellData.getNumberValue().toString().getBytes().length;
default:
return -1;
}
}
}
}
}
字段标识转换(在实体注解引用即可)
package com.workplat.administrate.excel.converter;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import com.alibaba.excel.util.StringUtils;
import com.google.common.collect.ImmutableMap;
import com.workplat.administrate.enums.WorkStatus;
import java.text.ParseException;
import java.util.Map;
import java.util.Objects;
/**
* @author stupi
*/
public class WorkStatusConverter implements Converter<Integer> {
public static final Map<String,Integer> WORK_STATUS_MAP = ImmutableMap.<String,Integer>builder()
.put("工作中",0)
.put("请假中",1)
.build();
@Override
public Class supportJavaTypeKey() {
return Integer.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws ParseException {
if (StringUtils.isBlank(cellData.getStringValue())){
return 0;
}
return WORK_STATUS_MAP.getOrDefault(cellData.getStringValue(),0);
}
@Override
public WriteCellData<?> convertToExcelData(Integer value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
if (Objects.nonNull(value)){
return new WriteCellData(WorkStatus.fromKey(value).getDesc());
}
return null;
}
}
- 表格的导入
使用poi对表格逐行读取,封装到实体类,批量保存到库里。 - 减少魔法值
平时在代码的编写中,对于一些状态值,都尽量用枚举去表示,减少魔法值的出现 - 优化文件的上传与下载
特别是在下载是要正确显示文件的名称,思路是在响应头中正确设置utf8的编码,期间还遇到swagger无法将调用的二进制流转为文件的问题(要正确设置swagger识别的响应媒体类型),还有postman只能下载不超过50M的文件的问题(一直报与远程服务器断开连接,大冤种一直找不到错误的原因),代码如下:
package com.workplat.administrate.controller;
import com.mongodb.client.MongoClient;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.model.GridFSFile;
import com.workplat.administrate.common.CommonResult;
import com.workplat.administrate.dto.FileEntity;
import com.workplat.administrate.file.service.FileEntityService;
import com.workplat.administrate.file.service.impl.FileEntityConverter;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.gridfs.GridFsResource;
import org.springframework.data.mongodb.gridfs.GridFsTemplate;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.*;
@Controller
@RequestMapping("/api/attachment")
@Slf4j
public class AttachmentApi {
@Autowired
private FileEntityConverter fileEntityConverter;
@Autowired
private FileEntityService fileEntityService;
@Autowired
GridFsTemplate gridFsTemplate;
private MongoClient mongoClient;
private GridFSBucket gridFSBucket;
/**
* 文件上传
* @param file
* @return
*/
@PostMapping(value = "/upload")
@ResponseBody
public CommonResult upload(@RequestParam MultipartFile file) throws Exception {
// 获得提交的文件名
String fileName = file.getOriginalFilename();
// 获得文件输入流
InputStream ins = file.getInputStream();
// 获得文件类型
String contentType = file.getContentType();
//保存文件信息
FileEntity fileEntity = new FileEntity();
try {
//如果文件大小大于15M则采用GridFS 方式存储
if(file.getSize() > 15*1024*1024){
// 将文件存储到mongodb中,mongodb 将会返回这个文件的具体信息
long startTime = System.currentTimeMillis();
ObjectId fileInfo = gridFsTemplate.store(ins, fileName, contentType);
long endTime = System.currentTimeMillis();
log.info("gridFs上传时间:{}s",(endTime - startTime)/1000);
fileEntity.setId(fileInfo.toString());
}else{
fileEntity.setContent(file.getBytes());
}
fileEntity.setFileType(contentType);
fileEntity.setFileName(fileName);
fileEntity.setCreateTime(new Date());
fileEntity.setFileSize(file.getSize());
//存入md5值
fileEntity.setMd5(DigestUtils.md5Hex(file.getBytes()));
if(Objects.nonNull(fileName)){
String[] splitArr = fileName.split("\\.");
if (splitArr.length != 0 && Objects.nonNull(splitArr[splitArr.length - 1])){
fileEntity.setExt(splitArr[splitArr.length - 1]);
}
}
fileEntity = fileEntityService.create(fileEntity);
}catch (Exception e){
log.error("上传失败,文件名:{}",fileName);
return CommonResult.failed("上传失败!");
}
return CommonResult.success(fileEntity);
}
/**
* 文件下载
* @param fileId
* @param response
*/
@ApiOperation(value = "下载文件",produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
@GetMapping(value = "/download")
public CommonResult download(String fileId, HttpServletResponse response,HttpServletRequest request) throws Exception{
FileEntity fileEntity = fileEntityService.findById(fileId);
if(fileEntity == null){
return CommonResult.failed("文件不存在");
}else{
OutputStream os = response.getOutputStream();
String fileName = fileEntity.getFileName();
String fileNameURL = URLEncoder.encode(fileName, "UTF-8");
//multipart/form-data:指定传输数据为二进制数据,例如图片、mp3、文件也可以是表单键值对
response.setContentType("multipart/form-data");
response.setCharacterEncoding("utf-8");
//2.设置文件头:最后一个参数是设置下载文件名(假如我们叫a.pdf) 告诉浏览器以附件形式下载文件
//response.setHeader("Content-Disposition", "attachment;filename="
// + URLEncoder.encode(fileName, "UTF-8"));该方式使用postman调用不能还原为中文,浏览器可以,下面的方式两种都是正确的中文
response.setHeader("Content-Disposition", "attachment;filename="+fileNameURL+";"+"filename*=utf-8''"+fileNameURL);
if(null != fileEntity.getContent()){
os.write(fileEntity.getContent());
}else{
//_id为主键
GridFSFile fs = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(fileId)));
GridFsResource fsdb = gridFsTemplate.getResource(fs);
byte bs[] = new byte[1024];
long total = 0;
long times = 0;
long len = 0;
try {
InputStream inputStream = fsdb.getInputStream();
while ((len = inputStream.read(bs))>0){
total += len;
times++;
log.info("fileEntity:{},total:{},times:{}",
fileEntity.getFileSize(),total,times);
os.write(bs);
}
inputStream.close();
}catch (IOException e){
log.error("e:{}",e);
}
}
os.flush();
os.close();
return CommonResult.success();
}
}
/**
* 删除证照
* @param id
* @return
*/
@RequestMapping(value = "/delete/{id}")
@ResponseBody
public CommonResult delFile(@PathVariable("id") String id){
fileEntityService.delete(id);
return CommonResult.success();
}
}
- 新增business微服务且日常负责将测试环境的代码合并到正式环境还有负责测试环境与正式环境的构建等的排错(经常代码被冲掉啊)
- 延时服务时长抵消请假时长加事务加抛出自定义报错提示用户
- 拦截器放掉一些swagger的资源路径