import db from "../index.js" class BookmarkModel { static async findAll(userId = null, options = {}) { let query = db("bookmarks") .select("bookmarks.*") .select("categories.name as category_name") .select("categories.color as category_color") .leftJoin("categories", "bookmarks.category_id", "categories.id") if (userId) { query = query.where("bookmarks.user_id", userId) } // 分类过滤 if (options.categoryId) { query = query.where("bookmarks.category_id", options.categoryId) } // 标签过滤 if (options.tagIds && options.tagIds.length > 0) { query = query .join("bookmark_tags", "bookmarks.id", "bookmark_tags.bookmark_id") .whereIn("bookmark_tags.tag_id", options.tagIds) } // 搜索过滤 if (options.search) { const searchTerm = `%${options.search}%` query = query.where(function() { this.where("bookmarks.title", "like", searchTerm) .orWhere("bookmarks.description", "like", searchTerm) .orWhere("bookmarks.url", "like", searchTerm) }) } // 排序 const orderBy = options.orderBy || "created_at" const orderDirection = options.orderDirection || "desc" query = query.orderBy(`bookmarks.${orderBy}`, orderDirection) // 分页 if (options.limit) { query = query.limit(options.limit) } if (options.offset) { query = query.offset(options.offset) } return query } static async findById(id, userId = null) { let query = db("bookmarks") .select("bookmarks.*") .select("categories.name as category_name") .select("categories.color as category_color") .leftJoin("categories", "bookmarks.category_id", "categories.id") .where("bookmarks.id", id) if (userId) { query = query.where("bookmarks.user_id", userId) } return query.first() } static async create(data) { // 提取 tags 和 links,避免插入到 bookmarks 表 const { tags, links, ...bookmarkData } = data const bookmark = await db("bookmarks").insert({ ...bookmarkData, created_at: db.fn.now(), updated_at: db.fn.now(), last_visited: db.fn.now(), }).returning("*") // 如果有标签,创建标签关联 if (tags && tags.length > 0) { await this.addTags(bookmark[0].id, tags, bookmarkData.user_id) } // 如果有多链接,创建链接记录 if (links && links.length > 0) { await this.addLinks(bookmark[0].id, links) } return bookmark[0] } static async update(id, data, userId = null) { // 提取 tags 和 links,避免更新到 bookmarks 表 const { tags, links, ...bookmarkData } = data let query = db("bookmarks").where("id", id) if (userId) { query = query.where("user_id", userId) } const bookmark = await query.update({ ...bookmarkData, updated_at: db.fn.now(), }).returning("*") // 更新标签 if (tags !== undefined) { await this.updateTags(id, tags, userId) } // 更新链接 if (links !== undefined) { await this.updateLinks(id, links) } return bookmark[0] } static async delete(id, userId = null) { let query = db("bookmarks").where("id", id) if (userId) { query = query.where("user_id", userId) } return query.del() } static async incrementClickCount(id) { return db("bookmarks") .where("id", id) .increment("click_count", 1) .update({ last_visited: db.fn.now(), updated_at: db.fn.now(), }) } static async toggleFavorite(id, userId) { const bookmark = await this.findById(id, userId) if (!bookmark) return null return db("bookmarks") .where("id", id) .where("user_id", userId) .update({ is_favorite: !bookmark.is_favorite, updated_at: db.fn.now(), }) .returning("*") } static async getFavorites(userId, limit = 20) { return db("bookmarks") .select("bookmarks.*") .select("categories.name as category_name") .leftJoin("categories", "bookmarks.category_id", "categories.id") .where("bookmarks.user_id", userId) .where("bookmarks.is_favorite", true) .orderBy("bookmarks.updated_at", "desc") .limit(limit) } static async getRecentBookmarks(userId, limit = 10) { return db("bookmarks") .select("bookmarks.*") .select("categories.name as category_name") .leftJoin("categories", "bookmarks.category_id", "categories.id") .where("bookmarks.user_id", userId) .orderBy("bookmarks.last_visited", "desc") .limit(limit) } static async getPopularBookmarks(userId, limit = 10) { return db("bookmarks") .select("bookmarks.*") .select("categories.name as category_name") .leftJoin("categories", "bookmarks.category_id", "categories.id") .where("bookmarks.user_id", userId) .orderBy("bookmarks.click_count", "desc") .limit(limit) } static async getBookmarksByCategory(categoryId, userId, limit = 20) { return db("bookmarks") .select("bookmarks.*") .select("categories.name as category_name") .leftJoin("categories", "bookmarks.category_id", "categories.id") .where("bookmarks.category_id", categoryId) .where("bookmarks.user_id", userId) .orderBy("bookmarks.sort_order", "asc") .orderBy("bookmarks.created_at", "desc") .limit(limit) } static async getBookmarksByTag(tagId, userId, limit = 20) { return db("bookmarks") .select("bookmarks.*") .select("categories.name as category_name") .leftJoin("categories", "bookmarks.category_id", "categories.id") .join("bookmark_tags", "bookmarks.id", "bookmark_tags.bookmark_id") .where("bookmark_tags.tag_id", tagId) .where("bookmarks.user_id", userId) .orderBy("bookmarks.created_at", "desc") .limit(limit) } // 标签相关方法 static async addTags(bookmarkId, tagNames, userId) { const tags = [] for (const tagName of tagNames) { // 使用 findOrCreate 自动创建不存在的标签 const tag = await db("tags") .where("name", tagName.trim()) .where("user_id", userId) .first() if (tag) { tags.push(tag.id) } else { // 如果标签不存在,创建新标签 const newTag = await db("tags").insert({ name: tagName.trim(), user_id: userId, created_at: db.fn.now(), updated_at: db.fn.now(), }).returning("*") tags.push(newTag[0].id) } } if (tags.length > 0) { const bookmarkTags = tags.map(tagId => ({ bookmark_id: bookmarkId, tag_id: tagId, created_at: db.fn.now(), })) await db("bookmark_tags").insert(bookmarkTags) } } static async updateTags(bookmarkId, tagNames, userId) { // 删除现有标签关联 await db("bookmark_tags").where("bookmark_id", bookmarkId).del() // 添加新标签关联 if (tagNames && tagNames.length > 0) { await this.addTags(bookmarkId, tagNames, userId) } } static async getTags(bookmarkId) { const tags = await db("tags") .select("tags.name") .join("bookmark_tags", "tags.id", "bookmark_tags.tag_id") .where("bookmark_tags.bookmark_id", bookmarkId) .orderBy("tags.name", "asc") // 返回标签名称数组 return tags.map(tag => tag.name) } // 链接相关方法 static async addLinks(bookmarkId, links) { if (links && links.length > 0) { const bookmarkLinks = links.map((link, index) => ({ bookmark_id: bookmarkId, title: link.title, url: link.url, description: link.description, type: link.type || "link", sort_order: link.sort_order || index, created_at: db.fn.now(), updated_at: db.fn.now(), })) await db("bookmark_links").insert(bookmarkLinks) } } static async updateLinks(bookmarkId, links) { // 删除现有链接 await db("bookmark_links").where("bookmark_id", bookmarkId).del() // 添加新链接 if (links && links.length > 0) { await this.addLinks(bookmarkId, links) } } static async getLinks(bookmarkId) { const links = await db("bookmark_links") .select("title", "url", "type", "description") .where("bookmark_id", bookmarkId) .where("is_active", true) .orderBy("sort_order", "asc") .orderBy("created_at", "asc") return links } // 统计方法 static async getStats(userId) { const [totalBookmarks] = await db("bookmarks") .where("user_id", userId) .count("* as count") const [favoriteBookmarks] = await db("bookmarks") .where("user_id", userId) .where("is_favorite", true) .count("* as count") const [totalClicks] = await db("bookmarks") .where("user_id", userId) .sum("click_count as total") return { total: parseInt(totalBookmarks.count), favorites: parseInt(favoriteBookmarks.count), totalClicks: parseInt(totalClicks.total) || 0, } } static async searchBookmarks(query, userId, options = {}) { const searchTerm = `%${query}%` let searchQuery = db("bookmarks") .select("bookmarks.*") .select("categories.name as category_name") .leftJoin("categories", "bookmarks.category_id", "categories.id") .where("bookmarks.user_id", userId) .where(function() { this.where("bookmarks.title", "like", searchTerm) .orWhere("bookmarks.description", "like", searchTerm) .orWhere("bookmarks.url", "like", searchTerm) }) // 标签搜索 if (options.includeTags) { searchQuery = searchQuery .leftJoin("bookmark_tags", "bookmarks.id", "bookmark_tags.bookmark_id") .leftJoin("tags", "bookmark_tags.tag_id", "tags.id") .orWhere("tags.name", "like", searchTerm) } return searchQuery .orderBy("bookmarks.updated_at", "desc") .limit(options.limit || 50) } } export default BookmarkModel export { BookmarkModel }