二期-音频内容付费收听系统设计.md 26 KB

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

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


一、业务流程全景

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

二、数据库设计

2.1 新增模型

// 有声集市 - 专辑上架定价
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 现有模型修改

// 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 内容鉴权中间件

// 核心逻辑:整张专辑一口价,购买后解锁全部章节
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 保持一致,避免浮点精度问题。

// 修改前
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. 过期后需重新请求
// 新增 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 重复购买防护

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

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

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

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 上架前置校验

// 上架前必须校验
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 字段

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

  @@index([isActive, category])
}

9.6 评分系统补全

// 新增:内容评分模型
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 结算定时任务

// 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 内容鉴权中间件(简化版)

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

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 提现安全增强

// 提现前验证
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
// AlbumMarketListing 新增全文索引
model AlbumMarketListing {
  // ... 现有字段 ...
  @@index([isActive, category])
  // MySQL 全文索引需要在 SQL 层添加
}
// 搜索逻辑(先用 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
  → 验证条件 → 创建退款 → 撤销权限 → 通知创作者

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

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 个工作日)