2026-01-14 12:50:00 +08:00
|
|
|
|
# 数据库设计
|
2026-01-09 11:58:08 +08:00
|
|
|
|
|
|
|
|
|
|
**我是卡若。**
|
|
|
|
|
|
|
2026-01-14 12:50:00 +08:00
|
|
|
|
这个项目当前使用LocalStorage做数据持久化,但未来会切换到MongoDB。这个文档定义了完整的数据库设计方案。
|
2026-01-09 11:58:08 +08:00
|
|
|
|
|
2026-01-14 12:50:00 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 一、数据库选型
|
|
|
|
|
|
|
|
|
|
|
|
### 1.1 为什么选MongoDB?
|
|
|
|
|
|
|
|
|
|
|
|
1. **文档型数据库**: 适合内容类产品,数据结构灵活
|
|
|
|
|
|
2. **无Schema约束**: 快速迭代不需要频繁改表结构
|
|
|
|
|
|
3. **JSON原生支持**: 前后端数据格式一致
|
|
|
|
|
|
4. **横向扩展能力**: 支持未来大规模用户增长
|
|
|
|
|
|
5. **向量搜索支持**: MongoDB Atlas支持向量检索(未来AI功能)
|
|
|
|
|
|
|
|
|
|
|
|
### 1.2 当前方案 vs 未来方案
|
|
|
|
|
|
|
|
|
|
|
|
**当前方案** (LocalStorage):
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-14 12:50:00 +08:00
|
|
|
|
// 优点
|
|
|
|
|
|
- 无需服务器
|
|
|
|
|
|
- 开发调试方便
|
|
|
|
|
|
- 适合MVP验证
|
|
|
|
|
|
|
|
|
|
|
|
// 缺点
|
|
|
|
|
|
- 数据仅存浏览器本地
|
|
|
|
|
|
- 多设备无法同步
|
|
|
|
|
|
- 数据容易丢失
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
**未来方案** (MongoDB):
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-14 12:50:00 +08:00
|
|
|
|
// 优点
|
|
|
|
|
|
- 数据持久化存储
|
|
|
|
|
|
- 多设备数据同步
|
|
|
|
|
|
- 支持复杂查询
|
|
|
|
|
|
- 支持事务(ACID)
|
|
|
|
|
|
|
|
|
|
|
|
// 迁移计划
|
|
|
|
|
|
1. 安装MongoDB驱动
|
|
|
|
|
|
2. 创建数据库连接
|
|
|
|
|
|
3. 逐步替换LocalStorage
|
|
|
|
|
|
4. 添加数据迁移脚本
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 二、数据库连接配置
|
|
|
|
|
|
|
|
|
|
|
|
### 2.1 本地开发环境
|
|
|
|
|
|
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`bash
|
2026-01-14 12:50:00 +08:00
|
|
|
|
# MongoDB本地安装
|
|
|
|
|
|
brew install mongodb-community@6.0
|
|
|
|
|
|
|
|
|
|
|
|
# 启动MongoDB
|
|
|
|
|
|
brew services start mongodb-community@6.0
|
|
|
|
|
|
|
|
|
|
|
|
# 连接字符串
|
|
|
|
|
|
mongodb://localhost:27017/soul-experiment
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
### 2.2 生产环境 (MongoDB Atlas)
|
|
|
|
|
|
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`bash
|
2026-01-14 12:50:00 +08:00
|
|
|
|
# 连接字符串
|
|
|
|
|
|
mongodb+srv://<username>:<password>@cluster0.mongodb.net/soul-experiment?retryWrites=true&w=majority
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
### 2.3 环境变量配置
|
|
|
|
|
|
|
|
|
|
|
|
**.env.local**:
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`bash
|
2026-01-14 12:50:00 +08:00
|
|
|
|
# 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
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 三、数据模型设计
|
|
|
|
|
|
|
|
|
|
|
|
### 3.1 用户集合 (users)
|
|
|
|
|
|
|
|
|
|
|
|
**集合名称**: `users`
|
|
|
|
|
|
|
|
|
|
|
|
**索引**:
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-14 12:50:00 +08:00
|
|
|
|
{
|
|
|
|
|
|
phone: 1, // 唯一索引
|
|
|
|
|
|
referralCode: 1, // 唯一索引
|
|
|
|
|
|
referredBy: 1, // 普通索引
|
|
|
|
|
|
createdAt: -1 // 降序索引
|
|
|
|
|
|
}
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
**文档结构**:
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-14 12:50:00 +08:00
|
|
|
|
{
|
|
|
|
|
|
_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")
|
|
|
|
|
|
}
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
**查询示例**:
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-14 12:50:00 +08:00
|
|
|
|
// 根据手机号查找用户
|
|
|
|
|
|
db.users.findOne({ phone: "15880802661" })
|
|
|
|
|
|
|
|
|
|
|
|
// 根据推荐码查找用户
|
|
|
|
|
|
db.users.findOne({ referralCode: "REFABC123" })
|
|
|
|
|
|
|
|
|
|
|
|
// 查找某推荐人的所有下级
|
|
|
|
|
|
db.users.find({ referredBy: "REFABC123" })
|
|
|
|
|
|
|
|
|
|
|
|
// 查找收益前10的推广者
|
|
|
|
|
|
db.users.find({}).sort({ earnings: -1 }).limit(10)
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
### 3.2 订单集合 (orders)
|
|
|
|
|
|
|
|
|
|
|
|
**集合名称**: `orders`
|
|
|
|
|
|
|
|
|
|
|
|
**索引**:
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-14 12:50:00 +08:00
|
|
|
|
{
|
|
|
|
|
|
orderId: 1, // 唯一索引
|
|
|
|
|
|
userId: 1, // 普通索引
|
|
|
|
|
|
status: 1, // 普通索引
|
|
|
|
|
|
createdAt: -1 // 降序索引
|
|
|
|
|
|
}
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
**文档结构**:
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-14 12:50:00 +08:00
|
|
|
|
{
|
|
|
|
|
|
_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")
|
|
|
|
|
|
}
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
**查询示例**:
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-14 12:50:00 +08:00
|
|
|
|
// 查找用户所有订单
|
|
|
|
|
|
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"
|
|
|
|
|
|
})
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
### 3.3 提现记录集合 (withdrawals)
|
|
|
|
|
|
|
|
|
|
|
|
**集合名称**: `withdrawals`
|
|
|
|
|
|
|
|
|
|
|
|
**索引**:
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-14 12:50:00 +08:00
|
|
|
|
{
|
|
|
|
|
|
userId: 1, // 普通索引
|
|
|
|
|
|
status: 1, // 普通索引
|
|
|
|
|
|
createdAt: -1 // 降序索引
|
|
|
|
|
|
}
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
**文档结构**:
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-14 12:50:00 +08:00
|
|
|
|
{
|
|
|
|
|
|
_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")
|
|
|
|
|
|
}
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
**查询示例**:
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-14 12:50:00 +08:00
|
|
|
|
// 查找待审核提现
|
|
|
|
|
|
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 }
|
|
|
|
|
|
}}
|
|
|
|
|
|
])
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
### 3.4 章节内容集合 (sections)
|
|
|
|
|
|
|
|
|
|
|
|
**集合名称**: `sections`
|
2026-01-09 11:58:08 +08:00
|
|
|
|
|
2026-01-14 12:50:00 +08:00
|
|
|
|
**索引**:
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-09 11:58:08 +08:00
|
|
|
|
{
|
2026-01-14 12:50:00 +08:00
|
|
|
|
sectionId: 1, // 唯一索引
|
|
|
|
|
|
isFree: 1, // 普通索引
|
|
|
|
|
|
createdAt: -1 // 降序索引
|
|
|
|
|
|
}
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
**文档结构**:
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-09 11:58:08 +08:00
|
|
|
|
{
|
2026-01-14 12:50:00 +08:00
|
|
|
|
_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")
|
|
|
|
|
|
}
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
**查询示例**:
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-14 12:50:00 +08:00
|
|
|
|
// 获取所有免费章节
|
|
|
|
|
|
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: "创业 私域" } })
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
### 3.5 阅读记录集合 (reading_logs)
|
|
|
|
|
|
|
|
|
|
|
|
**集合名称**: `reading_logs`
|
|
|
|
|
|
|
|
|
|
|
|
**索引**:
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-14 12:50:00 +08:00
|
|
|
|
{
|
|
|
|
|
|
userId: 1,
|
|
|
|
|
|
sectionId: 1,
|
|
|
|
|
|
createdAt: -1
|
|
|
|
|
|
}
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
**文档结构**:
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-09 11:58:08 +08:00
|
|
|
|
{
|
2026-01-14 12:50:00 +08:00
|
|
|
|
_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")
|
|
|
|
|
|
}
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
### 3.6 系统配置集合 (settings)
|
|
|
|
|
|
|
|
|
|
|
|
**集合名称**: `settings`
|
|
|
|
|
|
|
|
|
|
|
|
**文档结构**:
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-09 11:58:08 +08:00
|
|
|
|
{
|
2026-01-14 12:50:00 +08:00
|
|
|
|
_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")
|
|
|
|
|
|
}
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 四、数据迁移方案
|
|
|
|
|
|
|
|
|
|
|
|
### 4.1 LocalStorage to MongoDB
|
|
|
|
|
|
|
|
|
|
|
|
**步骤1**: 导出LocalStorage数据
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-14 12:50:00 +08:00
|
|
|
|
// 导出脚本 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))
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
**步骤2**: 导入MongoDB
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-14 12:50:00 +08:00
|
|
|
|
// 导入脚本 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()
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 五、数据库操作封装
|
|
|
|
|
|
|
|
|
|
|
|
### 5.1 用户操作
|
|
|
|
|
|
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`typescript
|
2026-01-14 12:50:00 +08:00
|
|
|
|
// 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() }
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
### 5.2 订单操作
|
|
|
|
|
|
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`typescript
|
2026-01-14 12:50:00 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
}}
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-01-09 11:58:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
2026-01-09 11:58:08 +08:00
|
|
|
|
|
2026-01-14 12:50:00 +08:00
|
|
|
|
## 六、数据备份策略
|
2026-01-09 11:58:08 +08:00
|
|
|
|
|
2026-01-14 12:50:00 +08:00
|
|
|
|
### 6.1 自动备份
|
|
|
|
|
|
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`bash
|
2026-01-14 12:50:00 +08:00
|
|
|
|
# 每日凌晨3点自动备份
|
|
|
|
|
|
0 3 * * * mongodump --uri="mongodb://localhost:27017/soul-experiment" --out="/backup/$(date +\%Y\%m\%d)"
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
### 6.2 恢复数据
|
|
|
|
|
|
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`bash
|
2026-01-14 12:50:00 +08:00
|
|
|
|
# 恢复指定日期的备份
|
|
|
|
|
|
mongorestore --uri="mongodb://localhost:27017/soul-experiment" --dir="/backup/20250114"
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 七、性能优化
|
|
|
|
|
|
|
|
|
|
|
|
### 7.1 索引优化
|
|
|
|
|
|
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-14 12:50:00 +08:00
|
|
|
|
// 创建复合索引
|
|
|
|
|
|
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")
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
### 7.2 查询优化
|
|
|
|
|
|
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`javascript
|
2026-01-14 12:50:00 +08:00
|
|
|
|
// 使用投影减少数据传输
|
|
|
|
|
|
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
|
|
|
|
|
|
}}
|
|
|
|
|
|
])
|
2026-01-14 05:10:32 +00:00
|
|
|
|
\`\`\`
|
2026-01-09 11:58:08 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
2026-01-14 12:50:00 +08:00
|
|
|
|
|
|
|
|
|
|
**总结**: 数据库设计是系统的基石,合理的结构设计能让后续开发事半功倍。当前使用LocalStorage做MVP验证,未来切换MongoDB后,整个系统的可靠性和扩展性都会大幅提升。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
**更新时间**: 2025年1月14日
|
|
|
|
|
|
**负责人**: 卡若
|
|
|
|
|
|
**数据库版本**: MongoDB 6.0+
|