17 KiB
数据库设计
我是卡若。
这个项目当前使用LocalStorage做数据持久化,但未来会切换到MongoDB。这个文档定义了完整的数据库设计方案。
一、数据库选型
1.1 为什么选MongoDB?
- 文档型数据库: 适合内容类产品,数据结构灵活
- 无Schema约束: 快速迭代不需要频繁改表结构
- JSON原生支持: 前后端数据格式一致
- 横向扩展能力: 支持未来大规模用户增长
- 向量搜索支持: MongoDB Atlas支持向量检索(未来AI功能)
1.2 当前方案 vs 未来方案
当前方案 (LocalStorage): ```javascript // 优点
- 无需服务器
- 开发调试方便
- 适合MVP验证
// 缺点
- 数据仅存浏览器本地
- 多设备无法同步
- 数据容易丢失 ```
未来方案 (MongoDB): ```javascript // 优点
- 数据持久化存储
- 多设备数据同步
- 支持复杂查询
- 支持事务(ACID)
// 迁移计划
- 安装MongoDB驱动
- 创建数据库连接
- 逐步替换LocalStorage
- 添加数据迁移脚本 ```
二、数据库连接配置
2.1 本地开发环境
```bash
MongoDB本地安装
brew install mongodb-community@6.0
启动MongoDB
brew services start mongodb-community@6.0
连接字符串
mongodb://localhost:27017/soul-experiment ```
2.2 生产环境 (MongoDB Atlas)
```bash
连接字符串
mongodb+srv://:@cluster0.mongodb.net/soul-experiment?retryWrites=true&w=majority ```
2.3 环境变量配置
.env.local: ```bash
MongoDB配置
MONGODB_URI=mongodb://localhost:27017/soul-experiment MONGODB_DB_NAME=soul-experiment
或使用云数据库
MONGODB_URI=mongodb://10.88.182.62:3306/soul-experiment
MONGODB_USERNAME=root
MONGODB_PASSWORD=Vtka(agu)-1
```
三、数据模型设计
3.1 用户集合 (users)
集合名称: users
索引: ```javascript { phone: 1, // 唯一索引 referralCode: 1, // 唯一索引 referredBy: 1, // 普通索引 createdAt: -1 // 降序索引 } ```
文档结构: ```javascript { _id: ObjectId("65a1234567890abcdef12345"), phone: "15880802661", // 手机号 nickname: "卡若", // 昵称 avatar: "https://cdn.example.com/avatar.jpg", // 头像 openid: "wx_openid_xxx", // 微信openid unionid: "wx_unionid_xxx", // 微信unionid
// 购买记录 purchasedSections: [ // 已购章节 "1.1", "1.2", "3.3" ], hasFullBook: false, // 是否购买整本书
// 分销数据 referralCode: "REFABC123", // 推荐码 referredBy: "REFXYZ789", // 推荐人的码 referralCount: 28, // 推荐人数 earnings: 256.80, // 总收益(元) pendingEarnings: 128.90, // 待提现(元) withdrawnEarnings: 127.90, // 已提现(元)
// 阅读数据 readingTime: 12480, // 阅读时长(秒) readingProgress: 45, // 阅读进度(%) lastReadSection: "3.2", // 最后阅读章节 lastReadAt: ISODate("2025-01-14T12:00:00Z"),
// 权限 isAdmin: false, // 是否管理员 isBanned: false, // 是否封禁
// 时间戳 createdAt: ISODate("2025-01-01T00:00:00Z"), updatedAt: ISODate("2025-01-14T12:00:00Z") } ```
查询示例: ```javascript // 根据手机号查找用户 db.users.findOne({ phone: "15880802661" })
// 根据推荐码查找用户 db.users.findOne({ referralCode: "REFABC123" })
// 查找某推荐人的所有下级 db.users.find({ referredBy: "REFABC123" })
// 查找收益前10的推广者 db.users.find({}).sort({ earnings: -1 }).limit(10) ```
3.2 订单集合 (orders)
集合名称: orders
索引: ```javascript { orderId: 1, // 唯一索引 userId: 1, // 普通索引 status: 1, // 普通索引 createdAt: -1 // 降序索引 } ```
文档结构: ```javascript { _id: ObjectId("65a1234567890abcdef12345"), orderId: "ORDER_1705200000_abc123", // 订单号 userId: ObjectId("65a1234567890abcdef00001"), // 用户ID userPhone: "15880802661", // 用户手机号 userNickname: "卡若", // 用户昵称
// 订单信息 type: "section", // "section" | "fullbook" sectionId: "1.1", // 章节ID(单章购买时) sectionTitle: "自行车荷总...", // 章节标题 amount: 1.00, // 金额(元)
// 支付信息 paymentMethod: "wechat", // 支付方式 transactionId: "wx_pay_123456789", // 第三方交易号 status: "completed", // "pending" | "completed" | "failed" | "refunded"
// 分销信息 referralCode: "REFXYZ789", // 推荐码 referrerUserId: ObjectId("65a1234567890abcdef00002"), // 推荐人ID referrerEarnings: 0.90, // 推荐人佣金(元)
// 时间戳 createdAt: ISODate("2025-01-14T12:00:00Z"), // 创建时间 paidAt: ISODate("2025-01-14T12:05:00Z"), // 支付时间 expireAt: ISODate("2025-01-14T12:30:00Z"), // 过期时间(30分钟) updatedAt: ISODate("2025-01-14T12:05:00Z") } ```
查询示例: ```javascript // 查找用户所有订单 db.orders.find({ userId: ObjectId("65a...") }).sort({ createdAt: -1 })
// 查找待支付订单 db.orders.find({ status: "pending", expireAt: { $gt: new Date() } })
// 统计今日收益 db.orders.aggregate([ { $match: { status: "completed", paidAt: { $gte: ISODate("2025-01-14T00:00:00Z"), $lt: ISODate("2025-01-15T00:00:00Z") } }}, { $group: { _id: null, totalRevenue: { $sum: "$amount" }, totalOrders: { $sum: 1 } }} ])
// 查找某推荐人的所有佣金记录 db.orders.find({ referralCode: "REFABC123", status: "completed" }) ```
3.3 提现记录集合 (withdrawals)
集合名称: withdrawals
索引: ```javascript { userId: 1, // 普通索引 status: 1, // 普通索引 createdAt: -1 // 降序索引 } ```
文档结构: ```javascript { _id: ObjectId("65a1234567890abcdef12345"), withdrawalId: "WD_1705200000_abc123", // 提现单号 userId: ObjectId("65a1234567890abcdef00001"), // 用户ID userPhone: "15880802661", // 用户手机号 userNickname: "卡若", // 用户昵称
// 提现信息 amount: 100.00, // 提现金额(元) method: "wechat", // "wechat" | "alipay" account: "微信号或支付宝账号", name: "真实姓名",
// 状态 status: "pending", // "pending" | "completed" | "rejected" rejectReason: "", // 拒绝原因
// 时间戳 createdAt: ISODate("2025-01-14T12:00:00Z"), // 申请时间 completedAt: ISODate("2025-01-14T14:00:00Z"), // 完成时间 updatedAt: ISODate("2025-01-14T14:00:00Z") } ```
查询示例: ```javascript // 查找待审核提现 db.withdrawals.find({ status: "pending" }).sort({ createdAt: 1 })
// 查找用户提现记录 db.withdrawals.find({ userId: ObjectId("65a...") }).sort({ createdAt: -1 })
// 统计今日提现金额 db.withdrawals.aggregate([ { $match: { status: "completed", completedAt: { $gte: ISODate("2025-01-14T00:00:00Z"), $lt: ISODate("2025-01-15T00:00:00Z") } }}, { $group: { _id: null, totalAmount: { $sum: "$amount" }, totalCount: { $sum: 1 } }} ]) ```
3.4 章节内容集合 (sections)
集合名称: sections
索引: ```javascript { sectionId: 1, // 唯一索引 isFree: 1, // 普通索引 createdAt: -1 // 降序索引 } ```
文档结构: ```javascript { _id: ObjectId("65a1234567890abcdef12345"), sectionId: "1.1", // 章节ID
// 章节信息 title: "自行车荷总:一个行业做到极致是什么样", content: "# 自行车荷总\n\n...", // Markdown内容 summary: "本章讲述了...", // 摘要 keywords: ["创业", "行业深耕"], // 关键词
// 层级关系 partId: "part-1", // 所属篇 partTitle: "真实的人", chapterId: "chapter-1", // 所属章 chapterTitle: "人与人之间的底层逻辑",
// 定价 price: 1, // 价格(元) isFree: true, // 是否免费 unlockAfterDays: 0, // 定时解锁(天数)
// 统计数据 viewCount: 1234, // 浏览次数 purchaseCount: 456, // 购买次数 avgReadingTime: 180, // 平均阅读时长(秒)
// 文件信息 filePath: "book/_第一篇|真实的人/...", wordCount: 3580, // 字数
// 发布状态 status: "published", // "draft" | "published" publishedAt: ISODate("2025-01-01T00:00:00Z"),
// 时间戳 createdAt: ISODate("2025-01-01T00:00:00Z"), updatedAt: ISODate("2025-01-14T12:00:00Z") } ```
查询示例: ```javascript // 获取所有免费章节 db.sections.find({ isFree: true })
// 获取最新发布的10章 db.sections.find({ status: "published" }) .sort({ publishedAt: -1 }) .limit(10)
// 按浏览量排序 db.sections.find().sort({ viewCount: -1 }).limit(10)
// 全文搜索(需创建文本索引) db.sections.createIndex({ title: "text", content: "text", keywords: "text" }) db.sections.find({ $text: { $search: "创业 私域" } }) ```
3.5 阅读记录集合 (reading_logs)
集合名称: reading_logs
索引: ```javascript { userId: 1, sectionId: 1, createdAt: -1 } ```
文档结构: ```javascript { _id: ObjectId("65a1234567890abcdef12345"), userId: ObjectId("65a1234567890abcdef00001"), sectionId: "3.2",
// 阅读数据 progress: 68, // 阅读进度(%) readingTime: 180, // 阅读时长(秒) scrollDepth: 75, // 滚动深度(%)
// 设备信息 device: "iPhone 14 Pro", browser: "Safari", ip: "121.xxx.xxx.xxx",
// 时间戳 createdAt: ISODate("2025-01-14T12:00:00Z") } ```
3.6 系统配置集合 (settings)
集合名称: settings
文档结构: ```javascript { _id: "global_settings",
// 分润配置 distributorShare: 90, // 推广者分成(%) authorShare: 10, // 作者分成(%)
// 定价配置 sectionPrice: 1, // 单章价格(元) fullBookPrice: 9.9, // 整书价格(元)
// 支付配置 paymentMethods: { wechat: { enabled: true, appId: "wx432c93e275548671", merchantId: "1318592501", apiKey: "" }, alipay: { enabled: true, partnerId: "2088511801157159", securityKey: "" } },
// 营销配置 partyGroupQrCode: "https://...", bannerText: "每天早上6-9点,Soul派对房不见不散",
// 时间戳 updatedAt: ISODate("2025-01-14T12:00:00Z") } ```
四、数据迁移方案
4.1 LocalStorage to MongoDB
步骤1: 导出LocalStorage数据 ```javascript // 导出脚本 scripts/export-localstorage.js const fs = require('fs')
const users = JSON.parse(localStorage.getItem('users') || '[]') const orders = JSON.parse(localStorage.getItem('all_purchases') || '[]') const settings = JSON.parse(localStorage.getItem('app_settings') || '{}')
const exportData = { users, orders, settings } fs.writeFileSync('data-export.json', JSON.stringify(exportData, null, 2)) ```
步骤2: 导入MongoDB ```javascript // 导入脚本 scripts/import-mongodb.js const { MongoClient } = require('mongodb') const fs = require('fs')
async function importData() { const client = await MongoClient.connect(process.env.MONGODB_URI) const db = client.db('soul-experiment')
const data = JSON.parse(fs.readFileSync('data-export.json', 'utf8'))
// 导入用户 await db.collection('users').insertMany(data.users)
// 导入订单 await db.collection('orders').insertMany(data.orders)
// 导入配置 await db.collection('settings').insertOne({ _id: 'global_settings', ...data.settings })
client.close() console.log('数据导入完成') }
importData() ```
五、数据库操作封装
5.1 用户操作
```typescript // lib/db/users.ts import { MongoClient, ObjectId } from 'mongodb'
export async function createUser(userData: Partial) { const db = await getDatabase() const result = await db.collection('users').insertOne({ ...userData, referralCode: generateReferralCode(), earnings: 0, pendingEarnings: 0, withdrawnEarnings: 0, referralCount: 0, purchasedSections: [], hasFullBook: false, createdAt: new Date(), updatedAt: new Date() }) return result.insertedId }
export async function findUserByPhone(phone: string) { const db = await getDatabase() return await db.collection('users').findOne({ phone }) }
export async function updateUserEarnings( userId: ObjectId, amount: number ) { const db = await getDatabase() await db.collection('users').updateOne( { _id: userId }, { $inc: { earnings: amount, pendingEarnings: amount }, $set: { updatedAt: new Date() } } ) } ```
5.2 订单操作
```typescript
// lib/db/orders.ts
export async function createOrder(orderData: Partial) {
const db = await getDatabase()
const orderId = ORDER_${Date.now()}_${randomString()}
const result = await db.collection('orders').insertOne({ orderId, ...orderData, status: 'pending', createdAt: new Date(), expireAt: new Date(Date.now() + 30 * 60 * 1000), // 30分钟 updatedAt: new Date() })
return { orderId, _id: result.insertedId } }
export async function completeOrder(orderId: string) { const db = await getDatabase() const order = await db.collection('orders').findOne({ orderId })
if (!order) throw new Error('订单不存在')
// 更新订单状态 await db.collection('orders').updateOne( { orderId }, { $set: { status: 'completed', paidAt: new Date(), updatedAt: new Date() } } )
// 解锁内容 if (order.type === 'section') { await db.collection('users').updateOne( { _id: order.userId }, { $addToSet: { purchasedSections: order.sectionId } } ) } else if (order.type === 'fullbook') { await db.collection('users').updateOne( { _id: order.userId }, { $set: { hasFullBook: true } } ) }
// 分配佣金 if (order.referralCode) { const referrer = await db.collection('users').findOne({ referralCode: order.referralCode })
if (referrer) {
const commission = order.amount * 0.9 // 90%佣金
await updateUserEarnings(referrer._id, commission)
// 记录佣金
await db.collection('orders').updateOne(
{ orderId },
{ $set: {
referrerUserId: referrer._id,
referrerEarnings: commission
}}
)
}
} } ```
六、数据备份策略
6.1 自动备份
```bash
每日凌晨3点自动备份
0 3 * * * mongodump --uri="mongodb://localhost:27017/soul-experiment" --out="/backup/$(date +%Y%m%d)" ```
6.2 恢复数据
```bash
恢复指定日期的备份
mongorestore --uri="mongodb://localhost:27017/soul-experiment" --dir="/backup/20250114" ```
七、性能优化
7.1 索引优化
```javascript // 创建复合索引 db.orders.createIndex({ userId: 1, createdAt: -1 }) db.orders.createIndex({ status: 1, expireAt: 1 }) db.users.createIndex({ referralCode: 1 }, { unique: true })
// 查看索引使用情况 db.orders.find({ userId: ObjectId("...") }).explain("executionStats") ```
7.2 查询优化
```javascript // 使用投影减少数据传输 db.users.find( { phone: "15880802661" }, { nickname: 1, referralCode: 1, earnings: 1 } )
// 使用聚合管道优化复杂查询 db.orders.aggregate([ { $match: { status: "completed" } }, { $lookup: { from: "users", localField: "userId", foreignField: "_id", as: "user" }}, { $unwind: "$user" }, { $project: { orderId: 1, amount: 1, "user.nickname": 1 }} ]) ```
总结: 数据库设计是系统的基石,合理的结构设计能让后续开发事半功倍。当前使用LocalStorage做MVP验证,未来切换MongoDB后,整个系统的可靠性和扩展性都会大幅提升。
更新时间: 2025年1月14日
负责人: 卡若
数据库版本: MongoDB 6.0+