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

709 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 数据库设计
**我是卡若。**
这个项目当前使用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+