主要变更: - 添加完整的项目结构和模块(admin、articles、comments、users、session、oauth2、email、moderation、analytics、jobs 等) - 实现系统初始化 API(/init/status 和 /init/run) - 重写部署流程:迁移到 package.json scripts,删除 Makefile - 优化部署脚本:deploy.sh、healthcheck.sh、backup.sh、restore.sh、verify-env.sh - 更新 README.md:简化文档,整合部署指南 - 优化 AGENTS.md:精简到约 150 行,包含完整的代码规范和命令速查 - 配置 Docker Compose 自动化部署(prisma migrate deploy + seed) - 生成 OAuth2 RSA 密钥对支持 - 添加环境变量验证和数据库备份恢复功能
268 lines
8.6 KiB
Plaintext
268 lines
8.6 KiB
Plaintext
// This is your Prisma schema file,
|
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
model User {
|
|
id Int @id @default(autoincrement())
|
|
email String @unique
|
|
username String @unique
|
|
passwordHash String
|
|
displayName String?
|
|
avatarUrl String?
|
|
role String @default("user") // admin, user, moderator
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
oauth2Tokens OAuth2Token[]
|
|
oauth2AuthCodes OAuth2AuthorizationCode[]
|
|
articles Article[]
|
|
comments Comment[]
|
|
reactions Reaction[] @relation("UserReactions")
|
|
follows Follow[] @relation("FollowFollower")
|
|
followees Follow[] @relation("FollowFollowee")
|
|
subscriptions Subscription[] @relation("SubscriptionSubscriber")
|
|
targets Subscription[] @relation("SubscriptionTarget")
|
|
notifications Notification[] @relation("UserNotifications")
|
|
editedVersions ArticleVersion[] @relation("ArticleVersionEditor")
|
|
reviewedCommentAudits CommentAudit[] @relation("CommentAuditReviewer")
|
|
}
|
|
|
|
model OAuth2Client {
|
|
id Int @id @default(autoincrement())
|
|
clientId String @unique
|
|
clientSecret String
|
|
redirectUris String[] // JSON array of URIs
|
|
scopes String @default("read write")
|
|
createdAt DateTime @default(now())
|
|
|
|
tokens OAuth2Token[]
|
|
authCodes OAuth2AuthorizationCode[]
|
|
}
|
|
|
|
model OAuth2AuthorizationCode {
|
|
id Int @id @default(autoincrement())
|
|
codeHash String @unique
|
|
clientId String
|
|
userId Int?
|
|
redirectUri String
|
|
scopes String
|
|
expiresAt DateTime
|
|
used Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
|
|
client OAuth2Client @relation(fields: [clientId], references: [clientId])
|
|
user User? @relation(fields: [userId], references: [id])
|
|
}
|
|
|
|
model OAuth2Token {
|
|
id Int @id @default(autoincrement())
|
|
accessTokenHash String @unique
|
|
refreshTokenHash String? @unique
|
|
clientId String
|
|
userId Int?
|
|
scopes String
|
|
expiresAt DateTime
|
|
revoked Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
|
|
client OAuth2Client @relation(fields: [clientId], references: [clientId])
|
|
user User? @relation(fields: [userId], references: [id])
|
|
|
|
@@index([userId])
|
|
@@index([clientId])
|
|
}
|
|
|
|
model Article {
|
|
id Int @id @default(autoincrement())
|
|
slug String @unique
|
|
title String
|
|
content String? // Markdown content
|
|
excerpt String?
|
|
coverImageUrl String?
|
|
status String @default("draft") // draft, published, archived
|
|
visibility String @default("public") // public, unlisted, private
|
|
passwordHash String? // for password-protected articles
|
|
token String? @unique // for token-based access
|
|
viewCount Int @default(0) // number of views
|
|
publishedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
authorId Int
|
|
author User @relation(fields: [authorId], references: [id])
|
|
versions ArticleVersion[] @relation("ArticleVersions")
|
|
comments Comment[]
|
|
reactions Reaction[] @relation("ArticleReactions")
|
|
linkAccesses LinkAccess[] @relation("ArticleLinkAccess")
|
|
|
|
@@index([authorId])
|
|
}
|
|
|
|
model ArticleVersion {
|
|
id Int @id @default(autoincrement())
|
|
articleId Int
|
|
version Int
|
|
title String
|
|
content String?
|
|
excerpt String?
|
|
editorId Int?
|
|
createdAt DateTime @default(now())
|
|
|
|
article Article @relation("ArticleVersions", fields: [articleId], references: [id])
|
|
editor User? @relation("ArticleVersionEditor", fields: [editorId], references: [id])
|
|
|
|
@@unique([articleId, version])
|
|
}
|
|
|
|
model Comment {
|
|
id Int @id @default(autoincrement())
|
|
articleId Int
|
|
parentId Int?
|
|
content String
|
|
status String @default("pending") // pending, approved, rejected, suspicious
|
|
authorName String?
|
|
authorEmail String?
|
|
authorId Int? // registered user
|
|
ipAddress String?
|
|
userAgent String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
article Article @relation(fields: [articleId], references: [id])
|
|
parent Comment? @relation("CommentReplies", fields: [parentId], references: [id])
|
|
replies Comment[] @relation("CommentReplies")
|
|
audits CommentAudit[] @relation("CommentAudits")
|
|
author User? @relation(fields: [authorId], references: [id])
|
|
|
|
@@index([articleId, status, parentId])
|
|
@@index([authorId])
|
|
}
|
|
|
|
model CommentAudit {
|
|
id Int @id @default(autoincrement())
|
|
commentId Int
|
|
status String // approved, rejected, suspicious
|
|
reason String? // rule reason or AI decision
|
|
score Float? // AI confidence score
|
|
reviewerId Int? // null for AI
|
|
reviewedAt DateTime @default(now())
|
|
|
|
comment Comment @relation("CommentAudits", fields: [commentId], references: [id])
|
|
reviewer User? @relation("CommentAuditReviewer", fields: [reviewerId], references: [id])
|
|
|
|
@@unique([commentId])
|
|
}
|
|
|
|
model LinkAccess {
|
|
id Int @id @default(autoincrement())
|
|
token String @unique
|
|
articleId Int
|
|
expiresAt DateTime?
|
|
maxViews Int?
|
|
viewCount Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
|
|
article Article @relation("ArticleLinkAccess", fields: [articleId], references: [id])
|
|
}
|
|
|
|
model Reaction {
|
|
id Int @id @default(autoincrement())
|
|
articleId Int
|
|
userId Int
|
|
type String // like, love, wow, etc.
|
|
createdAt DateTime @default(now())
|
|
|
|
article Article @relation("ArticleReactions", fields: [articleId], references: [id])
|
|
user User @relation("UserReactions", fields: [userId], references: [id])
|
|
|
|
@@unique([userId, articleId, type])
|
|
}
|
|
|
|
model Follow {
|
|
id Int @id @default(autoincrement())
|
|
followerId Int
|
|
followeeId Int
|
|
createdAt DateTime @default(now())
|
|
|
|
follower User @relation("FollowFollower", fields: [followerId], references: [id])
|
|
followee User @relation("FollowFollowee", fields: [followeeId], references: [id])
|
|
|
|
@@unique([followerId, followeeId])
|
|
}
|
|
|
|
model Subscription {
|
|
id Int @id @default(autoincrement())
|
|
subscriberId Int
|
|
targetId Int?
|
|
type String // article, user, comment
|
|
createdAt DateTime @default(now())
|
|
|
|
subscriber User @relation("SubscriptionSubscriber", fields: [subscriberId], references: [id])
|
|
target User? @relation("SubscriptionTarget", fields: [targetId], references: [id])
|
|
|
|
@@unique([subscriberId, targetId, type])
|
|
}
|
|
|
|
model Notification {
|
|
id Int @id @default(autoincrement())
|
|
userId Int
|
|
type String // comment_reply, reaction, follow, system
|
|
title String
|
|
message String
|
|
isRead Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
|
|
user User @relation("UserNotifications", fields: [userId], references: [id])
|
|
|
|
@@index([userId])
|
|
}
|
|
|
|
model EmailMessage {
|
|
id Int @id @default(autoincrement())
|
|
to String
|
|
subject String
|
|
body String
|
|
status String @default("pending") // pending, sent, failed
|
|
attempts Int @default(0)
|
|
lastError String?
|
|
scheduledAt DateTime @default(now())
|
|
sentAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([status, createdAt])
|
|
}
|
|
|
|
model AnalyticsEvent {
|
|
id String @id @default(cuid())
|
|
type String // page_view, article_view, comment_submit, etc.
|
|
userId Int?
|
|
sessionId String?
|
|
ip String?
|
|
userAgent String?
|
|
data Json? // additional event data
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([type, createdAt])
|
|
@@index([userId])
|
|
@@index([sessionId])
|
|
}
|
|
|
|
model AnalyticsAggregate {
|
|
id Int @id @default(autoincrement())
|
|
date DateTime // truncated to day
|
|
type String // page_views, unique_visitors, etc.
|
|
value Int
|
|
metadata Json?
|
|
|
|
@@unique([date, type])
|
|
}
|