Procházet zdrojové kódy

feat: 功能1开发 - 播放记录功能 | 状态:init→db-checking(等待MySQL)

MyFramework User před 5 měsíci
rodič
revize
3646d9c300

+ 445 - 0
.codebuddy/rules/harness.mdc

@@ -0,0 +1,445 @@
+---
+description: 
+alwaysApply: true
+enabled: true
+updatedAt: 2026-04-04T00:38:45.522Z
+provider: 
+---
+
+# 长时运行代理规则
+
+## 核心原则
+
+1. **双 Agent 架构**:Initializer Agent(仅首次)+ Coding Agent(后续所有会话)
+2. **外部持久化**:所有进度必须写入文件,不依赖 AI 记忆
+3. **增量开发**:每次会话只完成 1 个功能
+4. **状态流转**:功能状态按流程逐步推进(start → 编译 → 运行 → 数据库 → 测试 → done)
+5. **自动执行**:AI 必须自动执行所有命令,禁止输出命令让用户手动执行
+6. **失败处理**:验证失败 → 记录状态 → 修复 → 重新执行该环节
+
+---
+
+## 功能状态定义
+
+### 状态流转图
+
+```
+init → start → compiling → running → db-checking → backend-testing → frontend-testing → done
+  ↓       ↓         ↓          ↓            ↓            ↓              ↓              ↓      ↓
+初始化  开始开发  编译中    运行中     数据库验证中   后端测试中    前端测试中      完成
+```
+
+### 状态说明
+
+| 状态 | 含义 | 下一步 |
+|------|------|--------|
+| `init` | 功能已创建,未开始 | → `start` |
+| `start` | 开始开发 | → `compiling` |
+| `compiling` | 编译中/编译失败 | 成功 → `running`,失败 → 修复 |
+| `running` | 运行中/运行失败 | 成功 → `db-checking`,失败 → 修复 |
+| `db-checking` | 数据库验证中/失败 | 成功 → `backend-testing`,失败 → 修复 |
+| `backend-testing` | 后端接口测试中/失败 | 成功 → `frontend-testing`,失败 → 修复 |
+| `frontend-testing` | 前端页面测试中/失败 | 成功 → `done`,失败 → 修复 |
+| `done` | 功能完成 | - |
+
+### 测试阶段说明
+
+**为什么测试要分开**:
+> - **后端测试**:使用 curl 测试 API 接口,验证业务逻辑
+> - **前端测试**:使用浏览器自动化测试用户交互,验证页面功能
+> 
+> **顺序要求**:
+> 1. 先测后端接口 → 确保 API 正常
+> 2. 再测前端页面 → 确保交互正常
+> 
+> **原因**:如果后端接口都有问题,前端测试没有意义
+
+---
+
+## Initializer Agent 规则
+
+### 职责(仅第一次运行)
+
+**核心原则:AI 必须自动执行所有初始化步骤,禁止让用户手动操作**
+
+1. **生成 `init.sh`**:安装依赖、初始化数据库、启动服务
+2. **生成 `claude-progress.txt`**:进度日志
+3. **生成 `feature_list.json`**:100-200+ 细粒度功能清单(JSON 格式)
+4. **初始化 Git**:
+   ```bash
+   git init
+   git add .
+   git commit -m "initial: 初始化项目"
+   git branch main
+   ```
+5. **自动执行初始化**(关键!禁止输出命令让用户执行):
+   ```bash
+   # 5.1 初始化数据库
+   mysql -u root -p123456 < database/init.sql
+   
+   # 5.2 安装后端依赖
+   cd backend && mvn install
+   
+   # 5.3 安装前端依赖(包括 Playwright)
+   cd ../frontend && npm install && npx playwright install chromium
+   
+   # 5.4 启动后端(后台运行)
+   cd backend && mvn spring-boot:run &
+   
+   # 5.5 启动前端(后台运行)
+   cd ../frontend && npm run dev &
+   
+   # 5.6 等待服务启动
+   sleep 15
+   
+   # 5.7 验证服务可用性
+   curl -f http://localhost:8000/health
+   curl -f http://localhost:8080
+   python db_check.py
+   ```
+
+### 禁止行为
+- ❌ 编写功能代码
+- ❌ 跳过任何工件生成
+- ❌ 使用占位符
+- ❌ **输出初始化命令让用户手动执行**
+- ❌ **说"启动方式"、"访问地址"等,而不实际执行**
+
+---
+
+## Coding Agent 规则
+
+### 标准流程(按顺序执行,禁止跳步)
+
+#### 步骤 1:读取状态
+```bash
+pwd
+cat claude-progress.txt
+cat feature_list.json
+git log --oneline -20
+```
+
+#### 步骤 2:选择功能
+- 从 `feature_list.json` 选择最小 id 的未完成功能
+- 查看当前状态(应该是 `init` 或 `start`)
+- **只做这一条**
+
+#### 步骤 3:更新状态为 `start`
+```bash
+# 修改 feature_list.json
+{
+  "id": X,
+  "status": "start",  # 更新为 start
+  "description": "功能描述"
+}
+
+# 记录日志
+echo "[时间戳] 开始功能 X:功能描述" >> claude-progress.txt
+```
+
+#### 步骤 4:开发功能
+- 编写代码
+
+#### 步骤 5:编译验证(更新状态为 `compiling`)
+```bash
+# 先更新状态
+{
+  "id": X,
+  "status": "compiling"  # 更新为 compiling
+}
+
+# 执行编译
+python -m py_compile backend/*.py
+# 或
+npm run build
+```
+
+**编译结果处理**:
+- ✅ 成功 → 更新状态为 `running`,进入步骤 6
+- ❌ 失败 → 保持 `compiling` 状态,记录错误,修复后重新编译
+
+#### 步骤 6:运行验证(更新状态为 `running`)
+```bash
+# 先更新状态
+{
+  "id": X,
+  "status": "running"  # 更新为 running
+}
+
+# 执行运行验证
+pkill -f "python.*main.py" || true
+pkill -f "npm.*dev" || true
+sleep 2
+./init.sh
+sleep 10
+
+curl -f http://localhost:8000/health
+curl -f http://localhost:8080
+```
+
+**运行结果处理**:
+- ✅ 成功 → 更新状态为 `db-checking`,进入步骤 7
+- ❌ 失败 → 保持 `running` 状态,记录错误,修复后重新运行
+
+#### 步骤 7:数据库验证(更新状态为 `db-checking`)
+```bash
+# 先更新状态
+{
+  "id": X,
+  "status": "db-checking"  # 更新为 db-checking
+}
+
+# 执行数据库验证
+python backend/db_check.py
+python -c "from backend.db import DB; db = DB(); db.test_connection()"
+```
+
+**数据库结果处理**:
+- ✅ 成功 → 更新状态为 `testing`,进入步骤 8
+- ❌ 失败 → 保持 `db-checking` 状态,记录错误,修复后重新验证
+
+#### 步骤 8:后端接口测试(更新状态为 `backend-testing`)
+```bash
+# 先更新状态
+{
+  "id": X,
+  "status": "backend-testing"  # 更新为 backend-testing
+}
+
+# 使用 curl 测试后端接口
+# 按照 feature_list.json 中 backend_test_steps 逐项测试
+curl -s http://localhost:8000/api/chats/new | jq '.status' | grep -q "success"
+curl -f http://localhost:8000/health
+```
+
+**后端测试要求**:
+> - 必须按照 backend_test_steps 逐项测试
+> - 使用 curl 测试 API 接口
+> - 验证接口返回状态码和数据格式
+
+**后端测试结果处理**:
+- ✅ 成功 → 更新状态为 `frontend-testing`,进入步骤 9
+- ❌ 失败 → 保持 `backend-testing` 状态,记录错误,修复后端接口后重新测试
+
+#### 步骤 9:前端页面测试(更新状态为 `frontend-testing`)
+```bash
+# 先更新状态
+{
+  "id": X,
+  "status": "frontend-testing"  # 更新为 frontend-testing
+}
+
+# 复制模板并修改
+cp .codebuddy/rules/frontend-test-template.js tests/test-frontend.js
+
+# 根据当前功能的 frontend_test_steps 修改测试内容
+# 然后执行浏览器自动化测试
+node tests/test-frontend.js
+```
+
+**前端测试要求**:
+> - 必须按照 frontend_test_steps 逐项测试
+> - 使用浏览器自动化(Playwright/Puppeteer)
+> - 模拟真实用户操作
+
+**前端测试结果处理**:
+- ✅ 成功 → 更新状态为 `done`,进入步骤 10
+- ❌ 失败 → 保持 `frontend-testing` 状态,记录错误,修复前端后重新测试
+
+#### 步骤 10:功能完成(更新状态为 `done`)
+```bash
+# 更新 feature_list.json
+{
+  "id": X,
+  "status": "done",  # 更新为 done
+  "passes": true
+}
+
+# 更新日志
+echo "[时间戳] 功能 X 完成 | 状态流转:start→compiling→running→db-checking→backend-testing→frontend-testing→done" >> claude-progress.txt
+
+# Git 提交
+git add .
+git commit -m "feat: 完成功能 X | 状态流转:start→compiling→running→db-checking→backend-testing→frontend-testing→done"
+```
+
+### 状态流转规则
+
+**核心原则**:
+> 状态必须逐步流转,不能跳跃
+> 
+> 每个环节失败 → 保持当前状态 → 修复 → 重新执行该环节
+> 
+> 只有该环节成功 → 才能更新为下一个状态
+
+**状态流转示例**:
+```
+功能 1: init → start → compiling → running → db-checking → testing → done ✅
+功能 2: init → start → compiling → (失败) → 修复 → compiling → running → ...
+功能 3: init → start → compiling → running → (失败) → 修复 → running → ...
+```
+
+### 禁止行为
+- ❌ 每次做多个功能
+- ❌ **跳过状态直接更新为 done**
+- ❌ **不执行验证流程**
+- ❌ 不编译就测试
+- ❌ 不运行就测试
+- ❌ 不验证数据库连接
+- ❌ **前端功能测试只用 curl**
+- ❌ **不执行前端浏览器自动化测试**
+- ❌ 不测试就标记 done
+- ❌ 删除/修改 backend_test_steps(后端接口测试步骤)
+- ❌ 删除/修改 frontend_test_steps(前端页面测试步骤)
+- ❌ **验证失败不修复**
+- ❌ **输出初始化/启动命令让用户手动执行**
+- ❌ **说"启动方式"、"访问地址"、"请执行"等**
+
+---
+
+## passes: true 的条件
+
+只有状态流转到 `done`,才允许设置 `passes: true`:
+
+```json
+{
+  "id": X,
+  "status": "done",
+  "passes": true
+}
+```
+
+**状态流转要求**:
+> 必须经历完整流程:start → compiling → running → db-checking → backend-testing → frontend-testing → done
+> 
+> 不允许跳跃状态
+> 
+> 失败时保持当前状态,修复后重新执行
+
+**测试阶段要求**:
+> - 先测后端接口 → 确保 API 正常
+> - 再测前端页面 → 确保交互正常
+> - 后端测试失败 → 禁止进入前端测试
+
+---
+
+## 工件规范
+
+### feature_list.json
+```json
+{
+  "project_name": "项目名称",
+  "base_config": {
+    "backend_port": 8000,
+    "frontend_port": 8080,
+    "db_host": "localhost",
+    "db_port": 3306
+  },
+  "features": [
+    {
+      "id": 1,
+      "description": "功能描述",
+      "backend_test_steps": [
+        "1. curl POST /api/chats/new - 验证创建聊天接口返回 200",
+        "2. curl GET /api/chats - 验证获取聊天列表接口返回 200",
+        "3. curl POST /api/messages - 验证发送消息接口返回 200"
+      ],
+      "frontend_test_steps": [
+        "1. 点击'新聊天'按钮",
+        "2. 验证新聊天窗口创建",
+        "3. 输入消息内容",
+        "4. 点击'发送'按钮",
+        "5. 验证消息显示在聊天窗口"
+      ],
+      "status": "init",
+      "passes": false
+    }
+  ]
+}
+```
+
+**更新规则**:
+- ✅ 只允许按流程更新 status 字段
+- ✅ 只有 status 为 done 时才可修改 passes: true
+- ❌ 禁止删除功能
+- ❌ 禁止删除或修改 backend_test_steps
+- ❌ 禁止删除或修改 frontend_test_steps
+- ❌ 禁止合并功能
+
+**重要说明**:
+> 每个功能必须包含 backend_test_steps(后端接口测试)和 frontend_test_steps(前端页面测试)
+> - backend_test_steps:使用 curl 测试 API 接口
+> - frontend_test_steps:使用浏览器自动化测试用户交互
+
+### claude-progress.txt
+```
+[时间戳] 初始化完成
+[时间戳] 开始功能 1:功能描述 | 状态:init→start
+[时间戳] 功能 1 编译中 | 状态:start→compiling
+[时间戳] 功能 1 编译失败 | 错误:XXX | 状态:compiling
+[时间戳] 功能 1 编译成功 | 状态:compiling→running
+[时间戳] 功能 1 运行成功 | 状态:running→db-checking
+[时间戳] 功能 1 数据库验证成功 | 状态:db-checking→backend-testing
+[时间戳] 功能 1 后端接口测试成功 | 状态:backend-testing→frontend-testing
+[时间戳] 功能 1 前端页面测试成功 | 状态:frontend-testing→done
+[时间戳] 功能 1 完成 | passes: false→true
+```
+
+**更新规则**:
+- ✅ 每次会话追加新记录
+- ✅ 记录状态流转过程
+- ✅ 记录失败和修复
+- ❌ 禁止删除历史记录
+
+### init.sh
+- 格式:Bash 脚本
+- 位置:项目根目录
+- 要求:
+  - ✅ 一键启动项目
+  - ✅ 安装依赖(固定版本)
+  - ✅ 启动开发服务器
+  - ✅ 运行基础测试
+
+### Git
+- 提交频率:每次功能完成后
+- 提交信息:
+  ```
+  initial: 初始化项目
+  feat: 完成功能 X | 状态流转:start→compiling→running→db-checking→backend-testing→frontend-testing→done
+  fix: 修复功能 X | 状态:backend-testing (修复后端接口)
+  fix: 修复功能 X | 状态:frontend-testing (修复前端页面)
+  ```
+
+---
+
+## 失败模式处理
+
+| 问题 | 处理 |
+|------|------|
+| 过早宣布完成 | 必须按 feature_list.json 顺序完成 |
+| 一次做多个功能 | 每次只做 1 个 |
+| 留下 Bug | 每次会话前运行基础测试 |
+| 过早标记完成 | 必须状态流转到 done |
+| 数据库连不上 | 保持 db-checking 状态,修复后重新验证 |
+| 后端接口失败 | 保持 backend-testing 状态,修复后重新测试 |
+| 前端页面失败 | 保持 frontend-testing 状态,修复后重新测试 |
+| AI 推卸责任 | 立即纠正,必须自动执行命令 |
+| 一次性通过太难 | 使用状态流转,逐步推进 |
+
+---
+
+## 违规处理
+
+以下行为禁止,发现后立即纠正:
+
+1. ❌ 每次做多个功能 → 回退,重新按流程执行
+2. ❌ 跳过状态直接 done → 回退,重新执行流程
+3. ❌ 跳过测试阶段(后端→前端) → 回退,重新测试
+4. ❌ 不测试就标记 done → 重新测试
+5. ❌ 删除/修改 backend_test_steps → 恢复原文件
+6. ❌ 删除/修改 frontend_test_steps → 恢复原文件
+7. ❌ 不更新进度日志 → 补充更新
+8. ❌ 留下 Bug → 立即修复或回退
+9. ❌ 数据库失败还标记 done → 回退,修复数据库
+10. ❌ 后端测试失败就测前端 → 回退,先修复后端
+11. ❌ 输出命令让用户执行 → 立即纠正,AI 必须自己执行

+ 5 - 5
QUICK_START.md

@@ -4,7 +4,7 @@
 
 ### 前置要求
 - Node.js >= 18
-- MongoDB >= 6.0
+- MySQL >= 8.0
 - FFmpeg(可选,用于真实音频生成)
 
 ---
@@ -27,7 +27,7 @@ npm run dev
 
 看到以下输出表示成功:
 ```
-✅ MongoDB 连接成功
+✅ MySQL 连接成功
 🚀 服务启动成功:http://localhost:3000
 ```
 
@@ -91,10 +91,10 @@ npm run dev:h5
 
 ### Q1: 后端启动失败?
 ```bash
-# 检查 MongoDB 是否运行
-# Windows: 服务管理器查看 MongoDB 服务
+# 检查 MySQL 是否运行
+# Windows: 服务管理器查看 MySQL 服务
 # Mac: brew services list
-# Linux: systemctl status mongod
+# Linux: systemctl status mysql
 ```
 
 ### Q2: 前端无法访问后端?

+ 2 - 2
README.md

@@ -98,8 +98,8 @@ npm run build:mp-weixin
 PORT=3000
 NODE_ENV=development
 
-# MongoDB
-MONGODB_URI=mongodb://localhost:27017/audio-book
+# MySQL 数据库
+DATABASE_URL="mysql://root:password@localhost:3306/audio-book"
 
 # JWT
 JWT_SECRET=your-jwt-secret

+ 11 - 0
claude-progress.txt

@@ -0,0 +1,11 @@
+[2026-04-04 08:30] 第二期开发计划初始化
+[2026-04-04 08:44] feature_list_phase2.json 创建完成,共15个功能
+[2026-04-04 08:50] 开始按harness规则执行开发任务
+[2026-04-04 08:55] 开始功能 1:播放记录功能 | 状态:init→start
+[2026-04-04 09:00] 功能 1 编译中 | 状态:start→compiling
+[2026-04-04 09:05] 功能 1 数据库验证失败 | 错误:MySQL未运行在localhost:3306 | 状态:compiling→db-checking
+[2026-04-04 09:10] 功能 1 代码开发完成:
+  - 数据库: PlayRecord/UserPreference/Favorite/Category/Comment 表
+  - 后端: player.controller.ts, player.service.ts
+  - 前端: 播放器页面集成播放记录
+[2026-04-04 09:10] 功能 1 等待MySQL服务启动 | 状态:db-checking

+ 311 - 0
feature_list_phase2.json

@@ -0,0 +1,311 @@
+{
+  "project_name": "AI有声书生成工具-第二期",
+  "base_config": {
+    "backend_port": 8000,
+    "frontend_port": 8080,
+    "db_host": "localhost",
+    "db_port": 3306
+  },
+  "features": [
+    {
+      "id": 1,
+      "description": "播放记录功能 - 记录播放进度,支持续播",
+      "priority": "P1",
+      "phase": "第一阶段",
+      "backend_test_steps": [
+        "1. curl -X GET http://localhost:8000/api/player/progress - 验证获取播放进度接口返回 200",
+        "2. curl -X POST http://localhost:8000/api/player/progress -d '{\"audioId\":\"test\",\"progress\":30}' - 验证保存进度接口返回 200",
+        "3. curl -X GET http://localhost:8000/api/player/progress - 验证保存后能正确获取进度"
+      ],
+      "frontend_test_steps": [
+        "1. 打开首页,播放一个音频",
+        "2. 暂停播放,查看是否显示当前进度",
+        "3. 退出页面后重新进入",
+        "4. 验证是否显示'继续播放'按钮并正确续播到30秒位置"
+      ],
+      "status": "db-checking",
+      "passes": false,
+      "note": "等待MySQL服务启动,代码已编写完成"
+    },
+    {
+      "id": 2,
+      "description": "收藏功能 - 收藏喜欢的音频",
+      "priority": "P1",
+      "phase": "第一阶段",
+      "backend_test_steps": [
+        "1. curl -X GET http://localhost:8000/api/favorites - 验证获取收藏列表接口返回 200",
+        "2. curl -X POST http://localhost:8000/api/favorites -d '{\"audioId\":\"test-audio-1\"}' - 验证添加收藏接口返回 200",
+        "3. curl -X GET http://localhost:8000/api/favorites - 验证收藏列表包含新添加的音频",
+        "4. curl -X DELETE http://localhost:8000/api/favorites/test-audio-1 - 验证取消收藏接口返回 200"
+      ],
+      "frontend_test_steps": [
+        "1. 打开播放器页面播放音频",
+        "2. 点击收藏按钮(心形图标)",
+        "3. 验证按钮变为实心红色",
+        "4. 进入收藏页面,验证已收藏的音频显示在列表中",
+        "5. 点击取消收藏,验证列表中该音频消失"
+      ],
+      "status": "init",
+      "passes": false
+    },
+    {
+      "id": 3,
+      "description": "定时关闭功能 - 睡眠定时、15/30/60分钟自动停止",
+      "priority": "P1",
+      "phase": "第一阶段",
+      "backend_test_steps": [],
+      "frontend_test_steps": [
+        "1. 打开播放器页面开始播放",
+        "2. 点击定时按钮打开定时设置",
+        "3. 选择15分钟定时",
+        "4. 验证显示倒计时(如14:59)",
+        "5. 等待约1分钟后刷新页面",
+        "6. 验证倒计时正确递减",
+        "7. 等待定时结束,验证播放自动暂停"
+      ],
+      "status": "init",
+      "passes": false
+    },
+    {
+      "id": 4,
+      "description": "播放速度记忆 - 0.5x-2x速听,记忆用户偏好",
+      "priority": "P1",
+      "phase": "第一阶段",
+      "backend_test_steps": [
+        "1. curl -X GET http://localhost:8000/api/user/preferences - 验证获取用户偏好接口返回 200",
+        "2. curl -X PUT http://localhost:8000/api/user/preferences -d '{\"playSpeed\":1.5}' - 验证更新偏好接口返回 200",
+        "3. curl -X GET http://localhost:8000/api/user/preferences - 验证playSpeed为1.5"
+      ],
+      "frontend_test_steps": [
+        "1. 打开播放器页面",
+        "2. 点击倍速按钮选择1.5x",
+        "3. 验证播放速度变为1.5x",
+        "4. 退出页面",
+        "5. 重新进入播放器页面",
+        "6. 验证倍速按钮显示1.5x且播放速度为1.5x"
+      ],
+      "status": "init",
+      "passes": false
+    },
+    {
+      "id": 5,
+      "description": "分享功能 - 分享给微信好友/朋友圈",
+      "priority": "P1",
+      "phase": "第一阶段",
+      "backend_test_steps": [
+        "1. curl -X POST http://localhost:8000/api/share -d '{\"audioId\":\"test-audio-1\"}' - 验证生成分享链接接口返回 200"
+      ],
+      "frontend_test_steps": [
+        "1. 打开播放器页面",
+        "2. 点击分享按钮",
+        "3. 验证弹出分享菜单(微信好友、朋友圈、复制链接)",
+        "4. 选择'复制链接',验证剪贴板包含分享URL",
+        "5. 点击微信好友,验证调用微信分享API"
+      ],
+      "status": "init",
+      "passes": false
+    },
+    {
+      "id": 6,
+      "description": "加载优化 - 骨架屏、缓存优化",
+      "priority": "P1",
+      "phase": "贯穿全程",
+      "backend_test_steps": [],
+      "frontend_test_steps": [
+        "1. 清除浏览器缓存",
+        "2. 打开首页",
+        "3. 验证显示骨架屏加载动画",
+        "4. 验证内容加载后骨架屏消失",
+        "5. 再次打开首页",
+        "6. 验证第二次加载更快(使用缓存)"
+      ],
+      "status": "init",
+      "passes": false
+    },
+    {
+      "id": 7,
+      "description": "全局搜索 - 搜索有声书标题、历史记录",
+      "priority": "P1",
+      "phase": "第二阶段",
+      "backend_test_steps": [
+        "1. curl -X GET 'http://localhost:8000/api/search?q=test' - 验证搜索接口返回 200",
+        "2. curl -X GET 'http://localhost:8000/api/search?q=' - 验证空关键词返回历史记录",
+        "3. curl -X GET 'http://localhost:8000/api/search?q=小说' - 验证中文搜索正常"
+      ],
+      "frontend_test_steps": [
+        "1. 点击顶部搜索图标进入搜索页面",
+        "2. 验证显示搜索历史和热门推荐",
+        "3. 输入关键词'test'",
+        "4. 点击搜索按钮",
+        "5. 验证搜索结果列表显示匹配的内容",
+        "6. 点击搜索结果",
+        "7. 验证跳转到播放器页面"
+      ],
+      "status": "init",
+      "passes": false
+    },
+    {
+      "id": 8,
+      "description": "内容分类 - 按类型筛选(故事/小说/新闻/学习)",
+      "priority": "P2",
+      "phase": "第二阶段",
+      "backend_test_steps": [
+        "1. curl -X GET http://localhost:8000/api/categories - 验证获取分类列表接口返回 200",
+        "2. curl -X GET http://localhost:8000/api/categories/1 - 验证获取分类下音频接口返回 200"
+      ],
+      "frontend_test_steps": [
+        "1. 在首页点击分类标签(如'小说')",
+        "2. 验证内容列表刷新为该分类的内容",
+        "3. 切换到其他分类(如'学习')",
+        "4. 验证内容列表正确切换",
+        "5. 点击'全部'标签",
+        "6. 验证显示全部内容"
+      ],
+      "status": "init",
+      "passes": false
+    },
+    {
+      "id": 9,
+      "description": "音质选择 - 高/标准音质切换",
+      "priority": "P2",
+      "phase": "第二阶段",
+      "backend_test_steps": [
+        "1. curl -X PUT http://localhost:8000/api/user/preferences -d '{\"quality\":\"high\"}' - 验证更新音质偏好返回 200",
+        "2. curl -X GET http://localhost:8000/api/user/preferences - 验证quality为high"
+      ],
+      "frontend_test_steps": [
+        "1. 打开播放器页面",
+        "2. 打开音质设置",
+        "3. 切换到高清音质",
+        "4. 验证显示'高清'标识",
+        "5. 退出页面重新进入",
+        "6. 验证音质偏好保持为高清"
+      ],
+      "status": "init",
+      "passes": false
+    },
+    {
+      "id": 10,
+      "description": "离线缓存 - 下载到本地离线听",
+      "priority": "P2",
+      "phase": "第二阶段",
+      "backend_test_steps": [
+        "1. curl -X GET http://localhost:8000/api/downloads - 验证获取下载列表接口返回 200"
+      ],
+      "frontend_test_steps": [
+        "1. 在播放器页面点击下载按钮",
+        "2. 验证显示下载进度",
+        "3. 等待下载完成",
+        "4. 进入'我的下载'页面",
+        "5. 验证已下载的音频显示在列表中",
+        "6. 开启飞行模式",
+        "7. 点击离线音频播放",
+        "8. 验证离线播放正常"
+      ],
+      "status": "init",
+      "passes": false
+    },
+    {
+      "id": 11,
+      "description": "断点续传 - 大文件下载断点续传",
+      "priority": "P2",
+      "phase": "贯穿全程",
+      "backend_test_steps": [
+        "1. curl -X POST http://localhost:8000/api/downloads/check -d '{\"audioId\":\"test\",\"downloaded\":1024000}' - 验证断点查询返回正确信息"
+      ],
+      "frontend_test_steps": [
+        "1. 开始下载一个大音频文件",
+        "2. 在下载过程中关闭页面中断下载",
+        "3. 重新进入下载页面",
+        "4. 验证显示'继续下载'按钮",
+        "5. 点击继续下载",
+        "6. 验证从断点处继续下载"
+      ],
+      "status": "init",
+      "passes": false
+    },
+    {
+      "id": 12,
+      "description": "多端数据同步 - 多端数据实时同步",
+      "priority": "P2",
+      "phase": "贯穿全程",
+      "backend_test_steps": [
+        "1. curl -X POST http://localhost:8000/api/favorites -d '{\"audioId\":\"sync-test\"}' - 添加收藏",
+        "2. curl -X GET http://localhost:8000/api/favorites - 验证收藏添加成功",
+        "3. curl -X PUT http://localhost:8000/api/player/progress -d '{\"audioId\":\"sync-test\",\"progress\":50}' - 更新播放进度"
+      ],
+      "frontend_test_steps": [
+        "1. 在H5端收藏一个音频",
+        "2. 在小程序端打开收藏页面",
+        "3. 验证收藏列表包含刚添加的音频",
+        "4. 在H5端播放并暂停在1分钟位置",
+        "5. 在小程序端打开同一音频",
+        "6. 验证显示从1分钟位置继续播放"
+      ],
+      "status": "init",
+      "passes": false
+    },
+    {
+      "id": 13,
+      "description": "评论打分 - 对内容评论评分",
+      "priority": "P2",
+      "phase": "第三阶段",
+      "backend_test_steps": [
+        "1. curl -X GET http://localhost:8000/api/comments/test-audio-1 - 验证获取评论列表接口返回 200",
+        "2. curl -X POST http://localhost:8000/api/comments -d '{\"audioId\":\"test-audio-1\",\"content\":\"很好听\",\"rating\":5}' - 验证发表评论接口返回 200",
+        "3. curl -X GET http://localhost:8000/api/comments/test-audio-1 - 验证评论已添加"
+      ],
+      "frontend_test_steps": [
+        "1. 打开播放器页面",
+        "2. 点击评论按钮",
+        "3. 输入评论内容'非常棒的声音'",
+        "4. 选择5星评分",
+        "5. 点击提交",
+        "6. 验证评论显示在评论区",
+        "7. 验证评分为5星"
+      ],
+      "status": "init",
+      "passes": false
+    },
+    {
+      "id": 14,
+      "description": "消息通知 - 新内容推送、会员优惠",
+      "priority": "P3",
+      "phase": "第三阶段",
+      "backend_test_steps": [
+        "1. curl -X GET http://localhost:8000/api/notifications - 验证获取消息列表接口返回 200"
+      ],
+      "frontend_test_steps": [
+        "1. 进入设置页面",
+        "2. 找到消息通知开关",
+        "3. 开启消息通知",
+        "4. 验证弹出通知权限申请",
+        "5. 授权后收到推送消息",
+        "6. 点击消息通知",
+        "7. 验证跳转到对应页面"
+      ],
+      "status": "init",
+      "passes": false
+    },
+    {
+      "id": 15,
+      "description": "个性化主题 - 深色/浅色主题切换",
+      "priority": "P3",
+      "phase": "第三阶段",
+      "backend_test_steps": [
+        "1. curl -X PUT http://localhost:8000/api/user/preferences -d '{\"theme\":\"dark\"}' - 验证更新主题偏好返回 200",
+        "2. curl -X GET http://localhost:8000/api/user/preferences - 验证theme为dark"
+      ],
+      "frontend_test_steps": [
+        "1. 进入设置页面",
+        "2. 点击主题设置",
+        "3. 选择深色主题",
+        "4. 验证页面切换为深色模式",
+        "5. 退出页面重新进入",
+        "6. 验证主题保持为深色"
+      ],
+      "status": "init",
+      "passes": false
+    }
+  ]
+}

+ 36 - 0
init.sh

@@ -0,0 +1,36 @@
+#!/bin/bash
+# AI有声书生成工具 - 启动脚本
+# 第二期开发环境
+
+set -e
+
+echo "🚀 开始启动服务..."
+
+# 1. 安装后端依赖
+echo "📦 安装后端依赖..."
+cd server && npm install
+
+# 2. 数据库迁移
+echo "🗄️ 执行数据库迁移..."
+cd server && npx prisma generate
+cd server && npx prisma db push
+
+# 3. 启动后端服务
+echo "🔥 启动后端服务..."
+cd server && npm run dev &
+BACKEND_PID=$!
+
+# 4. 等待后端启动
+echo "⏳ 等待后端服务启动..."
+sleep 8
+
+# 5. 验证后端服务
+echo "✅ 验证后端服务..."
+curl -f http://localhost:8000/health || echo "后端服务启动失败"
+curl -f http://localhost:8000/api/audio/list || echo "API服务可能未就绪"
+
+echo ""
+echo "🎉 服务启动完成!"
+echo "后端服务: http://localhost:8000"
+echo "前端服务: npm run dev:h5 (在 my-uniapp-vue3 目录)"
+echo "后端进程PID: $BACKEND_PID"

+ 142 - 4
my-uniapp-vue3/src/pages/player/index.vue

@@ -23,6 +23,17 @@
       <text class="preview-text">{{ (audio?.text || '').slice(0, 200) }}{{ (audio?.text || '').length > 200 ? '...' : '' }}</text>
     </view>
 
+    <!-- 继续播放提示 -->
+    <view v-if="showResumePrompt" class="resume-prompt">
+      <view class="resume-content">
+        <text class="resume-text">上次播放到 {{ formatDuration(savedProgress) }}</text>
+        <view class="resume-buttons">
+          <button class="resume-btn" @click="playFromBeginning">从头开始</button>
+          <button class="resume-btn primary" @click="resumeFromProgress">继续播放</button>
+        </view>
+      </view>
+    </view>
+
     <!-- 进度条 -->
     <view class="progress-section">
       <text class="time">{{ formatDuration(currentTime) }}</text>
@@ -99,9 +110,9 @@
 </template>
 
 <script setup lang="ts">
-import { ref, computed, onMounted, onUnmounted } from 'vue';
+import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
 import { useAudioStore } from '../../store/audio';
-import { get, put } from '../../utils/request';
+import { get, post, put, getFullUrl } from '../../utils/request';
 import type { AudioItem } from '../../types';
 
 const audioStore = useAudioStore();
@@ -110,6 +121,8 @@ const audioStore = useAudioStore();
 const audioId = ref('');
 const audio = ref<AudioItem | null>(null);
 const showRatePicker = ref(false);
+const savedProgress = ref(0); // 保存的历史进度
+const showResumePrompt = ref(false); // 显示继续播放提示
 
 // 从 store 获取状态
 const isPlaying = computed(() => audioStore.isPlaying);
@@ -119,6 +132,9 @@ const playRate = computed(() => audioStore.playRate);
 const hasPrev = computed(() => audioStore.hasPrev);
 const hasNext = computed(() => audioStore.hasNext);
 
+// 播放进度自动保存定时器
+let progressSaveTimer: number | null = null;
+
 // 页面加载
 onMounted(async () => {
   const pages = getCurrentPages();
@@ -129,14 +145,93 @@ onMounted(async () => {
     await fetchAudio();
     // 获取播放列表
     await fetchPlaylist();
+    // 获取历史播放进度
+    await fetchPlayProgress();
+    // 开始自动保存进度
+    startProgressAutoSave();
   }
 });
 
 // 页面卸载
 onUnmounted(() => {
-  // 不销毁音频上下文,保持后台播放
+  // 保存最终进度
+  savePlayProgress();
+  // 停止定时器
+  if (progressSaveTimer) {
+    clearInterval(progressSaveTimer);
+  }
+});
+
+// 监听播放进度变化
+watch(currentTime, (newTime) => {
+  if (newTime > 0 && audio.value) {
+    savedProgress.value = newTime;
+  }
 });
 
+// 获取历史播放进度
+async function fetchPlayProgress() {
+  try {
+    const result = await get<any>(`/player/progress?audioId=${audioId.value}`);
+    if (result && result.progress > 0) {
+      savedProgress.value = result.progress;
+      // 如果进度不是从0开始,显示继续播放提示
+      if (result.progress > 10) {
+        showResumePrompt.value = true;
+      }
+    }
+  } catch (error) {
+    console.log('获取播放进度失败:', error);
+  }
+}
+
+// 开始自动保存进度(每5秒保存一次)
+function startProgressAutoSave() {
+  if (progressSaveTimer) {
+    clearInterval(progressSaveTimer);
+  }
+  progressSaveTimer = setInterval(() => {
+    if (audio.value && audioStore.currentTime > 0) {
+      savePlayProgress();
+    }
+  }, 5000) as unknown as number;
+}
+
+// 保存播放进度
+async function savePlayProgress() {
+  if (!audio.value || audioStore.currentTime <= 0) return;
+  
+  try {
+    await post('/player/progress', {
+      audioId: parseInt(audioId.value),
+      progress: audioStore.currentTime,
+      duration: audioStore.duration,
+    });
+  } catch (error) {
+    console.log('保存播放进度失败:', error);
+  }
+}
+
+// 继续从上次位置播放
+function resumeFromProgress() {
+  if (savedProgress.value > 0) {
+    audioStore.seek(savedProgress.value);
+    if (!isPlaying.value) {
+      audioStore.togglePlay();
+    }
+  }
+  showResumePrompt.value = false;
+}
+
+// 从头开始播放
+function playFromBeginning() {
+  audioStore.seek(0);
+  if (!isPlaying.value) {
+    audioStore.togglePlay();
+  }
+  showResumePrompt.value = false;
+}
+
 // 获取音频详情
 async function fetchAudio() {
   try {
@@ -235,7 +330,7 @@ function handleDownload() {
 
   uni.showLoading({ title: '下载中...' });
   uni.downloadFile({
-    url: audio.value.audioUrl,
+    url: getFullUrl(audio.value.audioUrl),
     success: (res) => {
       uni.hideLoading();
       if (res.statusCode === 200) {
@@ -528,4 +623,47 @@ function copyShareLink() {
   font-size: 28rpx;
   color: #ffffff;
 }
+
+/* 继续播放提示 */
+.resume-prompt {
+  margin: 0 32rpx 24rpx;
+  background: rgba(79, 70, 229, 0.2);
+  border: 1px solid rgba(79, 70, 229, 0.3);
+  border-radius: 16rpx;
+  padding: 24rpx;
+}
+
+.resume-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.resume-text {
+  font-size: 26rpx;
+  color: #ffffff;
+  margin-bottom: 20rpx;
+}
+
+.resume-buttons {
+  display: flex;
+  gap: 24rpx;
+}
+
+.resume-btn {
+  padding: 16rpx 32rpx;
+  border-radius: 12rpx;
+  font-size: 26rpx;
+  background: #374151;
+  color: #ffffff;
+  border: none;
+}
+
+.resume-btn.primary {
+  background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
+}
+
+.resume-btn::after {
+  border: none;
+}
 </style>

+ 6 - 3
my-uniapp-vue3/src/store/audio.ts

@@ -1,6 +1,6 @@
 import { defineStore } from 'pinia';
 import { ref, computed } from 'vue';
-import { get, post } from '../utils/request';
+import { get, post, getFullUrl } from '../utils/request';
 import type { AudioItem, Voice, VoiceParams } from '../types';
 
 export const useAudioStore = defineStore('audio', () => {
@@ -88,11 +88,14 @@ export const useAudioStore = defineStore('audio', () => {
 
     currentAudio.value = audio;
     
+    // 获取完整的音频 URL
+    const fullUrl = getFullUrl(audio.audioUrl);
+    
     // 如果音频源相同,只切换播放状态
-    if (audioContext && audioContext.src === audio.audioUrl) {
+    if (audioContext && audioContext.src === fullUrl) {
       audioContext.play();
     } else if (audioContext) {
-      audioContext.src = audio.audioUrl;
+      audioContext.src = fullUrl;
       audioContext.play();
     }
   }

+ 18 - 0
my-uniapp-vue3/src/utils/request.ts

@@ -5,6 +5,24 @@ const BASE_URL = (typeof import.meta !== 'undefined' && import.meta.env && impor
 // 开发环境使用本地 API,生产环境使用线上 API
 const isDev = typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'development';
 
+// 获取完整的后端 URL(用于音频文件等静态资源)
+export function getFullUrl(path: string): string {
+  // 如果已经是完整 URL,直接返回
+  if (path.startsWith('http://') || path.startsWith('https://')) {
+    return path;
+  }
+  // 开发环境使用完整的后端 URL
+  // @ts-ignore
+  const isDevEnv = typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.DEV;
+  if (isDevEnv) {
+    // @ts-ignore
+    const backendUrl = (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.VITE_API_BASE_URL) || 'http://localhost:3000';
+    return backendUrl + path;
+  }
+  // 生产环境使用相对路径
+  return path;
+}
+
 // 请求封装
 export async function request<T = unknown>(
   url: string,

+ 2 - 2
server/.env.example

@@ -2,8 +2,8 @@
 PORT=3000
 NODE_ENV=development
 
-# MongoDB
-MONGODB_URI=mongodb://localhost:27017/audio-book
+# MySQL 数据库
+DATABASE_URL="mysql://root:password@localhost:3306/audio-book"
 
 # JWT
 JWT_SECRET=your-super-secret-jwt-key-change-in-production

+ 5 - 0
server/.gitignore

@@ -0,0 +1,5 @@
+node_modules
+# Keep environment variables out of version control
+.env
+
+/src/generated/prisma

+ 477 - 184
server/package-lock.json

@@ -11,6 +11,7 @@
         "@koa/bodyparser": "^6.1.0",
         "@koa/cors": "^5.0.0",
         "@koa/router": "^12.0.1",
+        "@prisma/client": "^6.19.3",
         "axios": "^1.7.2",
         "crypto-js": "^4.2.0",
         "dotenv": "^16.4.5",
@@ -20,7 +21,8 @@
         "koa": "^3.1.2",
         "koa-mount": "^4.2.0",
         "koa-static": "^5.0.0",
-        "mongoose": "^8.4.1",
+        "mysql2": "^3.20.0",
+        "prisma": "^6.19.3",
         "uuid": "^9.0.1"
       },
       "devDependencies": {
@@ -620,15 +622,91 @@
         "node": ">= 12"
       }
     },
-    "node_modules/@mongodb-js/saslprep": {
-      "version": "1.4.6",
-      "resolved": "https://registry.npmmirror.com/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz",
-      "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==",
-      "license": "MIT",
+    "node_modules/@prisma/client": {
+      "version": "6.19.3",
+      "resolved": "https://registry.npmmirror.com/@prisma/client/-/client-6.19.3.tgz",
+      "integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==",
+      "hasInstallScript": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18"
+      },
+      "peerDependencies": {
+        "prisma": "*",
+        "typescript": ">=5.1.0"
+      },
+      "peerDependenciesMeta": {
+        "prisma": {
+          "optional": true
+        },
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@prisma/config": {
+      "version": "6.19.3",
+      "resolved": "https://registry.npmmirror.com/@prisma/config/-/config-6.19.3.tgz",
+      "integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==",
+      "license": "Apache-2.0",
       "dependencies": {
-        "sparse-bitfield": "^3.0.3"
+        "c12": "3.1.0",
+        "deepmerge-ts": "7.1.5",
+        "effect": "3.21.0",
+        "empathic": "2.0.0"
       }
     },
+    "node_modules/@prisma/debug": {
+      "version": "6.19.3",
+      "resolved": "https://registry.npmmirror.com/@prisma/debug/-/debug-6.19.3.tgz",
+      "integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/@prisma/engines": {
+      "version": "6.19.3",
+      "resolved": "https://registry.npmmirror.com/@prisma/engines/-/engines-6.19.3.tgz",
+      "integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==",
+      "hasInstallScript": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@prisma/debug": "6.19.3",
+        "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
+        "@prisma/fetch-engine": "6.19.3",
+        "@prisma/get-platform": "6.19.3"
+      }
+    },
+    "node_modules/@prisma/engines-version": {
+      "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
+      "resolved": "https://registry.npmmirror.com/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz",
+      "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/@prisma/fetch-engine": {
+      "version": "6.19.3",
+      "resolved": "https://registry.npmmirror.com/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz",
+      "integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@prisma/debug": "6.19.3",
+        "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
+        "@prisma/get-platform": "6.19.3"
+      }
+    },
+    "node_modules/@prisma/get-platform": {
+      "version": "6.19.3",
+      "resolved": "https://registry.npmmirror.com/@prisma/get-platform/-/get-platform-6.19.3.tgz",
+      "integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@prisma/debug": "6.19.3"
+      }
+    },
+    "node_modules/@standard-schema/spec": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz",
+      "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+      "license": "MIT"
+    },
     "node_modules/@tsconfig/node10": {
       "version": "1.0.12",
       "resolved": "https://registry.npmmirror.com/@tsconfig/node10/-/node10-1.0.12.tgz",
@@ -896,21 +974,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/@types/webidl-conversions": {
-      "version": "7.0.3",
-      "resolved": "https://registry.npmmirror.com/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
-      "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
-      "license": "MIT"
-    },
-    "node_modules/@types/whatwg-url": {
-      "version": "11.0.5",
-      "resolved": "https://registry.npmmirror.com/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
-      "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
-      "license": "MIT",
-      "dependencies": {
-        "@types/webidl-conversions": "*"
-      }
-    },
     "node_modules/accepts": {
       "version": "1.3.8",
       "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
@@ -968,6 +1031,15 @@
       "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
       "license": "MIT"
     },
+    "node_modules/aws-ssl-profiles": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
+      "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
     "node_modules/axios": {
       "version": "1.13.6",
       "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.6.tgz",
@@ -979,15 +1051,6 @@
         "proxy-from-env": "^1.1.0"
       }
     },
-    "node_modules/bson": {
-      "version": "6.10.4",
-      "resolved": "https://registry.npmmirror.com/bson/-/bson-6.10.4.tgz",
-      "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==",
-      "license": "Apache-2.0",
-      "engines": {
-        "node": ">=16.20.1"
-      }
-    },
     "node_modules/buffer-equal-constant-time": {
       "version": "1.0.1",
       "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -1003,6 +1066,34 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/c12": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/c12/-/c12-3.1.0.tgz",
+      "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
+      "license": "MIT",
+      "dependencies": {
+        "chokidar": "^4.0.3",
+        "confbox": "^0.2.2",
+        "defu": "^6.1.4",
+        "dotenv": "^16.6.1",
+        "exsolve": "^1.0.7",
+        "giget": "^2.0.0",
+        "jiti": "^2.4.2",
+        "ohash": "^2.0.11",
+        "pathe": "^2.0.3",
+        "perfect-debounce": "^1.0.0",
+        "pkg-types": "^2.2.0",
+        "rc9": "^2.1.2"
+      },
+      "peerDependencies": {
+        "magicast": "^0.3.5"
+      },
+      "peerDependenciesMeta": {
+        "magicast": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/call-bind-apply-helpers": {
       "version": "1.0.2",
       "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -1032,6 +1123,30 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/chokidar": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz",
+      "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+      "license": "MIT",
+      "dependencies": {
+        "readdirp": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 14.16.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/citty": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmmirror.com/citty/-/citty-0.1.6.tgz",
+      "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
+      "license": "MIT",
+      "dependencies": {
+        "consola": "^3.2.3"
+      }
+    },
     "node_modules/co-body": {
       "version": "6.2.0",
       "resolved": "https://registry.npmmirror.com/co-body/-/co-body-6.2.0.tgz",
@@ -1060,6 +1175,21 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/confbox": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.4.tgz",
+      "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
+      "license": "MIT"
+    },
+    "node_modules/consola": {
+      "version": "3.4.2",
+      "resolved": "https://registry.npmmirror.com/consola/-/consola-3.4.2.tgz",
+      "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
+      "license": "MIT",
+      "engines": {
+        "node": "^14.18.0 || >=16.10.0"
+      }
+    },
     "node_modules/content-disposition": {
       "version": "1.0.1",
       "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.0.1.tgz",
@@ -1131,6 +1261,21 @@
       "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==",
       "license": "MIT"
     },
+    "node_modules/deepmerge-ts": {
+      "version": "7.1.5",
+      "resolved": "https://registry.npmmirror.com/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
+      "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/defu": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmmirror.com/defu/-/defu-6.1.6.tgz",
+      "integrity": "sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug==",
+      "license": "MIT"
+    },
     "node_modules/delayed-stream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -1146,6 +1291,15 @@
       "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
       "license": "MIT"
     },
+    "node_modules/denque": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz",
+      "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
     "node_modules/depd": {
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
@@ -1155,6 +1309,12 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/destr": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/destr/-/destr-2.0.5.tgz",
+      "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
+      "license": "MIT"
+    },
     "node_modules/destroy": {
       "version": "1.2.0",
       "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz",
@@ -1216,6 +1376,25 @@
       "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
       "license": "MIT"
     },
+    "node_modules/effect": {
+      "version": "3.21.0",
+      "resolved": "https://registry.npmmirror.com/effect/-/effect-3.21.0.tgz",
+      "integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@standard-schema/spec": "^1.0.0",
+        "fast-check": "^3.23.1"
+      }
+    },
+    "node_modules/empathic": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/empathic/-/empathic-2.0.0.tgz",
+      "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14"
+      }
+    },
     "node_modules/encodeurl": {
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
@@ -1318,6 +1497,34 @@
       "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
       "license": "MIT"
     },
+    "node_modules/exsolve": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz",
+      "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
+      "license": "MIT"
+    },
+    "node_modules/fast-check": {
+      "version": "3.23.2",
+      "resolved": "https://registry.npmmirror.com/fast-check/-/fast-check-3.23.2.tgz",
+      "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/dubzzz"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fast-check"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "pure-rand": "^6.1.0"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
     "node_modules/fluent-ffmpeg": {
       "version": "2.1.3",
       "resolved": "https://registry.npmmirror.com/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz",
@@ -1401,6 +1608,15 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/generate-function": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz",
+      "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "is-property": "^1.0.2"
+      }
+    },
     "node_modules/generator-function": {
       "version": "2.0.1",
       "resolved": "https://registry.npmmirror.com/generator-function/-/generator-function-2.0.1.tgz",
@@ -1460,6 +1676,23 @@
         "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
       }
     },
+    "node_modules/giget": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/giget/-/giget-2.0.0.tgz",
+      "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
+      "license": "MIT",
+      "dependencies": {
+        "citty": "^0.1.6",
+        "consola": "^3.4.0",
+        "defu": "^6.1.4",
+        "node-fetch-native": "^1.6.6",
+        "nypm": "^0.6.0",
+        "pathe": "^2.0.3"
+      },
+      "bin": {
+        "giget": "dist/cli.mjs"
+      }
+    },
     "node_modules/gopd": {
       "version": "1.2.0",
       "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
@@ -1624,6 +1857,12 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-property": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz",
+      "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
+      "license": "MIT"
+    },
     "node_modules/is-regex": {
       "version": "1.2.1",
       "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz",
@@ -1648,6 +1887,15 @@
       "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
       "license": "ISC"
     },
+    "node_modules/jiti": {
+      "version": "2.6.1",
+      "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz",
+      "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+      "license": "MIT",
+      "bin": {
+        "jiti": "lib/jiti-cli.mjs"
+      }
+    },
     "node_modules/jsonwebtoken": {
       "version": "9.0.3",
       "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
@@ -1691,15 +1939,6 @@
         "safe-buffer": "^5.0.1"
       }
     },
-    "node_modules/kareem": {
-      "version": "2.6.3",
-      "resolved": "https://registry.npmmirror.com/kareem/-/kareem-2.6.3.tgz",
-      "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==",
-      "license": "Apache-2.0",
-      "engines": {
-        "node": ">=12.0.0"
-      }
-    },
     "node_modules/keygrip": {
       "version": "1.1.0",
       "resolved": "https://registry.npmmirror.com/keygrip/-/keygrip-1.1.0.tgz",
@@ -1927,6 +2166,27 @@
       "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
       "license": "MIT"
     },
+    "node_modules/long": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz",
+      "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/lru.min": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.4.tgz",
+      "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
+      "license": "MIT",
+      "engines": {
+        "bun": ">=1.0.0",
+        "deno": ">=1.30.0",
+        "node": ">=8.0.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wellwelwel"
+      }
+    },
     "node_modules/make-error": {
       "version": "1.3.6",
       "resolved": "https://registry.npmmirror.com/make-error/-/make-error-1.3.6.tgz",
@@ -1952,12 +2212,6 @@
         "node": ">= 0.6"
       }
     },
-    "node_modules/memory-pager": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmmirror.com/memory-pager/-/memory-pager-1.5.0.tgz",
-      "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
-      "license": "MIT"
-    },
     "node_modules/methods": {
       "version": "1.1.2",
       "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz",
@@ -1988,111 +2242,62 @@
         "node": ">= 0.6"
       }
     },
-    "node_modules/mongodb": {
-      "version": "6.20.0",
-      "resolved": "https://registry.npmmirror.com/mongodb/-/mongodb-6.20.0.tgz",
-      "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==",
-      "license": "Apache-2.0",
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/mysql2": {
+      "version": "3.20.0",
+      "resolved": "https://registry.npmmirror.com/mysql2/-/mysql2-3.20.0.tgz",
+      "integrity": "sha512-eCLUs7BNbgA6nf/MZXsaBO1SfGs0LtLVrJD3WeWq+jPLDWkSufTD+aGMwykfUVPdZnblaUK1a8G/P63cl9FkKg==",
+      "license": "MIT",
       "dependencies": {
-        "@mongodb-js/saslprep": "^1.3.0",
-        "bson": "^6.10.4",
-        "mongodb-connection-string-url": "^3.0.2"
+        "aws-ssl-profiles": "^1.1.2",
+        "denque": "^2.1.0",
+        "generate-function": "^2.3.1",
+        "iconv-lite": "^0.7.2",
+        "long": "^5.3.2",
+        "lru.min": "^1.1.4",
+        "named-placeholders": "^1.1.6",
+        "sql-escaper": "^1.3.3"
       },
       "engines": {
-        "node": ">=16.20.1"
+        "node": ">= 8.0"
       },
       "peerDependencies": {
-        "@aws-sdk/credential-providers": "^3.188.0",
-        "@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
-        "gcp-metadata": "^5.2.0",
-        "kerberos": "^2.0.1",
-        "mongodb-client-encryption": ">=6.0.0 <7",
-        "snappy": "^7.3.2",
-        "socks": "^2.7.1"
-      },
-      "peerDependenciesMeta": {
-        "@aws-sdk/credential-providers": {
-          "optional": true
-        },
-        "@mongodb-js/zstd": {
-          "optional": true
-        },
-        "gcp-metadata": {
-          "optional": true
-        },
-        "kerberos": {
-          "optional": true
-        },
-        "mongodb-client-encryption": {
-          "optional": true
-        },
-        "snappy": {
-          "optional": true
-        },
-        "socks": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/mongodb-connection-string-url": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmmirror.com/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
-      "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
-      "license": "Apache-2.0",
-      "dependencies": {
-        "@types/whatwg-url": "^11.0.2",
-        "whatwg-url": "^14.1.0 || ^13.0.0"
+        "@types/node": ">= 8"
       }
     },
-    "node_modules/mongoose": {
-      "version": "8.23.0",
-      "resolved": "https://registry.npmmirror.com/mongoose/-/mongoose-8.23.0.tgz",
-      "integrity": "sha512-Bul4Ha6J8IqzFrb0B1xpVzkC3S0sk43dmLSnhFOn8eJlZiLwL5WO6cRymmjaADdCMjUcCpj2ce8hZI6O4ZFSug==",
+    "node_modules/mysql2/node_modules/iconv-lite": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz",
+      "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
       "license": "MIT",
       "dependencies": {
-        "bson": "^6.10.4",
-        "kareem": "2.6.3",
-        "mongodb": "~6.20.0",
-        "mpath": "0.9.0",
-        "mquery": "5.0.0",
-        "ms": "2.1.3",
-        "sift": "17.1.3"
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
       },
       "engines": {
-        "node": ">=16.20.1"
+        "node": ">=0.10.0"
       },
       "funding": {
         "type": "opencollective",
-        "url": "https://opencollective.com/mongoose"
-      }
-    },
-    "node_modules/mpath": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmmirror.com/mpath/-/mpath-0.9.0.tgz",
-      "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=4.0.0"
+        "url": "https://opencollective.com/express"
       }
     },
-    "node_modules/mquery": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmmirror.com/mquery/-/mquery-5.0.0.tgz",
-      "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==",
+    "node_modules/named-placeholders": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.6.tgz",
+      "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
       "license": "MIT",
       "dependencies": {
-        "debug": "4.x"
+        "lru.min": "^1.1.0"
       },
       "engines": {
-        "node": ">=14.0.0"
+        "node": ">=8.0.0"
       }
     },
-    "node_modules/ms": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
-      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
-      "license": "MIT"
-    },
     "node_modules/negotiator": {
       "version": "0.6.3",
       "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz",
@@ -2102,6 +2307,35 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/node-fetch-native": {
+      "version": "1.6.7",
+      "resolved": "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
+      "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
+      "license": "MIT"
+    },
+    "node_modules/nypm": {
+      "version": "0.6.5",
+      "resolved": "https://registry.npmmirror.com/nypm/-/nypm-0.6.5.tgz",
+      "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==",
+      "license": "MIT",
+      "dependencies": {
+        "citty": "^0.2.0",
+        "pathe": "^2.0.3",
+        "tinyexec": "^1.0.2"
+      },
+      "bin": {
+        "nypm": "dist/cli.mjs"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/nypm/node_modules/citty": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmmirror.com/citty/-/citty-0.2.2.tgz",
+      "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==",
+      "license": "MIT"
+    },
     "node_modules/object-inspect": {
       "version": "1.13.4",
       "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -2114,6 +2348,12 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/ohash": {
+      "version": "2.0.11",
+      "resolved": "https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz",
+      "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
+      "license": "MIT"
+    },
     "node_modules/on-finished": {
       "version": "2.4.1",
       "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
@@ -2150,20 +2390,75 @@
       "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
       "license": "MIT"
     },
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+      "license": "MIT"
+    },
+    "node_modules/perfect-debounce": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+      "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+      "license": "MIT"
+    },
+    "node_modules/pkg-types": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.0.tgz",
+      "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
+      "license": "MIT",
+      "dependencies": {
+        "confbox": "^0.2.2",
+        "exsolve": "^1.0.7",
+        "pathe": "^2.0.3"
+      }
+    },
+    "node_modules/prisma": {
+      "version": "6.19.3",
+      "resolved": "https://registry.npmmirror.com/prisma/-/prisma-6.19.3.tgz",
+      "integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==",
+      "hasInstallScript": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@prisma/config": "6.19.3",
+        "@prisma/engines": "6.19.3"
+      },
+      "bin": {
+        "prisma": "build/index.js"
+      },
+      "engines": {
+        "node": ">=18.18"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.1.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/proxy-from-env": {
       "version": "1.1.0",
       "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
       "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
       "license": "MIT"
     },
-    "node_modules/punycode": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
-      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=6"
-      }
+    "node_modules/pure-rand": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmmirror.com/pure-rand/-/pure-rand-6.1.0.tgz",
+      "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/dubzzz"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fast-check"
+        }
+      ],
+      "license": "MIT"
     },
     "node_modules/qs": {
       "version": "6.15.0",
@@ -2195,6 +2490,29 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/rc9": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmmirror.com/rc9/-/rc9-2.1.2.tgz",
+      "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
+      "license": "MIT",
+      "dependencies": {
+        "defu": "^6.1.4",
+        "destr": "^2.0.3"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz",
+      "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14.18.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
     "node_modules/resolve-path": {
       "version": "1.4.0",
       "resolved": "https://registry.npmmirror.com/resolve-path/-/resolve-path-1.4.0.tgz",
@@ -2396,19 +2714,19 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/sift": {
-      "version": "17.1.3",
-      "resolved": "https://registry.npmmirror.com/sift/-/sift-17.1.3.tgz",
-      "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
-      "license": "MIT"
-    },
-    "node_modules/sparse-bitfield": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmmirror.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
-      "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
+    "node_modules/sql-escaper": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmmirror.com/sql-escaper/-/sql-escaper-1.3.3.tgz",
+      "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==",
       "license": "MIT",
-      "dependencies": {
-        "memory-pager": "^1.0.2"
+      "engines": {
+        "bun": ">=1.0.0",
+        "deno": ">=2.0.0",
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
       }
     },
     "node_modules/statuses": {
@@ -2420,6 +2738,15 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/tinyexec": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.4.tgz",
+      "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/toidentifier": {
       "version": "1.0.1",
       "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -2429,18 +2756,6 @@
         "node": ">=0.6"
       }
     },
-    "node_modules/tr46": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmmirror.com/tr46/-/tr46-5.1.1.tgz",
-      "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
-      "license": "MIT",
-      "dependencies": {
-        "punycode": "^2.3.1"
-      },
-      "engines": {
-        "node": ">=18"
-      }
-    },
     "node_modules/ts-node": {
       "version": "10.9.2",
       "resolved": "https://registry.npmmirror.com/ts-node/-/ts-node-10.9.2.tgz",
@@ -2531,7 +2846,7 @@
       "version": "5.9.3",
       "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
       "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
-      "dev": true,
+      "devOptional": true,
       "license": "Apache-2.0",
       "bin": {
         "tsc": "bin/tsc",
@@ -2585,28 +2900,6 @@
         "node": ">= 0.8"
       }
     },
-    "node_modules/webidl-conversions": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
-      "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
-      "license": "BSD-2-Clause",
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/whatwg-url": {
-      "version": "14.2.0",
-      "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-14.2.0.tgz",
-      "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
-      "license": "MIT",
-      "dependencies": {
-        "tr46": "^5.1.0",
-        "webidl-conversions": "^7.0.0"
-      },
-      "engines": {
-        "node": ">=18"
-      }
-    },
     "node_modules/which": {
       "version": "1.3.1",
       "resolved": "https://registry.npmmirror.com/which/-/which-1.3.1.tgz",

+ 3 - 1
server/package.json

@@ -12,6 +12,7 @@
     "@koa/bodyparser": "^6.1.0",
     "@koa/cors": "^5.0.0",
     "@koa/router": "^12.0.1",
+    "@prisma/client": "^6.19.3",
     "axios": "^1.7.2",
     "crypto-js": "^4.2.0",
     "dotenv": "^16.4.5",
@@ -21,7 +22,8 @@
     "koa": "^3.1.2",
     "koa-mount": "^4.2.0",
     "koa-static": "^5.0.0",
-    "mongoose": "^8.4.1",
+    "mysql2": "^3.20.0",
+    "prisma": "^6.19.3",
     "uuid": "^9.0.1"
   },
   "devDependencies": {

+ 14 - 0
server/prisma.config.ts

@@ -0,0 +1,14 @@
+// This file was generated by Prisma, and assumes you have installed the following:
+// npm install --save-dev prisma dotenv
+import "dotenv/config";
+import { defineConfig } from "prisma/config";
+
+export default defineConfig({
+  schema: "prisma/schema.prisma",
+  migrations: {
+    path: "prisma/migrations",
+  },
+  datasource: {
+    url: process.env["DATABASE_URL"] || "mysql://root:root@localhost:3306/audio_book",
+  },
+});

+ 148 - 0
server/prisma/schema.prisma

@@ -0,0 +1,148 @@
+generator client {
+  provider = "prisma-client-js"
+}
+
+datasource db {
+  provider = "mysql"
+  url      = env("DATABASE_URL")
+}
+
+model User {
+  id              Int       @id @default(autoincrement())
+  phone           String?   @unique
+  openid          String?   @unique
+  nickname        String    @default("用户")
+  avatar          String    @default("")
+  memberLevel     Int       @default(0)
+  memberExpireAt  DateTime?
+  dailyUsage      Int       @default(0)
+  lastUsageDate   String    @default("")
+  createdAt       DateTime  @default(now())
+  updatedAt       DateTime  @updatedAt
+  
+  audios          Audio[]
+  orders          Order[]
+  playRecords     PlayRecord[]
+  preferences     UserPreference?
+  favorites       Favorite[]
+  comments        Comment[]
+  
+  @@index([phone])
+  @@index([openid])
+}
+
+model Audio {
+  id              Int       @id @default(autoincrement())
+  userId          Int?
+  title           String
+  text            String    @db.Text
+  summary         String?   @db.Text
+  tags            String?   @db.Text // JSON 字符串存储数组
+  audioUrl        String    @db.Text
+  audioDuration   Int       @default(0)
+  audioSize       Int       @default(0)
+  wordCount       Int
+  voiceId         String
+  voiceParams     String?   @db.Text // JSON 存储 {speed, pitch, volume}
+  status          String    @default("processing")
+  isFavorite      Boolean   @default(false)
+  categoryId      Int?
+  createdAt       DateTime  @default(now())
+  updatedAt       DateTime  @updatedAt
+  
+  user            User?     @relation(fields: [userId], references: [id])
+  category        Category? @relation("CategoryAudios", fields: [categoryId], references: [id])
+  
+  playRecords     PlayRecord[]
+  favorites       Favorite[]
+  comments        Comment[]
+  
+  @@index([userId, createdAt])
+  @@index([userId, isFavorite])
+  @@index([categoryId])
+}
+
+model Order {
+  id              Int       @id @default(autoincrement())
+  userId          Int
+  orderNo         String    @unique
+  productType     String    // monthly 或 yearly
+  amount          Decimal   @db.Decimal(10, 2)
+  status          String    @default("pending")
+  paymentMethod   String?
+  paidAt          DateTime?
+  createdAt       DateTime  @default(now())
+  updatedAt       DateTime  @updatedAt
+  
+  user            User      @relation(fields: [userId], references: [id])
+  
+  @@index([userId, createdAt])
+  @@index([orderNo])
+}
+
+model PlayRecord {
+  id        Int       @id @default(autoincrement())
+  userId    Int
+  audioId   Int
+  progress  Float     @default(0)  // 播放进度(秒)
+  duration  Float     @default(0)  // 总时长
+  updatedAt DateTime  @updatedAt
+  createdAt DateTime  @default(now())
+  
+  user      User      @relation(fields: [userId], references: [id])
+  audio     Audio    @relation(fields: [audioId], references: [id])
+  
+  @@unique([userId, audioId])
+  @@index([userId])
+  @@index([audioId])
+}
+
+model UserPreference {
+  id        Int      @id @default(autoincrement())
+  userId    Int      @unique
+  playSpeed Float    @default(1.0)
+  quality   String   @default("standard")  // standard, high
+  theme     String   @default("light")
+  updatedAt DateTime @updatedAt
+  createdAt DateTime @default(now())
+  
+  user      User     @relation(fields: [userId], references: [id])
+}
+
+model Favorite {
+  id        Int      @id @default(autoincrement())
+  userId    Int
+  audioId   Int
+  createdAt DateTime @default(now())
+  
+  user      User     @relation(fields: [userId], references: [id])
+  audio     Audio   @relation(fields: [audioId], references: [id])
+  
+  @@unique([userId, audioId])
+  @@index([userId])
+}
+
+model Category {
+  id        Int      @id @default(autoincrement())
+  name      String
+  icon      String   @default("")
+  sort      Int      @default(0)
+  createdAt DateTime @default(now())
+  
+  audios    Audio[]  @relation("CategoryAudios")
+}
+
+model Comment {
+  id        Int      @id @default(autoincrement())
+  userId    Int
+  audioId   Int
+  content   String   @db.Text
+  rating    Int      // 1-5星
+  createdAt DateTime @default(now())
+  
+  user      User     @relation(fields: [userId], references: [id])
+  audio     Audio   @relation(fields: [audioId], references: [id])
+  
+  @@index([audioId])
+  @@index([userId])
+}

+ 3 - 1
server/src/app.ts

@@ -14,6 +14,7 @@ import audioRoutes from './modules/audio/audio.controller';
 import memberRoutes from './modules/member/member.controller';
 import shareRoutes from './modules/share/share.controller';
 import aiRoutes from './modules/ai/ai.controller';
+import playerRoutes from './modules/player/player.controller';
 
 const app = new Koa();
 const router = new Router();
@@ -42,6 +43,7 @@ router.use('/api/audio', audioRoutes.routes());
 router.use('/api/member', memberRoutes.routes());
 router.use('/api/share', shareRoutes.routes());
 router.use('/api/ai', aiRoutes.routes());
+router.use('/api/player', playerRoutes.routes());
 
 app.use(router.routes()).use(router.allowedMethods());
 
@@ -49,7 +51,7 @@ app.use(router.routes()).use(router.allowedMethods());
 async function start() {
   try {
     await connectDatabase();
-    console.log('✅ MongoDB 连接成功');
+    console.log('✅ MySQL 连接成功');
     
     app.listen(config.port, () => {
       console.log(`🚀 服务启动成功: http://localhost:${config.port}`);

+ 12 - 6
server/src/middleware/usageLimit.ts

@@ -1,7 +1,7 @@
 import { Context, Next } from 'koa';
-import { User } from '../models/User';
+import { prisma } from '../models';
 import { QuotaExceededError, ForbiddenError } from './errorHandler';
-import { MEMBER_QUOTA } from '../types';
+import { MEMBER_QUOTA, MemberLevel } from '../types';
 
 // 检查使用次数限制
 export async function usageLimitMiddleware(ctx: Context, next: Next): Promise<void> {
@@ -14,20 +14,26 @@ export async function usageLimitMiddleware(ctx: Context, next: Next): Promise<vo
     return;
   }
 
-  const user = await User.findById(userId);
+  const user = await prisma.user.findUnique({ where: { id: parseInt(userId) } });
   if (!user) {
     throw new ForbiddenError('用户不存在');
   }
 
   const today = new Date().toISOString().slice(0, 10);
-  const memberLevel = user.getEffectiveMemberLevel();
-  const quota = MEMBER_QUOTA[memberLevel];
+  const memberLevel = user.memberLevel;
+  const quota = MEMBER_QUOTA[memberLevel as MemberLevel];
 
   // 重置每日使用次数
   if (user.lastUsageDate !== today) {
+    await prisma.user.update({
+      where: { id: parseInt(userId) },
+      data: {
+        dailyUsage: 0,
+        lastUsageDate: today,
+      },
+    });
     user.dailyUsage = 0;
     user.lastUsageDate = today;
-    await user.save();
   }
 
   // 检查次数限制 (-1 表示无限制)

+ 0 - 39
server/src/models/Audio.ts

@@ -1,39 +0,0 @@
-import mongoose, { Schema, Document } from 'mongoose';
-import { IAudio, VoiceParams, AudioStatus } from '../types';
-
-export interface AudioDocument extends Omit<IAudio, '_id'>, Document {}
-
-const VoiceParamsSchema = new Schema<VoiceParams>({
-  speed: { type: Number, default: 1.0, min: 0.5, max: 2.0 },
-  pitch: { type: Number, default: 0, min: -500, max: 500 },
-  volume: { type: Number, default: 50, min: 0, max: 100 },
-});
-
-const AudioSchema = new Schema<AudioDocument>(
-  {
-    userId: { type: Schema.Types.ObjectId, ref: 'User', required: false },
-    title: { type: String, required: true },
-    text: { type: String, required: true },
-    summary: { type: String },
-    tags: [{ type: String }],
-    audioUrl: { type: String, required: true },
-    audioDuration: { type: Number, default: 0 },
-    audioSize: { type: Number, default: 0 },
-    wordCount: { type: Number, required: true },
-    voiceId: { type: String, required: true },
-    voiceParams: { type: VoiceParamsSchema, default: () => ({}) },
-    status: { 
-      type: String, 
-      enum: ['processing', 'completed', 'failed'] as AudioStatus[],
-      default: 'processing' 
-    },
-    isFavorite: { type: Boolean, default: false },
-  },
-  { timestamps: true }
-);
-
-// 索引
-AudioSchema.index({ userId: 1, createdAt: -1 });
-AudioSchema.index({ userId: 1, isFavorite: 1 });
-
-export const Audio = mongoose.model<AudioDocument>('Audio', AudioSchema);

+ 0 - 43
server/src/models/Order.ts

@@ -1,43 +0,0 @@
-import mongoose, { Schema, Document, Model } from 'mongoose';
-import { IOrder, ProductType, OrderStatus } from '../types';
-
-export interface OrderDocument extends Omit<IOrder, '_id'>, Document {
-  static: Model<OrderDocument> & {
-    generateOrderNo(): string;
-  };
-}
-
-const OrderSchema = new Schema<OrderDocument>(
-  {
-    userId: { type: Schema.Types.ObjectId, ref: 'User', required: true },
-    orderNo: { type: String, required: true, unique: true },
-    productType: { 
-      type: String, 
-      enum: ['monthly', 'yearly'] as ProductType[],
-      required: true 
-    },
-    amount: { type: Number, required: true },
-    status: { 
-      type: String, 
-      enum: ['pending', 'paid', 'failed', 'refunded'] as OrderStatus[],
-      default: 'pending' 
-    },
-    paymentMethod: { type: String },
-    paidAt: { type: Date },
-  },
-  { timestamps: true }
-);
-
-// 索引
-OrderSchema.index({ userId: 1, createdAt: -1 });
-OrderSchema.index({ orderNo: 1 });
-
-// 生成订单号
-OrderSchema.statics.generateOrderNo = function(): string {
-  const now = new Date();
-  const dateStr = now.toISOString().slice(0, 10).replace(/-/g, '');
-  const random = Math.random().toString(36).substring(2, 8).toUpperCase();
-  return `ORD${dateStr}${random}`;
-};
-
-export const Order = mongoose.model<OrderDocument>('Order', OrderSchema);

+ 0 - 35
server/src/models/User.ts

@@ -1,35 +0,0 @@
-import mongoose, { Schema, Document } from 'mongoose';
-import { IUser, MemberLevel } from '../types';
-
-export interface UserDocument extends Omit<IUser, '_id'>, Document {
-  isMemberValid(): boolean;
-  getEffectiveMemberLevel(): MemberLevel;
-}
-
-const UserSchema = new Schema<UserDocument>(
-  {
-    phone: { type: String, unique: true, sparse: true },
-    openid: { type: String, unique: true, sparse: true },
-    nickname: { type: String, default: '用户' },
-    avatar: { type: String, default: '' },
-    memberLevel: { type: Number, enum: [0, 1, 2], default: 0 as MemberLevel },
-    memberExpireAt: { type: Date },
-    dailyUsage: { type: Number, default: 0 },
-    lastUsageDate: { type: String, default: '' },
-  },
-  { timestamps: true }
-);
-
-// 检查会员是否有效
-UserSchema.methods.isMemberValid = function(): boolean {
-  if (this.memberLevel === 0) return false;
-  if (!this.memberExpireAt) return false;
-  return new Date() < this.memberExpireAt;
-};
-
-// 获取有效会员等级
-UserSchema.methods.getEffectiveMemberLevel = function(): MemberLevel {
-  return this.isMemberValid() ? this.memberLevel : 0;
-};
-
-export const User = mongoose.model<UserDocument>('User', UserSchema);

+ 6 - 9
server/src/models/index.ts

@@ -1,18 +1,15 @@
-import mongoose from 'mongoose';
-import { config } from '../config';
+import { PrismaClient } from '@prisma/client';
 
-export * from './User';
-export * from './Audio';
-export * from './Order';
+const prisma = new PrismaClient();
 
 export async function connectDatabase(): Promise<void> {
   try {
-    await mongoose.connect(config.mongodb.uri);
-    console.log('📦 MongoDB 连接成功');
+    await prisma.$connect();
+    console.log('📦 MySQL 连接成功');
   } catch (error) {
-    console.error('📦 MongoDB 连接失败:', error);
+    console.error('📦 MySQL 连接失败:', error);
     throw error;
   }
 }
 
-export { mongoose };
+export { prisma };

+ 96 - 66
server/src/modules/audio/audio.service.ts

@@ -1,7 +1,5 @@
-import { Audio, AudioDocument } from '../../models/Audio';
-import { User } from '../../models/User';
-import { PaginationResult } from '../../types';
-import mongoose from 'mongoose';
+import { prisma } from '../../models';
+import { PaginationResult, VoiceParams } from '../../types';
 
 // 获取用户音频列表
 export async function getAudioList(
@@ -13,40 +11,41 @@ export async function getAudioList(
     keyword?: string;
     category?: string;
   }
-): Promise<PaginationResult<AudioDocument>> {
+): Promise<PaginationResult<any>> {
   const { page = 1, pageSize = 10, isFavorite, keyword, category } = options;
 
   // 构建查询条件
-  const query: Record<string, unknown> = {};
+  const where: Record<string, any> = {};
 
   // 只有提供了有效的 userId 才添加用户过滤条件
-  if (userId && mongoose.Types.ObjectId.isValid(userId)) {
-    query.userId = userId;
+  if (userId) {
+    where.userId = parseInt(userId);
   }
-  // 如果没有 userId 或 userId 无效,返回所有音频(匿名用户可以看到所有)
 
   if (isFavorite !== undefined) {
-    query.isFavorite = isFavorite;
+    where.isFavorite = isFavorite;
   }
 
   if (keyword) {
-    query.$or = [
-      { title: { $regex: keyword, $options: 'i' } },
-      { text: { $regex: keyword, $options: 'i' } },
+    where.OR = [
+      { title: { contains: keyword } },
+      { text: { contains: keyword } },
     ];
   }
 
   if (category) {
-    query.category = category;
+    where.category = category;
   }
 
-  const total = await Audio.countDocuments(query);
+  const total = await prisma.audio.count({ where });
   const totalPages = Math.ceil(total / pageSize);
 
-  const list = await Audio.find(query)
-    .sort({ createdAt: -1 })
-    .skip((page - 1) * pageSize)
-    .limit(pageSize);
+  const list = await prisma.audio.findMany({
+    where,
+    orderBy: { createdAt: 'desc' },
+    skip: (page - 1) * pageSize,
+    take: pageSize,
+  });
 
   return {
     list,
@@ -58,41 +57,61 @@ export async function getAudioList(
 }
 
 // 获取单个音频
-export async function getAudioById(audioId: string, userId: string): Promise<AudioDocument | null> {
-  // 如果没有有效的 userId,只需要验证 audioId
-  if (!mongoose.Types.ObjectId.isValid(audioId)) {
+export async function getAudioById(audioId: string, userId: string): Promise<any | null> {
+  const id = parseInt(audioId);
+  if (isNaN(id)) {
     return null;
   }
   // 如果有有效的 userId,添加用户过滤
-  if (userId && mongoose.Types.ObjectId.isValid(userId)) {
-    return Audio.findOne({ _id: audioId, userId });
+  if (userId) {
+    return prisma.audio.findFirst({
+      where: { id, userId: parseInt(userId) },
+    });
   }
   // 没有 userId,返回该音频(允许匿名访问)
-  return Audio.findOne({ _id: audioId });
+  return prisma.audio.findUnique({ where: { id } });
 }
 
 // 删除音频
 export async function deleteAudio(audioId: string, userId: string): Promise<boolean> {
-  // 验证 userId 是否为有效的 ObjectId
-  if (!userId || !mongoose.Types.ObjectId.isValid(userId)) {
+  // 验证 userId 是否为有效
+  if (!userId) {
+    return false;
+  }
+  const id = parseInt(audioId);
+  if (isNaN(id)) {
+    return false;
+  }
+  try {
+    await prisma.audio.delete({
+      where: { id, userId: parseInt(userId) },
+    });
+    return true;
+  } catch {
     return false;
   }
-  const result = await Audio.findOneAndDelete({ _id: audioId, userId });
-  return !!result;
 }
 
 // 切换收藏状态
-export async function toggleFavorite(audioId: string, userId: string): Promise<AudioDocument | null> {
-  // 验证 userId 是否为有效的 ObjectId
-  if (!userId || !mongoose.Types.ObjectId.isValid(userId)) {
+export async function toggleFavorite(audioId: string, userId: string): Promise<any | null> {
+  // 验证 userId 是否为有效
+  if (!userId) {
     return null;
   }
-  const audio = await Audio.findOne({ _id: audioId, userId });
+  const id = parseInt(audioId);
+  if (isNaN(id)) {
+    return null;
+  }
+  const audio = await prisma.audio.findFirst({
+    where: { id, userId: parseInt(userId) },
+  });
   if (!audio) return null;
 
-  audio.isFavorite = !audio.isFavorite;
-  await audio.save();
-  return audio;
+  const updated = await prisma.audio.update({
+    where: { id: audio.id },
+    data: { isFavorite: !audio.isFavorite },
+  });
+  return updated;
 }
 
 // 更新音频信息
@@ -100,19 +119,29 @@ export async function updateAudio(
   audioId: string,
   userId: string,
   data: { title?: string; tags?: string[] }
-): Promise<AudioDocument | null> {
-  // 验证 userId 是否为有效的 ObjectId
-  if (!userId || !mongoose.Types.ObjectId.isValid(userId)) {
+): Promise<any | null> {
+  // 验证 userId 是否为有效
+  if (!userId) {
+    return null;
+  }
+  const id = parseInt(audioId);
+  if (isNaN(id)) {
     return null;
   }
-  const audio = await Audio.findOne({ _id: audioId, userId });
+  const audio = await prisma.audio.findFirst({
+    where: { id, userId: parseInt(userId) },
+  });
   if (!audio) return null;
 
-  if (data.title) audio.title = data.title;
-  if (data.tags) audio.tags = data.tags;
-  await audio.save();
+  const updated = await prisma.audio.update({
+    where: { id: audio.id },
+    data: {
+      ...(data.title && { title: data.title }),
+      ...(data.tags && { tags: JSON.stringify(data.tags) }),
+    },
+  });
 
-  return audio;
+  return updated;
 }
 
 // 获取用户使用统计
@@ -122,34 +151,35 @@ export async function getUserStats(userId: string): Promise<{
   totalWords: number;
   favoriteCount: number;
 }> {
-  // 验证 userId 是否为有效的 ObjectId
-  if (!userId || !mongoose.Types.ObjectId.isValid(userId)) {
+  // 验证 userId 是否为有效
+  if (!userId) {
     return { totalAudios: 0, totalDuration: 0, totalWords: 0, favoriteCount: 0 };
   }
 
-  const stats = await Audio.aggregate([
-    { $match: { userId: userId } },
-    {
-      $group: {
-        _id: null,
-        totalAudios: { $sum: 1 },
-        totalDuration: { $sum: '$audioDuration' },
-        totalWords: { $sum: '$wordCount' },
-        favoriteCount: {
-          $sum: { $cond: [{ $eq: ['$isFavorite', true] }, 1, 0] },
-        },
-      },
-    },
-  ]);
-
-  if (stats.length === 0) {
+  const uid = parseInt(userId);
+  if (isNaN(uid)) {
     return { totalAudios: 0, totalDuration: 0, totalWords: 0, favoriteCount: 0 };
   }
 
+  const totalAudios = await prisma.audio.count({ where: { userId: uid } });
+  
+  const allAudios = await prisma.audio.findMany({
+    where: { userId: uid },
+    select: {
+      audioDuration: true,
+      wordCount: true,
+      isFavorite: true,
+    },
+  });
+
+  const totalDuration = allAudios.reduce((sum, a) => sum + a.audioDuration, 0);
+  const totalWords = allAudios.reduce((sum, a) => sum + a.wordCount, 0);
+  const favoriteCount = allAudios.filter(a => a.isFavorite).length;
+
   return {
-    totalAudios: stats[0].totalAudios,
-    totalDuration: stats[0].totalDuration,
-    totalWords: stats[0].totalWords,
-    favoriteCount: stats[0].favoriteCount,
+    totalAudios,
+    totalDuration,
+    totalWords,
+    favoriteCount,
   };
 }

+ 11 - 8
server/src/modules/auth/auth.controller.ts

@@ -3,6 +3,7 @@ import { Context } from 'koa';
 import * as AuthService from './auth.service';
 import { BadRequestError } from '../../middleware/errorHandler';
 import { authMiddleware } from '../../middleware/auth';
+import { prisma } from '../../models';
 
 const router = new Router();
 
@@ -68,25 +69,27 @@ router.put('/user-info', authMiddleware, async (ctx: Context) => {
   const userId = ctx.state.user.userId;
   const { nickname, avatar } = ctx.request.body as { nickname?: string; avatar?: string };
 
-  const user = await User.findById(userId);
+  const user = await prisma.user.findUnique({ where: { id: parseInt(userId) } });
   if (!user) {
     throw new BadRequestError('用户不存在');
   }
 
-  if (nickname) user.nickname = nickname;
-  if (avatar) user.avatar = avatar;
-  await user.save();
+  const updatedUser = await prisma.user.update({
+    where: { id: parseInt(userId) },
+    data: {
+      ...(nickname && { nickname }),
+      ...(avatar && { avatar }),
+    },
+  });
 
   ctx.body = {
     code: 0,
     message: '更新成功',
     data: {
-      nickname: user.nickname,
-      avatar: user.avatar,
+      nickname: updatedUser.nickname,
+      avatar: updatedUser.avatar,
     },
   };
 });
 
-import { User } from '../../models/User';
-
 export default router;

+ 17 - 15
server/src/modules/auth/auth.service.ts

@@ -1,7 +1,7 @@
 import jwt from 'jsonwebtoken';
 import { v4 as uuidv4 } from 'uuid';
 import { config } from '../../config';
-import { User } from '../../models/User';
+import { prisma } from '../../models';
 import { JwtPayload } from '../../types';
 
 // 验证码存储(生产环境应使用 Redis)
@@ -66,31 +66,33 @@ export async function loginWithPhone(phone: string, code?: string): Promise<{
   }
 
   // 查找或创建用户
-  let user = await User.findOne({ phone });
+  let user = await prisma.user.findFirst({ where: { phone } });
   let isNewUser = false;
 
   if (!user) {
-    user = await User.create({
-      phone,
-      nickname: `用户${phone.slice(-4)}`,
-      avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${phone}`,
-      memberLevel: 0,
-      dailyUsage: 0,
-      lastUsageDate: '',
+    user = await prisma.user.create({
+      data: {
+        phone,
+        nickname: `用户${phone.slice(-4)}`,
+        avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${phone}`,
+        memberLevel: 0,
+        dailyUsage: 0,
+        lastUsageDate: '',
+      },
     });
     isNewUser = true;
   }
 
-  const token = generateToken(user._id.toString(), phone);
+  const token = generateToken(user.id.toString(), phone);
 
   return {
     token,
     user: {
-      id: user._id.toString(),
+      id: user.id.toString(),
       phone: user.phone!,
       nickname: user.nickname,
       avatar: user.avatar,
-      memberLevel: user.getEffectiveMemberLevel(),
+      memberLevel: user.memberLevel,
       isNewUser,
     },
   };
@@ -98,17 +100,17 @@ export async function loginWithPhone(phone: string, code?: string): Promise<{
 
 // 获取用户信息
 export async function getUserInfo(userId: string) {
-  const user = await User.findById(userId);
+  const user = await prisma.user.findUnique({ where: { id: parseInt(userId) } });
   if (!user) {
     throw new Error('用户不存在');
   }
 
   return {
-    id: user._id.toString(),
+    id: user.id.toString(),
     phone: user.phone,
     nickname: user.nickname,
     avatar: user.avatar,
-    memberLevel: user.getEffectiveMemberLevel(),
+    memberLevel: user.memberLevel,
     memberExpireAt: user.memberExpireAt,
     dailyUsage: user.dailyUsage,
   };

+ 58 - 31
server/src/modules/member/member.service.ts

@@ -1,5 +1,4 @@
-import { User } from '../../models/User';
-import { Order } from '../../models/Order';
+import { prisma } from '../../models';
 import { MemberLevel, MEMBER_QUOTA } from '../../types';
 
 // 会员价格
@@ -39,26 +38,28 @@ export function getMemberBenefits() {
 
 // 获取用户会员状态
 export async function getMemberStatus(userId: string) {
-  const user = await User.findById(userId);
+  const user = await prisma.user.findUnique({ where: { id: parseInt(userId) } });
   if (!user) {
     throw new Error('用户不存在');
   }
 
   const today = new Date().toISOString().slice(0, 10);
   let dailyUsage = user.dailyUsage;
-  const memberLevel = user.getEffectiveMemberLevel();
-  const quota = MEMBER_QUOTA[memberLevel];
+  const memberLevel = user.memberLevel;
+  const quota = MEMBER_QUOTA[memberLevel as MemberLevel];
 
   // 重置每日使用次数
   if (user.lastUsageDate !== today) {
     dailyUsage = 0;
   }
 
+  const isValid = memberLevel > 0 && user.memberExpireAt && new Date() < user.memberExpireAt;
+
   return {
     level: memberLevel,
     levelName: ['免费版', '月度会员', '年度会员'][memberLevel],
     expireAt: user.memberExpireAt,
-    isValid: user.isMemberValid(),
+    isValid,
     quota: {
       dailyLimit: quota.dailyLimit,
       dailyUsed: dailyUsage,
@@ -77,25 +78,29 @@ export async function createOrder(
   amount: number;
 }> {
   const priceInfo = MEMBER_PRICES[productType];
-  const orderNo = (Order as any).generateOrderNo();
-
-  const order = await Order.create({
-    userId,
-    orderNo,
-    productType,
-    amount: priceInfo.price,
-    status: 'pending',
+  const orderNo = generateOrderNo();
+
+  const order = await prisma.order.create({
+    data: {
+      userId: parseInt(userId),
+      orderNo,
+      productType,
+      amount: priceInfo.price,
+      status: 'pending',
+    },
   });
 
   return {
     orderNo: order.orderNo,
-    amount: order.amount,
+    amount: Number(order.amount),
   };
 }
 
 // 模拟支付成功
 export async function mockPaymentSuccess(orderNo: string, userId: string) {
-  const order = await Order.findOne({ orderNo, userId });
+  const order = await prisma.order.findFirst({ 
+    where: { orderNo, userId: parseInt(userId) } 
+  });
   if (!order) {
     throw new Error('订单不存在');
   }
@@ -105,13 +110,17 @@ export async function mockPaymentSuccess(orderNo: string, userId: string) {
   }
 
   // 更新订单状态
-  order.status = 'paid';
-  order.paymentMethod = 'mock';
-  order.paidAt = new Date();
-  await order.save();
+  await prisma.order.update({
+    where: { id: order.id },
+    data: {
+      status: 'paid',
+      paymentMethod: 'mock',
+      paidAt: new Date(),
+    },
+  });
 
   // 更新用户会员状态
-  const user = await User.findById(userId);
+  const user = await prisma.user.findUnique({ where: { id: parseInt(userId) } });
   if (!user) {
     throw new Error('用户不存在');
   }
@@ -124,24 +133,34 @@ export async function mockPaymentSuccess(orderNo: string, userId: string) {
     ? user.memberExpireAt
     : now;
 
-  user.memberLevel = order.productType === 'yearly' ? 2 : 1;
-  user.memberExpireAt = new Date(startDate.getTime() + days * 24 * 60 * 60 * 1000);
-  await user.save();
+  const memberLevel = order.productType === 'yearly' ? 2 : 1;
+  const memberExpireAt = new Date(startDate.getTime() + days * 24 * 60 * 60 * 1000);
+
+  await prisma.user.update({
+    where: { id: parseInt(userId) },
+    data: {
+      memberLevel,
+      memberExpireAt,
+    },
+  });
 
   return {
     success: true,
-    memberLevel: user.memberLevel,
-    memberExpireAt: user.memberExpireAt,
+    memberLevel,
+    memberExpireAt,
   };
 }
 
 // 获取订单列表
 export async function getOrders(userId: string, page: number = 1, pageSize: number = 10) {
-  const total = await Order.countDocuments({ userId });
-  const list = await Order.find({ userId })
-    .sort({ createdAt: -1 })
-    .skip((page - 1) * pageSize)
-    .limit(pageSize);
+  const uid = parseInt(userId);
+  const total = await prisma.order.count({ where: { userId: uid } });
+  const list = await prisma.order.findMany({
+    where: { userId: uid },
+    orderBy: { createdAt: 'desc' },
+    skip: (page - 1) * pageSize,
+    take: pageSize,
+  });
 
   return {
     list,
@@ -150,4 +169,12 @@ export async function getOrders(userId: string, page: number = 1, pageSize: numb
     pageSize,
     totalPages: Math.ceil(total / pageSize),
   };
+}
+
+// 生成订单号
+function generateOrderNo(): string {
+  const now = new Date();
+  const dateStr = now.toISOString().slice(0, 10).replace(/-/g, '');
+  const random = Math.random().toString(36).substring(2, 8).toUpperCase();
+  return `ORD${dateStr}${random}`;
 }

+ 61 - 0
server/src/modules/player/player.controller.ts

@@ -0,0 +1,61 @@
+import Router from '@koa/router';
+import { Context } from 'koa';
+import * as PlayerService from './player.service';
+import { BadRequestError } from '../../middleware/errorHandler';
+import { authMiddleware } from '../../middleware/auth';
+
+const router = new Router({ prefix: '/api/player' });
+
+// 获取播放进度列表
+router.get('/progress', authMiddleware, async (ctx: Context) => {
+  const userId = ctx.state.user.userId;
+  const { audioId } = ctx.query;
+
+  const records = await PlayerService.getPlayProgress(
+    userId,
+    audioId ? parseInt(audioId as string) : undefined
+  );
+
+  ctx.body = {
+    code: 0,
+    message: 'success',
+    data: records,
+  };
+});
+
+// 保存播放进度
+router.post('/progress', authMiddleware, async (ctx: Context) => {
+  const userId = ctx.state.user.userId;
+  const { audioId, progress, duration } = ctx.request.body as {
+    audioId: number;
+    progress: number;
+    duration: number;
+  };
+
+  if (!audioId || typeof progress !== 'number' || typeof duration !== 'number') {
+    throw new BadRequestError('参数错误');
+  }
+
+  const record = await PlayerService.savePlayProgress(userId, audioId, progress, duration);
+
+  ctx.body = {
+    code: 0,
+    message: 'success',
+    data: record,
+  };
+});
+
+// 删除播放记录
+router.delete('/progress/:audioId', authMiddleware, async (ctx: Context) => {
+  const userId = ctx.state.user.userId;
+  const audioId = parseInt(ctx.params.audioId);
+
+  await PlayerService.deletePlayRecord(userId, audioId);
+
+  ctx.body = {
+    code: 0,
+    message: '删除成功',
+  };
+});
+
+export default router;

+ 106 - 0
server/src/modules/player/player.service.ts

@@ -0,0 +1,106 @@
+import { PrismaClient } from '@prisma/client';
+
+const prisma = new PrismaClient();
+
+/**
+ * 获取用户播放记录
+ */
+export async function getPlayProgress(userId: number, audioId?: number) {
+  const where: any = { userId };
+  if (audioId) {
+    where.audioId = audioId;
+  }
+
+  const records = await prisma.playRecord.findMany({
+    where,
+    include: {
+      audio: {
+        select: {
+          id: true,
+          title: true,
+          audioUrl: true,
+          audioDuration: true,
+        },
+      },
+    },
+    orderBy: { updatedAt: 'desc' },
+  });
+
+  return records;
+}
+
+/**
+ * 保存播放进度
+ */
+export async function savePlayProgress(
+  userId: number,
+  audioId: number,
+  progress: number,
+  duration: number
+) {
+  // 使用 upsert 语义:如果不存在则创建,存在则更新
+  const existing = await prisma.playRecord.findUnique({
+    where: {
+      userId_audioId: {
+        userId,
+        audioId,
+      },
+    },
+  });
+
+  if (existing) {
+    // 更新
+    return await prisma.playRecord.update({
+      where: {
+        userId_audioId: {
+          userId,
+          audioId,
+        },
+      },
+      data: {
+        progress,
+        duration,
+      },
+    });
+  } else {
+    // 创建
+    return await prisma.playRecord.create({
+      data: {
+        userId,
+        audioId,
+        progress,
+        duration,
+      },
+    });
+  }
+}
+
+/**
+ * 删除播放记录
+ */
+export async function deletePlayRecord(userId: number, audioId: number) {
+  return await prisma.playRecord.delete({
+    where: {
+      userId_audioId: {
+        userId,
+        audioId,
+      },
+    },
+  });
+}
+
+/**
+ * 获取单个音频的播放进度
+ */
+export async function getSingleProgress(userId: number, audioId: number) {
+  const record = await prisma.playRecord.findUnique({
+    where: {
+      userId_audioId: {
+        userId,
+        audioId,
+      },
+    },
+  });
+
+  return record;
+}

+ 2 - 2
server/src/modules/share/share.service.ts

@@ -1,5 +1,5 @@
 import { v4 as uuidv4 } from 'uuid';
-import { Audio } from '../../models/Audio';
+import { prisma } from '../../models';
 
 /**
  * 分享服务
@@ -26,7 +26,7 @@ export class ShareService {
    * @param audioId 音频 ID
    */
   async generateShareCard(audioId: string) {
-    const audio = await Audio.findById(audioId);
+    const audio = await prisma.audio.findUnique({ where: { id: parseInt(audioId) } });
     
     if (!audio) {
       throw new Error('音频不存在');

+ 8 - 7
server/src/modules/tts/tts.controller.ts

@@ -4,8 +4,7 @@ import * as TtsService from './tts.service';
 import { BadRequestError } from '../../middleware/errorHandler';
 import { optionalAuth } from '../../middleware/auth';
 import { usageLimitMiddleware, checkWordLimit } from '../../middleware/usageLimit';
-import { User } from '../../models/User';
-import { Audio } from '../../models/Audio';
+import { prisma } from '../../models';
 
 const router = new Router();
 
@@ -20,10 +19,10 @@ router.get('/voices', async (ctx: Context) => {
   };
 });
 
-// 测试 MongoDB 连接
+// 测试 MySQL 连接
 router.get('/test-db', async (ctx: Context) => {
   try {
-    const count = await Audio.countDocuments();
+    const count = await prisma.audio.count();
     ctx.body = {
       code: 0,
       message: 'success',
@@ -79,10 +78,12 @@ router.post(
 
     // 如果用户已登录,更新使用次数
     if (userId) {
-      const user = await User.findById(userId);
+      const user = await prisma.user.findUnique({ where: { id: parseInt(userId) } });
       if (user) {
-        user.dailyUsage += 1;
-        await user.save();
+        await prisma.user.update({
+          where: { id: parseInt(userId) },
+          data: { dailyUsage: user.dailyUsage + 1 },
+        });
       }
     }
 

+ 18 - 16
server/src/modules/tts/tts.service.ts

@@ -2,7 +2,7 @@ import path from 'path';
 import fs from 'fs';
 import { v4 as uuidv4 } from 'uuid';
 import { config } from '../../config';
-import { Audio } from '../../models/Audio';
+import { prisma } from '../../models';
 import { VoiceParams, Voice } from '../../types';
 import { AliyunTtsProvider } from './aliyun.provider';
 import { MockTtsProvider } from './mock.provider';
@@ -197,28 +197,30 @@ export async function generateAudio(
 
   let audio;
   try {
-    audio = await Audio.create({
-      userId,
-      title,
-      text,
-      summary,
-      tags,
-      audioUrl: finalAudioUrl,
-      audioDuration: duration,
-      audioSize: size,
-      wordCount: text.length,
-      voiceId,
-      voiceParams,
-      status: 'completed',
+    audio = await prisma.audio.create({
+      data: {
+        userId: userId ? parseInt(userId) : undefined,
+        title,
+        text,
+        summary,
+        tags: JSON.stringify(tags),
+        audioUrl: finalAudioUrl,
+        audioDuration: duration,
+        audioSize: size,
+        wordCount: text.length,
+        voiceId,
+        voiceParams: JSON.stringify(voiceParams),
+        status: 'completed',
+      },
     });
-    console.log('✅ 音频记录创建成功:', audio._id);
+    console.log('✅ 音频记录创建成功:', audio.id);
   } catch (error) {
     console.error('❌ 音频记录创建失败:', error);
     throw error;
   }
 
   return {
-    audioId: audio._id.toString(),
+    audioId: audio.id.toString(),
     audioUrl: audio.audioUrl,
     duration,
     size,

+ 5 - 6
server/src/types/index.ts

@@ -1,9 +1,8 @@
 import { DefaultContext, DefaultState } from 'koa';
-import { ObjectId } from 'mongoose';
 
 // 用户相关类型
 export interface IUser {
-  _id: ObjectId;
+  id: number;
   phone?: string;
   openid?: string;
   nickname: string;
@@ -20,8 +19,8 @@ export type MemberLevel = 0 | 1 | 2; // 0免费 1月度 2年度
 
 // 音频相关类型
 export interface IAudio {
-  _id: ObjectId;
-  userId: ObjectId;
+  id: number;
+  userId: number;
   title: string;
   text: string;
   summary?: string;
@@ -48,8 +47,8 @@ export type AudioStatus = 'processing' | 'completed' | 'failed';
 
 // 订单相关类型
 export interface IOrder {
-  _id: ObjectId;
-  userId: ObjectId;
+  id: number;
+  userId: number;
   orderNo: string;
   productType: ProductType;
   amount: number;

+ 30 - 0
server/test-ai.js

@@ -0,0 +1,30 @@
+const axios = require('axios');
+
+const BASE_URL = 'http://localhost:3000';
+
+async function testAIModels() {
+  console.log('=== 测试 AI 模型接口 ===\n');
+
+  try {
+    console.log('1️⃣  测试获取 AI 模型列表...');
+    const result = await axios.get(`${BASE_URL}/api/ai/models`);
+    console.log('✅ AI 模型列表:', JSON.stringify(result.data, null, 2));
+
+    console.log('\n2️⃣  测试 AI 文本生成...');
+    const generate = await axios.post(`${BASE_URL}/api/ai/generate`, {
+      prompt: '写一首关于春天的短诗',
+      model: 'tongyi-xiaomi-analysis-pro'
+    }, {
+      timeout: 60000
+    });
+    console.log('✅ AI 生成成功:', JSON.stringify(generate.data, null, 2));
+
+  } catch (error) {
+    console.error('❌ 测试失败:', error.message);
+    if (error.response) {
+      console.error('📋 错误响应:', JSON.stringify(error.response.data, null, 2));
+    }
+  }
+}
+
+testAIModels();

+ 100 - 0
server/test-api.js

@@ -0,0 +1,100 @@
+const axios = require('axios');
+
+const BASE_URL = 'http://localhost:3000';
+
+async function testAPI() {
+  console.log('=== 开始测试 API 接口 ===\n');
+
+  // 1. 测试健康检查
+  try {
+    console.log('1️⃣  测试健康检查...');
+    const health = await axios.get(`${BASE_URL}/health`);
+    console.log('✅ 健康检查:', health.data);
+  } catch (error) {
+    console.error('❌ 健康检查失败:', error.message);
+  }
+
+  // 2. 测试数据库连接
+  try {
+    console.log('\n2️⃣  测试数据库连接...');
+    const db = await axios.get(`${BASE_URL}/api/tts/test-db`);
+    console.log('✅ 数据库连接:', db.data);
+  } catch (error) {
+    console.error('❌ 数据库连接失败:', error.message);
+  }
+
+  // 3. 测试发送验证码
+  try {
+    console.log('\n3️⃣  测试发送验证码...');
+    const sendCode = await axios.post(`${BASE_URL}/api/auth/send-code`, {
+      phone: '13800138000'
+    });
+    console.log('✅ 发送验证码:', sendCode.data);
+  } catch (error) {
+    console.error('❌ 发送验证码失败:', error.message);
+  }
+
+  // 4. 测试登录(开发环境免密)
+  try {
+    console.log('\n4️⃣  测试登录...');
+    const login = await axios.post(`${BASE_URL}/api/auth/login`, {
+      phone: '13800138000',
+      code: '123456'
+    });
+    console.log('✅ 登录成功:', login.data);
+    
+    const token = login.data.data.token;
+    const userId = login.data.data.user.id;
+    console.log('📝 Token:', token.substring(0, 50) + '...');
+    console.log('📝 用户ID:', userId);
+
+    // 5. 测试获取用户信息
+    try {
+      console.log('\n5️⃣  测试获取用户信息...');
+      const userInfo = await axios.get(`${BASE_URL}/api/auth/user-info`, {
+        headers: { Authorization: `Bearer ${token}` }
+      });
+      console.log('✅ 用户信息:', userInfo.data);
+    } catch (error) {
+      console.error('❌ 获取用户信息失败:', error.message);
+    }
+
+    // 6. 测试获取音色列表
+    try {
+      console.log('\n6️⃣  测试获取音色列表...');
+      const voices = await axios.get(`${BASE_URL}/api/tts/voices`);
+      console.log('✅ 音色列表:', voices.data);
+    } catch (error) {
+      console.error('❌ 获取音色列表失败:', error.message);
+    }
+
+    // 7. 测试获取音频列表
+    try {
+      console.log('\n7️⃣  测试获取音频列表...');
+      const audios = await axios.get(`${BASE_URL}/api/audio/list`, {
+        headers: { Authorization: `Bearer ${token}` }
+      });
+      console.log('✅ 音频列表:', audios.data);
+    } catch (error) {
+      console.error('❌ 获取音频列表失败:', error.message);
+    }
+
+    // 8. 测试会员状态
+    try {
+      console.log('\n8️⃣  测试会员状态...');
+      const member = await axios.get(`${BASE_URL}/api/member/status`, {
+        headers: { Authorization: `Bearer ${token}` }
+      });
+      console.log('✅ 会员状态:', member.data);
+    } catch (error) {
+      console.error('❌ 获取会员状态失败:', error.message);
+    }
+
+  } catch (error) {
+    console.error('❌ 登录失败:', error.message);
+  }
+
+  console.log('\n=== API 测试完成 ===');
+}
+
+testAPI();

+ 46 - 0
server/test-tts.js

@@ -0,0 +1,46 @@
+const axios = require('axios');
+
+const BASE_URL = 'http://localhost:3000';
+
+async function testTTSGenerate() {
+  console.log('=== 测试 TTS 音频生成 ===\n');
+
+  try {
+    // 1. 先登录获取 token
+    console.log('1️⃣  登录获取 Token...');
+    const login = await axios.post(`${BASE_URL}/api/auth/login`, {
+      phone: '13800138000',
+      code: '123456'
+    });
+    const token = login.data.data.token;
+    console.log('✅ 登录成功,用户ID:', login.data.data.user.id);
+
+    // 2. 生成音频
+    console.log('\n2️⃣  生成音频...');
+    const result = await axios.post(`${BASE_URL}/api/tts/generate`, {
+      text: '你好',
+      voiceId: 'cherry',
+      voiceParams: {
+        speed: 1,
+        pitch: 0,
+        volume: 50
+      }
+    }, {
+      headers: {
+        Authorization: `Bearer ${token}`
+      },
+      timeout: 60000 // 60秒超时
+    });
+
+    console.log('✅ 音频生成成功!');
+    console.log('📊 返回数据:', JSON.stringify(result.data, null, 2));
+
+  } catch (error) {
+    console.error('❌ 测试失败:', error.message);
+    if (error.response) {
+      console.error('📋 错误响应:', JSON.stringify(error.response.data, null, 2));
+    }
+  }
+}
+
+testTTSGenerate();

+ 354 - 0
开发计划-第二期.md

@@ -0,0 +1,354 @@
+# AI有声书生成工具 - 第二期开发计划
+
+## 产品定位
+
+基于MVP阶段的文本转语音核心功能,第二期聚焦**体验增强**,提升用户粘性和付费转化。
+
+## 功能优先级
+
+### P1 - 核心体验(第一阶段)
+
+| 功能 | 描述 | 前端 | 后端 | 测试重点 |
+|------|------|------|------|----------|
+| 播放记录 | 记录播放进度,支持续播 | player页面 | API | 续播准确性 |
+| 收藏功能 | 收藏喜欢的音频 | favorites页面 | API | 收藏列表CRUD |
+| 定时关闭 | 睡眠定时、15/30/60分钟自动停止 | player页面 | - | 定时器准确性 |
+| 播放速度记忆 | 0.5x-2x速听,记忆用户偏好 | player页面 | API | 偏好持久化 |
+| 分享功能 | 分享给微信好友/朋友圈 | player页面 | API | 分享卡片生成 |
+| 加载优化 | 骨架屏、缓存优化 | 全局 | - | 首屏加载速度 |
+
+### P2 - 丰富功能(第二阶段)
+
+| 功能 | 描述 | 前端 | 后端 | 测试重点 |
+|------|------|------|------|----------|
+| 全局搜索 | 搜索有声书标题、历史记录 | search页面 | API | 搜索相关性 |
+| 内容分类 | 按类型筛选(故事/小说/新闻/学习) | index页面 | API | 分类准确性 |
+| 音质选择 | 高/标准音质切换 | player页面 | API | 音质切换生效 |
+| 离线缓存 | 下载到本地离线听 | favorites页面 | API | 离线播放 |
+| 断点续传 | 大文件下载断点续传 | 全局 | API | 续传完整性 |
+| 数据同步 | 多端数据实时同步 | 全局 | API | 同步一致性 |
+
+### P3 - 完善优化(第三阶段)
+
+| 功能 | 描述 | 前端 | 后端 | 测试重点 |
+|------|------|------|------|----------|
+| 评论打分 | 对内容评论评分 | player页面 | API | 评论展示 |
+| 消息通知 | 新内容推送、会员优惠 | 全局 | API | 推送到达率 |
+| 个性化主题 | 深色/浅色主题切换 | settings页面 | - | 主题切换流畅 |
+
+---
+
+## 页面规划
+
+### 新增页面
+
+```
+pages/
+├── search/          # 搜索页面
+│   └── index.vue     - 搜索框、搜索历史、热门推荐、搜索结果列表
+├── favorites/       # 收藏页面
+│   └── index.vue    - 收藏列表、离线缓存管理
+└── settings/        # 设置页面
+    └── index.vue    - 主题设置、通知设置、清理缓存、关于我们
+```
+
+### 修改页面
+
+```
+pages/
+├── index/           # 首页
+│   └── index.vue     - 增加分类标签、推荐内容
+├── player/          # 播放器
+│   └── index.vue     - 增加定时关闭、音质选择、分享、收藏按钮
+├── history/         # 历史记录
+│   └── index.vue     - 增加搜索入口、收藏入口、分类筛选
+└── mine/            # 个人中心
+    └── index.vue     - 增加设置入口、收藏入口
+```
+
+---
+
+## API设计
+
+### 播放记录
+
+```
+GET    /api/player/progress      - 获取播放进度
+POST   /api/player/progress      - 保存播放进度
+DELETE /api/player/progress/:id  - 删除播放记录
+```
+
+### 收藏
+
+```
+GET    /api/favorites            - 获取收藏列表
+POST   /api/favorites            - 添加收藏
+DELETE /api/favorites/:id        - 取消收藏
+```
+
+### 搜索
+
+```
+GET    /api/search?q=关键词      - 搜索音频
+GET    /api/categories            - 获取分类列表
+GET    /api/categories/:id       - 获取分类下的音频
+```
+
+### 离线缓存
+
+```
+GET    /api/downloads            - 获取下载列表
+POST   /api/downloads/check       - 检查下载状态
+```
+
+### 评论
+
+```
+GET    /api/comments/:audioId    - 获取评论列表
+POST   /api/comments             - 发表评论
+```
+
+### 用户偏好
+
+```
+GET    /api/user/preferences     - 获取用户偏好
+PUT    /api/user/preferences     - 更新用户偏好
+```
+
+---
+
+## 数据库变更
+
+### 新增表
+
+**play_records** - 播放记录表
+```prisma
+model PlayRecord {
+  id        String   @id @default(uuid())
+  userId    String
+  audioId   String
+  progress  Float    // 播放进度(秒)
+  duration  Float    // 总时长
+  updatedAt DateTime @updatedAt
+  createdAt DateTime @default(now())
+  
+  @@index([userId])
+}
+```
+
+**favorites** - 收藏表
+```prisma
+model Favorite {
+  id        String   @id @default(uuid())
+  userId    String
+  audioId   String
+  createdAt DateTime @default(now())
+  
+  @@unique([userId, audioId])
+  @@index([userId])
+}
+```
+
+**categories** - 分类表
+```prisma
+model Category {
+  id        String   @id @default(uuid())
+  name      String
+  icon      String
+  sort      Int
+  createdAt DateTime @default(now())
+}
+```
+
+**comments** - 评论表
+```prisma
+model Comment {
+  id        String   @id @default(uuid())
+  userId    String
+  audioId   String
+  content   String
+  rating    Int      // 1-5星
+  createdAt DateTime @default(now())
+  
+  @@index([audioId])
+}
+```
+
+**user_preferences** - 用户偏好表
+```prisma
+model UserPreference {
+  id        String   @id @default(uuid())
+  userId    String   @unique
+  playSpeed Float    @default(1.0)
+  quality   String   @default("standard") // standard, high
+  theme     String   @default("light")
+  updatedAt DateTime @updatedAt
+}
+```
+
+### 修改表
+
+**audios** - 增加分类字段
+```prisma
+model Audio {
+  // ... 现有字段
+  categoryId String?
+  category    Category? @relation(fields: [categoryId], references: [id])
+}
+```
+
+---
+
+## 技术方案
+
+### 1. 播放记录与续播
+
+**前端**:
+- 使用Pinia管理播放状态
+- 播放进度每5秒自动保存
+- 退出页面时保存最新进度
+
+**后端**:
+- 记录播放进度到play_records表
+- 续播时返回保存的progress值
+
+### 2. 收藏功能
+
+**前端**:
+- 播放器页面添加收藏按钮
+- favorites页面管理收藏列表
+
+**后端**:
+- favorites表存储收藏关系
+- 支持批量操作
+
+### 3. 定时关闭
+
+**前端**:
+- 使用setTimeout实现倒计时
+- 倒计时结束自动暂停播放
+- 页面关闭时保存剩余时间
+
+### 4. 分享功能
+
+**前端**:
+- 使用微信JSSDK分享接口
+- 生成分享卡片(标题+封面+简介)
+
+**后端**:
+- 生成带参数的分享链接
+- 记录分享来源
+
+### 5. 离线缓存
+
+**前端**:
+- 使用uni.downloadFile下载音频
+- 存储到本地文件系统
+- 管理离线列表
+
+**后端**:
+- 支持CDN域名切换
+- 提供音频文件大小信息
+
+### 6. 加载优化
+
+**前端**:
+- 骨架屏组件
+- 图片懒加载
+- 列表虚拟滚动
+
+**缓存策略**:
+- 接口数据缓存(localStorage)
+- 音频封面缓存
+- 离线音频管理
+
+---
+
+## 开发顺序
+
+### 第一阶段(2周)
+
+1. **播放记录 + 续播** (3天)
+   - 数据库表设计
+   - API开发
+   - 前端播放器集成
+
+2. **收藏功能** (2天)
+   - 数据库表设计
+   - API开发
+   - favorites页面开发
+
+3. **定时关闭** (1天)
+   - 前端定时器实现
+
+4. **播放速度记忆** (1天)
+   - 用户偏好API
+   - 速度记忆实现
+
+5. **分享功能** (2天)
+   - 微信JSSDK集成
+   - 分享卡片生成
+
+6. **加载优化** (2天)
+   - 骨架屏组件
+   - 缓存优化
+
+### 第二阶段(2周)
+
+7. **全局搜索** (3天)
+   - 搜索API
+   - search页面开发
+
+8. **内容分类** (2天)
+   - 分类管理
+   - 分类筛选
+
+9. **音质选择** (1天)
+   - 音质切换逻辑
+   - CDN配置
+
+10. **离线缓存** (3天)
+    - 下载管理
+    - 离线播放
+
+### 第三阶段(1周)
+
+11. **评论打分** (2天)
+12. **消息通知** (2天)
+13. **个性化主题** (1天)
+
+---
+
+## 测试要点
+
+### 功能测试
+
+- [ ] 播放记录:续播位置准确性
+- [ ] 收藏:增删查操作正常
+- [ ] 定时:倒计时准确性
+- [ ] 分享:微信分享卡片正确
+- [ ] 搜索:搜索结果相关性
+- [ ] 离线:断网播放正常
+
+### 性能测试
+
+- [ ] 首屏加载 < 2秒
+- [ ] 列表滑动流畅
+- [ ] 音频播放无卡顿
+
+### 兼容性测试
+
+- [ ] iOS/Android微信环境
+- [ ] 不同网络环境(2G/3G/4G/WiFi)
+- [ ] 不同屏幕尺寸
+
+---
+
+## 里程碑
+
+| 阶段 | 完成时间 | 交付内容 |
+|------|----------|----------|
+| 第一阶段完成 | 第2周末 | 核心体验功能上线 |
+| 第二阶段完成 | 第4周末 | 丰富功能上线 |
+| 第三阶段完成 | 第5周末 | 全部功能上线 |
+| 测试验收 | 第6周 | 优化调优、发布 |

+ 3 - 9
开发计划.md

@@ -112,7 +112,8 @@
 | Node.js | 18 LTS | 运行时环境 |
 | Koa | 2.x | Web框架 |
 | TypeScript | 5.x | 类型安全 |
-| MongoDB | 6.x | 数据库 |
+| MySQL | 8.x | 数据库 |
+| Prisma | 7.x | ORM框架 |
 | Redis | 7.x | 缓存/会话 |
 | FFmpeg | 最新 | 音频处理 |
 
@@ -490,13 +491,6 @@ c:/Users/caoyg/test/audio/
 | 💬 评论打分 | P2 | 对内容评论评分 |
 | 🔔 消息通知 | P3 | 新内容推送、会员优惠 |
 
-### 五、UI/UX优化
-
-| 功能 | 优先级 | 说明 |
-|------|--------|------|
-| 🌙 夜间模式 | P2 | 护眼深色主题 |
-| 🎨 个性化主题 | P3 | 自定义主题色 |
-| 📱 首页改版 | P2 | 更直观的文本输入体验 |
 
 ### 六、技术优化
 
@@ -544,7 +538,7 @@ pages/
 **第二阶段(P2 - 丰富功能)**
 6. 全局搜索
 7. 内容分类
-8. 夜间模式
+
 9. 音质选择
 10. 离线缓存