数据访问层.md 19 KB

数据访问层

本文引用的文件

  • schema.prisma
  • models/index.ts
  • subscription.service.ts
  • subscription.controller.ts
  • book-generator.service.ts
  • book-generator.controller.ts
  • tts.service.ts
  • tts.controller.ts
  • player.service.ts
  • migrate-genstage.ts
  • migration_lock.toml

目录

  1. 简介
  2. 项目结构
  3. 核心组件
  4. 架构总览
  5. 详细组件分析
  6. 依赖分析
  7. 性能考虑
  8. 故障排查指南
  9. 结论
  10. 附录

简介

本文件面向AI有声书生成平台的数据访问层,围绕Prisma ORM的数据模型设计与DAO模式实现展开,覆盖用户、书籍、章节、音频、订阅等核心实体及其关系映射;同时阐述CRUD封装、事务与连接池、迁移策略、查询优化、索引设计、缓存策略,并提供数据模型ER图与最佳实践建议。

项目结构

数据访问层主要由以下部分组成:

  • 数据模型定义:基于Prisma Schema的实体与关系
  • DAO与服务层:封装CRUD、业务逻辑与配额校验
  • 控制器层:对外暴露REST接口
  • 迁移与同步:历史数据迁移脚本与迁移锁文件
  • 连接管理:Prisma客户端连接与生命周期

    graph TB
    subgraph "数据访问层"
    PRISMA["Prisma 客户端<br/>连接/查询"]
    MODELS["数据模型<br/>schema.prisma"]
    DAO_SUB["订阅服务<br/>subscription.service.ts"]
    DAO_TTS["TTS服务<br/>tts.service.ts"]
    DAO_PLAYER["播放服务<br/>player.service.ts"]
    DAO_BOOK["书籍生成服务<br/>book-generator.service.ts"]
    end
    subgraph "控制器层"
    CTRL_SUB["订阅控制器<br/>subscription.controller.ts"]
    CTRL_TTS["TTS控制器<br/>tts.controller.ts"]
    CTRL_BOOK["书籍生成控制器<br/>book-generator.controller.ts"]
    end
    MODELS --> PRISMA
    DAO_SUB --> PRISMA
    DAO_TTS --> PRISMA
    DAO_PLAYER --> PRISMA
    DAO_BOOK --> PRISMA
    CTRL_SUB --> DAO_SUB
    CTRL_TTS --> DAO_TTS
    CTRL_BOOK --> DAO_BOOK
    

图表来源

  • schema.prisma
  • models/index.ts
  • subscription.service.ts
  • tts.service.ts
  • player.service.ts
  • book-generator.service.ts
  • subscription.controller.ts
  • tts.controller.ts
  • book-generator.controller.ts

章节来源

  • schema.prisma
  • models/index.ts

核心组件

  • Prisma 客户端与连接管理:集中导出PrismaClient实例与连接方法,确保全局唯一连接与统一生命周期管理。
  • 订阅与配额服务:封装套餐查询、用户订阅状态、Token余额与使用记录、音频时长配额与计费、书籍生成配额等。
  • TTS 服务:封装音频生成流程、状态查询、LRC歌词生成、章节音频更新、音色与提供商选择。
  • 播放服务:封装播放进度读写、章节音频合并(自动聚合子小节音频)、最近播放记录。
  • 书籍生成服务:批量生成编排器,按步骤推进内容生成、音频生成、音频合并、视频生成与合并,并支持取消与进度推送。
  • 控制器:REST接口封装,参数校验、鉴权中间件、调用服务层并返回标准化响应。

章节来源

  • models/index.ts
  • subscription.service.ts
  • tts.service.ts
  • player.service.ts
  • book-generator.service.ts
  • subscription.controller.ts
  • tts.controller.ts
  • book-generator.controller.ts

架构总览

数据访问层采用“控制器-服务-DAO-Prisma”的分层架构,服务层负责业务规则与数据一致性,DAO层负责具体数据操作,Prisma负责ORM映射与SQL生成。

sequenceDiagram
participant C as "客户端"
participant Ctrl as "控制器"
participant Svc as "服务层"
participant Dao as "DAO/Prisma"
participant DB as "MySQL"
C->>Ctrl : "HTTP 请求"
Ctrl->>Svc : "调用业务方法"
Svc->>Dao : "执行数据操作"
Dao->>DB : "执行查询/更新"
DB-->>Dao : "返回结果"
Dao-->>Svc : "返回实体/统计"
Svc-->>Ctrl : "业务结果"
Ctrl-->>C : "JSON 响应"

图表来源

  • subscription.controller.ts
  • subscription.service.ts
  • tts.controller.ts
  • tts.service.ts
  • book-generator.controller.ts
  • book-generator.service.ts
  • player.service.ts
  • schema.prisma

详细组件分析

数据模型与关系映射(ER图)

下图为基于Prisma Schema的核心实体与关系的ER图,涵盖一对一、一对多、多对多关系及关键索引。

erDiagram
USER {
int id PK
string phone UK
string openid UK
string nickname
string avatar
int memberLevel
datetime memberExpireAt
int dailyUsage
string lastUsageDate
datetime createdAt
datetime updatedAt
int usedAudioMinutes
datetime subscriptionResetDate
}
SUBSCRIPTION_PLAN {
int id PK
string name
int level
decimal priceMonthly
decimal priceYearly
text description
text features
boolean isRecommended
boolean isActive
int sortOrder
int dailyGenerations
int perGenerationLimit
int monthlyTokens
int monthlyMinutes
int yearlyTokens
int voiceOptions
string audioQuality
boolean apiAccess
boolean batchProcessing
boolean teamManagement
boolean overageEnabled
decimal overagePrice
datetime createdAt
datetime updatedAt
}
SUBSCRIPTION {
int id PK
int userId FK
int planId FK
datetime startDate
datetime endDate
string status
boolean autoRenew
datetime createdAt
datetime updatedAt
}
TOKEN_BALANCE {
int id PK
int userId UK
int totalTokens
int usedTokens
datetime resetDate
datetime createdAt
datetime updatedAt
}
TOKEN_USAGE {
int id PK
int userId FK
string type
int amount
int contentLength
int orderId
text description
datetime createdAt
}
ORDER {
int id PK
int userId FK
string orderNo UK
int planId
string productType
decimal amount
string status
string paymentMethod
string paymentId
datetime paidAt
datetime createdAt
datetime updatedAt
}
BOOK {
int id PK
int userId
string title
string subtitle
text description
string coverUrl
string targetAudience
string style
string bookScale
int totalChapters
int estimatedWords
int progress
boolean isPublished
longtext outlineJson
text foreword
text afterword
text errorMsg
datetime createdAt
datetime updatedAt
string failedStage
string genStage
text bookAnalysis
}
BOOK_CHAPTER {
int id PK
int bookId FK
int parentId
int level
int number
string title
text summary
text keyPoints
int estimatedWords
longtext content
int wordCount
text contentError
datetime generatedAt
text audioUrl
int audioDuration
text videoUrl
int videoDuration
boolean isPublic
string genStage
text lrcLyrics
}
PLAY_RECORD {
int id PK
int userId FK
int chapterId FK
float progress
float duration
datetime createdAt
datetime updatedAt
}
COMMENT {
int id PK
int userId FK
int chapterId FK
text content
int rating
datetime createdAt
}
FAVORITE {
int id PK
int userId FK
int bookId FK
datetime createdAt
}
AUDIO_RECORD {
int id PK
int userId
string audioId UK
string title
longtext text
int wordCount
string voiceId
string voiceParams
text audioUrl
int audioDuration
int audioSize
string status
text errorMsg
datetime createdAt
datetime updatedAt
}
VIDEO_PROJECT {
int id PK
int userId
string title
text description
string coverUrl
longtext configJson
string outputUrl
int duration
int fileSize
int bookId
int chapterId
string status
int progress
text errorMsg
datetime createdAt
datetime updatedAt
}
USER_PREFERENCE {
int id PK
int userId UK
float playSpeed
string quality
string theme
string defaultVoiceId
int defaultVolume
boolean autoPlayNext
boolean wifiOnlyDownload
datetime createdAt
datetime updatedAt
}
PLAYLIST {
int id PK
int userId FK
string name
text description
datetime createdAt
datetime updatedAt
}
PLAYLIST_ITEM {
int id PK
int playlistId FK
int chapterId
string audioId
int order
}
SIGN_RECORD {
int id PK
int userId FK
datetime createdAt
}
DRAFT {
int id PK
int userId FK
string type
text title
longtext content
text metadata
datetime autoSavedAt
datetime createdAt
datetime updatedAt
}
NOTIFICATION {
string id PK
int userId
string title
text content
boolean isRead
datetime createdAt
}
HOT_SEARCH {
int id PK
string keyword
int count
int sort
datetime createdAt
datetime updatedAt
}
SEARCH_HISTORY {
int id PK
int userId FK
string keyword
datetime createdAt
}
FEEDBACK {
string id PK
string type
text title
text content
text contact
text screenshotUrls
string status
datetime createdAt
datetime updatedAt
}
PLATFORM_ACCOUNT {
int id PK
int userId FK
string platform
string nickname
string avatar
text cookies
text headers
boolean isValid
datetime expireTime
datetime createdAt
datetime updatedAt
}
PUBLISH_TASK {
int id PK
int userId FK
int videoProjectId FK
string platform
text title
text description
text tags
text coverUrl
text videoUrl
string status
text errorMsg
text publishedUrl
datetime createdAt
datetime updatedAt
}
VIDEO_MATERIAL {
int id PK
int userId
string type
string name
text url
string thumbnail
text tags
string category
int duration
int size
int width
int height
datetime createdAt
datetime updatedAt
}
USER ||--o{ ORDER : "拥有"
USER ||--o{ SUBSCRIPTION : "拥有"
USER ||--o{ TOKEN_BALANCE : "拥有"
USER ||--o{ TOKEN_USAGE : "产生"
USER ||--o{ BOOK : "创作"
USER ||--o{ COMMENT : "发表"
USER ||--o{ FAVORITE : "收藏"
USER ||--o{ PLAY_RECORD : "播放"
USER ||--o{ AUDIO_RECORD : "生成"
USER ||--o{ VIDEO_PROJECT : "创建"
USER ||--o{ PLAYLIST : "创建"
USER ||--o{ SIGN_RECORD : "签到"
USER ||--o{ DRAFT : "保存"
USER ||--o{ PLATFORM_ACCOUNT : "绑定"
USER ||--o{ PUBLISH_TASK : "发布"
SUBSCRIPTION_PLAN ||--o{ SUBSCRIPTION : "被订阅"
SUBSCRIPTION ||--o{ ORDER : "购买"
BOOK ||--o{ BOOK_CHAPTER : "包含"
BOOK ||--o{ VIDEO_PROJECT : "关联"
BOOK_CHAPTER ||--o{ COMMENT : "被评论"
BOOK_CHAPTER ||--o{ PLAY_RECORD : "被播放"
BOOK_CHAPTER ||--o{ PLAYLIST_ITEM : "被加入"
BOOK_CHAPTER ||--o{ VIDEO_PROJECT : "关联"
PLAYLIST ||--o{ PLAYLIST_ITEM : "包含"

图表来源

  • schema.prisma

章节来源

  • schema.prisma

DAO模式与CRUD封装

  • 订阅与配额:提供套餐查询、用户订阅查询、Token余额与使用记录、音频时长配额检查与扣减、书籍生成配额评估等。
  • TTS:提供音频生成、状态查询、LRC歌词生成、章节音频更新、音色与提供商选择。
  • 播放:提供播放进度读取与更新、章节音频合并(自动聚合子小节音频)、最近播放记录。
  • 书籍生成:提供批量生成编排器,按步骤推进并支持取消与进度推送。

章节来源

  • subscription.service.ts
  • tts.service.ts
  • player.service.ts
  • book-generator.service.ts

事务管理与连接池

  • Prisma客户端默认启用连接池与并发请求处理,通过统一的PrismaClient实例管理连接生命周期。
  • 在服务层中,涉及多步写入的场景(如消费Token并记录使用)建议在单事务中执行,避免并发竞态导致的余额不一致。

章节来源

  • models/index.ts
  • subscription.service.ts

数据库迁移策略

  • 迁移锁:使用migration_lock.toml锁定迁移执行,防止并发迁移。
  • 历史数据迁移:提供从旧字段到新genStage字段的迁移脚本,自动推断书籍与章节的生成阶段。
  • 版本管理:Prisma通过schema.prisma与migrations目录维护版本演进,建议每次变更均生成新迁移并审阅SQL。

章节来源

  • migration_lock.toml
  • migrate-genstage.ts
  • schema.prisma

查询优化与索引设计

  • 常用查询路径与索引:
    • 用户:phone、openid、id
    • 订单:userId、orderNo、status、planId
    • 播放记录:userId、chapterId、唯一组合(userId, chapterId)
    • 书籍:userId、createdAt
    • 章节:bookId、bookId+parentId、bookId+level、唯一组合(bookId, parentId, level, number)
    • Token使用:userId、type、orderId
    • 视频项目:userId、status、bookId、chapterId
    • 搜索:keyword、sort、count
    • 订阅:userId、status、endDate
    • 音频:userId、audioId、bookId
  • 建议:
    • 为高频过滤字段建立复合索引,避免全表扫描。
    • 对范围查询(createdAt、endDate)与排序(orderBy)字段建立合适索引。
    • 对唯一约束字段(如orderNo、audioId、openid)保持唯一性以提升查询效率。

章节来源

  • schema.prisma

缓存策略

  • 读多写少的静态数据(如套餐配置、音色列表)可在应用层缓存,减少数据库压力。
  • 对热点查询(如用户配额、Token余额)可引入Redis缓存,设置合理TTL与失效策略。
  • 对大字段(如longtext)建议延迟加载或分表存储,避免影响主表查询性能。

[本节为通用建议,不直接分析具体文件]

API工作流示例(TTS生成)

sequenceDiagram
participant Client as "客户端"
participant Ctrl as "TTS控制器"
participant Svc as "TTS服务"
participant SubSvc as "订阅服务"
participant Prisma as "Prisma"
participant Storage as "存储服务"
Client->>Ctrl : "POST /tts/generate"
Ctrl->>SubSvc : "checkAudioQuota(userId, textLength)"
SubSvc->>Prisma : "查询用户与配额"
Prisma-->>SubSvc : "返回配额信息"
SubSvc-->>Ctrl : "允许/拒绝"
Ctrl->>Svc : "generateAudio(userId, text, voiceId, params)"
Svc->>Prisma : "创建AudioRecord"
Svc->>Storage : "上传音频"
Storage-->>Svc : "返回URL"
Svc->>Prisma : "更新章节音频/记录"
Svc-->>Ctrl : "返回audioId与URL"
Ctrl-->>Client : "任务已创建"

图表来源

  • tts.controller.ts
  • tts.service.ts
  • subscription.service.ts
  • schema.prisma

依赖分析

  • 控制器依赖服务层,服务层依赖Prisma DAO,DAO依赖Prisma客户端。
  • 订阅服务与TTS服务存在跨模块调用(配额检查与扣减)。
  • 书籍生成服务与播放服务、视频生成服务存在间接耦合(章节音频合并、视频项目关联)。

    graph LR
    CTRL_SUB["subscription.controller.ts"] --> SVC_SUB["subscription.service.ts"]
    CTRL_TTS["tts.controller.ts"] --> SVC_TTS["tts.service.ts"]
    CTRL_BOOK["book-generator.controller.ts"] --> SVC_BOOK["book-generator.service.ts"]
    SVC_SUB --> PRISMA["Prisma Client"]
    SVC_TTS --> PRISMA
    SVC_BOOK --> PRISMA
    SVC_TTS --> SVC_SUB
    

图表来源

  • subscription.controller.ts
  • subscription.service.ts
  • tts.controller.ts
  • tts.service.ts
  • book-generator.controller.ts
  • book-generator.service.ts
  • models/index.ts

章节来源

  • subscription.controller.ts
  • tts.controller.ts
  • book-generator.controller.ts
  • subscription.service.ts
  • tts.service.ts
  • book-generator.service.ts
  • models/index.ts

性能考虑

  • 连接池:合理配置Prisma连接池大小,避免高并发下的连接争用。
  • 查询优化:为高频查询字段建立索引,避免SELECT *,使用投影查询。
  • 写入优化:批量插入/更新时使用事务,减少往返次数。
  • 缓存:热点数据缓存、读写分离、CDN加速静态资源。
  • IO优化:音频/视频上传使用对象存储,本地仅保留必要元数据。

[本节为通用建议,不直接分析具体文件]

故障排查指南

  • 连接失败:检查DATABASE_URL环境变量与网络连通性,确认Prisma连接方法调用。
  • 额度不足:检查用户订阅状态与Token余额,确认配额计算逻辑与重置日期。
  • 生成失败:检查TTS提供商配置、分段策略、存储上传失败回退逻辑。
  • 进度异常:检查播放记录唯一键与upsert逻辑,确认章节音频合并条件。

章节来源

  • models/index.ts
  • subscription.service.ts
  • tts.service.ts
  • player.service.ts

结论

本数据访问层以Prisma为核心,结合服务层封装与控制器接口,实现了从用户、书籍、章节、音频到订阅与配额的全链路数据管理。通过合理的索引设计、迁移策略与缓存策略,能够满足高并发与复杂业务场景的需求。建议持续完善事务边界、监控与告警体系,保障数据一致性与系统稳定性。

附录

  • 数据模型字段与索引参考:见“数据模型与关系映射”章节。
  • 迁移脚本与锁文件:见“数据库迁移策略”章节。
  • API接口与调用关系:见“架构总览”与“详细组件分析”。