You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

290 lines
9.1 KiB

import db from "../index.js"
class ArticleModel {
static async findAll() {
return db("articles").orderBy("created_at", "desc")
}
static async findPublished(offset, limit) {
let query = db("articles")
.where("status", "published")
.whereNotNull("published_at")
.orderBy("published_at", "desc")
if (typeof offset === "number") {
query = query.offset(offset)
}
if (typeof limit === "number") {
query = query.limit(limit)
}
return query
}
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 }