AI N8N ๆŠ€ๆœฏๆ–‡ๆกฃ

164 ้˜…่ฏป10ๅˆ†้’Ÿ

๐Ÿ“– AI N8N ๆŠ€ๆœฏๆ–‡ๆกฃ

่ฟ™ๆ˜ฏ AI N8N ้กน็›ฎ็š„่ฏฆ็ป†ๆŠ€ๆœฏๆ–‡ๆกฃ๏ผŒๅŒ…ๅซๆžถๆž„่ฎพ่ฎกใ€API ่ง„่Œƒใ€ๆ•ฐๆฎๅบ“่ฎพ่ฎกใ€ๅผ€ๅ‘ๆŒ‡ๅ—็ญ‰ๆŠ€ๆœฏ็ป†่Š‚ใ€‚

๐Ÿ“‹ ็›ฎๅฝ•

๐Ÿ—๏ธ ๆžถๆž„่ฎพ่ฎก

ๆ•ดไฝ“ๆžถๆž„

AI N8N ้‡‡็”จ็ŽฐไปฃๅŒ–็š„ๅ…จๆ ˆๆžถๆž„๏ผŒๅŸบไบŽ Next.js 15 App Router ๆž„ๅปบ๏ผš

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   Frontend                          โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚    ็”จๆˆท็•Œ้ข   โ”‚  โ”‚   ็ฎก็†ๅŽๅฐ   โ”‚  โ”‚   ็งปๅŠจ็ซฏ้€‚้…  โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                          โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                  Next.js API Routes                โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚  ่ฎค่ฏ API   โ”‚  โ”‚  ๆ”ฏไป˜ API   โ”‚  โ”‚   AI API    โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                          โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                  External Services                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚    Clerk    โ”‚  โ”‚   Stripe    โ”‚  โ”‚ OpenRouter  โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                          โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    Database                        โ”‚
โ”‚              PostgreSQL + Drizzle ORM              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

ๆŠ€ๆœฏ้€‰ๅž‹

ๅฑ‚็บงๆŠ€ๆœฏ็†็”ฑ
ๅ‰็ซฏๆก†ๆžถNext.js 15SSR/SSG ๆ”ฏๆŒ๏ผŒไผ˜็ง€็š„ๅผ€ๅ‘ไฝ“้ชŒ
UI ๅบ“React 19ๆœ€ๆ–ฐ็‰นๆ€งๆ”ฏๆŒ๏ผŒ็”Ÿๆ€ๆˆ็†Ÿ
ๆ ทๅผTailwind CSSๅฟซ้€Ÿๅผ€ๅ‘๏ผŒๅŽŸๅญๅŒ– CSS
็ป„ไปถๅบ“Radix UIๆ— ๆ ทๅผ็ป„ไปถ๏ผŒๅฏ่ฎฟ้—ฎๆ€งๅฅฝ
็Šถๆ€็ฎก็†Zustand่ฝป้‡็บง๏ผŒTypeScript ๅ‹ๅฅฝ
ๆ•ฐๆฎๅบ“PostgreSQLๅ…ณ็ณปๅž‹ๆ•ฐๆฎๅบ“๏ผŒๅŠŸ่ƒฝๅผบๅคง
ORMDrizzleTypeScript ๅŽŸ็”Ÿ๏ผŒๆ€ง่ƒฝไผ˜็ง€
่ฎค่ฏClerkๅŠŸ่ƒฝๅฎŒๆ•ด๏ผŒๆ˜“ไบŽ้›†ๆˆ
ๆ”ฏไป˜Stripeๅ…จ็ƒๆ”ฏๆŒ๏ผŒๅฎ‰ๅ…จๅฏ้ 
AI ๆœๅŠกOpenRouterๅคšๆจกๅž‹ๆ”ฏๆŒ๏ผŒไปทๆ ผไผ˜ๅŠฟ

๐Ÿ“Š ๆ•ฐๆฎๅบ“่ฎพ่ฎก

ๆ ธๅฟƒ่กจ็ป“ๆž„

็”จๆˆท็›ธๅ…ณ
-- ็”จๆˆท่กจ
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) NOT NULL UNIQUE,
    full_name VARCHAR(255),
    avatar VARCHAR(500),
    bio TEXT,
    skill_level VARCHAR(50) DEFAULT 'beginner',
    preferences JSONB,
    total_learning_time INTEGER DEFAULT 0,
    is_active BOOLEAN DEFAULT true,
    is_admin BOOLEAN DEFAULT false,
    provider VARCHAR(50) DEFAULT 'email',
    provider_id VARCHAR(255),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- ็”จๆˆท่ต„ๆ–™ๆ‰ฉๅฑ•่กจ
CREATE TABLE profiles (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID REFERENCES users(id) ON DELETE CASCADE,
    company VARCHAR(255),
    position VARCHAR(255),
    website VARCHAR(500),
    social_links JSONB,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
ๆ•™็จ‹็ณป็ปŸ
-- ๆ•™็จ‹ๅˆ†็ฑป่กจ
CREATE TABLE tutorial_sections (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    title TEXT NOT NULL,
    title_zh TEXT,
    description TEXT,
    description_zh TEXT,
    icon TEXT DEFAULT 'BookOpen',
    color TEXT DEFAULT 'blue',
    difficulty TEXT NOT NULL DEFAULT 'beginner',
    order INTEGER NOT NULL UNIQUE,
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- ๆ•™็จ‹ๆจกๅ—่กจ
CREATE TABLE tutorial_modules (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    section_id UUID NOT NULL REFERENCES tutorial_sections(id) ON DELETE CASCADE,
    title TEXT NOT NULL,
    title_zh TEXT,
    description TEXT,
    description_zh TEXT,
    content TEXT,
    content_zh TEXT,
    video_url TEXT,
    estimated_time_minutes INTEGER,
    difficulty TEXT NOT NULL DEFAULT 'beginner',
    prerequisites JSONB DEFAULT '[]',
    learning_objectives JSONB DEFAULT '[]',
    tags JSONB DEFAULT '[]',
    order INTEGER NOT NULL,
    is_published BOOLEAN DEFAULT false,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    UNIQUE(section_id, order)
);

-- ๆ•™็จ‹ๆญฅ้ชค่กจ
CREATE TABLE tutorial_steps (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    module_id UUID NOT NULL REFERENCES tutorial_modules(id) ON DELETE CASCADE,
    title TEXT NOT NULL,
    title_zh TEXT,
    content TEXT NOT NULL,
    content_zh TEXT,
    step_type TEXT NOT NULL DEFAULT 'content',
    video_url TEXT,
    exercise_data JSONB,
    order INTEGER NOT NULL,
    estimated_time_minutes INTEGER DEFAULT 5,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    UNIQUE(module_id, order)
);
ๅญฆไน ่ฟ›ๅบฆ
-- ็”จๆˆทๆ•™็จ‹่ฟ›ๅบฆ่กจ
CREATE TABLE user_tutorial_progress (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id TEXT NOT NULL,
    module_id UUID NOT NULL REFERENCES tutorial_modules(id) ON DELETE CASCADE,
    status TEXT NOT NULL DEFAULT 'not_started',
    progress INTEGER DEFAULT 0,
    time_spent INTEGER DEFAULT 0,
    notes TEXT,
    rating INTEGER,
    completed_at TIMESTAMP WITH TIME ZONE,
    started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    UNIQUE(user_id, module_id)
);

-- ็”จๆˆทๆญฅ้ชค่ฟ›ๅบฆ่กจ
CREATE TABLE user_step_progress (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id TEXT NOT NULL,
    step_id UUID NOT NULL REFERENCES tutorial_steps(id) ON DELETE CASCADE,
    is_completed BOOLEAN DEFAULT false,
    time_spent INTEGER DEFAULT 0,
    completed_at TIMESTAMP WITH TIME ZONE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    UNIQUE(user_id, step_id)
);
ๆ”ฏไป˜็ณป็ปŸ
-- ่ฎข้˜…่ฎกๅˆ’่กจ
CREATE TABLE subscription_plans (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(100) NOT NULL,
    name_zh VARCHAR(100),
    description TEXT,
    description_zh TEXT,
    price_monthly DECIMAL(10,2) NOT NULL,
    price_yearly DECIMAL(10,2),
    features JSON NOT NULL DEFAULT '[]',
    features_zh JSON DEFAULT '[]',
    max_use_cases INTEGER DEFAULT -1,
    max_tutorials INTEGER DEFAULT -1,
    max_blogs INTEGER DEFAULT -1,
    stripe_price_id VARCHAR(255),
    stripe_price_id_yearly VARCHAR(255),
    is_active BOOLEAN DEFAULT true,
    is_popular BOOLEAN DEFAULT false,
    sort_order INTEGER DEFAULT 0,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

-- ็”จๆˆท่ฎข้˜…่กจ
CREATE TABLE user_subscriptions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL,
    plan_id UUID NOT NULL,
    stripe_customer_id VARCHAR(255),
    stripe_subscription_id VARCHAR(255),
    status VARCHAR(50) NOT NULL DEFAULT 'active',
    current_period_start TIMESTAMP,
    current_period_end TIMESTAMP,
    cancel_at_period_end BOOLEAN DEFAULT false,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

ๆ•ฐๆฎๅบ“็ดขๅผ•็ญ–็•ฅ

-- ็”จๆˆท็›ธๅ…ณ็ดขๅผ•
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_provider ON users(provider, provider_id);
CREATE INDEX idx_users_created_at ON users(created_at);

-- ๆ•™็จ‹็›ธๅ…ณ็ดขๅผ•
CREATE INDEX idx_tutorial_sections_order ON tutorial_sections(order);
CREATE INDEX idx_tutorial_modules_section ON tutorial_modules(section_id);
CREATE INDEX idx_tutorial_modules_published ON tutorial_modules(is_published);
CREATE INDEX idx_tutorial_steps_module ON tutorial_steps(module_id);

-- ๅญฆไน ่ฟ›ๅบฆ็ดขๅผ•
CREATE INDEX idx_user_progress_user ON user_tutorial_progress(user_id);
CREATE INDEX idx_user_progress_module ON user_tutorial_progress(module_id);
CREATE INDEX idx_user_step_progress_user ON user_step_progress(user_id);

-- ๆ”ฏไป˜็›ธๅ…ณ็ดขๅผ•
CREATE INDEX idx_subscriptions_user ON user_subscriptions(user_id);
CREATE INDEX idx_subscriptions_stripe ON user_subscriptions(stripe_subscription_id);

๐Ÿ”Œ API ่ฎพ่ฎก

API ่ง„่Œƒ

้กน็›ฎ้‡‡็”จ RESTful API ่ฎพ่ฎก๏ผŒๆ‰€ๆœ‰ API ้ƒฝไฝไบŽ /api ่ทฏๅพ„ไธ‹๏ผš

/api
โ”œโ”€โ”€ auth/           # ่ฎค่ฏ็›ธๅ…ณ
โ”œโ”€โ”€ user/           # ็”จๆˆท็ฎก็†
โ”œโ”€โ”€ tutorial/       # ๆ•™็จ‹็ณป็ปŸ
โ”œโ”€โ”€ use-cases/      # ็”จไพ‹็ฎก็†
โ”œโ”€โ”€ blogs/          # ๅšๅฎข็ฎก็†
โ”œโ”€โ”€ payments/       # ๆ”ฏไป˜ๅค„็†
โ”œโ”€โ”€ ai/             # AI ๅŠŸ่ƒฝ
โ””โ”€โ”€ webhooks/       # Webhook ๅค„็†

่ฎค่ฏ API

็”จๆˆทไฟกๆฏ
// GET /api/user/profile
interface UserProfileResponse {
  success: boolean;
  data?: {
    id: string;
    email: string;
    fullName?: string;
    avatar?: string;
    bio?: string;
    skillLevel: 'beginner' | 'intermediate' | 'advanced';
    preferences: UserPreferences;
    totalLearningTime: number;
    isAdmin: boolean;
  };
  error?: string;
}

// PUT /api/user/profile
interface UpdateProfileRequest {
  fullName?: string;
  bio?: string;
  skillLevel?: 'beginner' | 'intermediate' | 'advanced';
  preferences?: UserPreferences;
}

ๆ•™็จ‹ API

่Žทๅ–ๆ•™็จ‹ๅˆ—่กจ
// GET /api/tutorial/sections
interface TutorialSectionsResponse {
  success: boolean;
  data?: TutorialSection[];
  error?: string;
}

// GET /api/tutorial/modules?sectionId=xxx
interface TutorialModulesResponse {
  success: boolean;
  data?: TutorialModule[];
  error?: string;
}

// GET /api/tutorial/steps?moduleId=xxx
interface TutorialStepsResponse {
  success: boolean;
  data?: TutorialStep[];
  error?: string;
}
ๅญฆไน ่ฟ›ๅบฆ็ฎก็†
// POST /api/tutorial/progress
interface UpdateProgressRequest {
  moduleId: string;
  status: 'not_started' | 'in_progress' | 'completed';
  progress?: number;
  timeSpent?: number;
  notes?: string;
  rating?: number;
}

// POST /api/tutorial/step-progress
interface UpdateStepProgressRequest {
  stepId: string;
  isCompleted: boolean;
  timeSpent?: number;
}

AI API

ๆ™บ่ƒฝ็ฟป่ฏ‘
// POST /api/ai/translate
interface TranslateRequest {
  text: string;
  targetLanguage: 'en' | 'zh';
  sourceLanguage?: 'en' | 'zh';
}

interface TranslateResponse {
  success: boolean;
  data?: {
    translatedText: string;
    sourceLanguage: string;
    targetLanguage: string;
  };
  error?: string;
}
ๅ†…ๅฎนๅˆ†ๆž
// POST /api/ai/analyze
interface AnalyzeRequest {
  content: string;
  type: 'sentiment' | 'keywords' | 'summary';
}

interface AnalyzeResponse {
  success: boolean;
  data?: {
    type: string;
    result: any;
    confidence?: number;
  };
  error?: string;
}

ๆ”ฏไป˜ API

ๅˆ›ๅปบ่ฎข้˜…
// POST /api/payments/create-subscription
interface CreateSubscriptionRequest {
  planId: string;
  paymentMethodId: string;
  billingCycle: 'monthly' | 'yearly';
}

interface CreateSubscriptionResponse {
  success: boolean;
  data?: {
    subscriptionId: string;
    clientSecret?: string;
    status: string;
  };
  error?: string;
}
Webhook ๅค„็†
// POST /api/payments/webhook
// Stripe webhook ไบ‹ไปถๅค„็†
// ๆ”ฏๆŒ็š„ไบ‹ไปถ๏ผš
// - customer.subscription.created
// - customer.subscription.updated
// - customer.subscription.deleted
// - invoice.payment_succeeded
// - invoice.payment_failed

้”™่ฏฏๅค„็†

ๆ‰€ๆœ‰ API ้ƒฝ้‡‡็”จ็ปŸไธ€็š„้”™่ฏฏๅค„็†ๆ ผๅผ๏ผš

interface APIResponse<T> {
  success: boolean;
  data?: T;
  error?: string;
  code?: string;
}

// ้”™่ฏฏไปฃ็ ่ง„่Œƒ
const ErrorCodes = {
  UNAUTHORIZED: 'UNAUTHORIZED',
  FORBIDDEN: 'FORBIDDEN',
  NOT_FOUND: 'NOT_FOUND',
  VALIDATION_ERROR: 'VALIDATION_ERROR',
  PAYMENT_REQUIRED: 'PAYMENT_REQUIRED',
  RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',
  INTERNAL_ERROR: 'INTERNAL_ERROR',
} as const;

๐ŸŽจ ๅ‰็ซฏๆžถๆž„

็ป„ไปถๆžถๆž„

components/
โ”œโ”€โ”€ ui/                 # ๅŸบ็ก€ UI ็ป„ไปถ๏ผˆRadix UI ๅฐ่ฃ…๏ผ‰
โ”‚   โ”œโ”€โ”€ button.tsx
โ”‚   โ”œโ”€โ”€ input.tsx
โ”‚   โ”œโ”€โ”€ dialog.tsx
โ”‚   โ””โ”€โ”€ ...
โ”œโ”€โ”€ magicui/            # ้ซ˜็บง UI ็ป„ไปถ
โ”‚   โ”œโ”€โ”€ animated-counter.tsx
โ”‚   โ”œโ”€โ”€ sparkles-text.tsx
โ”‚   โ””โ”€โ”€ ...
โ”œโ”€โ”€ payment/            # ๆ”ฏไป˜็›ธๅ…ณ็ป„ไปถ
โ”‚   โ”œโ”€โ”€ pricing-card.tsx
โ”‚   โ”œโ”€โ”€ checkout-form.tsx
โ”‚   โ””โ”€โ”€ ...
โ””โ”€โ”€ seo/                # SEO ็ป„ไปถ
    โ”œโ”€โ”€ meta-tags.tsx
    โ””โ”€โ”€ structured-data.tsx

ๅŠŸ่ƒฝๆจกๅ—ๆžถๆž„

features/
โ”œโ”€โ”€ auth/               # ่ฎค่ฏๆจกๅ—
โ”‚   โ”œโ”€โ”€ components/     # ่ฎค่ฏ็›ธๅ…ณ็ป„ไปถ
โ”‚   โ”œโ”€โ”€ hooks/          # ่ฎค่ฏ็›ธๅ…ณ hooks
โ”‚   โ””โ”€โ”€ utils/          # ่ฎค่ฏๅทฅๅ…ทๅ‡ฝๆ•ฐ
โ”œโ”€โ”€ tutorial/           # ๆ•™็จ‹ๆจกๅ—
โ”‚   โ”œโ”€โ”€ components/
โ”‚   โ”œโ”€โ”€ hooks/
โ”‚   โ”œโ”€โ”€ types/
โ”‚   โ””โ”€โ”€ utils/
โ”œโ”€โ”€ dashboard/          # ไปช่กจๆฟๆจกๅ—
โ”œโ”€โ”€ blogs/              # ๅšๅฎขๆจกๅ—
โ””โ”€โ”€ use-cases/          # ็”จไพ‹ๆจกๅ—

็Šถๆ€็ฎก็†

ไฝฟ็”จ Zustand ่ฟ›่กŒ็Šถๆ€็ฎก็†๏ผš

// stores/user-store.ts
interface UserState {
  user: User | null;
  loading: boolean;
  updateUser: (user: Partial<User>) => void;
  clearUser: () => void;
}

export const useUserStore = create<UserState>((set) => ({
  user: null,
  loading: false,
  updateUser: (userData) => 
    set((state) => ({ 
      user: state.user ? { ...state.user, ...userData } : null 
    })),
  clearUser: () => set({ user: null }),
}));

่ทฏ็”ฑ่ฎพ่ฎก

้‡‡็”จ Next.js App Router๏ผš

app/
โ”œโ”€โ”€ (auth)/             # ่ฎค่ฏ่ทฏ็”ฑ็ป„
โ”‚   โ”œโ”€โ”€ sign-in/
โ”‚   โ””โ”€โ”€ sign-up/
โ”œโ”€โ”€ front/              # ๅ‰ๅฐ้กต้ข
โ”‚   โ”œโ”€โ”€ dashboard/
โ”‚   โ”œโ”€โ”€ tutorial/
โ”‚   โ”œโ”€โ”€ use-cases/
โ”‚   โ”œโ”€โ”€ blogs/
โ”‚   โ””โ”€โ”€ settings/
โ”œโ”€โ”€ backend/            # ๅŽๅฐ็ฎก็†
โ”‚   โ”œโ”€โ”€ tutorial/
โ”‚   โ”œโ”€โ”€ use-cases/
โ”‚   โ”œโ”€โ”€ blogs/
โ”‚   โ””โ”€โ”€ users/
โ””โ”€โ”€ api/                # API ่ทฏ็”ฑ

๐Ÿ” ่ฎค่ฏๆŽˆๆƒ

Clerk ้›†ๆˆ

้กน็›ฎไฝฟ็”จ Clerk ไฝœไธบ่ฎค่ฏๆœๅŠกๆไพ›ๅ•†๏ผš

// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isProtectedRoute = createRouteMatcher([
  '/front/dashboard(.*)',
  '/front/settings(.*)',
  '/backend(.*)',
]);

export default clerkMiddleware((auth, req) => {
  if (isProtectedRoute(req)) auth().protect();
});

ๆƒ้™ๆŽงๅˆถ

// ็”จๆˆท่ง’่‰ฒๅฎšไน‰
enum UserRole {
  USER = 'user',
  ADMIN = 'admin',
  SUPER_ADMIN = 'super_admin'
}

// ๆƒ้™ๆฃ€ๆŸฅ Hook
export function usePermissions() {
  const { user } = useUser();
  
  const hasPermission = (permission: string) => {
    if (!user) return false;
    
    // ๆฃ€ๆŸฅ็”จๆˆทๆƒ้™้€ป่พ‘
    const userPermissions = user.publicMetadata.permissions as string[] || [];
    return userPermissions.includes(permission);
  };

  const isAdmin = () => {
    return user?.publicMetadata.role === UserRole.ADMIN ||
           user?.publicMetadata.role === UserRole.SUPER_ADMIN;
  };

  return { hasPermission, isAdmin };
}

ไฟๆŠค่ทฏ็”ฑ

// components/protected-route.tsx
interface ProtectedRouteProps {
  children: React.ReactNode;
  permission?: string;
  fallback?: React.ReactNode;
}

export function ProtectedRoute({ 
  children, 
  permission, 
  fallback 
}: ProtectedRouteProps) {
  const { hasPermission } = usePermissions();
  const { isSignedIn, isLoaded } = useUser();

  if (!isLoaded) {
    return <LoadingSpinner />;
  }

  if (!isSignedIn) {
    return <RedirectToSignIn />;
  }

  if (permission && !hasPermission(permission)) {
    return fallback || <div>ๆ— ๆƒ้™่ฎฟ้—ฎ</div>;
  }

  return <>{children}</>;
}

๐Ÿ’ณ ๆ”ฏไป˜็ณป็ปŸ

Stripe ้›†ๆˆๆžถๆž„

// lib/stripe.ts
import Stripe from 'stripe';

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16',
});

// ๅˆ›ๅปบ่ฎข้˜…
export async function createSubscription({
  customerId,
  priceId,
  paymentMethodId,
}: CreateSubscriptionParams) {
  const subscription = await stripe.subscriptions.create({
    customer: customerId,
    items: [{ price: priceId }],
    default_payment_method: paymentMethodId,
    expand: ['latest_invoice.payment_intent'],
  });

  return subscription;
}

่ฎข้˜…็ฎก็†

// features/payments/hooks/use-subscription.ts
export function useSubscription() {
  const [subscription, setSubscription] = useState<UserSubscription | null>(null);
  const [loading, setLoading] = useState(true);

  const updateSubscription = async (planId: string) => {
    try {
      const response = await fetch('/api/payments/update-subscription', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ planId }),
      });

      const result = await response.json();
      if (result.success) {
        setSubscription(result.data);
      }
    } catch (error) {
      console.error('Failed to update subscription:', error);
    }
  };

  const cancelSubscription = async () => {
    // ๅ–ๆถˆ่ฎข้˜…้€ป่พ‘
  };

  return {
    subscription,
    loading,
    updateSubscription,
    cancelSubscription,
  };
}

Webhook ๅค„็†

// app/api/payments/webhook/route.ts
import { stripe } from '@/lib/stripe';
import { updateUserSubscription } from '@/lib/payments';

export async function POST(req: Request) {
  const body = await req.text();
  const signature = req.headers.get('stripe-signature')!;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    return new Response('Webhook signature verification failed', { status: 400 });
  }

  switch (event.type) {
    case 'customer.subscription.created':
    case 'customer.subscription.updated':
      await handleSubscriptionUpdate(event.data.object as Stripe.Subscription);
      break;
    case 'customer.subscription.deleted':
      await handleSubscriptionCanceled(event.data.object as Stripe.Subscription);
      break;
    case 'invoice.payment_succeeded':
      await handlePaymentSucceeded(event.data.object as Stripe.Invoice);
      break;
  }

  return new Response('Webhook processed', { status: 200 });
}

๐Ÿค– AI ้›†ๆˆ

OpenRouter ้…็ฝฎ

// lib/openrouter.ts
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY;
const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';

export class OpenRouterClient {
  private apiKey: string;

  constructor(apiKey: string) {
    this.apiKey = apiKey;
  }

  async chat(messages: ChatMessage[], model?: string) {
    const response = await fetch(`${OPENROUTER_BASE_URL}/chat/completions`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json',
        'HTTP-Referer': process.env.NEXT_PUBLIC_SITE_URL,
        'X-Title': 'AI N8N',
      },
      body: JSON.stringify({
        model: model || 'anthropic/claude-3.5-sonnet',
        messages,
        temperature: 0.7,
        max_tokens: 1000,
      }),
    });

    return response.json();
  }
}

AI ๅŠŸ่ƒฝๅฎž็Žฐ

// app/api/ai/translate/route.ts
export async function POST(req: Request) {
  try {
    const { text, targetLanguage } = await req.json();
    
    const client = new OpenRouterClient(OPENROUTER_API_KEY!);
    
    const messages = [
      {
        role: 'system',
        content: `You are a professional translator. Translate the given text to ${targetLanguage}. Only return the translated text, no explanations.`
      },
      {
        role: 'user',
        content: text
      }
    ];

    const response = await client.chat(messages);
    const translatedText = response.choices[0].message.content;

    return NextResponse.json({
      success: true,
      data: {
        translatedText,
        sourceLanguage: 'auto',
        targetLanguage,
      }
    });
  } catch (error) {
    return NextResponse.json({
      success: false,
      error: 'Translation failed'
    }, { status: 500 });
  }
}

๐ŸŒ ๅ›ฝ้™…ๅŒ–ๅฎž็Žฐ

next-intl ้…็ฝฎ

// src/translate/i18n/request.ts
import { getRequestConfig } from 'next-intl/server';

export default getRequestConfig(async ({ locale }) => ({
  messages: (await import(`../messages/${locale}.json`)).default,
  timeZone: 'Asia/Shanghai',
  formats: {
    dateTime: {
      short: {
        day: 'numeric',
        month: 'short',
        year: 'numeric',
      },
    },
    number: {
      precise: {
        maximumFractionDigits: 5,
      },
    },
  },
}));

ๅคš่ฏญ่จ€่ทฏ็”ฑ

// middleware.ts
import createMiddleware from 'next-intl/middleware';

export default createMiddleware({
  locales: ['en', 'zh'],
  defaultLocale: 'zh',
  localePrefix: 'as-needed',
});

export const config = {
  matcher: [
    '/((?!api|_next|_vercel|.*\\..*).*)',
    '/([\\w-]+)?/users/(.+)',
  ],
};

็ฟป่ฏ‘ๆ–‡ไปถ็ป“ๆž„

// src/translate/messages/zh.json
{
  "common": {
    "title": "AI N8N",
    "description": "ๆ™บ่ƒฝ่‡ชๅŠจๅŒ–ๅญฆไน ๅนณๅฐ",
    "loading": "ๅŠ ่ฝฝไธญ...",
    "error": "ๅ‘็”Ÿ้”™่ฏฏ",
    "success": "ๆ“ไฝœๆˆๅŠŸ"
  },
  "navigation": {
    "home": "้ฆ–้กต",
    "tutorial": "ๆ•™็จ‹",
    "useCases": "็”จไพ‹",
    "blogs": "ๅšๅฎข",
    "pricing": "ๅฎšไปท",
    "dashboard": "ไปช่กจๆฟ"
  },
  "tutorial": {
    "sections": {
      "title": "ๆ•™็จ‹ๅˆ†็ฑป",
      "beginner": "ๅˆ็บงๆ•™็จ‹",
      "intermediate": "ไธญ็บงๆ•™็จ‹",
      "advanced": "้ซ˜็บงๆ•™็จ‹"
    }
  }
}

โšก ๆ€ง่ƒฝไผ˜ๅŒ–

ไปฃ็ ๅˆ†ๅ‰ฒ

// ๅŠจๆ€ๅฏผๅ…ฅ็ป„ไปถ
const TutorialEditor = dynamic(
  () => import('@/features/tutorial/components/TutorialEditor'),
  {
    loading: () => <EditorSkeleton />,
    ssr: false,
  }
);

// ่ทฏ็”ฑ็บงๅˆซ็š„ไปฃ็ ๅˆ†ๅ‰ฒ
const AdminDashboard = dynamic(
  () => import('@/features/admin/AdminDashboard'),
  {
    loading: () => <DashboardSkeleton />,
  }
);

ๅ›พ็‰‡ไผ˜ๅŒ–

// next.config.ts
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: '**',
      },
    ],
    formats: ['image/webp', 'image/avif'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
  },
};

็ผ“ๅญ˜็ญ–็•ฅ

// app/api/tutorial/route.ts
export async function GET() {
  const tutorials = await getTutorials();
  
  return NextResponse.json(tutorials, {
    headers: {
      'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
    },
  });
}

// ไฝฟ็”จ React ็ผ“ๅญ˜
import { cache } from 'react';

export const getTutorialSections = cache(async () => {
  return await db.select().from(tutorialSections);
});

ๆ•ฐๆฎๅบ“ไผ˜ๅŒ–

// ไฝฟ็”จ้ข„ๅค„็†่ฏญๅฅ
const getTutorialsBySection = db
  .select()
  .from(tutorialModules)
  .where(eq(tutorialModules.sectionId, placeholder('sectionId')))
  .prepare();

// ๆ‰น้‡ๆ“ไฝœ
const updateMultipleProgress = db.transaction(async (tx) => {
  for (const progress of progressUpdates) {
    await tx.insert(userTutorialProgress).values(progress);
  }
});

๐Ÿงช ๆต‹่ฏ•็ญ–็•ฅ

ๅ•ๅ…ƒๆต‹่ฏ•

// __tests__/lib/utils.test.ts
import { describe, it, expect } from 'vitest';
import { formatDuration, calculateProgress } from '@/lib/utils';

describe('Utils Functions', () => {
  it('should format duration correctly', () => {
    expect(formatDuration(65)).toBe('1h 5m');
    expect(formatDuration(30)).toBe('30m');
  });

  it('should calculate progress correctly', () => {
    expect(calculateProgress(3, 10)).toBe(30);
    expect(calculateProgress(0, 10)).toBe(0);
  });
});

้›†ๆˆๆต‹่ฏ•

// __tests__/api/tutorial.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { createMocks } from 'node-mocks-http';
import handler from '@/app/api/tutorial/route';

describe('/api/tutorial', () => {
  beforeEach(async () => {
    // ่ฎพ็ฝฎๆต‹่ฏ•ๆ•ฐๆฎ
  });

  afterEach(async () => {
    // ๆธ…็†ๆต‹่ฏ•ๆ•ฐๆฎ
  });

  it('should return tutorial sections', async () => {
    const { req, res } = createMocks({ method: 'GET' });
    
    await handler(req, res);
    
    expect(res._getStatusCode()).toBe(200);
    const data = JSON.parse(res._getData());
    expect(data.success).toBe(true);
  });
});

E2E ๆต‹่ฏ•

// e2e/tutorial.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Tutorial System', () => {
  test('should complete a tutorial module', async ({ page }) => {
    await page.goto('/front/tutorial');
    
    // ้€‰ๆ‹ฉไธ€ไธชๆ•™็จ‹ๅˆ†็ฑป
    await page.click('[data-testid="tutorial-section-beginner"]');
    
    // ้€‰ๆ‹ฉไธ€ไธชๆจกๅ—
    await page.click('[data-testid="tutorial-module-first"]');
    
    // ๅฎŒๆˆๆ‰€ๆœ‰ๆญฅ้ชค
    await page.click('[data-testid="start-tutorial"]');
    
    // ้ชŒ่ฏๅฎŒๆˆ็Šถๆ€
    await expect(page.locator('[data-testid="completion-badge"]')).toBeVisible();
  });
});

๐Ÿš€ ้ƒจ็ฝฒๆต็จ‹

Vercel ้ƒจ็ฝฒ้…็ฝฎ

// vercel.json
{
  "buildCommand": "pnpm build",
  "devCommand": "pnpm dev",
  "installCommand": "pnpm install",
  "framework": "nextjs",
  "regions": ["hkg1", "sfo1"],
  "functions": {
    "app/api/**/*.ts": {
      "maxDuration": 30
    }
  },
  "headers": [
    {
      "source": "/api/(.*)",
      "headers": [
        {
          "key": "Access-Control-Allow-Origin",
          "value": "*"
        }
      ]
    }
  ]
}

็Žฏๅขƒๅ˜้‡้…็ฝฎ

# .env.production
DATABASE_URL="postgresql://..."
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_live_..."
CLERK_SECRET_KEY="sk_live_..."
STRIPE_SECRET_KEY="sk_live_..."
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_live_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
OPENROUTER_API_KEY="sk-or-v1-..."
NEXT_PUBLIC_SITE_URL="https://ai-n8n-pro.vercel.app"

CI/CD ๆต็จ‹

# .github/workflows/deploy.yml
name: Deploy to Vercel

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'pnpm'
      
      - name: Install dependencies
        run: pnpm install
      
      - name: Run tests
        run: pnpm test
      
      - name: Build project
        run: pnpm build
      
      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v20
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}

๐Ÿ“ ๅผ€ๅ‘ๆŒ‡ๅ—

้กน็›ฎ่ง„่Œƒ

ไปฃ็ ้ฃŽๆ ผ
// .eslintrc.json
{
  "extends": [
    "next/core-web-vitals",
    "@typescript-eslint/recommended",
    "prettier"
  ],
  "plugins": ["@typescript-eslint", "import"],
  "rules": {
    "import/order": [
      "error",
      {
        "groups": [
          "builtin",
          "external",
          "internal",
          "parent",
          "sibling",
          "index"
        ],
        "newlines-between": "always"
      }
    ],
    "@typescript-eslint/no-unused-vars": "error",
    "@typescript-eslint/explicit-function-return-type": "off"
  }
}
TypeScript ้…็ฝฎ
// tsconfig.json
{
  "compilerOptions": {
    "target": "es2017",
    "lib": ["dom", "dom.iterable", "es6"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

ๅผ€ๅ‘ๅทฅไฝœๆต

1. ๅŠŸ่ƒฝๅผ€ๅ‘ๆต็จ‹
# 1. ๅˆ›ๅปบๅŠŸ่ƒฝๅˆ†ๆ”ฏ
git checkout -b feature/tutorial-system

# 2. ๅผ€ๅ‘ๅ’Œๆต‹่ฏ•
pnpm dev
pnpm test

# 3. ไปฃ็ ๆฃ€ๆŸฅ
pnpm lint
pnpm type-check

# 4. ๆไบคไปฃ็ 
git add .
git commit -m "feat: implement tutorial system"

# 5. ๆŽจ้€ๅนถๅˆ›ๅปบ PR
git push origin feature/tutorial-system
2. ๆ•ฐๆฎๅบ“่ฟ็งป
# 1. ไฟฎๆ”นๆ•ฐๆฎๅบ“ๆจกๅผ
# ็ผ–่พ‘ src/drizzle/schemas/*.ts

# 2. ็”Ÿๆˆ่ฟ็งปๆ–‡ไปถ
pnpm db:generate

# 3. ๅบ”็”จ่ฟ็งป
pnpm db:push

# 4. ๆŸฅ็œ‹ๆ•ฐๆฎๅบ“
pnpm db:studio
3. ็ป„ไปถๅผ€ๅ‘่ง„่Œƒ
// components/ui/button.tsx
import * as React from 'react';
import { cn } from '@/lib/utils';

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'outline';
  size?: 'sm' | 'md' | 'lg';
  loading?: boolean;
}

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant = 'primary', size = 'md', loading, ...props }, ref) => {
    return (
      <button
        className={cn(
          'inline-flex items-center justify-center rounded-md font-medium',
          {
            'bg-primary text-primary-foreground': variant === 'primary',
            'bg-secondary text-secondary-foreground': variant === 'secondary',
            'border border-input': variant === 'outline',
          },
          {
            'h-8 px-3 text-xs': size === 'sm',
            'h-10 px-4 text-sm': size === 'md',
            'h-12 px-6 text-base': size === 'lg',
          },
          className
        )}
        ref={ref}
        disabled={loading}
        {...props}
      />
    );
  }
);

Button.displayName = 'Button';

่ฐƒ่ฏ•ๆŠ€ๅทง

1. ๆ—ฅๅฟ—่ฎฐๅฝ•
// lib/logger.ts
enum LogLevel {
  DEBUG = 0,
  INFO = 1,
  WARN = 2,
  ERROR = 3,
}

class Logger {
  private level: LogLevel;

  constructor() {
    this.level = process.env.NODE_ENV === 'production' 
      ? LogLevel.WARN 
      : LogLevel.DEBUG;
  }

  debug(message: string, ...args: any[]) {
    if (this.level <= LogLevel.DEBUG) {
      console.log(`[DEBUG] ${message}`, ...args);
    }
  }

  info(message: string, ...args: any[]) {
    if (this.level <= LogLevel.INFO) {
      console.info(`[INFO] ${message}`, ...args);
    }
  }

  warn(message: string, ...args: any[]) {
    if (this.level <= LogLevel.WARN) {
      console.warn(`[WARN] ${message}`, ...args);
    }
  }

  error(message: string, error?: Error, ...args: any[]) {
    if (this.level <= LogLevel.ERROR) {
      console.error(`[ERROR] ${message}`, error, ...args);
    }
  }
}

export const logger = new Logger();
2. ้”™่ฏฏ่พน็•Œ
// components/error-boundary.tsx
'use client';

import { Component, ReactNode } from 'react';
import { logger } from '@/lib/logger';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}

interface State {
  hasError: boolean;
  error?: Error;
}

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: any) {
    logger.error('Error caught by boundary:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div className="flex items-center justify-center min-h-screen">
          <div className="text-center">
            <h2 className="text-2xl font-bold mb-4">ๅ‡บ็Žฐ้”™่ฏฏ</h2>
            <p className="text-muted-foreground mb-4">
              {this.state.error?.message || 'ๆœช็Ÿฅ้”™่ฏฏ'}
            </p>
            <button 
              onClick={() => this.setState({ hasError: false })}
              className="px-4 py-2 bg-primary text-primary-foreground rounded"
            >
              ้‡่ฏ•
            </button>
          </div>
        </div>
      );
    }

    return this.props.children;
  }
}

ๆ€ง่ƒฝ็›‘ๆŽง

1. Web Vitals
// lib/web-vitals.ts
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';

function sendToAnalytics(metric: any) {
  // ๅ‘้€ๅˆฐๅˆ†ๆžๆœๅŠก
  if (process.env.NODE_ENV === 'production') {
    fetch('/api/analytics/web-vitals', {
      method: 'POST',
      body: JSON.stringify(metric),
      headers: { 'Content-Type': 'application/json' },
    });
  }
}

export function trackWebVitals() {
  getCLS(sendToAnalytics);
  getFID(sendToAnalytics);
  getFCP(sendToAnalytics);
  getLCP(sendToAnalytics);
  getTTFB(sendToAnalytics);
}
2. ๆ•ฐๆฎๅบ“ๆ€ง่ƒฝ็›‘ๆŽง
// lib/db-monitor.ts
import { performance } from 'perf_hooks';
import { logger } from './logger';

export function withMonitoring<T>(
  operation: () => Promise<T>,
  operationName: string
): Promise<T> {
  return new Promise(async (resolve, reject) => {
    const start = performance.now();
    
    try {
      const result = await operation();
      const end = performance.now();
      const duration = end - start;
      
      logger.info(`Database operation: ${operationName}`, {
        duration: `${duration.toFixed(2)}ms`,
        timestamp: new Date().toISOString(),
      });
      
      // ๅฆ‚ๆžœๆŸฅ่ฏขๆ—ถ้—ด่ถ…่ฟ‡ 1 ็ง’๏ผŒ่ฎฐๅฝ•่ญฆๅ‘Š
      if (duration > 1000) {
        logger.warn(`Slow query detected: ${operationName}`, {
          duration: `${duration.toFixed(2)}ms`,
        });
      }
      
      resolve(result);
    } catch (error) {
      const end = performance.now();
      const duration = end - start;
      
      logger.error(`Database operation failed: ${operationName}`, error, {
        duration: `${duration.toFixed(2)}ms`,
      });
      
      reject(error);
    }
  });
}

๐Ÿ“š ๅ‚่€ƒ่ต„ๆบ

ๅฎ˜ๆ–นๆ–‡ๆกฃ

็ฌฌไธ‰ๆ–นๆœๅŠก

ๅผ€ๅ‘ๅทฅๅ…ท