Files
soul-yongping/开发文档/7、数据库/数据库设计.md
2026-02-09 15:09:29 +08:00

17 KiB
Raw Blame History

数据库设计

我是卡若。

这个项目当前使用LocalStorage做数据持久化,但未来会切换到MongoDB。这个文档定义了完整的数据库设计方案。


一、数据库选型

1.1 为什么选MongoDB?

  1. 文档型数据库: 适合内容类产品,数据结构灵活
  2. 无Schema约束: 快速迭代不需要频繁改表结构
  3. JSON原生支持: 前后端数据格式一致
  4. 横向扩展能力: 支持未来大规模用户增长
  5. 向量搜索支持: MongoDB Atlas支持向量检索(未来AI功能)

1.2 当前方案 vs 未来方案

当前方案 (LocalStorage): ```javascript // 优点

  • 无需服务器
  • 开发调试方便
  • 适合MVP验证

// 缺点

  • 数据仅存浏览器本地
  • 多设备无法同步
  • 数据容易丢失 ```

未来方案 (MongoDB): ```javascript // 优点

  • 数据持久化存储
  • 多设备数据同步
  • 支持复杂查询
  • 支持事务(ACID)

// 迁移计划

  1. 安装MongoDB驱动
  2. 创建数据库连接
  3. 逐步替换LocalStorage
  4. 添加数据迁移脚本 ```

二、数据库连接配置

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+