From 6a2c4fdae80c266b5f173cb6c1a6e40a970c32db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Sun, 8 Mar 2026 20:10:50 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E5=8F=AF=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=8C=96=20CMS=20=E7=B3=BB=E7=BB=9F=E5=AE=9E=E6=96=BD?= =?UTF-8?q?=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 详细规划 16 个实施任务 - 包含完整的代码示例和测试用例 - 分 6 个阶段,预计 7-8 天完成 --- ...6-03-08-configurable-cms-implementation.md | 2182 +++++++++++++++++ 1 file changed, 2182 insertions(+) create mode 100644 docs/plans/2026-03-08-configurable-cms-implementation.md diff --git a/docs/plans/2026-03-08-configurable-cms-implementation.md b/docs/plans/2026-03-08-configurable-cms-implementation.md new file mode 100644 index 0000000..b7f977f --- /dev/null +++ b/docs/plans/2026-03-08-configurable-cms-implementation.md @@ -0,0 +1,2182 @@ +# 可配置化 CMS 系统实施计划 + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**目标:** 为 Novalon Website 构建一个轻量级、可配置的内容管理系统,实现新闻、产品、服务、案例的动态管理和配置。 + +**架构:** 采用 Next.js API Routes + SQLite (libsql) + Drizzle ORM + NextAuth.js 的技术栈,实现前后端分离的 CMS 架构。管理后台使用客户端渲染(CSR),前端页面使用增量静态再生(ISR)和服务器端渲染(SSR)混合模式。 + +**技术栈:** Next.js 16, React 19, TypeScript, SQLite (libsql), Drizzle ORM, NextAuth.js, Tiptap, shadcn/ui, Zod, Vitest, Playwright + +--- + +## 前置准备 + +### 环境要求 + +- Node.js 18+ +- npm / pnpm +- Git + +### 设计文档 + +参考:`docs/plans/2026-03-08-configurable-cms-design.md` + +--- + +## 阶段一:基础架构搭建(预计 2 天) + +### Task 1: 安装依赖包 + +**文件:** +- 修改: `package.json` + +**步骤 1: 安装数据库相关依赖** + +```bash +npm install drizzle-orm @libsql/client +npm install -D drizzle-kit +``` + +**步骤 2: 安装认证相关依赖** + +```bash +npm install next-auth@beta @auth/drizzle-adapter +npm install bcryptjs +npm install -D @types/bcryptjs +``` + +**步骤 3: 安装富文本编辑器** + +```bash +npm install @tiptap/react @tiptap/starter-kit @tiptap/extension-image @tiptap/extension-link @tiptap/pm +``` + +**步骤 4: 安装其他工具库** + +```bash +npm install nanoid date-fns +npm install -D @types/nanoid +``` + +**步骤 5: 验证安装** + +运行: `npm list drizzle-orm next-auth @tiptap/react` + +预期: 所有依赖包版本正确显示 + +**步骤 6: 提交** + +```bash +git add package.json package-lock.json +git commit -m "chore: 添加 CMS 系统所需依赖包" +``` + +--- + +### Task 2: 配置数据库连接 + +**文件:** +- 创建: `src/db/index.ts` +- 创建: `.env.local` +- 修改: `.gitignore` + +**步骤 1: 创建环境变量文件** + +创建 `.env.local`: + +```env +# Database +DATABASE_URL="file:./data.db" + +# NextAuth +NEXTAUTH_SECRET="your-super-secret-key-change-in-production" +NEXTAUTH_URL="http://localhost:3000" + +# Admin (初始管理员账号) +ADMIN_EMAIL="admin@novalon.cn" +ADMIN_PASSWORD="admin123456" +``` + +**步骤 2: 更新 .gitignore** + +在 `.gitignore` 中添加: + +``` +# Database +*.db +*.db-journal +data.db + +# Environment +.env.local +.env.*.local +``` + +**步骤 3: 创建数据库连接文件** + +创建 `src/db/index.ts`: + +```typescript +import { drizzle } from 'drizzle-orm/libsql'; +import { createClient } from '@libsql/client'; + +const client = createClient({ + url: process.env.DATABASE_URL || 'file:./data.db', +}); + +export const db = drizzle(client); +``` + +**步骤 4: 验证数据库连接** + +创建临时测试文件 `src/db/test-connection.ts`: + +```typescript +import { db } from './index'; + +async function testConnection() { + try { + await db.run('SELECT 1'); + console.log('✅ Database connection successful'); + } catch (error) { + console.error('❌ Database connection failed:', error); + } +} + +testConnection(); +``` + +运行: `npx tsx src/db/test-connection.ts` + +预期: 输出 "✅ Database connection successful" + +**步骤 5: 删除测试文件** + +```bash +rm src/db/test-connection.ts +``` + +**步骤 6: 提交** + +```bash +git add src/db/index.ts .gitignore +git commit -m "feat: 配置数据库连接" +``` + +--- + +### Task 3: 定义数据库 Schema + +**文件:** +- 创建: `src/db/schema.ts` +- 创建: `drizzle.config.ts` + +**步骤 1: 创建 Schema 文件** + +创建 `src/db/schema.ts`: + +```typescript +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(), +}); + +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), +})); + +export type User = typeof users.$inferSelect; +export type NewUser = typeof users.$inferInsert; +export type Content = typeof content.$inferSelect; +export type NewContent = typeof content.$inferInsert; +export type SiteConfig = typeof siteConfig.$inferSelect; +export type NewSiteConfig = typeof siteConfig.$inferInsert; +``` + +**步骤 2: 创建 Drizzle 配置文件** + +创建 `drizzle.config.ts`: + +```typescript +import type { Config } from 'drizzle-kit'; + +export default { + schema: './src/db/schema.ts', + out: './drizzle', + driver: 'libsql', + dbCredentials: { + url: process.env.DATABASE_URL || 'file:./data.db', + }, +} satisfies Config; +``` + +**步骤 3: 更新 package.json scripts** + +在 `package.json` 的 `scripts` 中添加: + +```json +{ + "scripts": { + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:push": "drizzle-kit push", + "db:studio": "drizzle-kit studio", + "db:seed": "tsx src/db/seed.ts" + } +} +``` + +**步骤 4: 验证 Schema 定义** + +运行: `npm run db:generate` + +预期: 在 `drizzle/` 目录生成迁移文件 + +**步骤 5: 提交** + +```bash +git add src/db/schema.ts drizzle.config.ts package.json +git commit -m "feat: 定义数据库 Schema" +``` + +--- + +### Task 4: 生成并执行数据库迁移 + +**文件:** +- 创建: `drizzle/0000_initial_migration.sql` (自动生成) +- 创建: `data.db` (自动创建) + +**步骤 1: 生成迁移文件** + +运行: `npm run db:generate` + +预期: 生成 SQL 迁移文件 + +**步骤 2: 执行迁移** + +运行: `npm run db:push` + +预期: 创建 `data.db` 文件,包含所有表 + +**步骤 3: 验证表结构** + +运行: `sqlite3 data.db ".tables"` + +预期: 显示所有表名 (users, content, content_versions, site_config, audit_logs) + +**步骤 4: 提交** + +```bash +git add drizzle/ +git commit -m "feat: 生成数据库迁移文件" +``` + +--- + +### Task 5: 创建数据库种子数据 + +**文件:** +- 创建: `src/db/seed.ts` + +**步骤 1: 创建种子数据脚本** + +创建 `src/db/seed.ts`: + +```typescript +import { db } from './index'; +import { users, siteConfig } from './schema'; +import { nanoid } from 'nanoid'; +import bcrypt from 'bcryptjs'; + +async function seed() { + console.log('🌱 开始种子数据...'); + + // 创建管理员用户 + const hashedPassword = await bcrypt.hash('admin123456', 10); + await db.insert(users).values({ + id: nanoid(), + email: 'admin@novalon.cn', + passwordHash: hashedPassword, + name: '系统管理员', + role: 'admin', + createdAt: new Date(), + updatedAt: new Date(), + }); + + console.log('✅ 创建管理员用户: admin@novalon.cn'); + + // 创建默认配置 + const defaultConfigs = [ + { + id: nanoid(), + key: 'feature_news', + value: { + enabled: true, + displayCount: 6, + categories: ['公司新闻', '产品发布', '合作动态', '行业资讯'], + sortOrder: 'desc', + }, + category: 'feature', + description: '新闻模块配置', + updatedAt: new Date(), + }, + { + id: nanoid(), + key: 'feature_products', + value: { + enabled: true, + showPricing: true, + featuredProducts: ['erp', 'crm'], + }, + category: 'feature', + description: '产品模块配置', + updatedAt: new Date(), + }, + { + id: nanoid(), + key: 'feature_services', + value: { + enabled: true, + items: ['software', 'cloud', 'data', 'security'], + }, + category: 'feature', + description: '服务模块配置', + updatedAt: new Date(), + }, + { + id: nanoid(), + key: 'seo_default', + value: { + title: '四川睿新致远科技有限公司 - 企业数字化转型服务商', + description: '以智慧连接数字趋势,以伙伴身份陪您成长——您的数字化转型同行者', + keywords: ['数字化转型', '软件开发', '云服务', '数据分析', '信息安全'], + }, + category: 'seo', + description: '默认 SEO 配置', + updatedAt: new Date(), + }, + ]; + + for (const config of defaultConfigs) { + await db.insert(siteConfig).values(config); + console.log(`✅ 创建配置: ${config.key}`); + } + + console.log('🎉 种子数据完成!'); +} + +seed().catch((error) => { + console.error('❌ 种子数据失败:', error); + process.exit(1); +}); +``` + +**步骤 2: 执行种子数据** + +运行: `npm run db:seed` + +预期: 创建管理员用户和默认配置 + +**步骤 3: 验证数据** + +运行: `sqlite3 data.db "SELECT email, role FROM users"` + +预期: 显示管理员邮箱和角色 + +**步骤 4: 提交** + +```bash +git add src/db/seed.ts +git commit -m "feat: 添加数据库种子数据脚本" +``` + +--- + +### Task 6: 配置 NextAuth.js + +**文件:** +- 创建: `src/app/api/auth/[...nextauth]/route.ts` +- 创建: `src/lib/auth.ts` +- 修改: `src/app/layout.tsx` +- 创建: `src/providers/session-provider.tsx` + +**步骤 1: 创建认证配置** + +创建 `src/lib/auth.ts`: + +```typescript +import { NextAuthOptions } from 'next-auth'; +import CredentialsProvider from 'next-auth/providers/credentials'; +import EmailProvider from 'next-auth/providers/email'; +import { db } from '@/db'; +import { users } from '@/db/schema'; +import { eq } from 'drizzle-orm'; +import bcrypt from 'bcryptjs'; + +export const authOptions: NextAuthOptions = { + providers: [ + CredentialsProvider({ + name: '邮箱密码', + credentials: { + email: { label: '邮箱', type: 'email' }, + password: { label: '密码', type: 'password' }, + }, + async authorize(credentials) { + if (!credentials?.email || !credentials?.password) { + return null; + } + + const user = await db.select().from(users).where(eq(users.email, credentials.email)).limit(1); + + if (user.length === 0) { + return null; + } + + const isValid = await bcrypt.compare(credentials.password, user[0].passwordHash || ''); + + if (!isValid) { + return null; + } + + return { + id: user[0].id, + email: user[0].email, + name: user[0].name, + role: user[0].role, + }; + }, + }), + EmailProvider({ + server: { + host: process.env.SMTP_HOST || 'smtp.example.com', + port: parseInt(process.env.SMTP_PORT || '587'), + auth: { + user: process.env.SMTP_USER || '', + pass: process.env.SMTP_PASSWORD || '', + }, + }, + from: process.env.SMTP_FROM || 'noreply@novalon.cn', + }), + ], + callbacks: { + async jwt({ token, user }) { + if (user) { + token.id = user.id; + token.role = user.role; + } + return token; + }, + async session({ session, token }) { + if (session.user) { + session.user.id = token.id as string; + session.user.role = token.role as string; + } + return session; + }, + }, + pages: { + signIn: '/admin/login', + error: '/admin/login', + }, + session: { + strategy: 'jwt', + }, +}; +``` + +**步骤 2: 创建 API Route** + +创建 `src/app/api/auth/[...nextauth]/route.ts`: + +```typescript +import NextAuth from 'next-auth'; +import { authOptions } from '@/lib/auth'; + +const handler = NextAuth(authOptions); + +export { handler as GET, handler as POST }; +``` + +**步骤 3: 创建 Session Provider** + +创建 `src/providers/session-provider.tsx`: + +```typescript +'use client'; + +import { SessionProvider as NextAuthSessionProvider } from 'next-auth/react'; +import { ReactNode } from 'react'; + +export function SessionProvider({ children }: { children: ReactNode }) { + return {children}; +} +``` + +**步骤 4: 更新根布局** + +修改 `src/app/layout.tsx`, 在 `` 内添加 SessionProvider: + +```typescript +import { SessionProvider } from '@/providers/session-provider'; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ); +} +``` + +**步骤 5: 创建类型定义** + +创建 `src/types/next-auth.d.ts`: + +```typescript +import { DefaultSession } from 'next-auth'; + +declare module 'next-auth' { + interface Session { + user: { + id: string; + role: string; + } & DefaultSession['user']; + } + + interface User { + role: string; + } +} + +declare module 'next-auth/jwt' { + interface JWT { + id: string; + role: string; + } +} +``` + +**步骤 6: 提交** + +```bash +git add src/lib/auth.ts src/app/api/auth/ src/providers/ src/types/next-auth.d.ts src/app/layout.tsx +git commit -m "feat: 配置 NextAuth.js 认证系统" +``` + +--- + +### Task 7: 创建权限检查工具 + +**文件:** +- 创建: `src/lib/auth/permissions.ts` +- 创建: `src/lib/auth/middleware.ts` + +**步骤 1: 定义权限矩阵** + +创建 `src/lib/auth/permissions.ts`: + +```typescript +export 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; + +export type Role = keyof typeof PERMISSIONS; +export type Resource = keyof typeof PERMISSIONS.admin; +export type Action = 'create' | 'read' | 'update' | 'delete' | 'publish'; + +export function hasPermission( + role: Role, + resource: Resource, + action: Action +): boolean { + const permissions = PERMISSIONS[role]; + if (!permissions) return false; + + const resourcePermissions = permissions[resource]; + if (!resourcePermissions) return false; + + return resourcePermissions.includes(action as never); +} +``` + +**步骤 2: 创建权限检查 Hook** + +创建 `src/lib/auth/check-permission.ts`: + +```typescript +import { getServerSession } from 'next-auth'; +import { authOptions } from './auth'; +import { hasPermission, Role, Resource, Action } from './permissions'; + +export async function checkPermission( + resource: Resource, + action: Action +): Promise<{ allowed: boolean; userId?: string; role?: Role }> { + const session = await getServerSession(authOptions); + + if (!session || !session.user) { + return { allowed: false }; + } + + const userRole = session.user.role as Role; + const allowed = hasPermission(userRole, resource, action); + + return { + allowed, + userId: session.user.id, + role: userRole, + }; +} + +export async function requirePermission( + resource: Resource, + action: Action +): Promise<{ userId: string; role: Role }> { + const result = await checkPermission(resource, action); + + if (!result.allowed) { + throw new Error('无权限执行此操作'); + } + + return { + userId: result.userId!, + role: result.role!, + }; +} +``` + +**步骤 3: 创建测试** + +创建 `src/lib/auth/__tests__/permissions.test.ts`: + +```typescript +import { describe, it, expect } from 'vitest'; +import { hasPermission } from '../permissions'; + +describe('Permissions', () => { + it('should allow admin to do everything', () => { + expect(hasPermission('admin', 'content', 'create')).toBe(true); + expect(hasPermission('admin', 'config', 'update')).toBe(true); + expect(hasPermission('admin', 'users', 'delete')).toBe(true); + }); + + it('should restrict editor from managing users', () => { + expect(hasPermission('editor', 'content', 'create')).toBe(true); + expect(hasPermission('editor', 'users', 'create')).toBe(false); + }); + + it('should restrict viewer to read-only', () => { + expect(hasPermission('viewer', 'content', 'read')).toBe(true); + expect(hasPermission('viewer', 'content', 'create')).toBe(false); + expect(hasPermission('viewer', 'config', 'update')).toBe(false); + }); +}); +``` + +**步骤 4: 运行测试** + +运行: `npm test src/lib/auth/__tests__/permissions.test.ts` + +预期: 所有测试通过 + +**步骤 5: 提交** + +```bash +git add src/lib/auth/ +git commit -m "feat: 创建权限检查工具和测试" +``` + +--- + +## 阶段二:内容管理 API(预计 2 天) + +### Task 8: 创建内容 CRUD API + +**文件:** +- 创建: `src/app/api/v1/content/route.ts` +- 创建: `src/app/api/v1/content/[id]/route.ts` +- 创建: `src/lib/validators/content.ts` + +**步骤 1: 创建验证 Schema** + +创建 `src/lib/validators/content.ts`: + +```typescript +import { z } from 'zod'; + +export const ContentCreateSchema = z.object({ + type: z.enum(['news', 'product', 'service', 'case']), + title: z.string().min(1).max(200), + slug: z.string().min(1).max(200).regex(/^[a-z0-9-]+$/), + excerpt: z.string().max(500).optional(), + content: z.string().min(1), + coverImage: z.string().url().optional(), + category: z.string().optional(), + tags: z.array(z.string()).optional(), + status: z.enum(['draft', 'published', 'archived']).default('draft'), + publishedAt: z.coerce.date().optional(), + sortOrder: z.number().int().optional(), + metadata: z.record(z.any()).optional(), +}); + +export const ContentUpdateSchema = ContentCreateSchema.partial(); + +export 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'), +}); +``` + +**步骤 2: 创建列表 API** + +创建 `src/app/api/v1/content/route.ts`: + +```typescript +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/db'; +import { content } from '@/db/schema'; +import { eq, and, desc, asc, sql } from 'drizzle-orm'; +import { ContentQuerySchema, ContentCreateSchema } from '@/lib/validators/content'; +import { checkPermission } from '@/lib/auth/check-permission'; +import { nanoid } from 'nanoid'; + +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 sortField = query.sort.startsWith('-') ? query.sort.slice(1) : query.sort; + const sortDirection = query.sort.startsWith('-') ? desc : asc; + + const [items, [{ count }]] = await Promise.all([ + db.select() + .from(content) + .where(whereConditions.length > 0 ? and(...whereConditions) : undefined) + .orderBy(sortDirection(content[sortField as keyof typeof content] as any)) + .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, + totalPages: Math.ceil(count / query.pageSize), + }, + }); + } catch (error: any) { + return NextResponse.json( + { success: false, error: { code: 'INVALID_QUERY', message: error.message } }, + { status: 400 } + ); + } +} + +export async function POST(request: NextRequest) { + try { + const permission = await checkPermission('content', 'create'); + if (!permission.allowed) { + return NextResponse.json( + { success: false, error: { code: 'FORBIDDEN', message: '无权限创建内容' } }, + { status: 403 } + ); + } + + const body = await request.json(); + const validatedData = ContentCreateSchema.parse(body); + + const newContent = await db.insert(content).values({ + ...validatedData, + id: nanoid(), + authorId: permission.userId!, + createdAt: new Date(), + updatedAt: new Date(), + publishedAt: validatedData.status === 'published' ? new Date() : undefined, + }).returning(); + + return NextResponse.json({ success: true, data: newContent[0] }, { status: 201 }); + } catch (error: any) { + return NextResponse.json( + { success: false, error: { code: 'CREATE_FAILED', message: error.message } }, + { status: 500 } + ); + } +} +``` + +**步骤 3: 创建单个内容 API** + +创建 `src/app/api/v1/content/[id]/route.ts`: + +```typescript +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/db'; +import { content } from '@/db/schema'; +import { eq } from 'drizzle-orm'; +import { ContentUpdateSchema } from '@/lib/validators/content'; +import { checkPermission } from '@/lib/auth/check-permission'; + +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const item = await db.select() + .from(content) + .where(eq(content.id, params.id)) + .limit(1); + + if (item.length === 0) { + return NextResponse.json( + { success: false, error: { code: 'NOT_FOUND', message: '内容不存在' } }, + { status: 404 } + ); + } + + return NextResponse.json({ success: true, data: item[0] }); + } catch (error: any) { + return NextResponse.json( + { success: false, error: { code: 'FETCH_FAILED', message: error.message } }, + { status: 500 } + ); + } +} + +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const permission = await checkPermission('content', 'update'); + if (!permission.allowed) { + return NextResponse.json( + { success: false, error: { code: 'FORBIDDEN', message: '无权限更新内容' } }, + { status: 403 } + ); + } + + const body = await request.json(); + const validatedData = ContentUpdateSchema.parse(body); + + const updated = await db.update(content) + .set({ + ...validatedData, + updatedAt: new Date(), + publishedAt: validatedData.status === 'published' ? new Date() : undefined, + }) + .where(eq(content.id, params.id)) + .returning(); + + if (updated.length === 0) { + return NextResponse.json( + { success: false, error: { code: 'NOT_FOUND', message: '内容不存在' } }, + { status: 404 } + ); + } + + return NextResponse.json({ success: true, data: updated[0] }); + } catch (error: any) { + return NextResponse.json( + { success: false, error: { code: 'UPDATE_FAILED', message: error.message } }, + { status: 500 } + ); + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const permission = await checkPermission('content', 'delete'); + if (!permission.allowed) { + return NextResponse.json( + { success: false, error: { code: 'FORBIDDEN', message: '无权限删除内容' } }, + { status: 403 } + ); + } + + const deleted = await db.delete(content) + .where(eq(content.id, params.id)) + .returning(); + + if (deleted.length === 0) { + return NextResponse.json( + { success: false, error: { code: 'NOT_FOUND', message: '内容不存在' } }, + { status: 404 } + ); + } + + return NextResponse.json({ success: true, data: deleted[0] }); + } catch (error: any) { + return NextResponse.json( + { success: false, error: { code: 'DELETE_FAILED', message: error.message } }, + { status: 500 } + ); + } +} +``` + +**步骤 4: 创建 API 测试** + +创建 `src/app/api/v1/content/__tests__/route.test.ts`: + +```typescript +import { describe, it, expect, beforeEach } from 'vitest'; +import { NextRequest } from 'next/server'; +import { GET, POST } from '../route'; + +describe('Content API', () => { + it('should return empty list initially', async () => { + const request = new NextRequest('http://localhost:3000/api/v1/content'); + const response = await GET(request); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.success).toBe(true); + expect(data.data).toEqual([]); + }); + + it('should create content with valid data', async () => { + const request = new NextRequest('http://localhost:3000/api/v1/content', { + method: 'POST', + body: JSON.stringify({ + type: 'news', + title: '测试新闻', + slug: 'test-news', + content: '这是测试内容', + }), + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(201); + expect(data.success).toBe(true); + expect(data.data.title).toBe('测试新闻'); + }); +}); +``` + +**步骤 5: 运行测试** + +运行: `npm test src/app/api/v1/content/__tests__/route.test.ts` + +预期: 测试通过(可能需要 mock 数据库) + +**步骤 6: 提交** + +```bash +git add src/app/api/v1/content/ src/lib/validators/ +git commit -m "feat: 创建内容 CRUD API" +``` + +--- + +### Task 9: 创建配置管理 API + +**文件:** +- 创建: `src/app/api/v1/config/route.ts` +- 创建: `src/app/api/v1/config/[key]/route.ts` + +**步骤 1: 创建配置列表 API** + +创建 `src/app/api/v1/config/route.ts`: + +```typescript +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/db'; +import { siteConfig } from '@/db/schema'; +import { eq } from 'drizzle-orm'; +import { checkPermission } from '@/lib/auth/check-permission'; + +export async function GET() { + try { + const configs = await db.select().from(siteConfig); + + return NextResponse.json({ + success: true, + data: configs, + }); + } catch (error: any) { + return NextResponse.json( + { success: false, error: { code: 'FETCH_FAILED', message: error.message } }, + { status: 500 } + ); + } +} +``` + +**步骤 2: 创建单个配置 API** + +创建 `src/app/api/v1/config/[key]/route.ts`: + +```typescript +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/db'; +import { siteConfig } from '@/db/schema'; +import { eq } from 'drizzle-orm'; +import { checkPermission } from '@/lib/auth/check-permission'; +import { z } from 'zod'; + +const ConfigUpdateSchema = z.object({ + value: z.any(), +}); + +export async function GET( + request: NextRequest, + { params }: { params: { key: string } } +) { + try { + const config = await db.select() + .from(siteConfig) + .where(eq(siteConfig.key, params.key)) + .limit(1); + + if (config.length === 0) { + return NextResponse.json( + { success: false, error: { code: 'NOT_FOUND', message: '配置不存在' } }, + { status: 404 } + ); + } + + return NextResponse.json({ success: true, data: config[0] }); + } catch (error: any) { + return NextResponse.json( + { success: false, error: { code: 'FETCH_FAILED', message: error.message } }, + { status: 500 } + ); + } +} + +export async function PUT( + request: NextRequest, + { params }: { params: { key: string } } +) { + try { + const permission = await checkPermission('config', 'update'); + if (!permission.allowed) { + return NextResponse.json( + { success: false, error: { code: 'FORBIDDEN', message: '无权限更新配置' } }, + { status: 403 } + ); + } + + const body = await request.json(); + const { value } = ConfigUpdateSchema.parse(body); + + const updated = await db.update(siteConfig) + .set({ + value, + updatedAt: new Date(), + updatedBy: permission.userId, + }) + .where(eq(siteConfig.key, params.key)) + .returning(); + + if (updated.length === 0) { + return NextResponse.json( + { success: false, error: { code: 'NOT_FOUND', message: '配置不存在' } }, + { status: 404 } + ); + } + + return NextResponse.json({ success: true, data: updated[0] }); + } catch (error: any) { + return NextResponse.json( + { success: false, error: { code: 'UPDATE_FAILED', message: error.message } }, + { status: 500 } + ); + } +} +``` + +**步骤 3: 提交** + +```bash +git add src/app/api/v1/config/ +git commit -m "feat: 创建配置管理 API" +``` + +--- + +## 阶段三:管理后台界面(预计 2 天) + +### Task 10: 创建管理后台布局 + +**文件:** +- 创建: `src/app/admin/layout.tsx` +- 创建: `src/components/admin/sidebar.tsx` +- 创建: `src/components/admin/header.tsx` + +**步骤 1: 创建侧边栏组件** + +创建 `src/components/admin/sidebar.tsx`: + +```typescript +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { cn } from '@/lib/utils'; +import { + LayoutDashboard, + FileText, + Package, + Briefcase, + Trophy, + Settings, + Users, + History, +} from 'lucide-react'; + +const menuItems = [ + { icon: LayoutDashboard, label: '仪表盘', href: '/admin' }, + { icon: FileText, label: '新闻管理', href: '/admin/content/news' }, + { icon: Package, label: '产品管理', href: '/admin/content/products' }, + { icon: Briefcase, label: '服务管理', href: '/admin/content/services' }, + { icon: Trophy, label: '案例管理', href: '/admin/content/cases' }, + { icon: Settings, label: '配置中心', href: '/admin/config' }, + { icon: Users, label: '用户管理', href: '/admin/users' }, + { icon: History, label: '操作日志', href: '/admin/logs' }, +]; + +export function Sidebar() { + const pathname = usePathname(); + + return ( + + ); +} +``` + +**步骤 2: 创建顶部栏组件** + +创建 `src/components/admin/header.tsx`: + +```typescript +'use client'; + +import { useSession, signOut } from 'next-auth/react'; +import { Button } from '@/components/ui/button'; +import { Bell, User, LogOut } from 'lucide-react'; +import Link from 'next/link'; + +export function Header() { + const { data: session } = useSession(); + + return ( +
+
+ + 查看网站 + +
+ +
+ + +
+
+ +
+
+

{session?.user?.name}

+

{session?.user?.role}

+
+ +
+
+
+ ); +} +``` + +**步骤 3: 创建管理后台布局** + +创建 `src/app/admin/layout.tsx`: + +```typescript +import { redirect } from 'next/navigation'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { Sidebar } from '@/components/admin/sidebar'; +import { Header } from '@/components/admin/header'; + +export default async function AdminLayout({ + children, +}: { + children: React.ReactNode; +}) { + const session = await getServerSession(authOptions); + + if (!session) { + redirect('/admin/login'); + } + + return ( +
+ +
+
+
{children}
+
+
+ ); +} +``` + +**步骤 4: 提交** + +```bash +git add src/app/admin/layout.tsx src/components/admin/ +git commit -m "feat: 创建管理后台布局" +``` + +--- + +### Task 11: 创建登录页面 + +**文件:** +- 创建: `src/app/admin/login/page.tsx` + +**步骤 1: 创建登录页面** + +创建 `src/app/admin/login/page.tsx`: + +```typescript +'use client'; + +import { useState } from 'react'; +import { signIn } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Mail, Lock, ArrowRight } from 'lucide-react'; + +export default function LoginPage() { + const router = useRouter(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setError(''); + + try { + const result = await signIn('credentials', { + email, + password, + redirect: false, + }); + + if (result?.error) { + setError('邮箱或密码错误'); + } else { + router.push('/admin'); + router.refresh(); + } + } catch (err) { + setError('登录失败,请重试'); + } finally { + setIsLoading(false); + } + }; + + const handleMagicLink = async () => { + if (!email) { + setError('请输入邮箱地址'); + return; + } + + setIsLoading(true); + try { + await signIn('email', { email, redirect: false }); + setError('登录链接已发送到您的邮箱'); + } catch (err) { + setError('发送失败,请重试'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + + 管理后台登录 + 请使用邮箱密码或 Magic Link 登录 + + +
+
+ +
+ + setEmail(e.target.value)} + className="pl-10" + required + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + className="pl-10" + /> +
+
+ + {error && ( +

{error}

+ )} + + + +
+
+
+
+
+ +
+
+ + +
+
+
+
+ ); +} +``` + +**步骤 2: 测试登录功能** + +运行: `npm run dev` + +访问: `http://localhost:3000/admin/login` + +使用: `admin@novalon.cn` / `admin123456` 登录 + +预期: 成功登录并跳转到管理后台 + +**步骤 3: 提交** + +```bash +git add src/app/admin/login/ +git commit -m "feat: 创建管理后台登录页面" +``` + +--- + +### Task 12: 创建仪表盘页面 + +**文件:** +- 创建: `src/app/admin/page.tsx` +- 创建: `src/components/admin/stats-card.tsx` + +**步骤 1: 创建统计卡片组件** + +创建 `src/components/admin/stats-card.tsx`: + +```typescript +import { Card, CardContent } from '@/components/ui/card'; +import { LucideIcon } from 'lucide-react'; + +interface StatsCardProps { + title: string; + value: string | number; + icon: LucideIcon; + trend?: { + value: number; + isPositive: boolean; + }; +} + +export function StatsCard({ title, value, icon: Icon, trend }: StatsCardProps) { + return ( + + +
+
+

{title}

+

{value}

+ {trend && ( +

+ {trend.isPositive ? '↑' : '↓'} {Math.abs(trend.value)}% +

+ )} +
+
+ +
+
+
+
+ ); +} +``` + +**步骤 2: 创建仪表盘页面** + +创建 `src/app/admin/page.tsx`: + +```typescript +import { db } from '@/db'; +import { content, users } from '@/db/schema'; +import { sql } from 'drizzle-orm'; +import { StatsCard } from '@/components/admin/stats-card'; +import { FileText, Package, Briefcase, Users } from 'lucide-react'; + +export default async function DashboardPage() { + const [newsCount, productsCount, servicesCount, usersCount] = await Promise.all([ + db.select({ count: sql`count(*)` }).from(content).where(sql`type = 'news'`), + db.select({ count: sql`count(*)` }).from(content).where(sql`type = 'product'`), + db.select({ count: sql`count(*)` }).from(content).where(sql`type = 'service'`), + db.select({ count: sql`count(*)` }).from(users), + ]); + + return ( +
+
+

仪表盘

+

欢迎回来,查看网站运营数据

+
+ +
+ + + + +
+ +
+
+

最近更新

+

暂无最近更新

+
+ + +
+
+ ); +} +``` + +**步骤 3: 提交** + +```bash +git add src/app/admin/page.tsx src/components/admin/stats-card.tsx +git commit -m "feat: 创建管理后台仪表盘页面" +``` + +--- + +## 阶段四:内容管理界面(预计 1 天) + +### Task 13: 创建内容列表页面 + +**文件:** +- 创建: `src/app/admin/content/news/page.tsx` +- 创建: `src/components/admin/content-table.tsx` + +**步骤 1: 创建内容表格组件** + +创建 `src/components/admin/content-table.tsx`: + +```typescript +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { Edit, Trash2, Eye } from 'lucide-react'; +import { format } from 'date-fns'; +import { zhCN } from 'date-fns/locale'; + +interface ContentItem { + id: string; + title: string; + category?: string; + status: string; + publishedAt?: Date; + createdAt: Date; +} + +interface ContentTableProps { + data: ContentItem[]; + type: 'news' | 'product' | 'service' | 'case'; + onDelete: (id: string) => void; +} + +export function ContentTable({ data, type, onDelete }: ContentTableProps) { + const getStatusBadge = (status: string) => { + const variants: Record = { + published: 'default', + draft: 'secondary', + archived: 'outline', + }; + const labels: Record = { + published: '已发布', + draft: '草稿', + archived: '已归档', + }; + return ( + + {labels[status] || status} + + ); + }; + + return ( + + + + 标题 + 分类 + 状态 + 发布时间 + 创建时间 + 操作 + + + + {data.map((item) => ( + + {item.title} + {item.category || '-'} + {getStatusBadge(item.status)} + + {item.publishedAt + ? format(new Date(item.publishedAt), 'yyyy-MM-dd', { locale: zhCN }) + : '-'} + + + {format(new Date(item.createdAt), 'yyyy-MM-dd', { locale: zhCN })} + + +
+ + +
+
+
+ ))} +
+
+ ); +} +``` + +**步骤 2: 创建新闻列表页面** + +创建 `src/app/admin/content/news/page.tsx`: + +```typescript +import { db } from '@/db'; +import { content } from '@/db/schema'; +import { eq, desc } from 'drizzle-orm'; +import { Button } from '@/components/ui/button'; +import { ContentTable } from '@/components/admin/content-table'; +import { Plus } from 'lucide-react'; +import Link from 'next/link'; + +export default async function NewsListPage() { + const news = await db.select() + .from(content) + .where(eq(content.type, 'news')) + .orderBy(desc(content.createdAt)); + + return ( +
+
+
+

新闻管理

+

管理公司新闻、产品发布等内容

+
+ +
+ +
+ { + 'use server'; + await db.delete(content).where(eq(content.id, id)); + }} + /> +
+
+ ); +} +``` + +**步骤 3: 提交** + +```bash +git add src/app/admin/content/news/ src/components/admin/content-table.tsx +git commit -m "feat: 创建内容列表页面" +``` + +--- + +## 阶段五:测试和部署(预计 1 天) + +### Task 14: 编写 E2E 测试 + +**文件:** +- 创建: `e2e/tests/admin/login.spec.ts` +- 创建: `e2e/tests/admin/content.spec.ts` + +**步骤 1: 创建登录测试** + +创建 `e2e/tests/admin/login.spec.ts`: + +```typescript +import { test, expect } from '@playwright/test'; + +test.describe('管理后台登录', () => { + test('应该显示登录页面', async ({ page }) => { + await page.goto('/admin/login'); + await expect(page.locator('h2')).toContainText('管理后台登录'); + }); + + test('应该成功登录', async ({ page }) => { + await page.goto('/admin/login'); + + await page.fill('input[type="email"]', 'admin@novalon.cn'); + await page.fill('input[type="password"]', 'admin123456'); + await page.click('button[type="submit"]'); + + await expect(page).toHaveURL('/admin'); + await expect(page.locator('h1')).toContainText('仪表盘'); + }); + + test('应该显示错误提示', async ({ page }) => { + await page.goto('/admin/login'); + + await page.fill('input[type="email"]', 'wrong@example.com'); + await page.fill('input[type="password"]', 'wrongpassword'); + await page.click('button[type="submit"]'); + + await expect(page.locator('text=邮箱或密码错误')).toBeVisible(); + }); +}); +``` + +**步骤 2: 创建内容管理测试** + +创建 `e2e/tests/admin/content.spec.ts`: + +```typescript +import { test, expect } from '@playwright/test'; + +test.describe('内容管理', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/admin/login'); + await page.fill('input[type="email"]', 'admin@novalon.cn'); + await page.fill('input[type="password"]', 'admin123456'); + await page.click('button[type="submit"]'); + await expect(page).toHaveURL('/admin'); + }); + + test('应该显示新闻列表', async ({ page }) => { + await page.click('text=新闻管理'); + await expect(page).toHaveURL('/admin/content/news'); + await expect(page.locator('h1')).toContainText('新闻管理'); + }); + + test('应该创建新闻', async ({ page }) => { + await page.click('text=新闻管理'); + await page.click('text=发布新闻'); + + await page.fill('input[name="title"]', '测试新闻标题'); + await page.fill('input[name="slug"]', 'test-news-slug'); + await page.fill('textarea[name="excerpt"]', '这是测试新闻摘要'); + await page.fill('[name="content"]', '这是测试新闻内容'); + await page.click('button[type="submit"]'); + + await expect(page.locator('text=创建成功')).toBeVisible(); + }); +}); +``` + +**步骤 3: 运行测试** + +运行: `cd e2e && npm test tests/admin/` + +预期: 所有测试通过 + +**步骤 4: 提交** + +```bash +git add e2e/tests/admin/ +git commit -m "test: 添加管理后台 E2E 测试" +``` + +--- + +### Task 15: 性能优化和安全检查 + +**文件:** +- 修改: `src/app/api/v1/content/route.ts` +- 创建: `src/middleware.ts` + +**步骤 1: 添加 API 速率限制** + +创建 `src/middleware.ts`: + +```typescript +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; + +const rateLimit = new Map(); + +export function middleware(request: NextRequest) { + // 只对 API 路由进行速率限制 + if (request.nextUrl.pathname.startsWith('/api/')) { + const ip = request.ip || 'unknown'; + const now = Date.now(); + const windowMs = 60 * 1000; // 1 分钟 + const maxRequests = 100; // 每分钟最多 100 次请求 + + const userLimit = rateLimit.get(ip); + + if (userLimit) { + if (now > userLimit.resetTime) { + rateLimit.set(ip, { count: 1, resetTime: now + windowMs }); + } else if (userLimit.count >= maxRequests) { + return NextResponse.json( + { success: false, error: { code: 'RATE_LIMIT_EXCEEDED', message: '请求过于频繁' } }, + { status: 429 } + ); + } else { + userLimit.count++; + } + } else { + rateLimit.set(ip, { count: 1, resetTime: now + windowMs }); + } + } + + return NextResponse.next(); +} + +export const config = { + matcher: '/api/:path*', +}; +``` + +**步骤 2: 添加安全头** + +修改 `next.config.ts`, 在 `headers` 中添加: + +```typescript +{ + key: 'Content-Security-Policy', + value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;", +}, +{ + key: 'X-Content-Type-Options', + value: 'nosniff', +}, +{ + key: 'X-Frame-Options', + value: 'DENY', +}, +{ + key: 'X-XSS-Protection', + value: '1; mode=block', +}, +``` + +**步骤 3: 提交** + +```bash +git add src/middleware.ts next.config.ts +git commit -m "feat: 添加 API 速率限制和安全头" +``` + +--- + +## 阶段六:文档和部署 + +### Task 16: 更新项目文档 + +**文件:** +- 修改: `README.md` +- 创建: `docs/cms-usage.md` + +**步骤 1: 更新 README** + +在 `README.md` 中添加 CMS 相关内容: + +```markdown +## CMS 管理后台 + +### 访问地址 + +- 管理后台:http://localhost:3000/admin +- 默认账号:admin@novalon.cn +- 默认密码:admin123456 + +### 功能模块 + +- **内容管理**:新闻、产品、服务、案例的增删改查 +- **配置中心**:功能开关、样式配置、SEO 设置 +- **用户管理**:管理员、编辑、查看者角色管理 +- **操作日志**:审计追踪、安全监控 + +### 数据库管理 + +\`\`\`bash +# 生成迁移 +npm run db:generate + +# 执行迁移 +npm run db:migrate + +# 查看数据库 +npm run db:studio + +# 初始化数据 +npm run db:seed +\`\`\` +``` + +**步骤 2: 创建使用文档** + +创建 `docs/cms-usage.md`: + +```markdown +# CMS 使用手册 + +## 1. 登录管理后台 + +访问 `/admin/login`,使用邮箱密码或 Magic Link 登录。 + +## 2. 内容管理 + +### 2.1 发布新闻 + +1. 点击左侧菜单「新闻管理」 +2. 点击右上角「发布新闻」按钮 +3. 填写标题、摘要、内容等信息 +4. 选择分类和标签 +5. 点击「发布」按钮 + +### 2.2 编辑产品 + +1. 点击左侧菜单「产品管理」 +2. 找到需要编辑的产品,点击「编辑」按钮 +3. 修改内容后点击「保存」 + +## 3. 配置管理 + +### 3.1 功能开关 + +在「配置中心」可以启用或禁用各功能模块。 + +### 3.2 SEO 配置 + +配置网站标题、描述、关键词等 SEO 信息。 + +## 4. 用户管理 + +管理员可以创建、编辑、删除用户,并分配角色权限。 + +## 5. 操作日志 + +查看所有操作记录,包括创建、更新、删除等。 +``` + +**步骤 3: 提交** + +```bash +git add README.md docs/cms-usage.md +git commit -m "docs: 更新 CMS 使用文档" +``` + +--- + +## 完成清单 + +### 功能完成标准 + +- [ ] 用户可以使用邮箱密码或 Magic Link 登录 +- [ ] 管理员可以创建、编辑、删除内容 +- [ ] 配置更新实时生效 +- [ ] 权限控制正常工作 +- [ ] E2E 测试全部通过 +- [ ] 性能指标达标(响应时间 < 500ms) +- [ ] 安全检查通过 + +### 代码质量标准 + +- [ ] TypeScript 类型覆盖率 ≥ 90% +- [ ] ESLint 规则通过率 100% +- [ ] 单元测试覆盖率 ≥ 80% +- [ ] E2E 测试覆盖率 ≥ 60% + +### 文档完成标准 + +- [ ] README 更新完整 +- [ ] API 文档完善 +- [ ] 使用手册清晰 +- [ ] 部署文档详细 + +--- + +## 后续优化建议 + +1. **性能优化** + - 添加 Redis 缓存层 + - 实现图片 CDN 加速 + - 优化数据库查询 + +2. **功能增强** + - 实现内容定时发布 + - 添加内容审核工作流 + - 支持多语言内容 + +3. **安全加固** + - 实现双因素认证 + - 添加 IP 白名单 + - 完善审计日志 + +4. **运维监控** + - 集成 Sentry 错误监控 + - 添加性能监控 + - 实现自动备份 + +--- + +**计划完成!** 🎉 + +**保存位置:** `docs/plans/2026-03-08-configurable-cms-implementation.md`