# 可配置化 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 | ✅ 已批准 |