๐ AI N8N ๆๆฏๆๆกฃ
่ฟๆฏ AI N8N ้กน็ฎ็่ฏฆ็ปๆๆฏๆๆกฃ๏ผๅ ๅซๆถๆ่ฎพ่ฎกใAPI ่ง่ใๆฐๆฎๅบ่ฎพ่ฎกใๅผๅๆๅ็ญๆๆฏ็ป่ใ
๐ ็ฎๅฝ
- ๐๏ธ ๆถๆ่ฎพ่ฎก
- ๐ ๆฐๆฎๅบ่ฎพ่ฎก
- ๐ API ่ฎพ่ฎก
- ๐จ ๅ็ซฏๆถๆ
- ๐ ่ฎค่ฏๆๆ
- ๐ณ ๆฏไป็ณป็ป
- ๐ค AI ้ๆ
- ๐ ๅฝ้ ๅๅฎ็ฐ
- โก ๆง่ฝไผๅ
- ๐งช ๆต่ฏ็ญ็ฅ
- ๐ ้จ็ฝฒๆต็จ
- ๐ ๅผๅๆๅ
๐๏ธ ๆถๆ่ฎพ่ฎก
ๆดไฝๆถๆ
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 15 | SSR/SSG ๆฏๆ๏ผไผ็ง็ๅผๅไฝ้ช |
UI ๅบ | React 19 | ๆๆฐ็นๆงๆฏๆ๏ผ็ๆๆ็ |
ๆ ทๅผ | Tailwind CSS | ๅฟซ้ๅผๅ๏ผๅๅญๅ CSS |
็ปไปถๅบ | Radix UI | ๆ ๆ ทๅผ็ปไปถ๏ผๅฏ่ฎฟ้ฎๆงๅฅฝ |
็ถๆ็ฎก็ | Zustand | ่ฝป้็บง๏ผTypeScript ๅๅฅฝ |
ๆฐๆฎๅบ | PostgreSQL | ๅ ณ็ณปๅๆฐๆฎๅบ๏ผๅ่ฝๅผบๅคง |
ORM | Drizzle | TypeScript ๅ็๏ผๆง่ฝไผ็ง |
่ฎค่ฏ | 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);
}
});
}