483 lines
18 KiB
TypeScript
483 lines
18 KiB
TypeScript
/**
|
||
* 作者: 阿瑞
|
||
* 功能: 数据库填充脚本 (Seeding Script)
|
||
* 版本: 2.0.0
|
||
*
|
||
* 运行方式: npx tsx scripts/seed.ts
|
||
*/
|
||
|
||
import mongoose from 'mongoose';
|
||
import fs from 'fs';
|
||
import path from 'path';
|
||
import bcrypt from 'bcryptjs';
|
||
import {
|
||
User,
|
||
Article,
|
||
SystemConfig,
|
||
Comment,
|
||
Order,
|
||
MembershipPlan,
|
||
Category,
|
||
Tag
|
||
} from '../src/models/index';
|
||
|
||
// ------------------------------------------------------------------
|
||
// 1. 环境配置加载 (手动读取 .env.local,避免依赖 dotenv)
|
||
// ------------------------------------------------------------------
|
||
const loadEnv = () => {
|
||
const envPath = path.resolve(process.cwd(), '.env.local');
|
||
if (fs.existsSync(envPath)) {
|
||
console.log('📄 正在加载 .env.local 配置...');
|
||
const envConfig = fs.readFileSync(envPath, 'utf8');
|
||
envConfig.split('\n').forEach((line) => {
|
||
const parts = line.split('=');
|
||
if (parts.length >= 2) {
|
||
const key = parts[0].trim();
|
||
const value = parts.slice(1).join('=').trim();
|
||
if (key && value && !key.startsWith('#')) {
|
||
process.env[key] = value;
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
console.warn('⚠️ 未找到 .env.local 文件,尝试使用系统环境变量...');
|
||
}
|
||
};
|
||
|
||
loadEnv();
|
||
|
||
const MONGODB_URI = process.env.MONGODB_URI;
|
||
|
||
if (!MONGODB_URI) {
|
||
console.error('❌ 错误: MONGODB_URI 未定义,请检查 .env.local 文件');
|
||
process.exit(1);
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// 2. 辅助函数
|
||
// ------------------------------------------------------------------
|
||
const getRandomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
|
||
const getRandomItem = <T>(arr: T[]): T => arr[Math.floor(Math.random() * arr.length)];
|
||
const getRandomItems = <T>(arr: T[], count: number): T[] => {
|
||
const shuffled = [...arr].sort(() => 0.5 - Math.random());
|
||
return shuffled.slice(0, count);
|
||
};
|
||
|
||
// ------------------------------------------------------------------
|
||
// 3. 主逻辑
|
||
// ------------------------------------------------------------------
|
||
const seed = async () => {
|
||
try {
|
||
console.log('🔌 正在连接数据库...');
|
||
await mongoose.connect(MONGODB_URI);
|
||
console.log('✅ 数据库连接成功');
|
||
|
||
console.log('🧹 正在清理旧数据...');
|
||
await Promise.all([
|
||
User.deleteMany({}),
|
||
Article.deleteMany({}),
|
||
SystemConfig.deleteMany({}),
|
||
Comment.deleteMany({}),
|
||
Order.deleteMany({}),
|
||
MembershipPlan.deleteMany({}),
|
||
Category.deleteMany({}),
|
||
Tag.deleteMany({})
|
||
]);
|
||
console.log('✅ 旧数据清理完成');
|
||
|
||
// --------------------------------------------------------------
|
||
// 4. 创建基础数据
|
||
// --------------------------------------------------------------
|
||
|
||
// --- 4.1 系统配置 ---
|
||
console.log('⚙️ 创建系统配置...');
|
||
await SystemConfig.create({
|
||
配置标识: 'default',
|
||
站点设置: {
|
||
网站标题: 'AOUN - 独立开发者资源站',
|
||
网站副标题: '构建你的数字资产',
|
||
Logo地址: '/LOGO.png',
|
||
全局SEO关键词: 'React, Next.js, AI, 独立开发, 源码下载',
|
||
全局SEO描述: 'AOUN 是一个专注于分享高质量编程资源、AI 实战教程和独立开发经验的平台。',
|
||
底部版权信息: '© 2025 AounApp. All Rights Reserved.'
|
||
},
|
||
Banner配置: [
|
||
{
|
||
标题: 'Next.js 15 全栈实战',
|
||
描述: '深入探索 Next.js 15 的最新特性,从 App Router 到 Server Actions,构建高性能全栈应用。',
|
||
图片地址: 'https://images.unsplash.com/photo-1618477247222-ac59124545da?q=80&w=2070&auto=format&fit=crop',
|
||
按钮文本: '立即学习',
|
||
按钮链接: '/article/nextjs-15-fullstack',
|
||
状态: 'visible'
|
||
},
|
||
{
|
||
标题: 'AI Agent 开发指南',
|
||
描述: '掌握 LLM 应用开发核心,使用 LangChain 和 OpenAI 构建智能体。',
|
||
图片地址: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?q=80&w=2070&auto=format&fit=crop',
|
||
按钮文本: '查看教程',
|
||
按钮链接: '/article/ai-agent-guide',
|
||
状态: 'visible'
|
||
},
|
||
{
|
||
标题: '独立开发变现之路',
|
||
描述: '分享从 0 到 1 的产品构建、推广与商业化经验。',
|
||
图片地址: 'https://images.unsplash.com/photo-1553729459-efe14ef6055d?q=80&w=2070&auto=format&fit=crop',
|
||
按钮文本: '阅读专栏',
|
||
按钮链接: '/category/indie-hacker',
|
||
状态: 'visible'
|
||
}
|
||
],
|
||
AI配置列表: [
|
||
{
|
||
名称: '写作助手-Gemini',
|
||
接口地址: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||
API密钥: process.env.GEMINI_API_KEY || 'dummy-key',
|
||
模型: 'gemini-1.5-flash',
|
||
系统提示词: '你是一个专业的技术写作助手,擅长撰写清晰、有深度的技术文章。',
|
||
是否启用: true,
|
||
流式传输: true
|
||
}
|
||
]
|
||
});
|
||
|
||
// --- 4.2 会员套餐 ---
|
||
console.log('💎 创建会员套餐...');
|
||
const plans = await MembershipPlan.create([
|
||
{
|
||
套餐名称: '月度会员',
|
||
有效天数: 30,
|
||
价格: 29.9,
|
||
描述: '适合短期突击学习,畅享一个月无限下载',
|
||
特权配置: { 每日下载限制: 20, 购买折扣: 0.9 }
|
||
},
|
||
{
|
||
套餐名称: '年度会员',
|
||
有效天数: 365,
|
||
价格: 299,
|
||
描述: '超值推荐,全年无忧学习,低至 0.8 元/天',
|
||
特权配置: { 每日下载限制: 100, 购买折扣: 0.7 }
|
||
},
|
||
{
|
||
套餐名称: '永久会员',
|
||
有效天数: 36500,
|
||
价格: 999,
|
||
描述: '一次付费,终身受益,尊享所有未来更新',
|
||
特权配置: { 每日下载限制: 9999, 购买折扣: 0.5 }
|
||
}
|
||
]);
|
||
|
||
// --- 4.3 分类与标签 ---
|
||
console.log('🏷️ 创建分类与标签...');
|
||
const categories = await Category.create([
|
||
{ 分类名称: '全栈开发', 别名: 'fullstack', 排序权重: 10 },
|
||
{ 分类名称: 'AI 工程化', 别名: 'ai-engineering', 排序权重: 9 },
|
||
{ 分类名称: '系统设计', 别名: 'system-design', 排序权重: 8 },
|
||
{ 分类名称: '独立开发', 别名: 'indie-hacker', 排序权重: 7 },
|
||
{ 分类名称: '源码资源', 别名: 'resources', 排序权重: 6 }
|
||
]);
|
||
|
||
const tags = await Tag.create([
|
||
{ 标签名称: 'Next.js' }, { 标签名称: 'React' }, { 标签名称: 'TypeScript' },
|
||
{ 标签名称: 'TailwindCSS' }, { 标签名称: 'Node.js' }, { 标签名称: 'PostgreSQL' },
|
||
{ 标签名称: 'MongoDB' }, { 标签名称: 'Docker' }, { 标签名称: 'LangChain' },
|
||
{ 标签名称: 'OpenAI' }, { 标签名称: 'RAG' }, { 标签名称: 'SaaS' }
|
||
]);
|
||
|
||
// --- 4.4 用户 ---
|
||
console.log('👥 创建用户...');
|
||
|
||
// 加密密码
|
||
const adminPassword = await bcrypt.hash('admin@aoun.ltd', 10);
|
||
const userPassword = await bcrypt.hash('123456', 10);
|
||
|
||
const adminUser = await User.create({
|
||
用户名: 'Admin',
|
||
邮箱: 'admin@aoun.ltd',
|
||
密码: adminPassword,
|
||
角色: 'admin',
|
||
头像: 'https://api.dicebear.com/7.x/avataaars/svg?seed=admin',
|
||
钱包: { 当前积分: 99999, 历史总消费: 0 }
|
||
});
|
||
|
||
const demoUsers = await User.create([
|
||
{
|
||
用户名: 'Alex',
|
||
邮箱: 'alex@example.com',
|
||
密码: userPassword,
|
||
角色: 'user',
|
||
头像: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Alex',
|
||
钱包: { 当前积分: 50, 历史总消费: 100 }
|
||
},
|
||
{
|
||
用户名: 'Sarah',
|
||
邮箱: 'sarah@example.com',
|
||
密码: userPassword,
|
||
角色: 'user',
|
||
头像: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Sarah',
|
||
会员信息: {
|
||
当前等级ID: plans[1]._id, // 年度会员
|
||
过期时间: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000)
|
||
}
|
||
},
|
||
{
|
||
用户名: 'Developer',
|
||
邮箱: 'dev@example.com',
|
||
密码: userPassword,
|
||
角色: 'editor',
|
||
头像: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Dev'
|
||
}
|
||
]);
|
||
|
||
const allUsers = [adminUser, ...demoUsers];
|
||
|
||
// --- 4.5 文章/资源 ---
|
||
console.log('📚 创建文章与资源...');
|
||
const articlesData = [
|
||
{
|
||
title: 'Next.js 15 App Router 深度解析与最佳实践',
|
||
summary: '全面解析 Next.js 15 的 App Router 架构,探讨 Server Components、Server Actions 以及流式渲染的最佳实践。',
|
||
content: `
|
||
## 引言
|
||
|
||
Next.js 15 带来了革命性的变化,App Router 彻底改变了我们构建 React 应用的方式。本文将带你深入了解其核心概念。
|
||
|
||
## Server Components
|
||
|
||
React Server Components (RSC) 允许我们在服务器端渲染组件,减少发送到客户端的 JavaScript 体积。
|
||
|
||
\`\`\`tsx
|
||
async function GetData() {
|
||
const res = await fetch('https://api.example.com/data');
|
||
return res.json();
|
||
}
|
||
|
||
export default async function Page() {
|
||
const data = await GetData();
|
||
return <main>{data.title}</main>;
|
||
}
|
||
\`\`\`
|
||
|
||
## Server Actions
|
||
|
||
不再需要单独的 API 路由,直接在组件中定义服务器端逻辑。
|
||
|
||
## 总结
|
||
|
||
掌握 App Router 是成为现代 React 全栈开发者的关键。
|
||
`,
|
||
category: '全栈开发',
|
||
tags: ['Next.js', 'React', 'TypeScript'],
|
||
isPaid: false
|
||
},
|
||
{
|
||
title: '构建企业级 RAG 应用:从原理到实战',
|
||
summary: '基于 LangChain 和 Vector Database,手把手教你搭建一个能够理解私有数据的 AI 知识库问答系统。',
|
||
content: `
|
||
## 什么是 RAG?
|
||
|
||
检索增强生成 (Retrieval-Augmented Generation) 是解决 LLM 幻觉和知识过时的有效方案。
|
||
|
||
## 架构设计
|
||
|
||
1. **文档加载与切分**:使用 LangChain Loader。
|
||
2. **向量化 (Embedding)**:使用 OpenAI Embedding 模型。
|
||
3. **向量数据库**:使用 Pinecone 或 Milvus。
|
||
4. **检索与生成**:构建 Prompt,结合检索结果和 LLM。
|
||
|
||
## 代码实现
|
||
|
||
\`\`\`python
|
||
from langchain.chains import RetrievalQA
|
||
from langchain.llms import OpenAI
|
||
|
||
qa = RetrievalQA.from_chain_type(
|
||
llm=OpenAI(),
|
||
chain_type="stuff",
|
||
retriever=docsearch.as_retriever()
|
||
)
|
||
\`\`\`
|
||
`,
|
||
category: 'AI 工程化',
|
||
tags: ['LangChain', 'RAG', 'OpenAI'],
|
||
isPaid: true
|
||
},
|
||
{
|
||
title: 'SaaS 产品设计与系统架构指南',
|
||
summary: '如何设计一个高可用、多租户的 SaaS 系统?本文涵盖数据库设计、权限管理及计费系统实现。',
|
||
content: `
|
||
## 多租户架构
|
||
|
||
选择合适的隔离策略:
|
||
- **Database per Tenant**:最高隔离性,成本高。
|
||
- **Schema per Tenant**:平衡方案。
|
||
- **Discriminator Column**:共享表,成本最低,开发复杂度高。
|
||
|
||
## 权限系统 (RBAC)
|
||
|
||
设计灵活的角色和权限模型是 SaaS 的核心。
|
||
|
||
## 计费集成
|
||
|
||
集成 Stripe 或 Lemon Squeezy 实现订阅支付。
|
||
`,
|
||
category: '系统设计',
|
||
tags: ['SaaS', 'System Design', 'PostgreSQL'],
|
||
isPaid: true
|
||
},
|
||
{
|
||
title: '独立开发者如何寻找 Profitable Niche',
|
||
summary: '避开红海竞争,发现细分市场的机会。分享 5 个验证产品构想的低成本方法。',
|
||
content: `
|
||
## 寻找痛点
|
||
|
||
最好的产品往往源于开发者自己的痛点。
|
||
|
||
## 验证想法
|
||
|
||
1. **Landing Page MVP**:先做落地页,收集邮箱。
|
||
2. **Pre-sale**:尝试预售。
|
||
3. **社区反馈**:在 Reddit 或 Product Hunt 发布。
|
||
|
||
## 案例分析
|
||
|
||
分析几个成功的独立开发产品案例...
|
||
`,
|
||
category: '独立开发',
|
||
tags: ['Indie Hacker', 'SaaS'],
|
||
isPaid: false
|
||
},
|
||
{
|
||
title: 'React 19 Hook 使用指南',
|
||
summary: 'React 19 带来了 use()、useOptimistic() 等新 Hook,本文详细介绍它们的用法和场景。',
|
||
content: `
|
||
## use() Hook
|
||
|
||
\`use()\` 是一个新的 API,用于在组件中读取 Promise 或 Context 的值。
|
||
|
||
\`\`\`tsx
|
||
import { use } from 'react';
|
||
|
||
function Message({ messagePromise }) {
|
||
const message = use(messagePromise);
|
||
return <p>{message}</p>;
|
||
}
|
||
\`\`\`
|
||
|
||
## useOptimistic
|
||
|
||
用于处理乐观 UI 更新,提升用户体验。
|
||
`,
|
||
category: '全栈开发',
|
||
tags: ['React', 'Next.js'],
|
||
isPaid: false
|
||
}
|
||
];
|
||
|
||
const articles = [];
|
||
for (let i = 0; i < articlesData.length; i++) {
|
||
const data = articlesData[i];
|
||
const cat = categories.find(c => c.分类名称 === data.category) || categories[0];
|
||
const articleTags = tags.filter(t => data.tags.includes(t.标签名称));
|
||
const author = adminUser; // Admin 发布文章
|
||
|
||
articles.push({
|
||
文章标题: data.title,
|
||
URL别名: `article-${i + 1}`,
|
||
封面图: `https://picsum.photos/seed/${i + 100}/800/450`,
|
||
摘要: data.summary,
|
||
正文内容: data.content,
|
||
SEO关键词: [data.category, ...data.tags],
|
||
SEO描述: data.summary,
|
||
作者ID: author._id,
|
||
分类ID: cat._id,
|
||
标签ID列表: articleTags.map(t => t._id),
|
||
价格: data.isPaid ? 29.9 : 0,
|
||
支付方式: 'points',
|
||
资源属性: data.isPaid ? {
|
||
下载链接: 'https://pan.baidu.com/s/demo-link',
|
||
提取码: '8888',
|
||
解压密码: 'aounapp.com'
|
||
} : {},
|
||
统计数据: {
|
||
阅读数: getRandomInt(100, 5000),
|
||
点赞数: getRandomInt(10, 500),
|
||
评论数: 0,
|
||
收藏数: getRandomInt(5, 200)
|
||
},
|
||
发布状态: 'published'
|
||
});
|
||
}
|
||
|
||
const createdArticles = await Article.insertMany(articles);
|
||
|
||
// --- 4.6 评论 ---
|
||
console.log('💬 创建评论...');
|
||
const comments = [];
|
||
for (const article of createdArticles) {
|
||
const commentCount = getRandomInt(0, 3);
|
||
for (let j = 0; j < commentCount; j++) {
|
||
const user = getRandomItem(demoUsers);
|
||
comments.push({
|
||
文章ID: article._id,
|
||
用户ID: user._id,
|
||
评论内容: `这篇文章写得很好,对我帮助很大!期待更多关于 ${article.SEO关键词[0]} 的内容。`,
|
||
点赞数: getRandomInt(0, 10),
|
||
状态: 'visible'
|
||
});
|
||
}
|
||
}
|
||
await Comment.insertMany(comments);
|
||
|
||
// 更新文章评论数
|
||
for (const article of createdArticles) {
|
||
const count = comments.filter(c => c.文章ID === article._id).length;
|
||
await Article.findByIdAndUpdate(article._id, { '统计数据.评论数': count });
|
||
}
|
||
|
||
// --- 4.7 订单 ---
|
||
console.log('🧾 创建订单...');
|
||
const orders = [];
|
||
for (let k = 0; k < 3; k++) {
|
||
const user = getRandomItem(demoUsers);
|
||
const plan = getRandomItem(plans);
|
||
orders.push({
|
||
订单号: `ORDER${Date.now()}${k}`,
|
||
用户ID: user._id,
|
||
订单类型: 'buy_membership',
|
||
商品ID: plan._id,
|
||
商品快照: {
|
||
标题: plan.套餐名称,
|
||
封面: '/images/membership.png'
|
||
},
|
||
支付金额: plan.价格,
|
||
支付方式: 'alipay',
|
||
订单状态: 'paid',
|
||
支付时间: new Date()
|
||
});
|
||
}
|
||
await Order.insertMany(orders);
|
||
|
||
console.log('✨ 数据库填充完成!');
|
||
console.log(`
|
||
📊 数据统计:
|
||
- 用户: ${allUsers.length} (管理员: admin@aoun.ltd / admin@aoun.ltd)
|
||
- 文章: ${createdArticles.length}
|
||
- 评论: ${comments.length}
|
||
- 订单: ${orders.length}
|
||
- 分类: ${categories.length}
|
||
- 标签: ${tags.length}
|
||
- 会员套餐: ${plans.length}
|
||
`);
|
||
|
||
} catch (error) {
|
||
console.error('❌ 数据库填充失败:', error);
|
||
process.exit(1);
|
||
} finally {
|
||
await mongoose.disconnect();
|
||
console.log('👋 数据库连接已断开');
|
||
process.exit(0);
|
||
}
|
||
};
|
||
|
||
seed();
|