laobinghu 37742571ae feat: 完成项目初始化并重构部署流程
主要变更:
- 添加完整的项目结构和模块(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 密钥对支持
- 添加环境变量验证和数据库备份恢复功能
2026-03-28 16:53:25 +08:00

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