diff --git a/docs/.vitepress/theme/router/index.ts b/docs/.vitepress/theme/router/index.ts
index 4df5520..8d251ea 100644
--- a/docs/.vitepress/theme/router/index.ts
+++ b/docs/.vitepress/theme/router/index.ts
@@ -15,7 +15,8 @@ export const routers = [
{ text: 'JavaScript知识点整理', link: '/Web-Front/Others/JavaScript-Guide' },
{ text: '前端包管理器', link: '/Web-Front/Others/Package' },
{ text: 'Vite核心原理', link: '/Web-Front/Others/Vite-Principle' },
- { text: '浏览器渲染全流程解析', link: '/Web-Front/Others/Browser-Process' }
+ { text: '浏览器渲染全流程解析', link: '/Web-Front/Others/Browser-Process' },
+ { text: 'Element Plus 上传器', link: '/Web-Front/Others/ElUpload' }
]
},
]
@@ -61,7 +62,8 @@ export const routers = [
items: [
{ text: 'RustFS简介和使用', link: '/Web-Backend/Others/RustFS' },
{ text: 'AList简介和使用', link: '/Web-Backend/Others/AList' },
- { text: '网络编程简介', link: '/Web-Backend/Others/Netword-Program' }
+ { text: '网络编程简介', link: '/Web-Backend/Others/Netword-Program' },
+ { text: '视频压缩', link: '/Web-Backend/Others/VideoCompressor' }
]
},
]
diff --git a/docs/Others/WeCom.md b/docs/Others/WeCom.md
index 1c19331..82b7b48 100644
--- a/docs/Others/WeCom.md
+++ b/docs/Others/WeCom.md
@@ -3,8 +3,6 @@
date: 2026-01-23
---
- 接口文档地址:[接口](https://developer.work.weixin.qq.com/document/path/90664)
-
# 一、建立工作台
1. 进入企业微信后台管理系统,选择应用管理->应用管理。
2. 选择自建->创建应用。
@@ -19,12 +17,12 @@
# 二、JS-SDK开发
## 2.1 接口鉴权
[官方文档](https://developer.work.weixin.qq.com/document/path/90514)
- 参考第三节企业微信接口开发
+ 参考第三节企业微信鉴权接口开发
## 2.2 打开默认浏览器
使用系统浏览器打开指定 URL,支持传入 oauth2 链接,从而实现在系统浏览器内免登录的效果。
-# 三、接口开发
+# 三、鉴权接口开发
本教程以node.js为例。
## 3.1 通用企业微信服务端API
```js
@@ -448,4 +446,64 @@ module.exports = {
将该应用主页的URL放在工作台的应用主页中。
::: tip
需要在前端工程中配置路由参数访问,即通过ip:port/#/login?username=''&password=''的形式访问。
-:::
\ No newline at end of file
+:::
+::: tip
+如果需要在发送应用消息时,可以点击消息访问应用,绑定的URL链接也是该应用主页的链接。
+:::
+
+# 五、使用
+ 本教程以SpringBoot为例。
+## 5.1 配置RestClient
+```java
+@Configuration
+public class RestClientConfig {
+ final String weiComBaseUrl = "";
+
+ @Bean
+ public RestClient weiComClient() {
+ SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+ factory.setConnectTimeout(Duration.ofSeconds(5));
+ factory.setReadTimeout(Duration.ofSeconds(10));
+
+ return RestClient.builder()
+ .requestFactory(factory)
+ .baseUrl(weiComBaseUrl)
+ .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+ .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
+ .build();
+ }
+}
+```
+
+## 5.2 调用发送消息接口
+```java
+@Service
+@Slf4j
+public class NoticeService {
+ @Resource
+ private RestClient weiComClient;
+
+ @Async
+ public void sendRepairMessage(String touser, String content) {
+ String link = "";
+
+ WeComNoticeRequest request = new WeComNoticeRequest();
+ request.setTouser(touser);
+ request.setContent(content + "\n点击打开工作台应用:" + "打开应用");
+
+ WeComResponse response = weiComClient.post()
+ .uri("/message/repair")
+ .contentType(MediaType.APPLICATION_JSON)
+ .body(request)
+ .retrieve()
+ .body(WeComResponse.class);
+
+ if (response == null || response.getErrcode() != 0) {
+ throw new CustomException("企业微信发送消息失败 请联系管理员");
+ }
+ }
+}
+```
+
+# 附录
+企业微信官方文档:[接口](https://developer.work.weixin.qq.com/document/path/90664)
\ No newline at end of file
diff --git a/docs/Web-Backend/MySQL/MyBatis.md b/docs/Web-Backend/MySQL/MyBatis.md
index 8a7a9ea..e10987a 100644
--- a/docs/Web-Backend/MySQL/MyBatis.md
+++ b/docs/Web-Backend/MySQL/MyBatis.md
@@ -456,3 +456,66 @@ public class BlobToBase64TypeHandler extends BaseTypeHandler {
}
}
```
+
+ 例如自定义一个处理敏感字段(如密码、手机号)的自动加解密:
+```java
+public class EncryptTypeHandler extends BaseTypeHandler {
+ @Override
+ public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
+ ps.setString(i, AESUtil.encrypt(parameter)); // 自定义加密方法
+ }
+
+ @Override
+ public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
+ String encrypted = rs.getString(columnName);
+ return AESUtil.decrypt(encrypted); // 自定义解密方法
+ }
+}
+```
+
+ 然后在实体类字段上加上 @TableField(typeHandler = EncryptTypeHandler.class):
+```java
+@TableName("user")
+public class User {
+ private Long id;
+ private String name;
+
+ // 这个字段在数据库里存的是加密后的字符串
+ @TableField(typeHandler = EncryptTypeHandler.class)
+ private String phone; // 例如 "13800138000" → 存为 "U2FsdGVkX1+oO1W..."
+
+ @TableField(typeHandler = EncryptTypeHandler.class)
+ private String idCard; // 身份证号
+
+ // 普通字段,不加密
+ private String email;
+}
+```
+
+ 如果是在XML文件中查询,需要指定TypeHandler:
+```xml
+
+
+
+
+
+
+
+
+
+```
+
+ MyBatisPlus中定义了一些常用的类型处理器,例如:JacksonTypeHandler:
+```java
+@TableField(value = "fault_type", typeHandler = JacksonTypeHandler.class)
+```
+
+ 在XML文件中,将typeHandler设置为`com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler`。
+
+# 六、MyBatis/MyBatis Plus常见问题
+1. 更新值为null的字段时会失效
+
+ 需要更改字段的更新策略:
+```java
+@TableField(value = "file_name", updateStrategy = FieldStrategy.ALWAYS)
+```
\ No newline at end of file
diff --git a/docs/Web-Backend/Others/VideoCompressor.md b/docs/Web-Backend/Others/VideoCompressor.md
new file mode 100644
index 0000000..d14c67e
--- /dev/null
+++ b/docs/Web-Backend/Others/VideoCompressor.md
@@ -0,0 +1,243 @@
+---
+ title: 视频压缩
+ date: 2026-01-29
+---
+
+# 一、简介
+ 视频码率(Bit Rate),指的是视频文件在单位时间内(通常是每秒)所包含的数据量。单位通常是 Mbps(兆比特每秒)或 Kbps(千比特每秒)。
+ 码率越高:视频中包含的细节信息越多,画面越清晰、色彩越丰富、动态画面(如快速运动、爆炸等)越流畅,不容易出现马赛克或模糊。但代价是文件体积越大,传输所需的网络带宽也越高。码率越低:视频文件更小,传输更快,但在复杂画面中容易产生压缩瑕疵(如马赛克、模糊、色带等)。
+ 如果不压缩,原始视频数据量极其庞大,一部2小时的未压缩1080p电影可能占用数TB的存储空间,普通硬盘无法承受,家庭宽带或移动网络无法实时传输如此巨大的数据量,视频播放会不断卡顿。
+ 如果服务器的带宽为10M,适合 720p,勉强支持低码率 1080p。
+
+# 二、引入依赖
+```xml
+
+
+ ws.schild
+ jave-all-deps
+ 3.5.0
+
+```
+
+# 三、工具类
+```java
+import lombok.extern.slf4j.Slf4j;
+import ws.schild.jave.Encoder;
+import ws.schild.jave.MultimediaObject;
+import ws.schild.jave.encode.AudioAttributes;
+import ws.schild.jave.encode.EncodingAttributes;
+import ws.schild.jave.encode.VideoAttributes;
+import ws.schild.jave.info.VideoSize;
+
+import java.io.File;
+
+/**
+ * 视频压缩工具类
+ * 提供将视频压缩到720p的静态方法
+ */
+@Slf4j
+public class VideoCompressor {
+
+ // 支持的视频格式
+ private static final String[] SUPPORTED_FORMATS = {
+ ".mp4", ".avi", ".mov", ".mkv", ".flv",
+ ".wmv", ".webm", ".mpeg", ".mpg", ".3gp"
+ };
+
+ // 压缩参数常量
+ private static final int TARGET_WIDTH = 1280;
+ private static final int TARGET_HEIGHT = 720;
+ private static final int VIDEO_BITRATE = 1500000; // 1.5 Mbps
+ private static final int AUDIO_BITRATE = 128000; // 128 kbps
+ private static final String VIDEO_CODEC = "libx264";
+ private static final String AUDIO_CODEC = "aac";
+ private static final String OUTPUT_FORMAT = "mp4";
+ private static final String OUTPUT_SUFFIX = "_720p.mp4";
+
+ private static final int MAX_BITRATE = 2500; // 2.5 Mbps
+ private static final int MIN_BITRATE = 1000; // 1 Mbps
+
+ /**
+ * 压缩视频到720p,统一输出为MP4格式
+ *
+ * @param inputFile 输入视频文件
+ * @return 压缩后的视频文件,如果不需要压缩则返回原文件
+ * @throws Exception 压缩过程中可能出现的异常
+ */
+ public static File compressTo720p(File inputFile) throws Exception {
+ if (!isSupportedVideoFile(inputFile)) {
+ log.info("文件格式不支持或文件不存在,直接返回源文件");
+ return inputFile;
+ }
+
+ // 判断是否需要压缩
+ if (!needCompression(inputFile)) {
+ return inputFile;
+ }
+
+ File outputFile = createOutputFile(inputFile);
+ Encoder encoder = new Encoder();
+
+ // 配置编码参数
+ EncodingAttributes encodingAttributes = createEncodingAttributes();
+
+ log.info("开始压缩视频: {} -> {}", inputFile.getName(), outputFile.getName());
+ encoder.encode(new MultimediaObject(inputFile), outputFile, encodingAttributes);
+ log.info("视频压缩完成: {}", outputFile.getName());
+
+ return outputFile;
+ }
+
+ /**
+ * 智能判断视频是否需要压缩
+ * 基于分辨率、码率、数据密度等多维度综合判断
+ */
+ public static boolean needCompression(File videoFile) throws Exception {
+ MultimediaObject media = new MultimediaObject(videoFile);
+
+ // 获取视频基本信息
+ double fileSizeMB = videoFile.length() / 1048576.0; // 转换为MB
+ double duration = media.getInfo().getDuration() / 1000.0;
+ int bitRateKbps = media.getInfo().getVideo().getBitRate() / 1000; // 转换为kbps
+
+ // 获取视频分辨率
+ int width = media.getInfo().getVideo().getSize().getWidth();
+ int height = media.getInfo().getVideo().getSize().getHeight();
+
+ log.info("视频分析: 分辨率 {}x{}, 大小 {}MB, 时长 {}秒, 码率约 {}kbps", width, height, fileSizeMB, duration, bitRateKbps);
+
+ // 判断逻辑:基于分辨率和码率的智能判断
+ boolean needsCompression = false;
+
+ // 1. 如果分辨率已经<=720p,检查码率是否过高
+ if (height <= TARGET_HEIGHT && width <= TARGET_WIDTH) {
+ if (bitRateKbps > MAX_BITRATE) {
+ log.info("视频分辨率达标但码率({}kbps)过高,需要压缩", bitRateKbps);
+ needsCompression = true;
+ } else {
+ log.info("视频分辨率已达720p或以下,且码率合理,无需压缩");
+ }
+ }
+ // 2. 如果分辨率>720p,但码率很低,说明已是优化过的视频,无需压缩
+ else if (height > TARGET_HEIGHT && bitRateKbps < MIN_BITRATE) {
+ log.info("视频虽为{}p高分辨率,但码率({}kbps)已优化,无需压缩", height, bitRateKbps);
+ }
+ // 3. 高分辨率+高码率,需要压缩
+ else if (height > TARGET_HEIGHT && bitRateKbps >= MAX_BITRATE) {
+ log.info("视频为{}p高分辨率且码率较高({}kbps),需要压缩至720p", height, bitRateKbps);
+ needsCompression = true;
+ }
+ // 4. 其他情况(如异常数据)默认不压缩
+ else {
+ log.info("视频参数异常,保守处理:不进行压缩");
+ }
+
+ return needsCompression;
+ }
+
+ /**
+ * 检查文件是否为支持的视频格式
+ */
+ private static boolean isSupportedVideoFile(File file) {
+ if (file == null || !file.exists()) {
+ return false;
+ }
+
+ String filename = file.getName().toLowerCase();
+ for (String format : SUPPORTED_FORMATS) {
+ if (filename.endsWith(format)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 创建输出文件对象
+ */
+ private static File createOutputFile(File inputFile) {
+ String baseName = getFileBaseName(inputFile.getName());
+ String outputFilename = baseName + OUTPUT_SUFFIX;
+ return new File(inputFile.getParent(), outputFilename);
+ }
+
+ /**
+ * 获取文件名(不含扩展名)
+ */
+ private static String getFileBaseName(String filename) {
+ int dotIndex = filename.lastIndexOf('.');
+ return (dotIndex == -1) ? filename : filename.substring(0, dotIndex);
+ }
+
+ /**
+ * 创建编码配置参数
+ */
+ private static EncodingAttributes createEncodingAttributes() {
+ // 音频配置
+ AudioAttributes audio = new AudioAttributes();
+ audio.setCodec(AUDIO_CODEC);
+ audio.setBitRate(AUDIO_BITRATE);
+
+ // 视频配置
+ VideoAttributes video = new VideoAttributes();
+ video.setCodec(VIDEO_CODEC);
+ video.setBitRate(VIDEO_BITRATE);
+ video.setSize(new VideoSize(TARGET_WIDTH, TARGET_HEIGHT));
+
+ // 编码设置
+ EncodingAttributes attributes = new EncodingAttributes();
+ attributes.setOutputFormat(OUTPUT_FORMAT);
+ attributes.setAudioAttributes(audio);
+ attributes.setVideoAttributes(video);
+
+ return attributes;
+ }
+}
+```
+
+ 通过分辨率和码率的双重判断,确定是否需要压缩。
+
+# 四、使用
+```java
+public void uploadFile(MultipartFile file, String filePath) {
+ try {
+ MultiValueMap body = new LinkedMultiValueMap<>();
+
+ File tempFile = convertMultipartFileToFile(file);
+ log.info("临时文件地址:{}", tempFile.toPath());
+
+ log.info("开始压缩文件:{}", tempFile.getName());
+ File compressFile = VideoCompressor.compressTo720p(tempFile);
+ log.info("完成压缩文件:{}", tempFile.getName());
+
+ log.info("压缩文件地址:{}", tempFile.toPath());
+
+ body.add("file", new FileSystemResource(compressFile));
+
+ aListClient.put()
+ .uri("api/fs/form")
+ .header(HttpHeaders.AUTHORIZATION, token)
+ .header("File-Path", filePath)
+ .contentType(MediaType.MULTIPART_FORM_DATA)
+ .body(body)
+ .retrieve()
+ .body(Map.class);
+
+ Files.deleteIfExists(compressFile.toPath());
+ Files.deleteIfExists(tempFile.toPath());
+ } catch (Exception e) {
+ log.error("文件上传失败{}", e.getMessage());
+ throw new RuntimeException("文件上传失败", e);
+ }
+}
+
+private File convertMultipartFileToFile(MultipartFile file) throws IOException {
+ String suffix = (file.getOriginalFilename() != null && file.getOriginalFilename().contains("."))
+ ? file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."))
+ : ".tmp";
+
+ Path tempFile = Files.createTempFile(System.currentTimeMillis() + "", suffix);
+ file.transferTo(tempFile);
+ return tempFile.toFile();
+}
+```
\ No newline at end of file
diff --git a/docs/Web-Front/Others/ElUpload.md b/docs/Web-Front/Others/ElUpload.md
new file mode 100644
index 0000000..95890f7
--- /dev/null
+++ b/docs/Web-Front/Others/ElUpload.md
@@ -0,0 +1,313 @@
+---
+ title: Element Plus 上传器
+ date: 2026-01-29
+---
+
+# 一、上传图片
+```vue
+
+
+
+
+
+ 请上传小于10M的图片
+
+
+
+
+
+
+
+
+
+
+```
+
+# 二、上传视频
+```vue
+
+
+ 点击上传
+
+
+ 请上传小于200M的视频
+
+
+
+
+
+
+```
+
+# 三、工具类
+```ts
+import { hashSHA256 } from 'vue3-common/utils/cryptoUtil'
+
+export const getFileHashName = (name: string) => {
+ return hashSHA256(name)
+}
+
+export const getFileExtension = (name: string) => {
+ return name.substring(name.lastIndexOf('.') + 1).toLowerCase()
+}
+
+export const validateExcel = (fileType: string, fileExtension: string): boolean => {
+ // 常见的 Excel MIME 类型
+ const excelMimeTypes = [
+ 'application/vnd.ms-excel', // .xls
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
+ 'application/vnd.ms-excel.sheet.macroEnabled.12', // .xlsm
+ 'text/csv' // CSV
+ ]
+
+ // 常见的 Excel 文件扩展名
+ const excelExtensions = ['xls', 'xlsx', 'xlsm', 'csv']
+
+ return excelMimeTypes.includes(fileType) || excelExtensions.includes(fileExtension.toLowerCase())
+}
+
+export const validateImage = (fileType: string, fileExtension: string): boolean => {
+ // 常见的图片 MIME 类型
+ const imageMimeTypes = [
+ 'image/jpeg', // JPEG
+ 'image/jpg', // JPG
+ 'image/png', // PNG
+ 'image/gif', // GIF
+ 'image/bmp', // BMP
+ 'image/webp', // WebP
+ 'image/svg+xml' // SVG
+ ]
+
+ // 常见的图片文件扩展名
+ const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']
+
+ return imageMimeTypes.includes(fileType) || imageExtensions.includes(fileExtension.toLowerCase())
+}
+
+export const validateVideo = (fileType: string, fileExtension: string): boolean => {
+ // 常见的视频 MIME 类型
+ const videoMimeTypes = [
+ 'video/mp4', // MP4
+ 'video/mpeg', // MPEG
+ 'video/quicktime', // MOV
+ 'video/x-msvideo', // AVI
+ 'video/x-matroska', // MKV
+ 'video/webm', // WebM
+ 'video/3gpp', // 3GP
+ 'video/3gpp2', // 3G2
+ 'video/x-flv', // FLV
+ 'video/mp2t', // TS (MPEG Transport Stream)
+ 'application/x-mpegURL', // M3U8 (HLS)
+ 'video/H264', // H264
+ 'video/H265' // H265/HEVC
+ ]
+
+ // 常见的视频文件扩展名
+ const videoExtensions = [
+ 'mp4',
+ 'mpeg',
+ 'mpg',
+ 'mov',
+ 'avi',
+ 'mkv',
+ 'webm',
+ '3gp',
+ '3g2',
+ 'flv',
+ 'ts',
+ 'm3u8',
+ 'h264',
+ 'h265',
+ 'hevc'
+ ]
+
+ return videoMimeTypes.includes(fileType) || videoExtensions.includes(fileExtension.toLowerCase())
+}
+```