MENU

Lsky Pro+ v2 二次开发记录

2026 年 02 月 03 日 • 阅读: 10 • 技术,开发

基于 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.仪表盘“我的信息”添加头像

Image

基于1. 路由监听,注册路由:
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());
  }
});
返回文章列表 打赏
本页链接的二维码
打赏二维码