0afc050e0c
- 完成需求分析和技术选型 - 设计数据库结构和 API 接口 - 规划管理后台和权限体系 - 制定实施计划和测试策略
39 KiB
39 KiB
可配置化 CMS 系统设计文档
项目名称:Novalon Website 可配置化内容管理系统
创建日期:2026-03-08
版本:v1.0
作者:张翔
📋 目录
项目概述
背景
当前 Novalon Website 项目的新闻、产品、服务等内容数据硬编码在 src/lib/constants.ts 文件中,存在以下问题:
- ❌ 内容更新需要修改代码并重新部署
- ❌ 非技术人员无法自主管理内容
- ❌ 无法实时调整功能开关、样式配置
- ❌ 缺乏版本控制和审核机制
- ❌ SEO 配置分散,难以统一管理
目标
构建一个轻量级、易用、可扩展的内容管理系统(CMS),实现:
- ✅ 运营人员可自主管理内容(增删改查)
- ✅ 实时配置功能开关、样式、SEO
- ✅ 支持版本历史和内容回滚
- ✅ 权限分级管理(管理员/编辑/查看者)
- ✅ 保持现有前端架构不变,渐进式改造
成功标准
- 功能完整性:支持新闻、产品、服务、案例的完整 CRUD
- 易用性:运营人员无需培训即可上手使用
- 性能:管理后台响应时间 < 500ms,前端页面加载时间 < 2s
- 安全性:通过 OWASP Top 10 安全检查
- 可维护性:代码覆盖率 ≥ 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) 的优势:
- ✅ 零配置:无需安装数据库服务器,开箱即用
- ✅ 单文件存储:便于备份和迁移
- ✅ 性能优秀:对于中小型网站(< 10万条记录)性能足够
- ✅ 可迁移性:Drizzle ORM 支持无缝迁移到 PostgreSQL
- ✅ 成本最低:无需额外的数据库服务器费用
何时迁移到 PostgreSQL:
- 数据量超过 10 万条记录
- 并发写入超过 100 QPS
- 需要高级特性(全文搜索、JSON 索引等)
部署策略
混合模式(已确认):
| 页面类型 | 渲染方式 | 更新频率 | 理由 |
|---|---|---|---|
| 首页 | ISR | revalidate: 300s (5分钟) | 性能优先,内容更新不频繁 |
| 列表页 | ISR | revalidate: 300s | 平衡性能和实时性 |
| 详情页 | SSR | 实时 | SEO 优先,内容准确性要求高 |
| 管理后台 | CSR | 实时 | 交互性强,无需 SEO |
| API Routes | 动态 | 实时 | 数据操作实时性要求高 |
ISR 配置示例:
// src/app/(marketing)/news/page.tsx
export const revalidate = 300; // 5分钟重新验证
export default async function NewsPage() {
const news = await fetchNews();
return <NewsList news={news} />;
}
系统架构
整体架构图
┌─────────────────────────────────────────────────────────────┐
│ 客户端层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 前端页面 │ │ 管理后台 │ │ 移动端适配 │ │
│ │ (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)
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)
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)
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)
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)
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 定义
// 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<string[]>(),
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<Record<string, any>>(),
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<Record<string, any>>(),
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<Record<string, any>>(),
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
统一响应结构:
interface ApiResponse<T> {
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 实现示例
// 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<number>`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. 内容编辑器
// 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 (
<div className="border rounded-lg">
<MenuBar editor={editor} />
<EditorContent editor={editor} className="prose max-w-none p-4" />
</div>
);
}
2. 配置面板
// 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 (
<div className="space-y-6">
{configs?.map((config) => (
<ConfigField key={config.key} config={config} />
))}
</div>
);
}
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 (
<div className="border rounded-lg p-4">
<Label>{config.description}</Label>
{typeof value === 'boolean' ? (
<Switch checked={value} onCheckedChange={setValue} />
) : typeof value === 'number' ? (
<Input type="number" value={value} onChange={(e) => setValue(Number(e.target.value))} />
) : (
<Input value={value} onChange={(e) => setValue(e.target.value)} />
)}
<Button onClick={handleSave}>保存</Button>
</div>
);
}
3. 数据表格
// 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 (
<div>
<Table>
<TableHeader>
<TableRow>
<TableHead>标题</TableHead>
<TableHead>分类</TableHead>
<TableHead>状态</TableHead>
<TableHead>发布时间</TableHead>
<TableHead>操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data?.data?.map((item) => (
<TableRow key={item.id}>
<TableCell>{item.title}</TableCell>
<TableCell>{item.category}</TableCell>
<TableCell>
<Badge variant={item.status === 'published' ? 'default' : 'secondary'}>
{item.status}
</Badge>
</TableCell>
<TableCell>{formatDate(item.publishedAt)}</TableCell>
<TableCell>
<Button size="sm" variant="ghost" asChild>
<Link href={`/admin/content/${type}/${item.id}/edit`}>编辑</Link>
</Button>
<Button size="sm" variant="ghost" onClick={() => handleDelete(item.id)}>
删除
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Pagination
current={page}
total={data?.meta?.total}
pageSize={data?.meta?.pageSize}
onChange={setPage}
/>
</div>
);
}
权限体系
角色定义
| 角色 | 权限范围 | 说明 |
|---|---|---|
| admin | 全部权限 | 系统管理员,可管理用户、配置、所有内容 |
| editor | 内容管理 | 编辑人员,可创建、编辑、发布内容 |
| viewer | 只读权限 | 查看者,只能查看内容和配置 |
权限矩阵
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;
权限检查实现
// src/lib/auth/permissions.ts
import { getServerSession } from 'next-auth';
export async function checkPermission(
resource: string,
action: string
): Promise<boolean> {
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 }
);
}
// ... 创建内容逻辑
}
部署策略
环境配置
# .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"
数据库迁移
# 生成迁移文件
npm run db:generate
# 执行迁移
npm run db:migrate
# 查看数据库
npm run db:studio
生产部署
推荐平台:Vercel + Vercel KV (可选)
部署步骤:
- 推送代码到 GitHub
- 在 Vercel 中导入项目
- 配置环境变量
- 部署成功
数据库备份:
# 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 逻辑
- 数据验证
示例:
// __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(已有)
覆盖范围:
- 用户登录流程
- 内容创建流程
- 配置更新流程
示例:
// 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. 默认配置数据
{
"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. 数据库迁移脚本
// drizzle/migrations/0001_initial.ts
import { sql } from 'drizzle-orm';
import { drizzle } from 'drizzle-orm/libsql';
export async function up(db: ReturnType<typeof drizzle>) {
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<typeof drizzle>) {
await db.run(sql`DROP TABLE users`);
// ... 删除其他表
}
C. 参考资源
文档版本历史:
| 版本 | 日期 | 变更说明 | 作者 |
|---|---|---|---|
| v1.0 | 2026-03-08 | 初始版本 | 张翔 |
审批记录:
| 角色 | 姓名 | 日期 | 状态 |
|---|---|---|---|
| 技术负责人 | 张翔 | 2026-03-08 | ✅ 已批准 |