From cd1d6aa28a23bd601b7a774fec269a5ec8fdeace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Tue, 21 Apr 2026 07:51:27 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=B8=85=E7=90=86=E8=BF=87=E6=97=B6?= =?UTF-8?q?=E7=9A=84=E8=AE=A1=E5=88=92=E6=96=87=E6=A1=A3=EF=BC=8C=E4=BF=9D?= =?UTF-8?q?=E7=95=99=E6=9C=80=E6=96=B0=E4=BC=98=E5=8C=96=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...13-intelligent-tiered-test-optimization.md | 1612 ----------- .../2026-03-08-configurable-cms-design.md | 1260 -------- .../2026-03-08-configurable-cms-execution.md | 1780 ------------ ...6-03-08-configurable-cms-implementation.md | 2182 -------------- .../2026-03-09-production-readiness-plan.md | 1213 -------- ...26-03-09-test-coverage-improvement-plan.md | 1171 -------- ...26-03-10-full-module-test-coverage-plan.md | 947 ------ ...2026-03-10-gradual-coverage-improvement.md | 579 ---- ...03-10-phased-launch-implementation-plan.md | 2561 ----------------- ...-10-production-readiness-execution-plan.md | 2512 ---------------- ...26-03-10-test-coverage-improvement-plan.md | 1323 --------- ...-12-admin-e2e-test-coverage-improvement.md | 1176 -------- ...26-03-12-admin-e2e-test-coverage-report.md | 199 -- ...026-03-20-quality-improvement-iteration.md | 1586 ---------- ...26-03-24-code-quality-tools-integration.md | 906 ------ ...03-24-contact-form-security-enhancement.md | 2248 --------------- ...-03-28-monorepo-multi-site-architecture.md | 255 -- .../2026-04-21-project-optimization-plan.md | 769 +++++ 18 files changed, 769 insertions(+), 23510 deletions(-) delete mode 100644 docs/plans/2025-03-13-intelligent-tiered-test-optimization.md delete mode 100644 docs/plans/2026-03-08-configurable-cms-design.md delete mode 100644 docs/plans/2026-03-08-configurable-cms-execution.md delete mode 100644 docs/plans/2026-03-08-configurable-cms-implementation.md delete mode 100644 docs/plans/2026-03-09-production-readiness-plan.md delete mode 100644 docs/plans/2026-03-09-test-coverage-improvement-plan.md delete mode 100644 docs/plans/2026-03-10-full-module-test-coverage-plan.md delete mode 100644 docs/plans/2026-03-10-gradual-coverage-improvement.md delete mode 100644 docs/plans/2026-03-10-phased-launch-implementation-plan.md delete mode 100644 docs/plans/2026-03-10-production-readiness-execution-plan.md delete mode 100644 docs/plans/2026-03-10-test-coverage-improvement-plan.md delete mode 100644 docs/plans/2026-03-12-admin-e2e-test-coverage-improvement.md delete mode 100644 docs/plans/2026-03-12-admin-e2e-test-coverage-report.md delete mode 100644 docs/plans/2026-03-20-quality-improvement-iteration.md delete mode 100644 docs/plans/2026-03-24-code-quality-tools-integration.md delete mode 100644 docs/plans/2026-03-24-contact-form-security-enhancement.md delete mode 100644 docs/plans/2026-03-28-monorepo-multi-site-architecture.md create mode 100644 docs/plans/2026-04-21-project-optimization-plan.md diff --git a/docs/plans/2025-03-13-intelligent-tiered-test-optimization.md b/docs/plans/2025-03-13-intelligent-tiered-test-optimization.md deleted file mode 100644 index 163224a..0000000 --- a/docs/plans/2025-03-13-intelligent-tiered-test-optimization.md +++ /dev/null @@ -1,1612 +0,0 @@ -# 智能分层并行测试优化实施计划 - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** 实现智能分层并行测试策略,将E2E测试执行时间从38.7分钟减少到25-28分钟(提升30-35%),同时优化资源利用率和开发体验。 - -**Architecture:** 基于测试类型和资源需求的动态分层架构,包含快速层、标准层、深度层三个层级,每层使用不同的并行策略和超时配置,配合智能调度算法优化执行顺序。 - -**Tech Stack:** Playwright, TypeScript, Node.js, Shell Scripts, Git - ---- - -## 概述 - -本计划将智能分层并行测试策略分解为4个阶段,每个阶段包含具体的可执行任务。实施遵循TDD原则,每个任务都包含测试验证步骤。 - -**预期效果:** -- 执行时间减少30-35%(38.7分钟 → 25-28分钟) -- 快速反馈:5分钟内完成关键测试 -- 资源效率提升40-50% -- 代码可维护性提升 - ---- - -## 阶段1:基础配置(1-2天) - -### Task 1: 创建测试分层配置文件 - -**Files:** -- Create: `e2e/playwright.config.tiered.ts` -- Create: `e2e/src/config/test-tiers.ts` - -**Step 1: 定义测试层级配置** - -```typescript -// e2e/src/config/test-tiers.ts -export interface TestTierConfig { - name: string; - description: string; - testMatch: string | RegExp; - timeout: number; - retries: number; - workers: number | string; - fullyParallel: boolean; - failFast: boolean; -} - -export const TEST_TIERS: Record = { - fast: { - name: '快速层', - description: '冒烟测试、API测试、基础功能验证', - testMatch: /.*\.smoke\.spec\.ts$|.*\.api\.spec\.ts$/, - timeout: 30000, - retries: 1, - workers: process.env.CI ? 6 : '75%', - fullyParallel: true, - failFast: true, - }, - standard: { - name: '标准层', - description: '功能测试、响应式测试、移动端核心功能', - testMatch: /.*(admin|navigation|responsive|mobile).*\.spec\.ts$/, - timeout: 60000, - retries: 2, - workers: process.env.CI ? 4 : '50%', - fullyParallel: true, - failFast: false, - }, - deep: { - name: '深度层', - description: '视觉回归、性能测试、完整回归测试', - testMatch: /.*(visual|performance|regression).*\.spec\.ts$/, - timeout: 120000, - retries: 3, - workers: process.env.CI ? 2 : '25%', - fullyParallel: false, - failFast: false, - }, -}; - -export function getTestTier(tierName: string): TestTierConfig { - return TEST_TIERS[tierName] || TEST_TIERS.standard; -} -``` - -**Step 2: 创建分层Playwright配置** - -```typescript -// e2e/playwright.config.tiered.ts -import { defineConfig, devices } from '@playwright/test'; -import { getEnvironment } from './src/config/environments'; -import { getMobileDevices } from './src/utils/devices'; -import { getTestTier, TEST_TIERS } from './src/config/test-tiers'; - -const env = getEnvironment(); - -function createTieredConfig(tierName: string) { - const tier = getTestTier(tierName); - - return defineConfig({ - testDir: './src/tests', - fullyParallel: tier.fullyParallel, - forbidOnly: !!process.env.CI, - retries: tier.retries, - workers: tier.workers, - globalSetup: require.resolve('./global-setup'), - reporter: [ - ['html', { open: 'never' }], - ['json', { outputFile: `test-results/${tierName}-results.json` }], - ['junit', { outputFile: `test-results/${tierName}-junit.xml` }], - ['line'], - ['list'], - ], - timeout: tier.timeout, - expect: { - timeout: tier.timeout / 2, - }, - use: { - baseURL: env.baseURL, - trace: env.trace, - screenshot: env.screenshot, - video: env.video, - headless: true, - viewport: { width: 1280, height: 720 }, - actionTimeout: tier.timeout / 2, - navigationTimeout: tier.timeout, - launchOptions: { - slowMo: env.slowMo, - }, - storageState: '.auth/admin.json', - }, - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - { - name: 'Mobile Chrome', - use: { ...devices['Pixel 5'] }, - }, - ], - webServer: env.name === 'development' && !process.env.DISABLE_WEB_SERVER ? { - command: 'cd .. && npm run dev', - url: 'http://localhost:3000', - timeout: 120000, - reuseExistingServer: !process.env.CI, - } : undefined, - }); -} - -export default createTieredConfig(process.env.TEST_TIER || 'standard'); -``` - -**Step 3: 运行配置验证** - -```bash -# 验证快速层配置 -cd e2e && TEST_TIER=fast npx playwright test --config=playwright.config.tiered.ts --list - -# 预期输出:显示所有匹配快速层模式的测试文件 -``` - -Expected: 显示匹配.smoke和.api的测试文件列表 - -**Step 4: 提交配置文件** - -```bash -git add e2e/playwright.config.tiered.ts e2e/src/config/test-tiers.ts -git commit -m "feat: add test tier configuration and tiered playwright config" -``` - ---- - -### Task 2: 添加NPM脚本支持分层测试 - -**Files:** -- Modify: `package.json` - -**Step 1: 添加分层测试脚本** - -```json -{ - "scripts": { - "test:tier:fast": "cd e2e && TEST_TIER=fast npx playwright test --config=playwright.config.tiered.ts", - "test:tier:standard": "cd e2e && TEST_TIER=standard npx playwright test --config=playwright.config.tiered.ts", - "test:tier:deep": "cd e2e && TEST_TIER=deep npx playwright test --config=playwright.config.tiered.ts", - "test:tier:all": "npm run test:tier:fast && npm run test:tier:standard && npm run test:tier:deep", - "test:tier:ci": "npm run test:tier:fast && npm run test:tier:standard || npm run test:tier:deep" - } -} -``` - -**Step 2: 测试分层脚本** - -```bash -# 测试快速层 -npm run test:tier:fast - -# 预期输出:快速层测试执行完成,显示通过/失败统计 -``` - -Expected: 快速层测试在5-8分钟内完成 - -**Step 3: 提交脚本更新** - -```bash -git add package.json -git commit -m "feat: add tiered test scripts to package.json" -``` - ---- - -### Task 3: 创建测试标记和分类 - -**Files:** -- Create: `e2e/src/config/test-tags.ts` - -**Step 1: 定义测试标记** - -```typescript -// e2e/src/config/test-tags.ts -export const TEST_TAGS = { - CRITICAL: '@critical', - SMOKE: '@smoke', - REGRESSION: '@regression', - VISUAL: '@visual', - PERFORMANCE: '@performance', - API: '@api', - MOBILE: '@mobile', - RESPONSIVE: '@responsive', - ADMIN: '@admin', - ACCESSIBILITY: '@a11y', - SECURITY: '@security', -} as const; - -export type TestTag = typeof TEST_TAGS[keyof typeof TEST_TAGS]; - -export function getTestPriority(tags: string[]): number { - if (tags.includes(TEST_TAGS.CRITICAL)) return 1; - if (tags.includes(TEST_TAGS.SMOKE)) return 2; - if (tags.includes(TEST_TAGS.REGRESSION)) return 3; - return 4; -} -``` - -**Step 2: 更新现有测试文件添加标记** - -```typescript -// 在关键测试文件顶部添加标记 -// e2e/src/tests/smoke/navigation.smoke.spec.ts -test.describe('导航冒烟测试 @smoke @critical', () => { - // ... existing tests -}); -``` - -**Step 3: 验证标记识别** - -```bash -# 测试标记过滤 -cd e2e && npx playwright test --grep "@smoke" --list - -# 预期输出:显示所有@smoke标记的测试 -``` - -Expected: 显示所有冒烟测试 - -**Step 4: 提交标记配置** - -```bash -git add e2e/src/config/test-tags.ts -git commit -m "feat: add test tags and priority system" -``` - ---- - -## 阶段2:智能调度(2-3天) - -### Task 4: 实现测试执行历史收集 - -**Files:** -- Create: `e2e/src/utils/test-history.ts` -- Create: `e2e/test-history.json` - -**Step 1: 创建历史数据结构** - -```typescript -// e2e/src/utils/test-history.ts -import fs from 'fs'; -import path from 'path'; - -interface TestExecutionRecord { - testId: string; - file: string; - title: string; - duration: number; - timestamp: number; - success: boolean; - flaky: boolean; -} - -interface TestHistory { - records: TestExecutionRecord[]; - lastUpdated: number; -} - -const HISTORY_FILE = path.join(__dirname, '../../test-history.json'); - -export class TestHistoryManager { - private history: TestHistory; - - constructor() { - this.loadHistory(); - } - - private loadHistory(): void { - if (fs.existsSync(HISTORY_FILE)) { - const data = fs.readFileSync(HISTORY_FILE, 'utf-8'); - this.history = JSON.parse(data); - } else { - this.history = { records: [], lastUpdated: Date.now() }; - } - } - - private saveHistory(): void { - this.history.lastUpdated = Date.now(); - fs.writeFileSync(HISTORY_FILE, JSON.stringify(this.history, null, 2)); - } - - recordExecution(testId: string, file: string, title: string, duration: number, success: boolean): void { - const record: TestExecutionRecord = { - testId, - file, - title, - duration, - timestamp: Date.now(), - success, - flaky: this.isFlaky(testId), - }; - - this.history.records.push(record); - - // 只保留最近1000条记录 - if (this.history.records.length > 1000) { - this.history.records = this.history.records.slice(-1000); - } - - this.saveHistory(); - } - - getAverageDuration(testId: string): number { - const testRecords = this.history.records.filter(r => r.testId === testId); - if (testRecords.length === 0) return 0; - - const durations = testRecords.map(r => r.duration); - return durations.reduce((a, b) => a + b, 0) / durations.length; - } - - isFlaky(testId: string): boolean { - const testRecords = this.history.records - .filter(r => r.testId === testId) - .slice(-10); // 只看最近10次执行 - - if (testRecords.length < 5) return false; - - const failureCount = testRecords.filter(r => !r.success).length; - return failureCount >= 3; // 10次中有3次以上失败认为是flaky - } - - getSlowTests(threshold: number = 2): TestExecutionRecord[] { - const avgDurations = new Map(); - - this.history.records.forEach(record => { - const avg = avgDurations.get(record.testId) || 0; - const count = this.history.records.filter(r => r.testId === record.testId).length; - avgDurations.set(record.testId, avg + record.duration / count); - }); - - return Array.from(avgDurations.entries()) - .filter(([_, avg]) => avg > threshold * 60000) // 超过2倍平均时间 - .map(([testId, avg]) => ({ - testId, - file: this.history.records.find(r => r.testId === testId)!.file, - title: this.history.records.find(r => r.testId === testId)!.title, - duration: avg, - timestamp: Date.now(), - success: true, - flaky: false, - })) - .sort((a, b) => b.duration - a.duration); - } -} -``` - -**Step 2: 创建初始历史文件** - -```bash -# 创建空的历史文件 -cat > e2e/test-history.json << 'EOF' -{ - "records": [], - "lastUpdated": 0 -} -EOF -``` - -**Step 3: 测试历史管理器** - -```bash -# 创建测试脚本验证功能 -cat > e2e/test-history-test.js << 'EOF' -const { TestHistoryManager } = require('./src/utils/test-history.ts'); - -const manager = new TestHistoryManager(); -console.log('History loaded successfully'); -console.log('Average duration:', manager.getAverageDuration('test-1')); -EOF - -node e2e/test-history-test.js -``` - -Expected: 输出"History loaded successfully" - -**Step 4: 提交历史管理器** - -```bash -git add e2e/src/utils/test-history.ts e2e/test-history.json -git commit -m "feat: add test execution history manager" -``` - ---- - -### Task 5: 实现智能测试调度器 - -**Files:** -- Create: `e2e/src/utils/test-scheduler.ts` - -**Step 1: 创建调度器核心逻辑** - -```typescript -// e2e/src/utils/test-scheduler.ts -import { TestHistoryManager } from './test-history'; -import { getTestPriority } from '../config/test-tags'; - -interface TestSchedule { - testId: string; - file: string; - title: string; - priority: number; - estimatedDuration: number; - dependencies: string[]; -} - -export class TestScheduler { - private historyManager: TestHistoryManager; - - constructor() { - this.historyManager = new TestHistoryManager(); - } - - scheduleTests(testFiles: string[]): TestSchedule[] { - const schedules: TestSchedule[] = []; - - for (const file of testFiles) { - const testId = this.generateTestId(file); - const priority = this.calculatePriority(file); - const estimatedDuration = this.historyManager.getAverageDuration(testId) || 60000; - const dependencies = this.analyzeDependencies(file); - - schedules.push({ - testId, - file, - title: this.extractTestTitle(file), - priority, - estimatedDuration, - dependencies, - }); - } - - // 按优先级排序,同优先级按预计时间排序 - return schedules.sort((a, b) => { - if (a.priority !== b.priority) { - return a.priority - b.priority; - } - return a.estimatedDuration - b.estimatedDuration; - }); - } - - private generateTestId(file: string): string { - return file.replace(/[^a-zA-Z0-9]/g, '-'); - } - - private calculatePriority(file: string): number { - if (file.includes('smoke')) return 1; - if (file.includes('api')) return 2; - if (file.includes('admin')) return 3; - if (file.includes('regression')) return 4; - return 5; - } - - private extractTestTitle(file: string): string { - const parts = file.split('/'); - return parts[parts.length - 1].replace('.spec.ts', ''); - } - - private analyzeDependencies(file: string): string[] { - const dependencies: string[] = []; - - // 简单的依赖分析:admin测试依赖登录 - if (file.includes('admin') && !file.includes('login')) { - dependencies.push('admin-login'); - } - - return dependencies; - } - - optimizeExecutionOrder(schedules: TestSchedule[]): TestSchedule[] { - const optimized: TestSchedule[] = []; - const executed = new Set(); - - // 先执行无依赖的测试 - const noDeps = schedules.filter(s => s.dependencies.length === 0); - optimized.push(...noDeps); - noDeps.forEach(s => executed.add(s.testId)); - - // 按依赖顺序执行其他测试 - let remaining = schedules.filter(s => !executed.has(s.testId)); - let iterations = 0; - - while (remaining.length > 0 && iterations < 100) { - const canExecute = remaining.filter(s => - s.dependencies.every(dep => executed.has(dep)) - ); - - if (canExecute.length === 0) { - // 循环依赖,强制执行第一个 - optimized.push(remaining[0]); - executed.add(remaining[0].testId); - remaining = remaining.slice(1); - } else { - optimized.push(...canExecute); - canExecute.forEach(s => executed.add(s.testId)); - remaining = remaining.filter(s => !executed.has(s.testId)); - } - - iterations++; - } - - return optimized; - } -} -``` - -**Step 2: 测试调度器** - -```bash -# 创建测试脚本 -cat > e2e/test-scheduler-test.js << 'EOF' -const { TestScheduler } = require('./src/utils/test-scheduler.ts'); - -const scheduler = new TestScheduler(); -const testFiles = [ - 'smoke/navigation.smoke.spec.ts', - 'admin/news-management.spec.ts', - 'api/admin.api.spec.ts', -]; - -const schedule = scheduler.scheduleTests(testFiles); -console.log('Scheduled tests:', schedule); -EOF - -node e2e/test-scheduler-test.js -``` - -Expected: 输出按优先级排序的测试计划 - -**Step 3: 提交调度器** - -```bash -git add e2e/src/utils/test-scheduler.ts -git commit -m "feat: add intelligent test scheduler" -``` - ---- - -### Task 6: 集成历史记录到测试执行 - -**Files:** -- Modify: `e2e/global-setup.ts` - -**Step 1: 添加历史记录钩子** - -```typescript -// e2e/global-setup.ts -import { FullConfig, FullResult } from '@playwright/test'; -import { TestHistoryManager } from './src/utils/test-history'; - -const historyManager = new TestHistoryManager(); - -export async function globalSetup(config: FullConfig) { - // 现有的setup逻辑... -} - -export async function globalTeardown(config: FullConfig, result: FullResult) { - // 记录测试执行历史 - for (const suite of result.suites) { - for (const spec of suite.suites) { - for (const test of spec.tests) { - const testId = `${spec.file}::${test.title}`; - historyManager.recordExecution( - testId, - spec.file, - test.title, - test.results[0]?.duration || 0, - test.results[0]?.status === 'passed' - ); - } - } - } -} -``` - -**Step 2: 测试历史记录** - -```bash -# 运行一个测试并检查历史文件 -cd e2e && npx playwright test src/tests/smoke/navigation.smoke.spec.ts --project="Mobile Chrome" -cat test-history.json | jq '.records | length' -``` - -Expected: 显示历史记录数量增加 - -**Step 3: 提交集成** - -```bash -git add e2e/global-setup.ts -git commit -m "feat: integrate test history recording" -``` - ---- - -## 阶段3:监控优化(2-3天) - -### Task 7: 创建实时监控报告器 - -**Files:** -- Create: `e2e/src/reporters/real-time-reporter.ts` - -**Step 1: 实现实时监控报告器** - -```typescript -// e2e/src/reporters/real-time-reporter.ts -import { - FullConfig, - FullResult, - Suite, - TestCase, - TestResult, -} from '@playwright/test'; - -interface RealTimeStats { - total: number; - passed: number; - failed: number; - skipped: number; - duration: number; - currentFile: string; - currentTest: string; - estimatedRemaining: number; -} - -export class RealTimeReporter { - private startTime: number = 0; - private stats: RealTimeStats = { - total: 0, - passed: 0, - failed: 0, - skipped: 0, - duration: 0, - currentFile: '', - currentTest: '', - estimatedRemaining: 0, - }; - private testDurations: number[] = []; - - constructor(private config: FullConfig) {} - - onBegin(config: FullConfig, suite: Suite) { - this.startTime = Date.now(); - this.stats.total = suite.allTests().length; - this.printHeader(); - } - - onTestBegin(test: TestCase) { - this.stats.currentFile = test.location.file; - this.stats.currentTest = test.title; - this.printProgress(); - } - - onTestEnd(test: TestCase, result: TestResult) { - this.testDurations.push(result.duration); - - if (result.status === 'passed') { - this.stats.passed++; - } else if (result.status === 'failed') { - this.stats.failed++; - } else if (result.status === 'skipped') { - this.stats.skipped++; - } - - this.stats.duration = Date.now() - this.startTime; - this.updateEstimatedRemaining(); - this.printProgress(); - } - - onEnd(result: FullResult) { - this.printSummary(result); - } - - private updateEstimatedRemaining(): void { - const completed = this.stats.passed + this.stats.failed + this.stats.skipped; - const remaining = this.stats.total - completed; - - if (this.testDurations.length > 0) { - const avgDuration = this.testDurations.reduce((a, b) => a + b, 0) / this.testDurations.length; - this.stats.estimatedRemaining = remaining * avgDuration; - } - } - - private printHeader(): void { - console.log('\n🚀 开始执行测试套件'); - console.log(`📊 总测试数: ${this.stats.total}`); - console.log('─────────────────────────────────────\n'); - } - - private printProgress(): void { - const completed = this.stats.passed + this.stats.failed + this.stats.skipped; - const progress = ((completed / this.stats.total) * 100).toFixed(1); - const remainingTime = this.formatTime(this.stats.estimatedRemaining); - const elapsedTime = this.formatTime(this.stats.duration); - - process.stdout.write('\r' + ' '.repeat(100)); - process.stdout.write( - `\r✓ ${this.stats.passed} | ✗ ${this.stats.failed} | ⏭️ ${this.stats.skipped} | ` + - `📈 ${progress}% | ⏱️ ${elapsedTime} / ~${remainingTime} | ` + - `📁 ${this.getCurrentFileName()}` - ); - } - - private printSummary(result: FullResult): void { - console.log('\n\n─────────────────────────────────────'); - console.log('📊 测试执行完成'); - console.log(`✓ 通过: ${this.stats.passed}`); - console.log(`✗ 失败: ${this.stats.failed}`); - console.log(`⏭️ 跳过: ${this.stats.skipped}`); - console.log(`⏱️ 总耗时: ${this.formatTime(this.stats.duration)}`); - console.log(`📈 成功率: ${((this.stats.passed / this.stats.total) * 100).toFixed(1)}%`); - - if (this.stats.failed > 0) { - console.log('\n❌ 失败的测试:'); - // 这里可以添加失败测试的详细信息 - } - } - - private getCurrentFileName(): string { - const parts = this.stats.currentFile.split('/'); - return parts[parts.length - 1]; - } - - private formatTime(ms: number): string { - const seconds = Math.floor(ms / 1000); - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; - return `${minutes}m ${remainingSeconds}s`; - } -} - -export default RealTimeReporter; -``` - -**Step 2: 集成实时报告器** - -```typescript -// e2e/playwright.config.tiered.ts -import RealTimeReporter from './src/reporters/real-time-reporter'; - -export default createTieredConfig(process.env.TEST_TIER || 'standard'); - -function createTieredConfig(tierName: string) { - const tier = getTestTier(tierName); - - return defineConfig({ - // ... existing config - reporter: [ - [RealTimeReporter, {}], - ['html', { open: 'never' }], - ['json', { outputFile: `test-results/${tierName}-results.json` }], - ], - }); -} -``` - -**Step 3: 测试实时报告** - -```bash -# 运行测试查看实时报告 -cd e2e && TEST_TIER=fast npx playwright test --config=playwright.config.tiered.ts -``` - -Expected: 显示实时进度条和预计剩余时间 - -**Step 4: 提交实时报告器** - -```bash -git add e2e/src/reporters/real-time-reporter.ts -git commit -m "feat: add real-time monitoring reporter" -``` - ---- - -### Task 8: 创建性能分析工具 - -**Files:** -- Create: `e2e/src/utils/performance-analyzer.ts` - -**Step 1: 实现性能分析器** - -```typescript -// e2e/src/utils/performance-analyzer.ts -import { TestHistoryManager } from './test-history'; - -interface PerformanceMetrics { - totalTests: number; - totalDuration: number; - averageDuration: number; - slowestTests: Array<{ testId: string; duration: number }>; - fastestTests: Array<{ testId: string; duration: number }>; - flakyTests: Array<{ testId: string; failureRate: number }>; - workerUtilization: number; -} - -export class PerformanceAnalyzer { - private historyManager: TestHistoryManager; - - constructor() { - this.historyManager = new TestHistoryManager(); - } - - analyze(results: any): PerformanceMetrics { - const totalTests = results.suites.reduce((sum, suite) => - sum + suite.allTests().length, 0 - ); - - const totalDuration = results.duration; - const averageDuration = totalDuration / totalTests; - - const slowestTests = this.getSlowestTests(10); - const fastestTests = this.getFastestTests(10); - const flakyTests = this.getFlakyTests(); - const workerUtilization = this.calculateWorkerUtilization(results); - - return { - totalTests, - totalDuration, - averageDuration, - slowestTests, - fastestTests, - flakyTests, - workerUtilization, - }; - } - - private getSlowestTests(count: number): Array<{ testId: string; duration: number }> { - const slowTests = this.historyManager.getSlowTests(); - return slowTests.slice(0, count); - } - - private getFastestTests(count: number): Array<{ testId: string; duration: number }> { - const allTests = this.historyManager['history'].records; - const avgDurations = new Map(); - - allTests.forEach(record => { - const avg = avgDurations.get(record.testId) || 0; - const count = allTests.filter(r => r.testId === record.testId).length; - avgDurations.set(record.testId, avg + record.duration / count); - }); - - return Array.from(avgDurations.entries()) - .map(([testId, duration]) => ({ testId, duration })) - .sort((a, b) => a.duration - b.duration) - .slice(0, count); - } - - private getFlakyTests(): Array<{ testId: string; failureRate: number }> { - const flakyTests: Array<{ testId: string; failureRate: number }> = []; - const testRecords = new Map(); - - this.historyManager['history'].records.forEach(record => { - const records = testRecords.get(record.testId) || []; - records.push(record); - testRecords.set(record.testId, records); - }); - - testRecords.forEach((records, testId) => { - if (records.length >= 10) { - const failures = records.filter(r => !r.success).length; - const failureRate = (failures / records.length) * 100; - - if (failureRate > 20) { // 失败率超过20% - flakyTests.push({ testId, failureRate }); - } - } - }); - - return flakyTests.sort((a, b) => b.failureRate - a.failureRate); - } - - private calculateWorkerUtilization(results: any): number { - // 简化的worker利用率计算 - const totalTests = results.suites.reduce((sum, suite) => - sum + suite.allTests().length, 0 - ); - const workers = results.workers || 1; - const parallelism = totalTests / workers; - - return Math.min(parallelism / 10, 1) * 100; // 假设最大并行度为10 - } - - generateReport(metrics: PerformanceMetrics): string { - let report = '\n📊 性能分析报告\n'; - report += '─────────────────────────────────────\n'; - report += `📈 总测试数: ${metrics.totalTests}\n`; - report += `⏱️ 总耗时: ${this.formatTime(metrics.totalDuration)}\n`; - report += `📊 平均耗时: ${this.formatTime(metrics.averageDuration)}\n`; - report += `⚙️ Worker利用率: ${metrics.workerUtilization.toFixed(1)}%\n\n`; - - report += '🐌 最慢的10个测试:\n'; - metrics.slowestTests.forEach((test, index) => { - report += ` ${index + 1}. ${test.testId}: ${this.formatTime(test.duration)}\n`; - }); - - report += '\n⚡ 最快的10个测试:\n'; - metrics.fastestTests.forEach((test, index) => { - report += ` ${index + 1}. ${test.testId}: ${this.formatTime(test.duration)}\n`; - }); - - if (metrics.flakyTests.length > 0) { - report += '\n⚠️ 不稳定的测试:\n'; - metrics.flakyTests.forEach(test => { - report += ` ${test.testId}: 失败率 ${test.failureRate.toFixed(1)}%\n`; - }); - } - - return report; - } - - private formatTime(ms: number): string { - const seconds = Math.floor(ms / 1000); - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; - return `${minutes}m ${remainingSeconds}s`; - } -} -``` - -**Step 2: 测试性能分析** - -```bash -# 创建测试脚本 -cat > e2e/test-performance-analysis.js << 'EOF' -const { PerformanceAnalyzer } = require('./src/utils/performance-analyzer.ts'); -const fs = require('fs'); - -const results = JSON.parse(fs.readFileSync('test-results/fast-results.json', 'utf-8')); -const analyzer = new PerformanceAnalyzer(); -const metrics = analyzer.analyze(results); -console.log(analyzer.generateReport(metrics)); -EOF - -# 运行测试后分析 -cd e2e && TEST_TIER=fast npx playwright test --config=playwright.config.tiered.ts -node e2e/test-performance-analysis.js -``` - -Expected: 显示性能分析报告 - -**Step 3: 提交性能分析器** - -```bash -git add e2e/src/utils/performance-analyzer.ts -git commit -m "feat: add performance analysis tool" -``` - ---- - -## 阶段4:验证调优(1-2天) - -### Task 9: 创建对比测试脚本 - -**Files:** -- Create: `e2e/scripts/benchmark-tests.sh` - -**Step 1: 创建基准测试脚本** - -```bash -#!/bin/bash -# e2e/scripts/benchmark-tests.sh - -set -e - -echo "🚀 开始基准测试对比" -echo "================================" - -# 备份当前配置 -cp playwright.config.ts playwright.config.backup.ts - -# 测试原始配置 -echo "\n📊 测试原始配置..." -START_TIME=$(date +%s) -npm run test:e2e > /tmp/original-results.log 2>&1 -ORIGINAL_DURATION=$(($(date +%s) - START_TIME)) -echo "原始配置耗时: ${ORIGINAL_DURATION}秒" - -# 测试分层配置 -echo "\n📊 测试分层配置..." -START_TIME=$(date +%s) -npm run test:tier:all > /tmp/tiered-results.log 2>&1 -TIERED_DURATION=$(($(date +%s) - START_TIME)) -echo "分层配置耗时: ${TIERED_DURATION}秒" - -# 恢复配置 -mv playwright.config.backup.ts playwright.config.ts - -# 计算改进 -IMPROVEMENT=$((ORIGINAL_DURATION - TIERED_DURATION)) -IMPROVEMENT_PERCENT=$((IMPROVEMENT * 100 / ORIGINAL_DURATION)) - -echo "\n================================" -echo "📈 性能改进报告" -echo "================================" -echo "原始配置耗时: ${ORIGINAL_DURATION}秒" -echo "分层配置耗时: ${TIERED_DURATION}秒" -echo "时间节省: ${IMPROVEMENT}秒 (${IMPROVEMENT_PERCENT}%)" -echo "================================" - -# 保存结果 -cat > /tmp/benchmark-results.json << EOF -{ - "original_duration": ${ORIGINAL_DURATION}, - "tiered_duration": ${TIERED_DURATION}, - "improvement": ${IMPROVEMENT}, - "improvement_percent": ${IMPROVEMENT_PERCENT}, - "timestamp": $(date +%s) -} -EOF - -echo "✅ 基准测试完成,结果保存到 /tmp/benchmark-results.json" -``` - -**Step 2: 添加执行权限** - -```bash -chmod +x e2e/scripts/benchmark-tests.sh -``` - -**Step 3: 测试基准脚本** - -```bash -cd e2e && ./scripts/benchmark-tests.sh -``` - -Expected: 显示性能对比结果 - -**Step 4: 提交基准脚本** - -```bash -git add e2e/scripts/benchmark-tests.sh -git commit -m "feat: add benchmark test script" -``` - ---- - -### Task 10: 创建参数调优指南 - -**Files:** -- Create: `e2e/docs/TUNING_GUIDE.md` - -**Step 1: 编写调优指南** - -```markdown -# 测试性能调优指南 - -## 概述 - -本文档提供测试分层并行策略的参数调优指南,帮助优化测试执行性能。 - -## 关键参数 - -### Worker数量 - -**本地开发环境:** -- 快速层:`workers: '75%'` - 利用75%的CPU核心 -- 标准层:`workers: '50%'` - 利用50%的CPU核心 -- 深度层:`workers: '25%'` - 利用25%的CPU核心 - -**CI/CD环境:** -- 快速层:`workers: 6` - 固定6个worker -- 标准层:`workers: 4` - 固定4个worker -- 深度层:`workers: 2` - 固定2个worker - -**调优建议:** -1. 监控CPU使用率,保持在80-90% -2. 如果CPU使用率低于70%,增加worker数量 -3. 如果出现测试超时或失败,减少worker数量 - -### 超时设置 - -**快速层:** -- 测试超时:`timeout: 30000ms` (30秒) -- 操作超时:`actionTimeout: 15000ms` (15秒) -- 导航超时:`navigationTimeout: 30000ms` (30秒) - -**标准层:** -- 测试超时:`timeout: 60000ms` (60秒) -- 操作超时:`actionTimeout: 30000ms` (30秒) -- 导航超时:`navigationTimeout: 60000ms` (60秒) - -**深度层:** -- 测试超时:`timeout: 120000ms` (120秒) -- 操作超时:`actionTimeout: 60000ms` (60秒) -- 导航超时:`navigationTimeout: 120000ms` (120秒) - -**调优建议:** -1. 根据测试实际执行时间调整超时 -2. 超时应该设置为平均时间的2-3倍 -3. 定期审查超时设置,移除不必要的长超时 - -### 重试策略 - -**快速层:** -- 重试次数:`retries: 1` -- 原因:快速失败,节省时间 - -**标准层:** -- 重试次数:`retries: 2` -- 原因:平衡速度和稳定性 - -**深度层:** -- 重试次数:`retries: 3` -- 原因:最大化稳定性 - -**调优建议:** -1. 监控flaky测试,识别需要更多重试的测试 -2. 对于稳定的测试,减少重试次数 -3. 对于不稳定的测试,增加重试次数或修复测试 - -## 性能监控 - -### 关键指标 - -1. **执行时间趋势** - - 目标:持续减少 - - 监控:每周记录执行时间 - - 阈值:超过历史平均20%需要调查 - -2. **快速层通过率** - - 目标:>95% - - 监控:每次执行 - - 阈值:<90%需要调查 - -3. **Worker利用率** - - 目标:>80% - - 监控:实时监控 - - 阈值:<70%需要增加worker - -4. **测试稳定性** - - 目标:flaky rate <5% - - 监控:历史数据分析 - - 阈值:>10%需要修复 - -### 优化建议 - -1. **识别慢速测试** - - 使用性能分析工具找出最慢的10个测试 - - 分析慢速原因(网络、DOM操作、等待时间) - - 优化或重构慢速测试 - -2. **减少等待时间** - - 使用`waitForSelector`替代固定`waitForTimeout` - - 使用`waitForFunction`等待特定条件 - - 减少不必要的等待 - -3. **优化选择器** - - 使用`data-testid`属性 - - 避免使用复杂的选择器 - - 缓存常用的选择器 - -4. **并行化独立测试** - - 确保测试之间无依赖 - - 使用测试隔离 - - 避免共享状态 - -## 故障排查 - -### 常见问题 - -1. **测试超时** - - 检查网络连接 - - 增加超时时间 - - 优化等待策略 - -2. **Worker利用率低** - - 增加worker数量 - - 检查资源限制 - - 优化测试并行度 - -3. **Flaky测试** - - 检查测试依赖 - - 增加重试次数 - - 修复测试逻辑 - -4. **内存不足** - - 减少worker数量 - - 优化测试内存使用 - - 增加系统内存 - -## 持续改进 - -1. **定期审查** - - 每月审查测试配置 - - 分析性能趋势 - - 调整参数设置 - -2. **A/B测试** - - 对比不同配置 - - 选择最优方案 - - 记录改进效果 - -3. **团队协作** - - 分享最佳实践 - - 统一配置标准 - - 持续学习改进 -``` - -**Step 2: 提交调优指南** - -```bash -git add e2e/docs/TUNING_GUIDE.md -git commit -m "docs: add performance tuning guide" -``` - ---- - -### Task 11: 创建最终验证脚本 - -**Files:** -- Create: `e2e/scripts/validate-optimization.sh` - -**Step 1: 创建验证脚本** - -```bash -#!/bin/bash -# e2e/scripts/validate-optimization.sh - -set -e - -echo "🔍 验证测试优化效果" -echo "================================" - -# 检查配置文件 -echo "\n📁 检查配置文件..." -if [ -f "playwright.config.tiered.ts" ]; then - echo "✅ 分层配置文件存在" -else - echo "❌ 分层配置文件不存在" - exit 1 -fi - -if [ -f "src/config/test-tiers.ts" ]; then - echo "✅ 测试层级配置存在" -else - echo "❌ 测试层级配置不存在" - exit 1 -fi - -# 检查工具文件 -echo "\n🔧 检查工具文件..." -if [ -f "src/utils/test-history.ts" ]; then - echo "✅ 历史管理器存在" -else - echo "❌ 历史管理器不存在" - exit 1 -fi - -if [ -f "src/utils/test-scheduler.ts" ]; then - echo "✅ 测试调度器存在" -else - echo "❌ 测试调度器不存在" - exit 1 -fi - -if [ -f "src/reporters/real-time-reporter.ts" ]; then - echo "✅ 实时报告器存在" -else - echo "❌ 实时报告器不存在" - exit 1 -fi - -if [ -f "src/utils/performance-analyzer.ts" ]; then - echo "✅ 性能分析器存在" -else - echo "❌ 性能分析器不存在" - exit 1 -fi - -# 运行快速层测试 -echo "\n🚀 运行快速层测试..." -START_TIME=$(date +%s) -TEST_TIER=fast npx playwright test --config=playwright.config.tiered.ts -FAST_DURATION=$(($(date +%s) - START_TIME)) -echo "✅ 快速层完成,耗时: ${FAST_DURATION}秒" - -# 验证快速层时间 -if [ $FAST_DURATION -lt 480 ]; then - echo "✅ 快速层时间符合预期(<8分钟)" -else - echo "⚠️ 快速层时间超出预期(>8分钟)" -fi - -# 检查历史记录 -echo "\n📊 检查历史记录..." -if [ -f "test-history.json" ]; then - RECORD_COUNT=$(cat test-history.json | jq '.records | length') - echo "✅ 历史记录数量: ${RECORD_COUNT}" -else - echo "❌ 历史记录文件不存在" - exit 1 -fi - -# 生成性能报告 -echo "\n📈 生成性能报告..." -if [ -f "test-results/fast-results.json" ]; then - node test-performance-analysis.js - echo "✅ 性能报告生成成功" -else - echo "⚠️ 测试结果文件不存在,跳过性能报告" -fi - -echo "\n================================" -echo "✅ 验证完成" -echo "================================" -echo "📊 总结:" -echo " - 配置文件: ✅" -echo " - 工具文件: ✅" -echo " - 快速层测试: ✅ (${FAST_DURATION}秒)" -echo " - 历史记录: ✅ (${RECORD_COUNT}条)" -echo "================================" -``` - -**Step 2: 添加执行权限** - -```bash -chmod +x e2e/scripts/validate-optimization.sh -``` - -**Step 3: 运行验证脚本** - -```bash -cd e2e && ./scripts/validate-optimization.sh -``` - -Expected: 显示所有验证项通过 - -**Step 4: 提交验证脚本** - -```bash -git add e2e/scripts/validate-optimization.sh -git commit -m "feat: add optimization validation script" -``` - ---- - -### Task 12: 创建实施总结文档 - -**Files:** -- Create: `e2e/docs/IMPLEMENTATION_SUMMARY.md` - -**Step 1: 编写实施总结** - -```markdown -# 智能分层并行测试优化 - 实施总结 - -## 概述 - -本文档总结了智能分层并行测试优化策略的实施过程和效果。 - -## 实施内容 - -### 阶段1:基础配置(1-2天) - -✅ **完成的任务:** -1. 创建测试分层配置文件(`playwright.config.tiered.ts`, `test-tiers.ts`) -2. 添加NPM脚本支持分层测试 -3. 创建测试标记和分类系统 - -**关键文件:** -- `e2e/playwright.config.tiered.ts` - 分层Playwright配置 -- `e2e/src/config/test-tiers.ts` - 测试层级定义 -- `e2e/src/config/test-tags.ts` - 测试标记系统 -- `package.json` - 新增分层测试脚本 - -### 阶段2:智能调度(2-3天) - -✅ **完成的任务:** -1. 实现测试执行历史收集(`test-history.ts`) -2. 实现智能测试调度器(`test-scheduler.ts`) -3. 集成历史记录到测试执行(`global-setup.ts`) - -**关键文件:** -- `e2e/src/utils/test-history.ts` - 历史数据管理 -- `e2e/src/utils/test-scheduler.ts` - 智能调度算法 -- `e2e/global-setup.ts` - 历史记录钩子 -- `e2e/test-history.json` - 历史数据存储 - -### 阶段3:监控优化(2-3天) - -✅ **完成的任务:** -1. 创建实时监控报告器(`real-time-reporter.ts`) -2. 创建性能分析工具(`performance-analyzer.ts`) - -**关键文件:** -- `e2e/src/reporters/real-time-reporter.ts` - 实时进度监控 -- `e2e/src/utils/performance-analyzer.ts` - 性能分析工具 - -### 阶段4:验证调优(1-2天) - -✅ **完成的任务:** -1. 创建对比测试脚本(`benchmark-tests.sh`) -2. 创建参数调优指南(`TUNING_GUIDE.md`) -3. 创建最终验证脚本(`validate-optimization.sh`) - -**关键文件:** -- `e2e/scripts/benchmark-tests.sh` - 性能对比脚本 -- `e2e/docs/TUNING_GUIDE.md` - 调优指南 -- `e2e/scripts/validate-optimization.sh` - 验证脚本 - -## 效果评估 - -### 性能改进 - -**执行时间:** -- 原始配置:38.7分钟 -- 优化后配置:25-28分钟 -- 改进幅度:30-35% - -**快速反馈:** -- 快速层执行时间:5-8分钟 -- 关键测试覆盖:~80个测试 -- 通过率目标:>95% - -**资源效率:** -- Worker利用率:>80% -- CPU使用率:80-90% -- 内存使用:优化40-50% - -### 质量提升 - -**测试稳定性:** -- Flaky test检测:自动识别 -- 失败率监控:<5% -- 智能重试:基于历史数据 - -**可维护性:** -- 清晰的分层结构 -- 完善的文档 -- 自动化工具支持 - -## 使用指南 - -### 本地开发 - -```bash -# 运行快速层(推荐日常开发) -npm run test:tier:fast - -# 运行标准层 -npm run test:tier:standard - -# 运行深度层 -npm run test:tier:deep - -# 运行所有层级 -npm run test:tier:all -``` - -### CI/CD集成 - -```yaml -# .github/workflows/test.yml -name: E2E Tests - -on: [push, pull_request] - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: '18' - - - name: Install dependencies - run: npm ci - - - name: Run fast tier tests - run: npm run test:tier:fast - - - name: Run standard tier tests - run: npm run test:tier:standard - - - name: Run deep tier tests - if: success() - run: npm run test:tier:deep - - - name: Upload results - uses: actions/upload-artifact@v3 - with: - name: test-results - path: e2e/test-results/ -``` - -### 性能监控 - -```bash -# 查看实时进度 -npm run test:tier:fast - -# 分析性能 -node e2e/test-performance-analysis.js - -# 对比性能 -cd e2e && ./scripts/benchmark-tests.sh - -# 验证优化 -cd e2e && ./scripts/validate-optimization.sh -``` - -## 持续改进 - -1. **定期审查** - - 每月审查测试配置 - - 分析性能趋势 - - 调整参数设置 - -2. **A/B测试** - - 对比不同配置 - - 选择最优方案 - - 记录改进效果 - -3. **团队协作** - - 分享最佳实践 - - 统一配置标准 - - 持续学习改进 - -## 总结 - -智能分层并行测试优化策略成功实施,达到了预期目标: - -✅ **执行时间减少30-35%** -✅ **快速反馈机制建立** -✅ **资源效率显著提升** -✅ **代码可维护性改善** -✅ **完善的监控和调优工具** - -该优化为团队提供了高效、稳定、可维护的测试执行框架,为持续交付奠定了坚实基础。 -``` - -**Step 2: 提交实施总结** - -```bash -git add e2e/docs/IMPLEMENTATION_SUMMARY.md -git commit -m "docs: add implementation summary" -``` - ---- - -## 总结 - -本实施计划将智能分层并行测试策略分解为12个具体任务,涵盖4个阶段: - -**阶段1:基础配置**(Task 1-3) -- 创建分层配置和测试标记系统 -- 预计时间:1-2天 - -**阶段2:智能调度**(Task 4-6) -- 实现历史记录和智能调度 -- 预计时间:2-3天 - -**阶段3:监控优化**(Task 7-8) -- 创建实时监控和性能分析工具 -- 预计时间:2-3天 - -**阶段4:验证调优**(Task 9-12) -- 创建验证脚本和文档 -- 预计时间:1-2天 - -**总预计时间:6-10天** - -每个任务都遵循TDD原则,包含完整的测试验证步骤,确保代码质量和功能正确性。 diff --git a/docs/plans/2026-03-08-configurable-cms-design.md b/docs/plans/2026-03-08-configurable-cms-design.md deleted file mode 100644 index 4dabc82..0000000 --- a/docs/plans/2026-03-08-configurable-cms-design.md +++ /dev/null @@ -1,1260 +0,0 @@ -# 可配置化 CMS 系统设计文档 - -**项目名称**:Novalon Website 可配置化内容管理系统 -**创建日期**:2026-03-08 -**版本**:v1.0 -**作者**:张翔 - ---- - -## 📋 目录 - -1. [项目概述](#项目概述) -2. [需求分析](#需求分析) -3. [技术选型](#技术选型) -4. [系统架构](#系统架构) -5. [数据库设计](#数据库设计) -6. [API 设计](#api-设计) -7. [管理后台设计](#管理后台设计) -8. [权限体系](#权限体系) -9. [部署策略](#部署策略) -10. [实施计划](#实施计划) -11. [测试策略](#测试策略) -12. [风险与应对](#风险与应对) - ---- - -## 项目概述 - -### 背景 - -当前 Novalon Website 项目的新闻、产品、服务等内容数据硬编码在 `src/lib/constants.ts` 文件中,存在以下问题: - -- ❌ 内容更新需要修改代码并重新部署 -- ❌ 非技术人员无法自主管理内容 -- ❌ 无法实时调整功能开关、样式配置 -- ❌ 缺乏版本控制和审核机制 -- ❌ SEO 配置分散,难以统一管理 - -### 目标 - -构建一个**轻量级、易用、可扩展**的内容管理系统(CMS),实现: - -- ✅ 运营人员可自主管理内容(增删改查) -- ✅ 实时配置功能开关、样式、SEO -- ✅ 支持版本历史和内容回滚 -- ✅ 权限分级管理(管理员/编辑/查看者) -- ✅ 保持现有前端架构不变,渐进式改造 - -### 成功标准 - -1. **功能完整性**:支持新闻、产品、服务、案例的完整 CRUD -2. **易用性**:运营人员无需培训即可上手使用 -3. **性能**:管理后台响应时间 < 500ms,前端页面加载时间 < 2s -4. **安全性**:通过 OWASP Top 10 安全检查 -5. **可维护性**:代码覆盖率 ≥ 80%,文档完善 - ---- - -## 需求分析 - -### 功能需求矩阵 - -| 功能模块 | 子功能 | 优先级 | 角色 | -|---------|--------|-------|------| -| **内容管理** | 新闻 CRUD | P0 | 编辑、管理员 | -| | 产品 CRUD | P0 | 编辑、管理员 | -| | 服务 CRUD | P0 | 编辑、管理员 | -| | 案例管理 | P1 | 编辑、管理员 | -| | 富文本编辑 | P0 | 编辑、管理员 | -| | 图片上传 | P0 | 编辑、管理员 | -| | 定时发布 | P1 | 编辑、管理员 | -| **配置中心** | 功能开关 | P0 | 管理员 | -| | 样式配置 | P1 | 管理员 | -| | SEO 配置 | P0 | 管理员 | -| | 全局设置 | P0 | 管理员 | -| **系统管理** | 用户管理 | P0 | 管理员 | -| | 角色权限 | P0 | 管理员 | -| | 版本历史 | P1 | 编辑、管理员 | -| | 操作日志 | P1 | 管理员 | -| **仪表盘** | 数据统计 | P1 | 所有角色 | -| | 最近动态 | P1 | 所有角色 | - -### 非功能需求 - -- **性能**:支持 100+ 并发用户,响应时间 < 500ms -- **可用性**:99.9% 可用性(月停机时间 < 43 分钟) -- **安全性**:符合 OWASP Top 10,支持 HTTPS,防止 SQL 注入、XSS -- **可扩展性**:支持水平扩展,数据库可迁移到 PostgreSQL -- **兼容性**:支持 Chrome、Firefox、Safari、Edge 最新版本 - ---- - -## 技术选型 - -### 核心技术栈 - -| 类别 | 技术 | 版本 | 选择理由 | -|-----|------|------|---------| -| **框架** | Next.js | 16.x | 已有技术栈,支持 SSR/ISR/API Routes | -| **UI 库** | React | 19.x | 已有技术栈 | -| **语言** | TypeScript | 5.x | 已有技术栈,类型安全 | -| **样式** | Tailwind CSS | 4.x | 已有技术栈 | -| **组件库** | shadcn/ui | latest | 已有技术栈,一致性 | -| **数据库** | SQLite (libsql) | latest | ✅ **已确认**:零配置、单文件、可迁移 | -| **ORM** | Drizzle ORM | latest | 轻量、TypeScript 友好、性能优 | -| **验证** | Zod | 4.x | 已有技术栈 | -| **认证** | NextAuth.js | 5.x | ✅ **已确认**:邮箱密码 + Magic Link | -| **富文本** | Tiptap | 2.x | 现代化、可扩展、协作编辑 | -| **文件上传** | UploadThing | latest | 简单、支持本地/S3 | -| **图表** | @antv/g2 | 5.x | 已有依赖 | -| **动画** | Framer Motion | 12.x | 已有依赖 | - -### 数据库选择理由 - -**SQLite (libsql) 的优势:** - -1. ✅ **零配置**:无需安装数据库服务器,开箱即用 -2. ✅ **单文件存储**:便于备份和迁移 -3. ✅ **性能优秀**:对于中小型网站(< 10万条记录)性能足够 -4. ✅ **可迁移性**:Drizzle ORM 支持无缝迁移到 PostgreSQL -5. ✅ **成本最低**:无需额外的数据库服务器费用 - -**何时迁移到 PostgreSQL:** - -- 数据量超过 10 万条记录 -- 并发写入超过 100 QPS -- 需要高级特性(全文搜索、JSON 索引等) - -### 部署策略 - -**混合模式**(已确认): - -| 页面类型 | 渲染方式 | 更新频率 | 理由 | -|---------|---------|---------|------| -| **首页** | ISR | revalidate: 300s (5分钟) | 性能优先,内容更新不频繁 | -| **列表页** | ISR | revalidate: 300s | 平衡性能和实时性 | -| **详情页** | SSR | 实时 | SEO 优先,内容准确性要求高 | -| **管理后台** | CSR | 实时 | 交互性强,无需 SEO | -| **API Routes** | 动态 | 实时 | 数据操作实时性要求高 | - -**ISR 配置示例:** - -```typescript -// src/app/(marketing)/news/page.tsx -export const revalidate = 300; // 5分钟重新验证 - -export default async function NewsPage() { - const news = await fetchNews(); - return ; -} -``` - ---- - -## 系统架构 - -### 整体架构图 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 客户端层 │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ 前端页面 │ │ 管理后台 │ │ 移动端适配 │ │ -│ │ (ISR/SSR) │ │ (CSR) │ │ (响应式) │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - ↓ HTTPS -┌─────────────────────────────────────────────────────────────┐ -│ Next.js 应用层 │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ Pages │ │ API Routes │ │ Middleware │ │ -│ │ (App Router)│ │ (/api/*) │ │ (Auth/CORS) │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 业务逻辑层 │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ Config │ │ Content │ │ Auth │ │ -│ │ Manager │ │ Service │ │ Service │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 数据访问层 │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ Drizzle ORM │ │ Zod Schema │ │ Cache Layer │ │ -│ │ (Type-safe) │ │ (Validation) │ │ (In-memory) │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 数据存储层 │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ SQLite │ │ 文件系统 │ │ CDN/对象存储 │ │ -│ │ (libsql) │ │ (uploads/) │ │ (可选) │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 数据流设计 - -#### 1. 内容发布流程 - -``` -运营人员 → 管理后台 → 编辑内容 → 保存草稿 - ↓ -提交审核 → 管理员审核 → 发布 → API 验证 - ↓ -数据库更新 → 清除缓存 → ISR 重新生成 → 前端展示 -``` - -#### 2. 配置更新流程 - -``` -管理员 → 配置中心 → 修改配置 → API 验证 - ↓ -数据库更新 → 清除缓存 → 前端重新加载配置 - ↓ -实时生效(无需重新部署) -``` - -#### 3. 用户认证流程 - -``` -用户 → 登录页 → 选择登录方式 - ↓ -┌─────────────┬─────────────┐ -│ 邮箱密码 │ Magic Link │ -│ 验证密码 │ 发送邮件 │ -│ 生成 Token │ 点击链接 │ -└─────────────┴─────────────┘ - ↓ -创建 Session → 重定向到管理后台 -``` - ---- - -## 数据库设计 - -### ER 图 - -``` -┌─────────────┐ ┌─────────────┐ -│ users │ │ roles │ -├─────────────┤ ├─────────────┤ -│ id (PK) │ │ id (PK) │ -│ email │ │ name │ -│ passwordHash│ │ permissions │ -│ roleId (FK) │──────▶│ createdAt │ -│ createdAt │ └─────────────┘ -└─────────────┘ - │ - │ 1:N - ▼ -┌─────────────┐ ┌─────────────┐ -│ content │ │ versions │ -├─────────────┤ ├─────────────┤ -│ id (PK) │ │ id (PK) │ -│ type │ │ contentId │ -│ title │ │ version │ -│ slug │ │ changes │ -│ content │──────▶│ changedBy │ -│ authorId │ │ changedAt │ -│ status │ └─────────────┘ -│ publishedAt │ -│ sortOrder │ -└─────────────┘ - │ - │ 1:N - ▼ -┌─────────────┐ -│ audit_logs │ -├─────────────┤ -│ id (PK) │ -│ userId │ -│ action │ -│ resourceType│ -│ resourceId │ -│ timestamp │ -└─────────────┘ - -┌─────────────┐ -│ site_config │ -├─────────────┤ -│ id (PK) │ -│ key │ -│ value (JSON)│ -│ category │ -│ updatedAt │ -└─────────────┘ -``` - -### 表结构详细设计 - -#### 1. 用户表 (users) - -```sql -CREATE TABLE users ( - id TEXT PRIMARY KEY, - email TEXT UNIQUE NOT NULL, - password_hash TEXT, - name TEXT NOT NULL, - role TEXT NOT NULL DEFAULT 'editor', -- 'admin', 'editor', 'viewer' - avatar TEXT, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL -); - -CREATE INDEX idx_users_email ON users(email); -``` - -#### 2. 内容表 (content) - -```sql -CREATE TABLE content ( - id TEXT PRIMARY KEY, - type TEXT NOT NULL, -- 'news', 'product', 'service', 'case' - title TEXT NOT NULL, - slug TEXT UNIQUE NOT NULL, - excerpt TEXT, - content TEXT NOT NULL, - cover_image TEXT, - category TEXT, - tags TEXT, -- JSON array - status TEXT NOT NULL DEFAULT 'draft', -- 'draft', 'published', 'archived' - published_at INTEGER, - author_id TEXT NOT NULL, - sort_order INTEGER DEFAULT 0, - metadata TEXT, -- JSON for SEO, custom fields - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - FOREIGN KEY (author_id) REFERENCES users(id) -); - -CREATE INDEX idx_content_type ON content(type); -CREATE INDEX idx_content_status ON content(status); -CREATE INDEX idx_content_slug ON content(slug); -CREATE INDEX idx_content_published ON content(published_at DESC); -``` - -#### 3. 版本历史表 (content_versions) - -```sql -CREATE TABLE content_versions ( - id TEXT PRIMARY KEY, - content_id TEXT NOT NULL, - version INTEGER NOT NULL, - title TEXT NOT NULL, - content TEXT NOT NULL, - changes TEXT, -- JSON diff - changed_by TEXT NOT NULL, - changed_at INTEGER NOT NULL, - FOREIGN KEY (content_id) REFERENCES content(id), - FOREIGN KEY (changed_by) REFERENCES users(id) -); - -CREATE INDEX idx_versions_content ON content_versions(content_id, version DESC); -``` - -#### 4. 站点配置表 (site_config) - -```sql -CREATE TABLE site_config ( - id TEXT PRIMARY KEY, - key TEXT UNIQUE NOT NULL, - value TEXT NOT NULL, -- JSON - category TEXT NOT NULL, -- 'feature', 'style', 'seo', 'general' - description TEXT, - updated_at INTEGER NOT NULL, - updated_by TEXT, - FOREIGN KEY (updated_by) REFERENCES users(id) -); - -CREATE INDEX idx_config_key ON site_config(key); -CREATE INDEX idx_config_category ON site_config(category); -``` - -#### 5. 操作日志表 (audit_logs) - -```sql -CREATE TABLE audit_logs ( - id TEXT PRIMARY KEY, - user_id TEXT, - action TEXT NOT NULL, -- 'create', 'update', 'delete', 'publish', 'login' - resource_type TEXT NOT NULL, - resource_id TEXT, - details TEXT, -- JSON - ip_address TEXT, - user_agent TEXT, - timestamp INTEGER NOT NULL, - FOREIGN KEY (user_id) REFERENCES users(id) -); - -CREATE INDEX idx_logs_user ON audit_logs(user_id); -CREATE INDEX idx_logs_timestamp ON audit_logs(timestamp DESC); -``` - -### Drizzle Schema 定义 - -```typescript -// src/db/schema.ts -import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; -import { relations } from 'drizzle-orm'; - -export const users = sqliteTable('users', { - id: text('id').primaryKey(), - email: text('email').notNull().unique(), - passwordHash: text('password_hash'), - name: text('name').notNull(), - role: text('role', { enum: ['admin', 'editor', 'viewer'] }).notNull().default('editor'), - avatar: text('avatar'), - createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), - updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), -}); - -export const content = sqliteTable('content', { - id: text('id').primaryKey(), - type: text('type', { enum: ['news', 'product', 'service', 'case'] }).notNull(), - title: text('title').notNull(), - slug: text('slug').notNull().unique(), - excerpt: text('excerpt'), - content: text('content').notNull(), - coverImage: text('cover_image'), - category: text('category'), - tags: text('tags', { mode: 'json' }).$type(), - 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(), -}); - -// Relations -export const usersRelations = relations(users, ({ many }) => ({ - content: many(content), - versions: many(contentVersions), - logs: many(auditLogs), -})); - -export const contentRelations = relations(content, ({ one, many }) => ({ - author: one(users, { - fields: [content.authorId], - references: [users.id], - }), - versions: many(contentVersions), -})); -``` - ---- - -## API 设计 - -### RESTful API 规范 - -**基础路径**:`/api/v1` - -**认证方式**:Bearer Token (NextAuth.js Session) - -**响应格式**:JSON - -**统一响应结构**: - -```typescript -interface ApiResponse { - success: boolean; - data?: T; - error?: { - code: string; - message: string; - details?: any; - }; - meta?: { - total?: number; - page?: number; - pageSize?: number; - }; -} -``` - -### API 端点列表 - -#### 1. 认证 API - -``` -POST /api/auth/register # 用户注册 -POST /api/auth/login # 邮箱密码登录 -POST /api/auth/magic-link # 发送 Magic Link -POST /api/auth/verify # 验证 Magic Link -POST /api/auth/logout # 登出 -GET /api/auth/session # 获取当前会话 -``` - -#### 2. 内容管理 API - -``` -GET /api/v1/content # 获取内容列表(支持分页、筛选) -GET /api/v1/content/:id # 获取单个内容 -POST /api/v1/content # 创建内容 -PUT /api/v1/content/:id # 更新内容 -DELETE /api/v1/content/:id # 删除内容 -POST /api/v1/content/:id/publish # 发布内容 -GET /api/v1/content/:id/versions # 获取版本历史 -POST /api/v1/content/:id/rollback # 回滚到指定版本 -``` - -**查询参数**: - -``` -?type=news&status=published&category=公司新闻&page=1&pageSize=10&sort=-publishedAt -``` - -#### 3. 配置管理 API - -``` -GET /api/v1/config # 获取所有配置 -GET /api/v1/config/:key # 获取单个配置 -PUT /api/v1/config/:key # 更新配置 -GET /api/v1/config/category/:category # 按类别获取配置 -``` - -#### 4. 文件上传 API - -``` -POST /api/v1/upload # 上传文件 -DELETE /api/v1/upload/:id # 删除文件 -``` - -#### 5. 用户管理 API - -``` -GET /api/v1/users # 获取用户列表 -GET /api/v1/users/:id # 获取用户详情 -PUT /api/v1/users/:id # 更新用户信息 -DELETE /api/v1/users/:id # 删除用户 -``` - -#### 6. 操作日志 API - -``` -GET /api/v1/logs # 获取操作日志 -``` - -### API 实现示例 - -```typescript -// src/app/api/v1/content/route.ts -import { NextRequest, NextResponse } from 'next/server'; -import { getServerSession } from 'next-auth'; -import { db } from '@/db'; -import { content } from '@/db/schema'; -import { eq, and, desc, sql } from 'drizzle-orm'; -import { z } from 'zod'; - -const ContentQuerySchema = z.object({ - type: z.enum(['news', 'product', 'service', 'case']).optional(), - status: z.enum(['draft', 'published', 'archived']).optional(), - category: z.string().optional(), - page: z.coerce.number().int().positive().default(1), - pageSize: z.coerce.number().int().positive().max(100).default(10), - sort: z.string().default('-publishedAt'), -}); - -export async function GET(request: NextRequest) { - try { - const { searchParams } = new URL(request.url); - const query = ContentQuerySchema.parse(Object.fromEntries(searchParams)); - - const whereConditions = []; - if (query.type) whereConditions.push(eq(content.type, query.type)); - if (query.status) whereConditions.push(eq(content.status, query.status)); - if (query.category) whereConditions.push(eq(content.category, query.category)); - - const [items, [{ count }]] = await Promise.all([ - db.select() - .from(content) - .where(whereConditions.length > 0 ? and(...whereConditions) : undefined) - .orderBy(desc(content.publishedAt)) - .limit(query.pageSize) - .offset((query.page - 1) * query.pageSize), - db.select({ count: sql`count(*)` }) - .from(content) - .where(whereConditions.length > 0 ? and(...whereConditions) : undefined), - ]); - - return NextResponse.json({ - success: true, - data: items, - meta: { - total: count, - page: query.page, - pageSize: query.pageSize, - }, - }); - } catch (error) { - return NextResponse.json( - { success: false, error: { code: 'INVALID_QUERY', message: error.message } }, - { status: 400 } - ); - } -} - -export async function POST(request: NextRequest) { - try { - const session = await getServerSession(); - if (!session) { - return NextResponse.json( - { success: false, error: { code: 'UNAUTHORIZED', message: '请先登录' } }, - { status: 401 } - ); - } - - const body = await request.json(); - const newContent = await db.insert(content).values({ - ...body, - id: crypto.randomUUID(), - authorId: session.user.id, - createdAt: new Date(), - updatedAt: new Date(), - }).returning(); - - return NextResponse.json({ success: true, data: newContent[0] }, { status: 201 }); - } catch (error) { - return NextResponse.json( - { success: false, error: { code: 'CREATE_FAILED', message: error.message } }, - { status: 500 } - ); - } -} -``` - ---- - -## 管理后台设计 - -### 页面结构 - -``` -/admin -├── /login # 登录页 -├── /dashboard # 仪表盘 -├── /content -│ ├── /news # 新闻管理 -│ │ ├── / # 列表页 -│ │ ├── /create # 创建页 -│ │ └── /[id]/edit # 编辑页 -│ ├── /products # 产品管理 -│ ├── /services # 服务管理 -│ └── /cases # 案例管理 -├── /config -│ ├── /features # 功能开关 -│ ├── /style # 样式配置 -│ ├── /seo # SEO 配置 -│ └── /general # 全局设置 -├── /users # 用户管理 -└── /logs # 操作日志 -``` - -### 布局设计 - -``` -┌─────────────────────────────────────────────────────────┐ -│ Logo 首页 内容 配置 用户 日志 用户头像 ▼ │ -├──────────┬──────────────────────────────────────────────┤ -│ │ │ -│ 侧边栏 │ 主内容区 │ -│ │ │ -│ - 新闻 │ ┌──────────────────────────────────────┐ │ -│ - 产品 │ │ 页面标题 [创建] [导出] │ │ -│ - 服务 │ ├──────────────────────────────────────┤ │ -│ - 案例 │ │ │ │ -│ │ │ 数据表格 / 表单 / 图表 │ │ -│ 配置 │ │ │ │ -│ - 功能 │ │ │ │ -│ - 样式 │ │ │ │ -│ - SEO │ └──────────────────────────────────────┘ │ -│ │ │ -└──────────┴──────────────────────────────────────────────┘ -``` - -### 核心组件 - -#### 1. 内容编辑器 - -```typescript -// src/components/admin/content-editor.tsx -import { Editor } from '@tiptap/react'; -import StarterKit from '@tiptap/starter-kit'; -import Image from '@tiptap/extension-image'; -import Link from '@tiptap/extension-link'; - -export function ContentEditor({ value, onChange }: ContentEditorProps) { - const editor = useEditor({ - extensions: [StarterKit, Image, Link], - content: value, - onUpdate: ({ editor }) => { - onChange(editor.getHTML()); - }, - }); - - return ( -
- - -
- ); -} -``` - -#### 2. 配置面板 - -```typescript -// src/components/admin/config-panel.tsx -export function ConfigPanel({ category }: ConfigPanelProps) { - const { data: configs, isLoading } = useQuery({ - queryKey: ['configs', category], - queryFn: () => fetch(`/api/v1/config/category/${category}`).then(r => r.json()), - }); - - return ( -
- {configs?.map((config) => ( - - ))} -
- ); -} - -function ConfigField({ config }: ConfigFieldProps) { - const [value, setValue] = useState(config.value); - - const handleSave = async () => { - await fetch(`/api/v1/config/${config.key}`, { - method: 'PUT', - body: JSON.stringify({ value }), - }); - }; - - return ( -
- - {typeof value === 'boolean' ? ( - - ) : typeof value === 'number' ? ( - setValue(Number(e.target.value))} /> - ) : ( - setValue(e.target.value)} /> - )} - -
- ); -} -``` - -#### 3. 数据表格 - -```typescript -// src/components/admin/data-table.tsx -import { useQuery } from '@tanstack/react-query'; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; - -export function DataTable({ type }: DataTableProps) { - const [page, setPage] = useState(1); - const { data, isLoading } = useQuery({ - queryKey: ['content', type, page], - queryFn: () => fetch(`/api/v1/content?type=${type}&page=${page}`).then(r => r.json()), - }); - - return ( -
- - - - 标题 - 分类 - 状态 - 发布时间 - 操作 - - - - {data?.data?.map((item) => ( - - {item.title} - {item.category} - - - {item.status} - - - {formatDate(item.publishedAt)} - - - - - - ))} - -
- -
- ); -} -``` - ---- - -## 权限体系 - -### 角色定义 - -| 角色 | 权限范围 | 说明 | -|-----|---------|------| -| **admin** | 全部权限 | 系统管理员,可管理用户、配置、所有内容 | -| **editor** | 内容管理 | 编辑人员,可创建、编辑、发布内容 | -| **viewer** | 只读权限 | 查看者,只能查看内容和配置 | - -### 权限矩阵 - -```typescript -const PERMISSIONS = { - admin: { - content: ['create', 'read', 'update', 'delete', 'publish'], - config: ['read', 'update'], - users: ['create', 'read', 'update', 'delete'], - logs: ['read'], - }, - editor: { - content: ['create', 'read', 'update', 'publish'], - config: ['read'], - users: [], - logs: ['read'], - }, - viewer: { - content: ['read'], - config: ['read'], - users: [], - logs: [], - }, -} as const; -``` - -### 权限检查实现 - -```typescript -// src/lib/auth/permissions.ts -import { getServerSession } from 'next-auth'; - -export async function checkPermission( - resource: string, - action: string -): Promise { - const session = await getServerSession(); - if (!session) return false; - - const userRole = session.user.role; - const permissions = PERMISSIONS[userRole]; - - return permissions[resource]?.includes(action) ?? false; -} - -// API Route 中使用 -export async function POST(request: NextRequest) { - if (!await checkPermission('content', 'create')) { - return NextResponse.json( - { success: false, error: { code: 'FORBIDDEN', message: '无权限' } }, - { status: 403 } - ); - } - // ... 创建内容逻辑 -} -``` - ---- - -## 部署策略 - -### 环境配置 - -```bash -# .env.local -DATABASE_URL="file:./data.db" -NEXTAUTH_SECRET="your-secret-key" -NEXTAUTH_URL="http://localhost:3000" - -# 可选:文件上传 -UPLOADTHING_SECRET="your-uploadthing-secret" -UPLOADTHING_APP_ID="your-app-id" -``` - -### 数据库迁移 - -```bash -# 生成迁移文件 -npm run db:generate - -# 执行迁移 -npm run db:migrate - -# 查看数据库 -npm run db:studio -``` - -### 生产部署 - -**推荐平台**:Vercel + Vercel KV (可选) - -**部署步骤**: - -1. 推送代码到 GitHub -2. 在 Vercel 中导入项目 -3. 配置环境变量 -4. 部署成功 - -**数据库备份**: - -```bash -# SQLite 备份 -cp data.db data.db.backup - -# 或使用脚本 -npm run db:backup -``` - ---- - -## 实施计划 - -### 阶段一:基础架构(2 天) - -**目标**:搭建数据库、认证系统、基础 API - -**任务清单**: - -- [ ] 安装依赖(Drizzle ORM、NextAuth.js、Tiptap) -- [ ] 创建数据库 schema -- [ ] 配置数据库连接 -- [ ] 实现认证系统(邮箱密码 + Magic Link) -- [ ] 创建基础 API Routes -- [ ] 编写单元测试 - -**验收标准**: - -- ✅ 数据库表创建成功 -- ✅ 用户可以注册、登录 -- ✅ API 基础框架可用 - ---- - -### 阶段二:内容管理(2 天) - -**目标**:实现内容 CRUD 和管理界面 - -**任务清单**: - -- [ ] 实现内容 CRUD API -- [ ] 创建管理后台布局 -- [ ] 实现新闻管理界面 -- [ ] 实现产品管理界面 -- [ ] 实现服务管理界面 -- [ ] 集成富文本编辑器 -- [ ] 实现图片上传功能 - -**验收标准**: - -- ✅ 可以创建、编辑、删除内容 -- ✅ 富文本编辑器正常工作 -- ✅ 图片上传成功 - ---- - -### 阶段三:配置中心(1 天) - -**目标**:实现配置管理和功能开关 - -**任务清单**: - -- [ ] 实现配置 CRUD API -- [ ] 创建功能开关界面 -- [ ] 创建样式配置界面 -- [ ] 创建 SEO 配置界面 -- [ ] 实现配置加载器 -- [ ] 前端集成配置 - -**验收标准**: - -- ✅ 可以动态启用/禁用功能 -- ✅ 配置实时生效 - ---- - -### 阶段四:高级功能(1-2 天) - -**目标**:实现版本历史、操作日志、实时预览 - -**任务清单**: - -- [ ] 实现版本历史 API -- [ ] 创建版本对比界面 -- [ ] 实现内容回滚 -- [ ] 实现操作日志记录 -- [ ] 创建日志查询界面 -- [ ] 实现实时预览功能 - -**验收标准**: - -- ✅ 可以查看历史版本 -- ✅ 可以回滚到指定版本 -- ✅ 操作日志完整记录 - ---- - -### 阶段五:测试和部署(1 天) - -**目标**:完善测试、性能优化、部署上线 - -**任务清单**: - -- [ ] 编写 E2E 测试 -- [ ] 性能优化(缓存、懒加载) -- [ ] 安全审计(OWASP Top 10) -- [ ] 编写部署文档 -- [ ] 配置 CI/CD -- [ ] 生产环境部署 - -**验收标准**: - -- ✅ 测试覆盖率 ≥ 80% -- ✅ 性能指标达标 -- ✅ 安全检查通过 -- ✅ 成功部署上线 - ---- - -## 测试策略 - -### 单元测试 - -**框架**:Vitest + Testing Library - -**覆盖范围**: - -- 工具函数 -- React Hooks -- API 逻辑 -- 数据验证 - -**示例**: - -```typescript -// __tests__/lib/config-manager.test.ts -import { describe, it, expect } from 'vitest'; -import { ConfigManager } from '@/lib/config-manager'; - -describe('ConfigManager', () => { - it('should load config from database', async () => { - const config = await ConfigManager.get('feature_news'); - expect(config).toBeDefined(); - expect(config.enabled).toBe(true); - }); -}); -``` - -### 集成测试 - -**框架**:Vitest + MSW (Mock Service Worker) - -**覆盖范围**: - -- API Routes -- 数据库操作 -- 认证流程 - -### E2E 测试 - -**框架**:Playwright(已有) - -**覆盖范围**: - -- 用户登录流程 -- 内容创建流程 -- 配置更新流程 - -**示例**: - -```typescript -// e2e/tests/admin/content.spec.ts -import { test, expect } from '@playwright/test'; - -test('should create news article', async ({ page }) => { - await page.goto('/admin/login'); - await page.fill('input[name="email"]', 'admin@example.com'); - await page.fill('input[name="password"]', 'password'); - await page.click('button[type="submit"]'); - - await page.goto('/admin/content/news/create'); - await page.fill('input[name="title"]', '测试新闻'); - await page.fill('textarea[name="excerpt"]', '这是测试新闻摘要'); - await page.click('button[type="submit"]'); - - await expect(page.locator('text=创建成功')).toBeVisible(); -}); -``` - ---- - -## 风险与应对 - -### 技术风险 - -| 风险 | 影响 | 概率 | 应对措施 | -|-----|------|------|---------| -| SQLite 性能瓶颈 | 高 | 低 | 监控性能,准备迁移到 PostgreSQL | -| 文件上传安全漏洞 | 高 | 中 | 严格验证文件类型、大小,使用 CDN | -| 认证系统漏洞 | 高 | 低 | 使用成熟的 NextAuth.js,定期更新 | -| 数据库迁移失败 | 中 | 低 | 完善备份策略,测试迁移脚本 | - -### 业务风险 - -| 风险 | 影响 | 概率 | 应对措施 | -|-----|------|------|---------| -| 运营人员不会使用 | 中 | 中 | 编写详细操作手册,提供培训 | -| 内容误删 | 高 | 中 | 实现软删除、版本历史、回收站 | -| 配置错误导致网站异常 | 高 | 低 | 配置验证、预览功能、快速回滚 | - -### 项目风险 - -| 风险 | 影响 | 概率 | 应对措施 | -|-----|------|------|---------| -| 开发周期延误 | 中 | 中 | 采用敏捷开发,优先核心功能 | -| 需求变更 | 中 | 高 | 模块化设计,预留扩展接口 | -| 技术债务累积 | 中 | 中 | 代码审查、持续重构、完善文档 | - ---- - -## 附录 - -### A. 默认配置数据 - -```json -{ - "feature_news": { - "enabled": true, - "displayCount": 6, - "categories": ["公司新闻", "产品发布", "合作动态", "行业资讯"], - "sortOrder": "desc" - }, - "feature_products": { - "enabled": true, - "showPricing": true, - "featuredProducts": ["erp", "crm"] - }, - "feature_services": { - "enabled": true, - "items": ["software", "cloud", "data", "security"] - }, - "seo_default": { - "title": "四川睿新致远科技有限公司 - 企业数字化转型服务商", - "description": "以智慧连接数字趋势,以伙伴身份陪您成长", - "keywords": ["数字化转型", "软件开发", "云服务", "数据分析"] - } -} -``` - -### B. 数据库迁移脚本 - -```typescript -// drizzle/migrations/0001_initial.ts -import { sql } from 'drizzle-orm'; -import { drizzle } from 'drizzle-orm/libsql'; - -export async function up(db: ReturnType) { - await db.run(sql` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - email TEXT UNIQUE NOT NULL, - password_hash TEXT, - name TEXT NOT NULL, - role TEXT NOT NULL DEFAULT 'editor', - avatar TEXT, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ); - `); - // ... 其他表 -} - -export async function down(db: ReturnType) { - await db.run(sql`DROP TABLE users`); - // ... 删除其他表 -} -``` - -### C. 参考资源 - -- [Next.js 文档](https://nextjs.org/docs) -- [Drizzle ORM 文档](https://orm.drizzle.team/docs/overview) -- [NextAuth.js 文档](https://next-auth.js.org/) -- [Tiptap 文档](https://tiptap.dev/) -- [shadcn/ui 文档](https://ui.shadcn.com/) - ---- - -**文档版本历史**: - -| 版本 | 日期 | 变更说明 | 作者 | -|-----|------|---------|------| -| v1.0 | 2026-03-08 | 初始版本 | 张翔 | - ---- - -**审批记录**: - -| 角色 | 姓名 | 日期 | 状态 | -|-----|------|------|------| -| 技术负责人 | 张翔 | 2026-03-08 | ✅ 已批准 | diff --git a/docs/plans/2026-03-08-configurable-cms-execution.md b/docs/plans/2026-03-08-configurable-cms-execution.md deleted file mode 100644 index 211d431..0000000 --- a/docs/plans/2026-03-08-configurable-cms-execution.md +++ /dev/null @@ -1,1780 +0,0 @@ -# 可配置化 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 开始执行任务 diff --git a/docs/plans/2026-03-08-configurable-cms-implementation.md b/docs/plans/2026-03-08-configurable-cms-implementation.md deleted file mode 100644 index b7f977f..0000000 --- a/docs/plans/2026-03-08-configurable-cms-implementation.md +++ /dev/null @@ -1,2182 +0,0 @@ -# 可配置化 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` diff --git a/docs/plans/2026-03-09-production-readiness-plan.md b/docs/plans/2026-03-09-production-readiness-plan.md deleted file mode 100644 index fcd2c47..0000000 --- a/docs/plans/2026-03-09-production-readiness-plan.md +++ /dev/null @@ -1,1213 +0,0 @@ -# 生产就绪度修复与迭代计划 - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**目标:** 补齐生产就绪度和自动化体系,确保网站具备上线条件 - -**架构:** Next.js 16 + SQLite + Drizzle ORM + NextAuth.js + Playwright + Woodpecker CI + Sentry - -**技术栈:** Next.js, React, TypeScript, SQLite, Drizzle ORM, NextAuth.js, Playwright, Woodpecker CI, Sentry, Prometheus - -**关键配置:** -- CI/CD: Forgejo + Woodpecker CI -- 监控: Sentry (错误追踪) + Prometheus (性能监控) -- 测试: Playwright + Jest -- 质量门禁: ESLint + Prettier + 测试覆盖率 - ---- - -## 前置准备 - -### 环境要求 -- Node.js 18+ -- npm -- Git -- Forgejo账号(用于代码托管) -- Woodpecker CI实例(用于CI/CD) -- Sentry账号(用于错误监控) - -### 相关文档 -- 测试评估报告:见知识图谱 -- 上线条件评估:见知识图谱 - ---- - -## 阶段一:修复测试套件问题(预计 2 天) - -### Task 1: 修复API测试认证问题 - -**文件:** -- 修改: `e2e/src/tests/api/admin.api.spec.ts` -- 创建: `e2e/global-setup.ts` -- 修改: `e2e/playwright.config.ts` - -**步骤 1: 创建全局认证设置** - -创建文件 `e2e/global-setup.ts`: - -```typescript -import { chromium, FullConfig } from '@playwright/test'; - -async function globalSetup(config: FullConfig) { - const browser = await chromium.launch(); - const page = await browser.newPage(); - - // 登录并保存认证状态 - await page.goto('http://localhost:3000/admin/login'); - await page.fill('#email', 'admin@novalon.cn'); - await page.fill('#password', 'admin123456'); - await page.click('button[type="submit"]'); - - // 等待登录成功 - await page.waitForURL(/\/admin(?!\/login)/); - - // 保存认证状态 - await page.context().storageState({ path: 'e2e/.auth/admin.json' }); - - await browser.close(); -} - -export default globalSetup; -``` - -**步骤 2: 更新Playwright配置** - -修改 `e2e/playwright.config.ts`: - -```typescript -export default defineConfig({ - globalSetup: require.resolve('./global-setup'), - - use: { - storageState: '.auth/admin.json', // 使用保存的认证状态 - }, - - // ... 其他配置 -}); -``` - -**步骤 3: 更新API测试** - -修改 `e2e/src/tests/api/admin.api.spec.ts`: - -```typescript -import { test, expect } from '../../fixtures/base.fixture'; - -test.describe('管理后台API测试', () => { - // 移除 beforeAll 中的手动认证逻辑 - // 使用全局设置的认证状态 - - test.describe('内容管理API', () => { - test('应该能够获取内容列表', async ({ request }) => { - const response = await request.get('/api/admin/content'); - - expect(response.status()).toBe(200); - - const data = await response.json(); - expect(data).toHaveProperty('items'); - expect(Array.isArray(data.items)).toBe(true); - }); - - test('应该能够创建新内容', async ({ request }) => { - const response = await request.post('/api/admin/content', { - data: { - type: 'news', - title: '测试新闻', - slug: `test-news-${Date.now()}`, - content: '这是测试内容', - status: 'draft', - }, - }); - - expect([200, 201]).toContain(response.status()); - - const data = await response.json(); - expect(data).toHaveProperty('id'); - expect(data.title).toBe('测试新闻'); - }); - }); -}); -``` - -**步骤 4: 运行测试验证** - -运行命令: - -```bash -cd e2e -npm test -- tests/api/admin.api.spec.ts --project=chromium -``` - -预期结果:所有API测试通过,无跳过 - -**步骤 5: 提交更改** - -```bash -git add e2e/global-setup.ts e2e/playwright.config.ts e2e/src/tests/api/admin.api.spec.ts -git commit -m "fix: 修复API测试认证问题,使用全局认证状态" -``` - ---- - -### Task 2: 优化回归测试稳定性 - -**文件:** -- 修改: `e2e/src/tests/regression/admin.regression.spec.ts` - -**步骤 1: 增加超时时间** - -修改 `e2e/src/tests/regression/admin.regression.spec.ts`: - -```typescript -test.beforeEach(async ({ page }) => { - loginPage = new AdminLoginPage(page); - contentPage = new AdminContentPage(page); - - await loginPage.goto(); - await loginPage.login('admin@novalon.cn', 'admin123456'); - - try { - // 增加超时时间到15秒 - await page.waitForURL(/\/admin/, { timeout: 15000 }); - } catch (error) { - console.error('登录超时,跳过测试:', error); - test.skip(); - } -}); -``` - -**步骤 2: 修复列表页面定位器** - -修改 `e2e/src/pages/AdminPage.ts`: - -```typescript -export class AdminContentPage extends BasePage { - readonly createButton: Locator; - readonly contentList: Locator; - readonly searchInput: Locator; - readonly typeFilter: Locator; - - constructor(page: Page) { - super(page); - this.createButton = page.getByRole('button', { name: /创建|新建|create/i }); - // 使用更精确的定位器 - this.contentList = page.locator('table tbody tr').or(page.locator('[data-testid="content-item"]')); - this.searchInput = page.locator('input[type="search"], input[placeholder*="搜索"]'); - this.typeFilter = page.locator('select[name="type"], select[data-testid="type-filter"]'); - } -} -``` - -**步骤 3: 运行测试验证** - -运行命令: - -```bash -cd e2e -npm test -- tests/regression/admin.regression.spec.ts --project=chromium -``` - -预期结果:回归测试通过率提升到90%+ - -**步骤 4: 提交更改** - -```bash -git add e2e/src/tests/regression/admin.regression.spec.ts e2e/src/pages/AdminPage.ts -git commit -m "fix: 优化回归测试稳定性,增加超时时间,修复定位器" -``` - ---- - -## 阶段二:搭建CI/CD流水线(预计 3 天) - -### Task 3: 创建Woodpecker CI工作流 - -**文件:** -- 创建: `.woodpecker/ci.yml` -- 创建: `.woodpecker/deploy.yml` - -**步骤 1: 创建CI工作流** - -创建文件 `.woodpecker/ci.yml`: - -```yaml -when: - branch: [main, develop] - event: [push, pull_request] - -steps: - lint: - image: node:18-alpine - commands: - - npm ci - - npm run lint - - npm run type-check - - test: - image: node:18-alpine - commands: - - npm ci - - npm run db:push - - npm run test:unit - - npx playwright install --with-deps - - npm run test:e2e - - build: - image: node:18-alpine - commands: - - npm ci - - npm run build - when: - status: [success] -``` - -**步骤 2: 创建部署工作流** - -创建文件 `.woodpecker/deploy.yml`: - -```yaml -when: - branch: [main] - event: [push] - -steps: - deploy: - image: node:18-alpine - commands: - - npm ci - - npm run build - - echo "Deploying to production..." - secrets: [deploy_key] -``` - -**步骤 3: 更新package.json脚本** - -修改 `package.json`: - -```json -{ - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint", - "type-check": "tsc --noEmit", - "test:unit": "jest", - "test:e2e": "cd e2e && npm test", - "db:push": "drizzle-kit push:sqlite", - "db:migrate": "drizzle-kit generate:sqlite && drizzle-kit migrate" - } -} -``` - -**步骤 4: 提交更改** - -```bash -git add .woodpecker/ package.json -git commit -m "feat: 添加Woodpecker CI流水线配置" -``` - ---- - -### Task 4: 配置质量门禁 - -**文件:** -- 创建: `.github/workflows/quality-gate.yml` -- 创建: `jest.config.js` -- 修改: `package.json` - -**步骤 1: 创建Jest配置** - -创建文件 `jest.config.js`: - -```javascript -module.exports = { - testEnvironment: 'node', - collectCoverage: true, - coverageThreshold: { - global: { - branches: 70, - functions: 70, - lines: 70, - statements: 70 - } - }, - testMatch: ['**/__tests__/**/*.test.ts', '**/*.test.ts'], - moduleFileExtensions: ['ts', 'js', 'json'], - transform: { - '^.+\\.ts$': 'ts-jest' - } -}; -``` - -**步骤 2: 创建质量门禁工作流** - -创建文件 `.woodpecker/quality-gate.yml`: - -```yaml -when: - event: [pull_request] - branch: [main, develop] - -steps: - quality-check: - image: node:18-alpine - commands: - - npm ci - - npm run lint - - npm run type-check - - npm run test:unit -- --coverage - - | - COVERAGE=$(cat coverage/coverage-summary.json | grep -o '"lines":{"pct":[0-9.]*' | grep -o '[0-9.]*$') - if [ $(echo "$COVERAGE < 70" | bc -l) -eq 1 ]; then - echo "Coverage $COVERAGE% is below threshold 70%" - exit 1 - fi -``` - -**步骤 3: 提交更改** - -```bash -git add .woodpecker/quality-gate.yml jest.config.js package.json -git commit -m "feat: 添加质量门禁配置" -``` - ---- - -## 阶段三:建立监控告警体系(预计 3 天) - -### Task 5: 集成Sentry错误监控 - -**文件:** -- 修改: `package.json` -- 创建: `src/lib/sentry.ts` -- 修改: `src/app/layout.tsx` - -**步骤 1: 安装Sentry依赖** - -运行命令: - -```bash -npm install @sentry/nextjs -``` - -**步骤 2: 创建Sentry配置** - -创建文件 `src/lib/sentry.ts`: - -```typescript -import * as Sentry from '@sentry/nextjs'; - -export function initSentry() { - if (process.env.NODE_ENV === 'production') { - Sentry.init({ - dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, - environment: process.env.NODE_ENV, - tracesSampleRate: 0.1, - replaysSessionSampleRate: 0.1, - replaysOnErrorSampleRate: 1.0, - }); - } -} -``` - -**步骤 3: 在应用中初始化Sentry** - -修改 `src/app/layout.tsx`: - -```typescript -import { initSentry } from '@/lib/sentry'; - -// 在文件顶部初始化 -initSentry(); - -export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); -} -``` - -**步骤 4: 创建Sentry配置文件** - -创建文件 `sentry.client.config.ts`: - -```typescript -import * as Sentry from '@sentry/nextjs'; - -Sentry.init({ - dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, - environment: process.env.NODE_ENV, - tracesSampleRate: 0.1, -}); -``` - -创建文件 `sentry.server.config.ts`: - -```typescript -import * as Sentry from '@sentry/nextjs'; - -Sentry.init({ - dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, - environment: process.env.NODE_ENV, - tracesSampleRate: 0.1, -}); -``` - -**步骤 5: 更新环境变量** - -修改 `.env.example`: - -```bash -# Sentry -NEXT_PUBLIC_SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx -``` - -**步骤 6: 提交更改** - -```bash -git add src/lib/sentry.ts src/app/layout.tsx sentry.client.config.ts sentry.server.config.ts .env.example package.json -git commit -m "feat: 集成Sentry错误监控" -``` - ---- - -### Task 6: 配置性能监控 - -**文件:** -- 创建: `src/lib/monitoring.ts` -- 创建: `src/app/api/health/route.ts` - -**步骤 1: 创建监控工具** - -创建文件 `src/lib/monitoring.ts`: - -```typescript -export class PerformanceMonitor { - private static instance: PerformanceMonitor; - private metrics: Map = new Map(); - - static getInstance(): PerformanceMonitor { - if (!PerformanceMonitor.instance) { - PerformanceMonitor.instance = new PerformanceMonitor(); - } - return PerformanceMonitor.instance; - } - - recordMetric(name: string, value: number) { - if (!this.metrics.has(name)) { - this.metrics.set(name, []); - } - this.metrics.get(name)!.push(value); - } - - getAverage(name: string): number { - const values = this.metrics.get(name) || []; - if (values.length === 0) return 0; - return values.reduce((a, b) => a + b, 0) / values.length; - } - - getPercentile(name: string, percentile: number): number { - const values = this.metrics.get(name) || []; - if (values.length === 0) return 0; - - const sorted = [...values].sort((a, b) => a - b); - const index = Math.ceil((percentile / 100) * sorted.length) - 1; - return sorted[index]; - } -} - -export const monitor = PerformanceMonitor.getInstance(); -``` - -**步骤 2: 创建健康检查API** - -创建文件 `src/app/api/health/route.ts`: - -```typescript -import { NextResponse } from 'next/server'; -import { monitor } from '@/lib/monitoring'; - -export async function GET() { - const health = { - status: 'ok', - timestamp: new Date().toISOString(), - uptime: process.uptime(), - memory: process.memoryUsage(), - metrics: { - avgResponseTime: monitor.getAverage('response_time'), - p95ResponseTime: monitor.getPercentile('response_time', 95), - } - }; - - return NextResponse.json(health); -} -``` - -**步骤 3: 提交更改** - -```bash -git add src/lib/monitoring.ts src/app/api/health/route.ts -git commit -m "feat: 添加性能监控和健康检查API" -``` - ---- - -## 阶段四:配置生产环境(预计 2 天) - -### Task 7: 创建生产环境配置 - -**文件:** -- 创建: `.env.production.example` -- 创建: `docker-compose.prod.yml` -- 创建: `Dockerfile` - -**步骤 1: 创建生产环境变量模板** - -创建文件 `.env.production.example`: - -```bash -# Database -DATABASE_URL=file:./data/prod.db - -# NextAuth -NEXTAUTH_URL=https://novalon.cn -NEXTAUTH_SECRET=your-production-secret-here - -# Admin User -ADMIN_EMAIL=admin@novalon.cn -ADMIN_PASSWORD=your-secure-password - -# Sentry -NEXT_PUBLIC_SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx - -# Email (Resend) -RESEND_API_KEY=re_icMNpBzS_DL9GirDmhG5PbNU6PLRWvUtY - -# File Upload -UPLOAD_DIR=./uploads -MAX_FILE_SIZE=10485760 -``` - -**步骤 2: 创建Docker配置** - -创建文件 `Dockerfile`: - -```dockerfile -FROM node:18-alpine AS base - -# Install dependencies only when needed -FROM base AS deps -RUN apk add --no-cache libc6-compat -WORKDIR /app - -COPY package.json package-lock.json ./ -RUN npm ci - -# Rebuild the source code only when needed -FROM base AS builder -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY . . - -RUN npm run build - -# Production image, copy all the files and run next -FROM base AS runner -WORKDIR /app - -ENV NODE_ENV production - -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs - -COPY --from=builder /app/public ./public -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static - -USER nextjs - -EXPOSE 3000 - -ENV PORT 3000 -ENV HOSTNAME "0.0.0.0" - -CMD ["node", "server.js"] -``` - -**步骤 3: 创建Docker Compose配置** - -创建文件 `docker-compose.prod.yml`: - -```yaml -version: '3.8' - -services: - app: - build: - context: . - dockerfile: Dockerfile - ports: - - "3000:3000" - environment: - - NODE_ENV=production - env_file: - - .env.production - volumes: - - ./data:/app/data - - ./uploads:/app/uploads - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] - interval: 30s - timeout: 10s - retries: 3 -``` - -**步骤 4: 提交更改** - -```bash -git add .env.production.example docker-compose.prod.yml Dockerfile -git commit -m "feat: 添加生产环境配置" -``` - ---- - -### Task 8: 创建备份脚本 - -**文件:** -- 创建: `scripts/backup.sh` -- 创建: `scripts/restore.sh` - -**步骤 1: 创建备份脚本** - -创建文件 `scripts/backup.sh`: - -```bash -#!/bin/bash - -# 备份脚本 -BACKUP_DIR="./backups" -DATE=$(date +%Y%m%d_%H%M%S) -BACKUP_NAME="backup_$DATE" - -# 创建备份目录 -mkdir -p $BACKUP_DIR/$BACKUP_NAME - -# 备份数据库 -echo "Backing up database..." -cp ./data/prod.db $BACKUP_DIR/$BACKUP_NAME/database.db - -# 备份上传文件 -echo "Backing up uploads..." -cp -r ./uploads $BACKUP_DIR/$BACKUP_NAME/uploads - -# 备份配置 -echo "Backing up config..." -cp .env.production $BACKUP_DIR/$BACKUP_NAME/.env.production - -# 压缩备份 -echo "Compressing backup..." -tar -czf $BACKUP_DIR/$BACKUP_NAME.tar.gz -C $BACKUP_DIR $BACKUP_NAME - -# 删除临时目录 -rm -rf $BACKUP_DIR/$BACKUP_NAME - -# 保留最近7天的备份 -find $BACKUP_DIR -name "backup_*.tar.gz" -mtime +7 -delete - -echo "Backup completed: $BACKUP_DIR/$BACKUP_NAME.tar.gz" -``` - -**步骤 2: 创建恢复脚本** - -创建文件 `scripts/restore.sh`: - -```bash -#!/bin/bash - -# 恢复脚本 -if [ -z "$1" ]; then - echo "Usage: ./restore.sh " - exit 1 -fi - -BACKUP_FILE=$1 - -if [ ! -f $BACKUP_FILE ]; then - echo "Backup file not found: $BACKUP_FILE" - exit 1 -fi - -# 解压备份 -echo "Extracting backup..." -tar -xzf $BACKUP_FILE -C ./temp_restore - -# 恢复数据库 -echo "Restoring database..." -cp ./temp_restore/backup_*/database.db ./data/prod.db - -# 恢复上传文件 -echo "Restoring uploads..." -cp -r ./temp_restore/backup_*/uploads ./uploads - -# 清理临时文件 -rm -rf ./temp_restore - -echo "Restore completed" -``` - -**步骤 3: 设置定时备份** - -创建文件 `scripts/cron-backup.sh`: - -```bash -#!/bin/bash - -# 添加到crontab: 0 2 * * * /path/to/scripts/cron-backup.sh - -cd /path/to/novalon-website -./scripts/backup.sh >> ./logs/backup.log 2>&1 -``` - -**步骤 4: 提交更改** - -```bash -git add scripts/ -git commit -m "feat: 添加备份和恢复脚本" -``` - ---- - -## 阶段五:性能和安全测试(预计 2 天) - -### Task 9: 创建性能测试脚本 - -**文件:** -- 创建: `tests/performance/load-test.js` -- 创建: `tests/performance/stress-test.js` - -**步骤 1: 安装性能测试工具** - -运行命令: - -```bash -npm install -D k6 -``` - -**步骤 2: 创建负载测试** - -创建文件 `tests/performance/load-test.js`: - -```javascript -import http from 'k6/http'; -import { check, sleep } from 'k6'; - -export let options = { - stages: [ - { duration: '2m', target: 100 }, // 2分钟内增加到100用户 - { duration: '5m', target: 100 }, // 保持100用户5分钟 - { duration: '2m', target: 0 }, // 2分钟内降到0用户 - ], - thresholds: { - http_req_duration: ['p(95)<500'], // 95%的请求响应时间小于500ms - http_req_failed: ['rate<0.01'], // 错误率小于1% - }, -}; - -export default function () { - let res = http.get('http://localhost:3000/'); - check(res, { 'status was 200': (r) => r.status == 200 }); - sleep(1); -} -``` - -**步骤 3: 创建压力测试** - -创建文件 `tests/performance/stress-test.js`: - -```javascript -import http from 'k6/http'; -import { check } from 'k6'; - -export let options = { - stages: [ - { duration: '2m', target: 200 }, - { duration: '5m', target: 200 }, - { duration: '2m', target: 400 }, - { duration: '5m', target: 400 }, - { duration: '2m', target: 600 }, - { duration: '5m', target: 600 }, - { duration: '2m', target: 0 }, - ], - thresholds: { - http_req_duration: ['p(95)<2000'], - http_req_failed: ['rate<0.05'], - }, -}; - -export default function () { - let res = http.get('http://localhost:3000/'); - check(res, { 'status was 200': (r) => r.status == 200 }); -} -``` - -**步骤 4: 提交更改** - -```bash -git add tests/performance/ -git commit -m "feat: 添加性能测试脚本" -``` - ---- - -### Task 10: 创建安全测试脚本 - -**文件:** -- 创建: `tests/security/sql-injection-test.ts` -- 创建: `tests/security/xss-test.ts` - -**步骤 1: 创建SQL注入测试** - -创建文件 `tests/security/sql-injection-test.ts`: - -```typescript -import { test, expect } from '@playwright/test'; - -test.describe('SQL注入防护测试', () => { - const sqlInjectionPayloads = [ - "' OR '1'='1", - "'; DROP TABLE users; --", - "' UNION SELECT * FROM users --", - "1' OR '1' = '1", - ]; - - test('登录页面应该防护SQL注入', async ({ page }) => { - await page.goto('/admin/login'); - - for (const payload of sqlInjectionPayloads) { - await page.fill('#email', payload); - await page.fill('#password', payload); - await page.click('button[type="submit"]'); - - // 应该显示错误信息,而不是成功登录 - await expect(page.locator('.text-red-700')).toBeVisible(); - - // 不应该跳转到管理页面 - expect(page.url()).toContain('/admin/login'); - } - }); - - test('搜索功能应该防护SQL注入', async ({ page }) => { - await page.goto('/'); - - for (const payload of sqlInjectionPayloads) { - const searchInput = page.locator('input[type="search"]'); - if (await searchInput.isVisible()) { - await searchInput.fill(payload); - await page.keyboard.press('Enter'); - - // 应该显示正常结果或错误信息,而不是崩溃 - await page.waitForLoadState('networkidle'); - expect(page.url()).not.toContain('error'); - } - } - }); -}); -``` - -**步骤 2: 创建XSS测试** - -创建文件 `tests/security/xss-test.ts`: - -```typescript -import { test, expect } from '@playwright/test'; - -test.describe('XSS防护测试', () => { - const xssPayloads = [ - '', - '', - 'javascript:alert("XSS")', - '', - ]; - - test('表单应该防护XSS攻击', async ({ page }) => { - await page.goto('/contact'); - - for (const payload of xssPayloads) { - await page.fill('#name', payload); - await page.fill('#email', 'test@example.com'); - await page.fill('#subject', 'Test Subject'); - await page.fill('#message', 'Test Message'); - await page.click('button[type="submit"]'); - - // 应该显示成功或错误信息,而不是执行脚本 - await page.waitForLoadState('networkidle'); - - // 检查是否有alert弹窗(不应该有) - page.on('dialog', async dialog => { - expect(dialog.type()).not.toBe('alert'); - }); - } - }); -}); -``` - -**步骤 3: 提交更改** - -```bash -git add tests/security/ -git commit -m "feat: 添加安全测试脚本" -``` - ---- - -## 阶段六:文档和培训(预计 1 天) - -### Task 11: 更新部署文档 - -**文件:** -- 修改: `README.md` -- 创建: `docs/deployment-guide.md` -- 创建: `docs/monitoring-guide.md` - -**步骤 1: 更新README** - -修改 `README.md`,添加以下内容: - -```markdown -## 生产部署 - -### 环境要求 -- Node.js 18+ -- Docker & Docker Compose -- 域名和SSL证书 - -### 部署步骤 - -1. 克隆代码 -\`\`\`bash -git clone https://github.com/your-org/novalon-website.git -cd novalon-website -\`\`\` - -2. 配置环境变量 -\`\`\`bash -cp .env.production.example .env.production -# 编辑 .env.production,填入实际配置 -\`\`\` - -3. 启动服务 -\`\`\`bash -docker-compose -f docker-compose.prod.yml up -d -\`\`\` - -4. 初始化数据库 -\`\`\`bash -npm run db:push -\`\`\` - -5. 访问应用 -打开浏览器访问 https://novalon.cn - -### 监控和告警 - -- 错误监控: Sentry Dashboard -- 性能监控: /api/health -- 日志查看: docker logs novalon-website-app-1 - -### 备份和恢复 - -- 自动备份: 每天凌晨2点自动备份 -- 手动备份: ./scripts/backup.sh -- 恢复数据: ./scripts/restore.sh - -### 故障排查 - -详见: docs/troubleshooting.md -``` - -**步骤 2: 创建部署指南** - -创建文件 `docs/deployment-guide.md`: - -```markdown -# 部署指南 - -## 1. 环境准备 - -### 1.1 服务器要求 -- CPU: 2核+ -- 内存: 4GB+ -- 磁盘: 20GB+ -- 操作系统: Ubuntu 20.04+ - -### 1.2 软件要求 -- Docker 20.10+ -- Docker Compose 2.0+ -- Node.js 18+ (用于本地构建) - -## 2. 部署流程 - -### 2.1 首次部署 -... - -### 2.2 更新部署 -... - -### 2.3 回滚操作 -... - -## 3. 域名和SSL配置 - -### 3.1 域名解析 -... - -### 3.2 SSL证书配置 -... - -## 4. 监控配置 - -### 4.1 Sentry配置 -... - -### 4.2 日志收集 -... -``` - -**步骤 3: 创建监控指南** - -创建文件 `docs/monitoring-guide.md`: - -```markdown -# 监控指南 - -## 1. 监控指标 - -### 1.1 应用健康 -- 健康检查: /api/health -- 响应时间: < 500ms (P95) -- 错误率: < 1% - -### 1.2 系统资源 -- CPU使用率: < 70% -- 内存使用率: < 80% -- 磁盘使用率: < 80% - -## 2. 告警规则 - -### 2.1 严重告警 -- 应用宕机 -- 数据库连接失败 -- 错误率 > 5% - -### 2.2 警告告警 -- 响应时间 > 1s -- CPU使用率 > 80% -- 内存使用率 > 85% - -## 3. 日志查看 - -### 3.1 Docker日志 -\`\`\`bash -docker logs novalon-website-app-1 -f -\`\`\` - -### 3.2 应用日志 -\`\`\`bash -tail -f logs/app.log -\`\`\` -``` - -**步骤 4: 提交更改** - -```bash -git add README.md docs/ -git commit -m "docs: 更新部署和监控文档" -``` - ---- - -## 验收标准 - -### 阶段一验收标准 -- ✅ API测试全部通过,无跳过 -- ✅ 回归测试通过率 > 90% -- ✅ 测试覆盖率 > 70% - -### 阶段二验收标准 -- ✅ CI流水线正常运行 -- ✅ 代码提交自动触发测试 -- ✅ 质量门禁生效 - -### 阶段三验收标准 -- ✅ Sentry正常收集错误 -- ✅ 健康检查API正常工作 -- ✅ 性能指标可监控 - -### 阶段四验收标准 -- ✅ 生产环境配置完整 -- ✅ 备份脚本可正常执行 -- ✅ 恢复脚本可正常执行 - -### 阶段五验收标准 -- ✅ 负载测试通过 -- ✅ 压力测试通过 -- ✅ 安全测试通过 - -### 阶段六验收标准 -- ✅ 文档完整清晰 -- ✅ 团队成员了解部署流程 -- ✅ 团队成员了解监控方式 - ---- - -## 时间估算 - -| 阶段 | 任务数 | 预计时间 | 累计时间 | -|------|--------|---------|---------| -| 阶段一 | 2 | 2天 | 2天 | -| 阶段二 | 2 | 3天 | 5天 | -| 阶段三 | 2 | 3天 | 8天 | -| 阶段四 | 2 | 2天 | 10天 | -| 阶段五 | 2 | 2天 | 12天 | -| 阶段六 | 1 | 1天 | 13天 | - -**总计:13个工作日(约2-3周)** - ---- - -## 风险和依赖 - -### 风险 -1. **Sentry配置问题** - 需要提前注册账号并获取DSN -2. **Docker部署问题** - 需要运维团队支持 -3. **性能测试环境** - 需要独立的测试环境 - -### 依赖 -1. **运维团队支持** - 服务器配置、域名解析、SSL证书 -2. **Sentry账号** - 错误监控服务 -3. **Forgejo + Woodpecker CI** - 代码托管和CI/CD服务 - ---- - -## 执行建议 - -1. **优先级执行**:按照阶段顺序执行,优先完成P0任务 -2. **并行执行**:阶段二和阶段三可以并行进行 -3. **持续验证**:每个阶段完成后进行验收测试 -4. **文档同步**:实施过程中及时更新文档 - ---- - -## 后续优化 - -完成本计划后,可以考虑以下优化: - -1. **自动化测试增强**:增加更多边界情况测试 -2. **性能优化**:数据库查询优化、缓存策略优化 -3. **安全加固**:定期安全审计、渗透测试 -4. **监控完善**:增加业务指标监控、用户行为分析 -5. **灾备方案**:多地域部署、故障自动转移 diff --git a/docs/plans/2026-03-09-test-coverage-improvement-plan.md b/docs/plans/2026-03-09-test-coverage-improvement-plan.md deleted file mode 100644 index 12493e6..0000000 --- a/docs/plans/2026-03-09-test-coverage-improvement-plan.md +++ /dev/null @@ -1,1171 +0,0 @@ -# 测试覆盖率提升执行计划 - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** 将测试覆盖率从13.08%提升至70%,测试通过率从94.3%提升至100% - -**Architecture:** 采用分阶段迭代策略,优先修复现有测试失败,然后逐步补充核心模块、业务模块和集成测试。使用TDD方法确保测试质量,每个迭代都有明确的验收标准。 - -**Tech Stack:** Jest, React Testing Library, TypeScript, Next.js, Framer Motion - ---- - -## 总体目标 - -| 指标 | 当前值 | 目标值 | 提升幅度 | -|------|--------|--------|----------| -| 测试覆盖率 | 13.08% | 70% | +56.92% | -| 测试通过率 | 94.3% | 100% | +5.7% | -| 测试用例数 | 353 | ~950 | +600 | - ---- - -## 迭代1:修复现有测试(优先级P0) - -**目标:** 测试通过率100%,覆盖率保持13.08% -**预计时间:** 1-2天 -**前置条件:** 无 - ---- - -### Task 1.1: 修复IntersectionObserver mock实现 - -**Files:** -- Modify: `jest.setup.js:41-57` - -**Step 1: 分析当前IntersectionObserver mock的问题** - -运行测试查看失败详情: - -```bash -npm run test:unit -- --testPathPatterns="hero-section.test.tsx|contact-section.test.tsx" --verbose -``` - -Expected: 测试失败,错误信息显示IntersectionObserver相关错误 - -**Step 2: 改进IntersectionObserver mock实现** - -修改 `jest.setup.js`,替换现有的mock实现: - -```javascript -class MockIntersectionObserver { - constructor(callback, options = {}) { - this.callback = callback; - this.options = options; - this.elements = new Set(); - this.observationEntries = []; - } - - observe(element) { - this.elements.add(element); - const entry = { - isIntersecting: true, - target: element, - boundingClientRect: element.getBoundingClientRect ? element.getBoundingClientRect() : {}, - intersectionRatio: 1, - intersectionRect: {}, - rootBounds: {}, - time: Date.now(), - }; - this.observationEntries.push(entry); - this.callback(this.observationEntries, this); - } - - unobserve(element) { - this.elements.delete(element); - this.observationEntries = this.observationEntries.filter( - entry => entry.target !== element - ); - } - - disconnect() { - this.elements.clear(); - this.observationEntries = []; - } - - takeRecords() { - return this.observationEntries; - } -} - -global.IntersectionObserver = MockIntersectionObserver; -global.IntersectionObserverEntry = class IntersectionObserverEntry { - constructor() { - this.isIntersecting = true; - this.target = {}; - this.boundingClientRect = {}; - this.intersectionRatio = 1; - this.intersectionRect = {}; - this.rootBounds = {}; - this.time = Date.now(); - } -}; -``` - -**Step 3: 运行测试验证mock修复** - -```bash -npm run test:unit -- --testPathPatterns="hero-section.test.tsx|contact-section.test.tsx" --verbose -``` - -Expected: IntersectionObserver相关错误消失,但可能有其他测试失败 - -**Step 4: 提交修复** - -```bash -git add jest.setup.js -git commit -m "fix: improve IntersectionObserver mock implementation" -``` - ---- - -### Task 1.2: 简化hero-section测试用例 - -**Files:** -- Modify: `src/components/sections/hero-section.test.tsx:1-150` - -**Step 1: 分析hero-section测试失败原因** - -查看测试失败详情: - -```bash -npm run test:unit -- --testPathPatterns="hero-section.test.tsx" --verbose 2>&1 | grep -A 10 "FAIL" -``` - -Expected: 显示具体的失败测试用例和错误信息 - -**Step 2: 简化hero-section测试用例** - -修改 `src/components/sections/hero-section.test.tsx`,移除复杂的交互测试,保留核心功能测试: - -```typescript -import { describe, it, expect, beforeEach, jest } from '@jest/globals'; -import { render, screen } from '@testing-library/react'; -import '@testing-library/jest-dom'; -import { HeroSection } from './hero-section'; - -jest.mock('framer-motion', () => ({ - motion: { - div: ({ children, ...props }: any) =>
{children}
, - section: ({ children, ...props }: any) =>
{children}
, - span: ({ children, ...props }: any) => {children}, - }, - AnimatePresence: ({ children }: any) => <>{children}, -})); - -jest.mock('@/components/ui/ripple-button', () => ({ - RippleButton: ({ children, ...props }: any) => ( - - ), - SealButton: ({ children, ...props }: any) => ( - - ), -})); - -jest.mock('@/lib/animations', () => ({ - GradientText: ({ children }: any) => {children}, - MagneticButton: ({ children }: any) => , - BlurReveal: ({ children }: any) =>
{children}
, - CounterWithEffect: ({ value }: any) => {value}, -})); - -jest.mock('@/lib/constants', () => ({ - COMPANY_INFO: { - name: '诺瓦隆科技', - description: '专业的金融科技解决方案', - }, - STATS: [ - { label: '客户数量', value: 1000, suffix: '+' }, - { label: '服务年限', value: 10, suffix: '年' }, - { label: '项目案例', value: 500, suffix: '+' }, - ], -})); - -jest.mock('@/components/ui/ink-decoration', () => ({ - InkBackground: () =>
, -})); - -jest.mock('@/components/effects/data-particle-flow', () => ({ - DataParticleFlow: () =>
, -})); - -jest.mock('@/components/effects/subtle-dots', () => ({ - SubtleDots: () =>
, -})); - -describe('HeroSection', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('Rendering', () => { - it('should render hero section', () => { - render(); - const section = document.querySelector('section#home'); - expect(section).toBeInTheDocument(); - }); - - it('should render company name', () => { - render(); - expect(screen.getByText('诺瓦隆科技')).toBeInTheDocument(); - }); - - it('should render background effects', () => { - render(); - expect(screen.getByTestId('ink-background')).toBeInTheDocument(); - expect(screen.getByTestId('data-particle-flow')).toBeInTheDocument(); - }); - - it('should render features', () => { - render(); - expect(screen.getByText('安全可靠')).toBeInTheDocument(); - expect(screen.getByText('高效便捷')).toBeInTheDocument(); - expect(screen.getByText('专业服务')).toBeInTheDocument(); - }); - }); - - describe('Statistics', () => { - it('should render statistics section', () => { - render(); - expect(screen.getByText('客户数量')).toBeInTheDocument(); - expect(screen.getByText('服务年限')).toBeInTheDocument(); - expect(screen.getByText('项目案例')).toBeInTheDocument(); - }); - }); - - describe('Accessibility', () => { - it('should have proper ARIA labels', () => { - render(); - const section = document.querySelector('section#home'); - expect(section).toHaveAttribute('aria-labelledby', 'hero-heading'); - }); - - it('should have accessible buttons', () => { - render(); - const buttons = screen.getAllByRole('button'); - expect(buttons.length).toBeGreaterThan(0); - }); - }); -}); -``` - -**Step 3: 运行测试验证简化后的测试** - -```bash -npm run test:unit -- --testPathPatterns="hero-section.test.tsx" --verbose -``` - -Expected: 所有hero-section测试通过 - -**Step 4: 提交简化后的测试** - -```bash -git add src/components/sections/hero-section.test.tsx -git commit -m "refactor: simplify hero-section tests for stability" -``` - ---- - -### Task 1.3: 简化contact-section测试用例 - -**Files:** -- Modify: `src/components/sections/contact-section.test.tsx:1-320` - -**Step 1: 分析contact-section测试失败原因** - -```bash -npm run test:unit -- --testPathPatterns="contact-section.test.tsx" --verbose 2>&1 | grep -A 10 "FAIL" -``` - -Expected: 显示具体的失败测试用例和错误信息 - -**Step 2: 简化contact-section测试用例** - -修改 `src/components/sections/contact-section.test.tsx`,移除复杂的异步测试,保留核心功能测试: - -```typescript -import { describe, it, expect, beforeEach, jest } from '@jest/globals'; -import { render, screen } from '@testing-library/react'; -import '@testing-library/jest-dom'; -import { ContactSection } from './contact-section'; - -jest.mock('framer-motion', () => ({ - motion: { - div: ({ children, ...props }: any) =>
{children}
, - section: ({ children, ...props }: any) =>
{children}
, - }, - AnimatePresence: ({ children }: any) => <>{children}, -})); - -jest.mock('@/lib/sanitize', () => ({ - sanitizeInput: (value: string) => value, -})); - -jest.mock('@/lib/csrf', () => ({ - generateCSRFToken: () => 'test-csrf-token', - setCSRFTokenToStorage: jest.fn(), - getCSRFTokenFromStorage: () => 'test-csrf-token', -})); - -jest.mock('@/lib/constants', () => ({ - COMPANY_INFO: { - name: '诺瓦隆科技', - email: 'contact@novalon.cn', - phone: '400-123-4567', - address: '北京市朝阳区科技园区', - }, -})); - -describe('ContactSection', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('Rendering', () => { - it('should render contact section', () => { - render(); - const section = document.querySelector('section#contact'); - expect(section).toBeInTheDocument(); - }); - - it('should render contact form', () => { - render(); - expect(screen.getByRole('form')).toBeInTheDocument(); - }); - - it('should render all form fields', () => { - render(); - expect(screen.getByLabelText(/姓名/i)).toBeInTheDocument(); - expect(screen.getByLabelText(/电话/i)).toBeInTheDocument(); - expect(screen.getByLabelText(/邮箱/i)).toBeInTheDocument(); - expect(screen.getByLabelText(/留言/i)).toBeInTheDocument(); - }); - - it('should render submit button', () => { - render(); - expect(screen.getByRole('button', { name: /发送/i })).toBeInTheDocument(); - }); - - it('should render company contact information', () => { - render(); - expect(screen.getByText('contact@novalon.cn')).toBeInTheDocument(); - expect(screen.getByText('400-123-4567')).toBeInTheDocument(); - expect(screen.getByText('北京市朝阳区科技园区')).toBeInTheDocument(); - }); - }); - - describe('Accessibility', () => { - it('should have proper form labels', () => { - render(); - - expect(screen.getByLabelText(/姓名/i)).toBeInTheDocument(); - expect(screen.getByLabelText(/电话/i)).toBeInTheDocument(); - expect(screen.getByLabelText(/邮箱/i)).toBeInTheDocument(); - expect(screen.getByLabelText(/留言/i)).toBeInTheDocument(); - }); - }); - - describe('CSRF Protection', () => { - it('should generate CSRF token on mount', () => { - const { generateCSRFToken, setCSRFTokenToStorage } = require('@/lib/csrf'); - render(); - - expect(generateCSRFToken).toHaveBeenCalled(); - expect(setCSRFTokenToStorage).toHaveBeenCalledWith('test-csrf-token'); - }); - }); -}); -``` - -**Step 3: 运行测试验证简化后的测试** - -```bash -npm run test:unit -- --testPathPatterns="contact-section.test.tsx" --verbose -``` - -Expected: 所有contact-section测试通过 - -**Step 4: 提交简化后的测试** - -```bash -git add src/components/sections/contact-section.test.tsx -git commit -m "refactor: simplify contact-section tests for stability" -``` - ---- - -### Task 1.4: 验证所有测试通过 - -**Step 1: 运行完整测试套件** - -```bash -npm run test:unit -- --coverage --coverageReporters=text-summary -``` - -Expected: -- 测试套件:17个全部通过 -- 测试用例:约300个全部通过 -- 测试通过率:100% - -**Step 2: 验证覆盖率保持** - -检查覆盖率报告,确保覆盖率保持在13.08%左右: - -Expected: -``` -Statements : ~13% -Branches : ~10% -Functions : ~14% -Lines : ~12% -``` - -**Step 3: 提交迭代1完成标记** - -```bash -git add -A -git commit -m "feat: complete iteration 1 - all tests passing" -git tag -a v-test-iteration-1 -m "Test Coverage Iteration 1 Complete" -``` - ---- - -## 迭代2:提升覆盖率到30%(优先级P1) - -**目标:** 覆盖率30%,新增约150个测试用例 -**预计时间:** 3-4天 -**前置条件:** 迭代1完成,所有测试通过 - ---- - -### Task 2.1: 补充layout组件测试 - header.tsx - -**Files:** -- Create: `src/components/layout/header.test.tsx` -- Test: `src/components/layout/header.tsx` - -**Step 1: 读取header.tsx源码** - -```bash -cat src/components/layout/header.tsx | head -50 -``` - -Expected: 查看header组件的结构和功能 - -**Step 2: 编写header组件测试** - -创建 `src/components/layout/header.test.tsx`: - -```typescript -import { describe, it, expect, beforeEach, jest } from '@jest/globals'; -import { render, screen, fireEvent } from '@testing-library/react'; -import '@testing-library/jest-dom'; -import userEvent from '@testing-library/user-event'; - -const mockPush = jest.fn(); -const mockReplace = jest.fn(); - -jest.mock('next/navigation', () => ({ - usePathname: () => '/', - useSearchParams: () => new URLSearchParams(), - useRouter: () => ({ - push: mockPush, - replace: mockReplace, - }), -})); - -jest.mock('next/link', () => { - return ({ children, href, onClick, ...props }: any) => ( - - {children} - - ); -}); - -jest.mock('next/image', () => { - return ({ src, alt, ...props }: any) => ( - {alt} - ); -}); - -jest.mock('framer-motion', () => ({ - motion: { - div: ({ children, ...props }: any) =>
{children}
, - }, - AnimatePresence: ({ children }: any) => <>{children}, -})); - -jest.mock('@/lib/constants', () => ({ - COMPANY_INFO: { - name: '诺瓦隆科技', - }, - NAVIGATION: [ - { id: 'home', label: '首页', href: '/' }, - { id: 'services', label: '服务', href: '/#services' }, - { id: 'products', label: '产品', href: '/#products' }, - { id: 'cases', label: '案例', href: '/#cases' }, - { id: 'about', label: '关于', href: '/#about' }, - { id: 'news', label: '新闻', href: '/#news' }, - { id: 'contact', label: '联系', href: '/contact' }, - ], -})); - -jest.mock('@/hooks/use-focus-trap', () => ({ - useFocusTrap: () => ({ current: null }), -})); - -describe('Header', () => { - beforeEach(() => { - jest.clearAllMocks(); - window.scrollY = 0; - }); - - describe('Rendering', () => { - it('should render header with logo', () => { - const { Header } = require('./header'); - render(
); - expect(screen.getByAltText('诺瓦隆科技')).toBeInTheDocument(); - }); - - it('should render desktop navigation', () => { - const { Header } = require('./header'); - render(
); - expect(screen.getByTestId('desktop-navigation')).toBeInTheDocument(); - }); - - it('should render all navigation items', () => { - const { Header } = require('./header'); - render(
); - expect(screen.getByText('首页')).toBeInTheDocument(); - expect(screen.getByText('服务')).toBeInTheDocument(); - expect(screen.getByText('产品')).toBeInTheDocument(); - }); - - it('should render consult button', () => { - const { Header } = require('./header'); - render(
); - expect(screen.getByTestId('consult-button')).toBeInTheDocument(); - }); - - it('should render mobile menu button', () => { - const { Header } = require('./header'); - render(
); - expect(screen.getByTestId('mobile-menu-button')).toBeInTheDocument(); - }); - }); - - describe('Mobile Menu', () => { - it('should toggle mobile menu on button click', async () => { - const { Header } = require('./header'); - render(
); - - const menuButton = screen.getByTestId('mobile-menu-button'); - expect(screen.queryByTestId('mobile-navigation')).not.toBeInTheDocument(); - - await userEvent.click(menuButton); - expect(screen.getByTestId('mobile-navigation')).toBeInTheDocument(); - - await userEvent.click(menuButton); - expect(screen.queryByTestId('mobile-navigation')).not.toBeInTheDocument(); - }); - - it('should close mobile menu on Escape key', async () => { - const { Header } = require('./header'); - render(
); - - const menuButton = screen.getByTestId('mobile-menu-button'); - await userEvent.click(menuButton); - expect(screen.getByTestId('mobile-navigation')).toBeInTheDocument(); - - fireEvent.keyDown(document, { key: 'Escape' }); - expect(screen.queryByTestId('mobile-navigation')).not.toBeInTheDocument(); - }); - }); - - describe('Accessibility', () => { - it('should have proper ARIA attributes', () => { - const { Header } = require('./header'); - render(
); - - const nav = screen.getByTestId('desktop-navigation'); - expect(nav).toHaveAttribute('role', 'navigation'); - expect(nav).toHaveAttribute('aria-label', '主导航'); - }); - - it('should have accessible mobile menu button', () => { - const { Header } = require('./header'); - render(
); - - const menuButton = screen.getByTestId('mobile-menu-button'); - expect(menuButton).toHaveAttribute('aria-label'); - expect(menuButton).toHaveAttribute('aria-expanded'); - }); - }); -}); -``` - -**Step 3: 运行header测试** - -```bash -npm run test:unit -- --testPathPatterns="header.test.tsx" --verbose -``` - -Expected: 所有header测试通过 - -**Step 4: 提交header测试** - -```bash -git add src/components/layout/header.test.tsx -git commit -m "test: add comprehensive tests for header component" -``` - ---- - -### Task 2.2: 补充layout组件测试 - footer.tsx - -**Files:** -- Create: `src/components/layout/footer.test.tsx` -- Test: `src/components/layout/footer.tsx` - -**Step 1: 读取footer.tsx源码** - -```bash -cat src/components/layout/footer.tsx | head -50 -``` - -Expected: 查看footer组件的结构和功能 - -**Step 2: 编写footer组件测试** - -创建 `src/components/layout/footer.test.tsx`: - -```typescript -import { describe, it, expect, beforeEach, jest } from '@jest/globals'; -import { render, screen } from '@testing-library/react'; -import '@testing-library/jest-dom'; - -jest.mock('next/link', () => { - return ({ children, href, ...props }: any) => ( - - {children} - - ); -}); - -jest.mock('next/image', () => { - return ({ src, alt, ...props }: any) => ( - {alt} - ); -}); - -jest.mock('@/lib/constants', () => ({ - COMPANY_INFO: { - name: '诺瓦隆科技', - email: 'contact@novalon.cn', - phone: '400-123-4567', - address: '北京市朝阳区科技园区', - }, - NAVIGATION: [ - { id: 'home', label: '首页', href: '/' }, - { id: 'services', label: '服务', href: '/#services' }, - ], -})); - -describe('Footer', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('Rendering', () => { - it('should render footer with company name', () => { - const { Footer } = require('./footer'); - render(