AOP封装进行批量的数据查询并填充

在我们日常的项目开发中,我们经常会遇到这样的问题。我们有一张用户表,用户表中有用户ID和用户名称。我们其他表中会记录我们当前操作人的ID,一般,我们会记录一个创建人ID和修改人ID。那么,这个时候问题来了,我们在查询数据的时候,怎样优雅的根据用户ID来查询出相应的用户名称,并且填充到我们返回给前端的数据中。

我们每次在查询数据的时候,都与用户表进行关联查询吗?每次根据创建人的ID和修改人的ID来查询出用户姓名,然后填充到我们返回的数据中?这样当然是可以的,但是,这种重复性的代码,我们为什么不能写一个AOP封装,通过一个注解的形式来进行实现呢?那么,废话不多说,直接上代码;

1、填充创建人和修改人用户

我再来了解一下我们的需求。现在有一张用户表,表中有用户ID和用户名称;然后,我们现在每张表中都有创建人ID和修改人ID,我们想要查询数据的时候,根据创建人ID和修改人ID查询出相应的创建人姓名和修改人姓名,并且把这两个名称回填到我们方法返回值中。我们使用AOP的环绕通知来实现这个功能。

首先,我们要定义一个注解,这个注解要放在我们返回的实体对象中。指定要映射的字段名称,注意这里的字段名称一定要有规律才行。如(createUserId----->createUserName 、updateUserId----->updateUserName )

package com.scmpt.framework.aop.query.user;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @Author 张乔* @Date 2025/03/20 12:59* @Description 自定义的 AOP 注解,用于根据用户ID自动查询用户Grpc填充用户名称* 使用时加在返回的视图类上* 若字段命名不符合默认规则(如 creator_uid → creator_name),可自定义注解参数:* @AutoFillUserInfo(* userIdSuffix = "Uid",  // 匹配字段如 creatorUid*  userNameSuffix = "Name" // 对应字段如 creatorName*  )* public class CustomDTO {* private Long creatorUid;* private String creatorName;*}* @Version 1.0*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFillUserInfo {/*** 指定 UserId 字段的命名模式(默认后缀为 "UserId")*/String userIdSuffix() default "UserId";/*** 指定 UserName 字段的命名模式(默认后缀为 "UserName")*/String userNameSuffix() default "UserName";
}

在自定义一个注解,用来标识,我们那些方法需要字段填充;

package com.scmpt.framework.aop.query.user;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @Author 张乔* @Date 2025/03/20 12:59* @Description 自定义的 AOP 注解,用于根据用户ID自动查询用户Grpc填充用户名称* @Version 1.0*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFillUserNameByUserId {
}

最后,实现一个切面通知,并且,绑定我们的方法注解。

package com.scmpt.framework.aop.query.user;import com.scmpt.framework.core.web.response.ResultData;
import com.scmpt.user.grpc.userInfo.UserInfoDataProto;
import com.scmpt.user.grpc.userInfo.UserInfoListDataProto;
import com.scmpt.user.grpc.userInfo.UserInfoQueryProto;
import com.scmpt.user.grpc.userInfo.UserInfoServiceGrpc;
import lombok.extern.slf4j.Slf4j;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;/*** @Author 张乔* @Date 2025/03/20 12:59* @Description 自定义的 AOP 注解,用于根据用户ID自动查询用户Grpc填充用户名称* @Version 1.0*/@Aspect
@Slf4j
public class UserInfoAutoFillAspect {@GrpcClient("scmpt-users")private UserInfoServiceGrpc.UserInfoServiceBlockingStub userInfoServiceBlockingStub;@Around("@annotation(AutoFillUserNameByUserId)")public Object autoFillUserInfo(ProceedingJoinPoint joinPoint) throws Throwable {Object result = joinPoint.proceed();
//        log.info("进入切面");
//        long startTime = System.currentTimeMillis(); // 记录方法开始时间
//        processResult(result);
//        log.info("切面执行完毕");
//        long endTime = System.currentTimeMillis(); // 记录方法结束时间
//        long executionTime = endTime - startTime; // 计算执行时间(毫秒)
//        log.info("方法执行时间为---->{} ms", executionTime);log.info("进入用户信息切面");long startTime = System.currentTimeMillis();try {processResult(result);} catch (Exception e) {log.error("用户信息切面执行异常", e);}finally {log.info("用户信息切面执行完毕,耗时:{} ms", System.currentTimeMillis() - startTime);}return result;}private void processResult(Object result) {if (result == null) return;// 1. 收集所有需要处理的对象List<Object> targetObjects = new ArrayList<>();collectTargetObjects(result, targetObjects);// 2. 批量收集创建人ID和修改人IDSet<Long> creatorIds = new HashSet<>();Set<Long> updaterIds = new HashSet<>();Map<Object, Map<FieldMapping, Field>> creatorMappings = new HashMap<>();Map<Object, Map<FieldMapping, Field>> updaterMappings = new HashMap<>();for (Object obj : targetObjects) {Class<?> clazz = obj.getClass();AutoFillUserInfo annotation = clazz.getAnnotation(AutoFillUserInfo.class);if (annotation == null) continue;String userIdSuffix = annotation.userIdSuffix();String userNameSuffix = annotation.userNameSuffix();// 遍历所有字段,识别创建人/修改人字段Arrays.stream(clazz.getDeclaredFields()).filter(field -> field.getName().endsWith(userIdSuffix)).forEach(field -> processField(obj, field, userIdSuffix, userNameSuffix,creatorIds, updaterIds, creatorMappings, updaterMappings));}// 3. 批量查询用户信息(两次gRPC调用)Map<Long, String> creatorNameMap = !creatorIds.isEmpty() ?queryUserNames(creatorIds) : Collections.emptyMap();Map<Long, String> updaterNameMap = !updaterIds.isEmpty() ?queryUserNames(updaterIds) : Collections.emptyMap();// 4. 批量填充用户名称fillUserNames(creatorMappings, creatorNameMap);fillUserNames(updaterMappings, updaterNameMap);}/*** 递归收集所有需要处理的对象*/private void collectTargetObjects(Object result, List<Object> targetObjects) {if (result instanceof ResultData) {try {Field rowsField = result.getClass().getDeclaredField("rows");rowsField.setAccessible(true);Object rows = rowsField.get(result);collectTargetObjects(rows, targetObjects);} catch (Exception e) {log.error("ResultData rows字段访问失败", e);}} else if (result instanceof List) {for (Object item : (List<?>) result) {collectTargetObjects(item, targetObjects);}} else if (result != null) {targetObjects.add(result);}}/*** 处理单个字段,识别创建人/修改人字段*/private void processField(Object obj, Field userIdField,String userIdSuffix, String userNameSuffix,Set<Long> creatorIds, Set<Long> updaterIds,Map<Object, Map<FieldMapping, Field>> creatorMappings,Map<Object, Map<FieldMapping, Field>> updaterMappings) {try {userIdField.setAccessible(true);Long userId = (Long) userIdField.get(obj);if (userId == null) return;// 根据字段前缀判断类型(create/update)String fieldName = userIdField.getName();String prefix = fieldName.replace(userIdSuffix, "");boolean isCreatorField = prefix.toLowerCase().contains("create");boolean isUpdaterField = prefix.toLowerCase().contains("update");// 计算对应的用户名字段名String userNameFieldName = prefix + userNameSuffix;Field userNameField = obj.getClass().getDeclaredField(userNameFieldName);userNameField.setAccessible(true);// 记录映射关系FieldMapping mapping = new FieldMapping(userIdField, userNameField);if (isCreatorField) {creatorIds.add(userId);creatorMappings.computeIfAbsent(obj, k -> new HashMap<>()).put(mapping, userNameField);} else if (isUpdaterField) {updaterIds.add(userId);updaterMappings.computeIfAbsent(obj, k -> new HashMap<>()).put(mapping, userNameField);}} catch (Exception e) {log.error("字段处理失败: {}", userIdField.getName(), e);}}/*** 批量查询用户名称*/private Map<Long, String> queryUserNames(Set<Long> userIds) {UserInfoQueryProto query = UserInfoQueryProto.newBuilder().addAllIds(userIds).build();UserInfoListDataProto response = userInfoServiceBlockingStub.getUserInfoByIds(query);return response.getUserListList().stream().collect(Collectors.toMap(UserInfoDataProto::getId,UserInfoDataProto::getUserName,(oldVal, newVal) -> newVal));}/*** 批量填充用户名称*/private void fillUserNames(Map<Object, Map<FieldMapping, Field>> mappings,Map<Long, String> nameMap) {mappings.forEach((obj, fieldMap) -> fieldMap.forEach((mapping, userNameField) -> {try {Long userId = (Long) mapping.userIdField.get(obj);String userName = nameMap.getOrDefault(userId, "");userNameField.set(obj, userName);} catch (IllegalAccessException e) {log.error("用户名称填充失败: {}", userNameField.getName(), e);}}));}/*** 字段映射关系内部类*/private static class FieldMapping {Field userIdField;Field userNameField;FieldMapping(Field userIdField, Field userNameField) {this.userIdField = userIdField;this.userNameField = userNameField;}}
}

我这里需要说明的一点是,我是在微服务模块中。所以服务之间的通信时使用grpc远程通信的,我这里就是暴露了一个用户服务的客户端,并且通过一组用户Id来返回一组用户信息。使用Set类型的集合,能够减少我们相同ID的查询率。如果是单体项目的话,你要调用用户模块的接口,传入一组用户ID,返回一组用户信息就行了。

还要特别说明的一点是,我这里默认设置了填充对象只能有两种形式(也就是我在项目中封装好的返回两种类型的数据。如果是树形数据,直接返回就是List<T>;如果是非树形结构,返回一个ResultData对象,这个对象中有一个rows属性,这个属性是List<T>类型)所以,我封装的AOP切面通知,没有考虑到单个对象的填充,也就是不适用于单个T。

ResultData对象如下:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Schema(name = "非树形数据返回载体")
public class ResultData<T> {/*** 页码*/@Schema(name = "pageNum",description = "页码",  type = "Integer",example = "1")private Integer pageNum;/*** 每页条数*/@Schema(name = "pageSize",description = "每页条数",  type = "Integer",example = "20")private Integer pageSize;/*** 总页数*/@Schema(name = "totalPage",description = "总页数",  type = "Integer",example = "5")private Integer totalPage;/*** 数据总数*/@Schema(name = "recordCount",description = "数据总数",  type = "Long",example = "50L")private Long recordCount;/*** 返回数据*/@Schema(name = "rows",description = "返回数据",  type = "T")private List<T> rows;
}

我们现在要使用时,就是两个注解就搞定了。首先在你返回前端的实体对象T上加上@AutoFillUserInfo 注解。需要注意的是,你这个实体对象中填充的字段命名一定要有规律。如下所示;

@Data
@AutoFillUserInfo 
public class TestEntity {private Long createUserId;private Long updateUserId;private String createUserName;private String createUserName;}

接下来,在要填充的方法上加上@AutoFillUserNameByUserId注解即可。

2、填充单个ID和字段

之前介绍的是填充两个值,接下来写法封装是填充一个值的。

我现在要根据我们文件ID、查询出文件的路径。并且这步操作要放在AOP中进行自动填充,那么方法和上个方法是一样的,我们需要创建两个注解和一个切面通知,相应的代码如下;

 实体对象上的注解;

package com.scmpt.framework.aop.query.file.image;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @Author 张乔* @Date 2025/03/20 12:59*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFillFileInfo {/*** 文件ID字段后缀(默认后缀为 "StorageFileId")*/String fileIdSuffix() default "StorageFileId";/*** 文件URL字段后缀(默认后缀为 "ImageUrl")*/String imageUrlSuffix() default "ImageUrl";
}

方法上的注解;

package com.scmpt.framework.aop.query.file.image;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @Author 张乔* @Date 2025/03/20 12:59* @Description 自定义的 AOP 注解,用于根据用户ID自动查询用户Grpc填充用户名称* @Version 1.0*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFillImageUrlByFileId {
}

通知的切面类;

package com.scmpt.framework.aop.query.file.image;import com.scmpt.files.grpc.storages.StoragesDataProto;
import com.scmpt.files.grpc.storages.StoragesListDataProto;
import com.scmpt.files.grpc.storages.StoragesQueryProto;
import com.scmpt.files.grpc.storages.StoragesServiceGrpc;
import com.scmpt.framework.core.web.response.ResultData;
import lombok.extern.slf4j.Slf4j;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;/*** @Author 张乔* @Date 2025/03/20 12:59* @Description 自定义的 AOP 注解,用于根据用户ID自动查询用户Grpc填充用户名称* @Version 1.0*/@Aspect
@Slf4j
public class FilesImageAutoFillAspect {@GrpcClient("scmpt-files")private StoragesServiceGrpc.StoragesServiceBlockingStub stub;@Around("@annotation(AutoFillImageUrlByFileId)")public Object autoFillFileInfo(ProceedingJoinPoint joinPoint) throws Throwable {Object result = joinPoint.proceed();log.info("进入文件信息切面");long startTime = System.currentTimeMillis();try {processResult(result);} catch (Exception e) {log.error("文件信息切面执行异常", e);}finally {log.info("文件信息切面执行完毕,耗时:{} ms", System.currentTimeMillis() - startTime);}return result;}private void processResult(Object result) {if (result == null) return;// 1. 收集所有需要处理的对象List<Object> targetObjects = new ArrayList<>();collectTargetObjects(result, targetObjects);// 2. 批量收集文件IDSet<Long> fileIds = new HashSet<>();Map<Object, Map<FieldMapping, Field>> fileMappings = new HashMap<>();for (Object obj : targetObjects) {Class<?> clazz = obj.getClass();AutoFillFileInfo annotation = clazz.getAnnotation(AutoFillFileInfo.class);if (annotation == null) continue;String fileIdSuffix = annotation.fileIdSuffix();String imageUrlSuffix = annotation.imageUrlSuffix();// 遍历所有字段,识别文件ID字段Arrays.stream(clazz.getDeclaredFields()).filter(field -> field.getName().endsWith(fileIdSuffix)).forEach(field -> processFileField(obj, field, fileIdSuffix, imageUrlSuffix, fileIds, fileMappings));}// 3. 批量查询文件信息Map<Long, String> fileUrlMap = !fileIds.isEmpty() ?queryFileUrls(fileIds) : Collections.emptyMap();// 4. 批量填充文件URLfillFileUrls(fileMappings, fileUrlMap);}private void collectTargetObjects(Object result, List<Object> targetObjects) {// 复用原有收集逻辑(支持ResultData、List等嵌套结构)if (result instanceof ResultData) {try {Field rowsField = result.getClass().getDeclaredField("rows");rowsField.setAccessible(true);Object rows = rowsField.get(result);collectTargetObjects(rows, targetObjects);} catch (Exception e) {log.error("ResultData rows字段访问失败", e);}} else if (result instanceof List) {for (Object item : (List<?>) result) {collectTargetObjects(item, targetObjects);}} else if (result != null) {targetObjects.add(result);}}private void processFileField(Object obj, Field fileIdField,String fileIdSuffix, String imageUrlSuffix,Set<Long> fileIds,Map<Object, Map<FieldMapping, Field>> fileMappings) {try {fileIdField.setAccessible(true);Long fileId = (Long) fileIdField.get(obj);if (fileId == null) return;// 构建对应的URL字段名称String fieldName = fileIdField.getName();String prefix = fieldName.replace(fileIdSuffix, "");String imageUrlFieldName = prefix + imageUrlSuffix;// 获取对应的URL字段Field imageUrlField = obj.getClass().getDeclaredField(imageUrlFieldName);imageUrlField.setAccessible(true);// 记录映射关系FieldMapping mapping = new FieldMapping(fileIdField, imageUrlField);fileIds.add(fileId);fileMappings.computeIfAbsent(obj, k -> new HashMap<>()).put(mapping, imageUrlField);} catch (Exception e) {log.error("文件字段处理失败: {}", fileIdField.getName(), e);}}private Map<Long, String> queryFileUrls(Set<Long> fileIds) {StoragesQueryProto request = StoragesQueryProto .newBuilder().addAllIds(fileIds).build();StoragesListDataProto response = stub.getStoragesInfoByIds(request);return response.getStorageListList().stream().collect(Collectors.toMap(StoragesDataProto::getId,StoragesDataProto::getPath));}private void fillFileUrls(Map<Object, Map<FieldMapping, Field>> mappings,Map<Long, String> fileUrlMap) {mappings.forEach((obj, fieldMap) -> fieldMap.forEach((mapping, imageUrlField) -> {try {Long fileId = (Long) mapping.fileIdField.get(obj);String fileUrl = fileUrlMap.getOrDefault(fileId, "");imageUrlField.set(obj, fileUrl);} catch (IllegalAccessException e) {log.error("文件URL填充失败: {}", imageUrlField.getName(), e);}}));}private static class FieldMapping {Field fileIdField;Field imageUrlField;FieldMapping(Field fileIdField, Field imageUrlField) {this.fileIdField = fileIdField;this.imageUrlField = imageUrlField;}}
}

接下来,我们就可以在使用这个注解了。注意,如果你的文件ID和要填充的字段属性名称是自定义的时,只需要在类注解@AutoFillFileInfo 上指定相应的ID名称和要填充的属性值名称即可。

如下使用方式;

@Data
@AutoFillFileInfo(imageUrlSuffix="filePathUrl"  ,fileIdSuffix ="fileId")
public class TestEntity {private Long fileId;private String filePathUrl;}

然后,在要填充的方法上加上方法注解就可以了

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

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

相关文章

Java学习手册:数据库事务相关知识

一、事务的概念与特性 概念 &#xff1a;事务是数据库中一系列操作的集合&#xff0c;这些操作要么全部成功&#xff0c;要么全部失败&#xff0c;是一个不可分割的工作单位。例如&#xff0c;在银行转账系统中&#xff0c;从一个账户扣款和向另一个账户存款这两个操作必须作为…

java复杂度,包装类,泛型解析

如何衡量代码的好坏&#xff1f; 评价代码的好坏我们使用算法效率来判断&#xff0c;而算法效率分两种&#xff1a; 算法效率&#xff1a; 第一种是时间效率&#xff0c;第二种是空间效率&#xff0c;时间效率被称为时间复杂度&#xff0c;⽽空间效率被称作空间复杂度。 时间…

基于 SpringBoot + Vue 的校园管理系统设计与实现

一、项目简介 本系统以校园组织管理为主线&#xff0c;结合用户权限分离机制与模块化设计&#xff0c;实现对“单位类别、单位、通知推送、投票信息、用户回复”等内容的全流程管理&#xff0c;广泛适用于教育局、高校及下属组织的信息管理工作。 &#x1f3af; 项目亮点&…

iOS蓝牙技术实现及优化

以下是针对2025年iOS蓝牙技术实现的核心技术要点的深度解析&#xff0c;结合当前iOS 18&#xff08;推测版本&#xff09;的最新特性与开发实践&#xff0c;分模块结构化呈现&#xff1a; 一、硬件与协议层适配 BLE 5.3 支持 iOS 18默认支持蓝牙5.3协议&#xff0c;需注意&…

Qt 中实现观察者模式(Observer Pattern)

在 Qt 中实现**观察者模式(Observer Pattern)通常利用其内置的信号与槽(Signals & Slots)**机制,这是最符合 Qt 设计哲学的方式。以下是详细实现方法和关键点: —### 1. 观察者模式的核心思想- Subject(被观察者):维护一个观察者列表,在状态变化时通知观察者。- …

写程序,统计两会政府工作报告热词频率,并生成词云

import jieba from collections import Counter from wordcloud import WordCloud import matplotlib.pyplot as pltdef generate_wordcloud():try:# 读取文本文件with open(E:\\桌面\\s.txt, r, encodingutf-8) as file:text file.read()# 中文分词words jieba.lcut(text)# …

【Science Advances】普林斯顿大学利用非相干光打造可重构纳米光子神经网络

(导读 ) 人工智能对计算性能需求剧增&#xff0c;电子微处理器发展受功耗限制。光学计算有望解决这些问题&#xff0c;光学神经网络&#xff08;ONNs&#xff09;成为研究热点&#xff0c;但现有 ONNs 因设计缺陷&#xff0c;在图像分类任务中精度远低于现代电子神经网络&#…

gin + es 实践 01

项目结构说明 目录结构概览 Go-ES 项目采用领域驱动设计&#xff08;DDD&#xff09;架构&#xff0c;目录结构清晰&#xff0c;各层次职责分明。以下是项目的主要目录结构&#xff1a; go-es/ ├── cmd/ # 应用程序入口 │ └── api/ …

如何构建直播美颜SDK?从美颜API调用逻辑到GPU优化实战

随着短视频和直播行业的爆发&#xff0c;美颜SDK已成为各大直播平台的“标配”。从基础的磨皮、美白&#xff0c;到如今的AI滤镜、虚拟形象&#xff0c;这些功能的背后都离不开高效的美颜SDK支持。那么&#xff0c;如何构建一款性能优越、体验流畅的直播美颜SDK呢&#xff1f;本…

高组装导轨的特点

高组装导轨通常是四列式单圆弧齿形接触直线导轨&#xff0c;具有整合化的结构设计&#xff0c;适用于重负荷和精密应用。与其它直线导轨高组装导轨提升了负荷与刚性能力&#xff0c;具备四方向等负载特色和自动调心功能&#xff0c;能够吸收安装面的装配误差&#xff0c;达到高…

2025-05-07-FFmpeg视频裁剪(尺寸调整,画面比例不变)

原比例如图 原比例如图裁剪后的比例 代码&#xff1a; 方法一&#xff1a;极速 ffmpeg -i input.mp4 -vf "crop1080:750:0:345" -c:v libx264 -preset ultrafast -c:a copy output.mp4关键参数说明&#xff1a; vf “crop宽:高❌y”&#xff1a;定义裁剪区域。 …

一个.Net开源的协作办公套件,包括文档、表格、演示文稿和表单

从零学习构建一个完整的系统 推荐一个开源的文档协作办公套件&#xff0c;可以很好的满足团队对方便、高效、安全的方式来处理文档工作&#xff0c;促进团队协作和信息共享。 项目简介 ONLYOFFICE 是一个开源的办公套件&#xff0c;包括文档、表格、演示文稿和表单等应用程序…

虚幻基础:硬件输入

文章目录 triggered&#xff1a;按下一直触发 等于tickcompleted&#xff1a;必须等到triggered结束后 才触发松下triggered结束 默认按键触发顺序按下&#xff1a;触发两个先 Started后 Triggered 松开Completed 触发器&#xff1a;用于修改triggered 触发和结束驱动阈值&…

Python中的global与nonlocal关键字详解

一、前言 在Python编程中&#xff0c;变量作用域是一个非常重要的概念。对于初学者来说&#xff0c;经常会遇到在函数内部无法修改外部变量的问题。这时候&#xff0c;global和nonlocal关键字就能派上用场了。本文将详细介绍这两个关键字的用法、区别以及适用场景&#xff0c;…

vue-qr生成的二维码增加下载功能

大家好&#xff01;今天给大家分享一个超实用的前端小技巧——如何在 Vue 项目中生成二维码并实现下载功能。这个功能在分享链接、活动推广等场景特别有用&#xff0c;一起来学习吧&#xff01; &#x1f50d; 功能预览 使用 vue-qr 生成美观二维码点击按钮即可下载 PNG 格式的…

嵌入式C进阶路线指南

嵌入式是工科&#xff0c;工科讲究实践。说的再多、懂得再多&#xff0c;不能做出实际的东西&#xff0c;是没有意义的。学习嵌入式的核心原则之一就是多动手写代码。另外还有一个原则就是&#xff1a;从浅到深学习。接下来的内容将贯彻这两个原则。最后强调一点&#xff0c;各…

服务器上机用到的设备

服务器上机通常需要以下硬件设备&#xff1a; 服务器主机&#xff1a; CPU&#xff1a;选择高性能的多核处理器&#xff0c;如英特尔至强&#xff08;Xeon&#xff09;系列或AMD EPYC系列&#xff0c;以满足高并发和多任务处理需求。 内存&#xff08;RAM&#xff09;&#xf…

FreeCAD傻瓜教程-涡轮蜗杆的快速绘制FCGear工作台的使用方法

起因&#xff1a;涡轮蜗轴的组合&#xff0c;是一种比较简单且高效的传动结构。可以实现减速、加速、转动角度的放大、缩小等应用。 如何绘制呢&#xff1f;我搜索了不少教程&#xff0c;看起来都挺复杂的&#xff0c;对于小白来说有点像天书。这里介绍和记录一下利用FreeCAD 的…

daplink开发_一次开发DAPLink的详细开发流程及调试步骤

以下是针对第一次开发DAPLink的详细开发流程及调试步骤,结合STM32平台特性,分阶段指导您完成从零到一的完整开发过程: 一、开发流程1. 硬件准备阶段选型STM32芯片:推荐型号:STM32F103C8T6(成本低、资源足够)、STM32F405RGT6(性能更强,支持更多外设)。关键外设需求:U…

给小白的AI Agent 基本技术点分析与讲解

引言&#xff1a;重塑交互与自动化边界的 AI Agent 在人工智能技术飞速发展的浪潮中&#xff0c;AI Agent&#xff08;智能体&#xff09;概念的兴起标志着自动化和人机交互正迈向一个全新的阶段。传统的软件系统通常被设计来执行精确预设的指令序列&#xff0c;它们强大且高效…