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