Files
soul-yongping/开发文档/7、数据库/数据库设计.md

709 lines
17 KiB
Markdown
Raw Normal View 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://<username>:<password>@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<User>) {
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<Order>) {
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+