From 0afc050e0c0066724bf9757ed2c90a7d2973be84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Sun, 8 Mar 2026 20:05:23 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E5=8F=AF=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=8C=96=20CMS=20=E7=B3=BB=E7=BB=9F=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 完成需求分析和技术选型 - 设计数据库结构和 API 接口 - 规划管理后台和权限体系 - 制定实施计划和测试策略 --- .../2026-03-08-configurable-cms-design.md | 1260 +++++++++++++++++ 1 file changed, 1260 insertions(+) create mode 100644 docs/plans/2026-03-08-configurable-cms-design.md diff --git a/docs/plans/2026-03-08-configurable-cms-design.md b/docs/plans/2026-03-08-configurable-cms-design.md new file mode 100644 index 0000000..4dabc82 --- /dev/null +++ b/docs/plans/2026-03-08-configurable-cms-design.md @@ -0,0 +1,1260 @@ +# 可配置化 CMS 系统设计文档 + +**项目名称**:Novalon Website 可配置化内容管理系统 +**创建日期**:2026-03-08 +**版本**:v1.0 +**作者**:张翔 + +--- + +## 📋 目录 + +1. [项目概述](#项目概述) +2. [需求分析](#需求分析) +3. [技术选型](#技术选型) +4. [系统架构](#系统架构) +5. [数据库设计](#数据库设计) +6. [API 设计](#api-设计) +7. [管理后台设计](#管理后台设计) +8. [权限体系](#权限体系) +9. [部署策略](#部署策略) +10. [实施计划](#实施计划) +11. [测试策略](#测试策略) +12. [风险与应对](#风险与应对) + +--- + +## 项目概述 + +### 背景 + +当前 Novalon Website 项目的新闻、产品、服务等内容数据硬编码在 `src/lib/constants.ts` 文件中,存在以下问题: + +- ❌ 内容更新需要修改代码并重新部署 +- ❌ 非技术人员无法自主管理内容 +- ❌ 无法实时调整功能开关、样式配置 +- ❌ 缺乏版本控制和审核机制 +- ❌ SEO 配置分散,难以统一管理 + +### 目标 + +构建一个**轻量级、易用、可扩展**的内容管理系统(CMS),实现: + +- ✅ 运营人员可自主管理内容(增删改查) +- ✅ 实时配置功能开关、样式、SEO +- ✅ 支持版本历史和内容回滚 +- ✅ 权限分级管理(管理员/编辑/查看者) +- ✅ 保持现有前端架构不变,渐进式改造 + +### 成功标准 + +1. **功能完整性**:支持新闻、产品、服务、案例的完整 CRUD +2. **易用性**:运营人员无需培训即可上手使用 +3. **性能**:管理后台响应时间 < 500ms,前端页面加载时间 < 2s +4. **安全性**:通过 OWASP Top 10 安全检查 +5. **可维护性**:代码覆盖率 ≥ 80%,文档完善 + +--- + +## 需求分析 + +### 功能需求矩阵 + +| 功能模块 | 子功能 | 优先级 | 角色 | +|---------|--------|-------|------| +| **内容管理** | 新闻 CRUD | P0 | 编辑、管理员 | +| | 产品 CRUD | P0 | 编辑、管理员 | +| | 服务 CRUD | P0 | 编辑、管理员 | +| | 案例管理 | P1 | 编辑、管理员 | +| | 富文本编辑 | P0 | 编辑、管理员 | +| | 图片上传 | P0 | 编辑、管理员 | +| | 定时发布 | P1 | 编辑、管理员 | +| **配置中心** | 功能开关 | P0 | 管理员 | +| | 样式配置 | P1 | 管理员 | +| | SEO 配置 | P0 | 管理员 | +| | 全局设置 | P0 | 管理员 | +| **系统管理** | 用户管理 | P0 | 管理员 | +| | 角色权限 | P0 | 管理员 | +| | 版本历史 | P1 | 编辑、管理员 | +| | 操作日志 | P1 | 管理员 | +| **仪表盘** | 数据统计 | P1 | 所有角色 | +| | 最近动态 | P1 | 所有角色 | + +### 非功能需求 + +- **性能**:支持 100+ 并发用户,响应时间 < 500ms +- **可用性**:99.9% 可用性(月停机时间 < 43 分钟) +- **安全性**:符合 OWASP Top 10,支持 HTTPS,防止 SQL 注入、XSS +- **可扩展性**:支持水平扩展,数据库可迁移到 PostgreSQL +- **兼容性**:支持 Chrome、Firefox、Safari、Edge 最新版本 + +--- + +## 技术选型 + +### 核心技术栈 + +| 类别 | 技术 | 版本 | 选择理由 | +|-----|------|------|---------| +| **框架** | Next.js | 16.x | 已有技术栈,支持 SSR/ISR/API Routes | +| **UI 库** | React | 19.x | 已有技术栈 | +| **语言** | TypeScript | 5.x | 已有技术栈,类型安全 | +| **样式** | Tailwind CSS | 4.x | 已有技术栈 | +| **组件库** | shadcn/ui | latest | 已有技术栈,一致性 | +| **数据库** | SQLite (libsql) | latest | ✅ **已确认**:零配置、单文件、可迁移 | +| **ORM** | Drizzle ORM | latest | 轻量、TypeScript 友好、性能优 | +| **验证** | Zod | 4.x | 已有技术栈 | +| **认证** | NextAuth.js | 5.x | ✅ **已确认**:邮箱密码 + Magic Link | +| **富文本** | Tiptap | 2.x | 现代化、可扩展、协作编辑 | +| **文件上传** | UploadThing | latest | 简单、支持本地/S3 | +| **图表** | @antv/g2 | 5.x | 已有依赖 | +| **动画** | Framer Motion | 12.x | 已有依赖 | + +### 数据库选择理由 + +**SQLite (libsql) 的优势:** + +1. ✅ **零配置**:无需安装数据库服务器,开箱即用 +2. ✅ **单文件存储**:便于备份和迁移 +3. ✅ **性能优秀**:对于中小型网站(< 10万条记录)性能足够 +4. ✅ **可迁移性**:Drizzle ORM 支持无缝迁移到 PostgreSQL +5. ✅ **成本最低**:无需额外的数据库服务器费用 + +**何时迁移到 PostgreSQL:** + +- 数据量超过 10 万条记录 +- 并发写入超过 100 QPS +- 需要高级特性(全文搜索、JSON 索引等) + +### 部署策略 + +**混合模式**(已确认): + +| 页面类型 | 渲染方式 | 更新频率 | 理由 | +|---------|---------|---------|------| +| **首页** | ISR | revalidate: 300s (5分钟) | 性能优先,内容更新不频繁 | +| **列表页** | ISR | revalidate: 300s | 平衡性能和实时性 | +| **详情页** | SSR | 实时 | SEO 优先,内容准确性要求高 | +| **管理后台** | CSR | 实时 | 交互性强,无需 SEO | +| **API Routes** | 动态 | 实时 | 数据操作实时性要求高 | + +**ISR 配置示例:** + +```typescript +// src/app/(marketing)/news/page.tsx +export const revalidate = 300; // 5分钟重新验证 + +export default async function NewsPage() { + const news = await fetchNews(); + return ; +} +``` + +--- + +## 系统架构 + +### 整体架构图 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 客户端层 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 前端页面 │ │ 管理后台 │ │ 移动端适配 │ │ +│ │ (ISR/SSR) │ │ (CSR) │ │ (响应式) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ↓ HTTPS +┌─────────────────────────────────────────────────────────────┐ +│ Next.js 应用层 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Pages │ │ API Routes │ │ Middleware │ │ +│ │ (App Router)│ │ (/api/*) │ │ (Auth/CORS) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 业务逻辑层 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Config │ │ Content │ │ Auth │ │ +│ │ Manager │ │ Service │ │ Service │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 数据访问层 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Drizzle ORM │ │ Zod Schema │ │ Cache Layer │ │ +│ │ (Type-safe) │ │ (Validation) │ │ (In-memory) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 数据存储层 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ SQLite │ │ 文件系统 │ │ CDN/对象存储 │ │ +│ │ (libsql) │ │ (uploads/) │ │ (可选) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 数据流设计 + +#### 1. 内容发布流程 + +``` +运营人员 → 管理后台 → 编辑内容 → 保存草稿 + ↓ +提交审核 → 管理员审核 → 发布 → API 验证 + ↓ +数据库更新 → 清除缓存 → ISR 重新生成 → 前端展示 +``` + +#### 2. 配置更新流程 + +``` +管理员 → 配置中心 → 修改配置 → API 验证 + ↓ +数据库更新 → 清除缓存 → 前端重新加载配置 + ↓ +实时生效(无需重新部署) +``` + +#### 3. 用户认证流程 + +``` +用户 → 登录页 → 选择登录方式 + ↓ +┌─────────────┬─────────────┐ +│ 邮箱密码 │ Magic Link │ +│ 验证密码 │ 发送邮件 │ +│ 生成 Token │ 点击链接 │ +└─────────────┴─────────────┘ + ↓ +创建 Session → 重定向到管理后台 +``` + +--- + +## 数据库设计 + +### ER 图 + +``` +┌─────────────┐ ┌─────────────┐ +│ users │ │ roles │ +├─────────────┤ ├─────────────┤ +│ id (PK) │ │ id (PK) │ +│ email │ │ name │ +│ passwordHash│ │ permissions │ +│ roleId (FK) │──────▶│ createdAt │ +│ createdAt │ └─────────────┘ +└─────────────┘ + │ + │ 1:N + ▼ +┌─────────────┐ ┌─────────────┐ +│ content │ │ versions │ +├─────────────┤ ├─────────────┤ +│ id (PK) │ │ id (PK) │ +│ type │ │ contentId │ +│ title │ │ version │ +│ slug │ │ changes │ +│ content │──────▶│ changedBy │ +│ authorId │ │ changedAt │ +│ status │ └─────────────┘ +│ publishedAt │ +│ sortOrder │ +└─────────────┘ + │ + │ 1:N + ▼ +┌─────────────┐ +│ audit_logs │ +├─────────────┤ +│ id (PK) │ +│ userId │ +│ action │ +│ resourceType│ +│ resourceId │ +│ timestamp │ +└─────────────┘ + +┌─────────────┐ +│ site_config │ +├─────────────┤ +│ id (PK) │ +│ key │ +│ value (JSON)│ +│ category │ +│ updatedAt │ +└─────────────┘ +``` + +### 表结构详细设计 + +#### 1. 用户表 (users) + +```sql +CREATE TABLE users ( + id TEXT PRIMARY KEY, + email TEXT UNIQUE NOT NULL, + password_hash TEXT, + name TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'editor', -- 'admin', 'editor', 'viewer' + avatar TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); + +CREATE INDEX idx_users_email ON users(email); +``` + +#### 2. 内容表 (content) + +```sql +CREATE TABLE content ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL, -- 'news', 'product', 'service', 'case' + title TEXT NOT NULL, + slug TEXT UNIQUE NOT NULL, + excerpt TEXT, + content TEXT NOT NULL, + cover_image TEXT, + category TEXT, + tags TEXT, -- JSON array + status TEXT NOT NULL DEFAULT 'draft', -- 'draft', 'published', 'archived' + published_at INTEGER, + author_id TEXT NOT NULL, + sort_order INTEGER DEFAULT 0, + metadata TEXT, -- JSON for SEO, custom fields + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + FOREIGN KEY (author_id) REFERENCES users(id) +); + +CREATE INDEX idx_content_type ON content(type); +CREATE INDEX idx_content_status ON content(status); +CREATE INDEX idx_content_slug ON content(slug); +CREATE INDEX idx_content_published ON content(published_at DESC); +``` + +#### 3. 版本历史表 (content_versions) + +```sql +CREATE TABLE content_versions ( + id TEXT PRIMARY KEY, + content_id TEXT NOT NULL, + version INTEGER NOT NULL, + title TEXT NOT NULL, + content TEXT NOT NULL, + changes TEXT, -- JSON diff + changed_by TEXT NOT NULL, + changed_at INTEGER NOT NULL, + FOREIGN KEY (content_id) REFERENCES content(id), + FOREIGN KEY (changed_by) REFERENCES users(id) +); + +CREATE INDEX idx_versions_content ON content_versions(content_id, version DESC); +``` + +#### 4. 站点配置表 (site_config) + +```sql +CREATE TABLE site_config ( + id TEXT PRIMARY KEY, + key TEXT UNIQUE NOT NULL, + value TEXT NOT NULL, -- JSON + category TEXT NOT NULL, -- 'feature', 'style', 'seo', 'general' + description TEXT, + updated_at INTEGER NOT NULL, + updated_by TEXT, + FOREIGN KEY (updated_by) REFERENCES users(id) +); + +CREATE INDEX idx_config_key ON site_config(key); +CREATE INDEX idx_config_category ON site_config(category); +``` + +#### 5. 操作日志表 (audit_logs) + +```sql +CREATE TABLE audit_logs ( + id TEXT PRIMARY KEY, + user_id TEXT, + action TEXT NOT NULL, -- 'create', 'update', 'delete', 'publish', 'login' + resource_type TEXT NOT NULL, + resource_id TEXT, + details TEXT, -- JSON + ip_address TEXT, + user_agent TEXT, + timestamp INTEGER NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) +); + +CREATE INDEX idx_logs_user ON audit_logs(user_id); +CREATE INDEX idx_logs_timestamp ON audit_logs(timestamp DESC); +``` + +### Drizzle Schema 定义 + +```typescript +// src/db/schema.ts +import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; +import { relations } from 'drizzle-orm'; + +export const users = sqliteTable('users', { + id: text('id').primaryKey(), + email: text('email').notNull().unique(), + passwordHash: text('password_hash'), + name: text('name').notNull(), + role: text('role', { enum: ['admin', 'editor', 'viewer'] }).notNull().default('editor'), + avatar: text('avatar'), + createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), + updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), +}); + +export const content = sqliteTable('content', { + id: text('id').primaryKey(), + type: text('type', { enum: ['news', 'product', 'service', 'case'] }).notNull(), + title: text('title').notNull(), + slug: text('slug').notNull().unique(), + excerpt: text('excerpt'), + content: text('content').notNull(), + coverImage: text('cover_image'), + category: text('category'), + tags: text('tags', { mode: 'json' }).$type(), + status: text('status', { enum: ['draft', 'published', 'archived'] }).notNull().default('draft'), + publishedAt: integer('published_at', { mode: 'timestamp' }), + authorId: text('author_id').notNull().references(() => users.id), + sortOrder: integer('sort_order').default(0), + metadata: text('metadata', { mode: 'json' }).$type>(), + createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), + updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), +}); + +export const contentVersions = sqliteTable('content_versions', { + id: text('id').primaryKey(), + contentId: text('content_id').notNull().references(() => content.id), + version: integer('version').notNull(), + title: text('title').notNull(), + content: text('content').notNull(), + changes: text('changes', { mode: 'json' }).$type>(), + changedBy: text('changed_by').notNull().references(() => users.id), + changedAt: integer('changed_at', { mode: 'timestamp' }).notNull(), +}); + +export const siteConfig = sqliteTable('site_config', { + id: text('id').primaryKey(), + key: text('key').notNull().unique(), + value: text('value', { mode: 'json' }).notNull(), + category: text('category', { enum: ['feature', 'style', 'seo', 'general'] }).notNull(), + description: text('description'), + updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), + updatedBy: text('updated_by').references(() => users.id), +}); + +export const auditLogs = sqliteTable('audit_logs', { + id: text('id').primaryKey(), + userId: text('user_id').references(() => users.id), + action: text('action', { enum: ['create', 'update', 'delete', 'publish', 'login'] }).notNull(), + resourceType: text('resource_type').notNull(), + resourceId: text('resource_id'), + details: text('details', { mode: 'json' }).$type>(), + ipAddress: text('ip_address'), + userAgent: text('user_agent'), + timestamp: integer('timestamp', { mode: 'timestamp' }).notNull(), +}); + +// Relations +export const usersRelations = relations(users, ({ many }) => ({ + content: many(content), + versions: many(contentVersions), + logs: many(auditLogs), +})); + +export const contentRelations = relations(content, ({ one, many }) => ({ + author: one(users, { + fields: [content.authorId], + references: [users.id], + }), + versions: many(contentVersions), +})); +``` + +--- + +## API 设计 + +### RESTful API 规范 + +**基础路径**:`/api/v1` + +**认证方式**:Bearer Token (NextAuth.js Session) + +**响应格式**:JSON + +**统一响应结构**: + +```typescript +interface ApiResponse { + success: boolean; + data?: T; + error?: { + code: string; + message: string; + details?: any; + }; + meta?: { + total?: number; + page?: number; + pageSize?: number; + }; +} +``` + +### API 端点列表 + +#### 1. 认证 API + +``` +POST /api/auth/register # 用户注册 +POST /api/auth/login # 邮箱密码登录 +POST /api/auth/magic-link # 发送 Magic Link +POST /api/auth/verify # 验证 Magic Link +POST /api/auth/logout # 登出 +GET /api/auth/session # 获取当前会话 +``` + +#### 2. 内容管理 API + +``` +GET /api/v1/content # 获取内容列表(支持分页、筛选) +GET /api/v1/content/:id # 获取单个内容 +POST /api/v1/content # 创建内容 +PUT /api/v1/content/:id # 更新内容 +DELETE /api/v1/content/:id # 删除内容 +POST /api/v1/content/:id/publish # 发布内容 +GET /api/v1/content/:id/versions # 获取版本历史 +POST /api/v1/content/:id/rollback # 回滚到指定版本 +``` + +**查询参数**: + +``` +?type=news&status=published&category=公司新闻&page=1&pageSize=10&sort=-publishedAt +``` + +#### 3. 配置管理 API + +``` +GET /api/v1/config # 获取所有配置 +GET /api/v1/config/:key # 获取单个配置 +PUT /api/v1/config/:key # 更新配置 +GET /api/v1/config/category/:category # 按类别获取配置 +``` + +#### 4. 文件上传 API + +``` +POST /api/v1/upload # 上传文件 +DELETE /api/v1/upload/:id # 删除文件 +``` + +#### 5. 用户管理 API + +``` +GET /api/v1/users # 获取用户列表 +GET /api/v1/users/:id # 获取用户详情 +PUT /api/v1/users/:id # 更新用户信息 +DELETE /api/v1/users/:id # 删除用户 +``` + +#### 6. 操作日志 API + +``` +GET /api/v1/logs # 获取操作日志 +``` + +### API 实现示例 + +```typescript +// src/app/api/v1/content/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { db } from '@/db'; +import { content } from '@/db/schema'; +import { eq, and, desc, sql } from 'drizzle-orm'; +import { z } from 'zod'; + +const ContentQuerySchema = z.object({ + type: z.enum(['news', 'product', 'service', 'case']).optional(), + status: z.enum(['draft', 'published', 'archived']).optional(), + category: z.string().optional(), + page: z.coerce.number().int().positive().default(1), + pageSize: z.coerce.number().int().positive().max(100).default(10), + sort: z.string().default('-publishedAt'), +}); + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const query = ContentQuerySchema.parse(Object.fromEntries(searchParams)); + + const whereConditions = []; + if (query.type) whereConditions.push(eq(content.type, query.type)); + if (query.status) whereConditions.push(eq(content.status, query.status)); + if (query.category) whereConditions.push(eq(content.category, query.category)); + + const [items, [{ count }]] = await Promise.all([ + db.select() + .from(content) + .where(whereConditions.length > 0 ? and(...whereConditions) : undefined) + .orderBy(desc(content.publishedAt)) + .limit(query.pageSize) + .offset((query.page - 1) * query.pageSize), + db.select({ count: sql`count(*)` }) + .from(content) + .where(whereConditions.length > 0 ? and(...whereConditions) : undefined), + ]); + + return NextResponse.json({ + success: true, + data: items, + meta: { + total: count, + page: query.page, + pageSize: query.pageSize, + }, + }); + } catch (error) { + return NextResponse.json( + { success: false, error: { code: 'INVALID_QUERY', message: error.message } }, + { status: 400 } + ); + } +} + +export async function POST(request: NextRequest) { + try { + const session = await getServerSession(); + if (!session) { + return NextResponse.json( + { success: false, error: { code: 'UNAUTHORIZED', message: '请先登录' } }, + { status: 401 } + ); + } + + const body = await request.json(); + const newContent = await db.insert(content).values({ + ...body, + id: crypto.randomUUID(), + authorId: session.user.id, + createdAt: new Date(), + updatedAt: new Date(), + }).returning(); + + return NextResponse.json({ success: true, data: newContent[0] }, { status: 201 }); + } catch (error) { + return NextResponse.json( + { success: false, error: { code: 'CREATE_FAILED', message: error.message } }, + { status: 500 } + ); + } +} +``` + +--- + +## 管理后台设计 + +### 页面结构 + +``` +/admin +├── /login # 登录页 +├── /dashboard # 仪表盘 +├── /content +│ ├── /news # 新闻管理 +│ │ ├── / # 列表页 +│ │ ├── /create # 创建页 +│ │ └── /[id]/edit # 编辑页 +│ ├── /products # 产品管理 +│ ├── /services # 服务管理 +│ └── /cases # 案例管理 +├── /config +│ ├── /features # 功能开关 +│ ├── /style # 样式配置 +│ ├── /seo # SEO 配置 +│ └── /general # 全局设置 +├── /users # 用户管理 +└── /logs # 操作日志 +``` + +### 布局设计 + +``` +┌─────────────────────────────────────────────────────────┐ +│ Logo 首页 内容 配置 用户 日志 用户头像 ▼ │ +├──────────┬──────────────────────────────────────────────┤ +│ │ │ +│ 侧边栏 │ 主内容区 │ +│ │ │ +│ - 新闻 │ ┌──────────────────────────────────────┐ │ +│ - 产品 │ │ 页面标题 [创建] [导出] │ │ +│ - 服务 │ ├──────────────────────────────────────┤ │ +│ - 案例 │ │ │ │ +│ │ │ 数据表格 / 表单 / 图表 │ │ +│ 配置 │ │ │ │ +│ - 功能 │ │ │ │ +│ - 样式 │ │ │ │ +│ - SEO │ └──────────────────────────────────────┘ │ +│ │ │ +└──────────┴──────────────────────────────────────────────┘ +``` + +### 核心组件 + +#### 1. 内容编辑器 + +```typescript +// src/components/admin/content-editor.tsx +import { Editor } from '@tiptap/react'; +import StarterKit from '@tiptap/starter-kit'; +import Image from '@tiptap/extension-image'; +import Link from '@tiptap/extension-link'; + +export function ContentEditor({ value, onChange }: ContentEditorProps) { + const editor = useEditor({ + extensions: [StarterKit, Image, Link], + content: value, + onUpdate: ({ editor }) => { + onChange(editor.getHTML()); + }, + }); + + return ( +
+ + +
+ ); +} +``` + +#### 2. 配置面板 + +```typescript +// src/components/admin/config-panel.tsx +export function ConfigPanel({ category }: ConfigPanelProps) { + const { data: configs, isLoading } = useQuery({ + queryKey: ['configs', category], + queryFn: () => fetch(`/api/v1/config/category/${category}`).then(r => r.json()), + }); + + return ( +
+ {configs?.map((config) => ( + + ))} +
+ ); +} + +function ConfigField({ config }: ConfigFieldProps) { + const [value, setValue] = useState(config.value); + + const handleSave = async () => { + await fetch(`/api/v1/config/${config.key}`, { + method: 'PUT', + body: JSON.stringify({ value }), + }); + }; + + return ( +
+ + {typeof value === 'boolean' ? ( + + ) : typeof value === 'number' ? ( + setValue(Number(e.target.value))} /> + ) : ( + setValue(e.target.value)} /> + )} + +
+ ); +} +``` + +#### 3. 数据表格 + +```typescript +// src/components/admin/data-table.tsx +import { useQuery } from '@tanstack/react-query'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; + +export function DataTable({ type }: DataTableProps) { + const [page, setPage] = useState(1); + const { data, isLoading } = useQuery({ + queryKey: ['content', type, page], + queryFn: () => fetch(`/api/v1/content?type=${type}&page=${page}`).then(r => r.json()), + }); + + return ( +
+ + + + 标题 + 分类 + 状态 + 发布时间 + 操作 + + + + {data?.data?.map((item) => ( + + {item.title} + {item.category} + + + {item.status} + + + {formatDate(item.publishedAt)} + + + + + + ))} + +
+ +
+ ); +} +``` + +--- + +## 权限体系 + +### 角色定义 + +| 角色 | 权限范围 | 说明 | +|-----|---------|------| +| **admin** | 全部权限 | 系统管理员,可管理用户、配置、所有内容 | +| **editor** | 内容管理 | 编辑人员,可创建、编辑、发布内容 | +| **viewer** | 只读权限 | 查看者,只能查看内容和配置 | + +### 权限矩阵 + +```typescript +const PERMISSIONS = { + admin: { + content: ['create', 'read', 'update', 'delete', 'publish'], + config: ['read', 'update'], + users: ['create', 'read', 'update', 'delete'], + logs: ['read'], + }, + editor: { + content: ['create', 'read', 'update', 'publish'], + config: ['read'], + users: [], + logs: ['read'], + }, + viewer: { + content: ['read'], + config: ['read'], + users: [], + logs: [], + }, +} as const; +``` + +### 权限检查实现 + +```typescript +// src/lib/auth/permissions.ts +import { getServerSession } from 'next-auth'; + +export async function checkPermission( + resource: string, + action: string +): Promise { + const session = await getServerSession(); + if (!session) return false; + + const userRole = session.user.role; + const permissions = PERMISSIONS[userRole]; + + return permissions[resource]?.includes(action) ?? false; +} + +// API Route 中使用 +export async function POST(request: NextRequest) { + if (!await checkPermission('content', 'create')) { + return NextResponse.json( + { success: false, error: { code: 'FORBIDDEN', message: '无权限' } }, + { status: 403 } + ); + } + // ... 创建内容逻辑 +} +``` + +--- + +## 部署策略 + +### 环境配置 + +```bash +# .env.local +DATABASE_URL="file:./data.db" +NEXTAUTH_SECRET="your-secret-key" +NEXTAUTH_URL="http://localhost:3000" + +# 可选:文件上传 +UPLOADTHING_SECRET="your-uploadthing-secret" +UPLOADTHING_APP_ID="your-app-id" +``` + +### 数据库迁移 + +```bash +# 生成迁移文件 +npm run db:generate + +# 执行迁移 +npm run db:migrate + +# 查看数据库 +npm run db:studio +``` + +### 生产部署 + +**推荐平台**:Vercel + Vercel KV (可选) + +**部署步骤**: + +1. 推送代码到 GitHub +2. 在 Vercel 中导入项目 +3. 配置环境变量 +4. 部署成功 + +**数据库备份**: + +```bash +# SQLite 备份 +cp data.db data.db.backup + +# 或使用脚本 +npm run db:backup +``` + +--- + +## 实施计划 + +### 阶段一:基础架构(2 天) + +**目标**:搭建数据库、认证系统、基础 API + +**任务清单**: + +- [ ] 安装依赖(Drizzle ORM、NextAuth.js、Tiptap) +- [ ] 创建数据库 schema +- [ ] 配置数据库连接 +- [ ] 实现认证系统(邮箱密码 + Magic Link) +- [ ] 创建基础 API Routes +- [ ] 编写单元测试 + +**验收标准**: + +- ✅ 数据库表创建成功 +- ✅ 用户可以注册、登录 +- ✅ API 基础框架可用 + +--- + +### 阶段二:内容管理(2 天) + +**目标**:实现内容 CRUD 和管理界面 + +**任务清单**: + +- [ ] 实现内容 CRUD API +- [ ] 创建管理后台布局 +- [ ] 实现新闻管理界面 +- [ ] 实现产品管理界面 +- [ ] 实现服务管理界面 +- [ ] 集成富文本编辑器 +- [ ] 实现图片上传功能 + +**验收标准**: + +- ✅ 可以创建、编辑、删除内容 +- ✅ 富文本编辑器正常工作 +- ✅ 图片上传成功 + +--- + +### 阶段三:配置中心(1 天) + +**目标**:实现配置管理和功能开关 + +**任务清单**: + +- [ ] 实现配置 CRUD API +- [ ] 创建功能开关界面 +- [ ] 创建样式配置界面 +- [ ] 创建 SEO 配置界面 +- [ ] 实现配置加载器 +- [ ] 前端集成配置 + +**验收标准**: + +- ✅ 可以动态启用/禁用功能 +- ✅ 配置实时生效 + +--- + +### 阶段四:高级功能(1-2 天) + +**目标**:实现版本历史、操作日志、实时预览 + +**任务清单**: + +- [ ] 实现版本历史 API +- [ ] 创建版本对比界面 +- [ ] 实现内容回滚 +- [ ] 实现操作日志记录 +- [ ] 创建日志查询界面 +- [ ] 实现实时预览功能 + +**验收标准**: + +- ✅ 可以查看历史版本 +- ✅ 可以回滚到指定版本 +- ✅ 操作日志完整记录 + +--- + +### 阶段五:测试和部署(1 天) + +**目标**:完善测试、性能优化、部署上线 + +**任务清单**: + +- [ ] 编写 E2E 测试 +- [ ] 性能优化(缓存、懒加载) +- [ ] 安全审计(OWASP Top 10) +- [ ] 编写部署文档 +- [ ] 配置 CI/CD +- [ ] 生产环境部署 + +**验收标准**: + +- ✅ 测试覆盖率 ≥ 80% +- ✅ 性能指标达标 +- ✅ 安全检查通过 +- ✅ 成功部署上线 + +--- + +## 测试策略 + +### 单元测试 + +**框架**:Vitest + Testing Library + +**覆盖范围**: + +- 工具函数 +- React Hooks +- API 逻辑 +- 数据验证 + +**示例**: + +```typescript +// __tests__/lib/config-manager.test.ts +import { describe, it, expect } from 'vitest'; +import { ConfigManager } from '@/lib/config-manager'; + +describe('ConfigManager', () => { + it('should load config from database', async () => { + const config = await ConfigManager.get('feature_news'); + expect(config).toBeDefined(); + expect(config.enabled).toBe(true); + }); +}); +``` + +### 集成测试 + +**框架**:Vitest + MSW (Mock Service Worker) + +**覆盖范围**: + +- API Routes +- 数据库操作 +- 认证流程 + +### E2E 测试 + +**框架**:Playwright(已有) + +**覆盖范围**: + +- 用户登录流程 +- 内容创建流程 +- 配置更新流程 + +**示例**: + +```typescript +// e2e/tests/admin/content.spec.ts +import { test, expect } from '@playwright/test'; + +test('should create news article', async ({ page }) => { + await page.goto('/admin/login'); + await page.fill('input[name="email"]', 'admin@example.com'); + await page.fill('input[name="password"]', 'password'); + await page.click('button[type="submit"]'); + + await page.goto('/admin/content/news/create'); + await page.fill('input[name="title"]', '测试新闻'); + await page.fill('textarea[name="excerpt"]', '这是测试新闻摘要'); + await page.click('button[type="submit"]'); + + await expect(page.locator('text=创建成功')).toBeVisible(); +}); +``` + +--- + +## 风险与应对 + +### 技术风险 + +| 风险 | 影响 | 概率 | 应对措施 | +|-----|------|------|---------| +| SQLite 性能瓶颈 | 高 | 低 | 监控性能,准备迁移到 PostgreSQL | +| 文件上传安全漏洞 | 高 | 中 | 严格验证文件类型、大小,使用 CDN | +| 认证系统漏洞 | 高 | 低 | 使用成熟的 NextAuth.js,定期更新 | +| 数据库迁移失败 | 中 | 低 | 完善备份策略,测试迁移脚本 | + +### 业务风险 + +| 风险 | 影响 | 概率 | 应对措施 | +|-----|------|------|---------| +| 运营人员不会使用 | 中 | 中 | 编写详细操作手册,提供培训 | +| 内容误删 | 高 | 中 | 实现软删除、版本历史、回收站 | +| 配置错误导致网站异常 | 高 | 低 | 配置验证、预览功能、快速回滚 | + +### 项目风险 + +| 风险 | 影响 | 概率 | 应对措施 | +|-----|------|------|---------| +| 开发周期延误 | 中 | 中 | 采用敏捷开发,优先核心功能 | +| 需求变更 | 中 | 高 | 模块化设计,预留扩展接口 | +| 技术债务累积 | 中 | 中 | 代码审查、持续重构、完善文档 | + +--- + +## 附录 + +### A. 默认配置数据 + +```json +{ + "feature_news": { + "enabled": true, + "displayCount": 6, + "categories": ["公司新闻", "产品发布", "合作动态", "行业资讯"], + "sortOrder": "desc" + }, + "feature_products": { + "enabled": true, + "showPricing": true, + "featuredProducts": ["erp", "crm"] + }, + "feature_services": { + "enabled": true, + "items": ["software", "cloud", "data", "security"] + }, + "seo_default": { + "title": "四川睿新致远科技有限公司 - 企业数字化转型服务商", + "description": "以智慧连接数字趋势,以伙伴身份陪您成长", + "keywords": ["数字化转型", "软件开发", "云服务", "数据分析"] + } +} +``` + +### B. 数据库迁移脚本 + +```typescript +// drizzle/migrations/0001_initial.ts +import { sql } from 'drizzle-orm'; +import { drizzle } from 'drizzle-orm/libsql'; + +export async function up(db: ReturnType) { + await db.run(sql` + CREATE TABLE users ( + id TEXT PRIMARY KEY, + email TEXT UNIQUE NOT NULL, + password_hash TEXT, + name TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'editor', + avatar TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ); + `); + // ... 其他表 +} + +export async function down(db: ReturnType) { + await db.run(sql`DROP TABLE users`); + // ... 删除其他表 +} +``` + +### C. 参考资源 + +- [Next.js 文档](https://nextjs.org/docs) +- [Drizzle ORM 文档](https://orm.drizzle.team/docs/overview) +- [NextAuth.js 文档](https://next-auth.js.org/) +- [Tiptap 文档](https://tiptap.dev/) +- [shadcn/ui 文档](https://ui.shadcn.com/) + +--- + +**文档版本历史**: + +| 版本 | 日期 | 变更说明 | 作者 | +|-----|------|---------|------| +| v1.0 | 2026-03-08 | 初始版本 | 张翔 | + +--- + +**审批记录**: + +| 角色 | 姓名 | 日期 | 状态 | +|-----|------|------|------| +| 技术负责人 | 张翔 | 2026-03-08 | ✅ 已批准 |