# 可配置化 CMS 系统执行计划 > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **目标:** 为 Novalon Website 构建可配置的内容管理系统,使用 Resend 邮件服务、本地文件存储、域名 novalon.cn **架构:** Next.js 16 + SQLite + Drizzle ORM + NextAuth.js + Resend **技术栈:** Next.js, React, TypeScript, SQLite, Drizzle ORM, NextAuth.js, Tiptap, Resend **关键配置:** - 邮件服务:Resend (API Key: re_icMNpBzS_DL9GirDmhG5PbNU6PLRWvUtY) - 文件存储:本地存储 (uploads/) - 域名:novalon.cn --- ## 前置准备 ### 环境要求 - Node.js 18+ - npm - Git ### 设计文档 - 设计文档:`docs/plans/2026-03-08-configurable-cms-design.md` - 详细实施计划:`docs/plans/2026-03-08-configurable-cms-implementation.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 resend nanoid date-fns npm install -D @types/nanoid ``` **步骤 5: 验证安装** 运行命令: ```bash npm list drizzle-orm next-auth @tiptap/react resend ``` 预期输出:所有依赖包版本正确显示 **步骤 6: 提交** 运行命令: ```bash git add package.json package-lock.json git commit -m "chore: 添加 CMS 系统所需依赖包 - 数据库: drizzle-orm, @libsql/client - 认证: next-auth, bcryptjs - 编辑器: @tiptap/react - 邮件: resend - 工具: nanoid, date-fns" ``` --- ### Task 2: 配置环境变量 **文件:** - 创建: `.env.local` - 修改: `.gitignore` **步骤 1: 创建环境变量文件** 创建 `.env.local` 文件,内容如下: ```env # Database DATABASE_URL="file:./data.db" # NextAuth NEXTAUTH_SECRET="novalon-cms-secret-key-2026-change-in-production" NEXTAUTH_URL="http://localhost:3000" # Resend Email RESEND_API_KEY="re_icMNpBzS_DL9GirDmhG5PbNU6PLRWvUtY" EMAIL_FROM="noreply@novalon.cn" # Admin (初始管理员账号) ADMIN_EMAIL="admin@novalon.cn" ADMIN_PASSWORD="admin123456" # File Upload UPLOAD_DIR="./uploads" MAX_FILE_SIZE="10485760" # Site SITE_URL="https://novalon.cn" SITE_NAME="睿新致遠" ``` **步骤 2: 更新 .gitignore** 在 `.gitignore` 文件中添加: ``` # Database *.db *.db-journal data.db # Environment .env.local .env.*.local # Uploads uploads/ ``` **步骤 3: 提交** 运行命令: ```bash git add .gitignore git commit -m "chore: 配置环境变量和 .gitignore - 添加数据库、认证、邮件配置 - 配置 Resend API Key - 配置本地文件上传目录 - 忽略敏感文件" ``` --- ### Task 3: 配置数据库连接 **文件:** - 创建: `src/db/index.ts` **步骤 1: 创建数据库连接文件** 创建 `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); ``` **步骤 2: 提交** 运行命令: ```bash git add src/db/index.ts git commit -m "feat: 配置数据库连接" ``` --- ### Task 4: 定义数据库 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: 提交** 运行命令: ```bash git add src/db/schema.ts drizzle.config.ts package.json git commit -m "feat: 定义数据库 Schema - 用户表 (users) - 内容表 (content) - 版本历史表 (content_versions) - 站点配置表 (site_config) - 操作日志表 (audit_logs)" ``` --- ### Task 5: 生成并执行数据库迁移 **文件:** - 创建: `drizzle/0000_*.sql` (自动生成) - 创建: `data.db` (自动创建) **步骤 1: 生成迁移文件** 运行命令: ```bash npm run db:generate ``` 预期输出:在 `drizzle/` 目录生成 SQL 迁移文件 **步骤 2: 执行迁移** 运行命令: ```bash npm run db:push ``` 预期输出:创建 `data.db` 文件,包含所有表 **步骤 3: 验证表结构** 运行命令: ```bash sqlite3 data.db ".tables" ``` 预期输出:显示所有表名 (users, content, content_versions, site_config, audit_logs) **步骤 4: 提交** 运行命令: ```bash git add drizzle/ git commit -m "feat: 生成数据库迁移文件" ``` --- ### Task 6: 创建数据库种子数据 **文件:** - 创建: `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('🌱 开始种子数据...'); try { // 创建管理员用户 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('🎉 种子数据完成!'); console.log('📝 管理员账号: admin@novalon.cn'); console.log('🔑 默认密码: admin123456'); } catch (error) { console.error('❌ 种子数据失败:', error); process.exit(1); } } seed(); ``` **步骤 2: 执行种子数据** 运行命令: ```bash npm run db:seed ``` 预期输出:创建管理员用户和默认配置 **步骤 3: 验证数据** 运行命令: ```bash sqlite3 data.db "SELECT email, role FROM users" ``` 预期输出:显示管理员邮箱和角色 **步骤 4: 提交** 运行命令: ```bash git add src/db/seed.ts git commit -m "feat: 添加数据库种子数据脚本 - 创建管理员用户 (admin@novalon.cn) - 创建默认功能配置 - 创建默认 SEO 配置" ``` --- ### Task 7: 配置 NextAuth.js **文件:** - 创建: `src/lib/auth.ts` - 创建: `src/app/api/auth/[...nextauth]/route.ts` - 创建: `src/providers/session-provider.tsx` - 创建: `src/types/next-auth.d.ts` - 修改: `src/app/layout.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 { Resend } from 'resend'; import { db } from '@/db'; import { users } from '@/db/schema'; import { eq } from 'drizzle-orm'; import bcrypt from 'bcryptjs'; const resend = new Resend(process.env.RESEND_API_KEY); 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: {}, from: process.env.EMAIL_FROM || 'noreply@novalon.cn', sendVerificationRequest: async ({ identifier: email, url }) => { try { await resend.emails.send({ from: process.env.EMAIL_FROM || 'noreply@novalon.cn', to: email, subject: '睿新致遠 - 登录验证链接', html: `

睿新致遠管理后台登录

您好!

您收到这封邮件是因为您请求登录睿新致遠管理后台。

请点击下方按钮完成登录:

立即登录

如果您没有请求此链接,请忽略此邮件。


四川睿新致远科技有限公司

`, }); } catch (error) { console.error('发送邮件失败:', error); throw new Error('发送邮件失败'); } }, }), ], 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/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; } } ``` **步骤 5: 更新根布局** 修改 `src/app/layout.tsx`,在 `` 内添加 SessionProvider: ```typescript import { SessionProvider } from '@/providers/session-provider'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` **步骤 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 认证系统 - 支持邮箱密码登录 - 支持 Magic Link 登录(Resend) - 配置 Session Provider - 添加 TypeScript 类型定义" ``` --- ### Task 8: 创建权限检查工具 **文件:** - 创建: `src/lib/auth/permissions.ts` - 创建: `src/lib/auth/check-permission.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: 创建权限检查函数** 创建 `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: 提交** 运行命令: ```bash git add src/lib/auth/ git commit -m "feat: 创建权限检查工具 - 定义权限矩阵(admin/editor/viewer) - 实现权限检查函数 - 实现权限要求函数" ``` --- ## 阶段二:内容管理 API(预计 2 天) ### Task 9: 创建内容 CRUD API **文件:** - 创建: `src/lib/validators/content.ts` - 创建: `src/app/api/v1/content/route.ts` - 创建: `src/app/api/v1/content/[id]/route.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: 提交** 运行命令: ```bash git add src/lib/validators/ src/app/api/v1/content/ git commit -m "feat: 创建内容 CRUD API - GET /api/v1/content - 获取内容列表 - POST /api/v1/content - 创建内容 - GET /api/v1/content/:id - 获取单个内容 - PUT /api/v1/content/:id - 更新内容 - DELETE /api/v1/content/:id - 删除内容 - 添加 Zod 验证 - 添加权限检查" ``` --- ### Task 10: 创建配置管理 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 { NextResponse } from 'next/server'; import { db } from '@/db'; import { siteConfig } from '@/db/schema'; 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 - GET /api/v1/config - 获取所有配置 - GET /api/v1/config/:key - 获取单个配置 - PUT /api/v1/config/:key - 更新配置 - 添加权限检查" ``` --- ## 阶段三:管理后台界面(预计 2 天) ### Task 11: 创建管理后台布局 **文件:** - 创建: `src/components/admin/sidebar.tsx` - 创建: `src/components/admin/header.tsx` - 创建: `src/app/admin/layout.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/components/admin/ src/app/admin/layout.tsx git commit -m "feat: 创建管理后台布局 - 侧边栏导航 - 顶部栏(用户信息、登出) - 权限保护(未登录重定向)" ``` --- ### Task 12: 创建登录页面 **文件:** - 创建: `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: 提交** 运行命令: ```bash git add src/app/admin/login/ git commit -m "feat: 创建管理后台登录页面 - 邮箱密码登录 - Magic Link 登录 - 错误提示 - 加载状态" ``` --- ### Task 13: 创建仪表盘页面 **文件:** - 创建: `src/components/admin/stats-card.tsx` - 创建: `src/app/admin/page.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/components/admin/stats-card.tsx src/app/admin/page.tsx git commit -m "feat: 创建管理后台仪表盘页面 - 统计卡片(新闻、产品、服务、用户数量) - 最近更新区域 - 快捷操作区域" ``` --- ## 完成清单 ### 功能完成标准 - [ ] 用户可以使用邮箱密码或 Magic Link 登录 - [ ] 管理员可以创建、编辑、删除内容 - [ ] 配置更新实时生效 - [ ] 权限控制正常工作 - [ ] 数据库迁移成功 - [ ] 种子数据创建成功 ### 代码质量标准 - [ ] TypeScript 类型覆盖率 ≥ 90% - [ ] ESLint 规则通过率 100% - [ ] 所有文件提交到 Git ### 配置完成标准 - [ ] Resend API Key 配置正确 - [ ] 本地文件上传目录创建 - [ ] 域名配置正确 --- ## 后续任务 完成上述 13 个任务后,还需要: 1. **内容管理界面**(新闻列表、创建、编辑页面) 2. **富文本编辑器集成** 3. **文件上传功能** 4. **配置中心界面** 5. **用户管理界面** 6. **E2E 测试** 7. **性能优化** 8. **部署上线** --- **计划完成!** 🎉 **保存位置:** `docs/plans/2026-03-08-configurable-cms-execution.md` **预计完成时间:** 6-7 天 **下一步:** 使用 `executing-plans` skill 开始执行任务