diff --git a/docs/.vitepress/theme/router/index.ts b/docs/.vitepress/theme/router/index.ts index d2fbc6e..437f6b4 100644 --- a/docs/.vitepress/theme/router/index.ts +++ b/docs/.vitepress/theme/router/index.ts @@ -79,8 +79,8 @@ export const routers = [ { text: 'RustFS简介和使用', link: '/Web/Others/RustFS' }, { text: 'AList简介和使用', link: '/Web/Others/AList' }, { text: 'InfluxDB简介和使用', link: '/Web/Others/InfluxDB' }, - { text: '网络编程简介', link: '/Web/Others/Netword-Program' }, - { text: '视频压缩', link: '/Web/Others/VideoCompressor' } + { text: '视频压缩', link: '/Web/Others/VideoCompressor' }, + { text: '图片处理服务', link: '/Web/Others/ImageProcessor' } ] }, ] @@ -107,6 +107,7 @@ export const routers = [ { text: '👤 个人项目', items: [ + { text: '家庭管理系统', link: '/Practice/Personal/SweetHut' } ] }, ] diff --git a/docs/Practice/Company/Learn.md b/docs/Practice/Company/Learn.md index a4fb971..c3a86f8 100644 --- a/docs/Practice/Company/Learn.md +++ b/docs/Practice/Company/Learn.md @@ -28,48 +28,33 @@ | **前端** | Vue 3、Element Plus、Vue Router、Pinia、DPlayer (视频播放)、@vue-office (文档预览)、ECharts (数据可视化)| | **后端** | Spring Boot 3、Spring MVC、Sa-Token (鉴权)、MyBatis-Plus | | **中间件** | Redis、企业微信 SDK | +| **视频处理** | Java JAVE2 (FFmpeg 封装,视频压缩转码) | | **存储** | MySQL 8.0 (业务数据)、AList (视频/文件资源存储) | -| **视频处理** | Java JAVE 2 (FFmpeg 封装,视频压缩转码) | -| **运维** | Docker、Fluent Bit (日志采集)、OpenObserver (日志可视化) | +| **运维** | Docker、Nginx、Fluent Bit (日志采集)、OpenObserver (日志可视化) | # 五、核心设计 -- 基于 Spring Boot 3 + MyBatis‑Plus 构建稳定的后端架构,通过 Sa‑Token 实现接口级鉴权,保障系统安全与权限可控。 -- 集成企业微信 SDK,实现企业微信工作台免登录认证;通过企业微信消息通知机制,自动推送学习任务与提醒。 -- 设计课程、学习计划与考试等核心业务模型,支持批量下发培训任务;实现随机组卷、自动阅卷与成绩统计,并自动提醒员工完成学习任务。 -- 基于 JAVE2(FFmpeg)实现视频自动压缩与转码,适配多终端播放;采用 AList 作为统一文件存储平台,集中管理音视频及课件资源。 -- 基于 Vue 3 + Element Plus + Pinia 构建管理员端与员工端页面,集成 vue‑office 实现多格式文档在线预览,集合ECharts实现多维度数据分析。 -- 使用 Docker 容器化部署,结合 Fluent Bit + OpenObserver 构建统一日志监控体系,提升系统可观测性与运维效率。 +- 基于 **Spring Boot 3 + MyBatis‑Plus** 构建稳定的后端架构,通过 **Sa‑Token** 实现接口级鉴权,保障系统安全与权限可控。 +- 集成 **企业微信 SDK**,实现企业微信工作台免登录认证;通过**企业微信消息通知机制**,自动推送学习任务与提醒。 +- 设计 **课程资源、学习计划与在线考试** 等核心业务模型,支持批量下发培训任务;实现随机组卷、自动阅卷与成绩统计,并自动提醒员工完成学习任务。 +- 基于 **JAVE2(FFmpeg)** 实现视频自动压缩与转码,适配多终端播放;采用 AList 作为统一文件存储平台,集中管理音视频及课件资源。 +- 基于 **Vue 3 + Element Plus + Pinia** 构建管理员端与员工端页面,集成 **vue‑office** 实现多格式文档在线预览,集合**ECharts**实现多维度数据分析。 +- 使用 **Docker + Nginx** 部署,结合 **Fluent Bit + OpenObserver** 构建统一日志监控体系,提升系统可观测性与运维效率。 # 六、难点/亮点 ## 5.1 公网视频播放卡顿 -* 问题 -  服务器公网带宽小,高清视频并发播放严重卡顿。 - -* 方案 -  上传视频后,使用JAVE2调用FFmpeg将视频统一压缩为720p的清晰度视频。 - -* 效果 -  视频体积减少60%+,公网环境播放流畅。 +- **问题**:服务器公网带宽小,高清视频并发播放严重卡顿。 +- **方案**:上传视频后,使用JAVE2调用FFmpeg将视频统一压缩为720p的清晰度视频。 +- **效果**:视频体积减少60%+,公网环境播放流畅。 ## 5.2 学习进度防作弊 -* 问题 -  员工学习视频课程时,可通过拖动进度条快速完成课程,无法真实反映学习行为,存在“刷课”风险。 - -* 方案 -  根据管理员设定的课程学习时长,在员工学习时,启动定时器;每分钟会保存学习时长至数据库中,下次学习时,直接跳转至上次学习进度;课程是否完成已定时器结束为准。 - -* 效果 -  有效防止通过拖拽进度条刷课,保证学习时长真实可信;同时支持断点续学,避免重复学习,满足企业培训考核与合规审计要求。 +- **问题**:员工学习视频课程时,可通过拖动进度条快速完成课程,无法真实反映学习行为,存在“刷课”风险。 +- **方案**:根据管理员设定的课程学习时长,在员工学习时,启动定时器;每分钟会保存学习时长至数据库中,下次学习时,直接跳转至上次学习进度;课程是否完成已定时器结束为准。 +- **效果**:有效防止通过拖拽进度条刷课,保证学习时长真实可信;同时支持断点续学,避免重复学习,满足企业培训考核与合规审计要求。 ## 5.3 学习计划通知 -* 问题 -  培训计划、考试通知无法及时触达员工,导致培训参与率低、管理成本高。 - -* 方案 -  在发布培训计划时,调用企业微信消息推送接口,自动向指定员工发送应用消息通知;结合定时任务,对未完成培训的人员进行周期性提醒。 - -* 效果 -  培训通知实时触达,未读提醒显著提高培训完成率;管理员无需手动维护人员信息,降低了系统运维成本。 +- **问题**:培训计划、考试通知无法及时触达员工,导致培训参与率低、管理成本高。 +- **方案**:在发布培训计划时,调用企业微信消息推送接口,自动向指定员工发送应用消息通知;结合定时任务,对未完成培训的人员进行周期性提醒。 +- **效果**:培训通知实时触达,未读提醒显著提高培训完成率;管理员无需手动维护人员信息,降低了系统运维成本。 # 七、成果 1. 平台全面替代线下培训,年度培训计划完成率由 60% 提升至 95% 以上,满足了企业合规培训要求。 diff --git a/docs/Practice/Personal/SweetHut.md b/docs/Practice/Personal/SweetHut.md new file mode 100644 index 0000000..04f5813 --- /dev/null +++ b/docs/Practice/Personal/SweetHut.md @@ -0,0 +1,66 @@ +--- +title: 家庭管理系统 +date: 2026-06-04 +--- + +# 一、项目背景 +  针对家庭信息分散、难以统一管理的问题,设计并实现了一套移动端家庭管理平台,将账单、旅行、健康、纪念日等数据进行集中管理,并通过私有化部署与数据加密保障安全,实现家庭生活的数字化沉淀。 + +# 二、系统架构 +![架构图](../images/family_manage_arch.png) + +# 三、核心模块 +- **账单记录** + ![账单记录](../images/sweethut_bill.jpg) + +- **美食记录** + +- **旅行记录** + +- **物品管理** + +- **运动健康** + +- **纪念日管理** + +- **家庭资产** + +- **密码管家** + +- **奖状证书** + +- **时光手账** + +# 四、技术栈 +| 层级 | 技术 | +|---|---| +| **前端** | Vue 3、Vant、Vue Router、Pinia、Axios | +| **后端** | Spring Boot 3、Spring MVC、Satoken、MyBatis‑Plus | +| **中间件** | Express +Sharp(图片缩略图)、Quartz(定时任务) | +| **存储** | MySQL 8.0(业务数据)、AList(图片/视频对象存储) | +| **视频处理** | Java JAVE2 (FFmpeg 封装,视频压缩转码) | +| **安全** | AES 数据加密 | +| **移动端** | Flutter(WebView 封装为 Android APK) | +| **运维** | Docker、Nginx、Fluent Bit (日志采集)、OpenObserver (日志可视化) | + +# 五、核心设计 +- 基于 **Spring Boot 3 + MyBatis‑Plus** 构建稳定的后端架构,通过 **Sa‑Token** 实现接口级鉴权,保障系统安全与权限可控。 +- 所有敏感信息(密码、资产等)均使用 **AES 对称加密** 存储,防止数据库泄露导致隐私暴露。 +- 使用 **Node.js + Sharp** 搭建图片服务,移动端优先加载缩略图,点击后按需加载原图,显著优化首屏性能。 +- 视频资源上传后通过 **JAVE2 + FFmpeg** 压缩并转码,在保证清晰度的前提下减小体积。 +- 基于 **Quartz** 实现定时任务,用于纪念日提醒等场景。 +- 前端基于 **Vue 3 + Element Plus + Vant + Pinia** 开发移动端页面,并使用 **Flutter WebView** 将 Web 应用封装为 Android APK,实现一套代码多端使用。 +- 使用 **Docker + Nginx** 部署,结合 **Fluent Bit + OpenObserver** 构建统一日志监控体系,提升系统可观测性与运维效率。 + +# 六、难点 / 亮点 +## 6.1 图片资源加载性能优化 +- **问题**:手机拍摄的照片体积较大,移动端直接加载原图导致页面卡顿、流量消耗大。 +- **方案**:使用 **Node.js + Sharp** 构建图片实时处理服务,通过路径参数实时获取压缩图片,默认加载缩略图,点击后加载原图,并配合lazy懒加载优化首屏加载时间。 + +## 6.2 视频资源压缩与存储 +- **问题**:手机视频多为高清录制,体积大、加载慢,直接存储与播放影响用户体验。 +- **方案**:使用 **JAVE2 + FFmpeg** 在上传时将视频统一压缩并转码,减少视频空间。 + +## 6.3 敏感数据加密存储 +- **问题**:密码、资产等数据高度敏感,若数据库被攻破将造成严重隐私泄露。 +- **方案**:采用 AES 对敏感字段进行加密存储,密钥与应用配置隔离管理,服务端统一加解密。 \ No newline at end of file diff --git a/docs/Practice/images/sweethut_bill.jpg b/docs/Practice/images/sweethut_bill.jpg new file mode 100644 index 0000000..036fd0e Binary files /dev/null and b/docs/Practice/images/sweethut_bill.jpg differ diff --git a/docs/Web/Others/ImageProcessor.md b/docs/Web/Others/ImageProcessor.md new file mode 100644 index 0000000..5623f80 --- /dev/null +++ b/docs/Web/Others/ImageProcessor.md @@ -0,0 +1,154 @@ +--- + title: 图片处理服务 + date: 2026-06-04 +--- + +# 一、简介 +  基于 Node.js + Sharp​ 实现的轻量级图片处理服务。 +  [sharp](https://sharp.pixelplumbing.com/)是 Node.js 下非常常用的高性能图像处理库,主要用来缩放、裁剪、旋转、格式转换、压缩图片等,速度快、内存占用低。 +  支持路径+参数的形式实时压缩图片,例如`ip:port/a.png?q=80&w=1024&h=768` + +# 二、核心方法 +## 2.1 根据参数调整图片 +```JavaScript +const sharp = require('sharp'); + +// 压缩配置 +const compressOptions = { + quality: 0.7, + maxHeight: 800, + maxWidth: 600 +}; + +/** + * 处理图片压缩和调整大小 + * @param {string} imagePath 图片路径 + * @param {Object} query 查询参数 + * @returns {Promise} 处理后的图片Buffer + */ +async function processImage(imagePath, query) { + let sharpInstance = sharp(imagePath).rotate(); + + // 处理宽高参数 + let targetWidth = parseInt(query.w) || parseInt(query.width); + let targetHeight = parseInt(query.h) || parseInt(query.height); + + // 如果没有指定宽高,使用配置的默认最大宽高 + if (!targetWidth && !targetHeight) { + targetWidth = compressOptions.maxWidth; + targetHeight = compressOptions.maxHeight; + } + + // 如果指定了宽高,进行调整大小处理 + if (targetWidth || targetHeight) { + const resizeOptions = { + width: targetWidth, + height: targetHeight, + fit: sharp.fit.inside, // 保持宽高比,图片会完整显示在指定区域内 + withoutEnlargement: true // 不放大比原始尺寸小的图片 + }; + sharpInstance = sharpInstance.resize(resizeOptions); + } + + // 处理质量参数(优先使用查询参数,否则使用配置的默认值) + const quality = query.q ? Math.max(1, Math.min(100, parseInt(query.q))) : + Math.round(compressOptions.quality * 100); + const finalQuality = Math.max(1, Math.min(100, quality)); + + // 转换为JPEG格式并压缩 + return sharpInstance.jpeg({quality: finalQuality, mozjpeg: true}).toBuffer(); +} +``` + +  核心方法:[resize](https://sharp.nodejs.cn/api-resize/)、[jpeg](https://sharp.nodejs.cn/api-output/#jpeg)、[toBuffer](https://sharp.nodejs.cn/api-output/#tobuffer)。 + +## 2.2 返回图片数据 +```JavaScript +/** + * 发送响应 + * @param {ServerResponse} res 响应对象 + * @param {number} statusCode 状态码 + * @param {string} message 信息 + */ +function sendResponse(res, statusCode, message) { + res.writeHead(statusCode, { 'Content-Type': 'text/plain; charset=utf-8' }); + res.end(message); +} + +const server = http.createServer(async (req, res) => { + if (req.method !== 'GET') { + return sendResponse(res, 405, '仅支持GET请求'); + } + + try { + const parsedUrl = url.parse(req.url, true); + const requestPath = parsedUrl.pathname; + const query = parsedUrl.query; + + // 过滤根路径请求(避免直接访问服务根目录) + if (requestPath === '/') { + return sendResponse(res, 200, '图片处理服务已启动!'); + } + + const requestedFilePath = requestPath.slice(1); + const fullImagePath = path.join(IMAGE_BASE_DIR, requestedFilePath); + + // 安全检查:防止路径遍历 + if (!isSafePath(fullImagePath, IMAGE_BASE_DIR)) { + return sendResponse(res, 403, '访问拒绝:非法路径(路径遍历攻击)'); + } + + if (!fs.existsSync(fullImagePath)) { + return sendResponse(res, 404, `图片不存在:${requestedFilePath}`); + } + + // 如果没有压缩参数,直接返回原图 + if (!query.q && !query.w && !query.width && !query.h && !query.height) { + const fileStream = fs.createReadStream(fullImagePath); + const ext = path.extname(fullImagePath).slice(1); + res.writeHead(200, {'Content-Type': `image/${ext}`}); + // 流式传输文件(适合大文件) + fileStream.pipe(res); + fileStream.on('error', (err) => { + sendResponse(res, 500, `读取文件失败:${err.message}`); + }); + return; + } + + // 处理图片压缩和调整大小 + const imageBuffer = await processImage(fullImagePath, query); + + res.writeHead(200, { + 'Content-Type': 'image/jpeg', + 'Content-Length': imageBuffer.length, + 'Cache-Control': 'public, max-age=86400' // 缓存1天 + }); + res.end(imageBuffer); + + } catch (error) { + console.error('服务器异常:', error); + sendResponse(res, 500, `服务器内部错误:${error.message}`); + } +}); +``` + +  原生Node.js中res用法: +```js +res.writeHead(statusCode, [statusMessage], [headers]); +res.end(message) +``` + +::: tip +Express中req(请求对象)常用用法: +1. 获取请求路径参数:`/user/:id`->`req.params.id` +2. 获取查询参数(?key=value):`/search?name=tom&age=18`->`req.query.name` +3. 获取请求体:`req.body.username` +4. 获取请求头:`req.headers['user-agent']` + +Express中res(响应对象)常用用法: +1. 返回普通文本:`res.send('Hello Express');` +2. 返回JSON:`res.json({ success: true, data: {} });` +3. 设置状态码:`res.status(404).json({ msg: 'Not Found' });` +4. 设置响应头:`res.set('Content-Type', 'text/plain');` +5. 结束响应:`res.end()` +:::