# 二期 — 音频内容付费收听系统设计

> 更新时间：2026-05-21  
> 目标：用户生成书籍音频专辑后，其它用户通过付费购买进行收听

---

## 一、业务流程全景

```
【创作者 A】                    【消费者 B】
    │                              │
    ├─ 创建书籍 + AI 生成音频        ├─ 逛有声集市
    ├─ 发布到有声集市               ├─ 试听预览片段
    ├─ 设定价格（整张一口价）         ├─ 付费购买专辑
    ├─ 查看收益、提现               ├─ 解锁全部内容，永久收听
                                     ├─ 我的已购
```

---

## 二、数据库设计

### 2.1 新增模型

```prisma
// 有声集市 - 专辑上架定价
model AlbumMarketListing {
  id            Int      @id @default(autoincrement())
  bookId        Int      @unique
  creatorId     Int
  title         String
  description   String?  @db.Text
  coverUrl      String?

  price          Decimal  @default(0.00) @db.Decimal(10, 2)  // 售价（整张一口价）

  isActive      Boolean  @default(false)   // 是否上架
  isRecommended Boolean  @default(false)   // 推荐位

  totalSales    Int      @default(0)       // 累计销量
  totalRevenue  Float    @default(0.00)    // 累计销售额
  avgRating     Float    @default(0.00)

  previewChapterCount Int @default(3)      // 免费试听章节数

  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt

  book          Book     @relation(fields: [bookId], references: [id])

  @@index([isActive, totalSales])
  @@index([creatorId])
}

// 内容购买记录
model ContentPurchase {
  id            Int      @id @default(autoincrement())
  buyerId       Int
  listingId     Int
  bookId        Int
  orderNo       String   @unique
  amount        Decimal  @db.Decimal(10, 2)    // 实付金额
  status        String   @default("success") // 'success' | 'refunded'

  paymentMethod String?
  paymentId     String?

  platformFee   Decimal  @default(0) @db.Decimal(10, 2)
  creatorShare  Decimal  @default(0) @db.Decimal(10, 2)
  platformShare Decimal  @default(0) @db.Decimal(10, 2)

  createdAt     DateTime @default(now())

  listing       AlbumMarketListing @relation(fields: [listingId], references: [id])
  buyer         User               @relation("buyer", fields: [buyerId], references: [id])

  @@unique([buyerId, bookId])  // 每人每本书只能购买一次
  @@index([buyerId, createdAt])
  @@index([listingId])
  @@index([orderNo])
}

// 创作者收益记录
model CreatorEarning {
  id            Int      @id @default(autoincrement())
  creatorId     Int
  purchaseId    Int
  bookId        Int
  amount        Decimal  @db.Decimal(10, 2)    // Float→Decimal
  type          String                   // 'sale' | 'refund'
  status        String   @default("pending") // 'pending' | 'settled' | 'withdrawn'

  settleDate    DateTime?
  withdrawDate  DateTime?

  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt

  creator       User             @relation(fields: [creatorId], references: [id])
  purchase      ContentPurchase  @relation(fields: [purchaseId], references: [id])

  @@index([creatorId, status])
  @@index([creatorId, createdAt])
}

// 创作者钱包
model CreatorWallet {
  id              Int    @id @default(autoincrement())
  userId          Int    @unique
  balance         Decimal  @default(0.00) @db.Decimal(12, 2)   // 可提现余额
  totalEarned     Decimal  @default(0.00) @db.Decimal(12, 2)
  totalWithdrawn  Decimal  @default(0.00) @db.Decimal(12, 2)
  frozenBalance   Decimal  @default(0.00) @db.Decimal(12, 2)   // T+7 冻结余额

  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt

  user            User   @relation(fields: [userId], references: [id])
}

// 提现记录
model WithdrawRecord {
  id          Int      @id @default(autoincrement())
  userId      Int
  amount      Decimal  @db.Decimal(10, 2)    // 提现金额
  method      String   @default("alipay")
  account     String
  status      String   @default("pending")
  remark      String?

  approvedAt  DateTime?
  paidAt      DateTime?

  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  user        User     @relation(fields: [userId], references: [id])

  @@index([userId, status])
  @@index([status, createdAt])
}
```

### 2.2 现有模型修改

```prisma
// User 新增
model User {
  // ... 现有字段 ...
  isCreator         Boolean    @default(false)
  creatorBio        String?
  creatorListings   AlbumMarketListing[]
  purchases         ContentPurchase[]
  creatorEarnings   CreatorEarning[]
  creatorWallet     CreatorWallet?
  withdrawRecords   WithdrawRecord[]
}

// Book 新增
model Book {
  // ... 现有字段 ...
  totalSales    Int        @default(0)
  marketListing AlbumMarketListing?
}
```

---

## 三、API 设计

### 3.1 有声集市 API (`/api/marketplace`)

| 方法 | 路由 | 功能 |
|------|------|------|
| GET | `/api/marketplace/albums` | 集市列表（分类/排序/分页） |
| GET | `/api/marketplace/albums/:id` | 专辑详情（试听控制+购买状态） |
| GET | `/api/marketplace/albums/:id/try` | 获取试听章节内容 |
| POST | `/api/marketplace/albums/:id/purchase` | 购买专辑 |
| GET | `/api/marketplace/purchases` | 我的已购列表 |
| GET | `/api/marketplace/check-access/:bookId` | 检查访问权限 |

### 3.2 创作者中心 API (`/api/creator`)

| 方法 | 路由 | 功能 |
|------|------|------|
| POST | `/api/creator/activate` | 激活创作者身份 |
| GET | `/api/creator/dashboard` | 仪表盘汇总 |
| GET | `/api/creator/listings` | 我的上架作品 |
| POST | `/api/creator/listings/:bookId` | 创建/更新上架定价 |
| DELETE | `/api/creator/listings/:listingId` | 下架作品 |
| GET | `/api/creator/earnings` | 收益明细 |
| GET | `/api/creator/wallet` | 钱包信息 |
| POST | `/api/creator/withdraw` | 申请提现 |

### 3.3 内容鉴权中间件

```typescript
// 核心逻辑：整张专辑一口价，购买后解锁全部章节
export async function requireContentAccess(ctx, next) {
  const userId = ctx.state.user?.id;
  const bookId = Number(ctx.params.bookId);
  const chapterId = Number(ctx.params.chapterId);

  // 1. 创作者本人 → 放行
  const book = await prisma.book.findUnique({ where: { id: bookId } });
  if (book?.userId === userId) return next();

  // 2. 已购买整张专辑 → 放行
  const purchase = await prisma.contentPurchase.findFirst({
    where: { buyerId: userId, bookId, status: 'success' }
  });
  if (purchase) return next();

  // 3. 试听章节 → 放行
  const listing = await prisma.albumMarketListing.findUnique({
    where: { bookId, isActive: true }
  });
  if (listing) {
    const chapters = await prisma.bookChapter.findMany({
      where: { bookId, level: 1 }, orderBy: { number: 'asc' }
    });
    const idx = chapters.findIndex(c => c.id === chapterId);
    if (idx >= 0 && idx < listing.previewChapterCount) return next();
  }

  // 4. 拒绝
  ctx.status = 402;
  ctx.body = {
    code: 402,
    message: '此内容需要购买后才能收听',
    needPurchase: true,
    listingId: listing?.id
  };
}
```

---

## 四、定价与分账

### 定价模型

创作者自主定价，**整张专辑一口价**，简单清晰：

| 定价档位 | 建议价格 | 适用场景 |
|---------|---------|---------|
| 体验价 | ¥0.99 ~ ¥4.99 | 短篇/新人引流 |
| 标准价 | ¥4.99 ~ ¥19.99 | 中长篇/常规内容 |
| 精品价 | ¥19.99 ~ ¥49.99 | 高质量/独家内容 |
| 旗舰价 | ¥49.99 ~ ¥99.99 | 超长篇/系列合集 |

### 分账比例

```
消费者支付 ¥10
  ├─ 1%  支付手续费 → ¥0.10
  ├─ 20% 平台技术服务费 → ¥1.98
  └─ 79% 创作者分成 → ¥7.92
```

### 结算规则

- **结算周期**：T+7（购买后第7天自动结算）
- **提现方式**：支付宝 / 微信
- **最低提现**：¥10
- **到账时间**：1-3 个工作日

---

## 五、前端页面设计

### 新增页面（7个）

| # | 页面路径 | 名称 | 功能 |
|---|---------|------|------|
| 1 | `pages/marketplace/index` | 有声集市 | 浏览上架专辑，分类/排序/瀑布流 |
| 2 | `pages/marketplace/detail` | 专辑购买页 | 详情、试听、价格、购买 |
| 3 | `pages/creator/dashboard` | 创作者中心 | 数据仪表盘 |
| 4 | `pages/creator/listings` | 作品管理 | 上架/下架/定价 |
| 5 | `pages/creator/earnings` | 收益明细 | 流水查看 |
| 6 | `pages/creator/wallet` | 我的钱包 | 余额/提现 |
| 7 | `pages/purchases/index` | 我的已购 | 已购内容库 |

### 页面流程

```
【消费者】
首页 → [有声集市] → 专辑详情 → 试听 → 购买 → 支付 → 解锁 → 播放
                                                        └→ [我的已购]

【创作者】
我的 → [创作者中心] → [作品管理] → 选择作品 → 设定价格 → 上架
                    → [收益明细] → 查看流水
                    → [我的钱包] → 提现
```

### UI 关键页面说明

**有声集市首页**：分类标签栏（全部/有声书/知识/故事）+ 排序（销量/最新/价格）+ 双列瀑布流，每张卡片显示封面、标题、创作者、价格、评分。

**专辑购买页**：大封面 + 标题/创作者/评分/销量 + 章节列表（前3章可试听🎧，后面显示🔒）+ 底部固定购买按钮（显示价格）。

**创作者中心**：头像+名称 + 三指标卡片（累计销量/收益/余额）+ 收益趋势图 + 快捷入口。

---

## 六、功能开发清单

| ID | 功能 | 优先级 |
|----|------|--------|
| 1 | 数据库模型扩展 + Prisma 迁移 | 🔴 P0 |
| 2 | 有声集市列表 API + 前端页面 | 🔴 P0 |
| 3 | 专辑详情 API + 购买页 | 🔴 P0 |
| 4 | 购买流程 + 支付对接 | 🔴 P0 |
| 5 | 内容鉴权中间件 | 🔴 P0 |
| 6 | 我的已购页面 | 🟡 P1 |
| 7 | 创作者激活 | 🟡 P1 |
| 8 | 作品管理（上架/下架/定价） | 🟢 P2 |
| 9 | 创作者仪表盘 | 🟢 P2 |
| 10 | 收益明细 + 钱包 + 提现 | 🟢 P2 |
| 11 | 首页集市入口 + 推荐卡片 | 🟡 P1 |

**预估总工时：约 36 小时（4.5 个工作日）**

---

## 七、关键决策待确认

| # | 决策项 | 建议 | 
|---|--------|------|
| 1 | 分账比例 | 平台 20% / 创作者 79% / 支付费 1% |
| 2 | 结算周期 | T+7 |
| 3 | 最低提现额 | ¥10 |
| 4 | 试听章节数 | 前 3 章 |
| 5 | 定价范围 | ¥0.99 ~ ¥99.99（整张一口价） |
| 6 | 创作者门槛 | 完成至少 1 本书籍音频生成 |
| 7 | 内容审核 | 初期不上审核，后续接入 |

---

## 八、关键原则

- 会员订阅（生成配额）和内容购买（收听权限）**两套独立体系**
- 已购内容**永久有效**，不受会员到期影响
- 创作者**始终免费**访问自己的作品
- 复用现有支付网关（支付宝/微信）
- 下架不影响已购买用户

---

## 九、设计评审补充（2026-05-21）

> 以下为评审后发现的必须修复项和优化建议。

### 🔴 必须修复

#### 9.1 金额字段改用 Decimal

所有金额字段从 `Float` 改为 `Decimal`，与现有 `Order.amount` 保持一致，避免浮点精度问题。

```prisma
// 修改前
albumPrice    Float    @default(0.00)

// 修改后
albumPrice    Decimal  @default(0.00) @db.Decimal(10, 2)
```

**涉及字段**：AlbumMarketListing.price / totalRevenue、ContentPurchase.amount / platformFee / creatorShare / platformShare、CreatorEarning.amount、CreatorWallet.balance / totalEarned / totalWithdrawn / frozenBalance、WithdrawRecord.amount

#### 9.2 音频文件防盗链

核心问题：当前 `audioUrl` 是静态路径（如 `/public/videos/xxx.mp3`），购买者拿到 URL 可直接分享。

**方案：签名 URL + 音频代理端点**

```
改造前：前端直接播放 /public/videos/chapter1.mp3
改造后：前端请求 /api/marketplace/audio/:chapterId?token=xxx

流程：
1. 前端请求音频播放地址
2. 后端验证用户权限（所有者/已购买/试听章节）
3. 生成带签名的临时 URL（有效期 2 小时）
4. 返回签名 URL，前端用签名 URL 播放
5. 过期后需重新请求
```

```typescript
// 新增 API
GET /api/marketplace/audio/:chapterId
  → 鉴权 → 生成签名 URL → 302 重定向到签名 URL

// 签名 URL 格式
/protected/audio/chapter1.mp3?sign=abc123&expires=1716288000

// Nginx 层验证签名
location /protected/audio/ {
  secure_link $arg_sign;
  secure_link_md5 "$uri $arg_expires $secret";
  if ($secure_link = "") { return 403; }
  if ($secure_link = "0") { return 410; }  # expired
  alias /path/to/audio/;
}
```

#### 9.3 重复购买防护

```prisma
// ContentPurchase 联合唯一约束
model ContentPurchase {
  // ... 现有字段 ...

  @@unique([buyerId, bookId])  // 每人每本书只能购买一次
}
```

同时购买操作需用数据库事务 + 乐观锁：

```typescript
async function purchaseAlbum(buyerId: number, listingId: number) {
  return await prisma.$transaction(async (tx) => {
    // 1. 检查是否已购买（防重复）
    const existing = await tx.contentPurchase.findFirst({
      where: { buyerId, bookId: listing.bookId, status: 'success' }
    });
    if (existing) throw new Error('已购买该专辑');

    // 2. 创建订单 + 扣款 + 分账（原子操作）
    // ...
  });
}
```

#### 9.4 上架前置校验

```typescript
// 上架前必须校验
async function validateBeforeListing(bookId: number) {
  const book = await prisma.book.findUnique({
    where: { id: bookId },
    include: { chapters: { where: { level: 1 } } }
  });

  const errors: string[] = [];

  if (!book) errors.push('书籍不存在');
  if (book.progress < 100) errors.push('书籍内容尚未生成完成');
  if (book.chapters.length === 0) errors.push('书籍没有任何章节');

  // 检查至少有 1 个章节已生成音频
  const chaptersWithAudio = book.chapters.filter(c => c.audioUrl);
  if (chaptersWithAudio.length === 0) errors.push('书籍没有任何音频内容');

  // 检查是否已公开
  const publicChapters = book.chapters.filter(c => c.isPublic);
  if (publicChapters.length === 0) errors.push('请先将至少 1 个章节设为公开');

  return { valid: errors.length === 0, errors };
}
```

### 🟡 重要补充

#### 9.5 AlbumMarketListing 新增 category 字段

```prisma
model AlbumMarketListing {
  // ... 现有字段 ...
  category      String   @default("有声书")  // '有声书' | '知识' | '故事' | '其他'

  @@index([isActive, category])
}
```

#### 9.6 评分系统补全

```prisma
// 新增：内容评分模型
model ContentRating {
  id        Int      @id @default(autoincrement())
  userId    Int
  listingId Int
  rating    Int                      // 1-5 星
  review    String?  @db.Text        // 评价文字
  createdAt DateTime @default(now())

  listing   AlbumMarketListing @relation(fields: [listingId], references: [id])
  user      User               @relation(fields: [userId], references: [id])

  @@unique([userId, listingId])  // 每人只能评一次
  @@index([listingId])
}
```

新增 API：
```
POST /api/marketplace/albums/:id/rate   → 提交评分（购买后才能评）
GET  /api/marketplace/albums/:id/reviews → 获取评价列表
```

#### 9.7 T+7 结算定时任务

```typescript
// server/src/modules/marketplace/settlement.job.ts

/**
 * 每日凌晨执行：扫描购买后超过 7 天的 pending 收益，自动结算
 */
export async function settleEarnings() {
  const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);

  // 找到所有待结算的收益
  const pendingEarnings = await prisma.creatorEarning.findMany({
    where: {
      status: 'pending',
      createdAt: { lte: sevenDaysAgo }
    }
  });

  for (const earning of pendingEarnings) {
    await prisma.$transaction(async (tx) => {
      // 1. 更新收益状态为 settled
      await tx.creatorEarning.update({
        where: { id: earning.id },
        data: {
          status: 'settled',
          settleDate: new Date()
        }
      });

      // 2. 从冻结余额转入可提现余额
      await tx.creatorWallet.update({
        where: { userId: earning.creatorId },
        data: {
          frozenBalance: { decrement: earning.amount },
          balance: { increment: earning.amount },
          totalEarned: { increment: earning.amount }
        }
      });
    });
  }

  console.log(`[Settlement] Settled ${pendingEarnings.length} earnings`);
}

// 注册为定时任务（node-cron）
// cron.schedule('0 2 * * *', settleEarnings);  // 每天凌晨 2 点
```

#### 9.8 内容鉴权中间件（简化版）

只有整张专辑一种购买方式，鉴权逻辑清晰：

```typescript
export async function requireContentAccess(ctx, next) {
  const userId = ctx.state.user?.id;
  const bookId = Number(ctx.params.bookId);
  const chapterId = Number(ctx.params.chapterId);

  // 1. 创作者本人 → 放行
  const book = await prisma.book.findUnique({ where: { id: bookId } });
  if (book?.userId === userId) return next();

  // 2. 已购买 → 放行
  const purchase = await prisma.contentPurchase.findFirst({
    where: { buyerId: userId, bookId, status: 'success' }
  });
  if (purchase) return next();

  // 3. 试听章节 → 放行
  const listing = await prisma.albumMarketListing.findUnique({
    where: { bookId, isActive: true }
  });
  if (listing) {
    const chapters = await prisma.bookChapter.findMany({
      where: { bookId, level: 1 }, orderBy: { number: 'asc' }
    });
    const idx = chapters.findIndex(c => c.id === chapterId);
    if (idx >= 0 && idx < listing.previewChapterCount) return next();
  }

  // 4. 拒绝
  ctx.status = 402;
  ctx.body = {
    code: 402,
    message: '此内容需要购买后才能收听',
    needPurchase: true,
    listingId: listing?.id
  };
}
```

#### 9.9 专辑更新策略

```
策略：已购用户永久享受当前及新增内容

实现：
1. 创作者新增章节后，如专辑已上架，新章节自动加入
2. 已购买用户无需额外付费即可收听新章节
3. 价格调整不影响已购买用户
```

#### 9.10 提现安全增强

```typescript
// 提现前验证
async function validateWithdrawal(userId: number, amount: number, account: string) {
  // 1. 验证用户身份（已绑定手机号）
  const user = await prisma.user.findUnique({ where: { id: userId } });
  if (!user?.phone) throw new Error('请先绑定手机号');

  // 2. 验证码校验（前端获取验证码后传入）
  // request.body.verifyCode → 校验短信验证码

  // 3. 金额校验
  const wallet = await prisma.creatorWallet.findUnique({ where: { userId } });
  if (amount > wallet.balance) throw new Error('余额不足');
  if (amount < 10) throw new Error('最低提现金额 ¥10');

  // 4. 账号校验（支付宝：手机号或邮箱格式；微信：openid）
  if (method === 'alipay' && !/^(1\d{10}|[\w.]+@\w+\.\w+)$/.test(account)) {
    throw new Error('支付宝账号格式不正确');
  }

  // 5. 频率限制（每天最多 1 次提现）
  const todayWithdraw = await prisma.withdrawRecord.count({
    where: { userId, createdAt: { gte: new Date(new Date().setHours(0,0,0,0)) } }
  });
  if (todayWithdraw >= 1) throw new Error('每天最多提现 1 次');
}
```

### 🟢 优化建议

#### 9.11 集市搜索 API

```
GET /api/marketplace/search?q=三体&category=有声书&sort=sales
```

```prisma
// AlbumMarketListing 新增全文索引
model AlbumMarketListing {
  // ... 现有字段 ...
  @@index([isActive, category])
  // MySQL 全文索引需要在 SQL 层添加
}
```

```typescript
// 搜索逻辑（先用 LIKE，后期可接 Elasticsearch）
async function searchMarketplace(keyword: string, category?: string) {
  return prisma.albumMarketListing.findMany({
    where: {
      isActive: true,
      ...(category ? { category } : {}),
      OR: [
        { title: { contains: keyword } },
        { description: { contains: keyword } },
        { book: { title: { contains: keyword } } }
      ]
    },
    orderBy: { totalSales: 'desc' }
  });
}
```

#### 9.12 创作者公开主页

```
GET /api/marketplace/creators/:creatorId
  → 返回：创作者信息 + 作品列表 + 总销量 + 总评分
```

用途：消费者点击创作者头像，进入创作者主页查看所有作品。

#### 9.13 分享传播机制

```
POST /api/marketplace/albums/:id/share
  → 生成分享链接 + 海报图片

GET /api/marketplace/share/:shareCode
  → 解析分享链接，跳转到专辑详情页

追踪：分享来源统计（谁分享的 → 谁通过分享购买 → 分享者奖励）
```

#### 9.14 退款流程

```
策略：
· 购买后 24 小时内可申请退款
· 已收听超过 50% 内容不可退款
· 退款后立即撤销访问权限
· 退款金额从创作者冻结余额扣除

API：
POST /api/marketplace/purchases/:orderNo/refund
  → 验证条件 → 创建退款 → 撤销权限 → 通知创作者
```

---

## 十、修订后的完整数据模型

```prisma
model AlbumMarketListing {
  id                   Int      @id @default(autoincrement())
  bookId               Int      @unique
  creatorId            Int
  title                String
  description          String?  @db.Text
  coverUrl             String?
  category             String   @default("有声书")

  price                Decimal  @default(0.00) @db.Decimal(10, 2)   // 售价（整张一口价）

  isActive             Boolean  @default(false)
  isRecommended        Boolean  @default(false)

  totalSales           Int      @default(0)
  totalRevenue         Decimal  @default(0.00) @db.Decimal(12, 2)
  avgRating            Float    @default(0.00)

  previewChapterCount  Int      @default(3)

  createdAt            DateTime @default(now())
  updatedAt            DateTime @updatedAt

  book                 Book     @relation(fields: [bookId], references: [id])
  ratings              ContentRating[]

  @@index([isActive, category])
  @@index([isActive, totalSales])
  @@index([creatorId])
}

model ContentPurchase {
  id            Int      @id @default(autoincrement())
  buyerId       Int
  listingId     Int
  bookId        Int
  orderNo       String   @unique
  amount        Decimal  @db.Decimal(10, 2)
  status        String   @default("success")

  paymentMethod String?
  paymentId     String?

  platformFee   Decimal  @default(0) @db.Decimal(10, 2)
  creatorShare  Decimal  @default(0) @db.Decimal(10, 2)
  platformShare Decimal  @default(0) @db.Decimal(10, 2)

  createdAt     DateTime @default(now())

  listing       AlbumMarketListing @relation(fields: [listingId], references: [id])
  buyer         User               @relation("buyer", fields: [buyerId], references: [id])

  @@unique([buyerId, bookId])
  @@index([buyerId, createdAt])
  @@index([listingId])
  @@index([orderNo])
}

model ContentRating {
  id        Int      @id @default(autoincrement())
  userId    Int
  listingId Int
  rating    Int                      // 1-5
  review    String?  @db.Text
  createdAt DateTime @default(now())

  listing   AlbumMarketListing @relation(fields: [listingId], references: [id])
  user      User               @relation(fields: [userId], references: [id])

  @@unique([userId, listingId])
  @@index([listingId])
}

model CreatorEarning {
  id            Int      @id @default(autoincrement())
  creatorId     Int
  purchaseId    Int
  bookId        Int
  amount        Decimal  @db.Decimal(10, 2)    // Float→Decimal
  type          String
  status        String   @default("pending")

  settleDate    DateTime?
  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt

  creator       User             @relation(fields: [creatorId], references: [id])
  purchase      ContentPurchase  @relation(fields: [purchaseId], references: [id])

  @@index([creatorId, status])
  @@index([creatorId, createdAt])
}

model CreatorWallet {
  id              Int     @id @default(autoincrement())
  userId          Int     @unique
  balance         Decimal @default(0.00) @db.Decimal(12, 2)    // Float→Decimal
  totalEarned     Decimal @default(0.00) @db.Decimal(12, 2)
  totalWithdrawn  Decimal @default(0.00) @db.Decimal(12, 2)
  frozenBalance   Decimal @default(0.00) @db.Decimal(12, 2)

  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt

  user            User   @relation(fields: [userId], references: [id])
}

model WithdrawRecord {
  id          Int      @id @default(autoincrement())
  userId      Int
  amount      Decimal  @db.Decimal(10, 2)    // Float→Decimal
  method      String   @default("alipay")
  account     String
  status      String   @default("pending")
  remark      String?

  approvedAt  DateTime?
  paidAt      DateTime?

  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  user        User     @relation(fields: [userId], references: [id])

  @@index([userId, status])
  @@index([status, createdAt])
}
```

---

## 十一、修订后的 API 补充

| 方法 | 路由 | 功能 | 来源 |
|------|------|------|------|
| GET | `/api/marketplace/search` | 集市搜索 | 9.11 |
| GET | `/api/marketplace/creators/:id` | 创作者主页 | 9.12 |
| GET | `/api/marketplace/audio/:chapterId` | 音频鉴权代理（防盗链） | 9.2 |
| POST | `/api/marketplace/albums/:id/rate` | 提交评分 | 9.6 |
| GET | `/api/marketplace/albums/:id/reviews` | 评价列表 | 9.6 |
| POST | `/api/marketplace/purchases/:orderNo/refund` | 申请退款 | 9.14 |
| POST | `/api/marketplace/albums/:id/share` | 生成分享 | 9.13 |

---

## 十二、修订后功能开发清单

| ID | 功能 | 优先级 | 备注 |
|----|------|--------|------|
| 1 | 数据库模型扩展（含 Decimal 修正） | 🔴 P0 | 评审 9.1 |
| 2 | 音频防盗链 — 签名 URL + 代理端点 | 🔴 P0 | 评审 9.2 |
| 3 | 有声集市列表 API + 前端 | 🔴 P0 | 含 category 筛选 |
| 4 | 专辑详情 API + 购买页 | 🔴 P0 | |
| 5 | 购买流程（含防重复 + 事务安全） | 🔴 P0 | 评审 9.3 |
| 6 | 内容鉴权中间件 | 🔴 P0 | 评审 9.8 |
| 7 | 上架前置校验 | 🔴 P0 | 评审 9.4 |
| 8 | 我的已购页面 | 🟡 P1 | |
| 9 | 创作者激活 | 🟡 P1 | |
| 10 | 作品管理（上架/下架/定价） | 🟡 P1 | |
| 11 | T+7 结算定时任务 | 🟡 P1 | 评审 9.7 |
| 12 | 集市搜索 API | 🟡 P1 | 评审 9.11 |
| 13 | 评分系统 | 🟡 P1 | 评审 9.6 |
| 14 | 首页集市入口 | 🟡 P1 | |
| 15 | 创作者仪表盘 | 🟢 P2 | |
| 16 | 收益明细 + 钱包（含提现安全） | 🟢 P2 | 评审 9.10 |
| 17 | 创作者公开主页 | 🟢 P2 | 评审 9.12 |
| 18 | 分享传播 | 🟢 P2 | 评审 9.13 |
| 19 | 退款流程 | 🟢 P2 | 评审 9.14 |

**修订后预估：约 48 小时（6 个工作日）**
