竞赛 AR 展厅的项目经验

后端部分

后端部分主要是学习如何优化代码,由 CRUD 搬砖转到有一定后端架构能力,还有对 Java 8 一些特性的学习

基础

接口使用Body方式传递

需要在代码前加@RequstBody,而且不能是String这样的类型

正确写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@PostMapping("/addFavorites")
public R<String> addModelFavorites(@RequestBody Map<String, String> requestMap) {
String userId = requestMap.get("userId");
String favoritesId = requestMap.get("favoritesId");
int type = Integer.parseInt(requestMap.get("type"));

log.info("根据id收藏模型");
UserFavorites userFavorites = new UserFavorites();
Long userIdLong = Long.valueOf(userId);
Long favoritesIdLong = Long.valueOf(favoritesId);

userFavorites.setUserId(userIdLong);
userFavorites.setFavoritesId(favoritesIdLong);
userFavorites.setType(type);

userFavoritesService.save(userFavorites);

if (type == 1) {
Model model = modelService.getById(favoritesIdLong);
model.setFavoriteCount(model.getFavoriteCount() + 1);
modelService.updateById(model);
} else if (type == 2) {
Company company = companyService.getById(favoritesIdLong);
company.setFavoriteCount(company.getFavoriteCount() + 1);
companyService.updateById(company);
} else if (type == 3) {
// TODO: 这里是展厅增加收藏数的逻辑
}

return R.success("收藏模型成功");
}

错误写法,不能用json传递,只能用http://localhost:81/model/list?categoryId=1712732535265550338&status=1的方式传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@PostMapping("/addFavorites")
public R<String> addModelFavorites(String userId, String favoritesId, int type) {
log.info("根据id收藏模型");

Long userIdLong = Long.valueOf(userId);
Long favoritesIdLong = Long.valueOf(favoritesId);

UserFavorites userFavorites = new UserFavorites();
userFavorites.setUserId(userIdLong);
userFavorites.setFavoritesId(favoritesIdLong);
userFavorites.setType(type);

userFavoritesService.save(userFavorites);

if (type == 1) {
Model model = modelService.getById(favoritesIdLong);
model.setFavoriteCount(model.getFavoriteCount() + 1);
modelService.updateById(model);
} else if (type == 2) {
Company company = companyService.getById(favoritesIdLong);
company.setFavoriteCount(company.getFavoriteCount() + 1);
companyService.updateById(company);
} else if (type == 3) {
// TODO: 这里是展厅增加收藏数的逻辑
}

return R.success("收藏模型成功");
}

queryWrapper.in();

使用queryWrapper.in就可以直接根据限定条件为company的id中符合ids数组中的内容,这样就不需要再根据for循环去重复存入list

1
queryWrapper.in(Company::getId, ids);

saveBatch

mybatis-plus封装的一个可以批量保存的方法,参数为List类型

概念

IOC (控制反转)

Inversion of Control

SpringBoot 通过 @Component 注解实现 IOC

DI (依赖注入)

Dependency Injection

反射机制

1、Java反射机制的核心是在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法。本质是JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息。
2、Java属于先编译再运行的语言,程序中对象的类型在编译期就确定下来了,而当程序在运行时可能需要动态加载某些类,这些类因为之前用不到,所以没有被加载到JVM。通过反射,可以在运行时动态地创建对象并调用其属性,不需要提前在编译期知道运行的对象是谁。

思想

领域驱动设计(DDD)

代码设计模式

策略模式
装饰者模式

通过套娃的方式,将需要改变的类套在外层,这样在需求更改时只需要该相应的类

命令模式
状态模型

功能实现

java上传图片到服务器并通过服务器地址访问

关于文件的二进制转化,HuTool 提供了相关的功能来简化这一常见任务:

路径拼接:

  • 传统Java方式可能需要手动拼接路径字符串,而HuTool的FileUtil.file方法可以更方便地构建文件对象,自动处理路径的拼接,避免手动拼接路径字符串的繁琐操作。

IO操作:

  • 传统Java方式中,可能需要手动处理文件的输入流和输出流,并进行逐字节或逐块的复制。而使用HuTool的IoUtil.copy方法,可以更简单地实现文件的拷贝操作,提高了代码的简洁性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@GetMapping("/downloadGLB")
public void downloadGLB(String name, HttpServletResponse response) {
try {
// 基础路径
String basePath = "C:\\model\\AndroidEntry\\" + name + "\\";

// 使用Hutool提供的文件工具类
File file = FileUtil.file(basePath, name + ".glb");

// 设置响应头,使用原始文件名,不进行编码
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=" + name + ".glb");

// 传统Java方式
try (InputStream inputStream = new FileInputStream(file);
OutputStream outputStream = response.getOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
}

// 使用Hutool提供的IO工具类进行文件拷贝
IoUtil.copy(new FileInputStream(file), response.getOutputStream());

// 在方法中添加日志输出
System.out.println("Downloading file: " + file.getAbsolutePath());
System.out.println("File size: " + file.length());

} catch (Exception e) {
e.printStackTrace();
}
}

密码加盐

  1. 取用户输入的密码:

    1
    String password = user.getPassword();
  2. 查询数据库中对应用户名的用户信息:

    1
    2
    3
    LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(User::getUsername, user.getUsername());
    User emp = userService.getOne(queryWrapper);
  3. 获取数据库中存储的盐值:

    1
    String salt = emp.getSalt();
  4. 设置盐值到加密工具类中:

    1
    PasswordWithSaltUtils.setSalt(salt);
  5. 对用户输入的密码进行盐值加密:

    1
    String hashPassword = PasswordWithSaltUtils.hashPassword(password);
  6. 比较数据库中存储的加密密码和用户输入的加密密码是否一致,如果不一致则返回密码错误信息:

    1
    2
    3
    if (!emp.getPassword().equals(hashPassword)) {
    return R.error("密码错误");
    }

密码加盐封装的代码 PasswordWithSaltUtils 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class PasswordWithSaltUtils {

// 将随机生成的盐设置为密码加密的属性,便于获取
@Getter
private static String salt;

/**
* 对于salt的set方法,当新增员工的时候,salt为null,使用36位随机生成
* 当我们需要登录的时候
* 通过从数据库中查询到该用户在注册时生成的盐来进行计算,得出当前该用户在登录时候输入密码的hashPassword
* 将当前登录时候计算出的hashPassword和用户在注册时候存放在数据库中的hashPassword比对
* 如果一样则登录,否则失败
* @param salt
*/
public static void setSalt(String salt) {
if (salt == null){
PasswordWithSaltUtils.salt = UUID.randomUUID().toString();
}else {
PasswordWithSaltUtils.salt = salt;
}

}

public static String hashPassword(String password) {
try {
MessageDigest mDigest = MessageDigest.getInstance("SHA-512");
byte[] result = mDigest.digest((password + salt).getBytes());
return bytesToHex(result); // 将字节数组转换为十六进制字符串
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}

/**
* 将原始的字节数组转换为十六进制后返回
* @param hash
* @return
*/
private static String bytesToHex(byte[] hash) {
StringBuilder hexString = new StringBuilder(2 * hash.length);
for (int i = 0; i < hash.length; i++) {
String hex = Integer.toHexString(0xff & hash[i]);
if(hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
}

代码优化

foreach

1
2
3
4
5
6
7
8
9
10
11
Set<Long> ids = new HashSet<>(); // 使用Set来存储唯一的id

//下面是常规的for循环
for (ExhibitionCompany exhibitionCompanyItem : exhibitionCompanyList) {
Long id = exhibitionCompanyItem.getCompanyId();
ids.add(id);
}
//下面是foreach方式优化了代码
exhibitionCompanyList.forEach((exhibitionCompany) -> {
ids.add(exhibitionCompany.getId());
});

stream流

可以更方便的对集合或者数组进行链状流式的操作

1
2
3
4
5
6
7
8
9
10
// 使用传统的for循环方式
List<Long> companyIds = new ArrayList<>();
for (CompanyModel companyModel1 : companyModelList) {
companyIds.add(companyModel1.getCompanyId());
}

// 使用Stream和Lambda表达式方式
List<Long> companyIds = companyModelList.stream()
.map(CompanyModel::getCompanyId)
.collect(Collectors.toList());

flatMap

flatMap()可以把一个对象转换为多个对象放到流中

例如下面的listByCompanyId返回的就是list

1
2
3
List<CompanyModel> companyModelList =companyIds.stream()       
.flatMap(idItem->companyModelService.listByCompanyId(String.valueOf(idItem)).stream())
.toList();

Optional

使用Optional类可以避免手动的null检查,使代码更加简洁

Optional.orElseGet()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用 if-else 版本的null检查
Long categoryId = item.getCategoryId();
Category category = categoryService.getById(categoryId);
if (category != null) {
String categoryName = category.getName();
modelDto.setCategoryName(categoryName);
} else {
modelDto.setCategoryName("暂无分类");
}

Long categoryId = item.getCategoryId();
String categoryName = Optional.ofNullable(categoryService.getById(categoryId))
.map(Category::getName)
.orElse("暂无分类");
// 使用 Optional 版本的null检查
modelDto.setCategoryName(categoryName);

Optional.orElseThrow()

1
2
3
4
5
6
7
8
9
10
11
12
13
// 使用 if-else 版本的null检查,并返回
if (category != null) {
categoryName = category.getName();
} else {
return R.error("不存在该公司分类");
}

// 使用 Optional 版本的null检查,并返回,这里使用的是自己封装的optional类的内容
public String getNameError(String name,String msg) {
return Optional.ofNullable(name)
.orElseThrow(() -> new CustomException(msg));
}
String categoryName=optionalUtils.getNameError(category.getName(),"不存在该公司分类");

养成使用Optional的习惯可以写出更加优雅的代码来避免空指针异常。

Optional.ofNullable() 将对象封装为Optional对象。无论传入的参数是否为null都不会出现问题。(建议使用 )

Optional.of() 传入的参数必须不能为null。(不建议使用)

Optional.empty() 返回一个空的Optional对象。

Optional.ifPresent() 该方法会判断其内部封装的数据是否为空,不为空的时候才能执行具体的消费代码。

Optional.isPresent()  该方法会判断其内部封装的数据是否为空,为空返回false,不为空返回true.

Optional.filter()  在方法中进行逻辑判断,如果满足会返回Optional对象;不满足则返回null.

Optional.map() 将对象中的值转为Optional<List<T>>对象.

如果想要安全的获取Optional对象中的值,不推荐使用get()方法。推荐使用以下几种方法。

Optional.orElseGet() 如果Optional中的值为null,可以自定义返回一个对象。
Optional.orElseThrow()  如果Optional中的值为null,可以手动抛出异常。

有个会混淆的点在于orElseGet与orElse的区别,先说结论orElseGet会用的更多,因为orElse无论Optional的值是否为null都会进行,会导致内存的多余使用

链接查看:java中orElse()和orElseGet()的区别_java orelseget-CSDN博客

不返回实体类的某个属性

使用@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)来实现只写不返回

原文连接:java实体类,注解设置某些属性不返回前端-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
public class User implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;

private String username;

private String name;

@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;

}

优化数组存入

直接在实例化时存入数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
List<CompanyModel> companyModelList = new ArrayList<>();

QueryWrapper<CompanyModel> modelQueryWrapper = new QueryWrapper<>();
modelQueryWrapper.in("company_id", ids);
modelQueryWrapper.eq("category_name", categoryName);

List<CompanyModel> companyModelList1 = companyModelService.list(modelQueryWrapper);
if (companyModelList1.isEmpty()) {
return R.error("该分类下不存在模型");
}
/*for (CompanyModel model : companyModelList1) {
companyModelList.add(model);
}*/

List<CompanyModel> companyModelList = new ArrayList<>(companyModelList1);

Objects.requireNonNullElse

1
2
3
4
5
6
7
8
9
10
/*else if (admId != null) {
// 如果是"manager"角色,只显示创建者ID与Session中ID匹配的模型
queryWrapper.eq(Model::getCreateUserId, admId);
} else {
// 如果没有"manager"或 "employee"角色信息,不显示模型
queryWrapper.eq(Model::getCreateUserId, "0"); // 这是一个永远不成立的条件,确保不显示模型
}*/
else {
queryWrapper.eq(Model::getCreateUserId, Objects.requireNonNullElse(admId, "0"));
}

Switch

使用箭头的方法更简单易懂

1
2
3
4
5
6
7
switch (chosenDate) {
case "today" -> {

}
case "last7days"->{

}

自定义异常类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* CustomException 是一个自定义的运行时异常类,继承了标准的 RuntimeException。它旨在通过包含一个 ErrorCodeEnum 来更结构化地封装和处理异常,以表示与异常相关的错误代码。
*/
public class CustomException extends RuntimeException {
// 表示与异常相关联的错误代码
private ErrorCodeEnum errorCode;
/**
* CustomException 的构造函数,接受一个 ErrorCodeEnum 参数,并使用 ErrorCodeEnum 中相应的错误消息初始化异常。
*
* @param errorCode 表示特定错误条件的 ErrorCodeEnum。
*/
public CustomException(ErrorCodeEnum errorCode) {
// 调用超类构造函数,使用 ErrorCodeEnum 中的错误消息
super(errorCode.getMessage());

// 使用提供的 ErrorCodeEnum 初始化 errorCode 字段
this.errorCode = errorCode;
}
/**
* 用于检索与异常相关联的 ErrorCodeEnum 的 getter 方法。
*
* @return 表示特定错误条件的 ErrorCodeEnum。
*/
public ErrorCodeEnum getErrorCode() {
return errorCode;
}
}

统一异常处理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@ControllerAdvice(annotations = { RestController.class, Controller.class })
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {

// 进行异常处理方法,这里的异常是就是 SQLIntegrityConstraintViolationException 异常
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) {
// 处理 SQLIntegrityConstraintViolationException 异常的逻辑
log.error(ex.getMessage());

if (ex.getMessage().contains("Duplicate entry")) {
String[] split = ex.getMessage().split(" ");
String msg = split[2] + "已存在";
return R.error(msg);
}
return R.error("未知错误");
}

// 进行异常处理方法,这里的异常是就是自己建立的统一异常处理,简单版
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex) {
// 处理 CustomException 异常的逻辑
log.error(ex.getMessage());
return R.error(ex.getMessage());
}

// 进行异常处理方法,这里的异常是就是自己建立的统一异常处理,通过枚举错误类设置code以及msg版
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex) {
// 处理 CustomException 异常的逻辑
// 创建新的 R 对象并设置新的 code 值
log.error(ex.getMessage());
R<String> response = R.error(ex.getMessage());

ErrorCodeEnum errorCode = ex.getErrorCode();

// 假设 ErrorCodeEnum 的 code 是整数类型
response.setCode(errorCode.getCode());

return response;
}
}

抛出异常的方法

1
2
3
4
public <T> T getError(T value, ErrorCodeEnum errorCodeEnum) {
return Optional.ofNullable(value)
.orElseThrow(() -> new CustomException(errorCodeEnum));
}

枚举异常错误类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Getter
@AllArgsConstructor
public enum ErrorCodeEnum {
EXHIBITION_NULL(0,"不存在该展厅"),
CATEGORY_NULL(0,"不存在该分类"),
bannerModel_Null(0,"该展厅不存在推荐模型"),
COMPANY_ALREADY_ASSOCIATED(0,"已经关联公司,不能删除"),
MODEL_ALREADY_ASSOCIATED(0,"已经关联模型,不能删除");

/**
* 错误码
*/
private final Integer code;

/**
* 中文描述
*/
private final String message;
}

规范的日志打印

参考文章:Java:如果优雅地打印出完美日志-CSDN博客

方法的进入参数以及方法结束时返回值

这部分我使用 AOP 进行封装了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Aspect
@Component
@Slf4j
public class LoggingAspect {

@Before("@annotation(Loggable)")
public void logBefore(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String methodName = signature.toShortString();
Object[] args = joinPoint.getArgs();

MethodSignature methodSignature = (MethodSignature) signature;
String[] parameterNames = methodSignature.getParameterNames();

Map<String, Object> paramMap = new LinkedHashMap<>();
for (int i = 0; i < args.length; i++) {
paramMap.put(parameterNames[i], args[i]);
}

log.info("进入 {} 方法,传入值: {}", methodName, paramMap);
}

@AfterReturning(pointcut = "@annotation(Loggable)", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
log.info("结束 {} 方法,返回值: {}", joinPoint.getSignature().toShortString(), result);
}
}

if-else分支

1
log.info("进入 companyId 为 null 的分支");

关键部分(可能引发错误的部分)

1
log.info("通过 exhibitionCompanyList 获取的 companyIds:{}",companyIds);

mybatisPlus属性自动填充

使用MetaObjectHandler来实现

下面是AR项目所写的代码

自建的 MyMetaObjectHandler 类并继承 mp MetaObjectHandler 接口重写相关方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
//插入时自动填充
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充【insert】。。。");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser",new Long(1));
metaObject.setValue("updateUser",new Long(1));
}
//更新时自动填充
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充【update】。。。");
log.info(metaObject.toString());
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("updateUser",new Long(1));
}
}

同时实体类也需要进行相关的注解配置

1
2
3
4
5
@TableField(fill = FieldFill.INSERT)//插入时填充字段
private LocalDateTime createTime;

@TableField(fill = FieldFill.INSERT_UPDATE)//插入和更新时填充字段
private LocalDateTime updateTime;

AOP

AOP:面向切面编程,对面向对象编程的一种补充,一般处理非业务代码,比如打印日志

比如说每一个对象都需要开头结尾打印日志,那么可以把每个对象切一刀,再把切面抽象为对象,就可以实现封装每次开头结尾打印日志的效果

首先是自定义一个接口,这样就可以自由控制在哪个方法进行日志打印

自定义接口:Loggable

1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
}

然后定义一个切面类,在切面类里写好需要封装的日志

切面类:LoggingAspect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Aspect
@Component
@Slf4j
public class LoggingAspect {

@Before("@annotation(Loggable)")
public void logBefore(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String methodName = signature.toShortString();
Object[] args = joinPoint.getArgs();

MethodSignature methodSignature = (MethodSignature) signature;
String[] parameterNames = methodSignature.getParameterNames();

Map<String, Object> paramMap = new LinkedHashMap<>();
for (int i = 0; i < args.length; i++) {
paramMap.put(parameterNames[i], args[i]);
}

log.info("进入 {} 方法,参数: {}", methodName, paramMap);
}

@AfterReturning(pointcut = "@annotation(Loggable)", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
log.info("结束 {} 方法,返回值: {}", joinPoint.getSignature().toShortString(), result);
}
}

最后是使用方法

在需要打印日志的方法上面加上注解@Loggable

1
2
3
4
5
6
@GetMapping("/getAll")
@Loggable
public R<List<ExhibitionCompany>> listAllCompany(String exhibitionId) {
List<ExhibitionCompany> list = exhibitionCompanyService.listByExhibitionId(exhibitionId);
return R.success(list);
}

异步请求

参考文章:这8种java异步实现方式,性能炸裂!_Java精选的博客-CSDN博客

消息队列(MQ)

参考文章:java常用的消息队列 看完这篇你就懂了_java给外部推送消息队列都需要做什么-CSDN博客

主流框架有RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、Pulsar

目前使用阿里开发的 RocketMQ 较多

框架学习

HuTool框架

JPA

和mybatis-plus实在太像了,唯一不同的是dao(数据访问对象 data access object)在JPA中叫做repository,而mybaits的dao叫mapper,以及没有IService直接为service提供了基础的增删改查,需要自己写对应的接口,并借助repository来实现

基础链接:最详细的Spring-data-jpa入门(一)_springdatajpa-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Service
public class JpaUserServiceImpl implements JpaUserService {
@Resource
private JpaUserRepository jpaUserRepository;

@Override
public JpaUser insertUser(JpaUser user) {
return jpaUserRepository.save(user);
}

@Override
public void deleteUser(Long id) {
jpaUserRepository.deleteById(id);
}

@Override
public JpaUser updateUser(JpaUser user) {
return jpaUserRepository.save(user);
}

@Override
public List<JpaUser> findAllUser() {
return jpaUserRepository.findAll();
}

@Override
public JpaUser findUserById(Long id) {
return jpaUserRepository.findById(id).orElse(null);
}
}

Sa-Token

1. 添加依赖

Maven 方式

1
2
3
4
5
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.36.0</version>
</dependency>
2. 设置配置文件

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server:
port: 81

#Sa-Token 配置
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: satoken
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
# 这里不允许同一账号多地同时登录
is-concurrent: false
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
# 这里使用每次新建一个token
is-share: false
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
token-style: uuid
# 是否输出操作日志
is-log: true
3. 登录认证以及获取 token

登录时使用:login

获取 token 使用:getTokenValueByLoginId

1
2
3
4
5
6
7
8
9
10
11
@PostMapping("/login")
public R<User> login(HttpServletRequest request, @RequestBody User user){

// ...

// 使用 sa-token 的登录方法
StpUtil.login(emp.getId()); // 使用sa-token 中的登录方法,id值直接使用emp的id;
emp.setToken(StpUtil.getTokenValueByLoginId(emp.getId())); // 将每次登录产生的token返回

return R.success(emp);
}
4. 获取 userId

获取 userid 使用 getLoginIdByToken

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@GetMapping("/favoritesModels")
@Loggable
public R<List<ModelDto>> favoritesModels(String token) {
// 根据每次登录产生的token来获取当前登录用户收藏的模型
Long userIdLong = Long.valueOf((String) optionalUtils.getError(StpUtil.getLoginIdByToken(token), ErrorCodeEnum.INVALID_TOKEN));

log.info("开始获取收藏的模型...");

// ...

// 使用 sa-token 获取当前登录用户的ID
Long userIdLong = Long.valueOf((String)StpUtil.getLoginIdByToken(token);

// ...

// 返回包含模型信息的成功响应
return R.success(modelDtoList);
}

Bug复盘

循环依赖

好像这块是八股文的部分,但是实际开发还是遇到这个问题了,所以还是记录一下

循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。

解决方法:

加上Lazy注解,会延时引用bean,虽然这个解决方法,但是感觉好像有点头疼砍头,后续出问题了,再整理一下依赖关系吧

1
2
3
@Autowired
@Lazy
private CompanyService companyService;

前端部分

基础

JS基础

符号 == 判断有误的问题

可以使用 = = = 来实现,同样java中可以.isEmpty、== “”、== null都试试

axios封装API

1、下载 axios 插件

1
npm install axios

2、main.js 引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Vue from 'vue'
import './plugins/element-ui/index.css'
import App from './App.vue'
import store from './store'
import router from './router'
import axios from 'axios';

Vue.config.productionTip = false

new Vue({
store,
router,
axios,
ElementUI,
render: h => h(App)
}).$mount('#app')

3、封装请求方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import axios from 'axios';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import Vue from 'vue';
import router from '../router/index'; // 导入你的路由实例

Vue.use(ElementUI);


// 创建axios实例
const service = axios.create({
baseURL: 'api',
timeout: 1000000
});

// request拦截器
service.interceptors.request.use(
(config) => {
// 设置请求的Content-Type
config.headers['Content-Type'] = 'application/json;charset=utf-8';

// 是否需要设置 token
// const isToken = (config.headers || {}).isToken === false
// if (getToken() && !isToken) {
// config.headers['Authorization'] = 'Bearer ' + getToken(); // 根据实际情况修改
// }

// get请求映射params参数
if (config.method === 'get' && config.params) {
let url = config.url + '?';
for (const propName of Object.keys(config.params)) {
const value = config.params[propName];
var part = encodeURIComponent(propName) + '=';
if (value !== null && typeof value !== 'undefined') {
if (typeof value === 'object') {
for (const key of Object.keys(value)) {
let params = propName + '[' + key + ']';
var subPart = encodeURIComponent(params) + '=';
url += subPart + encodeURIComponent(value[key]) + '&';
}
} else {
url += part + encodeURIComponent(value) + '&';
}
}
}
url = url.slice(0, -1);
config.params = {};
config.url = url;
}
return config;
},
(error) => {
console.log(error);
return Promise.reject(error);
}
);

// 响应拦截器
service.interceptors.response.use(
(response) => {
console.log('---响应拦截器---', response);
// 检查响应是否包含数据以及 code 字段
if (response.data && response.data.code) {
const code = response.data.code;
const msg = response.data.msg;

if (code === 0 && msg === 'You have to login') {
// 返回登录页面
console.log('---/backend/page/login/login.html---', code);
localStorage.removeItem('userInfo');
// 使用传递的路由实例进行页面跳转
router.push({ name: 'login' });
} else {
return response.data;
}
} else {
// 处理未包含 code 字段的响应
// 这里可以添加适当的处理逻辑
return response;
}
},
(error) => {
console.log('err' + error);
let { message } = error;
if (message === 'Network Error') {
message = '后端接口连接异常';
} else if (message.includes('timeout')) {
message = '系统接口请求超时';
} else if (message.includes('Request failed with status code')) {
message = '系统接口' + message.substr(message.length - 3) + '异常';
}
ElementUI.Message({
message: message,
type: 'error',
duration: 5 * 1000
});

return Promise.reject(error);
}
);

export default service;

4、封装 api

注意传递参数有所不同,GET请求通常使用查询字符串(params)传递参数,而POST、PUT请求通常使用请求体(data)传递参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import $axios from '../utils/request';
// 查询列表数据
export const getCompanyPage = (params) => {
return $axios({
url: '/company/page',
method: 'get',
params
})
}

// 删除数据接口
export const deleteCompany = (ids) => {
return $axios({
url: '/company',
method: 'delete',
params: { ids }
})
}

// 修改数据接口
export const editCompany = (params) => {
return $axios({
url: '/company',
method: 'put',
data: { ...params }
})
}

// 新增数据接口
export const addCompany = (params) => {
return $axios({
url: '/company',
method: 'post',
data: { ...params }
})
}

// 查询详情接口
export const queryCompanyById = (id) => {
return $axios({
url: `/company/${id}`,
method: 'get'
})
}
// 查模型列表的接口
export const queryCompanyList = (params) => {
return $axios({
url: '/company/listCompanies',
method: 'get',
params
})
}

// 批量起售禁售
export const companyStatusByStatus = (params) => {
return $axios({
url: `/company/status/${params.status}`,
method: 'post',
params: { ids: params.ids }
})
}

5、使用方法

需要 import 导入

然后注意参数的传入有不同方法

比如直接建一个对象 params 然后传入getCompanyPage(params) 或者直接在deleteCompany(id)

还有注意具体的使用

比如 getCompanyPage 就直接接收就行 const res = await getCompanyPage(params);

而 deleteCompany会加入 then 以及箭头函数来进行后续操作

deleteCompany(type === ‘批量’ ? this.checkList.join(‘,’) : id)

​ .then(res => {

})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<template>
<div class="dashboard-container" id="company-app">
<div class="container">
<!-- ... 省略部分布局代码 ... -->
<el-table :data="tableData" stripe class="tableBox" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="25"></el-table-column>
<el-table-column prop="name" label="公司名称"></el-table-column>
<!-- ... 省略部分列配置 ... -->

<el-table-column label="操作" width="160" align="center"
v-if="userInfo.position == 'manager' || userInfo.position == 'admin'">
<template slot-scope="scope">
<el-button type="text" size="small" class="blueBug" @click="addSetMeal(scope.row.id)">
修改
</el-button>
<el-button type="text" size="small" class="blueBug" @click="statusHandle(scope.row)">
{{ scope.row.status == '0' ? '展示' : '隐藏' }}
</el-button>
<el-button type="text" size="small" class="delBut non" @click="deleteHandle('单删', scope.row.id)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination class="pageList" :page-sizes="[10, 20, 30, 40]" :page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper" :total="counts" @size-change="handleSizeChange"
:current-page.sync="page" @current-change="handleCurrentChange"></el-pagination>
</div>
</div>
</template>

<script>
import { getCompanyPage, deleteCompany } from '../../api/company.js';

export default {
// ... 其他部分保持不变 ...

methods: {
async init() {
const params = {
page: this.page,
pageSize: this.pageSize,
name: this.input ? this.input : undefined,
};
try {
const res = await getCompanyPage(params);
if (String(res.code) === '1') {
if (res.data != null) {
this.tableData = res.data.records || [];

// 遍历tableData中的每个记录
this.tableData.forEach(record => {
if (record.description === "") {
record.description = "暂无简介";
}
});
this.tableData.forEach(record => {
console.log("Description: " + record.description);
});
if (this.tableData.length > 0) {
this.ifNull = false
}

this.counts = res.data.total;
}

}
} catch (err) {
this.$message.error('请求出错了:' + err);
}
},

// 删除
deleteHandle(type, id) {
if (type === '批量' && id === null) {
if (this.checkList.length === 0) {
return this.$message.error('请选择删除对象');
}
}
this.$confirm('确定删除该公司, 是否继续?', '确定删除', {
'confirmButtonText': '确定',
'cancelButtonText': '取消',
}).then(() => {
deleteCompany(type === '批量' ? this.checkList.join(',') : id)
.then(res => {
if (res.code === 1) {
this.$message.success('删除成功!');
this.handleQuery();
} else {
this.$message.error(res.data.msg || '公司仍在展示状态');
}
})
.catch(err => {
this.$message.error('请求出错了:' + err);
});
}).catch(() => {
// 用户点击取消按钮的处理逻辑
// 可以不做任何处理,或者在这里添加一些取消操作的逻辑
});
},

// ... 其他方法保持不变 ...
},
};
</script>
js字符串处理
1
2
let fileName=file.name
this.ruleForm.modelPath = fileName.substring(0, fileName.lastIndexOf("."));
js判断数组为空

不能使用 == [] 或者 == “” 或者 == null 以及三个等号也不行,需要使用 .length==0

js数组处理常用方法
push() 末尾添加数据

作用: 就是往数组末尾添加数据

返回值: 就是这个数组的长度

1
2
3
4
var arr = [10, 20, 30, 40]
res = arr.push(20)
console.log(arr);//[10,20,30,40,20]
console.log(res);//5
splice() 截取数组

它仅能够截取数组中指定区段的元素,并返回这个子数组。

如果仅指定一个参数,则表示从该参数值指定的下标位置开始,截取到数组的尾部所有元素

1
2
3
4
var parts = routerPath.split('/'); 
// 获取第二个斜杠后面的内容
var result = parts.length >= 3 ? parts[2] : null;
console.log("result:" + result);
join() 数组转字符串

作用: 就是把一个数组转成字符串

返回值: 就是转好的一个字符串

1
2
3
4
var arr = [10, 20, 10, 30, 40, 50, 60]
res = arr.join("+")
console.log(arr)
console.log(res); // 10+20+30+40+50+60
map 映射数组

map() 方法返回一个新数组,这个新数组:由原数组中的每个元素调用一个指定方法后的返回值组成的新数组。

map() 不会对空数组进行检测。

map() 不会改变原始数组。

1
2
3
4
5
6
7
8
9
10
// 例子数值项求平方
let data = [1,2,3,4,5];
let newData = data.map(function (item){
return item * item;

});
console.log(newData);
//箭头函数的写法
let newData2 = data.map(item => item *item);
console.log(newData2);
filter 过滤数组

filter用于对数组进行过滤
它创建一个新数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。

1
2
3
4
5
6
7
let nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

let res = nums.filter((num) => {
return num > 5;
});

console.log(res); // [6, 7, 8, 9, 10]
find()用来获取数组中满足条件的第一个数据

获取数组中满足条件的第一个数据,最后会返回一个数组

1
const menu = this.menuList.find((item) => item.id === routerPath);
ES6核心语法

1、变量与常量

1
2
3
4
5
6
7
8
9
10
11
12
var count: number=0
let count: number=0

//常量注意全部大写的格式
const BASE_URL: string="http:..."

//let 与 var 的区别就在于 var 是函数作用域,let 是块作用域。例子如下,log 会报错为不存在变量count,而 var 在下面的情况下就不会报错
{
let count: number=0
count++
}
console.log(count)

2、模版字符串

1
2
3
4
5
const str1: string ='abc'+'efg'
const str2: string = `efg${str1}
这也是字符串的内容
`
// 反斜杠里就可以使用${}的方式来存入字符串变量,并且兼容换行

3、解构赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const arr: number[] = [1,2,3]
const a=arr[0]
const b=arr[1]
const c=arr[2]
//上面的方式获取较为麻烦,可以用下面的方式获取值,需要注意的是解构赋值在 ts 中不支持为变量附上类型

const [a, b, c]: number[] = [1, 2, 3];

//同时可以为为对象来实现相应的效果
const user: { username: string, age: number, gender: string } = {
username: 'TEC',
age: 18,
gender: 'male'
};

const { username, age, gender }: { username: string, age: number, gender: string } = {
username: 'TEC',
age: 18,
gender: 'male'
};

4、数组和对象的扩展

4.1、扩展运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 使用...数组名的方式可以直接将数组扩展到另一个数组上
const arr1 = [1, 2,3]

const arr2 = [4, 5, 6]

const arr3 = [...arr1, ...arr2, 10, 2]


// 同时对象也可以这么使用
const obj1={
a:1
}

const obj2={
b:1
}
const obj3={
name:'TEC',
...obj1,
...obj2
}

4.2、数组方法

1
2
3
4
5
6
7
// 可以使用Array.from来将伪数组转化为真数组,来使用数组的相关功能,但是ts里直接使用推荐使用剩余参数(rest parameters)代替 arguments 对象。arguments 是一个类数组对象,而剩余参数更灵活并且是真正的数组
function fn(...args) {
console.log(args);
}

fn(1, 2, 3, 4);

4.3、对象方法

1
2
3
4
5
6
// Object.assign为浅拷贝,浅拷贝的意义在于去改变 objB 的值时,不会影响到 objA ,而无论是直接数组赋值还是深拷贝都会产生影响
const objA={
name: 'TEC',
age: 18
}
const objB=Object.assign({},objA)

5、 Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 可以像 java 一样使用类的概念
class A {
name: string;
age: number;

constructor(name: string, age: number) {
this.name = name;
this.age = age;
}

introduce() {
console.log(`My name is ${this.name} and I am ${this.age} years old.`);
}
}

const a1 = new A("吴悠", 18);
a1.introduce();

// 也可以像 java 一样继承父类,调用 super()
class B extends A {
additionalProperty: string;

constructor(name: string, age: number, additionalProperty: string) {
super(name, age);
this.additionalProperty = additionalProperty;
}

introduceWithAdditional() {
console.log(`My name is ${this.name}, I am ${this.age} years old, and my additional property is ${this.additionalProperty}.`);
}
}

const b1 = new B("张三", 25, "Some additional info");
b1.introduceWithAdditional();

6、箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
13
const getSum1 = (n) => n + 3;

const getSum2 = (n1, n2) => n1 + n2;

const getSum3 = (n1, n2, ...other) => console.log(n1, n2, other);

const getResult = (arr) => {
let sum = 0;
arr.forEach(item => sum += item);
return sum;
};

console.log(getResult([1, 2, 3, 4, 5])); // Output: 15

布局基础

前端添加图标的方法

在阿里巴巴图标库中找到图标加入购物车,选完后最好加入一个项目中,因为购物车可能被清,然后点击下载,将下载下来的文件全部存到项目静态资源目录如assets,使用方法则是直接用class命名的方式,具体图标的,命名可在iconfont.json中查看,注意”css_prefix_text”: “icon-“,有这个就需要class前面加上icon-

在两个元素之间拉开一定的距离,并且两个元素加上距离依旧占到height的100%

使用flex布局中的gap可以达到这样的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div class="listView">
<div class="line3">
<!--下面的代码是实现效果的核心代码-->
<div class="l3Data2" style="display: flex; gap: 10px;flex-direction: column;">
<!-- 模型喜爱数据 -->
<el-card style="height: 50%">
<p class="card-header" slot="header">模型喜爱数据</p>
<div id="modelPieChart" style="height: 100%;"></div>
</el-card>
<!-- 类型喜爱数据 -->
<el-card style="height: 50%">
<p class="card-header" slot="header">类型喜爱数据</p>
<div id="categoryPieChart" style="height: 100%;"></div>
</el-card>
</div>
</div>
</div>
</template>
大屏适配

想要实现的效果为在任意屏幕下都能占满屏幕,使用 min-height: 100vh;来解决这个问题,这样无论数据是否超出限制,至少能够占满屏幕

接下来在数据超出时因为同一父元素下两个子元素同时使用使用了height:100%,导致可能出现两个滚动条,这也提醒了我height百分比的形式不是万能的,在内部会超出限制时会导致布局出错,百分比的形式还是应该出现在缩放时比较合理,常规不需要进行限制,只需要min-height: 100vh;然后元素会自己进行高度拉伸

Vue基础

router防止页面回退

直接使用replace代替push可以使得页面的回退消失

1
2
// this.$router.push({ name: 'index' });
this.$router.replace({ name: 'index' });
路由监听防止用户回退页面出错

通过watch的方式监听$route并进行相依的操作

1
2
3
4
5
watch: {
'$route'(to, from) {
this.handleRouteChange();
},
},

框架学习

Echarts

echarts折线图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
renderLineChart() {
// 渲染观看数量趋势折线图
const lineChart = echarts.init(document.getElementById('lineChart'));

// 添加折线图配置和数据
const option = {
// 配置选项
xAxis: {
type: 'category',
data: ['Day 1', 'Day 2', 'Day 3', 'Day 4', 'Day 5'], // 示例数据
},
yAxis: {
type: 'value',
},
legend: {
data: ['模型观看人数'], // 添加图例名称
orient: 'horizontal',
x: 'center',
y: 'top',
itemGap: 40, // 控制每一项的间距,也就是图例之间的距离
itemHeight: 10, // 控制图例图形的高度
},
series: [
{
name: '模型观看人数',
data: [100, 200, 150, 300, 250], // 示例数据
type: 'line',
color: ['#5DB1FF']
},
],
};

// 添加窗口大小改变监听事件,当窗口大小改变时,图表会重新绘制,自适应窗口大小
window.addEventListener("resize", function () {
lineChart.resize();
});

lineChart.setOption(option);
},

echarts饼图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
renderModelPieChart() {
// 渲染模型喜爱数据饼图
const modelPieChart = echarts.init(document.getElementById('modelPieChart'));

// 添加饼图配置和数据
const option = {
// 配置选项
legend: {
data: ['Model A', 'Model B'], // 添加图例名称
orient: 'horizontal',
x: 'center',
y: 'top',
itemGap: 40, // 控制每一项的间距,也就是图例之间的距离
itemHeight: 10, // 控制图例图形的高度
},
series: [
{
name: '模型收藏人数',
type: 'pie',
radius: '55%',
data: [
{ name: 'Model A', value: 30, itemStyle: { color: '#FAC858' } }, // 示例数据
{ name: 'Model B', value: 20, itemStyle: { color: '#91CC75' } }, // 示例数据
// 添加更多模型数据
],
},
],
};

// 添加窗口大小改变监听事件,当窗口大小改变时,图表会重新绘制,自适应窗口大小
window.addEventListener("resize", function () {
modelPieChart.resize();
});

modelPieChart.setOption(option);
},
echarts图例

具体参考Echarts legend属性使用-CSDN博客

在option中加入legend即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
renderLineChart() {
// 渲染观看数量趋势折线图
const lineChart = echarts.init(document.getElementById('lineChart'));

// 添加折线图配置和数据
const option = {
legend: {
data: ['模型观看人数'], // 添加图例名称
orient: 'horizontal',
x: 'center',
y: 'top',
itemGap: 40, // 控制每一项的间距,也就是图例之间的距离
itemHeight: 10, // 控制图例图形的高度
}
};

lineChart.setOption(option);
},
echarts改变图标颜色

折线图的折线换颜色,直接color底下加相应颜色即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
renderLineChart() {
// 渲染观看数量趋势折线图
const lineChart = echarts.init(document.getElementById('lineChart'));
// 添加折线图配置和数据
const option = {
series: [
{
name: '模型观看人数',
data: [100, 200, 150, 300, 250], // 示例数据
type: 'line',
color: ['#5DB1FF']
},
],
};
lineChart.setOption(option);
},

饼图换颜色需要在外层嵌套itemStyle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
renderModelPieChart() {
// 渲染模型喜爱数据饼图
const modelPieChart = echarts.init(document.getElementById('modelPieChart'));

// 添加饼图配置和数据
const option = {
series: [
{
name: '模型收藏人数',
type: 'pie',
radius: '55%',
data: [
{ name: 'Model A', value: 30, itemStyle: { color: '#FAC858' } }, // 示例数据
{ name: 'Model B', value: 20, itemStyle: { color: '#91CC75' } }, // 示例数据
// 添加更多模型数据
],
},
],
};

modelPieChart.setOption(option);
}
echarts美观折线图
圆润折线图以及折线面积效果还有增大转折点尺寸以及取消边界间隙
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
option = {
xAxis: {
type: 'category',
boundaryGap: false, // 取消边界间隙
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: [820, 932, 901, 934, 1290, 1330, 1320],
type: 'line',
smooth: true, // 圆润折线图
symbolSize: 7, // 增大转折点尺寸
areaStyle: {} // 折线面积效果
}
]
};

echarts不同尺寸下的优化
图表不跟随页面改变而缩放

这是因为echarts的相关图标需要resize一下才会改变大小

1
2
3
4
5
6
7
8
9
10
11
renderLineChart() {
// 渲染观看数量趋势折线图
const lineChart = echarts.init(document.getElementById('lineChart'));

// 添加窗口大小改变监听事件,当窗口大小改变时,图表会重新绘制,自适应窗口大小
window.addEventListener("resize", function () {
lineChart.resize();
});
//注意resize需要在setOption之前进行否则不生效
lineChart.setOption(option);
},
图表不跟随横向flex的大小进行缩放

这点问题是比如说我设置flex一个为2一个为1,最后两个元素缩放却缩放成了1:2,我本来以为是echarts配置问题,但是在查找后(原文链接:echarts不会随着flex布局自适应伸缩_flex不跟随内容自动伸展_zc自由飞~的博客-CSDN博客)我发现需要加上最小宽度,并且最小宽度还要和flex布局的大小一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.l3Data1 {
flex: 2;
min-width: 60%;
margin-right: 20px;
margin-top: 20px;
box-sizing: border-box;
}

.l3Data2 {
flex: 1;
min-width: 30%;
margin-top: 20px;
box-sizing: border-box;
}
图表不跟随竖直向flex的大小进行缩放

这个问题比较曲折,首先是竖直方向进行缩放,发现图标直接随着缩放大小而消失到屏幕外,后来发现原因来自于最外层添加了overflow-y: hidden;在修改为overflow-y: auto;后,出现了两个y轴点拖动条,然后发现页面里有多余的iframe并占据了4x4的大小导致最外层又多了一个拖动条,解决方法在于直接在iframe的id的css中加上display:none,但是这样图表在缩放后会变到不能看的程度,这时候需要加上min-height,这个还是挺基础的,关键在于这里并不能使用常规的百分比方式,因为会导致另外的元素与其高度不统一,下面会放上竖直方向如何进行flex缩放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.listView {
display: flex;
flex-direction: column;
}
.line1 {
flex: 1;
min-height: 40px;
}
.line2 {
flex: 10;
min-height: 250px;
}
.line3 {
flex: 20;
min-height: 460px;
}

Element-UI

修改el-card的样式

注意要加上::v-deep才能修改到样式

1
2
3
4
5
6
7
8
9
10
.el-card ::v-deep .el-card__header {
height: 10%;
padding-left: 20px;
}

.el-card ::v-deep .el-card__body {
display: grid;
align-items: center;
height: 90%;
}
element-ui 联排按钮效果

使用el-button-group可以去除el-button自带的右侧margin,代码的代码还实现了默认当日按钮被点击,点击其他按钮也会进行相应的点击变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<div class="listView">
<!-- 时间筛选功能 -->
<div class="line1">
<el-button-group>
<el-button :type="selectedButton === 'today' ? 'primary' : ''" @click="selectDate('today')">当日</el-button>
<el-button :type="selectedButton === 'last7days' ? 'primary' : ''"
@click="selectDate('last7days')">最近7天</el-button>
<el-button :type="selectedButton === 'last30days' ? 'primary' : ''"
@click="selectDate('last30days')">最近30天</el-button>
<el-button :type="selectedButton === 'last60days' ? 'primary' : ''"
@click="selectDate('last60days')">最近60天</el-button>
<el-button :type="selectedButton === 'last90days' ? 'primary' : ''"
@click="selectDate('last90days')">最近90天</el-button>
<el-button :type="selectedButton === 'lastyear' ? 'primary' : ''"
@click="selectDate('lastyear')">最近一年</el-button>
</el-button-group>
</div>
</div>
</template>
<script>
export default {
data() {
return {
selectedButton: 'today', // 默认选中当日按钮
};
},
};
</script>
el-checkbox-group的值为对象时的复选框回显问题

el-checkbox-group 值不可以为对象,所以新建了一个数组checkedListIds只存入id,然后label里存入item.id

Element-ui 进度条的使用

参考链接:axios进度条功能onDownloadProgress函数total参数为undefined问题 - 知乎 (zhihu.com)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<el-progress :percentage="this.downloadProgress" type="circle" :width="400" color="#ffc200"
v-if="this.downloadProgress > 0 && this.downloadProgress < 100"></el-progress>
<script>
export default {
data() {
return {
downloadProgress: 0,
};
},
components: {
vue3dLoader
},
methods: {

Preview(androidModelPath) {
// ... (other code) ...
// 初始化进度为0
this.downloadProgress = 0;
this.glbFileUrl = '';

// 发起文件下载请求
downloadGLBPreview({ name: androidModelPath }, (progressEvent) => {
// 计算下载进度
console.log('Download Progress:', progressEvent.loaded, '/', progressEvent.total);
this.downloadProgress = Math.round(progressEvent.loaded / progressEvent.total * 100);
}).then((url) => {
// 下载完成后,设置文件URL并显示对话框
this.glbFileUrl = url;
});
this.dialogVisible = true;
},
},
};
</script>

封装 api 部分

注意点在于我本来是通过localhost:8080进行转发到http://42.192.90.134:81的,但是这样做的话,返回头的ContentLength就会丢失,而关键的统计文件总大小的 progressEvent.total 就来自于返回头中的 ContentLength ,所以必须直接使用http://42.192.90.134:81/common/downloadGLBPreview的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import $axios from '../utils/request';

export const downloadGLBPreview = (params, onDownloadProgress) => {
return $axios({
url: `http://42.192.90.134:81/common/downloadGLBPreview`,
method: 'get',
params,
responseType: 'blob', // Set responseType to 'blob'
onDownloadProgress, // Pass the onDownloadProgress callback
}).then(response => {

// Handle the blob response
const blob = new Blob([response.data], { type: 'model/gltf-binary' });
const url = window.URL.createObjectURL(blob);


return url;
});
};

效果实现

loading动画加载

使用v-loading来控制某个页面动画的加载

1
<div class="listView" v-loading="loading">

在js部分中使用this.loading控制动画是否出现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<script>
export default {
data() {
return {
loading: true,
};
},
methods: {
async init(chosenDate) {
const params = {
chosenDate: chosenDate,
};
try {
this.loading = true
const res = await getAnalytics(params);
if (String(res.code) === '1') {
this.closeLoading()
}
} catch (err) {
this.$message.error('请求出错了:' + err);
}
},
closeLoading() {
this.timer = null;
this.timer = setTimeout(() => {
this.loading = false;
}, 600);
},
}
}
</script>

3D 模型预览

Vue-3D-Loader

框架地址:快速上手 | Vue 3d loader (king2088.github.io)

1、安装插件

注意 vue-3d-model 的最新版本不适配 vue2 ,要使用的话就需要指定版本

vue3请安装2.0.0及以上版本,vue2请安装1.x.x版本

1
npm install vue-3d-loader

2、使用方法

先 import 引入 ,然后使用插件所提供的组件vue3dLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<template>
<vue3dLoader v-else :width="700" :height="800" :fileType="'gltf'" :cameraPosition="{ x: 0, y: 0, z: 0.35 }"
:filePath="this.glbFileUrl" :backgroundColor="0x000000"></vue3dLoader>
</template>

<script>
import { getModelPage, deleteModel, modelStatusByStatus, addBannerModel, removeBannerModel, downloadGLBPreview } from '../../api/model';
import { vue3dLoader } from "vue-3d-loader";

export default {
data() {
return {
glbFileUrl: ''
};
},
methods: {
Preview(androidModelPath) {
console.log("androidModelPath:" + androidModelPath)

// 初始化进度为0
this.downloadProgress = 0;
this.glbFileUrl = ''

// 发起文件下载请求
downloadGLBPreview({ name: androidModelPath }, (progressEvent) => {
// 计算下载进度
console.log('Download Progress:', progressEvent.loaded, '/', progressEvent.total);
this.downloadProgress = Math.round(progressEvent.loaded / progressEvent.total * 100);
}).then((url) => {
// 下载完成后,设置文件URL并显示对话框
this.glbFileUrl = url;
});
this.dialogVisible = true
},
},
};
</script>

其他部分

服务器访问图片

原文如下如何访问存在服务器的图片或者视屏等静态资源_如何访问服务器上的静态视频-CSDN博客

方法有很多,比如springboot静态资源配置来访问或者nginx反向代理,这里我选择使用启动tomcat端口来进行访问

首先启动一个tomcat服务器,宝塔管理系统有一个插件java项目管理器,可以轻松的配置tomcat的配置文件(下面的配置文件为了简约我删了很多)新加Context标签然后docBase中输入服务器放图片的位置,path中则是访问的地址,最后网址为42.192.90.134:8082/images/014896a7-bb90-40e9-a988-64703ed80e12.jpg

1
2
3
4
5
6
7
8
9
10
11
12
<Server port="8805" shutdown="SHUTDOWN">
<Service name="Catalina">
<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log" suffix=".txt" pattern="%h %l %u %t &quot;%r&quot; %s %b" />
<!--关键在于下面的这句-->
<Context docBase ="C:\img" path ="/images" debug ="0" reloadable ="true"/>
</Host>
</Engine>
</Service>
</Server>

解决服务器图片跨域问题

因为我访问图片的方法是在服务器端开了一个tomcat端口8082,实现方法如下:
主要是新增了docBase绑定服务器地址

1
2
3
4
5
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log" suffix=".txt" pattern="%h %l %u %t &quot;%r&quot; %s %b" />
<!--下面是增加的代码-->
<Context docBase="C:\img" path="/images" debug="0" reloadable="true"/>
</Host>

然后在访问C:\img\iphone-14-pro这类的文件时,因为基础地址不等于docBase,所以存在跨域问题,但是tomcat的跨域不可能用@CrossOrigin去实现跨域,需要配置文件,设置跨域请求头,具体配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<filter>
<filter-name>CorsFilter</filter-name>
<filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
<init-param>
<param-name>cors.allowed.origins</param-name>
<param-value>*</param-value>
</init-param>
<init-param>
<param-name>cors.allowed.methods</param-name>
<param-value>GET,POST,HEAD,OPTIONS,PUT</param-value>
</init-param>
<init-param>
<param-name>cors.allowed.headers</param-name>
<param-value>Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers</param-value>
</init-param>
<init-param>
<param-name>cors.exposed.headers</param-name>
<param-value>Access-Control-Allow-Origin,Access-Control-Allow-Credentials</param-value>
</init-param>
<init-param>
<param-name>cors.preflight.maxage</param-name>
<param-value>10</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CorsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

原文如下:Tomcat设置跨域 - 知乎 (zhihu.com)

MySQL索引

![截屏2023-11-13 18.42.01](/Users/tec/Library/Application Support/typora-user-images/截屏2023-11-13 18.42.01.png)

IDEA快捷键

在方法后直接加上.var 可以自动创建相应的返回值的对象

.notnull 生成相应的 if 语句判断为非空(虽然现在基本都使用自己封装的 OptionalUtils 了)