# 可配置化 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`