diff --git a/data/users.db b/data/users.db index 0732016..b4ae37a 100644 Binary files a/data/users.db and b/data/users.db differ diff --git a/data/users.db-shm b/data/users.db-shm new file mode 100644 index 0000000..a9d0947 Binary files /dev/null and b/data/users.db-shm differ diff --git a/data/users.db-wal b/data/users.db-wal new file mode 100644 index 0000000..6aad829 Binary files /dev/null and b/data/users.db-wal differ diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 441d656..50c5e2a 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -31,6 +31,8 @@ services: - .:/app - /app/node_modules # 防止node_modules被覆盖 - moodist-dist:/app/dist + # 挂载 SQLite 数据库文件目录 + - ./data:/app/data:rw # 工作目录 working_dir: /app diff --git a/docker-compose.optimized.yml b/docker-compose.optimized.yml index 314946b..aecdf00 100644 --- a/docker-compose.optimized.yml +++ b/docker-compose.optimized.yml @@ -51,7 +51,14 @@ services: security_opt: - no-new-privileges:true - # 只读根文件系统 + # 数据卷挂载 + volumes: + # 挂载 SQLite 数据库文件目录(需要读写权限) + - ./data:/app/data:rw + # 挂载临时目录用于 SQLite WAL 文件 + - moodist-temp:/tmp:rw + + # 只读根文件系统(除了数据目录) read_only: true tmpfs: - /var/cache/nginx @@ -76,6 +83,10 @@ services: profiles: - proxy +volumes: + moodist-temp: + driver: local + networks: moodist-network: driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index d389090..062523a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,3 +8,8 @@ services: restart: always ports: - '8080:8080' + volumes: + # 挂载 SQLite 数据库文件和 WAL 文件 + - ./data:/app/data:rw + environment: + - NODE_ENV=production diff --git a/docker-database-mount.md b/docker-database-mount.md new file mode 100644 index 0000000..463d793 --- /dev/null +++ b/docker-database-mount.md @@ -0,0 +1,113 @@ +# Docker 数据库挂载说明 + +## 概述 + +本项目已配置 SQLite 数据库文件挂载,确保数据在容器重启后不会丢失。 + +## 数据库文件位置 + +SQLite 数据库文件位于项目的 `./data` 目录中: +- `./data/users.db` - 主数据库文件 +- `./data/users.db-wal` - Write-Ahead Log 文件 +- `./data/users.db-shm` - 共享内存文件 + +## Docker Compose 配置 + +### 1. 基础配置 (`docker-compose.yml`) + +```yaml +services: + moodist: + volumes: + # 挂载 SQLite 数据库文件和 WAL 文件 + - ./data:/app/data:rw + environment: + - NODE_ENV=production +``` + +### 2. 优化配置 (`docker-compose.optimized.yml`) + +```yaml +services: + moodist: + volumes: + # 挂载 SQLite 数据库文件目录(需要读写权限) + - ./data:/app/data:rw + # 挂载临时目录用于 SQLite WAL 文件 + - moodist-temp:/tmp:rw + +volumes: + moodist-temp: + driver: local +``` + +### 3. 开发配置 (`docker-compose.dev.yml`) + +```yaml +services: + moodist-dev: + volumes: + # 挂载 SQLite 数据库文件目录 + - ./data:/app/data:rw +``` + +## 使用方法 + +### 启动服务 + +```bash +# 生产环境 +docker-compose up -d + +# 优化环境 +docker-compose -f docker-compose.optimized.yml up -d + +# 开发环境 +docker-compose -f docker-compose.dev.yml up -d +``` + +### 数据持久化 + +- 数据库文件会自动创建在 `./data` 目录中 +- 容器重启或重新创建后数据不会丢失 +- 支持数据库备份和迁移 + +### 备份数据库 + +```bash +# 备份数据库 +cp ./data/users.db ./data/users.db.backup.$(date +%Y%m%d_%H%M%S) + +# 查看数据库文件 +ls -la ./data/ +``` + +## 注意事项 + +1. **权限问题**: 确保 `./data` 目录有正确的读写权限 +2. **WAL 模式**: SQLite 使用 WAL (Write-Ahead Logging) 模式,会产生额外的 WAL 和 SHM 文件 +3. **并发访问**: Docker 挂载确保文件系统的一致性 +4. **备份策略**: 建议定期备份数据库文件 + +## 故障排除 + +### 数据库锁定问题 +如果遇到数据库锁定错误: +1. 停止容器:`docker-compose down` +2. 删除 WAL 文件:`rm ./data/users.db-wal ./data/users.db-shm` +3. 重新启动容器:`docker-compose up -d` + +### 权限问题 +如果遇到权限错误: +```bash +# 设置正确的目录权限 +sudo chown -R 1000:1000 ./data +chmod 755 ./data +``` + +## 开发环境注意事项 + +开发环境中,数据库文件会被实时同步到本地文件系统,便于: +- 调试和测试 +- 数据分析 +- 快速重置测试数据 \ No newline at end of file diff --git a/package.json b/package.json index 5a97ab2..1650148 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "moodist", "type": "module", - "version": "2.5.0", + "version": "3.0.0", "scripts": { "dev": "astro dev", "start": "astro dev", diff --git a/src/components/selected-sounds-display/selected-sounds-display.tsx b/src/components/selected-sounds-display/selected-sounds-display.tsx index 46fd461..f4c7117 100644 --- a/src/components/selected-sounds-display/selected-sounds-display.tsx +++ b/src/components/selected-sounds-display/selected-sounds-display.tsx @@ -1,6 +1,6 @@ -import { useMemo, useState, useEffect } from 'react'; +import { useMemo, useState, useEffect, useRef } from 'react'; import { AnimatePresence, motion } from 'motion/react'; -import { FaSave, FaPlay, FaTrash, FaEdit, FaCog, FaSignOutAlt, FaMusic, FaChevronDown, FaChevronRight } from 'react-icons/fa/index'; +import { FaSave, FaPlay, FaStop, FaTrash, FaEdit, FaCog, FaSignOutAlt, FaMusic, FaChevronDown, FaChevronRight } from 'react-icons/fa/index'; import { SaveMusicButton } from '@/components/buttons/save-music/save-music'; import { DeleteMusicButton } from '@/components/buttons/delete-music/delete-music'; @@ -9,6 +9,7 @@ import { useLocalizedSounds } from '@/hooks/useLocalizedSounds'; import { useTranslation } from '@/hooks/useTranslation'; import { useAuthStore } from '@/stores/auth'; import { ApiClient } from '@/lib/api-client'; +import { Howl } from 'howler'; import { Sound } from '@/components/sounds/sound'; import styles from '../sounds/sounds.module.css'; @@ -42,6 +43,11 @@ export function SelectedSoundsDisplay() { const [error, setError] = useState(null); const [musicName, setMusicName] = useState(''); + // 独立的音乐播放状态 + const [currentlyPlayingMusic, setCurrentlyPlayingMusic] = useState(null); + const musicHowlInstances = useRef>({}); + const [isPlayingMusic, setIsPlayingMusic] = useState(false); + // 获取声音store const sounds = useSoundStore(state => state.sounds); @@ -65,34 +71,112 @@ export function SelectedSoundsDisplay() { } }; - // 获取声音store的操作函数 - const unselectAll = useSoundStore(state => state.unselectAll); - const select = useSoundStore(state => state.select); + // 获取声音store的操作函数(仅用于控制主要播放状态) + const play = useSoundStore(state => state.play); + const pause = useSoundStore(state => state.pause); - // 播放音乐记录 - 清空当前选择并加载音乐的声音配置 + // 停止音乐播放 + const stopMusic = () => { + console.log('🛑 停止音乐播放'); + + // 停止所有音乐相关的 Howl 实例 + Object.values(musicHowlInstances.current).forEach(howlInstance => { + if (howlInstance) { + howlInstance.stop(); + howlInstance.unload(); + } + }); + + musicHowlInstances.current = {}; + setCurrentlyPlayingMusic(null); + setIsPlayingMusic(false); + }; + + // 播放音乐记录 - 使用独立的音乐播放系统,不影响当前选中声音 const playMusicRecord = async (music: SavedMusic) => { try { - // 清空当前所有选择 - unselectAll(); + console.log('🎵 开始播放音乐:', music.name); + console.log('🎵 音乐数据:', { + sounds: music.sounds, + volume: music.volume, + speed: music.speed, + rate: music.rate, + random_effects: music.random_effects + }); - // 根据音乐记录重新选择声音并设置参数 - for (const [soundId, volume] of Object.entries(music.volume)) { - const speed = music.speed[soundId] || 1; + // 先停止当前播放的音乐 + stopMusic(); + + // 停止主要的选中声音播放(但不改变选中状态) + pause(); + + // 获取所有声音数据 + const allSounds = localizedCategories + .map(category => category.sounds) + .flat(); + + // 创建所有声音的 Howl 实例 + const howlPromises: Promise[] = []; + + for (const soundId of music.sounds) { + const soundData = allSounds.find(s => s.id === soundId); + if (!soundData || !soundData.src) continue; + + const volume = music.volume[soundId] || 0.5; const rate = music.rate[soundId] || 1; - const randomEffect = music.random_effects[soundId] || false; + const speed = music.speed[soundId] || 1; - // 选择声音并设置参数 - select(soundId, { - volume, - speed, - rate, - randomEffect + console.log(`🔊 创建音乐声音: ${soundId}`, { volume, rate, speed }); + + // 创建 Howl 实例的 Promise + const howlPromise = new Promise((resolve, reject) => { + const howl = new Howl({ + src: [soundData.src], + loop: true, + volume: volume, + rate: rate, + preload: true, + onload: () => { + console.log(`✅ 声音加载完成: ${soundId}`); + resolve(howl); + }, + onloaderror: (id, error) => { + console.error(`❌ 声音加载失败: ${soundId}`, error); + reject(error); + } + }); + + // 保存实例引用 + musicHowlInstances.current[soundId] = howl; }); + + howlPromises.push(howlPromise); } - console.log(`🎵 播放音乐记录: ${music.name}`); + // 等待所有声音加载完成 + console.log('⏳ 等待所有声音加载...'); + await Promise.all(howlPromises); + console.log('✅ 所有声音加载完成,开始播放'); + + // 播放所有声音 + Object.values(musicHowlInstances.current).forEach(howlInstance => { + if (howlInstance && howlInstance.state() === 'loaded') { + howlInstance.play(); + } + }); + + // 设置播放状态 + setCurrentlyPlayingMusic(music); + setIsPlayingMusic(true); + + // 展开对应的音乐记录 + setExpandedMusic(new Set([music.id])); + setExpandedCurrent(false); // 收起当前选中声音模块 + + console.log(`✅ 播放音乐记录完成: ${music.name}`); } catch (error) { console.error('❌ 播放音乐记录失败:', error); + stopMusic(); } }; @@ -253,6 +337,13 @@ export function SelectedSoundsDisplay() { } }, [isAuthenticated, user]); + // 组件卸载时清理音乐播放 + useEffect(() => { + return () => { + stopMusic(); + }; + }, []); + // 监听音乐列表数量,超过5个时默认收起 useEffect(() => { if (savedMusicList.length > 5) { @@ -262,66 +353,68 @@ export function SelectedSoundsDisplay() { } }, [savedMusicList.length]); - // 如果没有选中的声音,不渲染组件 - if (selectedSounds.length === 0) { + // 如果既没有选中声音,也没有音乐列表,则不渲染组件 + if (selectedSounds.length === 0 && (!isAuthenticated || savedMusicList.length === 0)) { return null; } return (
- {/* 当前选中声音模块 */} -
-
-

- - 当前选中的声音 -

- + {/* 当前选中声音模块 - 只有选中声音时才显示 */} + {selectedSounds.length > 0 && ( +
+
+

+ + 当前选中的声音 +

+ +
+ + {/* 音乐名称配置区域 */} + {expandedCurrent && ( +
+ setMusicName(e.target.value)} + placeholder="音乐名称" + className={styles.musicNameInput} + maxLength={50} + /> + +
+ )} + + {/* 选中的声音展示 */} + {expandedCurrent && ( +
+ + {selectedSounds.map((sound) => ( + +
+ )}
- - {/* 音乐名称配置区域 */} - {expandedCurrent && ( -
- setMusicName(e.target.value)} - placeholder="音乐名称" - className={styles.musicNameInput} - maxLength={50} - /> - -
- )} - - {/* 选中的声音展示 */} - {expandedCurrent && ( -
- - {selectedSounds.map((sound) => ( - -
- )} -
+ )} {/* 音乐列表模块 - 只有登录用户且有音乐时才显示 */} {isAuthenticated && savedMusicList.length > 0 && ( @@ -400,6 +493,21 @@ export function SelectedSoundsDisplay() {
) : (
+
{music.name}
@@ -443,18 +551,6 @@ export function SelectedSoundsDisplay() { {/* 展开时显示的声音内容 */} {expandedMusic.has(music.id) && (
- {/* 播放按钮 */} -
- -
- {/* 声音组件展示 */}
diff --git a/src/components/sounds/sounds.module.css b/src/components/sounds/sounds.module.css index 4fd2841..1a27557 100644 --- a/src/components/sounds/sounds.module.css +++ b/src/components/sounds/sounds.module.css @@ -181,6 +181,15 @@ background: var(--color-foreground-subtle); } +.playButton.playing { + background: #e74c3c; + color: white; +} + +.playButton.playing:hover { + background: #c0392b; +} + /* 音乐名称 */ .musicName { font-size: 14px; diff --git a/src/lib/database.ts b/src/lib/database.ts index b693fdf..50df099 100644 --- a/src/lib/database.ts +++ b/src/lib/database.ts @@ -51,7 +51,17 @@ export function getDatabase(): Database.Database { fs.mkdirSync(dbDir, { recursive: true }); } - db = new Database(dbPath); + // 以读写模式打开数据库,启用WAL模式提高并发性能 + db = new Database(dbPath, { + readonly: false, + fileMustExist: false, + verbose: console.log // 添加SQL执行日志 + }); + + // 启用WAL模式,提高并发写入性能 + db.pragma('journal_mode = WAL'); + db.pragma('synchronous = NORMAL'); + db.pragma('cache_size = 1000'); // 创建用户表 db.exec(` diff --git a/src/pages/api/auth/music/save.ts b/src/pages/api/auth/music/save.ts index 8b9d7cd..2d93e9c 100644 --- a/src/pages/api/auth/music/save.ts +++ b/src/pages/api/auth/music/save.ts @@ -26,6 +26,8 @@ export const POST: APIRoute = async ({ request }) => { const { data } = bodyResult; const { name, sounds, volume, speed, rate, random_effects } = data; + console.log('🎵 保存音乐请求:', { userId: user?.id, name, soundsCount: sounds?.length }); + // 验证输入 if (!name || !sounds || !Array.isArray(sounds)) { return new Response(JSON.stringify({ @@ -47,6 +49,8 @@ export const POST: APIRoute = async ({ request }) => { random_effects: random_effects || {}, }); + console.log('✅ 音乐保存成功:', { id: music.id, name: music.name }); + return new Response(JSON.stringify({ success: true, message: '音乐保存成功', @@ -61,6 +65,18 @@ export const POST: APIRoute = async ({ request }) => { }); } catch (error) { + console.error('❌ 保存音乐API错误:', error); + + // 处理特定的数据库错误 + if (error instanceof Error && error.message.includes('readonly')) { + return new Response(JSON.stringify({ + error: '数据库权限错误,请检查文件权限' + }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } + return handleApiError(error, '保存音乐'); } }; \ No newline at end of file