import db from "../index.js" class ArticleModel { static async findAll() { return db("articles").orderBy("created_at", "desc") } static async findPublished() { return db("articles") .where("status", "published") .whereNotNull("published_at") .orderBy("published_at", "desc") } static async findDrafts() { return db("articles") .where("status", "draft") .orderBy("updated_at", "desc") } static async findById(id) { return db("articles").where("id", id).first() } static async findBySlug(slug) { return db("articles").where("slug", slug).first() } static async findByAuthor(author) { return db("articles") .where("author", author) .where("status", "published") .orderBy("published_at", "desc") } static async findByCategory(category) { return db("articles") .where("category", category) .where("status", "published") .orderBy("published_at", "desc") } static async findByTags(tags) { // 支持多个标签搜索,标签以逗号分隔 const tagArray = tags.split(',').map(tag => tag.trim()) return db("articles") .where("status", "published") .whereRaw("tags LIKE ?", [`%${tagArray[0]}%`]) .orderBy("published_at", "desc") } static async searchByKeyword(keyword) { return db("articles") .where("status", "published") .where(function() { this.where("title", "like", `%${keyword}%`) .orWhere("content", "like", `%${keyword}%`) .orWhere("keywords", "like", `%${keyword}%`) .orWhere("description", "like", `%${keyword}%`) .orWhere("excerpt", "like", `%${keyword}%`) }) .orderBy("published_at", "desc") } static async create(data) { // 验证必填字段 if (!data.title || !data.content) { throw new Error("标题和内容为必填字段") } // 处理标签,确保格式一致 let tags = data.tags if (tags && typeof tags === "string") { tags = tags.split(',').map(tag => tag.trim()).filter(tag => tag).join(', ') } // 生成slug(如果未提供) let slug = data.slug if (!slug) { slug = this.generateSlug(data.title) } // 计算阅读时间(如果未提供) let readingTime = data.reading_time if (!readingTime) { readingTime = this.calculateReadingTime(data.content) } // 生成摘要(如果未提供) let excerpt = data.excerpt if (!excerpt && data.content) { excerpt = this.generateExcerpt(data.content) } return db("articles").insert({ ...data, tags, slug, reading_time: readingTime, excerpt, status: data.status || "draft", view_count: 0, created_at: db.fn.now(), updated_at: db.fn.now(), }).returning("*") } static async update(id, data) { const current = await db("articles").where("id", id).first() if (!current) { throw new Error("文章不存在") } // 处理标签,确保格式一致 let tags = data.tags if (tags && typeof tags === "string") { tags = tags.split(',').map(tag => tag.trim()).filter(tag => tag).join(', ') } // 生成slug(如果标题改变且未提供slug) let slug = data.slug if (data.title && data.title !== current.title && !slug) { slug = this.generateSlug(data.title) } // 计算阅读时间(如果内容改变且未提供) let readingTime = data.reading_time if (data.content && data.content !== current.content && !readingTime) { readingTime = this.calculateReadingTime(data.content) } // 生成摘要(如果内容改变且未提供) let excerpt = data.excerpt if (data.content && data.content !== current.content && !excerpt) { excerpt = this.generateExcerpt(data.content) } // 如果状态改为published,设置发布时间 let publishedAt = data.published_at if (data.status === "published" && current.status !== "published" && !publishedAt) { publishedAt = db.fn.now() } return db("articles").where("id", id).update({ ...data, tags: tags || current.tags, slug: slug || current.slug, reading_time: readingTime || current.reading_time, excerpt: excerpt || current.excerpt, published_at: publishedAt || current.published_at, updated_at: db.fn.now(), }).returning("*") } static async delete(id) { const article = await db("articles").where("id", id).first() if (!article) { throw new Error("文章不存在") } return db("articles").where("id", id).del() } static async publish(id) { return db("articles") .where("id", id) .update({ status: "published", published_at: db.fn.now(), updated_at: db.fn.now(), }) .returning("*") } static async unpublish(id) { return db("articles") .where("id", id) .update({ status: "draft", published_at: null, updated_at: db.fn.now(), }) .returning("*") } static async incrementViewCount(id) { return db("articles") .where("id", id) .increment("view_count", 1) .returning("*") } static async findByDateRange(startDate, endDate) { return db("articles") .where("status", "published") .whereBetween("published_at", [startDate, endDate]) .orderBy("published_at", "desc") } static async getArticleCount() { const result = await db("articles").count("id as count").first() return result ? result.count : 0 } static async getPublishedArticleCount() { const result = await db("articles") .where("status", "published") .count("id as count") .first() return result ? result.count : 0 } static async getArticleCountByCategory() { return db("articles") .select("category") .count("id as count") .where("status", "published") .groupBy("category") .orderBy("count", "desc") } static async getArticleCountByStatus() { return db("articles") .select("status") .count("id as count") .groupBy("status") .orderBy("count", "desc") } static async getRecentArticles(limit = 10) { return db("articles") .where("status", "published") .orderBy("published_at", "desc") .limit(limit) } static async getPopularArticles(limit = 10) { return db("articles") .where("status", "published") .orderBy("view_count", "desc") .limit(limit) } static async getFeaturedArticles(limit = 5) { return db("articles") .where("status", "published") .whereNotNull("featured_image") .orderBy("published_at", "desc") .limit(limit) } static async getRelatedArticles(articleId, limit = 5) { const current = await this.findById(articleId) if (!current) return [] return db("articles") .where("status", "published") .where("id", "!=", articleId) .where(function() { if (current.category) { this.orWhere("category", current.category) } if (current.tags) { const tags = current.tags.split(',').map(tag => tag.trim()) tags.forEach(tag => { this.orWhereRaw("tags LIKE ?", [`%${tag}%`]) }) } }) .orderBy("published_at", "desc") .limit(limit) } // 工具方法 static generateSlug(title) { return title .toLowerCase() .replace(/[^\w\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-') .trim() } static calculateReadingTime(content) { // 假设平均阅读速度为每分钟200个单词 const wordCount = content.split(/\s+/).length return Math.ceil(wordCount / 200) } static generateExcerpt(content, maxLength = 150) { if (content.length <= maxLength) { return content } return content.substring(0, maxLength).trim() + "..." } } export default ArticleModel export { ArticleModel }