图库+智能bi的笔记

智能BI

esayExcel

引入easyExcel来读取Excel文件

依赖

xml
复制代码
<!-- easy Excel --> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.1.0</version> </dependency>

配置实体类

java
复制代码
@Data @EqualsAndHashCode public class IndexOrNameData { /* * 用名字去匹配,这里需要注意,如果名字重复,会导致只有一个字段读取到数据 * @ExcelProperty("成员") 默认读的就是第一列的名字 */ @ExcelProperty("成员") private String name; @ExcelProperty("班级") private String aaa; }

配置项,拦截器,在发生前和完成后做点什么

java
复制代码
@Slf4j public class DemoDataListener implements ReadListener<IndexOrNameData> { /** * 每读一条数据都会调用这个方法 * @param data * @param context */ @Override public void invoke(IndexOrNameData data, AnalysisContext context) { // data就是所有数据 System.out.println(data); } /** * 所有数据解析完成了 都会来调用 * * @param context */ @Override public void doAfterAllAnalysed(AnalysisContext context) { // 这里也要保存数据,确保最后遗留的数据也存储到数据库 // saveData(); log.info("所有数据解析完成!"); } }

最后是就是调用了,2种方式同步和异步,同步适合数据量小,异步适合数据量大

java
复制代码
public class importexcel { public static void main(String[] args) { // since: 3.0.0-beta1 String fileName ="D:\\code\\Intelligent-BI\\Intelligent-BI-backend\\src\\main\\resources\\test1.xlsx"; // 这里默认每次会读取100条数据 然后返回过来 直接调用使用数据就行 // 具体需要返回多少行可以在`PageReadListener`的构造函数设置 readByListener(fileName); } // 同步读取 public static void readByListener(String fileName) { EasyExcel.read(fileName, IndexOrNameData.class,new DemoDataListener()).sheet().doRead(); } // 异步读取 public static void readSyncByListener(String fileName) throws FileNotFoundException { File file = ResourceUtils.getFile("classpath:test1.xlsx"); List<Map<Integer,String>> list = EasyExcel.read(file).excelType(ExcelTypeEnum.XLSX).sheet().headRowNumber(0).doReadSync(); System.out.println(list); } }

异步调用并将excel从xlsx转为csv格式(推荐,而且不需要实体类,拦截器看你需求,反正我是没加)

java
复制代码
public static String excelToCsv(MultipartFile multipartFile) { // 读取数据 List<Map<Integer, String>> list = null; try { // headRowNumber(0)表示表头在第一行 list = EasyExcel.read(multipartFile.getInputStream()) .excelType(ExcelTypeEnum.XLSX) .sheet() .headRowNumber(0) .doReadSync(); } catch (IOException e) { log.error("表格处理错误", e); } if (CollUtil.isEmpty(list)) { return ""; } // 转换为 csv StringBuilder stringBuilder = new StringBuilder(); // 读取表头(表头第一行) LinkedHashMap<Integer, String> headerMap = (LinkedHashMap) list.get(0); List<String> headerList = headerMap.values().stream().filter(ObjectUtils::isNotEmpty).collect(Collectors.toList()); stringBuilder.append(StringUtils.join(headerList, ",")).append("\n"); // 读取数据 for (int i = 1; i < list.size(); i++) { LinkedHashMap<Integer, String> dataMap = (LinkedHashMap) list.get(i); List<String> dataList = dataMap.values().stream().filter(ObjectUtils::isNotEmpty).collect(Collectors.toList()); stringBuilder.append(StringUtils.join(dataList, ",")).append("\n"); } return stringBuilder.toString(); }
java
复制代码
EasyExcel.read(multipartFile.getInputStream()). excelType(ExcelTypeEnum.XLSX).sheet().headRowNumber(0).doReadSync();

这行代码会读取Excel文件的全部数据,包括表头和数据

分析:

  1. 这段代码将读取到的数据转换为CSV格式。首先,读取表头并过滤掉空值,然后读取每一行的数据并过滤掉空值,最后将表头和数据拼接成CSV格式的字符串。

可能的问题

  1. Excel文件格式:确保上传的文件是.xlsx格式,否则excelType(ExcelTypeEnum.XLSX)可能会出错。
  2. 表头和数据行数:确保Excel文件至少有两行,第一行是表头,第二行开始是数据。
  3. 数据类型:确保Excel中的数据类型与Map<Integer, String>匹配,否则可能会出现类型转换错误。

拿到的结果

json
复制代码
分析目标:12 我的数据:成员,班级 徐瑜蔚,网络232 罗奕恒,物联网221 林鑫桐,移动223 陈炫东,移动223 蓝増鑫,物联网222 郑柳静,大数据221 潘瑞鸿,广告222 符城,移动221 谭思敏,物联网23 卢子坤,移动231 黄宇然,移动232 马朝凯,大数据232 扶政丰,大数据232 钟楚丹,企管233 沈丽玲,会计232

对了上面的代码用到了hutool工具库的方法,不要忘了引入

xml
复制代码
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.8</version> </dependency>

AI调用

引入SDK

xml
复制代码
<dependency> <groupId>cn.bigmodel.openapi</groupId> <artifactId>oapi-java-sdk</artifactId> <version>release-V4-2.3.0</version> </dependency>

在yml文件定义apiKey(自己封装)

yaml
复制代码
ai: api-key: ...

apikey封装配置类

java
复制代码
@Configuration @ConfigurationProperties(prefix = "ai") @Data public class AiConfig { private String apiKey; @Bean public ClientV4 clientV4(){ return new ClientV4.Builder(apiKey).build(); } }

prompt

tex
复制代码
你是一个数据分析师和前端开发专家,接下来我会按照以下固定格式给你提供内容: 分析需求: {数据分析的需求或者目标} 原始数据: {csv格式的原始数据,用,作为分隔符} 请根据这两部分内容,按照以下指定格式生成内容(此外不要输出任何多余的开头、结尾、注释) 【【【【【 {前端可视化工具 Echart 的 option 配置对象js代码,合理地将数据进行可视化,不要生成任何多余的内容,比如注释} 【【【【【 {明确的数据分析结论、越详细越好,不要生成多余的注释} 【【【【【 以下是示例代码 【【【【【 ({ type: "view", autoFit: true, data: [ { year: "1991", value: 3 }, { year: "1992", value: 4 }, { year: "1993", value: 3.5 }, { year: "1994", value: 5 }, { year: "1995", value: 4.9 }, { year: "1996", value: 6 }, { year: "1997", value: 7 }, { year: "1998", value: 9 }, { year: "1999", value: 13 }, ], encode: { x: "year", y: "value" }, scale: { x: { range: [0, 1] }, y: { domainMin: 0, nice: true } }, children: [ { type: "line", labels: [{ text: "value", style: { dx: -10, dy: -12 } }] }, { type: "point", style: { fill: "white" }, tooltip: false }, ], }); 【【【【【 根据数据分析可得,该网站用户数量逐日增长,时间越长,用户数量增长越多。

新版promet

tex
复制代码
你是一个数据分析师和前端开发专家,接下来我会按照以下固定格式给你提供内容: 分析需求: {数据分析的需求或者目标} 原始数据: {csv格式的原始数据,用,作为分隔符} 请根据这两部分内容,严格按照以下指定格式生成内容(此外不要输出任何多余的开头、结尾、注释)同时不要使用这个符号 '】' '【【【【【' {前端 ANTV的G2的v5.2.2版本 的 option 配置对象 JSON 代码, 不要生成任何多余的内容,比如注释和代码块标记} '【【【【【' {明确的数据分析结论、越详细越好,不要生成多余的注释} 下面是一个具体的例子的模板: '【【【【【' JSON格式代码 '【【【【【' 结论:

前端封装调用后端接口(axios)

这里在调用的时候遇到一个问题就是,axios不能单纯的传文件,必须修改一些属性

接口,这里接口的请求头要改

js
复制代码
export const genChartByAi = (data) => request.post('/chart/gen', data, { headers: { 'Content-Type': 'multipart/form-data', }, })

调用接口,可以看到我们这里如果要传文件和其他变量的话,就一定要new一个formdata,然后通过append来传参,这样后端就接收到所有参数了

js
复制代码
const formState = ref({ file: [], goal: '', ChartName: '', ChartType: '', }) const onFinish = async () => { console.log('formState:', formState.value) // 传入的第一个文件 const file = formState.value.file[0].originFileObj const formData = new FormData() formData.append('file', file) formData.append('goal', formState.value.goal) formData.append('chartName', formState.value.ChartName) formData.append('chartType', formState.value.ChartType) console.log(formData) const res = await genChartByAi(formData) console.log(res) }

封装后端传过来的图表数据

js
复制代码
//获取完数据直接解构并存pinia里面 const res = await genChartByAi(formData).catch((err) => { console.log(err) loading.value = false }) console.log(res) if (res?.code == 0) { message.success('提交成功') // result.value = res.data.aiGenResult chartStore.SET_AIGENRESULT(res.data.aiGenResult) result.value = res.data.aiGenResult chartStore.SET_AIGENCHART(JSON.parse(res.data.aiGenChart)) console.log('结论数据', res.data.aiGenResult) console.log('pinia的结论数据', chartStore.AIGENRESULT) } else { message.error('提交失败') } loading.value = false }

pinia

js
复制代码
import { reactive, ref } from 'vue' import { defineStore } from 'pinia' export const useChartStore = defineStore('chart', () => { const AIGENRESULT = ref('') const AIGENCHART = ref({}) const SET_AIGENRESULT = (data) => { AIGENRESULT.value = data } const SET_AIGENCHART = (data) => { AIGENCHART.value = data console.log('AIGENCHART', data) } return { AIGENRESULT, SET_AIGENRESULT, AIGENCHART, SET_AIGENCHART, } })

VUE3里加如果没有匹配的路由就自动跳转到404

在路由里面加上这两个路由

js
复制代码
// 404页面(1级) { path: '/404', component: () => import('@/components/error.vue'), hidden: true }, //这个*匹配必须放在最后,将改路由配置放到所有路由的配置信息的最后,否则会其他路由path匹配造成影响。 { path: '/:catchAll(.*)', // 不识别的path自动匹配404 redirect: '/404', },

强制关闭端口

powershell
复制代码
PS C:\Users\HYR> netstat -nao | findstr "8081" TCP 0.0.0.0:8081 0.0.0.0:0 LISTENING 50696 TCP [::]:8081 [::]:0 LISTENING 50696 PS C:\Users\HYR> taskkill -pid 50696 -f

##VUE登录权限,获取用户信息

Pinia响应式丢失问题

storeToRefs+数组解构能很好的解决问题

js
复制代码
const { AIGENRESULT: result } = storeToRefs(chartStore)

性能系统优化

限制文件大小 消息队列 加锁 线程池

校验文件

java
复制代码
// 校验文件 long size = multipartFile.getSize(); String filename = multipartFile.getOriginalFilename(); final long ONE_SIZE = 1024 * 1024 ; // 文件不能超过1M if (size > ONE_SIZE){ throw new BesinessException(ErrorCode.PARAMS_ERROR, "文件过大"); } // 校验文件后缀 String validFileSuffixList = FileUtil.getSuffix(filename); // 文件白名单(允许用的文件类型) List<String> list = Arrays.asList("xls", "xlsx","csv"); if(!list.contains(validFileSuffixList)){ throw new BesinessException(ErrorCode.PARAMS_ERROR, "文件类型错误"); }

限流

分布式限流

这里把以后要部署多台服务器的情况也考虑进去,分布式也是适用单机的。

####Redission限流实现

Redission内置了一个限流工具类,可以帮助你理由redis来统计

1.使用redisson(一个操作redis的工具库)

xml
复制代码
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.40.0</version> </dependency>

Redission配置类

java
复制代码
@Configuration @ConfigurationProperties(prefix = "spring.redis") @Data public class RedissionConfig { private Integer port; private String host; private Integer database; @Bean public RedissonClient getRedissonClient(){ Config config = new Config(); // 这里由于我们现在还是单机,所以使用单机配置useSingleServer,集群多机就要useClusterServers config.useSingleServer().setDatabase(1).setAddress("redis://"+host+":"+port); // or read config from file // config = Config.fromYAML(new File("config-file.yaml")); // Sync and Async API RedissonClient redisson = Redisson.create(config); return redisson; } }

Redission速率限制器的工具类

java
复制代码
@Service public class RedisLimiterManager { @Resource private RedissonClient redissonClient; public void doRateLimiter(String key) { RRateLimiter limiter = redissonClient.getRateLimiter(key); // Initialization required only once. // 5 permits per 2 seconds //1秒2次 limiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.SECONDS); // acquire 3 permits or block until they became available boolean canOP = limiter.tryAcquire(1); if (!canOP){ throw new BesinessException(ErrorCode.TOO_MANY_REQUEST); } } }

controller调用

java
复制代码
// 限流判断,每个用户一个限流器 redisLimiterManager.doRateLimiter("genChartByAI"+String.valueOf(userId));

这段代码的用途是限制对某个资源的访问频率,防止资源被过度使用。例如,可以用于限制API的调用频率,防止DDoS攻击,或者限制数据库查询的频率

异步化

这里先思考一个问题,加入你把这个项目单机部署在一台服务器上,此时有多个的用户同时调用AI生成接口,而1台服务器只能同时处理一个请求,ai的生成调用又需要较长时间,如果同时抛出2个接口,会导致接口压力过大,更甚直接崩(高并发)。那么怎么去处理这个问题呢?

解决:加一个中间件来处理这些用户的消息,给用户的请求排队(任务队列),上个请求处理完了,才可以开始处理下个请求,如果处理的压力过大就直接给拒绝掉,如果还有余力的话,就给他排队调用

这个中间件就是线程池

线程池

说白了,以上说的各类需求都是由于Ai服务加载较慢,我们需要给他排队列好需求防止崩掉

代码实现

编写线程池配置类

java
复制代码
@Configuration public class ThreadPoolExecutorConfig { @Bean public ThreadPoolExecutor threadPoolExecutor(){ ThreadFactory threadFactory=new ThreadFactory() { private int count=1; @Override public Thread newThread(@NotNull Runnable r) { Thread thread=new Thread(r); thread.setName("线程"+count); return thread; } }; //下面这行代码分别是线程池默认2个,大于2个可以放到任务队列里面,任务队列默认4个,但是如果任务多(超过任务队列4个了),全部线程有了6个之后,可以将线程池提到4个,线程持续时间100秒,任务队列默认4个,也就是说这里面可以执行的4个加上排队的4个,一共最多8个,超过8个再加报错 ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(2,4,100, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4),threadFactory); return threadPoolExecutor; } }

编写接口类测试

java
复制代码
@RestController @RequestMapping("/queue") @Slf4j public class QueueController { @Resource // 自动注入一个线程池的实例 private ThreadPoolExecutor threadPoolExecutor; @GetMapping("/add") // 接收一个参数name,然后将任务添加到线程池中 public void add(String name) { // 使用CompletableFuture运行一个异步任务 CompletableFuture.runAsync(() -> { // 打印一条日志信息,包括任务名称和执行线程的名称 log.info("任务执行中:" + name + ",执行人:" + Thread.currentThread().getName()); try { // 让线程休眠10分钟,模拟长时间运行的任务 Thread.sleep(600000); } catch (InterruptedException e) { e.printStackTrace(); } // 异步任务在threadPoolExecutor中执行 }, threadPoolExecutor); } @GetMapping("/get") // 该方法返回线程池的状态信息 public String get() { // 创建一个HashMap存储线程池的状态信息 Map<String, Object> map = new HashMap<>(); // 获取线程池的队列长度 int size = threadPoolExecutor.getQueue().size(); // 将队列长度放入map中 map.put("队列长度", size); // 获取线程池已接收的任务总数 long taskCount = threadPoolExecutor.getTaskCount(); // 将任务总数放入map中 map.put("任务总数", taskCount); // 获取线程池已完成的任务数 long completedTaskCount = threadPoolExecutor.getCompletedTaskCount(); // 将已完成的任务数放入map中 map.put("已完成任务数", completedTaskCount); // 获取线程池中正在执行任务的线程数 int activeCount = threadPoolExecutor.getActiveCount(); // 将正在工作的线程数放入map中 map.put("正在工作的线程数", activeCount); // 将map转换为JSON字符串并返回 return JSONUtil.toJsonStr(map); } }

RabbitMQ

首先去官网下载安装,按照提示

然后去cmd开启配置

powershell
复制代码
rabbitmq-plugins.bat enable rabbitmq_management

然后就可以访问打开ui界面

net
复制代码
http://localhost:15672

如果不行的话重启一下服务

powershell
复制代码
# 关闭服务 net stop rabbitmq # 开启服务 net start rabbitmq

账号密码默认都是guest

本地能运行起来就可以在代码层面实现了

代码实现

producer代码(发出"消息")

java
复制代码
public class SingProducer { private final static String QUEUE_NAME = "hello"; public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); try (Connection connection = factory.newConnection(); Channel channel = connection.createChannel()) { channel.queueDeclare(QUEUE_NAME, false, false, false, null); String message = "Hello World!"; channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8)); System.out.println(" [x] Sent '" + message + "'"); } } }

consumer代码(接收消息)

java
复制代码
import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.DeliverCallback; import java.nio.charset.StandardCharsets; public class Recv { private final static String QUEUE_NAME = "hello"; public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); channel.queueDeclare(QUEUE_NAME, false, false, false, null); System.out.println(" [*] Waiting for messages. To exit press CTRL+C"); DeliverCallback deliverCallback = (consumerTag, delivery) -> { String message = new String(delivery.getBody(), StandardCharsets.UTF_8); System.out.println(" [x] Received '" + message + "'"); }; channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { }); } }

注意:同名称的消息队列只能用同样的参数调用一次

这样就实现了一次消息的发生和接收了

交换机

交换机的部分真听不懂,可能是还没到需要的业务场景,这里留个空,以后补

前端精度丢失问题

解决:在后端加一个配置类

java
复制代码
/** * Spring MVC Json 配置 */ @JsonComponent public class JsonConfig { /** * 添加 Long 转 json 精度丢失的配置 */ @Bean public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { ObjectMapper objectMapper = builder.createXmlMapper(false).build(); SimpleModule module = new SimpleModule(); module.addSerializer(Long.class, ToStringSerializer.instance); module.addSerializer(Long.TYPE, ToStringSerializer.instance); objectMapper.registerModule(module); return objectMapper; } }
html
复制代码
<template #bodyCell="{ column, record }"> <template v-if="column.dataIndex === 'userAvatar'"> <a-image :src="record.userAvatar" :width="120" /> </template> <template v-else-if="column.dataIndex === 'userRole'"> <div v-if="record.userRole === 'admin'"> <a-tag color="green">管理员</a-tag> </div> <div v-else> <a-tag color="blue">普通用户</a-tag> </div> </template> <template v-else-if="column.dataIndex === 'createTime'"> {{ dayjs(record.createTime).format('YYYY-MM-DD HH:mm:ss') }} </template> <template v-else-if="column.key === 'action'"> <a-button danger>删除</a-button> </template> </template>

图片功能开发

全局跨域配置类

java
复制代码
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { // 覆盖所有请求 registry.addMapping("/**") // 允许发送 Cookie .allowCredentials(true) // 放行哪些域名(必须用 patterns,否则 * 会和 allowCredentials 冲突) .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .exposedHeaders("*"); } }

前端引入openapi

powershell
复制代码
npm i --save-dev @umijs/openapi

前端配置类

js
复制代码
import { generateService } from '@umijs/openapi' generateService({ requestLibPath: "import request from '@/utils/myaxios'", schemaPath: 'http://localhost:8081/api/v3/api-docs', serversPath: './src', })

在package.json加上

js
复制代码
"openapi": "node openapi.config.js",

这样用命令行就可以用了(注:使用会在src包下生成api文件,如果原来有的话会覆盖掉)

js
复制代码
npm run openapi
java
复制代码
public interface UserConstant { /** * 用户登录态键 */ String USER_LOGIN_STATE = "user_login"; // region 权限 /** * 默认角色 */ String DEFAULT_ROLE = "user"; /** * 管理员角色 */ String ADMIN_ROLE = "admin"; // endregion }

AOP切面编程,接口权限注解

给接口上权限注解,规定只有管理员才能用

依赖

xml
复制代码
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>

编写注解类

java
复制代码
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AuthCheck { /** * 必须有某个角色 */ String mustRole() default ""; }

权限注册切面(回去沉淀吧)

java
复制代码
@Aspect @Component public class AuthInterceptor { @Resource private UserService userService; /** * 执行拦截 * * @param joinPoint 切入点 * @param authCheck 权限校验注解 */ @Around("@annotation(authCheck)") public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable { String mustRole = authCheck.mustRole(); RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes(); HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); // 当前登录用户 User loginUser = userService.getLoginUser(request); UserRoleEnum mustRoleEnum = UserRoleEnum.getEnumByValue(mustRole); // 不需要权限,放行 if (mustRoleEnum == null) { return joinPoint.proceed(); } // 以下为:必须有该权限才通过 // 获取当前用户具有的权限 UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValue(loginUser.getUserRole()); // 没有权限,拒绝 if (userRoleEnum == null) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } // 要求必须有管理员权限,但用户没有管理员权限,拒绝 if (UserRoleEnum.ADMIN.equals(mustRoleEnum) && !UserRoleEnum.ADMIN.equals(userRoleEnum)) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } // 通过权限校验,放行 return joinPoint.proceed(); } }

##分页功能修复

要加上分页的插件

xml
复制代码
<!-- MyBatis-Plus Spring Boot Starter --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.9</version> </dependency> <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-jsqlparser --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-jsqlparser</artifactId> <version>3.5.9</version> </dependency>

配置类 拦截器

java
复制代码
@Configuration @MapperScan("com.yupi.yupicturebackend.mapper") public class MyBatisPlusConfig { /** * 拦截器配置 * * @return {@link MybatisPlusInterceptor} */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

ANTD国际化配置

vue
复制代码
<script setup> // 引入中文语言包 import zhCN from 'ant-design-vue/es/locale/zh_CN' </script> <template> <a-config-provider :locale="zhCN"> <RouterView></RouterView> </a-config-provider> </template> <style scoped></style>

##前端菜单的编写和权限管理

vue
复制代码
<a-menu v-model:selectedKeys="selectedKeys" mode="horizontal" :style="{ lineHeight: '64px' }" :items="filterMenusResult" @click="doMenuClick" ></a-menu>
js
复制代码
// 菜单(这里我们规定key以admin开头的菜单为管理员菜单,普通用户登录是看不到的) const items = ref([ { key: 'home', // icon: () => h(MailOutlined), label: '主页', title: '主页', }, { key: 'add_picture', label: '创建图片', title: '创建图片', }, { key: 'chart', label: '智能分析', title: '智能分析', }, { key: 'chartAsync', label: '异步分析', title: '异步分析', }, { key: 'mychart', // icon: () => h(MailOutlined), label: '图表管理', title: '图表管理', }, { key: 'adminUserManager', // icon: () => h(MailOutlined), label: '用户管理', title: '用户管理', }, { key: 'my', // icon: () => h(MailOutlined), label: '我的', title: '我的', }, ]) // 切换菜单 const doMenuClick = ({ key }) => { router.push({ name: key }) } // 路由更新时自动更新选中的菜单项 router.afterEach((to) => { console.log('菜单更新', to) selectedKeys.value = [to.name] }) // 根据权限过滤菜单项 const filterMenus = (menus) => { return menus?.filter((menu) => { // 如果菜单以admin开头 if (menu.key.startsWith('admin')) { // 获取pinia里的用户信息 // 如果用户信息不存在,或者存在了但userRole不是管理员,就返回false,不显示这个菜单 const loginUser = useUserStore().userInfo if (!loginUser || loginUser.userRole !== 'admin') { return false } } return true }) } // 写完方法,我们来引用,过滤原始的菜单项(有变化都要重新过滤一次用computed) const filterMenusResult = computed(() => { return filterMenus(items.value) })

这样子可以多字段查找,就一个搜索条件你可以搜name也可以搜introduce

java
复制代码
// 从多字段中搜索 if (StrUtil.isNotBlank(searchText)) { // 需要拼接查询条件 queryWrapper.and(qw -> qw.like("name", searchText) .or() .like("introduction", searchText) ); }

腾讯云COS存储

首先数据库图片表

sql
复制代码
-- 图片表 create table if not exists picture ( id bigint auto_increment comment 'id' primary key, url varchar(512) not null comment '图片 url', name varchar(128) not null comment '图片名称', introduction varchar(512) null comment '简介', category varchar(64) null comment '分类', tags varchar(512) null comment '标签(JSON 数组)', picSize bigint null comment '图片体积', picWidth int null comment '图片宽度', picHeight int null comment '图片高度', picScale double null comment '图片宽高比例', picFormat varchar(32) null comment '图片格式', userId bigint not null comment '创建用户 id', createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间', editTime datetime default CURRENT_TIMESTAMP not null comment '编辑时间', updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', isDelete tinyint default 0 not null comment '是否删除', INDEX idx_name (name), -- 提升基于图片名称的查询性能 INDEX idx_introduction (introduction), -- 用于模糊搜索图片简介 INDEX idx_category (category), -- 提升基于分类的查询性能 INDEX idx_tags (tags), -- 提升基于标签的查询性能 INDEX idx_userId (userId) -- 提升基于用户 ID 的查询性能 ) comment '图片' collate = utf8mb4_unicode_ci;

###导入依赖

xml
复制代码
<!-- 腾讯云对象存储--> <dependency> <groupId>com.qcloud</groupId> <artifactId>cos_api</artifactId> <version>5.6.227</version> </dependency>

yml配置腾讯云对象存储

yaml
复制代码
# 对象存储配置(需要从腾讯云获取) cos: client: host: xxx secretId: xxx secretKey: xxxx region: xxx bucket: xxx

封装配置类

java
复制代码
/** * 对象存储配置文件 */ @Configuration @ConfigurationProperties(prefix = "cos.client") @Data public class CosClientConfig { /** * 域名 */ private String host; /** * secretId */ private String secretId; /** * 密钥(注意不要泄露) */ private String secretKey; /** * 区域 */ private String region; /** * 桶名 */ private String bucket; @Bean public COSClient cosClient() { // 初始化用户身份信息(secretId, secretKey) COSCredentials cred = new BasicCOSCredentials(secretId, secretKey); // 设置bucket的区域, COS地域的简称请参照 https://www.qcloud.com/document/product/436/6224 ClientConfig clientConfig = new ClientConfig(new Region(region)); // 生成cos客户端 return new COSClient(cred, clientConfig); } }

封装通用方法类(上传下载) 注:腾讯云要开通数据万象

java
复制代码
@Component public class CosManager { @Resource private CosClientConfig cosClientConfig; @Resource private COSClient cosClient; /** * 上传对象 * * @param key 唯一键 * @param file 文件 */ public PutObjectResult putObject(String key, File file) { PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key, file); return cosClient.putObject(putObjectRequest); } /** * 下载对象 * * @param key 唯一键 */ public COSObject getObject(String key) { GetObjectRequest getObjectRequest = new GetObjectRequest(cosClientConfig.getBucket(), key); return cosClient.getObject(getObjectRequest); } /** * 上传对象,并且附带图片信息(解析) * 注:腾讯云要开通数据万象 * * @param key 唯一键 * @param file 文件 */ public PutObjectResult putPictureObject(String key, File file) { PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key, file); // 获取图片信息,解析 PicOperations picOperations = new PicOperations(); // 1表示返回图片信息 picOperations.setIsPicInfo(1); // 构造处理参数 putObjectRequest.setPicOperations(picOperations); return cosClient.putObject(putObjectRequest); } }

文件上传(逻辑)

java
复制代码
@Service @Slf4j public class FileManager { @Resource private CosClientConfig cosClientConfig; @Resource private CosManager cosManager; /** * 用户上传图片通用方法 * @param multipartFile * @param uploadPathPrefix * @return */ public UploadPictureResult uploadPicture(MultipartFile multipartFile,String uploadPathPrefix) { // 校验图片 validPicture(multipartFile); // 图片上传地址 // 随机生成16为字符串放在url地址前面避免重复 String uuid = RandomUtil.randomString(16); // 获取上传文件的原始文件名 example.txt的文件,那么multipartFile.getOriginalFilename()将返回example.txt String originalFilename = multipartFile.getOriginalFilename(); // 自己拼接文件名字,避免重复和增加安全性 String uploadFileName = String.format("%s_%s.%s", DateUtil.formatDate(new Date()), uuid,FileUtil.getSuffix(originalFilename)); // 定义文件路径,把不同用户的图片放到不同的文件夹下,文件夹的前缀由用户的id定义 String uploadPath = String.format("/%s/%s", uploadPathPrefix,uploadFileName); // 解析结果并返回 File file = null; try { // 上传文件 file = File.createTempFile(uploadPath, null); multipartFile.transferTo(file); // 对象存储到腾讯云 PutObjectResult putObjectResult = cosManager.putPictureObject(uploadPath, file); // 获取图片信息 ImageInfo imageInfo = putObjectResult.getCiUploadResult().getOriginalInfo().getImageInfo(); // 图片的宽高和宽高比 图片格式 int width = imageInfo.getWidth(); int height = imageInfo.getHeight(); double scale = NumberUtil.round(width *1.0 / height, 2).doubleValue(); String format = imageInfo.getFormat(); // 封装返回结果 UploadPictureResult uploadPictureResult = new UploadPictureResult(); uploadPictureResult.setUrl(cosClientConfig.getHost() + "/" + uploadPath ); uploadPictureResult.setPicName(FileUtil.mainName(originalFilename)); uploadPictureResult.setPicSize(FileUtil.size(file)); uploadPictureResult.setPicWidth(width); uploadPictureResult.setPicHeight(height); uploadPictureResult.setPicScale(scale); uploadPictureResult.setPicFormat(format); // 图片结果 return uploadPictureResult; } catch (Exception e) { log.error("file upload error, filepath = " + uploadPath, e); throw new BesinessException(ErrorCode.SYSTEM_ERROR, "上传失败"); } finally { // 临时文件清理 deleteTempFile(file); } } /** * 图片的校验逻辑 * @param multipartFile */ private void validPicture(MultipartFile multipartFile) { // 检验文件大小 long size = multipartFile.getSize(); final long ONE_M=1024 * 1024 ; if (size > ONE_M) { throw new BesinessException(ErrorCode.PARAMS_ERROR,"文件大小不能超过1M"); } // 校验文件后缀 String suffix = FileUtil.getSuffix(multipartFile.getOriginalFilename()); // 允许上传的文件列表 final List<String> ALLOW_FORMAT_LIST = Arrays.asList("png", "jpg", "jpeg", "bmp", "gif","webp"); if (!ALLOW_FORMAT_LIST.contains(suffix)) { throw new BesinessException(ErrorCode.PARAMS_ERROR,"文件类型错误"); } } /** * 图片的异常逻辑 * @param file */ public void deleteTempFile(File file) { if (file != null) { return; } // 删除临时文件 boolean delete = file.delete(); if (!delete) { log.error("file delete error, filepath = {}", file.getAbsolutePath()); } } }

Controller测试

java
复制代码
@PostMapping("/upload") public BaseResponse<PictureVO> UploadFile(@RequestPart("file") MultipartFile multipartFile, PictureUploadRequest pictureUploadRequest, HttpServletRequest request) { User loginuser = userService.getloginuser(request); PictureVO pictureVO = pictureService.uploadPicture(multipartFile, pictureUploadRequest, loginuser); // 这里有个bug,如果Spring为了防止文件攻击所以限制了上传文件的大小,所以我们要主动开启 return ResultUtils.success(pictureVO); }

这里有个bug,如果Spring为了防止文件攻击所以限制了上传文件的大小,所以我们要主动开启

yaml
复制代码
spring: servlet: multipart: max-file-size: 10MB

后端props父组件拿到回调的东西,再通过props给子组件

父组件

js
复制代码
<template> <div id="addPicturePage"> <PictureUpload :picture="picture" :onSuccess="onSuccess"></PictureUpload> </div> </template> <script setup> import PictureUpload from '@/components/pictureUpload.vue' import { ref } from 'vue' const picture = ref() const onSuccess = (newvalue) => { picture.value = newvalue } </script>

子组件

js
复制代码
const props = defineProps({ picture: { type: String, }, onSuccess: { type: Function, }, }) const loading = ref(false) /** * 上传图片 * @param param0 */ const handleUpload = async ({ file }) => { loading.value = true try { const formData = new FormData() const params = props.picture ? props.picture.id : '' formData.append('file', file) formData.append('id', params) const res = await uploadPicture(formData) if (res.code == 0 && res.data) { message.success('上传成功') // 将上传成功的信息返回给父组件 props.onSuccess?.(res.data) } else { message.error('上传失败' + res.message) } } catch (error) { message.error('error') message.error('上传失败' + error.message) } loading.value = false }

图片管理页50分钟,先去看主页

js
复制代码
// 数据 const dataList = ref([]) const total = ref(0) // 搜索条件 const searchParams = reactive<API.PictureQueryRequest>({ current: 1, pageSize: 10, sortField: 'createTime', sortOrder: 'descend', }) // 分页参数 const pagination = computed(() => { return { current: searchParams.current ?? 1, pageSize: searchParams.pageSize ?? 10, total: total.value, showSizeChanger: true, showTotal: (total) => `共 ${total} 条`, } }) // 获取数据 const fetchData = async () => { const res = await listPictureByPageUsingPost({ ...searchParams, }) if (res.data.data) { dataList.value = res.data.data.records ?? [] total.value = res.data.data.total ?? 0 } else { message.error('获取数据失败,' + res.data.message) } } // 页面加载时请求一次 onMounted(() => { fetchData() }) // 获取数据 const doSearch = () => { // 重置搜索条件 searchParams.current = 1 fetchData() } // 表格变化处理 const doTableChange = (page: any) => { searchParams.current = page.current searchParams.pageSize = page.pageSize fetchData() }

通过URL获取图片

主要是通过hutool里面的httpUtils.downfile来实现

这是以前的上传图片的代码,我们在以前的代码上改动更方便

1.首先将传入参数换成String

2.校验图片方法加一个判断String类型的,之前是MultipartFile类型的

3.获取URL名称

4.保存临时文件,下载下来保存到数据库

java
复制代码
public UploadPictureResult uploadPictureByURL(String fileUrl,String uploadPathPrefix) { // 校验图片 validPicture(fileUrl); // 图片上传地址 // 随机生成16为字符串放在url地址前面避免重复 String uuid = RandomUtil.randomString(16); // 获取上传文件的原始文件名 example.txt的文件,那么multipartFile.getOriginalFilename()将返回example.txt String originalFilename = FileUtil.mainName(fileUrl); // 自己拼接文件名字,避免重复和增加安全性 String uploadFileName = String.format("%s_%s.%s", DateUtil.formatDate(new Date()), uuid,FileUtil.getSuffix(originalFilename)); // 定义文件路径,把不同用户的图片放到不同的文件夹下,文件夹的前缀由用户的id定义 String uploadPath = String.format("/%s/%s", uploadPathPrefix,uploadFileName); // 解析结果并返回 File file = null; try { // 上传文件 // 创建临时文件夹 file = File.createTempFile(uploadPath, null); // multipartFile.transferTo(file); // 将文件下载下来 HttpUtil.downloadFile(fileUrl,file); // 对象存储到腾讯云 PutObjectResult putObjectResult = cosManager.putPictureObject(uploadPath, file); // 获取图片信息 ImageInfo imageInfo = putObjectResult.getCiUploadResult().getOriginalInfo().getImageInfo(); // 图片的宽高和宽高比 图片格式 int width = imageInfo.getWidth(); int height = imageInfo.getHeight(); double scale = NumberUtil.round(width *1.0 / height, 2).doubleValue(); String format = imageInfo.getFormat(); // 封装返回结果 UploadPictureResult uploadPictureResult = new UploadPictureResult(); uploadPictureResult.setUrl(cosClientConfig.getHost() + "/" + uploadPath ); uploadPictureResult.setPicName(FileUtil.mainName(originalFilename)); uploadPictureResult.setPicSize(FileUtil.size(file)); uploadPictureResult.setPicWidth(width); uploadPictureResult.setPicHeight(height); uploadPictureResult.setPicScale(scale); uploadPictureResult.setPicFormat(format); // 图片结果 return uploadPictureResult; } catch (Exception e) { log.error("file upload error, filepath = " + uploadPath, e); throw new BesinessException(ErrorCode.SYSTEM_ERROR, "上传失败"); } finally { // 临时文件清理 deleteTempFile(file); } }

URL校验方法

java
复制代码
private void validPicture(String fileUrl) { ThrowUtils.throwIf(StrUtil.isBlank(fileUrl), ErrorCode.PARAMS_ERROR, "文件地址不能为空"); try { // 1. 验证 URL 格式 new URL(fileUrl); // 验证是否是合法的 URL } catch (MalformedURLException e) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件地址格式不正确"); } // 2. 校验 URL 协议 ThrowUtils.throwIf(!(fileUrl.startsWith("http://") || fileUrl.startsWith("https://")), ErrorCode.PARAMS_ERROR, "仅支持 HTTP 或 HTTPS 协议的文件地址"); // 3. 发送 HEAD 请求以验证文件是否存在 HttpResponse response = null; try { response = HttpUtil.createRequest(Method.HEAD, fileUrl).execute(); // 未正常返回,无需执行其他判断 if (response.getStatus() != HttpStatus.HTTP_OK) { return; } // 4. 校验文件类型 String contentType = response.header("Content-Type"); if (StrUtil.isNotBlank(contentType)) { // 允许的图片类型 final List<String> ALLOW_CONTENT_TYPES = Arrays.asList("image/jpeg", "image/jpg", "image/png", "image/webp"); ThrowUtils.throwIf(!ALLOW_CONTENT_TYPES.contains(contentType.toLowerCase()), ErrorCode.PARAMS_ERROR, "文件类型错误"); } // 5. 校验文件大小 String contentLengthStr = response.header("Content-Length"); if (StrUtil.isNotBlank(contentLengthStr)) { try { long contentLength = Long.parseLong(contentLengthStr); final long TWO_MB = 2 * 1024 * 1024L; // 限制文件大小为 2MB ThrowUtils.throwIf(contentLength > TWO_MB, ErrorCode.PARAMS_ERROR, "文件大小不能超过 2M"); } catch (NumberFormatException e) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件大小格式错误"); } } } finally { if (response != null) { response.close(); } } }

设计模式---模版模式

我们现在在FileManager里面有对文件和URL地址的上传,他们之间逻辑 步骤相同,唯一区别在于类型,文件是File类型URL地址是String类型,还有基于这个类型不同做的不同校验,这时候就可以写一个模版抽象类来参考实现,相当于Service和serviceimpl的关系,但区别在于service一般只有一个serviceimpl,但模版抽象类可以有多个,而且完全一样 两边都能用的代码可以放在模版类里,子类可以继承用模版类的东西,那些需要改的有差异的东西,可以给子类override 写,这样就实现了FileManager的精简,把FileManager分成3个类。

模版类

java
复制代码
@Slf4j public abstract class PictureUploadTemplate { @Resource protected CosManager cosManager; @Resource protected CosClientConfig cosClientConfig; /** * 模板方法,定义上传流程 */ public final UploadPictureResult uploadPicture(Object inputSource, String uploadPathPrefix) { // 1. 校验图片 validPicture(inputSource); // 2. 图片上传地址 String uuid = RandomUtil.randomString(16); String originFilename = getOriginFilename(inputSource); String uploadFilename = String.format("%s_%s.%s", DateUtil.formatDate(new Date()), uuid, FileUtil.getSuffix(originFilename)); String uploadPath = String.format("/%s/%s", uploadPathPrefix, uploadFilename); File file = null; try { // 3. 创建临时文件 file = File.createTempFile(uploadPath, null); // 处理文件来源(本地或 URL) processFile(inputSource, file); // 4. 上传图片到对象存储 PutObjectResult putObjectResult = cosManager.putPictureObject(uploadPath, file); ImageInfo imageInfo = putObjectResult.getCiUploadResult().getOriginalInfo().getImageInfo(); // 5. 封装返回结果 return buildResult(originFilename, file, uploadPath, imageInfo); } catch (Exception e) { log.error("图片上传到对象存储失败", e); throw new BesinessException(ErrorCode.SYSTEM_ERROR, "上传失败"); } finally { // 6. 清理临时文件 deleteTempFile(file); } } /** * 校验输入源(本地文件或 URL) */ protected abstract void validPicture(Object inputSource); /** * 获取输入源的原始文件名 */ protected abstract String getOriginFilename(Object inputSource); /** * 处理输入源并生成本地临时文件 */ protected abstract void processFile(Object inputSource, File file) throws Exception; /** * 封装返回结果 * @param originFilename 文件名 * @param file * @param uploadPath 路径 * @param imageInfo 对象存储的文件信息 * @return */ private UploadPictureResult buildResult(String originFilename, File file, String uploadPath, ImageInfo imageInfo) { UploadPictureResult uploadPictureResult = new UploadPictureResult(); int picWidth = imageInfo.getWidth(); int picHeight = imageInfo.getHeight(); double picScale = NumberUtil.round(picWidth * 1.0 / picHeight, 2).doubleValue(); uploadPictureResult.setPicName(FileUtil.mainName(originFilename)); uploadPictureResult.setPicWidth(picWidth); uploadPictureResult.setPicHeight(picHeight); uploadPictureResult.setPicScale(picScale); uploadPictureResult.setPicFormat(imageInfo.getFormat()); uploadPictureResult.setPicSize(FileUtil.size(file)); uploadPictureResult.setUrl(cosClientConfig.getHost() + "/" + uploadPath); return uploadPictureResult; } /** * 删除临时文件 */ public void deleteTempFile(File file) { if (file == null) { return; } boolean deleteResult = file.delete(); if (!deleteResult) { log.error("file delete error, filepath = {}", file.getAbsolutePath()); } } }

继承的文件上传子类

java
复制代码
public class FilePictureUploadTemplateImpl extends PictureUploadTemplate { @Override protected void validPicture(Object inputSource) { // 强转一下 MultipartFile multipartFile = (MultipartFile) inputSource; // 检验文件大小 long size = multipartFile.getSize(); final long ONE_M=1024 * 1024 ; if (size > ONE_M) { throw new BesinessException(ErrorCode.PARAMS_ERROR,"文件大小不能超过1M"); } // 校验文件后缀 String suffix = FileUtil.getSuffix(multipartFile.getOriginalFilename()); // 允许上传的文件列表 final List<String> ALLOW_FORMAT_LIST = Arrays.asList("png", "jpg", "jpeg", "bmp", "gif","webp"); if (!ALLOW_FORMAT_LIST.contains(suffix)) { throw new BesinessException(ErrorCode.PARAMS_ERROR,"文件类型错误"); } } @Override protected String getOriginFilename(Object inputSource) { // 强转一下 MultipartFile multipartFile = (MultipartFile) inputSource; return multipartFile.getOriginalFilename(); } @Override protected void processFile(Object inputSource, File file) throws Exception { // 强转一下 MultipartFile multipartFile = (MultipartFile) inputSource; multipartFile.transferTo(file); } }

继承的URL上传子类

java
复制代码
public class UrlPictureUploadTemplateImpl extends PictureUploadTemplate { @Override protected void validPicture(Object inputSource) { String fileUrl = (String) inputSource; // 校验非空 ThrowUtils.throwIf(fileUrl == null, ErrorCode.PARAMS_ERROR, "文件地址为空"); // 校验URL格式 try { // 这里利用java自带的URL,new一个对象来校验,有问题就抛异常 new URL(fileUrl); } catch (MalformedURLException e) { throw new BesinessException(ErrorCode.PARAMS_ERROR, "文件地址格式错误"); } // 校验URL协议(如果不是以http或者https开头就有问题) ThrowUtils.throwIf(!fileUrl.startsWith("http://") && !fileUrl.startsWith("https://"), ErrorCode.PARAMS_ERROR, "仅支持 http 或 https 协议的文件地址"); // 3. 发送 HEAD 请求以验证文件是否存在 // 发送 HEAD 请求以验证文件是否存在 HttpResponse response = null; try { response = HttpUtil.createRequest(Method.HEAD, fileUrl).execute(); // 未正常返回,无需执行其他判断 注意不要抛异常 if (response.getStatus() != HttpStatus.HTTP_OK) { return; } // 4. 校验文件类型 String contentType = response.header("Content-Type"); if (StrUtil.isNotBlank(contentType)) { // 允许的图片类型 final List<String> ALLOW_CONTENT_TYPES = Arrays.asList("image/jpeg", "image/jpg", "image/png", "image/webp"); ThrowUtils.throwIf(!ALLOW_CONTENT_TYPES.contains(contentType.toLowerCase()), ErrorCode.PARAMS_ERROR, "文件类型错误"); } // 5. 校验文件大小 String contentLengthStr = response.header("Content-Length"); if (StrUtil.isNotBlank(contentLengthStr)) { try { long contentLength = Long.parseLong(contentLengthStr); final long TWO_MB = 2 * 1024 * 1024L; // 限制文件大小为 2MB ThrowUtils.throwIf(contentLength > TWO_MB, ErrorCode.PARAMS_ERROR, "文件大小不能超过 2M"); } catch (NumberFormatException e) { throw new BesinessException(ErrorCode.PARAMS_ERROR, "文件大小格式错误"); } } } finally { if (response != null) { response.close(); } } } @Override protected String getOriginFilename(Object inputSource) { // 强转一下 String fileUrl = (String) inputSource; // 获取上传文件的原始文件名 example.txt的文件,那么multipartFile.getOriginalFilename()将返回example.txt return FileUtil.mainName(fileUrl); } @Override protected void processFile(Object inputSource, File file) throws Exception { // 强转一下 String fileUrl = (String) inputSource; // 将文件下载下来 HttpUtil.downloadFile(fileUrl,file); } }

上面3个类的工作完成后,我们已经可以完全用这种方法来代替了,给原来的类加上 @Deprecated表示废弃了

java
复制代码
/** * 文件通用方法类 * @Deprecated已废弃,改为用upload的模版方法优化 */ @Service @Slf4j @Deprecated public class FileManager {

那既然整个文件改了,那用到了的service也要对应改

1.将原来的MultipartFile类型换成Object,跟上面的类同理,方便强转

java
复制代码
PictureVO uploadPicture(Object imputSource, PictureUploadRequest pictureUploadRequest, User LoginUser);

2.改用到的地方

java
复制代码
@Resource private FilePictureUpload filePictureUpload; @Resource private UrlPictureUpload urlPictureUpload; // 上传图片 public PictureVO uploadPicture(Object inputSource, PictureUploadRequest pictureUploadRequest, User loginUser) { if (inputSource == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "图片为空"); } // ... // 按照用户 id 划分目录 String uploadPathPrefix = String.format("public/%s", loginUser.getId()); // 根据 inputSource 的类型判断是上传文件还是上传url PictureUploadTemplate pictureUploadTemplate = filePictureUpload; if (inputSource instanceof String) { pictureUploadTemplate = urlPictureUpload; } UploadPictureResult uploadPictureResult = pictureUploadTemplate.uploadPicture(inputSource, uploadPathPrefix); // 构造要入库的图片信息 // ... }

URL文件上传的接口实现(这个可以直接接上一个,毕竟我们设计模式只是冗杂代码的优化)

java
复制代码
/** * 通过 URL 上传图片(可重新上传) */ @PostMapping("/upload/url") public BaseResponse<PictureVO> uploadPictureByUrl( @RequestBody PictureUploadRequest pictureUploadRequest, HttpServletRequest request) { User loginUser = userService.getLoginUser(request); String fileUrl = pictureUploadRequest.getFileUrl(); PictureVO pictureVO = pictureService.uploadPicture(fileUrl, pictureUploadRequest, loginUser); return ResultUtils.success(pictureVO); }

空间路由拦截

1.登录后用userid获取空间存pinia,有pinia里面就有数据,没有就空

2.在进入ai智绘页面进行1次路由拦截:

2.1:判断空间是否存在(也就是pinia里面是否有数据),没有重定向到创建空间页面,

2.2:判断空间是否是用户本人,pinia的space表 里面的userid是不是跟 pinia的user表里面的userid一样,如果一样就给进,如果不一样就抛一个无权限访问,然后重定向回我的空间

其实可以总结为1个点不用这么复杂,你只需要获取你进去页面spaceid的空间详情就可以了。但这样做有个弊端,用户要进入这个页面一定要从我的空间里面进入,没办法从页面的其他地方比如菜单栏,活动页什么的进入。如果要解决这个问题,就需要去调以userid获取space的接口,或者是一开始就将space存pinia里面,但这样不论是哪种我的接口逻辑都要改,那再说了空间一多怎么办呢

综上所述,现在比较好的方法是在进去ai智绘页面就给判断一次路由拦截,获取根据空间id获取这个空间详情,然后看这个空间详情的userid是不是跟pinia的user表里的userid一样,一样就进入,不一样就给一个"无权限,从”我的空间“内进入",然后重定向回我的空间,如果没有空间也不用管,因为我们在进入我的空间页面会自动进行一次判断如果没空间也会重定向到创建空间

组件内使用路由拦截

组件内使用路由拦截建议在路由配置里面使用,路由配置里有专门的配置项用来配置路由拦截

路由配置

js
复制代码
// AI生图页面(1级) { path: '/ai/generation', name: 'ai_generation', component: () => import('@/views/Pages/ai/AiPage.vue'), beforeEnter: async (to) => { const result = await checkSpaceAuth(to.query.spaceId) if (typeof result === 'string') { return { path: result } } return result }, },

路由拦截的方法

js
复制代码
export const checkSpaceAuth = async (spaceId) => { // 获取当前登录用户 const loginStore = useUserStore() const loginUser = loginStore.userInfo // 如果传入的spaceid为空,就给一个错误消息提示,并重定向到"我的空间" if (!spaceId) { message.error('访问链接不完整,请从「我的空间」重新进入') return '/my_space' // 返回重定向路径 } // 接口获取spaceid的空间信息,如果获取不到,就给一个无权限的错误消息提示,并重定向到"我的空间" try { const res = await getSpaceVOById({ id: spaceId }) // 空间不存在的情况,不给进,重定向到"我的空间",但给错误的消息提示 if (res.code !== 0) { message.error('请求失败,' + res.description) return '/my_space' // 允许继续导航 } if (res.data.user.id !== loginUser.id) { message.error('无权限,请从 "我的空间" 进入') return '/my_space' } return true // 放行 } catch (error) { message.error('请求失败,接口异常') return true } }

vue 3 遇到 process is not defined

使用 vite 安装 vue3 时,如果使用了 process.env,会遇到 process 未定义的情况,原因是 process.env 已经被移除了。解决办法是在 vite.config.ts 中增加 define

js
复制代码
import { defineConfig } from 'vite' // ... export default defineConfig({ // ... define: { 'process.env': {} } })
0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
阿轩
下载 APP