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 + + + +``` + +# 二、上传视频 +```vue + + + +``` + +# 三、工具类 +```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()) +} +```