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

86 阅读8分钟

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: 词库管理类,用于查询、处理分词

定时任务管理类

TaskScheduler主要功能:

  • 定时任务调度管理,这里使用了简单的setInterval,也可以考虑使用node-schedule这类更规范的定时任务库
// main/store/TaskScheduler.mjs

/**
 * 任务调度器
 * 负责管理所有定时任务
 */
export default class TaskScheduler {
  // 单例实例
  static _instance = null

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

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

    this.logger = logger
    // 初始化任务列表
    this.tasks = {}

    // 添加定时器管理
    this.timers = {
      autoSwitchWallpaper: null,
      autoRefreshDirectory: null,
      autoDownload: null,
      autoClearDownloaded: null,
      handleQuality: null,
      checkPrivacyPassword: null,
      handleWords: null,
      monitorMemory: null
    }

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

  // 调度任务
  scheduleTask(timerKey, interval, callback, initialDelay = 0) {
    // 清除已存在的定时器
    if (this.timers[timerKey]) {
      clearInterval(this.timers[timerKey])
    }

    // 如果有初始延迟
    if (initialDelay > 0) {
      setTimeout(() => {
        callback()
        this.timers[timerKey] = setInterval(callback, interval)
      }, initialDelay)
    } else {
      this.timers[timerKey] = setInterval(callback, interval)
    }
  }

  // 清除定时器
  clearTask(timerKey) {
    if (this.timers[timerKey]) {
      clearInterval(this.timers[timerKey])
      this.timers[timerKey] = null
    }
  }

  // 清除所有定时器
  clearAllTasks() {
    Object.keys(this.timers).forEach((key) => {
      this.clearTask(key)
    })
  }
}

壁纸管理类

WallpaperManager主要功能:

  • 切换壁纸,写入历史记录
  • 设置壁纸,调用wallpaper设置壁纸
  • 下载远程资源壁纸,支持通过定时任务执行搜索下载远程资源壁纸到本地
// main/store/WallpaperManager.mjs

import fs from 'fs'
import path from 'path'
import { setWallpaper } from 'wallpaper'
import axios from 'axios'
import { t } from '../../i18n/server.js'
import { isMac, handleTimeByUnit } from '../utils/utils.mjs'

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

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

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

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

    // 重置参数
    this.resetParams()

    WallpaperManager._instance = this
  }

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

  resetParams(keys = ['switchToPrevWallpaper', 'autoDownload']) {
    keys = Array.isArray(keys) ? keys : keys ? [keys] : []
    this.params = this.params || {}

    if (keys.includes('switchToPrevWallpaper')) {
      this.params.switchToPrevWallpaper = {
        // 切换上一个壁纸时,默认索引为0
        index: 0,
        count: 0
      }
    }
    if (keys.includes('autoDownload')) {
      this.params.autoDownload = {
        // 记录下载的页数,用于顺序下载时查找索引
        startPage: 1,
        pageSize: 10,
        // 添加当前下载源索引
        currentSourceIndex: 0
      }
    }
  }

  // 执行切换壁纸
  async doSwitchToNextWallpaper() {
    const {
      wallpaperResource = 'resources',
      filterKeywords,
      orientation,
      quality,
      switchType,
      sortField = 'created_at',
      sortType = -1
    } = this.settingData
    const isResources = wallpaperResource === 'resources'
    const isFavorites = wallpaperResource === 'favorites'

    // 获取最近使用的壁纸ID列表
    const recent_stmt = this.db.prepare(
      `SELECT resourceId FROM fbw_history ORDER BY created_at DESC LIMIT 10`
    )
    const recent_results = recent_stmt.all()
    const recentIds = recent_results.map((item) => item.resourceId)
    const prevSourceId = recentIds[0]

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

    if (!isFavorites && !isResources) {
      query_where.push(`resourceName = ?`)
      query_params.push(wallpaperResource)
    }

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

    if (filterKeywords) {
      const keywords = `%${filterKeywords}%`
      query_where.push(`(filePath LIKE ? OR title LIKE ? OR desc LIKE ?)`)
      query_params.push(keywords, keywords, keywords)
    }
    if (quality.length) {
      const placeholders = quality.map(() => '?').join(',')
      query_where.push(`quality IN (${placeholders})`)
      query_params.push(...quality)
    }

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

    // 随机切换
    if (switchType === 1) {
      // 处理收藏夹查询
      if (isFavorites) {
        // 如果有最近使用记录,尝试排除这些记录
        if (recentIds.length > 0) {
          // 先检查排除最近记录后是否还有壁纸可用
          const check_stmt = this.db.prepare(
            `SELECT COUNT(*) AS count
            FROM fbw_resources r
            JOIN fbw_favorites f ON r.id = f.resourceId
            ${query_where_str ? query_where_str + ' AND' : 'WHERE'}
            NOT EXISTS (SELECT 1 FROM fbw_privacy_space p WHERE p.resourceId = r.id) AND
            r.id NOT IN (${recentIds.map(() => '?').join(',')})`
          )
          const check_params = [...query_params, ...recentIds]
          const check_result = check_stmt.get(...check_params)

          // 如果排除后还有壁纸,则从未使用的壁纸中随机选择
          if (check_result && check_result.count > 0) {
            query_sql = `
              SELECT r.*
              FROM fbw_resources r
              JOIN fbw_favorites f ON r.id = f.resourceId
              ${query_where_str ? query_where_str + ' AND' : 'WHERE'}
              NOT EXISTS (SELECT 1 FROM fbw_privacy_space p WHERE p.resourceId = r.id) AND
              r.id NOT IN (${recentIds.map(() => '?').join(',')})
              ORDER BY RANDOM() LIMIT 1
            `
            query_params.push(...recentIds)
          } else {
            // 如果排除后没有壁纸了,则从所有符合条件的壁纸中随机选择
            query_sql = `
              SELECT r.*
              FROM fbw_resources r
              JOIN fbw_favorites f ON r.id = f.resourceId
              ${query_where_str ? query_where_str + ' AND' : 'WHERE'}
              NOT EXISTS (SELECT 1 FROM fbw_privacy_space p WHERE p.resourceId = r.id)
              ORDER BY RANDOM() LIMIT 1
            `
          }
        } else {
          // 没有历史记录,直接随机选择
          query_sql = `
            SELECT r.*
            FROM fbw_resources r
            JOIN fbw_favorites f ON r.id = f.resourceId
            ${query_where_str ? query_where_str + ' AND' : 'WHERE'}
            NOT EXISTS (SELECT 1 FROM fbw_privacy_space p WHERE p.resourceId = r.id)
            ORDER BY RANDOM() LIMIT 1
          `
        }
      } else {
        // 如果有最近使用记录,尝试排除这些记录
        if (recentIds.length > 0) {
          // 先检查排除最近记录后是否还有壁纸可用
          const check_stmt = this.db.prepare(
            `SELECT COUNT(*) AS count
            FROM fbw_resources
            ${query_where_str ? query_where_str + ' AND' : 'WHERE'}
            id NOT IN (${recentIds.map(() => '?').join(',')})`
          )
          const check_params = [...query_params, ...recentIds]
          const check_result = check_stmt.get(...check_params)

          // 如果排除后还有壁纸,则从未使用的壁纸中随机选择
          if (check_result && check_result.count > 0) {
            query_sql = `
              SELECT * FROM fbw_resources
              ${query_where_str ? query_where_str + ' AND' : 'WHERE'}
              id NOT IN (${recentIds.map(() => '?').join(',')})
              ORDER BY RANDOM() LIMIT 1
            `
            query_params.push(...recentIds)
          } else {
            // 如果排除后没有壁纸了,则从所有符合条件的壁纸中随机选择
            query_sql = `SELECT * FROM fbw_resources ${query_where_str} ORDER BY RANDOM() LIMIT 1`
          }
        } else {
          // 没有历史记录,直接随机选择
          query_sql = `SELECT * FROM fbw_resources ${query_where_str} ORDER BY RANDOM() LIMIT 1`
        }
      }
      // 顺序切换
    } else {
      // 处理收藏夹查询
      if (isFavorites) {
        // 如果有上一次切换的ID,则从该ID之后开始查询
        if (prevSourceId) {
          // 修改为使用收藏表的created_at字段作为排序依据
          const favSortField = 'created_at' // 使用收藏表的创建时间
          // 直接查询上一个壁纸在收藏表中的创建时间
          const index_stmt = this.db.prepare(
            `SELECT f.${favSortField}
              FROM fbw_favorites f
              WHERE f.resourceId = ?`
          )
          const index_result = index_stmt.get(prevSourceId)

          if (index_result && index_result[favSortField] !== undefined) {
            // 检查是否有符合条件的下一个壁纸
            const check_stmt = this.db.prepare(
              `SELECT COUNT(*) AS count
                FROM fbw_resources r
                JOIN fbw_favorites f ON r.id = f.resourceId
                ${query_where_str ? query_where_str + ' AND' : 'WHERE'}
                NOT EXISTS (SELECT 1 FROM fbw_privacy_space p WHERE p.resourceId = r.id) AND
                f.${favSortField} ${sortType === -1 ? '<=' : '>='} ?`
            )
            const check_params = [...query_params, index_result[favSortField]]
            const check_result = check_stmt.get(...check_params)

            // 如果有符合条件的下一个壁纸,则查询下一个壁纸
            if (check_result && check_result.count > 0) {
              // 查询下一个壁纸,按照收藏时间排序
              query_sql = `
                  SELECT r.*
                  FROM fbw_resources r
                  JOIN fbw_favorites f ON r.id = f.resourceId
                  ${query_where_str ? query_where_str + ' AND' : 'WHERE'}
                  NOT EXISTS (SELECT 1 FROM fbw_privacy_space p WHERE p.resourceId = r.id) AND
                  f.${favSortField} ${sortType === -1 ? '<=' : '>='} ?
                  ORDER BY f.${favSortField} ${sortType === -1 ? 'DESC' : 'ASC'}
                  LIMIT 1
                `
              // 与检查语句的参数相同
              query_params = [...check_params]
            } else {
              // 如果没有下一个壁纸,则从头开始
              query_sql = `
                  SELECT r.*
                  FROM fbw_resources r
                  JOIN fbw_favorites f ON r.id = f.resourceId
                  ${query_where_str ? query_where_str + ' AND' : 'WHERE'}
                  NOT EXISTS (SELECT 1 FROM fbw_privacy_space p WHERE p.resourceId = r.id)
                  ORDER BY f.${favSortField} ${sortType === -1 ? 'DESC' : 'ASC'}
                  LIMIT 1
                `
            }
          } else {
            // 如果获取不到上一个壁纸的排序字段值,则从头开始
            query_sql = `
                SELECT r.*
                FROM fbw_resources r
                JOIN fbw_favorites f ON r.id = f.resourceId
                ${query_where_str ? query_where_str + ' AND' : 'WHERE'}
                NOT EXISTS (SELECT 1 FROM fbw_privacy_space p WHERE p.resourceId = r.id)
                ORDER BY f.created_at ${sortType === -1 ? 'DESC' : 'ASC'}
                LIMIT 1
              `
          }
        } else {
          // 没有上一次切换的ID,从头开始,按收藏时间排序
          query_sql = `
            SELECT r.*
            FROM fbw_resources r
            JOIN fbw_favorites f ON r.id = f.resourceId
            ${query_where_str ? query_where_str + ' AND' : 'WHERE'}
            NOT EXISTS (SELECT 1 FROM fbw_privacy_space p WHERE p.resourceId = r.id)
            ORDER BY f.created_at ${sortType === -1 ? 'DESC' : 'ASC'}
            LIMIT 1
          `
        }
      } else {
        // 顺序切换时也排除最近10条历史记录,排序加id,循环切换
        if (prevSourceId) {
          // 获取上一个壁纸的排序字段值
          const index_stmt = this.db.prepare(`SELECT ${sortField} FROM fbw_resources WHERE id = ?`)
          const index_result = index_stmt.get(prevSourceId)
          const sortFieldVal = index_result[sortField]
          if (index_result && sortFieldVal !== undefined) {
            // 排除最近10条历史记录
            const excludeIds =
              recentIds.length > 0 ? `id NOT IN (${recentIds.map(() => '?').join(',')})` : ''
            const excludeParams = recentIds
            // 查找下一个
            const check_stmt = this.db.prepare(
              `SELECT COUNT(*) AS count
              FROM fbw_resources
              ${query_where_str ? query_where_str + ' AND' : 'WHERE'}
              ${excludeIds ? excludeIds + ' AND' : ''}
              (${sortField} ${sortType === -1 ? '<' : '>'} ? OR (${sortField} = ? AND id > ?))`
            )
            const check_params = [
              ...query_params,
              ...excludeParams,
              sortFieldVal,
              sortFieldVal,
              prevSourceId
            ]
            const check_result = check_stmt.get(...check_params)
            if (check_result && check_result.count > 0) {
              // 查找下一个
              query_sql = `
                SELECT *
                FROM fbw_resources
                ${query_where_str ? query_where_str + ' AND' : 'WHERE'}
                ${excludeIds ? excludeIds + ' AND' : ''}
                (${sortField} ${sortType === -1 ? '<' : '>'} ? OR (${sortField} = ? AND id > ?))
                ORDER BY ${sortField} ${sortType === -1 ? 'DESC' : 'ASC'}, id
                LIMIT 1
              `
              query_params = [
                ...query_params,
                ...excludeParams,
                sortFieldVal,
                sortFieldVal,
                prevSourceId
              ]
            } else {
              // 循环到头部,排除最近10条
              query_sql = `
                SELECT *
                FROM fbw_resources
                ${query_where_str ? query_where_str + ' AND' : 'WHERE'}
                ${excludeIds}
                ORDER BY ${sortField} ${sortType === -1 ? 'DESC' : 'ASC'}, id
                LIMIT 1
              `
              query_params = [...query_params, ...excludeParams]
            }
          } else {
            // 如果获取不到上一个壁纸的排序字段值,则从头开始
            query_sql = `
                SELECT *
                FROM fbw_resources
                ${query_where_str ? query_where_str + ' AND' : 'WHERE'}
                ORDER BY ${sortField} ${sortType === -1 ? 'DESC' : 'ASC'}, id
                LIMIT 1
              `
          }
        } else {
          // 没有上一次切换的ID,从头开始
          query_sql = `
            SELECT *
            FROM fbw_resources
            ${query_where_str}
            ORDER BY ${sortField} ${sortType === -1 ? 'DESC' : 'ASC'}, id
            LIMIT 1
          `
        }
      }
    }

    // 执行查询
    if (query_sql) {
      query_stmt = this.db.prepare(query_sql)
      const query_result = query_stmt.get(...query_params)
      if (query_result && query_result.id !== prevSourceId) {
        return await this.setAsWallpaper(query_result, true, true)
      }
    }

    return {
      success: false,
      msg: t('messages.operationFail')
    }
  }

  // 切换到上一个壁纸
  async doSwitchToPrevWallpaper() {
    const { index } = this.params.switchToPrevWallpaper
    // 查询历史记录总数
    const count_stmt = this.db.prepare(`SELECT COUNT(*) AS total FROM fbw_history`)
    const count_result = count_stmt.get()
    this.params.switchToPrevWallpaper.count =
      count_result && count_result.total ? count_result.total : 0

    // 支持循环切换
    if (this.params.switchToPrevWallpaper.count) {
      const nextIndex = index + 1 < this.params.switchToPrevWallpaper.count ? index + 1 : 0
      // 查询历史记录
      const query_stmt = this.db.prepare(
        `SELECT h.id as hid, r.* FROM fbw_history h LEFT JOIN fbw_resources r ON h.resourceId = r.id ORDER BY h.id DESC LIMIT ? OFFSET ?`
      )
      const query_result = query_stmt.get(1, nextIndex)

      if (query_result) {
        // 更新索引
        this.params.switchToPrevWallpaper.index = nextIndex

        return await this.setAsWallpaper(query_result, false, false)
      }
    }
    return {
      success: false,
      msg: t('messages.operationFail')
    }
  }

  // 设置为壁纸
  async setAsWallpaper(item, isAddToHistory = false, isResetParams = false) {
    if (!item || !item.filePath || !fs.existsSync(item.filePath)) {
      return {
        success: false,
        msg: t('messages.fileNotExist')
      }
    }

    try {
      // 设置壁纸
      await setWallpaper(item.filePath, {
        screen: this.settingData.allScreen && isMac() ? 'all' : 'main',
        scale: this.settingData.scaleType
      })

      // 记录到历史记录
      if (isAddToHistory) {
        const insert_stmt = this.db.prepare(`INSERT INTO fbw_history (resourceId) VALUES (?)`)
        insert_stmt.run(item.id)
      }

      // 重置参数
      if (isResetParams) {
        this.resetParams('switchToPrevWallpaper')
      }

      return {
        success: true,
        msg: t('messages.setWallpaperSuccess')
      }
    } catch (err) {
      this.logger.error(`设置壁纸失败: error => ${err}`)
      return {
        success: false,
        msg: t('messages.setWallpaperFail')
      }
    }
  }

  // 设置静态壁纸
  async setStaticWallpaper(imgPath) {
    if (!imgPath || !fs.existsSync(imgPath)) {
      return {
        success: false,
        msg: t('messages.fileNotExist')
      }
    }
    try {
      // 设置壁纸
      await setWallpaper(imgPath, {
        screen: this.settingData.allScreen && isMac() ? 'all' : 'main',
        scale: this.settingData.scaleType
      })
      return {
        success: true,
        msg: t('messages.setWallpaperSuccess')
      }
    } catch (err) {
      this.logger.error(`设置壁纸失败: error => ${err}`)
      return {
        success: false,
        msg: t('messages.setWallpaperFail')
      }
    }
  }

  // 下载并设置为壁纸
  async setAsWallpaperWithDownload(item) {
    if (!item) {
      return {
        success: false,
        msg: t('messages.paramsError')
      }
    }

    try {
      // 处理不同的 srcType
      if (item.srcType === 'file' && item.filePath) {
        // 处理本地文件
        const filePath = item.filePath
        const query_stmt = this.db.prepare(`SELECT * FROM fbw_resources WHERE filePath = ?`)
        const query_result = query_stmt.get(filePath)

        if (query_result) {
          return await this.setAsWallpaper(query_result, true, true)
        } else {
          return {
            success: false,
            msg: t('messages.fileNotExist')
          }
        }
      } else if (item.srcType === 'url' && item.url) {
        // 下载壁纸
        const { downloadFolder } = this.settingData
        if (!downloadFolder || !fs.existsSync(downloadFolder)) {
          return {
            success: false,
            msg: t('messages.downloadFolderNotExistOrNotSet')
          }
        }

        // 确保下载目录存在
        if (!fs.existsSync(downloadFolder)) {
          fs.mkdirSync(downloadFolder, { recursive: true })
        }
        // 生成文件名
        const fileName = `${item.fileName}.${item.fileExt}`
        const filePath = path.join(downloadFolder, fileName)

        if (fs.existsSync(filePath)) {
          // 文件已存在,取消写入
          this.logger.warn(`文件 ${filePath} 已存在,跳过写入`)
        } else {
          // 下载文件
          const response = await axios({
            method: 'GET',
            url: item.url,
            responseType: 'stream'
          })

          const writer = fs.createWriteStream(filePath)
          response.data.pipe(writer)

          await new Promise((resolve, reject) => {
            writer.on('finish', resolve)
            writer.on('error', reject)
          })
        }

        // 获取文件信息
        const stats = fs.statSync(filePath)

        // 插入到数据库
        try {
          const insert_stmt = this.db.prepare(
            `INSERT INTO fbw_resources
              (resourceName, fileName, filePath, fileExt, fileSize, url, author, link, title, desc, quality, width, height, isLandscape, atimeMs, mtimeMs, ctimeMs) VALUES
              (@resourceName, @fileName, @filePath, @fileExt, @fileSize, @url, @author, @link, @title, @desc, @quality, @width, @height, @isLandscape, @atimeMs, @mtimeMs, @ctimeMs)`
          )
          const insert_result = insert_stmt.run({
            resourceName: item.resourceName,
            fileName: item.fileName,
            filePath: filePath,
            fileExt: item.fileExt,
            fileSize: stats.size,
            url: item.url,
            author: item.author || '',
            link: item.link || '',
            title: item.title || '',
            desc: item.desc || '',
            quality: item.quality || '',
            width: item.width || 0,
            height: item.height || 0,
            isLandscape: item.isLandscape,
            atimeMs: stats.atimeMs,
            mtimeMs: stats.mtimeMs,
            ctimeMs: stats.ctimeMs
          })

          if (insert_result.changes > 0) {
            // 查询插入的记录
            const query_stmt = this.db.prepare(`SELECT * FROM fbw_resources WHERE id = ?`)
            const query_result = query_stmt.get(insert_result.lastInsertRowid)

            if (query_result) {
              // 设置为壁纸
              return await this.setAsWallpaper(query_result, true, true)
            }
          }
        } catch (err) {
          // 处理唯一键冲突
          if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') {
            this.logger.info(`文件路径已存在,尝试查询现有记录: ${filePath}`)
            // 查询已存在的记录
            const query_stmt = this.db.prepare(`SELECT * FROM fbw_resources WHERE filePath = ?`)
            const query_result = query_stmt.get(filePath)

            if (query_result) {
              // 设置为壁纸
              return await this.setAsWallpaper(query_result, true, true)
            }
          } else {
            throw err // 重新抛出非唯一键约束的错误
          }
        }
      }
      return {
        success: false,
        msg: t('messages.setWallpaperFail')
      }
    } catch (err) {
      this.logger.error(`下载壁纸失败: error => ${err}`)
      return {
        success: false,
        msg: t('messages.setWallpaperFail')
      }
    }
  }

  // 搜索并下载壁纸
  async searchWallpaperWithDownload(params) {
    const { resourceName, keywords, orientation, startPage, pageSize } = params
    const { downloadFolder, remoteResourceSecretKeys } = this.settingData

    let ret = {
      success: false,
      msg: t('messages.operationFail')
    }

    if (!downloadFolder || !fs.existsSync(downloadFolder)) {
      ret.msg = t('messages.downloadFolderNotExistOrNotSet')
      return ret
    }

    if (!keywords) {
      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
    }

    try {
      // 确保下载目录存在
      if (!fs.existsSync(downloadFolder)) {
        fs.mkdirSync(downloadFolder, { recursive: true })
      }

      const res = await this.apiManager.call(resourceName, 'search', {
        keywords,
        orientation,
        startPage,
        pageSize,
        secretKey: resourceMap.remoteResourceKeyNames.includes(resourceName)
          ? remoteResourceSecretKeys[resourceName]
          : ''
      })
      if (res) {
        if (res.list.length) {
          const docs = []
          const inserted_ids = []
          const duplicate_filePaths = []
          // 存储到本地
          for (let i = 0; i < res.list.length; i++) {
            const item = res.list[i]
            const filePath = `${downloadFolder}/${item.fileName}.${item.fileExt}`
            try {
              if (fs.existsSync(filePath)) {
                // 文件已存在,取消写入
                this.logger.warn(`文件 ${filePath} 已存在,跳过写入`)
              } else {
                // 方式一:同步写入
                const fileRes = await axios.get(item.url, { responseType: 'arraybuffer' })
                fs.writeFileSync(filePath, fileRes.data)
              }
              const stats = fs.statSync(filePath)
              docs.push({
                ...item,
                filePath,
                fileSize: stats.size,
                atimeMs: stats.atimeMs,
                mtimeMs: stats.mtimeMs,
                ctimeMs: stats.ctimeMs
              })
            } catch (err) {
              this.logger.error(`searchWallpaperWithDownload writeFileSync ERROR:: ${err}`)
            }
          }
          if (docs.length) {
            try {
              const insert_stmt = this.db.prepare(
                `INSERT OR IGNORE INTO fbw_resources
                 (resourceName, fileName, filePath, fileExt, fileSize, url, author, link, title, desc, quality, width, height, isLandscape, atimeMs, mtimeMs, ctimeMs) VALUES
                 (@resourceName, @fileName, @filePath, @fileExt, @fileSize, @url, @author, @link, @title, @desc, @quality, @width, @height, @isLandscape, @atimeMs, @mtimeMs, @ctimeMs)`
              )
              const transaction = this.db.transaction((docs) => {
                for (let i = 0; i < docs.length; i++) {
                  const item = docs[i]
                  try {
                    const insert_result = insert_stmt.run({
                      resourceName: item.resourceName,
                      fileName: item.fileName,
                      filePath: item.filePath,
                      fileExt: item.fileExt,
                      fileSize: item.fileSize,
                      url: item.url,
                      author: item.author,
                      link: item.link,
                      title: item.title,
                      desc: item.desc,
                      quality: item.quality,
                      width: item.width,
                      height: item.height,
                      isLandscape: item.isLandscape,
                      atimeMs: item.atimeMs,
                      mtimeMs: item.mtimeMs,
                      ctimeMs: item.ctimeMs
                    })
                    const lastInsertedId = insert_result.lastInsertRowid
                    if (lastInsertedId) {
                      inserted_ids.push(lastInsertedId)
                    }
                  } catch (err) {
                    if (err.message.includes('UNIQUE constraint failed')) {
                      // 处理唯一约束失败错误
                      this.logger.warn(`跳过重复数据: ${item.filePath}`)
                      duplicate_filePaths.push(item.filePath)
                    } else {
                      throw err // 抛出其他类型的错误
                    }
                  }
                }
              })
              transaction(docs)
            } catch (err) {
              this.logger.error(`searchWallpaperWithDownload insert ERROR:: ${err}`)
            }
          }
        }
        ret.success = true
        ret.msg = t(res.list.length ? 'messages.querySuccess' : 'messages.queryEmpty')
      }
      return ret
    } catch (err) {
      this.logger.error(`搜索并下载壁纸失败: error => ${err}`)
      return ret
    }
  }

  // 下载壁纸
  async downloadWallpaper() {
    const { downloadSources, downloadKeywords, downloadOrientation, downloadFolder, autoDownload } =
      this.settingData

    if (
      !autoDownload ||
      !downloadSources ||
      !downloadSources.length ||
      !downloadKeywords ||
      !downloadFolder
    ) {
      return false
    }

    try {
      // 确保下载目录存在
      if (!fs.existsSync(downloadFolder)) {
        fs.mkdirSync(downloadFolder, { recursive: true })
      }

      // 获取当前要使用的下载源
      // 如果是数组,则按顺序轮流使用每个下载源
      const currentSourceIndex =
        (this.params.autoDownload.currentSourceIndex || 0) % downloadSources.length
      const currentSource = downloadSources[currentSourceIndex]

      const { startPage, pageSize } = this.params.autoDownload
      const res = await this.searchWallpaperWithDownload({
        resourceName: currentSource,
        keywords: downloadKeywords,
        orientation: downloadOrientation,
        startPage,
        pageSize
      })

      // 查询有结果时向下翻页,否则切换到下一个下载源
      if (res.success) {
        this.params.autoDownload.startPage = startPage + 1
        return true
      } else {
        // 切换到下一个下载源,并重置页码
        this.params.autoDownload.currentSourceIndex =
          (currentSourceIndex + 1) % downloadSources.length
        this.params.autoDownload.startPage = 1
        return false
      }
    } catch (err) {
      this.logger.error(`下载壁纸失败: error => ${err}`)
      return false
    }
  }

  // 清理所有下载的壁纸
  async clearDownloadedAll() {
    let ret = {
      success: false,
      msg: t('messages.operationFail')
    }
    try {
      // 直接从数据库中查询所有非local的资源
      const query_stmt = this.db.prepare(
        `SELECT * FROM fbw_resources WHERE resourceName != 'local'`
      )
      const query_result = query_stmt.all()

      let allCount = 0
      let successCount = 0
      let failCount = 0

      if (Array.isArray(query_result) && query_result.length) {
        allCount = query_result.length

        for (let i = 0; i < query_result.length; i++) {
          try {
            const item = query_result[i]
            const res = await this.fileManager.deleteFile(item)
            if (res.success) {
              successCount++
            } else {
              failCount++
            }
          } catch (err) {
            this.logger.error(`处理文件时出错: ${err}`)
            failCount++
          }
        }
      }

      this.logger.info(`清理文件完成,共 ${allCount},成功 ${successCount},失败 ${failCount}!`)
      ret.success = true
      ret.msg = t('messages.clearDownloadedDone', {
        allCount,
        successCount,
        failCount
      })
    } catch (err) {
      this.logger.error(`清理文件失败: ${err}`)
    }
    return ret
  }

  // 清理过期下载的壁纸
  async clearDownloadedExpired() {
    let ret = {
      success: false,
      msg: t('messages.operationFail')
    }
    try {
      // 获取过期时间
      const { clearDownloadedExpiredTime, clearDownloadedExpiredUnit } = this.settingData
      const expiredTimeMs = handleTimeByUnit(clearDownloadedExpiredTime, clearDownloadedExpiredUnit)
      const expiredTimestamp = Date.now() - expiredTimeMs
      // 将时间戳转换为 SQLite 日期时间格式
      const expiredDate = new Date(expiredTimestamp).toISOString()

      // 直接从数据库中查询过期的非local资源
      const query_stmt = this.db.prepare(
        `SELECT * FROM fbw_resources WHERE resourceName != 'local' AND created_at < ?`
      )
      const query_result = query_stmt.all(expiredDate)

      let allCount = 0
      let successCount = 0
      let failCount = 0

      if (Array.isArray(query_result) && query_result.length) {
        allCount = query_result.length

        for (let i = 0; i < query_result.length; i++) {
          try {
            const item = query_result[i]
            const res = await this.fileManager.deleteFile(item)
            if (res.success) {
              successCount++
            } else {
              failCount++
            }
          } catch (err) {
            this.logger.error(`处理文件时出错: ${err}`)
            failCount++
          }
        }
      }

      this.logger.info(
        `清理过期文件完成,共 ${allCount},成功 ${successCount},失败 ${failCount}!`
      )
      ret.success = true
      ret.msg = t('messages.clearDownloadedDone', {
        allCount,
        successCount,
        failCount
      })
    } catch (err) {
      this.logger.error(`清理过期文件失败: ${err}`)
    }
    return ret
  }
}

词库管理类

WordsManager主要功能:

  • 通过@node-rs/jieba对本地资源、远程资源的fileNametitledesc进行分词记录
// main/store/WordsManager.mjs

import * as jieba from '@node-rs/jieba'
import { t } from '../../i18n/server.js'

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

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

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

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

    // 重置参数
    this.resetParams()

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

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

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

  /**
   * 定时处理词库
   * @param {Object} locks - 锁对象
   */
  intervalHandleWords(locks) {
    if (locks.handleWords) {
      return
    }

    locks.handleWords = true

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

    // 查询未处理分词的图片
    const query_stmt = this.db.prepare(
      `SELECT r.id, r.title, r.desc, r.fileName
      FROM fbw_resources r
      WHERE
      NOT EXISTS (SELECT 1 FROM fbw_resource_words rw WHERE rw.resourceId = r.id)
      LIMIT ? OFFSET ?`
    )
    const query_result = query_stmt.all(pageSize, (startPage - 1) * pageSize)

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

      // 处理分词
      this.handleWords(query_result)
      locks.handleWords = false
    } else {
      locks.handleWords = false
      this.resetParams()
    }
  }

  /**
   * 处理分词
   * @param {Array} list - 资源列表
   */
  handleWords(list) {
    if (!Array.isArray(list) || !list.length) {
      return
    }

    try {
      // 插入分词
      const insert_stmt = this.db.prepare(
        `INSERT OR IGNORE INTO fbw_words (word, count, type) VALUES (?, ?, ?)`
      )

      // 更新分词计数
      const update_word_stmt = this.db.prepare(
        `UPDATE fbw_words SET count = count + 1 WHERE word = ?`
      )

      // 获取分词ID
      const get_word_id_stmt = this.db.prepare(`SELECT id FROM fbw_words WHERE word = ?`)

      // 插入资源与分词的关联
      const insert_resource_word_stmt = this.db.prepare(
        `INSERT OR IGNORE INTO fbw_resource_words (resourceId, wordId) VALUES (?, ?)`
      )

      const transaction = this.db.transaction(() => {
        for (let i = 0; i < list.length; i++) {
          const item = list[i]

          // 处理标题和描述
          const content = item.title ? `${item.title} ${item.desc}`.trim() : item.fileName
          if (!content) continue

          // 简单分词处理
          const words = this.cutWords(content)

          // 插入分词
          for (const word of words) {
            if (!word.trim()) continue

            // 匹配中文、英文
            let chinesePattern = /[\u4e00-\u9fa5]/
            let englishPattern = /[a-zA-Z]/
            // 类型 1:中文 2:英文
            let type = 0
            if (chinesePattern.test(word)) {
              type = 1
            } else if (englishPattern.test(word)) {
              type = 2
            }

            // 先尝试插入
            const insert_result = insert_stmt.run(word, 1, type)

            // 如果插入失败,说明已存在,更新计数
            if (!insert_result.changes) {
              update_word_stmt.run(word)
            }

            // 获取分词ID
            const wordRecord = get_word_id_stmt.get(word)
            if (wordRecord && wordRecord.id) {
              // 插入资源与分词的关联
              insert_resource_word_stmt.run(item.id, wordRecord.id)
            }
          }
        }
      })

      // 执行事务
      transaction()
      this.logger.info(`处理分词成功: count => ${list.length}`)
    } catch (err) {
      this.logger.error(`处理分词失败: error => ${err}`)
    }
  }

  /**
   * 处理删除资源时的分词计数更新
   * @param {Object} resource - 被删除的资源
   */
  handleDeletedResource(resource) {
    if (!resource) {
      return
    }

    try {
      // 处理标题和描述
      const content = resource.title
        ? `${resource.title} ${resource.desc}`.trim()
        : resource.fileName
      if (!content) return

      // 获取分词
      const words = this.cutWords(content)

      // 更新分词计数
      const update_word_stmt = this.db.prepare(
        `UPDATE fbw_words SET count = count - 1 WHERE word = ?`
      )

      // 删除计数为0的分词
      const delete_word_stmt = this.db.prepare(
        `DELETE FROM fbw_words WHERE word = ? AND count <= 0`
      )

      // 删除资源与分词的关联
      const delete_resource_word_stmt = this.db.prepare(
        `DELETE FROM fbw_resource_words WHERE resourceId = ?`
      )

      const transaction = this.db.transaction(() => {
        // 删除资源与分词的关联
        delete_resource_word_stmt.run(resource.id)

        for (const word of words) {
          if (!word.trim()) continue

          // 减少计数
          update_word_stmt.run(word)

          // 删除计数为0的分词
          delete_word_stmt.run(word)
        }
      })

      // 执行事务
      transaction()
      this.logger.info(
        `更新删除资源的分词计数成功: 资源ID => ${resource.id},分词 => ${words.join(',')}`
      )
    } catch (err) {
      this.logger.error(`更新删除资源的分词计数失败: error => ${err}`)
    }
  }

  /**
   * 分词处理
   * @param {string} content - 内容
   * @returns {Array} 分词结果
   */
  cutWords(content) {
    if (!content) return []

    // 方式一:简单分词:按空格、标点符号分割
    // return content
    //   .replace(/[^\w\s\u4e00-\u9fa5]/g, ' ') // 替换非字母、数字、中文、空格为空格
    //   .split(/\s+/) // 按空格分割
    //   .filter((word) => word.length > 1) // 过滤掉长度为1的词

    // 方式二:结巴分词
    return jieba.cutForSearch(content, true)
  }

  /**
   * 获取词库
   * @param {Object} params - 查询参数
   * @returns {Object} 查询结果
   */
  getWords(params = {}) {
    const { types = [], size = 100 } = params

    let ret = {
      success: false,
      msg: t('messages.operationFail'),
      data: null
    }

    try {
      if (types.length) {
        const data = {}
        for (let i = 0; i < types.length; i++) {
          const type = types[i]
          const query_stmt = this.db.prepare(
            `SELECT word, count, type FROM fbw_words WHERE type = ? ORDER BY count DESC LIMIT ?`
          )
          const query_result = query_stmt.all(type, size)
          if (query_result && query_result.length) {
            data[type] = query_result
          } else {
            data[type] = []
          }
        }
        ret = {
          success: true,
          msg: t(Object.keys(data).length ? 'messages.querySuccess' : 'messages.queryEmpty'),
          data
        }
      } else {
        const query_stmt = this.db.prepare(
          `SELECT word, count, type FROM fbw_words ORDER BY count DESC LIMIT ?`
        )
        const query_result = query_stmt.all(size)
        if (Array.isArray(query_result)) {
          ret = {
            success: true,
            msg: t(query_result.length ? 'messages.querySuccess' : 'messages.queryEmpty'),
            data: query_result
          }
        }
      }
    } catch (err) {
      this.logger.error(`获取词库失败: error => ${err}`)
    }

    return ret
  }
}