0afc050e0c
- 完成需求分析和技术选型 - 设计数据库结构和 API 接口 - 规划管理后台和权限体系 - 制定实施计划和测试策略
1261 lines
39 KiB
Markdown
1261 lines
39 KiB
Markdown
# 可配置化 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 <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)
|
||
|
||
```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<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
|
||
|
||
**统一响应结构**:
|
||
|
||
```typescript
|
||
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 实现示例
|
||
|
||
```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<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. 内容编辑器
|
||
|
||
```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 (
|
||
<div className="border rounded-lg">
|
||
<MenuBar editor={editor} />
|
||
<EditorContent editor={editor} className="prose max-w-none p-4" />
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
#### 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 (
|
||
<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. 数据表格
|
||
|
||
```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 (
|
||
<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** | 只读权限 | 查看者,只能查看内容和配置 |
|
||
|
||
### 权限矩阵
|
||
|
||
```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<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 }
|
||
);
|
||
}
|
||
// ... 创建内容逻辑
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 部署策略
|
||
|
||
### 环境配置
|
||
|
||
```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<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. 参考资源
|
||
|
||
- [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 | ✅ 已批准 |
|