From 92edcba99fb22060f3f6210260c47a6854070edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Sun, 8 Mar 2026 20:21:38 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=88=9B=E5=BB=BA=E5=8F=AF=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=8C=96=20CMS=20=E6=89=A7=E8=A1=8C=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 针对 Resend 邮件服务配置 - 使用本地文件存储 - 配置域名 novalon.cn - 13 个详细任务,分 3 个阶段 - 预计 6-7 天完成 --- .../2026-03-08-configurable-cms-execution.md | 1780 +++++++++++++++++ 1 file changed, 1780 insertions(+) create mode 100644 docs/plans/2026-03-08-configurable-cms-execution.md diff --git a/docs/plans/2026-03-08-configurable-cms-execution.md b/docs/plans/2026-03-08-configurable-cms-execution.md new file mode 100644 index 0000000..8b16912 --- /dev/null +++ b/docs/plans/2026-03-08-configurable-cms-execution.md @@ -0,0 +1,1780 @@ +# 可配置化 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 开始执行任务