# 数据库设计

**我是卡若。**

这个项目当前使用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+
