Electron + Vue3开源跨平台壁纸工具实战(四)主进程-数据管理(1)

213 阅读15分钟

fbw_social_preview.png

系列

Electron + Vue3开源跨平台壁纸工具实战(一)

Electron + Vue3开源跨平台壁纸工具实战(二)本地运行

Electron + Vue3开源跨平台壁纸工具实战(三)主进程

Electron + Vue3开源跨平台壁纸工具实战(四)主进程-数据管理(1)

Electron + Vue3开源跨平台壁纸工具实战(五)主进程-数据管理(2)

Electron + Vue3开源跨平台壁纸工具实战(六)子进程服务

Electron + Vue3开源跨平台壁纸工具实战(七)进程通信

Electron + Vue3开源跨平台壁纸工具实战(八)主进程-核心功能

Electron + Vue3开源跨平台壁纸工具实战(九)子进程服务(2)

Electron + Vue3开源跨平台壁纸工具实战(十)渲染进程

源码

省流点我进Github获取源码,欢迎fork、star、PR

数据管理

主进程中的数据管理统一在store目录下

  • index.mjs: Store类,数据管理入口,用于实例化各个管理类,启动定时任务、子服务等
  • DatabaseManager.mjs: 数据库管理类,用于建表、初始化数据库
  • FileManager.mjs: 文件管理类,用于启动文件子服务、处理文件
  • ApiManager.mjs: 资源接口管理类,用于加载应用自带资源API、用户自定义资源API
  • ResourcesManager.mjs: 资源查询管理类,用于查询、操作资源数据
  • SettingManager.mjs: 设置数据管理类,用于获取、更新设置数据
  • TaskScheduler.mjs: 定时任务管理类,用于管理应用内各种定时任务的启停清理
  • WallpaperManager.mjs: 壁纸管理类,用于壁纸切换、下载、清理
  • WordsManager.mjs: 词库管理类,用于查询、处理分词

数据管理类

在主进程入库中实例化数据管理类

// main/index.mjs

// 初始化Store
store = new Store({
sendCommonData,
sendMsg
})
// 等待 Store 初始化完成
await store.waitForInitialization()

Store主要功能:

  • 统一主进程数据管理、逻辑处理,实例化各个管理类
  • 调度子进程服务
  • 定时任务管理
  • 与渲染进程数据通信

import { app, ipcMain, BrowserWindow, screen, powerMonitor } from 'electron'
import path from 'path'
import fs from 'fs'
import { createFileServer, createH5Server } from '../child_server/index.mjs'
import { t } from '../../i18n/server.js'
import DatabaseManager from './DatabaseManager.mjs'
import ApiManager from './ApiManager.mjs'
import ResourcesManager from './ResourcesManager.mjs'
import WallpaperManager from './WallpaperManager.mjs'
import TaskScheduler from './TaskScheduler.mjs'
import FileManager from './FileManager.mjs'
import WordsManager from './WordsManager.mjs'
import SettingManager from './SettingManager.mjs'
import VersionManager from './VersionManager.mjs'
// 导入动态壁纸工具
// import { setDynamicWallpaper, closeDynamicWallpaper } from '../utils/wallpaper.mjs'
import { handleTimeByUnit } from '../utils/utils.mjs'

export default class Store {
  constructor({ sendCommonData, sendMsg } = {}) {
    // 初始化完成标志
    this._initialized = false
    this.sendCommonData = sendCommonData
    this.sendMsg = sendMsg
    this.mainWindow = null
    this.suspensionBall = null

    // 锁
    this.locks = {
      refreshDirectory: false,
      handleQuality: false,
      handleWords: false
    }

    // 添加电源状态标志
    this.powerState = {
      isSystemIdle: false,
      wasAutoSwitchEnabled: false
    }

    // 初始化时等待设置加载完成
    this._initPromise = this._init()
  }

  // 初始化方法
  async _init() {
    try {
      // 初始化数据库管理器
      this.dbManager = DatabaseManager.getInstance(global.logger)
      // 等待数据库管理器初始化完成
      await this.dbManager.waitForInitialization()
      this.db = this.dbManager.db

      // 初始化版本管理器
      this.versionManager = VersionManager.getInstance(global.logger, this.dbManager)
      await this.versionManager.waitForInitialization()

      // 初始化设置管理器
      this.settingManager = SettingManager.getInstance(global.logger, this.dbManager)
      // 等待设置管理器初始化完成
      await this.settingManager.waitForInitialization()
      // 初始化API管理器
      this.apiManager = ApiManager.getInstance(global.logger, this.dbManager)
      // 等待API管理器初始化完成
      await this.apiManager.waitForInitialization()

      // 文件服务子进程
      this.fileServer = createFileServer()

      // h5服务子进程
      this.h5Server = createH5Server()
      this.h5ServerUrl = null

      // 初始化其他管理器
      this.taskScheduler = TaskScheduler.getInstance(global.logger)
      this.wordsManager = WordsManager.getInstance(
        global.logger,
        this.dbManager,
        this.settingManager
      )
      this.fileManager = FileManager.getInstance(
        global.logger,
        this.dbManager,
        this.settingManager,
        this.fileServer,
        this.wordsManager
      )
      this.resourcesManager = ResourcesManager.getInstance(
        global.logger,
        this.dbManager,
        this.settingManager,
        this.apiManager
      )
      this.wallpaperManager = WallpaperManager.getInstance(
        global.logger,
        this.dbManager,
        this.settingManager,
        this.fileManager,
        this.apiManager
      )

      // 处理IPC通信
      this.handleIpc()

      // 处理文件服务子进程启动
      this.handleFileServerStart()

      // 如果设置了自动启动H5服务,在初始化完成后启动
      if (this.settingData.startH5ServerOnStartup) {
        try {
          global.logger.info('初始化完成后启动H5服务...')
          // 添加延迟,给网络接口更多时间初始化
          setTimeout(() => {
            this.handleH5ServerStart(3, 2000)
          }, 5000) // 延迟5秒启动
        } catch (err) {
          global.logger.error(`初始化完成后启动H5服务失败: ${err.message}`)
        }
      }

      // 开启定时任务
      this.startScheduledTasks()

      // 处理开机自启动设置
      this.handleStartup()

      // 监听系统电源状态
      this.setupPowerMonitor()

      this._initialized = true
      global.logger.info('Store 初始化完成')
      return true
    } catch (err) {
      global.logger.error(`Store 初始化失败: ${err.message}`)
      return false
    }
  }

  // 等待初始化完成的方法
  async waitForInitialization() {
    if (this._initialized) {
      return true
    }
    return this._initPromise
  }

  get settingData() {
    return this.settingManager.settingData
  }

  // 设置主窗口
  setMainWindow(mainWindow) {
    this.mainWindow = mainWindow
  }

  // 设置悬浮球
  setSuspensionBall(suspensionBall) {
    this.suspensionBall = suspensionBall
  }

  // 更新开机自启动设置
  handleStartup() {
    if (app.isPackaged) {
      const exePath = process.execPath
      app.setLoginItemSettings({
        openAtLogin: this.settingData.startup,
        openAsHidden: !this.settingData.openMainWindowOnStartup,
        path: exePath,
        args: ['--autoStart']
      })
    }
  }

  // 开启定时任务
  startScheduledTasks() {
    this.startMonitorMemoryTask()
    this.startHandleQualityTask()
    this.startHandleWordsTask()

    // 处理定时任务
    this.intervalSwitchWallpaper()
    this.intervalRefreshDirectory()
    this.intervalDownload()
    this.intervalClearDownloaded()
  }

  // 启动内存监控任务
  startMonitorMemoryTask() {
    const key = 'monitorMemory'
    // 清理任务
    this.taskScheduler.clearTask(key)
    // 开启内存监控任务
    this.taskScheduler.scheduleTask('monitorMemory', 5 * 60 * 1000, () => {
      const memoryUsage = process.memoryUsage()
      const heapUsedMB = Math.round((memoryUsage.heapUsed / 1024 / 1024) * 100) / 100
      const heapTotalMB = Math.round((memoryUsage.heapTotal / 1024 / 1024) * 100) / 100

      global.logger.info(`内存使用情况: ${heapUsedMB}MB / ${heapTotalMB}MB`)

      // 如果堆内存使用超过80%,触发垃圾回收
      if (heapUsedMB / heapTotalMB > 0.8) {
        global.logger.info('内存使用率较高,触发垃圾回收')
        if (global.gc) {
          global.gc()
        }
      }
    })
  }

  // 启动图片质量处理任务
  startHandleQualityTask() {
    const key = 'handleQuality'
    // 清理任务
    this.taskScheduler.clearTask(key)
    // 开启定时计算图片质量
    this.taskScheduler.scheduleTask(
      key,
      7 * 60 * 1000,
      () => {
        this.fileManager.intervalHandleQuality(this.locks)
      },
      10 * 60 * 1000
    )
  }

  // 启动处理分词任务
  startHandleWordsTask() {
    const key = 'handleWords'
    // 清理任务
    this.taskScheduler.clearTask(key)
    // 检查是否启用词库处理任务
    if (this.settingData.enableSegmentationTask) {
      // 开启定时处理词库
      this.taskScheduler.scheduleTask(
        key,
        11 * 60 * 1000,
        () => {
          this.wordsManager.intervalHandleWords(this.locks)
        },
        10 * 60 * 1000
      )
    }
  }

  // 设置电源监控
  setupPowerMonitor() {
    // 监听系统挂起事件
    powerMonitor.on('suspend', () => {
      global.logger.info('系统挂起,暂停自动切换壁纸')
      this.handleSystemIdle(true)
    })

    // 监听系统恢复事件
    powerMonitor.on('resume', () => {
      global.logger.info('系统恢复,恢复自动切换壁纸状态')
      this.handleSystemIdle(false)
    })

    // 监听锁屏事件
    powerMonitor.on('lock-screen', () => {
      global.logger.info('系统锁屏,暂停自动切换壁纸')
      this.handleSystemIdle(true)
    })

    // 监听解锁事件
    powerMonitor.on('unlock-screen', () => {
      global.logger.info('系统解锁,恢复自动切换壁纸状态')
      this.handleSystemIdle(false)
    })

    // 监听系统空闲状态
    if (powerMonitor.getSystemIdleState) {
      // 每分钟检查一次系统空闲状态
      setInterval(() => {
        // 系统空闲阈值,单位为秒,默认5分钟
        const idleState = powerMonitor.getSystemIdleState(300)
        if (idleState === 'idle' && !this.powerState.isSystemIdle) {
          global.logger.info('系统空闲,暂停自动切换壁纸')
          this.handleSystemIdle(true)
        } else if (idleState === 'active' && this.powerState.isSystemIdle) {
          global.logger.info('系统活跃,恢复自动切换壁纸状态')
          this.handleSystemIdle(false)
        }
      }, 60000)
    }
  }

  // 处理系统空闲状态
  handleSystemIdle(isIdle) {
    if (isIdle && !this.powerState.isSystemIdle) {
      // 系统进入空闲状态,记录当前自动切换状态并暂停
      this.powerState.isSystemIdle = true
      this.powerState.wasAutoSwitchEnabled = this.settingData.autoSwitchWallpaper

      if (this.powerState.wasAutoSwitchEnabled) {
        // 暂停自动切换壁纸,但不更新设置
        this.taskScheduler.clearTask('autoSwitchWallpaper')
      }
    } else if (!isIdle && this.powerState.isSystemIdle) {
      // 系统恢复活跃状态,恢复之前的自动切换状态
      this.powerState.isSystemIdle = false

      if (this.powerState.wasAutoSwitchEnabled) {
        // 恢复自动切换壁纸
        this.intervalSwitchWallpaper()
      }
    }
  }

  // 处理文件服务子进程启动
  handleFileServerStart() {
    try {
      // 启动子进程
      this.fileServer?.start({
        onMessage: ({ data }) => {
          switch (data.event) {
            case 'REFRESH_DIRECTORY::SUCCESS':
              // 添加接收时间戳
              data.receiveMsgTime = Date.now()
              this.onRefreshDirectorySuccess(data)
              break
            case 'REFRESH_DIRECTORY::FAIL':
              // 添加接收时间戳
              data.receiveMsgTime = Date.now()
              this.onRefreshDirectoryFail(data)
              break
            case 'SERVER_LOG': {
              const type = data.level
              if (type && typeof global.logger[type] === 'function') {
                global.logger[type](data.msg)
              } else {
                global.logger.info(`[FileServer] INFO => ${data.msg}`)
              }
              break
            }
            case 'HANDLE_IMAGE_QUALITY::SUCCESS':
              this.fileManager.onHandleImageQualitySuccess(data, this.locks)
              break
            case 'HANDLE_IMAGE_QUALITY::FAIL':
              this.fileManager.onHandleImageQualityFail(this.locks)
              break
          }
        }
      })
    } catch (err) {
      global.logger.error(err)
    }
  }

  // 文件服务子进程-遍历目录完成
  onRefreshDirectorySuccess(data) {
    // 获取开始时间和接收时间,使用当前时间作为默认值而不是0
    const startTime = data.refreshDirStartTime
    // 父进程向子进程发送消息耗时
    const parentToChildCoast = data.readDirTime.start - startTime
    // 子进程向父进程发送消息耗时
    const childToParentCoast = data.receiveMsgTime - data.readDirTime.end
    // 遍历目录耗时
    const readDirCoast = data.readDirTime.end - data.readDirTime.start
    // 记录开始处理数据库的时间
    const processDataStartTime = Date.now()
    const res = this.fileManager.processDirectoryData(data)

    // 记录结束时间
    const endTime = Date.now()

    // 计算各阶段耗时
    const totalCoast = endTime - startTime
    const processDataCost = endTime - processDataStartTime

    // 转换成YYYY-MM-DD HH:mm:ss格式
    const timeNow = new Date().toLocaleString()
    // 打印耗时信息
    console.log(
      `刷新目录完成 - 时间: ${timeNow}  总耗时: ${totalCoast}ms, 父=>子: ${parentToChildCoast}, 遍历目录耗时: ${readDirCoast}ms, 子=>父: ${childToParentCoast}ms, 插入数据库耗时: ${processDataCost}ms`
    )

    // 清除锁
    this.locks.refreshDirectory = false
    // 手动刷新完成后发送消息
    if (data.isManual) {
      this.sendMsg(this.mainWindow, {
        type: res.success ? (res.data.insertedCount > 0 ? 'success' : 'info') : 'error',
        msg: res.msg
      })
      if (res.success && res.data.insertedCount > 0) {
        // 触发刷新动作
        this.triggerAction('refreshSearchList')
      }
    }
  }

  // 文件服务子进程-遍历目录失败
  onRefreshDirectoryFail(data) {
    // 清除锁
    this.locks.refreshDirectory = false
    // 手动刷新完成后发送消息
    if (data.isManual) {
      this.sendMsg(this.mainWindow, {
        type: 'error',
        msg: t('messages.refreshDirectoryFail')
      })
    }
  }

  // 带重试机制的H5服务启动方法
  handleH5ServerStart(maxRetries = 3, retryInterval = 2000) {
    let retryCount = 0

    const attemptStart = () => {
      try {
        this.h5Server?.start({
          options: {
            env: process.env
          },
          onMessage: async ({ data }) => {
            switch (data.event) {
              case 'SERVER_START::SUCCESS': {
                this.h5ServerUrl = data.url

                // 检查IP是否有效
                const urlObj = new URL(data.url)
                const ip = urlObj.hostname

                if (ip === '0.0.0.0' || ip === '127.0.0.1') {
                  global.logger.warn(`H5服务器IP无效: ${ip},尝试重启服务`)

                  // 停止当前服务
                  this.h5Server?.stop(() => {
                    if (retryCount < maxRetries) {
                      retryCount++
                      global.logger.info(`重试启动H5服务 (${retryCount}/${maxRetries})...`)
                      setTimeout(attemptStart, retryInterval)
                    } else {
                      global.logger.error(`H5服务器无法获取有效IP,已达到最大重试次数`)
                    }
                  })
                  return
                }

                global.logger.info(`H5服务器启动成功: ${this.h5ServerUrl}`)

                // 发送消息到主窗口
                if (this.mainWindow) {
                  this.sendCommonData(this.mainWindow)
                  this.sendMsg(this.mainWindow, {
                    type: 'success',
                    msg: t('messages.h5ServerStartSuccess')
                  })
                } else {
                  global.logger.warn('主窗口未初始化,无法发送H5服务器URL')
                }
                break
              }
              case 'SERVER_START::FAIL': {
                global.logger.error(`H5服务器启动失败: ${data}`)
                break
              }
              case 'SERVER_LOG': {
                const type = data.level
                if (type && typeof global.logger[type] === 'function') {
                  global.logger[type](data.msg)
                } else {
                  global.logger.info(`[H5Server] INFO => ${data.msg}`)
                }
                break
              }
              case 'H5_SETTING_UPDATED': {
                // 获取最新设置数据
                await this.settingManager.getSettingData()
                // 发送更新消息
                this.sendSettingDataUpdate()
                break
              }
            }
          }
        })
      } catch (err) {
        global.logger.error(`启动H5服务器失败: ${err}`)

        if (retryCount < maxRetries) {
          retryCount++
          global.logger.info(`重试启动H5服务 (${retryCount}/${maxRetries})...`)
          setTimeout(attemptStart, retryInterval)
        } else {
          // 发送错误消息
          if (this.mainWindow) {
            this.sendMsg(this.mainWindow, {
              type: 'error',
              msg: t('messages.h5ServerStartFail')
            })
          }
        }
      }
    }

    // 开始第一次尝试
    attemptStart()
  }

  // 处理H5服务子进程停止
  handleH5ServerStop() {
    try {
      this.h5Server?.stop((isSuccess) => {
        if (isSuccess) {
          this.h5ServerUrl = null
          this.sendCommonData(this.mainWindow)
          this.sendMsg(this.mainWindow, {
            type: 'success',
            msg: t('messages.h5ServerStopSuccess')
          })
        } else {
          // 发送错误消息
          this.sendMsg(this.mainWindow, {
            type: 'error',
            msg: t('messages.h5ServerStopFail')
          })
        }
      })
    } catch (err) {
      global.logger.error(err)
    }
  }

  // 触发动作
  triggerAction(action, data) {
    this.mainWindow.webContents.send('main:triggerAction', action, data)
  }

  // 发送设置数据更新
  sendSettingDataUpdate() {
    if (this.mainWindow) {
      this.mainWindow.webContents.send('main:settingDataUpdate', this.settingData)
    }
    if (this.suspensionBall) {
      this.suspensionBall.webContents.send('main:settingDataUpdate', this.settingData)
    }
  }

  // 处理IPC通信
  handleIpc() {
    // 获取设置数据
    ipcMain.handle('main:getSettingData', () => {
      return this.settingManager.getSettingData()
    })

    // 合并更新设置数据
    ipcMain.handle('main:updateSettingData', async (event, formData) => {
      return await this.updateSettingData(formData)
    })

    // 获取资源数据
    ipcMain.handle('main:getResourceMap', () => {
      return this.dbManager.getResourceMap()
    })

    // 验证隐私空间密码
    ipcMain.handle('main:checkPrivacyPassword', async (event, password) => {
      return await this.settingManager.checkPrivacyPassword(password)
    })

    // 检查是否设置了隐私密码
    ipcMain.handle('main:hasPrivacyPassword', async () => {
      return await this.settingManager.hasPrivacyPassword()
    })

    ipcMain.handle('main:updatePrivacyPassword', async (event, formData) => {
      return await this.settingManager.updatePrivacyPassword(formData)
    })

    // 加入收藏夹
    ipcMain.handle('main:addToFavorites', async (event, resourceId, isPrivacySpace = false) => {
      return await this.resourcesManager.addToFavorites(resourceId, isPrivacySpace)
    })

    // 移出收藏夹
    ipcMain.handle('main:removeFavorites', async (event, resourceId, isPrivacySpace = false) => {
      return await this.resourcesManager.removeFavorites(resourceId, isPrivacySpace)
    })

    // 删除文件
    ipcMain.handle('main:deleteFile', async (event, item) => {
      return await this.fileManager.deleteFile(item)
    })

    // 搜索资源数据
    ipcMain.handle('main:searchImages', async (event, params) => {
      return await this.resourcesManager.searchImages(params)
    })

    // 设置为壁纸
    ipcMain.handle('main:setAsWallpaperWithDownload', async (event, item) => {
      return await this.wallpaperManager.setAsWallpaperWithDownload(item)
    })

    // 切换壁纸
    ipcMain.handle('main:nextWallpaper', async () => {
      return this.doManualSwitchWallpaper('next')
    })

    // 切换至上一个壁纸
    ipcMain.handle('main:prevWallpaper', async () => {
      return this.doManualSwitchWallpaper('prev')
    })

    // 设置网页壁纸
    ipcMain.handle('main:setWebWallpaper', (event, url) => {
      return this.setWebWallpaper(url)
    })

    // 添加动态壁纸相关的IPC处理程序
    // ipcMain.handle('main:setDynamicWallpaper', async (event, videoPath) => {
    //   return await setDynamicWallpaper(videoPath)
    // })

    // ipcMain.handle('main:closeDynamicWallpaper', () => {
    //   return closeDynamicWallpaper()
    // })

    // 启停定时切换壁纸
    ipcMain.handle('main:toggleAutoSwitchWallpaper', async () => {
      return this.toggleAutoSwitchWallpaper()
    })

    // 清空当前资源DB
    ipcMain.handle('main:doClearDB', async (event, tableName, resourceName) => {
      const res = await this.dbManager.clearDB(tableName, resourceName)
      this.sendMsg(this.mainWindow, {
        type: res.success ? 'success' : 'error',
        msg: res.msg
      })
      return res
    })

    // 刷新当前资源目录
    ipcMain.handle('main:refreshDirectory', async () => {
      const res = this.fileManager.refreshDirectory(this.locks, true)
      if (res && !res.success) {
        this.sendMsg(this.mainWindow, {
          type: 'error',
          msg: res.msg
        })
      }
    })

    // 查找词库
    ipcMain.handle('main:getWords', async (event, params) => {
      return this.wordsManager.getWords(params)
    })

    // H5服务相关
    ipcMain.handle('main:startH5Server', () => {
      this.handleH5ServerStart(3, 2000)
    })

    ipcMain.handle('main:stopH5Server', () => {
      this.handleH5ServerStop()
    })

    ipcMain.handle('main:clearDownloadedAll', async () => {
      const res = await this.wallpaperManager.clearDownloadedAll()
      if (res) {
        this.sendMsg(this.mainWindow, {
          type: res.success ? 'success' : 'error',
          msg: res.msg
        })
      }
    })

    ipcMain.handle('main:clearDownloadedExpired', async () => {
      const res = await this.wallpaperManager.clearDownloadedExpired()
      if (res) {
        this.sendMsg(this.mainWindow, {
          type: res.success ? 'success' : 'error',
          msg: res.msg
        })
      }
    })
  }

  // 更新设置数据并触发变更
  async updateSettingData(data) {
    const oldData = JSON.parse(JSON.stringify(this.settingData))
    // 更新设置
    const res = await this.settingManager.updateSettingData(data)
    if (res.success) {
      const newData = JSON.parse(JSON.stringify(this.settingData))
      // 向H5子进程发送设置更新
      this.h5Server?.postMessage({
        event: 'APP_SETTING_UPDATED',
        data: this.settingData
      })
      // 发送更新消息
      this.sendSettingDataUpdate()

      // 处理定时任务,仅当设置项发生变化时触发
      if (
        oldData.autoSwitchWallpaper !== newData.autoSwitchWallpaper ||
        oldData.switchIntervalUnit !== newData.switchIntervalUnit ||
        oldData.switchIntervalTime !== newData.switchIntervalTime
      ) {
        this.intervalSwitchWallpaper()
      }
      if (
        oldData.autoRefreshDirectory !== newData.autoRefreshDirectory ||
        oldData.refreshDirectoryIntervalUnit !== newData.refreshDirectoryIntervalUnit ||
        oldData.refreshDirectoryIntervalTime !== newData.refreshDirectoryIntervalTime
      ) {
        this.intervalRefreshDirectory()
      }
      if (
        oldData.autoDownload !== newData.autoDownload ||
        oldData.downloadIntervalUnit !== newData.downloadIntervalUnit ||
        oldData.downloadIntervalTime !== newData.downloadIntervalTime
      ) {
        this.intervalDownload()
      }
      if (oldData.autoClearDownloaded !== newData.autoClearDownloaded) {
        this.intervalClearDownloaded()
      }

      // 处理分词任务
      this.startHandleWordsTask()
      // 处理应用打包后开机自启
      this.handleStartup()
    }
    return res
  }

  // 手动切换壁纸
  async doManualSwitchWallpaper(direction) {
    // 先关闭自动切换
    await this.toggleAutoSwitchWallpaper(false)
    let ret = {
      success: false,
      msg: t('messages.operationFail')
    }
    if (direction === 'next') {
      ret = await this.wallpaperManager.doSwitchToNextWallpaper()
    } else if (direction === 'prev') {
      ret = await this.wallpaperManager.doSwitchToPrevWallpaper()
    }
    // 触发动作
    this.triggerAction('setWallpaper', ret)
    return ret
  }

  // 定时切换壁纸
  intervalSwitchWallpaper() {
    const key = 'autoSwitchWallpaper'
    // 取消之前的定时任务
    this.taskScheduler.clearTask(key)
    // 重置切换壁纸参数
    this.wallpaperManager.resetParams(key)
    // 如果开启了自动切换壁纸
    if (this.settingData[key] && !this.powerState.isSystemIdle) {
      // 设置定时切换壁纸
      this.taskScheduler.scheduleTask(key, this.handleInterval(key), async () => {
        const res = await this.wallpaperManager.doSwitchToNextWallpaper()
        // 触发动作
        this.triggerAction('setWallpaper', res)
      })
    }
  }

  // 创建网页图片
  async getWebImage(url) {
    let tempWindow = null
    try {
      const { width, height } = screen.getPrimaryDisplay().workAreaSize
      // 创建一个隐藏的窗口来加载网页
      tempWindow = new BrowserWindow({
        width,
        height,
        show: false,
        webPreferences: {
          offscreen: true // 使用离屏渲染
        }
      })

      // 加载URL
      await tempWindow.loadURL(url)

      // 等待页面完全加载
      await new Promise((resolve) => setTimeout(resolve, 2000))

      // 捕获页面截图
      const image = await tempWindow.webContents.capturePage()
      const pngData = image.toPNG()

      // 保存截图到下载文件
      const downloadFilePath = path.join(process.env.FBW_DOWNLOAD_PATH, 'wallpaper.png')
      // console.log('downloadFilePath', downloadFilePath)
      fs.writeFileSync(downloadFilePath, pngData)

      return downloadFilePath
    } catch (err) {
      global.logger.error(`获取网页图片失败: error => ${err}`)
      return null
    } finally {
      // 确保在任何情况下都销毁临时窗口
      if (tempWindow) {
        tempWindow.destroy()
        tempWindow = null
      }
    }
  }

  // 设置网页壁纸
  async setWebWallpaper(url) {
    try {
      // 先关闭自动切换
      await this.toggleAutoSwitchWallpaper(false)
      url = url || this.settingData.webWallpaperUrl
      if (!url) {
        return {
          success: false,
          msg: t('messages.urlEmpty')
        }
      }
      try {
        const urlObj = new URL(url)
        if (!['http:', 'https:'].includes(urlObj.protocol)) {
          return {
            success: false,
            msg: t('messages.invalidUrl')
          }
        }
      } catch (err) {
        return {
          success: false,
          msg: t('messages.invalidUrl')
        }
      }
      const imgPath = await this.getWebImage(url)
      if (imgPath) {
        return await this.wallpaperManager.setStaticWallpaper(imgPath)
      } else {
        return {
          success: false,
          msg: t('messages.operationFail')
        }
      }
    } catch (err) {
      global.logger.error(err)
      return {
        success: false,
        msg: t('messages.operationFail')
      }
    }
  }

  // 启停定时切换壁纸
  async toggleAutoSwitchWallpaper(val) {
    const newValue = val === undefined ? !this.settingData.autoSwitchWallpaper : val
    await this.updateSettingData({
      autoSwitchWallpaper: newValue
    })
    // 更新当前电源状态记录
    if (!this.powerState.isSystemIdle) {
      this.powerState.wasAutoSwitchEnabled = newValue
    }
  }

  // 切换悬浮球可见性
  async toggleSuspensionBallVisible(val) {
    this.updateSettingData({
      suspensionBallVisible: val == undefined ? !this.settingData.suspensionBallVisible : val
    })
  }

  // 定时刷新目录
  intervalRefreshDirectory() {
    const key = 'autoRefreshDirectory'
    // 取消之前的定时任务
    this.taskScheduler.clearTask(key)

    // 如果开启了自动刷新目录
    if (this.settingData[key]) {
      // 设置定时刷新目录
      this.taskScheduler.scheduleTask(key, this.handleInterval(key), () => {
        this.fileManager.refreshDirectory(this.locks)
      })
    }
  }

  // 定时下载壁纸
  intervalDownload() {
    const key = 'autoDownload'
    // 取消之前的定时任务
    this.taskScheduler.clearTask(key)
    // 重置下载壁纸参数
    this.wallpaperManager.resetParams(key)
    // 如果开启了自动下载壁纸
    if (this.settingData[key]) {
      // 设置定时下载壁纸
      this.taskScheduler.scheduleTask(key, this.handleInterval(key), async () => {
        await this.wallpaperManager.downloadWallpaper()
      })
    }
  }

  // 定时清理下载资源
  intervalClearDownloaded() {
    const key = 'autoClearDownloaded'
    // 取消之前的定时任务
    this.taskScheduler.clearTask(key)

    // 如果开启了自动清理下载资源
    if (this.settingData[key]) {
      // 设置定时清理过期的下载的壁纸,每小时执行一次
      this.taskScheduler.scheduleTask(key, 60 * 60 * 1000, async () => {
        await this.wallpaperManager.clearDownloadedExpired()
      })
    }
  }

  // 处理延时时间,单位毫秒
  handleInterval(key, defaultMsVal = 15 * 60 * 1000) {
    const {
      switchIntervalUnit,
      switchIntervalTime,
      refreshDirectoryIntervalUnit,
      refreshDirectoryIntervalTime,
      downloadIntervalUnit,
      downloadIntervalTime,
      clearDownloadedExpiredTime,
      clearDownloadedExpiredUnit
    } = this.settingData

    const data = {
      autoSwitchWallpaper: {
        unit: switchIntervalUnit,
        intervalTime: switchIntervalTime
      },
      autoRefreshDirectory: {
        unit: refreshDirectoryIntervalUnit,
        intervalTime: refreshDirectoryIntervalTime
      },
      autoDownload: {
        unit: downloadIntervalUnit,
        intervalTime: downloadIntervalTime
      },
      autoClearDownloaded: {
        unit: clearDownloadedExpiredUnit,
        intervalTime: clearDownloadedExpiredTime
      }
    }

    const { unit, intervalTime } = data[key] || {}

    return handleTimeByUnit(intervalTime, unit) || defaultMsVal
  }

  // 关闭应用前清理
  cleanup() {
    try {
      // 取消所有定时任务
      this.taskScheduler?.clearAllTasks()

      // 停止文件服务子进程
      if (this.fileServer) {
        try {
          this.fileServer.stop()
          this.fileServer = null
        } catch (err) {
          global.logger.error(`停止文件服务子进程失败: ${err}`)
        }
      }

      // 停止H5服务子进程
      if (this.h5Server) {
        try {
          this.h5Server.stop()
          this.h5Server = null
          this.h5ServerUrl = null
        } catch (err) {
          global.logger.error(`停止H5服务子进程失败: ${err}`)
        }
      }

      // 关闭数据库连接
      if (this.db) {
        try {
          this.db.close()
          this.db = null
        } catch (err) {
          global.logger.error(`关闭数据库连接失败: ${err}`)
        }
      }

      // 清理其他资源
      this.mainWindow = null
      this.suspensionBall = null

      // 移除电源监听器
      if (powerMonitor.removeAllListeners) {
        powerMonitor.removeAllListeners('suspend')
        powerMonitor.removeAllListeners('resume')
        powerMonitor.removeAllListeners('lock-screen')
        powerMonitor.removeAllListeners('unlock-screen')
      }

      global.logger.info('应用资源已清理完毕')
    } catch (err) {
      global.logger.error(`清理资源失败: ${err}`)
    } finally {
      // 确保这些引用被清空
      this.mainWindow = null
      this.suspensionBall = null
      this.db = null
    }
  }
}

数据库管理类

建表语句

// main/store/sql.mjs

// 创建表
export const createTables = [
  // 数据表: fbw_sys 用于存储系统数据
  `CREATE TABLE IF NOT EXISTS fbw_sys (
    storeKey TEXT PRIMARY KEY, -- 存储Key
    storeData TEXT NOT NULL, -- 存储数据
    storeType TEXT NOT NULL DEFAULT 'string', -- 数据类型
    created_at DATETIME DEFAULT (datetime('now', 'localtime')), -- 记录创建时间
    updated_at DATETIME DEFAULT (datetime('now', 'localtime')), -- 记录修改时间
    UNIQUE (storeKey) -- 唯一键
  )`,
  // 数据表: fbw_favorites 用于存储收藏夹数据
  `CREATE TABLE IF NOT EXISTS fbw_favorites (
    id INTEGER PRIMARY KEY AUTOINCREMENT, -- 收藏记录自增ID
    resourceId INTEGER NOT NULL, -- 资源记录ID
    num INTEGER NOT NULL DEFAULT 1, -- 收藏次数
    created_at DATETIME DEFAULT (datetime('now', 'localtime')), -- 记录创建时间
    updated_at DATETIME DEFAULT (datetime('now', 'localtime')), -- 记录修改时间
    UNIQUE (resourceId) -- 唯一键
  )`,
  // 数据表: fbw_history 用于存储已设置的壁纸记录数据
  `CREATE TABLE IF NOT EXISTS fbw_history (
    id INTEGER PRIMARY KEY AUTOINCREMENT, -- 壁纸记录自增ID
    resourceId INTEGER NOT NULL, -- 资源记录ID
    created_at DATETIME DEFAULT (datetime('now', 'localtime')), -- 记录创建时间
    updated_at DATETIME DEFAULT (datetime('now', 'localtime')), -- 记录修改时间
    UNIQUE (id) -- 唯一键
  )`,
  // 数据表: fbw_privacy_space 用于存储隐私空间数据
  `CREATE TABLE IF NOT EXISTS fbw_privacy_space (
    id INTEGER PRIMARY KEY AUTOINCREMENT, -- 隐私空间记录自增ID
    resourceId INTEGER NOT NULL, -- 资源记录ID
    created_at DATETIME DEFAULT (datetime('now', 'localtime')), -- 记录创建时间
    updated_at DATETIME DEFAULT (datetime('now', 'localtime')), -- 记录修改时间
    UNIQUE (resourceId) -- 唯一键
  )`,
  // 数据表: resources 用于存储图片资源数据
  `CREATE TABLE IF NOT EXISTS fbw_resources (
    id INTEGER PRIMARY KEY AUTOINCREMENT, -- 资源记录自增ID
    resourceName TEXT NOT NULL DEFAULT '', -- 资源名称
    fileName TEXT NOT NULL DEFAULT '', -- 文件名
    filePath TEXT NOT NULL DEFAULT '', -- 文件路径
    fileExt TEXT NOT NULL DEFAULT '', -- 文件扩展名
    fileSize INTEGER NOT NULL DEFAULT 0, -- 文件大小
    url TEXT NOT NULl DEFAULT '', -- 远程资源网址
    author TEXT NOT NULL DEFAULT '', -- 作者
    link TEXT NOT NULL DEFAULT '', -- 页面链接
    title TEXT NOT NULL DEFAULT '', -- 标题
    desc TEXT NOT NULL DEFAULT '', -- 描述
    quality TEXT NOT NULL DEFAULT '', -- 图片质量
    width INTEGER NOT NULL DEFAULT 0, -- 图片宽度
    height INTEGER NOT NULL DEFAULT 0, -- 图片高度
    isLandscape INTEGER NOT NULL DEFAULT -1, -- 是否为横屏
    dominantColor TEXT NOT NULL DEFAULT '', -- 主色调
    atimeMs INTEGER NOT NULL DEFAULT 0, -- 本地文件最后访问时间
    mtimeMs INTEGER NOT NULL DEFAULT 0, -- 本地文件最后修改时间
    ctimeMs INTEGER NOT NULL DEFAULT 0, -- 本地文件创建时间
    created_at DATETIME DEFAULT (datetime('now', 'localtime')), -- 记录创建时间
    updated_at DATETIME DEFAULT (datetime('now', 'localtime')), -- 记录修改时间
    UNIQUE (filePath) -- 唯一键
  )`,
  // 数据表:资源分词关联表
  `CREATE TABLE IF NOT EXISTS fbw_resource_words (
    id INTEGER PRIMARY KEY AUTOINCREMENT, -- 记录自增ID
    resourceId INTEGER NOT NULL, -- 资源ID
    wordId INTEGER NOT NULL, -- 分词ID
    created_at DATETIME DEFAULT (datetime('now', 'localtime')), -- 记录创建时间
    UNIQUE (resourceId, wordId) -- 确保资源和分词的组合唯一
  )`,
  // 数据表:分词数据
  `CREATE TABLE IF NOT EXISTS fbw_words (
    id INTEGER PRIMARY KEY AUTOINCREMENT, -- 分词记录自增ID
    word TEXT NOT NULL DEFAULT '', -- 分词
    count INTEGER  NOT NULL DEFAULT 0, -- 计数
    type INTEGER  NOT NULL DEFAULT 0, -- 类型:中文、英文
    created_at DATETIME DEFAULT (datetime('now', 'localtime')), -- 记录创建时间
    updated_at DATETIME DEFAULT (datetime('now', 'localtime')), -- 记录修改时间
    UNIQUE (word) -- 唯一键
  )`,
  // 系统表:版本管理
  `CREATE TABLE IF NOT EXISTS fbw_version (
    id INTEGER PRIMARY KEY AUTOINCREMENT, -- 记录自增ID
    version TEXT NOT NULL, -- 版本号
    created_at DATETIME DEFAULT (datetime('now', 'localtime')), -- 记录创建时间
    updated_at DATETIME DEFAULT (datetime('now', 'localtime')), -- 记录修改时间
    UNIQUE (version) -- 唯一键
  )`
]

// 创建索引
export const createIndexes = [
  // 基础单列索引 - 按使用频率排序
  'CREATE INDEX IF NOT EXISTS idx_resources_resourcename ON fbw_resources(resourceName)',
  'CREATE INDEX IF NOT EXISTS idx_resources_created_at ON fbw_resources(created_at)',
  'CREATE INDEX IF NOT EXISTS idx_resources_landscape ON fbw_resources(isLandscape)',
  'CREATE INDEX IF NOT EXISTS idx_resources_quality ON fbw_resources(quality)',
  'CREATE INDEX IF NOT EXISTS idx_resources_filePath ON fbw_resources(filePath)',
  'CREATE INDEX IF NOT EXISTS idx_resources_fileExt ON fbw_resources(fileExt)',

  // 收藏、历史和隐私空间的基础索引
  'CREATE INDEX IF NOT EXISTS idx_favorites_resourceid ON fbw_favorites(resourceId)',
  'CREATE INDEX IF NOT EXISTS idx_favorites_created_at ON fbw_favorites(created_at)',
  'CREATE INDEX IF NOT EXISTS idx_history_resourceid ON fbw_history(resourceId)',
  'CREATE INDEX IF NOT EXISTS idx_history_created_at ON fbw_history(created_at)',
  'CREATE INDEX IF NOT EXISTS idx_privacy_space_resourceid ON fbw_privacy_space(resourceId)',
  'CREATE INDEX IF NOT EXISTS idx_privacy_space_created_at ON fbw_privacy_space(created_at)',

  // 分词表索引
  'CREATE INDEX IF NOT EXISTS idx_words_type_count ON fbw_words(type, count)',

  // 复合索引
  'CREATE INDEX IF NOT EXISTS idx_resources_name_created ON fbw_resources(resourceName, created_at)',
  // 分词相关索引
  'CREATE INDEX IF NOT EXISTS idx_resource_words_resourceid ON fbw_resource_words(resourceId)',
  'CREATE INDEX IF NOT EXISTS idx_resource_words_wordid ON fbw_resource_words(wordId)'
]

DatabaseManager主要功能:

  • 初始化数据库
  • 清除表数据
  • 系统表操作
// main/store/DatabaseManager.mjs

import Database from 'better-sqlite3'
import { t } from '../../i18n/server.js'
import { defaultSettingData, defaultResourceMap } from '../../common/publicData.js'
import { createTables, createIndexes } from './sql.mjs'

// 删除指定表
const dropTables = []

export default class DatabaseManager {
  // 单例实例
  static _instance = null

  // 获取单例实例
  static getInstance(logger) {
    if (!DatabaseManager._instance) {
      DatabaseManager._instance = new DatabaseManager(logger)
    }
    return DatabaseManager._instance
  }

  constructor(logger) {
    // 防止直接实例化
    if (DatabaseManager._instance) {
      return DatabaseManager._instance
    }

    this.logger = logger
    this.db = null
    this._initialized = false
    this._initPromise = this._init()

    // 保存实例
    DatabaseManager._instance = this
  }

  async _init() {
    try {
      this.db = new Database(process.env.FBW_DATABASE_FILE_PATH)
      this.db.pragma('journal_mode = WAL')
      this.db.pragma('busy_timeout = 5000')

      // 删除指定表
      if (Array.isArray(dropTables) && dropTables.length) {
        dropTables.forEach((tableName) => {
          try {
            this.db.exec(`DROP TABLE IF EXISTS ${tableName}`)
            this.logger.info(`表 ${tableName} 已删除`)
          } catch (err) {
            this.logger.error(`删除表 ${tableName} 失败: ${err}`)
          }
        })
      }

      // 创建表
      if (Array.isArray(createTables) && createTables.length) {
        createTables.forEach((createTableSql) => {
          try {
            this.db.exec(createTableSql)
          } catch (err) {
            this.logger.error(`创建表失败: ${err}`)
          }
        })
      }

      if (Array.isArray(createIndexes) && createIndexes.length) {
        createIndexes.forEach((indexSql) => {
          try {
            this.db.exec(indexSql)
          } catch (err) {
            this.logger.error(`创建索引失败: ${err}`)
          }
        })
      }

      // 初始化设置数据
      const res = await this.getSysRecord('settingData')
      if (res.success && res.data?.storeData) {
        const settingData = Object.assign({}, defaultSettingData, res.data.storeData)
        this.setSysRecord('settingData', settingData, 'object')
      } else {
        this.setSysRecord('settingData', defaultSettingData, 'object')
      }
      this._initialized = true
      this.logger.info('数据库初始化完成')
      return true
    } catch (err) {
      console.error(`创建数据库失败: ${err}`)
      this.logger.error(`创建数据库失败: ${err}`)
      throw err
    }
  }

  // 等待初始化完成的方法
  async waitForInitialization() {
    if (this._initialized) {
      return true
    }
    return this._initPromise
  }

  // 清空指定DB
  async clearDB(tableName, resourceName) {
    let ret = {
      success: false,
      msg: t('messages.operationFail')
    }
    tableName = tableName.startsWith('fbw_') ? tableName : `fbw_${tableName}`

    try {
      // 开始事务
      this.db.exec('BEGIN TRANSACTION')

      let delete_res
      // 检查是否为有效的表名
      if (tableName === 'fbw_resources') {
        if (resourceName) {
          // 获取要删除的资源ID
          const get_ids_stmt = this.db.prepare(`SELECT id FROM ${tableName} WHERE resourceName = ?`)
          const resources = get_ids_stmt.all(resourceName)

          if (resources && resources.length > 0) {
            const resourceIds = resources.map((r) => r.id)

            // 清除关联表中的数据
            for (const id of resourceIds) {
              // 清除收藏表中的关联数据
              const delete_favorites_stmt = this.db.prepare(
                `DELETE FROM fbw_favorites WHERE resourceId = ?`
              )
              delete_favorites_stmt.run(id)

              // 清除历史表中的关联数据
              const delete_history_stmt = this.db.prepare(
                `DELETE FROM fbw_history WHERE resourceId = ?`
              )
              delete_history_stmt.run(id)

              // 清除隐私空间表中的关联数据
              const delete_privacy_stmt = this.db.prepare(
                `DELETE FROM fbw_privacy_space WHERE resourceId = ?`
              )
              delete_privacy_stmt.run(id)

              // 清除资源分词关联表中的数据
              const delete_resource_words_stmt = this.db.prepare(
                `DELETE FROM fbw_resource_words WHERE resourceId = ?`
              )
              delete_resource_words_stmt.run(id)
            }

            // 更新词库中的计数
            // 获取要删除的资源关联的词条ID
            const get_word_ids_stmt = this.db.prepare(`
              SELECT DISTINCT wordId FROM fbw_resource_words
              WHERE resourceId IN (${resourceIds.map(() => '?').join(',')})
            `)
            const wordIds = get_word_ids_stmt.all(...resourceIds).map((w) => w.wordId)

            if (wordIds.length > 0) {
              // 更新词条计数
              const update_word_count_stmt = this.db.prepare(`
                UPDATE fbw_words SET count = count - 1
                WHERE id IN (${wordIds.map(() => '?').join(',')})
              `)
              update_word_count_stmt.run(...wordIds)

              // 删除计数为0或小于0的词条
              this.db.prepare(`DELETE FROM fbw_words WHERE count <= 0`).run()
            }
          }

          // 清空资源表下指定资源
          const delete_stmt = this.db.prepare(`DELETE FROM ${tableName} WHERE resourceName = ?`)
          delete_res = delete_stmt.run(resourceName)
        } else {
          // 清空所有关联表
          this.db.prepare(`DELETE FROM fbw_favorites`).run()
          this.db.prepare(`DELETE FROM fbw_history`).run()
          this.db.prepare(`DELETE FROM fbw_privacy_space`).run()
          this.db.prepare(`DELETE FROM fbw_resource_words`).run()
          this.db.prepare(`DELETE FROM fbw_words`).run()

          // 清空资源表下所有资源
          const delete_stmt = this.db.prepare(`DELETE FROM ${tableName}`)
          delete_res = delete_stmt.run()
        }
      } else if (tableName === 'fbw_words') {
        // 清空资源分词关联表
        this.db.prepare(`DELETE FROM fbw_resource_words`).run()

        // 清空分词表
        const delete_stmt = this.db.prepare(`DELETE FROM ${tableName}`)
        delete_res = delete_stmt.run()
      } else if (
        ['fbw_favorites', 'fbw_history', 'fbw_privacy_space', 'fbw_resource_words'].includes(
          tableName
        )
      ) {
        // 清空指定表下所有记录
        const delete_stmt = this.db.prepare(`DELETE FROM ${tableName}`)
        delete_res = delete_stmt.run()
      }

      if (delete_res && delete_res.changes > 0) {
        // 更新sqlite_sequence
        const update_sequence_stmt = this.db.prepare(
          `UPDATE sqlite_sequence SET seq = 0 WHERE name = ?`
        )
        const update_sequence_res = update_sequence_stmt.run(tableName)

        // 如果清除了fbw_resources或fbw_words,也需要重置关联表的自增ID
        if (tableName === 'fbw_resources' && !resourceName) {
          // 只有在清空整个资源表时才重置关联表的自增ID
          this.db.prepare(`UPDATE sqlite_sequence SET seq = 0 WHERE name = 'fbw_favorites'`).run()
          this.db.prepare(`UPDATE sqlite_sequence SET seq = 0 WHERE name = 'fbw_history'`).run()
          this.db
            .prepare(`UPDATE sqlite_sequence SET seq = 0 WHERE name = 'fbw_privacy_space'`)
            .run()
          this.db
            .prepare(`UPDATE sqlite_sequence SET seq = 0 WHERE name = 'fbw_resource_words'`)
            .run()
        } else if (tableName === 'fbw_words') {
          this.db
            .prepare(`UPDATE sqlite_sequence SET seq = 0 WHERE name = 'fbw_resource_words'`)
            .run()
        }

        // 提交事务
        this.db.exec('COMMIT')

        if (update_sequence_res.changes > 0) {
          ret = {
            success: true,
            msg: t('messages.clearTableSuccess')
          }
        } else {
          ret = {
            success: false,
            msg: t('messages.clearTableAutoincrementFail')
          }
        }
      } else {
        ret = {
          success: false,
          msg: t('messages.clearTableFail')
        }
        this.logger.error(
          `CLEAR DB FAIL:: tableName => ${tableName} resourceName => ${resourceName}`
        )
      }
    } catch (err) {
      // 发生错误时回滚事务
      try {
        this.db.exec('ROLLBACK')
      } catch (rollbackErr) {
        this.logger.error(`回滚事务失败: ${rollbackErr}`)
      }

      this.logger.error(
        `CLEAR DB FAIL:: tableName => ${tableName} resourceName => ${resourceName}, error: ${err}`
      )
    }
    return ret
  }

  // 获取sys表数据
  async getSysRecord(storeKey) {
    let ret = {
      success: false,
      msg: t('messages.operationFail'),
      data: null
    }
    if (!storeKey) {
      return ret
    }
    try {
      const query_stmt = this.db.prepare(`SELECT * FROM fbw_sys WHERE storeKey = ?`)
      const query_res = query_stmt.get(storeKey)
      if (query_res) {
        let storeData
        let storeType = query_res.storeType
        if (['array', 'object'].includes(storeType)) {
          storeData = JSON.parse(query_res.storeData)
        } else {
          storeData = query_res.storeData
        }
        ret.success = true
        ret.msg = t('messages.operationSuccess')
        ret.data = {
          storeKey,
          storeData,
          storeType
        }
      }
    } catch (err) {
      this.logger.error(`获取设置数据失败: ${err.message}`)
    }
    return ret
  }
  // 获取隐私密码
  async getPrivacyPassword() {
    let ret = {
      success: false,
      msg: t('messages.operationFail'),
      data: null
    }
    try {
      const query_stmt = this.db.prepare(`SELECT * FROM fbw_sys WHERE storeKey = ?`)
      const query_res = query_stmt.get('privacyPassword')
      ret.success = true
      ret.msg = t('messages.operationSuccess')
      ret.data =
        typeof query_res?.storeData === 'string'
          ? JSON.parse(query_res?.storeData)
          : query_res?.storeData
    } catch (err) {
      this.logger.error(`获取隐私密码失败: ${err}`)
    }
    return ret
  }

  // 获取resourceMap数据
  async getResourceMap() {
    let ret = {
      success: false,
      msg: t('messages.operationFail'),
      // FIXME 此次专门设置默认值
      data: JSON.parse(JSON.stringify(defaultResourceMap))
    }
    try {
      const res = await this.getSysRecord('resourceMap')
      if (res.success) {
        ret.success = true
        ret.msg = t('messages.operationSuccess')
        ret.data = res.data.storeData || JSON.parse(JSON.stringify(defaultResourceMap))
      }
    } catch (err) {
      this.logger.error(`获取resourceMap信息失败: ${err}`)
    }
    return ret
  }

  // 设置sys表数据
  async setSysRecord(storeKey, storeData, storeType = 'string') {
    let ret = {
      success: false,
      msg: t('messages.operationFail'),
      data: null
    }
    if (!storeKey || !storeData) {
      return ret
    }
    let storeDataStr
    if (['array', 'object'].includes(storeType)) {
      storeDataStr = JSON.stringify(storeData)
    } else {
      storeDataStr = storeData.toString()
    }

    // 更新或插入数据到sys表
    const update_stmt = this.db.prepare(
      `INSERT OR REPLACE INTO fbw_sys (storeKey, storeData, storeType) VALUES (@storeKey, @storeData, @storeType)`
    )
    const update_result = update_stmt.run({
      storeKey,
      storeData: storeDataStr,
      storeType
    })
    if (update_result.changes > 0) {
      ret = {
        success: true,
        msg: t('messages.operationSuccess'),
        data: {
          storeKey,
          storeData,
          storeType
        }
      }
    } else {
      this.logger.error(`更新失败: tableName => fbw_sys, storeKey => ${storeKey}`)
    }

    return ret
  }
}

文件管理类

FileManager主要功能:

  • 目录扫描
  • 图片质量处理
  • 文件删除
// main/store/FileManager.mjs

import fs from 'fs'
import { t } from '../../i18n/server.js'

export default class FileManager {
  // 单例实例
  static _instance = null

  // 获取单例实例
  static getInstance(logger, dbManager, settingManager, fileServer, wordsManager) {
    if (!FileManager._instance) {
      FileManager._instance = new FileManager(
        logger,
        dbManager,
        settingManager,
        fileServer,
        wordsManager
      )
    }
    return FileManager._instance
  }

  constructor(logger, dbManager, settingManager, fileServer, wordsManager) {
    // 防止直接实例化
    if (FileManager._instance) {
      return FileManager._instance
    }

    this.logger = logger
    this.dbManager = dbManager
    this.db = dbManager.db
    this.settingManager = settingManager
    this.fileServer = fileServer
    this.wordsManager = wordsManager

    this.resetParams()

    // 预编译SQL语句
    if (this.db) {
      this.preparedStatements = {
        insertResource: this.db.prepare(
          `INSERT OR IGNORE INTO fbw_resources
            (resourceName, fileName, filePath, fileExt, fileSize, atimeMs, mtimeMs, ctimeMs) VALUES
            (@resourceName, @fileName, @filePath, @fileExt, @fileSize, @atimeMs, @mtimeMs, @ctimeMs)`
        )
      }
    }

    FileManager._instance = this
  }

  // 使用 settingManager 获取设置
  get settingData() {
    return this.settingManager.settingData
  }

  resetParams() {
    this.params = {
      handleQuality: {
        startPage: 1,
        pageSize: 20
      }
    }
  }

  /**
   * 刷新目录
   * @param {Object} locks - 锁对象
   * @param {boolean} isManual - 是否手动刷新
   */
  refreshDirectory(locks, isManual = false) {
    if (!this.fileServer) {
      this.logger.error('文件服务未初始化')
      return {
        success: false,
        msg: t('messages.fileServerNotInitialized')
      }
    }
    if (locks.refreshDirectory) {
      return {
        success: false,
        msg: t('messages.refreshDirectoryFail')
      }
    }

    const { localResourceFolders, allowedFileExt } = this.settingData
    // FIXME 只处理本地资源
    const resourceName = 'local'
    // 将目录处理成数组,去重,并过滤空路径,再按路径长度降序排列
    const folderPaths = (
      Array.isArray(localResourceFolders) ? [...new Set(localResourceFolders)] : []
    )
      .filter((folderPath) => !!folderPath && fs.existsSync(folderPath))
      .sort((a, b) => b.length - a.length)
    if (!folderPaths.length) {
      return {
        success: false,
        msg: t('messages.refreshDirectoryFailNotSettingFolder')
      }
    }
    if (!allowedFileExt.length) {
      return {
        success: false,
        msg: t('messages.refreshDirectoryFailNotSettingFileExt')
      }
    }

    locks.refreshDirectory = true

    // 获取数据库中已有的文件信息
    try {
      // 查询所有本地资源的文件路径和修改时间
      const query_stmt = this.db.prepare(
        `SELECT id, filePath, mtimeMs FROM fbw_resources WHERE resourceName = ?`
      )
      const existingFiles = query_stmt.all(resourceName)

      // 发送消息到文件服务子进程,包含现有文件信息
      this.fileServer?.postMessage({
        event: 'REFRESH_DIRECTORY',
        isManual,
        allowedFileExt,
        resourceName,
        folderPaths,
        existingFiles, // 传递现有文件信息
        refreshDirStartTime: Date.now()
      })
    } catch (err) {
      this.logger.error(`获取现有文件信息失败: ${err}`)
      locks.refreshDirectory = false
      return {
        success: false,
        msg: t('messages.refreshDirectoryFail')
      }
    }
  }

  /**
   * 处理目录数据
   * @param {Object} data - 目录数据
   * @returns {Object} 处理结果
   */
  processDirectoryData(data) {
    const { list } = data

    const ret = {
      success: true,
      msg: t('messages.refreshDirectorySuccess', {
        insertedCount: 0,
        total: 0
      }),
      data: {
        insertedCount: 0,
        total: 0
      }
    }
    if (Array.isArray(list) && list.length) {
      try {
        // 使用预编译语句
        const insert_stmt =
          this.preparedStatements?.insertResource ||
          this.db.prepare(
            `INSERT OR IGNORE INTO fbw_resources
              (resourceName, fileName, filePath, fileExt, fileSize, atimeMs, mtimeMs, ctimeMs) VALUES
              (@resourceName, @fileName, @filePath, @fileExt, @fileSize, @atimeMs, @mtimeMs, @ctimeMs)`
          )

        // 记录插入成功的数量
        let insertedCount = 0

        const transaction = this.db.transaction((list) => {
          for (let i = 0; i < list.length; i++) {
            const insert_result = insert_stmt.run(list[i])
            if (insert_result.changes) {
              // 记录本次插入成功的数量
              insertedCount += insert_result.changes
            }
          }
        })

        // 尝试执行事务,批量插入资源
        transaction(
          list.map((item) => ({
            resourceName: item.resourceName,
            fileName: item.fileName,
            filePath: item.filePath,
            fileExt: item.fileExt,
            fileSize: item.fileSize,
            atimeMs: item.atimeMs,
            mtimeMs: item.mtimeMs,
            ctimeMs: item.ctimeMs
          }))
        )
        this.logger.info(
          `读取目录文件,批量插入资源事务执行成功: tableName => fbw_resources, list.length => ${list.length}`
        )
        ret.success = true
        ret.msg = t('messages.refreshDirectorySuccess', {
          insertedCount,
          total: list.length
        })
        ret.data = {
          insertedCount,
          total: list.length
        }
      } catch (err) {
        this.logger.error(
          `读取目录文件,批量插入资源事务执行失败: tableName => fbw_resources, error => ${err}`
        )
        ret.success = false
        ret.msg = t('messages.refreshDirectoryFail')
      }
    }
    return ret
  }

  /**
   * 定时处理图片质量
   * @param {Object} locks - 锁对象
   */
  // 优化图片质量处理,使用批处理
  intervalHandleQuality(locks) {
    if (locks.handleQuality) {
      return
    }

    locks.handleQuality = true

    const { startPage, pageSize } = this.params.handleQuality

    // 数据库查询批量大小,设置为 pageSize 的整数倍,提高效率
    const batchSize = pageSize * 2.5

    // 查询未处理质量的图片
    const query_stmt = this.db.prepare(
      `SELECT id, filePath FROM fbw_resources WHERE quality = '' LIMIT ? OFFSET ?`
    )
    const query_result = query_stmt.all(batchSize, (startPage - 1) * pageSize)

    if (Array.isArray(query_result) && query_result.length) {
      // 如果没有更多数据,重置处理质量逻辑参数
      if (query_result.length < batchSize) {
        this.resetParams()
      } else {
        this.params.handleQuality.startPage += 1
      }

      // 将大批量数据分成多个小批次处理,避免子进程负担过重
      const chunks = []
      for (let i = 0; i < query_result.length; i += pageSize) {
        chunks.push(query_result.slice(i, i + pageSize))
      }

      // 逐个处理小批次
      chunks.forEach((chunk, index) => {
        // 延迟发送,避免同时处理太多任务
        setTimeout(() => {
          this.fileServer?.postMessage({
            event: 'HANDLE_IMAGE_QUALITY',
            list: chunk
          })
        }, index * 500) // 每批次间隔500ms
      })
    } else {
      locks.handleQuality = false
      this.resetParams()
    }
  }

  /**
   * 处理图片质量完成
   * @param {Object} data - 处理结果
   * @param {Object} locks - 锁对象
   */
  onHandleImageQualitySuccess(data, locks) {
    const { list } = data

    if (!Array.isArray(list) || !list.length) {
      locks.handleQuality = false
      return
    }

    try {
      // 更新图片质量
      const update_stmt = this.db.prepare(
        `UPDATE fbw_resources SET quality = @quality, width = @width, height = @height, isLandscape = @isLandscape, dominantColor = @dominantColor WHERE id = @id`
      )

      const transaction = this.db.transaction(() => {
        for (const item of list) {
          update_stmt.run(item)
        }
      })

      // 执行事务
      transaction()
      this.logger.info(`处理图片质量成功: count => ${list.length}`)
    } catch (err) {
      this.logger.error(`处理图片质量失败: error => ${err}`)
    } finally {
      // 清除锁
      locks.handleQuality = false
    }
  }

  /**
   * 处理图片质量失败
   * @param {Object} locks - 锁对象
   */
  onHandleImageQualityFail(locks) {
    locks.handleQuality = false
    this.logger.error(`处理图片质量失败`)
  }

  /**
   * 删除文件
   * @param {Object} item - 资源项
   * @returns {Object} 删除结果
   */
  // 优化错误处理,添加重试机制
  async deleteFile(item) {
    let ret = {
      success: false,
      msg: t('messages.operationFail')
    }
    if (!item) {
      return ret
    }

    const id = item.id
    let filePath = item.filePath

    let retryCount = 0
    const maxRetries = 3

    while (retryCount < maxRetries) {
      try {
        if (!filePath) {
          // 查询文件信息
          const query_stmt = this.db.prepare(`SELECT * FROM fbw_resources WHERE id =?`)
          const query_result = query_stmt.get(id)
          if (!query_result) {
            ret.msg = t('messages.resourceNotExist')
            return ret
          }
          filePath = query_result.filePath
        }
        // 删除文件
        if (filePath && fs.existsSync(filePath)) {
          fs.unlinkSync(filePath)
        } else {
          ret.msg = t('messages.fileNotExist')
          return ret
        }

        // 使用事务删除数据库记录
        this.db.exec('BEGIN TRANSACTION')

        // 删除数据库记录
        const delete_stmt = this.db.prepare(`DELETE FROM fbw_resources WHERE id = ?`)
        delete_stmt.run(id)

        // 删除关联记录
        const delete_favorites_stmt = this.db.prepare(
          `DELETE FROM fbw_favorites WHERE resourceId = ?`
        )
        delete_favorites_stmt.run(id)

        const delete_history_stmt = this.db.prepare(`DELETE FROM fbw_history WHERE resourceId = ?`)
        delete_history_stmt.run(id)

        const delete_privacy_stmt = this.db.prepare(
          `DELETE FROM fbw_privacy_space WHERE resourceId = ?`
        )
        delete_privacy_stmt.run(id)

        // 提交事务
        this.db.exec('COMMIT')

        // 处理删除资源的分词
        this.wordsManager?.handleDeletedResource(item)

        ret = {
          success: true,
          msg: t('messages.operationSuccess')
        }
        return ret
      } catch (err) {
        // 回滚事务
        try {
          this.db.exec('ROLLBACK')
        } catch (rollbackErr) {
          // 忽略回滚错误
          this.logger.error(`删除文件失败,回滚事务失败: ${rollbackErr}`)
        }

        retryCount++
        if (retryCount >= maxRetries) {
          this.logger.error(`删除文件失败: ${err}`)
          return ret
        }
        // 等待一段时间后重试
        await new Promise((resolve) => setTimeout(resolve, 100))
      }
    }
    return ret
  }
}

资源接口管理类

ApiManager主要功能:

  • 加载资源API,便于扩展自定义接口,通过扫描指定目录解析API并入库
  • 调用资源API查询数据
// main/store/ApiManager.mjs

import fs from 'fs'
import path from 'path'
import ApiBase from '../ApiBase.js'
import { commonResourceMap, defaultResourceMap } from '../../common/publicData.js'

export default class ApiManager {
  // 单例实例
  static _instance = null

  // 获取单例实例
  static getInstance(logger, dbManager) {
    if (!ApiManager._instance) {
      ApiManager._instance = new ApiManager(logger, dbManager)
    }
    return ApiManager._instance
  }

  constructor(logger, dbManager) {
    // 防止直接实例化
    if (ApiManager._instance) {
      return ApiManager._instance
    }

    this.logger = logger
    this.dbManager = dbManager
    // API插件列表
    this.apiMap = {}

    // API目录 - 内置API
    this.sysApiDir = path.join(process.env.FBW_RESOURCES_PATH, 'api')
    // API目录 - 用户API
    this.userApiDir = path.join(process.env.FBW_PLUGINS_PATH, 'api')
    // 确保用户API目录存在
    if (!fs.existsSync(this.userApiDir)) {
      fs.mkdirSync(this.userApiDir, { recursive: true })
    }

    this._initialized = false
    this._initPromise = this._init()

    ApiManager._instance = this
  }

  async _init() {
    try {
      await this.loadApi()
      this._initialized = true
      return true
    } catch (err) {
      this.logger.error(`初始化API插件失败: ${err}`)
      return false
    }
  }

  // 等待初始化完成的方法
  async waitForInitialization() {
    if (this._initialized) {
      return true
    }
    return this._initPromise
  }

  // 加载API
  async loadApi() {
    const apiMap = {}
    const resourceMap = JSON.parse(JSON.stringify(defaultResourceMap))

    // 加载内置API
    await this.loadApiFromDir(this.sysApiDir, apiMap)

    // 加载用户API
    await this.loadApiFromDir(this.userApiDir, apiMap)

    this.apiMap = apiMap

    const count = Object.keys(apiMap).length
    if (count === 0) {
      this.logger.error(`未加载任何API插件`)
      return
    } else {
      this.logger.info(`成功加载 ${count} 个API插件`)
    }
    // 处理API信息,写入数据库
    for (const [resourceName, api] of Object.entries(apiMap)) {
      const apiInfo = api.info()
      resourceMap.remoteResourceMap[resourceName] = apiInfo
    }
    // 计算各种资源数据
    // 启用的远程资源
    const enabledRemoteResourceList = Object.values(resourceMap.remoteResourceMap).filter(
      (item) => item.enabled
    )
    // 支持搜索的远程资源
    const supportSearchRemoteResourceList = enabledRemoteResourceList.filter(
      (item) => item.supportSearch
    )
    // 支持下载的远程资源
    resourceMap.supportDownloadRemoteResourceList = enabledRemoteResourceList.filter(
      (item) => item.supportDownload
    )
    // 需要密钥的远程资源
    resourceMap.remoteResourceKeyNames = enabledRemoteResourceList
      .filter((item) => item.requireSecretKey)
      .map((item) => item.value)

    // 支持搜索的本地资源
    const supportSearchLocalResourceList = [
      commonResourceMap.resources,
      commonResourceMap.local,
      ...resourceMap.supportDownloadRemoteResourceList
    ].filter((item) => item.enabled)
    // 资源列表按资源类型分类
    resourceMap.resourceListByResourceType = {
      localResource: supportSearchLocalResourceList,
      remoteResource: supportSearchRemoteResourceList
    }
    // 壁纸资源列表
    resourceMap.wallpaperResourceList = [
      commonResourceMap.resources,
      commonResourceMap.local,
      commonResourceMap.favorites,
      ...supportSearchRemoteResourceList
    ].filter((item) => item.enabled)

    // 直接将整个resourceMap写入数据库
    const res = await this.dbManager.setSysRecord('resourceMap', resourceMap, 'object')
    if (res.success) {
      this.logger.info('写入resourceMap信息成功')
    } else {
      this.logger.error(`写入resourceMap信息失败: ${res.message}`)
    }
  }

  // 从指定目录加载插件
  async loadApiFromDir(dir, plugins) {
    this.logger.info(`开始加载API插件,目录: ${dir}`)
    if (!fs.existsSync(dir)) return

    const files = fs.readdirSync(dir)

    // 使用Promise.all等待所有插件加载完成
    await Promise.all(
      files.map(async (file) => {
        if (file.endsWith('.js') || file.endsWith('.mjs')) {
          try {
            const pluginPath = path.join(dir, file)

            // 使用await直接等待插件加载
            const module = await import(/* @vite-ignore */ `file://${pluginPath}`)
            const PluginClass = module.default

            // 检查是否是有效的插件类
            if (PluginClass && PluginClass.prototype instanceof ApiBase) {
              const pluginInstance = new PluginClass()
              const resourceName = pluginInstance.resourceName

              if (resourceName) {
                // 如果已存在同名插件,用户插件优先
                if (plugins[resourceName]) {
                  this.logger.warn(`发现重复的API插件: ${resourceName},将使用新加载的版本`)
                }

                plugins[resourceName] = pluginInstance
                this.logger.info(`成功加载API插件: ${resourceName}`)
              } else {
                this.logger.warn(`插件 ${file} 未提供resourceName,已跳过`)
              }
            } else {
              this.logger.warn(`${file} 不是有效的API插件类,已跳过`)
            }
          } catch (err) {
            this.logger.error(`加载插件 ${file} 失败: ${err}`)
          }
        }
      })
    )
  }

  // 获取API列表
  async getApiList() {
    let ret = {
      success: false,
      message: '',
      data: []
    }
    const res = await this.dbManager.getSysRecord('api')
    if (res.success && res.data?.storeData) {
      ret.success = true
      ret.data = res.data.storeData
    } else {
      ret.message = res.message
    }
    return ret
  }

  // 调用API
  async call(resourceName, funcName, params) {
    const api = this.apiMap[resourceName]
    if (!api) {
      throw new Error(`未找到API插件: ${resourceName}`)
    }
    if (!api[funcName] || typeof api[funcName] !== 'function') {
      throw new Error(`API插件 ${resourceName} 未提供函数: ${funcName}`)
    }
    return await api[funcName](params)
  }
}


资源管理类

ResourcesManager主要功能:

  • 图片搜索,支持入库数据查询、远程资源API查询
  • 图片收藏,加入、更新、删除收藏记录
  • 支持APP\H5共用
// main/stoe/ResourceManager.mjs

import { v4 as uuidv4 } from 'uuid'
import { t } from '../../i18n/server.js'

export default class ResourcesManager {
  // 单例实例
  static _instance = null

  // 获取单例实例
  static getInstance(logger, dbManager, settingManager, apiManager) {
    if (!ResourcesManager._instance) {
      ResourcesManager._instance = new ResourcesManager(
        logger,
        dbManager,
        settingManager,
        apiManager
      )
    }
    return ResourcesManager._instance
  }

  constructor(logger, dbManager, settingManager, apiManager) {
    // 防止直接实例化
    if (ResourcesManager._instance) {
      return ResourcesManager._instance
    }

    this.logger = logger
    this.dbManager = dbManager
    this.db = dbManager.db
    this.settingManager = settingManager
    this.apiManager = apiManager

    ResourcesManager._instance = this
  }

  // 使用 settingManager 获取设置
  get settingData() {
    return this.settingManager.settingData
  }

  // 搜索
  async searchImages(params = {}) {
    const {
      resourceType = 'localResource',
      resourceName = 'resources',
      filterKeywords,
      quality: qualityStr,
      orientation: orientationStr,
      startPage,
      pageSize,
      isRandom = false,
      sortField = 'created_at',
      sortType = -1
    } = params

    let ret = {
      success: false,
      msg: t('messages.operationFail'),
      data: {
        list: [],
        total: 0,
        startPage,
        pageSize
      }
    }

    try {
      const { remoteResourceSecretKeys } = this.settingData

      const sortOrder = sortType > 0 ? 'ASC' : 'DESC'
      const orientation = orientationStr ? orientationStr.split(',') : []

      if (resourceType === 'localResource') {
        const isResources = resourceName === 'resources'
        const isFavorites = resourceName === 'favorites'
        const isPrivacySpace = resourceName === 'privacy_space'
        const isHistory = resourceName === 'history'

        const keywords = `%${filterKeywords}%`
        const quality = qualityStr ? qualityStr.split(',') : []

        let query_where_str = ''
        const query_where = []
        const query_params = []
        let query_sql
        let count_sql

        // 非历史记录、非隐私空间需排除隐私表中的数据
        if (!(isHistory || isPrivacySpace)) {
          // 添加 NOT EXISTS 条件排除隐私表中的数据
          query_where.push(
            'NOT EXISTS (SELECT 1 FROM fbw_privacy_space p WHERE p.resourceId = r.id)'
          )
        }

        // 筛选资源类型
        if (!isResources && !isFavorites && !isHistory && !isPrivacySpace) {
          query_where.push(`r.resourceName = ?`)
          query_params.push(resourceName)
        }

        if (filterKeywords) {
          query_where.push(`(r.filePath LIKE ? OR r.title LIKE ? OR r.desc LIKE ?)`)
          query_params.push(keywords, keywords, keywords)
        }

        if (orientation.length === 1) {
          query_where.push(`r.isLandscape = ?`)
          query_params.push(orientation[0])
        }

        if (quality.length) {
          const placeholders = quality.map(() => '?').join(',')
          query_where.push(`r.quality IN (${placeholders})`)
          query_params.push(...quality)
        }

        if (query_where.length) {
          query_where_str = `WHERE ${query_where.join(' AND ')}`
        }

        if (isFavorites || isHistory || isPrivacySpace) {
          // FIXME 按顺序查询时排序字段固定
          const order_by_str = isRandom ? 'ORDER BY RANDOM()' : `ORDER BY s.created_at ${sortOrder}`

          query_sql = `
            SELECT
            r.*,
              (SELECT COUNT(*) FROM fbw_favorites f WHERE f.resourceId = r.id) AS isFavorite
            FROM fbw_${resourceName} s
            JOIN fbw_resources r ON s.resourceId = r.id
            ${query_where_str}
            ${order_by_str}
            LIMIT ? OFFSET ?
          `
          count_sql = `SELECT COUNT(*) AS total FROM fbw_${resourceName} s JOIN fbw_resources r ON s.resourceId = r.id ${query_where_str}`
        } else {
          const order_by_str = isRandom
            ? 'ORDER BY RANDOM()'
            : `ORDER BY r.${sortField} ${sortOrder}`

          query_sql = `
            SELECT
            r.*,
            (SELECT COUNT(*) FROM fbw_favorites f WHERE f.resourceId = r.id) AS isFavorite
            FROM fbw_resources r
            ${query_where_str}
            ${order_by_str}
            LIMIT ? OFFSET ?
          `
          count_sql = `SELECT COUNT(*) AS total FROM fbw_resources r ${query_where_str}`
        }

        const query_stmt = this.db.prepare(query_sql)
        const query_result = query_stmt.all(...query_params, pageSize, (startPage - 1) * pageSize)
        if (Array.isArray(query_result) && query_result.length) {
          ret.data.list = query_result.map((item) => {
            return {
              ...item,
              srcType: 'file',
              uniqueKey: uuidv4()
            }
          })
          if (count_sql) {
            const count_stmt = this.db.prepare(count_sql)
            const count_result = count_stmt.get(...query_params)
            if (count_result && count_result.total) {
              ret.data.total = count_result.total
            }
          }
        }
        ret.success = true
        ret.msg = t(ret.data.list.length ? 'messages.querySuccess' : 'messages.queryEmpty')
      } else {
        if (!filterKeywords) {
          ret.msg = t('messages.enterKeywords')
          return ret
        }

        // 先获取资源数据
        const resourceMapRes = await this.dbManager.getResourceMap()
        if (!resourceMapRes.success) {
          ret.msg = resourceMapRes.msg
          return ret
        }
        const resourceMap = resourceMapRes.data
        if (
          resourceMap.remoteResourceKeyNames.includes(resourceName) &&
          !remoteResourceSecretKeys[resourceName]
        ) {
          ret.msg = t('messages.resourceSecretKeyUnset')
          return ret
        }
        const res = await this.apiManager.call(resourceName, 'search', {
          keywords: filterKeywords,
          orientation,
          startPage: startPage,
          pageSize,
          secretKey: resourceMap.remoteResourceKeyNames.includes(resourceName)
            ? remoteResourceSecretKeys[resourceName]
            : ''
        })
        if (res) {
          if (Array.isArray(res.list) && res.list.length) {
            ret.data.total = res.total
            ret.data.list = res.list.map((item) => {
              return {
                ...item,
                srcType: 'url',
                uniqueKey: uuidv4()
              }
            })
          }
          ret.success = true
          ret.msg = t(ret.data.list.length ? 'messages.querySuccess' : 'messages.queryEmpty')
        }
      }
    } catch (err) {
      this.logger.error(`搜索失败: error => ${err}`)
    }

    return ret
  }

  /**
   * 检查资源是否已收藏
   * @param {number|string} resourceId - 资源ID
   * @param {boolean} isPrivacySpace - 是否为隐私空间
   * @returns {boolean} - 是否已收藏
   */
  async checkFavorite(resourceId, isPrivacySpace = false) {
    try {
      const tableName = isPrivacySpace ? 'fbw_privacy_space' : 'fbw_favorites'
      const query_stmt = this.db.prepare(`SELECT * FROM ${tableName} WHERE resourceId = ?`)
      const result = query_stmt.get(resourceId)
      return !!result
    } catch (err) {
      this.logger.error(`检查收藏状态失败: ${err}`)
      return false
    }
  }

  // 加入收藏夹
  async addToFavorites(resourceId, isPrivacySpace = false) {
    let ret = {
      success: false,
      msg: t('messages.operationFail')
    }

    try {
      if (isPrivacySpace) {
        const insert_stmt = this.db.prepare(
          `INSERT OR IGNORE INTO fbw_privacy_space (resourceId) VALUES (?)`
        )
        const insert_result = insert_stmt.run(resourceId)

        if (insert_result.changes > 0) {
          ret = {
            success: true,
            msg: t('messages.operationSuccess')
          }
        }
      } else {
        // 检查资源是否已收藏,如果已收藏则num+1, 未收藏则插入
        const check_stmt = this.db.prepare(`SELECT * FROM fbw_favorites WHERE resourceId =?`)
        const check_result = check_stmt.get(resourceId)
        if (check_result) {
          // 更新收藏数量
          const update_stmt = this.db.prepare(
            `UPDATE fbw_favorites SET num = num + 1 WHERE resourceId =?`
          )
          const update_result = update_stmt.run(resourceId)
          if (update_result.changes > 0) {
            ret = {
              success: true,
              msg: t('messages.operationSuccess')
            }
          }
        } else {
          const insert_stmt = this.db.prepare(
            `INSERT INTO fbw_favorites (resourceId, num) VALUES (?, 1)`
          )
          const insert_result = insert_stmt.run(resourceId)
          if (insert_result.changes > 0) {
            ret = {
              success: true,
              msg: t('messages.operationSuccess')
            }
          }
        }
      }
    } catch (err) {
      this.logger.error(`加入收藏夹失败: ${err}`)
    }

    return ret
  }

  // 更新收藏数量
  async updateFavoriteCount(resourceId, count) {
    let ret = {
      success: false,
      msg: t('messages.operationFail')
    }
    try {
      if (!resourceId || count === undefined || count <= 0) {
        ret.msg = t('messages.paramsError')
        return ret
      }
      // 当没有资源时插入,当有资源时num加count
      const check_stmt = this.db.prepare(`SELECT * FROM fbw_favorites WHERE resourceId =?`)
      const check_result = check_stmt.get(resourceId)
      if (!check_result) {
        const insert_stmt = this.db.prepare(
          `INSERT INTO fbw_favorites (resourceId, num) VALUES (?, ?)`
        )
        const insert_result = insert_stmt.run(resourceId, count)
        if (insert_result.changes > 0) {
          ret = {
            success: true,
            msg: t('messages.operationSuccess')
          }
          return ret
        }
      } else {
        count += check_result.num
        if (count <= 0) {
          return ret
        }
        // 更新收藏数量
        const update_stmt = this.db.prepare(`UPDATE fbw_favorites SET num =? WHERE resourceId =?`)
        const update_result = update_stmt.run(count, resourceId)
        if (update_result.changes > 0) {
          ret = {
            success: true,
            msg: t('messages.operationSuccess')
          }
        }
      }
    } catch (err) {
      this.logger.error(`更新收藏数量失败: ${err}`)
    }
    return ret
  }

  // 移出收藏夹
  async removeFavorites(resourceId, isPrivacySpace = false) {
    let ret = {
      success: false,
      msg: t('messages.operationFail')
    }

    try {
      const tableName = isPrivacySpace ? 'fbw_privacy_space' : 'fbw_favorites'
      const delete_stmt = this.db.prepare(`DELETE FROM ${tableName} WHERE resourceId = ?`)
      const delete_result = delete_stmt.run(resourceId)

      if (delete_result.changes > 0) {
        ret = {
          success: true,
          msg: t('messages.operationSuccess')
        }
      }
    } catch (err) {
      this.logger.error(`移出收藏夹失败: ${err}`)
    }

    return ret
  }
}

设置数据管理类

SettingManager主要功能:

  • 设置数据查询、更新
  • 隐私密码更新
// main/store/SettingManager.mjs

import EventEmitter from 'events'
import { t, changeLanguage } from '../../i18n/server.js'
import { defaultSettingData } from '../../common/publicData.js'
import { generateSalt, hashPassword, verifyPassword } from '../utils/utils.mjs'

export default class SettingManager extends EventEmitter {
  // 单例实例
  static _instance = null

  // 获取单例实例
  static getInstance(logger, dbManager) {
    if (!SettingManager._instance) {
      SettingManager._instance = new SettingManager(logger, dbManager)
    }
    return SettingManager._instance
  }

  constructor(logger, dbManager) {
    super()

    // 防止直接实例化
    if (SettingManager._instance) {
      return SettingManager._instance
    }

    this.logger = logger
    this.dbManager = dbManager
    this._settingData = { ...defaultSettingData }
    this._initialized = false
    this._initPromise = this._init()

    SettingManager._instance = this
  }

  // 初始化方法
  async _init() {
    try {
      await this.getSettingData()
      this._initialized = true
      this.emit('SETTING_INITIALIZED', this._settingData)
      return true
    } catch (err) {
      this.logger.error(`设置管理器初始化失败: ${err.message}`)
      throw err
    }
  }

  // 等待初始化完成的方法
  async waitForInitialization() {
    if (this._initialized) {
      return this._settingData
    }
    return this._initPromise
  }

  // 获取设置数据
  get settingData() {
    return this._settingData
  }

  async getSettingData() {
    let ret = {
      success: false,
      msg: t('messages.operationFail'),
      data: null
    }
    try {
      const res = await this.dbManager.getSysRecord('settingData')
      if (res.success && res.data?.storeData) {
        ret.success = true
        ret.msg = t('messages.operationSuccess')
        ret.data = this._settingData = res.data.storeData
      } else {
        // 如果获取失败,使用默认设置
        this.logger.warn('从数据库获取设置失败,使用默认设置')
        ret.success = true
        ret.msg = t('messages.useDefaultSettings')
        ret.data = this._settingData = { ...defaultSettingData }
      }
    } catch (err) {
      this.logger.error(`获取设置数据失败: ${err.message}`)
      // 发生错误时也使用默认设置
      ret.success = true
      ret.msg = t('messages.useDefaultSettings')
      ret.data = this._settingData = { ...defaultSettingData }
    }
    return ret
  }

  // 合并更新设置数据
  async updateSettingData(data) {
    let ret = {
      success: false,
      msg: t('messages.operationFail'),
      data: null
    }

    const storeKey = 'settingData'

    if (!data) {
      return ret
    }

    try {
      const query_res = await this.dbManager.getSysRecord(storeKey)
      let storeData = { ...defaultSettingData }

      if (query_res.success && query_res.data?.storeData) {
        // 合并数据
        storeData = query_res.data.storeData
      } else {
        this.logger.warn(`未找到设置数据,将创建新的设置记录`)
      }

      const newStoreData = Object.assign({}, storeData, data)
      // 更新或插入数据到sys表
      const update_res = await this.dbManager.setSysRecord(storeKey, newStoreData, 'object')
      if (update_res.success) {
        this._settingData = newStoreData
        // 更新语言
        await changeLanguage(newStoreData.locale)
        ret = {
          success: true,
          msg: t('messages.operationSuccess'),
          data: newStoreData
        }
      } else {
        this.logger.error(`更新设置数据失败:updateData => ${JSON.stringify(data)}`)
      }
    } catch (err) {
      this.logger.error(
        `更新设置数据失败:updateData => ${JSON.stringify(data)}, error: ${err.message}`
      )
    }

    return ret
  }

  // 验证隐私空间密码
  async checkPrivacyPassword(password) {
    let ret = {
      success: false,
      msg: t('messages.operationFail')
    }
    const res = await this.dbManager.getPrivacyPassword()
    if (res.success) {
      if (!res.data) {
        ret = {
          success: false,
          msg: t('messages.privacyPasswordNotSet')
        }
      } else {
        const { hash: storedHash, salt } = res.data
        if (verifyPassword(password, storedHash, salt)) {
          ret = {
            success: true,
            msg: t('messages.operationSuccess')
          }
        } else {
          ret = {
            success: false,
            msg: t('messages.passwordError')
          }
        }
      }
    }
    return ret
  }

  // 检查是否设置了隐私密码
  async hasPrivacyPassword() {
    let ret = {
      success: false,
      msg: t('messages.operationFail'),
      data: false
    }
    const res = await this.dbManager.getPrivacyPassword()
    if (res.success) {
      ret = {
        success: true,
        msg: t('messages.operationSuccess'),
        data: !!res.data
      }
    }
    return ret
  }

  // 更新隐私密码
  async updatePrivacyPassword(data) {
    let ret = {
      success: false,
      msg: t('messages.operationFail')
    }

    if (!data) {
      return ret
    }

    // 处理隐私密码设置
    try {
      const storeKey = 'privacyPassword'
      // 处理新旧密码验证
      const query_res = await this.dbManager.getSysRecord(storeKey)
      if (query_res.success && query_res.data?.storeData) {
        const { hash: storedHash, salt } = query_res.data.storeData
        if (!verifyPassword(data.old, storedHash, salt)) {
          ret = {
            success: false,
            msg: t('messages.oldPasswordError')
          }
          return ret
        }
      }
      if (!data.new) {
        ret = {
          success: false,
          msg: t('messages.newPasswordNotEmpty')
        }
        return ret
      }
      // 检查完后更新待插入的密码
      const newSalt = generateSalt()
      const newHash = hashPassword(data.new, newSalt)
      const update_res = await this.dbManager.setSysRecord(
        storeKey,
        { hash: newHash, salt: newSalt },
        'object'
      )
      ret.success = update_res.success
      ret.msg = update_res.msg
    } catch (err) {
      this.logger.error(`处理隐私密码失败: ${err}`)
    }
    return ret
  }
}