Files
novalon-website/docs/plans/2026-03-08-configurable-cms-design.md
T
张翔 0afc050e0c docs: 添加可配置化 CMS 系统设计文档
- 完成需求分析和技术选型
- 设计数据库结构和 API 接口
- 规划管理后台和权限体系
- 制定实施计划和测试策略
2026-03-08 20:05:23 +08:00

39 KiB
Raw Blame History

可配置化 CMS 系统设计文档

项目名称Novalon Website 可配置化内容管理系统
创建日期2026-03-08
版本v1.0
作者:张翔


📋 目录

  1. 项目概述
  2. 需求分析
  3. 技术选型
  4. 系统架构
  5. 数据库设计
  6. 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 配置示例:

// 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 (可选)

部署步骤

  1. 推送代码到 GitHub
  2. 在 Vercel 中导入项目
  3. 配置环境变量
  4. 部署成功

数据库备份

# 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 已批准