系列
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
}
}