Hello! 大家好今天给大家介绍一款我的原创图片压缩网站–PicSlim
原创点很明确:

原创业务闭环:上传 → 处理 → 预览 → 权益校验 → 下载 → 过期清理
原创账号体系:accountId + 设备绑定(最多 2 台)+ 前端请求头透传
原创支付体验:第一次点击就出二维码、支付成功提示并要求保存账号、且不自动下载
原创可运维治理:定时清理过期文件,避免长期运行磁盘占满
原创前端健壮性:统一解析 AjaxResult、blob 下载时解析 JSON 错误并展示给用户
原创体验增强:预览支持缩放/旋转/重置;支持 Ctrl+V 粘贴图片上传

  1. 项目定位 / 你做的是什么
    PicSlim 是一个面向普通用户的在线图片处理网站,主打“简单、快速、可控下载”的体验闭环:

图片压缩:按目标大小(KB/MB)压缩并输出结果对比
格式转换:多格式互转(jpg/png/webp/bmp/tiff…)
在线预览:处理结果可直接预览,并支持放大/缩小/旋转
受控下载 + 会员体系:下载前校验会员权益,支持支付开通、账号登录、设备绑定与次数扣减
服务器临时文件治理:定时清理过期文件,避免磁盘被长期占满
2. 核心功能模块 & 核心模块代码(前端 + 后端)
2.1 前端:整体架构与请求体系
技术栈:Vue3 + Vite + ElementPlus
入口级 UI/账号登录:pic-slim-frontend/src/App.vue
登录成功/退出后强制刷新页面,保证请求头立即生效
账号 ID 存储在 localStorage,不依赖 token
关键代码(登录逻辑)来自 App.vue:

const resp = await http.post('/vip/login', new URLSearchParams({ accountId }))
const savedAccountId = data?.accountId || accountId
localStorage.setItem(ACCOUNT_KEY, savedAccountId)
window.location.reload()

统一请求封装 + 设备/账号头注入:pic-slim-frontend/src/utils/http.js
自动携带 X-Device-Id
登录后自动携带 X-Account-Id
统一拦截后端 AjaxResult(code/msg/data),code!=200 直接 reject,便于页面弹错误
关键代码(请求拦截):

config.headers['X-Device-Id'] = getDeviceId()
if (accountId) config.headers['X-Account-Id'] = accountId

下载错误可视化:pic-slim-frontend/src/utils/download.js
下载接口返回 blob 时,如果后端实际上返回 JSON 错误(文件过期/不存在),会解析 msg 并抛错,前端用 ElMessage提示
2.2 图片压缩模块(核心页面)
页面:pic-slim-frontend/src/views/CompressView.vue
核心能力:
多文件上传、压缩参数(KB/MB + target)
发起压缩:POST /pic/compress
结果表格展示:压缩前后大小/分辨率、warning
预览:GET /pic/preview/{fileId}(不扣会员)
下载:GET /pic/download/{fileId} / GET /pic/download/batch/{batchId}(下载受控、会校验会员)
关键代码(发起压缩):

const res = await http.post('/pic/compress', form, { headers: {'Content-Type': 'multipart/form-data'} })
batchId.value = res.data.data.batchId
rows.value = res.data.data.files

2.3 格式转换模块(核心页面)
页面:pic-slim-frontend/src/views/ConvertView.vue
核心能力:
支持选择输出格式(单选)
发起转换:POST /pic/convert
预览/下载与压缩模块一致
关键代码(粘贴上传 Ctrl+V,提升效率):
window.addEventListener('paste', handlePaste) // 从 clipboardData.items 里抓 image/*,转 File 后塞入 fileList
2.4 支付弹窗(第一时间展示二维码 + 支付后不自动下载)
组件:pic-slim-frontend/src/components/PayDialog.vue
核心能力:
点击“支付宝扫码支付”立即调用 POST /pay/precreate 获取二维码并渲染
轮询 GET /pay/query 判断支付是否完成
支付成功后读取 GET /vip/me 获取 accountId 并提示用户保存
支付成功不触发自动下载(由用户重新点击下载按钮,避免“误操作/误下载”)
关键代码(precreate -> 立即渲染二维码 -> 开始轮询):

const res = await http.post('/pay/precreate', new URLSearchParams({ packageType: selected.value }))
outTradeNo.value = res.data.data.outTradeNo
qrCode.value = res.data.data.qrCode
await drawQrCode()
await startPolling()

2.5 后端:图片处理服务(压缩/转换/预览/下载/权益校验)
核心服务实现:RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/service/pic/impl/PicServiceImpl.java
关键职责:
生成批次 batchId、文件 fileId,输出文件落盘到 profile/pic/…
文件元信息、批次信息写入 Redis 并带 TTL(避免无限增长)
下载前校验会员权益(终身 or 次数),并在次数会员时执行扣减
下载/预览异常统一返回 AjaxResult JSON,前端可解析提示
你现在的存储策略(代码里写明):

输出文件:RuoYiConfig.getProfile() 下的 pic/日期/batchId
元信息:Redis + TTL(FILE_META_TTL_MINUTES)
2.6 会员账号系统(accountId 登录 + 设备绑定最多 2 台)
控制器:RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/vip/VipController.java
关键规则:
POST /vip/login:输入 accountId 登录
从请求头读取 X-Device-Id
若该 accountId 已绑定本设备:直接成功
否则检查已绑定设备数,>=2 则禁止登录
GET /vip/me:根据 X-Account-Id + 设备绑定关系返回会员信息
关键代码(限制最多 2 台设备):

int bound = picVipDeviceMapper.countByAccountId(accountId);
if (bound >= 2) return AjaxResult.error(403, "该账号已绑定2台设备,无法在本设备登录");

2.7 支付服务(precreate/query/notify)与发放会员权益
服务实现:RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/service/pay/impl/PayServiceImpl.java
关键点:
precreate:生成 outTradeNo 并向支付宝申请二维码,支持配置 notifyUrl
query/notify:确认成功后发放权益
发放权益时会确保账号存在(ensureAccountId),并绑定设备
关键代码(确保 accountId、写入权益、绑定设备):

String accountId = ensureAccountId(meta.getDeviceId());
picVipMapper.upsertVip(dbVip);
picVipDeviceMapper.bindDevice(accountId, meta.getDeviceId());

2.8 临时文件治理(防磁盘爆炸):定时清理过期文件
定时任务:RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/task/PicTempFileCleanupTask.java
配置:RuoYi-Vue/ruoyi-admin/src/main/resources/application.yml
picSlim.temp.cleanup.enabled
picSlim.temp.cleanup.ttlMinutes
picSlim.temp.cleanup.cron(默认每 10 分钟)
关键代码(按 TTL 删除 profile/pic 下过期文件,并清空空目录):

Instant cutoff = Instant.now().minusSeconds(ttlMinutes * 60L);
Files.walkFileTree(root, new SimpleFileVisitor<Path>() { ... })
cleanupEmptyDirs(root);

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐