Lsky Pro+ v2 二次开发记录
基于 Lsky Pro+ v2.2.7 开发 (v2.3.0+加密了核心代码,所以选用v2.2.7)
后端代码由于是php且未加密,是完全可以二次开发的
前端代码由于是vue且编译过,二次开发需要逆向,懒,所以采用前端js修改
目录
后端
1.挂载第三方图床
功能
写了一个 文章:图床外显为Webdav管理,但是有个问题是Lsky Pro数据库中是记录的是自己生成的路径,而非使用Webdav挂载的第三方图床的路径 (毕竟第三方图床,上传图片返回的链接一般是无法自定义的)。
Webdav图床逻辑
st=>start: 是否为图片直链?
e=>end: 访问图片
cond1=>condition: 是否为图片直链
op1=>operation: GET WebDAV/原文件名.png
op2=>operation: 302 重定向到直链
op3=>operation: GET 图像直链
st->cond1
cond1(yes)->op3->e
cond1(no)->op1->op2->e正好我写的Webdav图床外显程序包含了一个/api/get-url的接口来获取挂载图床的path,那么图床逻辑如下图:
st=>start: 用户上传
e=>end: 访问图片
op2=>operation: 通过WebDAV 上传到第三方图床
op5=>operation: 查询 /api/get-url ,获取第三方图床路径
op8=>operation: 写入 Lsky 数据库
op9=>operation: 返回储存[访问域名+第三方图床路径]
st->op2->op5->op8->op9->e代码
在.env中添加api-url地址,只有需要返回new path的需要写
# 格式:PHOTO_STORAGE_[STORAGE_ID]_GET_URL_API=https://api.example.com/api/get-url, 如:
PHOTO_STORAGE_1_GET_URL_API=https://api.example.com/api/get-url在储存照片时调用api, 修改/app/Services/PhotoService.php
public function store(array $data, array $albums = [], array $tags = []): Photo
{
return DB::transaction(function () use ($data, $albums, $tags) {
$originalPathname = $data['pathname'] ?? '';
$storageId = $data['storage_id'] ?? null;
$finalPathname = $originalPathname;
$externalUrl = null;
/* --------- 开始 ---------- */
// 从 .env 读取 PHOTO_STORAGE_{ID}_GET_URL_API
if ($storageId && is_numeric($storageId) && $originalPathname !== '') {
$envKey = 'PHOTO_STORAGE_' . $storageId . '_GET_URL_API';
$getUrlApi = env($envKey);
if ($getUrlApi) {
try {
$fullApiUrl = rtrim($getUrlApi, '/') . '?path=' . urlencode($originalPathname);
$response = Http::timeout(5)->get($fullApiUrl);
if (
$response->successful() &&
is_array($body = $response->json()) &&
!empty($body['url'])
) {
// 保存 clean path(如 "i/AbC123.png")
$finalPathname = ltrim((string) $body['url'], '/');
}
} catch (\Exception $e) {
Log::warning('Failed to fetch CDN path from get_url_api', [
'storage_id' => $storageId,
'pathname' => $originalPathname,
'env_key' => $envKey,
'error' => $e->getMessage(),
]);
}
}
}
/* --------- 结束 ---------- */
//其余代码不变
}
}2.特定储存策略免扣用户空间
功能
先明确Lsky Pro+的收费分开的:
- 会员套餐:控制储存策略和上传内容大小限制等
- 扩容套餐:控制用户的储存空间
如果有一个场景是:你的图床同时提供
- 免费储存:储存策略和空间都不用收费
- 空间收费储存:不需购买会员就能使用的,但是使用会占用用户空间
- 策略收费储存:需要购买会员才能解锁的收费储存策略(比如国内OSS等成本比较昂贵的)
为了优化收费策略,用户注册可以获得少量储存空间,免费储存随便用,希望使用空间收费储存就需要充值扩容,希望使用策略收费储存需要购买会员
上传逻辑图
st=>start: 用户上传图片
e=>end: 结束
cond1=>condition: 是否免扣
cond2=>condition: 剩余空间是否足够
op1=>operation: 已用空间不变
op2=>operation: 已用空间 += 文件大小
op3=>operation: 阻止上传
op4=>operation: free_storage = 1
op5=>operation: free_storage = 0
op6=>operation: 校验通过,上传照片
st->cond1
cond1(yes)->op1->op4->op6->e
cond1(no)->op2->cond2
cond2(no)->op3->e
cond2(yes)->op5->op6->e获取已用空间逻辑图 (用户端/前端)
st=>start: 获取已用空间
e=>end: 得到已用空间
op1=>operation: 表中 free_stroage=0 的 size 求和
st->op1->e代码
1.先修改表,添加free_stroage字段。
ALTER TABLE photos ADD COLUMN free_storage INTEGER DEFAULT 0;如果是Sqlite,则安装Sqlite后直接用sqlite打开修改。
2.修改/app/Services/PhotoService.php,增加free_storage的值。
public function store(array $data, array $albums = [], array $tags = []): Photo
{
return DB::transaction(function () use ($data, $albums, $tags) {
$originalPathname = $data['pathname'] ?? '';
$storageId = $data['storage_id'] ?? null;
$finalPathname = $originalPathname;
$externalUrl = null;
/* --------- 开始 ---------- */
$freeStorageIds = json_decode(env('FREE_STORAGE_ID', '[]'), true);
if (!is_array($freeStorageIds)) {
$freeStorageIds = [];
}
$data['free_storage'] = (
isset($data['storage_id']) &&
in_array((int) $data['storage_id'], $freeStorageIds, true)
) ? 1 : 0;
/* --------- 结束 ---------- */
//其余代码不变
}
}3.修改图片模型/app/Models/Photo.php,添加free_storage属性
class Photo extends Model
{
/* 以上代码保持不变 */
protected $fillable = [
'user_id',
'group_id',
'storage_id',
'name',
'intro',
'filename',
'pathname',
'mimetype',
'extension',
'md5',
'sha1',
'exif',
'size',
'width',
'height',
'is_public',
'status',
'ip_address',
'expired_at',
/* 添加'free_storage'属性 */
'free_storage',
];
/* 以下代码保持不变 */
}4.修改上传请求的剩余空间检查逻辑/app/Http/Requests/UploadRequest.php
修改以下代码
function (string $attribute, UploadedFile $value, Closure $fail) use ($user) {
// 验证储存空间是否充足
if (!is_null($user)) {
$total = UserCapacityService::getUserTotalCapacity();
$size = $value->getSize() / 1024 + $user->photos()->sum('size');
if ($size > $total) {
$fail('储存空间不足');
}
}
},为
function (string $attribute, UploadedFile $value, Closure $fail) use ($user) {
if (is_null($user)) {
return;
}
$total = UserCapacityService::getUserTotalCapacity();
$used = $user->photos()
->where('free_storage', 0)
->sum('size');
$freeStorageIds = json_decode(env('FREE_STORAGE_ID', '[]'), true);
if (!is_array($freeStorageIds)) {
$freeStorageIds = [];
}
$storageId = request()->input('storage_id');
$isFree = in_array((int) $storageId, $freeStorageIds, true);
if (! $isFree) {
$used += $value->getSize() / 1024;
}
if ($used > $total) {
$fail('储存空间不足');
}
}前端
1.路由监听
这个是做前端小修改的基础,可以放到网站后台-系统设置-网站设置-自定义JavaScript
模块化RouteDOM
const RouteDOM = (() => {
const routes = [];
let active = null;
let timer = null;
function match(route) {
if (typeof route === 'string') return location.pathname === route;
if (route instanceof RegExp) return route.test(location.pathname);
if (typeof route === 'function') return route(location.pathname);
return false;
}
function clear() {
if (timer) {
clearInterval(timer);
timer = null;
}
active?.onLeave?.();
active = null;
}
function run() {
const next = routes.find(r => match(r.route));
if (next !== active) {
clear();
if (!next) return;
active = next;
timer = setInterval(() => {
if (active.onEnter?.()) {
clearInterval(timer);
timer = null;
}
}, active.interval ?? 300);
}
}
function listen() {
const push = history.pushState;
const replace = history.replaceState;
history.pushState = function () {
push.apply(this, arguments);
run();
};
history.replaceState = function () {
replace.apply(this, arguments);
run();
};
window.addEventListener('popstate', run);
run();
}
function register(config) {
routes.push(config);
}
return { register, listen };
})();启动
RouteDOM.listen();2.仪表盘“我的信息”添加头像
RouteDOM.register({
route: '/user/dashboard',
onEnter() {
const rows = document.querySelectorAll('.n-card__content .flex');
const nameRow = [...rows].find(row =>
row.textContent.includes('姓名') ||
row.textContent.includes('Full Name')
);
if (!nameRow) return false;
const nameValue = nameRow.children[1];
const avatarImg = document.querySelector('.n-avatar img');
if (!nameValue || !avatarImg) return false;
if (nameValue.querySelector('.js-name-avatar')) return true;
const avatar = avatarImg.cloneNode(true);
avatar.className = 'js-name-avatar';
avatar.style.width = '24px';
avatar.style.height = '24px';
avatar.style.borderRadius = '50%';
avatar.style.marginRight = '8px';
nameValue.style.display = 'flex';
nameValue.style.alignItems = 'center';
nameValue.prepend(avatar);
return true;
},
onLeave() {
document.querySelectorAll('.js-name-avatar').forEach(e => e.remove());
}
}); 当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »