【ts + java】古玩系统开发总结

src别名的配置

开发中文件和文件的关系会比较复杂,我们需要给src文件夹一个别名@吧

vite.config.js

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'// https://vitejs.dev/config/
export default defineConfig({plugins: [vue()],resolve: {alias: {'@': path.resolve("./src")  // 相对路径别名配置,使用@代替src}}
})

typescript配置:

//tsconfig.app.json
{"compilerOptions":{"baseUrl":"./"   // 解析非相对模块的基地址,默认是当前目录"paths":{        // 路径映射,相对于baseUrl"@/*":["src/*"]}}
}

环境变量的配置

开发会经历开发,测试,生产环境三个阶段,不同阶段请求的状态不同,于是环境变量的配置需求就有了。只要做简单的配置,这样就可以将环境切换的配置交给代码。(一般一个环境对应一台服务器)

项目根目录下添加生产,开发,测试环境文件:

.env.deveoplment

.env.production

.env.test

文件内容如下:变量必须以VITE_为前缀

# 变量必须以VITE_为前缀才能暴露给外部读取
# 开发阶段可以获取到的开发环境变量
NODE_ENV = 'development'
VITE_APP_TITLE = '古玩售卖后台系统'
VITE_APP_BASE_API = '/dev-api'
VITE_serve = 'http://xxx.com'NODE_ENV = 'production'
VITE_APP_TITLE = '古玩售卖后台系统'
VITE_APP_BASE_API = '/prod-api'
VITE_serve = 'http://yyy.com'NODE_ENV = 'test'
VITE_APP_TITLE = '古玩售卖后台系统'
VITE_APP_BASE_API = '/test-api'
VITE_serve = 'http://zzz.com'

配置运行命令:package.json

  "scripts": {"dev": "vite","build:test":"vue-tsc && vite build --mode test","build:pro":"vue-tsc && vite build -- mode production"}

通过import.meta.env获取环境变量。例如在main.js中打印:可以发现在配置文件中配置的开发环境存储变量显示如下,可以在开发阶段使用。

SVG图标配置

安装:

pnpm install vite-plugin-svg-icons -D

cnpm i fast-glob -D

vite.config.ts中配置:

// 引入svg插件
import { createSvgIconsPlugin } from "vite-plugin-svg-icons";export default defineConfig({plugins: [vue(),createSvgIconsPlugin({// 将svg矢量图标放在项目的src/assets/icons目录下iconDirs:[path.resolve(process.cwd(),'src/assets/icons')],symbolId:'icon-[dir]-[name]'})]
})

在main.ts 中添加:

// svg插件需要的配置代码
import 'virtual:svg-icons-register'

按照配置文件在src/assets/icons下保存svg图片的代码,常用网站iconfont-阿里巴巴矢量图标库 

 

随后可以在项目中使用该目录下svg图片,使用方式如下,注意需要将svg标签use标签结合使用,xlink:href指定需要使用哪个图标,其命名方式遵循配置文件#icon-svg文件名,fill属性设置图标的颜色。

<template><div><!-- svg是图标外层的容器节点,内部需要和use标签结合使用 --><svg><!-- xlink:href指定需要使用哪个图标 --><use xlink:href="#icon-phone" fill="red"></use></svg></div>
</template>

由于在项目中会经常使用到svg图标,可以将其封装成一个组件(src/conpoments/SvgIcons/index.vue)

template><svg :style="{width,height}"><!-- xlink:href指定需要使用哪个图标 --><use :xlink:href="prefix + name" :fill="color"></use></svg>
</template><script setup lang="ts">defineProps({// xlink:href属性的前缀prefix:{type:String,default:'#icon-'},// 提供使用图标的名字name:String,color:{type:String,default:''},width:{type:String,defaule:'16px'},height:{type:String,defaule:'16px'}})
</script>

这样就可以在其他组件中使用,但使用时需要import引入,还可以将其注册为全局组件。Vue3中使用app.component()注册全局组件。main.js

import SvgIcon from '@/components/SvgIcon/index.vue'
app.component('SvgIcon',SvgIcon);

但项目中会存在许多组件都需要注册为全局组件,在main.ts中一一引入太过于繁琐。可以创建自定义插件对象,在其中注册整个项目的全局组件。在component目录下创建index.ts:

import SvgIcon from './SvgIcon/index.vue'
// 将所有要注册的全局组件放在一个对象中
const allGlobalComponent: { [key: string]: any } = {SvgIcon}// 对外暴露一个插件对象
export default {// 务必叫做install方法,会将app应用实例传递给我们install(app:any) {// 注册项目所有的全局组件Object.keys(allGlobalComponent).forEach(key => {// 注册为全局组件app.component(key,allGlobalComponent[key])})}
}

插件对象必须有install方法。将所有注册的全局组件放在allGlobalComponent对象中,{ [key: string]: any } 表示该对象可以使用任何字符串作为索引,并且可以返回任何类型的属性值。不这样写会报错。

main.ts:安装自定义插件

// 引入自定义插件对象,注册整个项目的全局组件
import globalComponent from '@/components'
// 安装自定义插件,此时会触发install方法
app.use(globalComponent);

这样在项目中就可以直接引入SvgIcon组件了

集成sass

项目中使用sass,需要在对应的style标签上添加属性lang="scss"

我们需要给项目添加一些全局样式。在src/style目录下创建一个index.scss(存放所有的全局样式),在里面引入reset.scss(清除默认样式),代码可以在scss-reset - npm上获取。

在main.ts中引入

// 引入模板的全局样式
import '@/styles/index.scss'

但是index.scss中没有办法使用$变量,需要给项目引入全局变量$(sass中变量使用$符开头),在style/variable.scss创建一个variable.scss文件(存放项目的scss全局变量),并在vite.config.ts文件配置如下:

export default defineConfig({// scss样式全局变量的配置css:{preprocessorOptions:{scss:{javascriptEnabled:true,additionalData:'@import "./src/styles/variable.scss";'}}}
})

在variable.scss中定义scss全局变量

$color:blue;

这样在项目中就可以使用例如:h1{ color:$color }

mock接口

vite安装:cnpm i vite-plugin-mock mockjs -D

在vite.config.ts中配置文件启用插件

export default defineConfig(({command}) => {return {plugins: [viteMockServe ({localEnabled:command === 'serve',  // 保证开发阶段能使用mock接口})],} })

在项目的根目录下创建mock文件夹,里面存放接口,例如,user.ts:

function createUserList () {return [{userId:1,avator:'',username:'admin',password:'11111',desc:'平台管理员',roles:['平台管理员'],buttons:['user.detail'],routes:['home'],token:'Admin1 Token'},{userId:2,avator:'',username:'system',password:'11111',desc:'平台管理员',roles:['平台管理员'],buttons:['user.detail','cuser.user'],routes:['home'],token:'Admin2 Token'}]
}export default [// 用户登录接口{url:'/api/user/login',method:'post',response:({body}) => {const { username, password} = body;const checkUser = createUserList().find((item) =>  item.username === username && item.password === password )if(!checkUser) {return {code:201,data:{message:'账号或密码不正确'}}}const {token} = checkUser;return { code:200,data:{token}}}},// 获取用户信息{url:'/api/user/info',method:'get',response:(request) => {// 获取请求头携带的tokenconst token = request.herder.token;const checkUser = createUserList().find((item) => item.token === token)if(!checkUser) {return {code:201,data:{message:'获取用户信息失败'}}}return {code:200,data:checkUser}}}]

vite脚手架跨域设置

vite提供一个方法loadEnv加载对应环境下的变量,loadEnv()方法一执行就会返回当前开发环境对象,包含项目.env.xxx文件下配置的变量。

此外defineConfig函数的回调会注入一个mode变量,默认是开发环境

还需要获取环境文件的位置,即传入项目根目录,用process.cwd()获取

即 let env = loadEnv(mode,process.cwd()) 表示要加载哪个环境对应的哪个文件,随后就可使用文件中配置的变量

import { defineConfig,loadEnv } from 'vite'// command获取当前的运行环境
export default defineConfig(({command,mode}) => {// 获取各种环境下对应的变量let env = loadEnv(mode,process.cwd())return {// 代理跨域server:{proxy:{// 获取对应环境代理跨域需要带的关键字 例如开发环境需要配置/api才能从指定服务器拿数据[env.VITE_APP_BASE_API]: {// 获取数据服务器的地址target:env.VITE_serve,// 是否代理跨域changeOrigin:true,// 路径重写 真实的接口路径前面是没有/api的,需要将'/api'其替换为''rewrite:(path) => path.replace(/^\/api/,''),}}}} })

Java实体类在项目中的分类

1)封装请求参数的实体类:定义的时候会携带到dto,如:数据传输对象 Data Transfer Object,会定义在dto包中

2)与数据库对应的实体类:和数据库表名一致,定义在domain,entity,pojo包中

3)封装响应结果的实体类:定义的时候会携带到vo(视图对象)字样,定义在vo包中

统一结果实体类

让项目中所有的后端接口返回相同的数据格式。项目中所有的controller接口返回的都是Result格式的数据,code是状态码,message是响应信息,data是响应的数据

@Data
@Schema(description = "响应结果实体类")
public class Result<T> {//返回码@Schema(description = "业务状态码")private Integer code;//返回消息@Schema(description = "响应消息")private String message;//返回数据@Schema(description = "业务数据")private T data;// 私有化构造private Result() {}// 通过它返回统一的数据public static <T> Result<T> build(T body, Integer code, String message) {Result<T> result = new Result<>();result.setData(body);result.setCode(code);result.setMessage(message);return result;}// 通过枚举构造Result对象public static <T> Result build(T body , ResultCodeEnum resultCodeEnum) {return build(body , resultCodeEnum.getCode() , resultCodeEnum.getMessage()) ;}}

 枚举类中配置好所有的响应状态码对应的响应信息,可以直接调用 public static <T> Result build(T body , ResultCodeEnum resultCodeEnum) 方法

@Getter // 提供获取属性值的getter方法
public enum ResultCodeEnum {SUCCESS(200 , "操作成功") ,LOGIN_ERROR(201 , "用户名或者密码错误"),VALIDATECODE_ERROR(202 , "验证码错误") ,LOGIN_AUTH(208 , "用户未登录"),USER_NAME_IS_EXISTS(209 , "用户名已经存在"),SYSTEM_ERROR(9999 , "您的网络有问题请稍后重试"),NODE_ERROR( 217, "该节点下有子节点,不可以删除"),DATA_ERROR(204, "数据异常"),ACCOUNT_STOP( 216, "账号已停用"),STOCK_LESS( 219, "库存不足"),;private Integer code ;      // 业务状态码private String message ;    // 响应消息private ResultCodeEnum(Integer code , String message) {this.code = code ;this.message = message ;}}

如下图,项目中所有的controller接口返回的数据都是Result类型的,调用Result.build方法获取一个Result对象返回前端: 

 

后端项目统一的异常处理

注解一:@ControllerAdvice  Controller增强器,给controller增加统一的操作和处理

注解二:@ExceptionHandler 捕获controller抛出的指定类型异常

1)创建一个统一异常处理类;

2)在这个类上添加注解@ControllerAdvice;

3)在该类上创建异常处理的方法,方法上加@ExceptionHandler注解,指定异常类型,当出现异常该方法就会执行;

4)在异常方法中返回统一结果实体类Result格式;

以下的代码就是给controller增加一个对于异常的额外处理操作,当出现ExceptionHandle参数中配置的Exception类的异常时候,就会执行error方法,返回一个统一异常返回格式,即Result.build方法的返回值。

@ControllerAdvice
public class GlobalExceptionHandler {// 全局异常处理@ExceptionHandler(Exception.class)@ResponseBodypublic Result error() {return Result.build(null, ResultCodeEnum.SYSTEM_ERROR);}// 自定义异常处理@ExceptionHandler(FrankException.class)@ResponseBodypublic Result error(FrankException e) {return Result.build(null,e.getResultCodeEnum());}
}

此外还可以自定义异常,自定义异常类需要继承RuntimeException类,在类中加上需要的属性,并在上述统一异常处理类中定义,注意此时ExceptionHandle的参数类名就为自定义异常类。

自定义异常需要手动抛出 throw new FrankException (ResultCodeEnum.Xxx)

@Data
public class FrankException extends RuntimeException {private Integer code;private String message;private ResultCodeEnum resultCodeEnum;public FrankException (ResultCodeEnum resultCodeEnum) {this.resultCodeEnum = resultCodeEnum;this.code = resultCodeEnum.getCode();this.message = resultCodeEnum.getMessage();}
}

后端解决跨域

使用如下配置类,在项目目录下创建config包,其下创建如下java类

@Component
public class WebMvcConfiguration implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {// 设置允许跨域访问registry.addMapping("/**").allowCredentials(true)    // 是否在允许跨域的情况下传递Cookie.allowedOriginPatterns("*").allowedMethods("*").allowedHeaders("*");}
}

后端图片验证码实现思路

后端需要实现两件事:

1)进入登录页面后,要生成图片验证码,将其存储在redis中,设置验证码的有效时间

2)用户提交登录表单后,需要验证验证码的正确性

推荐通过工具hutool生成验证码。

需要返回给前端的:1)验证码的key(使用UUID生成的随机字符串,作为key存入redis)2)验证码的base64编码,需要加上前缀 data:image/png;base64,  ,这样前端才能将其显示。

放如redis中的:1)验证码的key(由UUID随机生成)和对应的验证码的值

hutool常用的方法

class CaptchaUtil

1)public static CircleCaptcha createCircleCaptcha(验证码的宽度, 高度, 验证码的位数, 干扰线的数量)

class CircleCaptcha

2)  public String getCode() 获取验证码的值

3)public String getImageBase64()  获取验证码base64图片

public ValidateCodeVo generateValidateCode() {// 通过工具hutool生成图片验证码// int width, int height : 图片验证码的宽度和高度// int codeCount 图片验证码的位数// int circleCount 干扰线的数量CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(150, 48, 4, 2);String code = circleCaptcha.getCode();// 获取验证码的值String imageBase64 = circleCaptcha.getImageBase64();// 验证码图片 做了base64编码// 把验证码存储在redis中,设置key:UUID value:验证码的值String key = UUID.randomUUID().toString().replaceAll("-","");redisTemplate.opsForValue().set("user:validate" + key,code,5, TimeUnit.MINUTES);// 返回ValidateCodeVo对象ValidateCodeVo validateCodeVo = new ValidateCodeVo();validateCodeVo.setCodeKey(key);  // redis中存储数据的keyvalidateCodeVo.setCodeValue("data:image/png;base64," + imageBase64);return validateCodeVo;}
}

用户登录校验(后端)

使用拦截器。项目下除了经过登录接口和图片验证码接口之外的所有接口都需要经过拦截器拦截。判断当前接口是否需要校验登录,拦截器中具体做法如下:

0) 判断请求的类型,将option请求直接放行

1)从请求头中获取token,根据token查询redis,获取用户信息

2)没有登录信息,直接返回提示信息

3)有登录信息,获取用户信息存储到Threadlocal中。这是jdk提供的线程工具类(线程变量),Threadlocal充当的变量属于当前线程,对于其他线程是隔离的(线程私有的),可以实现在同一个线程进行数据的共享。即在当前这次请求中可以方便的获取其中的数据。

4)更新redis数据过期时间,增加xx分钟。防止当前请求时数据即将过期,即经过一个请求和响应的时间后数据过期了,用户又要重新登录。

创建工具类AuthContextUtil,实现对ThreadLocal的操作:

public class AuthContextUtil {// 创建Threadlocal对象private static final ThreadLocal<SysUser> threadlocal = new ThreadLocal<>();// 添加数据public static void set(SysUser sysUser) {threadlocal.set(sysUser);}// 获取数据public static SysUser get() {return threadlocal.get();}// 删除数据public static void remove() {threadlocal.remove();}
}

创建拦截器类,实现拦截的核心步骤:

@Component
public class LoginAuthInterceptor implements HandlerInterceptor {@Autowiredprivate RedisTemplate<String,String> redisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 获取请求方法// 如果请求方法是option(预检请求)比如查看一个当前是否支持跨域,支持再发正式请求 ,直接放行String method = request.getMethod();if ("OPTIONS".equals(method)) {return true;}// 请求头获取tokenString token = request.getHeader("token");// token为空返回错误信息if(StrUtil.isEmpty(token)) {responseNoLoginInfo(response);return false;}// 如果token不为空,拿着token查询redisString userInfoString = redisTemplate.opsForValue().get("user:login" + token);// redis查不到数据,返回错误提示if(StrUtil.isEmpty(userInfoString)) {responseNoLoginInfo(response);return false;}// redis查到错误信息,把用户的信息放到Threadlocal中SysUser sysUser = JSON.parseObject(userInfoString, SysUser.class);AuthContextUtil.set(sysUser);// 把redis用户信息数据更新过期时间redisTemplate.expire("user:login" + token,30, TimeUnit.MINUTES);// 放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 删除ThreadLocal中的数据AuthContextUtil.remove();}// 响应208状态码给前端private void responseNoLoginInfo(HttpServletResponse response) {Result<Object> result = Result.build(null, ResultCodeEnum.LOGIN_AUTH);PrintWriter writer = null;response.setCharacterEncoding("UTF-8");response.setContentType("text/html;charset=utf-8");try {writer = response.getWriter();writer.println(JSON.toJSONString(result));} catch(IOException e) {e.printStackTrace();} finally {if(writer != null) writer.close();}}
}

在配置类中注册拦截器:

@Component
public class WebMvcConfiguration implements WebMvcConfigurer {@Autowiredprivate LoginAuthInterceptor loginAuthInterceptor;@Autowiredprivate UserProperties userProperties;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginAuthInterceptor).excludePathPatterns(userProperties.getNoAuthUrls())
//                .excludePathPatterns("/admin/system/index/generateValidateCode","/admin/system/index/login")  // 除去登录和验证码接口不拦截.addPathPatterns("/**");  // 所有路径}
}

 excludePathPatterns中配置不需要拦截的路径,可以使用参数的形式传递这些接口路径,但路径一多不方便维护,需要将这些路径配置在application.yml中:其中的属性名可以自定义

antique:auth:noAuthUrls:- /admin/system/index/generateValidateCode- /admin/system/index/login

随后需要添加一个Properties去读取配置文件中的路径,在项目src目录下创建一个文件夹为properties,创建类UserProperties,添加@ConfigurationProperties 获取配置文件中的属性前缀,类中添加属性存放配置文件中的路径,属性名必须和配置文件中的名字保持一致

@Data
@ConfigurationProperties(prefix = "antique.auth")
public class UserProperties {// 变量名一定要和配置文件中的属性名一致private List<String> noAuthUrls;
}

随后在启动类上添加注解,让UserProperties类生效

@EnableConfigurationProperties(value = {UserProperties.class})

这样就可以在 WebMvcConfiguration 中使用properties获取配置文件中的路径名了

权限管理表结构设计

权限管理需求基本都遵循以下一套方案:

1)存在三个实体:用户,角色,菜单

2)用户表和角色表,角色表和菜单表都是多对多的关系。即一个用户拥有多个角色,一个角色包含多个用户。

3)为了表示这三者之间的多对多的关系,需要创建一张关系表,来表示角色和用户的关系,角色和菜单的关系。这张表至少包括两个字段,分别指向两个表的主键id。

 常见的需求的sql语句:

1)根据用户id查询用户具有的角色数据

select sys_role.* from 
sys_role INNER JOIN sys_user_role 
ON sys_role.id = sys_user_role.role_id 
WHERE sys_user_role.user_id = 11

需要查询的是 角色用户关系表 和 角色表 将角色用户关系表的角色id字段和角色表的角色id字段做关联,连接两表

角色表sys_role:

角色用户表sys_user_role:

查询结果:

根据用户id查询用户具有的菜单数据 

select distinct sys_menu.* from sys_menu 
inner join sys_role_menu on sys_menu.id = sys_role_menu.menu_id
inner join sys_user_role on sys_role_menu.role_id = sys_user_role.role_id
where sys_user_role.user_id = 11

需要查询的是角色用户关系表,角色菜单关系表,菜单表。前两表之间用角色id做关联,后面两张表之间用菜单id做关联。有时一个用户具有多个角色,二这些角色具有的权限往往都会重复,所以需要对查询结果做去重处理使用distinct关键字

菜单表 sys_menu:

角色菜单表sys_role_menu: 

用户角色表 sys_user_role:

Minio的使用

下载地址:https://dl.min.io/server/minio/release/windows-amd64/ 随后点击其中的minio.exe下载。下载成功后在任意目录下新建一个minio的文件夹,将该启动文件放下其下,并创建一个data文件夹用于存放数据。

随后在minio文件下使用cmd进行启动,在命令行中输入:minio.exe server 数据文件的路径  D:\Program Files\minio\data

出现如下图片即启动成功:

注意:尽量不要放在C盘programme file目录下,命令行启动后可能没有权限访问data目录;也不要下载最新版本的minio.exe会无法启动,并报错。

访问minio.io。命令行启动后会看到有两个端口号,一个是将资源上传的端口号,另一个是管理控制台的端口号。

使用系统默认的用户名和密码进行登录控制台,随后需要创建bucket才能使用minio存储文件

创建bucket 

随后将access policy设置bucket的访问策略为public 

访问官网:https://min.io/docs/minio/linux/developers/java/minio-java.html查看具体使用。

随后创建接口。接口的方法fileupload需要接收传输的文件对象,类型为MultipartFile,可以使用注解@RequestParam("file") ,也可以不使用如下。前端Element-plus默认的name属性的value就是file,即:<input type="file" name="file">。故注解参数file不能随便写。

@PostMapping("/fileupload")public Result fileupload(MultipartFile file) {// 获取上传的文件// 调用service方法上传,返回minio路径String url = fileUploadService.upload(file);return Result.build(url, ResultCodeEnum.SUCCESS);}

创建FileUploadServiceImpl,在官网首页中就有示例代码,

@Service
public class FileUploadServiceImpl implements FileUploadService {@Autowiredprivate MinioProperties minioProperties;@Overridepublic String upload(MultipartFile file) {try {// 创建MinioClient对象MinioClient minioClient =MinioClient.builder().endpoint(minioProperties.getEndpointUrl()).credentials(minioProperties.getAccessKey(), minioProperties.getSecreKey()).build();// 创建bucketboolean found =minioClient.bucketExists(BucketExistsArgs.builder().bucket(minioProperties.getBucketName()).build());if (!found) {// bucket不存在则进行创建minioClient.makeBucket(MakeBucketArgs.builder().bucket(minioProperties.getBucketName()).build());} else {System.out.println("Bucket 'antique-bucket' already exists.");}// 每个上传文件名称唯一 根据当前日期对上传的文件进行分组String dateToday = DateUtil.format(new Date(), "yyyyMMdd");String uuid = UUID.randomUUID().toString().replaceAll("-","");// 获取上传的文件名称String filename = dateToday + "/" + uuid + file.getOriginalFilename();// 文件上传操作minioClient.putObject(PutObjectArgs.builder().bucket(minioProperties.getBucketName()).object(filename)  // 文件的名称.stream(file.getInputStream(),file.getSize(),-1).build());String url = minioProperties.getEndpointUrl() + "/" + minioProperties.getBucketName() + "/" + filename;return url;} catch (Exception e) {e.printStackTrace();throw new FrankException(ResultCodeEnum.SYSTEM_ERROR);}}
}

上述代码中首先创建一个MinioClient对象,需要在endpointcredentials中添加连接的服务器地址和用户名及密码。

随后创建bucket,随后进行文件的上传,但官网中使用的是对象的方式上传。需要在官方文档中查找流的方式上传的方法

在里面找到MinioClient,需要在里面找用流的方式上传文件的方法putObject 

使用的是知道传输文件大小的代码 

 此外,为了保证上传的文件不重名,我们使用UUID.randomUUID随机数为文件名加前缀,再给上传的文件按上传日期存在不同的文件中。即date/filename.xxx的形式做为文件名,传递给PutObjectArgs.object()方法,那么minio就会将/前面的部分为文件名创建文件夹,/后面的部分为文件名存入创建的文件夹中。

String dateToday = DateUtil.format(new Date(), "yyyyMMdd");
String uuid = UUID.randomUUID().toString().replaceAll("-","");
// 获取上传的文件名称
String filename = dateToday + "/" + uuid + file.getOriginalFilename();

前端使用element-plus上传文件,需要给出文件的上传地址action。文件上传不是使用ajax,使用普通方式进行的,所以请求过程中不会自动携带token,需要在headers中手动传递token.

文件上传后执行on-success的方法,后端会返回上传文件在服务器的存储路径,将上传的地址赋值给sysUser对象,在提交表单信息给服务器时将地址存放到数据库中。

<el-upload class="avatar-uploader" action="http://localhost:8501/admin/system/fileupload" :show-file-list="false":on-success="handleAvatarSuccess":headers="headers"
><img :src="sysUser.avatar" v-if="sysUser.avatar" class="avatar"><el-icon v-else class="avatar-uploader-icon"><Plus></Plus></el-icon>
</el-upload>....// 文件上传
const headers = ref({token:useApp().authorization.token
})const handleAvatarSuccess = (response,uploadFile) => {console.log(response)if(response.code === 200) {sysUser.value.avatar = response.data} else {ElMessage.error(`文件上传失败,${response.message}`)}
}

后台菜单管理

需要以这样的形式显示加载后台所有的菜单,进行管理。但菜单是多级嵌套的结构,需要以这样的方式显示,要在后端进行一些处理。

前端使用element-plus的树形结构表格展示

需要使用el-table显示树形结构的表格,需要准备的数据类型如下:

若是子菜单,则以children属性表示。数据库为每一个菜单分配一个唯一的id,并给每一个菜单一个parent_id字段,为0代表没有上级菜单,非0即表示上级菜单的id值。

所以要在后端使用递归的方式将数据库中查询得到的菜单List集合转换为前端需要的格式。

在entity sysMenu中设置好children属性,用来对应前端存放子菜单节点。

现在数据库中查询好所有的菜单,存放在List集合中,传入buildTree方法,先找到一级菜单即parentId为0的菜单。随后使用递归查找对应一级菜单的子菜单,即parentId为当前一级菜单的id的菜单,依此类推。 

public class MenuHelper {// 递归实现封装的过程public static List<SysMenu> buildTree(List<SysMenu> sysMenuList) {// 创建一个list集合封装最终的数据List<SysMenu> trees = new ArrayList<>();for (SysMenu sysMenu:sysMenuList) {// 找到递归操作的入口 找到第一层菜单 parent_id = 0if (sysMenu.getParentId().longValue() == 0) {// 根据第一层找下层数据,使用递归 (第一层菜单,所有菜单集合)trees.add(findChildren(sysMenu,sysMenuList));}}return trees;}// 递归查找下层菜单public static SysMenu findChildren(SysMenu sysMenu, List<SysMenu> sysMenuList) {sysMenu.setChildren(new ArrayList<>());// 找下一层数据for(SysMenu it : sysMenuList) {// 判断id和parentId值是否相同if(sysMenu.getId().longValue() == it.getParentId().longValue()) {sysMenu.getChildren().add(findChildren(it,sysMenuList));}}return sysMenu;}
}

 serviceImpl:

    public List<SysMenu> findNodes() {// 查询所有菜单 返回List集合List<SysMenu> sysMenuList = sysMenuMapper.findAll();// 调用工具类中的方法,把返回list集合封装要求数据格式if (CollectionUtils.isEmpty(sysMenuList)) {return null;}List<SysMenu> treeList = MenuHelper.buildTree(sysMenuList);return treeList;}

EasyExcel导入导出数据

导出数据:导出数据,即文件的下载。需要查询数据库得到需要写入的内容,再把内容写到excel表格中返回给前端。

controller部分:不需要写返回值。文件下载需要使用HttpServletResponse对象

    // 导出分类@GetMapping(value="/exportData")public void exportData(HttpServletResponse response) {categoryService.exportData(response);}

service部分:

设置响应头信息Content-disposition让文件以下载方式打开,若不设置则文件不能下载,只有这个内容是不可或缺的。

response.setHeader("Content-disposition","attachment;filename=" + filename + ".xlsx");

最终浏览器查看到的响应头信息如下:

随后查询数据库将要写入到excel中的内容查出来,得到categoryList。随后使用EasyExcel的write(OutputStream输出流,类型Class)方法实现数据的写入。

EasyExcel.write(response.getOutputStream(), CategoryExcelVo.class).sheet("分类数据").doWrite(categoryExcelVoList);

其中sheet('')传入的参数表示工作表的名称,最终下载得到的内容显示如下:

其中doWrite()中传入的是和CategoryExcelVo.class类型一致的数据集合,即需要写入到excel中的内容。但mapper返回的list集合元素是Category类型的,需要将结果转化为CategoryExcelVo类型。

这里使用foreach循环遍历集合List<Category>,转化为List<CategoryExcelVo>类型。转化的过程哦就是将两个集合类型共有的属性先get后set到新的CategoryExcelVo中去,一个个转换比较繁琐,可以使用org.springframework.beans.BeanUtils下的copyProperties(from,to)方法直接拷贝

@Service
public class CategoryServiceImpl implements CategoryService {@Autowiredprivate CategoryMapper categoryMapper;@Overridepublic void exportData(HttpServletResponse response) {try {// 设置响应的头信息和其他信息response.setContentType("application/vnd,ms-excel");  // 设置内容的类型 微软的excel表格response.setCharacterEncoding("utf-8");  //// 防止中文乱码String filename = URLEncoder.encode("分类数据", "UTF-8");// 设置响应头信息 Content-disposition 作用是将文件以下载的方式打开response.setHeader("Content-disposition","attachment;filename=" + filename + ".xlsx");// 调用mapper方法查询所有的分类 返回list集合List<Category> categoryList = categoryMapper.findAll();// List<category> => List<CategoryExcelVo>List<CategoryExcelVo> categoryExcelVoList = new ArrayList<>();for(Category category : categoryList) {CategoryExcelVo categoryExcelVo = new CategoryExcelVo();// 把category值获取出来,设置到categoryExcelVo中去// categoryExcelVo.setId(category.getId());BeanUtils.copyProperties(category,categoryExcelVo); //  把一个对象值复制到另一个里面去categoryExcelVoList.add(categoryExcelVo);}// 调用EasyExecl的write方法完成写操作EasyExcel.write(response.getOutputStream(), CategoryExcelVo.class).sheet("分类数据").doWrite(categoryExcelVoList);} catch (Exception e) {e.printStackTrace();throw new FrankException(ResultCodeEnum.DATA_ERROR);}}
}

 其中传入的实体类类型CategoryExcelVo是与excel表格的列对应的java实体类

该实体类的属性上需要添加@ExcelProperty(value='',index=) 注解,其中value表示表头,index表示该属性在excel表中是第几列,从0开始。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CategoryExcelVo {@ExcelProperty(value = "id" ,index = 0)private Long id;@ExcelProperty(value = "名称" ,index = 1)private String name;@ExcelProperty(value = "图片url" ,index = 2)private String imageUrl ;@ExcelProperty(value = "上级id" ,index = 3)private Long parentId;@ExcelProperty(value = "状态" ,index = 4)private Integer status;@ExcelProperty(value = "排序" ,index = 5)private Integer orderNum;}

mapper部分: 

<mapper namespace="com.frank.spzx.manager.mapper.CategoryMapper"><resultMap id="categoryMap" type="com.atguigu.spzx.model.entity.product.Category"     autoMapping="true"></resultMap><sql id="columns">id,name,image_url,parent_id,status,order_num,create_time,update_time,is_deleted</sql>    <select id="findAll" resultMap="categoryMap">select <include refid="columns"/>from category where is_deleted=0 order by id</select>
</mapper>

前端部分:使用bolb类型作为响应类型,使用 URL.createObjectURL()方法会根据传入的参数创建一个指向该参数对象的URL,这里传递的就是从后端请求得到的需要下载的excel文件的二进制数据,最后创建一个超链接标签,绑定其连接路径,自动执行其click事件完成自动下载。

category.js:

// 指定响应类型为bolb类型,即二进制数据类型用于表示大量的二进制数据
export const exportCategoryData = () => 
request.get(`${api_name}/exportData`,{responseType:'blob'}) 
// 导出数据const exportData = () => {exportCategoryData().then(res => {const bolb = new Blob([res])const link = document.createElement('a')// 创建a标签后将bolb对象转换为url// URL.createObjectURL()方法会根据传入的参数创建一个指向该参数对象的URL. link.href = window.URL.createObjectURL(bolb)// 设置下载文件的名称link.download = "分类数据.xlsx"// 模拟点击下载链接link.click()})}

导入数据: 

将外部的excel文件导入系统,需要满足excel中的列名和和对应的Vo实体类的属性要一一对应。

controller:获取上传的文件需要使用MultipartFile对象

// 导入分类@PostMapping(value="/importData")public Result importData(MultipartFile file){// 获取上传的文件categoryService.importData(file);return Result.build(null,ResultCodeEnum.SUCCESS);}

service:使用EasyExcel获取上传的excel中的数据,需要监听器 + EasyExcel.read(InputStream文件输入流,类型Class,监听器).sheet().doRead()结合使用.

public void importData(MultipartFile file) {try {// 每次读取都是新创建的对象,避免了并发问题ExcelListener<CategoryExcelVo> excelListener = new ExcelListener(categoryMapper);EasyExcel.read(file.getInputStream(), CategoryExcelVo.class,excelListener).sheet().doRead();} catch (IOException e) {e.printStackTrace();throw new FrankException(ResultCodeEnum.DATA_ERROR);}}

在项目的Listener包下创建监听器文件:

ExcelListener:该类需要实现ReadListener接口,该接口中有两个方法是必须重写的,一个是invoke方法,该方法会从表格的第二行开始读取将每行的读取内容封装到T的对象中去,excel一共有几行数据该方法就是执行几次。

public void invoke(T data, AnalysisContext context) {}

另一个是doAfterAllAnalysed()方法,它是在excel读取所有的操作结束后执行。

public void doAfterAllAnalysed(AnalysisContext context) {}

ExcelListener这个类不能加上@Component注解,不能交给Spring管理!因为一旦给Spring管理,该类会变成单例,如果多个人同时读取文件会调用同一个Listener,无法区分是哪个文件读取出来的数据,造成并发问题。若需要在监听器中操作mapper,官网给出的解决方案是构造器传递mapper,在调用的时候需要手动进行new操作。

当读取的excel文件中的行数过多时,不能读一行添加一次数据库,会造成资源的消耗,产生OOM,官网给出的方案是批量添加数据,创建一个常量BATCH _COUNT用来存放一次读取的条数,再创建List集合cachedDataList用来存放这些内容。最后在invoke方法中批量添加。

若一次需要添加的数据没有超过BATCH _COUNT的值,则需要执行doAfterAllAnalysed方法,完成数据库的添加操作。

// 这个类不能交给spring管理,否则会变成单例,多个人同时读取文件时就调用同一个Listener 无法区分是哪个文件读取出来的(造成并发的问题)
public class ExcelListener<T> implements ReadListener<T> {// 创建List集合用于缓存数据 行数过多每读取一行加数据库会造成资源的消耗,容易OOM// 创建一个集合定义一个常量,为了做批量操作,每一百条数据加一次数据库private static final int BATCH_COUNT = 100;private List<T> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);// 使用构造传递mapper,操作数据库,因为不能交给spring管理private CategoryMapper categoryMapper;public ExcelListener(CategoryMapper categoryMapper) {this.categoryMapper = categoryMapper;}@Overridepublic void onException(Exception exception, AnalysisContext context) throws Exception {ReadListener.super.onException(exception, context);}@Overridepublic void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {ReadListener.super.invokeHead(headMap, context);}// 从表格的第二行开始读取将每行的读取内容封装到T的对象中去,将每行的数据加到cachedDataList集合中, 当集合达到100 调用方法把数据加到数据库中 加完后将集合清理为初始状态@Overridepublic void invoke(T data, AnalysisContext context) {// 把每行数据的对象t放到cachedDataList的集合中去cachedDataList.add(data);if(cachedDataList.size() >= BATCH_COUNT) {// 调用方法批量添加到数据库中saveData();// 清理list集合 即重新初始化cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);}}private void saveData() {categoryMapper.catchInsert((List<CategoryExcelVo>)cachedDataList);}@Overridepublic void extra(CellExtra extra, AnalysisContext context) {ReadListener.super.extra(extra, context);}// 所有操作都完成后执行@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {// 保存数据 如果数据的行数没有达到100行 就执行下面的代码将数据加到数据库中saveData();}@Overridepublic boolean hasNext(AnalysisContext context) {return ReadListener.super.hasNext(context);}// 使用构造方法传递
}

Mapper:

@Mapper
public interface CategoryMapper {void catchInsert(List<CategoryExcelVo> cachedDataList);
}
<mapper namespace="com.frank.spzx.manager.mapper.CategoryMapper"><!--    批量保存分类的方法--><insert id="catchInsert" useGeneratedKeys="true" keyProperty="id">insert into category(id,name,image_url,parent_id,status,order_num,create_time,update_time,is_deleted) values<foreach collection="cachedDataList" item="item" separator=",">(#{item.id},#{item.name},#{item.imageUrl},#{item.parentId},#{item.status},#{item.orderNum},now(),now(),0)</foreach></insert>
</mapper>

 前端:

<el-dialog v-model="dialogImportVisible" title="导入" width="30%"><el-form label-width="120px"><el-form-item label="分类文件"><el-upload class="upload-demo"action="http://localhost:8501/admin/product/category/importData":on-success="onUploadSuccess":headers="headers"><el-button type="primary">上传</el-button></el-upload></el-form-item></el-form>
</el-dialog>

let dialogImportVisible = ref(false)
let headers = {token:useApp().authorization.token
}
// 打开导入数据
const importData = () => {dialogImportVisible.value = true
}
//分类数据文件excel上传
const onUploadSuccess = async (response,file) => {ElMessage.success('操作成功')dialogImportVisible.value = falselet result = await findCategoryByParentId(0)list.value = result.data
}

SpringTask定时任务

现在需要统计每天的订单总交易金额,我们可以使用如下的sql语句实现:

但是如果数据量很大,每次查询交易总金额都需要执行一次这样的sql语句会很消耗性能。常用的方法是开启定时任务,设定定时任务程序每天两点执行一次,在订单表中查询前一天的交易总金额数据,然后将结果写入统计结果表。

SpringTask就是spring中的一个模块, 

在项目下创建一个task文件夹存放定时任务的类:

随后创建定时任务类:

在类上添加@Component注解将其交给Spring管理。需要执行定时任务需要在类的方法上加上@Scheduled + cron表达式,其中表达式的内容就是需要定时任务执行的要求

@Component
public class OrderStatisticsTask {// 测试定时任务 让该方法每五秒执行一次// 注解@Scheduled + cron 表达式// 其中cron表达式设定执行规则@Scheduled(cron="0/5 * * * * ?")public void test() {System.out.println(new Date().toInstant());}
}

在百度直接搜索cron表达式在线工具,自动生成表达式。这里的需求是间隔五秒打印一下当前时间。最后还要在启动类上加上@EnableScheduling开启定时任务。

随后启动项目控制台就会间隔五秒输出当前的时间。

注意:cron表达式这里只能由六位组成,即(秒 分 时 日 月 周 年) 中的年是不能包括在里面,因为表达式是不能跨年的只能使用前面六位

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/11120.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用Pygame制作“俄罗斯方块”游戏

1. 前言 俄罗斯方块&#xff08;Tetris&#xff09; 是一款由方块下落、行消除等核心规则构成的经典益智游戏&#xff1a; 每次从屏幕顶部出现一个随机的方块&#xff08;由若干小方格组成&#xff09;&#xff0c;玩家可以左右移动或旋转该方块&#xff0c;让它合适地堆叠在…

小程序设计和开发:什么是竞品分析,如何进行竞品分析

一、竞品分析的定义 竞品分析是指对竞争对手的产品进行深入研究和比较&#xff0c;以了解市场动态、发现自身产品的优势和不足&#xff0c;并为产品的设计、开发和营销策略提供参考依据。在小程序设计和开发中&#xff0c;竞品分析可以帮助开发者了解同类型小程序的功能、用户体…

Vue简介

目录 Vue是什么&#xff1f;为什么要使用Vue&#xff1f;Vue的三种加载方式拓展&#xff1a;什么是渐进式框架&#xff1f; Vue是什么&#xff1f; Vue是一套用于构建用户界面的渐进式 JavaScript (主张最少)框架 &#xff0c;开发者只需关注视图层。另一方面&#xff0c;当与…

Linux多路转接poll

Linux多路转接poll 1. poll() poll() 结构包含了要监视的 event 和发生的 event &#xff0c;接口使用比 select() 更方便。且 poll 并没有最大数量限制&#xff08;但是数量过大后性能也是会下降&#xff09;。 2. poll() 的工作原理 poll() 不再需要像 select() 那样自行…

C++【深入底层,手撕vector】

vector是向量的意思&#xff0c;看了vector的底层实现之后&#xff0c;能够很明确的认识到它其实就是我们经常使用的顺序表。在我们的认知中&#xff0c;顺序表会有一个数组、数据的size以及容量的大小。vector作为一个向量容器&#xff0c;它可以存放任意类型的数据。所以在实…

基于FPGA的BT656编解码

概述 BT656全称为“ITU-R BT.656-4”或简称“BT656”,是一种用于数字视频传输的接口标准。它规定了数字视频信号的编码方式、传输格式以及接口电气特性。在物理层面上,BT656接口通常包含10根线(在某些应用中可能略有不同,但标准配置为10根)。这些线分别用于传输视频数据、…

关于系统重构实践的一些思考与总结

文章目录 一、前言二、系统重构的范式1.明确目标和背景2.兼容屏蔽对上层的影响3.设计灰度迁移方案3.1 灰度策略3.2 灰度过程设计3.2.1 case1 业务逻辑变更3.2.2 case2 底层数据变更&#xff08;数据平滑迁移&#xff09;3.2.3 case3 在途新旧流程兼容3.2.4 case4 接口变更3.2.5…

Microsoft Power BI:融合 AI 的文本分析

Microsoft Power BI 是微软推出的一款功能强大的商业智能工具&#xff0c;旨在帮助用户从各种数据源中提取、分析和可视化数据&#xff0c;以支持业务决策和洞察。以下是关于 Power BI 的深度介绍&#xff1a; 1. 核心功能与特点 Power BI 提供了全面的数据分析和可视化功能&…

【机器学习】自定义数据集 ,使用朴素贝叶斯对其进行分类

一、贝叶斯原理 贝叶斯算法是基于贝叶斯公式的&#xff0c;其公式为&#xff1a; 其中叫做先验概率&#xff0c;叫做条件概率&#xff0c;叫做观察概率&#xff0c;叫做后验概率&#xff0c;也是我们求解的结果&#xff0c;通过比较后验概率的大小&#xff0c;将后验概率最大的…

AMS仿真方法

1. 准备好verilog文件。并且准备一份.vc文件&#xff0c;将所有的verilog file的路径全部写在里面。 2. 将verilog顶层导入到virtuoso中&#xff1a; 注意.v只要引入顶层即可。不需要全部引入。实际上顶层里面只要包含端口即可&#xff0c;即便是空的也没事。 引入时会报warni…

OpenAI o3-mini全面解析:最新免费推理模型重磅发布

引言 2025年1月31日&#xff0c;OpenAI重磅发布全新推理模型o3-mini。这款模型作为OpenAI推理系列的最新突破&#xff0c;不仅在性能和性价比方面实现跨越式提升&#xff0c;更是首次全面开放免费使用。这一重大举措彰显了OpenAI在人工智能技术普及和成本优化领域的创新决心。…

文件读写操作

写入文本文件 #include <iostream> #include <fstream>//ofstream类需要包含的头文件 using namespace std;void test01() {//1、包含头文件 fstream//2、创建流对象ofstream fout;/*3、指定打开方式&#xff1a;1.ios::out、ios::trunc 清除文件内容后打开2.ios:…

TensorFlow 示例摄氏度到华氏度的转换(一)

TensorFlow 实现神经网络模型来进行摄氏度到华氏度的转换&#xff0c;可以将其作为一个回归问题来处理。我们可以通过神经网络来拟合这个简单的转换公式。 1. 数据准备与预处理 2. 构建模型 3. 编译模型 4. 训练模型 5. 评估模型 6. 模型应用与预测 7. 保存与加载模型 …

99.24 金融难点通俗解释:MLF(中期借贷便利)vs LPR(贷款市场报价利率)

目录 0. 承前1. 什么是MLF&#xff1f;1.1 专业解释1.2 通俗解释1.3 MLF的三个关键点&#xff1a; 2. 什么是LPR&#xff1f;2.1 专业解释2.2 通俗解释2.3 LPR的三个关键点&#xff1a; 3. MLF和LPR的关系4. 传导机制4.1 第一步&#xff1a;央行调整MLF4.2 第二步&#xff1a;银…

此虚拟机的处理器所支持的功能不同于保存虚拟机状态的虚拟机的处理器所支持的功能

1.问题&#xff1a;今天记录下自己曾经遇到的一个问题&#xff0c;就是复制别人虚拟机时弹出来的一个报错&#xff1a; 如图&#xff0c;根本原因就在于虚拟机版本的问题&#xff0c;无法对应的上&#xff0c;所以必须升级虚拟机。 2.问题解决&#xff1a; 1.直接点击放弃,此时…

Linux命令入门

Linux命令入门 ls命令 ls命令的作用是列出目录下的内容&#xff0c;语法细节如下: 1s[-a -l -h] [Linux路径] -a -l -h是可选的选项 Linux路径是此命令可选的参数 当不使用选项和参数,直接使用ls命令本体,表示:以平铺形式,列出当前工作目录下的内容 ls命令的选项 -a -a选项&a…

10 Flink CDC

10 Flink CDC 1. CDC是什么2. CDC 的种类3. 传统CDC与Flink CDC对比4. Flink-CDC 案例5. Flink SQL 方式的案例 1. CDC是什么 CDC 是 Change Data Capture&#xff08;变更数据获取&#xff09;的简称。核心思想是&#xff0c;监测并捕获数据库的变动&#xff08;包括数据或数…

【Redis】set 和 zset 类型的介绍和常用命令

1. set 1.1 介绍 set 类型和 list 不同的是&#xff0c;存储的元素是无序的&#xff0c;并且元素不允许重复&#xff0c;Redis 除了支持集合内的增删查改操作&#xff0c;还支持多个集合取交集&#xff0c;并集&#xff0c;差集 1.2 常用命令 命令 介绍 时间复杂度 sadd …

happytime

happytime 一、查壳 无壳&#xff0c;64位 二、IDA分析 1.main 2.cry函数 总体&#xff1a;是魔改的XXTEA加密 在main中可以看到被加密且分段的flag在最后的循环中与V6进行比较&#xff0c;刚好和上面v6数组相同。 所以毫无疑问密文是v6. 而与flag一起进入加密函数的v5就…

【etcd】二进制安装etcd

由于生产服务器不能使用yum 安装 etcd ,或者 安装的etcd 版本比较老&#xff0c;这里介绍一个使用二进制安装的方式。 根据安装文档编写一个下载脚本即可 &#xff1a; 指定 etcd 的版本 提供了两个下载地址 一个 Google 一个 Github&#xff0c; 不过都需要外网 注释掉删除保…